valtech-components 4.0.224 → 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.
Files changed (23) hide show
  1. package/esm2022/lib/components/molecules/chat-composer/chat-composer.component.mjs +238 -0
  2. package/esm2022/lib/components/molecules/conversation-list-item/conversation-list-item.component.mjs +108 -0
  3. package/esm2022/lib/components/molecules/message-bubble/message-bubble.component.mjs +259 -191
  4. package/esm2022/lib/components/molecules/typing-indicator/typing-indicator.component.mjs +11 -12
  5. package/esm2022/lib/components/organisms/chat-window/chat-window.component.mjs +217 -179
  6. package/esm2022/lib/components/organisms/thread-panel/thread-panel.component.mjs +1 -1
  7. package/esm2022/lib/services/chat/types.mjs +1 -1
  8. package/esm2022/lib/services/i18n/default-content.mjs +5 -1
  9. package/esm2022/lib/shared/utils/datetime.mjs +54 -1
  10. package/esm2022/lib/version.mjs +2 -2
  11. package/esm2022/public-api.mjs +4 -2
  12. package/fesm2022/valtech-components.mjs +869 -375
  13. package/fesm2022/valtech-components.mjs.map +1 -1
  14. package/lib/components/molecules/chat-composer/chat-composer.component.d.ts +46 -0
  15. package/lib/components/molecules/conversation-list-item/conversation-list-item.component.d.ts +30 -0
  16. package/lib/components/molecules/message-bubble/message-bubble.component.d.ts +33 -32
  17. package/lib/components/molecules/typing-indicator/typing-indicator.component.d.ts +5 -4
  18. package/lib/components/organisms/chat-window/chat-window.component.d.ts +52 -51
  19. package/lib/services/chat/types.d.ts +6 -2
  20. package/lib/shared/utils/datetime.d.ts +19 -0
  21. package/lib/version.d.ts +1 -1
  22. package/package.json +1 -1
  23. package/public-api.d.ts +3 -1
@@ -25,7 +25,7 @@ import { getApps, getApp, initializeApp as initializeApp$1 } from 'firebase/app'
25
25
  import * as i2$1 from '@ionic/angular/standalone';
26
26
  import { AlertController, IonAvatar, IonCard, IonIcon, IonButton, IonSpinner, IonText, IonModal, IonHeader, IonToolbar, IonContent, IonButtons, IonTitle, IonProgressBar, IonSkeletonText, IonFab, IonFabButton, IonFabList, IonLabel, IonCardContent, IonCardHeader, IonCardTitle, IonCardSubtitle, IonCheckbox, IonRadio, IonRadioGroup, IonTextarea, IonDatetime, IonSelect, IonSelectOption, IonPopover, IonList, IonItem, IonInput, IonChip, IonRange, IonSearchbar, IonSegment, IonSegmentButton, IonToggle, IonRippleEffect, IonAccordion, IonAccordionGroup, IonTabBar, IonTabButton, IonBadge, IonBreadcrumb, IonBreadcrumbs, IonInfiniteScroll, IonInfiniteScrollContent, IonNote, IonDatetimeButton, ToastController as ToastController$1, IonCol, IonRow, IonRefresher, IonRefresherContent, IonMenuButton, IonFooter, IonListHeader, IonGrid, MenuController, IonMenu, IonMenuToggle, IonSplitPane } from '@ionic/angular/standalone';
27
27
  import { addIcons } from 'ionicons';
28
- import { addOutline, addCircleOutline, alertOutline, alertCircleOutline, arrowBackOutline, arrowForwardOutline, arrowDownOutline, settings, settingsOutline, checkmarkCircleOutline, ellipsisHorizontalOutline, notifications, notificationsOutline, openOutline, closeOutline, chatbubblesOutline, shareOutline, heart, heartOutline, home, homeOutline, eyeOffOutline, eyeOutline, scanOutline, chevronDownOutline, chevronForwardOutline, checkmarkOutline, clipboardOutline, copyOutline, filterOutline, locationOutline, calendarOutline, businessOutline, logoTwitter, logoInstagram, logoLinkedin, logoYoutube, logoTiktok, logoFacebook, logoGoogle, createOutline, trashOutline, playOutline, peopleOutline, phonePortraitOutline, refreshOutline, documentTextOutline, lockClosedOutline, cloudUploadOutline, textOutline, informationCircleOutline, logoNpm, removeOutline, optionsOutline, personOutline, shieldCheckmarkOutline, keyOutline, desktopOutline, logOutOutline, cloudDownloadOutline, cloudOutline, gitBranchOutline, layersOutline, serverOutline, warningOutline, bulbOutline, lockOpenOutline, diamondOutline, newspaperOutline, add, close, share, create, trash, star, camera, mic, send, downloadOutline, chevronDown, language, globeOutline, checkmark, list, grid, apps, menu, search, person, helpCircle, informationCircle, documentText, mail, calendar, folder, chevronForward, ellipsisHorizontal, chevronBack, playBack, playForward, ellipse, starOutline, starHalf, heartHalf, checkmarkCircle, timeOutline, flag, trendingUp, trendingDown, remove, analytics, people, cash, cart, eye, chatbubbleOutline, thumbsUpOutline, thumbsUp, happyOutline, happy, sadOutline, sad, chevronUp, pin, pencil, callOutline, logoWhatsapp, paperPlaneOutline, mailOutline, chevronDownCircleOutline, closeCircle, alertCircle, logoApple, logoMicrosoft, linkOutline, unlinkOutline, cloudOfflineOutline, documentOutline, attachOutline, cameraOutline, closeCircleOutline, imageOutline, printOutline, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, searchOutline, cartOutline, chatbubble, compass, compassOutline, gridOutline, listOutline, folderOutline, documents, documentsOutline, statsChart, statsChartOutline, laptopOutline, swapHorizontalOutline, codeSlashOutline, notificationsOffOutline, alarmOutline, chatboxEllipsesOutline, checkmarkDoneOutline, cogOutline, shareSocialOutline, helpCircleOutline, menuOutline, stopCircleOutline, videocamOffOutline, checkmarkDone, returnDownBackOutline } from 'ionicons/icons';
28
+ import { addOutline, addCircleOutline, alertOutline, alertCircleOutline, arrowBackOutline, arrowForwardOutline, arrowDownOutline, settings, settingsOutline, checkmarkCircleOutline, ellipsisHorizontalOutline, notifications, notificationsOutline, openOutline, closeOutline, chatbubblesOutline, shareOutline, heart, heartOutline, home, homeOutline, eyeOffOutline, eyeOutline, scanOutline, chevronDownOutline, chevronForwardOutline, checkmarkOutline, clipboardOutline, copyOutline, filterOutline, locationOutline, calendarOutline, businessOutline, logoTwitter, logoInstagram, logoLinkedin, logoYoutube, logoTiktok, logoFacebook, logoGoogle, createOutline, trashOutline, playOutline, peopleOutline, phonePortraitOutline, refreshOutline, documentTextOutline, lockClosedOutline, cloudUploadOutline, textOutline, informationCircleOutline, logoNpm, removeOutline, optionsOutline, personOutline, shieldCheckmarkOutline, keyOutline, desktopOutline, logOutOutline, cloudDownloadOutline, cloudOutline, gitBranchOutline, layersOutline, serverOutline, warningOutline, bulbOutline, lockOpenOutline, diamondOutline, newspaperOutline, add, close, share, create, trash, star, camera, mic, send, downloadOutline, chevronDown, language, globeOutline, checkmark, list, grid, apps, menu, search, person, helpCircle, informationCircle, documentText, mail, calendar, folder, chevronForward, ellipsisHorizontal, chevronBack, playBack, playForward, ellipse, starOutline, starHalf, heartHalf, checkmarkCircle, timeOutline, flag, trendingUp, trendingDown, remove, analytics, people, cash, cart, eye, chatbubbleOutline, thumbsUpOutline, thumbsUp, happyOutline, happy, sadOutline, sad, chevronUp, pin, pencil, callOutline, logoWhatsapp, paperPlaneOutline, mailOutline, chevronDownCircleOutline, closeCircle, alertCircle, logoApple, logoMicrosoft, linkOutline, unlinkOutline, cloudOfflineOutline, documentOutline, attachOutline, cameraOutline, closeCircleOutline, imageOutline, printOutline, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, searchOutline, cartOutline, chatbubble, compass, compassOutline, gridOutline, listOutline, folderOutline, documents, documentsOutline, statsChart, statsChartOutline, laptopOutline, swapHorizontalOutline, codeSlashOutline, notificationsOffOutline, alarmOutline, chatboxEllipsesOutline, checkmarkDoneOutline, cogOutline, shareSocialOutline, helpCircleOutline, menuOutline, stopCircleOutline, videocamOffOutline, checkmarkDone, arrowUndoOutline, micOutline, arrowUpOutline } from 'ionicons/icons';
29
29
  import { Browser } from '@capacitor/browser';
30
30
  import * as i1$6 from '@angular/platform-browser';
31
31
  import { DomSanitizer, Meta, Title } from '@angular/platform-browser';
@@ -56,7 +56,7 @@ import { BrowserMultiFormatReader } from '@zxing/browser';
56
56
  * Current version of valtech-components.
57
57
  * This is automatically updated during the publish process.
58
58
  */
59
- const VERSION = '4.0.224';
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.
@@ -7030,6 +7083,8 @@ const VALTECH_DEFAULT_CONTENT = {
7030
7083
  loading: 'Cargando...',
7031
7084
  search: 'Buscar',
7032
7085
  learnMore: 'Saber más',
7086
+ today: 'Hoy',
7087
+ yesterday: 'Ayer',
7033
7088
  // Estados
7034
7089
  success: 'Éxito',
7035
7090
  error: 'Error',
@@ -7187,6 +7242,8 @@ const VALTECH_DEFAULT_CONTENT = {
7187
7242
  loading: 'Loading...',
7188
7243
  search: 'Search',
7189
7244
  learnMore: 'Learn more',
7245
+ today: 'Today',
7246
+ yesterday: 'Yesterday',
7190
7247
  // States
7191
7248
  success: 'Success',
7192
7249
  error: 'Error',
@@ -72286,241 +72343,308 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72286
72343
  */
72287
72344
 
72288
72345
  addIcons({
72346
+ alertCircle,
72289
72347
  checkmark,
72290
72348
  checkmarkDone,
72349
+ createOutline,
72350
+ documentOutline,
72351
+ happyOutline,
72352
+ arrowUndoOutline,
72291
72353
  timeOutline,
72292
- alertCircle,
72293
- pencil,
72294
- trash,
72295
- returnDownBackOutline,
72354
+ trashOutline,
72296
72355
  });
72297
- const CHAT_I18N = {
72356
+ const MESSAGE_BUBBLE_I18N = {
72298
72357
  es: {
72299
- messageDeleted: '[mensaje eliminado]',
72300
- edited: '(editado)',
72301
- react: 'Reaccionar',
72302
72358
  reply: 'Responder',
72303
72359
  edit: 'Editar',
72304
72360
  delete: 'Eliminar',
72361
+ react: 'Reaccionar',
72362
+ deleted: 'Mensaje eliminado',
72363
+ edited: 'editado',
72305
72364
  },
72306
72365
  en: {
72307
- messageDeleted: '[message deleted]',
72308
- edited: '(edited)',
72309
- react: 'React',
72310
72366
  reply: 'Reply',
72311
72367
  edit: 'Edit',
72312
72368
  delete: 'Delete',
72369
+ react: 'React',
72370
+ deleted: 'Message deleted',
72371
+ edited: 'edited',
72313
72372
  },
72314
72373
  };
72315
72374
  /**
72316
- * val-message-bubble
72375
+ * val-message-bubble — burbuja de mensaje presentacional (signal inputs).
72317
72376
  *
72318
- * Burbuja de mensaje para el chat. Alineada a la derecha si isMine, a la izquierda si no.
72319
- * Muestra nombre del emisor, cuerpo, timestamp, estado de envio, reacciones y acciones.
72320
- * 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.
72321
72381
  *
72322
- * @example
72323
- * <val-message-bubble
72324
- * [props]="msg"
72325
- * (reactionClick)="onReaction($event)"
72326
- * (replyClick)="onReply($event)"
72327
- * (deleteClick)="onDelete($event)"
72328
- * />
72382
+ * Read receipts estilo WhatsApp: sending → reloj, sent → ✓, delivered/read → ✓✓
72383
+ * (read en color), failed → alerta.
72329
72384
  */
72330
72385
  class MessageBubbleComponent {
72331
72386
  constructor() {
72332
72387
  this.i18n = inject(I18nService);
72333
- this.reactionClick = new EventEmitter();
72334
- this.replyClick = new EventEmitter();
72335
- this.deleteClick = new EventEmitter();
72336
- this.editClick = new EventEmitter();
72337
- if (!this.i18n.hasNamespace('Chat')) {
72338
- this.i18n.registerDefaults('Chat', CHAT_I18N);
72339
- }
72340
- }
72341
- ngOnInit() { }
72342
- deletedText() {
72343
- this.i18n.lang();
72344
- return this.i18n.t('messageDeleted', 'Chat');
72345
- }
72346
- editedLabel() {
72347
- this.i18n.lang();
72348
- return this.i18n.t('edited', 'Chat');
72349
- }
72350
- formatTime(date) {
72351
- if (!date)
72352
- return '';
72353
- const d = date instanceof Date ? date : new Date(date);
72354
- return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
72355
- }
72356
- statusIcon() {
72357
- switch (this.props.status) {
72358
- case 'sending':
72359
- return 'time-outline';
72360
- case 'failed':
72361
- return 'alert-circle';
72362
- case 'sent':
72363
- default:
72364
- return 'checkmark';
72365
- }
72366
- }
72367
- onReactionClick(reaction) {
72368
- 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);
72369
72420
  }
72370
- onReplyClick() {
72371
- this.replyClick.emit({ msgId: this.props.msgId });
72421
+ toggleActions() {
72422
+ if (this.msg().isDeleted)
72423
+ return;
72424
+ this.actionsOpen.update(v => !v);
72372
72425
  }
72373
- onDeleteClick() {
72374
- 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 });
72375
72429
  }
72376
- onEditClick() {
72377
- this.editClick.emit({ msgId: this.props.msgId });
72430
+ t(key) {
72431
+ return this.i18n.t(key, 'MessageBubble');
72378
72432
  }
72379
72433
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageBubbleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
72380
- 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: `
72381
- <div class="message-bubble-wrapper" [class.is-mine]="props.isMine">
72382
- @if (!props.isMine && !props.isDeleted) {
72383
- <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>
72384
72444
  }
72385
72445
 
72386
- <div class="bubble">
72387
- @if (props.replyTo && !props.isDeleted) {
72388
- <div class="reply-preview">
72389
- <div class="reply-sender">{{ props.replyTo.senderName }}</div>
72390
- <div class="reply-body">{{ props.replyTo.body }}</div>
72391
- </div>
72392
- }
72393
-
72394
- @if (props.isDeleted) {
72395
- <span class="deleted-text">{{ deletedText() }}</span>
72396
- } @else {
72397
- <div class="bubble-body">{{ props.body }}</div>
72398
- }
72399
-
72400
- <div class="bubble-meta">
72401
- <span class="timestamp">{{ formatTime(props.createdAt) }}</span>
72402
- @if (props.isEdited && !props.isDeleted) {
72403
- <span class="edited-label">{{ editedLabel() }}</span>
72404
- }
72405
- @if (props.isMine && props.status) {
72406
- <ion-icon
72407
- [name]="statusIcon()"
72408
- [class]="'status-icon ' + (props.status ?? '')"
72409
- aria-hidden="true"
72410
- ></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>
72411
72456
  }
72412
- </div>
72413
- </div>
72414
72457
 
72415
- @if (!props.isDeleted && props.reactions && props.reactions.length > 0) {
72416
- <div class="reactions-bar">
72417
- @for (reaction of props.reactions; track reaction.token) {
72418
- <button class="reaction-btn" [class.active]="reaction.active" (click)="onReactionClick(reaction)">
72419
- <span>{{ reaction.token }}</span>
72420
- @if (reaction.count > 0) {
72421
- <span class="reaction-count">{{ reaction.count }}</span>
72422
- }
72423
- </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>
72424
72463
  }
72425
- </div>
72426
- }
72427
72464
 
72428
- @if (!props.isDeleted) {
72429
- <div class="bubble-actions">
72430
- <ion-button fill="clear" size="small" (click)="onReplyClick()">
72431
- <ion-icon name="return-down-back-outline" slot="icon-only" aria-hidden="true"></ion-icon>
72432
- </ion-button>
72433
- @if (props.isMine) {
72434
- <ion-button fill="clear" size="small" (click)="onEditClick()">
72435
- <ion-icon name="pencil" slot="icon-only" aria-hidden="true"></ion-icon>
72436
- </ion-button>
72437
- <ion-button fill="clear" size="small" color="danger" (click)="onDeleteClick()">
72438
- <ion-icon name="trash" slot="icon-only" aria-hidden="true"></ion-icon>
72439
- </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
+ }
72440
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>
72441
72505
  </div>
72442
- }
72443
- </div>
72444
- `, 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"] }] }); }
72445
- }
72446
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageBubbleComponent, decorators: [{
72447
- type: Component,
72448
- args: [{ selector: 'val-message-bubble', standalone: true, imports: [CommonModule, IonIcon, IonButton], template: `
72449
- <div class="message-bubble-wrapper" [class.is-mine]="props.isMine">
72450
- @if (!props.isMine && !props.isDeleted) {
72451
- <span class="sender-name">{{ props.senderName }}</span>
72452
- }
72453
72506
 
72454
- <div class="bubble">
72455
- @if (props.replyTo && !props.isDeleted) {
72456
- <div class="reply-preview">
72457
- <div class="reply-sender">{{ props.replyTo.senderName }}</div>
72458
- <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
+ }
72459
72515
  </div>
72460
72516
  }
72461
72517
 
72462
- @if (props.isDeleted) {
72463
- <span class="deleted-text">{{ deletedText() }}</span>
72464
- } @else {
72465
- <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>
72466
72535
  }
72467
-
72468
- <div class="bubble-meta">
72469
- <span class="timestamp">{{ formatTime(props.createdAt) }}</span>
72470
- @if (props.isEdited && !props.isDeleted) {
72471
- <span class="edited-label">{{ editedLabel() }}</span>
72472
- }
72473
- @if (props.isMine && props.status) {
72474
- <ion-icon
72475
- [name]="statusIcon()"
72476
- [class]="'status-icon ' + (props.status ?? '')"
72477
- aria-hidden="true"
72478
- ></ion-icon>
72479
- }
72480
- </div>
72481
72536
  </div>
72482
-
72483
- @if (!props.isDeleted && props.reactions && props.reactions.length > 0) {
72484
- <div class="reactions-bar">
72485
- @for (reaction of props.reactions; track reaction.token) {
72486
- <button class="reaction-btn" [class.active]="reaction.active" (click)="onReactionClick(reaction)">
72487
- <span>{{ reaction.token }}</span>
72488
- @if (reaction.count > 0) {
72489
- <span class="reaction-count">{{ reaction.count }}</span>
72490
- }
72491
- </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>
72492
72550
  }
72493
72551
  </div>
72494
72552
  }
72495
72553
 
72496
- @if (!props.isDeleted) {
72497
- <div class="bubble-actions">
72498
- <ion-button fill="clear" size="small" (click)="onReplyClick()">
72499
- <ion-icon name="return-down-back-outline" slot="icon-only" aria-hidden="true"></ion-icon>
72500
- </ion-button>
72501
- @if (props.isMine) {
72502
- <ion-button fill="clear" size="small" (click)="onEditClick()">
72503
- <ion-icon name="pencil" slot="icon-only" aria-hidden="true"></ion-icon>
72504
- </ion-button>
72505
- <ion-button fill="clear" size="small" color="danger" (click)="onDeleteClick()">
72506
- <ion-icon name="trash" slot="icon-only" aria-hidden="true"></ion-icon>
72507
- </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>
72508
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>
72509
72613
  </div>
72510
- }
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>
72511
72645
  </div>
72512
- `, 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"] }]
72513
- }], ctorParameters: () => [], propDecorators: { props: [{
72514
- type: Input
72515
- }], reactionClick: [{
72516
- type: Output
72517
- }], replyClick: [{
72518
- type: Output
72519
- }], deleteClick: [{
72520
- type: Output
72521
- }], editClick: [{
72522
- type: Output
72523
- }] } });
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: () => [] });
72524
72648
 
72525
72649
  addIcons({ sendOutline });
72526
72650
  const CHAT_INPUT_I18N = {
@@ -72672,6 +72796,340 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72672
72796
  type: Output
72673
72797
  }] } });
72674
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
+
72675
73133
  const TYPING_I18N = {
72676
73134
  es: {
72677
73135
  one: '{name} esta escribiendo',
@@ -72687,8 +73145,9 @@ const TYPING_I18N = {
72687
73145
  /**
72688
73146
  * val-typing-indicator
72689
73147
  *
72690
- * Muestra quienes estan escribiendo en el chat con animacion de tres puntos.
72691
- * 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.
72692
73151
  *
72693
73152
  * @example
72694
73153
  * <val-typing-indicator [typingUsers]="['Maria', 'Carlos']" />
@@ -72696,10 +73155,10 @@ const TYPING_I18N = {
72696
73155
  class TypingIndicatorComponent {
72697
73156
  constructor() {
72698
73157
  this.i18n = inject(I18nService);
72699
- this.typingUsers = [];
73158
+ this.typingUsers = input([]);
72700
73159
  this.typingText = computed(() => {
72701
73160
  this.i18n.lang();
72702
- const users = this.typingUsers ?? [];
73161
+ const users = this.typingUsers();
72703
73162
  if (users.length === 0)
72704
73163
  return '';
72705
73164
  if (users.length === 1) {
@@ -72713,8 +73172,8 @@ class TypingIndicatorComponent {
72713
73172
  this.i18n.registerDefaults('TypingIndicator', TYPING_I18N);
72714
73173
  }
72715
73174
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TypingIndicatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
72716
- 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: `
72717
- @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) {
72718
73177
  <div class="typing-indicator">
72719
73178
  <div class="dots">
72720
73179
  <span></span>
@@ -72729,7 +73188,7 @@ class TypingIndicatorComponent {
72729
73188
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TypingIndicatorComponent, decorators: [{
72730
73189
  type: Component,
72731
73190
  args: [{ selector: 'val-typing-indicator', standalone: true, imports: [CommonModule], template: `
72732
- @if (typingUsers && typingUsers.length > 0) {
73191
+ @if (typingUsers().length > 0) {
72733
73192
  <div class="typing-indicator">
72734
73193
  <div class="dots">
72735
73194
  <span></span>
@@ -72740,245 +73199,280 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72740
73199
  </div>
72741
73200
  }
72742
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"] }]
72743
- }], ctorParameters: () => [], propDecorators: { typingUsers: [{
72744
- type: Input
72745
- }] } });
73202
+ }], ctorParameters: () => [] });
72746
73203
 
72747
- addIcons({ chatbubblesOutline });
73204
+ addIcons({ arrowDownOutline, chatbubblesOutline });
72748
73205
  const CHAT_WINDOW_I18N = {
72749
73206
  es: {
72750
73207
  conversationClosed: 'Esta conversacion esta cerrada',
72751
73208
  loading: 'Cargando mensajes...',
72752
- noMessages: 'No hay mensajes aun',
73209
+ noMessages: 'Aun no hay mensajes. Escribe el primero.',
73210
+ newMessages: 'Mensajes nuevos',
72753
73211
  },
72754
73212
  en: {
72755
73213
  conversationClosed: 'This conversation is closed',
72756
73214
  loading: 'Loading messages...',
72757
- noMessages: 'No messages yet',
73215
+ noMessages: 'No messages yet. Send the first one.',
73216
+ newMessages: 'New messages',
72758
73217
  },
72759
73218
  };
72760
73219
  /**
72761
- * val-chat-window
73220
+ * val-chat-window — ventana de chat completa (signal inputs).
72762
73221
  *
72763
- * Ventana de chat completa. Muestra la lista de mensajes agrupados por fecha,
72764
- * el indicador de escritura, el input de mensaje (solo si isOpen) y un banner
72765
- * cuando la conversacion esta cerrada. Auto-scroll al ultimo mensaje cuando
72766
- * 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.
72767
73226
  *
72768
- * @example
72769
- * <val-chat-window
72770
- * [convId]="conv.id"
72771
- * [messages]="messages()"
72772
- * [currentUserId]="userId"
72773
- * [isOpen]="conv.status === 'open'"
72774
- * [typingUsers]="typingUsers()"
72775
- * (sendMessage)="onSend($event)"
72776
- * (reactionClick)="onReaction($event)"
72777
- * />
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).
72778
73230
  */
72779
73231
  class ChatWindowComponent {
72780
73232
  constructor() {
72781
73233
  this.i18n = inject(I18nService);
72782
- this.convId = '';
72783
- this.messages = [];
72784
- this.participants = [];
72785
- this.currentUserId = '';
72786
- this.isOpen = true;
72787
- this.typingUsers = [];
72788
- this.isLoading = false;
72789
- this.sendMessage = new EventEmitter();
72790
- this.loadMore = new EventEmitter();
72791
- this.reactionClick = new EventEmitter();
72792
- this.deleteMessage = new EventEmitter();
72793
- this.replyTo = new EventEmitter();
72794
- this.prevMessageCount = 0;
72795
- this.shouldScrollToBottom = false;
72796
- this.messageGroups = computed(() => {
72797
- const msgs = this.messages ?? [];
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();
72798
73256
  if (msgs.length === 0)
72799
73257
  return [];
72800
- const groups = [];
72801
- let currentLabel = '';
72802
- let currentGroup = [];
72803
- for (const msg of msgs) {
72804
- const label = this.formatDateLabel(msg.createdAt);
72805
- if (label !== currentLabel) {
72806
- if (currentGroup.length > 0) {
72807
- groups.push({ dateLabel: currentLabel, messages: currentGroup });
72808
- }
72809
- currentLabel = label;
72810
- currentGroup = [msg];
72811
- }
72812
- else {
72813
- 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;
72814
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
+ });
72815
73288
  }
72816
- if (currentGroup.length > 0) {
72817
- groups.push({ dateLabel: currentLabel, messages: currentGroup });
72818
- }
72819
- return groups;
72820
- });
72821
- this.closedText = computed(() => {
72822
- this.i18n.lang();
72823
- return this.i18n.t('conversationClosed', 'ChatWindow');
72824
- });
72825
- this.loadingText = computed(() => {
72826
- this.i18n.lang();
72827
- return this.i18n.t('loading', 'ChatWindow');
73289
+ return out;
72828
73290
  });
72829
73291
  this.i18n.registerDefaults('ChatWindow', CHAT_WINDOW_I18N);
72830
- }
72831
- ngOnChanges(changes) {
72832
- if (changes['messages']) {
72833
- const current = changes['messages'].currentValue ?? [];
72834
- if (current.length !== this.prevMessageCount) {
72835
- this.prevMessageCount = current.length;
72836
- 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);
72837
73306
  }
72838
- }
72839
- }
72840
- ngAfterViewChecked() {
72841
- if (this.shouldScrollToBottom) {
72842
- this.scrollToBottom();
72843
- this.shouldScrollToBottom = false;
72844
- }
73307
+ else if (grew) {
73308
+ this.hasNew.set(true);
73309
+ }
73310
+ });
72845
73311
  }
72846
- onReactionClick(event) {
72847
- 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();
72848
73322
  }
72849
- onEditClick(_msgId) {
72850
- // 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);
72851
73330
  }
72852
- onTyping() {
72853
- // 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);
72854
73334
  }
72855
- scrollToBottom() {
72856
- const el = this.messagesListRef?.nativeElement;
72857
- if (el) {
72858
- 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;
72859
73354
  }
72860
73355
  }
72861
- formatDateLabel(date) {
72862
- if (!date)
72863
- return '';
72864
- const d = date instanceof Date ? date : new Date(date);
72865
- const now = new Date();
72866
- const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
72867
- const msgDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
72868
- const diffMs = today.getTime() - msgDay.getTime();
72869
- const diffDays = Math.round(diffMs / 86400000);
72870
- if (diffDays === 0)
72871
- return this.i18n.t('today', '_global') || 'Hoy';
72872
- if (diffDays === 1)
72873
- return this.i18n.t('yesterday', '_global') || 'Ayer';
72874
- return d.toLocaleDateString([], { day: '2-digit', month: 'short', year: 'numeric' });
73356
+ t(key) {
73357
+ return this.i18n.t(key, 'ChatWindow');
72875
73358
  }
72876
73359
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatWindowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
72877
- 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: `
72878
- <div class="chat-window">
72879
- <div class="messages-list" #messagesList>
72880
- @if (isLoading) {
72881
- <div class="loading-state">
72882
- <ion-spinner name="dots"></ion-spinner>
72883
- <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>
72884
73371
  </div>
72885
73372
  } @else {
72886
- @for (group of messageGroups(); track group.dateLabel) {
72887
- <div class="date-separator">
72888
- <div class="date-line"></div>
72889
- <span class="date-label">{{ group.dateLabel }}</span>
72890
- <div class="date-line"></div>
72891
- </div>
72892
- @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 {
72893
73379
  <val-message-bubble
72894
- [props]="msg"
72895
- (reactionClick)="onReactionClick($event)"
72896
- (replyClick)="replyTo.emit($event.msgId)"
72897
- (deleteClick)="deleteMessage.emit($event.msgId)"
72898
- (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)"
72899
73386
  />
72900
73387
  }
72901
73388
  }
72902
73389
  }
73390
+ <val-typing-indicator [typingUsers]="typingUsers()" />
72903
73391
  </div>
72904
73392
 
72905
- <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
+ }
72906
73401
 
72907
- @if (!isOpen) {
72908
- <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
+ />
72909
73411
  } @else {
72910
- <val-chat-input [props]="{ disabled: isLoading }" (send)="sendMessage.emit($event)" (typing)="onTyping()" />
73412
+ <div class="closed-banner">{{ t('conversationClosed') }}</div>
72911
73413
  }
72912
73414
  </div>
72913
- `, 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"] }] }); }
72914
73416
  }
72915
73417
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatWindowComponent, decorators: [{
72916
73418
  type: Component,
72917
- args: [{ selector: 'val-chat-window', standalone: true, imports: [CommonModule, IonIcon, IonSpinner, MessageBubbleComponent, ChatInputComponent, TypingIndicatorComponent], template: `
72918
- <div class="chat-window">
72919
- <div class="messages-list" #messagesList>
72920
- @if (isLoading) {
72921
- <div class="loading-state">
72922
- <ion-spinner name="dots"></ion-spinner>
72923
- <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>
72924
73430
  </div>
72925
73431
  } @else {
72926
- @for (group of messageGroups(); track group.dateLabel) {
72927
- <div class="date-separator">
72928
- <div class="date-line"></div>
72929
- <span class="date-label">{{ group.dateLabel }}</span>
72930
- <div class="date-line"></div>
72931
- </div>
72932
- @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 {
72933
73438
  <val-message-bubble
72934
- [props]="msg"
72935
- (reactionClick)="onReactionClick($event)"
72936
- (replyClick)="replyTo.emit($event.msgId)"
72937
- (deleteClick)="deleteMessage.emit($event.msgId)"
72938
- (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)"
72939
73445
  />
72940
73446
  }
72941
73447
  }
72942
73448
  }
73449
+ <val-typing-indicator [typingUsers]="typingUsers()" />
72943
73450
  </div>
72944
73451
 
72945
- <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
+ }
72946
73460
 
72947
- @if (!isOpen) {
72948
- <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
+ />
72949
73470
  } @else {
72950
- <val-chat-input [props]="{ disabled: isLoading }" (send)="sendMessage.emit($event)" (typing)="onTyping()" />
73471
+ <div class="closed-banner">{{ t('conversationClosed') }}</div>
72951
73472
  }
72952
73473
  </div>
72953
- `, 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"] }]
72954
- }], ctorParameters: () => [], propDecorators: { convId: [{
72955
- type: Input
72956
- }], messages: [{
72957
- type: Input
72958
- }], participants: [{
72959
- type: Input
72960
- }], currentUserId: [{
72961
- type: Input
72962
- }], isOpen: [{
72963
- type: Input
72964
- }], typingUsers: [{
72965
- type: Input
72966
- }], isLoading: [{
72967
- type: Input
72968
- }], sendMessage: [{
72969
- type: Output
72970
- }], loadMore: [{
72971
- type: Output
72972
- }], reactionClick: [{
72973
- type: Output
72974
- }], deleteMessage: [{
72975
- type: Output
72976
- }], replyTo: [{
72977
- type: Output
72978
- }], messagesListRef: [{
72979
- type: ViewChild,
72980
- args: ['messagesList']
72981
- }] } });
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: () => [] });
72982
73476
 
72983
73477
  const THREAD_PANEL_I18N = {
72984
73478
  es: {
@@ -73136,7 +73630,7 @@ class ThreadPanelComponent {
73136
73630
  </div>
73137
73631
  }
73138
73632
  </div>
73139
- `, 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"] }] }); }
73140
73634
  }
73141
73635
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ThreadPanelComponent, decorators: [{
73142
73636
  type: Component,
@@ -73190,5 +73684,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
73190
73684
  * Generated bundle index. Do not edit.
73191
73685
  */
73192
73686
 
73193
- 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 };
73194
73688
  //# sourceMappingURL=valtech-components.mjs.map