valtech-components 4.0.231 → 4.0.233

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.231';
59
+ const VERSION = '4.0.233';
60
60
 
61
61
  // Control de estado de refresco (singleton a nivel de módulo)
62
62
  let isRefreshing = false;
@@ -72209,10 +72209,21 @@ class ConversationService {
72209
72209
  return this.http.delete(`${this.baseUrl}/conversations/${convId}/participants/${userId}`);
72210
72210
  }
72211
72211
  /**
72212
- * Envia un mensaje a una conversacion.
72212
+ * Envia un mensaje a una conversacion, opcionalmente con adjuntos.
72213
+ * El backend espera `attachments: { url, type, name, size }[]` (key `type` = MIME),
72214
+ * mientras que el front usa `mimeType` — se mapea aqui.
72213
72215
  */
72214
- sendMessage(convId, body) {
72215
- return this.http.post(`${this.baseUrl}/conversations/${convId}/messages`, { body });
72216
+ sendMessage(convId, body, attachments) {
72217
+ const payload = { body };
72218
+ if (attachments?.length) {
72219
+ payload['attachments'] = attachments.map(a => ({
72220
+ url: a.url,
72221
+ type: a.mimeType,
72222
+ name: a.name,
72223
+ size: a.size ?? 0,
72224
+ }));
72225
+ }
72226
+ return this.http.post(`${this.baseUrl}/conversations/${convId}/messages`, payload);
72216
72227
  }
72217
72228
  /**
72218
72229
  * Elimina (marca como eliminado) un mensaje.
@@ -72439,7 +72450,8 @@ class MessageBubbleComponent {
72439
72450
  return (parts[0][0] + (parts[1]?.[0] ?? '')).toUpperCase();
72440
72451
  });
72441
72452
  this.imageAttachments = computed(() => (this.msg().attachments ?? []).filter(a => a.mimeType?.startsWith('image/')));
72442
- this.fileAttachments = computed(() => (this.msg().attachments ?? []).filter(a => !a.mimeType?.startsWith('image/')));
72453
+ this.audioAttachments = computed(() => (this.msg().attachments ?? []).filter(a => a.mimeType?.startsWith('audio/')));
72454
+ this.fileAttachments = computed(() => (this.msg().attachments ?? []).filter(a => !a.mimeType?.startsWith('image/') && !a.mimeType?.startsWith('audio/')));
72443
72455
  this.statusIcon = computed(() => {
72444
72456
  switch (this.msg().status) {
72445
72457
  case 'sending':
@@ -72510,6 +72522,13 @@ class MessageBubbleComponent {
72510
72522
  }
72511
72523
  </div>
72512
72524
  }
72525
+ @if (audioAttachments().length > 0) {
72526
+ <div class="audios">
72527
+ @for (att of audioAttachments(); track att.url) {
72528
+ <audio class="att-audio" controls preload="metadata" [src]="att.url"></audio>
72529
+ }
72530
+ </div>
72531
+ }
72513
72532
  @if (fileAttachments().length > 0) {
72514
72533
  <div class="files">
72515
72534
  @for (att of fileAttachments(); track att.url) {
@@ -72573,7 +72592,7 @@ class MessageBubbleComponent {
72573
72592
  }
72574
72593
  </div>
72575
72594
  </div>
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"] }] }); }
72595
+ `, 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"] }] }); }
72577
72596
  }
72578
72597
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageBubbleComponent, decorators: [{
72579
72598
  type: Component,
@@ -72618,6 +72637,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72618
72637
  }
72619
72638
  </div>
72620
72639
  }
72640
+ @if (audioAttachments().length > 0) {
72641
+ <div class="audios">
72642
+ @for (att of audioAttachments(); track att.url) {
72643
+ <audio class="att-audio" controls preload="metadata" [src]="att.url"></audio>
72644
+ }
72645
+ </div>
72646
+ }
72621
72647
  @if (fileAttachments().length > 0) {
72622
72648
  <div class="files">
72623
72649
  @for (att of fileAttachments(); track att.url) {
@@ -72681,7 +72707,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72681
72707
  }
72682
72708
  </div>
72683
72709
  </div>
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"] }]
72710
+ `, 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"] }]
72685
72711
  }], ctorParameters: () => [] });
72686
72712
 
72687
72713
  addIcons({ sendOutline });
@@ -72834,35 +72860,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
72834
72860
  type: Output
72835
72861
  }] } });
72836
72862
 
72837
- addIcons({ addOutline, closeOutline, happyOutline, micOutline, arrowUpOutline });
72863
+ addIcons({ addOutline, closeOutline, documentOutline, micOutline, arrowUpOutline, trashOutline });
72838
72864
  const CHAT_COMPOSER_I18N = {
72839
72865
  es: {
72840
72866
  placeholder: 'Escribe un mensaje',
72841
72867
  send: 'Enviar',
72842
72868
  attach: 'Adjuntar',
72843
- emoji: 'Emoji',
72844
72869
  voice: 'Mensaje de voz',
72845
72870
  replyingTo: 'Respondiendo a',
72846
72871
  cancel: 'Cancelar',
72872
+ remove: 'Quitar',
72847
72873
  },
72848
72874
  en: {
72849
72875
  placeholder: 'Write a message',
72850
72876
  send: 'Send',
72851
72877
  attach: 'Attach',
72852
- emoji: 'Emoji',
72853
72878
  voice: 'Voice message',
72854
72879
  replyingTo: 'Replying to',
72855
72880
  cancel: 'Cancel',
72881
+ remove: 'Remove',
72856
72882
  },
72857
72883
  };
72858
- const QUICK_EMOJIS = ['👍', '❤️', '😂', '🎉', '🙏', '🔥', '😮', '😢'];
72859
72884
  /**
72860
72885
  * val-chat-composer — barra de redacción de chat moderna (signal inputs).
72861
72886
  *
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.
72887
+ * Pill con: adjuntar (+), textarea auto-grow, y mic/enviar (mic cuando está vacío,
72888
+ * enviar cuando hay texto o adjuntos). Los adjuntos (botón + / pegar imagen) se
72889
+ * acumulan como PREVIEW (miniaturas) y se emiten por `(attach)` recién al ENVIAR
72890
+ * no se van solos al chat. Audio grabado se emite directo al parar.
72891
+ * No incluye picker de emoji: el teclado del sistema ya lo trae.
72866
72892
  */
72867
72893
  class ChatComposerComponent {
72868
72894
  constructor() {
@@ -72872,7 +72898,6 @@ class ChatComposerComponent {
72872
72898
  this.maxLength = input(2000);
72873
72899
  this.replyingTo = input(null);
72874
72900
  this.showAttach = input(true);
72875
- this.showEmoji = input(true);
72876
72901
  this.showMic = input(true);
72877
72902
  this.send = output();
72878
72903
  this.typing = output();
@@ -72880,12 +72905,24 @@ class ChatComposerComponent {
72880
72905
  this.voice = output();
72881
72906
  this.cancelReply = output();
72882
72907
  this.body = signal('');
72883
- this.emojiOpen = signal(false);
72884
- this.quickEmojis = QUICK_EMOJIS;
72908
+ this.pending = signal([]);
72909
+ this.pendingId = 0;
72910
+ // Grabación de audio (MediaRecorder).
72911
+ this.recording = signal(false);
72912
+ this.recSeconds = signal(0);
72913
+ this.recChunks = [];
72914
+ this.recTimeLabel = computed(() => {
72915
+ const s = this.recSeconds();
72916
+ const mm = String(Math.floor(s / 60)).padStart(2, '0');
72917
+ const ss = String(s % 60).padStart(2, '0');
72918
+ return `${mm}:${ss}`;
72919
+ });
72885
72920
  this.lastTypingEmit = 0;
72886
72921
  this.canSend = computed(() => {
72887
72922
  const t = this.body().trim();
72888
- return t.length > 0 && t.length <= this.maxLength() && !this.disabled();
72923
+ if (this.disabled())
72924
+ return false;
72925
+ return (t.length > 0 && t.length <= this.maxLength()) || this.pending().length > 0;
72889
72926
  });
72890
72927
  this.placeholderText = computed(() => {
72891
72928
  this.i18n.lang();
@@ -72905,29 +72942,61 @@ class ChatComposerComponent {
72905
72942
  }
72906
72943
  }
72907
72944
  onSend() {
72908
- const trimmed = this.body().trim();
72909
- if (!trimmed || trimmed.length > this.maxLength())
72945
+ if (!this.canSend())
72910
72946
  return;
72911
- this.send.emit(trimmed);
72947
+ // Primero los adjuntos pendientes (cada uno se sube y manda en el consumer),
72948
+ // luego el texto como mensaje aparte si lo hay.
72949
+ const files = this.pending();
72950
+ for (const att of files) {
72951
+ this.attach.emit(att.file);
72952
+ if (att.url)
72953
+ URL.revokeObjectURL(att.url);
72954
+ }
72955
+ this.pending.set([]);
72956
+ const trimmed = this.body().trim();
72957
+ if (trimmed && trimmed.length <= this.maxLength()) {
72958
+ this.send.emit(trimmed);
72959
+ }
72912
72960
  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);
72920
72961
  }
72921
72962
  onFile(event) {
72922
72963
  const input = event.target;
72923
72964
  const file = input.files?.[0];
72924
72965
  if (file)
72925
- this.attach.emit(file);
72966
+ this.addPending(file);
72926
72967
  input.value = '';
72927
72968
  }
72969
+ /** Pegar imagen desde el portapapeles (captura de pantalla, copiar imagen). */
72970
+ onPaste(event) {
72971
+ const items = event.clipboardData?.items;
72972
+ if (!items)
72973
+ return;
72974
+ for (const item of Array.from(items)) {
72975
+ if (item.type.startsWith('image/')) {
72976
+ const file = item.getAsFile();
72977
+ if (file) {
72978
+ event.preventDefault();
72979
+ this.addPending(file);
72980
+ }
72981
+ return;
72982
+ }
72983
+ }
72984
+ }
72985
+ addPending(file) {
72986
+ const isImage = file.type.startsWith('image/');
72987
+ const url = isImage ? URL.createObjectURL(file) : undefined;
72988
+ this.pending.update(arr => [...arr, { id: this.pendingId++, file, url, isImage }]);
72989
+ }
72990
+ removePending(id) {
72991
+ this.pending.update(arr => {
72992
+ const found = arr.find(a => a.id === id);
72993
+ if (found?.url)
72994
+ URL.revokeObjectURL(found.url);
72995
+ return arr.filter(a => a.id !== id);
72996
+ });
72997
+ }
72928
72998
  /**
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.
72999
+ * Emite typing como máximo 1 vez cada 2s. performance.now es seguro en runtime.
72931
73000
  */
72932
73001
  emitTypingDebounced() {
72933
73002
  const now = performance.now();
@@ -72936,11 +73005,94 @@ class ChatComposerComponent {
72936
73005
  this.typing.emit();
72937
73006
  }
72938
73007
  }
73008
+ // ─── Grabación de audio ───────────────────────────────────────────────────
73009
+ /** Selecciona un mimeType de audio soportado por el navegador. */
73010
+ pickAudioMime() {
73011
+ const candidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4'];
73012
+ const MR = window.MediaRecorder;
73013
+ if (!MR?.isTypeSupported)
73014
+ return '';
73015
+ return candidates.find(c => MR.isTypeSupported(c)) ?? '';
73016
+ }
73017
+ async startRecording() {
73018
+ if (this.recording())
73019
+ return;
73020
+ if (!navigator.mediaDevices?.getUserMedia || !('MediaRecorder' in window)) {
73021
+ this.voice.emit(); // sin soporte: deja que el consumer decida (fallback)
73022
+ return;
73023
+ }
73024
+ try {
73025
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
73026
+ this.recStream = stream;
73027
+ const mimeType = this.pickAudioMime();
73028
+ this.recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined);
73029
+ this.recChunks = [];
73030
+ this.recorder.ondataavailable = e => {
73031
+ if (e.data.size > 0)
73032
+ this.recChunks.push(e.data);
73033
+ };
73034
+ this.recorder.start();
73035
+ this.recording.set(true);
73036
+ this.recSeconds.set(0);
73037
+ this.recTimer = setInterval(() => this.recSeconds.update(s => s + 1), 1000);
73038
+ }
73039
+ catch {
73040
+ this.cleanupRecording();
73041
+ }
73042
+ }
73043
+ stopAndSend() {
73044
+ const rec = this.recorder;
73045
+ if (!rec) {
73046
+ this.cleanupRecording();
73047
+ return;
73048
+ }
73049
+ rec.onstop = () => {
73050
+ const type = rec.mimeType || 'audio/webm';
73051
+ const blob = new Blob(this.recChunks, { type });
73052
+ const ext = type.includes('mp4') ? 'm4a' : 'webm';
73053
+ const file = new File([blob], `audio-${this.recSeconds()}s.${ext}`, { type });
73054
+ if (blob.size > 0)
73055
+ this.attach.emit(file);
73056
+ this.cleanupRecording();
73057
+ };
73058
+ rec.stop();
73059
+ }
73060
+ cancelRecording() {
73061
+ const rec = this.recorder;
73062
+ if (rec)
73063
+ rec.onstop = () => this.cleanupRecording();
73064
+ try {
73065
+ rec?.stop();
73066
+ }
73067
+ catch {
73068
+ this.cleanupRecording();
73069
+ }
73070
+ if (!rec)
73071
+ this.cleanupRecording();
73072
+ }
73073
+ cleanupRecording() {
73074
+ if (this.recTimer)
73075
+ clearInterval(this.recTimer);
73076
+ this.recTimer = undefined;
73077
+ this.recStream?.getTracks().forEach(t => t.stop());
73078
+ this.recStream = undefined;
73079
+ this.recorder = undefined;
73080
+ this.recChunks = [];
73081
+ this.recording.set(false);
73082
+ this.recSeconds.set(0);
73083
+ }
73084
+ ngOnDestroy() {
73085
+ this.cleanupRecording();
73086
+ for (const att of this.pending()) {
73087
+ if (att.url)
73088
+ URL.revokeObjectURL(att.url);
73089
+ }
73090
+ }
72939
73091
  t(key) {
72940
73092
  return this.i18n.t(key, 'ChatComposer');
72941
73093
  }
72942
73094
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatComposerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
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: `
73095
+ 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: `
72944
73096
  <div class="composer">
72945
73097
  @if (replyingTo()) {
72946
73098
  <div class="reply-bar">
@@ -72954,52 +73106,75 @@ class ChatComposerComponent {
72954
73106
  </div>
72955
73107
  }
72956
73108
 
72957
- @if (emojiOpen()) {
72958
- <div class="emoji-row">
72959
- @for (e of quickEmojis; track e) {
72960
- <button class="emoji" (click)="insertEmoji(e)">{{ e }}</button>
73109
+ @if (pending().length > 0) {
73110
+ <div class="pending-row">
73111
+ @for (att of pending(); track att.id) {
73112
+ <div class="thumb" [class.file]="!att.isImage">
73113
+ @if (att.isImage) {
73114
+ <img [src]="att.url" [alt]="att.file.name" />
73115
+ } @else {
73116
+ <ion-icon name="document-outline" aria-hidden="true" />
73117
+ <span class="thumb-name">{{ att.file.name }}</span>
73118
+ }
73119
+ <button class="thumb-remove" [attr.aria-label]="t('remove')" (click)="removePending(att.id)">
73120
+ <ion-icon name="close-outline" aria-hidden="true" />
73121
+ </button>
73122
+ </div>
72961
73123
  }
72962
73124
  </div>
72963
73125
  }
72964
73126
 
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" />
73127
+ @if (recording()) {
73128
+ <div class="rec-bar">
73129
+ <button class="icon-btn danger" [attr.aria-label]="t('cancel')" (click)="cancelRecording()">
73130
+ <ion-icon name="trash-outline" aria-hidden="true" />
72988
73131
  </button>
72989
- }
72990
-
72991
- @if (canSend()) {
72992
- <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73132
+ <span class="rec-dot"></span>
73133
+ <span class="rec-time">{{ recTimeLabel() }}</span>
73134
+ <span class="rec-spacer"></span>
73135
+ <button class="send-btn" [attr.aria-label]="t('send')" (click)="stopAndSend()">
72993
73136
  <ion-icon name="arrow-up-outline" aria-hidden="true" />
72994
73137
  </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>
73138
+ </div>
73139
+ } @else {
73140
+ <div class="pill">
73141
+ @if (showAttach()) {
73142
+ <label class="icon-btn" [attr.aria-label]="t('attach')">
73143
+ <ion-icon name="add-outline" aria-hidden="true" />
73144
+ <input type="file" hidden (change)="onFile($event)" accept="image/*,application/pdf" />
73145
+ </label>
73146
+ }
73147
+
73148
+ <ion-textarea
73149
+ class="field"
73150
+ [placeholder]="placeholderText()"
73151
+ [disabled]="disabled()"
73152
+ [maxlength]="maxLength()"
73153
+ [autoGrow]="true"
73154
+ [rows]="1"
73155
+ [value]="body()"
73156
+ [spellcheck]="false"
73157
+ autocapitalize="sentences"
73158
+ autocorrect="off"
73159
+ enterkeyhint="enter"
73160
+ (ionInput)="onInput($event)"
73161
+ (keydown)="onKeydown($event)"
73162
+ (paste)="onPaste($event)"
73163
+ ></ion-textarea>
73164
+
73165
+ @if (canSend()) {
73166
+ <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73167
+ <ion-icon name="arrow-up-outline" aria-hidden="true" />
73168
+ </button>
73169
+ } @else if (showMic()) {
73170
+ <button class="icon-btn mic" [attr.aria-label]="t('voice')" (click)="startRecording()">
73171
+ <ion-icon name="mic-outline" aria-hidden="true" />
73172
+ </button>
73173
+ }
73174
+ </div>
73175
+ }
73001
73176
  </div>
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"] }] }); }
73177
+ `, 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"] }] }); }
73003
73178
  }
73004
73179
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatComposerComponent, decorators: [{
73005
73180
  type: Component,
@@ -73017,52 +73192,75 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
73017
73192
  </div>
73018
73193
  }
73019
73194
 
73020
- @if (emojiOpen()) {
73021
- <div class="emoji-row">
73022
- @for (e of quickEmojis; track e) {
73023
- <button class="emoji" (click)="insertEmoji(e)">{{ e }}</button>
73195
+ @if (pending().length > 0) {
73196
+ <div class="pending-row">
73197
+ @for (att of pending(); track att.id) {
73198
+ <div class="thumb" [class.file]="!att.isImage">
73199
+ @if (att.isImage) {
73200
+ <img [src]="att.url" [alt]="att.file.name" />
73201
+ } @else {
73202
+ <ion-icon name="document-outline" aria-hidden="true" />
73203
+ <span class="thumb-name">{{ att.file.name }}</span>
73204
+ }
73205
+ <button class="thumb-remove" [attr.aria-label]="t('remove')" (click)="removePending(att.id)">
73206
+ <ion-icon name="close-outline" aria-hidden="true" />
73207
+ </button>
73208
+ </div>
73024
73209
  }
73025
73210
  </div>
73026
73211
  }
73027
73212
 
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" />
73213
+ @if (recording()) {
73214
+ <div class="rec-bar">
73215
+ <button class="icon-btn danger" [attr.aria-label]="t('cancel')" (click)="cancelRecording()">
73216
+ <ion-icon name="trash-outline" aria-hidden="true" />
73051
73217
  </button>
73052
- }
73053
-
73054
- @if (canSend()) {
73055
- <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73218
+ <span class="rec-dot"></span>
73219
+ <span class="rec-time">{{ recTimeLabel() }}</span>
73220
+ <span class="rec-spacer"></span>
73221
+ <button class="send-btn" [attr.aria-label]="t('send')" (click)="stopAndSend()">
73056
73222
  <ion-icon name="arrow-up-outline" aria-hidden="true" />
73057
73223
  </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>
73224
+ </div>
73225
+ } @else {
73226
+ <div class="pill">
73227
+ @if (showAttach()) {
73228
+ <label class="icon-btn" [attr.aria-label]="t('attach')">
73229
+ <ion-icon name="add-outline" aria-hidden="true" />
73230
+ <input type="file" hidden (change)="onFile($event)" accept="image/*,application/pdf" />
73231
+ </label>
73232
+ }
73233
+
73234
+ <ion-textarea
73235
+ class="field"
73236
+ [placeholder]="placeholderText()"
73237
+ [disabled]="disabled()"
73238
+ [maxlength]="maxLength()"
73239
+ [autoGrow]="true"
73240
+ [rows]="1"
73241
+ [value]="body()"
73242
+ [spellcheck]="false"
73243
+ autocapitalize="sentences"
73244
+ autocorrect="off"
73245
+ enterkeyhint="enter"
73246
+ (ionInput)="onInput($event)"
73247
+ (keydown)="onKeydown($event)"
73248
+ (paste)="onPaste($event)"
73249
+ ></ion-textarea>
73250
+
73251
+ @if (canSend()) {
73252
+ <button class="send-btn" [attr.aria-label]="t('send')" (click)="onSend()">
73253
+ <ion-icon name="arrow-up-outline" aria-hidden="true" />
73254
+ </button>
73255
+ } @else if (showMic()) {
73256
+ <button class="icon-btn mic" [attr.aria-label]="t('voice')" (click)="startRecording()">
73257
+ <ion-icon name="mic-outline" aria-hidden="true" />
73258
+ </button>
73259
+ }
73260
+ </div>
73261
+ }
73064
73262
  </div>
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"] }]
73263
+ `, 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"] }]
73066
73264
  }], ctorParameters: () => [] });
73067
73265
 
73068
73266
  /**
@@ -73450,7 +73648,7 @@ class ChatWindowComponent {
73450
73648
  <div class="closed-banner">{{ t('conversationClosed') }}</div>
73451
73649
  }
73452
73650
  </div>
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"] }] }); }
73651
+ `, 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"] }] }); }
73454
73652
  }
73455
73653
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatWindowComponent, decorators: [{
73456
73654
  type: Component,