@chat21/chat21-web-widget 5.1.18 → 5.1.20-rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.github/workflows/docker-community-push-latest.yml +23 -13
  2. package/.github/workflows/docker-image-tag-community-tag-push.yml +22 -12
  3. package/CHANGELOG.md +68 -2
  4. package/Dockerfile +4 -5
  5. package/angular.json +2 -1
  6. package/deploy_amazon_beta.sh +17 -7
  7. package/deploy_amazon_prod.sh +4 -4
  8. package/package.json +1 -1
  9. package/src/app/app.component.html +8 -1
  10. package/src/app/app.component.scss +60 -4
  11. package/src/app/app.component.ts +63 -28
  12. package/src/app/component/conversation-detail/conversation/conversation.component.ts +84 -10
  13. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +4 -0
  14. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +27 -1
  15. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +6 -6
  16. package/src/app/component/home-conversations/home-conversations.component.html +16 -6
  17. package/src/app/component/home-conversations/home-conversations.component.ts +29 -0
  18. package/src/app/component/launcher-button/launcher-button.component.html +1 -1
  19. package/src/app/component/launcher-button/launcher-button.component.ts +3 -2
  20. package/src/app/component/list-conversations/list-conversations.component.html +8 -3
  21. package/src/app/component/list-conversations/list-conversations.component.ts +29 -0
  22. package/src/app/component/message/html/html.component.html +5 -1
  23. package/src/app/component/message/html/html.component.scss +9 -0
  24. package/src/app/component/message/text/text.component.scss +4 -0
  25. package/src/app/pipe/marked.pipe.ts +31 -4
  26. package/src/app/providers/translator.service.ts +2 -0
  27. package/src/app/sass/normalize.scss +1 -0
  28. package/src/app/utils/globals.ts +1 -1
  29. package/src/assets/i18n/en.json +1 -0
  30. package/src/assets/i18n/es.json +1 -0
  31. package/src/assets/i18n/fr.json +1 -0
  32. package/src/assets/i18n/it.json +1 -0
  33. package/src/chat21-core/providers/abstract/upload.service.ts +5 -1
  34. package/src/chat21-core/providers/firebase/firebase-upload.service.ts +141 -12
  35. package/src/chat21-core/providers/native/native-image-repo.ts +1 -1
  36. package/src/chat21-core/providers/native/native-upload-service.ts +143 -46
  37. package/src/chat21-core/providers/tiledesk/tiledesk-requests.service.ts +1 -1
  38. package/src/chat21-core/utils/utils.ts +5 -2
  39. package/src/iframe-style.css +36 -12
  40. package/.vscode/settings.json +0 -3
@@ -298,6 +298,19 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
298
298
  if (this.afConversationComponent) {
299
299
  this.afConversationComponent.nativeElement.focus();
300
300
  }
301
+ // Sync initial "scroll to bottom" button/badge visibility.
302
+ // The state is normally driven by real scroll events, but on first render
303
+ // we might not get any scroll event -> stale UI.
304
+ setTimeout(() => {
305
+ try {
306
+ const isAtBottom = this.conversationContent?.checkContentScrollPosition();
307
+ if (typeof isAtBottom === 'boolean') {
308
+ this.onScrollContent(isAtBottom);
309
+ }
310
+ } catch (e) {
311
+ this.logger.error('[CONV-COMP] initial scroll state sync error:', e);
312
+ }
313
+ }, 0);
301
314
  this.isButtonsDisabled = false;
302
315
  }, 300);
303
316
  }
@@ -457,7 +470,7 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
457
470
  return this.isConversationArchived;
458
471
  }
459
472
 
460
- //FALLBACK TO TILEDESK
473
+ // //FALLBACK TO TILEDESK
461
474
  const requests_list = await this.tiledeskRequestService.getMyRequests().catch(err => {
462
475
  this.logger.error('[CONV-COMP] getConversationDetail: error getting request from Tiledesk', err);
463
476
  this.isConversationArchived=true
@@ -475,9 +488,9 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
475
488
  return this.isConversationArchived
476
489
  }
477
490
 
478
- this.isConversationArchived = true;
479
- return null;
480
- }
491
+ this.isConversationArchived = false;
492
+ return null;
493
+ }
481
494
 
482
495
  /**
483
496
  * this.g.recipientId:
@@ -827,6 +840,20 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
827
840
  this.subscriptions.push(subscribe);
828
841
  }
829
842
 
843
+ subscribtionKey = 'conversationsAdded';
844
+ subscribtion = this.subscriptions.find(item => item.key === subscribtionKey);
845
+ if(!subscribtion){
846
+
847
+ subscribtion = this.chatManager.conversationsHandlerService.conversationChanged.pipe(takeUntil(this.unsubscribe$)).subscribe((conversation) => {
848
+ this.logger.debug('[CONV-COMP] ***** DATAIL conversationsChanged *****', conversation, this.conversationWith, this.isConversationArchived);
849
+ if(conversation && conversation.recipient === this.conversationId){
850
+ this.isConversationArchived = false
851
+ }
852
+ });
853
+ const subscribe = {key: subscribtionKey, value: subscribtion };
854
+ this.subscriptions.push(subscribe);
855
+ }
856
+
830
857
  subscribtionKey = 'messageWait';
831
858
  subscribtion = this.subscriptions.find(item => item.key === subscribtionKey);
832
859
  if (!subscribtion) {
@@ -1074,29 +1101,76 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
1074
1101
  // this.hideTextAreaContent = true
1075
1102
  }
1076
1103
  /** CALLED BY: conv-header component */
1077
- onWidgetSizeChange(mode: 'min' | 'max' | 'top'){
1078
- var tiledeskDiv = this.g.windowContext.window.document.getElementById('tiledeskdiv');
1079
- this.g.size = mode
1104
+ onWidgetSizeChange(mode: any){
1105
+ const normalize = (val: any): 'min' | 'max' | 'top' => {
1106
+ const v = (typeof val === 'string') ? val.toLowerCase().trim() : '';
1107
+ return (v === 'min' || v === 'max' || v === 'top') ? (v as any) : 'min';
1108
+ };
1109
+
1110
+ const normalizedMode = normalize(mode);
1111
+ const tiledeskDiv = this.g.windowContext?.window?.document?.getElementById('tiledeskdiv');
1112
+ if(!tiledeskDiv){
1113
+ this.g.size = normalizedMode;
1114
+ this.isMenuShow = false;
1115
+ return;
1116
+ }
1117
+
1118
+ this.g.size = normalizedMode;
1080
1119
  const parent = tiledeskDiv.parentElement as HTMLElement | null;
1081
- if(mode==='max'){
1120
+ if(normalizedMode==='max'){
1121
+ this.restoreInlinePositionStylesForPopup(tiledeskDiv);
1082
1122
  tiledeskDiv.classList.add('max-size')
1083
1123
  tiledeskDiv.classList.remove('min-size')
1084
1124
  tiledeskDiv.classList.remove('top-size')
1085
1125
  if(parent) parent.classList.remove('overlay--popup');
1086
- } else if(mode==='min'){
1126
+ } else if(normalizedMode==='min'){
1127
+ this.restoreInlinePositionStylesForPopup(tiledeskDiv);
1087
1128
  tiledeskDiv.classList.add('min-size')
1088
1129
  tiledeskDiv.classList.remove('max-size')
1089
1130
  tiledeskDiv.classList.remove('top-size')
1090
1131
  if(parent) parent.classList.remove('overlay--popup');
1091
- } else if(mode=== 'top'){
1132
+ } else if(normalizedMode=== 'top'){
1133
+ // Remove inline positioning so CSS can control centering without needing `!important`.
1134
+ // this.clearInlinePositionStylesForPopup(tiledeskDiv);
1092
1135
  tiledeskDiv.classList.add('top-size')
1093
1136
  tiledeskDiv.classList.remove('max-size')
1094
1137
  tiledeskDiv.classList.remove('min-size')
1095
1138
  if(parent) parent.classList.add('overlay--popup');
1096
1139
  }
1140
+
1141
+ // Persist user-driven size changes so, when `size` is not specified via URL/settings,
1142
+ // GlobalSettingsService can restore it from storage (it already loads `size` from storage).
1143
+ try{
1144
+ this.appStorageService.setItem('size', normalizedMode);
1145
+ }catch(e){
1146
+ this.logger.warn('[CONV-COMP] onWidgetSizeChange > cannot persist size', e);
1147
+ }
1148
+
1097
1149
  this.isMenuShow = false;
1098
1150
  }
1099
1151
 
1152
+ // private clearInlinePositionStylesForPopup(tiledeskDiv: HTMLElement) {
1153
+ // tiledeskDiv.style.removeProperty('left');
1154
+ // tiledeskDiv.style.removeProperty('right');
1155
+ // tiledeskDiv.style.removeProperty('top');
1156
+ // tiledeskDiv.style.removeProperty('bottom');
1157
+ // }
1158
+
1159
+ private restoreInlinePositionStylesForPopup(tiledeskDiv: HTMLElement) {
1160
+ const marginX = this.g.isMobile ? this.g.mobileMarginX : this.g.marginX;
1161
+ const marginY = this.g.isMobile ? this.g.mobileMarginY : this.g.marginY;
1162
+
1163
+ if (this.g.align === 'left') {
1164
+ tiledeskDiv.style.left = marginX;
1165
+ tiledeskDiv.style.removeProperty('right');
1166
+ } else {
1167
+ tiledeskDiv.style.right = marginX;
1168
+ tiledeskDiv.style.removeProperty('left');
1169
+ }
1170
+ tiledeskDiv.style.bottom = marginY;
1171
+ tiledeskDiv.style.removeProperty('top');
1172
+ }
1173
+
1100
1174
 
1101
1175
  /** CALLED BY: conv-header component */
1102
1176
  onSignOutFN(event){
@@ -187,6 +187,10 @@ export class ConversationContentComponent implements OnInit {
187
187
  objDiv.parentElement.scrollTop = objDiv.scrollHeight;
188
188
  objDiv.style.opacity = '1';
189
189
  that.firstScroll = false;
190
+ // Keep parent state in sync even when scroll is programmatic.
191
+ // Without this, the "scroll to bottom" button/badge can remain visible
192
+ // because (scroll) event might not fire reliably for programmatic scrollTop.
193
+ that.onScrollContent.emit(true);
190
194
  }, 0);
191
195
  } catch (err) {
192
196
  this.logger.error('[CONV-CONTENT] scrollToBottom > Error :' + err);
@@ -192,9 +192,10 @@ textarea:active{
192
192
  max-height: 110px;
193
193
  min-height: auto;
194
194
  height: 20px;
195
- padding: 0px 12px; //0px 40px 0px 70px; //NEW FOR EMOJII BUTTON
195
+ padding: 0px 12px;
196
196
  margin: 10px 0px 10px;
197
197
  border: none;
198
+ display: inline-block;
198
199
 
199
200
  &::-webkit-scrollbar {
200
201
  width: 6px;
@@ -418,3 +419,28 @@ textarea:active{
418
419
  border: none;
419
420
  // margin: -2px -2px 0px;
420
421
  }
422
+
423
+
424
+ // aggiungi un'animazione di fade in e fade out quando .star-rating-widget è visibile con transition
425
+ .star-rating-widget {
426
+ transition: all 0.5s ease-in-out;
427
+ }
428
+
429
+ .star-rating-widget {
430
+ position: absolute;
431
+ left: 0;
432
+ right: 0;
433
+ bottom: -52px;
434
+ height: 100%;
435
+ width: 100%;
436
+ flex-direction: row;
437
+ justify-content: center;
438
+ background-color: rgb(255, 255, 255);
439
+ flex-wrap: nowrap;
440
+ &.active {
441
+ bottom: 0px;
442
+ }
443
+ &.inactive {
444
+ bottom: -52px;
445
+ }
446
+ }
@@ -87,6 +87,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
87
87
 
88
88
  file_size_limit = FILE_SIZE_LIMIT;
89
89
  attachmentTooltip: string = '';
90
+ isErrorNetwork: boolean = false;
90
91
 
91
92
 
92
93
  convertColorToRGBA = convertColorToRGBA;
@@ -316,7 +317,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
316
317
  // });
317
318
  // this.resetLoadImage();
318
319
 
319
- this.uploadService.upload(this.senderId, currentUpload).then(data => {
320
+ this.uploadService.uploadFile(this.senderId, currentUpload).then(data => {
320
321
  that.logger.log('[CONV-FOOTER] AppComponent::uploadSingle:: downloadURL', data);
321
322
  that.logger.log(`[CONV-FOOTER] Successfully uploaded file and got download link - ${data}`);
322
323
 
@@ -447,13 +448,11 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
447
448
  }
448
449
 
449
450
  private restoreTextArea() {
450
- // that.logger.log('[CONV-FOOTER] AppComponent:restoreTextArea::restoreTextArea');
451
- this.resizeInputField();
452
451
  const textArea = (<HTMLInputElement>document.getElementById('chat21-main-message-context'));
453
- this.textInputTextArea = ''; // clear the textarea
452
+ this.textInputTextArea = '';
454
453
  if (textArea) {
455
- textArea.value = ''; // clear the textarea
456
- textArea.placeholder = this.translationMap.get('LABEL_PLACEHOLDER'); // restore the placholder
454
+ textArea.value = '';
455
+ textArea.placeholder = this.translationMap.get('LABEL_PLACEHOLDER');
457
456
  if(textArea.style.height > this.HEIGHT_DEFAULT){
458
457
  document.getElementById('chat21-button-send').style.removeProperty('right')
459
458
  }
@@ -461,6 +460,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges {
461
460
  }
462
461
  this.setFocusOnId('chat21-main-message-context');
463
462
  this.isStopRec= false;
463
+ this.resizeInputField();
464
464
  }
465
465
 
466
466
  /**
@@ -38,9 +38,14 @@
38
38
  <!--CASE: no conversations EXIST - >1 agents is available -->
39
39
  <div *ngIf="(!listConversations || listConversations.length == 0) && availableAgents && availableAgents.length > 1 && g.showAvailableAgents === true" style="display: flex; margin: 20px 30px;">
40
40
  <div *ngFor="let agent of availableAgents" class="c21-pallozzo">
41
- <div class="c21-ball" [ngStyle] = "{ 'background-color':setColorFromString(agent.firstname) }" >
42
- <span class="c21-ball-label">{{avatarPlaceholder(agent.firstname)}}</span>
43
- <div *ngIf="agent.imageurl" #avatarImage class="c21-avatar-image" [style.background-image]="'url(' + agent.imageurl + ')'"></div>
41
+ <div class="c21-ball" [ngStyle] = "{ 'background-color': isImageLoaded(agent) ? 'transparent' : setColorFromString(agent.firstname) }" >
42
+ <span *ngIf="!isImageLoaded(agent)" class="c21-ball-label">{{avatarPlaceholder(agent.firstname)}}</span>
43
+ <img *ngIf="agent.imageurl"
44
+ [src]="agent.imageurl"
45
+ style="display: none;"
46
+ (load)="onImageLoad(agent)"
47
+ (error)="onImageError(agent)">
48
+ <div *ngIf="isImageLoaded(agent)" #avatarImage class="c21-avatar-image" [style.background-image]="'url(' + agent.imageurl + ')'"></div>
44
49
  </div>
45
50
  </div>
46
51
  </div>
@@ -48,9 +53,14 @@
48
53
  <!--CASE: no conversations EXIST - 1 agents is available -->
49
54
  <div class="flex-container" *ngIf="(!listConversations || listConversations.length == 0) && availableAgents && availableAgents.length === 1 && g.showAvailableAgents === true">
50
55
  <div *ngFor="let agent of availableAgents" class="c21-pallozzo flex-inline-agent ">
51
- <div class="c21-ball" [ngStyle] = "{ 'background-color':setColorFromString(agent.firstname) }" >
52
- <span class="c21-ball-label">{{avatarPlaceholder(agent.firstname)}}</span>
53
- <div *ngIf="agent.imageurl" #avatarImage class="c21-avatar-image" [style.background-image]="'url(' + agent.imageurl + ')'"></div>
56
+ <div class="c21-ball" [ngStyle] = "{ 'background-color': isImageLoaded(agent) ? 'transparent' : setColorFromString(agent.firstname) }" >
57
+ <span *ngIf="!isImageLoaded(agent)" class="c21-ball-label">{{avatarPlaceholder(agent.firstname)}}</span>
58
+ <img *ngIf="agent.imageurl"
59
+ [src]="agent.imageurl"
60
+ style="display: none;"
61
+ (load)="onImageLoad(agent)"
62
+ (error)="onImageError(agent)">
63
+ <div *ngIf="isImageLoaded(agent)" #avatarImage class="c21-avatar-image" [style.background-image]="'url(' + agent.imageurl + ')'"></div>
54
64
  </div>
55
65
  </div>
56
66
  <button tabindex="1040" aflistconv #aflistconv class="c21-button-primary" (click)="openNewConversation()" [ngStyle]="{'background-color': g.themeColor, 'border-color': g.themeColor, 'color': g.themeForegroundColor }">
@@ -65,6 +65,7 @@ export class HomeConversationsComponent implements OnInit, OnDestroy {
65
65
  themeForegroundColor = '';
66
66
  LABEL_START_NW_CONV: string;
67
67
  availableAgents: Array<UserAgent> = [];
68
+ imageLoadedMap: Map<string, boolean> = new Map<string, boolean>();
68
69
  // ========= end:: variabili del componente ======== //
69
70
 
70
71
  waitingTime: number;
@@ -203,6 +204,34 @@ export class HomeConversationsComponent implements OnInit, OnDestroy {
203
204
  this.onConversationLoaded.emit(conversation)
204
205
  }
205
206
 
207
+ /**
208
+ * Verifica se l'immagine dell'agente esiste e si carica correttamente
209
+ */
210
+ isImageLoaded(agent: UserAgent): boolean {
211
+ if (!agent?.imageurl) {
212
+ return false;
213
+ }
214
+ return this.imageLoadedMap.get(agent.id) === true;
215
+ }
216
+
217
+ /**
218
+ * Gestisce il caricamento riuscito dell'immagine
219
+ */
220
+ onImageLoad(agent: UserAgent) {
221
+ if (agent?.id && agent?.imageurl) {
222
+ this.imageLoadedMap.set(agent.id, true);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Gestisce l'errore di caricamento dell'immagine
228
+ */
229
+ onImageError(agent: UserAgent) {
230
+ if (agent?.id) {
231
+ this.imageLoadedMap.set(agent.id, false);
232
+ }
233
+ }
234
+
206
235
  private openConversationByID(conversation) {
207
236
  this.logger.debug('[HOMECONVERSATIONS] openConversationByID: ', conversation);
208
237
  if ( conversation ) {
@@ -1,7 +1,7 @@
1
1
  <!-- tabindex="000"-->
2
2
 
3
3
  <button aflauncherbutton #aflauncherbutton id="c21-launcher-button" class="c21-button-clean scale-in-center"
4
- *ngIf="g.isLogged == true && g.isOpen == false"
4
+ *ngIf="g.isOpen == false"
5
5
  [ngClass]="{'c21-align-left' : g.align === 'left', 'c21-align-right' : g.align !== 'left'}"
6
6
  [ngStyle]="{ 'background-color': g.baloonImage? null: g.themeColor, 'bottom': g.marginY+'px!important', 'left':(g.align==='left')?g.marginX+'px!important':'', 'right':(g.align==='right')?g.marginX+'px!important':'', 'width': g.launcherWidth, 'height': g.launcherHeight, 'border-radius': g.baloonShape}"
7
7
  (click)="openCloseWidget()"
@@ -67,8 +67,9 @@ export class LauncherButtonComponent implements OnInit, AfterViewInit {
67
67
  // this.g.isOpen = !this.g.isOpen;
68
68
  // this.g.setIsOpen(!this.g.isOpen);
69
69
  // this.appStorageService.setItem('isOpen', this.g.isOpen);
70
- this.onButtonClicked.emit( this.g.isOpen );
71
- }
70
+
71
+ }
72
+ this.onButtonClicked.emit( this.g.isOpen );
72
73
  }
73
74
 
74
75
  }
@@ -3,9 +3,14 @@
3
3
  <button tabindex="1103" class="c21-item-conversation" (click)="openConversationByID(conversation)">
4
4
  <div class="c21-body-conv">
5
5
  <div class="c21-left-conv">
6
- <div class="c21-ball" [ngStyle]="{'background': 'linear-gradient(rgb(255,255,255) -125%, ' + conversation.color + ')'}">
7
- <span class="c21-ball-label">{{conversation?.avatar}}</span>
8
- <div *ngIf="conversation?.image" #avatarImage class="c21-avatar-image" [style.background-image]="'url(' + conversation.image + ')'"></div>
6
+ <div class="c21-ball" [ngStyle]="{'background': isImageLoaded(conversation) ? 'transparent' : 'linear-gradient(rgb(255,255,255) -125%, ' + conversation.color + ')'}">
7
+ <span *ngIf="!isImageLoaded(conversation)" class="c21-ball-label">{{conversation?.avatar}}</span>
8
+ <img *ngIf="conversation?.image"
9
+ [src]="conversation.image"
10
+ style="display: none;"
11
+ (load)="onImageLoad(conversation)"
12
+ (error)="onImageError(conversation)">
13
+ <div *ngIf="isImageLoaded(conversation)" #avatarImage class="c21-avatar-image" [style.background-image]="'url(' + conversation.image + ')'"></div>
9
14
  </div>
10
15
  </div>
11
16
  <div class="c21-right-conv">
@@ -35,6 +35,7 @@ export class ListConversationsComponent implements OnInit {
35
35
  arrayDiffer: any;
36
36
 
37
37
  uidConvSelected: string;
38
+ imageLoadedMap: Map<string, boolean> = new Map<string, boolean>();
38
39
  constructor(private iterableDiffers: IterableDiffers) {
39
40
  this.iterableDifferListConv = this.iterableDiffers.find([]).create(null);
40
41
 
@@ -67,5 +68,33 @@ export class ListConversationsComponent implements OnInit {
67
68
 
68
69
  }
69
70
 
71
+ /**
72
+ * Verifica se l'immagine esiste e si carica correttamente
73
+ */
74
+ isImageLoaded(conversation: ConversationModel): boolean {
75
+ if (!conversation?.image) {
76
+ return false;
77
+ }
78
+ return this.imageLoadedMap.get(conversation.uid) === true;
79
+ }
80
+
81
+ /**
82
+ * Gestisce il caricamento riuscito dell'immagine
83
+ */
84
+ onImageLoad(conversation: ConversationModel) {
85
+ if (conversation?.uid && conversation?.image) {
86
+ this.imageLoadedMap.set(conversation.uid, true);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Gestisce l'errore di caricamento dell'immagine
92
+ */
93
+ onImageError(conversation: ConversationModel) {
94
+ if (conversation?.uid) {
95
+ this.imageLoadedMap.set(conversation.uid, false);
96
+ }
97
+ }
98
+
70
99
 
71
100
  }
@@ -1 +1,5 @@
1
- <div id="htmlCode" #htmlCode [innerHTML]="htmlText | safeHtml"></div>
1
+ <!--
2
+ Security: render HTML messages as plain text (no DOM interpretation).
3
+ This preserves the exact input (e.g. "<h1>...</h1>") including line breaks.
4
+ -->
5
+ <pre id="htmlCode" #htmlCode [textContent]="htmlText"></pre>
@@ -74,4 +74,13 @@
74
74
  overflow: hidden;
75
75
  }
76
76
 
77
+ }
78
+
79
+ /* Render raw HTML source safely */
80
+ #htmlCode {
81
+ white-space: pre-wrap; /* preserve newlines, allow wrapping */
82
+ word-break: break-word;
83
+ margin: 0;
84
+ font-size: 1.4em;
85
+ line-height: 1.4em;
77
86
  }
@@ -65,6 +65,10 @@ p h1, p h2, p h3, p h4, p h5, p h6 {
65
65
  color: inherit; // Eredita il colore dal parent
66
66
  }
67
67
 
68
+ .message_innerhtml.marked h1 {
69
+ line-height: normal;
70
+ }
71
+
68
72
  p ul {
69
73
  margin-block-end: 0em;
70
74
  margin-block-start: 0em;
@@ -1,6 +1,7 @@
1
1
  import { Pipe, PipeTransform } from '@angular/core';
2
2
  import { marked } from 'marked';
3
3
  import { BLOCKED_DOMAINS } from '../utils/utils';
4
+ import { htmlEntities } from 'src/chat21-core/utils/utils';
4
5
 
5
6
 
6
7
  @Pipe({
@@ -9,6 +10,32 @@ import { BLOCKED_DOMAINS } from '../utils/utils';
9
10
 
10
11
  export class MarkedPipe implements PipeTransform {
11
12
  transform(value: any): any {
13
+ // Security hardening:
14
+ // - Do not allow raw HTML from chat messages to be interpreted as DOM.
15
+ // - Keep Markdown working (marked will generate the needed HTML tags).
16
+ // This makes inputs like "<h1>Title</h1>" render exactly as typed.
17
+ const input =
18
+ typeof value === 'string'
19
+ ? value
20
+ : (value === null || value === undefined) ? '' : String(value);
21
+
22
+ // Converti i \n letterali in newline reali prima di htmlEntities
23
+ // così il markdown con breaks: true li renderizzerà correttamente
24
+ const inputWithNewlines = input.replace(/\\n/g, '\n');
25
+
26
+ // Proteggi i > usati per i blockquote markdown (all'inizio di riga)
27
+ // sostituendoli temporaneamente con un placeholder
28
+ const BLOCKQUOTE_PLACEHOLDER = '___MARKDOWN_BLOCKQUOTE___';
29
+ const protectedInput = inputWithNewlines.replace(/^(\s*)>/gm, (match, spaces) => {
30
+ return spaces + BLOCKQUOTE_PLACEHOLDER;
31
+ });
32
+
33
+ // Applica htmlEntities (che codificherà tutti gli altri >)
34
+ let safeInput = htmlEntities(protectedInput);
35
+
36
+ // Ripristina i > dei blockquote
37
+ safeInput = safeInput.replace(new RegExp(BLOCKQUOTE_PLACEHOLDER, 'g'), '>');
38
+
12
39
  const renderer = new marked.Renderer();
13
40
  renderer.link = function({ href, title, tokens }) {
14
41
  // Normalizza l'href per evitare falsi negativi
@@ -74,15 +101,15 @@ export class MarkedPipe implements PipeTransform {
74
101
  breaks: true
75
102
  });
76
103
 
77
- if (value && value.length > 0) {
104
+ if (safeInput && safeInput.length > 0) {
78
105
  try {
79
- return marked.parse(value);
106
+ return marked.parse(safeInput);
80
107
  } catch (err) {
81
108
  console.error('Errore nel parsing markdown:', err);
82
- return value;
109
+ return safeInput;
83
110
  }
84
111
  }
85
- return value;
112
+ return safeInput;
86
113
  }
87
114
 
88
115
 
@@ -302,6 +302,7 @@ export class TranslatorService {
302
302
  'CLOSED',
303
303
  'LABEL_PREVIEW',
304
304
  'MAX_ATTACHMENT',
305
+ 'MAX_ATTACHMENT_ERROR',
305
306
  'EMOJI'
306
307
  ];
307
308
 
@@ -358,6 +359,7 @@ export class TranslatorService {
358
359
  globals.LABEL_PREVIEW = res['LABEL_PREVIEW']
359
360
  globals.LABEL_ERROR_FIELD_REQUIRED= res['LABEL_ERROR_FIELD_REQUIRED']
360
361
  globals.MAX_ATTACHMENT = res['MAX_ATTACHMENT']
362
+ globals.MAX_ATTACHMENT_ERROR = res['MAX_ATTACHMENT_ERROR']
361
363
  globals.EMOJI = res['EMOJI']
362
364
 
363
365
 
@@ -40,6 +40,7 @@
40
40
  h1 {
41
41
  font-size: 2em;
42
42
  margin: 0.67em 0;
43
+ line-height: normal;
43
44
  }
44
45
 
45
46
  /* Grouping content
@@ -247,7 +247,7 @@ export class Globals {
247
247
 
248
248
  // ============ BEGIN: SET EXTERNAL PARAMETERS ==============//
249
249
  this.baseLocation = 'https://widget.tiledesk.com/v2';
250
- this.autoStart = true;
250
+ this.autoStart = false;
251
251
  /** start Authentication and startUI */
252
252
  this.startHidden = false;
253
253
  /** show/hide all widget -> js call: showAllWidget */
@@ -96,5 +96,6 @@
96
96
  "EMOJI_NOT_ELLOWED":"Emoji not allowed",
97
97
  "DOMAIN_NOT_ALLOWED":"URL contains a non-allowed domain",
98
98
  "MAX_ATTACHMENT": "Max allowed size {{FILE_SIZE_LIMIT}}Mb",
99
+ "MAX_ATTACHMENT_ERROR": "The file exceeds the maximum allowed size",
99
100
  "EMOJI": "Emoji"
100
101
  }
@@ -96,5 +96,6 @@
96
96
  "EMOJI_NOT_ELLOWED":"Emoji no permitido",
97
97
  "DOMAIN_NOT_ALLOWED":"La URL contiene un dominio no permitido",
98
98
  "MAX_ATTACHMENT": "Tamaño máximo permitido {{FILE_SIZE_LIMIT}}Mb",
99
+ "MAX_ATTACHMENT_ERROR": "El archivo supera el tamaño máximo permitido",
99
100
  "EMOJI": "Emoji"
100
101
  }
@@ -96,5 +96,6 @@
96
96
  "EMOJI_NOT_ELLOWED":"Emoji non autorisé",
97
97
  "DOMAIN_NOT_ALLOWED":"L'URL contient un domaine non autorisé",
98
98
  "MAX_ATTACHMENT": "Taille maximale autorisée {{FILE_SIZE_LIMIT}}Mo",
99
+ "MAX_ATTACHMENT_ERROR": "Le fichier dépasse la taille maximale autorisée",
99
100
  "EMOJI": "Emoji"
100
101
  }
@@ -94,5 +94,6 @@
94
94
  "EMOJI_NOT_ELLOWED":"Emoji non consentiti",
95
95
  "DOMAIN_NOT_ALLOWED":"L'URL contiene un dominio non consentito",
96
96
  "MAX_ATTACHMENT": "Dimensione massima consentita {{FILE_SIZE_LIMIT}}Mb",
97
+ "MAX_ATTACHMENT_ERROR": "Il file supera la dimensione massima consentita",
97
98
  "EMOJI": "Emoji"
98
99
  }
@@ -27,9 +27,13 @@ export abstract class UploadService {
27
27
  abstract BSStateUpload: BehaviorSubject<any>;
28
28
 
29
29
  // functions
30
- abstract initialize(): void;
30
+ abstract initialize(projectId?: string): void;
31
31
  abstract upload(userId: string, upload: UploadModel): Promise<{downloadURL: string, src: string}>;
32
+ abstract uploadFile(userId: string, upload: UploadModel): Promise<{downloadURL: string, src: string}>;
33
+ abstract uploadAsset(userId: string, upload: UploadModel, expiration?: number): Promise<{downloadURL: string, src: string}>;
32
34
  abstract uploadProfile(userId: string, upload: UploadModel): Promise<any>;
33
35
  abstract delete(userId: string, path: string): Promise<any>;
36
+ abstract deleteFile(userId: string, path: string): Promise<any>;
37
+ abstract deleteAsset(userId: string, path: string): Promise<any>
34
38
  abstract deleteProfile(userId: string, path: string): Promise<any>
35
39
  }