@chat21/chat21-web-widget 5.1.34 → 5.2.1

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 (191) hide show
  1. package/.angular-mcp-cache/package.json +1 -0
  2. package/.cursor/angular18-accessibility-auditor-skill.md +442 -0
  3. package/.cursor/mcp.json +15 -0
  4. package/.github/workflows/playwright.yml +27 -0
  5. package/CHANGELOG.md +25 -0
  6. package/Dockerfile +4 -5
  7. package/README.md +1 -1
  8. package/angular.json +21 -3
  9. package/docs/ACCESSIBILITY-STATEMENT.md +388 -0
  10. package/docs/TILEDESK_WIDGET_ACCESSIBILITY_ALIGNMENT.md +60 -0
  11. package/docs/TILEDESK_WIDGET_ACCESSIBILITY_STATEMENT_COMPLETE.md +386 -0
  12. package/env.sample +3 -2
  13. package/mocks/voice-websocket-mock/server.cjs +245 -0
  14. package/package.json +10 -3
  15. package/playwright.config.ts +41 -0
  16. package/src/app/app.component.html +2 -2
  17. package/src/app/app.component.scss +25 -14
  18. package/src/app/app.component.spec.ts +21 -6
  19. package/src/app/app.module.ts +13 -0
  20. package/src/app/component/conversation-detail/conversation/conversation.component.html +25 -11
  21. package/src/app/component/conversation-detail/conversation/conversation.component.scss +38 -0
  22. package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +644 -75
  23. package/src/app/component/conversation-detail/conversation/conversation.component.ts +70 -2
  24. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +25 -13
  25. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +123 -5
  26. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +1 -0
  27. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +23 -10
  28. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +18 -0
  29. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +241 -149
  30. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +8 -5
  31. package/src/app/component/conversation-detail/conversation-emojii/conversation-emojii.component.spec.ts +53 -3
  32. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +203 -110
  33. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +212 -1
  34. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +458 -78
  35. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +288 -76
  36. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +113 -53
  37. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.scss +12 -4
  38. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.spec.ts +274 -29
  39. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.html +23 -9
  40. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.spec.ts +80 -8
  41. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.html +29 -23
  42. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.spec.ts +185 -16
  43. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.ts +34 -14
  44. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +46 -0
  45. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +83 -0
  46. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +192 -0
  47. package/src/app/component/error-alert/error-alert.component.spec.ts +65 -5
  48. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.html +16 -7
  49. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.scss +21 -0
  50. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.spec.ts +89 -7
  51. package/src/app/component/form/form-builder/form-builder.component.html +1 -1
  52. package/src/app/component/form/form-builder/form-builder.component.spec.ts +163 -21
  53. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.html +8 -4
  54. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.scss +10 -5
  55. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.spec.ts +90 -16
  56. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.ts +26 -0
  57. package/src/app/component/form/inputs/form-label/form-label.component.spec.ts +45 -11
  58. package/src/app/component/form/inputs/form-radio-button/form-radio-button.component.spec.ts +24 -6
  59. package/src/app/component/form/inputs/form-select/form-select.component.spec.ts +14 -5
  60. package/src/app/component/form/inputs/form-text/form-text.component.html +14 -12
  61. package/src/app/component/form/inputs/form-text/form-text.component.scss +11 -1
  62. package/src/app/component/form/inputs/form-text/form-text.component.spec.ts +113 -17
  63. package/src/app/component/form/inputs/form-text/form-text.component.ts +35 -3
  64. package/src/app/component/form/inputs/form-textarea/form-textarea.component.html +13 -11
  65. package/src/app/component/form/inputs/form-textarea/form-textarea.component.scss +6 -5
  66. package/src/app/component/form/inputs/form-textarea/form-textarea.component.spec.ts +149 -13
  67. package/src/app/component/form/inputs/form-textarea/form-textarea.component.ts +26 -0
  68. package/src/app/component/form/prechat-form/prechat-form.component.html +14 -11
  69. package/src/app/component/form/prechat-form/prechat-form.component.spec.ts +102 -10
  70. package/src/app/component/form/prechat-form/prechat-form.component.ts +8 -1
  71. package/src/app/component/form/prechat-form-test-mock.ts +35 -0
  72. package/src/app/component/home/home.component.html +38 -31
  73. package/src/app/component/home/home.component.scss +4 -2
  74. package/src/app/component/home/home.component.spec.ts +226 -11
  75. package/src/app/component/home-conversations/home-conversations.component.html +30 -26
  76. package/src/app/component/home-conversations/home-conversations.component.scss +3 -0
  77. package/src/app/component/home-conversations/home-conversations.component.spec.ts +212 -36
  78. package/src/app/component/last-message/last-message.component.html +15 -9
  79. package/src/app/component/last-message/last-message.component.scss +16 -2
  80. package/src/app/component/last-message/last-message.component.spec.ts +204 -23
  81. package/src/app/component/launcher-button/launcher-button.component.html +8 -13
  82. package/src/app/component/launcher-button/launcher-button.component.spec.ts +104 -8
  83. package/src/app/component/list-all-conversations/list-all-conversations.component.html +12 -17
  84. package/src/app/component/list-all-conversations/list-all-conversations.component.scss +2 -0
  85. package/src/app/component/list-conversations/list-conversations.component.html +22 -22
  86. package/src/app/component/menu-options/menu-options.component.html +30 -20
  87. package/src/app/component/menu-options/menu-options.component.spec.ts +125 -9
  88. package/src/app/component/message/audio/audio.component.html +13 -15
  89. package/src/app/component/message/audio/audio.component.spec.ts +140 -5
  90. package/src/app/component/message/audio/audio.component.ts +1 -5
  91. package/src/app/component/message/audio-sync/audio-sync.component.html +18 -0
  92. package/src/app/component/message/audio-sync/audio-sync.component.scss +65 -0
  93. package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +112 -0
  94. package/src/app/component/message/audio-sync/audio-sync.component.ts +714 -0
  95. package/src/app/component/message/avatar/avatar.component.html +2 -2
  96. package/src/app/component/message/avatar/avatar.component.spec.ts +99 -7
  97. package/src/app/component/message/bubble-message/bubble-message.component.html +41 -51
  98. package/src/app/component/message/bubble-message/bubble-message.component.scss +54 -1
  99. package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +147 -57
  100. package/src/app/component/message/bubble-message/bubble-message.component.ts +95 -13
  101. package/src/app/component/message/buttons/action-button/action-button.component.html +3 -4
  102. package/src/app/component/message/buttons/action-button/action-button.component.spec.ts +49 -5
  103. package/src/app/component/message/buttons/link-button/link-button.component.scss +5 -8
  104. package/src/app/component/message/buttons/link-button/link-button.component.spec.ts +50 -5
  105. package/src/app/component/message/buttons/text-button/text-button.component.spec.ts +44 -5
  106. package/src/app/component/message/carousel/carousel.component.html +29 -16
  107. package/src/app/component/message/carousel/carousel.component.scss +20 -8
  108. package/src/app/component/message/carousel/carousel.component.spec.ts +80 -3
  109. package/src/app/component/message/carousel/carousel.component.ts +16 -0
  110. package/src/app/component/message/frame/frame.component.html +9 -4
  111. package/src/app/component/message/frame/frame.component.spec.ts +34 -15
  112. package/src/app/component/message/frame/frame.component.ts +7 -2
  113. package/src/app/component/message/html/html.component.html +1 -1
  114. package/src/app/component/message/html/html.component.scss +1 -1
  115. package/src/app/component/message/html/html.component.spec.ts +24 -7
  116. package/src/app/component/message/image/image.component.html +12 -10
  117. package/src/app/component/message/image/image.component.scss +16 -0
  118. package/src/app/component/message/image/image.component.spec.ts +101 -15
  119. package/src/app/component/message/image/image.component.ts +90 -51
  120. package/src/app/component/message/info-message/info-message.component.spec.ts +26 -14
  121. package/src/app/component/message/json-sources/json-sources.component.html +6 -5
  122. package/src/app/component/message/json-sources/json-sources.component.scss +26 -18
  123. package/src/app/component/message/json-sources/json-sources.component.ts +41 -0
  124. package/src/app/component/message/like-unlike/like-unlike.component.html +7 -9
  125. package/src/app/component/message/like-unlike/like-unlike.component.spec.ts +31 -3
  126. package/src/app/component/message/return-receipt/return-receipt.component.spec.ts +38 -17
  127. package/src/app/component/message/text/text.component.html +3 -3
  128. package/src/app/component/message/text/text.component.scss +80 -86
  129. package/src/app/component/message/text/text.component.spec.ts +106 -13
  130. package/src/app/component/message-attachment/message-attachment.component.spec.ts +134 -13
  131. package/src/app/component/selection-department/selection-department.component.html +21 -23
  132. package/src/app/component/selection-department/selection-department.component.spec.ts +159 -14
  133. package/src/app/component/selection-department/selection-department.component.ts +8 -1
  134. package/src/app/component/send-button/send-button.component.html +5 -13
  135. package/src/app/component/send-button/send-button.component.spec.ts +2 -2
  136. package/src/app/component/star-rating-widget/star-rating-widget.component.html +51 -81
  137. package/src/app/directives/tooltip.directive.spec.ts +8 -4
  138. package/src/app/modals/confirm-close/confirm-close.component.html +20 -8
  139. package/src/app/modals/confirm-close/confirm-close.component.scss +3 -0
  140. package/src/app/modals/confirm-close/confirm-close.component.spec.ts +13 -4
  141. package/src/app/modals/confirm-close/confirm-close.component.ts +8 -1
  142. package/src/app/pipe/html-entites-encode.pipe.spec.ts +35 -2
  143. package/src/app/pipe/marked.pipe.spec.ts +38 -2
  144. package/src/app/pipe/marked.pipe.ts +51 -41
  145. package/src/app/providers/app-config.service.ts +4 -2
  146. package/src/app/providers/brand.service.spec.ts +23 -2
  147. package/src/app/providers/brand.service.ts +1 -1
  148. package/src/app/providers/global-settings.service.spec.ts +1009 -14
  149. package/src/app/providers/global-settings.service.ts +40 -2
  150. package/src/app/providers/json-sources-parser.service.ts +13 -1
  151. package/src/app/providers/translator.service.ts +24 -7
  152. package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +116 -0
  153. package/src/app/providers/tts-audio-playback-coordinator.service.ts +122 -0
  154. package/src/app/providers/voice/STT&TTS/openai-voice.config.ts +12 -0
  155. package/src/app/providers/voice/STT&TTS/openai-voice.provider.ts +156 -0
  156. package/src/app/providers/voice/STT&TTS/speech-provider.abstract.ts +39 -0
  157. package/src/app/providers/voice/audio.types.ts +40 -0
  158. package/src/app/providers/voice/vad.service.spec.ts +28 -0
  159. package/src/app/providers/voice/vad.service.ts +70 -0
  160. package/src/app/providers/voice/voice-streaming.service.spec.ts +23 -0
  161. package/src/app/providers/voice/voice-streaming.service.ts +702 -0
  162. package/src/app/providers/voice/voice-streaming.types.ts +112 -0
  163. package/src/app/providers/voice/voice.service.spec.ts +227 -0
  164. package/src/app/providers/voice/voice.service.ts +969 -0
  165. package/src/app/sass/_variables.scss +2 -0
  166. package/src/app/sass/animations.scss +19 -1
  167. package/src/app/shims/onnxruntime-web-wasm.ts +4 -0
  168. package/src/app/utils/globals.ts +14 -0
  169. package/src/app/utils/utils-resources.ts +1 -1
  170. package/src/assets/i18n/en.json +128 -100
  171. package/src/assets/i18n/es.json +128 -100
  172. package/src/assets/i18n/fr.json +128 -100
  173. package/src/assets/i18n/it.json +128 -98
  174. package/src/assets/onnx/ort-wasm-simd-threaded.mjs +59 -0
  175. package/src/assets/onnx/ort-wasm-simd-threaded.wasm +0 -0
  176. package/src/assets/sounds/keyboard.mp3 +0 -0
  177. package/src/assets/vad/silero_vad_legacy.onnx +0 -0
  178. package/src/assets/vad/vad.worklet.bundle.min.js +1 -0
  179. package/src/chat21-core/models/message.ts +2 -1
  180. package/src/chat21-core/providers/chat-manager.spec.ts +72 -0
  181. package/src/chat21-core/providers/firebase/firebase-conversation-handler.ts +3 -2
  182. package/src/chat21-core/providers/firebase/firebase-init-service.ts +5 -5
  183. package/src/chat21-core/providers/mqtt/mqtt-conversation-handler.ts +12 -0
  184. package/src/chat21-core/providers/scripts/script.service.spec.ts +12 -2
  185. package/src/chat21-core/utils/utils-message.ts +7 -0
  186. package/src/widget-config-template.json +3 -1
  187. package/src/widget-config.json +28 -27
  188. package/tests/widget-form-rich.spec.ts +67 -0
  189. package/tests/widget-index-dev-settings.spec.ts +52 -0
  190. package/tests/widget-twp-iframe.spec.ts +39 -0
  191. package/tsconfig.json +5 -0
@@ -161,6 +161,11 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
161
161
  membersConversation = ['SYSTEM'];
162
162
  // ========== end:: typying =======
163
163
 
164
+ // ========== begin:: stream audio ======= //
165
+ public isStreamAudioActive = false;
166
+ public isStreamAudioConnecting = false;
167
+ // ========== end:: stream audio ======= //
168
+
164
169
  @ViewChild(ConversationFooterComponent) conversationFooter: ConversationFooterComponent
165
170
  @ViewChild(ConversationContentComponent) conversationContent: ConversationContentComponent
166
171
  conversationHandlerService: ConversationHandlerService
@@ -246,7 +251,21 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
246
251
  'CONTINUE',
247
252
  'EMOJI_NOT_ELLOWED',
248
253
  'ATTACHMENT',
249
- 'EMOJI'
254
+ 'EMOJI',
255
+ 'BUTTON_ATTACH_FILE',
256
+ 'BUTTON_SEND_MESSAGE',
257
+ 'BUTTON_RECORD_AUDIO',
258
+ 'BUTTON_DELETE_AUDIO',
259
+ 'BUTTON_SEND_AUDIO',
260
+ 'BUTTON_PLAY_AUDIO',
261
+ 'BUTTON_PAUSE_AUDIO',
262
+ 'SKIP_TO_COMPOSER',
263
+ 'CLOSE_CHAT',
264
+ 'CLOSE',
265
+ 'VOICE_CONNECTING',
266
+ 'VOICE_LISTENING',
267
+ 'VOICE_PROCESSING',
268
+ 'STREAM_AUDIO'
250
269
  ];
251
270
 
252
271
  const keysContent = [
@@ -266,13 +285,21 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
266
285
  'LABEL_THINKING',
267
286
  'LABEL_TO',
268
287
  'ARRAY_DAYS',
288
+ 'CONVERSATION_LOG_LABEL',
289
+ 'BUTTON_SCROLL_TO_BOTTOM',
290
+ 'CAROUSEL_PREVIOUS',
291
+ 'CAROUSEL_NEXT',
292
+ 'CAROUSEL_LABEL',
293
+ 'CAROUSEL_SLIDE_LABEL'
269
294
  ];
270
295
 
271
296
  const keysPreview= [
272
297
  'BACK',
273
298
  'CLOSE',
274
299
  'LABEL_PLACEHOLDER',
275
- 'LABEL_PREVIEW'
300
+ 'LABEL_PREVIEW',
301
+ 'BUTTON_CLOSE_PREVIEW',
302
+ 'BUTTON_SEND_MESSAGE'
276
303
  ];
277
304
 
278
305
  const keysCloseChatDialog= [
@@ -822,6 +849,10 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
822
849
  this.showThinkingMessage = false;
823
850
  }
824
851
 
852
+ if (this.isStreamAudioActive && msg.sender !== this.senderId) {
853
+ this.conversationFooter?.interruptStreamDueToPeerMessage();
854
+ }
855
+
825
856
  that.newMessageAdded(msg);
826
857
  // Update badge based on the latest message received from the server.
827
858
  // We rely on `messages` being kept in-sync by the conversation handler.
@@ -1032,6 +1063,21 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
1032
1063
 
1033
1064
 
1034
1065
 
1066
+ /**
1067
+ * Programmatically moves keyboard focus to the message composer textarea.
1068
+ * Wired to the visible-on-focus skip link in conversation.component.html (WCAG 2.4.1 Bypass Blocks).
1069
+ */
1070
+ skipToCompose() {
1071
+ try {
1072
+ const textarea = document.getElementById('chat21-main-message-context') as HTMLTextAreaElement | null;
1073
+ if (textarea) {
1074
+ textarea.focus();
1075
+ }
1076
+ } catch(e) {
1077
+ this.logger.warn('[CONV-COMP] skipToCompose error', e);
1078
+ }
1079
+ }
1080
+
1035
1081
  scrollToBottom() {
1036
1082
  this.conversationContent.scrollToBottom();
1037
1083
  // const that = this;
@@ -1383,6 +1429,28 @@ export class ConversationComponent implements OnInit, AfterViewInit, OnChanges {
1383
1429
  this.logger.debug('[CONV-COMP] floating onNewConversationButtonClicked')
1384
1430
  this.onNewConversationButtonClicked.emit()
1385
1431
  }
1432
+
1433
+ /** CALLED BY: conv-footer component */
1434
+ onStreamAudioActiveChange(event: boolean){
1435
+ this.isStreamAudioActive = event
1436
+ }
1437
+ /** CALLED BY: conv-footer when connecting state changes */
1438
+ onStreamAudioConnectingChange(event: boolean){
1439
+ this.isStreamAudioConnecting = event
1440
+ }
1441
+ /** CALLED BY: conv-footer component */
1442
+ onCloseChatButtonClickedFN(event){
1443
+ this.logger.debug('[CONV-COMP] onCloseChatButtonClicked::::', event)
1444
+ this.onCloseChat()
1445
+ }
1446
+
1447
+ /**
1448
+ * True quando è visibile il pulsante chiudi stream (`.close-stream-button`, `isStreamAudioActive`).
1449
+ * Solo in quel caso il bottom del foglio include `--chat-footer-stream-button-height`.
1450
+ */
1451
+ closeStreamButtonActiveForSheetBottom(): boolean {
1452
+ return !!(this.g?.showAudioStreamFooterButton && (this.isStreamAudioActive || this.isStreamAudioConnecting));
1453
+ }
1386
1454
  // =========== END: event emitter function ====== //
1387
1455
 
1388
1456
 
@@ -1,32 +1,44 @@
1
1
  <div class="audio-recorder">
2
- <button *ngIf="audioUrl" (click)="deleteRecording()">
2
+ <button *ngIf="audioUrl" type="button" [attr.aria-label]="translationMap?.get('BUTTON_DELETE_AUDIO') || 'Delete recording'" (click)="deleteRecording()">
3
3
  <span class="v-align-center">
4
- <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px">
4
+ <svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px">
5
5
  <path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm80-160h80v-360h-80v360Zm160 0h80v-360h-80v360Z"/>
6
6
  </svg>
7
- <!-- <i class="material-icons">delete_outline</i> -->
8
7
  </span>
9
8
  </button>
10
9
 
11
- <chat-audio class="test" *ngIf="audioBlob && audioUrl"
12
- [audioBlob] = "audioBlob"
10
+ <chat-audio class="test" *ngIf="audioBlob && audioUrl"
11
+ [audioBlob]="audioBlob"
13
12
  [color]="'var(--chat-footer-color)'"
13
+ [translationMap]="translationMap"
14
14
  [stylesMap]="stylesMap">
15
15
  </chat-audio>
16
-
17
- <button *ngIf="!audioUrl" class="mic-button" (mousedown)="startRecording($event)" (mouseup)="stopRecording($event)" (touchstart)="startRecording($event)" (touchend)="stopRecording($event)">
18
- <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px">
16
+
17
+ <button *ngIf="!audioUrl"
18
+ type="button"
19
+ class="mic-button"
20
+ [attr.aria-label]="translationMap?.get('BUTTON_RECORD_AUDIO') || 'Hold to record an audio message'"
21
+ [attr.aria-pressed]="isRecording ? 'true' : 'false'"
22
+ (mousedown)="startRecording($event)"
23
+ (mouseup)="stopRecording($event)"
24
+ (touchstart)="startRecording($event)"
25
+ (touchend)="stopRecording($event)"
26
+ (keydown.space)="$event.preventDefault(); !isRecording && startRecording($event)"
27
+ (keyup.space)="$event.preventDefault(); isRecording && stopRecording($event)">
28
+ <svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px">
19
29
  <path d="M480-400q-50 0-85-35t-35-85v-240q0-50 35-85t85-35q50 0 85 35t35 85v240q0 50-35 85t-85 35Zm0-240Zm-40 520v-123q-104-14-172-93t-68-184h80q0 83 58.5 141.5T480-320q83 0 141.5-58.5T680-520h80q0 105-68 184t-172 93v123h-80Zm40-360q17 0 28.5-11.5T520-520v-240q0-17-11.5-28.5T480-800q-17 0-28.5 11.5T440-760v240q0 17 11.5 28.5T480-480Z"/>
20
30
  </svg>
21
31
  </button>
22
- <!-- <button *ngIf="isRecording" (click)="stopRecording()"><i class="material-icons">pause_circle_outline</i></button> -->
23
-
24
- <button *ngIf="audioUrl" (click)="sendMessage()">
32
+
33
+ <button *ngIf="audioUrl"
34
+ type="button"
35
+ [attr.aria-label]="translationMap?.get('BUTTON_SEND_AUDIO') || 'Send audio message'"
36
+ (click)="sendMessage()">
25
37
  <span class="v-align-center">
26
- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="20" width="24" viewBox="0 0 24 20" xml:space="preserve">
38
+ <svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="20" width="24" viewBox="0 0 24 20" xml:space="preserve">
27
39
  <path d="M1.8,18.9V1.7L22,10.3L1.8,18.9z M3.9,15.6l12.6-5.4L3.9,4.9v3.7l6.4,1.6l-6.4,1.6V15.6z M3.9,15.6V4.9v7V15.6z"/>
28
40
  </svg>
29
41
  </span>
30
42
  </button>
31
43
 
32
- </div>
44
+ </div>
@@ -1,23 +1,141 @@
1
- import { ComponentFixture, TestBed } from '@angular/core/testing';
1
+ import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
2
2
 
3
3
  import { ConversationAudioRecorderComponent } from './conversation-audio-recorder.component';
4
4
 
5
- describe('AudioRecorderComponent', () => {
5
+ describe('ConversationAudioRecorderComponent', () => {
6
6
  let component: ConversationAudioRecorderComponent;
7
7
  let fixture: ComponentFixture<ConversationAudioRecorderComponent>;
8
+ let stopListeners: { stop?: () => void; data?: (e: { data: Blob }) => void };
9
+ let mediaRecorderInstance: {
10
+ start: jasmine.Spy;
11
+ stop: jasmine.Spy;
12
+ mimeType: string;
13
+ addEventListener: jasmine.Spy;
14
+ };
8
15
 
9
16
  beforeEach(async () => {
17
+ stopListeners = {};
18
+ mediaRecorderInstance = {
19
+ start: jasmine.createSpy('start'),
20
+ stop: jasmine.createSpy('stop').and.callFake(() => {
21
+ const fn = stopListeners.stop;
22
+ if (fn) {
23
+ fn();
24
+ }
25
+ }),
26
+ mimeType: 'audio/webm',
27
+ addEventListener: jasmine.createSpy('addEventListener').and.callFake((ev: string, fn: any) => {
28
+ if (ev === 'stop') {
29
+ stopListeners.stop = fn;
30
+ }
31
+ if (ev === 'dataavailable') {
32
+ stopListeners.data = fn;
33
+ }
34
+ }),
35
+ };
36
+
37
+ const stream = {
38
+ getTracks: () => [{ stop: jasmine.createSpy('trackStop') }],
39
+ };
40
+
41
+ spyOn(window.navigator.mediaDevices, 'getUserMedia').and.returnValue(Promise.resolve(stream as any));
42
+ (window as any).MediaRecorder = jasmine.createSpy('MediaRecorder').and.returnValue(mediaRecorderInstance);
43
+
10
44
  await TestBed.configureTestingModule({
11
- declarations: [ ConversationAudioRecorderComponent ]
12
- })
13
- .compileComponents();
45
+ declarations: [ConversationAudioRecorderComponent],
46
+ }).compileComponents();
14
47
 
15
48
  fixture = TestBed.createComponent(ConversationAudioRecorderComponent);
16
49
  component = fixture.componentInstance;
50
+ component.translationMap = new Map();
51
+ component.stylesMap = new Map();
52
+ spyOn(component.startRecordingEvent, 'emit');
53
+ spyOn(component.endRecordingEvent, 'emit');
17
54
  fixture.detectChanges();
18
55
  });
19
56
 
20
57
  it('should create', () => {
21
58
  expect(component).toBeTruthy();
22
59
  });
60
+
61
+ describe('startRecording', () => {
62
+ it('should preventDefault on touchstart', fakeAsync(() => {
63
+ const ev = { type: 'touchstart', preventDefault: jasmine.createSpy('pd') } as any;
64
+ component.startRecording(ev);
65
+ tick();
66
+ expect(ev.preventDefault).toHaveBeenCalled();
67
+ expect(component.startRecordingEvent.emit).toHaveBeenCalled();
68
+ }));
69
+
70
+ it('should request microphone and start MediaRecorder on mousedown', fakeAsync(() => {
71
+ const ev = new MouseEvent('mousedown');
72
+ component.startRecording(ev);
73
+ tick();
74
+ expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith({ audio: true });
75
+ expect(mediaRecorderInstance.start).toHaveBeenCalled();
76
+ expect(component.isRecording).toBe(true);
77
+ }));
78
+
79
+ it('should log when getUserMedia fails', fakeAsync(() => {
80
+ (navigator.mediaDevices.getUserMedia as jasmine.Spy).and.returnValue(Promise.reject(new Error('denied')));
81
+ spyOn(console, 'error');
82
+ component.startRecording(new MouseEvent('mousedown'));
83
+ tick();
84
+ expect(console.error).toHaveBeenCalled();
85
+ }));
86
+ });
87
+
88
+ describe('stopRecording', () => {
89
+ it('should discard very short press without stopping recorder', fakeAsync(() => {
90
+ component.startTime = Date.now();
91
+ component.stopRecording(new MouseEvent('mouseup'));
92
+ tick(400);
93
+ expect(mediaRecorderInstance.stop).not.toHaveBeenCalled();
94
+ }));
95
+
96
+ it('should stop recorder after long press', fakeAsync(() => {
97
+ component.mediaRecorder = mediaRecorderInstance as any;
98
+ component.startTime = Date.now() - 600;
99
+ component.stopRecording(new MouseEvent('mouseup'));
100
+ tick(400);
101
+ expect(mediaRecorderInstance.stop).toHaveBeenCalled();
102
+ }));
103
+
104
+ it('should preventDefault on touchend', () => {
105
+ const ev = { type: 'touchend', preventDefault: jasmine.createSpy('pd') } as any;
106
+ component.stopRecording(ev);
107
+ expect(ev.preventDefault).toHaveBeenCalled();
108
+ });
109
+ });
110
+
111
+ describe('deleteRecording', () => {
112
+ it('should reset state and emit', () => {
113
+ spyOn(component.deleteRecordingEvent, 'emit');
114
+ component.audioUrl = {} as any;
115
+ component.audioBlob = new Blob();
116
+ component.deleteRecording();
117
+ expect(component.audioUrl).toBeNull();
118
+ expect(component.audioBlob).toBeNull();
119
+ expect(component.deleteRecordingEvent.emit).toHaveBeenCalledWith(null);
120
+ });
121
+ });
122
+
123
+ describe('sendMessage', () => {
124
+ it('should emit blob and clear url when recording exists', () => {
125
+ spyOn(component.sendRecordingEvent, 'emit');
126
+ const b = new Blob(['a'], { type: 'audio/webm' });
127
+ component.audioBlob = b;
128
+ component.audioUrl = {} as any;
129
+ component.sendMessage();
130
+ expect(component.sendRecordingEvent.emit).toHaveBeenCalledWith(b);
131
+ expect(component.audioUrl).toBeNull();
132
+ });
133
+
134
+ it('should no-op when there is no audioUrl', () => {
135
+ spyOn(component.sendRecordingEvent, 'emit');
136
+ component.audioUrl = null;
137
+ component.sendMessage();
138
+ expect(component.sendRecordingEvent.emit).not.toHaveBeenCalled();
139
+ });
140
+ });
23
141
  });
@@ -9,6 +9,7 @@ import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
9
9
  export class ConversationAudioRecorderComponent {
10
10
 
11
11
  @Input() stylesMap: Map<string, string>;
12
+ @Input() translationMap: Map<string, string>;
12
13
  @Output() startRecordingEvent = new EventEmitter<void>();
13
14
  @Output() deleteRecordingEvent = new EventEmitter<void>();
14
15
  @Output() endRecordingEvent = new EventEmitter<Blob | null>();
@@ -2,7 +2,12 @@
2
2
 
3
3
  <div class="c21-body-container">
4
4
 
5
- <div class="c21-body-content" tabindex="1520" aria-label=" messaggi della conversazione: ">
5
+ <div class="c21-body-content"
6
+ role="log"
7
+ aria-live="polite"
8
+ aria-relevant="additions text"
9
+ aria-atomic="false"
10
+ [attr.aria-label]="translationMap?.get('CONVERSATION_LOG_LABEL') || 'Conversation messages'">
6
11
 
7
12
  <!-- USER TYPING (WAIT MESSAGE) -->
8
13
  <span *ngIf="messages && this.messages.length === 0 && !isTypings">
@@ -19,21 +24,22 @@
19
24
  <div #scrollMe id="scroll-me" (scroll)="onScroll($event)">
20
25
 
21
26
  <div id="{{idDivScroll}}" class="c21-contentScroll" > <!-- (resized)="onResized($event)" -->
22
- <div *ngFor="let message of messages; let first = first; let last = last; let i = index" tabindex="1521" class="rowMsg">
23
-
27
+ <div *ngFor="let message of messages; let first = first; let last = last; let i = index" class="rowMsg">
28
+
24
29
  <!-- message SENDER:: -->
25
- <div role="messaggio" *ngIf="messageType(MESSAGE_TYPE_MINE, message)" class="msg_container base_sent">
30
+ <div role="article" *ngIf="messageType(MESSAGE_TYPE_MINE, message) && !message.isJustRecived" class="msg_container base_sent">
26
31
 
27
32
  <!--backgroundColor non viene ancora usato -->
28
33
  <!-- class="messages msg_sent slide-in-right" -->
29
34
  <chat-bubble-message class="messages msg_sent"
30
- [class.no-background]="(isImage(message) || isFrame(message)) && ((message?.text && message?.text.trim() === '') || !message?.text)"
35
+ [class.no-background]="(isImage(message) || isFrame(message)) && ((message?.text && message?.text.trim() === '') || !message?.text)"
31
36
  [class.emoticon]="isEmojii(message?.text)"
32
37
  [ngStyle]="{'background': stylesMap.get('bubbleSentBackground'), 'color': stylesMap.get('bubbleSentTextColor')}"
33
38
  [ngClass]="{'button-in-msg' : message?.metadata && message?.metadata?.button}"
34
39
  [message]="message"
35
40
  [fontColor]="stylesMap.get('bubbleSentTextColor')"
36
41
  [stylesMap]="stylesMap"
42
+ [translationMap]="translationMap"
37
43
  (onBeforeMessageRender)="onBeforeMessageRenderFN($event)"
38
44
  (onAfterMessageRender)="onAfterMessageRenderFN($event)"
39
45
  (onElementRendered)="onElementRenderedFN($event)">
@@ -47,9 +53,9 @@
47
53
  </div>
48
54
 
49
55
  <!-- message RECIPIENT:: -->
50
- <div role="messaggio" *ngIf="messageType(MESSAGE_TYPE_OTHERS, message)" class="msg_container base_receive">
56
+ <div role="article" *ngIf="messageType(MESSAGE_TYPE_OTHERS, message)" class="msg_container base_receive">
51
57
 
52
- <chat-avatar-image *ngIf="!isSameSender(message?.sender, i)"
58
+ <chat-avatar-image *ngIf="!isSameSender(message?.sender, i) && !isStreamAudioActive"
53
59
  [ngClass]="{'slide-in-left': false}"
54
60
  [senderID]="message?.sender"
55
61
  [senderFullname]="message?.sender_fullname"
@@ -60,14 +66,17 @@
60
66
  <!-- [ngClass]="{'slide-in-left': !isFirstMessage(message?.sender, i)}" -->
61
67
  <chat-bubble-message class="messages msg_receive"
62
68
  [ngClass]="{'slide-in-left': false}"
63
- [class.no-background]="(isImage(message) || isFrame(message) || isCarousel(message)) && ((message?.text && message?.text.trim() === '') || !message?.text)"
69
+ [class.no-background]="(isImage(message) || isFrame(message) || isCarousel(message)) && ((message?.text && message?.text.trim() === '') || !message?.text)"
64
70
  [class.emoticon]="isEmojii(message?.text)"
65
- [style.margin-left]="isSameSender(message?.sender, i)? 'calc(var(--avatar-width) + 10px)': null"
71
+ [class.fullSizeMessage]="isStreamAudioActive"
72
+ [style.margin-left]="isSameSender(message?.sender, i) && !isStreamAudioActive ? 'calc(var(--avatar-width) + 10px)' : null"
66
73
  [ngStyle]="{'background': stylesMap.get('bubbleReceivedBackground'), 'color': stylesMap.get('bubbleReceivedTextColor'), 'width':isFrame(message) ?'100%' : null}"
67
74
  [isSameSender]="isSameSender(message?.sender, i)"
68
75
  [message]="message"
69
76
  [fontColor]="stylesMap.get('bubbleReceivedTextColor')"
70
77
  [stylesMap]="stylesMap"
78
+ [translationMap]="translationMap"
79
+ [streamOnArrival]="false"
71
80
  (onBeforeMessageRender)="onBeforeMessageRenderFN($event)"
72
81
  (onAfterMessageRender)="onAfterMessageRenderFN($event)"
73
82
  (onElementRendered)="onElementRenderedFN($event)">
@@ -110,6 +119,7 @@
110
119
  [isConversationArchived]="isConversationArchived"
111
120
  [isLastMessage] = "isLastMessage(message?.uid)"
112
121
  [stylesMap]="stylesMap"
122
+ [translationMap]="translationMap"
113
123
  (onElementRendered)="onElementRenderedFN($event)"
114
124
  (onAttachmentButtonClicked)="onAttachmentButtonClickedFN($event)">
115
125
  </chat-carousel>
@@ -134,9 +144,10 @@
134
144
  [senderFullname]="nameUserTypingNow"
135
145
  [baseLocation]="baseLocation">
136
146
  </chat-avatar-image>
147
+
137
148
  <user-typing
138
- [ngClass]="{'userTypingNowExist': !idUserTypingNow}"
139
149
  [color]="stylesMap?.get('iconColor')"
150
+ [ngClass]="{'userTypingNowExist': !idUserTypingNow}"
140
151
  [translationMap]="translationMap"
141
152
  [idUserTypingNow]="idUserTypingNow"
142
153
  [nameUserTypingNow]="nameUserTypingNow">
@@ -145,7 +156,9 @@
145
156
 
146
157
  <div *ngIf="showThinkingMessage && lastServerSenderKind === 'bot'" class="msg_container base_receive thinking_receive">
147
158
  <user-typing class="loading thinking-dots"
159
+ [class.fullSize]="isStreamAudioActive"
148
160
  [color]="stylesMap?.get('iconColor')"
161
+ [class.fullSize]="isStreamAudioActive"
149
162
  [translationMap]="translationMap"
150
163
  [idUserTypingNow]="idUserTypingNow"
151
164
  [nameUserTypingNow]="nameUserTypingNow">
@@ -27,6 +27,10 @@
27
27
  margin: 25px 50px
28
28
  }
29
29
 
30
+ :host .loading.fullSize ::ng-deep > div.spinner{
31
+ margin: 50px 0px !important;
32
+ }
33
+
30
34
  // ============= CSS c21-body ================= //
31
35
  .c21-body {
32
36
  // -webkit-box-shadow: inset 0 10px 10px -10px rgba(0,0,0,0.4);
@@ -242,6 +246,11 @@
242
246
  height: fit-content;
243
247
  width: auto;
244
248
 
249
+ &.fullSizeMessage {
250
+ max-width: 100%;
251
+ margin: auto 0 auto 0 !important;
252
+ }
253
+
245
254
  }
246
255
  .msg_receive.json-resources{
247
256
  min-height: unset;
@@ -283,6 +292,15 @@
283
292
  }// end c21-body-container
284
293
  }// end c21-body
285
294
 
295
+ /* Solo con pulsante chiudi stream (stream in ascolto): altezza extra come #streamAudioAlert */
296
+ :host-context(#chat21-conversation-component.chat21-conversation--close-stream-active) .c21-body .c21-body-container .c21-body-content .chat21-sheet-content {
297
+ bottom: calc(
298
+ var(--chat-footer-logo-height) +
299
+ var(--chat-footer-height) +
300
+ var(--chat-footer-stream-button-height)
301
+ );
302
+ }
303
+
286
304
  @keyframes thinking-dot {
287
305
  0%, 80%, 100% {
288
306
  opacity: 0.2;