@chat21/chat21-web-widget 5.1.33-rc11 → 5.1.33-rc9
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/CHANGELOG.md +0 -7
- package/package.json +1 -1
- package/playwright-report/index.html +90 -0
- package/src/app/component/conversation-detail/conversation/conversation.component.ts +1 -3
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +7 -0
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +5 -7
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +3 -4
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +18 -9
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +0 -6
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +5 -8
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +1 -5
- package/src/app/component/form/inputs/form-text/form-text.component.ts +3 -9
- package/src/app/component/message/bubble-message/bubble-message.component.scss +0 -5
- package/src/app/component/message/bubble-message/bubble-message.component.ts +0 -14
- package/src/app/component/message/json-sources/json-sources.component.scss +8 -12
- package/src/app/pipe/marked.pipe.ts +41 -51
- package/src/app/providers/global-settings.service.ts +0 -29
- package/src/app/providers/json-sources-parser.service.ts +32 -25
- package/src/app/providers/voice/voice-streaming.service.ts +19 -11
- package/src/app/providers/voice/voice-streaming.types.ts +1 -0
- package/src/app/providers/voice/voice.service.spec.ts +45 -12
- package/src/app/providers/voice/voice.service.ts +45 -215
- package/src/app/utils/globals.ts +0 -10
- package/src/assets/i18n/en.json +125 -106
- package/src/assets/i18n/es.json +0 -1
- package/src/assets/i18n/fr.json +0 -1
- package/src/assets/i18n/it.json +0 -1
- package/test-results/.last-run.json +4 -0
- package/src/assets/sounds/keyboard.mp3 +0 -0
|
@@ -264,9 +264,7 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
|
|
|
264
264
|
'CLOSE',
|
|
265
265
|
'VOICE_CONNECTING',
|
|
266
266
|
'VOICE_LISTENING',
|
|
267
|
-
'VOICE_PROCESSING'
|
|
268
|
-
'STREAM_AUDIO',
|
|
269
|
-
'MAX_ATTACHMENT'
|
|
267
|
+
'VOICE_PROCESSING'
|
|
270
268
|
];
|
|
271
269
|
|
|
272
270
|
const keysContent = [
|
|
@@ -264,4 +264,11 @@ describe('ConversationContentComponent', () => {
|
|
|
264
264
|
});
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
+
describe('ngAfterContentChecked', () => {
|
|
268
|
+
it('should trigger change detection', () => {
|
|
269
|
+
spyOn((component as any).cdref, 'detectChanges');
|
|
270
|
+
component.ngAfterContentChecked();
|
|
271
|
+
expect((component as any).cdref.detectChanges).toHaveBeenCalled();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
267
274
|
});
|
package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnInit,
|
|
2
|
-
import { Subscription } from 'rxjs';
|
|
1
|
+
import { ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core';
|
|
3
2
|
import { MAX_WIDTH_IMAGES, MSG_STATUS_RETURN_RECEIPT, MSG_STATUS_SENT, MSG_STATUS_SENT_SERVER } from 'src/app/utils/constants';
|
|
4
3
|
import { MessageModel } from 'src/chat21-core/models/message';
|
|
5
4
|
import { LoggerService } from 'src/chat21-core/providers/abstract/logger.service';
|
|
@@ -13,7 +12,7 @@ import { isCarousel, isEmojii, isFirstMessage, isFrame, isImage, isInfo, isLastM
|
|
|
13
12
|
templateUrl: './conversation-content.component.html',
|
|
14
13
|
styleUrls: ['./conversation-content.component.scss']
|
|
15
14
|
})
|
|
16
|
-
export class ConversationContentComponent implements OnInit
|
|
15
|
+
export class ConversationContentComponent implements OnInit {
|
|
17
16
|
@ViewChild('scrollMe') public scrollMe: ElementRef;
|
|
18
17
|
|
|
19
18
|
@Input() messages: MessageModel[]
|
|
@@ -74,7 +73,6 @@ export class ConversationContentComponent implements OnInit, OnDestroy {
|
|
|
74
73
|
showUploadProgress: boolean = false;
|
|
75
74
|
fileType: string;
|
|
76
75
|
private logger: LoggerService = LoggerInstance.getInstance();
|
|
77
|
-
private uploadSub?: Subscription;
|
|
78
76
|
|
|
79
77
|
constructor(private cdref: ChangeDetectorRef,
|
|
80
78
|
private elementRef: ElementRef,
|
|
@@ -84,8 +82,8 @@ export class ConversationContentComponent implements OnInit, OnDestroy {
|
|
|
84
82
|
this.listenToUploadFileProgress();
|
|
85
83
|
}
|
|
86
84
|
|
|
87
|
-
|
|
88
|
-
this.
|
|
85
|
+
ngAfterContentChecked() {
|
|
86
|
+
this.cdref.detectChanges();
|
|
89
87
|
}
|
|
90
88
|
|
|
91
89
|
ngOnChanges(changes: SimpleChanges){
|
|
@@ -123,7 +121,7 @@ export class ConversationContentComponent implements OnInit, OnDestroy {
|
|
|
123
121
|
|
|
124
122
|
// ENABLE HTML SECTION 'FILE PENDING UPLOAD'
|
|
125
123
|
listenToUploadFileProgress() {
|
|
126
|
-
this.
|
|
124
|
+
this.uploadService.BSStateUpload.subscribe((data: any) => {
|
|
127
125
|
this.logger.debug('[CONV-CONTENT] BSStateUpload', data);
|
|
128
126
|
// && data.type.startsWith("application")
|
|
129
127
|
if (data) {
|
package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html
CHANGED
|
@@ -59,9 +59,7 @@
|
|
|
59
59
|
<span class="v-align-center">
|
|
60
60
|
<svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" width="24px" height="24" viewBox="0 0 24 24" fill="currentColor">
|
|
61
61
|
<path d="M9.9,22.7c0,0-.1,0-.2,0-1.9.3-3.7-.2-5.2-1.4-3-2.3-3.6-6.4-1.4-9.5L9.5,2.5c.4-.5,1.1-.6,1.6-.3.5.4.6,1.1.3,1.6l-6.5,9.4c-1.4,2-1,4.8.9,6.3,1,.8,2.2,1.1,3.5.9,1.3-.2,2.4-.9,3.1-1.9l6-8.7c.9-1.2.6-3-.6-3.9-.6-.5-1.4-.6-2.1-.5-.8.1-1.4.5-1.9,1.1l-5.8,8.2c-.3.5-.2,1.1.2,1.5.2.2.5.3.8.2.3,0,.6-.2.7-.4l4.7-6.2c.4-.5,1.1-.6,1.6-.2.5.4.6,1.1.2,1.6l-4.7,6.2c-.5.7-1.4,1.2-2.3,1.3-.9.1-1.8-.2-2.5-.7-1.4-1.1-1.6-3.1-.6-4.6l5.8-8.2c.8-1.1,2-1.9,3.4-2.1,1.4-.2,2.7.1,3.8,1,2.2,1.7,2.7,4.8,1.1,7.1l-6,8.7c-1.1,1.5-2.6,2.5-4.4,2.8h0Z"/>
|
|
62
|
-
|
|
63
|
-
</svg>
|
|
64
|
-
|
|
62
|
+
</svg>
|
|
65
63
|
</span>
|
|
66
64
|
<input
|
|
67
65
|
[attr.disabled]="(isFilePendingToUpload || isConversationArchived || hideTextReply)? true : null"
|
|
@@ -175,7 +173,8 @@
|
|
|
175
173
|
class="chat21-textarea-button chat21-stream-button"
|
|
176
174
|
[class.active]="isStreamAudioActive || isStreamAudioConnecting || (!textInputTextArea && !hideTextReply)"
|
|
177
175
|
[class.chat21-stream-button--expanded]="isStreamAudioActive || isStreamAudioConnecting"
|
|
178
|
-
(click)="onStreamPressed($event)" [attr.aria-label]="(isStreamAudioActive || isStreamAudioConnecting) ? (translationMap?.get('CLOSE') || 'Chiudi stream') : (translationMap?.get('STREAM_AUDIO') || 'Stream audio')"
|
|
176
|
+
(click)="onStreamPressed($event)" [attr.aria-label]="(isStreamAudioActive || isStreamAudioConnecting) ? (translationMap?.get('CLOSE') || 'Chiudi stream') : (translationMap?.get('STREAM_AUDIO') || 'Stream audio')"
|
|
177
|
+
[ngStyle]="{ 'background-color': stylesMap?.get('iconColor') || stylesMap?.get('themeColor') }">
|
|
179
178
|
<chat-stream-audio-spectrum
|
|
180
179
|
mode="button"
|
|
181
180
|
[active]="isStreamAudioActive || isStreamAudioConnecting"
|
package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss
CHANGED
|
@@ -105,19 +105,28 @@
|
|
|
105
105
|
box-sizing: border-box;
|
|
106
106
|
flex-shrink: 0;
|
|
107
107
|
color: #ffffff;
|
|
108
|
-
transition:
|
|
108
|
+
transition:
|
|
109
|
+
min-width 220ms ease,
|
|
110
|
+
border-radius 180ms ease,
|
|
111
|
+
padding 180ms ease;
|
|
109
112
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
.chat21-stream-button__icon svg {
|
|
114
|
+
width: 20px;
|
|
115
|
+
height: 20px;
|
|
116
|
+
path {
|
|
117
|
+
fill: #ffffff;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
&.chat21-textarea-button span svg:hover {
|
|
122
|
+
background: rgba(255, 255, 255, 0.2) !important;
|
|
123
|
+
border-radius: 50%;
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
&.chat21-stream-button--expanded {
|
|
118
|
-
min-width:
|
|
127
|
+
min-width: 120px;
|
|
119
128
|
border-radius: 999px;
|
|
120
|
-
|
|
129
|
+
padding: 0 14px;
|
|
121
130
|
justify-content: center;
|
|
122
131
|
|
|
123
132
|
// stop the circle-hover background from looking odd on pill
|
|
@@ -418,7 +427,7 @@ textarea:active{
|
|
|
418
427
|
flex-direction: column;
|
|
419
428
|
justify-content: center;
|
|
420
429
|
position: absolute;
|
|
421
|
-
|
|
430
|
+
padding: 8px 16px;
|
|
422
431
|
overflow: hidden;
|
|
423
432
|
background-color: color-mix(in srgb, var(--content-background-color) 34%, transparent);
|
|
424
433
|
backdrop-filter: blur(20px) saturate(1.2);
|
package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts
CHANGED
|
@@ -124,12 +124,6 @@ export class ConversationFooterComponent implements OnInit, OnChanges, OnDestroy
|
|
|
124
124
|
return this.translationMap?.get('VOICE_LISTENING') || 'Listening...';
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
get maxAttachmentLabel(): string {
|
|
128
|
-
const template = this.translationMap?.get('MAX_ATTACHMENT')
|
|
129
|
-
|| `Max allowed size {{FILE_SIZE_LIMIT}}Mb`;
|
|
130
|
-
return template.replace(/\{\{FILE_SIZE_LIMIT\}\}/g, String(this.file_size_limit));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
127
|
file_size_limit = FILE_SIZE_LIMIT;
|
|
134
128
|
attachmentTooltip: string = '';
|
|
135
129
|
isErrorNetwork: boolean = false;
|
|
@@ -22,14 +22,11 @@
|
|
|
22
22
|
<!-- BUTTON: inactive icon / expanded pill content -->
|
|
23
23
|
<ng-container *ngSwitchCase="'button'">
|
|
24
24
|
<span class="stream-audio-button__icon" *ngIf="!active" aria-hidden="true">
|
|
25
|
-
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
<rect x="16" y="2.5" height="16px" fill-opacity="1" width="2px" rx="0.75" ry="0.75"></rect>
|
|
31
|
-
<rect x="20" y="7.5" height="6px" fill-opacity="1" width="2px" rx="0.75" ry="0.75"></rect>
|
|
32
|
-
<title id="altIconTitle">{{ translationMap.get('STREAM_AUDIO') }}</title>
|
|
25
|
+
<svg role="img" xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 30 30" fill="currentColor" preserveAspectRatio="xMidYMid meet">
|
|
26
|
+
<path class="s0" d="m5.21 7.41c-1.21 0-2.21 0.99-2.21 2.21v8.14c0 1.21 0.99 2.21 2.21 2.21 1.22 0 2.21-0.99 2.21-2.21v-8.14c0-1.21-0.99-2.21-2.21-2.21z"/>
|
|
27
|
+
<path class="s0" d="m11.64 3.01c-1.22 0-2.21 0.99-2.21 2.2v16.94c0 1.21 0.99 2.2 2.21 2.2 1.22 0 2.21-0.98 2.21-2.2v-16.94c0-1.21-0.99-2.21-2.21-2.21z"/>
|
|
28
|
+
<path class="s0" d="m15.86 9.25v8.88c0 1.21 0.99 2.21 2.21 2.21 1.22 0 2.21-0.99 2.21-2.21v-8.88c0-1.22-0.99-2.21-2.21-2.21-1.22 0-2.21 0.99-2.21 2.21z"/>
|
|
29
|
+
<path class="s0" d="m24.5 8.97c-1.22 0-2.21 0.99-2.21 2.21v5.02c0 1.22 0.99 2.21 2.21 2.21 1.22 0 2.21-0.99 2.21-2.21v-5.02c0-1.21-0.99-2.21-2.21-2.21z"/>
|
|
33
30
|
</svg>
|
|
34
31
|
</span>
|
|
35
32
|
|
|
@@ -39,9 +39,6 @@
|
|
|
39
39
|
width: 20px;
|
|
40
40
|
height: 20px;
|
|
41
41
|
display: block;
|
|
42
|
-
rect {
|
|
43
|
-
fill: var(--icon-fill-color);
|
|
44
|
-
}
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
.stream-audio-button__expanded {
|
|
@@ -51,7 +48,6 @@
|
|
|
51
48
|
gap: 12px;
|
|
52
49
|
width: 100%;
|
|
53
50
|
user-select: none;
|
|
54
|
-
color: var(--icon-fill-color);
|
|
55
51
|
}
|
|
56
52
|
|
|
57
53
|
.stream-audio-button__label {
|
|
@@ -78,6 +74,6 @@
|
|
|
78
74
|
width: 3px;
|
|
79
75
|
height: 100%;
|
|
80
76
|
border-radius: 2px;
|
|
81
|
-
background:
|
|
77
|
+
background: rgba(255, 255, 255, 0.92);
|
|
82
78
|
transform-origin: center;
|
|
83
79
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { Component, ElementRef, EventEmitter, Input, OnInit,
|
|
1
|
+
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, SimpleChange, ViewChild } from '@angular/core';
|
|
2
2
|
import { FormGroup, FormGroupDirective } from '@angular/forms';
|
|
3
|
-
import { Subscription } from 'rxjs';
|
|
4
3
|
import { FormArray } from '../../../../../chat21-core/models/formArray';
|
|
5
4
|
|
|
6
5
|
@Component({
|
|
@@ -8,7 +7,7 @@ import { FormArray } from '../../../../../chat21-core/models/formArray';
|
|
|
8
7
|
templateUrl: './form-text.component.html',
|
|
9
8
|
styleUrls: ['./form-text.component.scss']
|
|
10
9
|
})
|
|
11
|
-
export class FormTextComponent implements OnInit
|
|
10
|
+
export class FormTextComponent implements OnInit {
|
|
12
11
|
|
|
13
12
|
@Input() element: FormArray;
|
|
14
13
|
@Input() controlName: string;
|
|
@@ -20,7 +19,6 @@ export class FormTextComponent implements OnInit, OnDestroy {
|
|
|
20
19
|
@ViewChild('div_input') input: ElementRef;
|
|
21
20
|
form: FormGroup<any>;
|
|
22
21
|
inputType: string = 'text'
|
|
23
|
-
private valueChangesSub?: Subscription;
|
|
24
22
|
|
|
25
23
|
get fieldBaseId(): string {
|
|
26
24
|
const raw = this.element?.name || this.controlName || 'field';
|
|
@@ -53,17 +51,13 @@ export class FormTextComponent implements OnInit, OnDestroy {
|
|
|
53
51
|
ngOnInit() {
|
|
54
52
|
this.form = this.rootFormGroup.control as FormGroup<any>;
|
|
55
53
|
if(this.form && this.form.controls && this.form.controls[this.controlName]){
|
|
56
|
-
this.
|
|
54
|
+
this.form.controls[this.controlName].valueChanges.subscribe((value) => {
|
|
57
55
|
this.hasSubmitted= false;
|
|
58
56
|
this.setFormStyle();
|
|
59
57
|
})
|
|
60
58
|
}
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
ngOnDestroy() {
|
|
64
|
-
this.valueChangesSub?.unsubscribe();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
61
|
ngOnChanges(changes: SimpleChange){
|
|
68
62
|
if(this.controlName && (this.controlName.toLowerCase().includes('email') || this.controlName.toLowerCase().includes('e-mail')) ){
|
|
69
63
|
this.inputType = 'email';
|
|
@@ -40,18 +40,6 @@ export class BubbleMessageComponent implements OnInit, OnDestroy {
|
|
|
40
40
|
|
|
41
41
|
@HostBinding('class.no-background') get hostNoBackground() { return this.jsonSources !== null && this.jsonSources.length > 0; }
|
|
42
42
|
@HostBinding('class.json-resources') get hostIsJsonResources() { return this.jsonSources !== null && this.jsonSources.length > 0; }
|
|
43
|
-
@HostBinding('class.hidden-bubble') get hostHiddenBubble() { return !this.hasRenderableContent(); }
|
|
44
|
-
|
|
45
|
-
hasRenderableContent(): boolean {
|
|
46
|
-
const msg = this.message;
|
|
47
|
-
if (!msg) return false;
|
|
48
|
-
if (isImage(msg) || isFile(msg) || isFrame(msg) || isAudio(msg)) return true;
|
|
49
|
-
if (this.jsonSources && this.jsonSources.length > 0) return true;
|
|
50
|
-
// For url_preview messages, `text` may carry the raw JSON payload (not display text):
|
|
51
|
-
// if sources parsing yielded nothing, the bubble must stay hidden.
|
|
52
|
-
if (this.isUrlPreviewMessage) return false;
|
|
53
|
-
return !!(msg.text && String(msg.text).trim().length > 0);
|
|
54
|
-
}
|
|
55
43
|
|
|
56
44
|
readonly isImage = isImage;
|
|
57
45
|
readonly isFile = isFile;
|
|
@@ -67,7 +55,6 @@ export class BubbleMessageComponent implements OnInit, OnDestroy {
|
|
|
67
55
|
sizeImage: { width: number; height: number } = { width: 0, height: 0 };
|
|
68
56
|
fullnameColor: string = '';
|
|
69
57
|
jsonSources: JsonSourceItem[] | null = null;
|
|
70
|
-
isUrlPreviewMessage = false;
|
|
71
58
|
|
|
72
59
|
private urlPreviewReqId = 0;
|
|
73
60
|
|
|
@@ -152,7 +139,6 @@ export class BubbleMessageComponent implements OnInit, OnDestroy {
|
|
|
152
139
|
this.message?.type === TYPE_MSG_URL_PREVIEW
|
|
153
140
|
|| this.message?.metadata?.type === TYPE_MSG_URL_PREVIEW
|
|
154
141
|
|| this.message?.attributes?.type === TYPE_MSG_URL_PREVIEW;
|
|
155
|
-
this.isUrlPreviewMessage = !!urlPreviewLike;
|
|
156
142
|
if (urlPreviewLike) this.loadJsonSourcesFromUrlPreviewMessage();
|
|
157
143
|
}
|
|
158
144
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
:host {
|
|
2
|
-
--panel-bck: #
|
|
2
|
+
--panel-bck: #f7f8fa;
|
|
3
3
|
--row-sep: rgba(66, 133, 244, 0.18);
|
|
4
4
|
--text: rgba(0, 0, 0, 0.90);
|
|
5
5
|
--muted: rgba(0, 0, 0, 0.62);
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
.sources-header {
|
|
25
|
-
display:
|
|
25
|
+
display: flex;
|
|
26
26
|
align-items: center;
|
|
27
27
|
gap: 8px;
|
|
28
28
|
padding: 2px 4px 8px 4px;
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
.sources-panel {
|
|
57
57
|
display: flex;
|
|
58
58
|
flex-direction: column;
|
|
59
|
-
padding:
|
|
59
|
+
padding: 12px 12px 10px 12px;
|
|
60
60
|
background: var(--panel-bck);
|
|
61
61
|
border-radius: 18px;
|
|
62
62
|
font-size: 14px;
|
|
@@ -79,10 +79,6 @@
|
|
|
79
79
|
width: 100%;
|
|
80
80
|
box-sizing: border-box;
|
|
81
81
|
|
|
82
|
-
border-bottom: 1px solid var(--row-sep);
|
|
83
|
-
border-bottom-left-radius: 0;
|
|
84
|
-
border-bottom-right-radius: 0;
|
|
85
|
-
|
|
86
82
|
&:hover {
|
|
87
83
|
background: rgba(255, 255, 255, 0.55);
|
|
88
84
|
.source-row__title {
|
|
@@ -90,11 +86,11 @@
|
|
|
90
86
|
}
|
|
91
87
|
}
|
|
92
88
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
89
|
+
& + & {
|
|
90
|
+
border-top: 1px solid var(--row-sep);
|
|
91
|
+
border-top-left-radius: 0;
|
|
92
|
+
border-top-right-radius: 0;
|
|
93
|
+
}
|
|
98
94
|
|
|
99
95
|
.source-row__left {
|
|
100
96
|
min-width: 0;
|
|
@@ -5,71 +5,61 @@ import { marked, Tokens } from 'marked';
|
|
|
5
5
|
name: 'marked'
|
|
6
6
|
})
|
|
7
7
|
export class MarkedPipe implements PipeTransform {
|
|
8
|
-
private static renderer: any = null;
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
if (!MarkedPipe.renderer) {
|
|
12
|
-
const renderer = new marked.Renderer();
|
|
13
|
-
|
|
14
|
-
/* --------------------------------------------------
|
|
15
|
-
🔐 1. NON renderizzare HTML raw
|
|
16
|
-
-------------------------------------------------- */
|
|
17
|
-
renderer.html = function(token: Tokens.HTML | Tokens.Tag): string {
|
|
18
|
-
const html = 'text' in token ? token.text : '';
|
|
19
|
-
|
|
20
|
-
return html
|
|
21
|
-
.replace(/&/g, '&')
|
|
22
|
-
.replace(/</g, '<')
|
|
23
|
-
.replace(/>/g, '>');
|
|
24
|
-
};
|
|
9
|
+
transform(value: any): string {
|
|
25
10
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
11
|
+
const input =
|
|
12
|
+
typeof value === 'string'
|
|
13
|
+
? value
|
|
14
|
+
: (value === null || value === undefined) ? '' : String(value);
|
|
30
15
|
|
|
31
|
-
|
|
32
|
-
/^javascript:/i,
|
|
33
|
-
/^data:/i,
|
|
34
|
-
/^vbscript:/i
|
|
35
|
-
];
|
|
16
|
+
const inputWithNewlines = input.replace(/\\n/g, '\n');
|
|
36
17
|
|
|
37
|
-
|
|
18
|
+
const renderer = new marked.Renderer();
|
|
38
19
|
|
|
39
|
-
|
|
20
|
+
/* --------------------------------------------------
|
|
21
|
+
🔐 1. NON renderizzare HTML raw
|
|
22
|
+
-------------------------------------------------- */
|
|
23
|
+
renderer.html = function(token: Tokens.HTML | Tokens.Tag): string {
|
|
24
|
+
const html = 'text' in token ? token.text : '';
|
|
40
25
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
26
|
+
return html
|
|
27
|
+
.replace(/&/g, '&')
|
|
28
|
+
.replace(/</g, '<')
|
|
29
|
+
.replace(/>/g, '>');
|
|
30
|
+
};
|
|
44
31
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
32
|
+
/* --------------------------------------------------
|
|
33
|
+
🔐 2. Link sicuri
|
|
34
|
+
-------------------------------------------------- */
|
|
35
|
+
const originalLinkRenderer = renderer.link.bind(renderer);
|
|
48
36
|
|
|
49
|
-
|
|
37
|
+
const dangerousProtocols = [
|
|
38
|
+
/^javascript:/i,
|
|
39
|
+
/^data:/i,
|
|
40
|
+
/^vbscript:/i
|
|
41
|
+
];
|
|
50
42
|
|
|
51
|
-
|
|
52
|
-
return html.replace(
|
|
53
|
-
'<a ',
|
|
54
|
-
'<a target="_blank" rel="noopener noreferrer" '
|
|
55
|
-
);
|
|
56
|
-
};
|
|
43
|
+
renderer.link = function({ href, title, tokens }) {
|
|
57
44
|
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
return MarkedPipe.renderer;
|
|
61
|
-
}
|
|
45
|
+
const normalized = (href || '').trim();
|
|
62
46
|
|
|
63
|
-
|
|
47
|
+
const isDangerous = dangerousProtocols.some(pattern =>
|
|
48
|
+
pattern.test(normalized)
|
|
49
|
+
);
|
|
64
50
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
: (value === null || value === undefined) ? '' : String(value);
|
|
51
|
+
if (isDangerous) {
|
|
52
|
+
return tokens ? tokens.map(t => t.raw).join('') : href || '';
|
|
53
|
+
}
|
|
69
54
|
|
|
70
|
-
|
|
55
|
+
const html = originalLinkRenderer({ href, title, tokens });
|
|
71
56
|
|
|
72
|
-
|
|
57
|
+
// aggiunge sicurezza ai link
|
|
58
|
+
return html.replace(
|
|
59
|
+
'<a ',
|
|
60
|
+
'<a target="_blank" rel="noopener noreferrer" '
|
|
61
|
+
);
|
|
62
|
+
};
|
|
73
63
|
|
|
74
64
|
marked.setOptions({
|
|
75
65
|
renderer,
|
|
@@ -596,9 +596,6 @@ export class GlobalSettingsService {
|
|
|
596
596
|
if (variables.hasOwnProperty('allowedUploadExtentions')) {
|
|
597
597
|
globals['fileUploadAccept'] = variables['allowedUploadExtentions'];
|
|
598
598
|
}
|
|
599
|
-
if(variables.hasOwnProperty('showAudioStreamFooterButton')) {
|
|
600
|
-
globals['showAudioStreamFooterButton'] = variables['showAudioStreamFooterButton'];
|
|
601
|
-
}
|
|
602
599
|
|
|
603
600
|
}
|
|
604
601
|
}
|
|
@@ -950,14 +947,6 @@ export class GlobalSettingsService {
|
|
|
950
947
|
if (TEMP !== undefined) {
|
|
951
948
|
globals.soundEnabled = TEMP;
|
|
952
949
|
}
|
|
953
|
-
TEMP = tiledeskSettings['keyboardSoundVolume'];
|
|
954
|
-
if (TEMP !== undefined) {
|
|
955
|
-
globals.keyboardSoundVolume = +TEMP;
|
|
956
|
-
}
|
|
957
|
-
TEMP = tiledeskSettings['keyboardSoundFile'];
|
|
958
|
-
if (TEMP !== undefined) {
|
|
959
|
-
globals.keyboardSoundFile = TEMP;
|
|
960
|
-
}
|
|
961
950
|
TEMP = tiledeskSettings['openExternalLinkButton'];
|
|
962
951
|
// this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > openExternalLinkButton:: ', TEMP]);
|
|
963
952
|
if (TEMP !== undefined) {
|
|
@@ -1347,14 +1336,6 @@ export class GlobalSettingsService {
|
|
|
1347
1336
|
if (TEMP !== null) {
|
|
1348
1337
|
this.globals.soundEnabled = TEMP;
|
|
1349
1338
|
}
|
|
1350
|
-
TEMP = el.nativeElement.getAttribute('keyboardSoundVolume');
|
|
1351
|
-
if (TEMP !== null) {
|
|
1352
|
-
this.globals.keyboardSoundVolume = +TEMP;
|
|
1353
|
-
}
|
|
1354
|
-
TEMP = el.nativeElement.getAttribute('keyboardSoundFile');
|
|
1355
|
-
if (TEMP !== null) {
|
|
1356
|
-
this.globals.keyboardSoundFile = TEMP;
|
|
1357
|
-
}
|
|
1358
1339
|
TEMP = el.nativeElement.getAttribute('openExternalLinkButton');
|
|
1359
1340
|
if (TEMP !== null) {
|
|
1360
1341
|
this.globals.openExternalLinkButton = TEMP;
|
|
@@ -1754,16 +1735,6 @@ export class GlobalSettingsService {
|
|
|
1754
1735
|
globals.soundEnabled = stringToBoolean(TEMP);
|
|
1755
1736
|
}
|
|
1756
1737
|
|
|
1757
|
-
TEMP = getParameterByName(windowContext, 'tiledesk_keyboardSoundVolume');
|
|
1758
|
-
if (TEMP) {
|
|
1759
|
-
globals.keyboardSoundVolume = +TEMP;
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
TEMP = getParameterByName(windowContext, 'tiledesk_keyboardSoundFile');
|
|
1763
|
-
if (TEMP) {
|
|
1764
|
-
globals.keyboardSoundFile = TEMP;
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
1738
|
TEMP = getParameterByName(windowContext, 'tiledesk_openExternalLinkButton');
|
|
1768
1739
|
if (TEMP) {
|
|
1769
1740
|
globals.openExternalLinkButton = stringToBoolean(TEMP);
|
|
@@ -7,19 +7,25 @@ import { mergeJsonSourcesMissingFields } from 'src/app/utils/json-sources-utils'
|
|
|
7
7
|
|
|
8
8
|
export type UrlPreviewMessage = {
|
|
9
9
|
type?: string; // "url_preview"
|
|
10
|
+
activeMode?: 'form' | 'list' | 'text' | string;
|
|
11
|
+
form?: { sources?: any[] };
|
|
12
|
+
list?: string;
|
|
10
13
|
text?: string;
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Parse and enrich "url_preview" messages into `JsonSourceItem[]`.
|
|
15
18
|
*
|
|
19
|
+
* This service is intentionally isolated so it can be replaced/removed easily.
|
|
20
|
+
*
|
|
16
21
|
* Rules:
|
|
17
|
-
* -
|
|
18
|
-
* - `
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
22
|
+
* - It expects the full url_preview message object: `{ type: 'url_preview', activeMode: 'form'|'list'|'text', ... }`
|
|
23
|
+
* - `activeMode` selects the source field to use:
|
|
24
|
+
* - `form`: reads `msg.form.sources` (array of `{source_name, source_file_name, ...}`)
|
|
25
|
+
* - `list`: reads `msg.list` (free text) and extracts URLs (max 10)
|
|
26
|
+
* - `text`: reads `msg.text` (a JSON array string with the same schema as `form.sources`)
|
|
27
|
+
* - After building the initial array, it calls `url-preview` only for items that miss title or description,
|
|
28
|
+
* and merges the missing fields only (never overwriting existing values).
|
|
23
29
|
*/
|
|
24
30
|
@Injectable({ providedIn: 'root' })
|
|
25
31
|
export class JsonSourcesParserService {
|
|
@@ -92,11 +98,23 @@ export class JsonSourcesParserService {
|
|
|
92
98
|
private parseBaseJsonSources(msg?: UrlPreviewMessage | null): JsonSourceItem[] | null {
|
|
93
99
|
if (!msg || msg.type !== 'url_preview') return null;
|
|
94
100
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
const mode = (msg.activeMode || '').toString().trim();
|
|
102
|
+
const normalizedMode = mode === 'json_sources' ? 'form' : mode; // backward compat
|
|
103
|
+
|
|
104
|
+
if (normalizedMode === 'form') {
|
|
105
|
+
return this.mapSourcesArray(msg.form?.sources);
|
|
106
|
+
}
|
|
107
|
+
if (normalizedMode === 'list') {
|
|
108
|
+
return this.mapListToSources(msg.list);
|
|
109
|
+
}
|
|
110
|
+
if (normalizedMode === 'text') {
|
|
111
|
+
return this.mapTextToSources(msg.text);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// best-effort fallback order
|
|
115
|
+
return this.mapSourcesArray(msg.form?.sources)
|
|
116
|
+
|| this.mapTextToSources(msg.text)
|
|
117
|
+
|| this.mapListToSources(msg.list);
|
|
100
118
|
}
|
|
101
119
|
|
|
102
120
|
private mapListToSources(listValue?: string): JsonSourceItem[] | null {
|
|
@@ -104,16 +122,6 @@ export class JsonSourcesParserService {
|
|
|
104
122
|
return urls.length ? urls.map(u => ({ link: u, title: u })) : null;
|
|
105
123
|
}
|
|
106
124
|
|
|
107
|
-
private isJsonArrayOfObjects(text?: string): boolean {
|
|
108
|
-
if (!text) return false;
|
|
109
|
-
try {
|
|
110
|
-
const parsed = this.parseJsonLenient(text);
|
|
111
|
-
return Array.isArray(parsed) && parsed.some(it => it && typeof it === 'object' && !Array.isArray(it));
|
|
112
|
-
} catch {
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
125
|
private mapTextToSources(text?: string): JsonSourceItem[] | null {
|
|
118
126
|
if (!text) return null;
|
|
119
127
|
try {
|
|
@@ -129,10 +137,9 @@ export class JsonSourcesParserService {
|
|
|
129
137
|
if (!arr || arr.length === 0) return null;
|
|
130
138
|
const mapped = arr
|
|
131
139
|
.filter((s: any) => s && typeof s === 'object' && typeof s[JSON_SOURCE_FIELD_URL] === 'string')
|
|
132
|
-
.map((s: any): JsonSourceItem
|
|
140
|
+
.map((s: any): JsonSourceItem => {
|
|
133
141
|
const rawUrl = (s[JSON_SOURCE_FIELD_URL] || '').toString().trim();
|
|
134
|
-
const normalized = extractUrlsFromText(rawUrl, 1)[0];
|
|
135
|
-
if (!normalized) return null;
|
|
142
|
+
const normalized = extractUrlsFromText(rawUrl, 1)[0] || rawUrl;
|
|
136
143
|
return {
|
|
137
144
|
link: normalized,
|
|
138
145
|
title: (s[JSON_SOURCE_FIELD_TITLE] || rawUrl).toString(),
|
|
@@ -140,7 +147,7 @@ export class JsonSourcesParserService {
|
|
|
140
147
|
image: typeof s.source_image === 'string' ? s.source_image : undefined
|
|
141
148
|
};
|
|
142
149
|
})
|
|
143
|
-
.filter((x: JsonSourceItem
|
|
150
|
+
.filter((x: JsonSourceItem) => !!x.link);
|
|
144
151
|
return mapped.length ? mapped : null;
|
|
145
152
|
}
|
|
146
153
|
|