valtech-components 4.0.230 → 4.0.231

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.
@@ -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.230';
59
+ const VERSION = '4.0.231';
60
60
 
61
61
  // Control de estado de refresco (singleton a nivel de módulo)
62
62
  let isRefreshing = false;
@@ -61830,12 +61830,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
61830
61830
  }] } });
61831
61831
 
61832
61832
  /**
61833
- * Servicio para generación de PDFs via el PDF Lambda (/v2/pdf/generate).
61833
+ * Servicio de PDF de plataforma (ADR-042). Reutilizable por cualquier app.
61834
61834
  *
61835
61835
  * El JWT del usuario se inyecta automáticamente por el authInterceptor
61836
61836
  * (configurado vía provideValtechAuth). El app-id se toma de ValtechAuthConfig.
61837
61837
  *
61838
- * Modos de uso:
61838
+ * Tres modos:
61839
+ * - generate() → HTTP sync: genera y devuelve la URL presignada.
61840
+ * - createJob() → async: encola un job (delivery.mode='async') y devuelve
61841
+ * { jobId, status:'queued' }. El PDF se genera fuera de banda.
61842
+ * - watchJob() → observa el estado del job en tiempo real (Firestore),
61843
+ * proyectado por el backend al completarse.
61844
+ *
61845
+ * Fuentes (en los 3 modos):
61839
61846
  * - source.template → renderiza una plantilla desde DynamoDB
61840
61847
  * - source.html → HTML arbitrario provisto por el caller
61841
61848
  * - source.url → URL pública (Puppeteer la carga)
@@ -61844,6 +61851,8 @@ class PdfService {
61844
61851
  constructor(config) {
61845
61852
  this.config = config;
61846
61853
  this.http = inject(HttpClient);
61854
+ this.auth = inject(AuthService);
61855
+ this.collections = inject(FirestoreCollectionFactory);
61847
61856
  }
61848
61857
  get baseUrl() {
61849
61858
  return `${this.config?.apiUrl ?? ''}/v2/pdf`;
@@ -61860,6 +61869,35 @@ class PdfService {
61860
61869
  generateAndDownload(req, filename = 'documento.pdf') {
61861
61870
  return this.generate(req).pipe(switchMap$1(result => from(this._download(result.url, filename))));
61862
61871
  }
61872
+ /**
61873
+ * Encola un job PDF async. El backend publica `pdf.generate.requested`; el PDF
61874
+ * Lambda lo genera fuera de banda y, al completar, proyecta el estado a
61875
+ * Firestore. Usar watchJob(jobId) para observar el resultado en tiempo real.
61876
+ */
61877
+ createJob(req) {
61878
+ const body = {
61879
+ ...req,
61880
+ delivery: { ...(req.delivery ?? {}), mode: 'async' },
61881
+ };
61882
+ return this.http.post(`${this.baseUrl}/generate`, body, {
61883
+ headers: this.appIdHeader,
61884
+ });
61885
+ }
61886
+ /**
61887
+ * Observa el estado de un job PDF async en tiempo real (onSnapshot del doc
61888
+ * `apps/{appId}/orgs/{orgId}/pdf-jobs/{jobId}`). Emite null si no hay org
61889
+ * activa o hasta que el doc exista. Cuando el job completa/falla, el doc trae
61890
+ * status + url. Cierra la suscripción al completar tu propia lógica.
61891
+ */
61892
+ watchJob(jobId) {
61893
+ const orgId = this.auth.user()?.activeOrg ?? '';
61894
+ if (!orgId || !jobId)
61895
+ return of(null);
61896
+ const coll = this.collections.create(`orgs/${orgId}/pdf-jobs`, {
61897
+ timestamps: false,
61898
+ });
61899
+ return coll.watch(jobId);
61900
+ }
61863
61901
  async _download(url, _filename) {
61864
61902
  window.open(url, '_blank', 'noopener');
61865
61903
  }
@@ -72171,21 +72209,10 @@ class ConversationService {
72171
72209
  return this.http.delete(`${this.baseUrl}/conversations/${convId}/participants/${userId}`);
72172
72210
  }
72173
72211
  /**
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.
72212
+ * Envia un mensaje a una conversacion.
72177
72213
  */
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);
72214
+ sendMessage(convId, body) {
72215
+ return this.http.post(`${this.baseUrl}/conversations/${convId}/messages`, { body });
72189
72216
  }
72190
72217
  /**
72191
72218
  * Elimina (marca como eliminado) un mensaje.
@@ -72412,8 +72439,7 @@ class MessageBubbleComponent {
72412
72439
  return (parts[0][0] + (parts[1]?.[0] ?? '')).toUpperCase();
72413
72440
  });
72414
72441
  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/')));
72442
+ this.fileAttachments = computed(() => (this.msg().attachments ?? []).filter(a => !a.mimeType?.startsWith('image/')));
72417
72443
  this.statusIcon = computed(() => {
72418
72444
  switch (this.msg().status) {
72419
72445
  case 'sending':
@@ -72484,13 +72510,6 @@ class MessageBubbleComponent {
72484
72510
  }
72485
72511
  </div>
72486
72512
  }
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
72513
  @if (fileAttachments().length > 0) {
72495
72514
  <div class="files">
72496
72515
  @for (att of fileAttachments(); track att.url) {
@@ -72554,7 +72573,7 @@ class MessageBubbleComponent {
72554
72573
  }
72555
72574
  </div>
72556
72575
  </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"] }] }); }
72576
+ `, isInline: true, styles: [":host{display:block}.row{display:flex;align-items:flex-end;gap:8px;margin:1px 0}.row.mine{flex-direction:row-reverse}.row.tail{margin-bottom:8px}.avatar{width:28px;height:28px;border-radius:50%;overflow:hidden;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:var(--ion-color-primary);color:#fff;font-size:.6875rem;font-weight:700}.avatar.hidden{visibility:hidden}.avatar img{width:100%;height:100%;object-fit:cover}.bubble-wrap{position:relative;max-width:min(78%,520px);display:flex;flex-direction:column}.row.mine .bubble-wrap{align-items:flex-end}.bubble{position:relative;padding:7px 11px;border-radius:16px;background:var(--ion-card-background, var(--ion-background-color, #fff));border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));color:var(--ion-text-color, #000);font-size:.9375rem;line-height:1.35;cursor:pointer;outline:none;word-break:break-word}.row:not(.mine).tail .bubble{border-bottom-left-radius:5px}.mine .bubble{background:var(--ion-color-primary);border-color:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff)}.mine.tail .bubble{border-bottom-right-radius:5px}.bubble.deleted{background:transparent;border-style:dashed;cursor:default}.name{display:block;font-size:.75rem;font-weight:700;color:var(--ion-color-primary);margin-bottom:2px}.reply-preview{display:flex;flex-direction:column;gap:1px;padding:4px 8px;margin-bottom:4px;border-left:3px solid currentColor;border-radius:6px;background:var(--ion-color-step-100, rgba(127, 127, 127, .12));font-size:.8125rem;opacity:.85}.reply-name{font-weight:700}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}.body{white-space:pre-wrap}.deleted-text{font-style:italic;opacity:.6}.attachments{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.att-image{max-width:100%;border-radius:10px;display:block}.files{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.file-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .12));color:inherit;text-decoration:none;font-size:.8125rem}.meta{display:inline-flex;align-items:center;gap:4px;float:right;margin:2px 0 -2px 8px;font-size:.6875rem;opacity:.7}.edited{font-style:italic}.status{font-size:.875rem}.status.read{color:var(--ion-color-secondary, #4fc3f7);opacity:1}.status.failed{color:var(--ion-color-danger, #eb445a);opacity:1}.reactions{display:flex;flex-wrap:wrap;gap:4px;margin-top:3px}.reaction{display:inline-flex;align-items:center;gap:3px;padding:1px 7px;border-radius:999px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff));font-size:.75rem;cursor:pointer;color:var(--ion-text-color, #000)}.reaction.active{border-color:var(--ion-color-primary);background:var(--ion-color-primary-tint, var(--ion-color-primary));color:var(--ion-color-primary-contrast, #fff)}.reaction .count{opacity:.75}.actions{position:absolute;top:-14px;display:flex;gap:2px;padding:2px;border-radius:999px;background:var(--ion-card-background, var(--ion-background-color, #fff));border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));box-shadow:0 2px 8px #0000001f;opacity:0;pointer-events:none;transition:opacity .12s ease;z-index:2}.row.mine .actions{right:0}.row:not(.mine) .actions{left:0}.bubble-wrap:hover .actions,.actions.open{opacity:1;pointer-events:auto}.act{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1rem}.act:hover{background:var(--ion-color-step-100, rgba(127, 127, 127, .12));color:var(--ion-text-color, #000)}.act.danger:hover{color:var(--ion-color-danger, #eb445a)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }] }); }
72558
72577
  }
72559
72578
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageBubbleComponent, decorators: [{
72560
72579
  type: Component,
@@ -72599,13 +72618,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72599
72618
  }
72600
72619
  </div>
72601
72620
  }
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
72621
  @if (fileAttachments().length > 0) {
72610
72622
  <div class="files">
72611
72623
  @for (att of fileAttachments(); track att.url) {
@@ -72669,7 +72681,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72669
72681
  }
72670
72682
  </div>
72671
72683
  </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"] }]
72684
+ `, styles: [":host{display:block}.row{display:flex;align-items:flex-end;gap:8px;margin:1px 0}.row.mine{flex-direction:row-reverse}.row.tail{margin-bottom:8px}.avatar{width:28px;height:28px;border-radius:50%;overflow:hidden;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:var(--ion-color-primary);color:#fff;font-size:.6875rem;font-weight:700}.avatar.hidden{visibility:hidden}.avatar img{width:100%;height:100%;object-fit:cover}.bubble-wrap{position:relative;max-width:min(78%,520px);display:flex;flex-direction:column}.row.mine .bubble-wrap{align-items:flex-end}.bubble{position:relative;padding:7px 11px;border-radius:16px;background:var(--ion-card-background, var(--ion-background-color, #fff));border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));color:var(--ion-text-color, #000);font-size:.9375rem;line-height:1.35;cursor:pointer;outline:none;word-break:break-word}.row:not(.mine).tail .bubble{border-bottom-left-radius:5px}.mine .bubble{background:var(--ion-color-primary);border-color:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff)}.mine.tail .bubble{border-bottom-right-radius:5px}.bubble.deleted{background:transparent;border-style:dashed;cursor:default}.name{display:block;font-size:.75rem;font-weight:700;color:var(--ion-color-primary);margin-bottom:2px}.reply-preview{display:flex;flex-direction:column;gap:1px;padding:4px 8px;margin-bottom:4px;border-left:3px solid currentColor;border-radius:6px;background:var(--ion-color-step-100, rgba(127, 127, 127, .12));font-size:.8125rem;opacity:.85}.reply-name{font-weight:700}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}.body{white-space:pre-wrap}.deleted-text{font-style:italic;opacity:.6}.attachments{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.att-image{max-width:100%;border-radius:10px;display:block}.files{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.file-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .12));color:inherit;text-decoration:none;font-size:.8125rem}.meta{display:inline-flex;align-items:center;gap:4px;float:right;margin:2px 0 -2px 8px;font-size:.6875rem;opacity:.7}.edited{font-style:italic}.status{font-size:.875rem}.status.read{color:var(--ion-color-secondary, #4fc3f7);opacity:1}.status.failed{color:var(--ion-color-danger, #eb445a);opacity:1}.reactions{display:flex;flex-wrap:wrap;gap:4px;margin-top:3px}.reaction{display:inline-flex;align-items:center;gap:3px;padding:1px 7px;border-radius:999px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff));font-size:.75rem;cursor:pointer;color:var(--ion-text-color, #000)}.reaction.active{border-color:var(--ion-color-primary);background:var(--ion-color-primary-tint, var(--ion-color-primary));color:var(--ion-color-primary-contrast, #fff)}.reaction .count{opacity:.75}.actions{position:absolute;top:-14px;display:flex;gap:2px;padding:2px;border-radius:999px;background:var(--ion-card-background, var(--ion-background-color, #fff));border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));box-shadow:0 2px 8px #0000001f;opacity:0;pointer-events:none;transition:opacity .12s ease;z-index:2}.row.mine .actions{right:0}.row:not(.mine) .actions{left:0}.bubble-wrap:hover .actions,.actions.open{opacity:1;pointer-events:auto}.act{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1rem}.act:hover{background:var(--ion-color-step-100, rgba(127, 127, 127, .12));color:var(--ion-text-color, #000)}.act.danger:hover{color:var(--ion-color-danger, #eb445a)}\n"] }]
72673
72685
  }], ctorParameters: () => [] });
72674
72686
 
72675
72687
  addIcons({ sendOutline });
@@ -72822,37 +72834,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72822
72834
  type: Output
72823
72835
  }] } });
72824
72836
 
72825
- addIcons({ addOutline, checkmarkOutline, closeOutline, documentOutline, micOutline, arrowUpOutline, trashOutline });
72837
+ addIcons({ addOutline, closeOutline, happyOutline, micOutline, arrowUpOutline });
72826
72838
  const CHAT_COMPOSER_I18N = {
72827
72839
  es: {
72828
72840
  placeholder: 'Escribe un mensaje',
72829
72841
  send: 'Enviar',
72830
72842
  attach: 'Adjuntar',
72843
+ emoji: 'Emoji',
72831
72844
  voice: 'Mensaje de voz',
72832
- done: 'Listo',
72833
72845
  replyingTo: 'Respondiendo a',
72834
72846
  cancel: 'Cancelar',
72835
- remove: 'Quitar',
72836
72847
  },
72837
72848
  en: {
72838
72849
  placeholder: 'Write a message',
72839
72850
  send: 'Send',
72840
72851
  attach: 'Attach',
72852
+ emoji: 'Emoji',
72841
72853
  voice: 'Voice message',
72842
- done: 'Done',
72843
72854
  replyingTo: 'Replying to',
72844
72855
  cancel: 'Cancel',
72845
- remove: 'Remove',
72846
72856
  },
72847
72857
  };
72858
+ const QUICK_EMOJIS = ['👍', '❤️', '😂', '🎉', '🙏', '🔥', '😮', '😢'];
72848
72859
  /**
72849
72860
  * val-chat-composer — barra de redacción de chat moderna (signal inputs).
72850
72861
  *
72851
- * Pill con: adjuntar (+), textarea auto-grow, y mic/enviar (mic cuando está vacío,
72852
- * enviar cuando hay texto o adjuntos). Los adjuntos (botón + / pegar imagen) se
72853
- * acumulan como PREVIEW (miniaturas) y se emiten por `(attach)` recién al ENVIAR
72854
- * no se van solos al chat. Audio grabado se emite directo al parar.
72855
- * No incluye picker de emoji: el teclado del sistema ya lo trae.
72862
+ * Pill con: adjuntar (+), textarea auto-grow, emoji (fila rápida) y mic/enviar
72863
+ * (mic cuando está vacío, enviar cuando hay texto patrón WhatsApp/Telegram).
72864
+ * Muestra una barra de contexto "respondiendo a" cuando `replyingTo` está seteado.
72865
+ * `typing` se emite con debounce (máx. 1 vez cada 2s) para no spamear writes.
72856
72866
  */
72857
72867
  class ChatComposerComponent {
72858
72868
  constructor() {
@@ -72862,30 +72872,20 @@ class ChatComposerComponent {
72862
72872
  this.maxLength = input(2000);
72863
72873
  this.replyingTo = input(null);
72864
72874
  this.showAttach = input(true);
72875
+ this.showEmoji = input(true);
72865
72876
  this.showMic = input(true);
72866
72877
  this.send = output();
72867
72878
  this.typing = output();
72879
+ this.attach = output();
72868
72880
  this.voice = output();
72869
72881
  this.cancelReply = output();
72870
72882
  this.body = signal('');
72871
- this.pending = signal([]);
72872
- this.pendingId = 0;
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.emojiOpen = signal(false);
72884
+ this.quickEmojis = QUICK_EMOJIS;
72883
72885
  this.lastTypingEmit = 0;
72884
72886
  this.canSend = computed(() => {
72885
72887
  const t = this.body().trim();
72886
- if (this.disabled())
72887
- return false;
72888
- return (t.length > 0 && t.length <= this.maxLength()) || this.pending().length > 0;
72888
+ return t.length > 0 && t.length <= this.maxLength() && !this.disabled();
72889
72889
  });
72890
72890
  this.placeholderText = computed(() => {
72891
72891
  this.i18n.lang();
@@ -72905,62 +72905,29 @@ class ChatComposerComponent {
72905
72905
  }
72906
72906
  }
72907
72907
  onSend() {
72908
- if (!this.canSend())
72908
+ const trimmed = this.body().trim();
72909
+ if (!trimmed || trimmed.length > this.maxLength())
72909
72910
  return;
72910
- // Un solo evento: texto (caption) + todos los adjuntos staged. El consumer
72911
- // los sube y manda como UN mensaje con body + attachments.
72912
- const files = this.pending().map(p => p.file);
72913
- const text = this.body().trim();
72914
- this.send.emit({ text, files });
72915
- for (const att of this.pending()) {
72916
- if (att.url)
72917
- URL.revokeObjectURL(att.url);
72918
- }
72919
- this.pending.set([]);
72911
+ this.send.emit(trimmed);
72920
72912
  this.body.set('');
72913
+ this.emojiOpen.set(false);
72914
+ }
72915
+ toggleEmoji() {
72916
+ this.emojiOpen.update(v => !v);
72917
+ }
72918
+ insertEmoji(emoji) {
72919
+ this.body.update(v => v + emoji);
72921
72920
  }
72922
72921
  onFile(event) {
72923
72922
  const input = event.target;
72924
72923
  const file = input.files?.[0];
72925
72924
  if (file)
72926
- this.addPending(file);
72925
+ this.attach.emit(file);
72927
72926
  input.value = '';
72928
72927
  }
72929
- /** Pegar imagen desde el portapapeles (captura de pantalla, copiar imagen). */
72930
- onPaste(event) {
72931
- const items = event.clipboardData?.items;
72932
- if (!items)
72933
- return;
72934
- for (const item of Array.from(items)) {
72935
- if (item.type.startsWith('image/')) {
72936
- const file = item.getAsFile();
72937
- if (file) {
72938
- event.preventDefault();
72939
- this.addPending(file);
72940
- }
72941
- return;
72942
- }
72943
- }
72944
- }
72945
- addPending(file) {
72946
- const kind = file.type.startsWith('image/')
72947
- ? 'image'
72948
- : file.type.startsWith('audio/')
72949
- ? 'audio'
72950
- : 'file';
72951
- const url = kind === 'file' ? undefined : URL.createObjectURL(file);
72952
- this.pending.update(arr => [...arr, { id: this.pendingId++, file, url, kind }]);
72953
- }
72954
- removePending(id) {
72955
- this.pending.update(arr => {
72956
- const found = arr.find(a => a.id === id);
72957
- if (found?.url)
72958
- URL.revokeObjectURL(found.url);
72959
- return arr.filter(a => a.id !== id);
72960
- });
72961
- }
72962
72928
  /**
72963
- * Emite typing como máximo 1 vez cada 2s. performance.now es seguro en runtime.
72929
+ * Emite typing como máximo 1 vez cada 2s. No usa Date.now() directo en plantillas
72930
+ * de workflow; aquí estamos en runtime de navegador, performance.now es seguro.
72964
72931
  */
72965
72932
  emitTypingDebounced() {
72966
72933
  const now = performance.now();
@@ -72969,95 +72936,11 @@ class ChatComposerComponent {
72969
72936
  this.typing.emit();
72970
72937
  }
72971
72938
  }
72972
- // ─── Grabación de audio ───────────────────────────────────────────────────
72973
- /** Selecciona un mimeType de audio soportado por el navegador. */
72974
- pickAudioMime() {
72975
- const candidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4'];
72976
- const MR = window.MediaRecorder;
72977
- if (!MR?.isTypeSupported)
72978
- return '';
72979
- return candidates.find(c => MR.isTypeSupported(c)) ?? '';
72980
- }
72981
- async startRecording() {
72982
- if (this.recording())
72983
- return;
72984
- if (!navigator.mediaDevices?.getUserMedia || !('MediaRecorder' in window)) {
72985
- this.voice.emit(); // sin soporte: deja que el consumer decida (fallback)
72986
- return;
72987
- }
72988
- try {
72989
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
72990
- this.recStream = stream;
72991
- const mimeType = this.pickAudioMime();
72992
- this.recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined);
72993
- this.recChunks = [];
72994
- this.recorder.ondataavailable = e => {
72995
- if (e.data.size > 0)
72996
- this.recChunks.push(e.data);
72997
- };
72998
- this.recorder.start();
72999
- this.recording.set(true);
73000
- this.recSeconds.set(0);
73001
- this.recTimer = setInterval(() => this.recSeconds.update(s => s + 1), 1000);
73002
- }
73003
- catch {
73004
- this.cleanupRecording();
73005
- }
73006
- }
73007
- /** Para la grabación y la stagea como adjunto pendiente (con preview de audio). */
73008
- finishRecording() {
73009
- const rec = this.recorder;
73010
- if (!rec) {
73011
- this.cleanupRecording();
73012
- return;
73013
- }
73014
- rec.onstop = () => {
73015
- const type = rec.mimeType || 'audio/webm';
73016
- const blob = new Blob(this.recChunks, { type });
73017
- const ext = type.includes('mp4') ? 'm4a' : 'webm';
73018
- const file = new File([blob], `audio-${this.recSeconds()}s.${ext}`, { type });
73019
- if (blob.size > 0)
73020
- this.addPending(file);
73021
- this.cleanupRecording();
73022
- };
73023
- rec.stop();
73024
- }
73025
- cancelRecording() {
73026
- const rec = this.recorder;
73027
- if (rec)
73028
- rec.onstop = () => this.cleanupRecording();
73029
- try {
73030
- rec?.stop();
73031
- }
73032
- catch {
73033
- this.cleanupRecording();
73034
- }
73035
- if (!rec)
73036
- this.cleanupRecording();
73037
- }
73038
- cleanupRecording() {
73039
- if (this.recTimer)
73040
- clearInterval(this.recTimer);
73041
- this.recTimer = undefined;
73042
- this.recStream?.getTracks().forEach(t => t.stop());
73043
- this.recStream = undefined;
73044
- this.recorder = undefined;
73045
- this.recChunks = [];
73046
- this.recording.set(false);
73047
- this.recSeconds.set(0);
73048
- }
73049
- ngOnDestroy() {
73050
- this.cleanupRecording();
73051
- for (const att of this.pending()) {
73052
- if (att.url)
73053
- URL.revokeObjectURL(att.url);
73054
- }
73055
- }
73056
72939
  t(key) {
73057
72940
  return this.i18n.t(key, 'ChatComposer');
73058
72941
  }
73059
72942
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatComposerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
73060
- 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 }, showMic: { classPropertyName: "showMic", publicName: "showMic", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { send: "send", typing: "typing", voice: "voice", cancelReply: "cancelReply" }, ngImport: i0, template: `
72943
+ 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: `
73061
72944
  <div class="composer">
73062
72945
  @if (replyingTo()) {
73063
72946
  <div class="reply-bar">
@@ -73071,84 +72954,52 @@ class ChatComposerComponent {
73071
72954
  </div>
73072
72955
  }
73073
72956
 
73074
- @if (pending().length > 0) {
73075
- <div class="pending-row">
73076
- @for (att of pending(); track att.id) {
73077
- @if (att.kind === 'audio') {
73078
- <div class="audio-chip">
73079
- <audio controls preload="metadata" [src]="att.url"></audio>
73080
- <button class="thumb-remove inline" [attr.aria-label]="t('remove')" (click)="removePending(att.id)">
73081
- <ion-icon name="close-outline" aria-hidden="true" />
73082
- </button>
73083
- </div>
73084
- } @else {
73085
- <div class="thumb" [class.file]="att.kind === 'file'">
73086
- @if (att.kind === 'image') {
73087
- <img [src]="att.url" [alt]="att.file.name" />
73088
- } @else {
73089
- <ion-icon name="document-outline" aria-hidden="true" />
73090
- <span class="thumb-name">{{ att.file.name }}</span>
73091
- }
73092
- <button class="thumb-remove" [attr.aria-label]="t('remove')" (click)="removePending(att.id)">
73093
- <ion-icon name="close-outline" aria-hidden="true" />
73094
- </button>
73095
- </div>
73096
- }
72957
+ @if (emojiOpen()) {
72958
+ <div class="emoji-row">
72959
+ @for (e of quickEmojis; track e) {
72960
+ <button class="emoji" (click)="insertEmoji(e)">{{ e }}</button>
73097
72961
  }
73098
72962
  </div>
73099
72963
  }
73100
72964
 
73101
- @if (recording()) {
73102
- <div class="rec-bar">
73103
- <button class="icon-btn danger" [attr.aria-label]="t('cancel')" (click)="cancelRecording()">
73104
- <ion-icon name="trash-outline" aria-hidden="true" />
73105
- </button>
73106
- <span class="rec-dot"></span>
73107
- <span class="rec-time">{{ recTimeLabel() }}</span>
73108
- <span class="rec-spacer"></span>
73109
- <button class="send-btn" [attr.aria-label]="t('done')" (click)="finishRecording()">
73110
- <ion-icon name="checkmark-outline" aria-hidden="true" />
73111
- </button>
73112
- </div>
73113
- } @else {
73114
- <div class="pill">
73115
- @if (showAttach()) {
73116
- <label class="icon-btn" [attr.aria-label]="t('attach')">
73117
- <ion-icon name="add-outline" aria-hidden="true" />
73118
- <input type="file" hidden (change)="onFile($event)" accept="image/*,application/pdf" />
73119
- </label>
73120
- }
72965
+ <div class="pill">
72966
+ @if (showAttach()) {
72967
+ <label class="icon-btn" [attr.aria-label]="t('attach')">
72968
+ <ion-icon name="add-outline" aria-hidden="true" />
72969
+ <input type="file" hidden (change)="onFile($event)" accept="image/*,application/pdf" />
72970
+ </label>
72971
+ }
73121
72972
 
73122
- <ion-textarea
73123
- class="field"
73124
- [placeholder]="placeholderText()"
73125
- [disabled]="disabled()"
73126
- [maxlength]="maxLength()"
73127
- [autoGrow]="true"
73128
- [rows]="1"
73129
- [value]="body()"
73130
- [spellcheck]="false"
73131
- autocapitalize="sentences"
73132
- autocorrect="off"
73133
- enterkeyhint="enter"
73134
- (ionInput)="onInput($event)"
73135
- (keydown)="onKeydown($event)"
73136
- (paste)="onPaste($event)"
73137
- ></ion-textarea>
72973
+ <ion-textarea
72974
+ class="field"
72975
+ [placeholder]="placeholderText()"
72976
+ [disabled]="disabled()"
72977
+ [maxlength]="maxLength()"
72978
+ [autoGrow]="true"
72979
+ [rows]="1"
72980
+ [value]="body()"
72981
+ (ionInput)="onInput($event)"
72982
+ (keydown)="onKeydown($event)"
72983
+ ></ion-textarea>
73138
72984
 
73139
- @if (canSend()) {
73140
- <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73141
- <ion-icon name="arrow-up-outline" aria-hidden="true" />
73142
- </button>
73143
- } @else if (showMic()) {
73144
- <button class="icon-btn mic" [attr.aria-label]="t('voice')" (click)="startRecording()">
73145
- <ion-icon name="mic-outline" aria-hidden="true" />
73146
- </button>
73147
- }
73148
- </div>
73149
- }
72985
+ @if (showEmoji()) {
72986
+ <button class="icon-btn" [class.active]="emojiOpen()" [attr.aria-label]="t('emoji')" (click)="toggleEmoji()">
72987
+ <ion-icon name="happy-outline" aria-hidden="true" />
72988
+ </button>
72989
+ }
72990
+
72991
+ @if (canSend()) {
72992
+ <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
72993
+ <ion-icon name="arrow-up-outline" aria-hidden="true" />
72994
+ </button>
72995
+ } @else if (showMic()) {
72996
+ <button class="icon-btn mic" [attr.aria-label]="t('voice')" (click)="voice.emit()">
72997
+ <ion-icon name="mic-outline" aria-hidden="true" />
72998
+ </button>
72999
+ }
73000
+ </div>
73150
73001
  </div>
73151
- `, 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)}.pending-row{display:flex;gap:8px;flex-wrap:wrap;padding:2px 4px}.thumb{position:relative;width:64px;height:64px;border-radius:10px;overflow:hidden;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.thumb img{width:100%;height:100%;object-fit:cover}.thumb.file{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:4px;color:var(--ion-color-medium, #92949c);font-size:1.5rem}.thumb-name{font-size:.5625rem;line-height:1.1;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ion-text-color, #000)}.thumb-remove{position:absolute;top:2px;right:2px;width:20px;height:20px;border:none;border-radius:50%;background:#0000008c;color:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:.875rem}.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{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)}.audio-chip{display:flex;align-items:center;gap:6px;width:100%;padding:4px 6px;border-radius:12px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.audio-chip audio{flex:1;height:36px;min-width:0}.thumb-remove.inline{position:static;flex-shrink:0;background:var(--ion-color-medium, #92949c)}.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"] }] }); }
73002
+ `, isInline: true, styles: [":host{display:block}.composer{display:flex;flex-direction:column;gap:6px;padding:8px 10px calc(8px + env(safe-area-inset-bottom,0px));background:var(--ion-background-color, #fff);border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}.reply-bar{display:flex;align-items:center;gap:8px;padding:6px 10px;border-left:3px solid var(--ion-color-primary);border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.reply-content{display:flex;flex-direction:column;flex:1;min-width:0;font-size:.8125rem}.reply-name{font-weight:700;color:var(--ion-color-primary)}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ion-color-medium, #92949c)}.emoji-row{display:flex;gap:4px;flex-wrap:wrap}.emoji{border:none;background:var(--ion-color-step-100, rgba(127, 127, 127, .1));border-radius:10px;padding:4px 8px;font-size:1.25rem;cursor:pointer}.pill{display:flex;align-items:flex-end;gap:4px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.field{flex:1;--background: transparent;--padding-start: 6px;--padding-end: 6px;--padding-top: 8px;--padding-bottom: 8px;margin:0;max-height:120px;font-size:.9375rem}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1.375rem}.icon-btn:hover,.icon-btn.active,.icon-btn.mic:hover{color:var(--ion-color-primary)}.send-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff);cursor:pointer;font-size:1.25rem;transition:transform .1s ease}.send-btn:active{transform:scale(.92)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }] }); }
73152
73003
  }
73153
73004
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatComposerComponent, decorators: [{
73154
73005
  type: Component,
@@ -73166,84 +73017,52 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
73166
73017
  </div>
73167
73018
  }
73168
73019
 
73169
- @if (pending().length > 0) {
73170
- <div class="pending-row">
73171
- @for (att of pending(); track att.id) {
73172
- @if (att.kind === 'audio') {
73173
- <div class="audio-chip">
73174
- <audio controls preload="metadata" [src]="att.url"></audio>
73175
- <button class="thumb-remove inline" [attr.aria-label]="t('remove')" (click)="removePending(att.id)">
73176
- <ion-icon name="close-outline" aria-hidden="true" />
73177
- </button>
73178
- </div>
73179
- } @else {
73180
- <div class="thumb" [class.file]="att.kind === 'file'">
73181
- @if (att.kind === 'image') {
73182
- <img [src]="att.url" [alt]="att.file.name" />
73183
- } @else {
73184
- <ion-icon name="document-outline" aria-hidden="true" />
73185
- <span class="thumb-name">{{ att.file.name }}</span>
73186
- }
73187
- <button class="thumb-remove" [attr.aria-label]="t('remove')" (click)="removePending(att.id)">
73188
- <ion-icon name="close-outline" aria-hidden="true" />
73189
- </button>
73190
- </div>
73191
- }
73020
+ @if (emojiOpen()) {
73021
+ <div class="emoji-row">
73022
+ @for (e of quickEmojis; track e) {
73023
+ <button class="emoji" (click)="insertEmoji(e)">{{ e }}</button>
73192
73024
  }
73193
73025
  </div>
73194
73026
  }
73195
73027
 
73196
- @if (recording()) {
73197
- <div class="rec-bar">
73198
- <button class="icon-btn danger" [attr.aria-label]="t('cancel')" (click)="cancelRecording()">
73199
- <ion-icon name="trash-outline" aria-hidden="true" />
73200
- </button>
73201
- <span class="rec-dot"></span>
73202
- <span class="rec-time">{{ recTimeLabel() }}</span>
73203
- <span class="rec-spacer"></span>
73204
- <button class="send-btn" [attr.aria-label]="t('done')" (click)="finishRecording()">
73205
- <ion-icon name="checkmark-outline" aria-hidden="true" />
73206
- </button>
73207
- </div>
73208
- } @else {
73209
- <div class="pill">
73210
- @if (showAttach()) {
73211
- <label class="icon-btn" [attr.aria-label]="t('attach')">
73212
- <ion-icon name="add-outline" aria-hidden="true" />
73213
- <input type="file" hidden (change)="onFile($event)" accept="image/*,application/pdf" />
73214
- </label>
73215
- }
73028
+ <div class="pill">
73029
+ @if (showAttach()) {
73030
+ <label class="icon-btn" [attr.aria-label]="t('attach')">
73031
+ <ion-icon name="add-outline" aria-hidden="true" />
73032
+ <input type="file" hidden (change)="onFile($event)" accept="image/*,application/pdf" />
73033
+ </label>
73034
+ }
73216
73035
 
73217
- <ion-textarea
73218
- class="field"
73219
- [placeholder]="placeholderText()"
73220
- [disabled]="disabled()"
73221
- [maxlength]="maxLength()"
73222
- [autoGrow]="true"
73223
- [rows]="1"
73224
- [value]="body()"
73225
- [spellcheck]="false"
73226
- autocapitalize="sentences"
73227
- autocorrect="off"
73228
- enterkeyhint="enter"
73229
- (ionInput)="onInput($event)"
73230
- (keydown)="onKeydown($event)"
73231
- (paste)="onPaste($event)"
73232
- ></ion-textarea>
73036
+ <ion-textarea
73037
+ class="field"
73038
+ [placeholder]="placeholderText()"
73039
+ [disabled]="disabled()"
73040
+ [maxlength]="maxLength()"
73041
+ [autoGrow]="true"
73042
+ [rows]="1"
73043
+ [value]="body()"
73044
+ (ionInput)="onInput($event)"
73045
+ (keydown)="onKeydown($event)"
73046
+ ></ion-textarea>
73233
73047
 
73234
- @if (canSend()) {
73235
- <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73236
- <ion-icon name="arrow-up-outline" aria-hidden="true" />
73237
- </button>
73238
- } @else if (showMic()) {
73239
- <button class="icon-btn mic" [attr.aria-label]="t('voice')" (click)="startRecording()">
73240
- <ion-icon name="mic-outline" aria-hidden="true" />
73241
- </button>
73242
- }
73243
- </div>
73244
- }
73048
+ @if (showEmoji()) {
73049
+ <button class="icon-btn" [class.active]="emojiOpen()" [attr.aria-label]="t('emoji')" (click)="toggleEmoji()">
73050
+ <ion-icon name="happy-outline" aria-hidden="true" />
73051
+ </button>
73052
+ }
73053
+
73054
+ @if (canSend()) {
73055
+ <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73056
+ <ion-icon name="arrow-up-outline" aria-hidden="true" />
73057
+ </button>
73058
+ } @else if (showMic()) {
73059
+ <button class="icon-btn mic" [attr.aria-label]="t('voice')" (click)="voice.emit()">
73060
+ <ion-icon name="mic-outline" aria-hidden="true" />
73061
+ </button>
73062
+ }
73063
+ </div>
73245
73064
  </div>
73246
- `, 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)}.pending-row{display:flex;gap:8px;flex-wrap:wrap;padding:2px 4px}.thumb{position:relative;width:64px;height:64px;border-radius:10px;overflow:hidden;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.thumb img{width:100%;height:100%;object-fit:cover}.thumb.file{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:4px;color:var(--ion-color-medium, #92949c);font-size:1.5rem}.thumb-name{font-size:.5625rem;line-height:1.1;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ion-text-color, #000)}.thumb-remove{position:absolute;top:2px;right:2px;width:20px;height:20px;border:none;border-radius:50%;background:#0000008c;color:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:.875rem}.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{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)}.audio-chip{display:flex;align-items:center;gap:6px;width:100%;padding:4px 6px;border-radius:12px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.audio-chip audio{flex:1;height:36px;min-width:0}.thumb-remove.inline{position:static;flex-shrink:0;background:var(--ion-color-medium, #92949c)}.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"] }]
73065
+ `, styles: [":host{display:block}.composer{display:flex;flex-direction:column;gap:6px;padding:8px 10px calc(8px + env(safe-area-inset-bottom,0px));background:var(--ion-background-color, #fff);border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}.reply-bar{display:flex;align-items:center;gap:8px;padding:6px 10px;border-left:3px solid var(--ion-color-primary);border-radius:8px;background:var(--ion-color-step-100, rgba(127, 127, 127, .1))}.reply-content{display:flex;flex-direction:column;flex:1;min-width:0;font-size:.8125rem}.reply-name{font-weight:700;color:var(--ion-color-primary)}.reply-body{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ion-color-medium, #92949c)}.emoji-row{display:flex;gap:4px;flex-wrap:wrap}.emoji{border:none;background:var(--ion-color-step-100, rgba(127, 127, 127, .1));border-radius:10px;padding:4px 8px;font-size:1.25rem;cursor:pointer}.pill{display:flex;align-items:flex-end;gap:4px;background:var(--ion-color-light, #f4f5f8);border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08));border-radius:24px;padding:2px 6px}.field{flex:1;--background: transparent;--padding-start: 6px;--padding-end: 6px;--padding-top: 8px;--padding-bottom: 8px;margin:0;max-height:120px;font-size:.9375rem}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:transparent;color:var(--ion-color-medium, #92949c);cursor:pointer;font-size:1.375rem}.icon-btn:hover,.icon-btn.active,.icon-btn.mic:hover{color:var(--ion-color-primary)}.send-btn{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:none;border-radius:50%;background:var(--ion-color-primary);color:var(--ion-color-primary-contrast, #fff);cursor:pointer;font-size:1.25rem;transition:transform .1s ease}.send-btn:active{transform:scale(.92)}\n"] }]
73247
73066
  }], ctorParameters: () => [] });
73248
73067
 
73249
73068
  /**
@@ -73463,6 +73282,7 @@ class ChatWindowComponent {
73463
73282
  this.replyTo = output();
73464
73283
  this.editMessage = output();
73465
73284
  this.typing = output();
73285
+ this.attach = output();
73466
73286
  this.voice = output();
73467
73287
  this.msgsEl = viewChild('msgs');
73468
73288
  this.replyingTo = signal(null);
@@ -73546,8 +73366,8 @@ class ChatWindowComponent {
73546
73366
  this.atBottom.set(true);
73547
73367
  this.hasNew.set(false);
73548
73368
  }
73549
- onSend(event) {
73550
- this.sendMessage.emit(event);
73369
+ onSend(body) {
73370
+ this.sendMessage.emit(body);
73551
73371
  this.replyingTo.set(null);
73552
73372
  }
73553
73373
  onAction(event) {
@@ -73575,7 +73395,7 @@ class ChatWindowComponent {
73575
73395
  return this.i18n.t(key, 'ChatWindow');
73576
73396
  }
73577
73397
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatWindowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
73578
- 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", voice: "voice" }, viewQueries: [{ propertyName: "msgsEl", first: true, predicate: ["msgs"], descendants: true, isSignal: true }], ngImport: i0, template: `
73398
+ 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: `
73579
73399
  <div class="chat">
73580
73400
  <div #msgs class="messages" (scroll)="onScroll()">
73581
73401
  @if (isLoading()) {
@@ -73622,6 +73442,7 @@ class ChatWindowComponent {
73622
73442
  [replyingTo]="replyingTo()"
73623
73443
  (send)="onSend($event)"
73624
73444
  (typing)="typing.emit()"
73445
+ (attach)="attach.emit($event)"
73625
73446
  (voice)="voice.emit()"
73626
73447
  (cancelReply)="replyingTo.set(null)"
73627
73448
  />
@@ -73629,7 +73450,7 @@ class ChatWindowComponent {
73629
73450
  <div class="closed-banner">{{ t('conversationClosed') }}</div>
73630
73451
  }
73631
73452
  </div>
73632
- `, 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", "showMic"], outputs: ["send", "typing", "voice", "cancelReply"] }, { kind: "component", type: TypingIndicatorComponent, selector: "val-typing-indicator", inputs: ["typingUsers"] }] }); }
73453
+ `, 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"] }] }); }
73633
73454
  }
73634
73455
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatWindowComponent, decorators: [{
73635
73456
  type: Component,
@@ -73680,6 +73501,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
73680
73501
  [replyingTo]="replyingTo()"
73681
73502
  (send)="onSend($event)"
73682
73503
  (typing)="typing.emit()"
73504
+ (attach)="attach.emit($event)"
73683
73505
  (voice)="voice.emit()"
73684
73506
  (cancelReply)="replyingTo.set(null)"
73685
73507
  />
@@ -73793,13 +73615,11 @@ class ThreadPanelComponent {
73793
73615
  this.conversationSvc.stopTyping(conv.convId, orgId, appId);
73794
73616
  }
73795
73617
  }
73796
- onSend(event) {
73618
+ onSend(body) {
73797
73619
  const conv = this.conv();
73798
73620
  if (!conv || !this.conversationSvc)
73799
73621
  return;
73800
- // thread-panel no tiene pipeline de subida de adjuntos; solo envía el texto.
73801
- if (event.text)
73802
- this.conversationSvc.sendMessage(conv.convId, event.text).subscribe();
73622
+ this.conversationSvc.sendMessage(conv.convId, body).subscribe();
73803
73623
  }
73804
73624
  onReactionClick(event) {
73805
73625
  // Reacciones se implementan a nivel de app consumer
@@ -73848,7 +73668,7 @@ class ThreadPanelComponent {
73848
73668
  </div>
73849
73669
  }
73850
73670
  </div>
73851
- `, 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", "voice"] }] }); }
73671
+ `, 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"] }] }); }
73852
73672
  }
73853
73673
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ThreadPanelComponent, decorators: [{
73854
73674
  type: Component,