@chat21/chat21-web-widget 5.1.32-rc8 → 5.1.33-rc11

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 (246) 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/.playwright-mcp/console-2026-05-08T15-31-09-000Z.log +17 -0
  6. package/.playwright-mcp/console-2026-05-08T15-32-19-412Z.log +89 -0
  7. package/.playwright-mcp/console-2026-05-08T16-18-48-424Z.log +133 -0
  8. package/.playwright-mcp/console-2026-05-11T12-54-06-869Z.log +13 -0
  9. package/.playwright-mcp/console-2026-05-11T12-54-56-229Z.log +147 -0
  10. package/.playwright-mcp/console-2026-05-11T12-55-47-174Z.log +183 -0
  11. package/.playwright-mcp/console-2026-05-11T15-34-03-590Z.log +210 -0
  12. package/.playwright-mcp/console-2026-05-12T15-07-31-880Z.log +118 -0
  13. package/.playwright-mcp/page-2026-05-08T15-32-19-900Z.yml +851 -0
  14. package/.playwright-mcp/page-2026-05-08T15-32-47-264Z.yml +857 -0
  15. package/.playwright-mcp/page-2026-05-08T15-33-17-089Z.yml +1110 -0
  16. package/.playwright-mcp/page-2026-05-08T15-33-23-486Z.yml +1069 -0
  17. package/.playwright-mcp/page-2026-05-08T15-33-45-390Z.yml +1076 -0
  18. package/.playwright-mcp/page-2026-05-08T15-33-52-666Z.yml +1072 -0
  19. package/.playwright-mcp/page-2026-05-08T15-34-01-338Z.yml +1085 -0
  20. package/.playwright-mcp/page-2026-05-08T15-34-07-227Z.yml +1072 -0
  21. package/.playwright-mcp/page-2026-05-08T15-34-13-875Z.yml +1072 -0
  22. package/.playwright-mcp/page-2026-05-08T15-34-21-885Z.yml +1109 -0
  23. package/.playwright-mcp/page-2026-05-08T15-34-32-755Z.yml +1109 -0
  24. package/.playwright-mcp/page-2026-05-08T15-35-09-607Z.yml +1119 -0
  25. package/.playwright-mcp/page-2026-05-08T15-35-14-242Z.yml +1109 -0
  26. package/.playwright-mcp/page-2026-05-08T16-18-48-671Z.yml +44 -0
  27. package/.playwright-mcp/page-2026-05-08T16-18-52-753Z.png +0 -0
  28. package/.playwright-mcp/page-2026-05-08T16-19-13-919Z.yml +68 -0
  29. package/.playwright-mcp/page-2026-05-08T16-19-17-977Z.png +0 -0
  30. package/.playwright-mcp/page-2026-05-08T16-19-25-733Z.yml +120 -0
  31. package/.playwright-mcp/page-2026-05-08T16-19-29-252Z.png +0 -0
  32. package/.playwright-mcp/page-2026-05-08T16-19-39-269Z.yml +80 -0
  33. package/.playwright-mcp/page-2026-05-08T16-19-43-915Z.png +0 -0
  34. package/.playwright-mcp/page-2026-05-08T16-20-04-407Z.yml +81 -0
  35. package/.playwright-mcp/page-2026-05-08T16-20-08-984Z.png +0 -0
  36. package/.playwright-mcp/page-2026-05-08T16-20-32-397Z.png +0 -0
  37. package/.playwright-mcp/page-2026-05-08T16-20-58-658Z.png +0 -0
  38. package/.playwright-mcp/page-2026-05-08T16-21-12-320Z.yml +86 -0
  39. package/.playwright-mcp/page-2026-05-08T16-21-39-154Z.yml +91 -0
  40. package/.playwright-mcp/page-2026-05-08T16-21-45-420Z.png +0 -0
  41. package/.playwright-mcp/page-2026-05-08T16-22-21-062Z.yml +0 -0
  42. package/.playwright-mcp/page-2026-05-08T16-22-58-232Z.yml +91 -0
  43. package/.playwright-mcp/page-2026-05-08T16-23-36-520Z.yml +0 -0
  44. package/.playwright-mcp/page-2026-05-08T16-23-46-805Z.yml +100 -0
  45. package/.playwright-mcp/page-2026-05-08T16-23-55-169Z.png +0 -0
  46. package/.playwright-mcp/page-2026-05-08T16-24-26-574Z.yml +91 -0
  47. package/.playwright-mcp/page-2026-05-08T16-25-34-414Z.png +0 -0
  48. package/.playwright-mcp/page-2026-05-08T16-25-59-831Z.png +0 -0
  49. package/.playwright-mcp/page-2026-05-08T16-26-21-809Z.yml +91 -0
  50. package/.playwright-mcp/page-2026-05-08T16-26-47-443Z.yml +105 -0
  51. package/.playwright-mcp/page-2026-05-08T16-26-56-136Z.png +0 -0
  52. package/.playwright-mcp/page-2026-05-08T16-27-59-610Z.yml +48 -0
  53. package/.playwright-mcp/page-2026-05-11T12-54-07-180Z.yml +44 -0
  54. package/.playwright-mcp/page-2026-05-11T12-54-56-946Z.yml +4 -0
  55. package/.playwright-mcp/page-2026-05-11T12-55-47-503Z.yml +24 -0
  56. package/.playwright-mcp/page-2026-05-11T12-56-00-766Z.yml +28 -0
  57. package/.playwright-mcp/page-2026-05-11T12-56-06-438Z.yml +90 -0
  58. package/.playwright-mcp/page-2026-05-11T12-57-56-838Z.yml +106 -0
  59. package/.playwright-mcp/page-2026-05-11T12-58-00-124Z.yml +106 -0
  60. package/.playwright-mcp/page-2026-05-11T12-59-08-836Z.yml +61 -0
  61. package/.playwright-mcp/page-2026-05-11T12-59-12-088Z.yml +61 -0
  62. package/.playwright-mcp/page-2026-05-11T12-59-26-215Z.yml +69 -0
  63. package/.playwright-mcp/page-2026-05-11T12-59-29-519Z.yml +69 -0
  64. package/.playwright-mcp/page-2026-05-11T12-59-37-309Z.yml +0 -0
  65. package/.playwright-mcp/page-2026-05-11T12-59-39-968Z.yml +79 -0
  66. package/.playwright-mcp/page-2026-05-11T12-59-45-983Z.yml +78 -0
  67. package/.playwright-mcp/page-2026-05-11T12-59-49-951Z.yml +78 -0
  68. package/.playwright-mcp/page-2026-05-11T15-34-04-515Z.yml +0 -0
  69. package/.playwright-mcp/page-2026-05-12T15-07-32-171Z.yml +44 -0
  70. package/.playwright-mcp/page-2026-05-12T15-08-09-820Z.yml +119 -0
  71. package/CHANGELOG.md +68 -3
  72. package/angular.json +20 -3
  73. package/deploy_amazon_beta.sh +7 -17
  74. package/deploy_amazon_prod.sh +41 -0
  75. package/docs/TILEDESK_WIDGET_ACCESSIBILITY_STATEMENT_COMPLETE.md +379 -0
  76. package/env.sample +3 -2
  77. package/mocks/voice-websocket-mock/server.cjs +245 -0
  78. package/package.json +7 -3
  79. package/playwright.config.ts +41 -0
  80. package/src/app/app.component.html +2 -2
  81. package/src/app/app.component.scss +25 -14
  82. package/src/app/app.component.spec.ts +21 -6
  83. package/src/app/app.module.ts +4 -0
  84. package/src/app/component/conversation-detail/conversation/conversation.component.html +19 -11
  85. package/src/app/component/conversation-detail/conversation/conversation.component.scss +28 -0
  86. package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +644 -75
  87. package/src/app/component/conversation-detail/conversation/conversation.component.ts +63 -17
  88. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +25 -13
  89. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +123 -5
  90. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +1 -0
  91. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +22 -9
  92. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +23 -1
  93. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +242 -149
  94. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +7 -6
  95. package/src/app/component/conversation-detail/conversation-emojii/conversation-emojii.component.spec.ts +53 -3
  96. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component copy.html +172 -0
  97. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +112 -61
  98. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +133 -16
  99. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +452 -78
  100. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +199 -79
  101. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +113 -53
  102. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.scss +12 -4
  103. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.spec.ts +274 -29
  104. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.html +23 -9
  105. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.spec.ts +80 -8
  106. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.html +29 -23
  107. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.spec.ts +185 -16
  108. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.ts +34 -14
  109. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +46 -19
  110. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +67 -10
  111. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +142 -12
  112. package/src/app/component/error-alert/error-alert.component.spec.ts +65 -5
  113. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.html +16 -7
  114. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.scss +21 -0
  115. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.spec.ts +89 -7
  116. package/src/app/component/form/form-builder/form-builder.component.html +1 -1
  117. package/src/app/component/form/form-builder/form-builder.component.spec.ts +163 -21
  118. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.html +8 -4
  119. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.scss +10 -5
  120. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.spec.ts +90 -16
  121. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.ts +26 -0
  122. package/src/app/component/form/inputs/form-label/form-label.component.spec.ts +45 -11
  123. package/src/app/component/form/inputs/form-radio-button/form-radio-button.component.spec.ts +24 -6
  124. package/src/app/component/form/inputs/form-select/form-select.component.spec.ts +14 -5
  125. package/src/app/component/form/inputs/form-text/form-text.component.html +14 -12
  126. package/src/app/component/form/inputs/form-text/form-text.component.scss +11 -1
  127. package/src/app/component/form/inputs/form-text/form-text.component.spec.ts +113 -17
  128. package/src/app/component/form/inputs/form-text/form-text.component.ts +35 -3
  129. package/src/app/component/form/inputs/form-textarea/form-textarea.component.html +13 -11
  130. package/src/app/component/form/inputs/form-textarea/form-textarea.component.scss +6 -5
  131. package/src/app/component/form/inputs/form-textarea/form-textarea.component.spec.ts +149 -13
  132. package/src/app/component/form/inputs/form-textarea/form-textarea.component.ts +26 -0
  133. package/src/app/component/form/prechat-form/prechat-form.component.html +14 -11
  134. package/src/app/component/form/prechat-form/prechat-form.component.spec.ts +102 -10
  135. package/src/app/component/form/prechat-form/prechat-form.component.ts +8 -1
  136. package/src/app/component/form/prechat-form-test-mock.ts +35 -0
  137. package/src/app/component/home/home.component.html +38 -31
  138. package/src/app/component/home/home.component.scss +4 -2
  139. package/src/app/component/home/home.component.spec.ts +226 -11
  140. package/src/app/component/home-conversations/home-conversations.component.html +30 -26
  141. package/src/app/component/home-conversations/home-conversations.component.scss +3 -0
  142. package/src/app/component/home-conversations/home-conversations.component.spec.ts +212 -36
  143. package/src/app/component/last-message/last-message.component.html +15 -9
  144. package/src/app/component/last-message/last-message.component.scss +16 -2
  145. package/src/app/component/last-message/last-message.component.spec.ts +204 -23
  146. package/src/app/component/launcher-button/launcher-button.component.html +8 -13
  147. package/src/app/component/launcher-button/launcher-button.component.spec.ts +104 -8
  148. package/src/app/component/list-all-conversations/list-all-conversations.component.html +12 -17
  149. package/src/app/component/list-all-conversations/list-all-conversations.component.scss +2 -0
  150. package/src/app/component/list-conversations/list-conversations.component.html +22 -22
  151. package/src/app/component/menu-options/menu-options.component.html +30 -20
  152. package/src/app/component/menu-options/menu-options.component.spec.ts +125 -9
  153. package/src/app/component/message/audio/audio.component.html +13 -15
  154. package/src/app/component/message/audio/audio.component.spec.ts +140 -5
  155. package/src/app/component/message/audio/audio.component.ts +1 -0
  156. package/src/app/component/message/audio-sync/audio-sync.component.scss +1 -1
  157. package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +81 -1
  158. package/src/app/component/message/audio-sync/audio-sync.component.ts +134 -24
  159. package/src/app/component/message/avatar/avatar.component.html +2 -2
  160. package/src/app/component/message/avatar/avatar.component.spec.ts +99 -7
  161. package/src/app/component/message/bubble-message/bubble-message.component.html +39 -52
  162. package/src/app/component/message/bubble-message/bubble-message.component.scss +59 -1
  163. package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +154 -57
  164. package/src/app/component/message/bubble-message/bubble-message.component.ts +152 -110
  165. package/src/app/component/message/buttons/action-button/action-button.component.html +3 -4
  166. package/src/app/component/message/buttons/action-button/action-button.component.spec.ts +49 -5
  167. package/src/app/component/message/buttons/link-button/link-button.component.scss +5 -8
  168. package/src/app/component/message/buttons/link-button/link-button.component.spec.ts +50 -5
  169. package/src/app/component/message/buttons/text-button/text-button.component.spec.ts +44 -5
  170. package/src/app/component/message/carousel/carousel.component.html +29 -16
  171. package/src/app/component/message/carousel/carousel.component.scss +20 -8
  172. package/src/app/component/message/carousel/carousel.component.spec.ts +80 -3
  173. package/src/app/component/message/carousel/carousel.component.ts +16 -0
  174. package/src/app/component/message/frame/frame.component.html +9 -4
  175. package/src/app/component/message/frame/frame.component.spec.ts +34 -15
  176. package/src/app/component/message/frame/frame.component.ts +7 -2
  177. package/src/app/component/message/html/html.component.html +1 -1
  178. package/src/app/component/message/html/html.component.scss +1 -1
  179. package/src/app/component/message/html/html.component.spec.ts +24 -7
  180. package/src/app/component/message/image/image.component.html +12 -10
  181. package/src/app/component/message/image/image.component.scss +16 -0
  182. package/src/app/component/message/image/image.component.spec.ts +101 -15
  183. package/src/app/component/message/image/image.component.ts +90 -51
  184. package/src/app/component/message/info-message/info-message.component.spec.ts +26 -14
  185. package/src/app/component/message/json-sources/json-sources.component.html +38 -0
  186. package/src/app/component/message/json-sources/json-sources.component.scss +201 -0
  187. package/src/app/component/message/json-sources/json-sources.component.ts +89 -0
  188. package/src/app/component/message/like-unlike/like-unlike.component.html +7 -9
  189. package/src/app/component/message/like-unlike/like-unlike.component.spec.ts +31 -3
  190. package/src/app/component/message/return-receipt/return-receipt.component.spec.ts +38 -17
  191. package/src/app/component/message/text/text.component.html +3 -3
  192. package/src/app/component/message/text/text.component.scss +80 -86
  193. package/src/app/component/message/text/text.component.spec.ts +106 -13
  194. package/src/app/component/message-attachment/message-attachment.component.spec.ts +134 -13
  195. package/src/app/component/selection-department/selection-department.component.html +21 -23
  196. package/src/app/component/selection-department/selection-department.component.spec.ts +159 -14
  197. package/src/app/component/selection-department/selection-department.component.ts +8 -1
  198. package/src/app/component/send-button/send-button.component.html +5 -13
  199. package/src/app/component/send-button/send-button.component.spec.ts +2 -2
  200. package/src/app/component/star-rating-widget/star-rating-widget.component.html +51 -81
  201. package/src/app/directives/tooltip.directive.spec.ts +8 -4
  202. package/src/app/modals/confirm-close/confirm-close.component.html +20 -8
  203. package/src/app/modals/confirm-close/confirm-close.component.scss +3 -0
  204. package/src/app/modals/confirm-close/confirm-close.component.spec.ts +13 -4
  205. package/src/app/modals/confirm-close/confirm-close.component.ts +8 -1
  206. package/src/app/pipe/html-entites-encode.pipe.spec.ts +35 -2
  207. package/src/app/pipe/marked.pipe.spec.ts +38 -2
  208. package/src/app/pipe/marked.pipe.ts +51 -41
  209. package/src/app/providers/app-config.service.ts +4 -2
  210. package/src/app/providers/brand.service.spec.ts +23 -2
  211. package/src/app/providers/brand.service.ts +1 -1
  212. package/src/app/providers/global-settings.service.spec.ts +1009 -14
  213. package/src/app/providers/global-settings.service.ts +59 -2
  214. package/src/app/providers/json-sources-parser.service.ts +175 -0
  215. package/src/app/providers/translator.service.ts +24 -6
  216. package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +117 -0
  217. package/src/app/providers/tts-audio-playback-coordinator.service.ts +45 -7
  218. package/src/app/providers/url-preview.service.ts +82 -0
  219. package/src/app/providers/voice/audio.types.ts +6 -0
  220. package/src/app/providers/voice/voice-streaming.service.spec.ts +23 -0
  221. package/src/app/providers/voice/voice-streaming.service.ts +702 -0
  222. package/src/app/providers/voice/voice-streaming.types.ts +112 -0
  223. package/src/app/providers/voice/voice.service.spec.ts +170 -3
  224. package/src/app/providers/voice/voice.service.ts +695 -16
  225. package/src/app/sass/_variables.scss +1 -1
  226. package/src/app/sass/animations.scss +19 -1
  227. package/src/app/utils/globals.ts +14 -0
  228. package/src/app/utils/json-sources-utils.ts +27 -0
  229. package/src/app/utils/url-utils.ts +98 -0
  230. package/src/app/utils/utils-resources.ts +1 -1
  231. package/src/assets/i18n/en.json +106 -100
  232. package/src/assets/i18n/es.json +107 -101
  233. package/src/assets/i18n/fr.json +107 -101
  234. package/src/assets/i18n/it.json +107 -99
  235. package/src/assets/sounds/keyboard.mp3 +0 -0
  236. package/src/assets/twp/index-dev.html +18 -0
  237. package/src/assets/twp/tiledesk_widget_files/widget-css-override-example.css +14 -0
  238. package/src/chat21-core/providers/chat-manager.spec.ts +72 -0
  239. package/src/chat21-core/providers/scripts/script.service.spec.ts +12 -2
  240. package/src/chat21-core/utils/constants.ts +4 -0
  241. package/src/chat21-core/utils/utils-message.ts +23 -1
  242. package/src/widget-config-template.json +3 -1
  243. package/src/widget-config.json +28 -27
  244. package/tests/widget-form-rich.spec.ts +67 -0
  245. package/tests/widget-index-dev-settings.spec.ts +52 -0
  246. package/tests/widget-twp-iframe.spec.ts +39 -0
@@ -1,149 +1,191 @@
1
- import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
1
+ import { Component, EventEmitter, HostBinding, Input, OnDestroy, OnInit, Output } from '@angular/core';
2
+ import { Observable, Subscription } from 'rxjs';
3
+ import { map, startWith } from 'rxjs/operators';
2
4
  import { DomSanitizer } from '@angular/platform-browser';
3
5
  import { MessageModel } from 'src/chat21-core/models/message';
4
- import { LoggerService } from 'src/chat21-core/providers/abstract/logger.service';
5
- import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
6
- import { MAX_WIDTH_IMAGES, MESSAGE_TYPE_MINE, MESSAGE_TYPE_OTHERS, MIN_WIDTH_IMAGES } from 'src/chat21-core/utils/constants';
6
+ import { MESSAGE_TYPE_MINE, MESSAGE_TYPE_OTHERS, TYPE_MSG_URL_PREVIEW } from 'src/chat21-core/utils/constants';
7
7
  import { convertColorToRGBA } from 'src/chat21-core/utils/utils';
8
- import { isAudio, isAudioTTS, isFile, isFrame, isImage, messageType } from 'src/chat21-core/utils/utils-message';
8
+ import { JsonSourcesParserService } from 'src/app/providers/json-sources-parser.service';
9
+ import { calcImageSize, isAudio, isAudioTTS, isFile, isFrame, isImage, isJsonSources, messageType } from 'src/chat21-core/utils/utils-message';
9
10
  import { getColorBck } from 'src/chat21-core/utils/utils-user';
11
+ import { VoiceService } from 'src/app/providers/voice/voice.service';
12
+ import { JsonSourceItem } from '../json-sources/json-sources.component';
13
+ import { VoiceTtsKaraokeWord } from 'src/app/providers/voice/voice-streaming.types';
10
14
 
11
15
  @Component({
12
16
  selector: 'chat-bubble-message',
13
17
  templateUrl: './bubble-message.component.html',
14
18
  styleUrls: ['./bubble-message.component.scss']
15
19
  })
16
- export class BubbleMessageComponent implements OnInit {
20
+ export class BubbleMessageComponent implements OnInit, OnDestroy {
17
21
 
18
22
  @Input() message: MessageModel;
19
23
  @Input() isSameSender: boolean;
20
24
  @Input() fontColor: string;
21
25
  @Input() stylesMap: Map<string, string>;
26
+ @Input() translationMap: Map<string, string>;
27
+ /** When true, a newly-arrived bot text message reveals its words one by one. */
28
+ @Input() streamOnArrival = false;
29
+ /** One-shot flag: set once in ngOnChanges, never reverts so animation isn't replayed. */
30
+ _isStreaming = false;
31
+ /** Precomputed word list; rebuilt only when the message text changes. */
32
+ _streamingWords: Array<{ word: string; index: number }> = [];
33
+ /** Live karaoke word states driven by voiceTtsKaraoke$ during an active WSS session. */
34
+ _wssKaraokeWords$?: Observable<VoiceTtsKaraokeWord[]>;
35
+
36
+ private _kSub?: Subscription;
22
37
  @Output() onBeforeMessageRender = new EventEmitter();
23
38
  @Output() onAfterMessageRender = new EventEmitter();
24
- @Output() onElementRendered = new EventEmitter<{element: string, status: boolean}>();
25
- isImage = isImage;
26
- isFile = isFile;
27
- isFrame = isFrame;
28
- isAudio = isAudio;
29
- isAudioTTS=isAudioTTS;
30
- convertColorToRGBA = convertColorToRGBA
31
-
32
- // ========== begin:: check message type functions ======= //
33
- messageType = messageType;
34
-
35
- MESSAGE_TYPE_MINE = MESSAGE_TYPE_MINE;
36
- MESSAGE_TYPE_OTHERS = MESSAGE_TYPE_OTHERS;
37
- // ========== end:: check message type functions ======= //
38
- sizeImage : { width: number, height: number}
39
- fullnameColor: string;
40
- private logger: LoggerService = LoggerInstance.getInstance()
41
- constructor(public sanitizer: DomSanitizer) { }
42
-
43
- ngOnInit() {
44
- // console.log("---- > MSG:", this.message);
39
+ @Output() onElementRendered = new EventEmitter<{ element: string; status: boolean }>();
40
+
41
+ @HostBinding('class.no-background') get hostNoBackground() { return this.jsonSources !== null && this.jsonSources.length > 0; }
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);
45
54
  }
46
55
 
47
- ngOnChanges() {
48
- if (this.message && this.message.metadata && typeof this.message.metadata === 'object' ) {
49
- this.sizeImage = this.getMetadataSize(this.message.metadata)
50
- }
56
+ readonly isImage = isImage;
57
+ readonly isFile = isFile;
58
+ readonly isFrame = isFrame;
59
+ readonly isAudio = isAudio;
60
+ readonly isJsonSources = isJsonSources;
61
+ readonly isAudioTTS = isAudioTTS;
62
+ readonly messageType = messageType;
63
+ readonly convertColorToRGBA = convertColorToRGBA;
64
+ readonly MESSAGE_TYPE_MINE = MESSAGE_TYPE_MINE;
65
+ readonly MESSAGE_TYPE_OTHERS = MESSAGE_TYPE_OTHERS;
66
+
67
+ sizeImage: { width: number; height: number } = { width: 0, height: 0 };
68
+ fullnameColor: string = '';
69
+ jsonSources: JsonSourceItem[] | null = null;
70
+ isUrlPreviewMessage = false;
71
+
72
+ private urlPreviewReqId = 0;
73
+
74
+ constructor(
75
+ public sanitizer: DomSanitizer,
76
+ public voiceService: VoiceService,
77
+ private jsonSourcesParser: JsonSourcesParserService
78
+ ) { }
51
79
 
52
- if(this.fontColor){
53
- this.fullnameColor = convertColorToRGBA(this.fontColor, 65)
80
+ ngOnInit() {
81
+ // If this TTS message arrived while the voice proxy was active, mark it so
82
+ // audio-sync never replays it after the session ends.
83
+ if (isAudioTTS(this.message) && this.voiceService.isWssVoiceActive && this.message?.uid) {
84
+ this.voiceService.markProxyHandled(this.message.uid);
54
85
  }
55
- if(this.message && this.message.sender_fullname && this.message.sender_fullname.trim() !== ''){
56
- this.fullnameColor = getColorBck(this.message.sender_fullname)
86
+
87
+ // Set up karaoke observable for TTS messages during WSS sessions.
88
+ if (isAudioTTS(this.message) && this.message?.text) {
89
+ const text = this.message.text;
90
+ const rawWords = text.trim().split(/\s+/).filter((w) => w.length > 0);
91
+ // Always start as 'past' (fully visible). The karaoke RAF loop will drive
92
+ // words through future→active→past for the current speaking turn; using
93
+ // 'future' here would dimm old/history messages the moment voice opens.
94
+ const initialWords: VoiceTtsKaraokeWord[] = rawWords.map((w) => ({ text: w, state: 'past' as const }));
95
+
96
+ this._wssKaraokeWords$ = this.voiceService.voiceTtsKaraoke$.pipe(
97
+ startWith({ text, words: initialWords, activeIndex: -1 }),
98
+ map((frame) =>
99
+ frame.text === text
100
+ ? (frame.words as VoiceTtsKaraokeWord[])
101
+ : initialWords,
102
+ ),
103
+ );
57
104
  }
105
+ }
58
106
 
107
+ ngOnDestroy(): void {
108
+ this._kSub?.unsubscribe();
109
+ this._kSub = undefined;
59
110
  }
60
111
 
61
- /**
62
- *
63
- * @param message
64
- */
65
- // getMetadataSize(metadata): any {
66
- // if(metadata.width === undefined){
67
- // metadata.width= MAX_WIDTH_IMAGES
68
- // }
69
- // if(metadata.height === undefined){
70
- // metadata.height = MAX_WIDTH_IMAGES
71
- // }
72
- // // const MAX_WIDTH_IMAGES = 300;
73
- // const sizeImage = {
74
- // width: metadata.width,
75
- // height: metadata.height
76
- // };
77
- // // that.g.wdLog(['message::: ', metadata);
78
- // if (metadata.width && metadata.width > MAX_WIDTH_IMAGES) {
79
- // const rapporto = (metadata['width'] / metadata['height']);
80
- // sizeImage.width = MAX_WIDTH_IMAGES;
81
- // sizeImage.height = MAX_WIDTH_IMAGES / rapporto;
82
- // }
83
- // return sizeImage; // h.toString();
84
- // }
85
-
86
- /**
87
- *
88
- * @param message
89
- */
90
- getMetadataSize(metadata): {width, height} {
91
- // if (metadata.width === undefined) {
92
- // metadata.width = MAX_WIDTH_IMAGES
93
- // }
94
- // if (metadata.height === undefined) {
95
- // metadata.height = MAX_WIDTH_IMAGES
96
- // }
97
-
98
- const sizeImage = {
99
- width: metadata.width,
100
- height: metadata.height
101
- };
102
-
103
-
104
- if (metadata.width && metadata.width < MAX_WIDTH_IMAGES) {
105
- if (metadata.width <= 55) {
106
- const ratio = (metadata['width'] / metadata['height']);
107
- sizeImage.width = MIN_WIDTH_IMAGES;
108
- sizeImage.height = MIN_WIDTH_IMAGES / ratio;
109
- } else if (metadata.width > 55) {
110
- sizeImage.width = metadata.width;
111
- sizeImage.height = metadata.height
112
- }
113
- } else if (metadata.width && metadata.width > MAX_WIDTH_IMAGES) {
114
- const ratio = (metadata['width'] / metadata['height']);
115
- sizeImage.width = MAX_WIDTH_IMAGES;
116
- sizeImage.height = MAX_WIDTH_IMAGES / ratio;
112
+ ngOnChanges(): void {
113
+ if (this.message?.metadata && typeof this.message.metadata === 'object') {
114
+ this.sizeImage = calcImageSize(this.message.metadata);
117
115
  }
118
- return sizeImage
119
- }
120
116
 
121
- // ========= begin:: event emitter function ============//
117
+ this.fullnameColor = this.fontColor
118
+ ? convertColorToRGBA(this.fontColor, 65)
119
+ : this.fullnameColor;
120
+
121
+ if (this.message?.sender_fullname?.trim()) {
122
+ this.fullnameColor = getColorBck(this.message.sender_fullname);
123
+ }
124
+
125
+ // One-shot: activate word streaming for newly-arrived bot text messages during a voice session.
126
+ // Reset isJustRecived so the animation never replays on subsequent change detection cycles.
127
+ if (
128
+ !this._isStreaming &&
129
+ this.streamOnArrival &&
130
+ this.message?.isJustRecived === true &&
131
+ this.messageType(this.MESSAGE_TYPE_OTHERS, this.message) &&
132
+ !this.isAudio(this.message) &&
133
+ !this.isAudioTTS(this.message) &&
134
+ this.message?.type !== 'html'
135
+ ) {
136
+ this._isStreaming = true;
137
+ this._streamingWords = (this.message.text ?? '')
138
+ .trim()
139
+ .split(/\s+/)
140
+ .filter(w => w.length > 0)
141
+ .map((word, index) => ({ word, index }));
142
+ this.message.isJustRecived = false;
143
+ }
122
144
 
123
- // returnOpenAttachment(event: String) {
124
- // this.onOpenAttachment.emit(event)
125
- // }
145
+ if (this.message?.type !== TYPE_MSG_URL_PREVIEW) {
146
+ this.jsonSources = null;
147
+ return;
148
+ }
126
149
 
127
- // /** */
128
- // returnClickOnAttachmentButton(event: any) {
129
- // this.onClickAttachmentButton.emit(event)
130
- // }
150
+ // url_preview payload can live on message root OR inside metadata/attributes depending on the integration.
151
+ const urlPreviewLike =
152
+ this.message?.type === TYPE_MSG_URL_PREVIEW
153
+ || this.message?.metadata?.type === TYPE_MSG_URL_PREVIEW
154
+ || this.message?.attributes?.type === TYPE_MSG_URL_PREVIEW;
155
+ this.isUrlPreviewMessage = !!urlPreviewLike;
156
+ if (urlPreviewLike) this.loadJsonSourcesFromUrlPreviewMessage();
157
+ }
131
158
 
132
- onBeforeMessageRenderFN(event){
133
- const messageOBJ = { message: this.message, sanitizer: this.sanitizer, messageEl: event.messageEl, component: event.component}
134
- this.onBeforeMessageRender.emit(messageOBJ)
159
+ private async loadJsonSourcesFromUrlPreviewMessage(): Promise<void> {
160
+ // Protect the UI from out-of-order async responses when the input `message` changes quickly.
161
+ const reqId = ++this.urlPreviewReqId;
162
+ // 1) Parse-only, so the UI can render immediately (no url-preview calls).
163
+ const baseSources = this.jsonSourcesParser.parseBaseFromMessage(this.message);
164
+ this.jsonSources = baseSources;
165
+
166
+ // 2) Enrich in background via url-preview, then merge missing fields.
167
+ const enriched = await this.jsonSourcesParser.enrichSources(baseSources);
168
+ if (reqId !== this.urlPreviewReqId) return;
169
+ this.jsonSources = enriched;
135
170
  }
136
171
 
137
- onAfterMessageRenderFN(event){
138
- const messageOBJ = { message: this.message, sanitizer: this.sanitizer, messageEl: event.messageEl, component: event.component}
139
- this.onAfterMessageRender.emit(messageOBJ)
172
+ trackWord(_index: number, item: { word: string; index: number }): number {
173
+ return item.index;
140
174
  }
141
175
 
142
- onElementRenderedFN(event){
143
- this.onElementRendered.emit({element: event.element, status: event.status})
176
+ trackKaraokeWord(index: number): number {
177
+ return index;
144
178
  }
145
179
 
146
- // ========= END:: event emitter function ============//
180
+ onBeforeMessageRenderFN(event: any): void {
181
+ this.onBeforeMessageRender.emit({ message: this.message, sanitizer: this.sanitizer, messageEl: event.messageEl, component: event.component });
182
+ }
147
183
 
184
+ onAfterMessageRenderFN(event: any): void {
185
+ this.onAfterMessageRender.emit({ message: this.message, sanitizer: this.sanitizer, messageEl: event.messageEl, component: event.component });
186
+ }
148
187
 
188
+ onElementRenderedFN(event: any): void {
189
+ this.onElementRendered.emit({ element: event.element, status: event.status });
190
+ }
149
191
  }
@@ -1,8 +1,7 @@
1
- <div #actionButton id="actionButton" class="button-in-msg action"
1
+ <div #actionButton class="button-in-msg action action-button"
2
2
  [ngClass]="{'disabled': isConversationArchived}"
3
- (click)="actionButtonAction()"
4
- (mouseover)="onMouseOver($event)"
3
+ (click)="actionButtonAction()"
4
+ (mouseover)="onMouseOver($event)"
5
5
  (mouseout)="onMouseOut($event)">
6
6
  {{button?.value}}
7
7
  </div>
8
- <!-- title="{{button?.value}}" -->
@@ -1,4 +1,6 @@
1
- import { async, ComponentFixture, TestBed } from '@angular/core/testing';
1
+ import { SimpleChange } from '@angular/core';
2
+ import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing';
3
+ import { By } from '@angular/platform-browser';
2
4
 
3
5
  import { ActionButtonComponent } from './action-button.component';
4
6
 
@@ -6,20 +8,62 @@ describe('ActionButtonComponent', () => {
6
8
  let component: ActionButtonComponent;
7
9
  let fixture: ComponentFixture<ActionButtonComponent>;
8
10
 
9
- beforeEach(async(() => {
11
+ beforeEach(waitForAsync(() => {
10
12
  TestBed.configureTestingModule({
11
- declarations: [ ActionButtonComponent ]
12
- })
13
- .compileComponents();
13
+ declarations: [ActionButtonComponent],
14
+ }).compileComponents();
14
15
  }));
15
16
 
16
17
  beforeEach(() => {
17
18
  fixture = TestBed.createComponent(ActionButtonComponent);
18
19
  component = fixture.componentInstance;
20
+ component.button = { value: 'OK', action: 'go' };
19
21
  fixture.detectChanges();
20
22
  });
21
23
 
22
24
  it('should create', () => {
23
25
  expect(component).toBeTruthy();
24
26
  });
27
+
28
+ it('ngOnChanges should map theme CSS variables onto .action', () => {
29
+ component.fontSize = '16px';
30
+ component.backgroundColor = '#111';
31
+ component.textColor = '#222';
32
+ component.hoverBackgroundColor = '#333';
33
+ component.hoverTextColor = '#444';
34
+ component.ngOnChanges({
35
+ fontSize: new SimpleChange(null, '16px', true),
36
+ });
37
+ const el = fixture.nativeElement.querySelector('.action') as HTMLElement;
38
+ expect(el.style.getPropertyValue('--buttonFontSize').trim()).toBe('16px');
39
+ expect(el.style.getPropertyValue('--buttonBackgroundColor').trim()).toBeTruthy();
40
+ });
41
+
42
+ it('actionButtonAction should emit when action present', fakeAsync(() => {
43
+ spyOn(component.onButtonClicked, 'emit');
44
+ const de = fixture.debugElement.query(By.css('.action'));
45
+ de.triggerEventHandler('click', {});
46
+ tick(500);
47
+ expect(component.onButtonClicked.emit).toHaveBeenCalled();
48
+ }));
49
+
50
+ it('actionButtonAction should no-op when button has no action', () => {
51
+ component.button = { value: 'X', action: '' };
52
+ fixture.detectChanges();
53
+ spyOn(component.onButtonClicked, 'emit');
54
+ component.actionButtonAction();
55
+ expect(component.onButtonClicked.emit).not.toHaveBeenCalled();
56
+ });
57
+
58
+ it('mouseover and mouseout should not throw', () => {
59
+ expect(() => component.onMouseOver({} as any)).not.toThrow();
60
+ expect(() => component.onMouseOut({} as any)).not.toThrow();
61
+ });
62
+
63
+ it('template should mark archived conversations as disabled', () => {
64
+ component.isConversationArchived = true;
65
+ fixture.detectChanges();
66
+ const el = fixture.nativeElement.querySelector('.action');
67
+ expect(el.classList.contains('disabled')).toBe(true);
68
+ });
25
69
  });
@@ -42,20 +42,17 @@
42
42
  transition: background-color .6s ease;
43
43
  .icon-button-action {
44
44
  position: absolute;
45
- top: 50%;
46
- right: 8px;
47
- transform: translateY(-50%);
45
+ top: -1px;
46
+ right: 1px;
48
47
  svg {
49
- fill: var(--textColor);
48
+ fill: var(--buttonTextColor);
50
49
  }
51
50
  }
52
51
  .icon-button-action-self{
53
52
  position: absolute;
54
- top: 50%;
55
- right: 8px;
56
- transform: translateY(-50%);
53
+ right: 1px;
57
54
  svg {
58
- fill: var(--textColor);
55
+ fill: var(--buttonTextColor);
59
56
  }
60
57
  }
61
58
  &:focus,
@@ -1,4 +1,6 @@
1
- import { async, ComponentFixture, TestBed } from '@angular/core/testing';
1
+ import { SimpleChange } from '@angular/core';
2
+ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
3
+ import { By } from '@angular/platform-browser';
2
4
 
3
5
  import { LinkButtonComponent } from './link-button.component';
4
6
 
@@ -6,20 +8,63 @@ describe('LinkButtonComponent', () => {
6
8
  let component: LinkButtonComponent;
7
9
  let fixture: ComponentFixture<LinkButtonComponent>;
8
10
 
9
- beforeEach(async(() => {
11
+ beforeEach(waitForAsync(() => {
10
12
  TestBed.configureTestingModule({
11
- declarations: [ LinkButtonComponent ]
12
- })
13
- .compileComponents();
13
+ declarations: [LinkButtonComponent],
14
+ }).compileComponents();
14
15
  }));
15
16
 
16
17
  beforeEach(() => {
17
18
  fixture = TestBed.createComponent(LinkButtonComponent);
18
19
  component = fixture.componentInstance;
20
+ component.button = { value: 'Open', link: 'https://x.test', target: 'blank' };
19
21
  fixture.detectChanges();
20
22
  });
21
23
 
22
24
  it('should create', () => {
23
25
  expect(component).toBeTruthy();
24
26
  });
27
+
28
+ it('ngOnChanges should set CSS variables on .url', () => {
29
+ component.fontSize = '12px';
30
+ component.backgroundColor = '#aaa';
31
+ component.textColor = '#bbb';
32
+ component.hoverBackgroundColor = '#ccc';
33
+ component.hoverTextColor = '#ddd';
34
+ component.ngOnChanges({
35
+ fontSize: new SimpleChange(null, '12px', true),
36
+ });
37
+ const el = fixture.nativeElement.querySelector('.url') as HTMLElement;
38
+ expect(el.style.getPropertyValue('--buttonFontSize').trim()).toBe('12px');
39
+ });
40
+
41
+ it('actionButtonUrl should emit when link set', () => {
42
+ spyOn(component.onButtonClicked, 'emit');
43
+ component.actionButtonUrl();
44
+ expect(component.onButtonClicked.emit).toHaveBeenCalled();
45
+ });
46
+
47
+ it('actionButtonUrl should not emit when link empty', () => {
48
+ component.button = { value: 'x', link: '' };
49
+ spyOn(component.onButtonClicked, 'emit');
50
+ component.actionButtonUrl();
51
+ expect(component.onButtonClicked.emit).not.toHaveBeenCalled();
52
+ });
53
+
54
+ it('should render external icon when target is not self', () => {
55
+ component.button = { value: 'L', link: 'u', target: 'blank' };
56
+ fixture.detectChanges();
57
+ expect(fixture.debugElement.query(By.css('.icon-button-action'))).toBeTruthy();
58
+ });
59
+
60
+ it('should render self icon when target is self', () => {
61
+ component.button = { value: 'L', link: 'u', target: 'self' };
62
+ fixture.detectChanges();
63
+ expect(fixture.debugElement.query(By.css('.icon-button-action-self'))).toBeTruthy();
64
+ });
65
+
66
+ it('mouseover and mouseout should not throw', () => {
67
+ expect(() => component.onMouseOver({} as any)).not.toThrow();
68
+ expect(() => component.onMouseOut({} as any)).not.toThrow();
69
+ });
25
70
  });
@@ -1,4 +1,6 @@
1
- import { async, ComponentFixture, TestBed } from '@angular/core/testing';
1
+ import { SimpleChange } from '@angular/core';
2
+ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
3
+ import { By } from '@angular/platform-browser';
2
4
 
3
5
  import { TextButtonComponent } from './text-button.component';
4
6
 
@@ -6,20 +8,57 @@ describe('TextButtonComponent', () => {
6
8
  let component: TextButtonComponent;
7
9
  let fixture: ComponentFixture<TextButtonComponent>;
8
10
 
9
- beforeEach(async(() => {
11
+ beforeEach(waitForAsync(() => {
10
12
  TestBed.configureTestingModule({
11
- declarations: [ TextButtonComponent ]
12
- })
13
- .compileComponents();
13
+ declarations: [TextButtonComponent],
14
+ }).compileComponents();
14
15
  }));
15
16
 
16
17
  beforeEach(() => {
17
18
  fixture = TestBed.createComponent(TextButtonComponent);
18
19
  component = fixture.componentInstance;
20
+ component.button = { value: 'Reply' };
21
+ component.isConversationArchived = false;
19
22
  fixture.detectChanges();
20
23
  });
21
24
 
22
25
  it('should create', () => {
23
26
  expect(component).toBeTruthy();
24
27
  });
28
+
29
+ it('ngOnChanges should set CSS variables on .text', () => {
30
+ component.fontSize = '13px';
31
+ component.backgroundColor = '#111';
32
+ component.textColor = '#222';
33
+ component.hoverBackgroundColor = '#333';
34
+ component.hoverTextColor = '#444';
35
+ component.ngOnChanges({
36
+ fontSize: new SimpleChange(null, '13px', true),
37
+ });
38
+ const el = fixture.nativeElement.querySelector('.text') as HTMLElement;
39
+ expect(el.style.getPropertyValue('--buttonFontSize').trim()).toBe('13px');
40
+ });
41
+
42
+ it('actionButtonText should emit click payload', () => {
43
+ spyOn(component.onButtonClicked, 'emit');
44
+ component.actionButtonText();
45
+ expect(component.onButtonClicked.emit).toHaveBeenCalled();
46
+ });
47
+
48
+ it('click on template should invoke actionButtonText', () => {
49
+ spyOn(component, 'actionButtonText');
50
+ fixture.debugElement.query(By.css('.text')).triggerEventHandler('click', {});
51
+ expect(component.actionButtonText).toHaveBeenCalled();
52
+ });
53
+
54
+ it('should add disabled class when conversation archived', () => {
55
+ component.isConversationArchived = true;
56
+ fixture.detectChanges();
57
+ expect(fixture.nativeElement.querySelector('.text').classList.contains('disabled')).toBe(true);
58
+ });
59
+
60
+ it('mouseover and mouseout should not throw', () => {
61
+ expect(() => component.onMouseOver({} as any)).not.toThrow();
62
+ expect(() => component.onMouseOut({} as any)).not.toThrow();
63
+ });
25
64
  });
@@ -1,19 +1,28 @@
1
- <div class="wrapper">
2
- <div id="left" class="arrow left" (click)="goTo('previous')" *ngIf="activeElement > 1">
3
- <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
1
+ <div class="wrapper"
2
+ role="region"
3
+ aria-roledescription="carousel"
4
+ [attr.aria-label]="translationMap?.get('CAROUSEL_LABEL') || 'Cards carousel'">
5
+ <button type="button"
6
+ id="left"
7
+ class="arrow left c21-button-clean"
8
+ [attr.aria-label]="translationMap?.get('CAROUSEL_PREVIOUS') || 'Previous slide'"
9
+ (click)="goTo('previous')" *ngIf="activeElement > 1">
10
+ <svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
4
11
  <path d="M0 0h24v24H0V0z" fill="none"/><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12l4.58-4.59z"/>
5
12
  </svg>
6
- </div>
13
+ </button>
7
14
  <div class="carousel">
8
- <!-- <div class="card" style="width: 17px;"></div> -->
9
- <div class="card" *ngFor="let card of gallery; let i = index">
10
- <!-- <div [style.opacity]="i+1 === activeElement? 1: 0.5"> -->
15
+ <div class="card"
16
+ *ngFor="let card of gallery; let i = index"
17
+ role="group"
18
+ aria-roledescription="slide"
19
+ [attr.aria-label]="getSlideLabel(i + 1, gallery?.length)">
11
20
  <div>
12
21
  <div class="card-image" *ngIf="card?.preview?.src !== ''">
13
- <img [src]="card?.preview?.src" alt="img" draggable="false">
22
+ <img [src]="card?.preview?.src" [attr.alt]="card?.title || 'Carousel image'" draggable="false">
14
23
  </div>
15
24
  <div class="card-image card-image-placeholder" *ngIf="card?.preview?.src == ''">
16
- <img src="assets/images/icons/no-image.svg" alt="img" draggable="false">
25
+ <img src="assets/images/icons/no-image.svg" alt="" draggable="false">
17
26
  <span>Image not available</span>
18
27
  </div>
19
28
  <div class="card-content">
@@ -21,20 +30,24 @@
21
30
  <div class="card-description">{{card?.description}}</div>
22
31
  </div>
23
32
  <div class="buttons" *ngIf="card?.buttons && card?.buttons.length > 0">
24
- <div *ngFor="let button of card?.buttons"
25
- class="single-button action"
33
+ <button type="button" *ngFor="let button of card?.buttons"
34
+ class="single-button action c21-button-clean"
26
35
  [ngClass]="{'disabled': (isConversationArchived || (!isLastMessage && button.type !== TYPE_BUTTON.URL)), 'active': button?.active}"
36
+ [attr.aria-label]="button?.value"
27
37
  (click)="actionButtonClick($event, button, i)" >
28
38
  {{button.value}}
29
- </div>
39
+ </button>
30
40
  </div>
31
41
  </div>
32
42
  </div>
33
- <!-- <div class="card" style="width: 17px;"></div> -->
34
43
  </div>
35
- <div id="right" class="arrow right" (click)="goTo('next')" *ngIf="activeElement !== gallery.length">
36
- <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
44
+ <button type="button"
45
+ id="right"
46
+ class="arrow right c21-button-clean"
47
+ [attr.aria-label]="translationMap?.get('CAROUSEL_NEXT') || 'Next slide'"
48
+ (click)="goTo('next')" *ngIf="activeElement !== gallery.length">
49
+ <svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
37
50
  <path d="M0 0h24v24H0V0z" fill="none"/><path d="M10.02 6L8.61 7.41 13.19 12l-4.58 4.59L10.02 18l6-6-6-6z"/>
38
51
  </svg>
39
- </div>
52
+ </button>
40
53
  </div>