@acorex/components 21.0.3-next.21 → 21.0.3-next.22
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.
- package/fesm2022/acorex-components-conversation2.mjs +2253 -2237
- package/fesm2022/acorex-components-conversation2.mjs.map +1 -1
- package/fesm2022/acorex-components-loading.mjs +2 -2
- package/fesm2022/acorex-components-loading.mjs.map +1 -1
- package/fesm2022/acorex-components-popup.mjs +1 -1
- package/fesm2022/acorex-components-popup.mjs.map +1 -1
- package/fesm2022/acorex-components-select-box.mjs +5 -6
- package/fesm2022/acorex-components-select-box.mjs.map +1 -1
- package/fesm2022/acorex-components-selection-list.mjs +2 -2
- package/fesm2022/acorex-components-selection-list.mjs.map +1 -1
- package/fesm2022/acorex-components-tabs.mjs +2 -2
- package/fesm2022/acorex-components-tabs.mjs.map +1 -1
- package/package.json +3 -3
- package/types/acorex-components-conversation2.d.ts +3 -3
|
@@ -3,14 +3,14 @@ import { AXTabsComponent, AXTabItemComponent } from '@acorex/components/tabs';
|
|
|
3
3
|
import * as i1$4 from '@angular/common';
|
|
4
4
|
import { isPlatformBrowser, AsyncPipe, CommonModule, NgComponentOutlet, DOCUMENT } from '@angular/common';
|
|
5
5
|
import * as i0 from '@angular/core';
|
|
6
|
-
import { InjectionToken, signal, computed, inject,
|
|
6
|
+
import { Injectable, InjectionToken, signal, computed, inject, input, output, ChangeDetectionStrategy, Component, model, ElementRef, afterNextRender, PLATFORM_ID, DestroyRef, Injector, runInInjectionContext, effect, viewChild, untracked, viewChildren, SecurityContext, ViewContainerRef, EventEmitter, HostListener, Directive, NgModule } from '@angular/core';
|
|
7
|
+
import { AXUploaderBrowseDirective, AXUploaderZoneDirective, AXUploaderService } from '@acorex/cdk/uploader';
|
|
7
8
|
import { AXDialogService } from '@acorex/components/dialog';
|
|
8
9
|
import { AXPopupService } from '@acorex/components/popup';
|
|
10
|
+
import { createFileTypeMetadata, AXFileTypeInfoProvider, resolveFileTypeExtension, runFileTypeUtility, formatFileSizeBytes, AXFileService, AXFileTypeRegistryService, runFileTypeUtilityForFile, provideFileValidationRules, provideFileTypeInfoProvider, getFileExtension, runFileTypeCopy, resolveFileCopyText, runFileTypeOpen } from '@acorex/core/file';
|
|
9
11
|
import * as i3 from '@acorex/core/translation';
|
|
10
12
|
import { translateSync, AXTranslationService, AXTranslationModule } from '@acorex/core/translation';
|
|
11
13
|
import { Subject, BehaviorSubject, Observable, filter, firstValueFrom, takeUntil, catchError, EMPTY } from 'rxjs';
|
|
12
|
-
import { AXUploaderBrowseDirective, AXUploaderZoneDirective, AXUploaderService } from '@acorex/cdk/uploader';
|
|
13
|
-
import { createFileTypeMetadata, AXFileTypeInfoProvider, resolveFileTypeExtension, runFileTypeUtility, formatFileSizeBytes, AXFileService, AXFileTypeRegistryService, runFileTypeUtilityForFile, provideFileValidationRules, provideFileTypeInfoProvider, getFileExtension, runFileTypeCopy, resolveFileCopyText, runFileTypeOpen } from '@acorex/core/file';
|
|
14
14
|
import * as i1$2 from '@acorex/components/progress-bar';
|
|
15
15
|
import { AXProgressBarModule } from '@acorex/components/progress-bar';
|
|
16
16
|
import { AXToastService } from '@acorex/components/toast';
|
|
@@ -260,163 +260,6 @@ class AXUserApi {
|
|
|
260
260
|
*/
|
|
261
261
|
// Shared types
|
|
262
262
|
|
|
263
|
-
/**
|
|
264
|
-
* Conversation Configuration Interface
|
|
265
|
-
* Centralized configuration values to avoid magic numbers
|
|
266
|
-
*/
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Default Configuration Values
|
|
270
|
-
* Centralized defaults to avoid magic numbers throughout the codebase
|
|
271
|
-
*/
|
|
272
|
-
/**
|
|
273
|
-
* Default conversation configuration
|
|
274
|
-
* All values are explicitly defined here for easy maintenance and documentation
|
|
275
|
-
*/
|
|
276
|
-
const AX_DEFAULT_CONVERSATION_CONFIG = {
|
|
277
|
-
// Pagination
|
|
278
|
-
messagePageSize: 30,
|
|
279
|
-
conversationPageSize: 20,
|
|
280
|
-
// Scroll Configuration
|
|
281
|
-
scrollThreshold: 100,
|
|
282
|
-
infiniteScrollThreshold: 200,
|
|
283
|
-
// Timeout Durations (milliseconds)
|
|
284
|
-
typingIndicatorTimeout: 3000,
|
|
285
|
-
typingIndicatorThrottle: 1000,
|
|
286
|
-
messageHighlightDuration: 2000,
|
|
287
|
-
debounceSearch: 300,
|
|
288
|
-
// Message Storage Limits
|
|
289
|
-
maxMessagesPerConversation: 1000,
|
|
290
|
-
maxTotalMessages: 10000,
|
|
291
|
-
maxCachedConversations: 50,
|
|
292
|
-
// UI Dimensions (pixels)
|
|
293
|
-
minSidebarWidth: 250,
|
|
294
|
-
maxSidebarWidth: 500,
|
|
295
|
-
defaultSidebarWidth: 320,
|
|
296
|
-
// Cache
|
|
297
|
-
filterCacheSize: 100,
|
|
298
|
-
// Message Validation
|
|
299
|
-
maxMessageLength: 10000,
|
|
300
|
-
minMessageLength: 1,
|
|
301
|
-
maxFilesPerMessage: 3,
|
|
302
|
-
// Intersection Observer
|
|
303
|
-
messageReadThreshold: 0.3,
|
|
304
|
-
// Message list
|
|
305
|
-
messageListBackground: '',
|
|
306
|
-
};
|
|
307
|
-
/**
|
|
308
|
-
* Helper function to merge user config with defaults
|
|
309
|
-
* Properly handles array merging to avoid reference issues
|
|
310
|
-
* @param userConfig - User-provided configuration
|
|
311
|
-
* @returns Merged configuration with all required fields
|
|
312
|
-
*/
|
|
313
|
-
function mergeWithDefaults(userConfig) {
|
|
314
|
-
if (!userConfig) {
|
|
315
|
-
return { ...AX_DEFAULT_CONVERSATION_CONFIG };
|
|
316
|
-
}
|
|
317
|
-
return {
|
|
318
|
-
...AX_DEFAULT_CONVERSATION_CONFIG,
|
|
319
|
-
...userConfig,
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Dependency Injection Tokens
|
|
325
|
-
* InjectionTokens for configuration and dependencies
|
|
326
|
-
*/
|
|
327
|
-
/**
|
|
328
|
-
* Configuration token for conversation component
|
|
329
|
-
* Uses centralized defaults from AX_DEFAULT_CONVERSATION_CONFIG
|
|
330
|
-
*/
|
|
331
|
-
const CONVERSATION_CONFIG = new InjectionToken('CONVERSATION_CONFIG', {
|
|
332
|
-
providedIn: 'root',
|
|
333
|
-
factory: () => mergeWithDefaults(),
|
|
334
|
-
});
|
|
335
|
-
/**
|
|
336
|
-
* Token for configuring AXErrorHandlerService
|
|
337
|
-
*/
|
|
338
|
-
const ERROR_HANDLER_CONFIG = new InjectionToken('ERROR_HANDLER_CONFIG', {
|
|
339
|
-
providedIn: 'root',
|
|
340
|
-
factory: () => ({}),
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Pluggable avatar components for the conversation UI.
|
|
345
|
-
* Register via `provideConversation({ avatarComponents: { user, conversation } })`.
|
|
346
|
-
*/
|
|
347
|
-
const AX_CONVERSATION_USER_AVATAR_COMPONENT = new InjectionToken('AX_CONVERSATION_USER_AVATAR_COMPONENT');
|
|
348
|
-
const AX_CONVERSATION_CONVERSATION_AVATAR_COMPONENT = new InjectionToken('AX_CONVERSATION_CONVERSATION_AVATAR_COMPONENT');
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Registry Configuration Tokens
|
|
352
|
-
* Injection tokens for configuring default registry values
|
|
353
|
-
*/
|
|
354
|
-
/**
|
|
355
|
-
* Additional message renderers configuration
|
|
356
|
-
* Provide this token to add custom message renderers in addition to built-in ones
|
|
357
|
-
* Note: Built-in renderers (text, system, fallback) are registered by default; other types are provided via plugins/constants.
|
|
358
|
-
*/
|
|
359
|
-
const DEFAULT_MESSAGE_RENDERERS = new InjectionToken('DEFAULT_MESSAGE_RENDERERS', {
|
|
360
|
-
providedIn: 'root',
|
|
361
|
-
factory: () => [],
|
|
362
|
-
});
|
|
363
|
-
/**
|
|
364
|
-
* Default message actions configuration
|
|
365
|
-
* Provide this token to override default message actions
|
|
366
|
-
*/
|
|
367
|
-
const DEFAULT_MESSAGE_ACTIONS = new InjectionToken('DEFAULT_MESSAGE_ACTIONS', {
|
|
368
|
-
providedIn: 'root',
|
|
369
|
-
factory: () => [],
|
|
370
|
-
});
|
|
371
|
-
/**
|
|
372
|
-
* Default composer tabs configuration
|
|
373
|
-
* Provide this token to override default composer tabs (emoji, stickers, etc.)
|
|
374
|
-
*/
|
|
375
|
-
const DEFAULT_COMPOSER_TABS = new InjectionToken('DEFAULT_COMPOSER_TABS', {
|
|
376
|
-
providedIn: 'root',
|
|
377
|
-
factory: () => [],
|
|
378
|
-
});
|
|
379
|
-
/**
|
|
380
|
-
* Default composer actions configuration
|
|
381
|
-
* Provide this token to override default composer actions (attach, voice, etc.)
|
|
382
|
-
*/
|
|
383
|
-
const DEFAULT_COMPOSER_ACTIONS = new InjectionToken('DEFAULT_COMPOSER_ACTIONS', {
|
|
384
|
-
providedIn: 'root',
|
|
385
|
-
factory: () => [],
|
|
386
|
-
});
|
|
387
|
-
/**
|
|
388
|
-
* Default conversation tabs configuration
|
|
389
|
-
* Provide this token to override default conversation tabs (all, private, groups, etc.)
|
|
390
|
-
*/
|
|
391
|
-
const DEFAULT_CONVERSATION_TABS = new InjectionToken('DEFAULT_CONVERSATION_TABS', {
|
|
392
|
-
providedIn: 'root',
|
|
393
|
-
factory: () => [],
|
|
394
|
-
});
|
|
395
|
-
/**
|
|
396
|
-
* Default info bar actions configuration
|
|
397
|
-
* Provide this token to override default info bar actions (mute, archive, block, etc.)
|
|
398
|
-
*/
|
|
399
|
-
const DEFAULT_INFO_BAR_ACTIONS = new InjectionToken('DEFAULT_INFO_BAR_ACTIONS', {
|
|
400
|
-
providedIn: 'root',
|
|
401
|
-
factory: () => [],
|
|
402
|
-
});
|
|
403
|
-
/**
|
|
404
|
-
* Default conversation item actions configuration
|
|
405
|
-
* Provide this token to override default conversation item actions (mute, delete, archive, etc.)
|
|
406
|
-
*/
|
|
407
|
-
const DEFAULT_CONVERSATION_ITEM_ACTIONS = new InjectionToken('DEFAULT_CONVERSATION_ITEM_ACTIONS', {
|
|
408
|
-
providedIn: 'root',
|
|
409
|
-
factory: () => [],
|
|
410
|
-
});
|
|
411
|
-
/**
|
|
412
|
-
* Complete registry configuration token
|
|
413
|
-
* Provide this for comprehensive registry configuration
|
|
414
|
-
*/
|
|
415
|
-
const REGISTRY_CONFIG = new InjectionToken('REGISTRY_CONFIG', {
|
|
416
|
-
providedIn: 'root',
|
|
417
|
-
factory: () => ({}),
|
|
418
|
-
});
|
|
419
|
-
|
|
420
263
|
/** Inline or session-only URLs that must not be stored on message payloads or sent to APIs. */
|
|
421
264
|
function isNonPersistableMediaUrl(url) {
|
|
422
265
|
if (!url) {
|
|
@@ -463,20 +306,22 @@ function str$3(value) {
|
|
|
463
306
|
function num$2(value, fallback = 0) {
|
|
464
307
|
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
465
308
|
}
|
|
466
|
-
function
|
|
309
|
+
function sanitizeImageItem(item) {
|
|
467
310
|
const url = resolvePersistableMediaUrl(item.url);
|
|
468
|
-
|
|
311
|
+
const thumbnailUrl = resolvePersistedThumbnailUrl(item.thumbnailUrl, url);
|
|
312
|
+
if (url === item.url && thumbnailUrl === item.thumbnailUrl) {
|
|
469
313
|
return item;
|
|
470
314
|
}
|
|
471
|
-
return { ...item, url };
|
|
315
|
+
return { ...item, url, thumbnailUrl };
|
|
472
316
|
}
|
|
473
|
-
|
|
317
|
+
/** Format image message payload for rendering (array shape, safe thumbnails). */
|
|
318
|
+
function normalizeImagePayload(payload) {
|
|
474
319
|
const raw = loose$3(payload);
|
|
475
|
-
const existing = raw['
|
|
320
|
+
const existing = raw['images'];
|
|
476
321
|
if (Array.isArray(existing) && existing.length > 0) {
|
|
477
322
|
return {
|
|
478
|
-
type: '
|
|
479
|
-
|
|
323
|
+
type: 'image',
|
|
324
|
+
images: existing.map(sanitizeImageItem),
|
|
480
325
|
caption: payload.caption,
|
|
481
326
|
};
|
|
482
327
|
}
|
|
@@ -484,24 +329,27 @@ function normalizeAudioPayload(payload) {
|
|
|
484
329
|
const mediaId = str$3(raw['mediaId']);
|
|
485
330
|
if (url || mediaId) {
|
|
486
331
|
return {
|
|
487
|
-
type: '
|
|
332
|
+
type: 'image',
|
|
488
333
|
caption: payload.caption,
|
|
489
|
-
|
|
334
|
+
images: [
|
|
490
335
|
{
|
|
491
336
|
...(url ? { url } : {}),
|
|
492
|
-
|
|
493
|
-
|
|
337
|
+
thumbnailUrl: resolvePersistedThumbnailUrl(str$3(raw['thumbnailUrl']), url),
|
|
338
|
+
width: num$2(raw['width']),
|
|
339
|
+
height: num$2(raw['height']),
|
|
494
340
|
mimeType: str$3(raw['mimeType']),
|
|
495
341
|
size: typeof raw['size'] === 'number' ? raw['size'] : undefined,
|
|
496
342
|
mediaId,
|
|
497
|
-
|
|
498
|
-
coverUrl: str$3(raw['coverUrl']),
|
|
499
|
-
waveform: Array.isArray(raw['waveform']) ? raw['waveform'] : undefined,
|
|
343
|
+
blurhash: str$3(raw['blurhash']),
|
|
500
344
|
},
|
|
501
345
|
],
|
|
502
346
|
};
|
|
503
347
|
}
|
|
504
|
-
return { type: '
|
|
348
|
+
return { type: 'image', images: [], caption: payload.caption };
|
|
349
|
+
}
|
|
350
|
+
/** Preferred URL for grid / lightbox (never inline base64 thumbnails). */
|
|
351
|
+
function resolveImageDisplayUrl(image) {
|
|
352
|
+
return resolvePersistedThumbnailUrl(image.thumbnailUrl, image.url) ?? image.url;
|
|
505
353
|
}
|
|
506
354
|
|
|
507
355
|
function loose$2(payload) {
|
|
@@ -510,21 +358,23 @@ function loose$2(payload) {
|
|
|
510
358
|
function str$2(value) {
|
|
511
359
|
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
512
360
|
}
|
|
513
|
-
function
|
|
361
|
+
function num$1(value, fallback = 0) {
|
|
362
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
363
|
+
}
|
|
364
|
+
function sanitizeAudioItem(item) {
|
|
514
365
|
const url = resolvePersistableMediaUrl(item.url);
|
|
515
|
-
|
|
516
|
-
if (url === item.url && thumbnailUrl === item.thumbnailUrl) {
|
|
366
|
+
if (url === item.url) {
|
|
517
367
|
return item;
|
|
518
368
|
}
|
|
519
|
-
return { ...item, url
|
|
369
|
+
return { ...item, url };
|
|
520
370
|
}
|
|
521
|
-
function
|
|
371
|
+
function normalizeAudioPayload(payload) {
|
|
522
372
|
const raw = loose$2(payload);
|
|
523
|
-
const existing = raw['
|
|
373
|
+
const existing = raw['audios'];
|
|
524
374
|
if (Array.isArray(existing) && existing.length > 0) {
|
|
525
375
|
return {
|
|
526
|
-
type: '
|
|
527
|
-
|
|
376
|
+
type: 'audio',
|
|
377
|
+
audios: existing.map(sanitizeAudioItem),
|
|
528
378
|
caption: payload.caption,
|
|
529
379
|
};
|
|
530
380
|
}
|
|
@@ -532,49 +382,207 @@ function normalizeFilePayload(payload) {
|
|
|
532
382
|
const mediaId = str$2(raw['mediaId']);
|
|
533
383
|
if (url || mediaId) {
|
|
534
384
|
return {
|
|
535
|
-
type: '
|
|
385
|
+
type: 'audio',
|
|
536
386
|
caption: payload.caption,
|
|
537
|
-
|
|
387
|
+
audios: [
|
|
538
388
|
{
|
|
539
389
|
...(url ? { url } : {}),
|
|
540
|
-
|
|
541
|
-
|
|
390
|
+
title: str$2(raw['title']),
|
|
391
|
+
duration: num$1(raw['duration']),
|
|
392
|
+
mimeType: str$2(raw['mimeType']),
|
|
542
393
|
size: typeof raw['size'] === 'number' ? raw['size'] : undefined,
|
|
543
394
|
mediaId,
|
|
544
|
-
|
|
545
|
-
|
|
395
|
+
artist: str$2(raw['artist']),
|
|
396
|
+
coverUrl: str$2(raw['coverUrl']),
|
|
397
|
+
waveform: Array.isArray(raw['waveform']) ? raw['waveform'] : undefined,
|
|
546
398
|
},
|
|
547
399
|
],
|
|
548
400
|
};
|
|
549
401
|
}
|
|
550
|
-
return { type: '
|
|
402
|
+
return { type: 'audio', audios: [], caption: payload.caption };
|
|
551
403
|
}
|
|
552
404
|
|
|
553
|
-
function
|
|
554
|
-
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
|
|
405
|
+
function formatFileSize(bytes) {
|
|
406
|
+
if (bytes < 1024)
|
|
407
|
+
return `${bytes} B`;
|
|
408
|
+
if (bytes < 1024 * 1024)
|
|
409
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
410
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
411
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
412
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
558
413
|
}
|
|
559
|
-
function
|
|
560
|
-
|
|
414
|
+
function formatDuration(seconds) {
|
|
415
|
+
const mins = Math.floor(seconds / 60);
|
|
416
|
+
const secs = Math.floor(seconds % 60);
|
|
417
|
+
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
561
418
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
419
|
+
|
|
420
|
+
const CONVERSATION_AUDIO_CATALOG = 'conversation-audio';
|
|
421
|
+
const MB$4 = 1024 * 1024;
|
|
422
|
+
const CONVERSATION_AUDIO_PRESENTATION = {
|
|
423
|
+
icon: 'fa-light fa-music text-amber-500',
|
|
424
|
+
title: 'Audio',
|
|
425
|
+
};
|
|
426
|
+
const AUDIO_UTILITY = {
|
|
427
|
+
preview: 'preview',
|
|
428
|
+
duration: 'duration',
|
|
429
|
+
formatSize: 'formatSize',
|
|
430
|
+
formatDuration: 'formatDuration',
|
|
431
|
+
createLocalPreviewUrl: 'createLocalPreviewUrl',
|
|
432
|
+
};
|
|
433
|
+
function blobUrl$4(ctx, blob) {
|
|
434
|
+
if (!isPlatformBrowser(ctx.platformId)) {
|
|
435
|
+
return '';
|
|
567
436
|
}
|
|
568
|
-
return
|
|
437
|
+
return URL.createObjectURL(blob);
|
|
569
438
|
}
|
|
570
|
-
|
|
571
|
-
|
|
439
|
+
function mediaDuration$2(ctx, file) {
|
|
440
|
+
if (!isPlatformBrowser(ctx.platformId)) {
|
|
441
|
+
return Promise.resolve(0);
|
|
442
|
+
}
|
|
443
|
+
return new Promise((resolve, reject) => {
|
|
444
|
+
const el = document.createElement('audio');
|
|
445
|
+
el.preload = 'metadata';
|
|
446
|
+
el.onloadedmetadata = () => {
|
|
447
|
+
URL.revokeObjectURL(el.src);
|
|
448
|
+
resolve(el.duration);
|
|
449
|
+
};
|
|
450
|
+
el.onerror = () => {
|
|
451
|
+
URL.revokeObjectURL(el.src);
|
|
452
|
+
reject(new Error('Failed to load audio metadata'));
|
|
453
|
+
};
|
|
454
|
+
el.src = URL.createObjectURL(file);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
function conversationAudioUtilities() {
|
|
458
|
+
return {
|
|
459
|
+
[AUDIO_UTILITY.preview]: (ctx, file) => ctx.readAsDataUrl(file),
|
|
460
|
+
[AUDIO_UTILITY.duration]: (ctx, file) => mediaDuration$2(ctx, file),
|
|
461
|
+
[AUDIO_UTILITY.formatSize]: (_ctx, bytes) => formatFileSize(bytes),
|
|
462
|
+
[AUDIO_UTILITY.formatDuration]: (_ctx, seconds) => formatDuration(seconds),
|
|
463
|
+
[AUDIO_UTILITY.createLocalPreviewUrl]: (ctx, source) => blobUrl$4(ctx, source),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function createConversationAudioFileType() {
|
|
467
|
+
const presentation = CONVERSATION_AUDIO_PRESENTATION;
|
|
468
|
+
return {
|
|
469
|
+
name: CONVERSATION_AUDIO_CATALOG,
|
|
470
|
+
metadata: createFileTypeMetadata('conversation'),
|
|
471
|
+
title: presentation.title,
|
|
472
|
+
icon: presentation.icon,
|
|
473
|
+
validations: {
|
|
474
|
+
mimeTypes: ['audio/*'],
|
|
475
|
+
minSize: 1,
|
|
476
|
+
maxSize: 50 * MB$4,
|
|
477
|
+
},
|
|
478
|
+
extensions: [
|
|
479
|
+
{ name: 'mp3', title: 'MP3' },
|
|
480
|
+
{ name: 'wav', title: 'WAV', validations: { maxSize: 30 * MB$4 } },
|
|
481
|
+
{ name: 'ogg', title: 'OGG' },
|
|
482
|
+
{ name: 'm4a', title: 'M4A' },
|
|
483
|
+
],
|
|
484
|
+
utilities: conversationAudioUtilities(),
|
|
485
|
+
copy: (payload) => {
|
|
486
|
+
const audio = normalizeAudioPayload(payload);
|
|
487
|
+
const caption = audio.caption?.trim();
|
|
488
|
+
const items = audio.audios.map((item) => ({
|
|
489
|
+
url: item.url?.trim(),
|
|
490
|
+
title: item.title?.trim(),
|
|
491
|
+
}));
|
|
492
|
+
return {
|
|
493
|
+
text: caption ?? '',
|
|
494
|
+
meta: { kind: 'audio', caption, items, count: audio.audios.length },
|
|
495
|
+
};
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
class AXConversationAudioFileTypeProvider extends AXFileTypeInfoProvider {
|
|
500
|
+
items() {
|
|
501
|
+
return Promise.resolve([createConversationAudioFileType()]);
|
|
502
|
+
}
|
|
503
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationAudioFileTypeProvider, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
504
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationAudioFileTypeProvider, providedIn: 'root' }); }
|
|
505
|
+
}
|
|
506
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationAudioFileTypeProvider, decorators: [{
|
|
507
|
+
type: Injectable,
|
|
508
|
+
args: [{ providedIn: 'root' }]
|
|
509
|
+
}] });
|
|
510
|
+
|
|
511
|
+
function audioItemFromUpload(file, result, duration) {
|
|
512
|
+
const url = resolvePersistableMediaUrl(result.url);
|
|
513
|
+
const item = {
|
|
514
|
+
mediaId: result.mediaId,
|
|
515
|
+
mimeType: result.mimeType,
|
|
516
|
+
size: result.size,
|
|
517
|
+
duration,
|
|
518
|
+
title: file.name,
|
|
519
|
+
metadata: result.metadata,
|
|
520
|
+
};
|
|
521
|
+
if (url) {
|
|
522
|
+
item.url = url;
|
|
523
|
+
}
|
|
524
|
+
return item;
|
|
525
|
+
}
|
|
526
|
+
function mergeAudioUploadResult(payload, result) {
|
|
527
|
+
const base = normalizeAudioPayload(payload);
|
|
528
|
+
const audios = [...base.audios];
|
|
529
|
+
const i = audios.findIndex((x) => !x.mediaId && !resolvePersistableMediaUrl(x.url));
|
|
530
|
+
const slot = i >= 0 ? audios[i] : undefined;
|
|
531
|
+
const url = resolvePersistableMediaUrl(result.url);
|
|
532
|
+
const next = {
|
|
533
|
+
...(slot ?? { duration: 0 }),
|
|
534
|
+
mediaId: result.mediaId,
|
|
535
|
+
mimeType: result.mimeType,
|
|
536
|
+
size: result.size,
|
|
537
|
+
metadata: result.metadata,
|
|
538
|
+
};
|
|
539
|
+
if (url) {
|
|
540
|
+
next.url = url;
|
|
541
|
+
}
|
|
542
|
+
if (i >= 0)
|
|
543
|
+
audios[i] = next;
|
|
544
|
+
else
|
|
545
|
+
audios.push(next);
|
|
546
|
+
return { ...base, type: 'audio', audios };
|
|
547
|
+
}
|
|
548
|
+
function applyAudioLocalPreview(payload, localUrl, mimeType = 'application/octet-stream') {
|
|
549
|
+
const base = normalizeAudioPayload(payload);
|
|
550
|
+
const audios = [...base.audios];
|
|
551
|
+
const slot = audios.findIndex((x) => !x.mediaId && !resolvePersistableMediaUrl(x.url));
|
|
552
|
+
const preview = { url: localUrl, duration: 0, mimeType };
|
|
553
|
+
if (slot >= 0) {
|
|
554
|
+
audios[slot] = { ...audios[slot], ...preview };
|
|
555
|
+
}
|
|
556
|
+
else if (audios.length === 0) {
|
|
557
|
+
audios.push(preview);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
audios[0] = { ...audios[0], ...preview };
|
|
561
|
+
}
|
|
562
|
+
return { ...base, type: 'audio', audios };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function loose$1(payload) {
|
|
566
|
+
return payload;
|
|
567
|
+
}
|
|
568
|
+
function str$1(value) {
|
|
569
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
570
|
+
}
|
|
571
|
+
function sanitizeFileItem(item) {
|
|
572
|
+
const url = resolvePersistableMediaUrl(item.url);
|
|
573
|
+
const thumbnailUrl = resolvePersistedThumbnailUrl(item.thumbnailUrl, url);
|
|
574
|
+
if (url === item.url && thumbnailUrl === item.thumbnailUrl) {
|
|
575
|
+
return item;
|
|
576
|
+
}
|
|
577
|
+
return { ...item, url, thumbnailUrl };
|
|
578
|
+
}
|
|
579
|
+
function normalizeFilePayload(payload) {
|
|
572
580
|
const raw = loose$1(payload);
|
|
573
|
-
const existing = raw['
|
|
581
|
+
const existing = raw['files'];
|
|
574
582
|
if (Array.isArray(existing) && existing.length > 0) {
|
|
575
583
|
return {
|
|
576
|
-
type: '
|
|
577
|
-
|
|
584
|
+
type: 'file',
|
|
585
|
+
files: existing.map(sanitizeFileItem),
|
|
578
586
|
caption: payload.caption,
|
|
579
587
|
};
|
|
580
588
|
}
|
|
@@ -582,28 +590,99 @@ function normalizeImagePayload(payload) {
|
|
|
582
590
|
const mediaId = str$1(raw['mediaId']);
|
|
583
591
|
if (url || mediaId) {
|
|
584
592
|
return {
|
|
585
|
-
type: '
|
|
593
|
+
type: 'file',
|
|
586
594
|
caption: payload.caption,
|
|
587
|
-
|
|
595
|
+
files: [
|
|
588
596
|
{
|
|
589
597
|
...(url ? { url } : {}),
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
height: num$1(raw['height']),
|
|
593
|
-
mimeType: str$1(raw['mimeType']),
|
|
598
|
+
name: str$1(raw['name']) ?? 'file',
|
|
599
|
+
mimeType: str$1(raw['mimeType']) ?? 'application/octet-stream',
|
|
594
600
|
size: typeof raw['size'] === 'number' ? raw['size'] : undefined,
|
|
595
601
|
mediaId,
|
|
596
|
-
|
|
602
|
+
thumbnailUrl: resolvePersistedThumbnailUrl(str$1(raw['thumbnailUrl']), url),
|
|
603
|
+
extension: str$1(raw['extension']),
|
|
597
604
|
},
|
|
598
605
|
],
|
|
599
606
|
};
|
|
600
607
|
}
|
|
601
|
-
return { type: '
|
|
608
|
+
return { type: 'file', files: [], caption: payload.caption };
|
|
602
609
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
610
|
+
|
|
611
|
+
const CONVERSATION_IMAGE_CATALOG = 'conversation-image';
|
|
612
|
+
const MB$3 = 1024 * 1024;
|
|
613
|
+
const CONVERSATION_IMAGE_PRESENTATION = {
|
|
614
|
+
icon: 'fa-light fa-image text-purple-500',
|
|
615
|
+
title: 'Image',
|
|
616
|
+
};
|
|
617
|
+
const IMAGE_UTILITY = {
|
|
618
|
+
preview: 'preview',
|
|
619
|
+
formatSize: 'formatSize',
|
|
620
|
+
createLocalPreviewUrl: 'createLocalPreviewUrl',
|
|
621
|
+
};
|
|
622
|
+
function blobUrl$3(ctx, blob) {
|
|
623
|
+
if (!isPlatformBrowser(ctx.platformId)) {
|
|
624
|
+
return '';
|
|
625
|
+
}
|
|
626
|
+
return URL.createObjectURL(blob);
|
|
627
|
+
}
|
|
628
|
+
function conversationImageUtilities() {
|
|
629
|
+
return {
|
|
630
|
+
[IMAGE_UTILITY.preview]: (ctx, file) => ctx.readAsDataUrl(file),
|
|
631
|
+
[IMAGE_UTILITY.formatSize]: (_ctx, bytes) => formatFileSize(bytes),
|
|
632
|
+
[IMAGE_UTILITY.createLocalPreviewUrl]: (ctx, source) => {
|
|
633
|
+
const blob = source;
|
|
634
|
+
if (blob.type.startsWith('image/')) {
|
|
635
|
+
return ctx.readAsDataUrl(blob);
|
|
636
|
+
}
|
|
637
|
+
return blobUrl$3(ctx, blob);
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function createConversationImageFileType() {
|
|
642
|
+
const presentation = CONVERSATION_IMAGE_PRESENTATION;
|
|
643
|
+
return {
|
|
644
|
+
name: CONVERSATION_IMAGE_CATALOG,
|
|
645
|
+
metadata: createFileTypeMetadata('conversation'),
|
|
646
|
+
title: presentation.title,
|
|
647
|
+
icon: presentation.icon,
|
|
648
|
+
validations: {
|
|
649
|
+
mimeTypes: ['image/*'],
|
|
650
|
+
minSize: 1,
|
|
651
|
+
maxSize: 100 * MB$3,
|
|
652
|
+
},
|
|
653
|
+
extensions: [
|
|
654
|
+
{ name: 'jpg', title: 'JPEG' },
|
|
655
|
+
{ name: 'jpeg', title: 'JPEG', validations: { maxSize: 5 * MB$3 } },
|
|
656
|
+
{ name: 'png', title: 'PNG' },
|
|
657
|
+
{ name: 'gif', title: 'GIF' },
|
|
658
|
+
{ name: 'webp', title: 'WebP' },
|
|
659
|
+
{ name: 'svg', title: 'SVG', validations: { maxSize: 2 * MB$3 } },
|
|
660
|
+
],
|
|
661
|
+
utilities: conversationImageUtilities(),
|
|
662
|
+
copy: (payload) => {
|
|
663
|
+
const image = normalizeImagePayload(payload);
|
|
664
|
+
const caption = image.caption?.trim();
|
|
665
|
+
const urls = image.images
|
|
666
|
+
.map((item) => item.url?.trim() || item.thumbnailUrl?.trim())
|
|
667
|
+
.filter((url) => !!url);
|
|
668
|
+
return {
|
|
669
|
+
text: caption ?? '',
|
|
670
|
+
meta: { kind: 'image', caption, urls, count: image.images.length },
|
|
671
|
+
};
|
|
672
|
+
},
|
|
673
|
+
};
|
|
606
674
|
}
|
|
675
|
+
class AXConversationImageFileTypeProvider extends AXFileTypeInfoProvider {
|
|
676
|
+
items() {
|
|
677
|
+
return Promise.resolve([createConversationImageFileType()]);
|
|
678
|
+
}
|
|
679
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationImageFileTypeProvider, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
680
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationImageFileTypeProvider, providedIn: 'root' }); }
|
|
681
|
+
}
|
|
682
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationImageFileTypeProvider, decorators: [{
|
|
683
|
+
type: Injectable,
|
|
684
|
+
args: [{ providedIn: 'root' }]
|
|
685
|
+
}] });
|
|
607
686
|
|
|
608
687
|
function loose(payload) {
|
|
609
688
|
return payload;
|
|
@@ -655,2173 +734,2097 @@ function normalizeVideoPayload(payload) {
|
|
|
655
734
|
return { type: 'video', videos: [], caption: payload.caption };
|
|
656
735
|
}
|
|
657
736
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
* Validation Utilities
|
|
675
|
-
* Centralized validation functions for messages and user input
|
|
676
|
-
*/
|
|
677
|
-
/**
|
|
678
|
-
* Validate message text content
|
|
679
|
-
* @param text - Text to validate
|
|
680
|
-
* @param config - Configuration for validation rules
|
|
681
|
-
* @returns Validation result
|
|
682
|
-
*/
|
|
683
|
-
function validateMessageText(text, config) {
|
|
684
|
-
// Check for empty text
|
|
685
|
-
if (!text || text.trim().length === 0) {
|
|
686
|
-
return {
|
|
687
|
-
valid: false,
|
|
688
|
-
error: 'Message text cannot be empty',
|
|
689
|
-
errorCode: 'EMPTY_MESSAGE',
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
// Check minimum length
|
|
693
|
-
const minLength = config.minMessageLength ?? 1;
|
|
694
|
-
if (text.trim().length < minLength) {
|
|
695
|
-
return {
|
|
696
|
-
valid: false,
|
|
697
|
-
error: `Message must be at least ${minLength} character(s)`,
|
|
698
|
-
errorCode: 'MESSAGE_TOO_SHORT',
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
// Check maximum length
|
|
702
|
-
const maxLength = config.maxMessageLength ?? 10000;
|
|
703
|
-
if (text.length > maxLength) {
|
|
704
|
-
return {
|
|
705
|
-
valid: false,
|
|
706
|
-
error: `Message exceeds ${maxLength} character limit`,
|
|
707
|
-
errorCode: 'MESSAGE_TOO_LONG',
|
|
708
|
-
};
|
|
737
|
+
const CONVERSATION_VIDEO_CATALOG = 'conversation-video';
|
|
738
|
+
const MB$2 = 1024 * 1024;
|
|
739
|
+
const CONVERSATION_VIDEO_PRESENTATION = {
|
|
740
|
+
icon: 'fa-light fa-video ax-text-blue-500',
|
|
741
|
+
title: 'Video',
|
|
742
|
+
};
|
|
743
|
+
const VIDEO_UTILITY = {
|
|
744
|
+
preview: 'preview',
|
|
745
|
+
duration: 'duration',
|
|
746
|
+
formatSize: 'formatSize',
|
|
747
|
+
formatDuration: 'formatDuration',
|
|
748
|
+
createLocalPreviewUrl: 'createLocalPreviewUrl',
|
|
749
|
+
};
|
|
750
|
+
function blobUrl$2(ctx, blob) {
|
|
751
|
+
if (!isPlatformBrowser(ctx.platformId)) {
|
|
752
|
+
return '';
|
|
709
753
|
}
|
|
710
|
-
return
|
|
754
|
+
return URL.createObjectURL(blob);
|
|
711
755
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
* @returns Validation result
|
|
716
|
-
*/
|
|
717
|
-
function validateConversationId(conversationId) {
|
|
718
|
-
if (!conversationId || typeof conversationId !== 'string' || conversationId.trim().length === 0) {
|
|
719
|
-
return {
|
|
720
|
-
valid: false,
|
|
721
|
-
error: 'Conversation ID is required',
|
|
722
|
-
errorCode: 'MISSING_CONVERSATION_ID',
|
|
723
|
-
};
|
|
756
|
+
function mediaDuration$1(ctx, file, tag) {
|
|
757
|
+
if (!isPlatformBrowser(ctx.platformId)) {
|
|
758
|
+
return Promise.resolve(0);
|
|
724
759
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
760
|
+
return new Promise((resolve, reject) => {
|
|
761
|
+
const el = document.createElement(tag);
|
|
762
|
+
el.preload = 'metadata';
|
|
763
|
+
el.onloadedmetadata = () => {
|
|
764
|
+
URL.revokeObjectURL(el.src);
|
|
765
|
+
resolve(el.duration);
|
|
731
766
|
};
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
735
|
-
/**
|
|
736
|
-
* Validate message type
|
|
737
|
-
* @param type - Message type to validate
|
|
738
|
-
* @returns Validation result
|
|
739
|
-
*/
|
|
740
|
-
function validateMessageType(type) {
|
|
741
|
-
if (!type || type.trim().length === 0) {
|
|
742
|
-
return {
|
|
743
|
-
valid: false,
|
|
744
|
-
error: 'Message type is required',
|
|
745
|
-
errorCode: 'MISSING_MESSAGE_TYPE',
|
|
767
|
+
el.onerror = () => {
|
|
768
|
+
URL.revokeObjectURL(el.src);
|
|
769
|
+
reject(new Error(`Failed to load ${tag} metadata`));
|
|
746
770
|
};
|
|
747
|
-
|
|
748
|
-
|
|
771
|
+
el.src = URL.createObjectURL(file);
|
|
772
|
+
});
|
|
749
773
|
}
|
|
750
|
-
function
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
774
|
+
function conversationVideoUtilities() {
|
|
775
|
+
return {
|
|
776
|
+
[VIDEO_UTILITY.preview]: (ctx, file) => ctx.readAsDataUrl(file),
|
|
777
|
+
[VIDEO_UTILITY.duration]: (ctx, file) => mediaDuration$1(ctx, file, 'video'),
|
|
778
|
+
[VIDEO_UTILITY.formatSize]: (_ctx, bytes) => formatFileSize(bytes),
|
|
779
|
+
[VIDEO_UTILITY.formatDuration]: (_ctx, seconds) => formatDuration(seconds),
|
|
780
|
+
[VIDEO_UTILITY.createLocalPreviewUrl]: async (ctx, source) => {
|
|
781
|
+
const blob = source;
|
|
782
|
+
if (blob.type.startsWith('image/')) {
|
|
783
|
+
return ctx.readAsDataUrl(blob);
|
|
784
|
+
}
|
|
785
|
+
return blobUrl$2(ctx, blob);
|
|
786
|
+
},
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
function createConversationVideoFileType() {
|
|
790
|
+
const presentation = CONVERSATION_VIDEO_PRESENTATION;
|
|
791
|
+
return {
|
|
792
|
+
name: CONVERSATION_VIDEO_CATALOG,
|
|
793
|
+
metadata: createFileTypeMetadata('conversation'),
|
|
794
|
+
title: presentation.title,
|
|
795
|
+
icon: presentation.icon,
|
|
796
|
+
validations: {
|
|
797
|
+
mimeTypes: ['video/*'],
|
|
798
|
+
minSize: 1,
|
|
799
|
+
maxSize: 500 * MB$2,
|
|
800
|
+
},
|
|
801
|
+
extensions: [
|
|
802
|
+
{ name: 'mp4', title: 'MP4' },
|
|
803
|
+
{ name: 'webm', title: 'WebM', validations: { maxSize: 200 * MB$2 } },
|
|
804
|
+
{ name: 'ogg', title: 'OGG' },
|
|
805
|
+
],
|
|
806
|
+
utilities: conversationVideoUtilities(),
|
|
807
|
+
copy: (payload) => {
|
|
808
|
+
const video = normalizeVideoPayload(payload);
|
|
809
|
+
const caption = video.caption?.trim();
|
|
810
|
+
const urls = video.videos.map((item) => item.url?.trim()).filter((url) => !!url);
|
|
762
811
|
return {
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
errorCode: 'INVALID_MEDIA_PAYLOAD',
|
|
812
|
+
text: caption ?? '',
|
|
813
|
+
meta: { kind: 'video', caption, urls, count: video.videos.length },
|
|
766
814
|
};
|
|
767
|
-
}
|
|
815
|
+
},
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
class AXConversationVideoFileTypeProvider extends AXFileTypeInfoProvider {
|
|
819
|
+
items() {
|
|
820
|
+
return Promise.resolve([createConversationVideoFileType()]);
|
|
768
821
|
}
|
|
769
|
-
|
|
822
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationVideoFileTypeProvider, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
823
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationVideoFileTypeProvider, providedIn: 'root' }); }
|
|
770
824
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
825
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationVideoFileTypeProvider, decorators: [{
|
|
826
|
+
type: Injectable,
|
|
827
|
+
args: [{ providedIn: 'root' }]
|
|
828
|
+
}] });
|
|
829
|
+
|
|
830
|
+
const CONVERSATION_FILE_CATALOG = 'conversation-file';
|
|
831
|
+
const MB$1 = 1024 * 1024;
|
|
832
|
+
const CONVERSATION_FILE_PRESENTATION = {
|
|
833
|
+
icon: 'fa-light fa-file text-neutral-500',
|
|
834
|
+
title: 'File',
|
|
835
|
+
};
|
|
836
|
+
const CONVERSATION_FILE_ALLOWED_MIME_TYPES = [
|
|
837
|
+
'image/*',
|
|
838
|
+
'video/*',
|
|
839
|
+
'audio/*',
|
|
840
|
+
'application/pdf',
|
|
841
|
+
'application/msword',
|
|
842
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
843
|
+
'application/vnd.ms-excel',
|
|
844
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
845
|
+
'application/zip',
|
|
846
|
+
'application/x-zip-compressed',
|
|
847
|
+
'application/x-7z-compressed',
|
|
848
|
+
'application/vnd.rar',
|
|
849
|
+
'application/octet-stream',
|
|
850
|
+
'text/plain',
|
|
851
|
+
];
|
|
852
|
+
const FILE_UTILITY = {
|
|
853
|
+
preview: 'preview',
|
|
854
|
+
formatSize: 'formatSize',
|
|
855
|
+
createLocalPreviewUrl: 'createLocalPreviewUrl',
|
|
856
|
+
pickerCatalog: 'pickerCatalog',
|
|
857
|
+
};
|
|
858
|
+
function blobUrl$1(ctx, blob) {
|
|
859
|
+
if (!isPlatformBrowser(ctx.platformId)) {
|
|
860
|
+
return '';
|
|
784
861
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
if (
|
|
792
|
-
return
|
|
793
|
-
valid: false,
|
|
794
|
-
error: 'Text message must have a text property',
|
|
795
|
-
errorCode: 'INVALID_TEXT_PAYLOAD',
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
break;
|
|
799
|
-
case 'image':
|
|
800
|
-
return validateMediaItems(normalized.images, 'Image');
|
|
801
|
-
case 'video':
|
|
802
|
-
return validateMediaItems(normalized.videos, 'Video');
|
|
803
|
-
case 'audio':
|
|
804
|
-
return validateMediaItems(normalized.audios, 'Audio');
|
|
805
|
-
case 'file':
|
|
806
|
-
return validateMediaItems(normalized.files, 'File');
|
|
807
|
-
case 'voice':
|
|
808
|
-
case 'sticker': {
|
|
809
|
-
const media = payload;
|
|
810
|
-
const hasUrl = typeof media.url === 'string' && media.url.length > 0;
|
|
811
|
-
const hasMediaId = typeof media.mediaId === 'string' && media.mediaId.length > 0;
|
|
812
|
-
if (!hasUrl && !hasMediaId) {
|
|
813
|
-
return {
|
|
814
|
-
valid: false,
|
|
815
|
-
error: `${type} message must have a url or mediaId`,
|
|
816
|
-
errorCode: 'INVALID_MEDIA_PAYLOAD',
|
|
817
|
-
};
|
|
862
|
+
return URL.createObjectURL(blob);
|
|
863
|
+
}
|
|
864
|
+
function conversationFileUtilities() {
|
|
865
|
+
return {
|
|
866
|
+
[FILE_UTILITY.preview]: async (ctx, file) => {
|
|
867
|
+
const f = file;
|
|
868
|
+
if (f.type.startsWith('image/')) {
|
|
869
|
+
return ctx.readAsDataUrl(f);
|
|
818
870
|
}
|
|
819
|
-
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
return
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
}
|
|
834
|
-
return { valid: true };
|
|
871
|
+
return undefined;
|
|
872
|
+
},
|
|
873
|
+
[FILE_UTILITY.formatSize]: (_ctx, bytes) => formatFileSize(bytes),
|
|
874
|
+
[FILE_UTILITY.createLocalPreviewUrl]: (ctx, source) => blobUrl$1(ctx, source),
|
|
875
|
+
[FILE_UTILITY.pickerCatalog]: (_ctx, file) => {
|
|
876
|
+
const f = file;
|
|
877
|
+
if (f.type.startsWith('image/'))
|
|
878
|
+
return CONVERSATION_IMAGE_CATALOG;
|
|
879
|
+
if (f.type.startsWith('video/'))
|
|
880
|
+
return CONVERSATION_VIDEO_CATALOG;
|
|
881
|
+
if (f.type.startsWith('audio/'))
|
|
882
|
+
return CONVERSATION_AUDIO_CATALOG;
|
|
883
|
+
return CONVERSATION_FILE_CATALOG;
|
|
884
|
+
},
|
|
885
|
+
};
|
|
835
886
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}
|
|
887
|
+
function createConversationFileFileType() {
|
|
888
|
+
const presentation = CONVERSATION_FILE_PRESENTATION;
|
|
889
|
+
return {
|
|
890
|
+
name: CONVERSATION_FILE_CATALOG,
|
|
891
|
+
metadata: createFileTypeMetadata('conversation'),
|
|
892
|
+
title: presentation.title,
|
|
893
|
+
icon: presentation.icon,
|
|
894
|
+
validations: {
|
|
895
|
+
mimeTypes: [...CONVERSATION_FILE_ALLOWED_MIME_TYPES],
|
|
896
|
+
minSize: 1,
|
|
897
|
+
maxSize: 100 * MB$1,
|
|
898
|
+
},
|
|
899
|
+
extensions: [
|
|
900
|
+
{ name: 'pdf', title: 'PDF', validations: { mimeTypes: ['application/pdf'], maxSize: 25 * MB$1 } },
|
|
901
|
+
{ name: 'doc', title: 'Word' },
|
|
902
|
+
{ name: 'docx', title: 'Word' },
|
|
903
|
+
{ name: 'txt', title: 'Text', validations: { mimeTypes: ['text/plain'], maxSize: 5 * MB$1 } },
|
|
904
|
+
{
|
|
905
|
+
name: 'zip',
|
|
906
|
+
title: 'ZIP',
|
|
907
|
+
validations: {
|
|
908
|
+
mimeTypes: ['application/zip', 'application/x-zip-compressed', 'application/octet-stream'],
|
|
909
|
+
maxSize: 50 * MB$1,
|
|
910
|
+
},
|
|
911
|
+
},
|
|
912
|
+
],
|
|
913
|
+
utilities: conversationFileUtilities(),
|
|
914
|
+
copy: (payload) => {
|
|
915
|
+
const file = normalizeFilePayload(payload);
|
|
916
|
+
const caption = file.caption?.trim();
|
|
917
|
+
const items = file.files.map((item) => {
|
|
918
|
+
const name = item.name?.trim();
|
|
919
|
+
const url = item.url?.trim();
|
|
920
|
+
const line = name && url ? `${name} — ${url}` : url || name;
|
|
921
|
+
return { name, url, line };
|
|
922
|
+
});
|
|
923
|
+
return {
|
|
924
|
+
text: caption ?? '',
|
|
925
|
+
meta: { kind: 'file', caption, items, count: file.files.length },
|
|
926
|
+
};
|
|
927
|
+
},
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
class AXConversationFileFileTypeProvider extends AXFileTypeInfoProvider {
|
|
931
|
+
items() {
|
|
932
|
+
return Promise.resolve([createConversationFileFileType()]);
|
|
848
933
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
};
|
|
934
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationFileFileTypeProvider, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
935
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationFileFileTypeProvider, providedIn: 'root' }); }
|
|
936
|
+
}
|
|
937
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationFileFileTypeProvider, decorators: [{
|
|
938
|
+
type: Injectable,
|
|
939
|
+
args: [{ providedIn: 'root' }]
|
|
940
|
+
}] });
|
|
941
|
+
|
|
942
|
+
function fileItemFromUpload(file, result) {
|
|
943
|
+
const extension = file.name.includes('.') ? file.name.split('.').pop() : undefined;
|
|
944
|
+
const url = resolvePersistableMediaUrl(result.url);
|
|
945
|
+
const item = {
|
|
946
|
+
mediaId: result.mediaId,
|
|
947
|
+
mimeType: result.mimeType,
|
|
948
|
+
size: result.size,
|
|
949
|
+
name: file.name,
|
|
950
|
+
thumbnailUrl: resolvePersistedThumbnailUrl(result.thumbnailUrl, url),
|
|
951
|
+
extension,
|
|
952
|
+
metadata: result.metadata,
|
|
953
|
+
};
|
|
954
|
+
if (url) {
|
|
955
|
+
item.url = url;
|
|
856
956
|
}
|
|
857
|
-
return
|
|
957
|
+
return item;
|
|
858
958
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
959
|
+
function mergeFileUploadResult(payload, result) {
|
|
960
|
+
const base = normalizeFilePayload(payload);
|
|
961
|
+
const files = [...base.files];
|
|
962
|
+
const i = files.findIndex((x) => !x.mediaId && !resolvePersistableMediaUrl(x.url));
|
|
963
|
+
const slot = i >= 0 ? files[i] : undefined;
|
|
964
|
+
const name = slot?.name ?? result.metadata?.['fileName'] ?? 'file';
|
|
965
|
+
const url = resolvePersistableMediaUrl(result.url);
|
|
966
|
+
const next = {
|
|
967
|
+
...(slot ?? { name, mimeType: result.mimeType }),
|
|
968
|
+
mediaId: result.mediaId,
|
|
969
|
+
mimeType: result.mimeType,
|
|
970
|
+
size: result.size,
|
|
971
|
+
name,
|
|
972
|
+
thumbnailUrl: resolvePersistedThumbnailUrl(result.thumbnailUrl ?? slot?.thumbnailUrl, url),
|
|
973
|
+
metadata: result.metadata,
|
|
974
|
+
};
|
|
975
|
+
if (url) {
|
|
976
|
+
next.url = url;
|
|
873
977
|
}
|
|
874
|
-
if (
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
978
|
+
if (i >= 0)
|
|
979
|
+
files[i] = next;
|
|
980
|
+
else
|
|
981
|
+
files.push(next);
|
|
982
|
+
return { ...base, type: 'file', files };
|
|
983
|
+
}
|
|
984
|
+
function applyFileLocalPreview(payload, localUrl, mimeType = 'application/octet-stream') {
|
|
985
|
+
const base = normalizeFilePayload(payload);
|
|
986
|
+
const files = [...base.files];
|
|
987
|
+
const slot = files.findIndex((x) => !x.mediaId && !resolvePersistableMediaUrl(x.url));
|
|
988
|
+
const preview = { url: localUrl, name: 'upload', mimeType };
|
|
989
|
+
if (slot >= 0) {
|
|
990
|
+
files[slot] = { ...files[slot], ...preview };
|
|
880
991
|
}
|
|
881
|
-
if (
|
|
882
|
-
|
|
883
|
-
valid: false,
|
|
884
|
-
error: `Maximum ${maxCount} user(s) allowed`,
|
|
885
|
-
errorCode: 'TOO_MANY_USERS',
|
|
886
|
-
};
|
|
992
|
+
else if (files.length === 0) {
|
|
993
|
+
files.push(preview);
|
|
887
994
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
if (invalidIds.length > 0) {
|
|
891
|
-
return {
|
|
892
|
-
valid: false,
|
|
893
|
-
error: 'All user IDs must be non-empty strings',
|
|
894
|
-
errorCode: 'INVALID_USER_ID',
|
|
895
|
-
};
|
|
995
|
+
else {
|
|
996
|
+
files[0] = { ...files[0], ...preview };
|
|
896
997
|
}
|
|
897
|
-
return {
|
|
998
|
+
return { ...base, type: 'file', files };
|
|
898
999
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
// Check length constraints
|
|
915
|
-
if (trimmedEmail.length > 254) {
|
|
916
|
-
return {
|
|
917
|
-
valid: false,
|
|
918
|
-
error: 'Email is too long',
|
|
919
|
-
errorCode: 'INVALID_EMAIL',
|
|
920
|
-
};
|
|
1000
|
+
|
|
1001
|
+
function videoItemFromUpload(file, result, duration) {
|
|
1002
|
+
const url = resolvePersistableMediaUrl(result.url);
|
|
1003
|
+
const item = {
|
|
1004
|
+
mediaId: result.mediaId,
|
|
1005
|
+
mimeType: result.mimeType,
|
|
1006
|
+
size: result.size,
|
|
1007
|
+
thumbnailUrl: resolvePersistedThumbnailUrl(result.thumbnailUrl, url),
|
|
1008
|
+
duration,
|
|
1009
|
+
width: 0,
|
|
1010
|
+
height: 0,
|
|
1011
|
+
metadata: result.metadata,
|
|
1012
|
+
};
|
|
1013
|
+
if (url) {
|
|
1014
|
+
item.url = url;
|
|
921
1015
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1016
|
+
return item;
|
|
1017
|
+
}
|
|
1018
|
+
function mergeVideoUploadResult(payload, result) {
|
|
1019
|
+
const base = normalizeVideoPayload(payload);
|
|
1020
|
+
const videos = [...base.videos];
|
|
1021
|
+
const i = videos.findIndex((x) => !x.mediaId && !resolvePersistableMediaUrl(x.url));
|
|
1022
|
+
const slot = i >= 0 ? videos[i] : undefined;
|
|
1023
|
+
const url = resolvePersistableMediaUrl(result.url);
|
|
1024
|
+
const next = {
|
|
1025
|
+
...(slot ?? { duration: 0, width: 0, height: 0 }),
|
|
1026
|
+
mediaId: result.mediaId,
|
|
1027
|
+
mimeType: result.mimeType,
|
|
1028
|
+
size: result.size,
|
|
1029
|
+
thumbnailUrl: resolvePersistedThumbnailUrl(result.thumbnailUrl ?? slot?.thumbnailUrl, url),
|
|
1030
|
+
metadata: result.metadata,
|
|
1031
|
+
};
|
|
1032
|
+
if (url) {
|
|
1033
|
+
next.url = url;
|
|
930
1034
|
}
|
|
931
|
-
|
|
1035
|
+
if (i >= 0)
|
|
1036
|
+
videos[i] = next;
|
|
1037
|
+
else
|
|
1038
|
+
videos.push(next);
|
|
1039
|
+
return { ...base, type: 'video', videos };
|
|
932
1040
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
return {
|
|
941
|
-
valid: false,
|
|
942
|
-
error: 'URL is required',
|
|
943
|
-
errorCode: 'MISSING_URL',
|
|
944
|
-
};
|
|
945
|
-
}
|
|
946
|
-
const trimmedUrl = url.trim();
|
|
947
|
-
// Check for common URL issues
|
|
948
|
-
if (trimmedUrl.length > 2048) {
|
|
949
|
-
return {
|
|
950
|
-
valid: false,
|
|
951
|
-
error: 'URL is too long',
|
|
952
|
-
errorCode: 'INVALID_URL',
|
|
953
|
-
};
|
|
1041
|
+
function applyVideoLocalPreview(payload, localUrl, mimeType = 'application/octet-stream') {
|
|
1042
|
+
const base = normalizeVideoPayload(payload);
|
|
1043
|
+
const videos = [...base.videos];
|
|
1044
|
+
const slot = videos.findIndex((x) => !x.mediaId && !resolvePersistableMediaUrl(x.url));
|
|
1045
|
+
const preview = { url: localUrl, duration: 0, width: 0, height: 0, mimeType };
|
|
1046
|
+
if (slot >= 0) {
|
|
1047
|
+
videos[slot] = { ...videos[slot], ...preview };
|
|
954
1048
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
// Validate protocol
|
|
958
|
-
if (!['http:', 'https:', 'ftp:', 'ftps:'].includes(urlObj.protocol)) {
|
|
959
|
-
return {
|
|
960
|
-
valid: false,
|
|
961
|
-
error: 'Invalid URL protocol',
|
|
962
|
-
errorCode: 'INVALID_URL',
|
|
963
|
-
};
|
|
964
|
-
}
|
|
965
|
-
return { valid: true };
|
|
1049
|
+
else if (videos.length === 0) {
|
|
1050
|
+
videos.push(preview);
|
|
966
1051
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
valid: false,
|
|
970
|
-
error: 'Invalid URL format',
|
|
971
|
-
errorCode: 'INVALID_URL',
|
|
972
|
-
};
|
|
1052
|
+
else {
|
|
1053
|
+
videos[0] = { ...videos[0], ...preview };
|
|
973
1054
|
}
|
|
1055
|
+
return { ...base, type: 'video', videos };
|
|
974
1056
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1057
|
+
|
|
1058
|
+
const CONVERSATION_VOICE_CATALOG = 'conversation-voice';
|
|
1059
|
+
const MB = 1024 * 1024;
|
|
1060
|
+
const CONVERSATION_VOICE_PRESENTATION = {
|
|
1061
|
+
icon: 'fa-light fa-microphone text-green-500',
|
|
1062
|
+
title: 'Voice message',
|
|
1063
|
+
};
|
|
1064
|
+
const VOICE_UTILITY = {
|
|
1065
|
+
duration: 'duration',
|
|
1066
|
+
formatDuration: 'formatDuration',
|
|
1067
|
+
createLocalPreviewUrl: 'createLocalPreviewUrl',
|
|
1068
|
+
};
|
|
1069
|
+
function blobUrl(ctx, blob) {
|
|
1070
|
+
if (!isPlatformBrowser(ctx.platformId)) {
|
|
986
1071
|
return '';
|
|
987
|
-
return input
|
|
988
|
-
.replace(/&/g, '&')
|
|
989
|
-
.replace(/</g, '<')
|
|
990
|
-
.replace(/>/g, '>')
|
|
991
|
-
.replace(/"/g, '"')
|
|
992
|
-
.replace(/'/g, ''')
|
|
993
|
-
.replace(/\//g, '/')
|
|
994
|
-
.replace(/`/g, '`')
|
|
995
|
-
.replace(/=/g, '=');
|
|
996
|
-
}
|
|
997
|
-
/**
|
|
998
|
-
* Validate latitude coordinate
|
|
999
|
-
* @param latitude - Latitude to validate
|
|
1000
|
-
* @returns Validation result
|
|
1001
|
-
*/
|
|
1002
|
-
function validateLatitude(latitude) {
|
|
1003
|
-
if (latitude === undefined || latitude === null || typeof latitude !== 'number' || isNaN(latitude)) {
|
|
1004
|
-
return {
|
|
1005
|
-
valid: false,
|
|
1006
|
-
error: 'Latitude is required',
|
|
1007
|
-
errorCode: 'MISSING_LATITUDE',
|
|
1008
|
-
};
|
|
1009
|
-
}
|
|
1010
|
-
if (latitude < -90 || latitude > 90) {
|
|
1011
|
-
return {
|
|
1012
|
-
valid: false,
|
|
1013
|
-
error: 'Latitude must be between -90 and 90',
|
|
1014
|
-
errorCode: 'INVALID_LATITUDE',
|
|
1015
|
-
};
|
|
1016
1072
|
}
|
|
1017
|
-
return
|
|
1073
|
+
return URL.createObjectURL(blob);
|
|
1018
1074
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
* @returns Validation result
|
|
1023
|
-
*/
|
|
1024
|
-
function validateLongitude(longitude) {
|
|
1025
|
-
if (longitude === undefined || longitude === null || typeof longitude !== 'number' || isNaN(longitude)) {
|
|
1026
|
-
return {
|
|
1027
|
-
valid: false,
|
|
1028
|
-
error: 'Longitude is required',
|
|
1029
|
-
errorCode: 'MISSING_LONGITUDE',
|
|
1030
|
-
};
|
|
1075
|
+
function mediaDuration(ctx, file) {
|
|
1076
|
+
if (!isPlatformBrowser(ctx.platformId)) {
|
|
1077
|
+
return Promise.resolve(0);
|
|
1031
1078
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1079
|
+
return new Promise((resolve, reject) => {
|
|
1080
|
+
const el = document.createElement('audio');
|
|
1081
|
+
el.preload = 'metadata';
|
|
1082
|
+
el.onloadedmetadata = () => {
|
|
1083
|
+
URL.revokeObjectURL(el.src);
|
|
1084
|
+
resolve(el.duration);
|
|
1085
|
+
};
|
|
1086
|
+
el.onerror = () => {
|
|
1087
|
+
URL.revokeObjectURL(el.src);
|
|
1088
|
+
reject(new Error('Failed to load audio metadata'));
|
|
1037
1089
|
};
|
|
1090
|
+
el.src = URL.createObjectURL(file);
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
function conversationVoiceUtilities() {
|
|
1094
|
+
return {
|
|
1095
|
+
[VOICE_UTILITY.duration]: (ctx, file) => mediaDuration(ctx, file),
|
|
1096
|
+
[VOICE_UTILITY.formatDuration]: (_ctx, seconds) => formatDuration(seconds),
|
|
1097
|
+
[VOICE_UTILITY.createLocalPreviewUrl]: (ctx, source) => blobUrl(ctx, source),
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
function createConversationVoiceFileType() {
|
|
1101
|
+
const presentation = CONVERSATION_VOICE_PRESENTATION;
|
|
1102
|
+
return {
|
|
1103
|
+
name: CONVERSATION_VOICE_CATALOG,
|
|
1104
|
+
metadata: createFileTypeMetadata('conversation'),
|
|
1105
|
+
title: presentation.title,
|
|
1106
|
+
icon: presentation.icon,
|
|
1107
|
+
validations: {
|
|
1108
|
+
mimeTypes: ['audio/webm', 'audio/ogg', 'audio/mp4', 'audio/*'],
|
|
1109
|
+
minSize: 1,
|
|
1110
|
+
maxSize: 50 * MB,
|
|
1111
|
+
},
|
|
1112
|
+
utilities: conversationVoiceUtilities(),
|
|
1113
|
+
copy: (payload) => {
|
|
1114
|
+
const voice = payload;
|
|
1115
|
+
const url = voice.url?.trim() ?? '';
|
|
1116
|
+
return {
|
|
1117
|
+
text: '',
|
|
1118
|
+
meta: {
|
|
1119
|
+
kind: 'voice',
|
|
1120
|
+
url,
|
|
1121
|
+
duration: voice.duration,
|
|
1122
|
+
mimeType: voice.mimeType,
|
|
1123
|
+
},
|
|
1124
|
+
};
|
|
1125
|
+
},
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
class AXConversationVoiceFileTypeProvider extends AXFileTypeInfoProvider {
|
|
1129
|
+
items() {
|
|
1130
|
+
return Promise.resolve([createConversationVoiceFileType()]);
|
|
1038
1131
|
}
|
|
1039
|
-
|
|
1132
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationVoiceFileTypeProvider, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1133
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationVoiceFileTypeProvider, providedIn: 'root' }); }
|
|
1040
1134
|
}
|
|
1135
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationVoiceFileTypeProvider, decorators: [{
|
|
1136
|
+
type: Injectable,
|
|
1137
|
+
args: [{ providedIn: 'root' }]
|
|
1138
|
+
}] });
|
|
1041
1139
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1140
|
+
function mergeVoiceUploadResult(payload, result) {
|
|
1141
|
+
return {
|
|
1142
|
+
...payload,
|
|
1143
|
+
type: 'voice',
|
|
1144
|
+
url: result.url,
|
|
1145
|
+
mediaId: result.mediaId,
|
|
1146
|
+
mimeType: result.mimeType,
|
|
1147
|
+
size: result.size,
|
|
1148
|
+
metadata: result.metadata,
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
function applyVoiceLocalPreview(payload, localUrl) {
|
|
1152
|
+
return { ...payload, type: 'voice', url: localUrl };
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const MESSAGE_TYPE_FILE_TYPE = {
|
|
1156
|
+
image: CONVERSATION_IMAGE_CATALOG,
|
|
1157
|
+
video: CONVERSATION_VIDEO_CATALOG,
|
|
1158
|
+
audio: CONVERSATION_AUDIO_CATALOG,
|
|
1159
|
+
file: CONVERSATION_FILE_CATALOG,
|
|
1160
|
+
voice: CONVERSATION_VOICE_CATALOG,
|
|
1161
|
+
sticker: CONVERSATION_IMAGE_CATALOG,
|
|
1162
|
+
};
|
|
1163
|
+
/** Resolves {@link AXMessage.fileType} from command or message type. */
|
|
1164
|
+
function resolveMessageFileType(type, fileType) {
|
|
1165
|
+
return fileType ?? MESSAGE_TYPE_FILE_TYPE[type];
|
|
1166
|
+
}
|
|
1167
|
+
function mergeImageUploadResult(payload, result) {
|
|
1168
|
+
const base = normalizeImagePayload(payload);
|
|
1169
|
+
const images = [...base.images];
|
|
1170
|
+
const i = images.findIndex((x) => !x.mediaId && !resolvePersistableMediaUrl(x.url));
|
|
1171
|
+
const slot = i >= 0 ? images[i] : undefined;
|
|
1172
|
+
const url = resolvePersistableMediaUrl(result.url);
|
|
1173
|
+
const next = {
|
|
1174
|
+
...(slot ?? { width: 0, height: 0 }),
|
|
1175
|
+
mediaId: result.mediaId,
|
|
1176
|
+
mimeType: result.mimeType,
|
|
1177
|
+
size: result.size,
|
|
1178
|
+
thumbnailUrl: resolvePersistedThumbnailUrl(result.thumbnailUrl ?? slot?.thumbnailUrl, url),
|
|
1179
|
+
metadata: result.metadata,
|
|
1180
|
+
};
|
|
1181
|
+
if (url) {
|
|
1182
|
+
next.url = url;
|
|
1053
1183
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1184
|
+
if (i >= 0)
|
|
1185
|
+
images[i] = next;
|
|
1186
|
+
else
|
|
1187
|
+
images.push(next);
|
|
1188
|
+
return { ...base, type: 'image', images };
|
|
1189
|
+
}
|
|
1190
|
+
function applyImageLocalPreview(payload, localUrl, mimeType = 'application/octet-stream') {
|
|
1191
|
+
const base = normalizeImagePayload(payload);
|
|
1192
|
+
const images = [...base.images];
|
|
1193
|
+
const slot = images.findIndex((x) => !x.mediaId && !resolvePersistableMediaUrl(x.url));
|
|
1194
|
+
const preview = { url: localUrl, width: 0, height: 0, mimeType };
|
|
1195
|
+
if (slot >= 0) {
|
|
1196
|
+
images[slot] = { ...images[slot], ...preview };
|
|
1059
1197
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
*/
|
|
1063
|
-
static getConversationAvatarIcon(conversation) {
|
|
1064
|
-
if (AXConversationMessageUtilsService.getConversationAvatar(conversation)) {
|
|
1065
|
-
return undefined;
|
|
1066
|
-
}
|
|
1067
|
-
return AXConversationMessageUtilsService.normalizeOptionalMediaValue(conversation.icon);
|
|
1068
|
-
}
|
|
1069
|
-
/**
|
|
1070
|
-
* Get sender name from message
|
|
1071
|
-
*/
|
|
1072
|
-
static getSenderName(message, conversation) {
|
|
1073
|
-
const participant = conversation.participants.find((p) => p.id === message.senderId);
|
|
1074
|
-
return participant?.name || translateSync('@acorex:chat.fallbacks.unknown-user');
|
|
1075
|
-
}
|
|
1076
|
-
/**
|
|
1077
|
-
* Get sender avatar image URL (takes precedence over {@link getSenderAvatarIcon}).
|
|
1078
|
-
*/
|
|
1079
|
-
static getSenderAvatar(message, conversation) {
|
|
1080
|
-
const participant = conversation.participants.find((p) => p.id === message.senderId);
|
|
1081
|
-
return AXConversationMessageUtilsService.normalizeOptionalMediaValue(participant?.avatar);
|
|
1198
|
+
else if (images.length === 0) {
|
|
1199
|
+
images.push(preview);
|
|
1082
1200
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
* participant `icon` first, then conversation-level `icon`.
|
|
1086
|
-
*/
|
|
1087
|
-
static getSenderAvatarIcon(message, conversation) {
|
|
1088
|
-
const participant = conversation.participants.find((p) => p.id === message.senderId);
|
|
1089
|
-
if (AXConversationMessageUtilsService.normalizeOptionalMediaValue(participant?.avatar)) {
|
|
1090
|
-
return undefined;
|
|
1091
|
-
}
|
|
1092
|
-
return (AXConversationMessageUtilsService.normalizeOptionalMediaValue(participant?.icon) ??
|
|
1093
|
-
AXConversationMessageUtilsService.getConversationAvatarIcon(conversation));
|
|
1201
|
+
else {
|
|
1202
|
+
images[0] = { ...images[0], ...preview };
|
|
1094
1203
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
return
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1204
|
+
return { ...base, type: 'image', images };
|
|
1205
|
+
}
|
|
1206
|
+
function mergeUploadResult(type, payload, result) {
|
|
1207
|
+
switch (type) {
|
|
1208
|
+
case 'image':
|
|
1209
|
+
return mergeImageUploadResult(payload, result);
|
|
1210
|
+
case 'video':
|
|
1211
|
+
return mergeVideoUploadResult(payload, result);
|
|
1212
|
+
case 'audio':
|
|
1213
|
+
return mergeAudioUploadResult(payload, result);
|
|
1214
|
+
case 'file':
|
|
1215
|
+
return mergeFileUploadResult(payload, result);
|
|
1216
|
+
case 'voice':
|
|
1217
|
+
return mergeVoiceUploadResult(payload, result);
|
|
1218
|
+
case 'sticker':
|
|
1219
|
+
return {
|
|
1220
|
+
...payload,
|
|
1221
|
+
type: 'sticker',
|
|
1222
|
+
url: result.url,
|
|
1223
|
+
mediaId: result.mediaId,
|
|
1224
|
+
};
|
|
1225
|
+
default:
|
|
1226
|
+
return payload;
|
|
1107
1227
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1228
|
+
}
|
|
1229
|
+
function applyLocalPreview(type, payload, localUrl, mimeType = 'application/octet-stream') {
|
|
1230
|
+
switch (type) {
|
|
1231
|
+
case 'image':
|
|
1232
|
+
return applyImageLocalPreview(payload, localUrl, mimeType);
|
|
1233
|
+
case 'video':
|
|
1234
|
+
return applyVideoLocalPreview(payload, localUrl, mimeType);
|
|
1235
|
+
case 'audio':
|
|
1236
|
+
return applyAudioLocalPreview(payload, localUrl, mimeType);
|
|
1237
|
+
case 'file':
|
|
1238
|
+
return applyFileLocalPreview(payload, localUrl, mimeType);
|
|
1239
|
+
case 'voice':
|
|
1240
|
+
return applyVoiceLocalPreview(payload, localUrl);
|
|
1241
|
+
case 'sticker':
|
|
1242
|
+
return { ...payload, type: 'sticker', url: localUrl };
|
|
1243
|
+
default:
|
|
1244
|
+
return payload;
|
|
1113
1245
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
return
|
|
1246
|
+
}
|
|
1247
|
+
function toUploaderReference$1(payload) {
|
|
1248
|
+
switch (payload.type) {
|
|
1249
|
+
case 'image': {
|
|
1250
|
+
const first = payload.images[0];
|
|
1251
|
+
return { url: first?.url, mediaId: first?.mediaId, mimeType: first?.mimeType, size: first?.size };
|
|
1120
1252
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
switch (message.type) {
|
|
1129
|
-
case 'text': {
|
|
1130
|
-
const textPayload = message.payload;
|
|
1131
|
-
const text = textPayload.text || '';
|
|
1132
|
-
const truncatedText = text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
|
|
1133
|
-
return {
|
|
1134
|
-
value: truncatedText,
|
|
1135
|
-
type: 'text',
|
|
1136
|
-
icon: 'fa-light fa-message',
|
|
1137
|
-
};
|
|
1138
|
-
}
|
|
1139
|
-
case 'image': {
|
|
1140
|
-
const imagePayload = normalizeImagePayload(message.payload);
|
|
1141
|
-
const first = imagePayload.images[0];
|
|
1142
|
-
const label = imagePayload.caption?.trim() ||
|
|
1143
|
-
(imagePayload.images && imagePayload.images.length > 1
|
|
1144
|
-
? `${imagePayload.images.length} images`
|
|
1145
|
-
: '');
|
|
1146
|
-
return {
|
|
1147
|
-
value: label || first?.thumbnailUrl || first?.url || '',
|
|
1148
|
-
type: 'image',
|
|
1149
|
-
icon: 'fa-light fa-image',
|
|
1150
|
-
};
|
|
1151
|
-
}
|
|
1152
|
-
case 'video': {
|
|
1153
|
-
const videoPayload = normalizeVideoPayload(message.payload);
|
|
1154
|
-
const first = videoPayload.videos[0];
|
|
1155
|
-
const label = videoPayload.caption?.trim() ||
|
|
1156
|
-
(videoPayload.videos.length > 1 ? `${videoPayload.videos.length} videos` : '');
|
|
1157
|
-
return {
|
|
1158
|
-
value: label || first?.url || '',
|
|
1159
|
-
type: 'video',
|
|
1160
|
-
icon: 'fa-light fa-video',
|
|
1161
|
-
};
|
|
1162
|
-
}
|
|
1163
|
-
case 'audio': {
|
|
1164
|
-
const audioPayload = normalizeAudioPayload(message.payload);
|
|
1165
|
-
const names = audioPayload.audios.map((a) => a.title).filter(Boolean).join(', ');
|
|
1166
|
-
const first = audioPayload.audios[0];
|
|
1167
|
-
const label = audioPayload.caption?.trim() ||
|
|
1168
|
-
(audioPayload.audios.length > 1 ? `${audioPayload.audios.length} audio files` : names);
|
|
1169
|
-
return {
|
|
1170
|
-
value: label || first?.url || '',
|
|
1171
|
-
type: 'audio',
|
|
1172
|
-
icon: 'fa-light fa-music',
|
|
1173
|
-
};
|
|
1174
|
-
}
|
|
1175
|
-
case 'voice': {
|
|
1176
|
-
const voicePayload = message.payload;
|
|
1177
|
-
return {
|
|
1178
|
-
value: voicePayload.url || '',
|
|
1179
|
-
type: 'voice',
|
|
1180
|
-
icon: 'fa-light fa-microphone',
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
case 'file': {
|
|
1184
|
-
const filePayload = normalizeFilePayload(message.payload);
|
|
1185
|
-
const names = filePayload.files.map((f) => f.name).join(', ');
|
|
1186
|
-
const label = filePayload.caption?.trim() ||
|
|
1187
|
-
(filePayload.files.length > 1 ? `${filePayload.files.length} files` : names);
|
|
1188
|
-
return {
|
|
1189
|
-
value: label || names,
|
|
1190
|
-
type: 'file',
|
|
1191
|
-
icon: 'fa-light fa-file',
|
|
1192
|
-
};
|
|
1193
|
-
}
|
|
1194
|
-
case 'location': {
|
|
1195
|
-
const locationPayload = message.payload;
|
|
1196
|
-
return {
|
|
1197
|
-
value: locationPayload.latitude && locationPayload.longitude
|
|
1198
|
-
? `${locationPayload.latitude},${locationPayload.longitude}`
|
|
1199
|
-
: '',
|
|
1200
|
-
type: 'location',
|
|
1201
|
-
icon: 'fa-light fa-location-dot',
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
|
-
case 'sticker': {
|
|
1205
|
-
const stickerPayload = message.payload;
|
|
1206
|
-
return {
|
|
1207
|
-
value: stickerPayload.url || '',
|
|
1208
|
-
type: 'sticker',
|
|
1209
|
-
icon: 'fa-light fa-face-smile',
|
|
1210
|
-
};
|
|
1211
|
-
}
|
|
1212
|
-
default:
|
|
1213
|
-
return {
|
|
1214
|
-
value: '',
|
|
1215
|
-
type: message.type,
|
|
1216
|
-
icon: 'fa-light fa-message',
|
|
1217
|
-
};
|
|
1253
|
+
case 'video': {
|
|
1254
|
+
const first = payload.videos[0];
|
|
1255
|
+
return { url: first?.url, mediaId: first?.mediaId, mimeType: first?.mimeType, size: first?.size };
|
|
1256
|
+
}
|
|
1257
|
+
case 'audio': {
|
|
1258
|
+
const first = payload.audios[0];
|
|
1259
|
+
return { url: first?.url, mediaId: first?.mediaId, mimeType: first?.mimeType, size: first?.size };
|
|
1218
1260
|
}
|
|
1261
|
+
case 'file': {
|
|
1262
|
+
const first = payload.files[0];
|
|
1263
|
+
return { url: first?.url, mediaId: first?.mediaId, mimeType: first?.mimeType, size: first?.size };
|
|
1264
|
+
}
|
|
1265
|
+
case 'voice':
|
|
1266
|
+
case 'sticker':
|
|
1267
|
+
return {
|
|
1268
|
+
url: payload.url,
|
|
1269
|
+
mediaId: payload.mediaId,
|
|
1270
|
+
mimeType: payload.mimeType,
|
|
1271
|
+
size: payload.size,
|
|
1272
|
+
};
|
|
1273
|
+
default:
|
|
1274
|
+
return {};
|
|
1219
1275
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
return message.senderId === currentUserId;
|
|
1276
|
+
}
|
|
1277
|
+
function createObjectUrl(platformId, blob) {
|
|
1278
|
+
if (!isPlatformBrowser(platformId)) {
|
|
1279
|
+
return '';
|
|
1225
1280
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
case 'sending':
|
|
1232
|
-
return 'fa-light fa-clock';
|
|
1233
|
-
case 'sent':
|
|
1234
|
-
return 'fa-light fa-check';
|
|
1235
|
-
case 'delivered':
|
|
1236
|
-
return 'fa-light fa-check-double';
|
|
1237
|
-
case 'read':
|
|
1238
|
-
return 'fa-light fa-check-double';
|
|
1239
|
-
case 'failed':
|
|
1240
|
-
return 'fa-light fa-circle-exclamation';
|
|
1241
|
-
default:
|
|
1242
|
-
return 'fa-light fa-check';
|
|
1243
|
-
}
|
|
1281
|
+
return URL.createObjectURL(blob);
|
|
1282
|
+
}
|
|
1283
|
+
function revokeObjectUrl(platformId, url) {
|
|
1284
|
+
if (isPlatformBrowser(platformId) && url.startsWith('blob:')) {
|
|
1285
|
+
URL.revokeObjectURL(url);
|
|
1244
1286
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
// Always show avatar for group conversations
|
|
1250
|
-
if (conversation.type === 'group' || conversation.type === 'channel') {
|
|
1251
|
-
// Don't show if same sender as previous message within 5 minutes
|
|
1252
|
-
if (previousMessage && previousMessage.senderId === message.senderId) {
|
|
1253
|
-
const timeDiff = message.timestamp.getTime() - previousMessage.timestamp.getTime();
|
|
1254
|
-
return timeDiff > 5 * 60 * 1000; // 5 minutes
|
|
1255
|
-
}
|
|
1256
|
-
return true;
|
|
1257
|
-
}
|
|
1258
|
-
return false;
|
|
1259
|
-
}
|
|
1260
|
-
/**
|
|
1261
|
-
* Group messages by sender for consecutive messages
|
|
1262
|
-
*/
|
|
1263
|
-
static shouldGroupWithPrevious(message, previousMessage) {
|
|
1264
|
-
if (!previousMessage)
|
|
1265
|
-
return false;
|
|
1266
|
-
// Same sender
|
|
1267
|
-
if (message.senderId !== previousMessage.senderId)
|
|
1268
|
-
return false;
|
|
1269
|
-
// Within 5 minutes
|
|
1270
|
-
const timeDiff = message.timestamp.getTime() - previousMessage.timestamp.getTime();
|
|
1271
|
-
return timeDiff < 5 * 60 * 1000;
|
|
1272
|
-
}
|
|
1273
|
-
/**
|
|
1274
|
-
* Get conversation status for avatar (private conversations only)
|
|
1275
|
-
*/
|
|
1276
|
-
static getConversationStatus(conversation) {
|
|
1277
|
-
if (conversation.type === 'private') {
|
|
1278
|
-
return conversation.status.presence;
|
|
1279
|
-
}
|
|
1287
|
+
}
|
|
1288
|
+
async function createLocalPreviewUrl(fileService, platformId, source, messageType) {
|
|
1289
|
+
const catalog = MESSAGE_TYPE_FILE_TYPE[messageType];
|
|
1290
|
+
if (!catalog) {
|
|
1280
1291
|
return undefined;
|
|
1281
1292
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
static getTypingText(conversation) {
|
|
1286
|
-
const typingUsers = conversation.status.typingUsers;
|
|
1287
|
-
if (typingUsers.length === 0)
|
|
1288
|
-
return '';
|
|
1289
|
-
if (conversation.type === 'private') {
|
|
1290
|
-
return translateSync('@acorex:chat.status.typing');
|
|
1291
|
-
}
|
|
1292
|
-
const firstUser = conversation.participants.find((p) => p.id === typingUsers[0]);
|
|
1293
|
-
if (typingUsers.length === 1) {
|
|
1294
|
-
return translateSync('@acorex:chat.status.user-is-typing', {
|
|
1295
|
-
params: { userName: firstUser?.name || translateSync('@acorex:chat.fallbacks.someone') },
|
|
1296
|
-
});
|
|
1297
|
-
}
|
|
1298
|
-
return translateSync('@acorex:chat.status.people-typing', { params: { count: typingUsers.length } });
|
|
1299
|
-
}
|
|
1300
|
-
/**
|
|
1301
|
-
* Format last seen time
|
|
1302
|
-
*/
|
|
1303
|
-
static formatLastSeen(date) {
|
|
1304
|
-
const now = new Date();
|
|
1305
|
-
const diff = now.getTime() - date.getTime();
|
|
1306
|
-
const seconds = Math.floor(diff / 1000);
|
|
1307
|
-
if (seconds < 60)
|
|
1308
|
-
return translateSync('@acorex:chat.time.just-now');
|
|
1309
|
-
if (seconds < 3600)
|
|
1310
|
-
return translateSync('@acorex:chat.time.minutes-ago', { params: { count: Math.floor(seconds / 60) } });
|
|
1311
|
-
if (seconds < 86400)
|
|
1312
|
-
return translateSync('@acorex:chat.time.hours-ago', { params: { count: Math.floor(seconds / 3600) } });
|
|
1313
|
-
if (seconds < 604800)
|
|
1314
|
-
return translateSync('@acorex:chat.time.days-ago', { params: { count: Math.floor(seconds / 86400) } });
|
|
1315
|
-
return date.toLocaleDateString();
|
|
1316
|
-
}
|
|
1317
|
-
/**
|
|
1318
|
-
* Get conversation subtitle (status or member count)
|
|
1319
|
-
*/
|
|
1320
|
-
static getConversationSubtitle(conversation) {
|
|
1321
|
-
if (conversation.status.isTyping) {
|
|
1322
|
-
return AXConversationMessageUtilsService.getTypingText(conversation);
|
|
1323
|
-
}
|
|
1324
|
-
switch (conversation.type) {
|
|
1325
|
-
case 'private':
|
|
1326
|
-
if (conversation.status.presence === 'online') {
|
|
1327
|
-
return translateSync('@acorex:chat.status.online');
|
|
1328
|
-
}
|
|
1329
|
-
if (conversation.status.lastSeen) {
|
|
1330
|
-
return translateSync('@acorex:chat.status.last-seen', {
|
|
1331
|
-
params: { value: AXConversationMessageUtilsService.formatLastSeen(conversation.status.lastSeen) },
|
|
1332
|
-
});
|
|
1333
|
-
}
|
|
1334
|
-
return translateSync('@acorex:chat.status.offline');
|
|
1335
|
-
case 'group':
|
|
1336
|
-
return translateSync('@acorex:chat.members.count', { params: { count: conversation.participants.length } });
|
|
1337
|
-
case 'channel':
|
|
1338
|
-
return translateSync('@acorex:chat.members.subscribers-count', { params: { count: conversation.participants.length } });
|
|
1339
|
-
case 'bot':
|
|
1340
|
-
return translateSync('@acorex:chat.bot');
|
|
1341
|
-
default:
|
|
1342
|
-
return '';
|
|
1343
|
-
}
|
|
1293
|
+
const fileType = await fileService.getFileType(catalog);
|
|
1294
|
+
if (!fileType) {
|
|
1295
|
+
return undefined;
|
|
1344
1296
|
}
|
|
1297
|
+
const ctx = { readAsDataUrl: (f) => fileService.blobToBase64(f), platformId };
|
|
1298
|
+
const extension = resolveFileTypeExtension(fileType, {
|
|
1299
|
+
file: source instanceof File ? source : undefined,
|
|
1300
|
+
mimeType: source.type,
|
|
1301
|
+
});
|
|
1302
|
+
const result = await runFileTypeUtility(fileType, ctx, extension, 'createLocalPreviewUrl', source);
|
|
1303
|
+
return typeof result === 'string' ? result : undefined;
|
|
1345
1304
|
}
|
|
1346
1305
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1306
|
+
function normalizeMessagePayload(payload) {
|
|
1307
|
+
switch (payload.type) {
|
|
1308
|
+
case 'image':
|
|
1309
|
+
return normalizeImagePayload(payload);
|
|
1310
|
+
case 'video':
|
|
1311
|
+
return normalizeVideoPayload(payload);
|
|
1312
|
+
case 'audio':
|
|
1313
|
+
return normalizeAudioPayload(payload);
|
|
1314
|
+
case 'file':
|
|
1315
|
+
return normalizeFilePayload(payload);
|
|
1316
|
+
default:
|
|
1317
|
+
return payload;
|
|
1351
1318
|
}
|
|
1352
|
-
const currentId = currentUserId ?? 'current-user';
|
|
1353
|
-
return conversation.participants.find((participant) => participant.id !== currentId)?.id;
|
|
1354
|
-
}
|
|
1355
|
-
/** Whether `auto` kind should render a user avatar for this conversation. */
|
|
1356
|
-
function shouldUseUserAvatarForConversation(conversation, currentUserId) {
|
|
1357
|
-
return conversation.type === 'private' && !!resolvePrivatePeerUserId(conversation, currentUserId);
|
|
1358
1319
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Conversation Configuration Interface
|
|
1323
|
+
* Centralized configuration values to avoid magic numbers
|
|
1324
|
+
*/
|
|
1325
|
+
|
|
1326
|
+
/**
|
|
1327
|
+
* Default Configuration Values
|
|
1328
|
+
* Centralized defaults to avoid magic numbers throughout the codebase
|
|
1329
|
+
*/
|
|
1330
|
+
/**
|
|
1331
|
+
* Default conversation configuration
|
|
1332
|
+
* All values are explicitly defined here for easy maintenance and documentation
|
|
1333
|
+
*/
|
|
1334
|
+
const AX_DEFAULT_CONVERSATION_CONFIG = {
|
|
1335
|
+
// Pagination
|
|
1336
|
+
messagePageSize: 30,
|
|
1337
|
+
conversationPageSize: 20,
|
|
1338
|
+
// Scroll Configuration
|
|
1339
|
+
scrollThreshold: 100,
|
|
1340
|
+
infiniteScrollThreshold: 200,
|
|
1341
|
+
// Timeout Durations (milliseconds)
|
|
1342
|
+
typingIndicatorTimeout: 3000,
|
|
1343
|
+
typingIndicatorThrottle: 1000,
|
|
1344
|
+
messageHighlightDuration: 2000,
|
|
1345
|
+
debounceSearch: 300,
|
|
1346
|
+
// Message Storage Limits
|
|
1347
|
+
maxMessagesPerConversation: 1000,
|
|
1348
|
+
maxTotalMessages: 10000,
|
|
1349
|
+
maxCachedConversations: 50,
|
|
1350
|
+
// UI Dimensions (pixels)
|
|
1351
|
+
minSidebarWidth: 250,
|
|
1352
|
+
maxSidebarWidth: 500,
|
|
1353
|
+
defaultSidebarWidth: 320,
|
|
1354
|
+
// Cache
|
|
1355
|
+
filterCacheSize: 100,
|
|
1356
|
+
// Message Validation
|
|
1357
|
+
maxMessageLength: 10000,
|
|
1358
|
+
minMessageLength: 1,
|
|
1359
|
+
maxFilesPerMessage: 3,
|
|
1360
|
+
// Intersection Observer
|
|
1361
|
+
messageReadThreshold: 0.3,
|
|
1362
|
+
// Message list
|
|
1363
|
+
messageListBackground: '',
|
|
1364
|
+
};
|
|
1365
|
+
/**
|
|
1366
|
+
* Helper function to merge user config with defaults
|
|
1367
|
+
* Properly handles array merging to avoid reference issues
|
|
1368
|
+
* @param userConfig - User-provided configuration
|
|
1369
|
+
* @returns Merged configuration with all required fields
|
|
1370
|
+
*/
|
|
1371
|
+
function mergeWithDefaults(userConfig) {
|
|
1372
|
+
if (!userConfig) {
|
|
1373
|
+
return { ...AX_DEFAULT_CONVERSATION_CONFIG };
|
|
1366
1374
|
}
|
|
1367
|
-
const participant = conversation?.participants.find((p) => p.id === userId);
|
|
1368
|
-
return {
|
|
1369
|
-
name: participant?.name ?? userId,
|
|
1370
|
-
avatar: participant?.avatar,
|
|
1371
|
-
icon: participant?.icon,
|
|
1372
|
-
};
|
|
1373
|
-
}
|
|
1374
|
-
function resolveConversationAvatarDisplay(conversation, currentUserId) {
|
|
1375
|
-
const title = currentUserId !== undefined
|
|
1376
|
-
? resolveConversationTitleForViewer(conversation, currentUserId)
|
|
1377
|
-
: conversation.title;
|
|
1378
1375
|
return {
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
icon: AXConversationMessageUtilsService.getConversationAvatarIcon(conversation),
|
|
1376
|
+
...AX_DEFAULT_CONVERSATION_CONFIG,
|
|
1377
|
+
...userConfig,
|
|
1382
1378
|
};
|
|
1383
1379
|
}
|
|
1384
1380
|
|
|
1385
|
-
const GENERIC_PRIVATE_TITLES = new Set(['', 'new chat', 'new conversation']);
|
|
1386
|
-
/** True when the stored title is a placeholder, not a user-defined name. */
|
|
1387
|
-
function isGenericPrivateConversationTitle(title) {
|
|
1388
|
-
return GENERIC_PRIVATE_TITLES.has((title ?? '').trim().toLowerCase());
|
|
1389
|
-
}
|
|
1390
|
-
/** Other participant in a private 1v1 chat (excludes the current viewer). */
|
|
1391
|
-
function resolvePrivatePeerParticipant(conversation, currentUserId) {
|
|
1392
|
-
if (conversation.type !== 'private') {
|
|
1393
|
-
return undefined;
|
|
1394
|
-
}
|
|
1395
|
-
const peerId = resolvePrivatePeerUserId(conversation, currentUserId);
|
|
1396
|
-
if (!peerId) {
|
|
1397
|
-
return undefined;
|
|
1398
|
-
}
|
|
1399
|
-
return conversation.participants.find((participant) => participant.id === peerId);
|
|
1400
|
-
}
|
|
1401
1381
|
/**
|
|
1402
|
-
*
|
|
1403
|
-
*
|
|
1382
|
+
* Dependency Injection Tokens
|
|
1383
|
+
* InjectionTokens for configuration and dependencies
|
|
1404
1384
|
*/
|
|
1405
|
-
function resolveConversationTitleForViewer(conversation, currentUserId) {
|
|
1406
|
-
if (conversation.type !== 'private') {
|
|
1407
|
-
return conversation.title;
|
|
1408
|
-
}
|
|
1409
|
-
const peer = resolvePrivatePeerParticipant(conversation, currentUserId);
|
|
1410
|
-
if (peer?.name) {
|
|
1411
|
-
return peer.name;
|
|
1412
|
-
}
|
|
1413
|
-
if (isGenericPrivateConversationTitle(conversation.title)) {
|
|
1414
|
-
return conversation.title || 'New Chat';
|
|
1415
|
-
}
|
|
1416
|
-
return conversation.title;
|
|
1417
|
-
}
|
|
1418
1385
|
/**
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
1386
|
+
* Configuration token for conversation component
|
|
1387
|
+
* Uses centralized defaults from AX_DEFAULT_CONVERSATION_CONFIG
|
|
1421
1388
|
*/
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
if (title === conversation.title &&
|
|
1434
|
-
avatar === conversation.avatar &&
|
|
1435
|
-
icon === conversation.icon) {
|
|
1436
|
-
return conversation;
|
|
1437
|
-
}
|
|
1438
|
-
return {
|
|
1439
|
-
...conversation,
|
|
1440
|
-
title,
|
|
1441
|
-
avatar,
|
|
1442
|
-
icon,
|
|
1443
|
-
};
|
|
1444
|
-
}
|
|
1389
|
+
const CONVERSATION_CONFIG = new InjectionToken('CONVERSATION_CONFIG', {
|
|
1390
|
+
providedIn: 'root',
|
|
1391
|
+
factory: () => mergeWithDefaults(),
|
|
1392
|
+
});
|
|
1393
|
+
/**
|
|
1394
|
+
* Token for configuring AXErrorHandlerService
|
|
1395
|
+
*/
|
|
1396
|
+
const ERROR_HANDLER_CONFIG = new InjectionToken('ERROR_HANDLER_CONFIG', {
|
|
1397
|
+
providedIn: 'root',
|
|
1398
|
+
factory: () => ({}),
|
|
1399
|
+
});
|
|
1445
1400
|
|
|
1446
1401
|
/**
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
1402
|
+
* Pluggable avatar components for the conversation UI.
|
|
1403
|
+
* Register via `provideConversation({ avatarComponents: { user, conversation } })`.
|
|
1449
1404
|
*/
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1405
|
+
const AX_CONVERSATION_USER_AVATAR_COMPONENT = new InjectionToken('AX_CONVERSATION_USER_AVATAR_COMPONENT');
|
|
1406
|
+
const AX_CONVERSATION_CONVERSATION_AVATAR_COMPONENT = new InjectionToken('AX_CONVERSATION_CONVERSATION_AVATAR_COMPONENT');
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
* Registry Configuration Tokens
|
|
1410
|
+
* Injection tokens for configuring default registry values
|
|
1411
|
+
*/
|
|
1412
|
+
/**
|
|
1413
|
+
* Additional message renderers configuration
|
|
1414
|
+
* Provide this token to add custom message renderers in addition to built-in ones
|
|
1415
|
+
* Note: Built-in renderers (text, system, fallback) are registered by default; other types are provided via plugins/constants.
|
|
1416
|
+
*/
|
|
1417
|
+
const DEFAULT_MESSAGE_RENDERERS = new InjectionToken('DEFAULT_MESSAGE_RENDERERS', {
|
|
1418
|
+
providedIn: 'root',
|
|
1419
|
+
factory: () => [],
|
|
1420
|
+
});
|
|
1421
|
+
/**
|
|
1422
|
+
* Default message actions configuration
|
|
1423
|
+
* Provide this token to override default message actions
|
|
1424
|
+
*/
|
|
1425
|
+
const DEFAULT_MESSAGE_ACTIONS = new InjectionToken('DEFAULT_MESSAGE_ACTIONS', {
|
|
1426
|
+
providedIn: 'root',
|
|
1427
|
+
factory: () => [],
|
|
1428
|
+
});
|
|
1429
|
+
/**
|
|
1430
|
+
* Default composer tabs configuration
|
|
1431
|
+
* Provide this token to override default composer tabs (emoji, stickers, etc.)
|
|
1432
|
+
*/
|
|
1433
|
+
const DEFAULT_COMPOSER_TABS = new InjectionToken('DEFAULT_COMPOSER_TABS', {
|
|
1434
|
+
providedIn: 'root',
|
|
1435
|
+
factory: () => [],
|
|
1436
|
+
});
|
|
1437
|
+
/**
|
|
1438
|
+
* Default composer actions configuration
|
|
1439
|
+
* Provide this token to override default composer actions (attach, voice, etc.)
|
|
1440
|
+
*/
|
|
1441
|
+
const DEFAULT_COMPOSER_ACTIONS = new InjectionToken('DEFAULT_COMPOSER_ACTIONS', {
|
|
1442
|
+
providedIn: 'root',
|
|
1443
|
+
factory: () => [],
|
|
1444
|
+
});
|
|
1445
|
+
/**
|
|
1446
|
+
* Default conversation tabs configuration
|
|
1447
|
+
* Provide this token to override default conversation tabs (all, private, groups, etc.)
|
|
1448
|
+
*/
|
|
1449
|
+
const DEFAULT_CONVERSATION_TABS = new InjectionToken('DEFAULT_CONVERSATION_TABS', {
|
|
1450
|
+
providedIn: 'root',
|
|
1451
|
+
factory: () => [],
|
|
1452
|
+
});
|
|
1453
|
+
/**
|
|
1454
|
+
* Default info bar actions configuration
|
|
1455
|
+
* Provide this token to override default info bar actions (mute, archive, block, etc.)
|
|
1456
|
+
*/
|
|
1457
|
+
const DEFAULT_INFO_BAR_ACTIONS = new InjectionToken('DEFAULT_INFO_BAR_ACTIONS', {
|
|
1458
|
+
providedIn: 'root',
|
|
1459
|
+
factory: () => [],
|
|
1460
|
+
});
|
|
1461
|
+
/**
|
|
1462
|
+
* Default conversation item actions configuration
|
|
1463
|
+
* Provide this token to override default conversation item actions (mute, delete, archive, etc.)
|
|
1464
|
+
*/
|
|
1465
|
+
const DEFAULT_CONVERSATION_ITEM_ACTIONS = new InjectionToken('DEFAULT_CONVERSATION_ITEM_ACTIONS', {
|
|
1466
|
+
providedIn: 'root',
|
|
1467
|
+
factory: () => [],
|
|
1468
|
+
});
|
|
1469
|
+
/**
|
|
1470
|
+
* Complete registry configuration token
|
|
1471
|
+
* Provide this for comprehensive registry configuration
|
|
1472
|
+
*/
|
|
1473
|
+
const REGISTRY_CONFIG = new InjectionToken('REGISTRY_CONFIG', {
|
|
1474
|
+
providedIn: 'root',
|
|
1475
|
+
factory: () => ({}),
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
class AXConversationMessageUtilsService {
|
|
1479
|
+
/**
|
|
1480
|
+
* Normalize optional avatar/icon values so empty or whitespace-only strings
|
|
1481
|
+
* are treated as missing data.
|
|
1482
|
+
*/
|
|
1483
|
+
static normalizeOptionalMediaValue(value) {
|
|
1484
|
+
if (typeof value !== 'string') {
|
|
1485
|
+
return undefined;
|
|
1486
|
+
}
|
|
1487
|
+
const normalized = value.trim();
|
|
1488
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
1483
1489
|
}
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
});
|
|
1490
|
+
/**
|
|
1491
|
+
* Get conversation avatar image URL.
|
|
1492
|
+
*/
|
|
1493
|
+
static getConversationAvatar(conversation) {
|
|
1494
|
+
return AXConversationMessageUtilsService.normalizeOptionalMediaValue(conversation.avatar);
|
|
1490
1495
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1496
|
+
/**
|
|
1497
|
+
* Font Awesome icon class(es) for a conversation when there is no avatar image.
|
|
1498
|
+
*/
|
|
1499
|
+
static getConversationAvatarIcon(conversation) {
|
|
1500
|
+
if (AXConversationMessageUtilsService.getConversationAvatar(conversation)) {
|
|
1501
|
+
return undefined;
|
|
1502
|
+
}
|
|
1503
|
+
return AXConversationMessageUtilsService.normalizeOptionalMediaValue(conversation.icon);
|
|
1493
1504
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1505
|
+
/**
|
|
1506
|
+
* Get sender name from message
|
|
1507
|
+
*/
|
|
1508
|
+
static getSenderName(message, conversation) {
|
|
1509
|
+
const participant = conversation.participants.find((p) => p.id === message.senderId);
|
|
1510
|
+
return participant?.name || translateSync('@acorex:chat.fallbacks.unknown-user');
|
|
1499
1511
|
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
const messageIds = this._conversationMessages().get(conversationId) || [];
|
|
1507
|
-
this._messages.update((messages) => {
|
|
1508
|
-
const newMessages = new Map(messages);
|
|
1509
|
-
messageIds.forEach((id) => newMessages.delete(id));
|
|
1510
|
-
return newMessages;
|
|
1511
|
-
});
|
|
1512
|
-
this._conversationMessages.update((map) => {
|
|
1513
|
-
const newMap = new Map(map);
|
|
1514
|
-
newMap.delete(conversationId);
|
|
1515
|
-
return newMap;
|
|
1516
|
-
});
|
|
1512
|
+
/**
|
|
1513
|
+
* Get sender avatar image URL (takes precedence over {@link getSenderAvatarIcon}).
|
|
1514
|
+
*/
|
|
1515
|
+
static getSenderAvatar(message, conversation) {
|
|
1516
|
+
const participant = conversation.participants.find((p) => p.id === message.senderId);
|
|
1517
|
+
return AXConversationMessageUtilsService.normalizeOptionalMediaValue(participant?.avatar);
|
|
1517
1518
|
}
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1519
|
+
/**
|
|
1520
|
+
* Font Awesome icon class(es) for the sender when there is no avatar image:
|
|
1521
|
+
* participant `icon` first, then conversation-level `icon`.
|
|
1522
|
+
*/
|
|
1523
|
+
static getSenderAvatarIcon(message, conversation) {
|
|
1524
|
+
const participant = conversation.participants.find((p) => p.id === message.senderId);
|
|
1525
|
+
if (AXConversationMessageUtilsService.normalizeOptionalMediaValue(participant?.avatar)) {
|
|
1526
|
+
return undefined;
|
|
1527
|
+
}
|
|
1528
|
+
return (AXConversationMessageUtilsService.normalizeOptionalMediaValue(participant?.icon) ??
|
|
1529
|
+
AXConversationMessageUtilsService.getConversationAvatarIcon(conversation));
|
|
1524
1530
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1531
|
+
/**
|
|
1532
|
+
* Get initials from name
|
|
1533
|
+
*/
|
|
1534
|
+
static getInitials(name) {
|
|
1535
|
+
if (!name)
|
|
1536
|
+
return '?';
|
|
1537
|
+
return name
|
|
1538
|
+
.split(' ')
|
|
1539
|
+
.map((n) => n[0])
|
|
1540
|
+
.join('')
|
|
1541
|
+
.toUpperCase()
|
|
1542
|
+
.substring(0, 2);
|
|
1530
1543
|
}
|
|
1531
|
-
|
|
1532
|
-
|
|
1544
|
+
/**
|
|
1545
|
+
* Type guard for text payload
|
|
1546
|
+
*/
|
|
1547
|
+
static isTextPayload(payload) {
|
|
1548
|
+
return 'text' in payload && typeof payload.text === 'string';
|
|
1533
1549
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Get message text content
|
|
1552
|
+
*/
|
|
1553
|
+
static getMessageText(message) {
|
|
1554
|
+
if (message.type === 'text' && AXConversationMessageUtilsService.isTextPayload(message.payload)) {
|
|
1555
|
+
return message.payload.text;
|
|
1556
|
+
}
|
|
1557
|
+
return `[${message.type}]`;
|
|
1542
1558
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
return;
|
|
1559
|
-
let typingUsers = [...conversation.status.typingUsers];
|
|
1560
|
-
if (isTyping) {
|
|
1561
|
-
if (!typingUsers.includes(userId)) {
|
|
1562
|
-
typingUsers.push(userId);
|
|
1559
|
+
/**
|
|
1560
|
+
* Format message preview text
|
|
1561
|
+
*/
|
|
1562
|
+
static getPreviewText(message, maxLength = 50) {
|
|
1563
|
+
// Handle different message types
|
|
1564
|
+
switch (message.type) {
|
|
1565
|
+
case 'text': {
|
|
1566
|
+
const textPayload = message.payload;
|
|
1567
|
+
const text = textPayload.text || '';
|
|
1568
|
+
const truncatedText = text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
|
|
1569
|
+
return {
|
|
1570
|
+
value: truncatedText,
|
|
1571
|
+
type: 'text',
|
|
1572
|
+
icon: 'fa-light fa-message',
|
|
1573
|
+
};
|
|
1563
1574
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
updateParticipantPresence(userId, status, lastSeen) {
|
|
1577
|
-
this._conversations.update((conversations) => {
|
|
1578
|
-
const newConversations = new Map(conversations);
|
|
1579
|
-
for (const [id, conv] of conversations) {
|
|
1580
|
-
const participant = conv.participants.find((p) => p.id === userId);
|
|
1581
|
-
if (participant) {
|
|
1582
|
-
const updatedConversation = {
|
|
1583
|
-
...conv,
|
|
1584
|
-
participants: conv.participants.map((p) => (p.id === userId ? { ...p, status, lastSeen } : p)),
|
|
1585
|
-
status: conv.type === 'private' ? { ...conv.status, presence: status, lastSeen } : conv.status,
|
|
1586
|
-
};
|
|
1587
|
-
newConversations.set(id, updatedConversation);
|
|
1588
|
-
}
|
|
1575
|
+
case 'image': {
|
|
1576
|
+
const imagePayload = normalizeImagePayload(message.payload);
|
|
1577
|
+
const first = imagePayload.images[0];
|
|
1578
|
+
const label = imagePayload.caption?.trim() ||
|
|
1579
|
+
(imagePayload.images && imagePayload.images.length > 1
|
|
1580
|
+
? `${imagePayload.images.length} images`
|
|
1581
|
+
: '');
|
|
1582
|
+
return {
|
|
1583
|
+
value: label || first?.thumbnailUrl || first?.url || '',
|
|
1584
|
+
type: 'image',
|
|
1585
|
+
icon: 'fa-light fa-image',
|
|
1586
|
+
};
|
|
1589
1587
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
this._conversationMessages.update((map) => {
|
|
1601
|
-
const newMap = new Map(map);
|
|
1602
|
-
const existing = newMap.get(normalized.conversationId) || [];
|
|
1603
|
-
if (existing.includes(normalized.id)) {
|
|
1604
|
-
return newMap;
|
|
1588
|
+
case 'video': {
|
|
1589
|
+
const videoPayload = normalizeVideoPayload(message.payload);
|
|
1590
|
+
const first = videoPayload.videos[0];
|
|
1591
|
+
const label = videoPayload.caption?.trim() ||
|
|
1592
|
+
(videoPayload.videos.length > 1 ? `${videoPayload.videos.length} videos` : '');
|
|
1593
|
+
return {
|
|
1594
|
+
value: label || first?.url || '',
|
|
1595
|
+
type: 'video',
|
|
1596
|
+
icon: 'fa-light fa-video',
|
|
1597
|
+
};
|
|
1605
1598
|
}
|
|
1606
|
-
|
|
1607
|
-
const
|
|
1608
|
-
const
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
/**
|
|
1618
|
-
* Replace all messages for a conversation (initial page load).
|
|
1619
|
-
*/
|
|
1620
|
-
setConversationMessages(conversationId, messages) {
|
|
1621
|
-
const sorted = [...messages].map(withNormalizedPayload).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
1622
|
-
const sortedIds = sorted.map((m) => m.id);
|
|
1623
|
-
this._messages.update((msgs) => {
|
|
1624
|
-
const newMessages = new Map(msgs);
|
|
1625
|
-
const previousIds = this._conversationMessages().get(conversationId) || [];
|
|
1626
|
-
for (const id of previousIds) {
|
|
1627
|
-
newMessages.delete(id);
|
|
1599
|
+
case 'audio': {
|
|
1600
|
+
const audioPayload = normalizeAudioPayload(message.payload);
|
|
1601
|
+
const names = audioPayload.audios.map((a) => a.title).filter(Boolean).join(', ');
|
|
1602
|
+
const first = audioPayload.audios[0];
|
|
1603
|
+
const label = audioPayload.caption?.trim() ||
|
|
1604
|
+
(audioPayload.audios.length > 1 ? `${audioPayload.audios.length} audio files` : names);
|
|
1605
|
+
return {
|
|
1606
|
+
value: label || first?.url || '',
|
|
1607
|
+
type: 'audio',
|
|
1608
|
+
icon: 'fa-light fa-music',
|
|
1609
|
+
};
|
|
1628
1610
|
}
|
|
1629
|
-
|
|
1630
|
-
|
|
1611
|
+
case 'voice': {
|
|
1612
|
+
const voicePayload = message.payload;
|
|
1613
|
+
return {
|
|
1614
|
+
value: voicePayload.url || '',
|
|
1615
|
+
type: 'voice',
|
|
1616
|
+
icon: 'fa-light fa-microphone',
|
|
1617
|
+
};
|
|
1631
1618
|
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
if (messages.length === 0)
|
|
1643
|
-
return;
|
|
1644
|
-
const normalized = messages.map(withNormalizedPayload);
|
|
1645
|
-
this._messages.update((msgs) => {
|
|
1646
|
-
const newMessages = new Map(msgs);
|
|
1647
|
-
normalized.forEach((msg) => newMessages.set(msg.id, msg));
|
|
1648
|
-
return newMessages;
|
|
1649
|
-
});
|
|
1650
|
-
const conversationGroups = new Map();
|
|
1651
|
-
normalized.forEach((msg) => {
|
|
1652
|
-
const existing = conversationGroups.get(msg.conversationId) || [];
|
|
1653
|
-
existing.push(msg.id);
|
|
1654
|
-
conversationGroups.set(msg.conversationId, existing);
|
|
1655
|
-
});
|
|
1656
|
-
this._conversationMessages.update((map) => {
|
|
1657
|
-
const newMap = new Map(map);
|
|
1658
|
-
for (const [conversationId, newMsgIds] of conversationGroups) {
|
|
1659
|
-
const existing = newMap.get(conversationId) || [];
|
|
1660
|
-
const idSet = new Set(existing);
|
|
1661
|
-
const merged = [...existing];
|
|
1662
|
-
for (const id of newMsgIds) {
|
|
1663
|
-
if (!idSet.has(id)) {
|
|
1664
|
-
merged.push(id);
|
|
1665
|
-
idSet.add(id);
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
const sorted = merged.sort((a, b) => {
|
|
1669
|
-
const msgA = this._messages().get(a);
|
|
1670
|
-
const msgB = this._messages().get(b);
|
|
1671
|
-
if (!msgA || !msgB)
|
|
1672
|
-
return 0;
|
|
1673
|
-
return msgA.timestamp.getTime() - msgB.timestamp.getTime();
|
|
1674
|
-
});
|
|
1675
|
-
newMap.set(conversationId, sorted);
|
|
1619
|
+
case 'file': {
|
|
1620
|
+
const filePayload = normalizeFilePayload(message.payload);
|
|
1621
|
+
const names = filePayload.files.map((f) => f.name).join(', ');
|
|
1622
|
+
const label = filePayload.caption?.trim() ||
|
|
1623
|
+
(filePayload.files.length > 1 ? `${filePayload.files.length} files` : names);
|
|
1624
|
+
return {
|
|
1625
|
+
value: label || names,
|
|
1626
|
+
type: 'file',
|
|
1627
|
+
icon: 'fa-light fa-file',
|
|
1628
|
+
};
|
|
1676
1629
|
}
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1630
|
+
case 'location': {
|
|
1631
|
+
const locationPayload = message.payload;
|
|
1632
|
+
return {
|
|
1633
|
+
value: locationPayload.latitude && locationPayload.longitude
|
|
1634
|
+
? `${locationPayload.latitude},${locationPayload.longitude}`
|
|
1635
|
+
: '',
|
|
1636
|
+
type: 'location',
|
|
1637
|
+
icon: 'fa-light fa-location-dot',
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
case 'sticker': {
|
|
1641
|
+
const stickerPayload = message.payload;
|
|
1642
|
+
return {
|
|
1643
|
+
value: stickerPayload.url || '',
|
|
1644
|
+
type: 'sticker',
|
|
1645
|
+
icon: 'fa-light fa-face-smile',
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
default:
|
|
1649
|
+
return {
|
|
1650
|
+
value: '',
|
|
1651
|
+
type: message.type,
|
|
1652
|
+
icon: 'fa-light fa-message',
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1684
1655
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1656
|
+
/**
|
|
1657
|
+
* Check if message is from current user
|
|
1658
|
+
*/
|
|
1659
|
+
static isOwnMessage(message, currentUserId) {
|
|
1660
|
+
return message.senderId === currentUserId;
|
|
1688
1661
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1662
|
+
/**
|
|
1663
|
+
* Get message status icon class (Font Awesome)
|
|
1664
|
+
*/
|
|
1665
|
+
static getStatusIcon(message) {
|
|
1666
|
+
switch (message.status) {
|
|
1667
|
+
case 'sending':
|
|
1668
|
+
return 'fa-light fa-clock';
|
|
1669
|
+
case 'sent':
|
|
1670
|
+
return 'fa-light fa-check';
|
|
1671
|
+
case 'delivered':
|
|
1672
|
+
return 'fa-light fa-check-double';
|
|
1673
|
+
case 'read':
|
|
1674
|
+
return 'fa-light fa-check-double';
|
|
1675
|
+
case 'failed':
|
|
1676
|
+
return 'fa-light fa-circle-exclamation';
|
|
1677
|
+
default:
|
|
1678
|
+
return 'fa-light fa-check';
|
|
1696
1679
|
}
|
|
1697
|
-
this.addMessage(merged);
|
|
1698
|
-
}
|
|
1699
|
-
deleteMessage(messageId) {
|
|
1700
|
-
const message = this._messages().get(messageId);
|
|
1701
|
-
if (!message)
|
|
1702
|
-
return;
|
|
1703
|
-
this._messages.update((messages) => {
|
|
1704
|
-
const newMessages = new Map(messages);
|
|
1705
|
-
newMessages.delete(messageId);
|
|
1706
|
-
return newMessages;
|
|
1707
|
-
});
|
|
1708
|
-
this._conversationMessages.update((map) => {
|
|
1709
|
-
const newMap = new Map(map);
|
|
1710
|
-
const existing = newMap.get(message.conversationId);
|
|
1711
|
-
if (existing) {
|
|
1712
|
-
newMap.set(message.conversationId, existing.filter((id) => id !== messageId));
|
|
1713
|
-
}
|
|
1714
|
-
return newMap;
|
|
1715
|
-
});
|
|
1716
|
-
}
|
|
1717
|
-
cleanupOldMessages() {
|
|
1718
|
-
const totalMessages = this._messages().size;
|
|
1719
|
-
if (totalMessages > this.config.maxTotalMessages) {
|
|
1720
|
-
const allMessages = Array.from(this._messages().values()).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
1721
|
-
const toKeep = allMessages.slice(0, this.config.maxTotalMessages);
|
|
1722
|
-
const toKeepIds = new Set(toKeep.map((m) => m.id));
|
|
1723
|
-
this._messages.update(() => {
|
|
1724
|
-
const newMessages = new Map();
|
|
1725
|
-
toKeep.forEach((msg) => newMessages.set(msg.id, msg));
|
|
1726
|
-
return newMessages;
|
|
1727
|
-
});
|
|
1728
|
-
this._conversationMessages.update((map) => {
|
|
1729
|
-
const newMap = new Map(map);
|
|
1730
|
-
for (const [convId, messageIds] of newMap) {
|
|
1731
|
-
const filteredIds = messageIds.filter((id) => toKeepIds.has(id));
|
|
1732
|
-
newMap.set(convId, filteredIds);
|
|
1733
|
-
}
|
|
1734
|
-
return newMap;
|
|
1735
|
-
});
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
cleanupConversationMessages() {
|
|
1739
|
-
const maxMessages = this.config.maxMessagesPerConversation;
|
|
1740
|
-
const idsToRemove = [];
|
|
1741
|
-
this._conversationMessages.update((map) => {
|
|
1742
|
-
const newMap = new Map(map);
|
|
1743
|
-
for (const [convId, messageIds] of newMap) {
|
|
1744
|
-
if (messageIds.length > maxMessages) {
|
|
1745
|
-
idsToRemove.push(...messageIds.slice(0, messageIds.length - maxMessages));
|
|
1746
|
-
newMap.set(convId, messageIds.slice(-maxMessages));
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
return newMap;
|
|
1750
|
-
});
|
|
1751
|
-
if (idsToRemove.length > 0) {
|
|
1752
|
-
this._messages.update((messages) => {
|
|
1753
|
-
const newMessages = new Map(messages);
|
|
1754
|
-
idsToRemove.forEach((id) => newMessages.delete(id));
|
|
1755
|
-
return newMessages;
|
|
1756
|
-
});
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
/**
|
|
1762
|
-
* Error Handler Service
|
|
1763
|
-
* Centralized error handling and logging
|
|
1764
|
-
*/
|
|
1765
|
-
/**
|
|
1766
|
-
* Error Handler Service
|
|
1767
|
-
*/
|
|
1768
|
-
class AXErrorHandlerService {
|
|
1769
|
-
constructor() {
|
|
1770
|
-
this.injectedConfig = inject(ERROR_HANDLER_CONFIG);
|
|
1771
|
-
this._errors$ = new Subject();
|
|
1772
|
-
this._config = {
|
|
1773
|
-
logToConsole: true,
|
|
1774
|
-
showUserMessages: true,
|
|
1775
|
-
autoRetry: false,
|
|
1776
|
-
maxRetries: 3,
|
|
1777
|
-
};
|
|
1778
|
-
/** Error stream */
|
|
1779
|
-
this.errors$ = this._errors$.asObservable();
|
|
1780
|
-
this.configure(this.injectedConfig);
|
|
1781
1680
|
}
|
|
1782
1681
|
/**
|
|
1783
|
-
*
|
|
1682
|
+
* Check if message should show avatar
|
|
1784
1683
|
*/
|
|
1785
|
-
|
|
1786
|
-
|
|
1684
|
+
static shouldShowAvatar(message, previousMessage, conversation) {
|
|
1685
|
+
// Always show avatar for group conversations
|
|
1686
|
+
if (conversation.type === 'group' || conversation.type === 'channel') {
|
|
1687
|
+
// Don't show if same sender as previous message within 5 minutes
|
|
1688
|
+
if (previousMessage && previousMessage.senderId === message.senderId) {
|
|
1689
|
+
const timeDiff = message.timestamp.getTime() - previousMessage.timestamp.getTime();
|
|
1690
|
+
return timeDiff > 5 * 60 * 1000; // 5 minutes
|
|
1691
|
+
}
|
|
1692
|
+
return true;
|
|
1693
|
+
}
|
|
1694
|
+
return false;
|
|
1787
1695
|
}
|
|
1788
1696
|
/**
|
|
1789
|
-
*
|
|
1697
|
+
* Group messages by sender for consecutive messages
|
|
1790
1698
|
*/
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1699
|
+
static shouldGroupWithPrevious(message, previousMessage) {
|
|
1700
|
+
if (!previousMessage)
|
|
1701
|
+
return false;
|
|
1702
|
+
// Same sender
|
|
1703
|
+
if (message.senderId !== previousMessage.senderId)
|
|
1704
|
+
return false;
|
|
1705
|
+
// Within 5 minutes
|
|
1706
|
+
const timeDiff = message.timestamp.getTime() - previousMessage.timestamp.getTime();
|
|
1707
|
+
return timeDiff < 5 * 60 * 1000;
|
|
1795
1708
|
}
|
|
1796
1709
|
/**
|
|
1797
|
-
*
|
|
1710
|
+
* Get conversation status for avatar (private conversations only)
|
|
1798
1711
|
*/
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
return conversationError;
|
|
1803
|
-
}
|
|
1804
|
-
publish(conversationError) {
|
|
1805
|
-
this._errors$.next(conversationError);
|
|
1806
|
-
if (this._config.logToConsole) {
|
|
1807
|
-
this.logError(conversationError);
|
|
1808
|
-
}
|
|
1809
|
-
if (this._config.customHandler) {
|
|
1810
|
-
this._config.customHandler(conversationError);
|
|
1712
|
+
static getConversationStatus(conversation) {
|
|
1713
|
+
if (conversation.type === 'private') {
|
|
1714
|
+
return conversation.status.presence;
|
|
1811
1715
|
}
|
|
1716
|
+
return undefined;
|
|
1812
1717
|
}
|
|
1813
1718
|
/**
|
|
1814
|
-
*
|
|
1719
|
+
* Get typing indicator text for conversation
|
|
1815
1720
|
*/
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1721
|
+
static getTypingText(conversation) {
|
|
1722
|
+
const typingUsers = conversation.status.typingUsers;
|
|
1723
|
+
if (typingUsers.length === 0)
|
|
1724
|
+
return '';
|
|
1725
|
+
if (conversation.type === 'private') {
|
|
1726
|
+
return translateSync('@acorex:chat.status.typing');
|
|
1819
1727
|
}
|
|
1820
|
-
const
|
|
1821
|
-
|
|
1822
|
-
(
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
return {
|
|
1828
|
-
code,
|
|
1829
|
-
message,
|
|
1830
|
-
severity: 'error',
|
|
1831
|
-
operation,
|
|
1832
|
-
originalError: error,
|
|
1833
|
-
statusCode,
|
|
1834
|
-
context,
|
|
1835
|
-
timestamp: new Date(),
|
|
1836
|
-
handled: false,
|
|
1837
|
-
recoverySuggestions: this.getDefaultRecoverySuggestions(code),
|
|
1838
|
-
};
|
|
1839
|
-
}
|
|
1840
|
-
conversationErrorFromApi(apiError, operation, context) {
|
|
1841
|
-
return {
|
|
1842
|
-
code: apiError.code,
|
|
1843
|
-
message: apiError.message,
|
|
1844
|
-
severity: this.determineSeverity(apiError.statusCode),
|
|
1845
|
-
operation,
|
|
1846
|
-
originalError: apiError,
|
|
1847
|
-
statusCode: apiError.statusCode,
|
|
1848
|
-
context,
|
|
1849
|
-
timestamp: apiError.timestamp ?? new Date(),
|
|
1850
|
-
handled: false,
|
|
1851
|
-
recoverySuggestions: this.getRecoverySuggestions(apiError),
|
|
1852
|
-
};
|
|
1853
|
-
}
|
|
1854
|
-
isApiError(error) {
|
|
1855
|
-
return (typeof error === 'object' &&
|
|
1856
|
-
error !== null &&
|
|
1857
|
-
'code' in error &&
|
|
1858
|
-
'message' in error &&
|
|
1859
|
-
typeof error.code === 'string' &&
|
|
1860
|
-
typeof error.message === 'string');
|
|
1728
|
+
const firstUser = conversation.participants.find((p) => p.id === typingUsers[0]);
|
|
1729
|
+
if (typingUsers.length === 1) {
|
|
1730
|
+
return translateSync('@acorex:chat.status.user-is-typing', {
|
|
1731
|
+
params: { userName: firstUser?.name || translateSync('@acorex:chat.fallbacks.someone') },
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
return translateSync('@acorex:chat.status.people-typing', { params: { count: typingUsers.length } });
|
|
1861
1735
|
}
|
|
1862
1736
|
/**
|
|
1863
|
-
*
|
|
1737
|
+
* Format last seen time
|
|
1864
1738
|
*/
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1739
|
+
static formatLastSeen(date) {
|
|
1740
|
+
const now = new Date();
|
|
1741
|
+
const diff = now.getTime() - date.getTime();
|
|
1742
|
+
const seconds = Math.floor(diff / 1000);
|
|
1743
|
+
if (seconds < 60)
|
|
1744
|
+
return translateSync('@acorex:chat.time.just-now');
|
|
1745
|
+
if (seconds < 3600)
|
|
1746
|
+
return translateSync('@acorex:chat.time.minutes-ago', { params: { count: Math.floor(seconds / 60) } });
|
|
1747
|
+
if (seconds < 86400)
|
|
1748
|
+
return translateSync('@acorex:chat.time.hours-ago', { params: { count: Math.floor(seconds / 3600) } });
|
|
1749
|
+
if (seconds < 604800)
|
|
1750
|
+
return translateSync('@acorex:chat.time.days-ago', { params: { count: Math.floor(seconds / 86400) } });
|
|
1751
|
+
return date.toLocaleDateString();
|
|
1875
1752
|
}
|
|
1876
1753
|
/**
|
|
1877
|
-
* Get
|
|
1754
|
+
* Get conversation subtitle (status or member count)
|
|
1878
1755
|
*/
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
suggestions.push('Please log in again');
|
|
1883
|
-
suggestions.push('Check if your session has expired');
|
|
1884
|
-
}
|
|
1885
|
-
else if (error.statusCode === 403 || error.code === 'FORBIDDEN') {
|
|
1886
|
-
suggestions.push('You do not have permission for this action');
|
|
1887
|
-
suggestions.push('Ask your administrator for access');
|
|
1888
|
-
}
|
|
1889
|
-
else if (error.statusCode === 404 || error.code === 'NOT_FOUND') {
|
|
1890
|
-
suggestions.push('The requested resource was not found');
|
|
1891
|
-
suggestions.push('It may have been deleted or moved');
|
|
1756
|
+
static getConversationSubtitle(conversation) {
|
|
1757
|
+
if (conversation.status.isTyping) {
|
|
1758
|
+
return AXConversationMessageUtilsService.getTypingText(conversation);
|
|
1892
1759
|
}
|
|
1893
|
-
|
|
1894
|
-
|
|
1760
|
+
switch (conversation.type) {
|
|
1761
|
+
case 'private':
|
|
1762
|
+
if (conversation.status.presence === 'online') {
|
|
1763
|
+
return translateSync('@acorex:chat.status.online');
|
|
1764
|
+
}
|
|
1765
|
+
if (conversation.status.lastSeen) {
|
|
1766
|
+
return translateSync('@acorex:chat.status.last-seen', {
|
|
1767
|
+
params: { value: AXConversationMessageUtilsService.formatLastSeen(conversation.status.lastSeen) },
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
return translateSync('@acorex:chat.status.offline');
|
|
1771
|
+
case 'group':
|
|
1772
|
+
return translateSync('@acorex:chat.members.count', { params: { count: conversation.participants.length } });
|
|
1773
|
+
case 'channel':
|
|
1774
|
+
return translateSync('@acorex:chat.members.subscribers-count', { params: { count: conversation.participants.length } });
|
|
1775
|
+
case 'bot':
|
|
1776
|
+
return translateSync('@acorex:chat.bot');
|
|
1777
|
+
default:
|
|
1778
|
+
return '';
|
|
1895
1779
|
}
|
|
1896
|
-
else if (error.statusCode && error.statusCode >= 500) {
|
|
1897
|
-
suggestions.push('Server error occurred');
|
|
1898
|
-
suggestions.push('Please try again later');
|
|
1899
|
-
suggestions.push('If the problem persists, reach out to support');
|
|
1900
|
-
}
|
|
1901
|
-
else if (error.code === 'NETWORK_ERROR') {
|
|
1902
|
-
suggestions.push('Check your internet connection');
|
|
1903
|
-
suggestions.push('Try refreshing the page');
|
|
1904
|
-
}
|
|
1905
|
-
return suggestions;
|
|
1906
|
-
}
|
|
1907
|
-
/**
|
|
1908
|
-
* Get default recovery suggestions
|
|
1909
|
-
*/
|
|
1910
|
-
getDefaultRecoverySuggestions(code) {
|
|
1911
|
-
const suggestions = [];
|
|
1912
|
-
if (code.includes('NETWORK') || code.includes('CONNECTION')) {
|
|
1913
|
-
suggestions.push('Check your internet connection');
|
|
1914
|
-
suggestions.push('Try refreshing the page');
|
|
1915
|
-
}
|
|
1916
|
-
else if (code.includes('TIMEOUT')) {
|
|
1917
|
-
suggestions.push('The operation took too long');
|
|
1918
|
-
suggestions.push('Please try again');
|
|
1919
|
-
}
|
|
1920
|
-
else {
|
|
1921
|
-
suggestions.push('Please try again');
|
|
1922
|
-
suggestions.push('If the problem persists, reach out to support');
|
|
1923
|
-
}
|
|
1924
|
-
return suggestions;
|
|
1925
|
-
}
|
|
1926
|
-
/**
|
|
1927
|
-
* Log error to console
|
|
1928
|
-
*/
|
|
1929
|
-
logError(error) {
|
|
1930
|
-
const isError = error.severity === 'critical' || error.severity === 'error';
|
|
1931
|
-
const header = `[Conversation ${error.severity.toUpperCase()}] ${error.operation}:`;
|
|
1932
|
-
if (isError) {
|
|
1933
|
-
console.error(header, error.message, error.context || '');
|
|
1934
|
-
}
|
|
1935
|
-
else {
|
|
1936
|
-
console.warn(header, error.message, error.context || '');
|
|
1937
|
-
}
|
|
1938
|
-
if (error.originalError && error.severity !== 'info') {
|
|
1939
|
-
console.error('Original error:', error.originalError);
|
|
1940
|
-
}
|
|
1941
|
-
if (error.recoverySuggestions && error.recoverySuggestions.length > 0) {
|
|
1942
|
-
console.info('Recovery suggestions:', error.recoverySuggestions);
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
/**
|
|
1946
|
-
* Get user-friendly error message
|
|
1947
|
-
*/
|
|
1948
|
-
getUserFriendlyMessage(error) {
|
|
1949
|
-
// Map technical errors to user-friendly messages
|
|
1950
|
-
const messageMap = {
|
|
1951
|
-
NETWORK_ERROR: 'Unable to connect. Please check your internet connection.',
|
|
1952
|
-
UNAUTHORIZED: 'You are not authorized. Please log in again.',
|
|
1953
|
-
FORBIDDEN: 'You do not have permission to perform this action.',
|
|
1954
|
-
NOT_FOUND: 'The requested item could not be found.',
|
|
1955
|
-
RATE_LIMIT_EXCEEDED: 'Too many requests. Please slow down.',
|
|
1956
|
-
VALIDATION_ERROR: 'The provided data is invalid.',
|
|
1957
|
-
SERVER_ERROR: 'A server error occurred. Please try again later.',
|
|
1958
|
-
TIMEOUT: 'The operation timed out. Please try again.',
|
|
1959
|
-
};
|
|
1960
|
-
return messageMap[error.code] || error.message || 'An unexpected error occurred.';
|
|
1961
|
-
}
|
|
1962
|
-
/**
|
|
1963
|
-
* Check if error is retryable
|
|
1964
|
-
*/
|
|
1965
|
-
isRetryable(error) {
|
|
1966
|
-
const retryableCodes = ['NETWORK_ERROR', 'TIMEOUT', 'RATE_LIMIT_EXCEEDED', 'SERVER_ERROR'];
|
|
1967
|
-
const retryableStatusCodes = [408, 429, 500, 502, 503, 504];
|
|
1968
|
-
return (retryableCodes.includes(error.code) ||
|
|
1969
|
-
(error.statusCode !== undefined && retryableStatusCodes.includes(error.statusCode)));
|
|
1970
|
-
}
|
|
1971
|
-
/**
|
|
1972
|
-
* Execute an operation with automatic retry logic
|
|
1973
|
-
* @param operation - The async operation to execute
|
|
1974
|
-
* @param operationName - Name of the operation for error tracking
|
|
1975
|
-
* @param context - Additional context for error handling
|
|
1976
|
-
* @returns Promise resolving to the operation result
|
|
1977
|
-
* @throws {AXConversationError} If all retries fail
|
|
1978
|
-
*/
|
|
1979
|
-
async executeWithRetry(operation, operationName, context) {
|
|
1980
|
-
if (!this._config.autoRetry) {
|
|
1981
|
-
// If auto-retry is disabled, just execute once
|
|
1982
|
-
return operation();
|
|
1983
|
-
}
|
|
1984
|
-
const maxRetries = this._config.maxRetries ?? 3;
|
|
1985
|
-
let lastError;
|
|
1986
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1987
|
-
try {
|
|
1988
|
-
return await operation();
|
|
1989
|
-
}
|
|
1990
|
-
catch (error) {
|
|
1991
|
-
lastError = error;
|
|
1992
|
-
const conversationError = this.handle(error, operationName, { ...context, attempt });
|
|
1993
|
-
// Don't retry if error is not retryable or if this was the last attempt
|
|
1994
|
-
if (!this.isRetryable(conversationError) || attempt >= maxRetries) {
|
|
1995
|
-
throw conversationError;
|
|
1996
|
-
}
|
|
1997
|
-
// Exponential backoff: 1s, 2s, 4s, 8s...
|
|
1998
|
-
const delayMs = Math.pow(2, attempt) * 1000;
|
|
1999
|
-
await this.delay(delayMs);
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
throw this.handle(lastError, operationName, context);
|
|
2003
|
-
}
|
|
2004
|
-
/**
|
|
2005
|
-
* Delay helper for retry backoff
|
|
2006
|
-
* @param ms - Milliseconds to delay
|
|
2007
|
-
*/
|
|
2008
|
-
delay(ms) {
|
|
2009
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2010
1780
|
}
|
|
2011
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXErrorHandlerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2012
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXErrorHandlerService, providedIn: 'root' }); }
|
|
2013
|
-
}
|
|
2014
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXErrorHandlerService, decorators: [{
|
|
2015
|
-
type: Injectable,
|
|
2016
|
-
args: [{
|
|
2017
|
-
providedIn: 'root',
|
|
2018
|
-
}]
|
|
2019
|
-
}], ctorParameters: () => [] });
|
|
2020
|
-
|
|
2021
|
-
function formatFileSize(bytes) {
|
|
2022
|
-
if (bytes < 1024)
|
|
2023
|
-
return `${bytes} B`;
|
|
2024
|
-
if (bytes < 1024 * 1024)
|
|
2025
|
-
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
2026
|
-
if (bytes < 1024 * 1024 * 1024)
|
|
2027
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
2028
|
-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
2029
|
-
}
|
|
2030
|
-
function formatDuration(seconds) {
|
|
2031
|
-
const mins = Math.floor(seconds / 60);
|
|
2032
|
-
const secs = Math.floor(seconds % 60);
|
|
2033
|
-
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
2034
1781
|
}
|
|
2035
1782
|
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
title: 'Audio',
|
|
2041
|
-
};
|
|
2042
|
-
const AUDIO_UTILITY = {
|
|
2043
|
-
preview: 'preview',
|
|
2044
|
-
duration: 'duration',
|
|
2045
|
-
formatSize: 'formatSize',
|
|
2046
|
-
formatDuration: 'formatDuration',
|
|
2047
|
-
createLocalPreviewUrl: 'createLocalPreviewUrl',
|
|
2048
|
-
};
|
|
2049
|
-
function blobUrl$4(ctx, blob) {
|
|
2050
|
-
if (!isPlatformBrowser(ctx.platformId)) {
|
|
2051
|
-
return '';
|
|
1783
|
+
/** Other participant in a private chat (excludes the current user). */
|
|
1784
|
+
function resolvePrivatePeerUserId(conversation, currentUserId) {
|
|
1785
|
+
if (conversation.type !== 'private') {
|
|
1786
|
+
return undefined;
|
|
2052
1787
|
}
|
|
2053
|
-
|
|
1788
|
+
const currentId = currentUserId ?? 'current-user';
|
|
1789
|
+
return conversation.participants.find((participant) => participant.id !== currentId)?.id;
|
|
2054
1790
|
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
}
|
|
2059
|
-
return new Promise((resolve, reject) => {
|
|
2060
|
-
const el = document.createElement('audio');
|
|
2061
|
-
el.preload = 'metadata';
|
|
2062
|
-
el.onloadedmetadata = () => {
|
|
2063
|
-
URL.revokeObjectURL(el.src);
|
|
2064
|
-
resolve(el.duration);
|
|
2065
|
-
};
|
|
2066
|
-
el.onerror = () => {
|
|
2067
|
-
URL.revokeObjectURL(el.src);
|
|
2068
|
-
reject(new Error('Failed to load audio metadata'));
|
|
2069
|
-
};
|
|
2070
|
-
el.src = URL.createObjectURL(file);
|
|
2071
|
-
});
|
|
1791
|
+
/** Whether `auto` kind should render a user avatar for this conversation. */
|
|
1792
|
+
function shouldUseUserAvatarForConversation(conversation, currentUserId) {
|
|
1793
|
+
return conversation.type === 'private' && !!resolvePrivatePeerUserId(conversation, currentUserId);
|
|
2072
1794
|
}
|
|
2073
|
-
function
|
|
1795
|
+
function resolveUserAvatarDisplay(userId, conversation, message) {
|
|
1796
|
+
if (message && conversation) {
|
|
1797
|
+
return {
|
|
1798
|
+
name: AXConversationMessageUtilsService.getSenderName(message, conversation),
|
|
1799
|
+
avatar: AXConversationMessageUtilsService.getSenderAvatar(message, conversation),
|
|
1800
|
+
icon: AXConversationMessageUtilsService.getSenderAvatarIcon(message, conversation),
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
const participant = conversation?.participants.find((p) => p.id === userId);
|
|
2074
1804
|
return {
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
[AUDIO_UTILITY.formatDuration]: (_ctx, seconds) => formatDuration(seconds),
|
|
2079
|
-
[AUDIO_UTILITY.createLocalPreviewUrl]: (ctx, source) => blobUrl$4(ctx, source),
|
|
1805
|
+
name: participant?.name ?? userId,
|
|
1806
|
+
avatar: participant?.avatar,
|
|
1807
|
+
icon: participant?.icon,
|
|
2080
1808
|
};
|
|
2081
1809
|
}
|
|
2082
|
-
function
|
|
2083
|
-
const
|
|
1810
|
+
function resolveConversationAvatarDisplay(conversation, currentUserId) {
|
|
1811
|
+
const title = currentUserId !== undefined
|
|
1812
|
+
? resolveConversationTitleForViewer(conversation, currentUserId)
|
|
1813
|
+
: conversation.title;
|
|
2084
1814
|
return {
|
|
2085
|
-
name:
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
icon: presentation.icon,
|
|
2089
|
-
validations: {
|
|
2090
|
-
mimeTypes: ['audio/*'],
|
|
2091
|
-
minSize: 1,
|
|
2092
|
-
maxSize: 50 * MB$4,
|
|
2093
|
-
},
|
|
2094
|
-
extensions: [
|
|
2095
|
-
{ name: 'mp3', title: 'MP3' },
|
|
2096
|
-
{ name: 'wav', title: 'WAV', validations: { maxSize: 30 * MB$4 } },
|
|
2097
|
-
{ name: 'ogg', title: 'OGG' },
|
|
2098
|
-
{ name: 'm4a', title: 'M4A' },
|
|
2099
|
-
],
|
|
2100
|
-
utilities: conversationAudioUtilities(),
|
|
2101
|
-
copy: (payload) => {
|
|
2102
|
-
const audio = normalizeAudioPayload(payload);
|
|
2103
|
-
const caption = audio.caption?.trim();
|
|
2104
|
-
const items = audio.audios.map((item) => ({
|
|
2105
|
-
url: item.url?.trim(),
|
|
2106
|
-
title: item.title?.trim(),
|
|
2107
|
-
}));
|
|
2108
|
-
return {
|
|
2109
|
-
text: caption ?? '',
|
|
2110
|
-
meta: { kind: 'audio', caption, items, count: audio.audios.length },
|
|
2111
|
-
};
|
|
2112
|
-
},
|
|
1815
|
+
name: title,
|
|
1816
|
+
avatar: AXConversationMessageUtilsService.getConversationAvatar(conversation),
|
|
1817
|
+
icon: AXConversationMessageUtilsService.getConversationAvatarIcon(conversation),
|
|
2113
1818
|
};
|
|
2114
1819
|
}
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
1820
|
+
|
|
1821
|
+
const GENERIC_PRIVATE_TITLES = new Set(['', 'new chat', 'new conversation']);
|
|
1822
|
+
/** True when the stored title is a placeholder, not a user-defined name. */
|
|
1823
|
+
function isGenericPrivateConversationTitle(title) {
|
|
1824
|
+
return GENERIC_PRIVATE_TITLES.has((title ?? '').trim().toLowerCase());
|
|
1825
|
+
}
|
|
1826
|
+
/** Other participant in a private 1v1 chat (excludes the current viewer). */
|
|
1827
|
+
function resolvePrivatePeerParticipant(conversation, currentUserId) {
|
|
1828
|
+
if (conversation.type !== 'private') {
|
|
1829
|
+
return undefined;
|
|
2118
1830
|
}
|
|
2119
|
-
|
|
2120
|
-
|
|
1831
|
+
const peerId = resolvePrivatePeerUserId(conversation, currentUserId);
|
|
1832
|
+
if (!peerId) {
|
|
1833
|
+
return undefined;
|
|
1834
|
+
}
|
|
1835
|
+
return conversation.participants.find((participant) => participant.id === peerId);
|
|
2121
1836
|
}
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
const item = {
|
|
2130
|
-
mediaId: result.mediaId,
|
|
2131
|
-
mimeType: result.mimeType,
|
|
2132
|
-
size: result.size,
|
|
2133
|
-
duration,
|
|
2134
|
-
title: file.name,
|
|
2135
|
-
metadata: result.metadata,
|
|
2136
|
-
};
|
|
2137
|
-
if (url) {
|
|
2138
|
-
item.url = url;
|
|
1837
|
+
/**
|
|
1838
|
+
* Resolves the display title for the current viewer.
|
|
1839
|
+
* Private 1v1 chats show the other participant's name when no custom title is set.
|
|
1840
|
+
*/
|
|
1841
|
+
function resolveConversationTitleForViewer(conversation, currentUserId) {
|
|
1842
|
+
if (conversation.type !== 'private') {
|
|
1843
|
+
return conversation.title;
|
|
2139
1844
|
}
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
const base = normalizeAudioPayload(payload);
|
|
2144
|
-
const audios = [...base.audios];
|
|
2145
|
-
const i = audios.findIndex((x) => !x.mediaId && !resolvePersistableMediaUrl(x.url));
|
|
2146
|
-
const slot = i >= 0 ? audios[i] : undefined;
|
|
2147
|
-
const url = resolvePersistableMediaUrl(result.url);
|
|
2148
|
-
const next = {
|
|
2149
|
-
...(slot ?? { duration: 0 }),
|
|
2150
|
-
mediaId: result.mediaId,
|
|
2151
|
-
mimeType: result.mimeType,
|
|
2152
|
-
size: result.size,
|
|
2153
|
-
metadata: result.metadata,
|
|
2154
|
-
};
|
|
2155
|
-
if (url) {
|
|
2156
|
-
next.url = url;
|
|
1845
|
+
const peer = resolvePrivatePeerParticipant(conversation, currentUserId);
|
|
1846
|
+
if (peer?.name) {
|
|
1847
|
+
return peer.name;
|
|
2157
1848
|
}
|
|
2158
|
-
if (
|
|
2159
|
-
|
|
2160
|
-
else
|
|
2161
|
-
audios.push(next);
|
|
2162
|
-
return { ...base, type: 'audio', audios };
|
|
2163
|
-
}
|
|
2164
|
-
function applyAudioLocalPreview(payload, localUrl, mimeType = 'application/octet-stream') {
|
|
2165
|
-
const base = normalizeAudioPayload(payload);
|
|
2166
|
-
const audios = [...base.audios];
|
|
2167
|
-
const slot = audios.findIndex((x) => !x.mediaId && !resolvePersistableMediaUrl(x.url));
|
|
2168
|
-
const preview = { url: localUrl, duration: 0, mimeType };
|
|
2169
|
-
if (slot >= 0) {
|
|
2170
|
-
audios[slot] = { ...audios[slot], ...preview };
|
|
1849
|
+
if (isGenericPrivateConversationTitle(conversation.title)) {
|
|
1850
|
+
return conversation.title || 'New Chat';
|
|
2171
1851
|
}
|
|
2172
|
-
|
|
2173
|
-
|
|
1852
|
+
return conversation.title;
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Returns a viewer-scoped copy of a conversation with dynamic private title/avatar/icon.
|
|
1856
|
+
* Does not mutate the source object.
|
|
1857
|
+
*/
|
|
1858
|
+
function resolveConversationForViewer(conversation, currentUserId) {
|
|
1859
|
+
if (conversation.type !== 'private' || !currentUserId) {
|
|
1860
|
+
return conversation;
|
|
2174
1861
|
}
|
|
2175
|
-
|
|
2176
|
-
|
|
1862
|
+
const peer = resolvePrivatePeerParticipant(conversation, currentUserId);
|
|
1863
|
+
if (!peer) {
|
|
1864
|
+
return conversation;
|
|
2177
1865
|
}
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
title: 'Image',
|
|
2186
|
-
};
|
|
2187
|
-
const IMAGE_UTILITY = {
|
|
2188
|
-
preview: 'preview',
|
|
2189
|
-
formatSize: 'formatSize',
|
|
2190
|
-
createLocalPreviewUrl: 'createLocalPreviewUrl',
|
|
2191
|
-
};
|
|
2192
|
-
function blobUrl$3(ctx, blob) {
|
|
2193
|
-
if (!isPlatformBrowser(ctx.platformId)) {
|
|
2194
|
-
return '';
|
|
1866
|
+
const title = resolveConversationTitleForViewer(conversation, currentUserId);
|
|
1867
|
+
const avatar = conversation.avatar ?? peer.avatar;
|
|
1868
|
+
const icon = conversation.icon ?? peer.icon;
|
|
1869
|
+
if (title === conversation.title &&
|
|
1870
|
+
avatar === conversation.avatar &&
|
|
1871
|
+
icon === conversation.icon) {
|
|
1872
|
+
return conversation;
|
|
2195
1873
|
}
|
|
2196
|
-
return URL.createObjectURL(blob);
|
|
2197
|
-
}
|
|
2198
|
-
function conversationImageUtilities() {
|
|
2199
|
-
return {
|
|
2200
|
-
[IMAGE_UTILITY.preview]: (ctx, file) => ctx.readAsDataUrl(file),
|
|
2201
|
-
[IMAGE_UTILITY.formatSize]: (_ctx, bytes) => formatFileSize(bytes),
|
|
2202
|
-
[IMAGE_UTILITY.createLocalPreviewUrl]: (ctx, source) => {
|
|
2203
|
-
const blob = source;
|
|
2204
|
-
if (blob.type.startsWith('image/')) {
|
|
2205
|
-
return ctx.readAsDataUrl(blob);
|
|
2206
|
-
}
|
|
2207
|
-
return blobUrl$3(ctx, blob);
|
|
2208
|
-
},
|
|
2209
|
-
};
|
|
2210
|
-
}
|
|
2211
|
-
function createConversationImageFileType() {
|
|
2212
|
-
const presentation = CONVERSATION_IMAGE_PRESENTATION;
|
|
2213
1874
|
return {
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
icon
|
|
2218
|
-
validations: {
|
|
2219
|
-
mimeTypes: ['image/*'],
|
|
2220
|
-
minSize: 1,
|
|
2221
|
-
maxSize: 100 * MB$3,
|
|
2222
|
-
},
|
|
2223
|
-
extensions: [
|
|
2224
|
-
{ name: 'jpg', title: 'JPEG' },
|
|
2225
|
-
{ name: 'jpeg', title: 'JPEG', validations: { maxSize: 5 * MB$3 } },
|
|
2226
|
-
{ name: 'png', title: 'PNG' },
|
|
2227
|
-
{ name: 'gif', title: 'GIF' },
|
|
2228
|
-
{ name: 'webp', title: 'WebP' },
|
|
2229
|
-
{ name: 'svg', title: 'SVG', validations: { maxSize: 2 * MB$3 } },
|
|
2230
|
-
],
|
|
2231
|
-
utilities: conversationImageUtilities(),
|
|
2232
|
-
copy: (payload) => {
|
|
2233
|
-
const image = normalizeImagePayload(payload);
|
|
2234
|
-
const caption = image.caption?.trim();
|
|
2235
|
-
const urls = image.images
|
|
2236
|
-
.map((item) => item.url?.trim() || item.thumbnailUrl?.trim())
|
|
2237
|
-
.filter((url) => !!url);
|
|
2238
|
-
return {
|
|
2239
|
-
text: caption ?? '',
|
|
2240
|
-
meta: { kind: 'image', caption, urls, count: image.images.length },
|
|
2241
|
-
};
|
|
2242
|
-
},
|
|
1875
|
+
...conversation,
|
|
1876
|
+
title,
|
|
1877
|
+
avatar,
|
|
1878
|
+
icon,
|
|
2243
1879
|
};
|
|
2244
1880
|
}
|
|
2245
|
-
class AXConversationImageFileTypeProvider extends AXFileTypeInfoProvider {
|
|
2246
|
-
items() {
|
|
2247
|
-
return Promise.resolve([createConversationImageFileType()]);
|
|
2248
|
-
}
|
|
2249
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationImageFileTypeProvider, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2250
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationImageFileTypeProvider, providedIn: 'root' }); }
|
|
2251
|
-
}
|
|
2252
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationImageFileTypeProvider, decorators: [{
|
|
2253
|
-
type: Injectable,
|
|
2254
|
-
args: [{ providedIn: 'root' }]
|
|
2255
|
-
}] });
|
|
2256
1881
|
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
1882
|
+
/**
|
|
1883
|
+
* Validation Utilities
|
|
1884
|
+
* Centralized validation functions for messages and user input
|
|
1885
|
+
*/
|
|
1886
|
+
/**
|
|
1887
|
+
* Validate message text content
|
|
1888
|
+
* @param text - Text to validate
|
|
1889
|
+
* @param config - Configuration for validation rules
|
|
1890
|
+
* @returns Validation result
|
|
1891
|
+
*/
|
|
1892
|
+
function validateMessageText(text, config) {
|
|
1893
|
+
// Check for empty text
|
|
1894
|
+
if (!text || text.trim().length === 0) {
|
|
1895
|
+
return {
|
|
1896
|
+
valid: false,
|
|
1897
|
+
error: 'Message text cannot be empty',
|
|
1898
|
+
errorCode: 'EMPTY_MESSAGE',
|
|
1899
|
+
};
|
|
2273
1900
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
1901
|
+
// Check minimum length
|
|
1902
|
+
const minLength = config.minMessageLength ?? 1;
|
|
1903
|
+
if (text.trim().length < minLength) {
|
|
1904
|
+
return {
|
|
1905
|
+
valid: false,
|
|
1906
|
+
error: `Message must be at least ${minLength} character(s)`,
|
|
1907
|
+
errorCode: 'MESSAGE_TOO_SHORT',
|
|
1908
|
+
};
|
|
2279
1909
|
}
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
1910
|
+
// Check maximum length
|
|
1911
|
+
const maxLength = config.maxMessageLength ?? 10000;
|
|
1912
|
+
if (text.length > maxLength) {
|
|
1913
|
+
return {
|
|
1914
|
+
valid: false,
|
|
1915
|
+
error: `Message exceeds ${maxLength} character limit`,
|
|
1916
|
+
errorCode: 'MESSAGE_TOO_LONG',
|
|
2286
1917
|
};
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
1918
|
+
}
|
|
1919
|
+
return { valid: true };
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Validate conversation ID
|
|
1923
|
+
* @param conversationId - Conversation ID to validate
|
|
1924
|
+
* @returns Validation result
|
|
1925
|
+
*/
|
|
1926
|
+
function validateConversationId(conversationId) {
|
|
1927
|
+
if (!conversationId || typeof conversationId !== 'string' || conversationId.trim().length === 0) {
|
|
1928
|
+
return {
|
|
1929
|
+
valid: false,
|
|
1930
|
+
error: 'Conversation ID is required',
|
|
1931
|
+
errorCode: 'MISSING_CONVERSATION_ID',
|
|
2290
1932
|
};
|
|
2291
|
-
|
|
2292
|
-
|
|
1933
|
+
}
|
|
1934
|
+
// Check for reasonable length
|
|
1935
|
+
if (conversationId.length > 255) {
|
|
1936
|
+
return {
|
|
1937
|
+
valid: false,
|
|
1938
|
+
error: 'Conversation ID is too long',
|
|
1939
|
+
errorCode: 'MISSING_CONVERSATION_ID',
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
return { valid: true };
|
|
2293
1943
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
};
|
|
1944
|
+
/**
|
|
1945
|
+
* Validate message type
|
|
1946
|
+
* @param type - Message type to validate
|
|
1947
|
+
* @returns Validation result
|
|
1948
|
+
*/
|
|
1949
|
+
function validateMessageType(type) {
|
|
1950
|
+
if (!type || type.trim().length === 0) {
|
|
1951
|
+
return {
|
|
1952
|
+
valid: false,
|
|
1953
|
+
error: 'Message type is required',
|
|
1954
|
+
errorCode: 'MISSING_MESSAGE_TYPE',
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
return { valid: true };
|
|
2308
1958
|
}
|
|
2309
|
-
function
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
extensions: [
|
|
2322
|
-
{ name: 'mp4', title: 'MP4' },
|
|
2323
|
-
{ name: 'webm', title: 'WebM', validations: { maxSize: 200 * MB$2 } },
|
|
2324
|
-
{ name: 'ogg', title: 'OGG' },
|
|
2325
|
-
],
|
|
2326
|
-
utilities: conversationVideoUtilities(),
|
|
2327
|
-
copy: (payload) => {
|
|
2328
|
-
const video = normalizeVideoPayload(payload);
|
|
2329
|
-
const caption = video.caption?.trim();
|
|
2330
|
-
const urls = video.videos.map((item) => item.url?.trim()).filter((url) => !!url);
|
|
1959
|
+
function validateMediaItems(items, label) {
|
|
1960
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
1961
|
+
return {
|
|
1962
|
+
valid: false,
|
|
1963
|
+
error: `${label} message must include at least one attachment`,
|
|
1964
|
+
errorCode: 'INVALID_MEDIA_PAYLOAD',
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
for (const item of items) {
|
|
1968
|
+
const ok = (typeof item.url === 'string' && item.url.length > 0) ||
|
|
1969
|
+
(typeof item.mediaId === 'string' && item.mediaId.length > 0);
|
|
1970
|
+
if (!ok) {
|
|
2331
1971
|
return {
|
|
2332
|
-
|
|
2333
|
-
|
|
1972
|
+
valid: false,
|
|
1973
|
+
error: `Each ${label.toLowerCase()} attachment must have a url or mediaId`,
|
|
1974
|
+
errorCode: 'INVALID_MEDIA_PAYLOAD',
|
|
2334
1975
|
};
|
|
2335
|
-
}
|
|
2336
|
-
};
|
|
2337
|
-
}
|
|
2338
|
-
class AXConversationVideoFileTypeProvider extends AXFileTypeInfoProvider {
|
|
2339
|
-
items() {
|
|
2340
|
-
return Promise.resolve([createConversationVideoFileType()]);
|
|
1976
|
+
}
|
|
2341
1977
|
}
|
|
2342
|
-
|
|
2343
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationVideoFileTypeProvider, providedIn: 'root' }); }
|
|
1978
|
+
return { valid: true };
|
|
2344
1979
|
}
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
'video/*',
|
|
2359
|
-
'audio/*',
|
|
2360
|
-
'application/pdf',
|
|
2361
|
-
'application/msword',
|
|
2362
|
-
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
2363
|
-
'application/vnd.ms-excel',
|
|
2364
|
-
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
2365
|
-
'application/zip',
|
|
2366
|
-
'application/x-zip-compressed',
|
|
2367
|
-
'application/x-7z-compressed',
|
|
2368
|
-
'application/vnd.rar',
|
|
2369
|
-
'application/octet-stream',
|
|
2370
|
-
'text/plain',
|
|
2371
|
-
];
|
|
2372
|
-
const FILE_UTILITY = {
|
|
2373
|
-
preview: 'preview',
|
|
2374
|
-
formatSize: 'formatSize',
|
|
2375
|
-
createLocalPreviewUrl: 'createLocalPreviewUrl',
|
|
2376
|
-
pickerCatalog: 'pickerCatalog',
|
|
2377
|
-
};
|
|
2378
|
-
function blobUrl$1(ctx, blob) {
|
|
2379
|
-
if (!isPlatformBrowser(ctx.platformId)) {
|
|
2380
|
-
return '';
|
|
1980
|
+
/**
|
|
1981
|
+
* Validate message payload
|
|
1982
|
+
* @param payload - Message payload to validate
|
|
1983
|
+
* @param type - Message type
|
|
1984
|
+
* @returns Validation result
|
|
1985
|
+
*/
|
|
1986
|
+
function validateMessagePayload(payload, type) {
|
|
1987
|
+
if (!payload) {
|
|
1988
|
+
return {
|
|
1989
|
+
valid: false,
|
|
1990
|
+
error: 'Message payload is required',
|
|
1991
|
+
errorCode: 'MISSING_PAYLOAD',
|
|
1992
|
+
};
|
|
2381
1993
|
}
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
if (
|
|
2389
|
-
return
|
|
1994
|
+
const normalized = type === 'image' || type === 'video' || type === 'audio' || type === 'file'
|
|
1995
|
+
? normalizeMessagePayload(payload)
|
|
1996
|
+
: payload;
|
|
1997
|
+
// Type-specific validation
|
|
1998
|
+
switch (type) {
|
|
1999
|
+
case 'text':
|
|
2000
|
+
if (!('text' in payload) || typeof payload.text !== 'string') {
|
|
2001
|
+
return {
|
|
2002
|
+
valid: false,
|
|
2003
|
+
error: 'Text message must have a text property',
|
|
2004
|
+
errorCode: 'INVALID_TEXT_PAYLOAD',
|
|
2005
|
+
};
|
|
2390
2006
|
}
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
title: 'ZIP',
|
|
2427
|
-
validations: {
|
|
2428
|
-
mimeTypes: ['application/zip', 'application/x-zip-compressed', 'application/octet-stream'],
|
|
2429
|
-
maxSize: 50 * MB$1,
|
|
2430
|
-
},
|
|
2431
|
-
},
|
|
2432
|
-
],
|
|
2433
|
-
utilities: conversationFileUtilities(),
|
|
2434
|
-
copy: (payload) => {
|
|
2435
|
-
const file = normalizeFilePayload(payload);
|
|
2436
|
-
const caption = file.caption?.trim();
|
|
2437
|
-
const items = file.files.map((item) => {
|
|
2438
|
-
const name = item.name?.trim();
|
|
2439
|
-
const url = item.url?.trim();
|
|
2440
|
-
const line = name && url ? `${name} — ${url}` : url || name;
|
|
2441
|
-
return { name, url, line };
|
|
2442
|
-
});
|
|
2443
|
-
return {
|
|
2444
|
-
text: caption ?? '',
|
|
2445
|
-
meta: { kind: 'file', caption, items, count: file.files.length },
|
|
2446
|
-
};
|
|
2447
|
-
},
|
|
2448
|
-
};
|
|
2449
|
-
}
|
|
2450
|
-
class AXConversationFileFileTypeProvider extends AXFileTypeInfoProvider {
|
|
2451
|
-
items() {
|
|
2452
|
-
return Promise.resolve([createConversationFileFileType()]);
|
|
2007
|
+
break;
|
|
2008
|
+
case 'image':
|
|
2009
|
+
return validateMediaItems(normalized.images, 'Image');
|
|
2010
|
+
case 'video':
|
|
2011
|
+
return validateMediaItems(normalized.videos, 'Video');
|
|
2012
|
+
case 'audio':
|
|
2013
|
+
return validateMediaItems(normalized.audios, 'Audio');
|
|
2014
|
+
case 'file':
|
|
2015
|
+
return validateMediaItems(normalized.files, 'File');
|
|
2016
|
+
case 'voice':
|
|
2017
|
+
case 'sticker': {
|
|
2018
|
+
const media = payload;
|
|
2019
|
+
const hasUrl = typeof media.url === 'string' && media.url.length > 0;
|
|
2020
|
+
const hasMediaId = typeof media.mediaId === 'string' && media.mediaId.length > 0;
|
|
2021
|
+
if (!hasUrl && !hasMediaId) {
|
|
2022
|
+
return {
|
|
2023
|
+
valid: false,
|
|
2024
|
+
error: `${type} message must have a url or mediaId`,
|
|
2025
|
+
errorCode: 'INVALID_MEDIA_PAYLOAD',
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
break;
|
|
2029
|
+
}
|
|
2030
|
+
case 'location':
|
|
2031
|
+
if (!('latitude' in payload) ||
|
|
2032
|
+
!('longitude' in payload) ||
|
|
2033
|
+
typeof payload.latitude !== 'number' ||
|
|
2034
|
+
typeof payload.longitude !== 'number') {
|
|
2035
|
+
return {
|
|
2036
|
+
valid: false,
|
|
2037
|
+
error: 'Location message must have latitude and longitude properties',
|
|
2038
|
+
errorCode: 'INVALID_LOCATION_PAYLOAD',
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
break;
|
|
2453
2042
|
}
|
|
2454
|
-
|
|
2455
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationFileFileTypeProvider, providedIn: 'root' }); }
|
|
2043
|
+
return { valid: true };
|
|
2456
2044
|
}
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
function
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2045
|
+
/**
|
|
2046
|
+
* Validate user ID
|
|
2047
|
+
* @param userId - User ID to validate
|
|
2048
|
+
* @returns Validation result
|
|
2049
|
+
*/
|
|
2050
|
+
function validateUserId(userId) {
|
|
2051
|
+
if (!userId || typeof userId !== 'string' || userId.trim().length === 0) {
|
|
2052
|
+
return {
|
|
2053
|
+
valid: false,
|
|
2054
|
+
error: 'User ID is required',
|
|
2055
|
+
errorCode: 'MISSING_USER_ID',
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
// Check for reasonable length (prevent extremely long IDs)
|
|
2059
|
+
if (userId.length > 255) {
|
|
2060
|
+
return {
|
|
2061
|
+
valid: false,
|
|
2062
|
+
error: 'User ID is too long',
|
|
2063
|
+
errorCode: 'INVALID_USER_ID',
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
return { valid: true };
|
|
2478
2067
|
}
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
metadata: result.metadata,
|
|
2494
|
-
};
|
|
2495
|
-
if (url) {
|
|
2496
|
-
next.url = url;
|
|
2068
|
+
/**
|
|
2069
|
+
* Validate array of user IDs
|
|
2070
|
+
* @param userIds - Array of user IDs to validate
|
|
2071
|
+
* @param minCount - Minimum number of users required
|
|
2072
|
+
* @param maxCount - Maximum number of users allowed
|
|
2073
|
+
* @returns Validation result
|
|
2074
|
+
*/
|
|
2075
|
+
function validateUserIds(userIds, minCount = 1, maxCount) {
|
|
2076
|
+
if (!userIds || !Array.isArray(userIds)) {
|
|
2077
|
+
return {
|
|
2078
|
+
valid: false,
|
|
2079
|
+
error: 'User IDs must be an array',
|
|
2080
|
+
errorCode: 'INVALID_USER_IDS',
|
|
2081
|
+
};
|
|
2497
2082
|
}
|
|
2498
|
-
if (
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2083
|
+
if (userIds.length < minCount) {
|
|
2084
|
+
return {
|
|
2085
|
+
valid: false,
|
|
2086
|
+
error: `At least ${minCount} user(s) required`,
|
|
2087
|
+
errorCode: 'TOO_FEW_USERS',
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
if (maxCount && userIds.length > maxCount) {
|
|
2091
|
+
return {
|
|
2092
|
+
valid: false,
|
|
2093
|
+
error: `Maximum ${maxCount} user(s) allowed`,
|
|
2094
|
+
errorCode: 'TOO_MANY_USERS',
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
// Check for empty or invalid IDs
|
|
2098
|
+
const invalidIds = userIds.filter((id) => !id || id.trim().length === 0);
|
|
2099
|
+
if (invalidIds.length > 0) {
|
|
2100
|
+
return {
|
|
2101
|
+
valid: false,
|
|
2102
|
+
error: 'All user IDs must be non-empty strings',
|
|
2103
|
+
errorCode: 'INVALID_USER_ID',
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
return { valid: true };
|
|
2503
2107
|
}
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2108
|
+
/**
|
|
2109
|
+
* Validate email address
|
|
2110
|
+
* @param email - Email to validate
|
|
2111
|
+
* @returns Validation result
|
|
2112
|
+
*/
|
|
2113
|
+
function validateEmail(email) {
|
|
2114
|
+
if (!email || email.trim().length === 0) {
|
|
2115
|
+
return {
|
|
2116
|
+
valid: false,
|
|
2117
|
+
error: 'Email is required',
|
|
2118
|
+
errorCode: 'MISSING_EMAIL',
|
|
2119
|
+
};
|
|
2511
2120
|
}
|
|
2512
|
-
|
|
2513
|
-
|
|
2121
|
+
// Trim whitespace
|
|
2122
|
+
const trimmedEmail = email.trim();
|
|
2123
|
+
// Check length constraints
|
|
2124
|
+
if (trimmedEmail.length > 254) {
|
|
2125
|
+
return {
|
|
2126
|
+
valid: false,
|
|
2127
|
+
error: 'Email is too long',
|
|
2128
|
+
errorCode: 'INVALID_EMAIL',
|
|
2129
|
+
};
|
|
2514
2130
|
}
|
|
2515
|
-
|
|
2516
|
-
|
|
2131
|
+
// Enhanced email regex with better validation
|
|
2132
|
+
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])?)*$/;
|
|
2133
|
+
if (!emailRegex.test(trimmedEmail)) {
|
|
2134
|
+
return {
|
|
2135
|
+
valid: false,
|
|
2136
|
+
error: 'Invalid email format',
|
|
2137
|
+
errorCode: 'INVALID_EMAIL',
|
|
2138
|
+
};
|
|
2517
2139
|
}
|
|
2518
|
-
return {
|
|
2140
|
+
return { valid: true };
|
|
2519
2141
|
}
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
}
|
|
2533
|
-
|
|
2534
|
-
|
|
2142
|
+
/**
|
|
2143
|
+
* Validate URL
|
|
2144
|
+
* @param url - URL to validate
|
|
2145
|
+
* @returns Validation result
|
|
2146
|
+
*/
|
|
2147
|
+
function validateUrl(url) {
|
|
2148
|
+
if (!url || url.trim().length === 0) {
|
|
2149
|
+
return {
|
|
2150
|
+
valid: false,
|
|
2151
|
+
error: 'URL is required',
|
|
2152
|
+
errorCode: 'MISSING_URL',
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
const trimmedUrl = url.trim();
|
|
2156
|
+
// Check for common URL issues
|
|
2157
|
+
if (trimmedUrl.length > 2048) {
|
|
2158
|
+
return {
|
|
2159
|
+
valid: false,
|
|
2160
|
+
error: 'URL is too long',
|
|
2161
|
+
errorCode: 'INVALID_URL',
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
try {
|
|
2165
|
+
const urlObj = new URL(trimmedUrl);
|
|
2166
|
+
// Validate protocol
|
|
2167
|
+
if (!['http:', 'https:', 'ftp:', 'ftps:'].includes(urlObj.protocol)) {
|
|
2168
|
+
return {
|
|
2169
|
+
valid: false,
|
|
2170
|
+
error: 'Invalid URL protocol',
|
|
2171
|
+
errorCode: 'INVALID_URL',
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
return { valid: true };
|
|
2175
|
+
}
|
|
2176
|
+
catch {
|
|
2177
|
+
return {
|
|
2178
|
+
valid: false,
|
|
2179
|
+
error: 'Invalid URL format',
|
|
2180
|
+
errorCode: 'INVALID_URL',
|
|
2181
|
+
};
|
|
2535
2182
|
}
|
|
2536
|
-
return item;
|
|
2537
2183
|
}
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2184
|
+
// =====================
|
|
2185
|
+
// Helper Functions
|
|
2186
|
+
// =====================
|
|
2187
|
+
/**
|
|
2188
|
+
* Sanitize user input to prevent XSS
|
|
2189
|
+
* Note: Angular provides built-in sanitization, but this is an additional layer
|
|
2190
|
+
* @param input - User input to sanitize
|
|
2191
|
+
* @returns Sanitized input
|
|
2192
|
+
*/
|
|
2193
|
+
function sanitizeInput(input) {
|
|
2194
|
+
if (!input)
|
|
2195
|
+
return '';
|
|
2196
|
+
return input
|
|
2197
|
+
.replace(/&/g, '&')
|
|
2198
|
+
.replace(/</g, '<')
|
|
2199
|
+
.replace(/>/g, '>')
|
|
2200
|
+
.replace(/"/g, '"')
|
|
2201
|
+
.replace(/'/g, ''')
|
|
2202
|
+
.replace(/\//g, '/')
|
|
2203
|
+
.replace(/`/g, '`')
|
|
2204
|
+
.replace(/=/g, '=');
|
|
2205
|
+
}
|
|
2206
|
+
/**
|
|
2207
|
+
* Validate latitude coordinate
|
|
2208
|
+
* @param latitude - Latitude to validate
|
|
2209
|
+
* @returns Validation result
|
|
2210
|
+
*/
|
|
2211
|
+
function validateLatitude(latitude) {
|
|
2212
|
+
if (latitude === undefined || latitude === null || typeof latitude !== 'number' || isNaN(latitude)) {
|
|
2213
|
+
return {
|
|
2214
|
+
valid: false,
|
|
2215
|
+
error: 'Latitude is required',
|
|
2216
|
+
errorCode: 'MISSING_LATITUDE',
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
if (latitude < -90 || latitude > 90) {
|
|
2220
|
+
return {
|
|
2221
|
+
valid: false,
|
|
2222
|
+
error: 'Latitude must be between -90 and 90',
|
|
2223
|
+
errorCode: 'INVALID_LATITUDE',
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
return { valid: true };
|
|
2227
|
+
}
|
|
2228
|
+
/**
|
|
2229
|
+
* Validate longitude coordinate
|
|
2230
|
+
* @param longitude - Longitude to validate
|
|
2231
|
+
* @returns Validation result
|
|
2232
|
+
*/
|
|
2233
|
+
function validateLongitude(longitude) {
|
|
2234
|
+
if (longitude === undefined || longitude === null || typeof longitude !== 'number' || isNaN(longitude)) {
|
|
2235
|
+
return {
|
|
2236
|
+
valid: false,
|
|
2237
|
+
error: 'Longitude is required',
|
|
2238
|
+
errorCode: 'MISSING_LONGITUDE',
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
if (longitude < -180 || longitude > 180) {
|
|
2242
|
+
return {
|
|
2243
|
+
valid: false,
|
|
2244
|
+
error: 'Longitude must be between -180 and 180',
|
|
2245
|
+
errorCode: 'INVALID_LONGITUDE',
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
return { valid: true };
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
/**
|
|
2252
|
+
* In-memory conversation and message graph (signal-based).
|
|
2253
|
+
* Plain class — not DI-registered; instantiated by `AXConversationService`.
|
|
2254
|
+
*/
|
|
2255
|
+
function withNormalizedPayload(message) {
|
|
2256
|
+
const timestamp = message.timestamp instanceof Date ? message.timestamp : new Date(message.timestamp);
|
|
2257
|
+
return { ...message, timestamp, payload: normalizeMessagePayload(message.payload) };
|
|
2258
|
+
}
|
|
2259
|
+
class ConversationState {
|
|
2260
|
+
constructor(config) {
|
|
2261
|
+
this.config = config;
|
|
2262
|
+
this._conversations = signal(new Map(), ...(ngDevMode ? [{ debugName: "_conversations" }] : /* istanbul ignore next */ []));
|
|
2263
|
+
this._messages = signal(new Map(), ...(ngDevMode ? [{ debugName: "_messages" }] : /* istanbul ignore next */ []));
|
|
2264
|
+
this._conversationMessages = signal(new Map(), ...(ngDevMode ? [{ debugName: "_conversationMessages" }] : /* istanbul ignore next */ []));
|
|
2265
|
+
this.conversations = computed(() => {
|
|
2266
|
+
const convMap = this._conversations();
|
|
2267
|
+
return Array.from(convMap.values());
|
|
2268
|
+
}, { ...(ngDevMode ? { debugName: "conversations" } : /* istanbul ignore next */ {}), equal: (a, b) => a.length === b.length && a.every((v, i) => v === b[i]) });
|
|
2269
|
+
}
|
|
2270
|
+
setConversations(conversations) {
|
|
2271
|
+
const convMap = new Map();
|
|
2272
|
+
conversations.forEach((conv) => convMap.set(conv.id, conv));
|
|
2273
|
+
this._conversations.set(convMap);
|
|
2274
|
+
}
|
|
2275
|
+
addConversations(conversations) {
|
|
2276
|
+
this._conversations.update((existingConversations) => {
|
|
2277
|
+
const newConversations = new Map(existingConversations);
|
|
2278
|
+
conversations.forEach((conv) => newConversations.set(conv.id, conv));
|
|
2279
|
+
const maxCached = this.config.maxCachedConversations;
|
|
2280
|
+
if (newConversations.size > maxCached) {
|
|
2281
|
+
const sorted = Array.from(newConversations.values()).sort((a, b) => (b.lastMessageAt?.getTime() ?? 0) - (a.lastMessageAt?.getTime() ?? 0));
|
|
2282
|
+
const toKeep = sorted.slice(0, maxCached);
|
|
2283
|
+
const cleanedMap = new Map();
|
|
2284
|
+
toKeep.forEach((conv) => cleanedMap.set(conv.id, conv));
|
|
2285
|
+
return cleanedMap;
|
|
2286
|
+
}
|
|
2287
|
+
return newConversations;
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2290
|
+
setConversation(conversation) {
|
|
2291
|
+
this._conversations.update((conversations) => {
|
|
2292
|
+
const newConversations = new Map(conversations);
|
|
2293
|
+
newConversations.set(conversation.id, conversation);
|
|
2294
|
+
return newConversations;
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
getConversation(conversationId) {
|
|
2298
|
+
return this._conversations().get(conversationId);
|
|
2299
|
+
}
|
|
2300
|
+
updateConversation(conversationId, updates) {
|
|
2301
|
+
const conversation = this._conversations().get(conversationId);
|
|
2302
|
+
if (!conversation)
|
|
2303
|
+
return;
|
|
2304
|
+
this.setConversation({ ...conversation, ...updates });
|
|
2305
|
+
}
|
|
2306
|
+
deleteConversation(conversationId) {
|
|
2307
|
+
this._conversations.update((conversations) => {
|
|
2308
|
+
const newConversations = new Map(conversations);
|
|
2309
|
+
newConversations.delete(conversationId);
|
|
2310
|
+
return newConversations;
|
|
2311
|
+
});
|
|
2312
|
+
const messageIds = this._conversationMessages().get(conversationId) || [];
|
|
2313
|
+
this._messages.update((messages) => {
|
|
2314
|
+
const newMessages = new Map(messages);
|
|
2315
|
+
messageIds.forEach((id) => newMessages.delete(id));
|
|
2316
|
+
return newMessages;
|
|
2317
|
+
});
|
|
2318
|
+
this._conversationMessages.update((map) => {
|
|
2319
|
+
const newMap = new Map(map);
|
|
2320
|
+
newMap.delete(conversationId);
|
|
2321
|
+
return newMap;
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
updateLastMessage(message) {
|
|
2325
|
+
this.updateConversation(message.conversationId, {
|
|
2326
|
+
lastMessage: message,
|
|
2327
|
+
lastMessageAt: message.timestamp,
|
|
2328
|
+
updatedAt: message.timestamp,
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
incrementUnreadCount(conversationId) {
|
|
2332
|
+
const conversation = this._conversations().get(conversationId);
|
|
2333
|
+
if (!conversation)
|
|
2334
|
+
return;
|
|
2335
|
+
this.updateConversation(conversationId, { unreadCount: conversation.unreadCount + 1 });
|
|
2336
|
+
}
|
|
2337
|
+
resetUnreadCount(conversationId) {
|
|
2338
|
+
this.updateConversation(conversationId, { unreadCount: 0 });
|
|
2339
|
+
}
|
|
2340
|
+
updateSettings(conversationId, settings) {
|
|
2341
|
+
const conversation = this._conversations().get(conversationId);
|
|
2342
|
+
if (!conversation)
|
|
2343
|
+
return;
|
|
2344
|
+
this.updateConversation(conversationId, {
|
|
2345
|
+
settings: { ...conversation.settings, ...settings },
|
|
2346
|
+
updatedAt: new Date(),
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
updateTitle(conversationId, title) {
|
|
2350
|
+
this.updateConversation(conversationId, { title, updatedAt: new Date() });
|
|
2351
|
+
}
|
|
2352
|
+
updateMetadata(conversationId, metadata) {
|
|
2353
|
+
const conversation = this._conversations().get(conversationId);
|
|
2354
|
+
if (!conversation)
|
|
2355
|
+
return;
|
|
2356
|
+
this.updateConversation(conversationId, {
|
|
2357
|
+
metadata: { ...conversation.metadata, ...metadata },
|
|
2358
|
+
updatedAt: new Date(),
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
updateTypingIndicator(conversationId, userId, isTyping) {
|
|
2362
|
+
const conversation = this._conversations().get(conversationId);
|
|
2363
|
+
if (!conversation)
|
|
2364
|
+
return;
|
|
2365
|
+
let typingUsers = [...conversation.status.typingUsers];
|
|
2366
|
+
if (isTyping) {
|
|
2367
|
+
if (!typingUsers.includes(userId)) {
|
|
2368
|
+
typingUsers.push(userId);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
else {
|
|
2372
|
+
typingUsers = typingUsers.filter((id) => id !== userId);
|
|
2373
|
+
}
|
|
2374
|
+
this.updateConversation(conversationId, {
|
|
2375
|
+
status: {
|
|
2376
|
+
...conversation.status,
|
|
2377
|
+
isTyping: typingUsers.length > 0,
|
|
2378
|
+
typingUsers,
|
|
2379
|
+
},
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
updateParticipantPresence(userId, status, lastSeen) {
|
|
2383
|
+
this._conversations.update((conversations) => {
|
|
2384
|
+
const newConversations = new Map(conversations);
|
|
2385
|
+
for (const [id, conv] of conversations) {
|
|
2386
|
+
const participant = conv.participants.find((p) => p.id === userId);
|
|
2387
|
+
if (participant) {
|
|
2388
|
+
const updatedConversation = {
|
|
2389
|
+
...conv,
|
|
2390
|
+
participants: conv.participants.map((p) => (p.id === userId ? { ...p, status, lastSeen } : p)),
|
|
2391
|
+
status: conv.type === 'private' ? { ...conv.status, presence: status, lastSeen } : conv.status,
|
|
2392
|
+
};
|
|
2393
|
+
newConversations.set(id, updatedConversation);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
return newConversations;
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
addMessage(message) {
|
|
2400
|
+
const normalized = withNormalizedPayload(message);
|
|
2401
|
+
this._messages.update((messages) => {
|
|
2402
|
+
const newMessages = new Map(messages);
|
|
2403
|
+
newMessages.set(normalized.id, normalized);
|
|
2404
|
+
return newMessages;
|
|
2405
|
+
});
|
|
2406
|
+
this._conversationMessages.update((map) => {
|
|
2407
|
+
const newMap = new Map(map);
|
|
2408
|
+
const existing = newMap.get(normalized.conversationId) || [];
|
|
2409
|
+
if (existing.includes(normalized.id)) {
|
|
2410
|
+
return newMap;
|
|
2411
|
+
}
|
|
2412
|
+
const updated = [...existing, normalized.id].sort((a, b) => {
|
|
2413
|
+
const msgA = this._messages().get(a);
|
|
2414
|
+
const msgB = this._messages().get(b);
|
|
2415
|
+
if (!msgA || !msgB)
|
|
2416
|
+
return 0;
|
|
2417
|
+
return msgA.timestamp.getTime() - msgB.timestamp.getTime();
|
|
2418
|
+
});
|
|
2419
|
+
newMap.set(normalized.conversationId, updated);
|
|
2420
|
+
return newMap;
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
/**
|
|
2424
|
+
* Replace all messages for a conversation (initial page load).
|
|
2425
|
+
*/
|
|
2426
|
+
setConversationMessages(conversationId, messages) {
|
|
2427
|
+
const sorted = [...messages]
|
|
2428
|
+
.map(withNormalizedPayload)
|
|
2429
|
+
.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
2430
|
+
const sortedIds = sorted.map((m) => m.id);
|
|
2431
|
+
this._messages.update((msgs) => {
|
|
2432
|
+
const newMessages = new Map(msgs);
|
|
2433
|
+
const previousIds = this._conversationMessages().get(conversationId) || [];
|
|
2434
|
+
for (const id of previousIds) {
|
|
2435
|
+
newMessages.delete(id);
|
|
2436
|
+
}
|
|
2437
|
+
for (const msg of sorted) {
|
|
2438
|
+
newMessages.set(msg.id, msg);
|
|
2439
|
+
}
|
|
2440
|
+
return newMessages;
|
|
2441
|
+
});
|
|
2442
|
+
this._conversationMessages.update((map) => {
|
|
2443
|
+
const newMap = new Map(map);
|
|
2444
|
+
newMap.set(conversationId, sortedIds);
|
|
2445
|
+
return newMap;
|
|
2446
|
+
});
|
|
2447
|
+
this.cleanupOldMessages();
|
|
2448
|
+
}
|
|
2449
|
+
addMessages(messages) {
|
|
2450
|
+
if (messages.length === 0)
|
|
2451
|
+
return;
|
|
2452
|
+
const normalized = messages.map(withNormalizedPayload);
|
|
2453
|
+
this._messages.update((msgs) => {
|
|
2454
|
+
const newMessages = new Map(msgs);
|
|
2455
|
+
normalized.forEach((msg) => newMessages.set(msg.id, msg));
|
|
2456
|
+
return newMessages;
|
|
2457
|
+
});
|
|
2458
|
+
const conversationGroups = new Map();
|
|
2459
|
+
normalized.forEach((msg) => {
|
|
2460
|
+
const existing = conversationGroups.get(msg.conversationId) || [];
|
|
2461
|
+
existing.push(msg.id);
|
|
2462
|
+
conversationGroups.set(msg.conversationId, existing);
|
|
2463
|
+
});
|
|
2464
|
+
this._conversationMessages.update((map) => {
|
|
2465
|
+
const newMap = new Map(map);
|
|
2466
|
+
for (const [conversationId, newMsgIds] of conversationGroups) {
|
|
2467
|
+
const existing = newMap.get(conversationId) || [];
|
|
2468
|
+
const idSet = new Set(existing);
|
|
2469
|
+
const merged = [...existing];
|
|
2470
|
+
for (const id of newMsgIds) {
|
|
2471
|
+
if (!idSet.has(id)) {
|
|
2472
|
+
merged.push(id);
|
|
2473
|
+
idSet.add(id);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
const sorted = merged.sort((a, b) => {
|
|
2477
|
+
const msgA = this._messages().get(a);
|
|
2478
|
+
const msgB = this._messages().get(b);
|
|
2479
|
+
if (!msgA || !msgB)
|
|
2480
|
+
return 0;
|
|
2481
|
+
return msgA.timestamp.getTime() - msgB.timestamp.getTime();
|
|
2482
|
+
});
|
|
2483
|
+
newMap.set(conversationId, sorted);
|
|
2484
|
+
}
|
|
2485
|
+
return newMap;
|
|
2486
|
+
});
|
|
2487
|
+
this.cleanupOldMessages();
|
|
2488
|
+
this.cleanupConversationMessages();
|
|
2489
|
+
}
|
|
2490
|
+
getMessage(messageId) {
|
|
2491
|
+
return this._messages().get(messageId);
|
|
2492
|
+
}
|
|
2493
|
+
getConversationMessages(conversationId) {
|
|
2494
|
+
const messageIds = this._conversationMessages().get(conversationId) || [];
|
|
2495
|
+
return messageIds.map((id) => this._messages().get(id)).filter((msg) => msg !== undefined);
|
|
2496
|
+
}
|
|
2497
|
+
updateMessage(messageId, updates) {
|
|
2498
|
+
const message = this._messages().get(messageId);
|
|
2499
|
+
if (!message)
|
|
2500
|
+
return;
|
|
2501
|
+
const merged = { ...message, ...updates };
|
|
2502
|
+
if (updates.payload) {
|
|
2503
|
+
merged.payload = normalizeMessagePayload(merged.payload);
|
|
2504
|
+
}
|
|
2505
|
+
this.addMessage(merged);
|
|
2506
|
+
}
|
|
2507
|
+
deleteMessage(messageId) {
|
|
2508
|
+
const message = this._messages().get(messageId);
|
|
2509
|
+
if (!message)
|
|
2510
|
+
return;
|
|
2511
|
+
this._messages.update((messages) => {
|
|
2512
|
+
const newMessages = new Map(messages);
|
|
2513
|
+
newMessages.delete(messageId);
|
|
2514
|
+
return newMessages;
|
|
2515
|
+
});
|
|
2516
|
+
this._conversationMessages.update((map) => {
|
|
2517
|
+
const newMap = new Map(map);
|
|
2518
|
+
const existing = newMap.get(message.conversationId);
|
|
2519
|
+
if (existing) {
|
|
2520
|
+
newMap.set(message.conversationId, existing.filter((id) => id !== messageId));
|
|
2521
|
+
}
|
|
2522
|
+
return newMap;
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
cleanupOldMessages() {
|
|
2526
|
+
const totalMessages = this._messages().size;
|
|
2527
|
+
if (totalMessages > this.config.maxTotalMessages) {
|
|
2528
|
+
const allMessages = Array.from(this._messages().values()).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
2529
|
+
const toKeep = allMessages.slice(0, this.config.maxTotalMessages);
|
|
2530
|
+
const toKeepIds = new Set(toKeep.map((m) => m.id));
|
|
2531
|
+
this._messages.update(() => {
|
|
2532
|
+
const newMessages = new Map();
|
|
2533
|
+
toKeep.forEach((msg) => newMessages.set(msg.id, msg));
|
|
2534
|
+
return newMessages;
|
|
2535
|
+
});
|
|
2536
|
+
this._conversationMessages.update((map) => {
|
|
2537
|
+
const newMap = new Map(map);
|
|
2538
|
+
for (const [convId, messageIds] of newMap) {
|
|
2539
|
+
const filteredIds = messageIds.filter((id) => toKeepIds.has(id));
|
|
2540
|
+
newMap.set(convId, filteredIds);
|
|
2541
|
+
}
|
|
2542
|
+
return newMap;
|
|
2543
|
+
});
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
cleanupConversationMessages() {
|
|
2547
|
+
const maxMessages = this.config.maxMessagesPerConversation;
|
|
2548
|
+
const idsToRemove = [];
|
|
2549
|
+
this._conversationMessages.update((map) => {
|
|
2550
|
+
const newMap = new Map(map);
|
|
2551
|
+
for (const [convId, messageIds] of newMap) {
|
|
2552
|
+
if (messageIds.length > maxMessages) {
|
|
2553
|
+
idsToRemove.push(...messageIds.slice(0, messageIds.length - maxMessages));
|
|
2554
|
+
newMap.set(convId, messageIds.slice(-maxMessages));
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
return newMap;
|
|
2558
|
+
});
|
|
2559
|
+
if (idsToRemove.length > 0) {
|
|
2560
|
+
this._messages.update((messages) => {
|
|
2561
|
+
const newMessages = new Map(messages);
|
|
2562
|
+
idsToRemove.forEach((id) => newMessages.delete(id));
|
|
2563
|
+
return newMessages;
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2554
2566
|
}
|
|
2555
|
-
if (i >= 0)
|
|
2556
|
-
videos[i] = next;
|
|
2557
|
-
else
|
|
2558
|
-
videos.push(next);
|
|
2559
|
-
return { ...base, type: 'video', videos };
|
|
2560
2567
|
}
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
+
|
|
2569
|
+
/**
|
|
2570
|
+
* Error Handler Service
|
|
2571
|
+
* Centralized error handling and logging
|
|
2572
|
+
*/
|
|
2573
|
+
/**
|
|
2574
|
+
* Error Handler Service
|
|
2575
|
+
*/
|
|
2576
|
+
class AXErrorHandlerService {
|
|
2577
|
+
constructor() {
|
|
2578
|
+
this.injectedConfig = inject(ERROR_HANDLER_CONFIG);
|
|
2579
|
+
this._errors$ = new Subject();
|
|
2580
|
+
this._config = {
|
|
2581
|
+
logToConsole: true,
|
|
2582
|
+
showUserMessages: true,
|
|
2583
|
+
autoRetry: false,
|
|
2584
|
+
maxRetries: 3,
|
|
2585
|
+
};
|
|
2586
|
+
/** Error stream */
|
|
2587
|
+
this.errors$ = this._errors$.asObservable();
|
|
2588
|
+
this.configure(this.injectedConfig);
|
|
2568
2589
|
}
|
|
2569
|
-
|
|
2570
|
-
|
|
2590
|
+
/**
|
|
2591
|
+
* Configure error handler
|
|
2592
|
+
*/
|
|
2593
|
+
configure(config) {
|
|
2594
|
+
Object.assign(this._config, config);
|
|
2571
2595
|
}
|
|
2572
|
-
|
|
2573
|
-
|
|
2596
|
+
/**
|
|
2597
|
+
* Handle an error
|
|
2598
|
+
*/
|
|
2599
|
+
handle(error, operation, context) {
|
|
2600
|
+
const conversationError = this.normalizeError(error, operation, context);
|
|
2601
|
+
this.publish(conversationError);
|
|
2602
|
+
return conversationError;
|
|
2574
2603
|
}
|
|
2575
|
-
|
|
2576
|
-
}
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
const
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
title: 'Voice message',
|
|
2583
|
-
};
|
|
2584
|
-
const VOICE_UTILITY = {
|
|
2585
|
-
duration: 'duration',
|
|
2586
|
-
formatDuration: 'formatDuration',
|
|
2587
|
-
createLocalPreviewUrl: 'createLocalPreviewUrl',
|
|
2588
|
-
};
|
|
2589
|
-
function blobUrl(ctx, blob) {
|
|
2590
|
-
if (!isPlatformBrowser(ctx.platformId)) {
|
|
2591
|
-
return '';
|
|
2604
|
+
/**
|
|
2605
|
+
* Handle API error (same pipeline as {@link handle}, for typed API failures)
|
|
2606
|
+
*/
|
|
2607
|
+
handleApiError(apiError, operation, context) {
|
|
2608
|
+
const conversationError = this.conversationErrorFromApi(apiError, operation, context);
|
|
2609
|
+
this.publish(conversationError);
|
|
2610
|
+
return conversationError;
|
|
2592
2611
|
}
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2612
|
+
publish(conversationError) {
|
|
2613
|
+
this._errors$.next(conversationError);
|
|
2614
|
+
if (this._config.logToConsole) {
|
|
2615
|
+
this.logError(conversationError);
|
|
2616
|
+
}
|
|
2617
|
+
if (this._config.customHandler) {
|
|
2618
|
+
this._config.customHandler(conversationError);
|
|
2619
|
+
}
|
|
2598
2620
|
}
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
}
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2621
|
+
/**
|
|
2622
|
+
* Normalize any error to conversation error format (does not publish — use {@link handle})
|
|
2623
|
+
*/
|
|
2624
|
+
normalizeError(error, operation, context) {
|
|
2625
|
+
if (this.isApiError(error)) {
|
|
2626
|
+
return this.conversationErrorFromApi(error, operation, context);
|
|
2627
|
+
}
|
|
2628
|
+
const errorObj = error;
|
|
2629
|
+
const message = (typeof errorObj['message'] === 'string' && errorObj['message']) ||
|
|
2630
|
+
(error instanceof Error ? error.message : String(error)) ||
|
|
2631
|
+
'An unknown error occurred';
|
|
2632
|
+
const code = (typeof errorObj['code'] === 'string' && errorObj['code']) || 'UNKNOWN_ERROR';
|
|
2633
|
+
const statusCodeRaw = errorObj['statusCode'] ?? errorObj['status'];
|
|
2634
|
+
const statusCode = typeof statusCodeRaw === 'number' ? statusCodeRaw : undefined;
|
|
2635
|
+
return {
|
|
2636
|
+
code,
|
|
2637
|
+
message,
|
|
2638
|
+
severity: 'error',
|
|
2639
|
+
operation,
|
|
2640
|
+
originalError: error,
|
|
2641
|
+
statusCode,
|
|
2642
|
+
context,
|
|
2643
|
+
timestamp: new Date(),
|
|
2644
|
+
handled: false,
|
|
2645
|
+
recoverySuggestions: this.getDefaultRecoverySuggestions(code),
|
|
2609
2646
|
};
|
|
2610
|
-
el.src = URL.createObjectURL(file);
|
|
2611
|
-
});
|
|
2612
|
-
}
|
|
2613
|
-
function conversationVoiceUtilities() {
|
|
2614
|
-
return {
|
|
2615
|
-
[VOICE_UTILITY.duration]: (ctx, file) => mediaDuration(ctx, file),
|
|
2616
|
-
[VOICE_UTILITY.formatDuration]: (_ctx, seconds) => formatDuration(seconds),
|
|
2617
|
-
[VOICE_UTILITY.createLocalPreviewUrl]: (ctx, source) => blobUrl(ctx, source),
|
|
2618
|
-
};
|
|
2619
|
-
}
|
|
2620
|
-
function createConversationVoiceFileType() {
|
|
2621
|
-
const presentation = CONVERSATION_VOICE_PRESENTATION;
|
|
2622
|
-
return {
|
|
2623
|
-
name: CONVERSATION_VOICE_CATALOG,
|
|
2624
|
-
metadata: createFileTypeMetadata('conversation'),
|
|
2625
|
-
title: presentation.title,
|
|
2626
|
-
icon: presentation.icon,
|
|
2627
|
-
validations: {
|
|
2628
|
-
mimeTypes: ['audio/webm', 'audio/ogg', 'audio/mp4', 'audio/*'],
|
|
2629
|
-
minSize: 1,
|
|
2630
|
-
maxSize: 50 * MB,
|
|
2631
|
-
},
|
|
2632
|
-
utilities: conversationVoiceUtilities(),
|
|
2633
|
-
copy: (payload) => {
|
|
2634
|
-
const voice = payload;
|
|
2635
|
-
const url = voice.url?.trim() ?? '';
|
|
2636
|
-
return {
|
|
2637
|
-
text: '',
|
|
2638
|
-
meta: {
|
|
2639
|
-
kind: 'voice',
|
|
2640
|
-
url,
|
|
2641
|
-
duration: voice.duration,
|
|
2642
|
-
mimeType: voice.mimeType,
|
|
2643
|
-
},
|
|
2644
|
-
};
|
|
2645
|
-
},
|
|
2646
|
-
};
|
|
2647
|
-
}
|
|
2648
|
-
class AXConversationVoiceFileTypeProvider extends AXFileTypeInfoProvider {
|
|
2649
|
-
items() {
|
|
2650
|
-
return Promise.resolve([createConversationVoiceFileType()]);
|
|
2651
|
-
}
|
|
2652
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationVoiceFileTypeProvider, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2653
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationVoiceFileTypeProvider, providedIn: 'root' }); }
|
|
2654
|
-
}
|
|
2655
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXConversationVoiceFileTypeProvider, decorators: [{
|
|
2656
|
-
type: Injectable,
|
|
2657
|
-
args: [{ providedIn: 'root' }]
|
|
2658
|
-
}] });
|
|
2659
|
-
|
|
2660
|
-
function mergeVoiceUploadResult(payload, result) {
|
|
2661
|
-
return {
|
|
2662
|
-
...payload,
|
|
2663
|
-
type: 'voice',
|
|
2664
|
-
url: result.url,
|
|
2665
|
-
mediaId: result.mediaId,
|
|
2666
|
-
mimeType: result.mimeType,
|
|
2667
|
-
size: result.size,
|
|
2668
|
-
metadata: result.metadata,
|
|
2669
|
-
};
|
|
2670
|
-
}
|
|
2671
|
-
function applyVoiceLocalPreview(payload, localUrl) {
|
|
2672
|
-
return { ...payload, type: 'voice', url: localUrl };
|
|
2673
|
-
}
|
|
2674
|
-
|
|
2675
|
-
const MESSAGE_TYPE_FILE_TYPE = {
|
|
2676
|
-
image: CONVERSATION_IMAGE_CATALOG,
|
|
2677
|
-
video: CONVERSATION_VIDEO_CATALOG,
|
|
2678
|
-
audio: CONVERSATION_AUDIO_CATALOG,
|
|
2679
|
-
file: CONVERSATION_FILE_CATALOG,
|
|
2680
|
-
voice: CONVERSATION_VOICE_CATALOG,
|
|
2681
|
-
sticker: CONVERSATION_IMAGE_CATALOG,
|
|
2682
|
-
};
|
|
2683
|
-
/** Resolves {@link AXMessage.fileType} from command or message type. */
|
|
2684
|
-
function resolveMessageFileType(type, fileType) {
|
|
2685
|
-
return fileType ?? MESSAGE_TYPE_FILE_TYPE[type];
|
|
2686
|
-
}
|
|
2687
|
-
function mergeImageUploadResult(payload, result) {
|
|
2688
|
-
const base = normalizeImagePayload(payload);
|
|
2689
|
-
const images = [...base.images];
|
|
2690
|
-
const i = images.findIndex((x) => !x.mediaId && !resolvePersistableMediaUrl(x.url));
|
|
2691
|
-
const slot = i >= 0 ? images[i] : undefined;
|
|
2692
|
-
const url = resolvePersistableMediaUrl(result.url);
|
|
2693
|
-
const next = {
|
|
2694
|
-
...(slot ?? { width: 0, height: 0 }),
|
|
2695
|
-
mediaId: result.mediaId,
|
|
2696
|
-
mimeType: result.mimeType,
|
|
2697
|
-
size: result.size,
|
|
2698
|
-
thumbnailUrl: resolvePersistedThumbnailUrl(result.thumbnailUrl ?? slot?.thumbnailUrl, url),
|
|
2699
|
-
metadata: result.metadata,
|
|
2700
|
-
};
|
|
2701
|
-
if (url) {
|
|
2702
|
-
next.url = url;
|
|
2703
2647
|
}
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2648
|
+
conversationErrorFromApi(apiError, operation, context) {
|
|
2649
|
+
return {
|
|
2650
|
+
code: apiError.code,
|
|
2651
|
+
message: apiError.message,
|
|
2652
|
+
severity: this.determineSeverity(apiError.statusCode),
|
|
2653
|
+
operation,
|
|
2654
|
+
originalError: apiError,
|
|
2655
|
+
statusCode: apiError.statusCode,
|
|
2656
|
+
context,
|
|
2657
|
+
timestamp: apiError.timestamp ?? new Date(),
|
|
2658
|
+
handled: false,
|
|
2659
|
+
recoverySuggestions: this.getRecoverySuggestions(apiError),
|
|
2660
|
+
};
|
|
2717
2661
|
}
|
|
2718
|
-
|
|
2719
|
-
|
|
2662
|
+
isApiError(error) {
|
|
2663
|
+
return (typeof error === 'object' &&
|
|
2664
|
+
error !== null &&
|
|
2665
|
+
'code' in error &&
|
|
2666
|
+
'message' in error &&
|
|
2667
|
+
typeof error.code === 'string' &&
|
|
2668
|
+
typeof error.message === 'string');
|
|
2720
2669
|
}
|
|
2721
|
-
|
|
2722
|
-
|
|
2670
|
+
/**
|
|
2671
|
+
* Determine severity based on status code
|
|
2672
|
+
*/
|
|
2673
|
+
determineSeverity(statusCode) {
|
|
2674
|
+
if (!statusCode)
|
|
2675
|
+
return 'error';
|
|
2676
|
+
if (statusCode >= 500)
|
|
2677
|
+
return 'critical';
|
|
2678
|
+
if (statusCode >= 400)
|
|
2679
|
+
return 'error';
|
|
2680
|
+
if (statusCode >= 300)
|
|
2681
|
+
return 'warning';
|
|
2682
|
+
return 'info';
|
|
2723
2683
|
}
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2684
|
+
/**
|
|
2685
|
+
* Get recovery suggestions based on error
|
|
2686
|
+
*/
|
|
2687
|
+
getRecoverySuggestions(error) {
|
|
2688
|
+
const suggestions = [];
|
|
2689
|
+
if (error.statusCode === 401 || error.code === 'UNAUTHORIZED') {
|
|
2690
|
+
suggestions.push('Please log in again');
|
|
2691
|
+
suggestions.push('Check if your session has expired');
|
|
2692
|
+
}
|
|
2693
|
+
else if (error.statusCode === 403 || error.code === 'FORBIDDEN') {
|
|
2694
|
+
suggestions.push('You do not have permission for this action');
|
|
2695
|
+
suggestions.push('Ask your administrator for access');
|
|
2696
|
+
}
|
|
2697
|
+
else if (error.statusCode === 404 || error.code === 'NOT_FOUND') {
|
|
2698
|
+
suggestions.push('The requested resource was not found');
|
|
2699
|
+
suggestions.push('It may have been deleted or moved');
|
|
2700
|
+
}
|
|
2701
|
+
else if (error.statusCode === 429 || error.code === 'RATE_LIMIT_EXCEEDED') {
|
|
2702
|
+
suggestions.push('Too many requests. Please wait and try again');
|
|
2703
|
+
}
|
|
2704
|
+
else if (error.statusCode && error.statusCode >= 500) {
|
|
2705
|
+
suggestions.push('Server error occurred');
|
|
2706
|
+
suggestions.push('Please try again later');
|
|
2707
|
+
suggestions.push('If the problem persists, reach out to support');
|
|
2708
|
+
}
|
|
2709
|
+
else if (error.code === 'NETWORK_ERROR') {
|
|
2710
|
+
suggestions.push('Check your internet connection');
|
|
2711
|
+
suggestions.push('Try refreshing the page');
|
|
2712
|
+
}
|
|
2713
|
+
return suggestions;
|
|
2747
2714
|
}
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2715
|
+
/**
|
|
2716
|
+
* Get default recovery suggestions
|
|
2717
|
+
*/
|
|
2718
|
+
getDefaultRecoverySuggestions(code) {
|
|
2719
|
+
const suggestions = [];
|
|
2720
|
+
if (code.includes('NETWORK') || code.includes('CONNECTION')) {
|
|
2721
|
+
suggestions.push('Check your internet connection');
|
|
2722
|
+
suggestions.push('Try refreshing the page');
|
|
2723
|
+
}
|
|
2724
|
+
else if (code.includes('TIMEOUT')) {
|
|
2725
|
+
suggestions.push('The operation took too long');
|
|
2726
|
+
suggestions.push('Please try again');
|
|
2727
|
+
}
|
|
2728
|
+
else {
|
|
2729
|
+
suggestions.push('Please try again');
|
|
2730
|
+
suggestions.push('If the problem persists, reach out to support');
|
|
2731
|
+
}
|
|
2732
|
+
return suggestions;
|
|
2765
2733
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2734
|
+
/**
|
|
2735
|
+
* Log error to console
|
|
2736
|
+
*/
|
|
2737
|
+
logError(error) {
|
|
2738
|
+
const isError = error.severity === 'critical' || error.severity === 'error';
|
|
2739
|
+
const header = `[Conversation ${error.severity.toUpperCase()}] ${error.operation}:`;
|
|
2740
|
+
if (isError) {
|
|
2741
|
+
console.error(header, error.message, error.context || '');
|
|
2772
2742
|
}
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
return { url: first?.url, mediaId: first?.mediaId, mimeType: first?.mimeType, size: first?.size };
|
|
2743
|
+
else {
|
|
2744
|
+
console.warn(header, error.message, error.context || '');
|
|
2776
2745
|
}
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
return { url: first?.url, mediaId: first?.mediaId, mimeType: first?.mimeType, size: first?.size };
|
|
2746
|
+
if (error.originalError && error.severity !== 'info') {
|
|
2747
|
+
console.error('Original error:', error.originalError);
|
|
2780
2748
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
return { url: first?.url, mediaId: first?.mediaId, mimeType: first?.mimeType, size: first?.size };
|
|
2749
|
+
if (error.recoverySuggestions && error.recoverySuggestions.length > 0) {
|
|
2750
|
+
console.info('Recovery suggestions:', error.recoverySuggestions);
|
|
2784
2751
|
}
|
|
2785
|
-
case 'voice':
|
|
2786
|
-
case 'sticker':
|
|
2787
|
-
return {
|
|
2788
|
-
url: payload.url,
|
|
2789
|
-
mediaId: payload.mediaId,
|
|
2790
|
-
mimeType: payload.mimeType,
|
|
2791
|
-
size: payload.size,
|
|
2792
|
-
};
|
|
2793
|
-
default:
|
|
2794
|
-
return {};
|
|
2795
2752
|
}
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2753
|
+
/**
|
|
2754
|
+
* Get user-friendly error message
|
|
2755
|
+
*/
|
|
2756
|
+
getUserFriendlyMessage(error) {
|
|
2757
|
+
// Map technical errors to user-friendly messages
|
|
2758
|
+
const messageMap = {
|
|
2759
|
+
NETWORK_ERROR: 'Unable to connect. Please check your internet connection.',
|
|
2760
|
+
UNAUTHORIZED: 'You are not authorized. Please log in again.',
|
|
2761
|
+
FORBIDDEN: 'You do not have permission to perform this action.',
|
|
2762
|
+
NOT_FOUND: 'The requested item could not be found.',
|
|
2763
|
+
RATE_LIMIT_EXCEEDED: 'Too many requests. Please slow down.',
|
|
2764
|
+
VALIDATION_ERROR: 'The provided data is invalid.',
|
|
2765
|
+
SERVER_ERROR: 'A server error occurred. Please try again later.',
|
|
2766
|
+
TIMEOUT: 'The operation timed out. Please try again.',
|
|
2767
|
+
};
|
|
2768
|
+
return messageMap[error.code] || error.message || 'An unexpected error occurred.';
|
|
2800
2769
|
}
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2770
|
+
/**
|
|
2771
|
+
* Check if error is retryable
|
|
2772
|
+
*/
|
|
2773
|
+
isRetryable(error) {
|
|
2774
|
+
const retryableCodes = ['NETWORK_ERROR', 'TIMEOUT', 'RATE_LIMIT_EXCEEDED', 'SERVER_ERROR'];
|
|
2775
|
+
const retryableStatusCodes = [408, 429, 500, 502, 503, 504];
|
|
2776
|
+
return (retryableCodes.includes(error.code) ||
|
|
2777
|
+
(error.statusCode !== undefined && retryableStatusCodes.includes(error.statusCode)));
|
|
2806
2778
|
}
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2779
|
+
/**
|
|
2780
|
+
* Execute an operation with automatic retry logic
|
|
2781
|
+
* @param operation - The async operation to execute
|
|
2782
|
+
* @param operationName - Name of the operation for error tracking
|
|
2783
|
+
* @param context - Additional context for error handling
|
|
2784
|
+
* @returns Promise resolving to the operation result
|
|
2785
|
+
* @throws {AXConversationError} If all retries fail
|
|
2786
|
+
*/
|
|
2787
|
+
async executeWithRetry(operation, operationName, context) {
|
|
2788
|
+
if (!this._config.autoRetry) {
|
|
2789
|
+
// If auto-retry is disabled, just execute once
|
|
2790
|
+
return operation();
|
|
2791
|
+
}
|
|
2792
|
+
const maxRetries = this._config.maxRetries ?? 3;
|
|
2793
|
+
let lastError;
|
|
2794
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
2795
|
+
try {
|
|
2796
|
+
return await operation();
|
|
2797
|
+
}
|
|
2798
|
+
catch (error) {
|
|
2799
|
+
lastError = error;
|
|
2800
|
+
const conversationError = this.handle(error, operationName, { ...context, attempt });
|
|
2801
|
+
// Don't retry if error is not retryable or if this was the last attempt
|
|
2802
|
+
if (!this.isRetryable(conversationError) || attempt >= maxRetries) {
|
|
2803
|
+
throw conversationError;
|
|
2804
|
+
}
|
|
2805
|
+
// Exponential backoff: 1s, 2s, 4s, 8s...
|
|
2806
|
+
const delayMs = Math.pow(2, attempt) * 1000;
|
|
2807
|
+
await this.delay(delayMs);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
throw this.handle(lastError, operationName, context);
|
|
2812
2811
|
}
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2812
|
+
/**
|
|
2813
|
+
* Delay helper for retry backoff
|
|
2814
|
+
* @param ms - Milliseconds to delay
|
|
2815
|
+
*/
|
|
2816
|
+
delay(ms) {
|
|
2817
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2816
2818
|
}
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
file: source instanceof File ? source : undefined,
|
|
2820
|
-
mimeType: source.type,
|
|
2821
|
-
});
|
|
2822
|
-
const result = await runFileTypeUtility(fileType, ctx, extension, 'createLocalPreviewUrl', source);
|
|
2823
|
-
return typeof result === 'string' ? result : undefined;
|
|
2819
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXErrorHandlerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2820
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXErrorHandlerService, providedIn: 'root' }); }
|
|
2824
2821
|
}
|
|
2822
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXErrorHandlerService, decorators: [{
|
|
2823
|
+
type: Injectable,
|
|
2824
|
+
args: [{
|
|
2825
|
+
providedIn: 'root',
|
|
2826
|
+
}]
|
|
2827
|
+
}], ctorParameters: () => [] });
|
|
2825
2828
|
|
|
2826
2829
|
/**
|
|
2827
2830
|
* AXComposerService
|
|
@@ -9111,7 +9114,7 @@ class AXConversationIndexedDbRealtimeApi extends AXRealtimeApi {
|
|
|
9111
9114
|
// Message Events
|
|
9112
9115
|
// =====================
|
|
9113
9116
|
subscribeToMessages(conversationId) {
|
|
9114
|
-
return conversationSharedStorage.messageStream$.pipe(filter((msg) => msg.conversationId === conversationId));
|
|
9117
|
+
return conversationSharedStorage.messageStream$.pipe(filter((msg) => !conversationId || msg.conversationId === conversationId));
|
|
9115
9118
|
}
|
|
9116
9119
|
subscribeToMessageUpdates(_conversationId) {
|
|
9117
9120
|
return conversationSharedStorage.messageUpdates$;
|
|
@@ -15078,9 +15081,7 @@ class AXConversationService {
|
|
|
15078
15081
|
return null;
|
|
15079
15082
|
}
|
|
15080
15083
|
const conversation = this.state.getConversation(id);
|
|
15081
|
-
return conversation
|
|
15082
|
-
? resolveConversationForViewer(conversation, this._currentUser()?.id)
|
|
15083
|
-
: null;
|
|
15084
|
+
return conversation ? resolveConversationForViewer(conversation, this._currentUser()?.id) : null;
|
|
15084
15085
|
}, ...(ngDevMode ? [{ debugName: "activeConversation" }] : /* istanbul ignore next */ []));
|
|
15085
15086
|
/** Messages for active conversation */
|
|
15086
15087
|
this.activeMessages = computed(() => {
|
|
@@ -15215,6 +15216,18 @@ class AXConversationService {
|
|
|
15215
15216
|
.subscribe((conversation) => {
|
|
15216
15217
|
this.handleConversationUpdate(conversation);
|
|
15217
15218
|
});
|
|
15219
|
+
// Global message stream — backs message list even when the backend only pushes
|
|
15220
|
+
// conversation.lastMessage updates or when per-conversation subscribe races loadMessages.
|
|
15221
|
+
this.realtimeApi
|
|
15222
|
+
.subscribeToMessages('')
|
|
15223
|
+
.pipe(takeUntil(this.destroy$), catchError((error) => {
|
|
15224
|
+
this.errorHandler.handle(error, 'subscribeToMessages');
|
|
15225
|
+
return EMPTY;
|
|
15226
|
+
}))
|
|
15227
|
+
.subscribe((message) => {
|
|
15228
|
+
this.handleNewMessage(message);
|
|
15229
|
+
this._messageReceived$.next(message);
|
|
15230
|
+
});
|
|
15218
15231
|
}
|
|
15219
15232
|
/**
|
|
15220
15233
|
* Load conversations from server
|
|
@@ -15288,19 +15301,6 @@ class AXConversationService {
|
|
|
15288
15301
|
finally {
|
|
15289
15302
|
this._loadingActiveMessages.set(false);
|
|
15290
15303
|
}
|
|
15291
|
-
// Subscribe to new messages with error handling (only if realtime is available)
|
|
15292
|
-
if (this.realtimeApi) {
|
|
15293
|
-
this.realtimeApi
|
|
15294
|
-
.subscribeToMessages(conversationId)
|
|
15295
|
-
.pipe(takeUntil(this._conversationSwitch$), takeUntil(this.destroy$), catchError((error) => {
|
|
15296
|
-
this.errorHandler.handle(error, 'subscribeToMessages', { conversationId });
|
|
15297
|
-
return EMPTY;
|
|
15298
|
-
}))
|
|
15299
|
-
.subscribe((message) => {
|
|
15300
|
-
this.handleNewMessage(message);
|
|
15301
|
-
this._messageReceived$.next(message);
|
|
15302
|
-
});
|
|
15303
|
-
}
|
|
15304
15304
|
}
|
|
15305
15305
|
catch (error) {
|
|
15306
15306
|
this.errorHandler.handle(error, 'selectConversation', { conversationId });
|
|
@@ -15699,11 +15699,15 @@ class AXConversationService {
|
|
|
15699
15699
|
* Handle new message received
|
|
15700
15700
|
*/
|
|
15701
15701
|
handleNewMessage(message) {
|
|
15702
|
+
if (this.state.getMessage(message.id)) {
|
|
15703
|
+
return;
|
|
15704
|
+
}
|
|
15702
15705
|
this.state.addMessage(message);
|
|
15703
15706
|
this.state.updateLastMessage(message);
|
|
15704
15707
|
const currentId = this._currentUser()?.id ?? 'current-user';
|
|
15705
15708
|
const isFromOtherUser = message.senderId !== currentId;
|
|
15706
|
-
|
|
15709
|
+
const isActiveConversation = message.conversationId === this._activeConversationId();
|
|
15710
|
+
if (isFromOtherUser && !isActiveConversation) {
|
|
15707
15711
|
this.state.incrementUnreadCount(message.conversationId);
|
|
15708
15712
|
}
|
|
15709
15713
|
}
|
|
@@ -15711,6 +15715,13 @@ class AXConversationService {
|
|
|
15711
15715
|
* Handle message update
|
|
15712
15716
|
*/
|
|
15713
15717
|
handleMessageUpdate(message) {
|
|
15718
|
+
const existing = this.state.getMessage(message.id);
|
|
15719
|
+
if (!existing) {
|
|
15720
|
+
if (message.conversationId === this._activeConversationId()) {
|
|
15721
|
+
this.handleNewMessage(message);
|
|
15722
|
+
}
|
|
15723
|
+
return;
|
|
15724
|
+
}
|
|
15714
15725
|
this.state.updateMessage(message.id, message);
|
|
15715
15726
|
}
|
|
15716
15727
|
/**
|
|
@@ -15759,6 +15770,12 @@ class AXConversationService {
|
|
|
15759
15770
|
* Updates conversation metadata including unread count, last message, etc.
|
|
15760
15771
|
*/
|
|
15761
15772
|
handleConversationUpdate(conversation) {
|
|
15773
|
+
const lastMessage = conversation.lastMessage;
|
|
15774
|
+
if (lastMessage &&
|
|
15775
|
+
conversation.id === this._activeConversationId() &&
|
|
15776
|
+
!this.state.getMessage(lastMessage.id)) {
|
|
15777
|
+
this.state.addMessage(lastMessage);
|
|
15778
|
+
}
|
|
15762
15779
|
this.state.setConversation(conversation);
|
|
15763
15780
|
}
|
|
15764
15781
|
/**
|
|
@@ -15884,12 +15901,8 @@ class AXConversationService {
|
|
|
15884
15901
|
creatorId,
|
|
15885
15902
|
title: isPrivate ? undefined : metadata?.['title'],
|
|
15886
15903
|
description: metadata?.['description'],
|
|
15887
|
-
avatar: isPrivate
|
|
15888
|
-
|
|
15889
|
-
: AXConversationService.normalizeOptionalString(metadata?.['avatar']),
|
|
15890
|
-
icon: isPrivate
|
|
15891
|
-
? undefined
|
|
15892
|
-
: AXConversationService.normalizeOptionalString(metadata?.['icon']),
|
|
15904
|
+
avatar: isPrivate ? undefined : AXConversationService.normalizeOptionalString(metadata?.['avatar']),
|
|
15905
|
+
icon: isPrivate ? undefined : AXConversationService.normalizeOptionalString(metadata?.['icon']),
|
|
15893
15906
|
metadata: isPrivate ? undefined : metadata,
|
|
15894
15907
|
});
|
|
15895
15908
|
this.state.setConversation(conversation);
|
|
@@ -15938,7 +15951,9 @@ class AXConversationService {
|
|
|
15938
15951
|
const conversation = this.state.getConversation(conversationId);
|
|
15939
15952
|
const conversationTitle = conversation?.title || this.translation.translateSync('@acorex:chat.fallbacks.this-conversation');
|
|
15940
15953
|
// Show confirmation dialog
|
|
15941
|
-
const result = await this.dialogService.confirm(this.translation.translateSync('@acorex:chat.dialog.delete-conversation.title'), this.translation.translateSync('@acorex:chat.dialog.delete-conversation.message', {
|
|
15954
|
+
const result = await this.dialogService.confirm(this.translation.translateSync('@acorex:chat.dialog.delete-conversation.title'), this.translation.translateSync('@acorex:chat.dialog.delete-conversation.message', {
|
|
15955
|
+
params: { conversationTitle },
|
|
15956
|
+
}), 'danger', 'vertical', false);
|
|
15942
15957
|
// User cancelled
|
|
15943
15958
|
if (!result.result) {
|
|
15944
15959
|
return false;
|
|
@@ -20026,11 +20041,12 @@ class AXMessageListComponent {
|
|
|
20026
20041
|
}
|
|
20027
20042
|
}
|
|
20028
20043
|
/**
|
|
20029
|
-
* TrackBy function for message groups
|
|
20030
|
-
*
|
|
20044
|
+
* TrackBy function for message groups.
|
|
20045
|
+
* Include the latest message id so new messages in the same date group re-render.
|
|
20031
20046
|
*/
|
|
20032
20047
|
trackMessageGroup(index, group) {
|
|
20033
|
-
|
|
20048
|
+
const lastId = group.messages.at(-1)?.id ?? `empty-${index}`;
|
|
20049
|
+
return `${group.date}:${lastId}`;
|
|
20034
20050
|
}
|
|
20035
20051
|
/**
|
|
20036
20052
|
* TrackBy function for messages
|