valtech-components 4.0.219 → 4.0.220

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/esm2022/lib/components/molecules/chat-input/chat-input.component.mjs +157 -0
  2. package/esm2022/lib/components/molecules/chat-input/types.mjs +2 -0
  3. package/esm2022/lib/components/molecules/message-bubble/message-bubble.component.mjs +244 -0
  4. package/esm2022/lib/components/molecules/message-bubble/types.mjs +2 -0
  5. package/esm2022/lib/components/molecules/typing-indicator/typing-indicator.component.mjs +76 -0
  6. package/esm2022/lib/components/organisms/chat-window/chat-window.component.mjs +246 -0
  7. package/esm2022/lib/components/organisms/chat-window/types.mjs +2 -0
  8. package/esm2022/lib/components/organisms/thread-panel/thread-panel.component.mjs +211 -0
  9. package/esm2022/lib/services/chat/config.mjs +35 -0
  10. package/esm2022/lib/services/chat/conversation.service.mjs +274 -0
  11. package/esm2022/lib/services/chat/types.mjs +10 -0
  12. package/esm2022/lib/services/entity-feed/entity-feed.service.mjs +103 -0
  13. package/esm2022/lib/services/entity-feed/index.mjs +2 -0
  14. package/esm2022/lib/services/entity-feed/types.mjs +2 -0
  15. package/esm2022/lib/services/i18n/default-content.mjs +65 -1
  16. package/esm2022/public-api.mjs +16 -1
  17. package/fesm2022/valtech-components.mjs +1369 -2
  18. package/fesm2022/valtech-components.mjs.map +1 -1
  19. package/lib/components/atoms/glow/glow.component.d.ts +1 -1
  20. package/lib/components/atoms/rights-footer/rights-footer.component.d.ts +2 -2
  21. package/lib/components/atoms/text/text.component.d.ts +1 -1
  22. package/lib/components/atoms/user-avatar/user-avatar.component.d.ts +2 -2
  23. package/lib/components/molecules/article-card/article-card.component.d.ts +1 -1
  24. package/lib/components/molecules/chat-input/chat-input.component.d.ts +38 -0
  25. package/lib/components/molecules/chat-input/types.d.ts +20 -0
  26. package/lib/components/molecules/content-reaction/content-reaction.component.d.ts +2 -2
  27. package/lib/components/molecules/cta-card/cta-card.component.d.ts +1 -1
  28. package/lib/components/molecules/features-list/features-list.component.d.ts +4 -4
  29. package/lib/components/molecules/load-more/load-more.component.d.ts +1 -1
  30. package/lib/components/molecules/media-object/media-object.component.d.ts +1 -1
  31. package/lib/components/molecules/message-bubble/message-bubble.component.d.ts +40 -0
  32. package/lib/components/molecules/message-bubble/types.d.ts +64 -0
  33. package/lib/components/molecules/metric-card/metric-card.component.d.ts +1 -1
  34. package/lib/components/molecules/typing-indicator/typing-indicator.component.d.ts +19 -0
  35. package/lib/components/organisms/account-view/account-view.component.d.ts +1 -1
  36. package/lib/components/organisms/article/article.component.d.ts +6 -6
  37. package/lib/components/organisms/auth-cta/auth-cta.component.d.ts +1 -1
  38. package/lib/components/organisms/bottom-nav/bottom-nav.component.d.ts +1 -1
  39. package/lib/components/organisms/chat-window/chat-window.component.d.ts +63 -0
  40. package/lib/components/organisms/chat-window/types.d.ts +36 -0
  41. package/lib/components/organisms/landing-steps/landing-steps.component.d.ts +1 -1
  42. package/lib/components/organisms/login/login.component.d.ts +1 -1
  43. package/lib/components/organisms/member-import-modal/member-import-modal.component.d.ts +1 -1
  44. package/lib/components/organisms/notification-preferences-view/notification-preferences-view.component.d.ts +1 -1
  45. package/lib/components/organisms/organization-view/organization-view.component.d.ts +1 -1
  46. package/lib/components/organisms/thread-panel/thread-panel.component.d.ts +55 -0
  47. package/lib/components/templates/page-wrapper/page-wrapper.component.d.ts +2 -2
  48. package/lib/services/chat/config.d.ts +37 -0
  49. package/lib/services/chat/conversation.service.d.ts +147 -0
  50. package/lib/services/chat/types.d.ts +145 -0
  51. package/lib/services/entity-feed/entity-feed.service.d.ts +71 -0
  52. package/lib/services/entity-feed/index.d.ts +2 -0
  53. package/lib/services/entity-feed/types.d.ts +26 -0
  54. package/package.json +1 -1
  55. package/public-api.d.ts +13 -0
@@ -25,7 +25,7 @@ import { getApps, getApp, initializeApp as initializeApp$1 } from 'firebase/app'
25
25
  import * as i2$1 from '@ionic/angular/standalone';
26
26
  import { AlertController, IonAvatar, IonCard, IonIcon, IonButton, IonSpinner, IonText, IonModal, IonHeader, IonToolbar, IonContent, IonButtons, IonTitle, IonProgressBar, IonSkeletonText, IonFab, IonFabButton, IonFabList, IonLabel, IonCardContent, IonCardHeader, IonCardTitle, IonCardSubtitle, IonCheckbox, IonRadio, IonRadioGroup, IonTextarea, IonDatetime, IonSelect, IonSelectOption, IonPopover, IonList, IonItem, IonInput, IonChip, IonRange, IonSearchbar, IonSegment, IonSegmentButton, IonToggle, IonRippleEffect, IonAccordion, IonAccordionGroup, IonTabBar, IonTabButton, IonBadge, IonBreadcrumb, IonBreadcrumbs, IonInfiniteScroll, IonInfiniteScrollContent, IonNote, IonDatetimeButton, ToastController as ToastController$1, IonCol, IonRow, IonRefresher, IonRefresherContent, IonMenuButton, IonFooter, IonListHeader, IonGrid, MenuController, IonMenu, IonMenuToggle, IonSplitPane } from '@ionic/angular/standalone';
27
27
  import { addIcons } from 'ionicons';
28
- import { addOutline, addCircleOutline, alertOutline, alertCircleOutline, arrowBackOutline, arrowForwardOutline, arrowDownOutline, settings, settingsOutline, checkmarkCircleOutline, ellipsisHorizontalOutline, notifications, notificationsOutline, openOutline, closeOutline, chatbubblesOutline, shareOutline, heart, heartOutline, home, homeOutline, eyeOffOutline, eyeOutline, scanOutline, chevronDownOutline, chevronForwardOutline, checkmarkOutline, clipboardOutline, copyOutline, filterOutline, locationOutline, calendarOutline, businessOutline, logoTwitter, logoInstagram, logoLinkedin, logoYoutube, logoTiktok, logoFacebook, logoGoogle, createOutline, trashOutline, playOutline, peopleOutline, phonePortraitOutline, refreshOutline, documentTextOutline, lockClosedOutline, cloudUploadOutline, textOutline, informationCircleOutline, logoNpm, removeOutline, optionsOutline, personOutline, shieldCheckmarkOutline, keyOutline, desktopOutline, logOutOutline, cloudDownloadOutline, cloudOutline, gitBranchOutline, layersOutline, serverOutline, warningOutline, bulbOutline, lockOpenOutline, diamondOutline, newspaperOutline, add, close, share, create, trash, star, camera, mic, send, downloadOutline, chevronDown, language, globeOutline, checkmark, list, grid, apps, menu, search, person, helpCircle, informationCircle, documentText, mail, calendar, folder, chevronForward, ellipsisHorizontal, chevronBack, playBack, playForward, ellipse, starOutline, starHalf, heartHalf, checkmarkCircle, timeOutline, flag, trendingUp, trendingDown, remove, analytics, people, cash, cart, eye, chatbubbleOutline, thumbsUpOutline, thumbsUp, happyOutline, happy, sadOutline, sad, chevronUp, pin, pencil, callOutline, logoWhatsapp, paperPlaneOutline, mailOutline, chevronDownCircleOutline, closeCircle, alertCircle, logoApple, logoMicrosoft, linkOutline, unlinkOutline, cloudOfflineOutline, documentOutline, attachOutline, cameraOutline, closeCircleOutline, imageOutline, printOutline, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, searchOutline, cartOutline, chatbubble, compass, compassOutline, gridOutline, listOutline, folderOutline, documents, documentsOutline, statsChart, statsChartOutline, laptopOutline, swapHorizontalOutline, codeSlashOutline, notificationsOffOutline, alarmOutline, chatboxEllipsesOutline, checkmarkDoneOutline, cogOutline, shareSocialOutline, helpCircleOutline, menuOutline, stopCircleOutline, videocamOffOutline } from 'ionicons/icons';
28
+ import { addOutline, addCircleOutline, alertOutline, alertCircleOutline, arrowBackOutline, arrowForwardOutline, arrowDownOutline, settings, settingsOutline, checkmarkCircleOutline, ellipsisHorizontalOutline, notifications, notificationsOutline, openOutline, closeOutline, chatbubblesOutline, shareOutline, heart, heartOutline, home, homeOutline, eyeOffOutline, eyeOutline, scanOutline, chevronDownOutline, chevronForwardOutline, checkmarkOutline, clipboardOutline, copyOutline, filterOutline, locationOutline, calendarOutline, businessOutline, logoTwitter, logoInstagram, logoLinkedin, logoYoutube, logoTiktok, logoFacebook, logoGoogle, createOutline, trashOutline, playOutline, peopleOutline, phonePortraitOutline, refreshOutline, documentTextOutline, lockClosedOutline, cloudUploadOutline, textOutline, informationCircleOutline, logoNpm, removeOutline, optionsOutline, personOutline, shieldCheckmarkOutline, keyOutline, desktopOutline, logOutOutline, cloudDownloadOutline, cloudOutline, gitBranchOutline, layersOutline, serverOutline, warningOutline, bulbOutline, lockOpenOutline, diamondOutline, newspaperOutline, add, close, share, create, trash, star, camera, mic, send, downloadOutline, chevronDown, language, globeOutline, checkmark, list, grid, apps, menu, search, person, helpCircle, informationCircle, documentText, mail, calendar, folder, chevronForward, ellipsisHorizontal, chevronBack, playBack, playForward, ellipse, starOutline, starHalf, heartHalf, checkmarkCircle, timeOutline, flag, trendingUp, trendingDown, remove, analytics, people, cash, cart, eye, chatbubbleOutline, thumbsUpOutline, thumbsUp, happyOutline, happy, sadOutline, sad, chevronUp, pin, pencil, callOutline, logoWhatsapp, paperPlaneOutline, mailOutline, chevronDownCircleOutline, closeCircle, alertCircle, logoApple, logoMicrosoft, linkOutline, unlinkOutline, cloudOfflineOutline, documentOutline, attachOutline, cameraOutline, closeCircleOutline, imageOutline, printOutline, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, searchOutline, cartOutline, chatbubble, compass, compassOutline, gridOutline, listOutline, folderOutline, documents, documentsOutline, statsChart, statsChartOutline, laptopOutline, swapHorizontalOutline, codeSlashOutline, notificationsOffOutline, alarmOutline, chatboxEllipsesOutline, checkmarkDoneOutline, cogOutline, shareSocialOutline, helpCircleOutline, menuOutline, stopCircleOutline, videocamOffOutline, checkmarkDone, returnDownBackOutline } from 'ionicons/icons';
29
29
  import { Browser } from '@capacitor/browser';
30
30
  import * as i1$6 from '@angular/platform-browser';
31
31
  import { DomSanitizer, Meta, Title } from '@angular/platform-browser';
@@ -7845,6 +7845,70 @@ const VALTECH_DEFAULT_CONTENT = {
7845
7845
  decline: 'Decline',
7846
7846
  },
7847
7847
  },
7848
+ Chat: {
7849
+ es: {
7850
+ messageDeleted: '[mensaje eliminado]',
7851
+ edited: '(editado)',
7852
+ react: 'Reaccionar',
7853
+ reply: 'Responder',
7854
+ edit: 'Editar',
7855
+ delete: 'Eliminar',
7856
+ },
7857
+ en: {
7858
+ messageDeleted: '[message deleted]',
7859
+ edited: '(edited)',
7860
+ react: 'React',
7861
+ reply: 'Reply',
7862
+ edit: 'Edit',
7863
+ delete: 'Delete',
7864
+ },
7865
+ },
7866
+ ChatInput: {
7867
+ es: {
7868
+ placeholder: 'Escribe un mensaje',
7869
+ send: 'Enviar',
7870
+ counter: '{count}/{max}',
7871
+ },
7872
+ en: {
7873
+ placeholder: 'Write a message',
7874
+ send: 'Send',
7875
+ counter: '{count}/{max}',
7876
+ },
7877
+ },
7878
+ TypingIndicator: {
7879
+ es: {
7880
+ one: '{name} esta escribiendo',
7881
+ two: '{name} y {other} estan escribiendo',
7882
+ many: 'Varios estan escribiendo',
7883
+ },
7884
+ en: {
7885
+ one: '{name} is typing',
7886
+ two: '{name} and {other} are typing',
7887
+ many: 'Several people are typing',
7888
+ },
7889
+ },
7890
+ ChatWindow: {
7891
+ es: {
7892
+ conversationClosed: 'Esta conversacion esta cerrada',
7893
+ loading: 'Cargando mensajes...',
7894
+ noMessages: 'No hay mensajes aun',
7895
+ },
7896
+ en: {
7897
+ conversationClosed: 'This conversation is closed',
7898
+ loading: 'Loading messages...',
7899
+ noMessages: 'No messages yet',
7900
+ },
7901
+ },
7902
+ ThreadPanel: {
7903
+ es: {
7904
+ loading: 'Cargando conversacion...',
7905
+ thread: 'Hilo',
7906
+ },
7907
+ en: {
7908
+ loading: 'Loading conversation...',
7909
+ thread: 'Thread',
7910
+ },
7911
+ },
7848
7912
  };
7849
7913
 
7850
7914
  /**
@@ -67070,6 +67134,104 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
67070
67134
  args: [{ providedIn: 'root' }]
67071
67135
  }], ctorParameters: () => [] });
67072
67136
 
67137
+ /**
67138
+ * EntityFeedService — capa de lectura Firestore para entidades proyectables.
67139
+ *
67140
+ * Soporta cualquier tipo de entidad (posts, articles, adoptions, reels...)
67141
+ * siguiendo el esquema de paths:
67142
+ * - Entidades: apps/{appId}/orgs/{orgId}/{entityType}s/{entityId}
67143
+ * - Reacciones del usuario: apps/{appId}/users/{uid}/reactions/{entityType}_{entityId}
67144
+ *
67145
+ * FirestoreService auto-prefija los paths con apps/{appId}/.
67146
+ *
67147
+ * @example
67148
+ * const feed = inject(EntityFeedService);
67149
+ *
67150
+ * // Solo entidades
67151
+ * feed.watchEntities<Post>('org_x', 'post').subscribe(posts => ...);
67152
+ *
67153
+ * // Feed con reaccion del viewer incluida
67154
+ * feed.watchFeed<Post>('org_x', 'post').subscribe(items => {
67155
+ * items.forEach(item => console.log(item.viewerHasReacted, item.viewerToken));
67156
+ * });
67157
+ */
67158
+ class EntityFeedService {
67159
+ constructor() {
67160
+ this.fs = inject(FirestoreService);
67161
+ this.auth = inject(AuthService);
67162
+ }
67163
+ /**
67164
+ * Suscripcion en tiempo real a entidades publicadas de un tipo dado.
67165
+ *
67166
+ * Coleccion: orgs/{orgId}/{entityType}s
67167
+ * Auto-prefijada a: apps/{appId}/orgs/{orgId}/{entityType}s
67168
+ *
67169
+ * Filtra por status === 'published', ordena por publishedAt desc.
67170
+ *
67171
+ * @param orgId - ID de la organizacion
67172
+ * @param entityType - Tipo de entidad en singular (ej. 'post', 'article')
67173
+ * @param options - Opciones opcionales (limit, default 20)
67174
+ */
67175
+ watchEntities(orgId, entityType, options) {
67176
+ return this.fs.collectionChanges(`orgs/${orgId}/${entityType}s`, {
67177
+ where: [{ field: 'status', operator: '==', value: 'published' }],
67178
+ orderBy: [{ field: 'publishedAt', direction: 'desc' }],
67179
+ limit: options?.limit ?? 20,
67180
+ });
67181
+ }
67182
+ /**
67183
+ * Suscripcion en tiempo real a las reacciones del usuario actual para un tipo de entidad.
67184
+ *
67185
+ * Coleccion: users/{uid}/reactions
67186
+ * Auto-prefijada a: apps/{appId}/users/{uid}/reactions
67187
+ *
67188
+ * Retorna un Map<entityId, token> para lookup O(1) al combinar con el feed.
67189
+ * Si no hay usuario autenticado, retorna un Map vacio.
67190
+ *
67191
+ * @param entityType - Tipo de entidad a filtrar (ej. 'post', 'article')
67192
+ */
67193
+ watchMyReactions(entityType) {
67194
+ const user = this.auth.user();
67195
+ if (!user?.userId)
67196
+ return of(new Map());
67197
+ return this.fs
67198
+ .collectionChanges(`users/${user.userId}/reactions`, {
67199
+ where: [{ field: 'entityType', operator: '==', value: entityType }],
67200
+ })
67201
+ .pipe(map(docs => new Map(docs.map(d => [d.entityId, d.token]))));
67202
+ }
67203
+ /**
67204
+ * Combina watchEntities + watchMyReactions en un unico stream.
67205
+ *
67206
+ * Cada item del feed incluye viewerToken y viewerHasReacted, listos para
67207
+ * renderizar el estado de reaccion del usuario actual sin logica adicional
67208
+ * en la pagina.
67209
+ *
67210
+ * @param orgId - ID de la organizacion
67211
+ * @param entityType - Tipo de entidad en singular (ej. 'post', 'article')
67212
+ * @param options - Opciones opcionales (limit, default 20)
67213
+ */
67214
+ watchFeed(orgId, entityType, options) {
67215
+ return this.watchEntities(orgId, entityType, options).pipe(switchMap$1(entities => {
67216
+ const user = this.auth.user();
67217
+ if (!user?.userId || entities.length === 0) {
67218
+ return of(entities.map(e => ({ ...e, viewerToken: null, viewerHasReacted: false })));
67219
+ }
67220
+ return this.watchMyReactions(entityType).pipe(map(reactions => entities.map(e => ({
67221
+ ...e,
67222
+ viewerToken: reactions.get(e.id) ?? null,
67223
+ viewerHasReacted: reactions.has(e.id),
67224
+ }))));
67225
+ }));
67226
+ }
67227
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: EntityFeedService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
67228
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: EntityFeedService, providedIn: 'root' }); }
67229
+ }
67230
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: EntityFeedService, decorators: [{
67231
+ type: Injectable,
67232
+ args: [{ providedIn: 'root' }]
67233
+ }] });
67234
+
67073
67235
  const REACTION_BAR_I18N = {
67074
67236
  es: {
67075
67237
  loading: 'Cargando reacciones...',
@@ -71802,6 +71964,1211 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
71802
71964
  `, styles: [":host{display:grid;grid-template-columns:1fr;width:100%;box-sizing:border-box;margin:var(--val-form-field-gap, .5rem) 0;padding:var(--val-form-field-padding, 0)}.vff-label{margin-bottom:.25rem}.vff-label ::ng-deep p{margin:0}\n"] }]
71803
71965
  }] });
71804
71966
 
71967
+ /**
71968
+ * Token de inyeccion para la configuracion del Chat.
71969
+ */
71970
+ const VALTECH_CHAT_CONFIG = new InjectionToken('ValtechChatConfig');
71971
+ /**
71972
+ * Configuracion por defecto del Chat.
71973
+ */
71974
+ const DEFAULT_CHAT_CONFIG = {
71975
+ chatPrefix: '/v2/chat',
71976
+ };
71977
+ /**
71978
+ * Provee el servicio de Chat a la aplicacion Angular.
71979
+ *
71980
+ * @example
71981
+ * // main.ts
71982
+ * import { provideValtechChat } from 'valtech-components';
71983
+ *
71984
+ * bootstrapApplication(AppComponent, {
71985
+ * providers: [
71986
+ * provideValtechChat({
71987
+ * apiUrl: environment.apiUrl,
71988
+ * appId: 'showcase',
71989
+ * }),
71990
+ * ],
71991
+ * });
71992
+ */
71993
+ function provideValtechChat(config) {
71994
+ const mergedConfig = {
71995
+ ...DEFAULT_CHAT_CONFIG,
71996
+ ...config,
71997
+ };
71998
+ return makeEnvironmentProviders([{ provide: VALTECH_CHAT_CONFIG, useValue: mergedConfig }]);
71999
+ }
72000
+
72001
+ /**
72002
+ * ConversationService
72003
+ *
72004
+ * Gestiona conversaciones y mensajes del chat.
72005
+ *
72006
+ * Dos capas:
72007
+ * - HTTP (via backend /v2/chat/*): operaciones de escritura y consulta.
72008
+ * - Firestore real-time (directa): escucha mensajes y estado de escritura.
72009
+ *
72010
+ * Paths Firestore:
72011
+ * /apps/{appId}/orgs/{orgId}/conversations/{convId}/messages/{msgId}
72012
+ * /apps/{appId}/orgs/{orgId}/conversations/{convId}/typing/{userId}
72013
+ *
72014
+ * @example
72015
+ * // Enviar y escuchar mensajes
72016
+ * const svc = inject(ConversationService);
72017
+ *
72018
+ * // Enviar (HTTP)
72019
+ * svc.sendMessage(convId, 'Hola!').subscribe();
72020
+ *
72021
+ * // Escuchar en tiempo real (Firestore)
72022
+ * svc.watchMessages(convId, orgId, appId).subscribe(msgs => this.messages.set(msgs));
72023
+ */
72024
+ class ConversationService {
72025
+ constructor() {
72026
+ this.config = inject(VALTECH_CHAT_CONFIG, { optional: true });
72027
+ this.http = inject(HttpClient);
72028
+ this.firestoreService = inject(FirestoreService, { optional: true });
72029
+ this.firestore = inject(Firestore, { optional: true });
72030
+ if (!this.config && isDevMode()) {
72031
+ console.warn('[valtech-components] ConversationService: provideValtechChat() no esta en main.ts. ' +
72032
+ 'El chat no funcionara.');
72033
+ }
72034
+ }
72035
+ // ===========================================================================
72036
+ // HTTP — operaciones de escritura y consulta al backend
72037
+ // ===========================================================================
72038
+ get baseUrl() {
72039
+ return `${this.config?.apiUrl}${this.config?.chatPrefix ?? '/v2/chat'}`;
72040
+ }
72041
+ /**
72042
+ * Crea una nueva conversacion.
72043
+ */
72044
+ createConversation(req) {
72045
+ return this.http.post(`${this.baseUrl}/conversations`, req);
72046
+ }
72047
+ /**
72048
+ * Obtiene o crea una conversacion asociada a una entidad (post, request, etc.).
72049
+ * Usa el patron upsert del backend: si ya existe una conversacion para esa entidad,
72050
+ * la devuelve; si no, la crea.
72051
+ */
72052
+ getOrCreateForEntity(entityType, entityId, type = 'entity') {
72053
+ return this.http.post(`${this.baseUrl}/conversations/entity`, {
72054
+ entityType,
72055
+ entityId,
72056
+ type,
72057
+ });
72058
+ }
72059
+ /**
72060
+ * Lista las conversaciones del usuario autenticado.
72061
+ */
72062
+ listConversations(cursor) {
72063
+ const params = {};
72064
+ if (cursor)
72065
+ params['cursor'] = cursor;
72066
+ return this.http.get(`${this.baseUrl}/conversations`, { params });
72067
+ }
72068
+ /**
72069
+ * Obtiene una conversacion por ID.
72070
+ */
72071
+ getConversation(convId) {
72072
+ return this.http.get(`${this.baseUrl}/conversations/${convId}`);
72073
+ }
72074
+ /**
72075
+ * Actualiza el titulo o estado de una conversacion.
72076
+ */
72077
+ updateConversation(convId, req) {
72078
+ return this.http.patch(`${this.baseUrl}/conversations/${convId}`, req);
72079
+ }
72080
+ /**
72081
+ * Abre una conversacion cerrada.
72082
+ */
72083
+ openConversation(convId) {
72084
+ return this.http.post(`${this.baseUrl}/conversations/${convId}/open`, {});
72085
+ }
72086
+ /**
72087
+ * Cierra una conversacion.
72088
+ */
72089
+ closeConversation(convId) {
72090
+ return this.http.post(`${this.baseUrl}/conversations/${convId}/close`, {});
72091
+ }
72092
+ /**
72093
+ * Agrega un participante a la conversacion.
72094
+ */
72095
+ addParticipant(convId, userId) {
72096
+ return this.http.post(`${this.baseUrl}/conversations/${convId}/participants`, { userId });
72097
+ }
72098
+ /**
72099
+ * Elimina un participante de la conversacion.
72100
+ */
72101
+ removeParticipant(convId, userId) {
72102
+ return this.http.delete(`${this.baseUrl}/conversations/${convId}/participants/${userId}`);
72103
+ }
72104
+ /**
72105
+ * Envia un mensaje a una conversacion.
72106
+ */
72107
+ sendMessage(convId, body) {
72108
+ return this.http.post(`${this.baseUrl}/conversations/${convId}/messages`, { body });
72109
+ }
72110
+ /**
72111
+ * Elimina (marca como eliminado) un mensaje.
72112
+ */
72113
+ deleteMessage(convId, msgId) {
72114
+ return this.http.delete(`${this.baseUrl}/conversations/${convId}/messages/${msgId}`);
72115
+ }
72116
+ // ===========================================================================
72117
+ // Firestore — escucha en tiempo real (sin pasar por el backend)
72118
+ // ===========================================================================
72119
+ /**
72120
+ * Construye el path base para los datos de una conversacion.
72121
+ * Nota: usamos el path completo con skipPrefix porque el path ya incluye
72122
+ * apps/{appId}/orgs/{orgId} que el FirestoreService prefijaria de nuevo.
72123
+ * En cambio armamos el path como sub-coleccion directamente.
72124
+ */
72125
+ convMessagesPath(convId, orgId, appId) {
72126
+ return `apps/${appId}/orgs/${orgId}/conversations/${convId}/messages`;
72127
+ }
72128
+ convTypingPath(convId, orgId, appId) {
72129
+ return `apps/${appId}/orgs/${orgId}/conversations/${convId}/typing`;
72130
+ }
72131
+ /**
72132
+ * Escucha los mensajes de una conversacion en tiempo real.
72133
+ *
72134
+ * @param convId - ID de la conversacion
72135
+ * @param orgId - ID de la organizacion
72136
+ * @param appId - ID de la aplicacion
72137
+ * @param limit - Cantidad maxima de mensajes a cargar (default 50)
72138
+ */
72139
+ watchMessages(convId, orgId, appId, limit = 50) {
72140
+ if (!this.firestoreService) {
72141
+ throw new Error('[ConversationService] FirestoreService no disponible. Asegurate de provideValtechFirebase().');
72142
+ }
72143
+ // Usamos ABS_PATH_SENTINEL para evitar que FirestoreService anteponga el prefijo
72144
+ // porque el path ya contiene la ruta completa con appId/orgId.
72145
+ const { ABS_PATH_SENTINEL } = this.getAbsPathSentinel();
72146
+ const collectionPath = `${ABS_PATH_SENTINEL}${this.convMessagesPath(convId, orgId, appId)}`;
72147
+ return this.firestoreService.collectionChanges(collectionPath, {
72148
+ orderBy: [{ field: 'createdAt', direction: 'asc' }],
72149
+ limit,
72150
+ });
72151
+ }
72152
+ /**
72153
+ * Escucha los usuarios que estan escribiendo en una conversacion.
72154
+ * Devuelve un array de displayNames.
72155
+ *
72156
+ * @param convId - ID de la conversacion
72157
+ * @param orgId - ID de la organizacion
72158
+ * @param appId - ID de la aplicacion
72159
+ */
72160
+ watchTyping(convId, orgId, appId) {
72161
+ if (!this.firestoreService) {
72162
+ throw new Error('[ConversationService] FirestoreService no disponible. Asegurate de provideValtechFirebase().');
72163
+ }
72164
+ const { ABS_PATH_SENTINEL } = this.getAbsPathSentinel();
72165
+ const collectionPath = `${ABS_PATH_SENTINEL}${this.convTypingPath(convId, orgId, appId)}`;
72166
+ return this.firestoreService
72167
+ .collectionChanges(collectionPath)
72168
+ .pipe(map(docs => docs.map(d => d.displayName)));
72169
+ }
72170
+ /**
72171
+ * Registra que el usuario actual esta escribiendo.
72172
+ * El documento se sobreescribe para actualizar el timestamp.
72173
+ * Debe llamarse con un TTL/cleanup (ver stopTyping).
72174
+ *
72175
+ * @param convId - ID de la conversacion
72176
+ * @param orgId - ID de la organizacion
72177
+ * @param appId - ID de la aplicacion
72178
+ * @param displayName - Nombre visible del usuario
72179
+ */
72180
+ startTyping(convId, orgId, appId, displayName) {
72181
+ if (!this.firestore)
72182
+ return;
72183
+ const userId = this.getCurrentUserId();
72184
+ if (!userId)
72185
+ return;
72186
+ const path = `${this.convTypingPath(convId, orgId, appId)}/${userId}`;
72187
+ const docRef = doc(this.firestore, path);
72188
+ void setDoc(docRef, {
72189
+ displayName,
72190
+ updatedAt: serverTimestamp(),
72191
+ });
72192
+ }
72193
+ /**
72194
+ * Elimina el documento de "escribiendo" del usuario actual.
72195
+ *
72196
+ * @param convId - ID de la conversacion
72197
+ * @param orgId - ID de la organizacion
72198
+ * @param appId - ID de la aplicacion
72199
+ */
72200
+ stopTyping(convId, orgId, appId) {
72201
+ if (!this.firestore)
72202
+ return;
72203
+ const userId = this.getCurrentUserId();
72204
+ if (!userId)
72205
+ return;
72206
+ const path = `${this.convTypingPath(convId, orgId, appId)}/${userId}`;
72207
+ const docRef = doc(this.firestore, path);
72208
+ void deleteDoc(docRef);
72209
+ }
72210
+ /**
72211
+ * Marca un mensaje como leido por el usuario actual.
72212
+ * Escribe directamente en Firestore sin pasar por el backend.
72213
+ *
72214
+ * @param convId - ID de la conversacion
72215
+ * @param msgId - ID del ultimo mensaje leido
72216
+ * @param orgId - ID de la organizacion
72217
+ * @param appId - ID de la aplicacion
72218
+ */
72219
+ markRead(convId, msgId, orgId, appId) {
72220
+ if (!this.firestore)
72221
+ return;
72222
+ const userId = this.getCurrentUserId();
72223
+ if (!userId)
72224
+ return;
72225
+ const convPath = `apps/${appId}/orgs/${orgId}/conversations/${convId}`;
72226
+ const convRef = doc(this.firestore, convPath);
72227
+ // Usa dot-notation de Firestore para actualizar solo el campo del usuario
72228
+ const updateData = {
72229
+ [`participants_readMap.${userId}`]: msgId,
72230
+ updatedAt: serverTimestamp(),
72231
+ };
72232
+ void updateDoc(convRef, updateData);
72233
+ }
72234
+ // ===========================================================================
72235
+ // Helpers privados
72236
+ // ===========================================================================
72237
+ /**
72238
+ * Importa ABS_PATH_SENTINEL de forma lazy para evitar dependencias circulares.
72239
+ */
72240
+ getAbsPathSentinel() {
72241
+ // La sentinel se define en firestore.service.ts — replicamos el valor aqui
72242
+ // para no crear una dependencia circular en el import estatico.
72243
+ return { ABS_PATH_SENTINEL: '__abs__:' };
72244
+ }
72245
+ /**
72246
+ * Obtiene el userId del usuario actual desde sessionStorage / localStorage
72247
+ * como fallback no-DI para el contexto de escritura de Firestore.
72248
+ * En prod, el AuthService actualiza el userId en el contexto de la app.
72249
+ */
72250
+ getCurrentUserId() {
72251
+ // Intentar obtenerlo desde sessionStorage (el AuthService lo guarda ahi)
72252
+ if (typeof sessionStorage !== 'undefined') {
72253
+ const uid = sessionStorage.getItem('uid') ?? sessionStorage.getItem('userId');
72254
+ if (uid)
72255
+ return uid;
72256
+ }
72257
+ return null;
72258
+ }
72259
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ConversationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
72260
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ConversationService, providedIn: 'root' }); }
72261
+ }
72262
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ConversationService, decorators: [{
72263
+ type: Injectable,
72264
+ args: [{ providedIn: 'root' }]
72265
+ }], ctorParameters: () => [] });
72266
+
72267
+ /**
72268
+ * Chat Service Types
72269
+ *
72270
+ * Tipos compartidos para el sistema de chat en tiempo real.
72271
+ * Firestore paths:
72272
+ * /apps/{appId}/orgs/{orgId}/conversations/{convId}/messages/{msgId}
72273
+ * /apps/{appId}/orgs/{orgId}/conversations/{convId}/typing/{userId}
72274
+ */
72275
+
72276
+ addIcons({
72277
+ checkmark,
72278
+ checkmarkDone,
72279
+ timeOutline,
72280
+ alertCircle,
72281
+ pencil,
72282
+ trash,
72283
+ returnDownBackOutline,
72284
+ });
72285
+ const CHAT_I18N = {
72286
+ es: {
72287
+ messageDeleted: '[mensaje eliminado]',
72288
+ edited: '(editado)',
72289
+ react: 'Reaccionar',
72290
+ reply: 'Responder',
72291
+ edit: 'Editar',
72292
+ delete: 'Eliminar',
72293
+ },
72294
+ en: {
72295
+ messageDeleted: '[message deleted]',
72296
+ edited: '(edited)',
72297
+ react: 'React',
72298
+ reply: 'Reply',
72299
+ edit: 'Edit',
72300
+ delete: 'Delete',
72301
+ },
72302
+ };
72303
+ /**
72304
+ * val-message-bubble
72305
+ *
72306
+ * Burbuja de mensaje para el chat. Alineada a la derecha si isMine, a la izquierda si no.
72307
+ * Muestra nombre del emisor, cuerpo, timestamp, estado de envio, reacciones y acciones.
72308
+ * Si el mensaje fue eliminado muestra un texto en gris sin acciones.
72309
+ *
72310
+ * @example
72311
+ * <val-message-bubble
72312
+ * [props]="msg"
72313
+ * (reactionClick)="onReaction($event)"
72314
+ * (replyClick)="onReply($event)"
72315
+ * (deleteClick)="onDelete($event)"
72316
+ * />
72317
+ */
72318
+ class MessageBubbleComponent {
72319
+ constructor() {
72320
+ this.i18n = inject(I18nService);
72321
+ this.reactionClick = new EventEmitter();
72322
+ this.replyClick = new EventEmitter();
72323
+ this.deleteClick = new EventEmitter();
72324
+ this.editClick = new EventEmitter();
72325
+ if (!this.i18n.hasNamespace('Chat')) {
72326
+ this.i18n.registerDefaults('Chat', CHAT_I18N);
72327
+ }
72328
+ }
72329
+ ngOnInit() { }
72330
+ deletedText() {
72331
+ this.i18n.lang();
72332
+ return this.i18n.t('messageDeleted', 'Chat');
72333
+ }
72334
+ editedLabel() {
72335
+ this.i18n.lang();
72336
+ return this.i18n.t('edited', 'Chat');
72337
+ }
72338
+ formatTime(date) {
72339
+ if (!date)
72340
+ return '';
72341
+ const d = date instanceof Date ? date : new Date(date);
72342
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
72343
+ }
72344
+ statusIcon() {
72345
+ switch (this.props.status) {
72346
+ case 'sending':
72347
+ return 'time-outline';
72348
+ case 'failed':
72349
+ return 'alert-circle';
72350
+ case 'sent':
72351
+ default:
72352
+ return 'checkmark';
72353
+ }
72354
+ }
72355
+ onReactionClick(reaction) {
72356
+ this.reactionClick.emit({ token: reaction.token, msgId: this.props.msgId });
72357
+ }
72358
+ onReplyClick() {
72359
+ this.replyClick.emit({ msgId: this.props.msgId });
72360
+ }
72361
+ onDeleteClick() {
72362
+ this.deleteClick.emit({ msgId: this.props.msgId });
72363
+ }
72364
+ onEditClick() {
72365
+ this.editClick.emit({ msgId: this.props.msgId });
72366
+ }
72367
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageBubbleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
72368
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: MessageBubbleComponent, isStandalone: true, selector: "val-message-bubble", inputs: { props: "props" }, outputs: { reactionClick: "reactionClick", replyClick: "replyClick", deleteClick: "deleteClick", editClick: "editClick" }, ngImport: i0, template: `
72369
+ <div class="message-bubble-wrapper" [class.is-mine]="props.isMine">
72370
+ @if (!props.isMine && !props.isDeleted) {
72371
+ <span class="sender-name">{{ props.senderName }}</span>
72372
+ }
72373
+
72374
+ <div class="bubble">
72375
+ @if (props.replyTo && !props.isDeleted) {
72376
+ <div class="reply-preview">
72377
+ <div class="reply-sender">{{ props.replyTo.senderName }}</div>
72378
+ <div class="reply-body">{{ props.replyTo.body }}</div>
72379
+ </div>
72380
+ }
72381
+
72382
+ @if (props.isDeleted) {
72383
+ <span class="deleted-text">{{ deletedText() }}</span>
72384
+ } @else {
72385
+ <div class="bubble-body">{{ props.body }}</div>
72386
+ }
72387
+
72388
+ <div class="bubble-meta">
72389
+ <span class="timestamp">{{ formatTime(props.createdAt) }}</span>
72390
+ @if (props.isEdited && !props.isDeleted) {
72391
+ <span class="edited-label">{{ editedLabel() }}</span>
72392
+ }
72393
+ @if (props.isMine && props.status) {
72394
+ <ion-icon
72395
+ [name]="statusIcon()"
72396
+ [class]="'status-icon ' + (props.status ?? '')"
72397
+ aria-hidden="true"
72398
+ ></ion-icon>
72399
+ }
72400
+ </div>
72401
+ </div>
72402
+
72403
+ @if (!props.isDeleted && props.reactions && props.reactions.length > 0) {
72404
+ <div class="reactions-bar">
72405
+ @for (reaction of props.reactions; track reaction.token) {
72406
+ <button class="reaction-btn" [class.active]="reaction.active" (click)="onReactionClick(reaction)">
72407
+ <span>{{ reaction.token }}</span>
72408
+ @if (reaction.count > 0) {
72409
+ <span class="reaction-count">{{ reaction.count }}</span>
72410
+ }
72411
+ </button>
72412
+ }
72413
+ </div>
72414
+ }
72415
+
72416
+ @if (!props.isDeleted) {
72417
+ <div class="bubble-actions">
72418
+ <ion-button fill="clear" size="small" (click)="onReplyClick()">
72419
+ <ion-icon name="return-down-back-outline" slot="icon-only" aria-hidden="true"></ion-icon>
72420
+ </ion-button>
72421
+ @if (props.isMine) {
72422
+ <ion-button fill="clear" size="small" (click)="onEditClick()">
72423
+ <ion-icon name="pencil" slot="icon-only" aria-hidden="true"></ion-icon>
72424
+ </ion-button>
72425
+ <ion-button fill="clear" size="small" color="danger" (click)="onDeleteClick()">
72426
+ <ion-icon name="trash" slot="icon-only" aria-hidden="true"></ion-icon>
72427
+ </ion-button>
72428
+ }
72429
+ </div>
72430
+ }
72431
+ </div>
72432
+ `, isInline: true, styles: [".message-bubble-wrapper{display:flex;flex-direction:column;align-items:flex-start;margin-bottom:4px}.message-bubble-wrapper.is-mine{align-items:flex-end}.message-bubble-wrapper.is-mine .bubble{background:var(--ion-color-primary);color:#fff;border-radius:18px 18px 4px}.message-bubble-wrapper.is-mine .bubble-meta{justify-content:flex-end}.message-bubble-wrapper:not(.is-mine) .bubble{background:var(--ion-card-background, var(--ion-background-color, #fff));color:var(--ion-text-color, #000);border-radius:18px 18px 18px 4px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}.sender-name{font-size:.75rem;font-weight:600;color:var(--ion-color-medium, #92949c);margin:0 0 2px 12px}.bubble{max-width:75%;min-width:60px;padding:10px 14px;word-break:break-word;position:relative;box-shadow:0 1px 2px #0000000f}.reply-preview{background:#0000000f;border-left:3px solid var(--ion-color-primary);border-radius:6px;padding:6px 8px;margin-bottom:6px;font-size:.75rem;opacity:.85}.reply-preview .reply-sender{font-weight:700;margin-bottom:2px}.reply-preview .reply-body{opacity:.8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px}.bubble-body{font-size:.9375rem;line-height:1.4;white-space:pre-wrap}.deleted-text{font-size:.875rem;font-style:italic;opacity:.6}.bubble-meta{display:flex;align-items:center;gap:4px;margin-top:4px;font-size:.6875rem;opacity:.7}.edited-label{font-style:italic}.status-icon{font-size:.75rem}.status-icon.sending{color:var(--ion-color-medium)}.status-icon.sent{color:#fff}.status-icon.failed{color:var(--ion-color-danger)}.reactions-bar{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.reactions-bar .reaction-btn{display:inline-flex;align-items:center;gap:3px;background:var(--ion-card-background, var(--ion-background-color, #fff));border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));border-radius:12px;padding:2px 8px;font-size:.8125rem;cursor:pointer;transition:background .15s ease}.reactions-bar .reaction-btn.active{background:var(--ion-color-primary-tint, #4c8dff);color:#fff;border-color:var(--ion-color-primary)}.reactions-bar .reaction-btn .reaction-count{font-weight:600}.bubble-actions{display:flex;gap:4px;margin-top:2px}.bubble-actions ion-button{--padding-start: 6px;--padding-end: 6px;font-size:.75rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }] }); }
72433
+ }
72434
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MessageBubbleComponent, decorators: [{
72435
+ type: Component,
72436
+ args: [{ selector: 'val-message-bubble', standalone: true, imports: [CommonModule, IonIcon, IonButton], template: `
72437
+ <div class="message-bubble-wrapper" [class.is-mine]="props.isMine">
72438
+ @if (!props.isMine && !props.isDeleted) {
72439
+ <span class="sender-name">{{ props.senderName }}</span>
72440
+ }
72441
+
72442
+ <div class="bubble">
72443
+ @if (props.replyTo && !props.isDeleted) {
72444
+ <div class="reply-preview">
72445
+ <div class="reply-sender">{{ props.replyTo.senderName }}</div>
72446
+ <div class="reply-body">{{ props.replyTo.body }}</div>
72447
+ </div>
72448
+ }
72449
+
72450
+ @if (props.isDeleted) {
72451
+ <span class="deleted-text">{{ deletedText() }}</span>
72452
+ } @else {
72453
+ <div class="bubble-body">{{ props.body }}</div>
72454
+ }
72455
+
72456
+ <div class="bubble-meta">
72457
+ <span class="timestamp">{{ formatTime(props.createdAt) }}</span>
72458
+ @if (props.isEdited && !props.isDeleted) {
72459
+ <span class="edited-label">{{ editedLabel() }}</span>
72460
+ }
72461
+ @if (props.isMine && props.status) {
72462
+ <ion-icon
72463
+ [name]="statusIcon()"
72464
+ [class]="'status-icon ' + (props.status ?? '')"
72465
+ aria-hidden="true"
72466
+ ></ion-icon>
72467
+ }
72468
+ </div>
72469
+ </div>
72470
+
72471
+ @if (!props.isDeleted && props.reactions && props.reactions.length > 0) {
72472
+ <div class="reactions-bar">
72473
+ @for (reaction of props.reactions; track reaction.token) {
72474
+ <button class="reaction-btn" [class.active]="reaction.active" (click)="onReactionClick(reaction)">
72475
+ <span>{{ reaction.token }}</span>
72476
+ @if (reaction.count > 0) {
72477
+ <span class="reaction-count">{{ reaction.count }}</span>
72478
+ }
72479
+ </button>
72480
+ }
72481
+ </div>
72482
+ }
72483
+
72484
+ @if (!props.isDeleted) {
72485
+ <div class="bubble-actions">
72486
+ <ion-button fill="clear" size="small" (click)="onReplyClick()">
72487
+ <ion-icon name="return-down-back-outline" slot="icon-only" aria-hidden="true"></ion-icon>
72488
+ </ion-button>
72489
+ @if (props.isMine) {
72490
+ <ion-button fill="clear" size="small" (click)="onEditClick()">
72491
+ <ion-icon name="pencil" slot="icon-only" aria-hidden="true"></ion-icon>
72492
+ </ion-button>
72493
+ <ion-button fill="clear" size="small" color="danger" (click)="onDeleteClick()">
72494
+ <ion-icon name="trash" slot="icon-only" aria-hidden="true"></ion-icon>
72495
+ </ion-button>
72496
+ }
72497
+ </div>
72498
+ }
72499
+ </div>
72500
+ `, styles: [".message-bubble-wrapper{display:flex;flex-direction:column;align-items:flex-start;margin-bottom:4px}.message-bubble-wrapper.is-mine{align-items:flex-end}.message-bubble-wrapper.is-mine .bubble{background:var(--ion-color-primary);color:#fff;border-radius:18px 18px 4px}.message-bubble-wrapper.is-mine .bubble-meta{justify-content:flex-end}.message-bubble-wrapper:not(.is-mine) .bubble{background:var(--ion-card-background, var(--ion-background-color, #fff));color:var(--ion-text-color, #000);border-radius:18px 18px 18px 4px;border:1px solid var(--ion-border-color, rgba(0, 0, 0, .08))}.sender-name{font-size:.75rem;font-weight:600;color:var(--ion-color-medium, #92949c);margin:0 0 2px 12px}.bubble{max-width:75%;min-width:60px;padding:10px 14px;word-break:break-word;position:relative;box-shadow:0 1px 2px #0000000f}.reply-preview{background:#0000000f;border-left:3px solid var(--ion-color-primary);border-radius:6px;padding:6px 8px;margin-bottom:6px;font-size:.75rem;opacity:.85}.reply-preview .reply-sender{font-weight:700;margin-bottom:2px}.reply-preview .reply-body{opacity:.8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px}.bubble-body{font-size:.9375rem;line-height:1.4;white-space:pre-wrap}.deleted-text{font-size:.875rem;font-style:italic;opacity:.6}.bubble-meta{display:flex;align-items:center;gap:4px;margin-top:4px;font-size:.6875rem;opacity:.7}.edited-label{font-style:italic}.status-icon{font-size:.75rem}.status-icon.sending{color:var(--ion-color-medium)}.status-icon.sent{color:#fff}.status-icon.failed{color:var(--ion-color-danger)}.reactions-bar{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.reactions-bar .reaction-btn{display:inline-flex;align-items:center;gap:3px;background:var(--ion-card-background, var(--ion-background-color, #fff));border:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));border-radius:12px;padding:2px 8px;font-size:.8125rem;cursor:pointer;transition:background .15s ease}.reactions-bar .reaction-btn.active{background:var(--ion-color-primary-tint, #4c8dff);color:#fff;border-color:var(--ion-color-primary)}.reactions-bar .reaction-btn .reaction-count{font-weight:600}.bubble-actions{display:flex;gap:4px;margin-top:2px}.bubble-actions ion-button{--padding-start: 6px;--padding-end: 6px;font-size:.75rem}\n"] }]
72501
+ }], ctorParameters: () => [], propDecorators: { props: [{
72502
+ type: Input
72503
+ }], reactionClick: [{
72504
+ type: Output
72505
+ }], replyClick: [{
72506
+ type: Output
72507
+ }], deleteClick: [{
72508
+ type: Output
72509
+ }], editClick: [{
72510
+ type: Output
72511
+ }] } });
72512
+
72513
+ addIcons({ sendOutline });
72514
+ const CHAT_INPUT_I18N = {
72515
+ es: {
72516
+ placeholder: 'Escribe un mensaje',
72517
+ send: 'Enviar',
72518
+ counter: '{count}/{max}',
72519
+ },
72520
+ en: {
72521
+ placeholder: 'Write a message',
72522
+ send: 'Send',
72523
+ counter: '{count}/{max}',
72524
+ },
72525
+ };
72526
+ /**
72527
+ * val-chat-input
72528
+ *
72529
+ * Campo de entrada para mensajes de chat. Textarea auto-grow con contador
72530
+ * de caracteres y boton de enviar. Emite send() al pulsar el boton o Enter
72531
+ * (sin Shift), y typing() en cada keydown.
72532
+ *
72533
+ * @example
72534
+ * <val-chat-input
72535
+ * [props]="{ placeholder: 'Mensaje...', maxLength: 1000 }"
72536
+ * (send)="onSend($event)"
72537
+ * (typing)="onTyping()"
72538
+ * />
72539
+ */
72540
+ class ChatInputComponent {
72541
+ constructor() {
72542
+ this.i18n = inject(I18nService);
72543
+ this.props = {};
72544
+ this.send = new EventEmitter();
72545
+ this.typing = new EventEmitter();
72546
+ this.body = signal('');
72547
+ this.maxLen = computed(() => this.props?.maxLength ?? 2000);
72548
+ this.canSend = computed(() => {
72549
+ const trimmed = this.body().trim();
72550
+ return trimmed.length > 0 && trimmed.length <= this.maxLen();
72551
+ });
72552
+ this.isNearLimit = computed(() => {
72553
+ const len = this.body().length;
72554
+ const max = this.maxLen();
72555
+ return len >= max * 0.85 && len < max;
72556
+ });
72557
+ this.isAtLimit = computed(() => this.body().length >= this.maxLen());
72558
+ this.placeholderText = computed(() => {
72559
+ this.i18n.lang();
72560
+ return this.props?.placeholder ?? this.i18n.t('placeholder', 'ChatInput');
72561
+ });
72562
+ this.sendLabel = computed(() => {
72563
+ this.i18n.lang();
72564
+ return this.i18n.t('send', 'ChatInput');
72565
+ });
72566
+ this.counterText = computed(() => {
72567
+ this.i18n.lang();
72568
+ return this.i18n.t('counter', 'ChatInput', {
72569
+ count: String(this.body().length),
72570
+ max: String(this.maxLen()),
72571
+ });
72572
+ });
72573
+ this.i18n.registerDefaults('ChatInput', CHAT_INPUT_I18N);
72574
+ }
72575
+ onInput(event) {
72576
+ const target = event.target;
72577
+ this.body.set(target.value ?? '');
72578
+ }
72579
+ onKeydown(event) {
72580
+ this.typing.emit();
72581
+ if (event.key === 'Enter' && !event.shiftKey) {
72582
+ event.preventDefault();
72583
+ this.onSend();
72584
+ }
72585
+ }
72586
+ onSend() {
72587
+ const trimmed = this.body().trim();
72588
+ if (!trimmed || trimmed.length > this.maxLen())
72589
+ return;
72590
+ this.send.emit(trimmed);
72591
+ this.body.set('');
72592
+ }
72593
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
72594
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: ChatInputComponent, isStandalone: true, selector: "val-chat-input", inputs: { props: "props" }, outputs: { send: "send", typing: "typing" }, ngImport: i0, template: `
72595
+ <div class="chat-input-wrapper">
72596
+ <div class="textarea-wrap">
72597
+ <ion-textarea
72598
+ [placeholder]="placeholderText()"
72599
+ [disabled]="props?.disabled ?? false"
72600
+ [maxlength]="maxLen()"
72601
+ [autoGrow]="true"
72602
+ [rows]="1"
72603
+ [value]="body()"
72604
+ (ionInput)="onInput($event)"
72605
+ (keydown)="onKeydown($event)"
72606
+ ></ion-textarea>
72607
+ <span class="char-counter" [class.near-limit]="isNearLimit()" [class.at-limit]="isAtLimit()">
72608
+ {{ counterText() }}
72609
+ </span>
72610
+ </div>
72611
+
72612
+ <ion-button
72613
+ class="send-btn"
72614
+ shape="round"
72615
+ [disabled]="!canSend()"
72616
+ (click)="onSend()"
72617
+ [attr.aria-label]="sendLabel()"
72618
+ >
72619
+ <ion-icon name="send-outline" slot="icon-only" aria-hidden="true"></ion-icon>
72620
+ </ion-button>
72621
+ </div>
72622
+ `, isInline: true, styles: [".chat-input-wrapper{display:flex;align-items:flex-end;gap:8px;padding:8px 12px;background:var(--ion-card-background, var(--ion-background-color, #fff));border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .1))}.textarea-wrap{flex:1;position:relative}.textarea-wrap ion-textarea{--background: var(--ion-color-light, #f4f5f8);--border-radius: 20px;--padding-start: 14px;--padding-end: 14px;--padding-top: 8px;--padding-bottom: 8px;font-size:.9375rem;min-height:40px}.char-counter{position:absolute;bottom:4px;right:14px;font-size:.6875rem;color:var(--ion-color-medium, #92949c);pointer-events:none}.char-counter.near-limit{color:var(--ion-color-warning, #ffc409)}.char-counter.at-limit{color:var(--ion-color-danger, #eb445a)}.send-btn{--border-radius: 50%;--padding-start: 0;--padding-end: 0;width:40px;height:40px;flex-shrink:0;margin:0}.send-btn ion-icon{font-size:1.25rem}\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: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }] }); }
72623
+ }
72624
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatInputComponent, decorators: [{
72625
+ type: Component,
72626
+ args: [{ selector: 'val-chat-input', standalone: true, imports: [CommonModule, IonTextarea, IonButton, IonIcon], template: `
72627
+ <div class="chat-input-wrapper">
72628
+ <div class="textarea-wrap">
72629
+ <ion-textarea
72630
+ [placeholder]="placeholderText()"
72631
+ [disabled]="props?.disabled ?? false"
72632
+ [maxlength]="maxLen()"
72633
+ [autoGrow]="true"
72634
+ [rows]="1"
72635
+ [value]="body()"
72636
+ (ionInput)="onInput($event)"
72637
+ (keydown)="onKeydown($event)"
72638
+ ></ion-textarea>
72639
+ <span class="char-counter" [class.near-limit]="isNearLimit()" [class.at-limit]="isAtLimit()">
72640
+ {{ counterText() }}
72641
+ </span>
72642
+ </div>
72643
+
72644
+ <ion-button
72645
+ class="send-btn"
72646
+ shape="round"
72647
+ [disabled]="!canSend()"
72648
+ (click)="onSend()"
72649
+ [attr.aria-label]="sendLabel()"
72650
+ >
72651
+ <ion-icon name="send-outline" slot="icon-only" aria-hidden="true"></ion-icon>
72652
+ </ion-button>
72653
+ </div>
72654
+ `, styles: [".chat-input-wrapper{display:flex;align-items:flex-end;gap:8px;padding:8px 12px;background:var(--ion-card-background, var(--ion-background-color, #fff));border-top:1px solid var(--ion-border-color, rgba(0, 0, 0, .1))}.textarea-wrap{flex:1;position:relative}.textarea-wrap ion-textarea{--background: var(--ion-color-light, #f4f5f8);--border-radius: 20px;--padding-start: 14px;--padding-end: 14px;--padding-top: 8px;--padding-bottom: 8px;font-size:.9375rem;min-height:40px}.char-counter{position:absolute;bottom:4px;right:14px;font-size:.6875rem;color:var(--ion-color-medium, #92949c);pointer-events:none}.char-counter.near-limit{color:var(--ion-color-warning, #ffc409)}.char-counter.at-limit{color:var(--ion-color-danger, #eb445a)}.send-btn{--border-radius: 50%;--padding-start: 0;--padding-end: 0;width:40px;height:40px;flex-shrink:0;margin:0}.send-btn ion-icon{font-size:1.25rem}\n"] }]
72655
+ }], ctorParameters: () => [], propDecorators: { props: [{
72656
+ type: Input
72657
+ }], send: [{
72658
+ type: Output
72659
+ }], typing: [{
72660
+ type: Output
72661
+ }] } });
72662
+
72663
+ const TYPING_I18N = {
72664
+ es: {
72665
+ one: '{name} esta escribiendo',
72666
+ two: '{name} y {other} estan escribiendo',
72667
+ many: 'Varios estan escribiendo',
72668
+ },
72669
+ en: {
72670
+ one: '{name} is typing',
72671
+ two: '{name} and {other} are typing',
72672
+ many: 'Several people are typing',
72673
+ },
72674
+ };
72675
+ /**
72676
+ * val-typing-indicator
72677
+ *
72678
+ * Muestra quienes estan escribiendo en el chat con animacion de tres puntos.
72679
+ * Se oculta automaticamente si typingUsers esta vacio.
72680
+ *
72681
+ * @example
72682
+ * <val-typing-indicator [typingUsers]="['Maria', 'Carlos']" />
72683
+ */
72684
+ class TypingIndicatorComponent {
72685
+ constructor() {
72686
+ this.i18n = inject(I18nService);
72687
+ this.typingUsers = [];
72688
+ this.typingText = computed(() => {
72689
+ this.i18n.lang();
72690
+ const users = this.typingUsers ?? [];
72691
+ if (users.length === 0)
72692
+ return '';
72693
+ if (users.length === 1) {
72694
+ return this.i18n.t('one', 'TypingIndicator', { name: users[0] });
72695
+ }
72696
+ if (users.length === 2) {
72697
+ return this.i18n.t('two', 'TypingIndicator', { name: users[0], other: users[1] });
72698
+ }
72699
+ return this.i18n.t('many', 'TypingIndicator');
72700
+ });
72701
+ this.i18n.registerDefaults('TypingIndicator', TYPING_I18N);
72702
+ }
72703
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TypingIndicatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
72704
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: TypingIndicatorComponent, isStandalone: true, selector: "val-typing-indicator", inputs: { typingUsers: "typingUsers" }, ngImport: i0, template: `
72705
+ @if (typingUsers && typingUsers.length > 0) {
72706
+ <div class="typing-indicator">
72707
+ <div class="dots">
72708
+ <span></span>
72709
+ <span></span>
72710
+ <span></span>
72711
+ </div>
72712
+ <span class="typing-text">{{ typingText() }}</span>
72713
+ </div>
72714
+ }
72715
+ `, isInline: true, styles: [".typing-indicator{display:flex;align-items:center;gap:8px;padding:4px 12px;min-height:28px}.typing-text{font-size:.8125rem;color:var(--ion-color-medium, #92949c);font-style:italic}.dots{display:flex;align-items:center;gap:3px}.dots span{display:inline-block;width:5px;height:5px;border-radius:50%;background:var(--ion-color-medium, #92949c);animation:typing-dot 1.2s infinite ease-in-out}.dots span:nth-child(1){animation-delay:0s}.dots span:nth-child(2){animation-delay:.2s}.dots span:nth-child(3){animation-delay:.4s}@keyframes typing-dot{0%,60%,to{transform:translateY(0);opacity:.4}30%{transform:translateY(-4px);opacity:1}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] }); }
72716
+ }
72717
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TypingIndicatorComponent, decorators: [{
72718
+ type: Component,
72719
+ args: [{ selector: 'val-typing-indicator', standalone: true, imports: [CommonModule], template: `
72720
+ @if (typingUsers && typingUsers.length > 0) {
72721
+ <div class="typing-indicator">
72722
+ <div class="dots">
72723
+ <span></span>
72724
+ <span></span>
72725
+ <span></span>
72726
+ </div>
72727
+ <span class="typing-text">{{ typingText() }}</span>
72728
+ </div>
72729
+ }
72730
+ `, styles: [".typing-indicator{display:flex;align-items:center;gap:8px;padding:4px 12px;min-height:28px}.typing-text{font-size:.8125rem;color:var(--ion-color-medium, #92949c);font-style:italic}.dots{display:flex;align-items:center;gap:3px}.dots span{display:inline-block;width:5px;height:5px;border-radius:50%;background:var(--ion-color-medium, #92949c);animation:typing-dot 1.2s infinite ease-in-out}.dots span:nth-child(1){animation-delay:0s}.dots span:nth-child(2){animation-delay:.2s}.dots span:nth-child(3){animation-delay:.4s}@keyframes typing-dot{0%,60%,to{transform:translateY(0);opacity:.4}30%{transform:translateY(-4px);opacity:1}}\n"] }]
72731
+ }], ctorParameters: () => [], propDecorators: { typingUsers: [{
72732
+ type: Input
72733
+ }] } });
72734
+
72735
+ addIcons({ chatbubblesOutline });
72736
+ const CHAT_WINDOW_I18N = {
72737
+ es: {
72738
+ conversationClosed: 'Esta conversacion esta cerrada',
72739
+ loading: 'Cargando mensajes...',
72740
+ noMessages: 'No hay mensajes aun',
72741
+ },
72742
+ en: {
72743
+ conversationClosed: 'This conversation is closed',
72744
+ loading: 'Loading messages...',
72745
+ noMessages: 'No messages yet',
72746
+ },
72747
+ };
72748
+ /**
72749
+ * val-chat-window
72750
+ *
72751
+ * Ventana de chat completa. Muestra la lista de mensajes agrupados por fecha,
72752
+ * el indicador de escritura, el input de mensaje (solo si isOpen) y un banner
72753
+ * cuando la conversacion esta cerrada. Auto-scroll al ultimo mensaje cuando
72754
+ * llegan mensajes nuevos.
72755
+ *
72756
+ * @example
72757
+ * <val-chat-window
72758
+ * [convId]="conv.id"
72759
+ * [messages]="messages()"
72760
+ * [currentUserId]="userId"
72761
+ * [isOpen]="conv.status === 'open'"
72762
+ * [typingUsers]="typingUsers()"
72763
+ * (sendMessage)="onSend($event)"
72764
+ * (reactionClick)="onReaction($event)"
72765
+ * />
72766
+ */
72767
+ class ChatWindowComponent {
72768
+ constructor() {
72769
+ this.i18n = inject(I18nService);
72770
+ this.convId = '';
72771
+ this.messages = [];
72772
+ this.participants = [];
72773
+ this.currentUserId = '';
72774
+ this.isOpen = true;
72775
+ this.typingUsers = [];
72776
+ this.isLoading = false;
72777
+ this.sendMessage = new EventEmitter();
72778
+ this.loadMore = new EventEmitter();
72779
+ this.reactionClick = new EventEmitter();
72780
+ this.deleteMessage = new EventEmitter();
72781
+ this.replyTo = new EventEmitter();
72782
+ this.prevMessageCount = 0;
72783
+ this.shouldScrollToBottom = false;
72784
+ this.messageGroups = computed(() => {
72785
+ const msgs = this.messages ?? [];
72786
+ if (msgs.length === 0)
72787
+ return [];
72788
+ const groups = [];
72789
+ let currentLabel = '';
72790
+ let currentGroup = [];
72791
+ for (const msg of msgs) {
72792
+ const label = this.formatDateLabel(msg.createdAt);
72793
+ if (label !== currentLabel) {
72794
+ if (currentGroup.length > 0) {
72795
+ groups.push({ dateLabel: currentLabel, messages: currentGroup });
72796
+ }
72797
+ currentLabel = label;
72798
+ currentGroup = [msg];
72799
+ }
72800
+ else {
72801
+ currentGroup.push(msg);
72802
+ }
72803
+ }
72804
+ if (currentGroup.length > 0) {
72805
+ groups.push({ dateLabel: currentLabel, messages: currentGroup });
72806
+ }
72807
+ return groups;
72808
+ });
72809
+ this.closedText = computed(() => {
72810
+ this.i18n.lang();
72811
+ return this.i18n.t('conversationClosed', 'ChatWindow');
72812
+ });
72813
+ this.loadingText = computed(() => {
72814
+ this.i18n.lang();
72815
+ return this.i18n.t('loading', 'ChatWindow');
72816
+ });
72817
+ this.i18n.registerDefaults('ChatWindow', CHAT_WINDOW_I18N);
72818
+ }
72819
+ ngOnChanges(changes) {
72820
+ if (changes['messages']) {
72821
+ const current = changes['messages'].currentValue ?? [];
72822
+ if (current.length !== this.prevMessageCount) {
72823
+ this.prevMessageCount = current.length;
72824
+ this.shouldScrollToBottom = true;
72825
+ }
72826
+ }
72827
+ }
72828
+ ngAfterViewChecked() {
72829
+ if (this.shouldScrollToBottom) {
72830
+ this.scrollToBottom();
72831
+ this.shouldScrollToBottom = false;
72832
+ }
72833
+ }
72834
+ onReactionClick(event) {
72835
+ this.reactionClick.emit({ msgId: event.msgId, token: event.token });
72836
+ }
72837
+ onEditClick(_msgId) {
72838
+ // Implementacion futura: emitir evento de edicion
72839
+ }
72840
+ onTyping() {
72841
+ // El padre puede suscribirse al output typing si necesita propagar a Firestore
72842
+ }
72843
+ scrollToBottom() {
72844
+ const el = this.messagesListRef?.nativeElement;
72845
+ if (el) {
72846
+ el.scrollTop = el.scrollHeight;
72847
+ }
72848
+ }
72849
+ formatDateLabel(date) {
72850
+ if (!date)
72851
+ return '';
72852
+ const d = date instanceof Date ? date : new Date(date);
72853
+ const now = new Date();
72854
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
72855
+ const msgDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
72856
+ const diffMs = today.getTime() - msgDay.getTime();
72857
+ const diffDays = Math.round(diffMs / 86400000);
72858
+ if (diffDays === 0)
72859
+ return this.i18n.t('today', '_global') || 'Hoy';
72860
+ if (diffDays === 1)
72861
+ return this.i18n.t('yesterday', '_global') || 'Ayer';
72862
+ return d.toLocaleDateString([], { day: '2-digit', month: 'short', year: 'numeric' });
72863
+ }
72864
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatWindowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
72865
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ChatWindowComponent, isStandalone: true, selector: "val-chat-window", inputs: { convId: "convId", messages: "messages", participants: "participants", currentUserId: "currentUserId", isOpen: "isOpen", typingUsers: "typingUsers", isLoading: "isLoading" }, outputs: { sendMessage: "sendMessage", loadMore: "loadMore", reactionClick: "reactionClick", deleteMessage: "deleteMessage", replyTo: "replyTo" }, viewQueries: [{ propertyName: "messagesListRef", first: true, predicate: ["messagesList"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
72866
+ <div class="chat-window">
72867
+ <div class="messages-list" #messagesList>
72868
+ @if (isLoading) {
72869
+ <div class="loading-state">
72870
+ <ion-spinner name="dots"></ion-spinner>
72871
+ <span>{{ loadingText() }}</span>
72872
+ </div>
72873
+ } @else {
72874
+ @for (group of messageGroups(); track group.dateLabel) {
72875
+ <div class="date-separator">
72876
+ <div class="date-line"></div>
72877
+ <span class="date-label">{{ group.dateLabel }}</span>
72878
+ <div class="date-line"></div>
72879
+ </div>
72880
+ @for (msg of group.messages; track msg.msgId) {
72881
+ <val-message-bubble
72882
+ [props]="msg"
72883
+ (reactionClick)="onReactionClick($event)"
72884
+ (replyClick)="replyTo.emit($event.msgId)"
72885
+ (deleteClick)="deleteMessage.emit($event.msgId)"
72886
+ (editClick)="onEditClick($event.msgId)"
72887
+ />
72888
+ }
72889
+ }
72890
+ }
72891
+ </div>
72892
+
72893
+ <val-typing-indicator [typingUsers]="typingUsers" />
72894
+
72895
+ @if (!isOpen) {
72896
+ <div class="closed-banner">{{ closedText() }}</div>
72897
+ } @else {
72898
+ <val-chat-input [props]="{ disabled: isLoading }" (send)="sendMessage.emit($event)" (typing)="onTyping()" />
72899
+ }
72900
+ </div>
72901
+ `, isInline: true, styles: [".chat-window{display:flex;flex-direction:column;height:100%;background:var(--ion-background-color, #f4f5f8);overflow:hidden}.messages-list{flex:1;overflow-y:auto;padding:12px 8px;scroll-behavior:smooth}.messages-list::-webkit-scrollbar{width:4px}.messages-list::-webkit-scrollbar-thumb{background:var(--ion-color-light-shade, #d7d8da);border-radius:2px}.date-separator{display:flex;align-items:center;gap:8px;margin:12px 0}.date-separator .date-line{flex:1;height:1px;background:var(--ion-border-color, rgba(0, 0, 0, .1))}.date-separator .date-label{font-size:.75rem;color:var(--ion-color-medium, #92949c);white-space:nowrap;padding:0 4px}.loading-state{display:flex;align-items:center;justify-content:center;padding:24px;color:var(--ion-color-medium, #92949c);font-size:.875rem;gap:8px}.closed-banner{padding:10px 16px;background:var(--ion-color-warning-tint, #fff9e6);border-top:1px solid var(--ion-color-warning, #ffc409);text-align:center;font-size:.875rem;color:var(--ion-color-warning-shade, #b29500)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: MessageBubbleComponent, selector: "val-message-bubble", inputs: ["props"], outputs: ["reactionClick", "replyClick", "deleteClick", "editClick"] }, { kind: "component", type: ChatInputComponent, selector: "val-chat-input", inputs: ["props"], outputs: ["send", "typing"] }, { kind: "component", type: TypingIndicatorComponent, selector: "val-typing-indicator", inputs: ["typingUsers"] }] }); }
72902
+ }
72903
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChatWindowComponent, decorators: [{
72904
+ type: Component,
72905
+ args: [{ selector: 'val-chat-window', standalone: true, imports: [CommonModule, IonIcon, IonSpinner, MessageBubbleComponent, ChatInputComponent, TypingIndicatorComponent], template: `
72906
+ <div class="chat-window">
72907
+ <div class="messages-list" #messagesList>
72908
+ @if (isLoading) {
72909
+ <div class="loading-state">
72910
+ <ion-spinner name="dots"></ion-spinner>
72911
+ <span>{{ loadingText() }}</span>
72912
+ </div>
72913
+ } @else {
72914
+ @for (group of messageGroups(); track group.dateLabel) {
72915
+ <div class="date-separator">
72916
+ <div class="date-line"></div>
72917
+ <span class="date-label">{{ group.dateLabel }}</span>
72918
+ <div class="date-line"></div>
72919
+ </div>
72920
+ @for (msg of group.messages; track msg.msgId) {
72921
+ <val-message-bubble
72922
+ [props]="msg"
72923
+ (reactionClick)="onReactionClick($event)"
72924
+ (replyClick)="replyTo.emit($event.msgId)"
72925
+ (deleteClick)="deleteMessage.emit($event.msgId)"
72926
+ (editClick)="onEditClick($event.msgId)"
72927
+ />
72928
+ }
72929
+ }
72930
+ }
72931
+ </div>
72932
+
72933
+ <val-typing-indicator [typingUsers]="typingUsers" />
72934
+
72935
+ @if (!isOpen) {
72936
+ <div class="closed-banner">{{ closedText() }}</div>
72937
+ } @else {
72938
+ <val-chat-input [props]="{ disabled: isLoading }" (send)="sendMessage.emit($event)" (typing)="onTyping()" />
72939
+ }
72940
+ </div>
72941
+ `, styles: [".chat-window{display:flex;flex-direction:column;height:100%;background:var(--ion-background-color, #f4f5f8);overflow:hidden}.messages-list{flex:1;overflow-y:auto;padding:12px 8px;scroll-behavior:smooth}.messages-list::-webkit-scrollbar{width:4px}.messages-list::-webkit-scrollbar-thumb{background:var(--ion-color-light-shade, #d7d8da);border-radius:2px}.date-separator{display:flex;align-items:center;gap:8px;margin:12px 0}.date-separator .date-line{flex:1;height:1px;background:var(--ion-border-color, rgba(0, 0, 0, .1))}.date-separator .date-label{font-size:.75rem;color:var(--ion-color-medium, #92949c);white-space:nowrap;padding:0 4px}.loading-state{display:flex;align-items:center;justify-content:center;padding:24px;color:var(--ion-color-medium, #92949c);font-size:.875rem;gap:8px}.closed-banner{padding:10px 16px;background:var(--ion-color-warning-tint, #fff9e6);border-top:1px solid var(--ion-color-warning, #ffc409);text-align:center;font-size:.875rem;color:var(--ion-color-warning-shade, #b29500)}\n"] }]
72942
+ }], ctorParameters: () => [], propDecorators: { convId: [{
72943
+ type: Input
72944
+ }], messages: [{
72945
+ type: Input
72946
+ }], participants: [{
72947
+ type: Input
72948
+ }], currentUserId: [{
72949
+ type: Input
72950
+ }], isOpen: [{
72951
+ type: Input
72952
+ }], typingUsers: [{
72953
+ type: Input
72954
+ }], isLoading: [{
72955
+ type: Input
72956
+ }], sendMessage: [{
72957
+ type: Output
72958
+ }], loadMore: [{
72959
+ type: Output
72960
+ }], reactionClick: [{
72961
+ type: Output
72962
+ }], deleteMessage: [{
72963
+ type: Output
72964
+ }], replyTo: [{
72965
+ type: Output
72966
+ }], messagesListRef: [{
72967
+ type: ViewChild,
72968
+ args: ['messagesList']
72969
+ }] } });
72970
+
72971
+ const THREAD_PANEL_I18N = {
72972
+ es: {
72973
+ loading: 'Cargando conversacion...',
72974
+ thread: 'Hilo',
72975
+ },
72976
+ en: {
72977
+ loading: 'Loading conversation...',
72978
+ thread: 'Thread',
72979
+ },
72980
+ };
72981
+ /**
72982
+ * val-thread-panel
72983
+ *
72984
+ * Panel de hilo de conversacion asociado a una entidad (post, request, etc.).
72985
+ * Internamente usa ConversationService.getOrCreateForEntity() para inicializar
72986
+ * la conversacion y luego watchMessages() para escuchar en tiempo real.
72987
+ * Renderiza val-chat-window con los mensajes ya mapeados.
72988
+ *
72989
+ * Requiere provideValtechChat() y provideValtechFirebase() en main.ts.
72990
+ *
72991
+ * @example
72992
+ * <val-thread-panel
72993
+ * entityType="post"
72994
+ * entityId="post-abc123"
72995
+ * currentUserId="user-xyz"
72996
+ * title="Comentarios"
72997
+ * />
72998
+ */
72999
+ class ThreadPanelComponent {
73000
+ constructor() {
73001
+ this.i18n = inject(I18nService);
73002
+ this.conversationSvc = inject(ConversationService, { optional: true });
73003
+ this.chatConfig = inject(VALTECH_CHAT_CONFIG, { optional: true });
73004
+ this.auth = inject(AuthService, { optional: true });
73005
+ this.entityType = '';
73006
+ this.entityId = '';
73007
+ this.currentUserId = '';
73008
+ this.loading = signal(true);
73009
+ this.conv = signal(null);
73010
+ this.messages = signal([]);
73011
+ this.typingUsers = signal([]);
73012
+ this.isConvOpen = computed(() => this.conv()?.status === 'open');
73013
+ this.loadingText = computed(() => {
73014
+ this.i18n.lang();
73015
+ return this.i18n.t('loading', 'ThreadPanel');
73016
+ });
73017
+ this.bubbleMessages = computed(() => {
73018
+ const uid = this.currentUserId || this.auth?.user()?.userId || '';
73019
+ return this.messages().map(msg => ({
73020
+ msgId: msg.id,
73021
+ senderId: msg.senderId,
73022
+ senderName: msg.senderName,
73023
+ senderAvatar: msg.senderAvatar,
73024
+ body: msg.body,
73025
+ type: msg.type,
73026
+ createdAt: msg.createdAt,
73027
+ isMine: msg.senderId === uid,
73028
+ isDeleted: msg.isDeleted,
73029
+ isEdited: msg.isEdited,
73030
+ replyTo: msg.replyTo,
73031
+ attachments: msg.attachments,
73032
+ reactions: msg.reactions,
73033
+ status: msg.status,
73034
+ }));
73035
+ });
73036
+ this.i18n.registerDefaults('ThreadPanel', THREAD_PANEL_I18N);
73037
+ }
73038
+ async ngOnInit() {
73039
+ if (!this.conversationSvc || !this.entityType || !this.entityId) {
73040
+ this.loading.set(false);
73041
+ return;
73042
+ }
73043
+ try {
73044
+ const conversation = await firstValueFrom(this.conversationSvc.getOrCreateForEntity(this.entityType, this.entityId));
73045
+ this.conv.set(conversation);
73046
+ const orgId = this.getOrgId();
73047
+ const appId = this.chatConfig?.appId ?? '';
73048
+ if (orgId && appId) {
73049
+ this.msgSub = this.conversationSvc
73050
+ .watchMessages(conversation.id, orgId, appId)
73051
+ .subscribe(msgs => this.messages.set(msgs));
73052
+ this.typingSub = this.conversationSvc
73053
+ .watchTyping(conversation.id, orgId, appId)
73054
+ .subscribe(users => this.typingUsers.set(users));
73055
+ }
73056
+ }
73057
+ catch (err) {
73058
+ console.error('[ThreadPanel] Error al inicializar conversacion:', err);
73059
+ }
73060
+ finally {
73061
+ this.loading.set(false);
73062
+ }
73063
+ }
73064
+ ngOnDestroy() {
73065
+ this.msgSub?.unsubscribe();
73066
+ this.typingSub?.unsubscribe();
73067
+ const conv = this.conv();
73068
+ const orgId = this.getOrgId();
73069
+ const appId = this.chatConfig?.appId ?? '';
73070
+ if (conv && orgId && appId && this.conversationSvc) {
73071
+ this.conversationSvc.stopTyping(conv.id, orgId, appId);
73072
+ }
73073
+ }
73074
+ onSend(body) {
73075
+ const conv = this.conv();
73076
+ if (!conv || !this.conversationSvc)
73077
+ return;
73078
+ this.conversationSvc.sendMessage(conv.id, body).subscribe();
73079
+ }
73080
+ onReactionClick(event) {
73081
+ // Reacciones se implementan a nivel de app consumer
73082
+ console.debug('[ThreadPanel] reactionClick', event);
73083
+ }
73084
+ onDeleteMessage(msgId) {
73085
+ const conv = this.conv();
73086
+ if (!conv || !this.conversationSvc)
73087
+ return;
73088
+ this.conversationSvc.deleteMessage(conv.id, msgId).subscribe();
73089
+ }
73090
+ onReplyTo(_msgId) {
73091
+ // Implementacion futura: seleccionar mensaje de respuesta
73092
+ }
73093
+ getOrgId() {
73094
+ return this.auth?.user()?.activeOrg ?? '';
73095
+ }
73096
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ThreadPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
73097
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ThreadPanelComponent, isStandalone: true, selector: "val-thread-panel", inputs: { entityType: "entityType", entityId: "entityId", currentUserId: "currentUserId", title: "title" }, ngImport: i0, template: `
73098
+ <div class="thread-panel">
73099
+ @if (title) {
73100
+ <div class="thread-header">
73101
+ <p class="thread-title">{{ title }}</p>
73102
+ </div>
73103
+ }
73104
+
73105
+ @if (loading()) {
73106
+ <div class="thread-loading">
73107
+ <ion-spinner name="dots"></ion-spinner>
73108
+ <span>{{ loadingText() }}</span>
73109
+ </div>
73110
+ } @else {
73111
+ <div class="thread-chat">
73112
+ <val-chat-window
73113
+ [convId]="conv()?.id ?? ''"
73114
+ [messages]="bubbleMessages()"
73115
+ [currentUserId]="currentUserId"
73116
+ [isOpen]="isConvOpen()"
73117
+ [typingUsers]="typingUsers()"
73118
+ [isLoading]="false"
73119
+ (sendMessage)="onSend($event)"
73120
+ (reactionClick)="onReactionClick($event)"
73121
+ (deleteMessage)="onDeleteMessage($event)"
73122
+ (replyTo)="onReplyTo($event)"
73123
+ />
73124
+ </div>
73125
+ }
73126
+ </div>
73127
+ `, isInline: true, styles: [".thread-panel{display:flex;flex-direction:column;height:100%;background:var(--ion-background-color, #f4f5f8)}.thread-header{padding:12px 16px;border-bottom:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff))}.thread-header .thread-title{font-size:1rem;font-weight:600;color:var(--ion-text-color, #000);margin:0}.thread-loading{display:flex;align-items:center;justify-content:center;flex:1;gap:10px;color:var(--ion-color-medium, #92949c);font-size:.875rem}.thread-chat{flex:1;overflow:hidden;display:flex;flex-direction:column}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: ChatWindowComponent, selector: "val-chat-window", inputs: ["convId", "messages", "participants", "currentUserId", "isOpen", "typingUsers", "isLoading"], outputs: ["sendMessage", "loadMore", "reactionClick", "deleteMessage", "replyTo"] }] }); }
73128
+ }
73129
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ThreadPanelComponent, decorators: [{
73130
+ type: Component,
73131
+ args: [{ selector: 'val-thread-panel', standalone: true, imports: [CommonModule, IonSpinner, ChatWindowComponent], template: `
73132
+ <div class="thread-panel">
73133
+ @if (title) {
73134
+ <div class="thread-header">
73135
+ <p class="thread-title">{{ title }}</p>
73136
+ </div>
73137
+ }
73138
+
73139
+ @if (loading()) {
73140
+ <div class="thread-loading">
73141
+ <ion-spinner name="dots"></ion-spinner>
73142
+ <span>{{ loadingText() }}</span>
73143
+ </div>
73144
+ } @else {
73145
+ <div class="thread-chat">
73146
+ <val-chat-window
73147
+ [convId]="conv()?.id ?? ''"
73148
+ [messages]="bubbleMessages()"
73149
+ [currentUserId]="currentUserId"
73150
+ [isOpen]="isConvOpen()"
73151
+ [typingUsers]="typingUsers()"
73152
+ [isLoading]="false"
73153
+ (sendMessage)="onSend($event)"
73154
+ (reactionClick)="onReactionClick($event)"
73155
+ (deleteMessage)="onDeleteMessage($event)"
73156
+ (replyTo)="onReplyTo($event)"
73157
+ />
73158
+ </div>
73159
+ }
73160
+ </div>
73161
+ `, styles: [".thread-panel{display:flex;flex-direction:column;height:100%;background:var(--ion-background-color, #f4f5f8)}.thread-header{padding:12px 16px;border-bottom:1px solid var(--ion-border-color, rgba(0, 0, 0, .1));background:var(--ion-card-background, var(--ion-background-color, #fff))}.thread-header .thread-title{font-size:1rem;font-weight:600;color:var(--ion-text-color, #000);margin:0}.thread-loading{display:flex;align-items:center;justify-content:center;flex:1;gap:10px;color:var(--ion-color-medium, #92949c);font-size:.875rem}.thread-chat{flex:1;overflow:hidden;display:flex;flex-direction:column}\n"] }]
73162
+ }], ctorParameters: () => [], propDecorators: { entityType: [{
73163
+ type: Input
73164
+ }], entityId: [{
73165
+ type: Input
73166
+ }], currentUserId: [{
73167
+ type: Input
73168
+ }], title: [{
73169
+ type: Input
73170
+ }] } });
73171
+
71805
73172
  /*
71806
73173
  * Public API Surface of valtech-components
71807
73174
  */
@@ -71811,5 +73178,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
71811
73178
  * Generated bundle index. Do not edit.
71812
73179
  */
71813
73180
 
71814
- export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, APP_VERSION_PLATFORM_PLUGIN, APP_VERSION_REMOTE_PLUGIN, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AboutViewComponent, AccordionComponent, AccountViewComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, ApiKeyCreateModalComponent, ApiKeyService, ApiKeysModalComponent, ApiKeysViewComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentService, ContentTransformer, CookieBannerComponent, CookieSettingsComponent, CountdownComponent, CreateOrgModalComponent, CtaCardComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_BUTTON_PRESETS, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_LOGIN_LOGO, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_POST_UPDATE_GRACE_MS, DEFAULT_PRESETS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_SPLASH_SCREEN_CONFIG, DataTableComponent, DateInputComponent, DatePickerComponent, DateRangeInputComponent, DebugConsoleComponent, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EditOrgModalComponent, EmptyStateComponent, EntityCardComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FieldListComponent, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FontSizeOption, FontSizeSelectorComponent, FontSizeService, FooterComponent, FooterLinksComponent, FormComponent, FormFieldComponent, FormSkeletonComponent, FunHeaderComponent, FunModalComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HapticsService, HasPermissionDirective, HeaderComponent, HintComponent, HorizontalScrollComponent, HrefComponent, HtmlViewerModalComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, IONIC_COLORS$5 as IONIC_COLORS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, InviteMemberModalComponent, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGGED_IN_HINT_COOKIE, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LanguageSelectorV2Component, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadMoreComponent, LoadingDirective, LocalStorageService, LocaleService, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MemberDetailModalComponent, MemberImportModalComponent, MenuComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationPreferencesViewComponent, NotificationsService, NotificationsViewComponent, NumberFromToComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OptionCardsComponent, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PLATFORM_CONFIGS, POST_UPDATE_TS_KEY, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PdfService, PermissionCatalogService, PermissionSelectorComponent, PermissionsModalComponent, PermissionsViewComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PreferencesViewComponent, PresetService, PriceTagComponent, ProcessLinksPipe, ProfileSkeletonComponent, ProfileViewComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QrScannerComponent, QueryBuilder, QuoteBoxComponent, REQUEST_STATUSES, RadioInputComponent, RangeInputComponent, RatingComponent, ReactionBarComponent, ReactionsService, RefresherComponent, RequestFirestoreService, RequestFormBuilderService, RequestFormComponent, RequestModalComponent, RequestService, RightsFooterComponent, RotatingTextComponent, SEARCH_HEADER_DEFAULTS, SETTINGS_SECTIONS_CATALOG, SHAPE_KEYS, SHARE_PROFILE_MODAL_DEFAULTS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, STATS_BAR_DEFAULTS, STROKE_KEYS, SearchHeaderComponent, SearchSelectorComponent, SearchbarComponent, SectionHeaderComponent, SecurityViewComponent, SegmentControlComponent, SelectSearchComponent, SessionListModalComponent, SessionService, SettingsHubComponent, ShareButtonsComponent, ShareProfileModalComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SplashComponent, SplashScreenService, StatsBarComponent, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TicketCardComponent, TicketCardImageService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, TranslatePipe, TypedCollection, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_NETWORK_ERROR_KEY, VALTECH_REACTIONS_CONFIG, VALTECH_SETTINGS_MENU_LINKS, VALTECH_SITE_PATHS, VALTECH_SOCIAL_LINKS, VALTECH_SPLASH_SCREEN, VALTECH_WEB_BASE_URLS, VALTECH_WHATSAPP_CONFIG, VERSION, ValtechErrorService, VerifyViewComponent, WhatsappFabComponent, WhatsappService, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, authPasswordValidator, beautifyLegalArticle, blogPost, buildCompanyFooterProps, buildFooterLinks, buildLegalLinkResolver, buildPath, buildSettingsCards, button, canSubmitRequestType, collections, connectPageRefresh, createErrorStateProps, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createPageState, createPermissionLabeler, createRefreshableStream, createTitleProps, docs, errorLoggingInterceptor, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, groupPermissionsByScope, guestGuard, hasEmulators, iconButton, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isIonicColor, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideSplashScreen, provideValtechAboutRoutes, provideValtechAccountRoutes, provideValtechAds, provideValtechApiKeysRoutes, provideValtechAppConfig, provideValtechAppVersion, provideValtechAppVersionHttp, provideValtechAuth, provideValtechAuthInterceptor, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationPreferencesRoutes, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPermissionsRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechReactions, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, provideValtechWhatsapp, query, rbacGuard, renderPatternSvgInner, replaceSpecialChars, requestSubmitMode, resolveColor, resolveInputDefaultValue, resolveIonicColor, resolveWebBaseUrl, roleGuard, selectableRequestTypes, storagePaths, superAdminGuard, toArticle };
73181
+ export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, APP_VERSION_PLATFORM_PLUGIN, APP_VERSION_REMOTE_PLUGIN, ARTICLE_CARD_DEFAULTS, ARTICLE_SPACING, ARTICLE_STRIP_DEFAULTS, AUTH_CTA_DEFAULTS, AVATAR_UPLOAD_DEFAULTS, AboutViewComponent, AccordionComponent, AccountViewComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AnimatedTerminalComponent, ApiKeyCreateModalComponent, ApiKeyService, ApiKeysModalComponent, ApiKeysViewComponent, AppConfigService, AppVersionService, ArticleBuilder, ArticleCardComponent, ArticleComponent, ArticleStripComponent, AttachmentUploaderComponent, AuthBackgroundComponent, AuthCtaComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, CIRCLE_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CORNER_KEYS, CTA_CARD_DEFAULTS, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangeEmailModalComponent, ChangePasswordModalComponent, ChatInputComponent, ChatWindowComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentService, ContentTransformer, ConversationService, CookieBannerComponent, CookieSettingsComponent, CountdownComponent, CreateOrgModalComponent, CtaCardComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_BUTTON_PRESETS, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_LOGIN_LOGO, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_POST_UPDATE_GRACE_MS, DEFAULT_PRESETS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_SPLASH_SCREEN_CONFIG, DataTableComponent, DateInputComponent, DatePickerComponent, DateRangeInputComponent, DebugConsoleComponent, DeleteAccountModalComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EditOrgModalComponent, EmptyStateComponent, EntityCardComponent, EntityFeedService, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FUN_MODAL_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FieldListComponent, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FontSizeOption, FontSizeSelectorComponent, FontSizeService, FooterComponent, FooterLinksComponent, FormComponent, FormFieldComponent, FormSkeletonComponent, FunHeaderComponent, FunModalComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HapticsService, HasPermissionDirective, HeaderComponent, HintComponent, HorizontalScrollComponent, HrefComponent, HtmlViewerModalComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, INVITATION_CARD_DEFAULTS, IONIC_COLORS$5 as IONIC_COLORS, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, InvitationCardComponent, InviteMemberModalComponent, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGGED_IN_HINT_COOKIE, LOGIN_DEFAULTS, LandingSplitComponent, LandingStepsComponent, LanguageSelectorComponent, LanguageSelectorV2Component, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadMoreComponent, LoadingDirective, LocalStorageService, LocaleService, LoginAttemptModalComponent, LoginComponent, MEDIA_OBJECT_DEFAULTS, MEMBER_CARD_DEFAULTS, METRIC_CARD_DEFAULTS, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MediaObjectComponent, MemberCardComponent, MemberDetailModalComponent, MemberImportModalComponent, MenuComponent, MessageBubbleComponent, MessagingService, MetaService, MetricCardComponent, MfaModalComponent, ModalService, ModalShellComponent, MultiSelectSearchComponent, NUM_KEYS, NavigationService, NetworkBannerComponent, NetworkStatusService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationPreferencesViewComponent, NotificationsService, NotificationsViewComponent, NumberFromToComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OptionCardsComponent, OrgInfoSheetComponent, OrgService, OrgSwitchService, OrganizationViewComponent, PATTERN_MOTIFS, PATTERN_PALETTES, PATTERN_STYLE_CONFIGS, PLATFORM_CONFIGS, POST_UPDATE_TS_KEY, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWavesComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PdfService, PermissionCatalogService, PermissionSelectorComponent, PermissionsModalComponent, PermissionsViewComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PreferencesViewComponent, PresetService, PriceTagComponent, ProcessLinksPipe, ProfileSkeletonComponent, ProfileViewComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QrScannerComponent, QueryBuilder, QuoteBoxComponent, REQUEST_STATUSES, RadioInputComponent, RangeInputComponent, RatingComponent, ReactionBarComponent, ReactionsService, RefresherComponent, RequestFirestoreService, RequestFormBuilderService, RequestFormComponent, RequestModalComponent, RequestService, RightsFooterComponent, RotatingTextComponent, SEARCH_HEADER_DEFAULTS, SETTINGS_SECTIONS_CATALOG, SHAPE_KEYS, SHARE_PROFILE_MODAL_DEFAULTS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, STATS_BAR_DEFAULTS, STROKE_KEYS, SearchHeaderComponent, SearchSelectorComponent, SearchbarComponent, SectionHeaderComponent, SecurityViewComponent, SegmentControlComponent, SelectSearchComponent, SessionListModalComponent, SessionService, SettingsHubComponent, ShareButtonsComponent, ShareProfileModalComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SplashComponent, SplashScreenService, StatsBarComponent, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, SwitchOrgModalComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, ThreadPanelComponent, TicketCardComponent, TicketCardImageService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TransferOwnershipModalComponent, TranslatePipe, TypedCollection, TypingIndicatorComponent, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_CHAT_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_CONTENT_CONFIG, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_NETWORK_ERROR_KEY, VALTECH_REACTIONS_CONFIG, VALTECH_SETTINGS_MENU_LINKS, VALTECH_SITE_PATHS, VALTECH_SOCIAL_LINKS, VALTECH_SPLASH_SCREEN, VALTECH_WEB_BASE_URLS, VALTECH_WHATSAPP_CONFIG, VERSION, ValtechErrorService, VerifyViewComponent, WhatsappFabComponent, WhatsappService, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, authPasswordValidator, beautifyLegalArticle, blogPost, buildCompanyFooterProps, buildFooterLinks, buildLegalLinkResolver, buildPath, buildSettingsCards, button, canSubmitRequestType, collections, connectPageRefresh, createErrorStateProps, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createPageState, createPermissionLabeler, createRefreshableStream, createTitleProps, docs, errorLoggingInterceptor, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, groupPermissionsByScope, guestGuard, hasEmulators, iconButton, interpretError, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isIonicColor, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideSplashScreen, provideValtechAboutRoutes, provideValtechAccountRoutes, provideValtechAds, provideValtechApiKeysRoutes, provideValtechAppConfig, provideValtechAppVersion, provideValtechAppVersionHttp, provideValtechAuth, provideValtechAuthInterceptor, provideValtechChat, provideValtechContent, provideValtechDebugConsole, provideValtechDonations, provideValtechErrorHandling, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechNotificationPreferencesRoutes, provideValtechNotificationsRoutes, provideValtechOrganizationRoutes, provideValtechPermissionsRoutes, provideValtechPreferencesRoutes, provideValtechPresets, provideValtechProfileRoutes, provideValtechReactions, provideValtechSecurityRoutes, provideValtechSettingsRoutes, provideValtechSite, provideValtechSkeleton, provideValtechWhatsapp, query, rbacGuard, renderPatternSvgInner, replaceSpecialChars, requestSubmitMode, resolveColor, resolveInputDefaultValue, resolveIonicColor, resolveWebBaseUrl, roleGuard, selectableRequestTypes, storagePaths, superAdminGuard, toArticle };
71815
73182
  //# sourceMappingURL=valtech-components.mjs.map