valtech-components 4.0.225 → 4.0.227

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 (24) hide show
  1. package/esm2022/lib/components/molecules/chat-composer/chat-composer.component.mjs +391 -0
  2. package/esm2022/lib/components/molecules/conversation-list-item/conversation-list-item.component.mjs +108 -0
  3. package/esm2022/lib/components/molecules/message-bubble/message-bubble.component.mjs +274 -191
  4. package/esm2022/lib/components/molecules/typing-indicator/typing-indicator.component.mjs +11 -12
  5. package/esm2022/lib/components/organisms/chat-window/chat-window.component.mjs +217 -184
  6. package/esm2022/lib/components/organisms/thread-panel/thread-panel.component.mjs +1 -1
  7. package/esm2022/lib/services/chat/conversation.service.mjs +15 -4
  8. package/esm2022/lib/services/chat/types.mjs +1 -1
  9. package/esm2022/lib/shared/utils/datetime.mjs +54 -1
  10. package/esm2022/lib/version.mjs +2 -2
  11. package/esm2022/public-api.mjs +4 -2
  12. package/fesm2022/valtech-components.mjs +1047 -383
  13. package/fesm2022/valtech-components.mjs.map +1 -1
  14. package/lib/components/molecules/chat-composer/chat-composer.component.d.ts +63 -0
  15. package/lib/components/molecules/conversation-list-item/conversation-list-item.component.d.ts +30 -0
  16. package/lib/components/molecules/message-bubble/message-bubble.component.d.ts +34 -32
  17. package/lib/components/molecules/typing-indicator/typing-indicator.component.d.ts +5 -4
  18. package/lib/components/organisms/chat-window/chat-window.component.d.ts +52 -52
  19. package/lib/services/chat/conversation.service.d.ts +5 -3
  20. package/lib/services/chat/types.d.ts +6 -2
  21. package/lib/shared/utils/datetime.d.ts +19 -0
  22. package/lib/version.d.ts +1 -1
  23. package/package.json +1 -1
  24. package/public-api.d.ts +3 -1
@@ -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, returnDownBackOutline } 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, arrowUndoOutline, micOutline, arrowUpOutline } 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.225';
59
+ const VERSION = '4.0.227';
60
60
 
61
61
  // Control de estado de refresco (singleton a nivel de módulo)
62
62
  let isRefreshing = false;
@@ -1538,6 +1538,59 @@ 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
+ }
1541
1594
 
1542
1595
  /**
1543
1596
  * Servicio de internacionalización basado en Angular Signals.
@@ -72118,10 +72171,21 @@ class ConversationService {
72118
72171
  return this.http.delete(`${this.baseUrl}/conversations/${convId}/participants/${userId}`);
72119
72172
  }
72120
72173
  /**
72121
- * Envia un mensaje a una conversacion.
72174
+ * Envia un mensaje a una conversacion, opcionalmente con adjuntos.
72175
+ * El backend espera `attachments: { url, type, name, size }[]` (key `type` = MIME),
72176
+ * mientras que el front usa `mimeType` — se mapea aqui.
72122
72177
  */
72123
- sendMessage(convId, body) {
72124
- return this.http.post(`${this.baseUrl}/conversations/${convId}/messages`, { body });
72178
+ sendMessage(convId, body, attachments) {
72179
+ const payload = { body };
72180
+ if (attachments?.length) {
72181
+ payload['attachments'] = attachments.map(a => ({
72182
+ url: a.url,
72183
+ type: a.mimeType,
72184
+ name: a.name,
72185
+ size: a.size ?? 0,
72186
+ }));
72187
+ }
72188
+ return this.http.post(`${this.baseUrl}/conversations/${convId}/messages`, payload);
72125
72189
  }
72126
72190
  /**
72127
72191
  * Elimina (marca como eliminado) un mensaje.
@@ -72290,241 +72354,323 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72290
72354
  */
72291
72355
 
72292
72356
  addIcons({
72357
+ alertCircle,
72293
72358
  checkmark,
72294
72359
  checkmarkDone,
72360
+ createOutline,
72361
+ documentOutline,
72362
+ happyOutline,
72363
+ arrowUndoOutline,
72295
72364
  timeOutline,
72296
- alertCircle,
72297
- pencil,
72298
- trash,
72299
- returnDownBackOutline,
72365
+ trashOutline,
72300
72366
  });
72301
- const CHAT_I18N = {
72367
+ const MESSAGE_BUBBLE_I18N = {
72302
72368
  es: {
72303
- messageDeleted: '[mensaje eliminado]',
72304
- edited: '(editado)',
72305
- react: 'Reaccionar',
72306
72369
  reply: 'Responder',
72307
72370
  edit: 'Editar',
72308
72371
  delete: 'Eliminar',
72372
+ react: 'Reaccionar',
72373
+ deleted: 'Mensaje eliminado',
72374
+ edited: 'editado',
72309
72375
  },
72310
72376
  en: {
72311
- messageDeleted: '[message deleted]',
72312
- edited: '(edited)',
72313
- react: 'React',
72314
72377
  reply: 'Reply',
72315
72378
  edit: 'Edit',
72316
72379
  delete: 'Delete',
72380
+ react: 'React',
72381
+ deleted: 'Message deleted',
72382
+ edited: 'edited',
72317
72383
  },
72318
72384
  };
72319
72385
  /**
72320
- * val-message-bubble
72386
+ * val-message-bubble — burbuja de mensaje presentacional (signal inputs).
72321
72387
  *
72322
- * Burbuja de mensaje para el chat. Alineada a la derecha si isMine, a la izquierda si no.
72323
- * Muestra nombre del emisor, cuerpo, timestamp, estado de envio, reacciones y acciones.
72324
- * Si el mensaje fue eliminado muestra un texto en gris sin acciones.
72388
+ * Sin lógica de negocio: el contenedor (`val-chat-window`) decide agrupación y
72389
+ * pasa flags (showAvatar/showName/tail). Las acciones (responder/editar/borrar/
72390
+ * reaccionar) NO están siempre visibles: se revelan en hover (desktop) o al tocar
72391
+ * la burbuja (móvil), y se emiten por un único `(action)` tipado.
72325
72392
  *
72326
- * @example
72327
- * <val-message-bubble
72328
- * [props]="msg"
72329
- * (reactionClick)="onReaction($event)"
72330
- * (replyClick)="onReply($event)"
72331
- * (deleteClick)="onDelete($event)"
72332
- * />
72393
+ * Read receipts estilo WhatsApp: sending → reloj, sent → ✓, delivered/read → ✓✓
72394
+ * (read en color), failed → alerta.
72333
72395
  */
72334
72396
  class MessageBubbleComponent {
72335
72397
  constructor() {
72336
72398
  this.i18n = inject(I18nService);
72337
- this.reactionClick = new EventEmitter();
72338
- this.replyClick = new EventEmitter();
72339
- this.deleteClick = new EventEmitter();
72340
- this.editClick = new EventEmitter();
72341
- if (!this.i18n.hasNamespace('Chat')) {
72342
- this.i18n.registerDefaults('Chat', CHAT_I18N);
72343
- }
72344
- }
72345
- ngOnInit() { }
72346
- deletedText() {
72347
- this.i18n.lang();
72348
- return this.i18n.t('messageDeleted', 'Chat');
72349
- }
72350
- editedLabel() {
72351
- this.i18n.lang();
72352
- return this.i18n.t('edited', 'Chat');
72353
- }
72354
- formatTime(date) {
72355
- if (!date)
72356
- return '';
72357
- const d = date instanceof Date ? date : new Date(date);
72358
- return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
72359
- }
72360
- statusIcon() {
72361
- switch (this.props.status) {
72362
- case 'sending':
72363
- return 'time-outline';
72364
- case 'failed':
72365
- return 'alert-circle';
72366
- case 'sent':
72367
- default:
72368
- return 'checkmark';
72369
- }
72370
- }
72371
- onReactionClick(reaction) {
72372
- this.reactionClick.emit({ token: reaction.token, msgId: this.props.msgId });
72399
+ this.msg = input.required();
72400
+ this.showAvatar = input(false);
72401
+ this.showName = input(false);
72402
+ this.tail = input(true);
72403
+ this.locale = input('es-CL');
72404
+ this.action = output();
72405
+ this.actionsOpen = signal(false);
72406
+ this.time = computed(() => formatClockTime(this.msg().createdAt, this.locale()));
72407
+ this.initials = computed(() => {
72408
+ const name = this.msg().senderName?.trim() ?? '';
72409
+ if (!name)
72410
+ return '?';
72411
+ const parts = name.split(/\s+/);
72412
+ return (parts[0][0] + (parts[1]?.[0] ?? '')).toUpperCase();
72413
+ });
72414
+ this.imageAttachments = computed(() => (this.msg().attachments ?? []).filter(a => a.mimeType?.startsWith('image/')));
72415
+ this.audioAttachments = computed(() => (this.msg().attachments ?? []).filter(a => a.mimeType?.startsWith('audio/')));
72416
+ this.fileAttachments = computed(() => (this.msg().attachments ?? []).filter(a => !a.mimeType?.startsWith('image/') && !a.mimeType?.startsWith('audio/')));
72417
+ this.statusIcon = computed(() => {
72418
+ switch (this.msg().status) {
72419
+ case 'sending':
72420
+ return 'time-outline';
72421
+ case 'failed':
72422
+ return 'alert-circle';
72423
+ case 'delivered':
72424
+ case 'read':
72425
+ return 'checkmark-done';
72426
+ case 'sent':
72427
+ default:
72428
+ return 'checkmark';
72429
+ }
72430
+ });
72431
+ this.i18n.registerDefaults('MessageBubble', MESSAGE_BUBBLE_I18N);
72373
72432
  }
72374
- onReplyClick() {
72375
- this.replyClick.emit({ msgId: this.props.msgId });
72433
+ toggleActions() {
72434
+ if (this.msg().isDeleted)
72435
+ return;
72436
+ this.actionsOpen.update(v => !v);
72376
72437
  }
72377
- onDeleteClick() {
72378
- this.deleteClick.emit({ msgId: this.props.msgId });
72438
+ emit(type, token) {
72439
+ this.actionsOpen.set(false);
72440
+ this.action.emit({ type, msgId: this.msg().msgId, token });
72379
72441
  }
72380
- onEditClick() {
72381
- this.editClick.emit({ msgId: this.props.msgId });
72442
+ t(key) {
72443
+ return this.i18n.t(key, 'MessageBubble');
72382
72444
  }
72383
72445
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageBubbleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
72384
- 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: `
72385
- <div class="message-bubble-wrapper" [class.is-mine]="props.isMine">
72386
- @if (!props.isMine && !props.isDeleted) {
72387
- <span class="sender-name">{{ props.senderName }}</span>
72446
+ 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: `
72447
+ <div class="row" [class.mine]="msg().isMine" [class.tail]="tail()" [class.with-avatar]="showAvatar()">
72448
+ @if (showAvatar() && !msg().isMine) {
72449
+ <div class="avatar" [class.hidden]="!tail()">
72450
+ @if (msg().senderAvatar) {
72451
+ <img [src]="msg().senderAvatar" [alt]="msg().senderName" />
72452
+ } @else {
72453
+ <span>{{ initials() }}</span>
72454
+ }
72455
+ </div>
72388
72456
  }
72389
72457
 
72390
- <div class="bubble">
72391
- @if (props.replyTo && !props.isDeleted) {
72392
- <div class="reply-preview">
72393
- <div class="reply-sender">{{ props.replyTo.senderName }}</div>
72394
- <div class="reply-body">{{ props.replyTo.body }}</div>
72395
- </div>
72396
- }
72397
-
72398
- @if (props.isDeleted) {
72399
- <span class="deleted-text">{{ deletedText() }}</span>
72400
- } @else {
72401
- <div class="bubble-body">{{ props.body }}</div>
72402
- }
72403
-
72404
- <div class="bubble-meta">
72405
- <span class="timestamp">{{ formatTime(props.createdAt) }}</span>
72406
- @if (props.isEdited && !props.isDeleted) {
72407
- <span class="edited-label">{{ editedLabel() }}</span>
72408
- }
72409
- @if (props.isMine && props.status) {
72410
- <ion-icon
72411
- [name]="statusIcon()"
72412
- [class]="'status-icon ' + (props.status ?? '')"
72413
- aria-hidden="true"
72414
- ></ion-icon>
72458
+ <div class="bubble-wrap">
72459
+ <div
72460
+ class="bubble"
72461
+ [class.deleted]="msg().isDeleted"
72462
+ (click)="toggleActions()"
72463
+ (keyup.enter)="toggleActions()"
72464
+ tabindex="0"
72465
+ >
72466
+ @if (showName() && !msg().isMine && !msg().isDeleted) {
72467
+ <span class="name">{{ msg().senderName }}</span>
72415
72468
  }
72416
- </div>
72417
- </div>
72418
72469
 
72419
- @if (!props.isDeleted && props.reactions && props.reactions.length > 0) {
72420
- <div class="reactions-bar">
72421
- @for (reaction of props.reactions; track reaction.token) {
72422
- <button class="reaction-btn" [class.active]="reaction.active" (click)="onReactionClick(reaction)">
72423
- <span>{{ reaction.token }}</span>
72424
- @if (reaction.count > 0) {
72425
- <span class="reaction-count">{{ reaction.count }}</span>
72426
- }
72427
- </button>
72470
+ @if (msg().replyTo && !msg().isDeleted) {
72471
+ <div class="reply-preview">
72472
+ <span class="reply-name">{{ msg().replyTo!.senderName }}</span>
72473
+ <span class="reply-body">{{ msg().replyTo!.body }}</span>
72474
+ </div>
72428
72475
  }
72429
- </div>
72430
- }
72431
72476
 
72432
- @if (!props.isDeleted) {
72433
- <div class="bubble-actions">
72434
- <ion-button fill="clear" size="small" (click)="onReplyClick()">
72435
- <ion-icon name="return-down-back-outline" slot="icon-only" aria-hidden="true"></ion-icon>
72436
- </ion-button>
72437
- @if (props.isMine) {
72438
- <ion-button fill="clear" size="small" (click)="onEditClick()">
72439
- <ion-icon name="pencil" slot="icon-only" aria-hidden="true"></ion-icon>
72440
- </ion-button>
72441
- <ion-button fill="clear" size="small" color="danger" (click)="onDeleteClick()">
72442
- <ion-icon name="trash" slot="icon-only" aria-hidden="true"></ion-icon>
72443
- </ion-button>
72477
+ @if (msg().isDeleted) {
72478
+ <span class="body deleted-text">{{ t('deleted') }}</span>
72479
+ } @else {
72480
+ @if (imageAttachments().length > 0) {
72481
+ <div class="attachments">
72482
+ @for (att of imageAttachments(); track att.url) {
72483
+ <img class="att-image" [src]="att.url" [alt]="att.name" />
72484
+ }
72485
+ </div>
72486
+ }
72487
+ @if (audioAttachments().length > 0) {
72488
+ <div class="audios">
72489
+ @for (att of audioAttachments(); track att.url) {
72490
+ <audio class="att-audio" controls preload="metadata" [src]="att.url"></audio>
72491
+ }
72492
+ </div>
72493
+ }
72494
+ @if (fileAttachments().length > 0) {
72495
+ <div class="files">
72496
+ @for (att of fileAttachments(); track att.url) {
72497
+ <a class="file-chip" [href]="att.url" target="_blank" rel="noopener">
72498
+ <ion-icon name="document-outline" aria-hidden="true" />
72499
+ <span>{{ att.name }}</span>
72500
+ </a>
72501
+ }
72502
+ </div>
72503
+ }
72504
+ @if (msg().body) {
72505
+ <span class="body">{{ msg().body }}</span>
72506
+ }
72444
72507
  }
72508
+
72509
+ <span class="meta">
72510
+ @if (msg().isEdited && !msg().isDeleted) {
72511
+ <span class="edited">{{ t('edited') }}</span>
72512
+ }
72513
+ <span class="time">{{ time() }}</span>
72514
+ @if (msg().isMine && !msg().isDeleted) {
72515
+ <ion-icon
72516
+ class="status"
72517
+ [class.read]="msg().status === 'read'"
72518
+ [class.failed]="msg().status === 'failed'"
72519
+ [name]="statusIcon()"
72520
+ aria-hidden="true"
72521
+ />
72522
+ }
72523
+ </span>
72445
72524
  </div>
72446
- }
72447
- </div>
72448
- `, 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"] }] }); }
72449
- }
72450
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageBubbleComponent, decorators: [{
72451
- type: Component,
72452
- args: [{ selector: 'val-message-bubble', standalone: true, imports: [CommonModule, IonIcon, IonButton], template: `
72453
- <div class="message-bubble-wrapper" [class.is-mine]="props.isMine">
72454
- @if (!props.isMine && !props.isDeleted) {
72455
- <span class="sender-name">{{ props.senderName }}</span>
72456
- }
72457
72525
 
72458
- <div class="bubble">
72459
- @if (props.replyTo && !props.isDeleted) {
72460
- <div class="reply-preview">
72461
- <div class="reply-sender">{{ props.replyTo.senderName }}</div>
72462
- <div class="reply-body">{{ props.replyTo.body }}</div>
72526
+ @if (msg().reactions && msg().reactions!.length > 0) {
72527
+ <div class="reactions">
72528
+ @for (r of msg().reactions!; track r.token) {
72529
+ <button class="reaction" [class.active]="r.active" (click)="emit('react', r.token)">
72530
+ <span>{{ r.token }}</span>
72531
+ <span class="count">{{ r.count }}</span>
72532
+ </button>
72533
+ }
72463
72534
  </div>
72464
72535
  }
72465
72536
 
72466
- @if (props.isDeleted) {
72467
- <span class="deleted-text">{{ deletedText() }}</span>
72468
- } @else {
72469
- <div class="bubble-body">{{ props.body }}</div>
72537
+ @if (!msg().isDeleted) {
72538
+ <div class="actions" [class.open]="actionsOpen()">
72539
+ <button class="act" [attr.aria-label]="t('react')" (click)="emit('react', '👍')">
72540
+ <ion-icon name="happy-outline" aria-hidden="true" />
72541
+ </button>
72542
+ <button class="act" [attr.aria-label]="t('reply')" (click)="emit('reply')">
72543
+ <ion-icon name="arrow-undo-outline" aria-hidden="true" />
72544
+ </button>
72545
+ @if (msg().isMine) {
72546
+ <button class="act" [attr.aria-label]="t('edit')" (click)="emit('edit')">
72547
+ <ion-icon name="create-outline" aria-hidden="true" />
72548
+ </button>
72549
+ <button class="act danger" [attr.aria-label]="t('delete')" (click)="emit('delete')">
72550
+ <ion-icon name="trash-outline" aria-hidden="true" />
72551
+ </button>
72552
+ }
72553
+ </div>
72470
72554
  }
72471
-
72472
- <div class="bubble-meta">
72473
- <span class="timestamp">{{ formatTime(props.createdAt) }}</span>
72474
- @if (props.isEdited && !props.isDeleted) {
72475
- <span class="edited-label">{{ editedLabel() }}</span>
72476
- }
72477
- @if (props.isMine && props.status) {
72478
- <ion-icon
72479
- [name]="statusIcon()"
72480
- [class]="'status-icon ' + (props.status ?? '')"
72481
- aria-hidden="true"
72482
- ></ion-icon>
72483
- }
72484
- </div>
72485
72555
  </div>
72486
-
72487
- @if (!props.isDeleted && props.reactions && props.reactions.length > 0) {
72488
- <div class="reactions-bar">
72489
- @for (reaction of props.reactions; track reaction.token) {
72490
- <button class="reaction-btn" [class.active]="reaction.active" (click)="onReactionClick(reaction)">
72491
- <span>{{ reaction.token }}</span>
72492
- @if (reaction.count > 0) {
72493
- <span class="reaction-count">{{ reaction.count }}</span>
72494
- }
72495
- </button>
72556
+ </div>
72557
+ `, 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}.audios{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.att-audio{width:240px;max-width:100%;height:40px}.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"] }] }); }
72558
+ }
72559
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageBubbleComponent, decorators: [{
72560
+ type: Component,
72561
+ args: [{ selector: 'val-message-bubble', standalone: true, imports: [CommonModule, IonIcon], template: `
72562
+ <div class="row" [class.mine]="msg().isMine" [class.tail]="tail()" [class.with-avatar]="showAvatar()">
72563
+ @if (showAvatar() && !msg().isMine) {
72564
+ <div class="avatar" [class.hidden]="!tail()">
72565
+ @if (msg().senderAvatar) {
72566
+ <img [src]="msg().senderAvatar" [alt]="msg().senderName" />
72567
+ } @else {
72568
+ <span>{{ initials() }}</span>
72496
72569
  }
72497
72570
  </div>
72498
72571
  }
72499
72572
 
72500
- @if (!props.isDeleted) {
72501
- <div class="bubble-actions">
72502
- <ion-button fill="clear" size="small" (click)="onReplyClick()">
72503
- <ion-icon name="return-down-back-outline" slot="icon-only" aria-hidden="true"></ion-icon>
72504
- </ion-button>
72505
- @if (props.isMine) {
72506
- <ion-button fill="clear" size="small" (click)="onEditClick()">
72507
- <ion-icon name="pencil" slot="icon-only" aria-hidden="true"></ion-icon>
72508
- </ion-button>
72509
- <ion-button fill="clear" size="small" color="danger" (click)="onDeleteClick()">
72510
- <ion-icon name="trash" slot="icon-only" aria-hidden="true"></ion-icon>
72511
- </ion-button>
72573
+ <div class="bubble-wrap">
72574
+ <div
72575
+ class="bubble"
72576
+ [class.deleted]="msg().isDeleted"
72577
+ (click)="toggleActions()"
72578
+ (keyup.enter)="toggleActions()"
72579
+ tabindex="0"
72580
+ >
72581
+ @if (showName() && !msg().isMine && !msg().isDeleted) {
72582
+ <span class="name">{{ msg().senderName }}</span>
72583
+ }
72584
+
72585
+ @if (msg().replyTo && !msg().isDeleted) {
72586
+ <div class="reply-preview">
72587
+ <span class="reply-name">{{ msg().replyTo!.senderName }}</span>
72588
+ <span class="reply-body">{{ msg().replyTo!.body }}</span>
72589
+ </div>
72590
+ }
72591
+
72592
+ @if (msg().isDeleted) {
72593
+ <span class="body deleted-text">{{ t('deleted') }}</span>
72594
+ } @else {
72595
+ @if (imageAttachments().length > 0) {
72596
+ <div class="attachments">
72597
+ @for (att of imageAttachments(); track att.url) {
72598
+ <img class="att-image" [src]="att.url" [alt]="att.name" />
72599
+ }
72600
+ </div>
72601
+ }
72602
+ @if (audioAttachments().length > 0) {
72603
+ <div class="audios">
72604
+ @for (att of audioAttachments(); track att.url) {
72605
+ <audio class="att-audio" controls preload="metadata" [src]="att.url"></audio>
72606
+ }
72607
+ </div>
72608
+ }
72609
+ @if (fileAttachments().length > 0) {
72610
+ <div class="files">
72611
+ @for (att of fileAttachments(); track att.url) {
72612
+ <a class="file-chip" [href]="att.url" target="_blank" rel="noopener">
72613
+ <ion-icon name="document-outline" aria-hidden="true" />
72614
+ <span>{{ att.name }}</span>
72615
+ </a>
72616
+ }
72617
+ </div>
72618
+ }
72619
+ @if (msg().body) {
72620
+ <span class="body">{{ msg().body }}</span>
72621
+ }
72512
72622
  }
72623
+
72624
+ <span class="meta">
72625
+ @if (msg().isEdited && !msg().isDeleted) {
72626
+ <span class="edited">{{ t('edited') }}</span>
72627
+ }
72628
+ <span class="time">{{ time() }}</span>
72629
+ @if (msg().isMine && !msg().isDeleted) {
72630
+ <ion-icon
72631
+ class="status"
72632
+ [class.read]="msg().status === 'read'"
72633
+ [class.failed]="msg().status === 'failed'"
72634
+ [name]="statusIcon()"
72635
+ aria-hidden="true"
72636
+ />
72637
+ }
72638
+ </span>
72513
72639
  </div>
72514
- }
72640
+
72641
+ @if (msg().reactions && msg().reactions!.length > 0) {
72642
+ <div class="reactions">
72643
+ @for (r of msg().reactions!; track r.token) {
72644
+ <button class="reaction" [class.active]="r.active" (click)="emit('react', r.token)">
72645
+ <span>{{ r.token }}</span>
72646
+ <span class="count">{{ r.count }}</span>
72647
+ </button>
72648
+ }
72649
+ </div>
72650
+ }
72651
+
72652
+ @if (!msg().isDeleted) {
72653
+ <div class="actions" [class.open]="actionsOpen()">
72654
+ <button class="act" [attr.aria-label]="t('react')" (click)="emit('react', '👍')">
72655
+ <ion-icon name="happy-outline" aria-hidden="true" />
72656
+ </button>
72657
+ <button class="act" [attr.aria-label]="t('reply')" (click)="emit('reply')">
72658
+ <ion-icon name="arrow-undo-outline" aria-hidden="true" />
72659
+ </button>
72660
+ @if (msg().isMine) {
72661
+ <button class="act" [attr.aria-label]="t('edit')" (click)="emit('edit')">
72662
+ <ion-icon name="create-outline" aria-hidden="true" />
72663
+ </button>
72664
+ <button class="act danger" [attr.aria-label]="t('delete')" (click)="emit('delete')">
72665
+ <ion-icon name="trash-outline" aria-hidden="true" />
72666
+ </button>
72667
+ }
72668
+ </div>
72669
+ }
72670
+ </div>
72515
72671
  </div>
72516
- `, 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"] }]
72517
- }], ctorParameters: () => [], propDecorators: { props: [{
72518
- type: Input
72519
- }], reactionClick: [{
72520
- type: Output
72521
- }], replyClick: [{
72522
- type: Output
72523
- }], deleteClick: [{
72524
- type: Output
72525
- }], editClick: [{
72526
- type: Output
72527
- }] } });
72672
+ `, 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}.audios{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.att-audio{width:240px;max-width:100%;height:40px}.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"] }]
72673
+ }], ctorParameters: () => [] });
72528
72674
 
72529
72675
  addIcons({ sendOutline });
72530
72676
  const CHAT_INPUT_I18N = {
@@ -72676,6 +72822,493 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72676
72822
  type: Output
72677
72823
  }] } });
72678
72824
 
72825
+ addIcons({ addOutline, closeOutline, happyOutline, micOutline, arrowUpOutline, trashOutline });
72826
+ const CHAT_COMPOSER_I18N = {
72827
+ es: {
72828
+ placeholder: 'Escribe un mensaje',
72829
+ send: 'Enviar',
72830
+ attach: 'Adjuntar',
72831
+ emoji: 'Emoji',
72832
+ voice: 'Mensaje de voz',
72833
+ replyingTo: 'Respondiendo a',
72834
+ cancel: 'Cancelar',
72835
+ },
72836
+ en: {
72837
+ placeholder: 'Write a message',
72838
+ send: 'Send',
72839
+ attach: 'Attach',
72840
+ emoji: 'Emoji',
72841
+ voice: 'Voice message',
72842
+ replyingTo: 'Replying to',
72843
+ cancel: 'Cancel',
72844
+ },
72845
+ };
72846
+ const QUICK_EMOJIS = ['👍', '❤️', '😂', '🎉', '🙏', '🔥', '😮', '😢'];
72847
+ /**
72848
+ * val-chat-composer — barra de redacción de chat moderna (signal inputs).
72849
+ *
72850
+ * Pill con: adjuntar (+), textarea auto-grow, emoji (fila rápida) y mic/enviar
72851
+ * (mic cuando está vacío, enviar cuando hay texto — patrón WhatsApp/Telegram).
72852
+ * Muestra una barra de contexto "respondiendo a" cuando `replyingTo` está seteado.
72853
+ * `typing` se emite con debounce (máx. 1 vez cada 2s) para no spamear writes.
72854
+ */
72855
+ class ChatComposerComponent {
72856
+ constructor() {
72857
+ this.i18n = inject(I18nService);
72858
+ this.placeholder = input('');
72859
+ this.disabled = input(false);
72860
+ this.maxLength = input(2000);
72861
+ this.replyingTo = input(null);
72862
+ this.showAttach = input(true);
72863
+ this.showEmoji = input(true);
72864
+ this.showMic = input(true);
72865
+ this.send = output();
72866
+ this.typing = output();
72867
+ this.attach = output();
72868
+ this.voice = output();
72869
+ this.cancelReply = output();
72870
+ this.body = signal('');
72871
+ this.emojiOpen = signal(false);
72872
+ this.quickEmojis = QUICK_EMOJIS;
72873
+ // Grabación de audio (MediaRecorder).
72874
+ this.recording = signal(false);
72875
+ this.recSeconds = signal(0);
72876
+ this.recChunks = [];
72877
+ this.recTimeLabel = computed(() => {
72878
+ const s = this.recSeconds();
72879
+ const mm = String(Math.floor(s / 60)).padStart(2, '0');
72880
+ const ss = String(s % 60).padStart(2, '0');
72881
+ return `${mm}:${ss}`;
72882
+ });
72883
+ this.lastTypingEmit = 0;
72884
+ this.canSend = computed(() => {
72885
+ const t = this.body().trim();
72886
+ return t.length > 0 && t.length <= this.maxLength() && !this.disabled();
72887
+ });
72888
+ this.placeholderText = computed(() => {
72889
+ this.i18n.lang();
72890
+ return this.placeholder() || this.i18n.t('placeholder', 'ChatComposer');
72891
+ });
72892
+ this.i18n.registerDefaults('ChatComposer', CHAT_COMPOSER_I18N);
72893
+ }
72894
+ onInput(event) {
72895
+ const target = event.target;
72896
+ this.body.set(target.value ?? '');
72897
+ this.emitTypingDebounced();
72898
+ }
72899
+ onKeydown(event) {
72900
+ if (event.key === 'Enter' && !event.shiftKey) {
72901
+ event.preventDefault();
72902
+ this.onSend();
72903
+ }
72904
+ }
72905
+ onSend() {
72906
+ const trimmed = this.body().trim();
72907
+ if (!trimmed || trimmed.length > this.maxLength())
72908
+ return;
72909
+ this.send.emit(trimmed);
72910
+ this.body.set('');
72911
+ this.emojiOpen.set(false);
72912
+ }
72913
+ toggleEmoji() {
72914
+ this.emojiOpen.update(v => !v);
72915
+ }
72916
+ insertEmoji(emoji) {
72917
+ this.body.update(v => v + emoji);
72918
+ }
72919
+ onFile(event) {
72920
+ const input = event.target;
72921
+ const file = input.files?.[0];
72922
+ if (file)
72923
+ this.attach.emit(file);
72924
+ input.value = '';
72925
+ }
72926
+ /** Pegar imagen desde el portapapeles (captura de pantalla, copiar imagen). */
72927
+ onPaste(event) {
72928
+ const items = event.clipboardData?.items;
72929
+ if (!items)
72930
+ return;
72931
+ for (const item of Array.from(items)) {
72932
+ if (item.type.startsWith('image/')) {
72933
+ const file = item.getAsFile();
72934
+ if (file) {
72935
+ event.preventDefault();
72936
+ this.attach.emit(file);
72937
+ }
72938
+ return;
72939
+ }
72940
+ }
72941
+ }
72942
+ /**
72943
+ * Emite typing como máximo 1 vez cada 2s. No usa Date.now() directo en plantillas
72944
+ * de workflow; aquí estamos en runtime de navegador, performance.now es seguro.
72945
+ */
72946
+ emitTypingDebounced() {
72947
+ const now = performance.now();
72948
+ if (now - this.lastTypingEmit > 2000) {
72949
+ this.lastTypingEmit = now;
72950
+ this.typing.emit();
72951
+ }
72952
+ }
72953
+ // ─── Grabación de audio ───────────────────────────────────────────────────
72954
+ /** Selecciona un mimeType de audio soportado por el navegador. */
72955
+ pickAudioMime() {
72956
+ const candidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4'];
72957
+ const MR = window.MediaRecorder;
72958
+ if (!MR?.isTypeSupported)
72959
+ return '';
72960
+ return candidates.find(c => MR.isTypeSupported(c)) ?? '';
72961
+ }
72962
+ async startRecording() {
72963
+ if (this.recording())
72964
+ return;
72965
+ if (!navigator.mediaDevices?.getUserMedia || !('MediaRecorder' in window)) {
72966
+ this.voice.emit(); // sin soporte: deja que el consumer decida (fallback)
72967
+ return;
72968
+ }
72969
+ try {
72970
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
72971
+ this.recStream = stream;
72972
+ const mimeType = this.pickAudioMime();
72973
+ this.recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined);
72974
+ this.recChunks = [];
72975
+ this.recorder.ondataavailable = e => {
72976
+ if (e.data.size > 0)
72977
+ this.recChunks.push(e.data);
72978
+ };
72979
+ this.recorder.start();
72980
+ this.recording.set(true);
72981
+ this.recSeconds.set(0);
72982
+ this.recTimer = setInterval(() => this.recSeconds.update(s => s + 1), 1000);
72983
+ }
72984
+ catch {
72985
+ this.cleanupRecording();
72986
+ }
72987
+ }
72988
+ stopAndSend() {
72989
+ const rec = this.recorder;
72990
+ if (!rec) {
72991
+ this.cleanupRecording();
72992
+ return;
72993
+ }
72994
+ rec.onstop = () => {
72995
+ const type = rec.mimeType || 'audio/webm';
72996
+ const blob = new Blob(this.recChunks, { type });
72997
+ const ext = type.includes('mp4') ? 'm4a' : 'webm';
72998
+ const file = new File([blob], `audio-${this.recSeconds()}s.${ext}`, { type });
72999
+ if (blob.size > 0)
73000
+ this.attach.emit(file);
73001
+ this.cleanupRecording();
73002
+ };
73003
+ rec.stop();
73004
+ }
73005
+ cancelRecording() {
73006
+ const rec = this.recorder;
73007
+ if (rec)
73008
+ rec.onstop = () => this.cleanupRecording();
73009
+ try {
73010
+ rec?.stop();
73011
+ }
73012
+ catch {
73013
+ this.cleanupRecording();
73014
+ }
73015
+ if (!rec)
73016
+ this.cleanupRecording();
73017
+ }
73018
+ cleanupRecording() {
73019
+ if (this.recTimer)
73020
+ clearInterval(this.recTimer);
73021
+ this.recTimer = undefined;
73022
+ this.recStream?.getTracks().forEach(t => t.stop());
73023
+ this.recStream = undefined;
73024
+ this.recorder = undefined;
73025
+ this.recChunks = [];
73026
+ this.recording.set(false);
73027
+ this.recSeconds.set(0);
73028
+ }
73029
+ ngOnDestroy() {
73030
+ this.cleanupRecording();
73031
+ }
73032
+ t(key) {
73033
+ return this.i18n.t(key, 'ChatComposer');
73034
+ }
73035
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatComposerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
73036
+ 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: `
73037
+ <div class="composer">
73038
+ @if (replyingTo()) {
73039
+ <div class="reply-bar">
73040
+ <div class="reply-content">
73041
+ <span class="reply-name">{{ replyingTo()!.senderName }}</span>
73042
+ <span class="reply-body">{{ replyingTo()!.body }}</span>
73043
+ </div>
73044
+ <button class="icon-btn" [attr.aria-label]="t('cancel')" (click)="cancelReply.emit()">
73045
+ <ion-icon name="close-outline" aria-hidden="true" />
73046
+ </button>
73047
+ </div>
73048
+ }
73049
+
73050
+ @if (emojiOpen()) {
73051
+ <div class="emoji-row">
73052
+ @for (e of quickEmojis; track e) {
73053
+ <button class="emoji" (click)="insertEmoji(e)">{{ e }}</button>
73054
+ }
73055
+ </div>
73056
+ }
73057
+
73058
+ @if (recording()) {
73059
+ <div class="rec-bar">
73060
+ <button class="icon-btn danger" [attr.aria-label]="t('cancel')" (click)="cancelRecording()">
73061
+ <ion-icon name="trash-outline" aria-hidden="true" />
73062
+ </button>
73063
+ <span class="rec-dot"></span>
73064
+ <span class="rec-time">{{ recTimeLabel() }}</span>
73065
+ <span class="rec-spacer"></span>
73066
+ <button class="send-btn" [attr.aria-label]="t('send')" (click)="stopAndSend()">
73067
+ <ion-icon name="arrow-up-outline" aria-hidden="true" />
73068
+ </button>
73069
+ </div>
73070
+ } @else {
73071
+ <div class="pill">
73072
+ @if (showAttach()) {
73073
+ <label class="icon-btn" [attr.aria-label]="t('attach')">
73074
+ <ion-icon name="add-outline" aria-hidden="true" />
73075
+ <input type="file" hidden (change)="onFile($event)" accept="image/*,application/pdf" />
73076
+ </label>
73077
+ }
73078
+
73079
+ <ion-textarea
73080
+ class="field"
73081
+ [placeholder]="placeholderText()"
73082
+ [disabled]="disabled()"
73083
+ [maxlength]="maxLength()"
73084
+ [autoGrow]="true"
73085
+ [rows]="1"
73086
+ [value]="body()"
73087
+ [spellcheck]="false"
73088
+ autocapitalize="sentences"
73089
+ autocorrect="off"
73090
+ enterkeyhint="enter"
73091
+ (ionInput)="onInput($event)"
73092
+ (keydown)="onKeydown($event)"
73093
+ (paste)="onPaste($event)"
73094
+ ></ion-textarea>
73095
+
73096
+ @if (showEmoji()) {
73097
+ <button
73098
+ class="icon-btn"
73099
+ [class.active]="emojiOpen()"
73100
+ [attr.aria-label]="t('emoji')"
73101
+ (click)="toggleEmoji()"
73102
+ >
73103
+ <ion-icon name="happy-outline" aria-hidden="true" />
73104
+ </button>
73105
+ }
73106
+
73107
+ @if (canSend()) {
73108
+ <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73109
+ <ion-icon name="arrow-up-outline" aria-hidden="true" />
73110
+ </button>
73111
+ } @else if (showMic()) {
73112
+ <button class="icon-btn mic" [attr.aria-label]="t('voice')" (click)="startRecording()">
73113
+ <ion-icon name="mic-outline" aria-hidden="true" />
73114
+ </button>
73115
+ }
73116
+ </div>
73117
+ }
73118
+ </div>
73119
+ `, 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)}.icon-btn.danger{color:var(--ion-color-danger, #eb445a)}.rec-bar{display:flex;align-items:center;gap:10px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.rec-dot{width:10px;height:10px;border-radius:50%;background:var(--ion-color-danger, #eb445a);animation:rec-pulse 1.2s ease-in-out infinite}.rec-time{font-variant-numeric:tabular-nums;font-size:.9375rem;color:var(--ion-text-color, #000)}.rec-spacer{flex:1}@keyframes rec-pulse{0%,to{opacity:1}50%{opacity:.3}}\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"] }] }); }
73120
+ }
73121
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatComposerComponent, decorators: [{
73122
+ type: Component,
73123
+ args: [{ selector: 'val-chat-composer', standalone: true, imports: [CommonModule, IonTextarea, IonIcon], template: `
73124
+ <div class="composer">
73125
+ @if (replyingTo()) {
73126
+ <div class="reply-bar">
73127
+ <div class="reply-content">
73128
+ <span class="reply-name">{{ replyingTo()!.senderName }}</span>
73129
+ <span class="reply-body">{{ replyingTo()!.body }}</span>
73130
+ </div>
73131
+ <button class="icon-btn" [attr.aria-label]="t('cancel')" (click)="cancelReply.emit()">
73132
+ <ion-icon name="close-outline" aria-hidden="true" />
73133
+ </button>
73134
+ </div>
73135
+ }
73136
+
73137
+ @if (emojiOpen()) {
73138
+ <div class="emoji-row">
73139
+ @for (e of quickEmojis; track e) {
73140
+ <button class="emoji" (click)="insertEmoji(e)">{{ e }}</button>
73141
+ }
73142
+ </div>
73143
+ }
73144
+
73145
+ @if (recording()) {
73146
+ <div class="rec-bar">
73147
+ <button class="icon-btn danger" [attr.aria-label]="t('cancel')" (click)="cancelRecording()">
73148
+ <ion-icon name="trash-outline" aria-hidden="true" />
73149
+ </button>
73150
+ <span class="rec-dot"></span>
73151
+ <span class="rec-time">{{ recTimeLabel() }}</span>
73152
+ <span class="rec-spacer"></span>
73153
+ <button class="send-btn" [attr.aria-label]="t('send')" (click)="stopAndSend()">
73154
+ <ion-icon name="arrow-up-outline" aria-hidden="true" />
73155
+ </button>
73156
+ </div>
73157
+ } @else {
73158
+ <div class="pill">
73159
+ @if (showAttach()) {
73160
+ <label class="icon-btn" [attr.aria-label]="t('attach')">
73161
+ <ion-icon name="add-outline" aria-hidden="true" />
73162
+ <input type="file" hidden (change)="onFile($event)" accept="image/*,application/pdf" />
73163
+ </label>
73164
+ }
73165
+
73166
+ <ion-textarea
73167
+ class="field"
73168
+ [placeholder]="placeholderText()"
73169
+ [disabled]="disabled()"
73170
+ [maxlength]="maxLength()"
73171
+ [autoGrow]="true"
73172
+ [rows]="1"
73173
+ [value]="body()"
73174
+ [spellcheck]="false"
73175
+ autocapitalize="sentences"
73176
+ autocorrect="off"
73177
+ enterkeyhint="enter"
73178
+ (ionInput)="onInput($event)"
73179
+ (keydown)="onKeydown($event)"
73180
+ (paste)="onPaste($event)"
73181
+ ></ion-textarea>
73182
+
73183
+ @if (showEmoji()) {
73184
+ <button
73185
+ class="icon-btn"
73186
+ [class.active]="emojiOpen()"
73187
+ [attr.aria-label]="t('emoji')"
73188
+ (click)="toggleEmoji()"
73189
+ >
73190
+ <ion-icon name="happy-outline" aria-hidden="true" />
73191
+ </button>
73192
+ }
73193
+
73194
+ @if (canSend()) {
73195
+ <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73196
+ <ion-icon name="arrow-up-outline" aria-hidden="true" />
73197
+ </button>
73198
+ } @else if (showMic()) {
73199
+ <button class="icon-btn mic" [attr.aria-label]="t('voice')" (click)="startRecording()">
73200
+ <ion-icon name="mic-outline" aria-hidden="true" />
73201
+ </button>
73202
+ }
73203
+ </div>
73204
+ }
73205
+ </div>
73206
+ `, 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)}.icon-btn.danger{color:var(--ion-color-danger, #eb445a)}.rec-bar{display:flex;align-items:center;gap:10px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.rec-dot{width:10px;height:10px;border-radius:50%;background:var(--ion-color-danger, #eb445a);animation:rec-pulse 1.2s ease-in-out infinite}.rec-time{font-variant-numeric:tabular-nums;font-size:.9375rem;color:var(--ion-text-color, #000)}.rec-spacer{flex:1}@keyframes rec-pulse{0%,to{opacity:1}50%{opacity:.3}}\n"] }]
73207
+ }], ctorParameters: () => [] });
73208
+
73209
+ /**
73210
+ * val-conversation-list-item — fila de lista de conversaciones estilo
73211
+ * Telegram/WhatsApp: avatar (imagen o iniciales), nombre + hora relativa,
73212
+ * preview del último mensaje + badge de no-leídos. Signal inputs, dark-safe.
73213
+ *
73214
+ * @example
73215
+ * <val-conversation-list-item
73216
+ * [name]="conv.name"
73217
+ * [lastMessage]="conv.lastMsgPreview"
73218
+ * [timestamp]="conv.lastMsgAt"
73219
+ * [unreadCount]="conv.unread"
73220
+ * (select)="open(conv.convId)" />
73221
+ */
73222
+ class ConversationListItemComponent {
73223
+ constructor() {
73224
+ this.i18n = inject(I18nService);
73225
+ this.name = input('');
73226
+ this.avatarUrl = input(undefined);
73227
+ this.lastMessage = input('');
73228
+ this.timestamp = input(undefined);
73229
+ this.unreadCount = input(0);
73230
+ this.locale = input('es-CL');
73231
+ this.select = output();
73232
+ this.initials = computed(() => {
73233
+ const name = this.name().trim();
73234
+ if (!name)
73235
+ return '?';
73236
+ const parts = name.split(/\s+/);
73237
+ return (parts[0][0] + (parts[1]?.[0] ?? '')).toUpperCase();
73238
+ });
73239
+ this.timeLabel = computed(() => formatRelativeTime(this.timestamp(), this.locale(), {
73240
+ today: '',
73241
+ yesterday: this.i18n.t('yesterday', '_global'),
73242
+ }));
73243
+ this.i18n.registerDefaults('ConversationListItem', {
73244
+ es: { empty: 'Sin mensajes' },
73245
+ en: { empty: 'No messages' },
73246
+ });
73247
+ }
73248
+ emptyText() {
73249
+ this.i18n.lang();
73250
+ return this.i18n.t('empty', 'ConversationListItem');
73251
+ }
73252
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ConversationListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
73253
+ 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: `
73254
+ <button class="item" (click)="select.emit()">
73255
+ <div class="avatar">
73256
+ @if (avatarUrl()) {
73257
+ <img [src]="avatarUrl()" [alt]="name()" />
73258
+ } @else {
73259
+ <span>{{ initials() }}</span>
73260
+ }
73261
+ </div>
73262
+ <div class="content">
73263
+ <div class="top">
73264
+ <span class="name">{{ name() }}</span>
73265
+ @if (timeLabel()) {
73266
+ <span class="time" [class.unread]="unreadCount() > 0">{{ timeLabel() }}</span>
73267
+ }
73268
+ </div>
73269
+ <div class="bottom">
73270
+ <span class="preview" [class.muted]="!lastMessage()">
73271
+ {{ lastMessage() || emptyText() }}
73272
+ </span>
73273
+ @if (unreadCount() > 0) {
73274
+ <span class="badge">{{ unreadCount() > 99 ? '99+' : unreadCount() }}</span>
73275
+ }
73276
+ </div>
73277
+ </div>
73278
+ </button>
73279
+ `, 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 }] }); }
73280
+ }
73281
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ConversationListItemComponent, decorators: [{
73282
+ type: Component,
73283
+ args: [{ selector: 'val-conversation-list-item', standalone: true, imports: [CommonModule], template: `
73284
+ <button class="item" (click)="select.emit()">
73285
+ <div class="avatar">
73286
+ @if (avatarUrl()) {
73287
+ <img [src]="avatarUrl()" [alt]="name()" />
73288
+ } @else {
73289
+ <span>{{ initials() }}</span>
73290
+ }
73291
+ </div>
73292
+ <div class="content">
73293
+ <div class="top">
73294
+ <span class="name">{{ name() }}</span>
73295
+ @if (timeLabel()) {
73296
+ <span class="time" [class.unread]="unreadCount() > 0">{{ timeLabel() }}</span>
73297
+ }
73298
+ </div>
73299
+ <div class="bottom">
73300
+ <span class="preview" [class.muted]="!lastMessage()">
73301
+ {{ lastMessage() || emptyText() }}
73302
+ </span>
73303
+ @if (unreadCount() > 0) {
73304
+ <span class="badge">{{ unreadCount() > 99 ? '99+' : unreadCount() }}</span>
73305
+ }
73306
+ </div>
73307
+ </div>
73308
+ </button>
73309
+ `, 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"] }]
73310
+ }], ctorParameters: () => [] });
73311
+
72679
73312
  const TYPING_I18N = {
72680
73313
  es: {
72681
73314
  one: '{name} esta escribiendo',
@@ -72691,8 +73324,9 @@ const TYPING_I18N = {
72691
73324
  /**
72692
73325
  * val-typing-indicator
72693
73326
  *
72694
- * Muestra quienes estan escribiendo en el chat con animacion de tres puntos.
72695
- * Se oculta automaticamente si typingUsers esta vacio.
73327
+ * Muestra quienes estan escribiendo con animacion de tres puntos. Se oculta
73328
+ * si `typingUsers` esta vacio. Usa signal input para que `typingText` (computed)
73329
+ * recompute al cambiar la lista.
72696
73330
  *
72697
73331
  * @example
72698
73332
  * <val-typing-indicator [typingUsers]="['Maria', 'Carlos']" />
@@ -72700,10 +73334,10 @@ const TYPING_I18N = {
72700
73334
  class TypingIndicatorComponent {
72701
73335
  constructor() {
72702
73336
  this.i18n = inject(I18nService);
72703
- this.typingUsers = [];
73337
+ this.typingUsers = input([]);
72704
73338
  this.typingText = computed(() => {
72705
73339
  this.i18n.lang();
72706
- const users = this.typingUsers ?? [];
73340
+ const users = this.typingUsers();
72707
73341
  if (users.length === 0)
72708
73342
  return '';
72709
73343
  if (users.length === 1) {
@@ -72717,8 +73351,8 @@ class TypingIndicatorComponent {
72717
73351
  this.i18n.registerDefaults('TypingIndicator', TYPING_I18N);
72718
73352
  }
72719
73353
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TypingIndicatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
72720
- 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: `
72721
- @if (typingUsers && typingUsers.length > 0) {
73354
+ 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: `
73355
+ @if (typingUsers().length > 0) {
72722
73356
  <div class="typing-indicator">
72723
73357
  <div class="dots">
72724
73358
  <span></span>
@@ -72733,7 +73367,7 @@ class TypingIndicatorComponent {
72733
73367
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TypingIndicatorComponent, decorators: [{
72734
73368
  type: Component,
72735
73369
  args: [{ selector: 'val-typing-indicator', standalone: true, imports: [CommonModule], template: `
72736
- @if (typingUsers && typingUsers.length > 0) {
73370
+ @if (typingUsers().length > 0) {
72737
73371
  <div class="typing-indicator">
72738
73372
  <div class="dots">
72739
73373
  <span></span>
@@ -72744,250 +73378,280 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72744
73378
  </div>
72745
73379
  }
72746
73380
  `, 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"] }]
72747
- }], ctorParameters: () => [], propDecorators: { typingUsers: [{
72748
- type: Input
72749
- }] } });
73381
+ }], ctorParameters: () => [] });
72750
73382
 
72751
- addIcons({ chatbubblesOutline });
73383
+ addIcons({ arrowDownOutline, chatbubblesOutline });
72752
73384
  const CHAT_WINDOW_I18N = {
72753
73385
  es: {
72754
73386
  conversationClosed: 'Esta conversacion esta cerrada',
72755
73387
  loading: 'Cargando mensajes...',
72756
- noMessages: 'No hay mensajes aun',
73388
+ noMessages: 'Aun no hay mensajes. Escribe el primero.',
73389
+ newMessages: 'Mensajes nuevos',
72757
73390
  },
72758
73391
  en: {
72759
73392
  conversationClosed: 'This conversation is closed',
72760
73393
  loading: 'Loading messages...',
72761
- noMessages: 'No messages yet',
73394
+ noMessages: 'No messages yet. Send the first one.',
73395
+ newMessages: 'New messages',
72762
73396
  },
72763
73397
  };
72764
73398
  /**
72765
- * val-chat-window
73399
+ * val-chat-window — ventana de chat completa (signal inputs).
72766
73400
  *
72767
- * Ventana de chat completa. Muestra la lista de mensajes agrupados por fecha,
72768
- * el indicador de escritura, el input de mensaje (solo si isOpen) y un banner
72769
- * cuando la conversacion esta cerrada. Auto-scroll al ultimo mensaje cuando
72770
- * llegan mensajes nuevos.
73401
+ * Agrupa mensajes consecutivos del mismo emisor y por día (separadores sticky),
73402
+ * muestra avatar/nombre solo en los bordes del grupo, scroll-to-bottom inteligente
73403
+ * (auto-scroll si estás abajo, pill "mensajes nuevos" si estás leyendo arriba),
73404
+ * composer moderno con contexto de respuesta, y typing indicator.
72771
73405
  *
72772
- * @example
72773
- * <val-chat-window
72774
- * [convId]="conv.id"
72775
- * [messages]="messages()"
72776
- * [currentUserId]="userId"
72777
- * [isOpen]="conv.status === 'open'"
72778
- * [typingUsers]="typingUsers()"
72779
- * (sendMessage)="onSend($event)"
72780
- * (reactionClick)="onReaction($event)"
72781
- * />
73406
+ * API pública estable (consumida por thread-panel y showcase): inputs convId,
73407
+ * messages, currentUserId, isOpen, typingUsers, isLoading; outputs sendMessage,
73408
+ * loadMore, reactionClick, deleteMessage, replyTo (+ editMessage/attach/voice nuevos).
72782
73409
  */
72783
73410
  class ChatWindowComponent {
72784
73411
  constructor() {
72785
73412
  this.i18n = inject(I18nService);
72786
- this.convId = '';
72787
- this.messages = [];
72788
- this.participants = [];
72789
- this.currentUserId = '';
72790
- this.isOpen = true;
72791
- this.typingUsers = [];
72792
- this.isLoading = false;
72793
- this.sendMessage = new EventEmitter();
72794
- this.loadMore = new EventEmitter();
72795
- this.reactionClick = new EventEmitter();
72796
- this.deleteMessage = new EventEmitter();
72797
- this.replyTo = new EventEmitter();
72798
- this.prevMessageCount = 0;
72799
- this.shouldScrollToBottom = false;
72800
- // Espejo signal de @Input() messages. `messageGroups` es un computed() y solo
72801
- // reacciona a señales — leer `this.messages` (input clasico) directo NO lo
72802
- // recomputa cuando el input cambia, dejando la lista de mensajes congelada.
72803
- this.messagesSig = signal([]);
72804
- this.messageGroups = computed(() => {
72805
- const msgs = this.messagesSig();
73413
+ this.convId = input('');
73414
+ this.messages = input([]);
73415
+ this.currentUserId = input('');
73416
+ this.isOpen = input(true);
73417
+ this.typingUsers = input([]);
73418
+ this.isLoading = input(false);
73419
+ this.sendMessage = output();
73420
+ this.loadMore = output();
73421
+ this.reactionClick = output();
73422
+ this.deleteMessage = output();
73423
+ this.replyTo = output();
73424
+ this.editMessage = output();
73425
+ this.typing = output();
73426
+ this.attach = output();
73427
+ this.voice = output();
73428
+ this.msgsEl = viewChild('msgs');
73429
+ this.replyingTo = signal(null);
73430
+ this.atBottom = signal(true);
73431
+ this.hasNew = signal(false);
73432
+ this.locale = computed(() => (this.i18n.lang() === 'en' ? 'en-US' : 'es-CL'));
73433
+ this.rows = computed(() => {
73434
+ const msgs = this.messages();
72806
73435
  if (msgs.length === 0)
72807
73436
  return [];
72808
- const groups = [];
72809
- let currentLabel = '';
72810
- let currentGroup = [];
72811
- for (const msg of msgs) {
72812
- const label = this.formatDateLabel(msg.createdAt);
72813
- if (label !== currentLabel) {
72814
- if (currentGroup.length > 0) {
72815
- groups.push({ dateLabel: currentLabel, messages: currentGroup });
72816
- }
72817
- currentLabel = label;
72818
- currentGroup = [msg];
72819
- }
72820
- else {
72821
- currentGroup.push(msg);
73437
+ const labels = {
73438
+ today: this.i18n.t('today', '_global'),
73439
+ yesterday: this.i18n.t('yesterday', '_global'),
73440
+ };
73441
+ const out = [];
73442
+ let prevDay = '';
73443
+ for (let i = 0; i < msgs.length; i++) {
73444
+ const m = msgs[i];
73445
+ const d = m.createdAt instanceof Date ? m.createdAt : new Date(m.createdAt);
73446
+ const day = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
73447
+ const newDay = day !== prevDay;
73448
+ if (newDay) {
73449
+ out.push({ kind: 'date', id: 'date-' + day, label: formatDateSeparator(d, this.locale(), labels) });
73450
+ prevDay = day;
72822
73451
  }
73452
+ const prev = msgs[i - 1];
73453
+ const next = msgs[i + 1];
73454
+ const firstInGroup = newDay || !prev || prev.senderId !== m.senderId;
73455
+ const nextDay = next
73456
+ ? `${new Date(next.createdAt).getFullYear()}-${new Date(next.createdAt).getMonth()}-${new Date(next.createdAt).getDate()}`
73457
+ : '';
73458
+ const lastInGroup = !next || next.senderId !== m.senderId || nextDay !== day;
73459
+ out.push({
73460
+ kind: 'msg',
73461
+ id: m.msgId,
73462
+ msg: m,
73463
+ showName: firstInGroup && !m.isMine,
73464
+ showAvatar: lastInGroup && !m.isMine,
73465
+ tail: lastInGroup,
73466
+ });
72823
73467
  }
72824
- if (currentGroup.length > 0) {
72825
- groups.push({ dateLabel: currentLabel, messages: currentGroup });
72826
- }
72827
- return groups;
72828
- });
72829
- this.closedText = computed(() => {
72830
- this.i18n.lang();
72831
- return this.i18n.t('conversationClosed', 'ChatWindow');
72832
- });
72833
- this.loadingText = computed(() => {
72834
- this.i18n.lang();
72835
- return this.i18n.t('loading', 'ChatWindow');
73468
+ return out;
72836
73469
  });
72837
73470
  this.i18n.registerDefaults('ChatWindow', CHAT_WINDOW_I18N);
72838
- }
72839
- ngOnChanges(changes) {
72840
- if (changes['messages']) {
72841
- const current = changes['messages'].currentValue ?? [];
72842
- this.messagesSig.set(current);
72843
- if (current.length !== this.prevMessageCount) {
72844
- this.prevMessageCount = current.length;
72845
- this.shouldScrollToBottom = true;
73471
+ // Auto-scroll al fondo cuando llegan mensajes y el usuario ya estaba abajo
73472
+ // (o el último mensaje es propio). Si está leyendo arriba, marca "nuevos".
73473
+ let prevCount = 0;
73474
+ effect(() => {
73475
+ const rows = this.rows();
73476
+ const count = rows.length;
73477
+ if (count === prevCount)
73478
+ return;
73479
+ const grew = count > prevCount;
73480
+ prevCount = count;
73481
+ const last = this.messages()[this.messages().length - 1];
73482
+ const mine = last?.isMine ?? false;
73483
+ if (this.atBottom() || mine) {
73484
+ setTimeout(() => this.scrollToBottom(false), 0);
72846
73485
  }
72847
- }
72848
- }
72849
- ngAfterViewChecked() {
72850
- if (this.shouldScrollToBottom) {
72851
- this.scrollToBottom();
72852
- this.shouldScrollToBottom = false;
72853
- }
73486
+ else if (grew) {
73487
+ this.hasNew.set(true);
73488
+ }
73489
+ });
72854
73490
  }
72855
- onReactionClick(event) {
72856
- this.reactionClick.emit({ msgId: event.msgId, token: event.token });
73491
+ onScroll() {
73492
+ const el = this.msgsEl()?.nativeElement;
73493
+ if (!el)
73494
+ return;
73495
+ const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
73496
+ this.atBottom.set(nearBottom);
73497
+ if (nearBottom)
73498
+ this.hasNew.set(false);
73499
+ if (el.scrollTop < 40)
73500
+ this.loadMore.emit();
72857
73501
  }
72858
- onEditClick(_msgId) {
72859
- // Implementacion futura: emitir evento de edicion
73502
+ scrollToBottom(smooth) {
73503
+ const el = this.msgsEl()?.nativeElement;
73504
+ if (!el)
73505
+ return;
73506
+ el.scrollTo({ top: el.scrollHeight, behavior: smooth ? 'smooth' : 'auto' });
73507
+ this.atBottom.set(true);
73508
+ this.hasNew.set(false);
72860
73509
  }
72861
- onTyping() {
72862
- // El padre puede suscribirse al output typing si necesita propagar a Firestore
73510
+ onSend(body) {
73511
+ this.sendMessage.emit(body);
73512
+ this.replyingTo.set(null);
72863
73513
  }
72864
- scrollToBottom() {
72865
- const el = this.messagesListRef?.nativeElement;
72866
- if (el) {
72867
- el.scrollTop = el.scrollHeight;
73514
+ onAction(event) {
73515
+ switch (event.type) {
73516
+ case 'reply': {
73517
+ const m = this.messages().find(x => x.msgId === event.msgId);
73518
+ if (m) {
73519
+ this.replyingTo.set({ msgId: m.msgId, senderName: m.senderName, body: m.body });
73520
+ }
73521
+ this.replyTo.emit(event.msgId);
73522
+ break;
73523
+ }
73524
+ case 'edit':
73525
+ this.editMessage.emit(event.msgId);
73526
+ break;
73527
+ case 'delete':
73528
+ this.deleteMessage.emit(event.msgId);
73529
+ break;
73530
+ case 'react':
73531
+ this.reactionClick.emit({ msgId: event.msgId, token: event.token ?? '👍' });
73532
+ break;
72868
73533
  }
72869
73534
  }
72870
- formatDateLabel(date) {
72871
- if (!date)
72872
- return '';
72873
- const d = date instanceof Date ? date : new Date(date);
72874
- const now = new Date();
72875
- const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
72876
- const msgDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
72877
- const diffMs = today.getTime() - msgDay.getTime();
72878
- const diffDays = Math.round(diffMs / 86400000);
72879
- if (diffDays === 0)
72880
- return this.i18n.t('today', '_global') || 'Hoy';
72881
- if (diffDays === 1)
72882
- return this.i18n.t('yesterday', '_global') || 'Ayer';
72883
- return d.toLocaleDateString([], { day: '2-digit', month: 'short', year: 'numeric' });
73535
+ t(key) {
73536
+ return this.i18n.t(key, 'ChatWindow');
72884
73537
  }
72885
73538
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatWindowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
72886
- 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 }], usesOnChanges: true, ngImport: i0, template: `
72887
- <div class="chat-window">
72888
- <div class="messages-list" #messagesList>
72889
- @if (isLoading) {
72890
- <div class="loading-state">
72891
- <ion-spinner name="dots"></ion-spinner>
72892
- <span>{{ loadingText() }}</span>
73539
+ 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: `
73540
+ <div class="chat">
73541
+ <div #msgs class="messages" (scroll)="onScroll()">
73542
+ @if (isLoading()) {
73543
+ <div class="state">
73544
+ <span>{{ t('loading') }}</span>
73545
+ </div>
73546
+ } @else if (rows().length === 0) {
73547
+ <div class="state empty">
73548
+ <ion-icon name="chatbubbles-outline" aria-hidden="true" />
73549
+ <span>{{ t('noMessages') }}</span>
72893
73550
  </div>
72894
73551
  } @else {
72895
- @for (group of messageGroups(); track group.dateLabel) {
72896
- <div class="date-separator">
72897
- <div class="date-line"></div>
72898
- <span class="date-label">{{ group.dateLabel }}</span>
72899
- <div class="date-line"></div>
72900
- </div>
72901
- @for (msg of group.messages; track msg.msgId) {
73552
+ @for (row of rows(); track row.id) {
73553
+ @if (row.kind === 'date') {
73554
+ <div class="date-sep">
73555
+ <span>{{ row.label }}</span>
73556
+ </div>
73557
+ } @else {
72902
73558
  <val-message-bubble
72903
- [props]="msg"
72904
- (reactionClick)="onReactionClick($event)"
72905
- (replyClick)="replyTo.emit($event.msgId)"
72906
- (deleteClick)="deleteMessage.emit($event.msgId)"
72907
- (editClick)="onEditClick($event.msgId)"
73559
+ [msg]="row.msg"
73560
+ [showName]="row.showName"
73561
+ [showAvatar]="row.showAvatar"
73562
+ [tail]="row.tail"
73563
+ [locale]="locale()"
73564
+ (action)="onAction($event)"
72908
73565
  />
72909
73566
  }
72910
73567
  }
72911
73568
  }
73569
+ <val-typing-indicator [typingUsers]="typingUsers()" />
72912
73570
  </div>
72913
73571
 
72914
- <val-typing-indicator [typingUsers]="typingUsers" />
73572
+ @if (!atBottom()) {
73573
+ <button class="scroll-down" (click)="scrollToBottom(true)" aria-label="scroll">
73574
+ <ion-icon name="arrow-down-outline" aria-hidden="true" />
73575
+ @if (hasNew()) {
73576
+ <span class="dot"></span>
73577
+ }
73578
+ </button>
73579
+ }
72915
73580
 
72916
- @if (!isOpen) {
72917
- <div class="closed-banner">{{ closedText() }}</div>
73581
+ @if (isOpen()) {
73582
+ <val-chat-composer
73583
+ [replyingTo]="replyingTo()"
73584
+ (send)="onSend($event)"
73585
+ (typing)="typing.emit()"
73586
+ (attach)="attach.emit($event)"
73587
+ (voice)="voice.emit()"
73588
+ (cancelReply)="replyingTo.set(null)"
73589
+ />
72918
73590
  } @else {
72919
- <val-chat-input [props]="{ disabled: isLoading }" (send)="sendMessage.emit($event)" (typing)="onTyping()" />
73591
+ <div class="closed-banner">{{ t('conversationClosed') }}</div>
72920
73592
  }
72921
73593
  </div>
72922
- `, 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"] }] }); }
73594
+ `, 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"] }] }); }
72923
73595
  }
72924
73596
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatWindowComponent, decorators: [{
72925
73597
  type: Component,
72926
- args: [{ selector: 'val-chat-window', standalone: true, imports: [CommonModule, IonIcon, IonSpinner, MessageBubbleComponent, ChatInputComponent, TypingIndicatorComponent], template: `
72927
- <div class="chat-window">
72928
- <div class="messages-list" #messagesList>
72929
- @if (isLoading) {
72930
- <div class="loading-state">
72931
- <ion-spinner name="dots"></ion-spinner>
72932
- <span>{{ loadingText() }}</span>
73598
+ args: [{ selector: 'val-chat-window', standalone: true, imports: [CommonModule, IonIcon, MessageBubbleComponent, ChatComposerComponent, TypingIndicatorComponent], template: `
73599
+ <div class="chat">
73600
+ <div #msgs class="messages" (scroll)="onScroll()">
73601
+ @if (isLoading()) {
73602
+ <div class="state">
73603
+ <span>{{ t('loading') }}</span>
73604
+ </div>
73605
+ } @else if (rows().length === 0) {
73606
+ <div class="state empty">
73607
+ <ion-icon name="chatbubbles-outline" aria-hidden="true" />
73608
+ <span>{{ t('noMessages') }}</span>
72933
73609
  </div>
72934
73610
  } @else {
72935
- @for (group of messageGroups(); track group.dateLabel) {
72936
- <div class="date-separator">
72937
- <div class="date-line"></div>
72938
- <span class="date-label">{{ group.dateLabel }}</span>
72939
- <div class="date-line"></div>
72940
- </div>
72941
- @for (msg of group.messages; track msg.msgId) {
73611
+ @for (row of rows(); track row.id) {
73612
+ @if (row.kind === 'date') {
73613
+ <div class="date-sep">
73614
+ <span>{{ row.label }}</span>
73615
+ </div>
73616
+ } @else {
72942
73617
  <val-message-bubble
72943
- [props]="msg"
72944
- (reactionClick)="onReactionClick($event)"
72945
- (replyClick)="replyTo.emit($event.msgId)"
72946
- (deleteClick)="deleteMessage.emit($event.msgId)"
72947
- (editClick)="onEditClick($event.msgId)"
73618
+ [msg]="row.msg"
73619
+ [showName]="row.showName"
73620
+ [showAvatar]="row.showAvatar"
73621
+ [tail]="row.tail"
73622
+ [locale]="locale()"
73623
+ (action)="onAction($event)"
72948
73624
  />
72949
73625
  }
72950
73626
  }
72951
73627
  }
73628
+ <val-typing-indicator [typingUsers]="typingUsers()" />
72952
73629
  </div>
72953
73630
 
72954
- <val-typing-indicator [typingUsers]="typingUsers" />
73631
+ @if (!atBottom()) {
73632
+ <button class="scroll-down" (click)="scrollToBottom(true)" aria-label="scroll">
73633
+ <ion-icon name="arrow-down-outline" aria-hidden="true" />
73634
+ @if (hasNew()) {
73635
+ <span class="dot"></span>
73636
+ }
73637
+ </button>
73638
+ }
72955
73639
 
72956
- @if (!isOpen) {
72957
- <div class="closed-banner">{{ closedText() }}</div>
73640
+ @if (isOpen()) {
73641
+ <val-chat-composer
73642
+ [replyingTo]="replyingTo()"
73643
+ (send)="onSend($event)"
73644
+ (typing)="typing.emit()"
73645
+ (attach)="attach.emit($event)"
73646
+ (voice)="voice.emit()"
73647
+ (cancelReply)="replyingTo.set(null)"
73648
+ />
72958
73649
  } @else {
72959
- <val-chat-input [props]="{ disabled: isLoading }" (send)="sendMessage.emit($event)" (typing)="onTyping()" />
73650
+ <div class="closed-banner">{{ t('conversationClosed') }}</div>
72960
73651
  }
72961
73652
  </div>
72962
- `, 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"] }]
72963
- }], ctorParameters: () => [], propDecorators: { convId: [{
72964
- type: Input
72965
- }], messages: [{
72966
- type: Input
72967
- }], participants: [{
72968
- type: Input
72969
- }], currentUserId: [{
72970
- type: Input
72971
- }], isOpen: [{
72972
- type: Input
72973
- }], typingUsers: [{
72974
- type: Input
72975
- }], isLoading: [{
72976
- type: Input
72977
- }], sendMessage: [{
72978
- type: Output
72979
- }], loadMore: [{
72980
- type: Output
72981
- }], reactionClick: [{
72982
- type: Output
72983
- }], deleteMessage: [{
72984
- type: Output
72985
- }], replyTo: [{
72986
- type: Output
72987
- }], messagesListRef: [{
72988
- type: ViewChild,
72989
- args: ['messagesList']
72990
- }] } });
73653
+ `, 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"] }]
73654
+ }], ctorParameters: () => [] });
72991
73655
 
72992
73656
  const THREAD_PANEL_I18N = {
72993
73657
  es: {
@@ -73145,7 +73809,7 @@ class ThreadPanelComponent {
73145
73809
  </div>
73146
73810
  }
73147
73811
  </div>
73148
- `, 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"] }] }); }
73812
+ `, 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"] }] }); }
73149
73813
  }
73150
73814
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ThreadPanelComponent, decorators: [{
73151
73815
  type: Component,
@@ -73199,5 +73863,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
73199
73863
  * Generated bundle index. Do not edit.
73200
73864
  */
73201
73865
 
73202
- 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 };
73866
+ 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 };
73203
73867
  //# sourceMappingURL=valtech-components.mjs.map