valtech-components 4.0.229 → 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.229';
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,35 +72834,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72822
72834
  type: Output
72823
72835
  }] } });
72824
72836
 
72825
- addIcons({ addOutline, 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
72845
  replyingTo: 'Respondiendo a',
72833
72846
  cancel: 'Cancelar',
72834
- remove: 'Quitar',
72835
72847
  },
72836
72848
  en: {
72837
72849
  placeholder: 'Write a message',
72838
72850
  send: 'Send',
72839
72851
  attach: 'Attach',
72852
+ emoji: 'Emoji',
72840
72853
  voice: 'Voice message',
72841
72854
  replyingTo: 'Replying to',
72842
72855
  cancel: 'Cancel',
72843
- remove: 'Remove',
72844
72856
  },
72845
72857
  };
72858
+ const QUICK_EMOJIS = ['👍', '❤️', '😂', '🎉', '🙏', '🔥', '😮', '😢'];
72846
72859
  /**
72847
72860
  * val-chat-composer — barra de redacción de chat moderna (signal inputs).
72848
72861
  *
72849
- * Pill con: adjuntar (+), textarea auto-grow, y mic/enviar (mic cuando está vacío,
72850
- * enviar cuando hay texto o adjuntos). Los adjuntos (botón + / pegar imagen) se
72851
- * acumulan como PREVIEW (miniaturas) y se emiten por `(attach)` recién al ENVIAR
72852
- * no se van solos al chat. Audio grabado se emite directo al parar.
72853
- * 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.
72854
72866
  */
72855
72867
  class ChatComposerComponent {
72856
72868
  constructor() {
@@ -72860,6 +72872,7 @@ class ChatComposerComponent {
72860
72872
  this.maxLength = input(2000);
72861
72873
  this.replyingTo = input(null);
72862
72874
  this.showAttach = input(true);
72875
+ this.showEmoji = input(true);
72863
72876
  this.showMic = input(true);
72864
72877
  this.send = output();
72865
72878
  this.typing = output();
@@ -72867,24 +72880,12 @@ class ChatComposerComponent {
72867
72880
  this.voice = output();
72868
72881
  this.cancelReply = output();
72869
72882
  this.body = signal('');
72870
- this.pending = signal([]);
72871
- this.pendingId = 0;
72872
- // Grabación de audio (MediaRecorder).
72873
- this.recording = signal(false);
72874
- this.recSeconds = signal(0);
72875
- this.recChunks = [];
72876
- this.recTimeLabel = computed(() => {
72877
- const s = this.recSeconds();
72878
- const mm = String(Math.floor(s / 60)).padStart(2, '0');
72879
- const ss = String(s % 60).padStart(2, '0');
72880
- return `${mm}:${ss}`;
72881
- });
72883
+ this.emojiOpen = signal(false);
72884
+ this.quickEmojis = QUICK_EMOJIS;
72882
72885
  this.lastTypingEmit = 0;
72883
72886
  this.canSend = computed(() => {
72884
72887
  const t = this.body().trim();
72885
- if (this.disabled())
72886
- return false;
72887
- return (t.length > 0 && t.length <= this.maxLength()) || this.pending().length > 0;
72888
+ return t.length > 0 && t.length <= this.maxLength() && !this.disabled();
72888
72889
  });
72889
72890
  this.placeholderText = computed(() => {
72890
72891
  this.i18n.lang();
@@ -72904,61 +72905,29 @@ class ChatComposerComponent {
72904
72905
  }
72905
72906
  }
72906
72907
  onSend() {
72907
- if (!this.canSend())
72908
- return;
72909
- // Primero los adjuntos pendientes (cada uno se sube y manda en el consumer),
72910
- // luego el texto como mensaje aparte si lo hay.
72911
- const files = this.pending();
72912
- for (const att of files) {
72913
- this.attach.emit(att.file);
72914
- if (att.url)
72915
- URL.revokeObjectURL(att.url);
72916
- }
72917
- this.pending.set([]);
72918
72908
  const trimmed = this.body().trim();
72919
- if (trimmed && trimmed.length <= this.maxLength()) {
72920
- this.send.emit(trimmed);
72921
- }
72909
+ if (!trimmed || trimmed.length > this.maxLength())
72910
+ return;
72911
+ this.send.emit(trimmed);
72922
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);
72923
72920
  }
72924
72921
  onFile(event) {
72925
72922
  const input = event.target;
72926
72923
  const file = input.files?.[0];
72927
72924
  if (file)
72928
- this.addPending(file);
72925
+ this.attach.emit(file);
72929
72926
  input.value = '';
72930
72927
  }
72931
- /** Pegar imagen desde el portapapeles (captura de pantalla, copiar imagen). */
72932
- onPaste(event) {
72933
- const items = event.clipboardData?.items;
72934
- if (!items)
72935
- return;
72936
- for (const item of Array.from(items)) {
72937
- if (item.type.startsWith('image/')) {
72938
- const file = item.getAsFile();
72939
- if (file) {
72940
- event.preventDefault();
72941
- this.addPending(file);
72942
- }
72943
- return;
72944
- }
72945
- }
72946
- }
72947
- addPending(file) {
72948
- const isImage = file.type.startsWith('image/');
72949
- const url = isImage ? URL.createObjectURL(file) : undefined;
72950
- this.pending.update(arr => [...arr, { id: this.pendingId++, file, url, isImage }]);
72951
- }
72952
- removePending(id) {
72953
- this.pending.update(arr => {
72954
- const found = arr.find(a => a.id === id);
72955
- if (found?.url)
72956
- URL.revokeObjectURL(found.url);
72957
- return arr.filter(a => a.id !== id);
72958
- });
72959
- }
72960
72928
  /**
72961
- * 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.
72962
72931
  */
72963
72932
  emitTypingDebounced() {
72964
72933
  const now = performance.now();
@@ -72967,94 +72936,11 @@ class ChatComposerComponent {
72967
72936
  this.typing.emit();
72968
72937
  }
72969
72938
  }
72970
- // ─── Grabación de audio ───────────────────────────────────────────────────
72971
- /** Selecciona un mimeType de audio soportado por el navegador. */
72972
- pickAudioMime() {
72973
- const candidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4'];
72974
- const MR = window.MediaRecorder;
72975
- if (!MR?.isTypeSupported)
72976
- return '';
72977
- return candidates.find(c => MR.isTypeSupported(c)) ?? '';
72978
- }
72979
- async startRecording() {
72980
- if (this.recording())
72981
- return;
72982
- if (!navigator.mediaDevices?.getUserMedia || !('MediaRecorder' in window)) {
72983
- this.voice.emit(); // sin soporte: deja que el consumer decida (fallback)
72984
- return;
72985
- }
72986
- try {
72987
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
72988
- this.recStream = stream;
72989
- const mimeType = this.pickAudioMime();
72990
- this.recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined);
72991
- this.recChunks = [];
72992
- this.recorder.ondataavailable = e => {
72993
- if (e.data.size > 0)
72994
- this.recChunks.push(e.data);
72995
- };
72996
- this.recorder.start();
72997
- this.recording.set(true);
72998
- this.recSeconds.set(0);
72999
- this.recTimer = setInterval(() => this.recSeconds.update(s => s + 1), 1000);
73000
- }
73001
- catch {
73002
- this.cleanupRecording();
73003
- }
73004
- }
73005
- stopAndSend() {
73006
- const rec = this.recorder;
73007
- if (!rec) {
73008
- this.cleanupRecording();
73009
- return;
73010
- }
73011
- rec.onstop = () => {
73012
- const type = rec.mimeType || 'audio/webm';
73013
- const blob = new Blob(this.recChunks, { type });
73014
- const ext = type.includes('mp4') ? 'm4a' : 'webm';
73015
- const file = new File([blob], `audio-${this.recSeconds()}s.${ext}`, { type });
73016
- if (blob.size > 0)
73017
- this.attach.emit(file);
73018
- this.cleanupRecording();
73019
- };
73020
- rec.stop();
73021
- }
73022
- cancelRecording() {
73023
- const rec = this.recorder;
73024
- if (rec)
73025
- rec.onstop = () => this.cleanupRecording();
73026
- try {
73027
- rec?.stop();
73028
- }
73029
- catch {
73030
- this.cleanupRecording();
73031
- }
73032
- if (!rec)
73033
- this.cleanupRecording();
73034
- }
73035
- cleanupRecording() {
73036
- if (this.recTimer)
73037
- clearInterval(this.recTimer);
73038
- this.recTimer = undefined;
73039
- this.recStream?.getTracks().forEach(t => t.stop());
73040
- this.recStream = undefined;
73041
- this.recorder = undefined;
73042
- this.recChunks = [];
73043
- this.recording.set(false);
73044
- this.recSeconds.set(0);
73045
- }
73046
- ngOnDestroy() {
73047
- this.cleanupRecording();
73048
- for (const att of this.pending()) {
73049
- if (att.url)
73050
- URL.revokeObjectURL(att.url);
73051
- }
73052
- }
73053
72939
  t(key) {
73054
72940
  return this.i18n.t(key, 'ChatComposer');
73055
72941
  }
73056
72942
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatComposerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
73057
- 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", attach: "attach", 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: `
73058
72944
  <div class="composer">
73059
72945
  @if (replyingTo()) {
73060
72946
  <div class="reply-bar">
@@ -73068,75 +72954,52 @@ class ChatComposerComponent {
73068
72954
  </div>
73069
72955
  }
73070
72956
 
73071
- @if (pending().length > 0) {
73072
- <div class="pending-row">
73073
- @for (att of pending(); track att.id) {
73074
- <div class="thumb" [class.file]="!att.isImage">
73075
- @if (att.isImage) {
73076
- <img [src]="att.url" [alt]="att.file.name" />
73077
- } @else {
73078
- <ion-icon name="document-outline" aria-hidden="true" />
73079
- <span class="thumb-name">{{ att.file.name }}</span>
73080
- }
73081
- <button class="thumb-remove" [attr.aria-label]="t('remove')" (click)="removePending(att.id)">
73082
- <ion-icon name="close-outline" aria-hidden="true" />
73083
- </button>
73084
- </div>
72957
+ @if (emojiOpen()) {
72958
+ <div class="emoji-row">
72959
+ @for (e of quickEmojis; track e) {
72960
+ <button class="emoji" (click)="insertEmoji(e)">{{ e }}</button>
73085
72961
  }
73086
72962
  </div>
73087
72963
  }
73088
72964
 
73089
- @if (recording()) {
73090
- <div class="rec-bar">
73091
- <button class="icon-btn danger" [attr.aria-label]="t('cancel')" (click)="cancelRecording()">
73092
- <ion-icon name="trash-outline" aria-hidden="true" />
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
+ }
72972
+
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>
72984
+
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" />
73093
72988
  </button>
73094
- <span class="rec-dot"></span>
73095
- <span class="rec-time">{{ recTimeLabel() }}</span>
73096
- <span class="rec-spacer"></span>
73097
- <button class="send-btn" [attr.aria-label]="t('send')" (click)="stopAndSend()">
72989
+ }
72990
+
72991
+ @if (canSend()) {
72992
+ <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73098
72993
  <ion-icon name="arrow-up-outline" aria-hidden="true" />
73099
72994
  </button>
73100
- </div>
73101
- } @else {
73102
- <div class="pill">
73103
- @if (showAttach()) {
73104
- <label class="icon-btn" [attr.aria-label]="t('attach')">
73105
- <ion-icon name="add-outline" aria-hidden="true" />
73106
- <input type="file" hidden (change)="onFile($event)" accept="image/*,application/pdf" />
73107
- </label>
73108
- }
73109
-
73110
- <ion-textarea
73111
- class="field"
73112
- [placeholder]="placeholderText()"
73113
- [disabled]="disabled()"
73114
- [maxlength]="maxLength()"
73115
- [autoGrow]="true"
73116
- [rows]="1"
73117
- [value]="body()"
73118
- [spellcheck]="false"
73119
- autocapitalize="sentences"
73120
- autocorrect="off"
73121
- enterkeyhint="enter"
73122
- (ionInput)="onInput($event)"
73123
- (keydown)="onKeydown($event)"
73124
- (paste)="onPaste($event)"
73125
- ></ion-textarea>
73126
-
73127
- @if (canSend()) {
73128
- <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73129
- <ion-icon name="arrow-up-outline" aria-hidden="true" />
73130
- </button>
73131
- } @else if (showMic()) {
73132
- <button class="icon-btn mic" [attr.aria-label]="t('voice')" (click)="startRecording()">
73133
- <ion-icon name="mic-outline" aria-hidden="true" />
73134
- </button>
73135
- }
73136
- </div>
73137
- }
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>
73138
73001
  </div>
73139
- `, 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)}.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"] }] }); }
73140
73003
  }
73141
73004
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatComposerComponent, decorators: [{
73142
73005
  type: Component,
@@ -73154,75 +73017,52 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
73154
73017
  </div>
73155
73018
  }
73156
73019
 
73157
- @if (pending().length > 0) {
73158
- <div class="pending-row">
73159
- @for (att of pending(); track att.id) {
73160
- <div class="thumb" [class.file]="!att.isImage">
73161
- @if (att.isImage) {
73162
- <img [src]="att.url" [alt]="att.file.name" />
73163
- } @else {
73164
- <ion-icon name="document-outline" aria-hidden="true" />
73165
- <span class="thumb-name">{{ att.file.name }}</span>
73166
- }
73167
- <button class="thumb-remove" [attr.aria-label]="t('remove')" (click)="removePending(att.id)">
73168
- <ion-icon name="close-outline" aria-hidden="true" />
73169
- </button>
73170
- </div>
73020
+ @if (emojiOpen()) {
73021
+ <div class="emoji-row">
73022
+ @for (e of quickEmojis; track e) {
73023
+ <button class="emoji" (click)="insertEmoji(e)">{{ e }}</button>
73171
73024
  }
73172
73025
  </div>
73173
73026
  }
73174
73027
 
73175
- @if (recording()) {
73176
- <div class="rec-bar">
73177
- <button class="icon-btn danger" [attr.aria-label]="t('cancel')" (click)="cancelRecording()">
73178
- <ion-icon name="trash-outline" aria-hidden="true" />
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
+ }
73035
+
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>
73047
+
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" />
73179
73051
  </button>
73180
- <span class="rec-dot"></span>
73181
- <span class="rec-time">{{ recTimeLabel() }}</span>
73182
- <span class="rec-spacer"></span>
73183
- <button class="send-btn" [attr.aria-label]="t('send')" (click)="stopAndSend()">
73052
+ }
73053
+
73054
+ @if (canSend()) {
73055
+ <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73184
73056
  <ion-icon name="arrow-up-outline" aria-hidden="true" />
73185
73057
  </button>
73186
- </div>
73187
- } @else {
73188
- <div class="pill">
73189
- @if (showAttach()) {
73190
- <label class="icon-btn" [attr.aria-label]="t('attach')">
73191
- <ion-icon name="add-outline" aria-hidden="true" />
73192
- <input type="file" hidden (change)="onFile($event)" accept="image/*,application/pdf" />
73193
- </label>
73194
- }
73195
-
73196
- <ion-textarea
73197
- class="field"
73198
- [placeholder]="placeholderText()"
73199
- [disabled]="disabled()"
73200
- [maxlength]="maxLength()"
73201
- [autoGrow]="true"
73202
- [rows]="1"
73203
- [value]="body()"
73204
- [spellcheck]="false"
73205
- autocapitalize="sentences"
73206
- autocorrect="off"
73207
- enterkeyhint="enter"
73208
- (ionInput)="onInput($event)"
73209
- (keydown)="onKeydown($event)"
73210
- (paste)="onPaste($event)"
73211
- ></ion-textarea>
73212
-
73213
- @if (canSend()) {
73214
- <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73215
- <ion-icon name="arrow-up-outline" aria-hidden="true" />
73216
- </button>
73217
- } @else if (showMic()) {
73218
- <button class="icon-btn mic" [attr.aria-label]="t('voice')" (click)="startRecording()">
73219
- <ion-icon name="mic-outline" aria-hidden="true" />
73220
- </button>
73221
- }
73222
- </div>
73223
- }
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>
73224
73064
  </div>
73225
- `, 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)}.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"] }]
73226
73066
  }], ctorParameters: () => [] });
73227
73067
 
73228
73068
  /**
@@ -73610,7 +73450,7 @@ class ChatWindowComponent {
73610
73450
  <div class="closed-banner">{{ t('conversationClosed') }}</div>
73611
73451
  }
73612
73452
  </div>
73613
- `, 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", "attach", "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"] }] }); }
73614
73454
  }
73615
73455
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatWindowComponent, decorators: [{
73616
73456
  type: Component,