@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.
Files changed (29) hide show
  1. package/CHANGELOG.md +0 -7
  2. package/package.json +1 -1
  3. package/playwright-report/index.html +90 -0
  4. package/src/app/component/conversation-detail/conversation/conversation.component.ts +1 -3
  5. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +7 -0
  6. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +5 -7
  7. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +3 -4
  8. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +18 -9
  9. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +0 -6
  10. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +5 -8
  11. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +1 -5
  12. package/src/app/component/form/inputs/form-text/form-text.component.ts +3 -9
  13. package/src/app/component/message/bubble-message/bubble-message.component.scss +0 -5
  14. package/src/app/component/message/bubble-message/bubble-message.component.ts +0 -14
  15. package/src/app/component/message/json-sources/json-sources.component.scss +8 -12
  16. package/src/app/pipe/marked.pipe.ts +41 -51
  17. package/src/app/providers/global-settings.service.ts +0 -29
  18. package/src/app/providers/json-sources-parser.service.ts +32 -25
  19. package/src/app/providers/voice/voice-streaming.service.ts +19 -11
  20. package/src/app/providers/voice/voice-streaming.types.ts +1 -0
  21. package/src/app/providers/voice/voice.service.spec.ts +45 -12
  22. package/src/app/providers/voice/voice.service.ts +45 -215
  23. package/src/app/utils/globals.ts +0 -10
  24. package/src/assets/i18n/en.json +125 -106
  25. package/src/assets/i18n/es.json +0 -1
  26. package/src/assets/i18n/fr.json +0 -1
  27. package/src/assets/i18n/it.json +0 -1
  28. package/test-results/.last-run.json +4 -0
  29. 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
  });
@@ -1,5 +1,4 @@
1
- import { ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnInit, OnDestroy, Output, SimpleChanges, ViewChild } from '@angular/core';
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, OnDestroy {
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
- ngOnDestroy() {
88
- this.uploadSub?.unsubscribe();
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.uploadSub = this.uploadService.BSStateUpload.subscribe((data: any) => {
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) {
@@ -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
- <title id="altIconTitle">{{ maxAttachmentLabel }}</title>
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"
@@ -105,19 +105,28 @@
105
105
  box-sizing: border-box;
106
106
  flex-shrink: 0;
107
107
  color: #ffffff;
108
- transition: min-width 220ms ease, border-radius 180ms ease, padding 180ms ease;
108
+ transition:
109
+ min-width 220ms ease,
110
+ border-radius 180ms ease,
111
+ padding 180ms ease;
109
112
 
110
- &:hover{
111
- background: rgba(240, 240, 240, 0.4) !important;
112
- transition: all 0.45s ease-in-out 0s !important;
113
- -moz-transition: all 0.45s ease-in-out 0s !important;
114
- -webkit-transition: all 0.45s ease-in-out 0s !important;
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: 110px;
127
+ min-width: 120px;
119
128
  border-radius: 999px;
120
- // padding: 0 14px;
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
- // padding: 8px 16px;
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);
@@ -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" height="26" viewBox="0 0 23 23" aria-labelledby="altIconTitle">
26
- <rect x="0" y="7.5" height="6px" fill-opacity="1" width="2px" rx="0.75" ry="0.75"></rect>
27
- <rect x="4" y="5.5" height="10px" fill-opacity="1" width="2px" rx="0.75" ry="0.75"></rect>
28
- <rect x="8" y="2.5" height="16px" fill-opacity="1" width="2px" rx="0.75" ry="0.75"></rect>
29
- <rect x="12" y="5.5" height="10px" fill-opacity="1" width="2px" rx="0.75" ry="0.75"></rect>
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: var(--icon-fill-color);
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, OnDestroy, Output, SimpleChange, ViewChild } from '@angular/core';
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, OnDestroy {
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.valueChangesSub = this.form.controls[this.controlName].valueChanges.subscribe((value) => {
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';
@@ -1,10 +1,5 @@
1
1
  /* ====== SET MESSAGES ====== */
2
2
 
3
- :host(.hidden-bubble) {
4
- display: none !important;
5
- }
6
-
7
-
8
3
  .messages {
9
4
  border-radius: var(--border-radius-bubble-message);
10
5
  padding: 0;
@@ -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: #ffffff; // #f7f8fa;
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: none;
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: 18px 12px 10px 12px;
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
- // border-top: 1px solid var(--row-sep);
95
- // border-top-left-radius: 0;
96
- // border-top-right-radius: 0;
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
- private static getRenderer(): any {
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, '&amp;')
22
- .replace(/</g, '&lt;')
23
- .replace(/>/g, '&gt;');
24
- };
9
+ transform(value: any): string {
25
10
 
26
- /* --------------------------------------------------
27
- 🔐 2. Link sicuri
28
- -------------------------------------------------- */
29
- const originalLinkRenderer = renderer.link.bind(renderer);
11
+ const input =
12
+ typeof value === 'string'
13
+ ? value
14
+ : (value === null || value === undefined) ? '' : String(value);
30
15
 
31
- const dangerousProtocols = [
32
- /^javascript:/i,
33
- /^data:/i,
34
- /^vbscript:/i
35
- ];
16
+ const inputWithNewlines = input.replace(/\\n/g, '\n');
36
17
 
37
- renderer.link = function({ href, title, tokens }: any) {
18
+ const renderer = new marked.Renderer();
38
19
 
39
- const normalized = (href || '').trim();
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
- const isDangerous = dangerousProtocols.some(pattern =>
42
- pattern.test(normalized)
43
- );
26
+ return html
27
+ .replace(/&/g, '&amp;')
28
+ .replace(/</g, '&lt;')
29
+ .replace(/>/g, '&gt;');
30
+ };
44
31
 
45
- if (isDangerous) {
46
- return tokens ? tokens.map((t: any) => t.raw).join('') : href || '';
47
- }
32
+ /* --------------------------------------------------
33
+ 🔐 2. Link sicuri
34
+ -------------------------------------------------- */
35
+ const originalLinkRenderer = renderer.link.bind(renderer);
48
36
 
49
- const html = originalLinkRenderer({ href, title, tokens });
37
+ const dangerousProtocols = [
38
+ /^javascript:/i,
39
+ /^data:/i,
40
+ /^vbscript:/i
41
+ ];
50
42
 
51
- // aggiunge sicurezza ai link
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
- MarkedPipe.renderer = renderer;
59
- }
60
- return MarkedPipe.renderer;
61
- }
45
+ const normalized = (href || '').trim();
62
46
 
63
- transform(value: any): string {
47
+ const isDangerous = dangerousProtocols.some(pattern =>
48
+ pattern.test(normalized)
49
+ );
64
50
 
65
- const input =
66
- typeof value === 'string'
67
- ? value
68
- : (value === null || value === undefined) ? '' : String(value);
51
+ if (isDangerous) {
52
+ return tokens ? tokens.map(t => t.raw).join('') : href || '';
53
+ }
69
54
 
70
- const inputWithNewlines = input.replace(/\\n/g, '\n');
55
+ const html = originalLinkRenderer({ href, title, tokens });
71
56
 
72
- const renderer = MarkedPipe.getRenderer();
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
- * - The payload is always read from `msg.text`, regardless of `activeMode`.
18
- * - `msg.text` may be either:
19
- * - a JSON array of source objects (`{source_name, source_file_name, ...}`), or
20
- * - a plain string from which URLs are extracted (split by whitespace/punctuation).
21
- * - After building the initial array, `url-preview` is called only for items that miss
22
- * title or description, and missing fields are merged in (never overwriting).
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
- // Regardless of `activeMode`, the payload is always read from `msg.text`.
96
- // It can be either a JSON array of source objects, or a plain string with URLs.
97
- return this.isJsonArrayOfObjects(msg.text)
98
- ? this.mapTextToSources(msg.text)
99
- : this.mapListToSources(msg.text);
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 | null => {
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 | null): x is JsonSourceItem => !!x && !!x.link);
150
+ .filter((x: JsonSourceItem) => !!x.link);
144
151
  return mapped.length ? mapped : null;
145
152
  }
146
153