valtech-components 4.0.226 → 4.0.228

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/esm2022/lib/components/molecules/chat-input/chat-input.component.mjs +9 -1
  2. package/esm2022/lib/components/molecules/message-bubble/message-bubble.component.mjs +191 -259
  3. package/esm2022/lib/components/molecules/typing-indicator/typing-indicator.component.mjs +12 -11
  4. package/esm2022/lib/components/organisms/chat-window/chat-window.component.mjs +203 -216
  5. package/esm2022/lib/components/organisms/thread-panel/thread-panel.component.mjs +10 -10
  6. package/esm2022/lib/services/chat/conversation.service.mjs +11 -23
  7. package/esm2022/lib/services/chat/types.mjs +1 -1
  8. package/esm2022/lib/services/i18n/default-content.mjs +1 -5
  9. package/esm2022/lib/shared/utils/datetime.mjs +1 -54
  10. package/esm2022/lib/version.mjs +2 -2
  11. package/esm2022/public-api.mjs +2 -4
  12. package/fesm2022/valtech-components.mjs +425 -898
  13. package/fesm2022/valtech-components.mjs.map +1 -1
  14. package/lib/components/molecules/message-bubble/message-bubble.component.d.ts +32 -33
  15. package/lib/components/molecules/typing-indicator/typing-indicator.component.d.ts +4 -5
  16. package/lib/components/organisms/chat-window/chat-window.component.d.ts +56 -52
  17. package/lib/services/chat/conversation.service.d.ts +2 -7
  18. package/lib/services/chat/types.d.ts +52 -86
  19. package/lib/shared/utils/datetime.d.ts +0 -19
  20. package/lib/version.d.ts +1 -1
  21. package/package.json +1 -1
  22. package/public-api.d.ts +1 -3
  23. package/src/lib/services/firebase/firebase-messaging-sw.js +125 -70
  24. package/esm2022/lib/components/molecules/chat-composer/chat-composer.component.mjs +0 -238
  25. package/esm2022/lib/components/molecules/conversation-list-item/conversation-list-item.component.mjs +0 -108
  26. package/lib/components/molecules/chat-composer/chat-composer.component.d.ts +0 -46
  27. package/lib/components/molecules/conversation-list-item/conversation-list-item.component.d.ts +0 -30
@@ -25,7 +25,7 @@ import { getApps, getApp, initializeApp as initializeApp$1 } from 'firebase/app'
25
25
  import * as i2$1 from '@ionic/angular/standalone';
26
26
  import { AlertController, IonAvatar, IonCard, IonIcon, IonButton, IonSpinner, IonText, IonModal, IonHeader, IonToolbar, IonContent, IonButtons, IonTitle, IonProgressBar, IonSkeletonText, IonFab, IonFabButton, IonFabList, IonLabel, IonCardContent, IonCardHeader, IonCardTitle, IonCardSubtitle, IonCheckbox, IonRadio, IonRadioGroup, IonTextarea, IonDatetime, IonSelect, IonSelectOption, IonPopover, IonList, IonItem, IonInput, IonChip, IonRange, IonSearchbar, IonSegment, IonSegmentButton, IonToggle, IonRippleEffect, IonAccordion, IonAccordionGroup, IonTabBar, IonTabButton, IonBadge, IonBreadcrumb, IonBreadcrumbs, IonInfiniteScroll, IonInfiniteScrollContent, IonNote, IonDatetimeButton, ToastController as ToastController$1, IonCol, IonRow, IonRefresher, IonRefresherContent, IonMenuButton, IonFooter, IonListHeader, IonGrid, MenuController, IonMenu, IonMenuToggle, IonSplitPane } from '@ionic/angular/standalone';
27
27
  import { addIcons } from 'ionicons';
28
- import { addOutline, addCircleOutline, alertOutline, alertCircleOutline, arrowBackOutline, arrowForwardOutline, arrowDownOutline, settings, settingsOutline, checkmarkCircleOutline, ellipsisHorizontalOutline, notifications, notificationsOutline, openOutline, closeOutline, chatbubblesOutline, shareOutline, heart, heartOutline, home, homeOutline, eyeOffOutline, eyeOutline, scanOutline, chevronDownOutline, chevronForwardOutline, checkmarkOutline, clipboardOutline, copyOutline, filterOutline, locationOutline, calendarOutline, businessOutline, logoTwitter, logoInstagram, logoLinkedin, logoYoutube, logoTiktok, logoFacebook, logoGoogle, createOutline, trashOutline, playOutline, peopleOutline, phonePortraitOutline, refreshOutline, documentTextOutline, lockClosedOutline, cloudUploadOutline, textOutline, informationCircleOutline, logoNpm, removeOutline, optionsOutline, personOutline, shieldCheckmarkOutline, keyOutline, desktopOutline, logOutOutline, cloudDownloadOutline, cloudOutline, gitBranchOutline, layersOutline, serverOutline, warningOutline, bulbOutline, lockOpenOutline, diamondOutline, newspaperOutline, add, close, share, create, trash, star, camera, mic, send, downloadOutline, chevronDown, language, globeOutline, checkmark, list, grid, apps, menu, search, person, helpCircle, informationCircle, documentText, mail, calendar, folder, chevronForward, ellipsisHorizontal, chevronBack, playBack, playForward, ellipse, starOutline, starHalf, heartHalf, checkmarkCircle, timeOutline, flag, trendingUp, trendingDown, remove, analytics, people, cash, cart, eye, chatbubbleOutline, thumbsUpOutline, thumbsUp, happyOutline, happy, sadOutline, sad, chevronUp, pin, pencil, callOutline, logoWhatsapp, paperPlaneOutline, mailOutline, chevronDownCircleOutline, closeCircle, alertCircle, logoApple, logoMicrosoft, linkOutline, unlinkOutline, cloudOfflineOutline, documentOutline, attachOutline, cameraOutline, closeCircleOutline, imageOutline, printOutline, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, searchOutline, cartOutline, chatbubble, compass, compassOutline, gridOutline, listOutline, folderOutline, documents, documentsOutline, statsChart, statsChartOutline, laptopOutline, swapHorizontalOutline, codeSlashOutline, notificationsOffOutline, alarmOutline, chatboxEllipsesOutline, checkmarkDoneOutline, cogOutline, shareSocialOutline, helpCircleOutline, menuOutline, stopCircleOutline, videocamOffOutline, checkmarkDone, arrowUndoOutline, micOutline, arrowUpOutline } from 'ionicons/icons';
28
+ import { addOutline, addCircleOutline, alertOutline, alertCircleOutline, arrowBackOutline, arrowForwardOutline, arrowDownOutline, settings, settingsOutline, checkmarkCircleOutline, ellipsisHorizontalOutline, notifications, notificationsOutline, openOutline, closeOutline, chatbubblesOutline, shareOutline, heart, heartOutline, home, homeOutline, eyeOffOutline, eyeOutline, scanOutline, chevronDownOutline, chevronForwardOutline, checkmarkOutline, clipboardOutline, copyOutline, filterOutline, locationOutline, calendarOutline, businessOutline, logoTwitter, logoInstagram, logoLinkedin, logoYoutube, logoTiktok, logoFacebook, logoGoogle, createOutline, trashOutline, playOutline, peopleOutline, phonePortraitOutline, refreshOutline, documentTextOutline, lockClosedOutline, cloudUploadOutline, textOutline, informationCircleOutline, logoNpm, removeOutline, optionsOutline, personOutline, shieldCheckmarkOutline, keyOutline, desktopOutline, logOutOutline, cloudDownloadOutline, cloudOutline, gitBranchOutline, layersOutline, serverOutline, warningOutline, bulbOutline, lockOpenOutline, diamondOutline, newspaperOutline, add, close, share, create, trash, star, camera, mic, send, downloadOutline, chevronDown, language, globeOutline, checkmark, list, grid, apps, menu, search, person, helpCircle, informationCircle, documentText, mail, calendar, folder, chevronForward, ellipsisHorizontal, chevronBack, playBack, playForward, ellipse, starOutline, starHalf, heartHalf, checkmarkCircle, timeOutline, flag, trendingUp, trendingDown, remove, analytics, people, cash, cart, eye, chatbubbleOutline, thumbsUpOutline, thumbsUp, happyOutline, happy, sadOutline, sad, chevronUp, pin, pencil, callOutline, logoWhatsapp, paperPlaneOutline, mailOutline, chevronDownCircleOutline, closeCircle, alertCircle, logoApple, logoMicrosoft, linkOutline, unlinkOutline, cloudOfflineOutline, documentOutline, attachOutline, cameraOutline, closeCircleOutline, imageOutline, printOutline, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, searchOutline, cartOutline, chatbubble, compass, compassOutline, gridOutline, listOutline, folderOutline, documents, documentsOutline, statsChart, statsChartOutline, laptopOutline, swapHorizontalOutline, codeSlashOutline, notificationsOffOutline, alarmOutline, chatboxEllipsesOutline, checkmarkDoneOutline, cogOutline, shareSocialOutline, helpCircleOutline, menuOutline, stopCircleOutline, videocamOffOutline, checkmarkDone, returnDownBackOutline } from 'ionicons/icons';
29
29
  import { Browser } from '@capacitor/browser';
30
30
  import * as i1$6 from '@angular/platform-browser';
31
31
  import { DomSanitizer, Meta, Title } from '@angular/platform-browser';
@@ -56,7 +56,7 @@ import { BrowserMultiFormatReader } from '@zxing/browser';
56
56
  * Current version of valtech-components.
57
57
  * This is automatically updated during the publish process.
58
58
  */
59
- const VERSION = '4.0.226';
59
+ const VERSION = '4.0.228';
60
60
 
61
61
  // Control de estado de refresco (singleton a nivel de módulo)
62
62
  let isRefreshing = false;
@@ -1538,59 +1538,6 @@ function getTimeOfDayKey(date = new Date()) {
1538
1538
  return 'goodAfternoon';
1539
1539
  return 'goodEvening';
1540
1540
  }
1541
- /** Convierte un valor de fecha (Date | ISO string | undefined) a Date válido o null. */
1542
- function toDate(value) {
1543
- if (!value)
1544
- return null;
1545
- const d = value instanceof Date ? value : new Date(value);
1546
- return Number.isNaN(d.getTime()) ? null : d;
1547
- }
1548
- /** Diferencia en días-calendario entre `from` y `to` (to - from), ignorando la hora. */
1549
- function calendarDayDiff(from, to) {
1550
- const a = new Date(from.getFullYear(), from.getMonth(), from.getDate());
1551
- const b = new Date(to.getFullYear(), to.getMonth(), to.getDate());
1552
- return Math.round((b.getTime() - a.getTime()) / 86_400_000);
1553
- }
1554
- /** Hora corta local, ej. "14:05". */
1555
- function formatClockTime(value, locale = 'es-CL') {
1556
- const d = toDate(value);
1557
- if (!d)
1558
- return '';
1559
- return d.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' });
1560
- }
1561
- /**
1562
- * Etiqueta corta para listas de conversaciones (estilo Telegram/WhatsApp):
1563
- * hoy → hora ("14:05"), ayer → label `yesterday`, esta semana → día de semana,
1564
- * más viejo → fecha corta. `labels.today/yesterday` se inyectan ya traducidos.
1565
- */
1566
- function formatRelativeTime(value, locale = 'es-CL', labels = {}) {
1567
- const d = toDate(value);
1568
- if (!d)
1569
- return '';
1570
- const diff = calendarDayDiff(d, new Date());
1571
- if (diff <= 0)
1572
- return formatClockTime(d, locale);
1573
- if (diff === 1)
1574
- return labels.yesterday ?? 'Ayer';
1575
- if (diff < 7)
1576
- return d.toLocaleDateString(locale, { weekday: 'short' });
1577
- return d.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: '2-digit' });
1578
- }
1579
- /**
1580
- * Etiqueta para separadores de fecha dentro de un chat: "Hoy" / "Ayer" /
1581
- * fecha larga ("12 de junio de 2026"). `labels.today/yesterday` ya traducidos.
1582
- */
1583
- function formatDateSeparator(value, locale = 'es-CL', labels = {}) {
1584
- const d = toDate(value);
1585
- if (!d)
1586
- return '';
1587
- const diff = calendarDayDiff(d, new Date());
1588
- if (diff <= 0)
1589
- return labels.today ?? 'Hoy';
1590
- if (diff === 1)
1591
- return labels.yesterday ?? 'Ayer';
1592
- return d.toLocaleDateString(locale, { day: 'numeric', month: 'long', year: 'numeric' });
1593
- }
1594
1541
 
1595
1542
  /**
1596
1543
  * Servicio de internacionalización basado en Angular Signals.
@@ -7083,8 +7030,6 @@ const VALTECH_DEFAULT_CONTENT = {
7083
7030
  loading: 'Cargando...',
7084
7031
  search: 'Buscar',
7085
7032
  learnMore: 'Saber más',
7086
- today: 'Hoy',
7087
- yesterday: 'Ayer',
7088
7033
  // Estados
7089
7034
  success: 'Éxito',
7090
7035
  error: 'Error',
@@ -7242,8 +7187,6 @@ const VALTECH_DEFAULT_CONTENT = {
7242
7187
  loading: 'Loading...',
7243
7188
  search: 'Search',
7244
7189
  learnMore: 'Learn more',
7245
- today: 'Today',
7246
- yesterday: 'Yesterday',
7247
7190
  // States
7248
7191
  success: 'Success',
7249
7192
  error: 'Error',
@@ -72097,29 +72040,21 @@ class ConversationService {
72097
72040
  }
72098
72041
  /**
72099
72042
  * Crea una nueva conversacion.
72100
- * El backend responde con un envelope { operationId, conversation } — lo
72101
- * desenvolvemos para devolver la Conversation directa.
72102
72043
  */
72103
72044
  createConversation(req) {
72104
- return this.http
72105
- .post(`${this.baseUrl}/conversations`, req)
72106
- .pipe(map(r => r.conversation));
72045
+ return this.http.post(`${this.baseUrl}/conversations`, req);
72107
72046
  }
72108
72047
  /**
72109
- * Obtiene o crea una conversacion asociada a una entidad (post, request, org, etc.).
72048
+ * Obtiene o crea una conversacion asociada a una entidad (post, request, etc.).
72110
72049
  * Usa el patron upsert del backend: si ya existe una conversacion para esa entidad,
72111
72050
  * la devuelve; si no, la crea.
72112
- *
72113
- * El backend espera { entityRef: { type, id }, type } y responde con un envelope
72114
- * { operationId, conversation, created }.
72115
72051
  */
72116
- getOrCreateForEntity(entityType, entityId, type = 'thread') {
72117
- return this.http
72118
- .post(`${this.baseUrl}/entity-conversations`, {
72119
- entityRef: { type: entityType, id: entityId },
72052
+ getOrCreateForEntity(entityType, entityId, type = 'entity') {
72053
+ return this.http.post(`${this.baseUrl}/conversations/entity-conversations`, {
72054
+ entityType,
72055
+ entityId,
72120
72056
  type,
72121
- })
72122
- .pipe(map(r => r.conversation));
72057
+ });
72123
72058
  }
72124
72059
  /**
72125
72060
  * Lista las conversaciones del usuario autenticado.
@@ -72134,17 +72069,13 @@ class ConversationService {
72134
72069
  * Obtiene una conversacion por ID.
72135
72070
  */
72136
72071
  getConversation(convId) {
72137
- return this.http
72138
- .get(`${this.baseUrl}/conversations/${convId}`)
72139
- .pipe(map(r => r.conversation));
72072
+ return this.http.get(`${this.baseUrl}/conversations/${convId}`);
72140
72073
  }
72141
72074
  /**
72142
- * Actualiza el nombre o settings de una conversacion.
72075
+ * Actualiza el titulo o estado de una conversacion.
72143
72076
  */
72144
72077
  updateConversation(convId, req) {
72145
- return this.http
72146
- .patch(`${this.baseUrl}/conversations/${convId}`, req)
72147
- .pipe(map(r => r.conversation));
72078
+ return this.http.patch(`${this.baseUrl}/conversations/${convId}`, req);
72148
72079
  }
72149
72080
  /**
72150
72081
  * Abre una conversacion cerrada.
@@ -72343,308 +72274,241 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72343
72274
  */
72344
72275
 
72345
72276
  addIcons({
72346
- alertCircle,
72347
72277
  checkmark,
72348
72278
  checkmarkDone,
72349
- createOutline,
72350
- documentOutline,
72351
- happyOutline,
72352
- arrowUndoOutline,
72353
72279
  timeOutline,
72354
- trashOutline,
72280
+ alertCircle,
72281
+ pencil,
72282
+ trash,
72283
+ returnDownBackOutline,
72355
72284
  });
72356
- const MESSAGE_BUBBLE_I18N = {
72285
+ const CHAT_I18N = {
72357
72286
  es: {
72287
+ messageDeleted: '[mensaje eliminado]',
72288
+ edited: '(editado)',
72289
+ react: 'Reaccionar',
72358
72290
  reply: 'Responder',
72359
72291
  edit: 'Editar',
72360
72292
  delete: 'Eliminar',
72361
- react: 'Reaccionar',
72362
- deleted: 'Mensaje eliminado',
72363
- edited: 'editado',
72364
72293
  },
72365
72294
  en: {
72295
+ messageDeleted: '[message deleted]',
72296
+ edited: '(edited)',
72297
+ react: 'React',
72366
72298
  reply: 'Reply',
72367
72299
  edit: 'Edit',
72368
72300
  delete: 'Delete',
72369
- react: 'React',
72370
- deleted: 'Message deleted',
72371
- edited: 'edited',
72372
72301
  },
72373
72302
  };
72374
72303
  /**
72375
- * val-message-bubble — burbuja de mensaje presentacional (signal inputs).
72304
+ * val-message-bubble
72376
72305
  *
72377
- * Sin lógica de negocio: el contenedor (`val-chat-window`) decide agrupación y
72378
- * pasa flags (showAvatar/showName/tail). Las acciones (responder/editar/borrar/
72379
- * reaccionar) NO están siempre visibles: se revelan en hover (desktop) o al tocar
72380
- * la burbuja (móvil), y se emiten por un único `(action)` tipado.
72306
+ * Burbuja de mensaje para el chat. Alineada a la derecha si isMine, a la izquierda si no.
72307
+ * Muestra nombre del emisor, cuerpo, timestamp, estado de envio, reacciones y acciones.
72308
+ * Si el mensaje fue eliminado muestra un texto en gris sin acciones.
72381
72309
  *
72382
- * Read receipts estilo WhatsApp: sending → reloj, sent → ✓, delivered/read → ✓✓
72383
- * (read en color), failed → alerta.
72310
+ * @example
72311
+ * <val-message-bubble
72312
+ * [props]="msg"
72313
+ * (reactionClick)="onReaction($event)"
72314
+ * (replyClick)="onReply($event)"
72315
+ * (deleteClick)="onDelete($event)"
72316
+ * />
72384
72317
  */
72385
72318
  class MessageBubbleComponent {
72386
72319
  constructor() {
72387
72320
  this.i18n = inject(I18nService);
72388
- this.msg = input.required();
72389
- this.showAvatar = input(false);
72390
- this.showName = input(false);
72391
- this.tail = input(true);
72392
- this.locale = input('es-CL');
72393
- this.action = output();
72394
- this.actionsOpen = signal(false);
72395
- this.time = computed(() => formatClockTime(this.msg().createdAt, this.locale()));
72396
- this.initials = computed(() => {
72397
- const name = this.msg().senderName?.trim() ?? '';
72398
- if (!name)
72399
- return '?';
72400
- const parts = name.split(/\s+/);
72401
- return (parts[0][0] + (parts[1]?.[0] ?? '')).toUpperCase();
72402
- });
72403
- this.imageAttachments = computed(() => (this.msg().attachments ?? []).filter(a => a.mimeType?.startsWith('image/')));
72404
- this.fileAttachments = computed(() => (this.msg().attachments ?? []).filter(a => !a.mimeType?.startsWith('image/')));
72405
- this.statusIcon = computed(() => {
72406
- switch (this.msg().status) {
72407
- case 'sending':
72408
- return 'time-outline';
72409
- case 'failed':
72410
- return 'alert-circle';
72411
- case 'delivered':
72412
- case 'read':
72413
- return 'checkmark-done';
72414
- case 'sent':
72415
- default:
72416
- return 'checkmark';
72417
- }
72418
- });
72419
- this.i18n.registerDefaults('MessageBubble', MESSAGE_BUBBLE_I18N);
72321
+ this.reactionClick = new EventEmitter();
72322
+ this.replyClick = new EventEmitter();
72323
+ this.deleteClick = new EventEmitter();
72324
+ this.editClick = new EventEmitter();
72325
+ if (!this.i18n.hasNamespace('Chat')) {
72326
+ this.i18n.registerDefaults('Chat', CHAT_I18N);
72327
+ }
72420
72328
  }
72421
- toggleActions() {
72422
- if (this.msg().isDeleted)
72423
- return;
72424
- this.actionsOpen.update(v => !v);
72329
+ ngOnInit() { }
72330
+ deletedText() {
72331
+ this.i18n.lang();
72332
+ return this.i18n.t('messageDeleted', 'Chat');
72425
72333
  }
72426
- emit(type, token) {
72427
- this.actionsOpen.set(false);
72428
- this.action.emit({ type, msgId: this.msg().msgId, token });
72334
+ editedLabel() {
72335
+ this.i18n.lang();
72336
+ return this.i18n.t('edited', 'Chat');
72429
72337
  }
72430
- t(key) {
72431
- return this.i18n.t(key, 'MessageBubble');
72338
+ formatTime(date) {
72339
+ if (!date)
72340
+ return '';
72341
+ const d = date instanceof Date ? date : new Date(date);
72342
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
72343
+ }
72344
+ statusIcon() {
72345
+ switch (this.props.status) {
72346
+ case 'sending':
72347
+ return 'time-outline';
72348
+ case 'failed':
72349
+ return 'alert-circle';
72350
+ case 'sent':
72351
+ default:
72352
+ return 'checkmark';
72353
+ }
72354
+ }
72355
+ onReactionClick(reaction) {
72356
+ this.reactionClick.emit({ token: reaction.token, msgId: this.props.msgId });
72357
+ }
72358
+ onReplyClick() {
72359
+ this.replyClick.emit({ msgId: this.props.msgId });
72360
+ }
72361
+ onDeleteClick() {
72362
+ this.deleteClick.emit({ msgId: this.props.msgId });
72363
+ }
72364
+ onEditClick() {
72365
+ this.editClick.emit({ msgId: this.props.msgId });
72432
72366
  }
72433
72367
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageBubbleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
72434
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: MessageBubbleComponent, isStandalone: true, selector: "val-message-bubble", inputs: { msg: { classPropertyName: "msg", publicName: "msg", isSignal: true, isRequired: true, transformFunction: null }, showAvatar: { classPropertyName: "showAvatar", publicName: "showAvatar", isSignal: true, isRequired: false, transformFunction: null }, showName: { classPropertyName: "showName", publicName: "showName", isSignal: true, isRequired: false, transformFunction: null }, tail: { classPropertyName: "tail", publicName: "tail", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { action: "action" }, ngImport: i0, template: `
72435
- <div class="row" [class.mine]="msg().isMine" [class.tail]="tail()" [class.with-avatar]="showAvatar()">
72436
- @if (showAvatar() && !msg().isMine) {
72437
- <div class="avatar" [class.hidden]="!tail()">
72438
- @if (msg().senderAvatar) {
72439
- <img [src]="msg().senderAvatar" [alt]="msg().senderName" />
72440
- } @else {
72441
- <span>{{ initials() }}</span>
72442
- }
72443
- </div>
72368
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: MessageBubbleComponent, isStandalone: true, selector: "val-message-bubble", inputs: { props: "props" }, outputs: { reactionClick: "reactionClick", replyClick: "replyClick", deleteClick: "deleteClick", editClick: "editClick" }, ngImport: i0, template: `
72369
+ <div class="message-bubble-wrapper" [class.is-mine]="props.isMine">
72370
+ @if (!props.isMine && !props.isDeleted) {
72371
+ <span class="sender-name">{{ props.senderName }}</span>
72444
72372
  }
72445
72373
 
72446
- <div class="bubble-wrap">
72447
- <div
72448
- class="bubble"
72449
- [class.deleted]="msg().isDeleted"
72450
- (click)="toggleActions()"
72451
- (keyup.enter)="toggleActions()"
72452
- tabindex="0"
72453
- >
72454
- @if (showName() && !msg().isMine && !msg().isDeleted) {
72455
- <span class="name">{{ msg().senderName }}</span>
72456
- }
72374
+ <div class="bubble">
72375
+ @if (props.replyTo && !props.isDeleted) {
72376
+ <div class="reply-preview">
72377
+ <div class="reply-sender">{{ props.replyTo.senderName }}</div>
72378
+ <div class="reply-body">{{ props.replyTo.body }}</div>
72379
+ </div>
72380
+ }
72457
72381
 
72458
- @if (msg().replyTo && !msg().isDeleted) {
72459
- <div class="reply-preview">
72460
- <span class="reply-name">{{ msg().replyTo!.senderName }}</span>
72461
- <span class="reply-body">{{ msg().replyTo!.body }}</span>
72462
- </div>
72463
- }
72382
+ @if (props.isDeleted) {
72383
+ <span class="deleted-text">{{ deletedText() }}</span>
72384
+ } @else {
72385
+ <div class="bubble-body">{{ props.body }}</div>
72386
+ }
72464
72387
 
72465
- @if (msg().isDeleted) {
72466
- <span class="body deleted-text">{{ t('deleted') }}</span>
72467
- } @else {
72468
- @if (imageAttachments().length > 0) {
72469
- <div class="attachments">
72470
- @for (att of imageAttachments(); track att.url) {
72471
- <img class="att-image" [src]="att.url" [alt]="att.name" />
72472
- }
72473
- </div>
72474
- }
72475
- @if (fileAttachments().length > 0) {
72476
- <div class="files">
72477
- @for (att of fileAttachments(); track att.url) {
72478
- <a class="file-chip" [href]="att.url" target="_blank" rel="noopener">
72479
- <ion-icon name="document-outline" aria-hidden="true" />
72480
- <span>{{ att.name }}</span>
72481
- </a>
72482
- }
72483
- </div>
72484
- }
72485
- @if (msg().body) {
72486
- <span class="body">{{ msg().body }}</span>
72487
- }
72388
+ <div class="bubble-meta">
72389
+ <span class="timestamp">{{ formatTime(props.createdAt) }}</span>
72390
+ @if (props.isEdited && !props.isDeleted) {
72391
+ <span class="edited-label">{{ editedLabel() }}</span>
72392
+ }
72393
+ @if (props.isMine && props.status) {
72394
+ <ion-icon
72395
+ [name]="statusIcon()"
72396
+ [class]="'status-icon ' + (props.status ?? '')"
72397
+ aria-hidden="true"
72398
+ ></ion-icon>
72488
72399
  }
72489
-
72490
- <span class="meta">
72491
- @if (msg().isEdited && !msg().isDeleted) {
72492
- <span class="edited">{{ t('edited') }}</span>
72493
- }
72494
- <span class="time">{{ time() }}</span>
72495
- @if (msg().isMine && !msg().isDeleted) {
72496
- <ion-icon
72497
- class="status"
72498
- [class.read]="msg().status === 'read'"
72499
- [class.failed]="msg().status === 'failed'"
72500
- [name]="statusIcon()"
72501
- aria-hidden="true"
72502
- />
72503
- }
72504
- </span>
72505
72400
  </div>
72401
+ </div>
72506
72402
 
72507
- @if (msg().reactions && msg().reactions!.length > 0) {
72508
- <div class="reactions">
72509
- @for (r of msg().reactions!; track r.token) {
72510
- <button class="reaction" [class.active]="r.active" (click)="emit('react', r.token)">
72511
- <span>{{ r.token }}</span>
72512
- <span class="count">{{ r.count }}</span>
72513
- </button>
72514
- }
72515
- </div>
72516
- }
72517
-
72518
- @if (!msg().isDeleted) {
72519
- <div class="actions" [class.open]="actionsOpen()">
72520
- <button class="act" [attr.aria-label]="t('react')" (click)="emit('react', '👍')">
72521
- <ion-icon name="happy-outline" aria-hidden="true" />
72522
- </button>
72523
- <button class="act" [attr.aria-label]="t('reply')" (click)="emit('reply')">
72524
- <ion-icon name="arrow-undo-outline" aria-hidden="true" />
72403
+ @if (!props.isDeleted && props.reactions && props.reactions.length > 0) {
72404
+ <div class="reactions-bar">
72405
+ @for (reaction of props.reactions; track reaction.token) {
72406
+ <button class="reaction-btn" [class.active]="reaction.active" (click)="onReactionClick(reaction)">
72407
+ <span>{{ reaction.token }}</span>
72408
+ @if (reaction.count > 0) {
72409
+ <span class="reaction-count">{{ reaction.count }}</span>
72410
+ }
72525
72411
  </button>
72526
- @if (msg().isMine) {
72527
- <button class="act" [attr.aria-label]="t('edit')" (click)="emit('edit')">
72528
- <ion-icon name="create-outline" aria-hidden="true" />
72529
- </button>
72530
- <button class="act danger" [attr.aria-label]="t('delete')" (click)="emit('delete')">
72531
- <ion-icon name="trash-outline" aria-hidden="true" />
72532
- </button>
72533
- }
72534
- </div>
72535
- }
72536
- </div>
72537
- </div>
72538
- `, isInline: true, styles: [":host{display:block}.row{display:flex;align-items:flex-end;gap:8px;margin:1px 0}.row.mine{flex-direction:row-reverse}.row.tail{margin-bottom:8px}.avatar{width:28px;height:28px;border-radius:50%;overflow:hidden;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:var(--ion-color-primary);color:#fff;font-size:.6875rem;font-weight:700}.avatar.hidden{visibility:hidden}.avatar img{width:100%;height:100%;object-fit:cover}.bubble-wrap{position:relative;max-width:min(78%,520px);display:flex;flex-direction:column}.row.mine .bubble-wrap{align-items:flex-end}.bubble{position:relative;padding:7px 11px;border-radius:16px;background:var(--ion-card-background, var(--ion-background-color, #fff));border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));color:var(--ion-text-color, #000);font-size:.9375rem;line-height:1.35;cursor:pointer;outline:none;word-break:break-word}.row:not(.mine).tail .bubble{border-bottom-left-radius:5px}.mine .bubble{background:var(--ion-color-primary);border-color:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff)}.mine.tail .bubble{border-bottom-right-radius:5px}.bubble.deleted{background:transparent;border-style:dashed;cursor:default}.name{display:block;font-size:.75rem;font-weight:700;color:var(--ion-color-primary);margin-bottom:2px}.reply-preview{display:flex;flex-direction:column;gap:1px;padding:4px 8px;margin-bottom:4px;border-left:3px solid currentColor;border-radius:6px;background:var(--ion-color-step-100, rgba(127, 127, 127, .12));font-size:.8125rem;opacity:.85}.reply-name{font-weight:700}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}.body{white-space:pre-wrap}.deleted-text{font-style:italic;opacity:.6}.attachments{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.att-image{max-width:100%;border-radius:10px;display:block}.files{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.file-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .12));color:inherit;text-decoration:none;font-size:.8125rem}.meta{display:inline-flex;align-items:center;gap:4px;float:right;margin:2px 0 -2px 8px;font-size:.6875rem;opacity:.7}.edited{font-style:italic}.status{font-size:.875rem}.status.read{color:var(--ion-color-secondary, #4fc3f7);opacity:1}.status.failed{color:var(--ion-color-danger, #eb445a);opacity:1}.reactions{display:flex;flex-wrap:wrap;gap:4px;margin-top:3px}.reaction{display:inline-flex;align-items:center;gap:3px;padding:1px 7px;border-radius:999px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff));font-size:.75rem;cursor:pointer;color:var(--ion-text-color, #000)}.reaction.active{border-color:var(--ion-color-primary);background:var(--ion-color-primary-tint, var(--ion-color-primary));color:var(--ion-color-primary-contrast, #fff)}.reaction .count{opacity:.75}.actions{position:absolute;top:-14px;display:flex;gap:2px;padding:2px;border-radius:999px;background:var(--ion-card-background, var(--ion-background-color, #fff));border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));box-shadow:0 2px 8px #0000001f;opacity:0;pointer-events:none;transition:opacity .12s ease;z-index:2}.row.mine .actions{right:0}.row:not(.mine) .actions{left:0}.bubble-wrap:hover .actions,.actions.open{opacity:1;pointer-events:auto}.act{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1rem}.act:hover{background:var(--ion-color-step-100, rgba(127, 127, 127, .12));color:var(--ion-text-color, #000)}.act.danger:hover{color:var(--ion-color-danger, #eb445a)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }] }); }
72539
- }
72540
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageBubbleComponent, decorators: [{
72541
- type: Component,
72542
- args: [{ selector: 'val-message-bubble', standalone: true, imports: [CommonModule, IonIcon], template: `
72543
- <div class="row" [class.mine]="msg().isMine" [class.tail]="tail()" [class.with-avatar]="showAvatar()">
72544
- @if (showAvatar() && !msg().isMine) {
72545
- <div class="avatar" [class.hidden]="!tail()">
72546
- @if (msg().senderAvatar) {
72547
- <img [src]="msg().senderAvatar" [alt]="msg().senderName" />
72548
- } @else {
72549
- <span>{{ initials() }}</span>
72550
72412
  }
72551
72413
  </div>
72552
72414
  }
72553
72415
 
72554
- <div class="bubble-wrap">
72555
- <div
72556
- class="bubble"
72557
- [class.deleted]="msg().isDeleted"
72558
- (click)="toggleActions()"
72559
- (keyup.enter)="toggleActions()"
72560
- tabindex="0"
72561
- >
72562
- @if (showName() && !msg().isMine && !msg().isDeleted) {
72563
- <span class="name">{{ msg().senderName }}</span>
72564
- }
72565
-
72566
- @if (msg().replyTo && !msg().isDeleted) {
72567
- <div class="reply-preview">
72568
- <span class="reply-name">{{ msg().replyTo!.senderName }}</span>
72569
- <span class="reply-body">{{ msg().replyTo!.body }}</span>
72570
- </div>
72571
- }
72572
-
72573
- @if (msg().isDeleted) {
72574
- <span class="body deleted-text">{{ t('deleted') }}</span>
72575
- } @else {
72576
- @if (imageAttachments().length > 0) {
72577
- <div class="attachments">
72578
- @for (att of imageAttachments(); track att.url) {
72579
- <img class="att-image" [src]="att.url" [alt]="att.name" />
72580
- }
72581
- </div>
72582
- }
72583
- @if (fileAttachments().length > 0) {
72584
- <div class="files">
72585
- @for (att of fileAttachments(); track att.url) {
72586
- <a class="file-chip" [href]="att.url" target="_blank" rel="noopener">
72587
- <ion-icon name="document-outline" aria-hidden="true" />
72588
- <span>{{ att.name }}</span>
72589
- </a>
72590
- }
72591
- </div>
72592
- }
72593
- @if (msg().body) {
72594
- <span class="body">{{ msg().body }}</span>
72595
- }
72416
+ @if (!props.isDeleted) {
72417
+ <div class="bubble-actions">
72418
+ <ion-button fill="clear" size="small" (click)="onReplyClick()">
72419
+ <ion-icon name="return-down-back-outline" slot="icon-only" aria-hidden="true"></ion-icon>
72420
+ </ion-button>
72421
+ @if (props.isMine) {
72422
+ <ion-button fill="clear" size="small" (click)="onEditClick()">
72423
+ <ion-icon name="pencil" slot="icon-only" aria-hidden="true"></ion-icon>
72424
+ </ion-button>
72425
+ <ion-button fill="clear" size="small" color="danger" (click)="onDeleteClick()">
72426
+ <ion-icon name="trash" slot="icon-only" aria-hidden="true"></ion-icon>
72427
+ </ion-button>
72596
72428
  }
72597
-
72598
- <span class="meta">
72599
- @if (msg().isEdited && !msg().isDeleted) {
72600
- <span class="edited">{{ t('edited') }}</span>
72601
- }
72602
- <span class="time">{{ time() }}</span>
72603
- @if (msg().isMine && !msg().isDeleted) {
72604
- <ion-icon
72605
- class="status"
72606
- [class.read]="msg().status === 'read'"
72607
- [class.failed]="msg().status === 'failed'"
72608
- [name]="statusIcon()"
72609
- aria-hidden="true"
72610
- />
72611
- }
72612
- </span>
72613
72429
  </div>
72430
+ }
72431
+ </div>
72432
+ `, isInline: true, styles: [".message-bubble-wrapper{display:flex;flex-direction:column;align-items:flex-start;margin-bottom:4px}.message-bubble-wrapper.is-mine{align-items:flex-end}.message-bubble-wrapper.is-mine .bubble{background:var(--ion-color-primary);color:#fff;border-radius:18px 18px 4px}.message-bubble-wrapper.is-mine .bubble-meta{justify-content:flex-end}.message-bubble-wrapper:not(.is-mine) .bubble{background:var(--ion-card-background, var(--ion-background-color, #fff));color:var(--ion-text-color, #000);border-radius:18px 18px 18px 4px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}.sender-name{font-size:.75rem;font-weight:600;color:var(--ion-color-medium, #92949c);margin:0 0 2px 12px}.bubble{max-width:75%;min-width:60px;padding:10px 14px;word-break:break-word;position:relative;box-shadow:0 1px 2px #0000000f}.reply-preview{background:#0000000f;border-left:3px solid var(--ion-color-primary);border-radius:6px;padding:6px 8px;margin-bottom:6px;font-size:.75rem;opacity:.85}.reply-preview .reply-sender{font-weight:700;margin-bottom:2px}.reply-preview .reply-body{opacity:.8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px}.bubble-body{font-size:.9375rem;line-height:1.4;white-space:pre-wrap}.deleted-text{font-size:.875rem;font-style:italic;opacity:.6}.bubble-meta{display:flex;align-items:center;gap:4px;margin-top:4px;font-size:.6875rem;opacity:.7}.edited-label{font-style:italic}.status-icon{font-size:.75rem}.status-icon.sending{color:var(--ion-color-medium)}.status-icon.sent{color:#fff}.status-icon.failed{color:var(--ion-color-danger)}.reactions-bar{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.reactions-bar .reaction-btn{display:inline-flex;align-items:center;gap:3px;background:var(--ion-card-background, var(--ion-background-color, #fff));border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));border-radius:12px;padding:2px 8px;font-size:.8125rem;cursor:pointer;transition:background .15s ease}.reactions-bar .reaction-btn.active{background:var(--ion-color-primary-tint, #4c8dff);color:#fff;border-color:var(--ion-color-primary)}.reactions-bar .reaction-btn .reaction-count{font-weight:600}.bubble-actions{display:flex;gap:4px;margin-top:2px}.bubble-actions ion-button{--padding-start: 6px;--padding-end: 6px;font-size:.75rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }] }); }
72433
+ }
72434
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageBubbleComponent, decorators: [{
72435
+ type: Component,
72436
+ args: [{ selector: 'val-message-bubble', standalone: true, imports: [CommonModule, IonIcon, IonButton], template: `
72437
+ <div class="message-bubble-wrapper" [class.is-mine]="props.isMine">
72438
+ @if (!props.isMine && !props.isDeleted) {
72439
+ <span class="sender-name">{{ props.senderName }}</span>
72440
+ }
72614
72441
 
72615
- @if (msg().reactions && msg().reactions!.length > 0) {
72616
- <div class="reactions">
72617
- @for (r of msg().reactions!; track r.token) {
72618
- <button class="reaction" [class.active]="r.active" (click)="emit('react', r.token)">
72619
- <span>{{ r.token }}</span>
72620
- <span class="count">{{ r.count }}</span>
72621
- </button>
72622
- }
72442
+ <div class="bubble">
72443
+ @if (props.replyTo && !props.isDeleted) {
72444
+ <div class="reply-preview">
72445
+ <div class="reply-sender">{{ props.replyTo.senderName }}</div>
72446
+ <div class="reply-body">{{ props.replyTo.body }}</div>
72623
72447
  </div>
72624
72448
  }
72625
72449
 
72626
- @if (!msg().isDeleted) {
72627
- <div class="actions" [class.open]="actionsOpen()">
72628
- <button class="act" [attr.aria-label]="t('react')" (click)="emit('react', '👍')">
72629
- <ion-icon name="happy-outline" aria-hidden="true" />
72630
- </button>
72631
- <button class="act" [attr.aria-label]="t('reply')" (click)="emit('reply')">
72632
- <ion-icon name="arrow-undo-outline" aria-hidden="true" />
72633
- </button>
72634
- @if (msg().isMine) {
72635
- <button class="act" [attr.aria-label]="t('edit')" (click)="emit('edit')">
72636
- <ion-icon name="create-outline" aria-hidden="true" />
72637
- </button>
72638
- <button class="act danger" [attr.aria-label]="t('delete')" (click)="emit('delete')">
72639
- <ion-icon name="trash-outline" aria-hidden="true" />
72640
- </button>
72641
- }
72642
- </div>
72450
+ @if (props.isDeleted) {
72451
+ <span class="deleted-text">{{ deletedText() }}</span>
72452
+ } @else {
72453
+ <div class="bubble-body">{{ props.body }}</div>
72643
72454
  }
72455
+
72456
+ <div class="bubble-meta">
72457
+ <span class="timestamp">{{ formatTime(props.createdAt) }}</span>
72458
+ @if (props.isEdited && !props.isDeleted) {
72459
+ <span class="edited-label">{{ editedLabel() }}</span>
72460
+ }
72461
+ @if (props.isMine && props.status) {
72462
+ <ion-icon
72463
+ [name]="statusIcon()"
72464
+ [class]="'status-icon ' + (props.status ?? '')"
72465
+ aria-hidden="true"
72466
+ ></ion-icon>
72467
+ }
72468
+ </div>
72644
72469
  </div>
72470
+
72471
+ @if (!props.isDeleted && props.reactions && props.reactions.length > 0) {
72472
+ <div class="reactions-bar">
72473
+ @for (reaction of props.reactions; track reaction.token) {
72474
+ <button class="reaction-btn" [class.active]="reaction.active" (click)="onReactionClick(reaction)">
72475
+ <span>{{ reaction.token }}</span>
72476
+ @if (reaction.count > 0) {
72477
+ <span class="reaction-count">{{ reaction.count }}</span>
72478
+ }
72479
+ </button>
72480
+ }
72481
+ </div>
72482
+ }
72483
+
72484
+ @if (!props.isDeleted) {
72485
+ <div class="bubble-actions">
72486
+ <ion-button fill="clear" size="small" (click)="onReplyClick()">
72487
+ <ion-icon name="return-down-back-outline" slot="icon-only" aria-hidden="true"></ion-icon>
72488
+ </ion-button>
72489
+ @if (props.isMine) {
72490
+ <ion-button fill="clear" size="small" (click)="onEditClick()">
72491
+ <ion-icon name="pencil" slot="icon-only" aria-hidden="true"></ion-icon>
72492
+ </ion-button>
72493
+ <ion-button fill="clear" size="small" color="danger" (click)="onDeleteClick()">
72494
+ <ion-icon name="trash" slot="icon-only" aria-hidden="true"></ion-icon>
72495
+ </ion-button>
72496
+ }
72497
+ </div>
72498
+ }
72645
72499
  </div>
72646
- `, styles: [":host{display:block}.row{display:flex;align-items:flex-end;gap:8px;margin:1px 0}.row.mine{flex-direction:row-reverse}.row.tail{margin-bottom:8px}.avatar{width:28px;height:28px;border-radius:50%;overflow:hidden;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:var(--ion-color-primary);color:#fff;font-size:.6875rem;font-weight:700}.avatar.hidden{visibility:hidden}.avatar img{width:100%;height:100%;object-fit:cover}.bubble-wrap{position:relative;max-width:min(78%,520px);display:flex;flex-direction:column}.row.mine .bubble-wrap{align-items:flex-end}.bubble{position:relative;padding:7px 11px;border-radius:16px;background:var(--ion-card-background, var(--ion-background-color, #fff));border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));color:var(--ion-text-color, #000);font-size:.9375rem;line-height:1.35;cursor:pointer;outline:none;word-break:break-word}.row:not(.mine).tail .bubble{border-bottom-left-radius:5px}.mine .bubble{background:var(--ion-color-primary);border-color:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff)}.mine.tail .bubble{border-bottom-right-radius:5px}.bubble.deleted{background:transparent;border-style:dashed;cursor:default}.name{display:block;font-size:.75rem;font-weight:700;color:var(--ion-color-primary);margin-bottom:2px}.reply-preview{display:flex;flex-direction:column;gap:1px;padding:4px 8px;margin-bottom:4px;border-left:3px solid currentColor;border-radius:6px;background:var(--ion-color-step-100, rgba(127, 127, 127, .12));font-size:.8125rem;opacity:.85}.reply-name{font-weight:700}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}.body{white-space:pre-wrap}.deleted-text{font-style:italic;opacity:.6}.attachments{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.att-image{max-width:100%;border-radius:10px;display:block}.files{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.file-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .12));color:inherit;text-decoration:none;font-size:.8125rem}.meta{display:inline-flex;align-items:center;gap:4px;float:right;margin:2px 0 -2px 8px;font-size:.6875rem;opacity:.7}.edited{font-style:italic}.status{font-size:.875rem}.status.read{color:var(--ion-color-secondary, #4fc3f7);opacity:1}.status.failed{color:var(--ion-color-danger, #eb445a);opacity:1}.reactions{display:flex;flex-wrap:wrap;gap:4px;margin-top:3px}.reaction{display:inline-flex;align-items:center;gap:3px;padding:1px 7px;border-radius:999px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff));font-size:.75rem;cursor:pointer;color:var(--ion-text-color, #000)}.reaction.active{border-color:var(--ion-color-primary);background:var(--ion-color-primary-tint, var(--ion-color-primary));color:var(--ion-color-primary-contrast, #fff)}.reaction .count{opacity:.75}.actions{position:absolute;top:-14px;display:flex;gap:2px;padding:2px;border-radius:999px;background:var(--ion-card-background, var(--ion-background-color, #fff));border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));box-shadow:0 2px 8px #0000001f;opacity:0;pointer-events:none;transition:opacity .12s ease;z-index:2}.row.mine .actions{right:0}.row:not(.mine) .actions{left:0}.bubble-wrap:hover .actions,.actions.open{opacity:1;pointer-events:auto}.act{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1rem}.act:hover{background:var(--ion-color-step-100, rgba(127, 127, 127, .12));color:var(--ion-text-color, #000)}.act.danger:hover{color:var(--ion-color-danger, #eb445a)}\n"] }]
72647
- }], ctorParameters: () => [] });
72500
+ `, styles: [".message-bubble-wrapper{display:flex;flex-direction:column;align-items:flex-start;margin-bottom:4px}.message-bubble-wrapper.is-mine{align-items:flex-end}.message-bubble-wrapper.is-mine .bubble{background:var(--ion-color-primary);color:#fff;border-radius:18px 18px 4px}.message-bubble-wrapper.is-mine .bubble-meta{justify-content:flex-end}.message-bubble-wrapper:not(.is-mine) .bubble{background:var(--ion-card-background, var(--ion-background-color, #fff));color:var(--ion-text-color, #000);border-radius:18px 18px 18px 4px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}.sender-name{font-size:.75rem;font-weight:600;color:var(--ion-color-medium, #92949c);margin:0 0 2px 12px}.bubble{max-width:75%;min-width:60px;padding:10px 14px;word-break:break-word;position:relative;box-shadow:0 1px 2px #0000000f}.reply-preview{background:#0000000f;border-left:3px solid var(--ion-color-primary);border-radius:6px;padding:6px 8px;margin-bottom:6px;font-size:.75rem;opacity:.85}.reply-preview .reply-sender{font-weight:700;margin-bottom:2px}.reply-preview .reply-body{opacity:.8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px}.bubble-body{font-size:.9375rem;line-height:1.4;white-space:pre-wrap}.deleted-text{font-size:.875rem;font-style:italic;opacity:.6}.bubble-meta{display:flex;align-items:center;gap:4px;margin-top:4px;font-size:.6875rem;opacity:.7}.edited-label{font-style:italic}.status-icon{font-size:.75rem}.status-icon.sending{color:var(--ion-color-medium)}.status-icon.sent{color:#fff}.status-icon.failed{color:var(--ion-color-danger)}.reactions-bar{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.reactions-bar .reaction-btn{display:inline-flex;align-items:center;gap:3px;background:var(--ion-card-background, var(--ion-background-color, #fff));border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));border-radius:12px;padding:2px 8px;font-size:.8125rem;cursor:pointer;transition:background .15s ease}.reactions-bar .reaction-btn.active{background:var(--ion-color-primary-tint, #4c8dff);color:#fff;border-color:var(--ion-color-primary)}.reactions-bar .reaction-btn .reaction-count{font-weight:600}.bubble-actions{display:flex;gap:4px;margin-top:2px}.bubble-actions ion-button{--padding-start: 6px;--padding-end: 6px;font-size:.75rem}\n"] }]
72501
+ }], ctorParameters: () => [], propDecorators: { props: [{
72502
+ type: Input
72503
+ }], reactionClick: [{
72504
+ type: Output
72505
+ }], replyClick: [{
72506
+ type: Output
72507
+ }], deleteClick: [{
72508
+ type: Output
72509
+ }], editClick: [{
72510
+ type: Output
72511
+ }] } });
72648
72512
 
72649
72513
  addIcons({ sendOutline });
72650
72514
  const CHAT_INPUT_I18N = {
@@ -72737,6 +72601,10 @@ class ChatInputComponent {
72737
72601
  [autoGrow]="true"
72738
72602
  [rows]="1"
72739
72603
  [value]="body()"
72604
+ enterkeyhint="send"
72605
+ autocomplete="off"
72606
+ autocorrect="off"
72607
+ spellcheck="false"
72740
72608
  (ionInput)="onInput($event)"
72741
72609
  (keydown)="onKeydown($event)"
72742
72610
  ></ion-textarea>
@@ -72769,6 +72637,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72769
72637
  [autoGrow]="true"
72770
72638
  [rows]="1"
72771
72639
  [value]="body()"
72640
+ enterkeyhint="send"
72641
+ autocomplete="off"
72642
+ autocorrect="off"
72643
+ spellcheck="false"
72772
72644
  (ionInput)="onInput($event)"
72773
72645
  (keydown)="onKeydown($event)"
72774
72646
  ></ion-textarea>
@@ -72796,340 +72668,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72796
72668
  type: Output
72797
72669
  }] } });
72798
72670
 
72799
- addIcons({ addOutline, closeOutline, happyOutline, micOutline, arrowUpOutline });
72800
- const CHAT_COMPOSER_I18N = {
72801
- es: {
72802
- placeholder: 'Escribe un mensaje',
72803
- send: 'Enviar',
72804
- attach: 'Adjuntar',
72805
- emoji: 'Emoji',
72806
- voice: 'Mensaje de voz',
72807
- replyingTo: 'Respondiendo a',
72808
- cancel: 'Cancelar',
72809
- },
72810
- en: {
72811
- placeholder: 'Write a message',
72812
- send: 'Send',
72813
- attach: 'Attach',
72814
- emoji: 'Emoji',
72815
- voice: 'Voice message',
72816
- replyingTo: 'Replying to',
72817
- cancel: 'Cancel',
72818
- },
72819
- };
72820
- const QUICK_EMOJIS = ['👍', '❤️', '😂', '🎉', '🙏', '🔥', '😮', '😢'];
72821
- /**
72822
- * val-chat-composer — barra de redacción de chat moderna (signal inputs).
72823
- *
72824
- * Pill con: adjuntar (+), textarea auto-grow, emoji (fila rápida) y mic/enviar
72825
- * (mic cuando está vacío, enviar cuando hay texto — patrón WhatsApp/Telegram).
72826
- * Muestra una barra de contexto "respondiendo a" cuando `replyingTo` está seteado.
72827
- * `typing` se emite con debounce (máx. 1 vez cada 2s) para no spamear writes.
72828
- */
72829
- class ChatComposerComponent {
72830
- constructor() {
72831
- this.i18n = inject(I18nService);
72832
- this.placeholder = input('');
72833
- this.disabled = input(false);
72834
- this.maxLength = input(2000);
72835
- this.replyingTo = input(null);
72836
- this.showAttach = input(true);
72837
- this.showEmoji = input(true);
72838
- this.showMic = input(true);
72839
- this.send = output();
72840
- this.typing = output();
72841
- this.attach = output();
72842
- this.voice = output();
72843
- this.cancelReply = output();
72844
- this.body = signal('');
72845
- this.emojiOpen = signal(false);
72846
- this.quickEmojis = QUICK_EMOJIS;
72847
- this.lastTypingEmit = 0;
72848
- this.canSend = computed(() => {
72849
- const t = this.body().trim();
72850
- return t.length > 0 && t.length <= this.maxLength() && !this.disabled();
72851
- });
72852
- this.placeholderText = computed(() => {
72853
- this.i18n.lang();
72854
- return this.placeholder() || this.i18n.t('placeholder', 'ChatComposer');
72855
- });
72856
- this.i18n.registerDefaults('ChatComposer', CHAT_COMPOSER_I18N);
72857
- }
72858
- onInput(event) {
72859
- const target = event.target;
72860
- this.body.set(target.value ?? '');
72861
- this.emitTypingDebounced();
72862
- }
72863
- onKeydown(event) {
72864
- if (event.key === 'Enter' && !event.shiftKey) {
72865
- event.preventDefault();
72866
- this.onSend();
72867
- }
72868
- }
72869
- onSend() {
72870
- const trimmed = this.body().trim();
72871
- if (!trimmed || trimmed.length > this.maxLength())
72872
- return;
72873
- this.send.emit(trimmed);
72874
- this.body.set('');
72875
- this.emojiOpen.set(false);
72876
- }
72877
- toggleEmoji() {
72878
- this.emojiOpen.update(v => !v);
72879
- }
72880
- insertEmoji(emoji) {
72881
- this.body.update(v => v + emoji);
72882
- }
72883
- onFile(event) {
72884
- const input = event.target;
72885
- const file = input.files?.[0];
72886
- if (file)
72887
- this.attach.emit(file);
72888
- input.value = '';
72889
- }
72890
- /**
72891
- * Emite typing como máximo 1 vez cada 2s. No usa Date.now() directo en plantillas
72892
- * de workflow; aquí estamos en runtime de navegador, performance.now es seguro.
72893
- */
72894
- emitTypingDebounced() {
72895
- const now = performance.now();
72896
- if (now - this.lastTypingEmit > 2000) {
72897
- this.lastTypingEmit = now;
72898
- this.typing.emit();
72899
- }
72900
- }
72901
- t(key) {
72902
- return this.i18n.t(key, 'ChatComposer');
72903
- }
72904
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatComposerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
72905
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ChatComposerComponent, isStandalone: true, selector: "val-chat-composer", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, replyingTo: { classPropertyName: "replyingTo", publicName: "replyingTo", isSignal: true, isRequired: false, transformFunction: null }, showAttach: { classPropertyName: "showAttach", publicName: "showAttach", isSignal: true, isRequired: false, transformFunction: null }, showEmoji: { classPropertyName: "showEmoji", publicName: "showEmoji", isSignal: true, isRequired: false, transformFunction: null }, showMic: { classPropertyName: "showMic", publicName: "showMic", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { send: "send", typing: "typing", attach: "attach", voice: "voice", cancelReply: "cancelReply" }, ngImport: i0, template: `
72906
- <div class="composer">
72907
- @if (replyingTo()) {
72908
- <div class="reply-bar">
72909
- <div class="reply-content">
72910
- <span class="reply-name">{{ replyingTo()!.senderName }}</span>
72911
- <span class="reply-body">{{ replyingTo()!.body }}</span>
72912
- </div>
72913
- <button class="icon-btn" [attr.aria-label]="t('cancel')" (click)="cancelReply.emit()">
72914
- <ion-icon name="close-outline" aria-hidden="true" />
72915
- </button>
72916
- </div>
72917
- }
72918
-
72919
- @if (emojiOpen()) {
72920
- <div class="emoji-row">
72921
- @for (e of quickEmojis; track e) {
72922
- <button class="emoji" (click)="insertEmoji(e)">{{ e }}</button>
72923
- }
72924
- </div>
72925
- }
72926
-
72927
- <div class="pill">
72928
- @if (showAttach()) {
72929
- <label class="icon-btn" [attr.aria-label]="t('attach')">
72930
- <ion-icon name="add-outline" aria-hidden="true" />
72931
- <input type="file" hidden (change)="onFile($event)" accept="image/*,application/pdf" />
72932
- </label>
72933
- }
72934
-
72935
- <ion-textarea
72936
- class="field"
72937
- [placeholder]="placeholderText()"
72938
- [disabled]="disabled()"
72939
- [maxlength]="maxLength()"
72940
- [autoGrow]="true"
72941
- [rows]="1"
72942
- [value]="body()"
72943
- (ionInput)="onInput($event)"
72944
- (keydown)="onKeydown($event)"
72945
- ></ion-textarea>
72946
-
72947
- @if (showEmoji()) {
72948
- <button class="icon-btn" [class.active]="emojiOpen()" [attr.aria-label]="t('emoji')" (click)="toggleEmoji()">
72949
- <ion-icon name="happy-outline" aria-hidden="true" />
72950
- </button>
72951
- }
72952
-
72953
- @if (canSend()) {
72954
- <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
72955
- <ion-icon name="arrow-up-outline" aria-hidden="true" />
72956
- </button>
72957
- } @else if (showMic()) {
72958
- <button class="icon-btn mic" [attr.aria-label]="t('voice')" (click)="voice.emit()">
72959
- <ion-icon name="mic-outline" aria-hidden="true" />
72960
- </button>
72961
- }
72962
- </div>
72963
- </div>
72964
- `, isInline: true, styles: [":host{display:block}.composer{display:flex;flex-direction:column;gap:6px;padding:8px 10px calc(8px + env(safe-area-inset-bottom,0px));background:var(--ion-background-color, #fff);border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}.reply-bar{display:flex;align-items:center;gap:8px;padding:6px 10px;border-left:3px solid var(--ion-color-primary);border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.reply-content{display:flex;flex-direction:column;flex:1;min-width:0;font-size:.8125rem}.reply-name{font-weight:700;color:var(--ion-color-primary)}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ion-color-medium, #92949c)}.emoji-row{display:flex;gap:4px;flex-wrap:wrap}.emoji{border:none;background:var(--ion-color-step-100, rgba(127, 127, 127, .1));border-radius:10px;padding:4px 8px;font-size:1.25rem;cursor:pointer}.pill{display:flex;align-items:flex-end;gap:4px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.field{flex:1;--background: transparent;--padding-start: 6px;--padding-end: 6px;--padding-top: 8px;--padding-bottom: 8px;margin:0;max-height:120px;font-size:.9375rem}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1.375rem}.icon-btn:hover,.icon-btn.active,.icon-btn.mic:hover{color:var(--ion-color-primary)}.send-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff);cursor:pointer;font-size:1.25rem;transition:transform .1s ease}.send-btn:active{transform:scale(.92)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }] }); }
72965
- }
72966
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatComposerComponent, decorators: [{
72967
- type: Component,
72968
- args: [{ selector: 'val-chat-composer', standalone: true, imports: [CommonModule, IonTextarea, IonIcon], template: `
72969
- <div class="composer">
72970
- @if (replyingTo()) {
72971
- <div class="reply-bar">
72972
- <div class="reply-content">
72973
- <span class="reply-name">{{ replyingTo()!.senderName }}</span>
72974
- <span class="reply-body">{{ replyingTo()!.body }}</span>
72975
- </div>
72976
- <button class="icon-btn" [attr.aria-label]="t('cancel')" (click)="cancelReply.emit()">
72977
- <ion-icon name="close-outline" aria-hidden="true" />
72978
- </button>
72979
- </div>
72980
- }
72981
-
72982
- @if (emojiOpen()) {
72983
- <div class="emoji-row">
72984
- @for (e of quickEmojis; track e) {
72985
- <button class="emoji" (click)="insertEmoji(e)">{{ e }}</button>
72986
- }
72987
- </div>
72988
- }
72989
-
72990
- <div class="pill">
72991
- @if (showAttach()) {
72992
- <label class="icon-btn" [attr.aria-label]="t('attach')">
72993
- <ion-icon name="add-outline" aria-hidden="true" />
72994
- <input type="file" hidden (change)="onFile($event)" accept="image/*,application/pdf" />
72995
- </label>
72996
- }
72997
-
72998
- <ion-textarea
72999
- class="field"
73000
- [placeholder]="placeholderText()"
73001
- [disabled]="disabled()"
73002
- [maxlength]="maxLength()"
73003
- [autoGrow]="true"
73004
- [rows]="1"
73005
- [value]="body()"
73006
- (ionInput)="onInput($event)"
73007
- (keydown)="onKeydown($event)"
73008
- ></ion-textarea>
73009
-
73010
- @if (showEmoji()) {
73011
- <button class="icon-btn" [class.active]="emojiOpen()" [attr.aria-label]="t('emoji')" (click)="toggleEmoji()">
73012
- <ion-icon name="happy-outline" aria-hidden="true" />
73013
- </button>
73014
- }
73015
-
73016
- @if (canSend()) {
73017
- <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73018
- <ion-icon name="arrow-up-outline" aria-hidden="true" />
73019
- </button>
73020
- } @else if (showMic()) {
73021
- <button class="icon-btn mic" [attr.aria-label]="t('voice')" (click)="voice.emit()">
73022
- <ion-icon name="mic-outline" aria-hidden="true" />
73023
- </button>
73024
- }
73025
- </div>
73026
- </div>
73027
- `, styles: [":host{display:block}.composer{display:flex;flex-direction:column;gap:6px;padding:8px 10px calc(8px + env(safe-area-inset-bottom,0px));background:var(--ion-background-color, #fff);border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}.reply-bar{display:flex;align-items:center;gap:8px;padding:6px 10px;border-left:3px solid var(--ion-color-primary);border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.reply-content{display:flex;flex-direction:column;flex:1;min-width:0;font-size:.8125rem}.reply-name{font-weight:700;color:var(--ion-color-primary)}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ion-color-medium, #92949c)}.emoji-row{display:flex;gap:4px;flex-wrap:wrap}.emoji{border:none;background:var(--ion-color-step-100, rgba(127, 127, 127, .1));border-radius:10px;padding:4px 8px;font-size:1.25rem;cursor:pointer}.pill{display:flex;align-items:flex-end;gap:4px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.field{flex:1;--background: transparent;--padding-start: 6px;--padding-end: 6px;--padding-top: 8px;--padding-bottom: 8px;margin:0;max-height:120px;font-size:.9375rem}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1.375rem}.icon-btn:hover,.icon-btn.active,.icon-btn.mic:hover{color:var(--ion-color-primary)}.send-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff);cursor:pointer;font-size:1.25rem;transition:transform .1s ease}.send-btn:active{transform:scale(.92)}\n"] }]
73028
- }], ctorParameters: () => [] });
73029
-
73030
- /**
73031
- * val-conversation-list-item — fila de lista de conversaciones estilo
73032
- * Telegram/WhatsApp: avatar (imagen o iniciales), nombre + hora relativa,
73033
- * preview del último mensaje + badge de no-leídos. Signal inputs, dark-safe.
73034
- *
73035
- * @example
73036
- * <val-conversation-list-item
73037
- * [name]="conv.name"
73038
- * [lastMessage]="conv.lastMsgPreview"
73039
- * [timestamp]="conv.lastMsgAt"
73040
- * [unreadCount]="conv.unread"
73041
- * (select)="open(conv.convId)" />
73042
- */
73043
- class ConversationListItemComponent {
73044
- constructor() {
73045
- this.i18n = inject(I18nService);
73046
- this.name = input('');
73047
- this.avatarUrl = input(undefined);
73048
- this.lastMessage = input('');
73049
- this.timestamp = input(undefined);
73050
- this.unreadCount = input(0);
73051
- this.locale = input('es-CL');
73052
- this.select = output();
73053
- this.initials = computed(() => {
73054
- const name = this.name().trim();
73055
- if (!name)
73056
- return '?';
73057
- const parts = name.split(/\s+/);
73058
- return (parts[0][0] + (parts[1]?.[0] ?? '')).toUpperCase();
73059
- });
73060
- this.timeLabel = computed(() => formatRelativeTime(this.timestamp(), this.locale(), {
73061
- today: '',
73062
- yesterday: this.i18n.t('yesterday', '_global'),
73063
- }));
73064
- this.i18n.registerDefaults('ConversationListItem', {
73065
- es: { empty: 'Sin mensajes' },
73066
- en: { empty: 'No messages' },
73067
- });
73068
- }
73069
- emptyText() {
73070
- this.i18n.lang();
73071
- return this.i18n.t('empty', 'ConversationListItem');
73072
- }
73073
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ConversationListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
73074
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ConversationListItemComponent, isStandalone: true, selector: "val-conversation-list-item", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, avatarUrl: { classPropertyName: "avatarUrl", publicName: "avatarUrl", isSignal: true, isRequired: false, transformFunction: null }, lastMessage: { classPropertyName: "lastMessage", publicName: "lastMessage", isSignal: true, isRequired: false, transformFunction: null }, timestamp: { classPropertyName: "timestamp", publicName: "timestamp", isSignal: true, isRequired: false, transformFunction: null }, unreadCount: { classPropertyName: "unreadCount", publicName: "unreadCount", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { select: "select" }, ngImport: i0, template: `
73075
- <button class="item" (click)="select.emit()">
73076
- <div class="avatar">
73077
- @if (avatarUrl()) {
73078
- <img [src]="avatarUrl()" [alt]="name()" />
73079
- } @else {
73080
- <span>{{ initials() }}</span>
73081
- }
73082
- </div>
73083
- <div class="content">
73084
- <div class="top">
73085
- <span class="name">{{ name() }}</span>
73086
- @if (timeLabel()) {
73087
- <span class="time" [class.unread]="unreadCount() > 0">{{ timeLabel() }}</span>
73088
- }
73089
- </div>
73090
- <div class="bottom">
73091
- <span class="preview" [class.muted]="!lastMessage()">
73092
- {{ lastMessage() || emptyText() }}
73093
- </span>
73094
- @if (unreadCount() > 0) {
73095
- <span class="badge">{{ unreadCount() > 99 ? '99+' : unreadCount() }}</span>
73096
- }
73097
- </div>
73098
- </div>
73099
- </button>
73100
- `, isInline: true, styles: [":host{display:block}.item{display:flex;align-items:center;gap:12px;width:100%;padding:10px 12px;border:none;background:transparent;cursor:pointer;text-align:left;border-radius:12px;transition:background .12s ease}.item:hover{background:var(--ion-color-step-50, rgba(127, 127, 127, .06))}.avatar{width:48px;height:48px;flex-shrink:0;border-radius:50%;overflow:hidden;display:flex;align-items:center;justify-content:center;background:var(--ion-color-primary);color:#fff;font-weight:700;font-size:1rem}.avatar img{width:100%;height:100%;object-fit:cover}.content{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.top,.bottom{display:flex;align-items:center;gap:8px}.name{flex:1;min-width:0;font-weight:600;font-size:.9375rem;color:var(--ion-text-color, #000);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.time{flex-shrink:0;font-size:.75rem;color:var(--ion-color-medium, #92949c)}.time.unread{color:var(--ion-color-primary);font-weight:600}.preview{flex:1;min-width:0;font-size:.875rem;color:var(--ion-color-medium, #92949c);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.preview.muted{font-style:italic;opacity:.7}.badge{flex-shrink:0;min-width:20px;height:20px;padding:0 6px;border-radius:10px;background:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff);font-size:.6875rem;font-weight:700;display:inline-flex;align-items:center;justify-content:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] }); }
73101
- }
73102
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ConversationListItemComponent, decorators: [{
73103
- type: Component,
73104
- args: [{ selector: 'val-conversation-list-item', standalone: true, imports: [CommonModule], template: `
73105
- <button class="item" (click)="select.emit()">
73106
- <div class="avatar">
73107
- @if (avatarUrl()) {
73108
- <img [src]="avatarUrl()" [alt]="name()" />
73109
- } @else {
73110
- <span>{{ initials() }}</span>
73111
- }
73112
- </div>
73113
- <div class="content">
73114
- <div class="top">
73115
- <span class="name">{{ name() }}</span>
73116
- @if (timeLabel()) {
73117
- <span class="time" [class.unread]="unreadCount() > 0">{{ timeLabel() }}</span>
73118
- }
73119
- </div>
73120
- <div class="bottom">
73121
- <span class="preview" [class.muted]="!lastMessage()">
73122
- {{ lastMessage() || emptyText() }}
73123
- </span>
73124
- @if (unreadCount() > 0) {
73125
- <span class="badge">{{ unreadCount() > 99 ? '99+' : unreadCount() }}</span>
73126
- }
73127
- </div>
73128
- </div>
73129
- </button>
73130
- `, styles: [":host{display:block}.item{display:flex;align-items:center;gap:12px;width:100%;padding:10px 12px;border:none;background:transparent;cursor:pointer;text-align:left;border-radius:12px;transition:background .12s ease}.item:hover{background:var(--ion-color-step-50, rgba(127, 127, 127, .06))}.avatar{width:48px;height:48px;flex-shrink:0;border-radius:50%;overflow:hidden;display:flex;align-items:center;justify-content:center;background:var(--ion-color-primary);color:#fff;font-weight:700;font-size:1rem}.avatar img{width:100%;height:100%;object-fit:cover}.content{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.top,.bottom{display:flex;align-items:center;gap:8px}.name{flex:1;min-width:0;font-weight:600;font-size:.9375rem;color:var(--ion-text-color, #000);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.time{flex-shrink:0;font-size:.75rem;color:var(--ion-color-medium, #92949c)}.time.unread{color:var(--ion-color-primary);font-weight:600}.preview{flex:1;min-width:0;font-size:.875rem;color:var(--ion-color-medium, #92949c);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.preview.muted{font-style:italic;opacity:.7}.badge{flex-shrink:0;min-width:20px;height:20px;padding:0 6px;border-radius:10px;background:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff);font-size:.6875rem;font-weight:700;display:inline-flex;align-items:center;justify-content:center}\n"] }]
73131
- }], ctorParameters: () => [] });
73132
-
73133
72671
  const TYPING_I18N = {
73134
72672
  es: {
73135
72673
  one: '{name} esta escribiendo',
@@ -73145,9 +72683,8 @@ const TYPING_I18N = {
73145
72683
  /**
73146
72684
  * val-typing-indicator
73147
72685
  *
73148
- * Muestra quienes estan escribiendo con animacion de tres puntos. Se oculta
73149
- * si `typingUsers` esta vacio. Usa signal input para que `typingText` (computed)
73150
- * recompute al cambiar la lista.
72686
+ * Muestra quienes estan escribiendo en el chat con animacion de tres puntos.
72687
+ * Se oculta automaticamente si typingUsers esta vacio.
73151
72688
  *
73152
72689
  * @example
73153
72690
  * <val-typing-indicator [typingUsers]="['Maria', 'Carlos']" />
@@ -73155,10 +72692,10 @@ const TYPING_I18N = {
73155
72692
  class TypingIndicatorComponent {
73156
72693
  constructor() {
73157
72694
  this.i18n = inject(I18nService);
73158
- this.typingUsers = input([]);
72695
+ this.typingUsers = [];
73159
72696
  this.typingText = computed(() => {
73160
72697
  this.i18n.lang();
73161
- const users = this.typingUsers();
72698
+ const users = this.typingUsers ?? [];
73162
72699
  if (users.length === 0)
73163
72700
  return '';
73164
72701
  if (users.length === 1) {
@@ -73172,8 +72709,8 @@ class TypingIndicatorComponent {
73172
72709
  this.i18n.registerDefaults('TypingIndicator', TYPING_I18N);
73173
72710
  }
73174
72711
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TypingIndicatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
73175
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: TypingIndicatorComponent, isStandalone: true, selector: "val-typing-indicator", inputs: { typingUsers: { classPropertyName: "typingUsers", publicName: "typingUsers", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
73176
- @if (typingUsers().length > 0) {
72712
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: TypingIndicatorComponent, isStandalone: true, selector: "val-typing-indicator", inputs: { typingUsers: "typingUsers" }, ngImport: i0, template: `
72713
+ @if (typingUsers && typingUsers.length > 0) {
73177
72714
  <div class="typing-indicator">
73178
72715
  <div class="dots">
73179
72716
  <span></span>
@@ -73188,7 +72725,7 @@ class TypingIndicatorComponent {
73188
72725
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TypingIndicatorComponent, decorators: [{
73189
72726
  type: Component,
73190
72727
  args: [{ selector: 'val-typing-indicator', standalone: true, imports: [CommonModule], template: `
73191
- @if (typingUsers().length > 0) {
72728
+ @if (typingUsers && typingUsers.length > 0) {
73192
72729
  <div class="typing-indicator">
73193
72730
  <div class="dots">
73194
72731
  <span></span>
@@ -73199,280 +72736,270 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
73199
72736
  </div>
73200
72737
  }
73201
72738
  `, styles: [".typing-indicator{display:flex;align-items:center;gap:8px;padding:4px 12px;min-height:28px}.typing-text{font-size:.8125rem;color:var(--ion-color-medium, #92949c);font-style:italic}.dots{display:flex;align-items:center;gap:3px}.dots span{display:inline-block;width:5px;height:5px;border-radius:50%;background:var(--ion-color-medium, #92949c);animation:typing-dot 1.2s infinite ease-in-out}.dots span:nth-child(1){animation-delay:0s}.dots span:nth-child(2){animation-delay:.2s}.dots span:nth-child(3){animation-delay:.4s}@keyframes typing-dot{0%,60%,to{transform:translateY(0);opacity:.4}30%{transform:translateY(-4px);opacity:1}}\n"] }]
73202
- }], ctorParameters: () => [] });
72739
+ }], ctorParameters: () => [], propDecorators: { typingUsers: [{
72740
+ type: Input
72741
+ }] } });
73203
72742
 
73204
- addIcons({ arrowDownOutline, chatbubblesOutline });
72743
+ addIcons({ chatbubblesOutline });
73205
72744
  const CHAT_WINDOW_I18N = {
73206
72745
  es: {
73207
72746
  conversationClosed: 'Esta conversacion esta cerrada',
73208
72747
  loading: 'Cargando mensajes...',
73209
- noMessages: 'Aun no hay mensajes. Escribe el primero.',
73210
- newMessages: 'Mensajes nuevos',
72748
+ noMessages: 'No hay mensajes aun',
73211
72749
  },
73212
72750
  en: {
73213
72751
  conversationClosed: 'This conversation is closed',
73214
72752
  loading: 'Loading messages...',
73215
- noMessages: 'No messages yet. Send the first one.',
73216
- newMessages: 'New messages',
72753
+ noMessages: 'No messages yet',
73217
72754
  },
73218
72755
  };
73219
72756
  /**
73220
- * val-chat-window — ventana de chat completa (signal inputs).
72757
+ * val-chat-window
73221
72758
  *
73222
- * Agrupa mensajes consecutivos del mismo emisor y por día (separadores sticky),
73223
- * muestra avatar/nombre solo en los bordes del grupo, scroll-to-bottom inteligente
73224
- * (auto-scroll si estás abajo, pill "mensajes nuevos" si estás leyendo arriba),
73225
- * composer moderno con contexto de respuesta, y typing indicator.
72759
+ * Ventana de chat completa. Muestra la lista de mensajes agrupados por fecha,
72760
+ * el indicador de escritura, el input de mensaje (solo si isOpen) y un banner
72761
+ * cuando la conversacion esta cerrada. Auto-scroll al ultimo mensaje cuando
72762
+ * llegan mensajes nuevos.
73226
72763
  *
73227
- * API pública estable (consumida por thread-panel y showcase): inputs convId,
73228
- * messages, currentUserId, isOpen, typingUsers, isLoading; outputs sendMessage,
73229
- * loadMore, reactionClick, deleteMessage, replyTo (+ editMessage/attach/voice nuevos).
72764
+ * @example
72765
+ * <val-chat-window
72766
+ * [convId]="conv.id"
72767
+ * [messages]="messages()"
72768
+ * [currentUserId]="userId"
72769
+ * [isOpen]="conv.status === 'open'"
72770
+ * [typingUsers]="typingUsers()"
72771
+ * (sendMessage)="onSend($event)"
72772
+ * (reactionClick)="onReaction($event)"
72773
+ * />
73230
72774
  */
73231
72775
  class ChatWindowComponent {
73232
72776
  constructor() {
73233
72777
  this.i18n = inject(I18nService);
73234
- this.convId = input('');
73235
- this.messages = input([]);
73236
- this.currentUserId = input('');
73237
- this.isOpen = input(true);
73238
- this.typingUsers = input([]);
73239
- this.isLoading = input(false);
73240
- this.sendMessage = output();
73241
- this.loadMore = output();
73242
- this.reactionClick = output();
73243
- this.deleteMessage = output();
73244
- this.replyTo = output();
73245
- this.editMessage = output();
73246
- this.typing = output();
73247
- this.attach = output();
73248
- this.voice = output();
73249
- this.msgsEl = viewChild('msgs');
73250
- this.replyingTo = signal(null);
73251
- this.atBottom = signal(true);
73252
- this.hasNew = signal(false);
73253
- this.locale = computed(() => (this.i18n.lang() === 'en' ? 'en-US' : 'es-CL'));
73254
- this.rows = computed(() => {
73255
- const msgs = this.messages();
72778
+ this.convId = '';
72779
+ this.messages = [];
72780
+ this.participants = [];
72781
+ this.currentUserId = '';
72782
+ this.isOpen = true;
72783
+ this.typingUsers = [];
72784
+ this.isLoading = false;
72785
+ this.sendMessage = new EventEmitter();
72786
+ this.loadMore = new EventEmitter();
72787
+ this.reactionClick = new EventEmitter();
72788
+ this.deleteMessage = new EventEmitter();
72789
+ this.replyTo = new EventEmitter();
72790
+ this.prevMessageCount = 0;
72791
+ this.shouldScrollToBottom = false;
72792
+ this.messageGroups = computed(() => {
72793
+ const msgs = this.messages ?? [];
73256
72794
  if (msgs.length === 0)
73257
72795
  return [];
73258
- const labels = {
73259
- today: this.i18n.t('today', '_global'),
73260
- yesterday: this.i18n.t('yesterday', '_global'),
73261
- };
73262
- const out = [];
73263
- let prevDay = '';
73264
- for (let i = 0; i < msgs.length; i++) {
73265
- const m = msgs[i];
73266
- const d = m.createdAt instanceof Date ? m.createdAt : new Date(m.createdAt);
73267
- const day = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
73268
- const newDay = day !== prevDay;
73269
- if (newDay) {
73270
- out.push({ kind: 'date', id: 'date-' + day, label: formatDateSeparator(d, this.locale(), labels) });
73271
- prevDay = day;
72796
+ const groups = [];
72797
+ let currentLabel = '';
72798
+ let currentGroup = [];
72799
+ for (const msg of msgs) {
72800
+ const label = this.formatDateLabel(msg.createdAt);
72801
+ if (label !== currentLabel) {
72802
+ if (currentGroup.length > 0) {
72803
+ groups.push({ dateLabel: currentLabel, messages: currentGroup });
72804
+ }
72805
+ currentLabel = label;
72806
+ currentGroup = [msg];
72807
+ }
72808
+ else {
72809
+ currentGroup.push(msg);
73272
72810
  }
73273
- const prev = msgs[i - 1];
73274
- const next = msgs[i + 1];
73275
- const firstInGroup = newDay || !prev || prev.senderId !== m.senderId;
73276
- const nextDay = next
73277
- ? `${new Date(next.createdAt).getFullYear()}-${new Date(next.createdAt).getMonth()}-${new Date(next.createdAt).getDate()}`
73278
- : '';
73279
- const lastInGroup = !next || next.senderId !== m.senderId || nextDay !== day;
73280
- out.push({
73281
- kind: 'msg',
73282
- id: m.msgId,
73283
- msg: m,
73284
- showName: firstInGroup && !m.isMine,
73285
- showAvatar: lastInGroup && !m.isMine,
73286
- tail: lastInGroup,
73287
- });
73288
72811
  }
73289
- return out;
72812
+ if (currentGroup.length > 0) {
72813
+ groups.push({ dateLabel: currentLabel, messages: currentGroup });
72814
+ }
72815
+ return groups;
72816
+ });
72817
+ this.closedText = computed(() => {
72818
+ this.i18n.lang();
72819
+ return this.i18n.t('conversationClosed', 'ChatWindow');
72820
+ });
72821
+ this.loadingText = computed(() => {
72822
+ this.i18n.lang();
72823
+ return this.i18n.t('loading', 'ChatWindow');
73290
72824
  });
73291
72825
  this.i18n.registerDefaults('ChatWindow', CHAT_WINDOW_I18N);
73292
- // Auto-scroll al fondo cuando llegan mensajes y el usuario ya estaba abajo
73293
- // (o el último mensaje es propio). Si está leyendo arriba, marca "nuevos".
73294
- let prevCount = 0;
73295
- effect(() => {
73296
- const rows = this.rows();
73297
- const count = rows.length;
73298
- if (count === prevCount)
73299
- return;
73300
- const grew = count > prevCount;
73301
- prevCount = count;
73302
- const last = this.messages()[this.messages().length - 1];
73303
- const mine = last?.isMine ?? false;
73304
- if (this.atBottom() || mine) {
73305
- setTimeout(() => this.scrollToBottom(false), 0);
73306
- }
73307
- else if (grew) {
73308
- this.hasNew.set(true);
72826
+ }
72827
+ ngAfterViewInit() {
72828
+ if (typeof window !== 'undefined' && window.visualViewport) {
72829
+ this.vvListener = () => this.adjustToKeyboard();
72830
+ window.visualViewport.addEventListener('resize', this.vvListener);
72831
+ this.adjustToKeyboard();
72832
+ }
72833
+ }
72834
+ ngOnDestroy() {
72835
+ if (this.vvListener && typeof window !== 'undefined' && window.visualViewport) {
72836
+ window.visualViewport.removeEventListener('resize', this.vvListener);
72837
+ }
72838
+ }
72839
+ ngOnChanges(changes) {
72840
+ if (changes['messages']) {
72841
+ const current = changes['messages'].currentValue ?? [];
72842
+ if (current.length !== this.prevMessageCount) {
72843
+ this.prevMessageCount = current.length;
72844
+ this.shouldScrollToBottom = true;
73309
72845
  }
73310
- });
72846
+ }
73311
72847
  }
73312
- onScroll() {
73313
- const el = this.msgsEl()?.nativeElement;
73314
- if (!el)
73315
- return;
73316
- const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
73317
- this.atBottom.set(nearBottom);
73318
- if (nearBottom)
73319
- this.hasNew.set(false);
73320
- if (el.scrollTop < 40)
73321
- this.loadMore.emit();
72848
+ ngAfterViewChecked() {
72849
+ if (this.shouldScrollToBottom) {
72850
+ this.scrollToBottom();
72851
+ this.shouldScrollToBottom = false;
72852
+ }
73322
72853
  }
73323
- scrollToBottom(smooth) {
73324
- const el = this.msgsEl()?.nativeElement;
73325
- if (!el)
72854
+ adjustToKeyboard() {
72855
+ const el = this.chatWindowElRef?.nativeElement;
72856
+ if (!el || !window.visualViewport)
73326
72857
  return;
73327
- el.scrollTo({ top: el.scrollHeight, behavior: smooth ? 'smooth' : 'auto' });
73328
- this.atBottom.set(true);
73329
- this.hasNew.set(false);
72858
+ const rect = el.getBoundingClientRect();
72859
+ const available = window.visualViewport.height - rect.top;
72860
+ if (available > 50) {
72861
+ el.style.height = `${available}px`;
72862
+ }
73330
72863
  }
73331
- onSend(body) {
73332
- this.sendMessage.emit(body);
73333
- this.replyingTo.set(null);
72864
+ onReactionClick(event) {
72865
+ this.reactionClick.emit({ msgId: event.msgId, token: event.token });
73334
72866
  }
73335
- onAction(event) {
73336
- switch (event.type) {
73337
- case 'reply': {
73338
- const m = this.messages().find(x => x.msgId === event.msgId);
73339
- if (m) {
73340
- this.replyingTo.set({ msgId: m.msgId, senderName: m.senderName, body: m.body });
73341
- }
73342
- this.replyTo.emit(event.msgId);
73343
- break;
73344
- }
73345
- case 'edit':
73346
- this.editMessage.emit(event.msgId);
73347
- break;
73348
- case 'delete':
73349
- this.deleteMessage.emit(event.msgId);
73350
- break;
73351
- case 'react':
73352
- this.reactionClick.emit({ msgId: event.msgId, token: event.token ?? '👍' });
73353
- break;
72867
+ onEditClick(_msgId) {
72868
+ // Implementacion futura: emitir evento de edicion
72869
+ }
72870
+ onTyping() {
72871
+ // El padre puede suscribirse al output typing si necesita propagar a Firestore
72872
+ }
72873
+ scrollToBottom() {
72874
+ const el = this.messagesListRef?.nativeElement;
72875
+ if (el) {
72876
+ el.scrollTop = el.scrollHeight;
73354
72877
  }
73355
72878
  }
73356
- t(key) {
73357
- return this.i18n.t(key, 'ChatWindow');
72879
+ formatDateLabel(date) {
72880
+ if (!date)
72881
+ return '';
72882
+ const d = date instanceof Date ? date : new Date(date);
72883
+ const now = new Date();
72884
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
72885
+ const msgDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
72886
+ const diffMs = today.getTime() - msgDay.getTime();
72887
+ const diffDays = Math.round(diffMs / 86400000);
72888
+ if (diffDays === 0)
72889
+ return this.i18n.t('today', '_global') || 'Hoy';
72890
+ if (diffDays === 1)
72891
+ return this.i18n.t('yesterday', '_global') || 'Ayer';
72892
+ return d.toLocaleDateString([], { day: '2-digit', month: 'short', year: 'numeric' });
73358
72893
  }
73359
72894
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatWindowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
73360
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ChatWindowComponent, isStandalone: true, selector: "val-chat-window", inputs: { convId: { classPropertyName: "convId", publicName: "convId", isSignal: true, isRequired: false, transformFunction: null }, messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, currentUserId: { classPropertyName: "currentUserId", publicName: "currentUserId", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, typingUsers: { classPropertyName: "typingUsers", publicName: "typingUsers", isSignal: true, isRequired: false, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sendMessage: "sendMessage", loadMore: "loadMore", reactionClick: "reactionClick", deleteMessage: "deleteMessage", replyTo: "replyTo", editMessage: "editMessage", typing: "typing", attach: "attach", voice: "voice" }, viewQueries: [{ propertyName: "msgsEl", first: true, predicate: ["msgs"], descendants: true, isSignal: true }], ngImport: i0, template: `
73361
- <div class="chat">
73362
- <div #msgs class="messages" (scroll)="onScroll()">
73363
- @if (isLoading()) {
73364
- <div class="state">
73365
- <span>{{ t('loading') }}</span>
73366
- </div>
73367
- } @else if (rows().length === 0) {
73368
- <div class="state empty">
73369
- <ion-icon name="chatbubbles-outline" aria-hidden="true" />
73370
- <span>{{ t('noMessages') }}</span>
72895
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ChatWindowComponent, isStandalone: true, selector: "val-chat-window", inputs: { convId: "convId", messages: "messages", participants: "participants", currentUserId: "currentUserId", isOpen: "isOpen", typingUsers: "typingUsers", isLoading: "isLoading" }, outputs: { sendMessage: "sendMessage", loadMore: "loadMore", reactionClick: "reactionClick", deleteMessage: "deleteMessage", replyTo: "replyTo" }, viewQueries: [{ propertyName: "messagesListRef", first: true, predicate: ["messagesList"], descendants: true }, { propertyName: "chatWindowElRef", first: true, predicate: ["chatWindowEl"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
72896
+ <div class="chat-window" #chatWindowEl>
72897
+ <div class="messages-list" #messagesList>
72898
+ @if (isLoading) {
72899
+ <div class="loading-state">
72900
+ <ion-spinner name="dots"></ion-spinner>
72901
+ <span>{{ loadingText() }}</span>
73371
72902
  </div>
73372
72903
  } @else {
73373
- @for (row of rows(); track row.id) {
73374
- @if (row.kind === 'date') {
73375
- <div class="date-sep">
73376
- <span>{{ row.label }}</span>
73377
- </div>
73378
- } @else {
72904
+ @for (group of messageGroups(); track group.dateLabel) {
72905
+ <div class="date-separator">
72906
+ <div class="date-line"></div>
72907
+ <span class="date-label">{{ group.dateLabel }}</span>
72908
+ <div class="date-line"></div>
72909
+ </div>
72910
+ @for (msg of group.messages; track msg.msgId) {
73379
72911
  <val-message-bubble
73380
- [msg]="row.msg"
73381
- [showName]="row.showName"
73382
- [showAvatar]="row.showAvatar"
73383
- [tail]="row.tail"
73384
- [locale]="locale()"
73385
- (action)="onAction($event)"
72912
+ [props]="msg"
72913
+ (reactionClick)="onReactionClick($event)"
72914
+ (replyClick)="replyTo.emit($event.msgId)"
72915
+ (deleteClick)="deleteMessage.emit($event.msgId)"
72916
+ (editClick)="onEditClick($event.msgId)"
73386
72917
  />
73387
72918
  }
73388
72919
  }
73389
72920
  }
73390
- <val-typing-indicator [typingUsers]="typingUsers()" />
73391
72921
  </div>
73392
72922
 
73393
- @if (!atBottom()) {
73394
- <button class="scroll-down" (click)="scrollToBottom(true)" aria-label="scroll">
73395
- <ion-icon name="arrow-down-outline" aria-hidden="true" />
73396
- @if (hasNew()) {
73397
- <span class="dot"></span>
73398
- }
73399
- </button>
73400
- }
72923
+ <val-typing-indicator [typingUsers]="typingUsers" />
73401
72924
 
73402
- @if (isOpen()) {
73403
- <val-chat-composer
73404
- [replyingTo]="replyingTo()"
73405
- (send)="onSend($event)"
73406
- (typing)="typing.emit()"
73407
- (attach)="attach.emit($event)"
73408
- (voice)="voice.emit()"
73409
- (cancelReply)="replyingTo.set(null)"
73410
- />
72925
+ @if (!isOpen) {
72926
+ <div class="closed-banner">{{ closedText() }}</div>
73411
72927
  } @else {
73412
- <div class="closed-banner">{{ t('conversationClosed') }}</div>
72928
+ <val-chat-input [props]="{ disabled: isLoading }" (send)="sendMessage.emit($event)" (typing)="onTyping()" />
73413
72929
  }
73414
72930
  </div>
73415
- `, isInline: true, styles: [":host{display:block;height:100%}.chat{position:relative;display:flex;flex-direction:column;height:100%;min-height:0}.messages{flex:1;min-height:0;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:0}.state{margin:auto;display:flex;flex-direction:column;align-items:center;gap:8px;color:var(--ion-color-medium, #92949c);font-size:.9375rem;text-align:center;padding:24px}.state.empty ion-icon{font-size:2.5rem;opacity:.5}.date-sep{position:sticky;top:4px;z-index:1;display:flex;justify-content:center;margin:8px 0}.date-sep span{font-size:.75rem;color:var(--ion-color-medium, #92949c);background:var(--ion-color-step-100, rgba(127, 127, 127, .14));padding:3px 12px;border-radius:999px;backdrop-filter:blur(4px)}.scroll-down{position:absolute;right:14px;bottom:84px;width:40px;height:40px;border-radius:50%;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff));color:var(--ion-color-medium, #92949c);box-shadow:0 2px 10px #00000026;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:1.25rem;z-index:3}.scroll-down .dot{position:absolute;top:0;right:0;width:12px;height:12px;border-radius:50%;background:var(--ion-color-primary);border:2px solid var(--ion-card-background, #fff)}.closed-banner{text-align:center;padding:14px;color:var(--ion-color-medium, #92949c);font-size:.875rem;border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: MessageBubbleComponent, selector: "val-message-bubble", inputs: ["msg", "showAvatar", "showName", "tail", "locale"], outputs: ["action"] }, { kind: "component", type: ChatComposerComponent, selector: "val-chat-composer", inputs: ["placeholder", "disabled", "maxLength", "replyingTo", "showAttach", "showEmoji", "showMic"], outputs: ["send", "typing", "attach", "voice", "cancelReply"] }, { kind: "component", type: TypingIndicatorComponent, selector: "val-typing-indicator", inputs: ["typingUsers"] }] }); }
72931
+ `, isInline: true, styles: [".chat-window{display:flex;flex-direction:column;height:100%;background:var(--ion-background-color, #f4f5f8);overflow:hidden}.messages-list{flex:1;overflow-y:auto;padding:12px 8px;scroll-behavior:smooth}.messages-list::-webkit-scrollbar{width:4px}.messages-list::-webkit-scrollbar-thumb{background:var(--ion-color-light-shade, #d7d8da);border-radius:2px}.date-separator{display:flex;align-items:center;gap:8px;margin:12px 0}.date-separator .date-line{flex:1;height:1px;background:var(--ion-border-color, rgba(0, 0, 0, .1))}.date-separator .date-label{font-size:.75rem;color:var(--ion-color-medium, #92949c);white-space:nowrap;padding:0 4px}.loading-state{display:flex;align-items:center;justify-content:center;padding:24px;color:var(--ion-color-medium, #92949c);font-size:.875rem;gap:8px}.closed-banner{padding:10px 16px;background:var(--ion-color-warning-tint, #fff9e6);border-top:1px solid var(--ion-color-warning, #ffc409);text-align:center;font-size:.875rem;color:var(--ion-color-warning-shade, #b29500)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: MessageBubbleComponent, selector: "val-message-bubble", inputs: ["props"], outputs: ["reactionClick", "replyClick", "deleteClick", "editClick"] }, { kind: "component", type: ChatInputComponent, selector: "val-chat-input", inputs: ["props"], outputs: ["send", "typing"] }, { kind: "component", type: TypingIndicatorComponent, selector: "val-typing-indicator", inputs: ["typingUsers"] }] }); }
73416
72932
  }
73417
72933
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatWindowComponent, decorators: [{
73418
72934
  type: Component,
73419
- args: [{ selector: 'val-chat-window', standalone: true, imports: [CommonModule, IonIcon, MessageBubbleComponent, ChatComposerComponent, TypingIndicatorComponent], template: `
73420
- <div class="chat">
73421
- <div #msgs class="messages" (scroll)="onScroll()">
73422
- @if (isLoading()) {
73423
- <div class="state">
73424
- <span>{{ t('loading') }}</span>
73425
- </div>
73426
- } @else if (rows().length === 0) {
73427
- <div class="state empty">
73428
- <ion-icon name="chatbubbles-outline" aria-hidden="true" />
73429
- <span>{{ t('noMessages') }}</span>
72935
+ args: [{ selector: 'val-chat-window', standalone: true, imports: [CommonModule, IonIcon, IonSpinner, MessageBubbleComponent, ChatInputComponent, TypingIndicatorComponent], template: `
72936
+ <div class="chat-window" #chatWindowEl>
72937
+ <div class="messages-list" #messagesList>
72938
+ @if (isLoading) {
72939
+ <div class="loading-state">
72940
+ <ion-spinner name="dots"></ion-spinner>
72941
+ <span>{{ loadingText() }}</span>
73430
72942
  </div>
73431
72943
  } @else {
73432
- @for (row of rows(); track row.id) {
73433
- @if (row.kind === 'date') {
73434
- <div class="date-sep">
73435
- <span>{{ row.label }}</span>
73436
- </div>
73437
- } @else {
72944
+ @for (group of messageGroups(); track group.dateLabel) {
72945
+ <div class="date-separator">
72946
+ <div class="date-line"></div>
72947
+ <span class="date-label">{{ group.dateLabel }}</span>
72948
+ <div class="date-line"></div>
72949
+ </div>
72950
+ @for (msg of group.messages; track msg.msgId) {
73438
72951
  <val-message-bubble
73439
- [msg]="row.msg"
73440
- [showName]="row.showName"
73441
- [showAvatar]="row.showAvatar"
73442
- [tail]="row.tail"
73443
- [locale]="locale()"
73444
- (action)="onAction($event)"
72952
+ [props]="msg"
72953
+ (reactionClick)="onReactionClick($event)"
72954
+ (replyClick)="replyTo.emit($event.msgId)"
72955
+ (deleteClick)="deleteMessage.emit($event.msgId)"
72956
+ (editClick)="onEditClick($event.msgId)"
73445
72957
  />
73446
72958
  }
73447
72959
  }
73448
72960
  }
73449
- <val-typing-indicator [typingUsers]="typingUsers()" />
73450
72961
  </div>
73451
72962
 
73452
- @if (!atBottom()) {
73453
- <button class="scroll-down" (click)="scrollToBottom(true)" aria-label="scroll">
73454
- <ion-icon name="arrow-down-outline" aria-hidden="true" />
73455
- @if (hasNew()) {
73456
- <span class="dot"></span>
73457
- }
73458
- </button>
73459
- }
72963
+ <val-typing-indicator [typingUsers]="typingUsers" />
73460
72964
 
73461
- @if (isOpen()) {
73462
- <val-chat-composer
73463
- [replyingTo]="replyingTo()"
73464
- (send)="onSend($event)"
73465
- (typing)="typing.emit()"
73466
- (attach)="attach.emit($event)"
73467
- (voice)="voice.emit()"
73468
- (cancelReply)="replyingTo.set(null)"
73469
- />
72965
+ @if (!isOpen) {
72966
+ <div class="closed-banner">{{ closedText() }}</div>
73470
72967
  } @else {
73471
- <div class="closed-banner">{{ t('conversationClosed') }}</div>
72968
+ <val-chat-input [props]="{ disabled: isLoading }" (send)="sendMessage.emit($event)" (typing)="onTyping()" />
73472
72969
  }
73473
72970
  </div>
73474
- `, styles: [":host{display:block;height:100%}.chat{position:relative;display:flex;flex-direction:column;height:100%;min-height:0}.messages{flex:1;min-height:0;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:0}.state{margin:auto;display:flex;flex-direction:column;align-items:center;gap:8px;color:var(--ion-color-medium, #92949c);font-size:.9375rem;text-align:center;padding:24px}.state.empty ion-icon{font-size:2.5rem;opacity:.5}.date-sep{position:sticky;top:4px;z-index:1;display:flex;justify-content:center;margin:8px 0}.date-sep span{font-size:.75rem;color:var(--ion-color-medium, #92949c);background:var(--ion-color-step-100, rgba(127, 127, 127, .14));padding:3px 12px;border-radius:999px;backdrop-filter:blur(4px)}.scroll-down{position:absolute;right:14px;bottom:84px;width:40px;height:40px;border-radius:50%;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff));color:var(--ion-color-medium, #92949c);box-shadow:0 2px 10px #00000026;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:1.25rem;z-index:3}.scroll-down .dot{position:absolute;top:0;right:0;width:12px;height:12px;border-radius:50%;background:var(--ion-color-primary);border:2px solid var(--ion-card-background, #fff)}.closed-banner{text-align:center;padding:14px;color:var(--ion-color-medium, #92949c);font-size:.875rem;border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}\n"] }]
73475
- }], ctorParameters: () => [] });
72971
+ `, styles: [".chat-window{display:flex;flex-direction:column;height:100%;background:var(--ion-background-color, #f4f5f8);overflow:hidden}.messages-list{flex:1;overflow-y:auto;padding:12px 8px;scroll-behavior:smooth}.messages-list::-webkit-scrollbar{width:4px}.messages-list::-webkit-scrollbar-thumb{background:var(--ion-color-light-shade, #d7d8da);border-radius:2px}.date-separator{display:flex;align-items:center;gap:8px;margin:12px 0}.date-separator .date-line{flex:1;height:1px;background:var(--ion-border-color, rgba(0, 0, 0, .1))}.date-separator .date-label{font-size:.75rem;color:var(--ion-color-medium, #92949c);white-space:nowrap;padding:0 4px}.loading-state{display:flex;align-items:center;justify-content:center;padding:24px;color:var(--ion-color-medium, #92949c);font-size:.875rem;gap:8px}.closed-banner{padding:10px 16px;background:var(--ion-color-warning-tint, #fff9e6);border-top:1px solid var(--ion-color-warning, #ffc409);text-align:center;font-size:.875rem;color:var(--ion-color-warning-shade, #b29500)}\n"] }]
72972
+ }], ctorParameters: () => [], propDecorators: { convId: [{
72973
+ type: Input
72974
+ }], messages: [{
72975
+ type: Input
72976
+ }], participants: [{
72977
+ type: Input
72978
+ }], currentUserId: [{
72979
+ type: Input
72980
+ }], isOpen: [{
72981
+ type: Input
72982
+ }], typingUsers: [{
72983
+ type: Input
72984
+ }], isLoading: [{
72985
+ type: Input
72986
+ }], sendMessage: [{
72987
+ type: Output
72988
+ }], loadMore: [{
72989
+ type: Output
72990
+ }], reactionClick: [{
72991
+ type: Output
72992
+ }], deleteMessage: [{
72993
+ type: Output
72994
+ }], replyTo: [{
72995
+ type: Output
72996
+ }], messagesListRef: [{
72997
+ type: ViewChild,
72998
+ args: ['messagesList']
72999
+ }], chatWindowElRef: [{
73000
+ type: ViewChild,
73001
+ args: ['chatWindowEl']
73002
+ }] } });
73476
73003
 
73477
73004
  const THREAD_PANEL_I18N = {
73478
73005
  es: {
@@ -73515,7 +73042,7 @@ class ThreadPanelComponent {
73515
73042
  this.conv = signal(null);
73516
73043
  this.messages = signal([]);
73517
73044
  this.typingUsers = signal([]);
73518
- this.isConvOpen = computed(() => this.conv()?.settings?.isOpen === true);
73045
+ this.isConvOpen = computed(() => this.conv()?.status === 'open');
73519
73046
  this.loadingText = computed(() => {
73520
73047
  this.i18n.lang();
73521
73048
  return this.i18n.t('loading', 'ThreadPanel');
@@ -73553,10 +73080,10 @@ class ThreadPanelComponent {
73553
73080
  const appId = this.chatConfig?.appId ?? '';
73554
73081
  if (orgId && appId) {
73555
73082
  this.msgSub = this.conversationSvc
73556
- .watchMessages(conversation.convId, orgId, appId)
73083
+ .watchMessages(conversation.id, orgId, appId)
73557
73084
  .subscribe(msgs => this.messages.set(msgs));
73558
73085
  this.typingSub = this.conversationSvc
73559
- .watchTyping(conversation.convId, orgId, appId)
73086
+ .watchTyping(conversation.id, orgId, appId)
73560
73087
  .subscribe(users => this.typingUsers.set(users));
73561
73088
  }
73562
73089
  }
@@ -73574,14 +73101,14 @@ class ThreadPanelComponent {
73574
73101
  const orgId = this.getOrgId();
73575
73102
  const appId = this.chatConfig?.appId ?? '';
73576
73103
  if (conv && orgId && appId && this.conversationSvc) {
73577
- this.conversationSvc.stopTyping(conv.convId, orgId, appId);
73104
+ this.conversationSvc.stopTyping(conv.id, orgId, appId);
73578
73105
  }
73579
73106
  }
73580
73107
  onSend(body) {
73581
73108
  const conv = this.conv();
73582
73109
  if (!conv || !this.conversationSvc)
73583
73110
  return;
73584
- this.conversationSvc.sendMessage(conv.convId, body).subscribe();
73111
+ this.conversationSvc.sendMessage(conv.id, body).subscribe();
73585
73112
  }
73586
73113
  onReactionClick(event) {
73587
73114
  // Reacciones se implementan a nivel de app consumer
@@ -73591,7 +73118,7 @@ class ThreadPanelComponent {
73591
73118
  const conv = this.conv();
73592
73119
  if (!conv || !this.conversationSvc)
73593
73120
  return;
73594
- this.conversationSvc.deleteMessage(conv.convId, msgId).subscribe();
73121
+ this.conversationSvc.deleteMessage(conv.id, msgId).subscribe();
73595
73122
  }
73596
73123
  onReplyTo(_msgId) {
73597
73124
  // Implementacion futura: seleccionar mensaje de respuesta
@@ -73616,7 +73143,7 @@ class ThreadPanelComponent {
73616
73143
  } @else {
73617
73144
  <div class="thread-chat">
73618
73145
  <val-chat-window
73619
- [convId]="conv()?.convId ?? ''"
73146
+ [convId]="conv()?.id ?? ''"
73620
73147
  [messages]="bubbleMessages()"
73621
73148
  [currentUserId]="currentUserId"
73622
73149
  [isOpen]="isConvOpen()"
@@ -73630,7 +73157,7 @@ class ThreadPanelComponent {
73630
73157
  </div>
73631
73158
  }
73632
73159
  </div>
73633
- `, isInline: true, styles: [".thread-panel{display:flex;flex-direction:column;height:100%;background:var(--ion-background-color, #f4f5f8)}.thread-header{padding:12px 16px;border-bottom:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff))}.thread-header .thread-title{font-size:1rem;font-weight:600;color:var(--ion-text-color, #000);margin:0}.thread-loading{display:flex;align-items:center;justify-content:center;flex:1;gap:10px;color:var(--ion-color-medium, #92949c);font-size:.875rem}.thread-chat{flex:1;overflow:hidden;display:flex;flex-direction:column}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: ChatWindowComponent, selector: "val-chat-window", inputs: ["convId", "messages", "currentUserId", "isOpen", "typingUsers", "isLoading"], outputs: ["sendMessage", "loadMore", "reactionClick", "deleteMessage", "replyTo", "editMessage", "typing", "attach", "voice"] }] }); }
73160
+ `, isInline: true, styles: [".thread-panel{display:flex;flex-direction:column;height:100%;background:var(--ion-background-color, #f4f5f8)}.thread-header{padding:12px 16px;border-bottom:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff))}.thread-header .thread-title{font-size:1rem;font-weight:600;color:var(--ion-text-color, #000);margin:0}.thread-loading{display:flex;align-items:center;justify-content:center;flex:1;gap:10px;color:var(--ion-color-medium, #92949c);font-size:.875rem}.thread-chat{flex:1;overflow:hidden;display:flex;flex-direction:column}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: ChatWindowComponent, selector: "val-chat-window", inputs: ["convId", "messages", "participants", "currentUserId", "isOpen", "typingUsers", "isLoading"], outputs: ["sendMessage", "loadMore", "reactionClick", "deleteMessage", "replyTo"] }] }); }
73634
73161
  }
73635
73162
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ThreadPanelComponent, decorators: [{
73636
73163
  type: Component,
@@ -73650,7 +73177,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
73650
73177
  } @else {
73651
73178
  <div class="thread-chat">
73652
73179
  <val-chat-window
73653
- [convId]="conv()?.convId ?? ''"
73180
+ [convId]="conv()?.id ?? ''"
73654
73181
  [messages]="bubbleMessages()"
73655
73182
  [currentUserId]="currentUserId"
73656
73183
  [isOpen]="isConvOpen()"
@@ -73684,5 +73211,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
73684
73211
  * Generated bundle index. Do not edit.
73685
73212
  */
73686
73213
 
73687
- export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, APP_VERSION_PLATFORM_PLUGIN, APP_VERSION_REMOTE_PLUGIN, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AboutViewComponent, AccordionComponent, AccountViewComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, ApiKeyCreateModalComponent, ApiKeyService, ApiKeysModalComponent, ApiKeysViewComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, ChatComposerComponent, ChatInputComponent, ChatWindowComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentService, ContentTransformer, ConversationListItemComponent, ConversationService, CookieBannerComponent, CookieSettingsComponent, CountdownComponent, CreateOrgModalComponent, CtaCardComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_BUTTON_PRESETS, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_LOGIN_LOGO, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_POST_UPDATE_GRACE_MS, DEFAULT_PRESETS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_SPLASH_SCREEN_CONFIG, DataTableComponent, DateInputComponent, DatePickerComponent, DateRangeInputComponent, DebugConsoleComponent, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EditOrgModalComponent, EmptyStateComponent, EntityCardComponent, EntityFeedService, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FieldListComponent, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FontSizeOption, FontSizeSelectorComponent, FontSizeService, FooterComponent, FooterLinksComponent, FormComponent, FormFieldComponent, FormSkeletonComponent, FunHeaderComponent, FunModalComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HapticsService, HasPermissionDirective, HeaderComponent, HintComponent, HorizontalScrollComponent, HrefComponent, HtmlViewerModalComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, IONIC_COLORS$5 as IONIC_COLORS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, InviteMemberModalComponent, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGGED_IN_HINT_COOKIE, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LanguageSelectorV2Component, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadMoreComponent, LoadingDirective, LocalStorageService, LocaleService, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MemberDetailModalComponent, MemberImportModalComponent, MenuComponent, MessageBubbleComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationPreferencesViewComponent, NotificationsService, NotificationsViewComponent, NumberFromToComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OptionCardsComponent, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PLATFORM_CONFIGS, POST_UPDATE_TS_KEY, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PdfService, PermissionCatalogService, PermissionSelectorComponent, PermissionsModalComponent, PermissionsViewComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PreferencesViewComponent, PresetService, PriceTagComponent, ProcessLinksPipe, ProfileSkeletonComponent, ProfileViewComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QrScannerComponent, QueryBuilder, QuoteBoxComponent, REQUEST_STATUSES, RadioInputComponent, RangeInputComponent, RatingComponent, ReactionBarComponent, ReactionsService, RefresherComponent, RequestFirestoreService, RequestFormBuilderService, RequestFormComponent, RequestModalComponent, RequestService, RightsFooterComponent, RotatingTextComponent, SEARCH_HEADER_DEFAULTS, SETTINGS_SECTIONS_CATALOG, SHAPE_KEYS, SHARE_PROFILE_MODAL_DEFAULTS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, STATS_BAR_DEFAULTS, STROKE_KEYS, SearchHeaderComponent, SearchSelectorComponent, SearchbarComponent, SectionHeaderComponent, SecurityViewComponent, SegmentControlComponent, SelectSearchComponent, SessionListModalComponent, SessionService, SettingsHubComponent, ShareButtonsComponent, ShareProfileModalComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SplashComponent, SplashScreenService, StatsBarComponent, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, ThreadPanelComponent, TicketCardComponent, TicketCardImageService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, TranslatePipe, TypedCollection, TypingIndicatorComponent, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_CHAT_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_NETWORK_ERROR_KEY, VALTECH_REACTIONS_CONFIG, VALTECH_SETTINGS_MENU_LINKS, VALTECH_SITE_PATHS, VALTECH_SOCIAL_LINKS, VALTECH_SPLASH_SCREEN, VALTECH_WEB_BASE_URLS, VALTECH_WHATSAPP_CONFIG, VERSION, ValtechErrorService, VerifyViewComponent, WhatsappFabComponent, WhatsappService, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, authPasswordValidator, beautifyLegalArticle, blogPost, buildCompanyFooterProps, buildFooterLinks, buildLegalLinkResolver, buildPath, buildSettingsCards, button, canSubmitRequestType, collections, connectPageRefresh, createErrorStateProps, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createPageState, createPermissionLabeler, createRefreshableStream, createTitleProps, docs, errorLoggingInterceptor, extractPathParams, formatClockTime, formatDateSeparator, formatRelativeTime, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, groupPermissionsByScope, guestGuard, hasEmulators, iconButton, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isIonicColor, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideSplashScreen, provideValtechAboutRoutes, provideValtechAccountRoutes, provideValtechAds, provideValtechApiKeysRoutes, provideValtechAppConfig, provideValtechAppVersion, provideValtechAppVersionHttp, provideValtechAuth, provideValtechAuthInterceptor, provideValtechChat, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationPreferencesRoutes, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPermissionsRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechReactions, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, provideValtechWhatsapp, query, rbacGuard, renderPatternSvgInner, replaceSpecialChars, requestSubmitMode, resolveColor, resolveInputDefaultValue, resolveIonicColor, resolveWebBaseUrl, roleGuard, selectableRequestTypes, storagePaths, superAdminGuard, toArticle };
73214
+ export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, APP_VERSION_PLATFORM_PLUGIN, APP_VERSION_REMOTE_PLUGIN, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AboutViewComponent, AccordionComponent, AccountViewComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, ApiKeyCreateModalComponent, ApiKeyService, ApiKeysModalComponent, ApiKeysViewComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, ChatInputComponent, ChatWindowComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentService, ContentTransformer, ConversationService, CookieBannerComponent, CookieSettingsComponent, CountdownComponent, CreateOrgModalComponent, CtaCardComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_BUTTON_PRESETS, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_LOGIN_LOGO, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_POST_UPDATE_GRACE_MS, DEFAULT_PRESETS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_SPLASH_SCREEN_CONFIG, DataTableComponent, DateInputComponent, DatePickerComponent, DateRangeInputComponent, DebugConsoleComponent, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EditOrgModalComponent, EmptyStateComponent, EntityCardComponent, EntityFeedService, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FieldListComponent, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FontSizeOption, FontSizeSelectorComponent, FontSizeService, FooterComponent, FooterLinksComponent, FormComponent, FormFieldComponent, FormSkeletonComponent, FunHeaderComponent, FunModalComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HapticsService, HasPermissionDirective, HeaderComponent, HintComponent, HorizontalScrollComponent, HrefComponent, HtmlViewerModalComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, IONIC_COLORS$5 as IONIC_COLORS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, InviteMemberModalComponent, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGGED_IN_HINT_COOKIE, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LanguageSelectorV2Component, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadMoreComponent, LoadingDirective, LocalStorageService, LocaleService, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MemberDetailModalComponent, MemberImportModalComponent, MenuComponent, MessageBubbleComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationPreferencesViewComponent, NotificationsService, NotificationsViewComponent, NumberFromToComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OptionCardsComponent, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PLATFORM_CONFIGS, POST_UPDATE_TS_KEY, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PdfService, PermissionCatalogService, PermissionSelectorComponent, PermissionsModalComponent, PermissionsViewComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PreferencesViewComponent, PresetService, PriceTagComponent, ProcessLinksPipe, ProfileSkeletonComponent, ProfileViewComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QrScannerComponent, QueryBuilder, QuoteBoxComponent, REQUEST_STATUSES, RadioInputComponent, RangeInputComponent, RatingComponent, ReactionBarComponent, ReactionsService, RefresherComponent, RequestFirestoreService, RequestFormBuilderService, RequestFormComponent, RequestModalComponent, RequestService, RightsFooterComponent, RotatingTextComponent, SEARCH_HEADER_DEFAULTS, SETTINGS_SECTIONS_CATALOG, SHAPE_KEYS, SHARE_PROFILE_MODAL_DEFAULTS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, STATS_BAR_DEFAULTS, STROKE_KEYS, SearchHeaderComponent, SearchSelectorComponent, SearchbarComponent, SectionHeaderComponent, SecurityViewComponent, SegmentControlComponent, SelectSearchComponent, SessionListModalComponent, SessionService, SettingsHubComponent, ShareButtonsComponent, ShareProfileModalComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SplashComponent, SplashScreenService, StatsBarComponent, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, ThreadPanelComponent, TicketCardComponent, TicketCardImageService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, TranslatePipe, TypedCollection, TypingIndicatorComponent, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_CHAT_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_NETWORK_ERROR_KEY, VALTECH_REACTIONS_CONFIG, VALTECH_SETTINGS_MENU_LINKS, VALTECH_SITE_PATHS, VALTECH_SOCIAL_LINKS, VALTECH_SPLASH_SCREEN, VALTECH_WEB_BASE_URLS, VALTECH_WHATSAPP_CONFIG, VERSION, ValtechErrorService, VerifyViewComponent, WhatsappFabComponent, WhatsappService, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, authPasswordValidator, beautifyLegalArticle, blogPost, buildCompanyFooterProps, buildFooterLinks, buildLegalLinkResolver, buildPath, buildSettingsCards, button, canSubmitRequestType, collections, connectPageRefresh, createErrorStateProps, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createPageState, createPermissionLabeler, createRefreshableStream, createTitleProps, docs, errorLoggingInterceptor, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, groupPermissionsByScope, guestGuard, hasEmulators, iconButton, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isIonicColor, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideSplashScreen, provideValtechAboutRoutes, provideValtechAccountRoutes, provideValtechAds, provideValtechApiKeysRoutes, provideValtechAppConfig, provideValtechAppVersion, provideValtechAppVersionHttp, provideValtechAuth, provideValtechAuthInterceptor, provideValtechChat, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationPreferencesRoutes, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPermissionsRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechReactions, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, provideValtechWhatsapp, query, rbacGuard, renderPatternSvgInner, replaceSpecialChars, requestSubmitMode, resolveColor, resolveInputDefaultValue, resolveIonicColor, resolveWebBaseUrl, roleGuard, selectableRequestTypes, storagePaths, superAdminGuard, toArticle };
73688
73215
  //# sourceMappingURL=valtech-components.mjs.map