@acorex/components 20.6.32 → 20.6.34

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,873 +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: "20.3.3", ngImport: i0, type: AXErrorHandlerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
683
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXErrorHandlerService, providedIn: 'root' }); }
684
- }
685
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.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
- equal: (a, b) => a.length === b.length && a.every((v, i) => v === b[i]),
728
- }]));
729
- /** All messages as array */
730
- 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]) }] : [{ equal: (a, b) => a.length === b.length && a.every((v, i) => v === b[i]) }]));
731
- }
732
- // =====================
733
- // Initialization
734
- // =====================
735
- /**
736
- * Initialize the store
737
- */
738
- async initialize() {
739
- // Store is initialized with empty state
740
- // Data will be loaded from API
741
- }
742
- // =====================
743
- // Conversation Operations
744
- // =====================
745
- /**
746
- * Set all conversations (replaces existing)
747
- * @param conversations - Array of conversations
748
- */
749
- setConversations(conversations) {
750
- const convMap = new Map();
751
- conversations.forEach((conv) => convMap.set(conv.id, conv));
752
- this._conversations.set(convMap);
753
- }
754
- /**
755
- * Add multiple conversations (append to existing)
756
- * Used for pagination - appends new conversations without replacing existing ones
757
- * @param conversations - Array of conversations to add
758
- */
759
- addConversations(conversations) {
760
- this._conversations.update((existingConversations) => {
761
- const newConversations = new Map(existingConversations);
762
- conversations.forEach((conv) => newConversations.set(conv.id, conv));
763
- // Cleanup old conversations if cache is too large
764
- const maxCached = this.config.maxCachedConversations;
765
- if (newConversations.size > maxCached) {
766
- // Sort by last activity and keep only the most recent
767
- const sorted = Array.from(newConversations.values()).sort((a, b) => (b.lastMessageAt?.getTime() ?? 0) - (a.lastMessageAt?.getTime() ?? 0));
768
- // Keep only the most recent conversations
769
- const toKeep = sorted.slice(0, maxCached);
770
- const cleanedMap = new Map();
771
- toKeep.forEach((conv) => cleanedMap.set(conv.id, conv));
772
- return cleanedMap;
773
- }
774
- return newConversations;
775
- });
776
- }
777
- /**
778
- * Add or update a single conversation
779
- * @param conversation - Conversation to add/update
780
- */
781
- setConversation(conversation) {
782
- this._conversations.update((conversations) => {
783
- const newConversations = new Map(conversations);
784
- newConversations.set(conversation.id, conversation);
785
- return newConversations;
786
- });
787
- }
788
- /**
789
- * Get a conversation by ID
790
- * @param conversationId - Conversation ID
791
- * @returns Conversation or undefined
792
- */
793
- getConversation(conversationId) {
794
- return this._conversations().get(conversationId);
795
- }
796
- /**
797
- * Update a conversation with partial data
798
- * @param conversationId - Conversation ID
799
- * @param updates - Partial conversation updates
800
- */
801
- updateConversation(conversationId, updates) {
802
- const conversation = this._conversations().get(conversationId);
803
- if (!conversation)
804
- return;
805
- const updatedConversation = { ...conversation, ...updates };
806
- this.setConversation(updatedConversation);
807
- }
808
- /**
809
- * Delete a conversation
810
- * @param conversationId - Conversation ID
811
- */
812
- deleteConversation(conversationId) {
813
- // Remove conversation
814
- this._conversations.update((conversations) => {
815
- const newConversations = new Map(conversations);
816
- newConversations.delete(conversationId);
817
- return newConversations;
818
- });
819
- // Remove all messages for this conversation
820
- const messageIds = this._conversationMessages().get(conversationId) || [];
821
- this._messages.update((messages) => {
822
- const newMessages = new Map(messages);
823
- messageIds.forEach((id) => newMessages.delete(id));
824
- return newMessages;
825
- });
826
- this._conversationMessages.update((map) => {
827
- const newMap = new Map(map);
828
- newMap.delete(conversationId);
829
- return newMap;
830
- });
831
- }
832
- // =====================
833
- // Conversation Updates
834
- // =====================
835
- /**
836
- * Update conversation's last message
837
- * @param message - The message to set as last message
838
- */
839
- updateLastMessage(message) {
840
- this.updateConversation(message.conversationId, {
841
- lastMessage: message,
842
- lastMessageAt: message.timestamp,
843
- updatedAt: message.timestamp,
844
- });
845
- }
846
- /**
847
- * Increment unread count
848
- * @param conversationId - Conversation ID
849
- */
850
- incrementUnreadCount(conversationId) {
851
- const conversation = this._conversations().get(conversationId);
852
- if (!conversation)
853
- return;
854
- this.updateConversation(conversationId, { unreadCount: conversation.unreadCount + 1 });
855
- }
856
- /**
857
- * Reset unread count to zero
858
- * @param conversationId - Conversation ID
859
- */
860
- resetUnreadCount(conversationId) {
861
- this.updateConversation(conversationId, { unreadCount: 0 });
862
- }
863
- /**
864
- * Update conversation settings
865
- * @param conversationId - Conversation ID
866
- * @param settings - Settings to merge
867
- */
868
- updateSettings(conversationId, settings) {
869
- const conversation = this._conversations().get(conversationId);
870
- if (!conversation)
871
- return;
872
- this.updateConversation(conversationId, {
873
- settings: { ...conversation.settings, ...settings },
874
- updatedAt: new Date(),
875
- });
876
- }
877
- /**
878
- * Update conversation title
879
- * @param conversationId - Conversation ID
880
- * @param title - New title
881
- */
882
- updateTitle(conversationId, title) {
883
- this.updateConversation(conversationId, { title, updatedAt: new Date() });
884
- }
885
- /**
886
- * Update conversation metadata
887
- * @param conversationId - Conversation ID
888
- * @param metadata - Metadata to merge
889
- */
890
- updateMetadata(conversationId, metadata) {
891
- const conversation = this._conversations().get(conversationId);
892
- if (!conversation)
893
- return;
894
- this.updateConversation(conversationId, {
895
- metadata: { ...conversation.metadata, ...metadata },
896
- updatedAt: new Date(),
897
- });
898
- }
899
- /**
900
- * Update typing indicator
901
- * @param conversationId - Conversation ID
902
- * @param userId - User ID
903
- * @param isTyping - Whether user is typing
904
- */
905
- updateTypingIndicator(conversationId, userId, isTyping) {
906
- const conversation = this._conversations().get(conversationId);
907
- if (!conversation)
908
- return;
909
- let typingUsers = [...conversation.status.typingUsers];
910
- if (isTyping) {
911
- if (!typingUsers.includes(userId)) {
912
- typingUsers.push(userId);
913
- }
914
- }
915
- else {
916
- typingUsers = typingUsers.filter((id) => id !== userId);
917
- }
918
- this.updateConversation(conversationId, {
919
- status: {
920
- ...conversation.status,
921
- isTyping: typingUsers.length > 0,
922
- typingUsers,
923
- },
924
- });
925
- }
926
- /**
927
- * Update participant presence across all conversations
928
- * @param userId - User ID
929
- * @param status - Presence status
930
- * @param lastSeen - Last seen date
931
- */
932
- updateParticipantPresence(userId, status, lastSeen) {
933
- this._conversations.update((conversations) => {
934
- const newConversations = new Map(conversations);
935
- for (const [id, conv] of conversations) {
936
- const participant = conv.participants.find((p) => p.id === userId);
937
- if (participant) {
938
- const updatedConversation = {
939
- ...conv,
940
- participants: conv.participants.map((p) => (p.id === userId ? { ...p, status, lastSeen } : p)),
941
- status: conv.type === 'private' ? { ...conv.status, presence: status, lastSeen } : conv.status,
942
- };
943
- newConversations.set(id, updatedConversation);
944
- }
945
- }
946
- return newConversations;
947
- });
948
- }
949
- // =====================
950
- // Message Operations
951
- // =====================
952
- /**
953
- * Add a message to the store
954
- * @param message - Message to add
955
- */
956
- addMessage(message) {
957
- // Add to messages map
958
- this._messages.update((messages) => {
959
- const newMessages = new Map(messages);
960
- newMessages.set(message.id, message);
961
- return newMessages;
962
- });
963
- // Update conversation messages list (sorted) — always produce new arrays
964
- this._conversationMessages.update((map) => {
965
- const newMap = new Map(map);
966
- const existing = newMap.get(message.conversationId) || [];
967
- if (existing.includes(message.id)) {
968
- return newMap;
969
- }
970
- const updated = [...existing, message.id].sort((a, b) => {
971
- const msgA = this._messages().get(a);
972
- const msgB = this._messages().get(b);
973
- if (!msgA || !msgB)
974
- return 0;
975
- return msgA.timestamp.getTime() - msgB.timestamp.getTime();
976
- });
977
- newMap.set(message.conversationId, updated);
978
- return newMap;
979
- });
980
- }
981
- /**
982
- * Add multiple messages
983
- * @param messages - Messages to add
984
- */
985
- addMessages(messages) {
986
- if (messages.length === 0)
987
- return;
988
- // Add all messages to map
989
- this._messages.update((msgs) => {
990
- const newMessages = new Map(msgs);
991
- messages.forEach((msg) => newMessages.set(msg.id, msg));
992
- return newMessages;
993
- });
994
- // Update conversation messages lists
995
- const conversationGroups = new Map();
996
- messages.forEach((msg) => {
997
- const existing = conversationGroups.get(msg.conversationId) || [];
998
- existing.push(msg.id);
999
- conversationGroups.set(msg.conversationId, existing);
1000
- });
1001
- this._conversationMessages.update((map) => {
1002
- const newMap = new Map(map);
1003
- for (const [conversationId, newMsgIds] of conversationGroups) {
1004
- const existing = newMap.get(conversationId) || [];
1005
- const idSet = new Set(existing);
1006
- const merged = [...existing];
1007
- for (const id of newMsgIds) {
1008
- if (!idSet.has(id)) {
1009
- merged.push(id);
1010
- idSet.add(id);
1011
- }
1012
- }
1013
- const sorted = merged.sort((a, b) => {
1014
- const msgA = this._messages().get(a);
1015
- const msgB = this._messages().get(b);
1016
- if (!msgA || !msgB)
1017
- return 0;
1018
- return msgA.timestamp.getTime() - msgB.timestamp.getTime();
1019
- });
1020
- newMap.set(conversationId, sorted);
1021
- }
1022
- return newMap;
1023
- });
1024
- // Cleanup old messages to prevent memory leaks
1025
- this.cleanupOldMessages();
1026
- this.cleanupConversationMessages();
1027
- }
1028
- /**
1029
- * Get a message by ID
1030
- * @param messageId - Message ID
1031
- * @returns Message or undefined
1032
- */
1033
- getMessage(messageId) {
1034
- return this._messages().get(messageId);
1035
- }
1036
- /**
1037
- * Get messages for a conversation
1038
- * @param conversationId - Conversation ID
1039
- * @returns Array of messages sorted by timestamp
1040
- */
1041
- getConversationMessages(conversationId) {
1042
- const messageIds = this._conversationMessages().get(conversationId) || [];
1043
- return messageIds.map((id) => this._messages().get(id)).filter((msg) => msg !== undefined);
1044
- }
1045
- /**
1046
- * Get messages signal for a conversation
1047
- * @param conversationId - Conversation ID
1048
- * @returns Computed signal of messages
1049
- */
1050
- getConversationMessagesSignal(conversationId) {
1051
- return computed(() => this.getConversationMessages(conversationId));
1052
- }
1053
- /**
1054
- * Update a message
1055
- * @param messageId - Message ID
1056
- * @param updates - Partial message updates
1057
- */
1058
- updateMessage(messageId, updates) {
1059
- const message = this._messages().get(messageId);
1060
- if (!message)
1061
- return;
1062
- const updatedMessage = { ...message, ...updates };
1063
- this.addMessage(updatedMessage);
1064
- }
1065
- /**
1066
- * Delete a message
1067
- * @param messageId - Message ID
1068
- */
1069
- deleteMessage(messageId) {
1070
- const message = this._messages().get(messageId);
1071
- if (!message)
1072
- return;
1073
- // Remove from messages map
1074
- this._messages.update((messages) => {
1075
- const newMessages = new Map(messages);
1076
- newMessages.delete(messageId);
1077
- return newMessages;
1078
- });
1079
- // Remove from conversation messages list — produce a new array via filter
1080
- this._conversationMessages.update((map) => {
1081
- const newMap = new Map(map);
1082
- const existing = newMap.get(message.conversationId);
1083
- if (existing) {
1084
- newMap.set(message.conversationId, existing.filter((id) => id !== messageId));
1085
- }
1086
- return newMap;
1087
- });
1088
- }
1089
- /**
1090
- * Clear messages for a conversation
1091
- * @param conversationId - Conversation ID
1092
- */
1093
- clearConversationMessages(conversationId) {
1094
- const messageIds = this._conversationMessages().get(conversationId) || [];
1095
- // Remove messages
1096
- this._messages.update((messages) => {
1097
- const newMessages = new Map(messages);
1098
- messageIds.forEach((id) => newMessages.delete(id));
1099
- return newMessages;
1100
- });
1101
- // Clear conversation messages list
1102
- this._conversationMessages.update((map) => {
1103
- const newMap = new Map(map);
1104
- newMap.delete(conversationId);
1105
- return newMap;
1106
- });
1107
- }
1108
- /**
1109
- * Clear all data
1110
- */
1111
- clearAll() {
1112
- this._conversations.set(new Map());
1113
- this._messages.set(new Map());
1114
- this._conversationMessages.set(new Map());
1115
- }
1116
- // =====================
1117
- // Memory Management
1118
- // =====================
1119
- /**
1120
- * Cleanup old messages to prevent unbounded memory growth
1121
- * Keeps only the most recent messages up to MAX_TOTAL_MESSAGES
1122
- */
1123
- cleanupOldMessages() {
1124
- const totalMessages = this._messages().size;
1125
- if (totalMessages > this.config.maxTotalMessages) {
1126
- // Get all messages sorted by timestamp (newest first)
1127
- const allMessages = Array.from(this._messages().values()).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
1128
- // Keep only the newest messages
1129
- const toKeep = allMessages.slice(0, this.config.maxTotalMessages);
1130
- const toKeepIds = new Set(toKeep.map((m) => m.id));
1131
- // Remove old messages
1132
- this._messages.update(() => {
1133
- const newMessages = new Map();
1134
- toKeep.forEach((msg) => newMessages.set(msg.id, msg));
1135
- return newMessages;
1136
- });
1137
- // Update conversation messages lists to remove deleted message IDs
1138
- this._conversationMessages.update((map) => {
1139
- const newMap = new Map(map);
1140
- for (const [convId, messageIds] of newMap) {
1141
- const filteredIds = messageIds.filter((id) => toKeepIds.has(id));
1142
- newMap.set(convId, filteredIds);
1143
- }
1144
- return newMap;
1145
- });
1146
- }
1147
- }
1148
- /**
1149
- * Cleanup messages per conversation to prevent memory leaks
1150
- * Keeps only recent messages per conversation
1151
- */
1152
- cleanupConversationMessages() {
1153
- const maxMessages = this.config.maxMessagesPerConversation;
1154
- const idsToRemove = [];
1155
- this._conversationMessages.update((map) => {
1156
- const newMap = new Map(map);
1157
- for (const [convId, messageIds] of newMap) {
1158
- if (messageIds.length > maxMessages) {
1159
- idsToRemove.push(...messageIds.slice(0, messageIds.length - maxMessages));
1160
- newMap.set(convId, messageIds.slice(-maxMessages));
1161
- }
1162
- }
1163
- return newMap;
1164
- });
1165
- if (idsToRemove.length > 0) {
1166
- this._messages.update((messages) => {
1167
- const newMessages = new Map(messages);
1168
- idsToRemove.forEach((id) => newMessages.delete(id));
1169
- return newMessages;
1170
- });
1171
- }
1172
- }
1173
- // =====================
1174
- // Statistics
1175
- // =====================
1176
- /**
1177
- * Get store statistics
1178
- */
1179
- getStats() {
1180
- return {
1181
- conversationCount: this._conversations().size,
1182
- messageCount: this._messages().size,
1183
- conversationsWithMessages: this._conversationMessages().size,
1184
- };
1185
- }
1186
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationStoreService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1187
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationStoreService }); }
1188
- }
1189
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXConversationStoreService, decorators: [{
1190
- type: Injectable
1191
- }] });
1192
-
1193
- /**
1194
- * Validation Utilities
1195
- * Centralized validation functions for messages, files, and user input
1196
- */
1197
- /**
1198
- * Validate message text content
1199
- * @param text - Text to validate
1200
- * @param config - Configuration for validation rules
1201
- * @returns Validation result
1202
- */
1203
- function validateMessageText(text, config) {
1204
- // Check for empty text
1205
- if (!text || text.trim().length === 0) {
1206
- return {
1207
- valid: false,
1208
- error: 'Message text cannot be empty',
1209
- errorCode: 'EMPTY_MESSAGE',
1210
- };
1211
- }
1212
- // Check minimum length
1213
- const minLength = config.minMessageLength ?? 1;
1214
- if (text.trim().length < minLength) {
1215
- return {
1216
- valid: false,
1217
- error: `Message must be at least ${minLength} character(s)`,
1218
- errorCode: 'MESSAGE_TOO_SHORT',
1219
- };
1220
- }
1221
- // Check maximum length
1222
- const maxLength = config.maxMessageLength ?? 10000;
1223
- if (text.length > maxLength) {
1224
- return {
1225
- valid: false,
1226
- error: `Message exceeds ${maxLength} character limit`,
1227
- errorCode: 'MESSAGE_TOO_LONG',
1228
- };
1229
- }
1230
- return { valid: true };
1231
- }
1232
- /**
1233
- * Validate file upload
1234
- * @param file - File to validate
1235
- * @param config - Configuration for validation rules
1236
- * @returns File validation result
1237
- */
1238
- function validateFile(file, config) {
1239
- // Check file size
1240
- const maxSize = config.maxFileSize ?? 10 * 1024 * 1024; // 10MB default
1241
- if (file.size > maxSize) {
1242
- return {
1243
- valid: false,
1244
- error: `File size exceeds ${formatFileSize(maxSize)} limit`,
1245
- errorCode: 'FILE_TOO_LARGE',
1246
- size: file.size,
1247
- type: file.type,
1248
- };
1249
- }
1250
- // Check file type if restrictions exist
1251
- const allowedTypes = config.allowedFileTypes;
1252
- if (allowedTypes && allowedTypes.length > 0) {
1253
- const isAllowed = allowedTypes.some((pattern) => matchMimeType(file.type, pattern));
1254
- if (!isAllowed) {
1255
- return {
1256
- valid: false,
1257
- error: `File type "${file.type}" is not allowed`,
1258
- errorCode: 'FILE_TYPE_NOT_ALLOWED',
1259
- size: file.size,
1260
- type: file.type,
1261
- };
1262
- }
1263
- }
1264
- return {
1265
- valid: true,
1266
- size: file.size,
1267
- type: file.type,
1268
- };
1269
- }
1270
- /**
1271
- * Validate conversation ID
1272
- * @param conversationId - Conversation ID to validate
1273
- * @returns Validation result
1274
- */
1275
- function validateConversationId(conversationId) {
1276
- if (!conversationId || typeof conversationId !== 'string' || conversationId.trim().length === 0) {
1277
- return {
1278
- valid: false,
1279
- error: 'Conversation ID is required',
1280
- errorCode: 'MISSING_CONVERSATION_ID',
1281
- };
1282
- }
1283
- // Check for reasonable length
1284
- if (conversationId.length > 255) {
1285
- return {
1286
- valid: false,
1287
- error: 'Conversation ID is too long',
1288
- 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',
1289
517
  };
1290
518
  }
1291
519
  return { valid: true };
@@ -1356,242 +584,786 @@ function validateMessagePayload(payload, type) {
1356
584
  }
1357
585
  break;
1358
586
  }
1359
- 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;
1360
746
  }
1361
747
  /**
1362
- * Validate user ID
1363
- * @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
1364
782
  * @returns Validation result
1365
783
  */
1366
- function validateUserId(userId) {
1367
- if (!userId || typeof userId !== 'string' || userId.trim().length === 0) {
784
+ function validateLatitude(latitude) {
785
+ if (latitude === undefined || latitude === null || typeof latitude !== 'number' || isNaN(latitude)) {
1368
786
  return {
1369
787
  valid: false,
1370
- error: 'User ID is required',
1371
- errorCode: 'MISSING_USER_ID',
788
+ error: 'Latitude is required',
789
+ errorCode: 'MISSING_LATITUDE',
1372
790
  };
1373
791
  }
1374
- // Check for reasonable length (prevent extremely long IDs)
1375
- if (userId.length > 255) {
792
+ if (latitude < -90 || latitude > 90) {
1376
793
  return {
1377
794
  valid: false,
1378
- error: 'User ID is too long',
1379
- errorCode: 'INVALID_USER_ID',
795
+ error: 'Latitude must be between -90 and 90',
796
+ errorCode: 'INVALID_LATITUDE',
1380
797
  };
1381
798
  }
1382
799
  return { valid: true };
1383
800
  }
1384
801
  /**
1385
- * Validate array of user IDs
1386
- * @param userIds - Array of user IDs to validate
1387
- * @param minCount - Minimum number of users required
1388
- * @param maxCount - Maximum number of users allowed
802
+ * Validate longitude coordinate
803
+ * @param longitude - Longitude to validate
1389
804
  * @returns Validation result
1390
805
  */
1391
- function validateUserIds(userIds, minCount = 1, maxCount) {
1392
- if (!userIds || !Array.isArray(userIds)) {
806
+ function validateLongitude(longitude) {
807
+ if (longitude === undefined || longitude === null || typeof longitude !== 'number' || isNaN(longitude)) {
1393
808
  return {
1394
809
  valid: false,
1395
- error: 'User IDs must be an array',
1396
- errorCode: 'INVALID_USER_IDS',
810
+ error: 'Longitude is required',
811
+ errorCode: 'MISSING_LONGITUDE',
1397
812
  };
1398
813
  }
1399
- if (userIds.length < minCount) {
814
+ if (longitude < -180 || longitude > 180) {
1400
815
  return {
1401
816
  valid: false,
1402
- error: `At least ${minCount} user(s) required`,
1403
- errorCode: 'TOO_FEW_USERS',
817
+ error: 'Longitude must be between -180 and 180',
818
+ errorCode: 'INVALID_LONGITUDE',
1404
819
  };
1405
820
  }
1406
- if (maxCount && userIds.length > maxCount) {
1407
- return {
1408
- valid: false,
1409
- error: `Maximum ${maxCount} user(s) allowed`,
1410
- errorCode: 'TOO_MANY_USERS',
1411
- };
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]) }] : [{
838
+ equal: (a, b) => a.length === b.length && a.every((v, i) => v === b[i]),
839
+ }]));
1412
840
  }
1413
- // Check for empty or invalid IDs
1414
- const invalidIds = userIds.filter((id) => !id || id.trim().length === 0);
1415
- if (invalidIds.length > 0) {
1416
- return {
1417
- valid: false,
1418
- error: 'All user IDs must be non-empty strings',
1419
- errorCode: 'INVALID_USER_ID',
1420
- };
841
+ setConversations(conversations) {
842
+ const convMap = new Map();
843
+ conversations.forEach((conv) => convMap.set(conv.id, conv));
844
+ this._conversations.set(convMap);
845
+ }
846
+ addConversations(conversations) {
847
+ this._conversations.update((existingConversations) => {
848
+ const newConversations = new Map(existingConversations);
849
+ conversations.forEach((conv) => newConversations.set(conv.id, conv));
850
+ const maxCached = this.config.maxCachedConversations;
851
+ if (newConversations.size > maxCached) {
852
+ const sorted = Array.from(newConversations.values()).sort((a, b) => (b.lastMessageAt?.getTime() ?? 0) - (a.lastMessageAt?.getTime() ?? 0));
853
+ const toKeep = sorted.slice(0, maxCached);
854
+ const cleanedMap = new Map();
855
+ toKeep.forEach((conv) => cleanedMap.set(conv.id, conv));
856
+ return cleanedMap;
857
+ }
858
+ return newConversations;
859
+ });
860
+ }
861
+ setConversation(conversation) {
862
+ this._conversations.update((conversations) => {
863
+ const newConversations = new Map(conversations);
864
+ newConversations.set(conversation.id, conversation);
865
+ return newConversations;
866
+ });
867
+ }
868
+ getConversation(conversationId) {
869
+ return this._conversations().get(conversationId);
870
+ }
871
+ updateConversation(conversationId, updates) {
872
+ const conversation = this._conversations().get(conversationId);
873
+ if (!conversation)
874
+ return;
875
+ this.setConversation({ ...conversation, ...updates });
876
+ }
877
+ deleteConversation(conversationId) {
878
+ this._conversations.update((conversations) => {
879
+ const newConversations = new Map(conversations);
880
+ newConversations.delete(conversationId);
881
+ return newConversations;
882
+ });
883
+ const messageIds = this._conversationMessages().get(conversationId) || [];
884
+ this._messages.update((messages) => {
885
+ const newMessages = new Map(messages);
886
+ messageIds.forEach((id) => newMessages.delete(id));
887
+ return newMessages;
888
+ });
889
+ this._conversationMessages.update((map) => {
890
+ const newMap = new Map(map);
891
+ newMap.delete(conversationId);
892
+ return newMap;
893
+ });
894
+ }
895
+ updateLastMessage(message) {
896
+ this.updateConversation(message.conversationId, {
897
+ lastMessage: message,
898
+ lastMessageAt: message.timestamp,
899
+ updatedAt: message.timestamp,
900
+ });
901
+ }
902
+ incrementUnreadCount(conversationId) {
903
+ const conversation = this._conversations().get(conversationId);
904
+ if (!conversation)
905
+ return;
906
+ this.updateConversation(conversationId, { unreadCount: conversation.unreadCount + 1 });
907
+ }
908
+ resetUnreadCount(conversationId) {
909
+ this.updateConversation(conversationId, { unreadCount: 0 });
910
+ }
911
+ updateSettings(conversationId, settings) {
912
+ const conversation = this._conversations().get(conversationId);
913
+ if (!conversation)
914
+ return;
915
+ this.updateConversation(conversationId, {
916
+ settings: { ...conversation.settings, ...settings },
917
+ updatedAt: new Date(),
918
+ });
919
+ }
920
+ updateTitle(conversationId, title) {
921
+ this.updateConversation(conversationId, { title, updatedAt: new Date() });
922
+ }
923
+ updateMetadata(conversationId, metadata) {
924
+ const conversation = this._conversations().get(conversationId);
925
+ if (!conversation)
926
+ return;
927
+ this.updateConversation(conversationId, {
928
+ metadata: { ...conversation.metadata, ...metadata },
929
+ updatedAt: new Date(),
930
+ });
931
+ }
932
+ updateTypingIndicator(conversationId, userId, isTyping) {
933
+ const conversation = this._conversations().get(conversationId);
934
+ if (!conversation)
935
+ return;
936
+ let typingUsers = [...conversation.status.typingUsers];
937
+ if (isTyping) {
938
+ if (!typingUsers.includes(userId)) {
939
+ typingUsers.push(userId);
940
+ }
941
+ }
942
+ else {
943
+ typingUsers = typingUsers.filter((id) => id !== userId);
944
+ }
945
+ this.updateConversation(conversationId, {
946
+ status: {
947
+ ...conversation.status,
948
+ isTyping: typingUsers.length > 0,
949
+ typingUsers,
950
+ },
951
+ });
952
+ }
953
+ updateParticipantPresence(userId, status, lastSeen) {
954
+ this._conversations.update((conversations) => {
955
+ const newConversations = new Map(conversations);
956
+ for (const [id, conv] of conversations) {
957
+ const participant = conv.participants.find((p) => p.id === userId);
958
+ if (participant) {
959
+ const updatedConversation = {
960
+ ...conv,
961
+ participants: conv.participants.map((p) => (p.id === userId ? { ...p, status, lastSeen } : p)),
962
+ status: conv.type === 'private' ? { ...conv.status, presence: status, lastSeen } : conv.status,
963
+ };
964
+ newConversations.set(id, updatedConversation);
965
+ }
966
+ }
967
+ return newConversations;
968
+ });
969
+ }
970
+ addMessage(message) {
971
+ this._messages.update((messages) => {
972
+ const newMessages = new Map(messages);
973
+ newMessages.set(message.id, message);
974
+ return newMessages;
975
+ });
976
+ this._conversationMessages.update((map) => {
977
+ const newMap = new Map(map);
978
+ const existing = newMap.get(message.conversationId) || [];
979
+ if (existing.includes(message.id)) {
980
+ return newMap;
981
+ }
982
+ const updated = [...existing, message.id].sort((a, b) => {
983
+ const msgA = this._messages().get(a);
984
+ const msgB = this._messages().get(b);
985
+ if (!msgA || !msgB)
986
+ return 0;
987
+ return msgA.timestamp.getTime() - msgB.timestamp.getTime();
988
+ });
989
+ newMap.set(message.conversationId, updated);
990
+ return newMap;
991
+ });
1421
992
  }
1422
- return { valid: true };
1423
- }
1424
- /**
1425
- * Validate email address
1426
- * @param email - Email to validate
1427
- * @returns Validation result
1428
- */
1429
- function validateEmail(email) {
1430
- if (!email || email.trim().length === 0) {
1431
- return {
1432
- valid: false,
1433
- error: 'Email is required',
1434
- errorCode: 'MISSING_EMAIL',
1435
- };
993
+ addMessages(messages) {
994
+ if (messages.length === 0)
995
+ return;
996
+ this._messages.update((msgs) => {
997
+ const newMessages = new Map(msgs);
998
+ messages.forEach((msg) => newMessages.set(msg.id, msg));
999
+ return newMessages;
1000
+ });
1001
+ const conversationGroups = new Map();
1002
+ messages.forEach((msg) => {
1003
+ const existing = conversationGroups.get(msg.conversationId) || [];
1004
+ existing.push(msg.id);
1005
+ conversationGroups.set(msg.conversationId, existing);
1006
+ });
1007
+ this._conversationMessages.update((map) => {
1008
+ const newMap = new Map(map);
1009
+ for (const [conversationId, newMsgIds] of conversationGroups) {
1010
+ const existing = newMap.get(conversationId) || [];
1011
+ const idSet = new Set(existing);
1012
+ const merged = [...existing];
1013
+ for (const id of newMsgIds) {
1014
+ if (!idSet.has(id)) {
1015
+ merged.push(id);
1016
+ idSet.add(id);
1017
+ }
1018
+ }
1019
+ const sorted = merged.sort((a, b) => {
1020
+ const msgA = this._messages().get(a);
1021
+ const msgB = this._messages().get(b);
1022
+ if (!msgA || !msgB)
1023
+ return 0;
1024
+ return msgA.timestamp.getTime() - msgB.timestamp.getTime();
1025
+ });
1026
+ newMap.set(conversationId, sorted);
1027
+ }
1028
+ return newMap;
1029
+ });
1030
+ this.cleanupOldMessages();
1031
+ this.cleanupConversationMessages();
1436
1032
  }
1437
- // Trim whitespace
1438
- const trimmedEmail = email.trim();
1439
- // Check length constraints
1440
- if (trimmedEmail.length > 254) {
1441
- return {
1442
- valid: false,
1443
- error: 'Email is too long',
1444
- errorCode: 'INVALID_EMAIL',
1445
- };
1033
+ getMessage(messageId) {
1034
+ return this._messages().get(messageId);
1446
1035
  }
1447
- // Enhanced email regex with better validation
1448
- 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])?)*$/;
1449
- if (!emailRegex.test(trimmedEmail)) {
1450
- return {
1451
- valid: false,
1452
- error: 'Invalid email format',
1453
- errorCode: 'INVALID_EMAIL',
1454
- };
1036
+ getConversationMessages(conversationId) {
1037
+ const messageIds = this._conversationMessages().get(conversationId) || [];
1038
+ return messageIds.map((id) => this._messages().get(id)).filter((msg) => msg !== undefined);
1455
1039
  }
1456
- return { valid: true };
1457
- }
1458
- /**
1459
- * Validate URL
1460
- * @param url - URL to validate
1461
- * @returns Validation result
1462
- */
1463
- function validateUrl(url) {
1464
- if (!url || url.trim().length === 0) {
1465
- return {
1466
- valid: false,
1467
- error: 'URL is required',
1468
- errorCode: 'MISSING_URL',
1469
- };
1040
+ updateMessage(messageId, updates) {
1041
+ const message = this._messages().get(messageId);
1042
+ if (!message)
1043
+ return;
1044
+ this.addMessage({ ...message, ...updates });
1470
1045
  }
1471
- const trimmedUrl = url.trim();
1472
- // Check for common URL issues
1473
- if (trimmedUrl.length > 2048) {
1474
- return {
1475
- valid: false,
1476
- error: 'URL is too long',
1477
- errorCode: 'INVALID_URL',
1478
- };
1046
+ deleteMessage(messageId) {
1047
+ const message = this._messages().get(messageId);
1048
+ if (!message)
1049
+ return;
1050
+ this._messages.update((messages) => {
1051
+ const newMessages = new Map(messages);
1052
+ newMessages.delete(messageId);
1053
+ return newMessages;
1054
+ });
1055
+ this._conversationMessages.update((map) => {
1056
+ const newMap = new Map(map);
1057
+ const existing = newMap.get(message.conversationId);
1058
+ if (existing) {
1059
+ newMap.set(message.conversationId, existing.filter((id) => id !== messageId));
1060
+ }
1061
+ return newMap;
1062
+ });
1479
1063
  }
1480
- try {
1481
- const urlObj = new URL(trimmedUrl);
1482
- // Validate protocol
1483
- if (!['http:', 'https:', 'ftp:', 'ftps:'].includes(urlObj.protocol)) {
1484
- return {
1485
- valid: false,
1486
- error: 'Invalid URL protocol',
1487
- errorCode: 'INVALID_URL',
1488
- };
1064
+ cleanupOldMessages() {
1065
+ const totalMessages = this._messages().size;
1066
+ if (totalMessages > this.config.maxTotalMessages) {
1067
+ const allMessages = Array.from(this._messages().values()).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
1068
+ const toKeep = allMessages.slice(0, this.config.maxTotalMessages);
1069
+ const toKeepIds = new Set(toKeep.map((m) => m.id));
1070
+ this._messages.update(() => {
1071
+ const newMessages = new Map();
1072
+ toKeep.forEach((msg) => newMessages.set(msg.id, msg));
1073
+ return newMessages;
1074
+ });
1075
+ this._conversationMessages.update((map) => {
1076
+ const newMap = new Map(map);
1077
+ for (const [convId, messageIds] of newMap) {
1078
+ const filteredIds = messageIds.filter((id) => toKeepIds.has(id));
1079
+ newMap.set(convId, filteredIds);
1080
+ }
1081
+ return newMap;
1082
+ });
1489
1083
  }
1490
- return { valid: true };
1491
- }
1492
- catch {
1493
- return {
1494
- valid: false,
1495
- error: 'Invalid URL format',
1496
- errorCode: 'INVALID_URL',
1497
- };
1498
- }
1499
- }
1500
- // =====================
1501
- // Helper Functions
1502
- // =====================
1503
- /**
1504
- * Match MIME type against a pattern (supports wildcards)
1505
- * @param mimeType - MIME type to check
1506
- * @param pattern - Pattern to match (e.g., "image/*", "video/mp4")
1507
- * @returns Whether the MIME type matches the pattern
1508
- */
1509
- function matchMimeType(mimeType, pattern) {
1510
- if (pattern === '*/*' || pattern === '*') {
1511
- return true;
1512
1084
  }
1513
- if (pattern.endsWith('/*')) {
1514
- const prefix = pattern.slice(0, -2);
1515
- return mimeType.startsWith(prefix);
1085
+ cleanupConversationMessages() {
1086
+ const maxMessages = this.config.maxMessagesPerConversation;
1087
+ const idsToRemove = [];
1088
+ this._conversationMessages.update((map) => {
1089
+ const newMap = new Map(map);
1090
+ for (const [convId, messageIds] of newMap) {
1091
+ if (messageIds.length > maxMessages) {
1092
+ idsToRemove.push(...messageIds.slice(0, messageIds.length - maxMessages));
1093
+ newMap.set(convId, messageIds.slice(-maxMessages));
1094
+ }
1095
+ }
1096
+ return newMap;
1097
+ });
1098
+ if (idsToRemove.length > 0) {
1099
+ this._messages.update((messages) => {
1100
+ const newMessages = new Map(messages);
1101
+ idsToRemove.forEach((id) => newMessages.delete(id));
1102
+ return newMessages;
1103
+ });
1104
+ }
1516
1105
  }
1517
- return mimeType === pattern;
1518
- }
1519
- /**
1520
- * Format file size in human-readable format
1521
- * @param bytes - File size in bytes
1522
- * @returns Formatted file size string
1523
- */
1524
- function formatFileSize(bytes) {
1525
- if (bytes === 0)
1526
- return '0 Bytes';
1527
- const k = 1024;
1528
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
1529
- const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);
1530
- return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
1531
1106
  }
1107
+
1532
1108
  /**
1533
- * Sanitize user input to prevent XSS
1534
- * Note: Angular provides built-in sanitization, but this is an additional layer
1535
- * @param input - User input to sanitize
1536
- * @returns Sanitized input
1109
+ * Error Handler Service
1110
+ * Centralized error handling and logging
1537
1111
  */
1538
- function sanitizeInput(input) {
1539
- if (!input)
1540
- return '';
1541
- return input
1542
- .replace(/&/g, '&amp;')
1543
- .replace(/</g, '&lt;')
1544
- .replace(/>/g, '&gt;')
1545
- .replace(/"/g, '&quot;')
1546
- .replace(/'/g, '&#x27;')
1547
- .replace(/\//g, '&#x2F;')
1548
- .replace(/`/g, '&#x60;')
1549
- .replace(/=/g, '&#x3D;');
1550
- }
1551
1112
  /**
1552
- * Validate latitude coordinate
1553
- * @param latitude - Latitude to validate
1554
- * @returns Validation result
1113
+ * Error Handler Service
1555
1114
  */
1556
- function validateLatitude(latitude) {
1557
- if (latitude === undefined || latitude === null || typeof latitude !== 'number' || isNaN(latitude)) {
1558
- return {
1559
- valid: false,
1560
- error: 'Latitude is required',
1561
- errorCode: 'MISSING_LATITUDE',
1115
+ class AXErrorHandlerService {
1116
+ constructor() {
1117
+ this.injectedConfig = inject(ERROR_HANDLER_CONFIG);
1118
+ this._errors$ = new Subject();
1119
+ this._config = {
1120
+ logToConsole: true,
1121
+ showUserMessages: true,
1122
+ autoRetry: false,
1123
+ maxRetries: 3,
1562
1124
  };
1125
+ /** Error stream */
1126
+ this.errors$ = this._errors$.asObservable();
1127
+ this.configure(this.injectedConfig);
1128
+ }
1129
+ /**
1130
+ * Configure error handler
1131
+ */
1132
+ configure(config) {
1133
+ Object.assign(this._config, config);
1134
+ }
1135
+ /**
1136
+ * Handle an error
1137
+ */
1138
+ handle(error, operation, context) {
1139
+ const conversationError = this.normalizeError(error, operation, context);
1140
+ this.publish(conversationError);
1141
+ return conversationError;
1563
1142
  }
1564
- if (latitude < -90 || latitude > 90) {
1143
+ /**
1144
+ * Handle API error (same pipeline as {@link handle}, for typed API failures)
1145
+ */
1146
+ handleApiError(apiError, operation, context) {
1147
+ const conversationError = this.conversationErrorFromApi(apiError, operation, context);
1148
+ this.publish(conversationError);
1149
+ return conversationError;
1150
+ }
1151
+ publish(conversationError) {
1152
+ this._errors$.next(conversationError);
1153
+ if (this._config.logToConsole) {
1154
+ this.logError(conversationError);
1155
+ }
1156
+ if (this._config.customHandler) {
1157
+ this._config.customHandler(conversationError);
1158
+ }
1159
+ }
1160
+ /**
1161
+ * Normalize any error to conversation error format (does not publish — use {@link handle})
1162
+ */
1163
+ normalizeError(error, operation, context) {
1164
+ if (this.isApiError(error)) {
1165
+ return this.conversationErrorFromApi(error, operation, context);
1166
+ }
1167
+ const errorObj = error;
1168
+ const message = (typeof errorObj['message'] === 'string' && errorObj['message']) ||
1169
+ (error instanceof Error ? error.message : String(error)) ||
1170
+ 'An unknown error occurred';
1171
+ const code = (typeof errorObj['code'] === 'string' && errorObj['code']) || 'UNKNOWN_ERROR';
1172
+ const statusCodeRaw = errorObj['statusCode'] ?? errorObj['status'];
1173
+ const statusCode = typeof statusCodeRaw === 'number' ? statusCodeRaw : undefined;
1565
1174
  return {
1566
- valid: false,
1567
- error: 'Latitude must be between -90 and 90',
1568
- errorCode: 'INVALID_LATITUDE',
1175
+ code,
1176
+ message,
1177
+ severity: 'error',
1178
+ operation,
1179
+ originalError: error,
1180
+ statusCode,
1181
+ context,
1182
+ timestamp: new Date(),
1183
+ handled: false,
1184
+ recoverySuggestions: this.getDefaultRecoverySuggestions(code),
1569
1185
  };
1570
1186
  }
1571
- return { valid: true };
1572
- }
1573
- /**
1574
- * Validate longitude coordinate
1575
- * @param longitude - Longitude to validate
1576
- * @returns Validation result
1577
- */
1578
- function validateLongitude(longitude) {
1579
- if (longitude === undefined || longitude === null || typeof longitude !== 'number' || isNaN(longitude)) {
1187
+ conversationErrorFromApi(apiError, operation, context) {
1580
1188
  return {
1581
- valid: false,
1582
- error: 'Longitude is required',
1583
- errorCode: 'MISSING_LONGITUDE',
1189
+ code: apiError.code,
1190
+ message: apiError.message,
1191
+ severity: this.determineSeverity(apiError.statusCode),
1192
+ operation,
1193
+ originalError: apiError,
1194
+ statusCode: apiError.statusCode,
1195
+ context,
1196
+ timestamp: apiError.timestamp ?? new Date(),
1197
+ handled: false,
1198
+ recoverySuggestions: this.getRecoverySuggestions(apiError),
1584
1199
  };
1585
1200
  }
1586
- if (longitude < -180 || longitude > 180) {
1587
- return {
1588
- valid: false,
1589
- error: 'Longitude must be between -180 and 180',
1590
- errorCode: 'INVALID_LONGITUDE',
1201
+ isApiError(error) {
1202
+ return (typeof error === 'object' &&
1203
+ error !== null &&
1204
+ 'code' in error &&
1205
+ 'message' in error &&
1206
+ typeof error.code === 'string' &&
1207
+ typeof error.message === 'string');
1208
+ }
1209
+ /**
1210
+ * Determine severity based on status code
1211
+ */
1212
+ determineSeverity(statusCode) {
1213
+ if (!statusCode)
1214
+ return 'error';
1215
+ if (statusCode >= 500)
1216
+ return 'critical';
1217
+ if (statusCode >= 400)
1218
+ return 'error';
1219
+ if (statusCode >= 300)
1220
+ return 'warning';
1221
+ return 'info';
1222
+ }
1223
+ /**
1224
+ * Get recovery suggestions based on error
1225
+ */
1226
+ getRecoverySuggestions(error) {
1227
+ const suggestions = [];
1228
+ if (error.statusCode === 401 || error.code === 'UNAUTHORIZED') {
1229
+ suggestions.push('Please log in again');
1230
+ suggestions.push('Check if your session has expired');
1231
+ }
1232
+ else if (error.statusCode === 403 || error.code === 'FORBIDDEN') {
1233
+ suggestions.push('You do not have permission for this action');
1234
+ suggestions.push('Ask your administrator for access');
1235
+ }
1236
+ else if (error.statusCode === 404 || error.code === 'NOT_FOUND') {
1237
+ suggestions.push('The requested resource was not found');
1238
+ suggestions.push('It may have been deleted or moved');
1239
+ }
1240
+ else if (error.statusCode === 429 || error.code === 'RATE_LIMIT_EXCEEDED') {
1241
+ suggestions.push('Too many requests. Please wait and try again');
1242
+ }
1243
+ else if (error.statusCode && error.statusCode >= 500) {
1244
+ suggestions.push('Server error occurred');
1245
+ suggestions.push('Please try again later');
1246
+ suggestions.push('If the problem persists, reach out to support');
1247
+ }
1248
+ else if (error.code === 'NETWORK_ERROR') {
1249
+ suggestions.push('Check your internet connection');
1250
+ suggestions.push('Try refreshing the page');
1251
+ }
1252
+ return suggestions;
1253
+ }
1254
+ /**
1255
+ * Get default recovery suggestions
1256
+ */
1257
+ getDefaultRecoverySuggestions(code) {
1258
+ const suggestions = [];
1259
+ if (code.includes('NETWORK') || code.includes('CONNECTION')) {
1260
+ suggestions.push('Check your internet connection');
1261
+ suggestions.push('Try refreshing the page');
1262
+ }
1263
+ else if (code.includes('TIMEOUT')) {
1264
+ suggestions.push('The operation took too long');
1265
+ suggestions.push('Please try again');
1266
+ }
1267
+ else {
1268
+ suggestions.push('Please try again');
1269
+ suggestions.push('If the problem persists, reach out to support');
1270
+ }
1271
+ return suggestions;
1272
+ }
1273
+ /**
1274
+ * Log error to console
1275
+ */
1276
+ logError(error) {
1277
+ const isError = error.severity === 'critical' || error.severity === 'error';
1278
+ const header = `[Conversation ${error.severity.toUpperCase()}] ${error.operation}:`;
1279
+ if (isError) {
1280
+ console.error(header, error.message, error.context || '');
1281
+ }
1282
+ else {
1283
+ console.warn(header, error.message, error.context || '');
1284
+ }
1285
+ if (error.originalError && error.severity !== 'info') {
1286
+ console.error('Original error:', error.originalError);
1287
+ }
1288
+ if (error.recoverySuggestions && error.recoverySuggestions.length > 0) {
1289
+ console.info('Recovery suggestions:', error.recoverySuggestions);
1290
+ }
1291
+ }
1292
+ /**
1293
+ * Get user-friendly error message
1294
+ */
1295
+ getUserFriendlyMessage(error) {
1296
+ // Map technical errors to user-friendly messages
1297
+ const messageMap = {
1298
+ NETWORK_ERROR: 'Unable to connect. Please check your internet connection.',
1299
+ UNAUTHORIZED: 'You are not authorized. Please log in again.',
1300
+ FORBIDDEN: 'You do not have permission to perform this action.',
1301
+ NOT_FOUND: 'The requested item could not be found.',
1302
+ RATE_LIMIT_EXCEEDED: 'Too many requests. Please slow down.',
1303
+ VALIDATION_ERROR: 'The provided data is invalid.',
1304
+ SERVER_ERROR: 'A server error occurred. Please try again later.',
1305
+ TIMEOUT: 'The operation timed out. Please try again.',
1591
1306
  };
1307
+ return messageMap[error.code] || error.message || 'An unexpected error occurred.';
1592
1308
  }
1593
- return { valid: true };
1309
+ /**
1310
+ * Check if error is retryable
1311
+ */
1312
+ isRetryable(error) {
1313
+ const retryableCodes = ['NETWORK_ERROR', 'TIMEOUT', 'RATE_LIMIT_EXCEEDED', 'SERVER_ERROR'];
1314
+ const retryableStatusCodes = [408, 429, 500, 502, 503, 504];
1315
+ return (retryableCodes.includes(error.code) ||
1316
+ (error.statusCode !== undefined && retryableStatusCodes.includes(error.statusCode)));
1317
+ }
1318
+ /**
1319
+ * Execute an operation with automatic retry logic
1320
+ * @param operation - The async operation to execute
1321
+ * @param operationName - Name of the operation for error tracking
1322
+ * @param context - Additional context for error handling
1323
+ * @returns Promise resolving to the operation result
1324
+ * @throws {AXConversationError} If all retries fail
1325
+ */
1326
+ async executeWithRetry(operation, operationName, context) {
1327
+ if (!this._config.autoRetry) {
1328
+ // If auto-retry is disabled, just execute once
1329
+ return operation();
1330
+ }
1331
+ const maxRetries = this._config.maxRetries ?? 3;
1332
+ let lastError;
1333
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1334
+ try {
1335
+ return await operation();
1336
+ }
1337
+ catch (error) {
1338
+ lastError = error;
1339
+ const conversationError = this.handle(error, operationName, { ...context, attempt });
1340
+ // Don't retry if error is not retryable or if this was the last attempt
1341
+ if (!this.isRetryable(conversationError) || attempt >= maxRetries) {
1342
+ throw conversationError;
1343
+ }
1344
+ // Exponential backoff: 1s, 2s, 4s, 8s...
1345
+ const delayMs = Math.pow(2, attempt) * 1000;
1346
+ await this.delay(delayMs);
1347
+ }
1348
+ }
1349
+ throw this.handle(lastError, operationName, context);
1350
+ }
1351
+ /**
1352
+ * Delay helper for retry backoff
1353
+ * @param ms - Milliseconds to delay
1354
+ */
1355
+ delay(ms) {
1356
+ return new Promise((resolve) => setTimeout(resolve, ms));
1357
+ }
1358
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXErrorHandlerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1359
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXErrorHandlerService, providedIn: 'root' }); }
1594
1360
  }
1361
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXErrorHandlerService, decorators: [{
1362
+ type: Injectable,
1363
+ args: [{
1364
+ providedIn: 'root',
1365
+ }]
1366
+ }], ctorParameters: () => [] });
1595
1367
 
1596
1368
  /**
1597
1369
  * Composer Action Registry
@@ -4970,24 +4742,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImpor
4970
4742
  */
4971
4743
 
4972
4744
  /**
4973
- * File Upload Service
4974
- * Centralized service for handling file uploads and processing
4745
+ * File helpers for composer pickers: previews, validation, and metadata.
4975
4746
  */
4976
4747
  class AXFileUploadService {
4977
4748
  constructor() {
4978
4749
  this.config = inject(CONVERSATION_CONFIG);
4979
4750
  this.platformId = inject(PLATFORM_ID);
4980
- this.uploadProgress$ = new Subject();
4981
- }
4982
- /**
4983
- * Get upload progress observable
4984
- */
4985
- getUploadProgress() {
4986
- return this.uploadProgress$.asObservable();
4987
4751
  }
4988
- /**
4989
- * Read file as data URL for preview
4990
- */
4991
4752
  async readFileAsDataURL(file) {
4992
4753
  return new Promise((resolve, reject) => {
4993
4754
  const reader = new FileReader();
@@ -4996,9 +4757,6 @@ class AXFileUploadService {
4996
4757
  reader.readAsDataURL(file);
4997
4758
  });
4998
4759
  }
4999
- /**
5000
- * Get file type category
5001
- */
5002
4760
  getFileType(file) {
5003
4761
  if (file.type.startsWith('image/'))
5004
4762
  return 'image';
@@ -5008,9 +4766,6 @@ class AXFileUploadService {
5008
4766
  return 'audio';
5009
4767
  return 'file';
5010
4768
  }
5011
- /**
5012
- * Generate file preview
5013
- */
5014
4769
  async generatePreview(file) {
5015
4770
  const type = this.getFileType(file);
5016
4771
  let preview;
@@ -5024,15 +4779,9 @@ class AXFileUploadService {
5024
4779
  }
5025
4780
  return { file, preview, type };
5026
4781
  }
5027
- /**
5028
- * Generate previews for multiple files
5029
- */
5030
4782
  async generatePreviews(files) {
5031
4783
  return Promise.all(files.map((file) => this.generatePreview(file)));
5032
4784
  }
5033
- /**
5034
- * Get video duration
5035
- */
5036
4785
  async getVideoDuration(file) {
5037
4786
  if (!isPlatformBrowser(this.platformId)) {
5038
4787
  return 0;
@@ -5051,9 +4800,6 @@ class AXFileUploadService {
5051
4800
  video.src = URL.createObjectURL(file);
5052
4801
  });
5053
4802
  }
5054
- /**
5055
- * Get audio duration
5056
- */
5057
4803
  async getAudioDuration(file) {
5058
4804
  if (!isPlatformBrowser(this.platformId)) {
5059
4805
  return 0;
@@ -5071,9 +4817,6 @@ class AXFileUploadService {
5071
4817
  audio.src = URL.createObjectURL(file);
5072
4818
  });
5073
4819
  }
5074
- /**
5075
- * Format file size
5076
- */
5077
4820
  formatFileSize(bytes) {
5078
4821
  if (bytes < 1024)
5079
4822
  return `${bytes} B`;
@@ -5083,17 +4826,11 @@ class AXFileUploadService {
5083
4826
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
5084
4827
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
5085
4828
  }
5086
- /**
5087
- * Format duration (seconds to mm:ss)
5088
- */
5089
4829
  formatDuration(seconds) {
5090
4830
  const mins = Math.floor(seconds / 60);
5091
4831
  const secs = Math.floor(seconds % 60);
5092
4832
  return `${mins}:${secs.toString().padStart(2, '0')}`;
5093
4833
  }
5094
- /**
5095
- * Validate file type
5096
- */
5097
4834
  validateFileType(file, allowedTypes) {
5098
4835
  return allowedTypes.some((type) => {
5099
4836
  if (type.endsWith('/*')) {
@@ -5103,34 +4840,19 @@ class AXFileUploadService {
5103
4840
  return file.type === type;
5104
4841
  });
5105
4842
  }
5106
- /**
5107
- * Validate file size
5108
- */
5109
4843
  validateFileSize(file, maxSizeInMB) {
5110
4844
  const maxSizeInBytes = maxSizeInMB * 1024 * 1024;
5111
4845
  return file.size <= maxSizeInBytes;
5112
4846
  }
5113
- /**
5114
- * Filter files by type
5115
- */
5116
4847
  filterFilesByType(files, allowedTypes) {
5117
4848
  return files.filter((file) => this.validateFileType(file, allowedTypes));
5118
4849
  }
5119
- /**
5120
- * Validate file type against conversation config
5121
- */
5122
4850
  isFileTypeAllowed(file) {
5123
4851
  return this.validateFileType(file, this.config.allowedFileTypes);
5124
4852
  }
5125
- /**
5126
- * Validate file size against conversation config
5127
- */
5128
4853
  isFileSizeAllowed(file) {
5129
4854
  return file.size <= this.config.maxFileSize;
5130
4855
  }
5131
- /**
5132
- * Validate a set of files using conversation config
5133
- */
5134
4856
  validateFilesWithConfig(files) {
5135
4857
  const accepted = [];
5136
4858
  const rejected = [];
@@ -11895,10 +11617,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImpor
11895
11617
  type: Injectable
11896
11618
  }], ctorParameters: () => [] });
11897
11619
 
11898
- /**
11899
- * Registry Service
11900
- * Central service for managing all registries and extensions
11901
- */
11620
+ /** Unified access to conversation2 registries (renderers, actions, tabs). */
11902
11621
  /**
11903
11622
  * Central Registry Service
11904
11623
  * Provides unified access to all registries
@@ -11967,12 +11686,12 @@ class AXConversationService {
11967
11686
  }
11968
11687
  constructor() {
11969
11688
  this.config = inject(CONVERSATION_CONFIG);
11689
+ this.state = new ConversationState(this.config);
11970
11690
  // New separated APIs
11971
11691
  this.userApi = inject(AXUserApi);
11972
11692
  this.conversationApi = inject(AXConversationApi);
11973
11693
  this.messageApi = inject(AXMessageApi);
11974
11694
  this.realtimeApi = inject(AXRealtimeApi, { optional: true });
11975
- this.store = inject(AXConversationStoreService);
11976
11695
  this.errorHandler = inject(AXErrorHandlerService);
11977
11696
  this.dialogService = inject(AXDialogService);
11978
11697
  this.popupService = inject(AXPopupService);
@@ -11994,18 +11713,18 @@ class AXConversationService {
11994
11713
  this._typingIndicator$ = new Subject();
11995
11714
  this._presenceUpdate$ = new Subject();
11996
11715
  /** All conversations */
11997
- this.conversations = this.store.conversations;
11716
+ this.conversations = this.state.conversations;
11998
11717
  /** Active conversation ID */
11999
11718
  this.activeConversationId = this._activeConversationId.asReadonly();
12000
11719
  /** Active conversation */
12001
11720
  this.activeConversation = computed(() => {
12002
11721
  const id = this._activeConversationId();
12003
- return id ? this.store.getConversation(id) : null;
11722
+ return id ? this.state.getConversation(id) : null;
12004
11723
  }, ...(ngDevMode ? [{ debugName: "activeConversation" }] : []));
12005
11724
  /** Messages for active conversation */
12006
11725
  this.activeMessages = computed(() => {
12007
11726
  const convId = this._activeConversationId();
12008
- return convId ? this.store.getConversationMessages(convId) : [];
11727
+ return convId ? this.state.getConversationMessages(convId) : [];
12009
11728
  }, ...(ngDevMode ? [{ debugName: "activeMessages" }] : []));
12010
11729
  /** Loading state */
12011
11730
  this.loading = this._loading.asReadonly();
@@ -12041,8 +11760,6 @@ class AXConversationService {
12041
11760
  */
12042
11761
  async initializeService() {
12043
11762
  try {
12044
- // Initialize store
12045
- await this.store.initialize();
12046
11763
  // Connect to real-time API (optional)
12047
11764
  if (this.realtimeApi) {
12048
11765
  await this.realtimeApi.connect();
@@ -12139,7 +11856,7 @@ class AXConversationService {
12139
11856
  pageSize: this.config.conversationPageSize,
12140
11857
  }, undefined);
12141
11858
  // Store conversations
12142
- this.store.setConversations(result.items);
11859
+ this.state.setConversations(result.items);
12143
11860
  }
12144
11861
  catch (error) {
12145
11862
  const handledError = this.errorHandler.handle(error, 'loadConversations');
@@ -12161,7 +11878,7 @@ class AXConversationService {
12161
11878
  }, undefined);
12162
11879
  // Append new conversations to existing ones
12163
11880
  if (result.items.length > 0) {
12164
- this.store.addConversations(result.items);
11881
+ this.state.addConversations(result.items);
12165
11882
  }
12166
11883
  return result.hasMore;
12167
11884
  }
@@ -12216,7 +11933,7 @@ class AXConversationService {
12216
11933
  page,
12217
11934
  pageSize: this.config.messagePageSize,
12218
11935
  });
12219
- this.store.addMessages(result.items);
11936
+ this.state.addMessages(result.items);
12220
11937
  return result.items;
12221
11938
  }
12222
11939
  catch (error) {
@@ -12254,12 +11971,12 @@ class AXConversationService {
12254
11971
  metadata: command.metadata,
12255
11972
  };
12256
11973
  // Add to store immediately (optimistic update)
12257
- this.store.addMessage(tempMessage);
11974
+ this.state.addMessage(tempMessage);
12258
11975
  // Send to server
12259
11976
  const sentMessage = await this.messageApi.sendMessage(command);
12260
11977
  // Replace temporary message with real one
12261
- this.store.deleteMessage(tempMessageId);
12262
- this.store.addMessage(sentMessage);
11978
+ this.state.deleteMessage(tempMessageId);
11979
+ this.state.addMessage(sentMessage);
12263
11980
  // Update conversation's last message
12264
11981
  this.updateConversationLastMessage(sentMessage);
12265
11982
  // Emit forward count update if this is a forwarded message
@@ -12272,7 +11989,7 @@ class AXConversationService {
12272
11989
  }
12273
11990
  catch (error) {
12274
11991
  // Mark temp message as failed
12275
- this.store.updateMessage(tempMessageId, { status: 'failed' });
11992
+ this.state.updateMessage(tempMessageId, { status: 'failed' });
12276
11993
  this.errorHandler.handle(error, 'sendMessage', {
12277
11994
  conversationId: command.conversationId,
12278
11995
  type: command.type,
@@ -12284,12 +12001,12 @@ class AXConversationService {
12284
12001
  * Retry a failed message
12285
12002
  */
12286
12003
  async retryFailedMessage(messageId) {
12287
- const message = this.store.getMessage(messageId);
12004
+ const message = this.state.getMessage(messageId);
12288
12005
  if (!message || message.status !== 'failed') {
12289
12006
  return;
12290
12007
  }
12291
12008
  // Update status to sending
12292
- this.store.updateMessage(messageId, { status: 'sending' });
12009
+ this.state.updateMessage(messageId, { status: 'sending' });
12293
12010
  try {
12294
12011
  // Recreate command from message
12295
12012
  const command = {
@@ -12304,14 +12021,14 @@ class AXConversationService {
12304
12021
  // Send to server
12305
12022
  const sentMessage = await this.messageApi.sendMessage(command);
12306
12023
  // Replace with real message
12307
- this.store.deleteMessage(messageId);
12308
- this.store.addMessage(sentMessage);
12024
+ this.state.deleteMessage(messageId);
12025
+ this.state.addMessage(sentMessage);
12309
12026
  // Update conversation's last message
12310
12027
  this.updateConversationLastMessage(sentMessage);
12311
12028
  }
12312
12029
  catch (error) {
12313
12030
  // Mark as failed again
12314
- this.store.updateMessage(messageId, { status: 'failed' });
12031
+ this.state.updateMessage(messageId, { status: 'failed' });
12315
12032
  this.errorHandler.handle(error, 'retryFailedMessage', { messageId });
12316
12033
  throw error;
12317
12034
  }
@@ -12322,10 +12039,10 @@ class AXConversationService {
12322
12039
  */
12323
12040
  async editMessage(messageId, payload) {
12324
12041
  // Store original message for rollback
12325
- const originalMessage = this.store.getMessage(messageId);
12042
+ const originalMessage = this.state.getMessage(messageId);
12326
12043
  try {
12327
12044
  // Optimistic update
12328
- this.store.updateMessage(messageId, {
12045
+ this.state.updateMessage(messageId, {
12329
12046
  payload,
12330
12047
  editedAt: new Date(),
12331
12048
  });
@@ -12335,7 +12052,7 @@ class AXConversationService {
12335
12052
  catch (error) {
12336
12053
  // Rollback on failure
12337
12054
  if (originalMessage) {
12338
- this.store.updateMessage(messageId, {
12055
+ this.state.updateMessage(messageId, {
12339
12056
  payload: originalMessage.payload,
12340
12057
  editedAt: originalMessage.editedAt,
12341
12058
  });
@@ -12358,17 +12075,17 @@ class AXConversationService {
12358
12075
  return;
12359
12076
  }
12360
12077
  // Store message for rollback
12361
- const message = this.store.getMessage(messageId);
12078
+ const message = this.state.getMessage(messageId);
12362
12079
  try {
12363
12080
  // Optimistic delete
12364
- this.store.deleteMessage(messageId);
12081
+ this.state.deleteMessage(messageId);
12365
12082
  // Sync with server
12366
12083
  await this.messageApi.deleteMessage(messageId, forEveryone);
12367
12084
  }
12368
12085
  catch (error) {
12369
12086
  // Rollback on failure
12370
12087
  if (message) {
12371
- this.store.addMessage(message);
12088
+ this.state.addMessage(message);
12372
12089
  }
12373
12090
  this.errorHandler.handle(error, 'deleteMessage', { messageId, forEveryone });
12374
12091
  throw error;
@@ -12407,25 +12124,25 @@ class AXConversationService {
12407
12124
  * Updates message status locally and syncs with server
12408
12125
  */
12409
12126
  async markMessageAsRead(messageId) {
12410
- const message = this.store.getMessage(messageId);
12127
+ const message = this.state.getMessage(messageId);
12411
12128
  if (!message || message.status === 'read')
12412
12129
  return;
12413
12130
  try {
12414
12131
  // Optimistic update
12415
- this.store.updateMessage(messageId, { status: 'read' });
12132
+ this.state.updateMessage(messageId, { status: 'read' });
12416
12133
  // Sync with server
12417
12134
  await this.messageApi.markAsRead(message.conversationId, [messageId]);
12418
12135
  // Update conversation unread count
12419
- const conversation = this.store.getConversation(message.conversationId);
12136
+ const conversation = this.state.getConversation(message.conversationId);
12420
12137
  if (conversation && conversation.unreadCount > 0) {
12421
- this.store.updateConversation(message.conversationId, {
12138
+ this.state.updateConversation(message.conversationId, {
12422
12139
  unreadCount: Math.max(0, conversation.unreadCount - 1),
12423
12140
  });
12424
12141
  }
12425
12142
  }
12426
12143
  catch (error) {
12427
12144
  // Rollback on failure
12428
- this.store.updateMessage(messageId, { status: message.status });
12145
+ this.state.updateMessage(messageId, { status: message.status });
12429
12146
  this.errorHandler.handle(error, 'markMessageAsRead', { messageId });
12430
12147
  // Don't throw - marking as read failure shouldn't break the app
12431
12148
  }
@@ -12435,20 +12152,20 @@ class AXConversationService {
12435
12152
  * Marks messages locally and syncs with server
12436
12153
  */
12437
12154
  async markAsRead(conversationId) {
12438
- const messages = this.store.getConversationMessages(conversationId);
12155
+ const messages = this.state.getConversationMessages(conversationId);
12439
12156
  const currentId = this._currentUser()?.id ?? 'current-user';
12440
12157
  const unreadMessageIds = messages.filter((m) => m.status !== 'read' && m.senderId !== currentId).map((m) => m.id);
12441
12158
  if (unreadMessageIds.length === 0)
12442
12159
  return;
12443
12160
  try {
12444
12161
  // Optimistic update
12445
- this.store.resetUnreadCount(conversationId);
12162
+ this.state.resetUnreadCount(conversationId);
12446
12163
  // Sync with server
12447
12164
  await this.messageApi.markAsRead(conversationId, unreadMessageIds);
12448
12165
  }
12449
12166
  catch (error) {
12450
12167
  // Rollback on failure
12451
- this.store.updateConversation(conversationId, {
12168
+ this.state.updateConversation(conversationId, {
12452
12169
  unreadCount: unreadMessageIds.length,
12453
12170
  });
12454
12171
  this.errorHandler.handle(error, 'markAsRead', { conversationId });
@@ -12485,21 +12202,21 @@ class AXConversationService {
12485
12202
  * Handle new message received
12486
12203
  */
12487
12204
  handleNewMessage(message) {
12488
- this.store.addMessage(message);
12489
- this.store.updateLastMessage(message);
12205
+ this.state.addMessage(message);
12206
+ this.state.updateLastMessage(message);
12490
12207
  // Increment unread count for messages from other users
12491
12208
  // The intersection observer will mark them as read when they become visible
12492
12209
  const currentId = this._currentUser()?.id ?? 'current-user';
12493
12210
  const isFromOtherUser = message.senderId !== currentId;
12494
12211
  if (isFromOtherUser) {
12495
- this.store.incrementUnreadCount(message.conversationId);
12212
+ this.state.incrementUnreadCount(message.conversationId);
12496
12213
  }
12497
12214
  }
12498
12215
  /**
12499
12216
  * Handle message update
12500
12217
  */
12501
12218
  handleMessageUpdate(message) {
12502
- this.store.updateMessage(message.id, message);
12219
+ this.state.updateMessage(message.id, message);
12503
12220
  }
12504
12221
  /**
12505
12222
  * Handle message count update
@@ -12519,7 +12236,7 @@ class AXConversationService {
12519
12236
  * Handle message deletion
12520
12237
  */
12521
12238
  handleMessageDeletion(messageId) {
12522
- this.store.deleteMessage(messageId);
12239
+ this.state.deleteMessage(messageId);
12523
12240
  }
12524
12241
  /**
12525
12242
  * Handle typing indicator
@@ -12530,30 +12247,30 @@ class AXConversationService {
12530
12247
  if (currentUser && indicator.userId === currentUser.id) {
12531
12248
  return;
12532
12249
  }
12533
- this.store.updateTypingIndicator(indicator.conversationId, indicator.userId, true);
12250
+ this.state.updateTypingIndicator(indicator.conversationId, indicator.userId, true);
12534
12251
  // Clear typing indicator after timeout
12535
12252
  setTimeout(() => {
12536
- this.store.updateTypingIndicator(indicator.conversationId, indicator.userId, false);
12253
+ this.state.updateTypingIndicator(indicator.conversationId, indicator.userId, false);
12537
12254
  }, this.config.typingIndicatorTimeout ?? 3000);
12538
12255
  }
12539
12256
  /**
12540
12257
  * Handle presence update
12541
12258
  */
12542
12259
  handlePresenceUpdate(update) {
12543
- this.store.updateParticipantPresence(update.userId, update.status, update.lastSeen);
12260
+ this.state.updateParticipantPresence(update.userId, update.status, update.lastSeen);
12544
12261
  }
12545
12262
  /**
12546
12263
  * Handle conversation update
12547
12264
  * Updates conversation metadata including unread count, last message, etc.
12548
12265
  */
12549
12266
  handleConversationUpdate(conversation) {
12550
- this.store.setConversation(conversation);
12267
+ this.state.setConversation(conversation);
12551
12268
  }
12552
12269
  /**
12553
12270
  * Update conversation's last message
12554
12271
  */
12555
12272
  updateConversationLastMessage(message) {
12556
- this.store.updateLastMessage(message);
12273
+ this.state.updateLastMessage(message);
12557
12274
  }
12558
12275
  // =====================
12559
12276
  // Conversation Update APIs
@@ -12565,7 +12282,7 @@ class AXConversationService {
12565
12282
  */
12566
12283
  async updateConversation(conversationId, updates) {
12567
12284
  try {
12568
- this.store.updateConversation(conversationId, updates);
12285
+ this.state.updateConversation(conversationId, updates);
12569
12286
  }
12570
12287
  catch (error) {
12571
12288
  this.errorHandler.handle(error, 'updateConversation', { conversationId });
@@ -12579,7 +12296,7 @@ class AXConversationService {
12579
12296
  */
12580
12297
  async updateConversationSettings(conversationId, settings) {
12581
12298
  try {
12582
- this.store.updateSettings(conversationId, settings);
12299
+ this.state.updateSettings(conversationId, settings);
12583
12300
  }
12584
12301
  catch (error) {
12585
12302
  this.errorHandler.handle(error, 'updateConversationSettings', { conversationId });
@@ -12593,7 +12310,7 @@ class AXConversationService {
12593
12310
  */
12594
12311
  async updateConversationTitle(conversationId, title) {
12595
12312
  try {
12596
- this.store.updateTitle(conversationId, title);
12313
+ this.state.updateTitle(conversationId, title);
12597
12314
  }
12598
12315
  catch (error) {
12599
12316
  this.errorHandler.handle(error, 'updateConversationTitle', { conversationId, title });
@@ -12607,7 +12324,7 @@ class AXConversationService {
12607
12324
  */
12608
12325
  async updateConversationMetadata(conversationId, metadata) {
12609
12326
  try {
12610
- this.store.updateMetadata(conversationId, metadata);
12327
+ this.state.updateMetadata(conversationId, metadata);
12611
12328
  }
12612
12329
  catch (error) {
12613
12330
  this.errorHandler.handle(error, 'updateConversationMetadata', { conversationId });
@@ -12620,7 +12337,7 @@ class AXConversationService {
12620
12337
  * @returns Conversation or null
12621
12338
  */
12622
12339
  getConversation(conversationId) {
12623
- return this.store.getConversation(conversationId) || null;
12340
+ return this.state.getConversation(conversationId) || null;
12624
12341
  }
12625
12342
  /**
12626
12343
  * Create a new conversation
@@ -12640,7 +12357,7 @@ class AXConversationService {
12640
12357
  icon: AXConversationService.normalizeOptionalString(metadata?.['icon']),
12641
12358
  metadata,
12642
12359
  });
12643
- this.store.setConversation(conversation);
12360
+ this.state.setConversation(conversation);
12644
12361
  return conversation;
12645
12362
  }
12646
12363
  catch (error) {
@@ -12669,7 +12386,7 @@ class AXConversationService {
12669
12386
  async markConversationAsRead(conversationId) {
12670
12387
  try {
12671
12388
  await this.conversationApi.markConversationAsRead(conversationId);
12672
- this.store.resetUnreadCount(conversationId);
12389
+ this.state.resetUnreadCount(conversationId);
12673
12390
  }
12674
12391
  catch (error) {
12675
12392
  this.errorHandler.handle(error, 'markConversationAsRead', { conversationId });
@@ -12683,7 +12400,7 @@ class AXConversationService {
12683
12400
  */
12684
12401
  async deleteConversation(conversationId) {
12685
12402
  // Get conversation title for confirmation message
12686
- const conversation = this.store.getConversation(conversationId);
12403
+ const conversation = this.state.getConversation(conversationId);
12687
12404
  const conversationTitle = conversation?.title || this.translation.translateSync('@acorex:chat.fallbacks.this-conversation');
12688
12405
  // Show confirmation dialog
12689
12406
  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);
@@ -12697,7 +12414,7 @@ class AXConversationService {
12697
12414
  if (!result)
12698
12415
  return false;
12699
12416
  // Delete from store
12700
- this.store.deleteConversation(conversationId);
12417
+ this.state.deleteConversation(conversationId);
12701
12418
  // If this was the active conversation, clear selection
12702
12419
  if (this._activeConversationId() === conversationId) {
12703
12420
  this._activeConversationId.set(null);
@@ -12762,7 +12479,7 @@ class AXConversationService {
12762
12479
  async archiveConversation(conversationId) {
12763
12480
  try {
12764
12481
  await this.conversationApi.archiveConversation(conversationId);
12765
- this.store.updateConversation(conversationId, { archived: true });
12482
+ this.state.updateConversation(conversationId, { archived: true });
12766
12483
  }
12767
12484
  catch (error) {
12768
12485
  this.errorHandler.handle(error, 'archiveConversation', { conversationId });
@@ -12776,7 +12493,7 @@ class AXConversationService {
12776
12493
  async unarchiveConversation(conversationId) {
12777
12494
  try {
12778
12495
  await this.conversationApi.unarchiveConversation(conversationId);
12779
- this.store.updateConversation(conversationId, { archived: false });
12496
+ this.state.updateConversation(conversationId, { archived: false });
12780
12497
  }
12781
12498
  catch (error) {
12782
12499
  this.errorHandler.handle(error, 'unarchiveConversation', { conversationId });
@@ -12790,7 +12507,7 @@ class AXConversationService {
12790
12507
  async pinConversation(conversationId) {
12791
12508
  try {
12792
12509
  await this.conversationApi.pinConversation(conversationId);
12793
- this.store.updateConversation(conversationId, { pinned: true });
12510
+ this.state.updateConversation(conversationId, { pinned: true });
12794
12511
  }
12795
12512
  catch (error) {
12796
12513
  this.errorHandler.handle(error, 'pinConversation', { conversationId });
@@ -12804,7 +12521,7 @@ class AXConversationService {
12804
12521
  async unpinConversation(conversationId) {
12805
12522
  try {
12806
12523
  await this.conversationApi.unpinConversation(conversationId);
12807
- this.store.updateConversation(conversationId, { pinned: false });
12524
+ this.state.updateConversation(conversationId, { pinned: false });
12808
12525
  }
12809
12526
  catch (error) {
12810
12527
  this.errorHandler.handle(error, 'unpinConversation', { conversationId });
@@ -12820,7 +12537,7 @@ class AXConversationService {
12820
12537
  try {
12821
12538
  await this.conversationApi.muteConversation(conversationId, duration);
12822
12539
  const mutedUntil = duration ? new Date(Date.now() + duration) : undefined;
12823
- this.store.updateSettings(conversationId, { mutedUntil, notifications: false });
12540
+ this.state.updateSettings(conversationId, { mutedUntil, notifications: false });
12824
12541
  }
12825
12542
  catch (error) {
12826
12543
  this.errorHandler.handle(error, 'muteConversation', { conversationId, duration });
@@ -12834,7 +12551,7 @@ class AXConversationService {
12834
12551
  async unmuteConversation(conversationId) {
12835
12552
  try {
12836
12553
  await this.conversationApi.unmuteConversation(conversationId);
12837
- this.store.updateSettings(conversationId, { mutedUntil: undefined, notifications: true });
12554
+ this.state.updateSettings(conversationId, { mutedUntil: undefined, notifications: true });
12838
12555
  }
12839
12556
  catch (error) {
12840
12557
  this.errorHandler.handle(error, 'unmuteConversation', { conversationId });
@@ -12849,7 +12566,7 @@ class AXConversationService {
12849
12566
  async addParticipants(conversationId, userIds) {
12850
12567
  try {
12851
12568
  const updatedConversation = await this.conversationApi.addParticipants(conversationId, userIds);
12852
- this.store.setConversation(updatedConversation);
12569
+ this.state.setConversation(updatedConversation);
12853
12570
  }
12854
12571
  catch (error) {
12855
12572
  this.errorHandler.handle(error, 'addParticipants', { conversationId, userIds });
@@ -12864,7 +12581,7 @@ class AXConversationService {
12864
12581
  async removeParticipant(conversationId, userId) {
12865
12582
  try {
12866
12583
  const updatedConversation = await this.conversationApi.removeParticipant(conversationId, userId);
12867
- this.store.setConversation(updatedConversation);
12584
+ this.state.setConversation(updatedConversation);
12868
12585
  }
12869
12586
  catch (error) {
12870
12587
  this.errorHandler.handle(error, 'removeParticipant', { conversationId, userId });
@@ -12878,7 +12595,7 @@ class AXConversationService {
12878
12595
  async leaveConversation(conversationId) {
12879
12596
  try {
12880
12597
  await this.conversationApi.leaveConversation(conversationId);
12881
- this.store.deleteConversation(conversationId);
12598
+ this.state.deleteConversation(conversationId);
12882
12599
  if (this._activeConversationId() === conversationId) {
12883
12600
  this._activeConversationId.set(null);
12884
12601
  }
@@ -12896,7 +12613,7 @@ class AXConversationService {
12896
12613
  async saveDraft(conversationId, draft) {
12897
12614
  try {
12898
12615
  await this.conversationApi.saveDraft(conversationId, draft);
12899
- this.store.updateConversation(conversationId, { draft });
12616
+ this.state.updateConversation(conversationId, { draft });
12900
12617
  }
12901
12618
  catch (error) {
12902
12619
  this.errorHandler.handle(error, 'saveDraft', { conversationId });
@@ -12910,7 +12627,7 @@ class AXConversationService {
12910
12627
  async clearDraft(conversationId) {
12911
12628
  try {
12912
12629
  await this.conversationApi.clearDraft(conversationId);
12913
- this.store.updateConversation(conversationId, { draft: undefined });
12630
+ this.state.updateConversation(conversationId, { draft: undefined });
12914
12631
  }
12915
12632
  catch (error) {
12916
12633
  this.errorHandler.handle(error, 'clearDraft', { conversationId });
@@ -12928,7 +12645,7 @@ class AXConversationService {
12928
12645
  async pinMessage(conversationId, messageId) {
12929
12646
  try {
12930
12647
  await this.messageApi.pinMessage(conversationId, messageId);
12931
- this.store.updateMessage(messageId, { pinned: true });
12648
+ this.state.updateMessage(messageId, { pinned: true });
12932
12649
  }
12933
12650
  catch (error) {
12934
12651
  this.errorHandler.handle(error, 'pinMessage', { conversationId, messageId });
@@ -12943,7 +12660,7 @@ class AXConversationService {
12943
12660
  async unpinMessage(conversationId, messageId) {
12944
12661
  try {
12945
12662
  await this.messageApi.unpinMessage(conversationId, messageId);
12946
- this.store.updateMessage(messageId, { pinned: false });
12663
+ this.state.updateMessage(messageId, { pinned: false });
12947
12664
  }
12948
12665
  catch (error) {
12949
12666
  this.errorHandler.handle(error, 'unpinMessage', { conversationId, messageId });
@@ -18600,7 +18317,7 @@ function createProviders(options, includeServices) {
18600
18317
  if (realtimeApi) {
18601
18318
  providers.push({ provide: AXRealtimeApi, useClass: realtimeApi });
18602
18319
  }
18603
- providers.push(AXConversationStoreService, AXConversationService, AXComposerService, AXInfoBarService, AXMessageListService, AXSidebarService);
18320
+ providers.push(AXConversationService, AXComposerService, AXInfoBarService, AXMessageListService, AXSidebarService);
18604
18321
  }
18605
18322
  if (config) {
18606
18323
  providers.push({ provide: CONVERSATION_CONFIG, useValue: mergeWithDefaults(config) });
@@ -19022,5 +18739,5 @@ function getErrorMessage(code, params) {
19022
18739
  * Generated bundle index. Do not edit.
19023
18740
  */
19024
18741
 
19025
- 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 };
18742
+ 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 };
19026
18743
  //# sourceMappingURL=acorex-components-conversation2.mjs.map