@acorex/components 21.0.1-next.85 → 21.0.1-next.87

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.
@@ -4,12 +4,12 @@ import { AXTabsModule, AXTabsComponent, AXTabItemComponent } from '@acorex/compo
4
4
  import * as i1$2 from '@angular/common';
5
5
  import { isPlatformBrowser, AsyncPipe, CommonModule, NgComponentOutlet } from '@angular/common';
6
6
  import * as i0 from '@angular/core';
7
- import { InjectionToken, inject, Injectable, signal, computed, Injector, runInInjectionContext, PLATFORM_ID, input, output, Component, viewChild, DestroyRef, effect, model, ChangeDetectionStrategy, SecurityContext, untracked, ViewContainerRef, Directive, EventEmitter, ElementRef, afterNextRender, NgModule } from '@angular/core';
8
- import { Subject, BehaviorSubject, Observable, filter, firstValueFrom, takeUntil, catchError, EMPTY } from 'rxjs';
7
+ import { InjectionToken, signal, computed, inject, Injectable, Injector, runInInjectionContext, PLATFORM_ID, input, output, Component, viewChild, DestroyRef, effect, model, ChangeDetectionStrategy, SecurityContext, untracked, ViewContainerRef, Directive, EventEmitter, ElementRef, afterNextRender, NgModule } from '@angular/core';
9
8
  import { AXDialogService } from '@acorex/components/dialog';
10
9
  import { AXPopupService } from '@acorex/components/popup';
11
10
  import * as i4 from '@acorex/core/translation';
12
11
  import { AXTranslationService, AXTranslationModule, translateSync } from '@acorex/core/translation';
12
+ import { Subject, BehaviorSubject, Observable, filter, firstValueFrom, takeUntil, catchError, EMPTY } from 'rxjs';
13
13
  import * as i1 from '@acorex/components/button';
14
14
  import { AXButtonModule, AXButtonComponent } from '@acorex/components/button';
15
15
  import * as i2 from '@acorex/components/decorators';
@@ -419,871 +419,101 @@ const REGISTRY_CONFIG = new InjectionToken('REGISTRY_CONFIG', {
419
419
  });
420
420
 
421
421
  /**
422
- * Error Handler Service
423
- * Centralized error handling and logging
422
+ * Validation Utilities
423
+ * Centralized validation functions for messages, files, and user input
424
424
  */
425
425
  /**
426
- * Error Handler Service
426
+ * Validate message text content
427
+ * @param text - Text to validate
428
+ * @param config - Configuration for validation rules
429
+ * @returns Validation result
427
430
  */
428
- class AXErrorHandlerService {
429
- constructor() {
430
- this.injectedConfig = inject(ERROR_HANDLER_CONFIG);
431
- this._errors$ = new Subject();
432
- this._config = {
433
- logToConsole: true,
434
- showUserMessages: true,
435
- autoRetry: false,
436
- maxRetries: 3,
431
+ function validateMessageText(text, config) {
432
+ // Check for empty text
433
+ if (!text || text.trim().length === 0) {
434
+ return {
435
+ valid: false,
436
+ error: 'Message text cannot be empty',
437
+ errorCode: 'EMPTY_MESSAGE',
437
438
  };
438
- /** Error stream */
439
- this.errors$ = this._errors$.asObservable();
440
- // Apply injected configuration at construction
441
- if (this.injectedConfig) {
442
- this.configure(this.injectedConfig);
443
- }
444
- }
445
- /**
446
- * Configure error handler
447
- */
448
- configure(config) {
449
- Object.assign(this._config, config);
450
- }
451
- /**
452
- * Handle an error
453
- */
454
- handle(error, operation, context) {
455
- const conversationError = this.normalizeError(error, operation, context);
456
- // Emit error event
457
- this._errors$.next(conversationError);
458
- // Log to console if enabled
459
- if (this._config.logToConsole) {
460
- this.logError(conversationError);
461
- }
462
- // Call custom handler if provided
463
- if (this._config.customHandler) {
464
- this._config.customHandler(conversationError);
465
- }
466
- return conversationError;
467
439
  }
468
- /**
469
- * Handle API error
470
- */
471
- handleApiError(apiError, operation, context) {
472
- const conversationError = {
473
- code: apiError.code,
474
- message: apiError.message,
475
- severity: this.determineSeverity(apiError.statusCode),
476
- operation,
477
- originalError: apiError,
478
- statusCode: apiError.statusCode,
479
- context,
480
- timestamp: apiError.timestamp || new Date(),
481
- handled: false,
482
- recoverySuggestions: this.getRecoverySuggestions(apiError),
440
+ // Check minimum length
441
+ const minLength = config.minMessageLength ?? 1;
442
+ if (text.trim().length < minLength) {
443
+ return {
444
+ valid: false,
445
+ error: `Message must be at least ${minLength} character(s)`,
446
+ errorCode: 'MESSAGE_TOO_SHORT',
483
447
  };
484
- this._errors$.next(conversationError);
485
- if (this._config.logToConsole) {
486
- this.logError(conversationError);
487
- }
488
- if (this._config.customHandler) {
489
- this._config.customHandler(conversationError);
490
- }
491
- return conversationError;
492
448
  }
493
- /**
494
- * Normalize any error to conversation error format
495
- */
496
- normalizeError(error, operation, context) {
497
- // Handle AXApiError
498
- if (this.isApiError(error)) {
499
- return this.handleApiError(error, operation, context);
500
- }
501
- // Handle standard Error
502
- const errorObj = error;
503
- const message = errorObj?.['message'] || String(error) || 'An unknown error occurred';
504
- const code = errorObj?.['code'] || 'UNKNOWN_ERROR';
505
- const statusCode = errorObj?.['statusCode'] || errorObj?.['status'];
449
+ // Check maximum length
450
+ const maxLength = config.maxMessageLength ?? 10000;
451
+ if (text.length > maxLength) {
506
452
  return {
507
- code,
508
- message,
509
- severity: 'error',
510
- operation,
511
- originalError: error,
512
- statusCode,
513
- context,
514
- timestamp: new Date(),
515
- handled: false,
516
- recoverySuggestions: this.getDefaultRecoverySuggestions(code),
453
+ valid: false,
454
+ error: `Message exceeds ${maxLength} character limit`,
455
+ errorCode: 'MESSAGE_TOO_LONG',
517
456
  };
518
457
  }
519
- /**
520
- * Check if error is API error
521
- */
522
- isApiError(error) {
523
- return error && typeof error === 'object' && 'code' in error && 'message' in error;
524
- }
525
- /**
526
- * Determine severity based on status code
527
- */
528
- determineSeverity(statusCode) {
529
- if (!statusCode)
530
- return 'error';
531
- if (statusCode >= 500)
532
- return 'critical';
533
- if (statusCode >= 400)
534
- return 'error';
535
- if (statusCode >= 300)
536
- return 'warning';
537
- return 'info';
538
- }
539
- /**
540
- * Get recovery suggestions based on error
541
- */
542
- getRecoverySuggestions(error) {
543
- const suggestions = [];
544
- if (error.statusCode === 401 || error.code === 'UNAUTHORIZED') {
545
- suggestions.push('Please log in again');
546
- suggestions.push('Check if your session has expired');
547
- }
548
- else if (error.statusCode === 403 || error.code === 'FORBIDDEN') {
549
- suggestions.push('You do not have permission for this action');
550
- suggestions.push('Ask your administrator for access');
551
- }
552
- else if (error.statusCode === 404 || error.code === 'NOT_FOUND') {
553
- suggestions.push('The requested resource was not found');
554
- suggestions.push('It may have been deleted or moved');
555
- }
556
- else if (error.statusCode === 429 || error.code === 'RATE_LIMIT_EXCEEDED') {
557
- suggestions.push('Too many requests. Please wait and try again');
558
- }
559
- else if (error.statusCode && error.statusCode >= 500) {
560
- suggestions.push('Server error occurred');
561
- suggestions.push('Please try again later');
562
- suggestions.push('If the problem persists, reach out to support');
563
- }
564
- else if (error.code === 'NETWORK_ERROR') {
565
- suggestions.push('Check your internet connection');
566
- suggestions.push('Try refreshing the page');
567
- }
568
- return suggestions;
458
+ return { valid: true };
459
+ }
460
+ /**
461
+ * Validate file upload
462
+ * @param file - File to validate
463
+ * @param config - Configuration for validation rules
464
+ * @returns File validation result
465
+ */
466
+ function validateFile(file, config) {
467
+ // Check file size
468
+ const maxSize = config.maxFileSize ?? 10 * 1024 * 1024; // 10MB default
469
+ if (file.size > maxSize) {
470
+ return {
471
+ valid: false,
472
+ error: `File size exceeds ${formatFileSize(maxSize)} limit`,
473
+ errorCode: 'FILE_TOO_LARGE',
474
+ size: file.size,
475
+ type: file.type,
476
+ };
569
477
  }
570
- /**
571
- * Get default recovery suggestions
572
- */
573
- getDefaultRecoverySuggestions(code) {
574
- const suggestions = [];
575
- if (code.includes('NETWORK') || code.includes('CONNECTION')) {
576
- suggestions.push('Check your internet connection');
577
- suggestions.push('Try refreshing the page');
578
- }
579
- else if (code.includes('TIMEOUT')) {
580
- suggestions.push('The operation took too long');
581
- suggestions.push('Please try again');
582
- }
583
- else {
584
- suggestions.push('Please try again');
585
- suggestions.push('If the problem persists, reach out to support');
478
+ // Check file type if restrictions exist
479
+ const allowedTypes = config.allowedFileTypes;
480
+ if (allowedTypes && allowedTypes.length > 0) {
481
+ const isAllowed = allowedTypes.some((pattern) => matchMimeType(file.type, pattern));
482
+ if (!isAllowed) {
483
+ return {
484
+ valid: false,
485
+ error: `File type "${file.type}" is not allowed`,
486
+ errorCode: 'FILE_TYPE_NOT_ALLOWED',
487
+ size: file.size,
488
+ type: file.type,
489
+ };
586
490
  }
587
- return suggestions;
588
491
  }
589
- /**
590
- * Log error to console
591
- */
592
- logError(error) {
593
- const isError = error.severity === 'critical' || error.severity === 'error';
594
- const header = `[Conversation ${error.severity.toUpperCase()}] ${error.operation}:`;
595
- if (isError) {
596
- console.error(header, error.message, error.context || '');
597
- }
598
- else {
599
- console.warn(header, error.message, error.context || '');
600
- }
601
- if (error.originalError && error.severity !== 'info') {
602
- console.error('Original error:', error.originalError);
603
- }
604
- if (error.recoverySuggestions && error.recoverySuggestions.length > 0) {
605
- console.info('Recovery suggestions:', error.recoverySuggestions);
606
- }
492
+ return {
493
+ valid: true,
494
+ size: file.size,
495
+ type: file.type,
496
+ };
497
+ }
498
+ /**
499
+ * Validate conversation ID
500
+ * @param conversationId - Conversation ID to validate
501
+ * @returns Validation result
502
+ */
503
+ function validateConversationId(conversationId) {
504
+ if (!conversationId || typeof conversationId !== 'string' || conversationId.trim().length === 0) {
505
+ return {
506
+ valid: false,
507
+ error: 'Conversation ID is required',
508
+ errorCode: 'MISSING_CONVERSATION_ID',
509
+ };
607
510
  }
608
- /**
609
- * Get user-friendly error message
610
- */
611
- getUserFriendlyMessage(error) {
612
- // Map technical errors to user-friendly messages
613
- const messageMap = {
614
- NETWORK_ERROR: 'Unable to connect. Please check your internet connection.',
615
- UNAUTHORIZED: 'You are not authorized. Please log in again.',
616
- FORBIDDEN: 'You do not have permission to perform this action.',
617
- NOT_FOUND: 'The requested item could not be found.',
618
- RATE_LIMIT_EXCEEDED: 'Too many requests. Please slow down.',
619
- VALIDATION_ERROR: 'The provided data is invalid.',
620
- SERVER_ERROR: 'A server error occurred. Please try again later.',
621
- TIMEOUT: 'The operation timed out. Please try again.',
622
- };
623
- return messageMap[error.code] || error.message || 'An unexpected error occurred.';
624
- }
625
- /**
626
- * Check if error is retryable
627
- */
628
- isRetryable(error) {
629
- const retryableCodes = ['NETWORK_ERROR', 'TIMEOUT', 'RATE_LIMIT_EXCEEDED', 'SERVER_ERROR'];
630
- const retryableStatusCodes = [408, 429, 500, 502, 503, 504];
631
- return (retryableCodes.includes(error.code) ||
632
- (error.statusCode !== undefined && retryableStatusCodes.includes(error.statusCode)));
633
- }
634
- /**
635
- * Execute an operation with automatic retry logic
636
- * @param operation - The async operation to execute
637
- * @param operationName - Name of the operation for error tracking
638
- * @param context - Additional context for error handling
639
- * @returns Promise resolving to the operation result
640
- * @throws {AXConversationError} If all retries fail
641
- */
642
- async executeWithRetry(operation, operationName, context) {
643
- if (!this._config.autoRetry) {
644
- // If auto-retry is disabled, just execute once
645
- return operation();
646
- }
647
- const maxRetries = this._config.maxRetries ?? 3;
648
- let lastError;
649
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
650
- try {
651
- return await operation();
652
- }
653
- catch (error) {
654
- lastError = error;
655
- const conversationError = this.handle(error, operationName, { ...context, attempt });
656
- // Don't retry if error is not retryable or if this was the last attempt
657
- if (!this.isRetryable(conversationError) || attempt >= maxRetries) {
658
- throw conversationError;
659
- }
660
- // Exponential backoff: 1s, 2s, 4s, 8s...
661
- const delayMs = Math.pow(2, attempt) * 1000;
662
- await this.delay(delayMs);
663
- }
664
- }
665
- // This should never be reached, but TypeScript needs it
666
- throw this.handle(lastError, operationName, context);
667
- }
668
- /**
669
- * Delay helper for retry backoff
670
- * @param ms - Milliseconds to delay
671
- */
672
- delay(ms) {
673
- return new Promise((resolve) => setTimeout(resolve, ms));
674
- }
675
- /**
676
- * Clear error history
677
- */
678
- clear() {
679
- // Errors are not stored, just emitted through observable
680
- // This method is here for future extensions if needed
681
- }
682
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXErrorHandlerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
683
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXErrorHandlerService, providedIn: 'root' }); }
684
- }
685
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXErrorHandlerService, decorators: [{
686
- type: Injectable,
687
- args: [{
688
- providedIn: 'root',
689
- }]
690
- }], ctorParameters: () => [] });
691
-
692
- /**
693
- * Conversation Store Service
694
- * Single signal store for managing all conversation and message state
695
- *
696
- * This is the single source of truth for:
697
- * - Conversations list and metadata
698
- * - Messages for all conversations
699
- * - Active conversation selection
700
- * - In-memory state management
701
- */
702
- /**
703
- * Conversation Store Service
704
- * Unified signal-based store for conversations and messages
705
- */
706
- class AXConversationStoreService {
707
- constructor() {
708
- this.config = inject(CONVERSATION_CONFIG);
709
- this.errorHandler = inject(AXErrorHandlerService);
710
- // =====================
711
- // State Signals
712
- // =====================
713
- /** All conversations (keyed by ID) */
714
- this._conversations = signal(new Map(), ...(ngDevMode ? [{ debugName: "_conversations" }] : []));
715
- /** All messages (keyed by message ID) */
716
- this._messages = signal(new Map(), ...(ngDevMode ? [{ debugName: "_messages" }] : []));
717
- /** Message IDs grouped by conversation */
718
- this._conversationMessages = signal(new Map(), ...(ngDevMode ? [{ debugName: "_conversationMessages" }] : []));
719
- // =====================
720
- // Public Computed Signals
721
- // =====================
722
- /** All conversations as array (unsorted - sorting is handled by consumers) */
723
- this.conversations = computed(() => {
724
- const convMap = this._conversations();
725
- return Array.from(convMap.values());
726
- }, { ...(ngDevMode ? { debugName: "conversations" } : {}), equal: (a, b) => a.length === b.length && a.every((v, i) => v === b[i]) });
727
- /** All messages as array */
728
- this.messages = computed(() => Array.from(this._messages().values()), { ...(ngDevMode ? { debugName: "messages" } : {}), equal: (a, b) => a.length === b.length && a.every((v, i) => v === b[i]) });
729
- }
730
- // =====================
731
- // Initialization
732
- // =====================
733
- /**
734
- * Initialize the store
735
- */
736
- async initialize() {
737
- // Store is initialized with empty state
738
- // Data will be loaded from API
739
- }
740
- // =====================
741
- // Conversation Operations
742
- // =====================
743
- /**
744
- * Set all conversations (replaces existing)
745
- * @param conversations - Array of conversations
746
- */
747
- setConversations(conversations) {
748
- const convMap = new Map();
749
- conversations.forEach((conv) => convMap.set(conv.id, conv));
750
- this._conversations.set(convMap);
751
- }
752
- /**
753
- * Add multiple conversations (append to existing)
754
- * Used for pagination - appends new conversations without replacing existing ones
755
- * @param conversations - Array of conversations to add
756
- */
757
- addConversations(conversations) {
758
- this._conversations.update((existingConversations) => {
759
- const newConversations = new Map(existingConversations);
760
- conversations.forEach((conv) => newConversations.set(conv.id, conv));
761
- // Cleanup old conversations if cache is too large
762
- const maxCached = this.config.maxCachedConversations;
763
- if (newConversations.size > maxCached) {
764
- // Sort by last activity and keep only the most recent
765
- const sorted = Array.from(newConversations.values()).sort((a, b) => (b.lastMessageAt?.getTime() ?? 0) - (a.lastMessageAt?.getTime() ?? 0));
766
- // Keep only the most recent conversations
767
- const toKeep = sorted.slice(0, maxCached);
768
- const cleanedMap = new Map();
769
- toKeep.forEach((conv) => cleanedMap.set(conv.id, conv));
770
- return cleanedMap;
771
- }
772
- return newConversations;
773
- });
774
- }
775
- /**
776
- * Add or update a single conversation
777
- * @param conversation - Conversation to add/update
778
- */
779
- setConversation(conversation) {
780
- this._conversations.update((conversations) => {
781
- const newConversations = new Map(conversations);
782
- newConversations.set(conversation.id, conversation);
783
- return newConversations;
784
- });
785
- }
786
- /**
787
- * Get a conversation by ID
788
- * @param conversationId - Conversation ID
789
- * @returns Conversation or undefined
790
- */
791
- getConversation(conversationId) {
792
- return this._conversations().get(conversationId);
793
- }
794
- /**
795
- * Update a conversation with partial data
796
- * @param conversationId - Conversation ID
797
- * @param updates - Partial conversation updates
798
- */
799
- updateConversation(conversationId, updates) {
800
- const conversation = this._conversations().get(conversationId);
801
- if (!conversation)
802
- return;
803
- const updatedConversation = { ...conversation, ...updates };
804
- this.setConversation(updatedConversation);
805
- }
806
- /**
807
- * Delete a conversation
808
- * @param conversationId - Conversation ID
809
- */
810
- deleteConversation(conversationId) {
811
- // Remove conversation
812
- this._conversations.update((conversations) => {
813
- const newConversations = new Map(conversations);
814
- newConversations.delete(conversationId);
815
- return newConversations;
816
- });
817
- // Remove all messages for this conversation
818
- const messageIds = this._conversationMessages().get(conversationId) || [];
819
- this._messages.update((messages) => {
820
- const newMessages = new Map(messages);
821
- messageIds.forEach((id) => newMessages.delete(id));
822
- return newMessages;
823
- });
824
- this._conversationMessages.update((map) => {
825
- const newMap = new Map(map);
826
- newMap.delete(conversationId);
827
- return newMap;
828
- });
829
- }
830
- // =====================
831
- // Conversation Updates
832
- // =====================
833
- /**
834
- * Update conversation's last message
835
- * @param message - The message to set as last message
836
- */
837
- updateLastMessage(message) {
838
- this.updateConversation(message.conversationId, {
839
- lastMessage: message,
840
- lastMessageAt: message.timestamp,
841
- updatedAt: message.timestamp,
842
- });
843
- }
844
- /**
845
- * Increment unread count
846
- * @param conversationId - Conversation ID
847
- */
848
- incrementUnreadCount(conversationId) {
849
- const conversation = this._conversations().get(conversationId);
850
- if (!conversation)
851
- return;
852
- this.updateConversation(conversationId, { unreadCount: conversation.unreadCount + 1 });
853
- }
854
- /**
855
- * Reset unread count to zero
856
- * @param conversationId - Conversation ID
857
- */
858
- resetUnreadCount(conversationId) {
859
- this.updateConversation(conversationId, { unreadCount: 0 });
860
- }
861
- /**
862
- * Update conversation settings
863
- * @param conversationId - Conversation ID
864
- * @param settings - Settings to merge
865
- */
866
- updateSettings(conversationId, settings) {
867
- const conversation = this._conversations().get(conversationId);
868
- if (!conversation)
869
- return;
870
- this.updateConversation(conversationId, {
871
- settings: { ...conversation.settings, ...settings },
872
- updatedAt: new Date(),
873
- });
874
- }
875
- /**
876
- * Update conversation title
877
- * @param conversationId - Conversation ID
878
- * @param title - New title
879
- */
880
- updateTitle(conversationId, title) {
881
- this.updateConversation(conversationId, { title, updatedAt: new Date() });
882
- }
883
- /**
884
- * Update conversation metadata
885
- * @param conversationId - Conversation ID
886
- * @param metadata - Metadata to merge
887
- */
888
- updateMetadata(conversationId, metadata) {
889
- const conversation = this._conversations().get(conversationId);
890
- if (!conversation)
891
- return;
892
- this.updateConversation(conversationId, {
893
- metadata: { ...conversation.metadata, ...metadata },
894
- updatedAt: new Date(),
895
- });
896
- }
897
- /**
898
- * Update typing indicator
899
- * @param conversationId - Conversation ID
900
- * @param userId - User ID
901
- * @param isTyping - Whether user is typing
902
- */
903
- updateTypingIndicator(conversationId, userId, isTyping) {
904
- const conversation = this._conversations().get(conversationId);
905
- if (!conversation)
906
- return;
907
- let typingUsers = [...conversation.status.typingUsers];
908
- if (isTyping) {
909
- if (!typingUsers.includes(userId)) {
910
- typingUsers.push(userId);
911
- }
912
- }
913
- else {
914
- typingUsers = typingUsers.filter((id) => id !== userId);
915
- }
916
- this.updateConversation(conversationId, {
917
- status: {
918
- ...conversation.status,
919
- isTyping: typingUsers.length > 0,
920
- typingUsers,
921
- },
922
- });
923
- }
924
- /**
925
- * Update participant presence across all conversations
926
- * @param userId - User ID
927
- * @param status - Presence status
928
- * @param lastSeen - Last seen date
929
- */
930
- updateParticipantPresence(userId, status, lastSeen) {
931
- this._conversations.update((conversations) => {
932
- const newConversations = new Map(conversations);
933
- for (const [id, conv] of conversations) {
934
- const participant = conv.participants.find((p) => p.id === userId);
935
- if (participant) {
936
- const updatedConversation = {
937
- ...conv,
938
- participants: conv.participants.map((p) => (p.id === userId ? { ...p, status, lastSeen } : p)),
939
- status: conv.type === 'private' ? { ...conv.status, presence: status, lastSeen } : conv.status,
940
- };
941
- newConversations.set(id, updatedConversation);
942
- }
943
- }
944
- return newConversations;
945
- });
946
- }
947
- // =====================
948
- // Message Operations
949
- // =====================
950
- /**
951
- * Add a message to the store
952
- * @param message - Message to add
953
- */
954
- addMessage(message) {
955
- // Add to messages map
956
- this._messages.update((messages) => {
957
- const newMessages = new Map(messages);
958
- newMessages.set(message.id, message);
959
- return newMessages;
960
- });
961
- // Update conversation messages list (sorted) — always produce new arrays
962
- this._conversationMessages.update((map) => {
963
- const newMap = new Map(map);
964
- const existing = newMap.get(message.conversationId) || [];
965
- if (existing.includes(message.id)) {
966
- return newMap;
967
- }
968
- const updated = [...existing, message.id].sort((a, b) => {
969
- const msgA = this._messages().get(a);
970
- const msgB = this._messages().get(b);
971
- if (!msgA || !msgB)
972
- return 0;
973
- return msgA.timestamp.getTime() - msgB.timestamp.getTime();
974
- });
975
- newMap.set(message.conversationId, updated);
976
- return newMap;
977
- });
978
- }
979
- /**
980
- * Add multiple messages
981
- * @param messages - Messages to add
982
- */
983
- addMessages(messages) {
984
- if (messages.length === 0)
985
- return;
986
- // Add all messages to map
987
- this._messages.update((msgs) => {
988
- const newMessages = new Map(msgs);
989
- messages.forEach((msg) => newMessages.set(msg.id, msg));
990
- return newMessages;
991
- });
992
- // Update conversation messages lists
993
- const conversationGroups = new Map();
994
- messages.forEach((msg) => {
995
- const existing = conversationGroups.get(msg.conversationId) || [];
996
- existing.push(msg.id);
997
- conversationGroups.set(msg.conversationId, existing);
998
- });
999
- this._conversationMessages.update((map) => {
1000
- const newMap = new Map(map);
1001
- for (const [conversationId, newMsgIds] of conversationGroups) {
1002
- const existing = newMap.get(conversationId) || [];
1003
- const idSet = new Set(existing);
1004
- const merged = [...existing];
1005
- for (const id of newMsgIds) {
1006
- if (!idSet.has(id)) {
1007
- merged.push(id);
1008
- idSet.add(id);
1009
- }
1010
- }
1011
- const sorted = merged.sort((a, b) => {
1012
- const msgA = this._messages().get(a);
1013
- const msgB = this._messages().get(b);
1014
- if (!msgA || !msgB)
1015
- return 0;
1016
- return msgA.timestamp.getTime() - msgB.timestamp.getTime();
1017
- });
1018
- newMap.set(conversationId, sorted);
1019
- }
1020
- return newMap;
1021
- });
1022
- // Cleanup old messages to prevent memory leaks
1023
- this.cleanupOldMessages();
1024
- this.cleanupConversationMessages();
1025
- }
1026
- /**
1027
- * Get a message by ID
1028
- * @param messageId - Message ID
1029
- * @returns Message or undefined
1030
- */
1031
- getMessage(messageId) {
1032
- return this._messages().get(messageId);
1033
- }
1034
- /**
1035
- * Get messages for a conversation
1036
- * @param conversationId - Conversation ID
1037
- * @returns Array of messages sorted by timestamp
1038
- */
1039
- getConversationMessages(conversationId) {
1040
- const messageIds = this._conversationMessages().get(conversationId) || [];
1041
- return messageIds.map((id) => this._messages().get(id)).filter((msg) => msg !== undefined);
1042
- }
1043
- /**
1044
- * Get messages signal for a conversation
1045
- * @param conversationId - Conversation ID
1046
- * @returns Computed signal of messages
1047
- */
1048
- getConversationMessagesSignal(conversationId) {
1049
- return computed(() => this.getConversationMessages(conversationId));
1050
- }
1051
- /**
1052
- * Update a message
1053
- * @param messageId - Message ID
1054
- * @param updates - Partial message updates
1055
- */
1056
- updateMessage(messageId, updates) {
1057
- const message = this._messages().get(messageId);
1058
- if (!message)
1059
- return;
1060
- const updatedMessage = { ...message, ...updates };
1061
- this.addMessage(updatedMessage);
1062
- }
1063
- /**
1064
- * Delete a message
1065
- * @param messageId - Message ID
1066
- */
1067
- deleteMessage(messageId) {
1068
- const message = this._messages().get(messageId);
1069
- if (!message)
1070
- return;
1071
- // Remove from messages map
1072
- this._messages.update((messages) => {
1073
- const newMessages = new Map(messages);
1074
- newMessages.delete(messageId);
1075
- return newMessages;
1076
- });
1077
- // Remove from conversation messages list — produce a new array via filter
1078
- this._conversationMessages.update((map) => {
1079
- const newMap = new Map(map);
1080
- const existing = newMap.get(message.conversationId);
1081
- if (existing) {
1082
- newMap.set(message.conversationId, existing.filter((id) => id !== messageId));
1083
- }
1084
- return newMap;
1085
- });
1086
- }
1087
- /**
1088
- * Clear messages for a conversation
1089
- * @param conversationId - Conversation ID
1090
- */
1091
- clearConversationMessages(conversationId) {
1092
- const messageIds = this._conversationMessages().get(conversationId) || [];
1093
- // Remove messages
1094
- this._messages.update((messages) => {
1095
- const newMessages = new Map(messages);
1096
- messageIds.forEach((id) => newMessages.delete(id));
1097
- return newMessages;
1098
- });
1099
- // Clear conversation messages list
1100
- this._conversationMessages.update((map) => {
1101
- const newMap = new Map(map);
1102
- newMap.delete(conversationId);
1103
- return newMap;
1104
- });
1105
- }
1106
- /**
1107
- * Clear all data
1108
- */
1109
- clearAll() {
1110
- this._conversations.set(new Map());
1111
- this._messages.set(new Map());
1112
- this._conversationMessages.set(new Map());
1113
- }
1114
- // =====================
1115
- // Memory Management
1116
- // =====================
1117
- /**
1118
- * Cleanup old messages to prevent unbounded memory growth
1119
- * Keeps only the most recent messages up to MAX_TOTAL_MESSAGES
1120
- */
1121
- cleanupOldMessages() {
1122
- const totalMessages = this._messages().size;
1123
- if (totalMessages > this.config.maxTotalMessages) {
1124
- // Get all messages sorted by timestamp (newest first)
1125
- const allMessages = Array.from(this._messages().values()).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
1126
- // Keep only the newest messages
1127
- const toKeep = allMessages.slice(0, this.config.maxTotalMessages);
1128
- const toKeepIds = new Set(toKeep.map((m) => m.id));
1129
- // Remove old messages
1130
- this._messages.update(() => {
1131
- const newMessages = new Map();
1132
- toKeep.forEach((msg) => newMessages.set(msg.id, msg));
1133
- return newMessages;
1134
- });
1135
- // Update conversation messages lists to remove deleted message IDs
1136
- this._conversationMessages.update((map) => {
1137
- const newMap = new Map(map);
1138
- for (const [convId, messageIds] of newMap) {
1139
- const filteredIds = messageIds.filter((id) => toKeepIds.has(id));
1140
- newMap.set(convId, filteredIds);
1141
- }
1142
- return newMap;
1143
- });
1144
- }
1145
- }
1146
- /**
1147
- * Cleanup messages per conversation to prevent memory leaks
1148
- * Keeps only recent messages per conversation
1149
- */
1150
- cleanupConversationMessages() {
1151
- const maxMessages = this.config.maxMessagesPerConversation;
1152
- const idsToRemove = [];
1153
- this._conversationMessages.update((map) => {
1154
- const newMap = new Map(map);
1155
- for (const [convId, messageIds] of newMap) {
1156
- if (messageIds.length > maxMessages) {
1157
- idsToRemove.push(...messageIds.slice(0, messageIds.length - maxMessages));
1158
- newMap.set(convId, messageIds.slice(-maxMessages));
1159
- }
1160
- }
1161
- return newMap;
1162
- });
1163
- if (idsToRemove.length > 0) {
1164
- this._messages.update((messages) => {
1165
- const newMessages = new Map(messages);
1166
- idsToRemove.forEach((id) => newMessages.delete(id));
1167
- return newMessages;
1168
- });
1169
- }
1170
- }
1171
- // =====================
1172
- // Statistics
1173
- // =====================
1174
- /**
1175
- * Get store statistics
1176
- */
1177
- getStats() {
1178
- return {
1179
- conversationCount: this._conversations().size,
1180
- messageCount: this._messages().size,
1181
- conversationsWithMessages: this._conversationMessages().size,
1182
- };
1183
- }
1184
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXConversationStoreService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1185
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXConversationStoreService }); }
1186
- }
1187
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXConversationStoreService, decorators: [{
1188
- type: Injectable
1189
- }] });
1190
-
1191
- /**
1192
- * Validation Utilities
1193
- * Centralized validation functions for messages, files, and user input
1194
- */
1195
- /**
1196
- * Validate message text content
1197
- * @param text - Text to validate
1198
- * @param config - Configuration for validation rules
1199
- * @returns Validation result
1200
- */
1201
- function validateMessageText(text, config) {
1202
- // Check for empty text
1203
- if (!text || text.trim().length === 0) {
1204
- return {
1205
- valid: false,
1206
- error: 'Message text cannot be empty',
1207
- errorCode: 'EMPTY_MESSAGE',
1208
- };
1209
- }
1210
- // Check minimum length
1211
- const minLength = config.minMessageLength ?? 1;
1212
- if (text.trim().length < minLength) {
1213
- return {
1214
- valid: false,
1215
- error: `Message must be at least ${minLength} character(s)`,
1216
- errorCode: 'MESSAGE_TOO_SHORT',
1217
- };
1218
- }
1219
- // Check maximum length
1220
- const maxLength = config.maxMessageLength ?? 10000;
1221
- if (text.length > maxLength) {
1222
- return {
1223
- valid: false,
1224
- error: `Message exceeds ${maxLength} character limit`,
1225
- errorCode: 'MESSAGE_TOO_LONG',
1226
- };
1227
- }
1228
- return { valid: true };
1229
- }
1230
- /**
1231
- * Validate file upload
1232
- * @param file - File to validate
1233
- * @param config - Configuration for validation rules
1234
- * @returns File validation result
1235
- */
1236
- function validateFile(file, config) {
1237
- // Check file size
1238
- const maxSize = config.maxFileSize ?? 10 * 1024 * 1024; // 10MB default
1239
- if (file.size > maxSize) {
1240
- return {
1241
- valid: false,
1242
- error: `File size exceeds ${formatFileSize(maxSize)} limit`,
1243
- errorCode: 'FILE_TOO_LARGE',
1244
- size: file.size,
1245
- type: file.type,
1246
- };
1247
- }
1248
- // Check file type if restrictions exist
1249
- const allowedTypes = config.allowedFileTypes;
1250
- if (allowedTypes && allowedTypes.length > 0) {
1251
- const isAllowed = allowedTypes.some((pattern) => matchMimeType(file.type, pattern));
1252
- if (!isAllowed) {
1253
- return {
1254
- valid: false,
1255
- error: `File type "${file.type}" is not allowed`,
1256
- errorCode: 'FILE_TYPE_NOT_ALLOWED',
1257
- size: file.size,
1258
- type: file.type,
1259
- };
1260
- }
1261
- }
1262
- return {
1263
- valid: true,
1264
- size: file.size,
1265
- type: file.type,
1266
- };
1267
- }
1268
- /**
1269
- * Validate conversation ID
1270
- * @param conversationId - Conversation ID to validate
1271
- * @returns Validation result
1272
- */
1273
- function validateConversationId(conversationId) {
1274
- if (!conversationId || typeof conversationId !== 'string' || conversationId.trim().length === 0) {
1275
- return {
1276
- valid: false,
1277
- error: 'Conversation ID is required',
1278
- errorCode: 'MISSING_CONVERSATION_ID',
1279
- };
1280
- }
1281
- // Check for reasonable length
1282
- if (conversationId.length > 255) {
1283
- return {
1284
- valid: false,
1285
- error: 'Conversation ID is too long',
1286
- errorCode: 'MISSING_CONVERSATION_ID',
511
+ // Check for reasonable length
512
+ if (conversationId.length > 255) {
513
+ return {
514
+ valid: false,
515
+ error: 'Conversation ID is too long',
516
+ errorCode: 'MISSING_CONVERSATION_ID',
1287
517
  };
1288
518
  }
1289
519
  return { valid: true };
@@ -1354,242 +584,784 @@ function validateMessagePayload(payload, type) {
1354
584
  }
1355
585
  break;
1356
586
  }
1357
- return { valid: true };
587
+ return { valid: true };
588
+ }
589
+ /**
590
+ * Validate user ID
591
+ * @param userId - User ID to validate
592
+ * @returns Validation result
593
+ */
594
+ function validateUserId(userId) {
595
+ if (!userId || typeof userId !== 'string' || userId.trim().length === 0) {
596
+ return {
597
+ valid: false,
598
+ error: 'User ID is required',
599
+ errorCode: 'MISSING_USER_ID',
600
+ };
601
+ }
602
+ // Check for reasonable length (prevent extremely long IDs)
603
+ if (userId.length > 255) {
604
+ return {
605
+ valid: false,
606
+ error: 'User ID is too long',
607
+ errorCode: 'INVALID_USER_ID',
608
+ };
609
+ }
610
+ return { valid: true };
611
+ }
612
+ /**
613
+ * Validate array of user IDs
614
+ * @param userIds - Array of user IDs to validate
615
+ * @param minCount - Minimum number of users required
616
+ * @param maxCount - Maximum number of users allowed
617
+ * @returns Validation result
618
+ */
619
+ function validateUserIds(userIds, minCount = 1, maxCount) {
620
+ if (!userIds || !Array.isArray(userIds)) {
621
+ return {
622
+ valid: false,
623
+ error: 'User IDs must be an array',
624
+ errorCode: 'INVALID_USER_IDS',
625
+ };
626
+ }
627
+ if (userIds.length < minCount) {
628
+ return {
629
+ valid: false,
630
+ error: `At least ${minCount} user(s) required`,
631
+ errorCode: 'TOO_FEW_USERS',
632
+ };
633
+ }
634
+ if (maxCount && userIds.length > maxCount) {
635
+ return {
636
+ valid: false,
637
+ error: `Maximum ${maxCount} user(s) allowed`,
638
+ errorCode: 'TOO_MANY_USERS',
639
+ };
640
+ }
641
+ // Check for empty or invalid IDs
642
+ const invalidIds = userIds.filter((id) => !id || id.trim().length === 0);
643
+ if (invalidIds.length > 0) {
644
+ return {
645
+ valid: false,
646
+ error: 'All user IDs must be non-empty strings',
647
+ errorCode: 'INVALID_USER_ID',
648
+ };
649
+ }
650
+ return { valid: true };
651
+ }
652
+ /**
653
+ * Validate email address
654
+ * @param email - Email to validate
655
+ * @returns Validation result
656
+ */
657
+ function validateEmail(email) {
658
+ if (!email || email.trim().length === 0) {
659
+ return {
660
+ valid: false,
661
+ error: 'Email is required',
662
+ errorCode: 'MISSING_EMAIL',
663
+ };
664
+ }
665
+ // Trim whitespace
666
+ const trimmedEmail = email.trim();
667
+ // Check length constraints
668
+ if (trimmedEmail.length > 254) {
669
+ return {
670
+ valid: false,
671
+ error: 'Email is too long',
672
+ errorCode: 'INVALID_EMAIL',
673
+ };
674
+ }
675
+ // Enhanced email regex with better validation
676
+ const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
677
+ if (!emailRegex.test(trimmedEmail)) {
678
+ return {
679
+ valid: false,
680
+ error: 'Invalid email format',
681
+ errorCode: 'INVALID_EMAIL',
682
+ };
683
+ }
684
+ return { valid: true };
685
+ }
686
+ /**
687
+ * Validate URL
688
+ * @param url - URL to validate
689
+ * @returns Validation result
690
+ */
691
+ function validateUrl(url) {
692
+ if (!url || url.trim().length === 0) {
693
+ return {
694
+ valid: false,
695
+ error: 'URL is required',
696
+ errorCode: 'MISSING_URL',
697
+ };
698
+ }
699
+ const trimmedUrl = url.trim();
700
+ // Check for common URL issues
701
+ if (trimmedUrl.length > 2048) {
702
+ return {
703
+ valid: false,
704
+ error: 'URL is too long',
705
+ errorCode: 'INVALID_URL',
706
+ };
707
+ }
708
+ try {
709
+ const urlObj = new URL(trimmedUrl);
710
+ // Validate protocol
711
+ if (!['http:', 'https:', 'ftp:', 'ftps:'].includes(urlObj.protocol)) {
712
+ return {
713
+ valid: false,
714
+ error: 'Invalid URL protocol',
715
+ errorCode: 'INVALID_URL',
716
+ };
717
+ }
718
+ return { valid: true };
719
+ }
720
+ catch {
721
+ return {
722
+ valid: false,
723
+ error: 'Invalid URL format',
724
+ errorCode: 'INVALID_URL',
725
+ };
726
+ }
727
+ }
728
+ // =====================
729
+ // Helper Functions
730
+ // =====================
731
+ /**
732
+ * Match MIME type against a pattern (supports wildcards)
733
+ * @param mimeType - MIME type to check
734
+ * @param pattern - Pattern to match (e.g., "image/*", "video/mp4")
735
+ * @returns Whether the MIME type matches the pattern
736
+ */
737
+ function matchMimeType(mimeType, pattern) {
738
+ if (pattern === '*/*' || pattern === '*') {
739
+ return true;
740
+ }
741
+ if (pattern.endsWith('/*')) {
742
+ const prefix = pattern.slice(0, -2);
743
+ return mimeType.startsWith(prefix);
744
+ }
745
+ return mimeType === pattern;
1358
746
  }
1359
747
  /**
1360
- * Validate user ID
1361
- * @param userId - User ID to validate
748
+ * Format file size in human-readable format
749
+ * @param bytes - File size in bytes
750
+ * @returns Formatted file size string
751
+ */
752
+ function formatFileSize(bytes) {
753
+ if (bytes === 0)
754
+ return '0 Bytes';
755
+ const k = 1024;
756
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
757
+ const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);
758
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
759
+ }
760
+ /**
761
+ * Sanitize user input to prevent XSS
762
+ * Note: Angular provides built-in sanitization, but this is an additional layer
763
+ * @param input - User input to sanitize
764
+ * @returns Sanitized input
765
+ */
766
+ function sanitizeInput(input) {
767
+ if (!input)
768
+ return '';
769
+ return input
770
+ .replace(/&/g, '&amp;')
771
+ .replace(/</g, '&lt;')
772
+ .replace(/>/g, '&gt;')
773
+ .replace(/"/g, '&quot;')
774
+ .replace(/'/g, '&#x27;')
775
+ .replace(/\//g, '&#x2F;')
776
+ .replace(/`/g, '&#x60;')
777
+ .replace(/=/g, '&#x3D;');
778
+ }
779
+ /**
780
+ * Validate latitude coordinate
781
+ * @param latitude - Latitude to validate
1362
782
  * @returns Validation result
1363
783
  */
1364
- function validateUserId(userId) {
1365
- if (!userId || typeof userId !== 'string' || userId.trim().length === 0) {
784
+ function validateLatitude(latitude) {
785
+ if (latitude === undefined || latitude === null || typeof latitude !== 'number' || isNaN(latitude)) {
1366
786
  return {
1367
787
  valid: false,
1368
- error: 'User ID is required',
1369
- errorCode: 'MISSING_USER_ID',
788
+ error: 'Latitude is required',
789
+ errorCode: 'MISSING_LATITUDE',
1370
790
  };
1371
791
  }
1372
- // Check for reasonable length (prevent extremely long IDs)
1373
- if (userId.length > 255) {
792
+ if (latitude < -90 || latitude > 90) {
1374
793
  return {
1375
794
  valid: false,
1376
- error: 'User ID is too long',
1377
- errorCode: 'INVALID_USER_ID',
795
+ error: 'Latitude must be between -90 and 90',
796
+ errorCode: 'INVALID_LATITUDE',
1378
797
  };
1379
798
  }
1380
799
  return { valid: true };
1381
800
  }
1382
801
  /**
1383
- * Validate array of user IDs
1384
- * @param userIds - Array of user IDs to validate
1385
- * @param minCount - Minimum number of users required
1386
- * @param maxCount - Maximum number of users allowed
802
+ * Validate longitude coordinate
803
+ * @param longitude - Longitude to validate
1387
804
  * @returns Validation result
1388
805
  */
1389
- function validateUserIds(userIds, minCount = 1, maxCount) {
1390
- if (!userIds || !Array.isArray(userIds)) {
806
+ function validateLongitude(longitude) {
807
+ if (longitude === undefined || longitude === null || typeof longitude !== 'number' || isNaN(longitude)) {
1391
808
  return {
1392
809
  valid: false,
1393
- error: 'User IDs must be an array',
1394
- errorCode: 'INVALID_USER_IDS',
810
+ error: 'Longitude is required',
811
+ errorCode: 'MISSING_LONGITUDE',
1395
812
  };
1396
813
  }
1397
- if (userIds.length < minCount) {
814
+ if (longitude < -180 || longitude > 180) {
1398
815
  return {
1399
816
  valid: false,
1400
- error: `At least ${minCount} user(s) required`,
1401
- errorCode: 'TOO_FEW_USERS',
817
+ error: 'Longitude must be between -180 and 180',
818
+ errorCode: 'INVALID_LONGITUDE',
1402
819
  };
1403
820
  }
1404
- if (maxCount && userIds.length > maxCount) {
1405
- return {
1406
- valid: false,
1407
- error: `Maximum ${maxCount} user(s) allowed`,
1408
- errorCode: 'TOO_MANY_USERS',
1409
- };
821
+ return { valid: true };
822
+ }
823
+
824
+ /**
825
+ * In-memory conversation and message graph (signal-based).
826
+ * Plain class — not DI-registered; instantiated by `AXConversationService`.
827
+ */
828
+ class ConversationState {
829
+ constructor(config) {
830
+ this.config = config;
831
+ this._conversations = signal(new Map(), ...(ngDevMode ? [{ debugName: "_conversations" }] : []));
832
+ this._messages = signal(new Map(), ...(ngDevMode ? [{ debugName: "_messages" }] : []));
833
+ this._conversationMessages = signal(new Map(), ...(ngDevMode ? [{ debugName: "_conversationMessages" }] : []));
834
+ this.conversations = computed(() => {
835
+ const convMap = this._conversations();
836
+ return Array.from(convMap.values());
837
+ }, { ...(ngDevMode ? { debugName: "conversations" } : {}), equal: (a, b) => a.length === b.length && a.every((v, i) => v === b[i]) });
1410
838
  }
1411
- // Check for empty or invalid IDs
1412
- const invalidIds = userIds.filter((id) => !id || id.trim().length === 0);
1413
- if (invalidIds.length > 0) {
1414
- return {
1415
- valid: false,
1416
- error: 'All user IDs must be non-empty strings',
1417
- errorCode: 'INVALID_USER_ID',
1418
- };
839
+ setConversations(conversations) {
840
+ const convMap = new Map();
841
+ conversations.forEach((conv) => convMap.set(conv.id, conv));
842
+ this._conversations.set(convMap);
843
+ }
844
+ addConversations(conversations) {
845
+ this._conversations.update((existingConversations) => {
846
+ const newConversations = new Map(existingConversations);
847
+ conversations.forEach((conv) => newConversations.set(conv.id, conv));
848
+ const maxCached = this.config.maxCachedConversations;
849
+ if (newConversations.size > maxCached) {
850
+ const sorted = Array.from(newConversations.values()).sort((a, b) => (b.lastMessageAt?.getTime() ?? 0) - (a.lastMessageAt?.getTime() ?? 0));
851
+ const toKeep = sorted.slice(0, maxCached);
852
+ const cleanedMap = new Map();
853
+ toKeep.forEach((conv) => cleanedMap.set(conv.id, conv));
854
+ return cleanedMap;
855
+ }
856
+ return newConversations;
857
+ });
858
+ }
859
+ setConversation(conversation) {
860
+ this._conversations.update((conversations) => {
861
+ const newConversations = new Map(conversations);
862
+ newConversations.set(conversation.id, conversation);
863
+ return newConversations;
864
+ });
865
+ }
866
+ getConversation(conversationId) {
867
+ return this._conversations().get(conversationId);
868
+ }
869
+ updateConversation(conversationId, updates) {
870
+ const conversation = this._conversations().get(conversationId);
871
+ if (!conversation)
872
+ return;
873
+ this.setConversation({ ...conversation, ...updates });
874
+ }
875
+ deleteConversation(conversationId) {
876
+ this._conversations.update((conversations) => {
877
+ const newConversations = new Map(conversations);
878
+ newConversations.delete(conversationId);
879
+ return newConversations;
880
+ });
881
+ const messageIds = this._conversationMessages().get(conversationId) || [];
882
+ this._messages.update((messages) => {
883
+ const newMessages = new Map(messages);
884
+ messageIds.forEach((id) => newMessages.delete(id));
885
+ return newMessages;
886
+ });
887
+ this._conversationMessages.update((map) => {
888
+ const newMap = new Map(map);
889
+ newMap.delete(conversationId);
890
+ return newMap;
891
+ });
892
+ }
893
+ updateLastMessage(message) {
894
+ this.updateConversation(message.conversationId, {
895
+ lastMessage: message,
896
+ lastMessageAt: message.timestamp,
897
+ updatedAt: message.timestamp,
898
+ });
899
+ }
900
+ incrementUnreadCount(conversationId) {
901
+ const conversation = this._conversations().get(conversationId);
902
+ if (!conversation)
903
+ return;
904
+ this.updateConversation(conversationId, { unreadCount: conversation.unreadCount + 1 });
905
+ }
906
+ resetUnreadCount(conversationId) {
907
+ this.updateConversation(conversationId, { unreadCount: 0 });
908
+ }
909
+ updateSettings(conversationId, settings) {
910
+ const conversation = this._conversations().get(conversationId);
911
+ if (!conversation)
912
+ return;
913
+ this.updateConversation(conversationId, {
914
+ settings: { ...conversation.settings, ...settings },
915
+ updatedAt: new Date(),
916
+ });
917
+ }
918
+ updateTitle(conversationId, title) {
919
+ this.updateConversation(conversationId, { title, updatedAt: new Date() });
920
+ }
921
+ updateMetadata(conversationId, metadata) {
922
+ const conversation = this._conversations().get(conversationId);
923
+ if (!conversation)
924
+ return;
925
+ this.updateConversation(conversationId, {
926
+ metadata: { ...conversation.metadata, ...metadata },
927
+ updatedAt: new Date(),
928
+ });
929
+ }
930
+ updateTypingIndicator(conversationId, userId, isTyping) {
931
+ const conversation = this._conversations().get(conversationId);
932
+ if (!conversation)
933
+ return;
934
+ let typingUsers = [...conversation.status.typingUsers];
935
+ if (isTyping) {
936
+ if (!typingUsers.includes(userId)) {
937
+ typingUsers.push(userId);
938
+ }
939
+ }
940
+ else {
941
+ typingUsers = typingUsers.filter((id) => id !== userId);
942
+ }
943
+ this.updateConversation(conversationId, {
944
+ status: {
945
+ ...conversation.status,
946
+ isTyping: typingUsers.length > 0,
947
+ typingUsers,
948
+ },
949
+ });
950
+ }
951
+ updateParticipantPresence(userId, status, lastSeen) {
952
+ this._conversations.update((conversations) => {
953
+ const newConversations = new Map(conversations);
954
+ for (const [id, conv] of conversations) {
955
+ const participant = conv.participants.find((p) => p.id === userId);
956
+ if (participant) {
957
+ const updatedConversation = {
958
+ ...conv,
959
+ participants: conv.participants.map((p) => (p.id === userId ? { ...p, status, lastSeen } : p)),
960
+ status: conv.type === 'private' ? { ...conv.status, presence: status, lastSeen } : conv.status,
961
+ };
962
+ newConversations.set(id, updatedConversation);
963
+ }
964
+ }
965
+ return newConversations;
966
+ });
967
+ }
968
+ addMessage(message) {
969
+ this._messages.update((messages) => {
970
+ const newMessages = new Map(messages);
971
+ newMessages.set(message.id, message);
972
+ return newMessages;
973
+ });
974
+ this._conversationMessages.update((map) => {
975
+ const newMap = new Map(map);
976
+ const existing = newMap.get(message.conversationId) || [];
977
+ if (existing.includes(message.id)) {
978
+ return newMap;
979
+ }
980
+ const updated = [...existing, message.id].sort((a, b) => {
981
+ const msgA = this._messages().get(a);
982
+ const msgB = this._messages().get(b);
983
+ if (!msgA || !msgB)
984
+ return 0;
985
+ return msgA.timestamp.getTime() - msgB.timestamp.getTime();
986
+ });
987
+ newMap.set(message.conversationId, updated);
988
+ return newMap;
989
+ });
1419
990
  }
1420
- return { valid: true };
1421
- }
1422
- /**
1423
- * Validate email address
1424
- * @param email - Email to validate
1425
- * @returns Validation result
1426
- */
1427
- function validateEmail(email) {
1428
- if (!email || email.trim().length === 0) {
1429
- return {
1430
- valid: false,
1431
- error: 'Email is required',
1432
- errorCode: 'MISSING_EMAIL',
1433
- };
991
+ addMessages(messages) {
992
+ if (messages.length === 0)
993
+ return;
994
+ this._messages.update((msgs) => {
995
+ const newMessages = new Map(msgs);
996
+ messages.forEach((msg) => newMessages.set(msg.id, msg));
997
+ return newMessages;
998
+ });
999
+ const conversationGroups = new Map();
1000
+ messages.forEach((msg) => {
1001
+ const existing = conversationGroups.get(msg.conversationId) || [];
1002
+ existing.push(msg.id);
1003
+ conversationGroups.set(msg.conversationId, existing);
1004
+ });
1005
+ this._conversationMessages.update((map) => {
1006
+ const newMap = new Map(map);
1007
+ for (const [conversationId, newMsgIds] of conversationGroups) {
1008
+ const existing = newMap.get(conversationId) || [];
1009
+ const idSet = new Set(existing);
1010
+ const merged = [...existing];
1011
+ for (const id of newMsgIds) {
1012
+ if (!idSet.has(id)) {
1013
+ merged.push(id);
1014
+ idSet.add(id);
1015
+ }
1016
+ }
1017
+ const sorted = merged.sort((a, b) => {
1018
+ const msgA = this._messages().get(a);
1019
+ const msgB = this._messages().get(b);
1020
+ if (!msgA || !msgB)
1021
+ return 0;
1022
+ return msgA.timestamp.getTime() - msgB.timestamp.getTime();
1023
+ });
1024
+ newMap.set(conversationId, sorted);
1025
+ }
1026
+ return newMap;
1027
+ });
1028
+ this.cleanupOldMessages();
1029
+ this.cleanupConversationMessages();
1434
1030
  }
1435
- // Trim whitespace
1436
- const trimmedEmail = email.trim();
1437
- // Check length constraints
1438
- if (trimmedEmail.length > 254) {
1439
- return {
1440
- valid: false,
1441
- error: 'Email is too long',
1442
- errorCode: 'INVALID_EMAIL',
1443
- };
1031
+ getMessage(messageId) {
1032
+ return this._messages().get(messageId);
1444
1033
  }
1445
- // Enhanced email regex with better validation
1446
- const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
1447
- if (!emailRegex.test(trimmedEmail)) {
1448
- return {
1449
- valid: false,
1450
- error: 'Invalid email format',
1451
- errorCode: 'INVALID_EMAIL',
1452
- };
1034
+ getConversationMessages(conversationId) {
1035
+ const messageIds = this._conversationMessages().get(conversationId) || [];
1036
+ return messageIds.map((id) => this._messages().get(id)).filter((msg) => msg !== undefined);
1453
1037
  }
1454
- return { valid: true };
1455
- }
1456
- /**
1457
- * Validate URL
1458
- * @param url - URL to validate
1459
- * @returns Validation result
1460
- */
1461
- function validateUrl(url) {
1462
- if (!url || url.trim().length === 0) {
1463
- return {
1464
- valid: false,
1465
- error: 'URL is required',
1466
- errorCode: 'MISSING_URL',
1467
- };
1038
+ updateMessage(messageId, updates) {
1039
+ const message = this._messages().get(messageId);
1040
+ if (!message)
1041
+ return;
1042
+ this.addMessage({ ...message, ...updates });
1468
1043
  }
1469
- const trimmedUrl = url.trim();
1470
- // Check for common URL issues
1471
- if (trimmedUrl.length > 2048) {
1472
- return {
1473
- valid: false,
1474
- error: 'URL is too long',
1475
- errorCode: 'INVALID_URL',
1476
- };
1044
+ deleteMessage(messageId) {
1045
+ const message = this._messages().get(messageId);
1046
+ if (!message)
1047
+ return;
1048
+ this._messages.update((messages) => {
1049
+ const newMessages = new Map(messages);
1050
+ newMessages.delete(messageId);
1051
+ return newMessages;
1052
+ });
1053
+ this._conversationMessages.update((map) => {
1054
+ const newMap = new Map(map);
1055
+ const existing = newMap.get(message.conversationId);
1056
+ if (existing) {
1057
+ newMap.set(message.conversationId, existing.filter((id) => id !== messageId));
1058
+ }
1059
+ return newMap;
1060
+ });
1477
1061
  }
1478
- try {
1479
- const urlObj = new URL(trimmedUrl);
1480
- // Validate protocol
1481
- if (!['http:', 'https:', 'ftp:', 'ftps:'].includes(urlObj.protocol)) {
1482
- return {
1483
- valid: false,
1484
- error: 'Invalid URL protocol',
1485
- errorCode: 'INVALID_URL',
1486
- };
1062
+ cleanupOldMessages() {
1063
+ const totalMessages = this._messages().size;
1064
+ if (totalMessages > this.config.maxTotalMessages) {
1065
+ const allMessages = Array.from(this._messages().values()).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
1066
+ const toKeep = allMessages.slice(0, this.config.maxTotalMessages);
1067
+ const toKeepIds = new Set(toKeep.map((m) => m.id));
1068
+ this._messages.update(() => {
1069
+ const newMessages = new Map();
1070
+ toKeep.forEach((msg) => newMessages.set(msg.id, msg));
1071
+ return newMessages;
1072
+ });
1073
+ this._conversationMessages.update((map) => {
1074
+ const newMap = new Map(map);
1075
+ for (const [convId, messageIds] of newMap) {
1076
+ const filteredIds = messageIds.filter((id) => toKeepIds.has(id));
1077
+ newMap.set(convId, filteredIds);
1078
+ }
1079
+ return newMap;
1080
+ });
1487
1081
  }
1488
- return { valid: true };
1489
- }
1490
- catch {
1491
- return {
1492
- valid: false,
1493
- error: 'Invalid URL format',
1494
- errorCode: 'INVALID_URL',
1495
- };
1496
- }
1497
- }
1498
- // =====================
1499
- // Helper Functions
1500
- // =====================
1501
- /**
1502
- * Match MIME type against a pattern (supports wildcards)
1503
- * @param mimeType - MIME type to check
1504
- * @param pattern - Pattern to match (e.g., "image/*", "video/mp4")
1505
- * @returns Whether the MIME type matches the pattern
1506
- */
1507
- function matchMimeType(mimeType, pattern) {
1508
- if (pattern === '*/*' || pattern === '*') {
1509
- return true;
1510
1082
  }
1511
- if (pattern.endsWith('/*')) {
1512
- const prefix = pattern.slice(0, -2);
1513
- return mimeType.startsWith(prefix);
1083
+ cleanupConversationMessages() {
1084
+ const maxMessages = this.config.maxMessagesPerConversation;
1085
+ const idsToRemove = [];
1086
+ this._conversationMessages.update((map) => {
1087
+ const newMap = new Map(map);
1088
+ for (const [convId, messageIds] of newMap) {
1089
+ if (messageIds.length > maxMessages) {
1090
+ idsToRemove.push(...messageIds.slice(0, messageIds.length - maxMessages));
1091
+ newMap.set(convId, messageIds.slice(-maxMessages));
1092
+ }
1093
+ }
1094
+ return newMap;
1095
+ });
1096
+ if (idsToRemove.length > 0) {
1097
+ this._messages.update((messages) => {
1098
+ const newMessages = new Map(messages);
1099
+ idsToRemove.forEach((id) => newMessages.delete(id));
1100
+ return newMessages;
1101
+ });
1102
+ }
1514
1103
  }
1515
- return mimeType === pattern;
1516
- }
1517
- /**
1518
- * Format file size in human-readable format
1519
- * @param bytes - File size in bytes
1520
- * @returns Formatted file size string
1521
- */
1522
- function formatFileSize(bytes) {
1523
- if (bytes === 0)
1524
- return '0 Bytes';
1525
- const k = 1024;
1526
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
1527
- const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);
1528
- return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
1529
1104
  }
1105
+
1530
1106
  /**
1531
- * Sanitize user input to prevent XSS
1532
- * Note: Angular provides built-in sanitization, but this is an additional layer
1533
- * @param input - User input to sanitize
1534
- * @returns Sanitized input
1107
+ * Error Handler Service
1108
+ * Centralized error handling and logging
1535
1109
  */
1536
- function sanitizeInput(input) {
1537
- if (!input)
1538
- return '';
1539
- return input
1540
- .replace(/&/g, '&amp;')
1541
- .replace(/</g, '&lt;')
1542
- .replace(/>/g, '&gt;')
1543
- .replace(/"/g, '&quot;')
1544
- .replace(/'/g, '&#x27;')
1545
- .replace(/\//g, '&#x2F;')
1546
- .replace(/`/g, '&#x60;')
1547
- .replace(/=/g, '&#x3D;');
1548
- }
1549
1110
  /**
1550
- * Validate latitude coordinate
1551
- * @param latitude - Latitude to validate
1552
- * @returns Validation result
1111
+ * Error Handler Service
1553
1112
  */
1554
- function validateLatitude(latitude) {
1555
- if (latitude === undefined || latitude === null || typeof latitude !== 'number' || isNaN(latitude)) {
1556
- return {
1557
- valid: false,
1558
- error: 'Latitude is required',
1559
- errorCode: 'MISSING_LATITUDE',
1113
+ class AXErrorHandlerService {
1114
+ constructor() {
1115
+ this.injectedConfig = inject(ERROR_HANDLER_CONFIG);
1116
+ this._errors$ = new Subject();
1117
+ this._config = {
1118
+ logToConsole: true,
1119
+ showUserMessages: true,
1120
+ autoRetry: false,
1121
+ maxRetries: 3,
1560
1122
  };
1123
+ /** Error stream */
1124
+ this.errors$ = this._errors$.asObservable();
1125
+ this.configure(this.injectedConfig);
1126
+ }
1127
+ /**
1128
+ * Configure error handler
1129
+ */
1130
+ configure(config) {
1131
+ Object.assign(this._config, config);
1132
+ }
1133
+ /**
1134
+ * Handle an error
1135
+ */
1136
+ handle(error, operation, context) {
1137
+ const conversationError = this.normalizeError(error, operation, context);
1138
+ this.publish(conversationError);
1139
+ return conversationError;
1561
1140
  }
1562
- if (latitude < -90 || latitude > 90) {
1141
+ /**
1142
+ * Handle API error (same pipeline as {@link handle}, for typed API failures)
1143
+ */
1144
+ handleApiError(apiError, operation, context) {
1145
+ const conversationError = this.conversationErrorFromApi(apiError, operation, context);
1146
+ this.publish(conversationError);
1147
+ return conversationError;
1148
+ }
1149
+ publish(conversationError) {
1150
+ this._errors$.next(conversationError);
1151
+ if (this._config.logToConsole) {
1152
+ this.logError(conversationError);
1153
+ }
1154
+ if (this._config.customHandler) {
1155
+ this._config.customHandler(conversationError);
1156
+ }
1157
+ }
1158
+ /**
1159
+ * Normalize any error to conversation error format (does not publish — use {@link handle})
1160
+ */
1161
+ normalizeError(error, operation, context) {
1162
+ if (this.isApiError(error)) {
1163
+ return this.conversationErrorFromApi(error, operation, context);
1164
+ }
1165
+ const errorObj = error;
1166
+ const message = (typeof errorObj['message'] === 'string' && errorObj['message']) ||
1167
+ (error instanceof Error ? error.message : String(error)) ||
1168
+ 'An unknown error occurred';
1169
+ const code = (typeof errorObj['code'] === 'string' && errorObj['code']) || 'UNKNOWN_ERROR';
1170
+ const statusCodeRaw = errorObj['statusCode'] ?? errorObj['status'];
1171
+ const statusCode = typeof statusCodeRaw === 'number' ? statusCodeRaw : undefined;
1563
1172
  return {
1564
- valid: false,
1565
- error: 'Latitude must be between -90 and 90',
1566
- errorCode: 'INVALID_LATITUDE',
1173
+ code,
1174
+ message,
1175
+ severity: 'error',
1176
+ operation,
1177
+ originalError: error,
1178
+ statusCode,
1179
+ context,
1180
+ timestamp: new Date(),
1181
+ handled: false,
1182
+ recoverySuggestions: this.getDefaultRecoverySuggestions(code),
1567
1183
  };
1568
1184
  }
1569
- return { valid: true };
1570
- }
1571
- /**
1572
- * Validate longitude coordinate
1573
- * @param longitude - Longitude to validate
1574
- * @returns Validation result
1575
- */
1576
- function validateLongitude(longitude) {
1577
- if (longitude === undefined || longitude === null || typeof longitude !== 'number' || isNaN(longitude)) {
1185
+ conversationErrorFromApi(apiError, operation, context) {
1578
1186
  return {
1579
- valid: false,
1580
- error: 'Longitude is required',
1581
- errorCode: 'MISSING_LONGITUDE',
1187
+ code: apiError.code,
1188
+ message: apiError.message,
1189
+ severity: this.determineSeverity(apiError.statusCode),
1190
+ operation,
1191
+ originalError: apiError,
1192
+ statusCode: apiError.statusCode,
1193
+ context,
1194
+ timestamp: apiError.timestamp ?? new Date(),
1195
+ handled: false,
1196
+ recoverySuggestions: this.getRecoverySuggestions(apiError),
1582
1197
  };
1583
1198
  }
1584
- if (longitude < -180 || longitude > 180) {
1585
- return {
1586
- valid: false,
1587
- error: 'Longitude must be between -180 and 180',
1588
- errorCode: 'INVALID_LONGITUDE',
1199
+ isApiError(error) {
1200
+ return (typeof error === 'object' &&
1201
+ error !== null &&
1202
+ 'code' in error &&
1203
+ 'message' in error &&
1204
+ typeof error.code === 'string' &&
1205
+ typeof error.message === 'string');
1206
+ }
1207
+ /**
1208
+ * Determine severity based on status code
1209
+ */
1210
+ determineSeverity(statusCode) {
1211
+ if (!statusCode)
1212
+ return 'error';
1213
+ if (statusCode >= 500)
1214
+ return 'critical';
1215
+ if (statusCode >= 400)
1216
+ return 'error';
1217
+ if (statusCode >= 300)
1218
+ return 'warning';
1219
+ return 'info';
1220
+ }
1221
+ /**
1222
+ * Get recovery suggestions based on error
1223
+ */
1224
+ getRecoverySuggestions(error) {
1225
+ const suggestions = [];
1226
+ if (error.statusCode === 401 || error.code === 'UNAUTHORIZED') {
1227
+ suggestions.push('Please log in again');
1228
+ suggestions.push('Check if your session has expired');
1229
+ }
1230
+ else if (error.statusCode === 403 || error.code === 'FORBIDDEN') {
1231
+ suggestions.push('You do not have permission for this action');
1232
+ suggestions.push('Ask your administrator for access');
1233
+ }
1234
+ else if (error.statusCode === 404 || error.code === 'NOT_FOUND') {
1235
+ suggestions.push('The requested resource was not found');
1236
+ suggestions.push('It may have been deleted or moved');
1237
+ }
1238
+ else if (error.statusCode === 429 || error.code === 'RATE_LIMIT_EXCEEDED') {
1239
+ suggestions.push('Too many requests. Please wait and try again');
1240
+ }
1241
+ else if (error.statusCode && error.statusCode >= 500) {
1242
+ suggestions.push('Server error occurred');
1243
+ suggestions.push('Please try again later');
1244
+ suggestions.push('If the problem persists, reach out to support');
1245
+ }
1246
+ else if (error.code === 'NETWORK_ERROR') {
1247
+ suggestions.push('Check your internet connection');
1248
+ suggestions.push('Try refreshing the page');
1249
+ }
1250
+ return suggestions;
1251
+ }
1252
+ /**
1253
+ * Get default recovery suggestions
1254
+ */
1255
+ getDefaultRecoverySuggestions(code) {
1256
+ const suggestions = [];
1257
+ if (code.includes('NETWORK') || code.includes('CONNECTION')) {
1258
+ suggestions.push('Check your internet connection');
1259
+ suggestions.push('Try refreshing the page');
1260
+ }
1261
+ else if (code.includes('TIMEOUT')) {
1262
+ suggestions.push('The operation took too long');
1263
+ suggestions.push('Please try again');
1264
+ }
1265
+ else {
1266
+ suggestions.push('Please try again');
1267
+ suggestions.push('If the problem persists, reach out to support');
1268
+ }
1269
+ return suggestions;
1270
+ }
1271
+ /**
1272
+ * Log error to console
1273
+ */
1274
+ logError(error) {
1275
+ const isError = error.severity === 'critical' || error.severity === 'error';
1276
+ const header = `[Conversation ${error.severity.toUpperCase()}] ${error.operation}:`;
1277
+ if (isError) {
1278
+ console.error(header, error.message, error.context || '');
1279
+ }
1280
+ else {
1281
+ console.warn(header, error.message, error.context || '');
1282
+ }
1283
+ if (error.originalError && error.severity !== 'info') {
1284
+ console.error('Original error:', error.originalError);
1285
+ }
1286
+ if (error.recoverySuggestions && error.recoverySuggestions.length > 0) {
1287
+ console.info('Recovery suggestions:', error.recoverySuggestions);
1288
+ }
1289
+ }
1290
+ /**
1291
+ * Get user-friendly error message
1292
+ */
1293
+ getUserFriendlyMessage(error) {
1294
+ // Map technical errors to user-friendly messages
1295
+ const messageMap = {
1296
+ NETWORK_ERROR: 'Unable to connect. Please check your internet connection.',
1297
+ UNAUTHORIZED: 'You are not authorized. Please log in again.',
1298
+ FORBIDDEN: 'You do not have permission to perform this action.',
1299
+ NOT_FOUND: 'The requested item could not be found.',
1300
+ RATE_LIMIT_EXCEEDED: 'Too many requests. Please slow down.',
1301
+ VALIDATION_ERROR: 'The provided data is invalid.',
1302
+ SERVER_ERROR: 'A server error occurred. Please try again later.',
1303
+ TIMEOUT: 'The operation timed out. Please try again.',
1589
1304
  };
1305
+ return messageMap[error.code] || error.message || 'An unexpected error occurred.';
1590
1306
  }
1591
- return { valid: true };
1307
+ /**
1308
+ * Check if error is retryable
1309
+ */
1310
+ isRetryable(error) {
1311
+ const retryableCodes = ['NETWORK_ERROR', 'TIMEOUT', 'RATE_LIMIT_EXCEEDED', 'SERVER_ERROR'];
1312
+ const retryableStatusCodes = [408, 429, 500, 502, 503, 504];
1313
+ return (retryableCodes.includes(error.code) ||
1314
+ (error.statusCode !== undefined && retryableStatusCodes.includes(error.statusCode)));
1315
+ }
1316
+ /**
1317
+ * Execute an operation with automatic retry logic
1318
+ * @param operation - The async operation to execute
1319
+ * @param operationName - Name of the operation for error tracking
1320
+ * @param context - Additional context for error handling
1321
+ * @returns Promise resolving to the operation result
1322
+ * @throws {AXConversationError} If all retries fail
1323
+ */
1324
+ async executeWithRetry(operation, operationName, context) {
1325
+ if (!this._config.autoRetry) {
1326
+ // If auto-retry is disabled, just execute once
1327
+ return operation();
1328
+ }
1329
+ const maxRetries = this._config.maxRetries ?? 3;
1330
+ let lastError;
1331
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1332
+ try {
1333
+ return await operation();
1334
+ }
1335
+ catch (error) {
1336
+ lastError = error;
1337
+ const conversationError = this.handle(error, operationName, { ...context, attempt });
1338
+ // Don't retry if error is not retryable or if this was the last attempt
1339
+ if (!this.isRetryable(conversationError) || attempt >= maxRetries) {
1340
+ throw conversationError;
1341
+ }
1342
+ // Exponential backoff: 1s, 2s, 4s, 8s...
1343
+ const delayMs = Math.pow(2, attempt) * 1000;
1344
+ await this.delay(delayMs);
1345
+ }
1346
+ }
1347
+ throw this.handle(lastError, operationName, context);
1348
+ }
1349
+ /**
1350
+ * Delay helper for retry backoff
1351
+ * @param ms - Milliseconds to delay
1352
+ */
1353
+ delay(ms) {
1354
+ return new Promise((resolve) => setTimeout(resolve, ms));
1355
+ }
1356
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXErrorHandlerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1357
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXErrorHandlerService, providedIn: 'root' }); }
1592
1358
  }
1359
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXErrorHandlerService, decorators: [{
1360
+ type: Injectable,
1361
+ args: [{
1362
+ providedIn: 'root',
1363
+ }]
1364
+ }], ctorParameters: () => [] });
1593
1365
 
1594
1366
  /**
1595
1367
  * Composer Action Registry
@@ -4968,24 +4740,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4968
4740
  */
4969
4741
 
4970
4742
  /**
4971
- * File Upload Service
4972
- * Centralized service for handling file uploads and processing
4743
+ * File helpers for composer pickers: previews, validation, and metadata.
4973
4744
  */
4974
4745
  class AXFileUploadService {
4975
4746
  constructor() {
4976
4747
  this.config = inject(CONVERSATION_CONFIG);
4977
4748
  this.platformId = inject(PLATFORM_ID);
4978
- this.uploadProgress$ = new Subject();
4979
- }
4980
- /**
4981
- * Get upload progress observable
4982
- */
4983
- getUploadProgress() {
4984
- return this.uploadProgress$.asObservable();
4985
4749
  }
4986
- /**
4987
- * Read file as data URL for preview
4988
- */
4989
4750
  async readFileAsDataURL(file) {
4990
4751
  return new Promise((resolve, reject) => {
4991
4752
  const reader = new FileReader();
@@ -4994,9 +4755,6 @@ class AXFileUploadService {
4994
4755
  reader.readAsDataURL(file);
4995
4756
  });
4996
4757
  }
4997
- /**
4998
- * Get file type category
4999
- */
5000
4758
  getFileType(file) {
5001
4759
  if (file.type.startsWith('image/'))
5002
4760
  return 'image';
@@ -5006,9 +4764,6 @@ class AXFileUploadService {
5006
4764
  return 'audio';
5007
4765
  return 'file';
5008
4766
  }
5009
- /**
5010
- * Generate file preview
5011
- */
5012
4767
  async generatePreview(file) {
5013
4768
  const type = this.getFileType(file);
5014
4769
  let preview;
@@ -5022,15 +4777,9 @@ class AXFileUploadService {
5022
4777
  }
5023
4778
  return { file, preview, type };
5024
4779
  }
5025
- /**
5026
- * Generate previews for multiple files
5027
- */
5028
4780
  async generatePreviews(files) {
5029
4781
  return Promise.all(files.map((file) => this.generatePreview(file)));
5030
4782
  }
5031
- /**
5032
- * Get video duration
5033
- */
5034
4783
  async getVideoDuration(file) {
5035
4784
  if (!isPlatformBrowser(this.platformId)) {
5036
4785
  return 0;
@@ -5049,9 +4798,6 @@ class AXFileUploadService {
5049
4798
  video.src = URL.createObjectURL(file);
5050
4799
  });
5051
4800
  }
5052
- /**
5053
- * Get audio duration
5054
- */
5055
4801
  async getAudioDuration(file) {
5056
4802
  if (!isPlatformBrowser(this.platformId)) {
5057
4803
  return 0;
@@ -5069,9 +4815,6 @@ class AXFileUploadService {
5069
4815
  audio.src = URL.createObjectURL(file);
5070
4816
  });
5071
4817
  }
5072
- /**
5073
- * Format file size
5074
- */
5075
4818
  formatFileSize(bytes) {
5076
4819
  if (bytes < 1024)
5077
4820
  return `${bytes} B`;
@@ -5081,17 +4824,11 @@ class AXFileUploadService {
5081
4824
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
5082
4825
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
5083
4826
  }
5084
- /**
5085
- * Format duration (seconds to mm:ss)
5086
- */
5087
4827
  formatDuration(seconds) {
5088
4828
  const mins = Math.floor(seconds / 60);
5089
4829
  const secs = Math.floor(seconds % 60);
5090
4830
  return `${mins}:${secs.toString().padStart(2, '0')}`;
5091
4831
  }
5092
- /**
5093
- * Validate file type
5094
- */
5095
4832
  validateFileType(file, allowedTypes) {
5096
4833
  return allowedTypes.some((type) => {
5097
4834
  if (type.endsWith('/*')) {
@@ -5101,34 +4838,19 @@ class AXFileUploadService {
5101
4838
  return file.type === type;
5102
4839
  });
5103
4840
  }
5104
- /**
5105
- * Validate file size
5106
- */
5107
4841
  validateFileSize(file, maxSizeInMB) {
5108
4842
  const maxSizeInBytes = maxSizeInMB * 1024 * 1024;
5109
4843
  return file.size <= maxSizeInBytes;
5110
4844
  }
5111
- /**
5112
- * Filter files by type
5113
- */
5114
4845
  filterFilesByType(files, allowedTypes) {
5115
4846
  return files.filter((file) => this.validateFileType(file, allowedTypes));
5116
4847
  }
5117
- /**
5118
- * Validate file type against conversation config
5119
- */
5120
4848
  isFileTypeAllowed(file) {
5121
4849
  return this.validateFileType(file, this.config.allowedFileTypes);
5122
4850
  }
5123
- /**
5124
- * Validate file size against conversation config
5125
- */
5126
4851
  isFileSizeAllowed(file) {
5127
4852
  return file.size <= this.config.maxFileSize;
5128
4853
  }
5129
- /**
5130
- * Validate a set of files using conversation config
5131
- */
5132
4854
  validateFilesWithConfig(files) {
5133
4855
  const accepted = [];
5134
4856
  const rejected = [];
@@ -11893,10 +11615,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
11893
11615
  type: Injectable
11894
11616
  }], ctorParameters: () => [] });
11895
11617
 
11896
- /**
11897
- * Registry Service
11898
- * Central service for managing all registries and extensions
11899
- */
11618
+ /** Unified access to conversation2 registries (renderers, actions, tabs). */
11900
11619
  /**
11901
11620
  * Central Registry Service
11902
11621
  * Provides unified access to all registries
@@ -11965,12 +11684,12 @@ class AXConversationService {
11965
11684
  }
11966
11685
  constructor() {
11967
11686
  this.config = inject(CONVERSATION_CONFIG);
11687
+ this.state = new ConversationState(this.config);
11968
11688
  // New separated APIs
11969
11689
  this.userApi = inject(AXUserApi);
11970
11690
  this.conversationApi = inject(AXConversationApi);
11971
11691
  this.messageApi = inject(AXMessageApi);
11972
11692
  this.realtimeApi = inject(AXRealtimeApi, { optional: true });
11973
- this.store = inject(AXConversationStoreService);
11974
11693
  this.errorHandler = inject(AXErrorHandlerService);
11975
11694
  this.dialogService = inject(AXDialogService);
11976
11695
  this.popupService = inject(AXPopupService);
@@ -11992,18 +11711,18 @@ class AXConversationService {
11992
11711
  this._typingIndicator$ = new Subject();
11993
11712
  this._presenceUpdate$ = new Subject();
11994
11713
  /** All conversations */
11995
- this.conversations = this.store.conversations;
11714
+ this.conversations = this.state.conversations;
11996
11715
  /** Active conversation ID */
11997
11716
  this.activeConversationId = this._activeConversationId.asReadonly();
11998
11717
  /** Active conversation */
11999
11718
  this.activeConversation = computed(() => {
12000
11719
  const id = this._activeConversationId();
12001
- return id ? this.store.getConversation(id) : null;
11720
+ return id ? this.state.getConversation(id) : null;
12002
11721
  }, ...(ngDevMode ? [{ debugName: "activeConversation" }] : []));
12003
11722
  /** Messages for active conversation */
12004
11723
  this.activeMessages = computed(() => {
12005
11724
  const convId = this._activeConversationId();
12006
- return convId ? this.store.getConversationMessages(convId) : [];
11725
+ return convId ? this.state.getConversationMessages(convId) : [];
12007
11726
  }, ...(ngDevMode ? [{ debugName: "activeMessages" }] : []));
12008
11727
  /** Loading state */
12009
11728
  this.loading = this._loading.asReadonly();
@@ -12039,8 +11758,6 @@ class AXConversationService {
12039
11758
  */
12040
11759
  async initializeService() {
12041
11760
  try {
12042
- // Initialize store
12043
- await this.store.initialize();
12044
11761
  // Connect to real-time API (optional)
12045
11762
  if (this.realtimeApi) {
12046
11763
  await this.realtimeApi.connect();
@@ -12137,7 +11854,7 @@ class AXConversationService {
12137
11854
  pageSize: this.config.conversationPageSize,
12138
11855
  }, undefined);
12139
11856
  // Store conversations
12140
- this.store.setConversations(result.items);
11857
+ this.state.setConversations(result.items);
12141
11858
  }
12142
11859
  catch (error) {
12143
11860
  const handledError = this.errorHandler.handle(error, 'loadConversations');
@@ -12159,7 +11876,7 @@ class AXConversationService {
12159
11876
  }, undefined);
12160
11877
  // Append new conversations to existing ones
12161
11878
  if (result.items.length > 0) {
12162
- this.store.addConversations(result.items);
11879
+ this.state.addConversations(result.items);
12163
11880
  }
12164
11881
  return result.hasMore;
12165
11882
  }
@@ -12214,7 +11931,7 @@ class AXConversationService {
12214
11931
  page,
12215
11932
  pageSize: this.config.messagePageSize,
12216
11933
  });
12217
- this.store.addMessages(result.items);
11934
+ this.state.addMessages(result.items);
12218
11935
  return result.items;
12219
11936
  }
12220
11937
  catch (error) {
@@ -12252,12 +11969,12 @@ class AXConversationService {
12252
11969
  metadata: command.metadata,
12253
11970
  };
12254
11971
  // Add to store immediately (optimistic update)
12255
- this.store.addMessage(tempMessage);
11972
+ this.state.addMessage(tempMessage);
12256
11973
  // Send to server
12257
11974
  const sentMessage = await this.messageApi.sendMessage(command);
12258
11975
  // Replace temporary message with real one
12259
- this.store.deleteMessage(tempMessageId);
12260
- this.store.addMessage(sentMessage);
11976
+ this.state.deleteMessage(tempMessageId);
11977
+ this.state.addMessage(sentMessage);
12261
11978
  // Update conversation's last message
12262
11979
  this.updateConversationLastMessage(sentMessage);
12263
11980
  // Emit forward count update if this is a forwarded message
@@ -12270,7 +11987,7 @@ class AXConversationService {
12270
11987
  }
12271
11988
  catch (error) {
12272
11989
  // Mark temp message as failed
12273
- this.store.updateMessage(tempMessageId, { status: 'failed' });
11990
+ this.state.updateMessage(tempMessageId, { status: 'failed' });
12274
11991
  this.errorHandler.handle(error, 'sendMessage', {
12275
11992
  conversationId: command.conversationId,
12276
11993
  type: command.type,
@@ -12282,12 +11999,12 @@ class AXConversationService {
12282
11999
  * Retry a failed message
12283
12000
  */
12284
12001
  async retryFailedMessage(messageId) {
12285
- const message = this.store.getMessage(messageId);
12002
+ const message = this.state.getMessage(messageId);
12286
12003
  if (!message || message.status !== 'failed') {
12287
12004
  return;
12288
12005
  }
12289
12006
  // Update status to sending
12290
- this.store.updateMessage(messageId, { status: 'sending' });
12007
+ this.state.updateMessage(messageId, { status: 'sending' });
12291
12008
  try {
12292
12009
  // Recreate command from message
12293
12010
  const command = {
@@ -12302,14 +12019,14 @@ class AXConversationService {
12302
12019
  // Send to server
12303
12020
  const sentMessage = await this.messageApi.sendMessage(command);
12304
12021
  // Replace with real message
12305
- this.store.deleteMessage(messageId);
12306
- this.store.addMessage(sentMessage);
12022
+ this.state.deleteMessage(messageId);
12023
+ this.state.addMessage(sentMessage);
12307
12024
  // Update conversation's last message
12308
12025
  this.updateConversationLastMessage(sentMessage);
12309
12026
  }
12310
12027
  catch (error) {
12311
12028
  // Mark as failed again
12312
- this.store.updateMessage(messageId, { status: 'failed' });
12029
+ this.state.updateMessage(messageId, { status: 'failed' });
12313
12030
  this.errorHandler.handle(error, 'retryFailedMessage', { messageId });
12314
12031
  throw error;
12315
12032
  }
@@ -12320,10 +12037,10 @@ class AXConversationService {
12320
12037
  */
12321
12038
  async editMessage(messageId, payload) {
12322
12039
  // Store original message for rollback
12323
- const originalMessage = this.store.getMessage(messageId);
12040
+ const originalMessage = this.state.getMessage(messageId);
12324
12041
  try {
12325
12042
  // Optimistic update
12326
- this.store.updateMessage(messageId, {
12043
+ this.state.updateMessage(messageId, {
12327
12044
  payload,
12328
12045
  editedAt: new Date(),
12329
12046
  });
@@ -12333,7 +12050,7 @@ class AXConversationService {
12333
12050
  catch (error) {
12334
12051
  // Rollback on failure
12335
12052
  if (originalMessage) {
12336
- this.store.updateMessage(messageId, {
12053
+ this.state.updateMessage(messageId, {
12337
12054
  payload: originalMessage.payload,
12338
12055
  editedAt: originalMessage.editedAt,
12339
12056
  });
@@ -12356,17 +12073,17 @@ class AXConversationService {
12356
12073
  return;
12357
12074
  }
12358
12075
  // Store message for rollback
12359
- const message = this.store.getMessage(messageId);
12076
+ const message = this.state.getMessage(messageId);
12360
12077
  try {
12361
12078
  // Optimistic delete
12362
- this.store.deleteMessage(messageId);
12079
+ this.state.deleteMessage(messageId);
12363
12080
  // Sync with server
12364
12081
  await this.messageApi.deleteMessage(messageId, forEveryone);
12365
12082
  }
12366
12083
  catch (error) {
12367
12084
  // Rollback on failure
12368
12085
  if (message) {
12369
- this.store.addMessage(message);
12086
+ this.state.addMessage(message);
12370
12087
  }
12371
12088
  this.errorHandler.handle(error, 'deleteMessage', { messageId, forEveryone });
12372
12089
  throw error;
@@ -12405,25 +12122,25 @@ class AXConversationService {
12405
12122
  * Updates message status locally and syncs with server
12406
12123
  */
12407
12124
  async markMessageAsRead(messageId) {
12408
- const message = this.store.getMessage(messageId);
12125
+ const message = this.state.getMessage(messageId);
12409
12126
  if (!message || message.status === 'read')
12410
12127
  return;
12411
12128
  try {
12412
12129
  // Optimistic update
12413
- this.store.updateMessage(messageId, { status: 'read' });
12130
+ this.state.updateMessage(messageId, { status: 'read' });
12414
12131
  // Sync with server
12415
12132
  await this.messageApi.markAsRead(message.conversationId, [messageId]);
12416
12133
  // Update conversation unread count
12417
- const conversation = this.store.getConversation(message.conversationId);
12134
+ const conversation = this.state.getConversation(message.conversationId);
12418
12135
  if (conversation && conversation.unreadCount > 0) {
12419
- this.store.updateConversation(message.conversationId, {
12136
+ this.state.updateConversation(message.conversationId, {
12420
12137
  unreadCount: Math.max(0, conversation.unreadCount - 1),
12421
12138
  });
12422
12139
  }
12423
12140
  }
12424
12141
  catch (error) {
12425
12142
  // Rollback on failure
12426
- this.store.updateMessage(messageId, { status: message.status });
12143
+ this.state.updateMessage(messageId, { status: message.status });
12427
12144
  this.errorHandler.handle(error, 'markMessageAsRead', { messageId });
12428
12145
  // Don't throw - marking as read failure shouldn't break the app
12429
12146
  }
@@ -12433,20 +12150,20 @@ class AXConversationService {
12433
12150
  * Marks messages locally and syncs with server
12434
12151
  */
12435
12152
  async markAsRead(conversationId) {
12436
- const messages = this.store.getConversationMessages(conversationId);
12153
+ const messages = this.state.getConversationMessages(conversationId);
12437
12154
  const currentId = this._currentUser()?.id ?? 'current-user';
12438
12155
  const unreadMessageIds = messages.filter((m) => m.status !== 'read' && m.senderId !== currentId).map((m) => m.id);
12439
12156
  if (unreadMessageIds.length === 0)
12440
12157
  return;
12441
12158
  try {
12442
12159
  // Optimistic update
12443
- this.store.resetUnreadCount(conversationId);
12160
+ this.state.resetUnreadCount(conversationId);
12444
12161
  // Sync with server
12445
12162
  await this.messageApi.markAsRead(conversationId, unreadMessageIds);
12446
12163
  }
12447
12164
  catch (error) {
12448
12165
  // Rollback on failure
12449
- this.store.updateConversation(conversationId, {
12166
+ this.state.updateConversation(conversationId, {
12450
12167
  unreadCount: unreadMessageIds.length,
12451
12168
  });
12452
12169
  this.errorHandler.handle(error, 'markAsRead', { conversationId });
@@ -12483,21 +12200,21 @@ class AXConversationService {
12483
12200
  * Handle new message received
12484
12201
  */
12485
12202
  handleNewMessage(message) {
12486
- this.store.addMessage(message);
12487
- this.store.updateLastMessage(message);
12203
+ this.state.addMessage(message);
12204
+ this.state.updateLastMessage(message);
12488
12205
  // Increment unread count for messages from other users
12489
12206
  // The intersection observer will mark them as read when they become visible
12490
12207
  const currentId = this._currentUser()?.id ?? 'current-user';
12491
12208
  const isFromOtherUser = message.senderId !== currentId;
12492
12209
  if (isFromOtherUser) {
12493
- this.store.incrementUnreadCount(message.conversationId);
12210
+ this.state.incrementUnreadCount(message.conversationId);
12494
12211
  }
12495
12212
  }
12496
12213
  /**
12497
12214
  * Handle message update
12498
12215
  */
12499
12216
  handleMessageUpdate(message) {
12500
- this.store.updateMessage(message.id, message);
12217
+ this.state.updateMessage(message.id, message);
12501
12218
  }
12502
12219
  /**
12503
12220
  * Handle message count update
@@ -12517,7 +12234,7 @@ class AXConversationService {
12517
12234
  * Handle message deletion
12518
12235
  */
12519
12236
  handleMessageDeletion(messageId) {
12520
- this.store.deleteMessage(messageId);
12237
+ this.state.deleteMessage(messageId);
12521
12238
  }
12522
12239
  /**
12523
12240
  * Handle typing indicator
@@ -12528,30 +12245,30 @@ class AXConversationService {
12528
12245
  if (currentUser && indicator.userId === currentUser.id) {
12529
12246
  return;
12530
12247
  }
12531
- this.store.updateTypingIndicator(indicator.conversationId, indicator.userId, true);
12248
+ this.state.updateTypingIndicator(indicator.conversationId, indicator.userId, true);
12532
12249
  // Clear typing indicator after timeout
12533
12250
  setTimeout(() => {
12534
- this.store.updateTypingIndicator(indicator.conversationId, indicator.userId, false);
12251
+ this.state.updateTypingIndicator(indicator.conversationId, indicator.userId, false);
12535
12252
  }, this.config.typingIndicatorTimeout ?? 3000);
12536
12253
  }
12537
12254
  /**
12538
12255
  * Handle presence update
12539
12256
  */
12540
12257
  handlePresenceUpdate(update) {
12541
- this.store.updateParticipantPresence(update.userId, update.status, update.lastSeen);
12258
+ this.state.updateParticipantPresence(update.userId, update.status, update.lastSeen);
12542
12259
  }
12543
12260
  /**
12544
12261
  * Handle conversation update
12545
12262
  * Updates conversation metadata including unread count, last message, etc.
12546
12263
  */
12547
12264
  handleConversationUpdate(conversation) {
12548
- this.store.setConversation(conversation);
12265
+ this.state.setConversation(conversation);
12549
12266
  }
12550
12267
  /**
12551
12268
  * Update conversation's last message
12552
12269
  */
12553
12270
  updateConversationLastMessage(message) {
12554
- this.store.updateLastMessage(message);
12271
+ this.state.updateLastMessage(message);
12555
12272
  }
12556
12273
  // =====================
12557
12274
  // Conversation Update APIs
@@ -12563,7 +12280,7 @@ class AXConversationService {
12563
12280
  */
12564
12281
  async updateConversation(conversationId, updates) {
12565
12282
  try {
12566
- this.store.updateConversation(conversationId, updates);
12283
+ this.state.updateConversation(conversationId, updates);
12567
12284
  }
12568
12285
  catch (error) {
12569
12286
  this.errorHandler.handle(error, 'updateConversation', { conversationId });
@@ -12577,7 +12294,7 @@ class AXConversationService {
12577
12294
  */
12578
12295
  async updateConversationSettings(conversationId, settings) {
12579
12296
  try {
12580
- this.store.updateSettings(conversationId, settings);
12297
+ this.state.updateSettings(conversationId, settings);
12581
12298
  }
12582
12299
  catch (error) {
12583
12300
  this.errorHandler.handle(error, 'updateConversationSettings', { conversationId });
@@ -12591,7 +12308,7 @@ class AXConversationService {
12591
12308
  */
12592
12309
  async updateConversationTitle(conversationId, title) {
12593
12310
  try {
12594
- this.store.updateTitle(conversationId, title);
12311
+ this.state.updateTitle(conversationId, title);
12595
12312
  }
12596
12313
  catch (error) {
12597
12314
  this.errorHandler.handle(error, 'updateConversationTitle', { conversationId, title });
@@ -12605,7 +12322,7 @@ class AXConversationService {
12605
12322
  */
12606
12323
  async updateConversationMetadata(conversationId, metadata) {
12607
12324
  try {
12608
- this.store.updateMetadata(conversationId, metadata);
12325
+ this.state.updateMetadata(conversationId, metadata);
12609
12326
  }
12610
12327
  catch (error) {
12611
12328
  this.errorHandler.handle(error, 'updateConversationMetadata', { conversationId });
@@ -12618,7 +12335,7 @@ class AXConversationService {
12618
12335
  * @returns Conversation or null
12619
12336
  */
12620
12337
  getConversation(conversationId) {
12621
- return this.store.getConversation(conversationId) || null;
12338
+ return this.state.getConversation(conversationId) || null;
12622
12339
  }
12623
12340
  /**
12624
12341
  * Create a new conversation
@@ -12638,7 +12355,7 @@ class AXConversationService {
12638
12355
  icon: AXConversationService.normalizeOptionalString(metadata?.['icon']),
12639
12356
  metadata,
12640
12357
  });
12641
- this.store.setConversation(conversation);
12358
+ this.state.setConversation(conversation);
12642
12359
  return conversation;
12643
12360
  }
12644
12361
  catch (error) {
@@ -12667,7 +12384,7 @@ class AXConversationService {
12667
12384
  async markConversationAsRead(conversationId) {
12668
12385
  try {
12669
12386
  await this.conversationApi.markConversationAsRead(conversationId);
12670
- this.store.resetUnreadCount(conversationId);
12387
+ this.state.resetUnreadCount(conversationId);
12671
12388
  }
12672
12389
  catch (error) {
12673
12390
  this.errorHandler.handle(error, 'markConversationAsRead', { conversationId });
@@ -12681,7 +12398,7 @@ class AXConversationService {
12681
12398
  */
12682
12399
  async deleteConversation(conversationId) {
12683
12400
  // Get conversation title for confirmation message
12684
- const conversation = this.store.getConversation(conversationId);
12401
+ const conversation = this.state.getConversation(conversationId);
12685
12402
  const conversationTitle = conversation?.title || this.translation.translateSync('@acorex:chat.fallbacks.this-conversation');
12686
12403
  // Show confirmation dialog
12687
12404
  const result = await this.dialogService.confirm(this.translation.translateSync('@acorex:chat.dialog.delete-conversation.title'), this.translation.translateSync('@acorex:chat.dialog.delete-conversation.message', { params: { conversationTitle } }), 'danger', 'vertical', false);
@@ -12695,7 +12412,7 @@ class AXConversationService {
12695
12412
  if (!result)
12696
12413
  return false;
12697
12414
  // Delete from store
12698
- this.store.deleteConversation(conversationId);
12415
+ this.state.deleteConversation(conversationId);
12699
12416
  // If this was the active conversation, clear selection
12700
12417
  if (this._activeConversationId() === conversationId) {
12701
12418
  this._activeConversationId.set(null);
@@ -12760,7 +12477,7 @@ class AXConversationService {
12760
12477
  async archiveConversation(conversationId) {
12761
12478
  try {
12762
12479
  await this.conversationApi.archiveConversation(conversationId);
12763
- this.store.updateConversation(conversationId, { archived: true });
12480
+ this.state.updateConversation(conversationId, { archived: true });
12764
12481
  }
12765
12482
  catch (error) {
12766
12483
  this.errorHandler.handle(error, 'archiveConversation', { conversationId });
@@ -12774,7 +12491,7 @@ class AXConversationService {
12774
12491
  async unarchiveConversation(conversationId) {
12775
12492
  try {
12776
12493
  await this.conversationApi.unarchiveConversation(conversationId);
12777
- this.store.updateConversation(conversationId, { archived: false });
12494
+ this.state.updateConversation(conversationId, { archived: false });
12778
12495
  }
12779
12496
  catch (error) {
12780
12497
  this.errorHandler.handle(error, 'unarchiveConversation', { conversationId });
@@ -12788,7 +12505,7 @@ class AXConversationService {
12788
12505
  async pinConversation(conversationId) {
12789
12506
  try {
12790
12507
  await this.conversationApi.pinConversation(conversationId);
12791
- this.store.updateConversation(conversationId, { pinned: true });
12508
+ this.state.updateConversation(conversationId, { pinned: true });
12792
12509
  }
12793
12510
  catch (error) {
12794
12511
  this.errorHandler.handle(error, 'pinConversation', { conversationId });
@@ -12802,7 +12519,7 @@ class AXConversationService {
12802
12519
  async unpinConversation(conversationId) {
12803
12520
  try {
12804
12521
  await this.conversationApi.unpinConversation(conversationId);
12805
- this.store.updateConversation(conversationId, { pinned: false });
12522
+ this.state.updateConversation(conversationId, { pinned: false });
12806
12523
  }
12807
12524
  catch (error) {
12808
12525
  this.errorHandler.handle(error, 'unpinConversation', { conversationId });
@@ -12818,7 +12535,7 @@ class AXConversationService {
12818
12535
  try {
12819
12536
  await this.conversationApi.muteConversation(conversationId, duration);
12820
12537
  const mutedUntil = duration ? new Date(Date.now() + duration) : undefined;
12821
- this.store.updateSettings(conversationId, { mutedUntil, notifications: false });
12538
+ this.state.updateSettings(conversationId, { mutedUntil, notifications: false });
12822
12539
  }
12823
12540
  catch (error) {
12824
12541
  this.errorHandler.handle(error, 'muteConversation', { conversationId, duration });
@@ -12832,7 +12549,7 @@ class AXConversationService {
12832
12549
  async unmuteConversation(conversationId) {
12833
12550
  try {
12834
12551
  await this.conversationApi.unmuteConversation(conversationId);
12835
- this.store.updateSettings(conversationId, { mutedUntil: undefined, notifications: true });
12552
+ this.state.updateSettings(conversationId, { mutedUntil: undefined, notifications: true });
12836
12553
  }
12837
12554
  catch (error) {
12838
12555
  this.errorHandler.handle(error, 'unmuteConversation', { conversationId });
@@ -12847,7 +12564,7 @@ class AXConversationService {
12847
12564
  async addParticipants(conversationId, userIds) {
12848
12565
  try {
12849
12566
  const updatedConversation = await this.conversationApi.addParticipants(conversationId, userIds);
12850
- this.store.setConversation(updatedConversation);
12567
+ this.state.setConversation(updatedConversation);
12851
12568
  }
12852
12569
  catch (error) {
12853
12570
  this.errorHandler.handle(error, 'addParticipants', { conversationId, userIds });
@@ -12862,7 +12579,7 @@ class AXConversationService {
12862
12579
  async removeParticipant(conversationId, userId) {
12863
12580
  try {
12864
12581
  const updatedConversation = await this.conversationApi.removeParticipant(conversationId, userId);
12865
- this.store.setConversation(updatedConversation);
12582
+ this.state.setConversation(updatedConversation);
12866
12583
  }
12867
12584
  catch (error) {
12868
12585
  this.errorHandler.handle(error, 'removeParticipant', { conversationId, userId });
@@ -12876,7 +12593,7 @@ class AXConversationService {
12876
12593
  async leaveConversation(conversationId) {
12877
12594
  try {
12878
12595
  await this.conversationApi.leaveConversation(conversationId);
12879
- this.store.deleteConversation(conversationId);
12596
+ this.state.deleteConversation(conversationId);
12880
12597
  if (this._activeConversationId() === conversationId) {
12881
12598
  this._activeConversationId.set(null);
12882
12599
  }
@@ -12894,7 +12611,7 @@ class AXConversationService {
12894
12611
  async saveDraft(conversationId, draft) {
12895
12612
  try {
12896
12613
  await this.conversationApi.saveDraft(conversationId, draft);
12897
- this.store.updateConversation(conversationId, { draft });
12614
+ this.state.updateConversation(conversationId, { draft });
12898
12615
  }
12899
12616
  catch (error) {
12900
12617
  this.errorHandler.handle(error, 'saveDraft', { conversationId });
@@ -12908,7 +12625,7 @@ class AXConversationService {
12908
12625
  async clearDraft(conversationId) {
12909
12626
  try {
12910
12627
  await this.conversationApi.clearDraft(conversationId);
12911
- this.store.updateConversation(conversationId, { draft: undefined });
12628
+ this.state.updateConversation(conversationId, { draft: undefined });
12912
12629
  }
12913
12630
  catch (error) {
12914
12631
  this.errorHandler.handle(error, 'clearDraft', { conversationId });
@@ -12926,7 +12643,7 @@ class AXConversationService {
12926
12643
  async pinMessage(conversationId, messageId) {
12927
12644
  try {
12928
12645
  await this.messageApi.pinMessage(conversationId, messageId);
12929
- this.store.updateMessage(messageId, { pinned: true });
12646
+ this.state.updateMessage(messageId, { pinned: true });
12930
12647
  }
12931
12648
  catch (error) {
12932
12649
  this.errorHandler.handle(error, 'pinMessage', { conversationId, messageId });
@@ -12941,7 +12658,7 @@ class AXConversationService {
12941
12658
  async unpinMessage(conversationId, messageId) {
12942
12659
  try {
12943
12660
  await this.messageApi.unpinMessage(conversationId, messageId);
12944
- this.store.updateMessage(messageId, { pinned: false });
12661
+ this.state.updateMessage(messageId, { pinned: false });
12945
12662
  }
12946
12663
  catch (error) {
12947
12664
  this.errorHandler.handle(error, 'unpinMessage', { conversationId, messageId });
@@ -18608,7 +18325,7 @@ function createProviders(options, includeServices) {
18608
18325
  if (realtimeApi) {
18609
18326
  providers.push({ provide: AXRealtimeApi, useClass: realtimeApi });
18610
18327
  }
18611
- providers.push(AXConversationStoreService, AXConversationService, AXComposerService, AXInfoBarService, AXMessageListService, AXSidebarService);
18328
+ providers.push(AXConversationService, AXComposerService, AXInfoBarService, AXMessageListService, AXSidebarService);
18612
18329
  }
18613
18330
  if (config) {
18614
18331
  providers.push({ provide: CONVERSATION_CONFIG, useValue: mergeWithDefaults(config) });
@@ -19030,5 +18747,5 @@ function getErrorMessage(code, params) {
19030
18747
  * Generated bundle index. Do not edit.
19031
18748
  */
19032
18749
 
19033
- export { AXAudioInfoBarBannerComponent, AXAudioPickerComponent, AXAudioRendererComponent, AXBaseRegistry, AXComposerActionRegistry, AXComposerComponent, AXComposerPopupComponent, AXComposerService, AXComposerTabRegistry, AXConversation2Module, AXConversationAiApiKey, AXConversationAiResponderService, AXConversationApi, AXConversationContainerComponent, AXConversationContainerDirective, AXConversationDateUtilsService, AXConversationIndexedDbConversationApi, AXConversationIndexedDbMessageAiApi, AXConversationIndexedDbMessageApi, AXConversationIndexedDbRealtimeApi, AXConversationIndexedDbStorage, AXConversationIndexedDbStores, AXConversationIndexedDbUserApi, AXConversationInfoPanelComponent, AXConversationItemActionRegistry, AXConversationMessageRendererStateComponent, AXConversationMessageUtilsService, AXConversationService, AXConversationSharedStorage, AXConversationStoreService, AXConversationTabRegistry, AXEmojiTabComponent, AXErrorHandlerService, AXFallbackRendererComponent, AXFilePickerComponent, AXFileRendererComponent, AXFileUploadService, AXForwardMessageDialogComponent, AXImagePickerComponent, AXImageRendererComponent, AXInfiniteScrollDirective, AXInfoBarActionRegistry, AXInfoBarComponent, AXInfoBarSearchComponent, AXInfoBarService, AXLocationPickerComponent, AXLocationRendererComponent, AXMessageActionRegistry, AXMessageApi, AXMessageListComponent, AXMessageListNoActiveDefaultComponent, AXMessageListService, AXMessageRendererRegistry, AXNewConversationDialogComponent, AXPickerFooterComponent, AXPickerHeaderComponent, AXRealtimeApi, AXRegistryService, AXSidebarComponent, AXSidebarService, AXStickerRendererComponent, AXStickerTabComponent, AXSystemRendererComponent, AXTextRendererComponent, AXUserApi, AXVideoInfoBarBannerComponent, AXVideoPickerComponent, AXVideoRendererComponent, AXVoiceInfoBarBannerComponent, AXVoiceRecorderComponent, AXVoiceRendererComponent, AX_CONVERSATION_AUDIO_RENDERER, AX_CONVERSATION_COMPOSER_AUDIO_ACTION, AX_CONVERSATION_COMPOSER_EMOJI_ACTION, AX_CONVERSATION_COMPOSER_EMOJI_TAB, AX_CONVERSATION_COMPOSER_FILE_ACTION, AX_CONVERSATION_COMPOSER_IMAGE_ACTION, AX_CONVERSATION_COMPOSER_LOCATION_ACTION, AX_CONVERSATION_COMPOSER_STICKER_TAB, AX_CONVERSATION_COMPOSER_VIDEO_ACTION, AX_CONVERSATION_COMPOSER_VOICE_RECORDING_ACTION, AX_CONVERSATION_FALLBACK_RENDERER, AX_CONVERSATION_FILE_RENDERER, AX_CONVERSATION_IMAGE_RENDERER, AX_CONVERSATION_INFO_BAR_ARCHIVE_ACTION, AX_CONVERSATION_INFO_BAR_BLOCK_ACTION, AX_CONVERSATION_INFO_BAR_DELETE_ACTION, AX_CONVERSATION_INFO_BAR_DIVIDER, AX_CONVERSATION_INFO_BAR_INFO_ACTION, AX_CONVERSATION_INFO_BAR_MUTE_ACTION, AX_CONVERSATION_INFO_BAR_SEARCH_ACTION, AX_CONVERSATION_ITEM_BLOCK_ACTION, AX_CONVERSATION_ITEM_DELETE_ACTION, AX_CONVERSATION_ITEM_DIVIDER, AX_CONVERSATION_ITEM_MARK_READ_ACTION, AX_CONVERSATION_ITEM_MUTE_ACTION, AX_CONVERSATION_ITEM_PIN_ACTION, AX_CONVERSATION_LOCATION_RENDERER, AX_CONVERSATION_MESSAGE_COPY_ACTION, AX_CONVERSATION_MESSAGE_DELETE_ACTION, AX_CONVERSATION_MESSAGE_EDIT_ACTION, AX_CONVERSATION_MESSAGE_FORWARD_ACTION, AX_CONVERSATION_MESSAGE_REPLY_ACTION, AX_CONVERSATION_STICKER_RENDERER, AX_CONVERSATION_SYSTEM_RENDERER, AX_CONVERSATION_TAB_ALL, AX_CONVERSATION_TAB_ARCHIVED, AX_CONVERSATION_TAB_BOT, AX_CONVERSATION_TAB_CHANNELS, AX_CONVERSATION_TAB_GROUPS, AX_CONVERSATION_TAB_PRIVATE, AX_CONVERSATION_TAB_UNREAD, AX_CONVERSATION_TEXT_RENDERER, AX_CONVERSATION_VIDEO_RENDERER, AX_CONVERSATION_VOICE_RENDERER, AX_DEFAULT_CONVERSATION_CONFIG, AX_STICKER_API_KEY, CONNECTION_ERRORS, CONVERSATION_CONFIG, CONVERSATION_ERRORS, DEFAULT_COMPOSER_ACTIONS, DEFAULT_COMPOSER_TABS, DEFAULT_CONVERSATION_ITEM_ACTIONS, DEFAULT_CONVERSATION_TABS, DEFAULT_INFO_BAR_ACTIONS, DEFAULT_MESSAGE_ACTIONS, DEFAULT_MESSAGE_RENDERERS, ERROR_HANDLER_CONFIG, ERROR_MESSAGES, FILE_ERRORS, LOCATION_ERRORS, MESSAGE_ERRORS, PERMISSION_ERRORS, REGISTRY_CONFIG, URL_ERRORS, USER_ERRORS, axConversationIndexedDbStorage, conversationSharedStorage, formatErrorMessage, getDefaultConversationItemActions, getErrorMessage, mergeWithDefaults, provideConversation, sanitizeInput, validateConversationId, validateEmail, validateFile, validateLatitude, validateLongitude, validateMessagePayload, validateMessageText, validateMessageType, validateUrl, validateUserId, validateUserIds };
18750
+ export { AXAudioInfoBarBannerComponent, AXAudioPickerComponent, AXAudioRendererComponent, AXBaseRegistry, AXComposerActionRegistry, AXComposerComponent, AXComposerPopupComponent, AXComposerService, AXComposerTabRegistry, AXConversation2Module, AXConversationAiApiKey, AXConversationAiResponderService, AXConversationApi, AXConversationContainerComponent, AXConversationContainerDirective, AXConversationDateUtilsService, AXConversationIndexedDbConversationApi, AXConversationIndexedDbMessageAiApi, AXConversationIndexedDbMessageApi, AXConversationIndexedDbRealtimeApi, AXConversationIndexedDbStorage, AXConversationIndexedDbStores, AXConversationIndexedDbUserApi, AXConversationInfoPanelComponent, AXConversationItemActionRegistry, AXConversationMessageRendererStateComponent, AXConversationMessageUtilsService, AXConversationService, AXConversationSharedStorage, AXConversationTabRegistry, AXEmojiTabComponent, AXErrorHandlerService, AXFallbackRendererComponent, AXFilePickerComponent, AXFileRendererComponent, AXFileUploadService, AXForwardMessageDialogComponent, AXImagePickerComponent, AXImageRendererComponent, AXInfiniteScrollDirective, AXInfoBarActionRegistry, AXInfoBarComponent, AXInfoBarSearchComponent, AXInfoBarService, AXLocationPickerComponent, AXLocationRendererComponent, AXMessageActionRegistry, AXMessageApi, AXMessageListComponent, AXMessageListNoActiveDefaultComponent, AXMessageListService, AXMessageRendererRegistry, AXNewConversationDialogComponent, AXPickerFooterComponent, AXPickerHeaderComponent, AXRealtimeApi, AXRegistryService, AXSidebarComponent, AXSidebarService, AXStickerRendererComponent, AXStickerTabComponent, AXSystemRendererComponent, AXTextRendererComponent, AXUserApi, AXVideoInfoBarBannerComponent, AXVideoPickerComponent, AXVideoRendererComponent, AXVoiceInfoBarBannerComponent, AXVoiceRecorderComponent, AXVoiceRendererComponent, AX_CONVERSATION_AUDIO_RENDERER, AX_CONVERSATION_COMPOSER_AUDIO_ACTION, AX_CONVERSATION_COMPOSER_EMOJI_ACTION, AX_CONVERSATION_COMPOSER_EMOJI_TAB, AX_CONVERSATION_COMPOSER_FILE_ACTION, AX_CONVERSATION_COMPOSER_IMAGE_ACTION, AX_CONVERSATION_COMPOSER_LOCATION_ACTION, AX_CONVERSATION_COMPOSER_STICKER_TAB, AX_CONVERSATION_COMPOSER_VIDEO_ACTION, AX_CONVERSATION_COMPOSER_VOICE_RECORDING_ACTION, AX_CONVERSATION_FALLBACK_RENDERER, AX_CONVERSATION_FILE_RENDERER, AX_CONVERSATION_IMAGE_RENDERER, AX_CONVERSATION_INFO_BAR_ARCHIVE_ACTION, AX_CONVERSATION_INFO_BAR_BLOCK_ACTION, AX_CONVERSATION_INFO_BAR_DELETE_ACTION, AX_CONVERSATION_INFO_BAR_DIVIDER, AX_CONVERSATION_INFO_BAR_INFO_ACTION, AX_CONVERSATION_INFO_BAR_MUTE_ACTION, AX_CONVERSATION_INFO_BAR_SEARCH_ACTION, AX_CONVERSATION_ITEM_BLOCK_ACTION, AX_CONVERSATION_ITEM_DELETE_ACTION, AX_CONVERSATION_ITEM_DIVIDER, AX_CONVERSATION_ITEM_MARK_READ_ACTION, AX_CONVERSATION_ITEM_MUTE_ACTION, AX_CONVERSATION_ITEM_PIN_ACTION, AX_CONVERSATION_LOCATION_RENDERER, AX_CONVERSATION_MESSAGE_COPY_ACTION, AX_CONVERSATION_MESSAGE_DELETE_ACTION, AX_CONVERSATION_MESSAGE_EDIT_ACTION, AX_CONVERSATION_MESSAGE_FORWARD_ACTION, AX_CONVERSATION_MESSAGE_REPLY_ACTION, AX_CONVERSATION_STICKER_RENDERER, AX_CONVERSATION_SYSTEM_RENDERER, AX_CONVERSATION_TAB_ALL, AX_CONVERSATION_TAB_ARCHIVED, AX_CONVERSATION_TAB_BOT, AX_CONVERSATION_TAB_CHANNELS, AX_CONVERSATION_TAB_GROUPS, AX_CONVERSATION_TAB_PRIVATE, AX_CONVERSATION_TAB_UNREAD, AX_CONVERSATION_TEXT_RENDERER, AX_CONVERSATION_VIDEO_RENDERER, AX_CONVERSATION_VOICE_RENDERER, AX_DEFAULT_CONVERSATION_CONFIG, AX_STICKER_API_KEY, CONNECTION_ERRORS, CONVERSATION_CONFIG, CONVERSATION_ERRORS, DEFAULT_COMPOSER_ACTIONS, DEFAULT_COMPOSER_TABS, DEFAULT_CONVERSATION_ITEM_ACTIONS, DEFAULT_CONVERSATION_TABS, DEFAULT_INFO_BAR_ACTIONS, DEFAULT_MESSAGE_ACTIONS, DEFAULT_MESSAGE_RENDERERS, ERROR_HANDLER_CONFIG, ERROR_MESSAGES, FILE_ERRORS, LOCATION_ERRORS, MESSAGE_ERRORS, PERMISSION_ERRORS, REGISTRY_CONFIG, URL_ERRORS, USER_ERRORS, axConversationIndexedDbStorage, conversationSharedStorage, formatErrorMessage, getDefaultConversationItemActions, getErrorMessage, mergeWithDefaults, provideConversation, sanitizeInput, validateConversationId, validateEmail, validateFile, validateLatitude, validateLongitude, validateMessagePayload, validateMessageText, validateMessageType, validateUrl, validateUserId, validateUserIds };
19034
18751
  //# sourceMappingURL=acorex-components-conversation2.mjs.map