@chat21/chat21-web-widget 5.1.18 → 5.1.20-rc1
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/.github/workflows/docker-community-push-latest.yml +23 -13
- package/.github/workflows/docker-image-tag-community-tag-push.yml +22 -12
- package/CHANGELOG.md +64 -2
- package/Dockerfile +4 -5
- package/angular.json +2 -1
- package/deploy_amazon_beta.sh +17 -7
- package/deploy_amazon_prod.sh +4 -4
- package/package.json +1 -1
- package/src/app/app.component.html +8 -1
- package/src/app/app.component.scss +60 -4
- package/src/app/app.component.ts +63 -28
- package/src/app/component/conversation-detail/conversation/conversation.component.ts +84 -10
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +4 -0
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +27 -1
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +5 -5
- package/src/app/component/home-conversations/home-conversations.component.html +16 -6
- package/src/app/component/home-conversations/home-conversations.component.ts +29 -0
- package/src/app/component/launcher-button/launcher-button.component.html +1 -1
- package/src/app/component/launcher-button/launcher-button.component.ts +3 -2
- package/src/app/component/list-conversations/list-conversations.component.html +8 -3
- package/src/app/component/list-conversations/list-conversations.component.ts +29 -0
- package/src/app/component/message/html/html.component.html +5 -1
- package/src/app/component/message/html/html.component.scss +9 -0
- package/src/app/component/message/text/text.component.scss +4 -0
- package/src/app/pipe/marked.pipe.ts +15 -4
- package/src/app/providers/translator.service.ts +2 -0
- package/src/app/sass/normalize.scss +1 -0
- package/src/app/utils/globals.ts +1 -1
- package/src/assets/i18n/en.json +1 -0
- package/src/assets/i18n/es.json +1 -0
- package/src/assets/i18n/fr.json +1 -0
- package/src/assets/i18n/it.json +1 -0
- package/src/chat21-core/providers/abstract/upload.service.ts +1 -1
- package/src/chat21-core/providers/firebase/firebase-upload.service.ts +1 -1
- package/src/chat21-core/providers/native/native-image-repo.ts +1 -1
- package/src/chat21-core/providers/native/native-upload-service.ts +76 -54
- package/src/chat21-core/providers/tiledesk/tiledesk-requests.service.ts +1 -1
- package/src/chat21-core/utils/utils.ts +5 -2
- package/src/iframe-style.css +36 -12
- 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
|
-
|
|
479
|
-
|
|
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:
|
|
1078
|
-
|
|
1079
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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){
|
package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts
CHANGED
|
@@ -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);
|
package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss
CHANGED
|
@@ -192,9 +192,10 @@ textarea:active{
|
|
|
192
192
|
max-height: 110px;
|
|
193
193
|
min-height: auto;
|
|
194
194
|
height: 20px;
|
|
195
|
-
padding: 0px 12px;
|
|
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
|
+
}
|
package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts
CHANGED
|
@@ -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;
|
|
@@ -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 = '';
|
|
452
|
+
this.textInputTextArea = '';
|
|
454
453
|
if (textArea) {
|
|
455
|
-
textArea.value = '';
|
|
456
|
-
textArea.placeholder = this.translationMap.get('LABEL_PLACEHOLDER');
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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.
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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>
|
|
@@ -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,16 @@ 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
|
+
const safeInput = htmlEntities(input);
|
|
22
|
+
|
|
12
23
|
const renderer = new marked.Renderer();
|
|
13
24
|
renderer.link = function({ href, title, tokens }) {
|
|
14
25
|
// Normalizza l'href per evitare falsi negativi
|
|
@@ -74,15 +85,15 @@ export class MarkedPipe implements PipeTransform {
|
|
|
74
85
|
breaks: true
|
|
75
86
|
});
|
|
76
87
|
|
|
77
|
-
if (
|
|
88
|
+
if (safeInput && safeInput.length > 0) {
|
|
78
89
|
try {
|
|
79
|
-
return marked.parse(
|
|
90
|
+
return marked.parse(safeInput);
|
|
80
91
|
} catch (err) {
|
|
81
92
|
console.error('Errore nel parsing markdown:', err);
|
|
82
|
-
return
|
|
93
|
+
return safeInput;
|
|
83
94
|
}
|
|
84
95
|
}
|
|
85
|
-
return
|
|
96
|
+
return safeInput;
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
|
|
@@ -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
|
|
package/src/app/utils/globals.ts
CHANGED
|
@@ -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 =
|
|
250
|
+
this.autoStart = false;
|
|
251
251
|
/** start Authentication and startUI */
|
|
252
252
|
this.startHidden = false;
|
|
253
253
|
/** show/hide all widget -> js call: showAllWidget */
|
package/src/assets/i18n/en.json
CHANGED
|
@@ -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
|
}
|
package/src/assets/i18n/es.json
CHANGED
|
@@ -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
|
}
|
package/src/assets/i18n/fr.json
CHANGED
|
@@ -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
|
}
|
package/src/assets/i18n/it.json
CHANGED
|
@@ -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,7 +27,7 @@ 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
32
|
abstract uploadProfile(userId: string, upload: UploadModel): Promise<any>;
|
|
33
33
|
abstract delete(userId: string, path: string): Promise<any>;
|
|
@@ -31,7 +31,7 @@ export class FirebaseUploadService extends UploadService {
|
|
|
31
31
|
super();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
public async initialize() {
|
|
34
|
+
public async initialize(projectId: string) {
|
|
35
35
|
this.logger.debug('[FIREBASEUploadSERVICE] initialize');
|
|
36
36
|
|
|
37
37
|
const { default: firebase} = await import("firebase/app");
|
|
@@ -16,7 +16,7 @@ export class NativeImageRepoService extends ImageRepoService {
|
|
|
16
16
|
* @param uid
|
|
17
17
|
*/
|
|
18
18
|
getImagePhotoUrl(uid: string): string {
|
|
19
|
-
this.baseImageURL = this.getImageBaseUrl() + '
|
|
19
|
+
this.baseImageURL = this.getImageBaseUrl() + 'files'
|
|
20
20
|
let sender_id = '';
|
|
21
21
|
if (uid.includes('bot_')) {
|
|
22
22
|
sender_id = uid.slice(4)
|