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.
- package/esm2022/lib/components/molecules/chat-input/chat-input.component.mjs +157 -0
- package/esm2022/lib/components/molecules/chat-input/types.mjs +2 -0
- package/esm2022/lib/components/molecules/message-bubble/message-bubble.component.mjs +244 -0
- package/esm2022/lib/components/molecules/message-bubble/types.mjs +2 -0
- package/esm2022/lib/components/molecules/typing-indicator/typing-indicator.component.mjs +76 -0
- package/esm2022/lib/components/organisms/chat-window/chat-window.component.mjs +246 -0
- package/esm2022/lib/components/organisms/chat-window/types.mjs +2 -0
- package/esm2022/lib/components/organisms/thread-panel/thread-panel.component.mjs +211 -0
- package/esm2022/lib/services/chat/config.mjs +35 -0
- package/esm2022/lib/services/chat/conversation.service.mjs +274 -0
- package/esm2022/lib/services/chat/types.mjs +10 -0
- package/esm2022/lib/services/entity-feed/entity-feed.service.mjs +103 -0
- package/esm2022/lib/services/entity-feed/index.mjs +2 -0
- package/esm2022/lib/services/entity-feed/types.mjs +2 -0
- package/esm2022/lib/services/i18n/default-content.mjs +65 -1
- package/esm2022/public-api.mjs +16 -1
- package/fesm2022/valtech-components.mjs +1369 -2
- package/fesm2022/valtech-components.mjs.map +1 -1
- package/lib/components/atoms/glow/glow.component.d.ts +1 -1
- package/lib/components/atoms/rights-footer/rights-footer.component.d.ts +2 -2
- package/lib/components/atoms/text/text.component.d.ts +1 -1
- package/lib/components/atoms/user-avatar/user-avatar.component.d.ts +2 -2
- package/lib/components/molecules/article-card/article-card.component.d.ts +1 -1
- package/lib/components/molecules/chat-input/chat-input.component.d.ts +38 -0
- package/lib/components/molecules/chat-input/types.d.ts +20 -0
- package/lib/components/molecules/content-reaction/content-reaction.component.d.ts +2 -2
- package/lib/components/molecules/cta-card/cta-card.component.d.ts +1 -1
- package/lib/components/molecules/features-list/features-list.component.d.ts +4 -4
- package/lib/components/molecules/load-more/load-more.component.d.ts +1 -1
- package/lib/components/molecules/media-object/media-object.component.d.ts +1 -1
- package/lib/components/molecules/message-bubble/message-bubble.component.d.ts +40 -0
- package/lib/components/molecules/message-bubble/types.d.ts +64 -0
- package/lib/components/molecules/metric-card/metric-card.component.d.ts +1 -1
- package/lib/components/molecules/typing-indicator/typing-indicator.component.d.ts +19 -0
- package/lib/components/organisms/account-view/account-view.component.d.ts +1 -1
- package/lib/components/organisms/article/article.component.d.ts +6 -6
- package/lib/components/organisms/auth-cta/auth-cta.component.d.ts +1 -1
- package/lib/components/organisms/bottom-nav/bottom-nav.component.d.ts +1 -1
- package/lib/components/organisms/chat-window/chat-window.component.d.ts +63 -0
- package/lib/components/organisms/chat-window/types.d.ts +36 -0
- package/lib/components/organisms/landing-steps/landing-steps.component.d.ts +1 -1
- package/lib/components/organisms/login/login.component.d.ts +1 -1
- package/lib/components/organisms/member-import-modal/member-import-modal.component.d.ts +1 -1
- package/lib/components/organisms/notification-preferences-view/notification-preferences-view.component.d.ts +1 -1
- package/lib/components/organisms/organization-view/organization-view.component.d.ts +1 -1
- package/lib/components/organisms/thread-panel/thread-panel.component.d.ts +55 -0
- package/lib/components/templates/page-wrapper/page-wrapper.component.d.ts +2 -2
- package/lib/services/chat/config.d.ts +37 -0
- package/lib/services/chat/conversation.service.d.ts +147 -0
- package/lib/services/chat/types.d.ts +145 -0
- package/lib/services/entity-feed/entity-feed.service.d.ts +71 -0
- package/lib/services/entity-feed/index.d.ts +2 -0
- package/lib/services/entity-feed/types.d.ts +26 -0
- package/package.json +1 -1
- 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
|