valtech-components 4.0.219 → 4.0.221
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/lib/version.mjs +2 -2
- package/esm2022/public-api.mjs +16 -1
- package/fesm2022/valtech-components.mjs +1370 -3
- package/fesm2022/valtech-components.mjs.map +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 +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/cta-card/cta-card.component.d.ts +1 -1
- package/lib/components/molecules/features-list/features-list.component.d.ts +1 -1
- package/lib/components/molecules/load-more/load-more.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/article/article.component.d.ts +7 -7
- package/lib/components/organisms/auth-cta/auth-cta.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/thread-panel/thread-panel.component.d.ts +55 -0
- 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/lib/version.d.ts +1 -1
- package/package.json +2 -2
- 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';
|
|
@@ -56,7 +56,7 @@ import { BrowserMultiFormatReader } from '@zxing/browser';
|
|
|
56
56
|
* Current version of valtech-components.
|
|
57
57
|
* This is automatically updated during the publish process.
|
|
58
58
|
*/
|
|
59
|
-
const VERSION = '4.0.
|
|
59
|
+
const VERSION = '4.0.221';
|
|
60
60
|
|
|
61
61
|
// Control de estado de refresco (singleton a nivel de módulo)
|
|
62
62
|
let isRefreshing = false;
|
|
@@ -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-conversations`, {
|
|
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
|