valtech-components 4.0.227 → 4.0.228

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