valtech-components 4.0.225 → 4.0.226

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.
@@ -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.226';
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.
@@ -72290,241 +72343,308 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72290
72343
  */
72291
72344
 
72292
72345
  addIcons({
72346
+ alertCircle,
72293
72347
  checkmark,
72294
72348
  checkmarkDone,
72349
+ createOutline,
72350
+ documentOutline,
72351
+ happyOutline,
72352
+ arrowUndoOutline,
72295
72353
  timeOutline,
72296
- alertCircle,
72297
- pencil,
72298
- trash,
72299
- returnDownBackOutline,
72354
+ trashOutline,
72300
72355
  });
72301
- const CHAT_I18N = {
72356
+ const MESSAGE_BUBBLE_I18N = {
72302
72357
  es: {
72303
- messageDeleted: '[mensaje eliminado]',
72304
- edited: '(editado)',
72305
- react: 'Reaccionar',
72306
72358
  reply: 'Responder',
72307
72359
  edit: 'Editar',
72308
72360
  delete: 'Eliminar',
72361
+ react: 'Reaccionar',
72362
+ deleted: 'Mensaje eliminado',
72363
+ edited: 'editado',
72309
72364
  },
72310
72365
  en: {
72311
- messageDeleted: '[message deleted]',
72312
- edited: '(edited)',
72313
- react: 'React',
72314
72366
  reply: 'Reply',
72315
72367
  edit: 'Edit',
72316
72368
  delete: 'Delete',
72369
+ react: 'React',
72370
+ deleted: 'Message deleted',
72371
+ edited: 'edited',
72317
72372
  },
72318
72373
  };
72319
72374
  /**
72320
- * val-message-bubble
72375
+ * val-message-bubble — burbuja de mensaje presentacional (signal inputs).
72321
72376
  *
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.
72377
+ * Sin lógica de negocio: el contenedor (`val-chat-window`) decide agrupación y
72378
+ * pasa flags (showAvatar/showName/tail). Las acciones (responder/editar/borrar/
72379
+ * reaccionar) NO están siempre visibles: se revelan en hover (desktop) o al tocar
72380
+ * la burbuja (móvil), y se emiten por un único `(action)` tipado.
72325
72381
  *
72326
- * @example
72327
- * <val-message-bubble
72328
- * [props]="msg"
72329
- * (reactionClick)="onReaction($event)"
72330
- * (replyClick)="onReply($event)"
72331
- * (deleteClick)="onDelete($event)"
72332
- * />
72382
+ * Read receipts estilo WhatsApp: sending → reloj, sent → ✓, delivered/read → ✓✓
72383
+ * (read en color), failed → alerta.
72333
72384
  */
72334
72385
  class MessageBubbleComponent {
72335
72386
  constructor() {
72336
72387
  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 });
72388
+ this.msg = input.required();
72389
+ this.showAvatar = input(false);
72390
+ this.showName = input(false);
72391
+ this.tail = input(true);
72392
+ this.locale = input('es-CL');
72393
+ this.action = output();
72394
+ this.actionsOpen = signal(false);
72395
+ this.time = computed(() => formatClockTime(this.msg().createdAt, this.locale()));
72396
+ this.initials = computed(() => {
72397
+ const name = this.msg().senderName?.trim() ?? '';
72398
+ if (!name)
72399
+ return '?';
72400
+ const parts = name.split(/\s+/);
72401
+ return (parts[0][0] + (parts[1]?.[0] ?? '')).toUpperCase();
72402
+ });
72403
+ this.imageAttachments = computed(() => (this.msg().attachments ?? []).filter(a => a.mimeType?.startsWith('image/')));
72404
+ this.fileAttachments = computed(() => (this.msg().attachments ?? []).filter(a => !a.mimeType?.startsWith('image/')));
72405
+ this.statusIcon = computed(() => {
72406
+ switch (this.msg().status) {
72407
+ case 'sending':
72408
+ return 'time-outline';
72409
+ case 'failed':
72410
+ return 'alert-circle';
72411
+ case 'delivered':
72412
+ case 'read':
72413
+ return 'checkmark-done';
72414
+ case 'sent':
72415
+ default:
72416
+ return 'checkmark';
72417
+ }
72418
+ });
72419
+ this.i18n.registerDefaults('MessageBubble', MESSAGE_BUBBLE_I18N);
72373
72420
  }
72374
- onReplyClick() {
72375
- this.replyClick.emit({ msgId: this.props.msgId });
72421
+ toggleActions() {
72422
+ if (this.msg().isDeleted)
72423
+ return;
72424
+ this.actionsOpen.update(v => !v);
72376
72425
  }
72377
- onDeleteClick() {
72378
- this.deleteClick.emit({ msgId: this.props.msgId });
72426
+ emit(type, token) {
72427
+ this.actionsOpen.set(false);
72428
+ this.action.emit({ type, msgId: this.msg().msgId, token });
72379
72429
  }
72380
- onEditClick() {
72381
- this.editClick.emit({ msgId: this.props.msgId });
72430
+ t(key) {
72431
+ return this.i18n.t(key, 'MessageBubble');
72382
72432
  }
72383
72433
  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>
72434
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: MessageBubbleComponent, isStandalone: true, selector: "val-message-bubble", inputs: { msg: { classPropertyName: "msg", publicName: "msg", isSignal: true, isRequired: true, transformFunction: null }, showAvatar: { classPropertyName: "showAvatar", publicName: "showAvatar", isSignal: true, isRequired: false, transformFunction: null }, showName: { classPropertyName: "showName", publicName: "showName", isSignal: true, isRequired: false, transformFunction: null }, tail: { classPropertyName: "tail", publicName: "tail", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { action: "action" }, ngImport: i0, template: `
72435
+ <div class="row" [class.mine]="msg().isMine" [class.tail]="tail()" [class.with-avatar]="showAvatar()">
72436
+ @if (showAvatar() && !msg().isMine) {
72437
+ <div class="avatar" [class.hidden]="!tail()">
72438
+ @if (msg().senderAvatar) {
72439
+ <img [src]="msg().senderAvatar" [alt]="msg().senderName" />
72440
+ } @else {
72441
+ <span>{{ initials() }}</span>
72442
+ }
72443
+ </div>
72388
72444
  }
72389
72445
 
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>
72446
+ <div class="bubble-wrap">
72447
+ <div
72448
+ class="bubble"
72449
+ [class.deleted]="msg().isDeleted"
72450
+ (click)="toggleActions()"
72451
+ (keyup.enter)="toggleActions()"
72452
+ tabindex="0"
72453
+ >
72454
+ @if (showName() && !msg().isMine && !msg().isDeleted) {
72455
+ <span class="name">{{ msg().senderName }}</span>
72415
72456
  }
72416
- </div>
72417
- </div>
72418
72457
 
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>
72458
+ @if (msg().replyTo && !msg().isDeleted) {
72459
+ <div class="reply-preview">
72460
+ <span class="reply-name">{{ msg().replyTo!.senderName }}</span>
72461
+ <span class="reply-body">{{ msg().replyTo!.body }}</span>
72462
+ </div>
72428
72463
  }
72429
- </div>
72430
- }
72431
72464
 
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>
72465
+ @if (msg().isDeleted) {
72466
+ <span class="body deleted-text">{{ t('deleted') }}</span>
72467
+ } @else {
72468
+ @if (imageAttachments().length > 0) {
72469
+ <div class="attachments">
72470
+ @for (att of imageAttachments(); track att.url) {
72471
+ <img class="att-image" [src]="att.url" [alt]="att.name" />
72472
+ }
72473
+ </div>
72474
+ }
72475
+ @if (fileAttachments().length > 0) {
72476
+ <div class="files">
72477
+ @for (att of fileAttachments(); track att.url) {
72478
+ <a class="file-chip" [href]="att.url" target="_blank" rel="noopener">
72479
+ <ion-icon name="document-outline" aria-hidden="true" />
72480
+ <span>{{ att.name }}</span>
72481
+ </a>
72482
+ }
72483
+ </div>
72484
+ }
72485
+ @if (msg().body) {
72486
+ <span class="body">{{ msg().body }}</span>
72487
+ }
72444
72488
  }
72489
+
72490
+ <span class="meta">
72491
+ @if (msg().isEdited && !msg().isDeleted) {
72492
+ <span class="edited">{{ t('edited') }}</span>
72493
+ }
72494
+ <span class="time">{{ time() }}</span>
72495
+ @if (msg().isMine && !msg().isDeleted) {
72496
+ <ion-icon
72497
+ class="status"
72498
+ [class.read]="msg().status === 'read'"
72499
+ [class.failed]="msg().status === 'failed'"
72500
+ [name]="statusIcon()"
72501
+ aria-hidden="true"
72502
+ />
72503
+ }
72504
+ </span>
72445
72505
  </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
72506
 
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>
72507
+ @if (msg().reactions && msg().reactions!.length > 0) {
72508
+ <div class="reactions">
72509
+ @for (r of msg().reactions!; track r.token) {
72510
+ <button class="reaction" [class.active]="r.active" (click)="emit('react', r.token)">
72511
+ <span>{{ r.token }}</span>
72512
+ <span class="count">{{ r.count }}</span>
72513
+ </button>
72514
+ }
72463
72515
  </div>
72464
72516
  }
72465
72517
 
72466
- @if (props.isDeleted) {
72467
- <span class="deleted-text">{{ deletedText() }}</span>
72468
- } @else {
72469
- <div class="bubble-body">{{ props.body }}</div>
72518
+ @if (!msg().isDeleted) {
72519
+ <div class="actions" [class.open]="actionsOpen()">
72520
+ <button class="act" [attr.aria-label]="t('react')" (click)="emit('react', '👍')">
72521
+ <ion-icon name="happy-outline" aria-hidden="true" />
72522
+ </button>
72523
+ <button class="act" [attr.aria-label]="t('reply')" (click)="emit('reply')">
72524
+ <ion-icon name="arrow-undo-outline" aria-hidden="true" />
72525
+ </button>
72526
+ @if (msg().isMine) {
72527
+ <button class="act" [attr.aria-label]="t('edit')" (click)="emit('edit')">
72528
+ <ion-icon name="create-outline" aria-hidden="true" />
72529
+ </button>
72530
+ <button class="act danger" [attr.aria-label]="t('delete')" (click)="emit('delete')">
72531
+ <ion-icon name="trash-outline" aria-hidden="true" />
72532
+ </button>
72533
+ }
72534
+ </div>
72470
72535
  }
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
72536
  </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>
72537
+ </div>
72538
+ `, isInline: true, styles: [":host{display:block}.row{display:flex;align-items:flex-end;gap:8px;margin:1px 0}.row.mine{flex-direction:row-reverse}.row.tail{margin-bottom:8px}.avatar{width:28px;height:28px;border-radius:50%;overflow:hidden;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:var(--ion-color-primary);color:#fff;font-size:.6875rem;font-weight:700}.avatar.hidden{visibility:hidden}.avatar img{width:100%;height:100%;object-fit:cover}.bubble-wrap{position:relative;max-width:min(78%,520px);display:flex;flex-direction:column}.row.mine .bubble-wrap{align-items:flex-end}.bubble{position:relative;padding:7px 11px;border-radius:16px;background:var(--ion-card-background, var(--ion-background-color, #fff));border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));color:var(--ion-text-color, #000);font-size:.9375rem;line-height:1.35;cursor:pointer;outline:none;word-break:break-word}.row:not(.mine).tail .bubble{border-bottom-left-radius:5px}.mine .bubble{background:var(--ion-color-primary);border-color:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff)}.mine.tail .bubble{border-bottom-right-radius:5px}.bubble.deleted{background:transparent;border-style:dashed;cursor:default}.name{display:block;font-size:.75rem;font-weight:700;color:var(--ion-color-primary);margin-bottom:2px}.reply-preview{display:flex;flex-direction:column;gap:1px;padding:4px 8px;margin-bottom:4px;border-left:3px solid currentColor;border-radius:6px;background:var(--ion-color-step-100, rgba(127, 127, 127, .12));font-size:.8125rem;opacity:.85}.reply-name{font-weight:700}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}.body{white-space:pre-wrap}.deleted-text{font-style:italic;opacity:.6}.attachments{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.att-image{max-width:100%;border-radius:10px;display:block}.files{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.file-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .12));color:inherit;text-decoration:none;font-size:.8125rem}.meta{display:inline-flex;align-items:center;gap:4px;float:right;margin:2px 0 -2px 8px;font-size:.6875rem;opacity:.7}.edited{font-style:italic}.status{font-size:.875rem}.status.read{color:var(--ion-color-secondary, #4fc3f7);opacity:1}.status.failed{color:var(--ion-color-danger, #eb445a);opacity:1}.reactions{display:flex;flex-wrap:wrap;gap:4px;margin-top:3px}.reaction{display:inline-flex;align-items:center;gap:3px;padding:1px 7px;border-radius:999px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff));font-size:.75rem;cursor:pointer;color:var(--ion-text-color, #000)}.reaction.active{border-color:var(--ion-color-primary);background:var(--ion-color-primary-tint, var(--ion-color-primary));color:var(--ion-color-primary-contrast, #fff)}.reaction .count{opacity:.75}.actions{position:absolute;top:-14px;display:flex;gap:2px;padding:2px;border-radius:999px;background:var(--ion-card-background, var(--ion-background-color, #fff));border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));box-shadow:0 2px 8px #0000001f;opacity:0;pointer-events:none;transition:opacity .12s ease;z-index:2}.row.mine .actions{right:0}.row:not(.mine) .actions{left:0}.bubble-wrap:hover .actions,.actions.open{opacity:1;pointer-events:auto}.act{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1rem}.act:hover{background:var(--ion-color-step-100, rgba(127, 127, 127, .12));color:var(--ion-text-color, #000)}.act.danger:hover{color:var(--ion-color-danger, #eb445a)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }] }); }
72539
+ }
72540
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageBubbleComponent, decorators: [{
72541
+ type: Component,
72542
+ args: [{ selector: 'val-message-bubble', standalone: true, imports: [CommonModule, IonIcon], template: `
72543
+ <div class="row" [class.mine]="msg().isMine" [class.tail]="tail()" [class.with-avatar]="showAvatar()">
72544
+ @if (showAvatar() && !msg().isMine) {
72545
+ <div class="avatar" [class.hidden]="!tail()">
72546
+ @if (msg().senderAvatar) {
72547
+ <img [src]="msg().senderAvatar" [alt]="msg().senderName" />
72548
+ } @else {
72549
+ <span>{{ initials() }}</span>
72496
72550
  }
72497
72551
  </div>
72498
72552
  }
72499
72553
 
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>
72554
+ <div class="bubble-wrap">
72555
+ <div
72556
+ class="bubble"
72557
+ [class.deleted]="msg().isDeleted"
72558
+ (click)="toggleActions()"
72559
+ (keyup.enter)="toggleActions()"
72560
+ tabindex="0"
72561
+ >
72562
+ @if (showName() && !msg().isMine && !msg().isDeleted) {
72563
+ <span class="name">{{ msg().senderName }}</span>
72564
+ }
72565
+
72566
+ @if (msg().replyTo && !msg().isDeleted) {
72567
+ <div class="reply-preview">
72568
+ <span class="reply-name">{{ msg().replyTo!.senderName }}</span>
72569
+ <span class="reply-body">{{ msg().replyTo!.body }}</span>
72570
+ </div>
72512
72571
  }
72572
+
72573
+ @if (msg().isDeleted) {
72574
+ <span class="body deleted-text">{{ t('deleted') }}</span>
72575
+ } @else {
72576
+ @if (imageAttachments().length > 0) {
72577
+ <div class="attachments">
72578
+ @for (att of imageAttachments(); track att.url) {
72579
+ <img class="att-image" [src]="att.url" [alt]="att.name" />
72580
+ }
72581
+ </div>
72582
+ }
72583
+ @if (fileAttachments().length > 0) {
72584
+ <div class="files">
72585
+ @for (att of fileAttachments(); track att.url) {
72586
+ <a class="file-chip" [href]="att.url" target="_blank" rel="noopener">
72587
+ <ion-icon name="document-outline" aria-hidden="true" />
72588
+ <span>{{ att.name }}</span>
72589
+ </a>
72590
+ }
72591
+ </div>
72592
+ }
72593
+ @if (msg().body) {
72594
+ <span class="body">{{ msg().body }}</span>
72595
+ }
72596
+ }
72597
+
72598
+ <span class="meta">
72599
+ @if (msg().isEdited && !msg().isDeleted) {
72600
+ <span class="edited">{{ t('edited') }}</span>
72601
+ }
72602
+ <span class="time">{{ time() }}</span>
72603
+ @if (msg().isMine && !msg().isDeleted) {
72604
+ <ion-icon
72605
+ class="status"
72606
+ [class.read]="msg().status === 'read'"
72607
+ [class.failed]="msg().status === 'failed'"
72608
+ [name]="statusIcon()"
72609
+ aria-hidden="true"
72610
+ />
72611
+ }
72612
+ </span>
72513
72613
  </div>
72514
- }
72614
+
72615
+ @if (msg().reactions && msg().reactions!.length > 0) {
72616
+ <div class="reactions">
72617
+ @for (r of msg().reactions!; track r.token) {
72618
+ <button class="reaction" [class.active]="r.active" (click)="emit('react', r.token)">
72619
+ <span>{{ r.token }}</span>
72620
+ <span class="count">{{ r.count }}</span>
72621
+ </button>
72622
+ }
72623
+ </div>
72624
+ }
72625
+
72626
+ @if (!msg().isDeleted) {
72627
+ <div class="actions" [class.open]="actionsOpen()">
72628
+ <button class="act" [attr.aria-label]="t('react')" (click)="emit('react', '👍')">
72629
+ <ion-icon name="happy-outline" aria-hidden="true" />
72630
+ </button>
72631
+ <button class="act" [attr.aria-label]="t('reply')" (click)="emit('reply')">
72632
+ <ion-icon name="arrow-undo-outline" aria-hidden="true" />
72633
+ </button>
72634
+ @if (msg().isMine) {
72635
+ <button class="act" [attr.aria-label]="t('edit')" (click)="emit('edit')">
72636
+ <ion-icon name="create-outline" aria-hidden="true" />
72637
+ </button>
72638
+ <button class="act danger" [attr.aria-label]="t('delete')" (click)="emit('delete')">
72639
+ <ion-icon name="trash-outline" aria-hidden="true" />
72640
+ </button>
72641
+ }
72642
+ </div>
72643
+ }
72644
+ </div>
72515
72645
  </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
- }] } });
72646
+ `, styles: [":host{display:block}.row{display:flex;align-items:flex-end;gap:8px;margin:1px 0}.row.mine{flex-direction:row-reverse}.row.tail{margin-bottom:8px}.avatar{width:28px;height:28px;border-radius:50%;overflow:hidden;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:var(--ion-color-primary);color:#fff;font-size:.6875rem;font-weight:700}.avatar.hidden{visibility:hidden}.avatar img{width:100%;height:100%;object-fit:cover}.bubble-wrap{position:relative;max-width:min(78%,520px);display:flex;flex-direction:column}.row.mine .bubble-wrap{align-items:flex-end}.bubble{position:relative;padding:7px 11px;border-radius:16px;background:var(--ion-card-background, var(--ion-background-color, #fff));border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));color:var(--ion-text-color, #000);font-size:.9375rem;line-height:1.35;cursor:pointer;outline:none;word-break:break-word}.row:not(.mine).tail .bubble{border-bottom-left-radius:5px}.mine .bubble{background:var(--ion-color-primary);border-color:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff)}.mine.tail .bubble{border-bottom-right-radius:5px}.bubble.deleted{background:transparent;border-style:dashed;cursor:default}.name{display:block;font-size:.75rem;font-weight:700;color:var(--ion-color-primary);margin-bottom:2px}.reply-preview{display:flex;flex-direction:column;gap:1px;padding:4px 8px;margin-bottom:4px;border-left:3px solid currentColor;border-radius:6px;background:var(--ion-color-step-100, rgba(127, 127, 127, .12));font-size:.8125rem;opacity:.85}.reply-name{font-weight:700}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}.body{white-space:pre-wrap}.deleted-text{font-style:italic;opacity:.6}.attachments{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.att-image{max-width:100%;border-radius:10px;display:block}.files{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.file-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .12));color:inherit;text-decoration:none;font-size:.8125rem}.meta{display:inline-flex;align-items:center;gap:4px;float:right;margin:2px 0 -2px 8px;font-size:.6875rem;opacity:.7}.edited{font-style:italic}.status{font-size:.875rem}.status.read{color:var(--ion-color-secondary, #4fc3f7);opacity:1}.status.failed{color:var(--ion-color-danger, #eb445a);opacity:1}.reactions{display:flex;flex-wrap:wrap;gap:4px;margin-top:3px}.reaction{display:inline-flex;align-items:center;gap:3px;padding:1px 7px;border-radius:999px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff));font-size:.75rem;cursor:pointer;color:var(--ion-text-color, #000)}.reaction.active{border-color:var(--ion-color-primary);background:var(--ion-color-primary-tint, var(--ion-color-primary));color:var(--ion-color-primary-contrast, #fff)}.reaction .count{opacity:.75}.actions{position:absolute;top:-14px;display:flex;gap:2px;padding:2px;border-radius:999px;background:var(--ion-card-background, var(--ion-background-color, #fff));border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));box-shadow:0 2px 8px #0000001f;opacity:0;pointer-events:none;transition:opacity .12s ease;z-index:2}.row.mine .actions{right:0}.row:not(.mine) .actions{left:0}.bubble-wrap:hover .actions,.actions.open{opacity:1;pointer-events:auto}.act{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1rem}.act:hover{background:var(--ion-color-step-100, rgba(127, 127, 127, .12));color:var(--ion-text-color, #000)}.act.danger:hover{color:var(--ion-color-danger, #eb445a)}\n"] }]
72647
+ }], ctorParameters: () => [] });
72528
72648
 
72529
72649
  addIcons({ sendOutline });
72530
72650
  const CHAT_INPUT_I18N = {
@@ -72676,6 +72796,340 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72676
72796
  type: Output
72677
72797
  }] } });
72678
72798
 
72799
+ addIcons({ addOutline, closeOutline, happyOutline, micOutline, arrowUpOutline });
72800
+ const CHAT_COMPOSER_I18N = {
72801
+ es: {
72802
+ placeholder: 'Escribe un mensaje',
72803
+ send: 'Enviar',
72804
+ attach: 'Adjuntar',
72805
+ emoji: 'Emoji',
72806
+ voice: 'Mensaje de voz',
72807
+ replyingTo: 'Respondiendo a',
72808
+ cancel: 'Cancelar',
72809
+ },
72810
+ en: {
72811
+ placeholder: 'Write a message',
72812
+ send: 'Send',
72813
+ attach: 'Attach',
72814
+ emoji: 'Emoji',
72815
+ voice: 'Voice message',
72816
+ replyingTo: 'Replying to',
72817
+ cancel: 'Cancel',
72818
+ },
72819
+ };
72820
+ const QUICK_EMOJIS = ['👍', '❤️', '😂', '🎉', '🙏', '🔥', '😮', '😢'];
72821
+ /**
72822
+ * val-chat-composer — barra de redacción de chat moderna (signal inputs).
72823
+ *
72824
+ * Pill con: adjuntar (+), textarea auto-grow, emoji (fila rápida) y mic/enviar
72825
+ * (mic cuando está vacío, enviar cuando hay texto — patrón WhatsApp/Telegram).
72826
+ * Muestra una barra de contexto "respondiendo a" cuando `replyingTo` está seteado.
72827
+ * `typing` se emite con debounce (máx. 1 vez cada 2s) para no spamear writes.
72828
+ */
72829
+ class ChatComposerComponent {
72830
+ constructor() {
72831
+ this.i18n = inject(I18nService);
72832
+ this.placeholder = input('');
72833
+ this.disabled = input(false);
72834
+ this.maxLength = input(2000);
72835
+ this.replyingTo = input(null);
72836
+ this.showAttach = input(true);
72837
+ this.showEmoji = input(true);
72838
+ this.showMic = input(true);
72839
+ this.send = output();
72840
+ this.typing = output();
72841
+ this.attach = output();
72842
+ this.voice = output();
72843
+ this.cancelReply = output();
72844
+ this.body = signal('');
72845
+ this.emojiOpen = signal(false);
72846
+ this.quickEmojis = QUICK_EMOJIS;
72847
+ this.lastTypingEmit = 0;
72848
+ this.canSend = computed(() => {
72849
+ const t = this.body().trim();
72850
+ return t.length > 0 && t.length <= this.maxLength() && !this.disabled();
72851
+ });
72852
+ this.placeholderText = computed(() => {
72853
+ this.i18n.lang();
72854
+ return this.placeholder() || this.i18n.t('placeholder', 'ChatComposer');
72855
+ });
72856
+ this.i18n.registerDefaults('ChatComposer', CHAT_COMPOSER_I18N);
72857
+ }
72858
+ onInput(event) {
72859
+ const target = event.target;
72860
+ this.body.set(target.value ?? '');
72861
+ this.emitTypingDebounced();
72862
+ }
72863
+ onKeydown(event) {
72864
+ if (event.key === 'Enter' && !event.shiftKey) {
72865
+ event.preventDefault();
72866
+ this.onSend();
72867
+ }
72868
+ }
72869
+ onSend() {
72870
+ const trimmed = this.body().trim();
72871
+ if (!trimmed || trimmed.length > this.maxLength())
72872
+ return;
72873
+ this.send.emit(trimmed);
72874
+ this.body.set('');
72875
+ this.emojiOpen.set(false);
72876
+ }
72877
+ toggleEmoji() {
72878
+ this.emojiOpen.update(v => !v);
72879
+ }
72880
+ insertEmoji(emoji) {
72881
+ this.body.update(v => v + emoji);
72882
+ }
72883
+ onFile(event) {
72884
+ const input = event.target;
72885
+ const file = input.files?.[0];
72886
+ if (file)
72887
+ this.attach.emit(file);
72888
+ input.value = '';
72889
+ }
72890
+ /**
72891
+ * Emite typing como máximo 1 vez cada 2s. No usa Date.now() directo en plantillas
72892
+ * de workflow; aquí estamos en runtime de navegador, performance.now es seguro.
72893
+ */
72894
+ emitTypingDebounced() {
72895
+ const now = performance.now();
72896
+ if (now - this.lastTypingEmit > 2000) {
72897
+ this.lastTypingEmit = now;
72898
+ this.typing.emit();
72899
+ }
72900
+ }
72901
+ t(key) {
72902
+ return this.i18n.t(key, 'ChatComposer');
72903
+ }
72904
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatComposerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
72905
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ChatComposerComponent, isStandalone: true, selector: "val-chat-composer", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, replyingTo: { classPropertyName: "replyingTo", publicName: "replyingTo", isSignal: true, isRequired: false, transformFunction: null }, showAttach: { classPropertyName: "showAttach", publicName: "showAttach", isSignal: true, isRequired: false, transformFunction: null }, showEmoji: { classPropertyName: "showEmoji", publicName: "showEmoji", isSignal: true, isRequired: false, transformFunction: null }, showMic: { classPropertyName: "showMic", publicName: "showMic", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { send: "send", typing: "typing", attach: "attach", voice: "voice", cancelReply: "cancelReply" }, ngImport: i0, template: `
72906
+ <div class="composer">
72907
+ @if (replyingTo()) {
72908
+ <div class="reply-bar">
72909
+ <div class="reply-content">
72910
+ <span class="reply-name">{{ replyingTo()!.senderName }}</span>
72911
+ <span class="reply-body">{{ replyingTo()!.body }}</span>
72912
+ </div>
72913
+ <button class="icon-btn" [attr.aria-label]="t('cancel')" (click)="cancelReply.emit()">
72914
+ <ion-icon name="close-outline" aria-hidden="true" />
72915
+ </button>
72916
+ </div>
72917
+ }
72918
+
72919
+ @if (emojiOpen()) {
72920
+ <div class="emoji-row">
72921
+ @for (e of quickEmojis; track e) {
72922
+ <button class="emoji" (click)="insertEmoji(e)">{{ e }}</button>
72923
+ }
72924
+ </div>
72925
+ }
72926
+
72927
+ <div class="pill">
72928
+ @if (showAttach()) {
72929
+ <label class="icon-btn" [attr.aria-label]="t('attach')">
72930
+ <ion-icon name="add-outline" aria-hidden="true" />
72931
+ <input type="file" hidden (change)="onFile($event)" accept="image/*,application/pdf" />
72932
+ </label>
72933
+ }
72934
+
72935
+ <ion-textarea
72936
+ class="field"
72937
+ [placeholder]="placeholderText()"
72938
+ [disabled]="disabled()"
72939
+ [maxlength]="maxLength()"
72940
+ [autoGrow]="true"
72941
+ [rows]="1"
72942
+ [value]="body()"
72943
+ (ionInput)="onInput($event)"
72944
+ (keydown)="onKeydown($event)"
72945
+ ></ion-textarea>
72946
+
72947
+ @if (showEmoji()) {
72948
+ <button class="icon-btn" [class.active]="emojiOpen()" [attr.aria-label]="t('emoji')" (click)="toggleEmoji()">
72949
+ <ion-icon name="happy-outline" aria-hidden="true" />
72950
+ </button>
72951
+ }
72952
+
72953
+ @if (canSend()) {
72954
+ <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
72955
+ <ion-icon name="arrow-up-outline" aria-hidden="true" />
72956
+ </button>
72957
+ } @else if (showMic()) {
72958
+ <button class="icon-btn mic" [attr.aria-label]="t('voice')" (click)="voice.emit()">
72959
+ <ion-icon name="mic-outline" aria-hidden="true" />
72960
+ </button>
72961
+ }
72962
+ </div>
72963
+ </div>
72964
+ `, isInline: true, styles: [":host{display:block}.composer{display:flex;flex-direction:column;gap:6px;padding:8px 10px calc(8px + env(safe-area-inset-bottom,0px));background:var(--ion-background-color, #fff);border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}.reply-bar{display:flex;align-items:center;gap:8px;padding:6px 10px;border-left:3px solid var(--ion-color-primary);border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.reply-content{display:flex;flex-direction:column;flex:1;min-width:0;font-size:.8125rem}.reply-name{font-weight:700;color:var(--ion-color-primary)}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ion-color-medium, #92949c)}.emoji-row{display:flex;gap:4px;flex-wrap:wrap}.emoji{border:none;background:var(--ion-color-step-100, rgba(127, 127, 127, .1));border-radius:10px;padding:4px 8px;font-size:1.25rem;cursor:pointer}.pill{display:flex;align-items:flex-end;gap:4px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.field{flex:1;--background: transparent;--padding-start: 6px;--padding-end: 6px;--padding-top: 8px;--padding-bottom: 8px;margin:0;max-height:120px;font-size:.9375rem}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1.375rem}.icon-btn:hover,.icon-btn.active,.icon-btn.mic:hover{color:var(--ion-color-primary)}.send-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff);cursor:pointer;font-size:1.25rem;transition:transform .1s ease}.send-btn:active{transform:scale(.92)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }] }); }
72965
+ }
72966
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatComposerComponent, decorators: [{
72967
+ type: Component,
72968
+ args: [{ selector: 'val-chat-composer', standalone: true, imports: [CommonModule, IonTextarea, IonIcon], template: `
72969
+ <div class="composer">
72970
+ @if (replyingTo()) {
72971
+ <div class="reply-bar">
72972
+ <div class="reply-content">
72973
+ <span class="reply-name">{{ replyingTo()!.senderName }}</span>
72974
+ <span class="reply-body">{{ replyingTo()!.body }}</span>
72975
+ </div>
72976
+ <button class="icon-btn" [attr.aria-label]="t('cancel')" (click)="cancelReply.emit()">
72977
+ <ion-icon name="close-outline" aria-hidden="true" />
72978
+ </button>
72979
+ </div>
72980
+ }
72981
+
72982
+ @if (emojiOpen()) {
72983
+ <div class="emoji-row">
72984
+ @for (e of quickEmojis; track e) {
72985
+ <button class="emoji" (click)="insertEmoji(e)">{{ e }}</button>
72986
+ }
72987
+ </div>
72988
+ }
72989
+
72990
+ <div class="pill">
72991
+ @if (showAttach()) {
72992
+ <label class="icon-btn" [attr.aria-label]="t('attach')">
72993
+ <ion-icon name="add-outline" aria-hidden="true" />
72994
+ <input type="file" hidden (change)="onFile($event)" accept="image/*,application/pdf" />
72995
+ </label>
72996
+ }
72997
+
72998
+ <ion-textarea
72999
+ class="field"
73000
+ [placeholder]="placeholderText()"
73001
+ [disabled]="disabled()"
73002
+ [maxlength]="maxLength()"
73003
+ [autoGrow]="true"
73004
+ [rows]="1"
73005
+ [value]="body()"
73006
+ (ionInput)="onInput($event)"
73007
+ (keydown)="onKeydown($event)"
73008
+ ></ion-textarea>
73009
+
73010
+ @if (showEmoji()) {
73011
+ <button class="icon-btn" [class.active]="emojiOpen()" [attr.aria-label]="t('emoji')" (click)="toggleEmoji()">
73012
+ <ion-icon name="happy-outline" aria-hidden="true" />
73013
+ </button>
73014
+ }
73015
+
73016
+ @if (canSend()) {
73017
+ <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73018
+ <ion-icon name="arrow-up-outline" aria-hidden="true" />
73019
+ </button>
73020
+ } @else if (showMic()) {
73021
+ <button class="icon-btn mic" [attr.aria-label]="t('voice')" (click)="voice.emit()">
73022
+ <ion-icon name="mic-outline" aria-hidden="true" />
73023
+ </button>
73024
+ }
73025
+ </div>
73026
+ </div>
73027
+ `, styles: [":host{display:block}.composer{display:flex;flex-direction:column;gap:6px;padding:8px 10px calc(8px + env(safe-area-inset-bottom,0px));background:var(--ion-background-color, #fff);border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}.reply-bar{display:flex;align-items:center;gap:8px;padding:6px 10px;border-left:3px solid var(--ion-color-primary);border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.reply-content{display:flex;flex-direction:column;flex:1;min-width:0;font-size:.8125rem}.reply-name{font-weight:700;color:var(--ion-color-primary)}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ion-color-medium, #92949c)}.emoji-row{display:flex;gap:4px;flex-wrap:wrap}.emoji{border:none;background:var(--ion-color-step-100, rgba(127, 127, 127, .1));border-radius:10px;padding:4px 8px;font-size:1.25rem;cursor:pointer}.pill{display:flex;align-items:flex-end;gap:4px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.field{flex:1;--background: transparent;--padding-start: 6px;--padding-end: 6px;--padding-top: 8px;--padding-bottom: 8px;margin:0;max-height:120px;font-size:.9375rem}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1.375rem}.icon-btn:hover,.icon-btn.active,.icon-btn.mic:hover{color:var(--ion-color-primary)}.send-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff);cursor:pointer;font-size:1.25rem;transition:transform .1s ease}.send-btn:active{transform:scale(.92)}\n"] }]
73028
+ }], ctorParameters: () => [] });
73029
+
73030
+ /**
73031
+ * val-conversation-list-item — fila de lista de conversaciones estilo
73032
+ * Telegram/WhatsApp: avatar (imagen o iniciales), nombre + hora relativa,
73033
+ * preview del último mensaje + badge de no-leídos. Signal inputs, dark-safe.
73034
+ *
73035
+ * @example
73036
+ * <val-conversation-list-item
73037
+ * [name]="conv.name"
73038
+ * [lastMessage]="conv.lastMsgPreview"
73039
+ * [timestamp]="conv.lastMsgAt"
73040
+ * [unreadCount]="conv.unread"
73041
+ * (select)="open(conv.convId)" />
73042
+ */
73043
+ class ConversationListItemComponent {
73044
+ constructor() {
73045
+ this.i18n = inject(I18nService);
73046
+ this.name = input('');
73047
+ this.avatarUrl = input(undefined);
73048
+ this.lastMessage = input('');
73049
+ this.timestamp = input(undefined);
73050
+ this.unreadCount = input(0);
73051
+ this.locale = input('es-CL');
73052
+ this.select = output();
73053
+ this.initials = computed(() => {
73054
+ const name = this.name().trim();
73055
+ if (!name)
73056
+ return '?';
73057
+ const parts = name.split(/\s+/);
73058
+ return (parts[0][0] + (parts[1]?.[0] ?? '')).toUpperCase();
73059
+ });
73060
+ this.timeLabel = computed(() => formatRelativeTime(this.timestamp(), this.locale(), {
73061
+ today: '',
73062
+ yesterday: this.i18n.t('yesterday', '_global'),
73063
+ }));
73064
+ this.i18n.registerDefaults('ConversationListItem', {
73065
+ es: { empty: 'Sin mensajes' },
73066
+ en: { empty: 'No messages' },
73067
+ });
73068
+ }
73069
+ emptyText() {
73070
+ this.i18n.lang();
73071
+ return this.i18n.t('empty', 'ConversationListItem');
73072
+ }
73073
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ConversationListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
73074
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ConversationListItemComponent, isStandalone: true, selector: "val-conversation-list-item", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, avatarUrl: { classPropertyName: "avatarUrl", publicName: "avatarUrl", isSignal: true, isRequired: false, transformFunction: null }, lastMessage: { classPropertyName: "lastMessage", publicName: "lastMessage", isSignal: true, isRequired: false, transformFunction: null }, timestamp: { classPropertyName: "timestamp", publicName: "timestamp", isSignal: true, isRequired: false, transformFunction: null }, unreadCount: { classPropertyName: "unreadCount", publicName: "unreadCount", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { select: "select" }, ngImport: i0, template: `
73075
+ <button class="item" (click)="select.emit()">
73076
+ <div class="avatar">
73077
+ @if (avatarUrl()) {
73078
+ <img [src]="avatarUrl()" [alt]="name()" />
73079
+ } @else {
73080
+ <span>{{ initials() }}</span>
73081
+ }
73082
+ </div>
73083
+ <div class="content">
73084
+ <div class="top">
73085
+ <span class="name">{{ name() }}</span>
73086
+ @if (timeLabel()) {
73087
+ <span class="time" [class.unread]="unreadCount() > 0">{{ timeLabel() }}</span>
73088
+ }
73089
+ </div>
73090
+ <div class="bottom">
73091
+ <span class="preview" [class.muted]="!lastMessage()">
73092
+ {{ lastMessage() || emptyText() }}
73093
+ </span>
73094
+ @if (unreadCount() > 0) {
73095
+ <span class="badge">{{ unreadCount() > 99 ? '99+' : unreadCount() }}</span>
73096
+ }
73097
+ </div>
73098
+ </div>
73099
+ </button>
73100
+ `, isInline: true, styles: [":host{display:block}.item{display:flex;align-items:center;gap:12px;width:100%;padding:10px 12px;border:none;background:transparent;cursor:pointer;text-align:left;border-radius:12px;transition:background .12s ease}.item:hover{background:var(--ion-color-step-50, rgba(127, 127, 127, .06))}.avatar{width:48px;height:48px;flex-shrink:0;border-radius:50%;overflow:hidden;display:flex;align-items:center;justify-content:center;background:var(--ion-color-primary);color:#fff;font-weight:700;font-size:1rem}.avatar img{width:100%;height:100%;object-fit:cover}.content{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.top,.bottom{display:flex;align-items:center;gap:8px}.name{flex:1;min-width:0;font-weight:600;font-size:.9375rem;color:var(--ion-text-color, #000);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.time{flex-shrink:0;font-size:.75rem;color:var(--ion-color-medium, #92949c)}.time.unread{color:var(--ion-color-primary);font-weight:600}.preview{flex:1;min-width:0;font-size:.875rem;color:var(--ion-color-medium, #92949c);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.preview.muted{font-style:italic;opacity:.7}.badge{flex-shrink:0;min-width:20px;height:20px;padding:0 6px;border-radius:10px;background:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff);font-size:.6875rem;font-weight:700;display:inline-flex;align-items:center;justify-content:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] }); }
73101
+ }
73102
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ConversationListItemComponent, decorators: [{
73103
+ type: Component,
73104
+ args: [{ selector: 'val-conversation-list-item', standalone: true, imports: [CommonModule], template: `
73105
+ <button class="item" (click)="select.emit()">
73106
+ <div class="avatar">
73107
+ @if (avatarUrl()) {
73108
+ <img [src]="avatarUrl()" [alt]="name()" />
73109
+ } @else {
73110
+ <span>{{ initials() }}</span>
73111
+ }
73112
+ </div>
73113
+ <div class="content">
73114
+ <div class="top">
73115
+ <span class="name">{{ name() }}</span>
73116
+ @if (timeLabel()) {
73117
+ <span class="time" [class.unread]="unreadCount() > 0">{{ timeLabel() }}</span>
73118
+ }
73119
+ </div>
73120
+ <div class="bottom">
73121
+ <span class="preview" [class.muted]="!lastMessage()">
73122
+ {{ lastMessage() || emptyText() }}
73123
+ </span>
73124
+ @if (unreadCount() > 0) {
73125
+ <span class="badge">{{ unreadCount() > 99 ? '99+' : unreadCount() }}</span>
73126
+ }
73127
+ </div>
73128
+ </div>
73129
+ </button>
73130
+ `, styles: [":host{display:block}.item{display:flex;align-items:center;gap:12px;width:100%;padding:10px 12px;border:none;background:transparent;cursor:pointer;text-align:left;border-radius:12px;transition:background .12s ease}.item:hover{background:var(--ion-color-step-50, rgba(127, 127, 127, .06))}.avatar{width:48px;height:48px;flex-shrink:0;border-radius:50%;overflow:hidden;display:flex;align-items:center;justify-content:center;background:var(--ion-color-primary);color:#fff;font-weight:700;font-size:1rem}.avatar img{width:100%;height:100%;object-fit:cover}.content{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.top,.bottom{display:flex;align-items:center;gap:8px}.name{flex:1;min-width:0;font-weight:600;font-size:.9375rem;color:var(--ion-text-color, #000);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.time{flex-shrink:0;font-size:.75rem;color:var(--ion-color-medium, #92949c)}.time.unread{color:var(--ion-color-primary);font-weight:600}.preview{flex:1;min-width:0;font-size:.875rem;color:var(--ion-color-medium, #92949c);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.preview.muted{font-style:italic;opacity:.7}.badge{flex-shrink:0;min-width:20px;height:20px;padding:0 6px;border-radius:10px;background:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff);font-size:.6875rem;font-weight:700;display:inline-flex;align-items:center;justify-content:center}\n"] }]
73131
+ }], ctorParameters: () => [] });
73132
+
72679
73133
  const TYPING_I18N = {
72680
73134
  es: {
72681
73135
  one: '{name} esta escribiendo',
@@ -72691,8 +73145,9 @@ const TYPING_I18N = {
72691
73145
  /**
72692
73146
  * val-typing-indicator
72693
73147
  *
72694
- * Muestra quienes estan escribiendo en el chat con animacion de tres puntos.
72695
- * Se oculta automaticamente si typingUsers esta vacio.
73148
+ * Muestra quienes estan escribiendo con animacion de tres puntos. Se oculta
73149
+ * si `typingUsers` esta vacio. Usa signal input para que `typingText` (computed)
73150
+ * recompute al cambiar la lista.
72696
73151
  *
72697
73152
  * @example
72698
73153
  * <val-typing-indicator [typingUsers]="['Maria', 'Carlos']" />
@@ -72700,10 +73155,10 @@ const TYPING_I18N = {
72700
73155
  class TypingIndicatorComponent {
72701
73156
  constructor() {
72702
73157
  this.i18n = inject(I18nService);
72703
- this.typingUsers = [];
73158
+ this.typingUsers = input([]);
72704
73159
  this.typingText = computed(() => {
72705
73160
  this.i18n.lang();
72706
- const users = this.typingUsers ?? [];
73161
+ const users = this.typingUsers();
72707
73162
  if (users.length === 0)
72708
73163
  return '';
72709
73164
  if (users.length === 1) {
@@ -72717,8 +73172,8 @@ class TypingIndicatorComponent {
72717
73172
  this.i18n.registerDefaults('TypingIndicator', TYPING_I18N);
72718
73173
  }
72719
73174
  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) {
73175
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: TypingIndicatorComponent, isStandalone: true, selector: "val-typing-indicator", inputs: { typingUsers: { classPropertyName: "typingUsers", publicName: "typingUsers", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
73176
+ @if (typingUsers().length > 0) {
72722
73177
  <div class="typing-indicator">
72723
73178
  <div class="dots">
72724
73179
  <span></span>
@@ -72733,7 +73188,7 @@ class TypingIndicatorComponent {
72733
73188
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TypingIndicatorComponent, decorators: [{
72734
73189
  type: Component,
72735
73190
  args: [{ selector: 'val-typing-indicator', standalone: true, imports: [CommonModule], template: `
72736
- @if (typingUsers && typingUsers.length > 0) {
73191
+ @if (typingUsers().length > 0) {
72737
73192
  <div class="typing-indicator">
72738
73193
  <div class="dots">
72739
73194
  <span></span>
@@ -72744,250 +73199,280 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72744
73199
  </div>
72745
73200
  }
72746
73201
  `, 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
- }] } });
73202
+ }], ctorParameters: () => [] });
72750
73203
 
72751
- addIcons({ chatbubblesOutline });
73204
+ addIcons({ arrowDownOutline, chatbubblesOutline });
72752
73205
  const CHAT_WINDOW_I18N = {
72753
73206
  es: {
72754
73207
  conversationClosed: 'Esta conversacion esta cerrada',
72755
73208
  loading: 'Cargando mensajes...',
72756
- noMessages: 'No hay mensajes aun',
73209
+ noMessages: 'Aun no hay mensajes. Escribe el primero.',
73210
+ newMessages: 'Mensajes nuevos',
72757
73211
  },
72758
73212
  en: {
72759
73213
  conversationClosed: 'This conversation is closed',
72760
73214
  loading: 'Loading messages...',
72761
- noMessages: 'No messages yet',
73215
+ noMessages: 'No messages yet. Send the first one.',
73216
+ newMessages: 'New messages',
72762
73217
  },
72763
73218
  };
72764
73219
  /**
72765
- * val-chat-window
73220
+ * val-chat-window — ventana de chat completa (signal inputs).
72766
73221
  *
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.
73222
+ * Agrupa mensajes consecutivos del mismo emisor y por día (separadores sticky),
73223
+ * muestra avatar/nombre solo en los bordes del grupo, scroll-to-bottom inteligente
73224
+ * (auto-scroll si estás abajo, pill "mensajes nuevos" si estás leyendo arriba),
73225
+ * composer moderno con contexto de respuesta, y typing indicator.
72771
73226
  *
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
- * />
73227
+ * API pública estable (consumida por thread-panel y showcase): inputs convId,
73228
+ * messages, currentUserId, isOpen, typingUsers, isLoading; outputs sendMessage,
73229
+ * loadMore, reactionClick, deleteMessage, replyTo (+ editMessage/attach/voice nuevos).
72782
73230
  */
72783
73231
  class ChatWindowComponent {
72784
73232
  constructor() {
72785
73233
  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();
73234
+ this.convId = input('');
73235
+ this.messages = input([]);
73236
+ this.currentUserId = input('');
73237
+ this.isOpen = input(true);
73238
+ this.typingUsers = input([]);
73239
+ this.isLoading = input(false);
73240
+ this.sendMessage = output();
73241
+ this.loadMore = output();
73242
+ this.reactionClick = output();
73243
+ this.deleteMessage = output();
73244
+ this.replyTo = output();
73245
+ this.editMessage = output();
73246
+ this.typing = output();
73247
+ this.attach = output();
73248
+ this.voice = output();
73249
+ this.msgsEl = viewChild('msgs');
73250
+ this.replyingTo = signal(null);
73251
+ this.atBottom = signal(true);
73252
+ this.hasNew = signal(false);
73253
+ this.locale = computed(() => (this.i18n.lang() === 'en' ? 'en-US' : 'es-CL'));
73254
+ this.rows = computed(() => {
73255
+ const msgs = this.messages();
72806
73256
  if (msgs.length === 0)
72807
73257
  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);
73258
+ const labels = {
73259
+ today: this.i18n.t('today', '_global'),
73260
+ yesterday: this.i18n.t('yesterday', '_global'),
73261
+ };
73262
+ const out = [];
73263
+ let prevDay = '';
73264
+ for (let i = 0; i < msgs.length; i++) {
73265
+ const m = msgs[i];
73266
+ const d = m.createdAt instanceof Date ? m.createdAt : new Date(m.createdAt);
73267
+ const day = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
73268
+ const newDay = day !== prevDay;
73269
+ if (newDay) {
73270
+ out.push({ kind: 'date', id: 'date-' + day, label: formatDateSeparator(d, this.locale(), labels) });
73271
+ prevDay = day;
72822
73272
  }
73273
+ const prev = msgs[i - 1];
73274
+ const next = msgs[i + 1];
73275
+ const firstInGroup = newDay || !prev || prev.senderId !== m.senderId;
73276
+ const nextDay = next
73277
+ ? `${new Date(next.createdAt).getFullYear()}-${new Date(next.createdAt).getMonth()}-${new Date(next.createdAt).getDate()}`
73278
+ : '';
73279
+ const lastInGroup = !next || next.senderId !== m.senderId || nextDay !== day;
73280
+ out.push({
73281
+ kind: 'msg',
73282
+ id: m.msgId,
73283
+ msg: m,
73284
+ showName: firstInGroup && !m.isMine,
73285
+ showAvatar: lastInGroup && !m.isMine,
73286
+ tail: lastInGroup,
73287
+ });
72823
73288
  }
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');
73289
+ return out;
72836
73290
  });
72837
73291
  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;
73292
+ // Auto-scroll al fondo cuando llegan mensajes y el usuario ya estaba abajo
73293
+ // (o el último mensaje es propio). Si está leyendo arriba, marca "nuevos".
73294
+ let prevCount = 0;
73295
+ effect(() => {
73296
+ const rows = this.rows();
73297
+ const count = rows.length;
73298
+ if (count === prevCount)
73299
+ return;
73300
+ const grew = count > prevCount;
73301
+ prevCount = count;
73302
+ const last = this.messages()[this.messages().length - 1];
73303
+ const mine = last?.isMine ?? false;
73304
+ if (this.atBottom() || mine) {
73305
+ setTimeout(() => this.scrollToBottom(false), 0);
72846
73306
  }
72847
- }
72848
- }
72849
- ngAfterViewChecked() {
72850
- if (this.shouldScrollToBottom) {
72851
- this.scrollToBottom();
72852
- this.shouldScrollToBottom = false;
72853
- }
73307
+ else if (grew) {
73308
+ this.hasNew.set(true);
73309
+ }
73310
+ });
72854
73311
  }
72855
- onReactionClick(event) {
72856
- this.reactionClick.emit({ msgId: event.msgId, token: event.token });
73312
+ onScroll() {
73313
+ const el = this.msgsEl()?.nativeElement;
73314
+ if (!el)
73315
+ return;
73316
+ const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
73317
+ this.atBottom.set(nearBottom);
73318
+ if (nearBottom)
73319
+ this.hasNew.set(false);
73320
+ if (el.scrollTop < 40)
73321
+ this.loadMore.emit();
72857
73322
  }
72858
- onEditClick(_msgId) {
72859
- // Implementacion futura: emitir evento de edicion
73323
+ scrollToBottom(smooth) {
73324
+ const el = this.msgsEl()?.nativeElement;
73325
+ if (!el)
73326
+ return;
73327
+ el.scrollTo({ top: el.scrollHeight, behavior: smooth ? 'smooth' : 'auto' });
73328
+ this.atBottom.set(true);
73329
+ this.hasNew.set(false);
72860
73330
  }
72861
- onTyping() {
72862
- // El padre puede suscribirse al output typing si necesita propagar a Firestore
73331
+ onSend(body) {
73332
+ this.sendMessage.emit(body);
73333
+ this.replyingTo.set(null);
72863
73334
  }
72864
- scrollToBottom() {
72865
- const el = this.messagesListRef?.nativeElement;
72866
- if (el) {
72867
- el.scrollTop = el.scrollHeight;
73335
+ onAction(event) {
73336
+ switch (event.type) {
73337
+ case 'reply': {
73338
+ const m = this.messages().find(x => x.msgId === event.msgId);
73339
+ if (m) {
73340
+ this.replyingTo.set({ msgId: m.msgId, senderName: m.senderName, body: m.body });
73341
+ }
73342
+ this.replyTo.emit(event.msgId);
73343
+ break;
73344
+ }
73345
+ case 'edit':
73346
+ this.editMessage.emit(event.msgId);
73347
+ break;
73348
+ case 'delete':
73349
+ this.deleteMessage.emit(event.msgId);
73350
+ break;
73351
+ case 'react':
73352
+ this.reactionClick.emit({ msgId: event.msgId, token: event.token ?? '👍' });
73353
+ break;
72868
73354
  }
72869
73355
  }
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' });
73356
+ t(key) {
73357
+ return this.i18n.t(key, 'ChatWindow');
72884
73358
  }
72885
73359
  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>
73360
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ChatWindowComponent, isStandalone: true, selector: "val-chat-window", inputs: { convId: { classPropertyName: "convId", publicName: "convId", isSignal: true, isRequired: false, transformFunction: null }, messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, currentUserId: { classPropertyName: "currentUserId", publicName: "currentUserId", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, typingUsers: { classPropertyName: "typingUsers", publicName: "typingUsers", isSignal: true, isRequired: false, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sendMessage: "sendMessage", loadMore: "loadMore", reactionClick: "reactionClick", deleteMessage: "deleteMessage", replyTo: "replyTo", editMessage: "editMessage", typing: "typing", attach: "attach", voice: "voice" }, viewQueries: [{ propertyName: "msgsEl", first: true, predicate: ["msgs"], descendants: true, isSignal: true }], ngImport: i0, template: `
73361
+ <div class="chat">
73362
+ <div #msgs class="messages" (scroll)="onScroll()">
73363
+ @if (isLoading()) {
73364
+ <div class="state">
73365
+ <span>{{ t('loading') }}</span>
73366
+ </div>
73367
+ } @else if (rows().length === 0) {
73368
+ <div class="state empty">
73369
+ <ion-icon name="chatbubbles-outline" aria-hidden="true" />
73370
+ <span>{{ t('noMessages') }}</span>
72893
73371
  </div>
72894
73372
  } @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) {
73373
+ @for (row of rows(); track row.id) {
73374
+ @if (row.kind === 'date') {
73375
+ <div class="date-sep">
73376
+ <span>{{ row.label }}</span>
73377
+ </div>
73378
+ } @else {
72902
73379
  <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)"
73380
+ [msg]="row.msg"
73381
+ [showName]="row.showName"
73382
+ [showAvatar]="row.showAvatar"
73383
+ [tail]="row.tail"
73384
+ [locale]="locale()"
73385
+ (action)="onAction($event)"
72908
73386
  />
72909
73387
  }
72910
73388
  }
72911
73389
  }
73390
+ <val-typing-indicator [typingUsers]="typingUsers()" />
72912
73391
  </div>
72913
73392
 
72914
- <val-typing-indicator [typingUsers]="typingUsers" />
73393
+ @if (!atBottom()) {
73394
+ <button class="scroll-down" (click)="scrollToBottom(true)" aria-label="scroll">
73395
+ <ion-icon name="arrow-down-outline" aria-hidden="true" />
73396
+ @if (hasNew()) {
73397
+ <span class="dot"></span>
73398
+ }
73399
+ </button>
73400
+ }
72915
73401
 
72916
- @if (!isOpen) {
72917
- <div class="closed-banner">{{ closedText() }}</div>
73402
+ @if (isOpen()) {
73403
+ <val-chat-composer
73404
+ [replyingTo]="replyingTo()"
73405
+ (send)="onSend($event)"
73406
+ (typing)="typing.emit()"
73407
+ (attach)="attach.emit($event)"
73408
+ (voice)="voice.emit()"
73409
+ (cancelReply)="replyingTo.set(null)"
73410
+ />
72918
73411
  } @else {
72919
- <val-chat-input [props]="{ disabled: isLoading }" (send)="sendMessage.emit($event)" (typing)="onTyping()" />
73412
+ <div class="closed-banner">{{ t('conversationClosed') }}</div>
72920
73413
  }
72921
73414
  </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"] }] }); }
73415
+ `, isInline: true, styles: [":host{display:block;height:100%}.chat{position:relative;display:flex;flex-direction:column;height:100%;min-height:0}.messages{flex:1;min-height:0;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:0}.state{margin:auto;display:flex;flex-direction:column;align-items:center;gap:8px;color:var(--ion-color-medium, #92949c);font-size:.9375rem;text-align:center;padding:24px}.state.empty ion-icon{font-size:2.5rem;opacity:.5}.date-sep{position:sticky;top:4px;z-index:1;display:flex;justify-content:center;margin:8px 0}.date-sep span{font-size:.75rem;color:var(--ion-color-medium, #92949c);background:var(--ion-color-step-100, rgba(127, 127, 127, .14));padding:3px 12px;border-radius:999px;backdrop-filter:blur(4px)}.scroll-down{position:absolute;right:14px;bottom:84px;width:40px;height:40px;border-radius:50%;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff));color:var(--ion-color-medium, #92949c);box-shadow:0 2px 10px #00000026;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:1.25rem;z-index:3}.scroll-down .dot{position:absolute;top:0;right:0;width:12px;height:12px;border-radius:50%;background:var(--ion-color-primary);border:2px solid var(--ion-card-background, #fff)}.closed-banner{text-align:center;padding:14px;color:var(--ion-color-medium, #92949c);font-size:.875rem;border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: MessageBubbleComponent, selector: "val-message-bubble", inputs: ["msg", "showAvatar", "showName", "tail", "locale"], outputs: ["action"] }, { kind: "component", type: ChatComposerComponent, selector: "val-chat-composer", inputs: ["placeholder", "disabled", "maxLength", "replyingTo", "showAttach", "showEmoji", "showMic"], outputs: ["send", "typing", "attach", "voice", "cancelReply"] }, { kind: "component", type: TypingIndicatorComponent, selector: "val-typing-indicator", inputs: ["typingUsers"] }] }); }
72923
73416
  }
72924
73417
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatWindowComponent, decorators: [{
72925
73418
  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>
73419
+ args: [{ selector: 'val-chat-window', standalone: true, imports: [CommonModule, IonIcon, MessageBubbleComponent, ChatComposerComponent, TypingIndicatorComponent], template: `
73420
+ <div class="chat">
73421
+ <div #msgs class="messages" (scroll)="onScroll()">
73422
+ @if (isLoading()) {
73423
+ <div class="state">
73424
+ <span>{{ t('loading') }}</span>
73425
+ </div>
73426
+ } @else if (rows().length === 0) {
73427
+ <div class="state empty">
73428
+ <ion-icon name="chatbubbles-outline" aria-hidden="true" />
73429
+ <span>{{ t('noMessages') }}</span>
72933
73430
  </div>
72934
73431
  } @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) {
73432
+ @for (row of rows(); track row.id) {
73433
+ @if (row.kind === 'date') {
73434
+ <div class="date-sep">
73435
+ <span>{{ row.label }}</span>
73436
+ </div>
73437
+ } @else {
72942
73438
  <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)"
73439
+ [msg]="row.msg"
73440
+ [showName]="row.showName"
73441
+ [showAvatar]="row.showAvatar"
73442
+ [tail]="row.tail"
73443
+ [locale]="locale()"
73444
+ (action)="onAction($event)"
72948
73445
  />
72949
73446
  }
72950
73447
  }
72951
73448
  }
73449
+ <val-typing-indicator [typingUsers]="typingUsers()" />
72952
73450
  </div>
72953
73451
 
72954
- <val-typing-indicator [typingUsers]="typingUsers" />
73452
+ @if (!atBottom()) {
73453
+ <button class="scroll-down" (click)="scrollToBottom(true)" aria-label="scroll">
73454
+ <ion-icon name="arrow-down-outline" aria-hidden="true" />
73455
+ @if (hasNew()) {
73456
+ <span class="dot"></span>
73457
+ }
73458
+ </button>
73459
+ }
72955
73460
 
72956
- @if (!isOpen) {
72957
- <div class="closed-banner">{{ closedText() }}</div>
73461
+ @if (isOpen()) {
73462
+ <val-chat-composer
73463
+ [replyingTo]="replyingTo()"
73464
+ (send)="onSend($event)"
73465
+ (typing)="typing.emit()"
73466
+ (attach)="attach.emit($event)"
73467
+ (voice)="voice.emit()"
73468
+ (cancelReply)="replyingTo.set(null)"
73469
+ />
72958
73470
  } @else {
72959
- <val-chat-input [props]="{ disabled: isLoading }" (send)="sendMessage.emit($event)" (typing)="onTyping()" />
73471
+ <div class="closed-banner">{{ t('conversationClosed') }}</div>
72960
73472
  }
72961
73473
  </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
- }] } });
73474
+ `, styles: [":host{display:block;height:100%}.chat{position:relative;display:flex;flex-direction:column;height:100%;min-height:0}.messages{flex:1;min-height:0;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:0}.state{margin:auto;display:flex;flex-direction:column;align-items:center;gap:8px;color:var(--ion-color-medium, #92949c);font-size:.9375rem;text-align:center;padding:24px}.state.empty ion-icon{font-size:2.5rem;opacity:.5}.date-sep{position:sticky;top:4px;z-index:1;display:flex;justify-content:center;margin:8px 0}.date-sep span{font-size:.75rem;color:var(--ion-color-medium, #92949c);background:var(--ion-color-step-100, rgba(127, 127, 127, .14));padding:3px 12px;border-radius:999px;backdrop-filter:blur(4px)}.scroll-down{position:absolute;right:14px;bottom:84px;width:40px;height:40px;border-radius:50%;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff));color:var(--ion-color-medium, #92949c);box-shadow:0 2px 10px #00000026;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:1.25rem;z-index:3}.scroll-down .dot{position:absolute;top:0;right:0;width:12px;height:12px;border-radius:50%;background:var(--ion-color-primary);border:2px solid var(--ion-card-background, #fff)}.closed-banner{text-align:center;padding:14px;color:var(--ion-color-medium, #92949c);font-size:.875rem;border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}\n"] }]
73475
+ }], ctorParameters: () => [] });
72991
73476
 
72992
73477
  const THREAD_PANEL_I18N = {
72993
73478
  es: {
@@ -73145,7 +73630,7 @@ class ThreadPanelComponent {
73145
73630
  </div>
73146
73631
  }
73147
73632
  </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"] }] }); }
73633
+ `, isInline: true, styles: [".thread-panel{display:flex;flex-direction:column;height:100%;background:var(--ion-background-color, #f4f5f8)}.thread-header{padding:12px 16px;border-bottom:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff))}.thread-header .thread-title{font-size:1rem;font-weight:600;color:var(--ion-text-color, #000);margin:0}.thread-loading{display:flex;align-items:center;justify-content:center;flex:1;gap:10px;color:var(--ion-color-medium, #92949c);font-size:.875rem}.thread-chat{flex:1;overflow:hidden;display:flex;flex-direction:column}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: ChatWindowComponent, selector: "val-chat-window", inputs: ["convId", "messages", "currentUserId", "isOpen", "typingUsers", "isLoading"], outputs: ["sendMessage", "loadMore", "reactionClick", "deleteMessage", "replyTo", "editMessage", "typing", "attach", "voice"] }] }); }
73149
73634
  }
73150
73635
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ThreadPanelComponent, decorators: [{
73151
73636
  type: Component,
@@ -73199,5 +73684,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
73199
73684
  * Generated bundle index. Do not edit.
73200
73685
  */
73201
73686
 
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 };
73687
+ export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, APP_VERSION_PLATFORM_PLUGIN, APP_VERSION_REMOTE_PLUGIN, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AboutViewComponent, AccordionComponent, AccountViewComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, ApiKeyCreateModalComponent, ApiKeyService, ApiKeysModalComponent, ApiKeysViewComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, ChatComposerComponent, ChatInputComponent, ChatWindowComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentService, ContentTransformer, ConversationListItemComponent, ConversationService, CookieBannerComponent, CookieSettingsComponent, CountdownComponent, CreateOrgModalComponent, CtaCardComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_BUTTON_PRESETS, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_LOGIN_LOGO, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_POST_UPDATE_GRACE_MS, DEFAULT_PRESETS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_SPLASH_SCREEN_CONFIG, DataTableComponent, DateInputComponent, DatePickerComponent, DateRangeInputComponent, DebugConsoleComponent, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EditOrgModalComponent, EmptyStateComponent, EntityCardComponent, EntityFeedService, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FieldListComponent, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FontSizeOption, FontSizeSelectorComponent, FontSizeService, FooterComponent, FooterLinksComponent, FormComponent, FormFieldComponent, FormSkeletonComponent, FunHeaderComponent, FunModalComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HapticsService, HasPermissionDirective, HeaderComponent, HintComponent, HorizontalScrollComponent, HrefComponent, HtmlViewerModalComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, IONIC_COLORS$5 as IONIC_COLORS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, InviteMemberModalComponent, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGGED_IN_HINT_COOKIE, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LanguageSelectorV2Component, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadMoreComponent, LoadingDirective, LocalStorageService, LocaleService, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MemberDetailModalComponent, MemberImportModalComponent, MenuComponent, MessageBubbleComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationPreferencesViewComponent, NotificationsService, NotificationsViewComponent, NumberFromToComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OptionCardsComponent, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PLATFORM_CONFIGS, POST_UPDATE_TS_KEY, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PdfService, PermissionCatalogService, PermissionSelectorComponent, PermissionsModalComponent, PermissionsViewComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PreferencesViewComponent, PresetService, PriceTagComponent, ProcessLinksPipe, ProfileSkeletonComponent, ProfileViewComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QrScannerComponent, QueryBuilder, QuoteBoxComponent, REQUEST_STATUSES, RadioInputComponent, RangeInputComponent, RatingComponent, ReactionBarComponent, ReactionsService, RefresherComponent, RequestFirestoreService, RequestFormBuilderService, RequestFormComponent, RequestModalComponent, RequestService, RightsFooterComponent, RotatingTextComponent, SEARCH_HEADER_DEFAULTS, SETTINGS_SECTIONS_CATALOG, SHAPE_KEYS, SHARE_PROFILE_MODAL_DEFAULTS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, STATS_BAR_DEFAULTS, STROKE_KEYS, SearchHeaderComponent, SearchSelectorComponent, SearchbarComponent, SectionHeaderComponent, SecurityViewComponent, SegmentControlComponent, SelectSearchComponent, SessionListModalComponent, SessionService, SettingsHubComponent, ShareButtonsComponent, ShareProfileModalComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SplashComponent, SplashScreenService, StatsBarComponent, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, ThreadPanelComponent, TicketCardComponent, TicketCardImageService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, TranslatePipe, TypedCollection, TypingIndicatorComponent, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_CHAT_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_NETWORK_ERROR_KEY, VALTECH_REACTIONS_CONFIG, VALTECH_SETTINGS_MENU_LINKS, VALTECH_SITE_PATHS, VALTECH_SOCIAL_LINKS, VALTECH_SPLASH_SCREEN, VALTECH_WEB_BASE_URLS, VALTECH_WHATSAPP_CONFIG, VERSION, ValtechErrorService, VerifyViewComponent, WhatsappFabComponent, WhatsappService, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, authPasswordValidator, beautifyLegalArticle, blogPost, buildCompanyFooterProps, buildFooterLinks, buildLegalLinkResolver, buildPath, buildSettingsCards, button, canSubmitRequestType, collections, connectPageRefresh, createErrorStateProps, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createPageState, createPermissionLabeler, createRefreshableStream, createTitleProps, docs, errorLoggingInterceptor, extractPathParams, formatClockTime, formatDateSeparator, formatRelativeTime, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, groupPermissionsByScope, guestGuard, hasEmulators, iconButton, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isIonicColor, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideSplashScreen, provideValtechAboutRoutes, provideValtechAccountRoutes, provideValtechAds, provideValtechApiKeysRoutes, provideValtechAppConfig, provideValtechAppVersion, provideValtechAppVersionHttp, provideValtechAuth, provideValtechAuthInterceptor, provideValtechChat, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationPreferencesRoutes, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPermissionsRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechReactions, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, provideValtechWhatsapp, query, rbacGuard, renderPatternSvgInner, replaceSpecialChars, requestSubmitMode, resolveColor, resolveInputDefaultValue, resolveIonicColor, resolveWebBaseUrl, roleGuard, selectableRequestTypes, storagePaths, superAdminGuard, toArticle };
73203
73688
  //# sourceMappingURL=valtech-components.mjs.map