stream-chat-react-native-core 9.10.2 → 9.11.0-beta.3

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 (190) hide show
  1. package/lib/commonjs/ai-components/README.md +50 -2
  2. package/lib/commonjs/ai-components/index.js +24 -0
  3. package/lib/commonjs/ai-components/index.js.map +1 -1
  4. package/lib/commonjs/ai-components/message-composer/ComposerView.js +4 -2
  5. package/lib/commonjs/ai-components/message-composer/ComposerView.js.map +1 -1
  6. package/lib/commonjs/ai-components/message-composer/SpeechToTextButton.js +4 -1
  7. package/lib/commonjs/ai-components/message-composer/SpeechToTextButton.js.map +1 -1
  8. package/lib/commonjs/ai-components/native/AIDictation.js +6 -0
  9. package/lib/commonjs/ai-components/native/AIDictation.js.map +1 -1
  10. package/lib/commonjs/ai-components/store/dictation/store.js +4 -2
  11. package/lib/commonjs/ai-components/store/dictation/store.js.map +1 -1
  12. package/lib/commonjs/ai-components/store/dictation/useDictationState.js +4 -2
  13. package/lib/commonjs/ai-components/store/dictation/useDictationState.js.map +1 -1
  14. package/lib/commonjs/ai-components/store/index.js +22 -0
  15. package/lib/commonjs/ai-components/store/index.js.map +1 -1
  16. package/lib/commonjs/ai-components/theme.js +3 -0
  17. package/lib/commonjs/ai-components/theme.js.map +1 -1
  18. package/lib/commonjs/ai-components/transcription/useDictation.js +7 -3
  19. package/lib/commonjs/ai-components/transcription/useDictation.js.map +1 -1
  20. package/lib/commonjs/ai-components/transcription/useDictationAudioLevel.js +32 -0
  21. package/lib/commonjs/ai-components/transcription/useDictationAudioLevel.js.map +1 -0
  22. package/lib/commonjs/ai-components/voice-visualization/VoiceGlow.js +79 -0
  23. package/lib/commonjs/ai-components/voice-visualization/VoiceGlow.js.map +1 -0
  24. package/lib/commonjs/ai-components/voice-visualization/VoiceVisualizationView.js +147 -0
  25. package/lib/commonjs/ai-components/voice-visualization/VoiceVisualizationView.js.map +1 -0
  26. package/lib/commonjs/ai-components/voice-visualization/VoiceWave.js +58 -0
  27. package/lib/commonjs/ai-components/voice-visualization/VoiceWave.js.map +1 -0
  28. package/lib/commonjs/ai-components/voice-visualization/bounce.js +60 -0
  29. package/lib/commonjs/ai-components/voice-visualization/bounce.js.map +1 -0
  30. package/lib/commonjs/ai-components/voice-visualization/driver.js +208 -0
  31. package/lib/commonjs/ai-components/voice-visualization/driver.js.map +1 -0
  32. package/lib/commonjs/ai-components/voice-visualization/glowShader.js +218 -0
  33. package/lib/commonjs/ai-components/voice-visualization/glowShader.js.map +1 -0
  34. package/lib/commonjs/ai-components/voice-visualization/index.js +18 -0
  35. package/lib/commonjs/ai-components/voice-visualization/index.js.map +1 -0
  36. package/lib/commonjs/ai-components/voice-visualization/math.js +70 -0
  37. package/lib/commonjs/ai-components/voice-visualization/math.js.map +1 -0
  38. package/lib/commonjs/ai-components/voice-visualization/palette.js +47 -0
  39. package/lib/commonjs/ai-components/voice-visualization/palette.js.map +1 -0
  40. package/lib/commonjs/ai-components/voice-visualization/simplex.js +77 -0
  41. package/lib/commonjs/ai-components/voice-visualization/simplex.js.map +1 -0
  42. package/lib/commonjs/ai-components/voice-visualization/styles.js +33 -0
  43. package/lib/commonjs/ai-components/voice-visualization/styles.js.map +1 -0
  44. package/lib/commonjs/ai-components/voice-visualization/types.js +4 -0
  45. package/lib/commonjs/ai-components/voice-visualization/types.js.map +1 -0
  46. package/lib/commonjs/ai-components/voice-visualization/useReduceMotion.js +28 -0
  47. package/lib/commonjs/ai-components/voice-visualization/useReduceMotion.js.map +1 -0
  48. package/lib/commonjs/ai-components/voice-visualization/useVoiceVisualization.js +129 -0
  49. package/lib/commonjs/ai-components/voice-visualization/useVoiceVisualization.js.map +1 -0
  50. package/lib/commonjs/ai-components/voice-visualization/waveGeometry.js +124 -0
  51. package/lib/commonjs/ai-components/voice-visualization/waveGeometry.js.map +1 -0
  52. package/lib/commonjs/components/MessageInput/MessageComposer.js +2 -1
  53. package/lib/commonjs/components/MessageInput/MessageComposer.js.map +1 -1
  54. package/lib/commonjs/version.json +1 -1
  55. package/lib/module/ai-components/README.md +50 -2
  56. package/lib/module/ai-components/index.js +24 -0
  57. package/lib/module/ai-components/index.js.map +1 -1
  58. package/lib/module/ai-components/message-composer/ComposerView.js +4 -2
  59. package/lib/module/ai-components/message-composer/ComposerView.js.map +1 -1
  60. package/lib/module/ai-components/message-composer/SpeechToTextButton.js +4 -1
  61. package/lib/module/ai-components/message-composer/SpeechToTextButton.js.map +1 -1
  62. package/lib/module/ai-components/native/AIDictation.js +6 -0
  63. package/lib/module/ai-components/native/AIDictation.js.map +1 -1
  64. package/lib/module/ai-components/store/dictation/store.js +4 -2
  65. package/lib/module/ai-components/store/dictation/store.js.map +1 -1
  66. package/lib/module/ai-components/store/dictation/useDictationState.js +4 -2
  67. package/lib/module/ai-components/store/dictation/useDictationState.js.map +1 -1
  68. package/lib/module/ai-components/store/index.js +22 -0
  69. package/lib/module/ai-components/store/index.js.map +1 -1
  70. package/lib/module/ai-components/theme.js +3 -0
  71. package/lib/module/ai-components/theme.js.map +1 -1
  72. package/lib/module/ai-components/transcription/useDictation.js +7 -3
  73. package/lib/module/ai-components/transcription/useDictation.js.map +1 -1
  74. package/lib/module/ai-components/transcription/useDictationAudioLevel.js +32 -0
  75. package/lib/module/ai-components/transcription/useDictationAudioLevel.js.map +1 -0
  76. package/lib/module/ai-components/voice-visualization/VoiceGlow.js +79 -0
  77. package/lib/module/ai-components/voice-visualization/VoiceGlow.js.map +1 -0
  78. package/lib/module/ai-components/voice-visualization/VoiceVisualizationView.js +147 -0
  79. package/lib/module/ai-components/voice-visualization/VoiceVisualizationView.js.map +1 -0
  80. package/lib/module/ai-components/voice-visualization/VoiceWave.js +58 -0
  81. package/lib/module/ai-components/voice-visualization/VoiceWave.js.map +1 -0
  82. package/lib/module/ai-components/voice-visualization/bounce.js +60 -0
  83. package/lib/module/ai-components/voice-visualization/bounce.js.map +1 -0
  84. package/lib/module/ai-components/voice-visualization/driver.js +208 -0
  85. package/lib/module/ai-components/voice-visualization/driver.js.map +1 -0
  86. package/lib/module/ai-components/voice-visualization/glowShader.js +218 -0
  87. package/lib/module/ai-components/voice-visualization/glowShader.js.map +1 -0
  88. package/lib/module/ai-components/voice-visualization/index.js +18 -0
  89. package/lib/module/ai-components/voice-visualization/index.js.map +1 -0
  90. package/lib/module/ai-components/voice-visualization/math.js +70 -0
  91. package/lib/module/ai-components/voice-visualization/math.js.map +1 -0
  92. package/lib/module/ai-components/voice-visualization/palette.js +47 -0
  93. package/lib/module/ai-components/voice-visualization/palette.js.map +1 -0
  94. package/lib/module/ai-components/voice-visualization/simplex.js +77 -0
  95. package/lib/module/ai-components/voice-visualization/simplex.js.map +1 -0
  96. package/lib/module/ai-components/voice-visualization/styles.js +33 -0
  97. package/lib/module/ai-components/voice-visualization/styles.js.map +1 -0
  98. package/lib/module/ai-components/voice-visualization/types.js +4 -0
  99. package/lib/module/ai-components/voice-visualization/types.js.map +1 -0
  100. package/lib/module/ai-components/voice-visualization/useReduceMotion.js +28 -0
  101. package/lib/module/ai-components/voice-visualization/useReduceMotion.js.map +1 -0
  102. package/lib/module/ai-components/voice-visualization/useVoiceVisualization.js +129 -0
  103. package/lib/module/ai-components/voice-visualization/useVoiceVisualization.js.map +1 -0
  104. package/lib/module/ai-components/voice-visualization/waveGeometry.js +124 -0
  105. package/lib/module/ai-components/voice-visualization/waveGeometry.js.map +1 -0
  106. package/lib/module/components/MessageInput/MessageComposer.js +2 -1
  107. package/lib/module/components/MessageInput/MessageComposer.js.map +1 -1
  108. package/lib/module/version.json +1 -1
  109. package/lib/typescript/ai-components/index.d.ts +3 -1
  110. package/lib/typescript/ai-components/index.d.ts.map +1 -1
  111. package/lib/typescript/ai-components/message-composer/ComposerView.d.ts.map +1 -1
  112. package/lib/typescript/ai-components/native/AIDictation.d.ts +6 -0
  113. package/lib/typescript/ai-components/native/AIDictation.d.ts.map +1 -1
  114. package/lib/typescript/ai-components/store/dictation/store.d.ts +7 -0
  115. package/lib/typescript/ai-components/store/dictation/store.d.ts.map +1 -1
  116. package/lib/typescript/ai-components/store/dictation/useDictationState.d.ts +1 -0
  117. package/lib/typescript/ai-components/store/dictation/useDictationState.d.ts.map +1 -1
  118. package/lib/typescript/ai-components/store/index.d.ts +2 -0
  119. package/lib/typescript/ai-components/store/index.d.ts.map +1 -1
  120. package/lib/typescript/ai-components/theme.d.ts +13 -0
  121. package/lib/typescript/ai-components/theme.d.ts.map +1 -1
  122. package/lib/typescript/ai-components/transcription/useDictation.d.ts +9 -1
  123. package/lib/typescript/ai-components/transcription/useDictation.d.ts.map +1 -1
  124. package/lib/typescript/ai-components/transcription/useDictationAudioLevel.d.ts +11 -0
  125. package/lib/typescript/ai-components/transcription/useDictationAudioLevel.d.ts.map +1 -0
  126. package/lib/typescript/ai-components/voice-visualization/VoiceGlow.d.ts +13 -0
  127. package/lib/typescript/ai-components/voice-visualization/VoiceGlow.d.ts.map +1 -0
  128. package/lib/typescript/ai-components/voice-visualization/VoiceVisualizationView.d.ts +26 -0
  129. package/lib/typescript/ai-components/voice-visualization/VoiceVisualizationView.d.ts.map +1 -0
  130. package/lib/typescript/ai-components/voice-visualization/VoiceWave.d.ts +13 -0
  131. package/lib/typescript/ai-components/voice-visualization/VoiceWave.d.ts.map +1 -0
  132. package/lib/typescript/ai-components/voice-visualization/bounce.d.ts +20 -0
  133. package/lib/typescript/ai-components/voice-visualization/bounce.d.ts.map +1 -0
  134. package/lib/typescript/ai-components/voice-visualization/driver.d.ts +96 -0
  135. package/lib/typescript/ai-components/voice-visualization/driver.d.ts.map +1 -0
  136. package/lib/typescript/ai-components/voice-visualization/glowShader.d.ts +7 -0
  137. package/lib/typescript/ai-components/voice-visualization/glowShader.d.ts.map +1 -0
  138. package/lib/typescript/ai-components/voice-visualization/index.d.ts +4 -0
  139. package/lib/typescript/ai-components/voice-visualization/index.d.ts.map +1 -0
  140. package/lib/typescript/ai-components/voice-visualization/math.d.ts +26 -0
  141. package/lib/typescript/ai-components/voice-visualization/math.d.ts.map +1 -0
  142. package/lib/typescript/ai-components/voice-visualization/palette.d.ts +18 -0
  143. package/lib/typescript/ai-components/voice-visualization/palette.d.ts.map +1 -0
  144. package/lib/typescript/ai-components/voice-visualization/simplex.d.ts +11 -0
  145. package/lib/typescript/ai-components/voice-visualization/simplex.d.ts.map +1 -0
  146. package/lib/typescript/ai-components/voice-visualization/styles.d.ts +24 -0
  147. package/lib/typescript/ai-components/voice-visualization/styles.d.ts.map +1 -0
  148. package/lib/typescript/ai-components/voice-visualization/types.d.ts +45 -0
  149. package/lib/typescript/ai-components/voice-visualization/types.d.ts.map +1 -0
  150. package/lib/typescript/ai-components/voice-visualization/useReduceMotion.d.ts +10 -0
  151. package/lib/typescript/ai-components/voice-visualization/useReduceMotion.d.ts.map +1 -0
  152. package/lib/typescript/ai-components/voice-visualization/useVoiceVisualization.d.ts +18 -0
  153. package/lib/typescript/ai-components/voice-visualization/useVoiceVisualization.d.ts.map +1 -0
  154. package/lib/typescript/ai-components/voice-visualization/waveGeometry.d.ts +25 -0
  155. package/lib/typescript/ai-components/voice-visualization/waveGeometry.d.ts.map +1 -0
  156. package/lib/typescript/components/MessageInput/MessageComposer.d.ts.map +1 -1
  157. package/package.json +2 -2
  158. package/src/ai-components/README.md +50 -2
  159. package/src/ai-components/__tests__/VoiceVisualization.test.tsx +208 -0
  160. package/src/ai-components/__tests__/dictation.test.tsx +58 -1
  161. package/src/ai-components/__tests__/voiceDriver.test.ts +317 -0
  162. package/src/ai-components/__tests__/voicePalette.test.ts +124 -0
  163. package/src/ai-components/index.ts +3 -1
  164. package/src/ai-components/message-composer/ComposerView.tsx +6 -3
  165. package/src/ai-components/message-composer/SpeechToTextButton.tsx +5 -2
  166. package/src/ai-components/native/AIDictation.ts +11 -0
  167. package/src/ai-components/store/dictation/store.ts +9 -0
  168. package/src/ai-components/store/dictation/useDictationState.ts +2 -1
  169. package/src/ai-components/store/index.ts +2 -0
  170. package/src/ai-components/theme.ts +16 -0
  171. package/src/ai-components/transcription/useDictation.ts +16 -3
  172. package/src/ai-components/transcription/useDictationAudioLevel.ts +38 -0
  173. package/src/ai-components/voice-visualization/VoiceGlow.tsx +79 -0
  174. package/src/ai-components/voice-visualization/VoiceVisualizationView.tsx +179 -0
  175. package/src/ai-components/voice-visualization/VoiceWave.tsx +67 -0
  176. package/src/ai-components/voice-visualization/bounce.ts +61 -0
  177. package/src/ai-components/voice-visualization/driver.ts +240 -0
  178. package/src/ai-components/voice-visualization/glowShader.ts +227 -0
  179. package/src/ai-components/voice-visualization/index.ts +3 -0
  180. package/src/ai-components/voice-visualization/math.ts +73 -0
  181. package/src/ai-components/voice-visualization/palette.ts +59 -0
  182. package/src/ai-components/voice-visualization/simplex.ts +92 -0
  183. package/src/ai-components/voice-visualization/styles.ts +42 -0
  184. package/src/ai-components/voice-visualization/types.ts +44 -0
  185. package/src/ai-components/voice-visualization/useReduceMotion.ts +34 -0
  186. package/src/ai-components/voice-visualization/useVoiceVisualization.ts +153 -0
  187. package/src/ai-components/voice-visualization/waveGeometry.ts +137 -0
  188. package/src/components/ChannelPreview/hooks/__tests__/useChannelPreviewPollLabel.test.tsx +1 -1
  189. package/src/components/MessageInput/MessageComposer.tsx +4 -2
  190. package/src/version.json +1 -1
@@ -0,0 +1,24 @@
1
+ import { type LevelResponse } from './driver';
2
+ /**
3
+ * What tells the glow and the wave apart besides how they draw, as on iOS.
4
+ *
5
+ * - `levelResponse`: the wave's height tracks the level directly, so it follows it smoothly;
6
+ * per-syllable jumps would make it jitter. The glow follows it closely.
7
+ * - `maxScale`: the densest it renders. The glow is soft all over, so 2x is enough; the wave's
8
+ * thin line is drawn at the screen's own scale, up to 3x, to stay crisp.
9
+ * - `contains`: whether a point, centred and normalized to the shorter side with y up, is on the
10
+ * shape rather than the empty space around it.
11
+ */
12
+ export type VoiceVisualizationStyle = {
13
+ contains: (x: number, y: number) => boolean;
14
+ /** The shape's bounds as a share of the shorter side, for the area that takes touches. */
15
+ hitArea: {
16
+ width: number;
17
+ height: number;
18
+ };
19
+ levelResponse: LevelResponse;
20
+ maxScale: number;
21
+ };
22
+ export declare const GLOW_STYLE: VoiceVisualizationStyle;
23
+ export declare const WAVE_STYLE: VoiceVisualizationStyle;
24
+ //# sourceMappingURL=styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../../../src/ai-components/voice-visualization/styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9D;;;;;;;;;GASG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5C,0FAA0F;IAC1F,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,aAAa,EAAE,aAAa,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAKF,eAAO,MAAM,UAAU,EAAE,uBAQxB,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,uBAQxB,CAAC"}
@@ -0,0 +1,45 @@
1
+ import type { ViewProps } from 'react-native';
2
+ import type { SharedValue } from 'react-native-reanimated';
3
+ /**
4
+ * What a voice agent is doing, which a voice visualization shows as motion.
5
+ *
6
+ * The states differ only in how a visualization moves, never in its colors, and changing the
7
+ * state eases from one motion into the next.
8
+ *
9
+ * - `idle`: waiting to be spoken to, a calm, barely-there shimmer.
10
+ * - `connecting`: not ready yet, a loader sweep with a pulsing breath.
11
+ * - `listening`: hearing somebody, a gentle expand and contract.
12
+ * - `thinking`: working on an answer, a slow turn or a travelling wave.
13
+ * - `speaking`: talking, strong motion that ebbs and pauses like speech.
14
+ */
15
+ export type VoiceAgentState = 'idle' | 'connecting' | 'listening' | 'thinking' | 'speaking';
16
+ export type VoiceVisualizationProps = Omit<ViewProps, 'children'> & {
17
+ /** What the agent is doing, which sets how the visualization moves. */
18
+ state: VoiceAgentState;
19
+ /**
20
+ * The level of whoever is talking, from 0 to 1, read once a frame on the UI thread. Feed it at
21
+ * 15 Hz or more for the visualization to move with the words; `null` animates speech on its own.
22
+ * See `useDictationAudioLevel` for the level of the microphone while dictating.
23
+ */
24
+ audioLevel?: SharedValue<number | null>;
25
+ /**
26
+ * One to five colors. Past five are ignored, and colors that cannot be read are skipped. When
27
+ * omitted or empty, the theme's `voiceVisualization.colors` are used.
28
+ */
29
+ colors?: string[];
30
+ /** From 0 to 2: how far and how fast it moves. Colors never change with it. Defaults to 1. */
31
+ expressivity?: number;
32
+ /**
33
+ * What screen readers read as the value for each state, for example to translate them. Defaults
34
+ * to "Ready", "Connecting", "Listening", "Thinking" and "Speaking". The label, "Voice agent", is
35
+ * `accessibilityLabel`.
36
+ */
37
+ stateLabels?: Partial<Record<VoiceAgentState, string>>;
38
+ };
39
+ /** A color as hue (a fraction of a turn), saturation and value, each from 0 to 1. */
40
+ export type Hsv = {
41
+ hue: number;
42
+ saturation: number;
43
+ value: number;
44
+ };
45
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/ai-components/voice-visualization/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,YAAY,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,CAAC;AAE5F,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,GAAG;IAClE,uEAAuE;IACvE,KAAK,EAAE,eAAe,CAAC;IACvB;;;;OAIG;IACH,UAAU,CAAC,EAAE,WAAW,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,8FAA8F;IAC9F,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC;CACxD,CAAC;AAEF,qFAAqF;AACrF,MAAM,MAAM,GAAG,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Whether the system asks for less motion: Reduce Motion on iOS, animations off on Android. It
3
+ * follows the setting while mounted.
4
+ *
5
+ * The AI entry point cannot reach the core SDK's own hook, which also only applies once its
6
+ * accessibility layer is enabled; the voice visualizations always honour the setting, as on iOS
7
+ * and Android.
8
+ */
9
+ export declare const useReduceMotion: () => boolean;
10
+ //# sourceMappingURL=useReduceMotion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useReduceMotion.d.ts","sourceRoot":"","sources":["../../../../src/ai-components/voice-visualization/useReduceMotion.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,eAsB3B,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { VoiceVisualizationStyle } from './styles';
2
+ import type { VoiceVisualizationProps } from './types';
3
+ /**
4
+ * A voice visualization's motion, advanced once a frame on the UI thread.
5
+ *
6
+ * The driver lives in a shared value the renderers derive from, so a frame redraws without
7
+ * re-rendering React, and `audioLevel` is read in the frame callback, so a level changing many
8
+ * times a second re-renders nothing either.
9
+ */
10
+ export declare const useVoiceVisualization: ({ audioLevel, colors, expressivity, state }: VoiceVisualizationProps, style: VoiceVisualizationStyle) => {
11
+ dark: import("react-native-reanimated").SharedValue<number>;
12
+ driver: import("react-native-reanimated").SharedValue<import("./driver").VoiceDriverState>;
13
+ hover: (x: number | null, y: number | null) => void;
14
+ opacity: import("react-native-reanimated").SharedValue<number>;
15
+ tap: (x: number, y: number) => void;
16
+ };
17
+ export type VoiceVisualization = ReturnType<typeof useVoiceVisualization>;
18
+ //# sourceMappingURL=useVoiceVisualization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useVoiceVisualization.d.ts","sourceRoot":"","sources":["../../../../src/ai-components/voice-visualization/useVoiceVisualization.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAC;AACxD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAkBvD;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,GAChC,6CAA6C,uBAAuB,EACpE,OAAO,uBAAuB;;;eAwFxB,MAAM,GAAG,IAAI,KAAK,MAAM,GAAG,IAAI;;aAf/B,MAAM,KAAK,MAAM;CA6BxB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { type SkPath } from '@shopify/react-native-skia';
2
+ import { type VoiceDriverState } from './driver';
3
+ export declare const WAVE_STOPS = 13;
4
+ export declare const WAVE_START = -0.42;
5
+ export declare const WAVE_END = 0.42;
6
+ /** The stops' positions along the gradient, evenly spaced. */
7
+ export declare const WAVE_POSITIONS: number[];
8
+ /** The string's height above its centre line at each point, centred and normalized, y up. */
9
+ export declare const waveHeights: (d: VoiceDriverState) => {
10
+ xs: number[];
11
+ ys: number[];
12
+ };
13
+ /**
14
+ * The wave as a path in a `width` by `height` canvas. Newer Skia versions build immutable paths with
15
+ * `PathBuilder`; earlier 2.x versions mutate `SkPath` itself.
16
+ */
17
+ export declare const wavePath: (d: VoiceDriverState, width: number, height: number) => SkPath;
18
+ /**
19
+ * The string's colors at its gradient stops, as RGBA from 0 to 1: up to five bands drifting along
20
+ * it, weighed by how near their centres are, its ends fading out.
21
+ */
22
+ export declare const waveColors: (d: VoiceDriverState, dark: number) => Float32Array<ArrayBufferLike>[];
23
+ /** The line is 0.6% of the shorter side, and never under 1.5 device pixels, as on Android and iOS. */
24
+ export declare const waveStrokeWidth: (width: number, height: number, pixelRatio: number) => number;
25
+ //# sourceMappingURL=waveGeometry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"waveGeometry.d.ts","sourceRoot":"","sources":["../../../../src/ai-components/voice-visualization/waveGeometry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAE/D,OAAO,EAA4B,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAI3E,eAAO,MAAM,UAAU,KAAK,CAAC;AAC7B,eAAO,MAAM,UAAU,QAAQ,CAAC;AAChC,eAAO,MAAM,QAAQ,OAAO,CAAC;AAE7B,8DAA8D;AAC9D,eAAO,MAAM,cAAc,UAAqE,CAAC;AAuBjG,6FAA6F;AAC7F,eAAO,MAAM,WAAW,GAAI,GAAG,gBAAgB;;;CAkC9C,CAAC;AAOF;;;GAGG;AACH,eAAO,MAAM,QAAQ,GAAI,GAAG,gBAAgB,EAAE,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAG,MAiB7E,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,GAAG,gBAAgB,EAAE,MAAM,MAAM,oCA6B3D,CAAC;AAEF,sGAAsG;AACtG,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,EAAE,QAAQ,MAAM,EAAE,YAAY,MAAM,WACpB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"MessageComposer.d.ts","sourceRoot":"","sources":["../../../../src/components/MessageInput/MessageComposer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6B,MAAM,OAAO,CAAC;AAoBlD,OAAO,EACL,gBAAgB,EAIjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,mBAAmB,EAEpB,MAAM,8CAA8C,CAAC;AAEtD,OAAO,EACL,8BAA8B,EAE/B,MAAM,iEAAiE,CAAC;AAEzE,OAAO,EACL,wBAAwB,EAEzB,MAAM,wDAAwD,CAAC;AAGhE,OAAO,EACL,uBAAuB,EAExB,MAAM,sDAAsD,CAAC;AAK9D,OAAO,EAAE,yBAAyB,EAAE,MAAM,0CAA0C,CAAC;AAIrF,OAAO,EAAE,KAAK,0BAA0B,EAAE,MAAM,wCAAwC,CAAC;AAqFzF,KAAK,+BAA+B,GAAG,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,GACvE,IAAI,CAAC,mBAAmB,EAAE,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC,GAC7D,IAAI,CACF,wBAAwB,EACtB,sBAAsB,GACtB,0BAA0B,GAC1B,8BAA8B,GAC9B,uBAAuB,GACvB,2BAA2B,GAC3B,mCAAmC,GACnC,oCAAoC,GACpC,uBAAuB,GACvB,sBAAsB,GACtB,aAAa,GACb,sBAAsB,GACtB,yBAAyB,GACzB,eAAe,GACf,wBAAwB,GACxB,yBAAyB,GACzB,wBAAwB,GACxB,aAAa,GACb,qBAAqB,CACxB,GACD,IAAI,CAAC,uBAAuB,EAAE,GAAG,CAAC,GAClC,IAAI,CAAC,8BAA8B,EAAE,mBAAmB,CAAC,GACzD,IAAI,CAAC,yBAAyB,EAAE,WAAW,CAAC,GAAG;IAC7C,OAAO,EAAE,OAAO,CAAC;IACjB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,kBAAkB,CAAC,EAAE,0BAA0B,CAAC;IAChD,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AA4aJ,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,+BAA+B,CAAC,CAAC;AAE5E;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;YAAW,oBAAoB;;CAkF1D,CAAC"}
1
+ {"version":3,"file":"MessageComposer.d.ts","sourceRoot":"","sources":["../../../../src/components/MessageInput/MessageComposer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6B,MAAM,OAAO,CAAC;AAoBlD,OAAO,EACL,gBAAgB,EAIjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,mBAAmB,EAEpB,MAAM,8CAA8C,CAAC;AAEtD,OAAO,EACL,8BAA8B,EAE/B,MAAM,iEAAiE,CAAC;AAEzE,OAAO,EACL,wBAAwB,EAEzB,MAAM,wDAAwD,CAAC;AAGhE,OAAO,EACL,uBAAuB,EAExB,MAAM,sDAAsD,CAAC;AAK9D,OAAO,EAAE,yBAAyB,EAAE,MAAM,0CAA0C,CAAC;AAIrF,OAAO,EAAE,KAAK,0BAA0B,EAAE,MAAM,wCAAwC,CAAC;AAqFzF,KAAK,+BAA+B,GAAG,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,GACvE,IAAI,CAAC,mBAAmB,EAAE,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC,GAC7D,IAAI,CACF,wBAAwB,EACtB,sBAAsB,GACtB,0BAA0B,GAC1B,8BAA8B,GAC9B,uBAAuB,GACvB,2BAA2B,GAC3B,mCAAmC,GACnC,oCAAoC,GACpC,uBAAuB,GACvB,sBAAsB,GACtB,aAAa,GACb,sBAAsB,GACtB,yBAAyB,GACzB,eAAe,GACf,wBAAwB,GACxB,yBAAyB,GACzB,wBAAwB,GACxB,aAAa,GACb,qBAAqB,CACxB,GACD,IAAI,CAAC,uBAAuB,EAAE,GAAG,CAAC,GAClC,IAAI,CAAC,8BAA8B,EAAE,mBAAmB,CAAC,GACzD,IAAI,CAAC,yBAAyB,EAAE,WAAW,CAAC,GAAG;IAC7C,OAAO,EAAE,OAAO,CAAC;IACjB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,kBAAkB,CAAC,EAAE,0BAA0B,CAAC;IAChD,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AA8aJ,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,+BAA+B,CAAC,CAAC;AAE5E;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;YAAW,oBAAoB;;CAkF1D,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stream-chat-react-native-core",
3
3
  "description": "The official React Native and Expo components for Stream Chat, a service for building chat applications",
4
- "version": "9.10.2",
4
+ "version": "9.11.0-beta.3",
5
5
  "author": {
6
6
  "company": "Stream.io Inc",
7
7
  "name": "Stream.io Inc"
@@ -84,7 +84,7 @@
84
84
  "react-native-markdown-package": "1.8.2",
85
85
  "react-native-url-polyfill": "^2.0.0",
86
86
  "react-syntax-highlighter": "^16.1.0",
87
- "stream-chat": "^9.54.1",
87
+ "stream-chat": "^9.54.2",
88
88
  "use-sync-external-store": "^1.5.0",
89
89
  "zod": "^4.1.12"
90
90
  },
@@ -22,12 +22,12 @@ These components used to ship as the standalone `@stream-io/chat-react-native-ai
22
22
 
23
23
  2. Dictation is now opt-in on iOS (see [Dictation](#dictation-speech-to-text)): add `$StreamChatEnableDictation = true` to your `Podfile`, or, with Expo, replace the `@stream-io/chat-react-native-ai` config plugin with `stream-chat-expo`.
24
24
  3. Keep the `export-namespace-from` Babel plugin (see [Installation](#️-installation)); it is needed by the `zod` dependency.
25
- 4. `ShimmeringView` and `toHexFromRNColor` were removed. `AITypingIndicatorView` now uses the SDK's native shimmer, so `@shopify/react-native-skia` is only needed for charts.
25
+ 4. `ShimmeringView` and `toHexFromRNColor` were removed. `AITypingIndicatorView` now uses the SDK's native shimmer, so `@shopify/react-native-skia` is only needed for charts and the voice visualizations.
26
26
  5. `SpeechToTextButton` and `isDictationAvailable` are now exported.
27
27
 
28
28
  ## 🛠️ Installation
29
29
 
30
- The AI components ship with `stream-chat-react-native` and `stream-chat-expo`; their JavaScript dependencies are installed with them. Charts additionally need the following peer dependencies, which only apps using the AI components have to install:
30
+ The AI components ship with `stream-chat-react-native` and `stream-chat-expo`; their JavaScript dependencies are installed with them. Charts and the voice visualizations (`VoiceGlow`, `VoiceWave`) additionally need the following peer dependencies, which only apps using the AI components have to install:
31
31
 
32
32
  ```bash
33
33
  yarn add victory-native @shopify/react-native-skia
@@ -244,6 +244,54 @@ const options = {
244
244
 
245
245
  The `SpeechToTextButton` is already integrated within the `ComposerView`, however feel free to use it elsewhere as well. It renders nothing when dictation is not part of the build (see [Dictation](#dictation-speech-to-text)); `isDictationAvailable()` tells you whether it is.
246
246
 
247
+ ### `VoiceGlow` and `VoiceWave`
248
+
249
+ Two visualizations of a voice agent, alike on iOS, Android, the web and React Native:
250
+
251
+ - `VoiceGlow`: a soft, airy globe whose color masses orbit inside it and swell with speech. It is drawn by a Skia shader, every pixel on the GPU.
252
+ - `VoiceWave`: a single string carrying a wave packet that erupts with speech, its colors flowing along it.
253
+
254
+ Neither has a size of its own: each draws in the middle of the view it is given, the glow about half as wide as its shorter side and the wave about 80% as wide, so give the glow a square and the wave a square or a wide box. Both take the same props, and any other `View` prop goes to their root:
255
+
256
+ | Name | Type | Required | Description |
257
+ | -------------- | ------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
258
+ | `state` | `'idle' \| 'connecting' \| 'listening' \| 'thinking' \| 'speaking'` | yes | What the agent is doing. The states differ only in how it moves, never in its colors, and changing the state eases from one motion into the next. |
259
+ | `audioLevel` | `SharedValue<number \| null>` | no | The level of whoever is talking, from 0 to 1, read once a frame on the UI thread. Feed it at 15 Hz or more for the motion to follow the words. While it is `null` or absent, speech animates on its own. |
260
+ | `colors` | `string[]` | no | One to five colors. Past five are ignored, and colors that cannot be read are skipped. Defaults to the theme's `voiceVisualization.colors`. |
261
+ | `expressivity` | `number` | no | From 0 to 2: how far and how fast it moves. Colors never change with it. Defaults to `1`. |
262
+ | `stateLabels` | `Partial<Record<VoiceAgentState, string>>` | no | What screen readers read as the value for each state, for example to translate them. Defaults to "Ready", "Connecting", "Listening", "Thinking" and "Speaking". |
263
+
264
+ ```tsx
265
+ import { useDictationAudioLevel, VoiceGlow, VoiceWave } from 'stream-chat-react-native/ai-components';
266
+
267
+ const VoiceAgent = ({ agentState, isListening }) => {
268
+ const micLevel = useDictationAudioLevel();
269
+
270
+ return (
271
+ <>
272
+ <VoiceGlow state={agentState} style={{ width: 320, height: 320 }} />
273
+ <VoiceWave
274
+ audioLevel={micLevel}
275
+ state={isListening ? 'listening' : 'idle'}
276
+ style={{ width: '100%', height: 120 }}
277
+ />
278
+ </>
279
+ );
280
+ };
281
+ ```
282
+
283
+ The motion runs entirely on the UI thread with Reanimated and Skia, so the visualizations stay smooth while the JS thread is busy streaming a reply, and a changing `audioLevel` re-renders nothing.
284
+
285
+ **Audio level.** `useDictationAudioLevel()` gives the microphone's level while `useDictation` (or the `SpeechToTextButton`) is listening, and `null` otherwise. For any other source, such as a remote participant's audio level from Stream Video, keep a shared value of your own and write to it: `const level = useSharedValue<number | null>(null)`, then `level.value = participant.audioLevel`.
286
+
287
+ **Interaction.** Tapping the shape bounces it and sends out a ripple, and a hovering mouse or pen ripples it too. Touches are only observed, never claimed, so a `Pressable` around the visualization still gets its press; touches beside the shape reach whatever is behind it.
288
+
289
+ **Theming.** The theme's `voiceVisualization.colors` are the default palette (the accent color unless you set them). The visualizations draw for a light or a dark background following `voiceVisualization.backdropColor` when you set it, and the system color scheme otherwise.
290
+
291
+ **Accessibility.** Each is an image named "Voice agent" with its state as the value ("Ready", "Connecting", "Listening", "Thinking", "Speaking"), as on iOS and Android. Pass `stateLabels` to translate the state values, `accessibilityLabel` to change the label, or `accessibilityValue` to replace the value outright. Under the system's Reduce Motion (animations off on Android), they barely move and ignore taps.
292
+
293
+ **Differences from the native SDKs.** The glow has no `rendering` option: the shader runs on every Android version through Skia, so there is no Canvas fallback, and it does not switch to a cheaper look in battery saver as on Android. A visualization keeps animating while it is mounted, even when scrolled off screen; it pauses only while the app is in the background.
294
+
247
295
  ### Theming
248
296
 
249
297
  Each one of the components in the SDK is fully theme-compatible. The `StreamTheme` provider takes care of this for you.
@@ -0,0 +1,208 @@
1
+ import React from 'react';
2
+ import { AccessibilityInfo, Pressable } from 'react-native';
3
+
4
+ import { act, fireEvent, render, renderHook, screen } from '@testing-library/react-native';
5
+
6
+ import { type DeepPartial, StreamTheme, type Theme, VoiceGlow, VoiceWave } from '../index';
7
+ import { rgbToHsv } from '../voice-visualization/palette';
8
+ import { GLOW_STYLE, WAVE_STYLE } from '../voice-visualization/styles';
9
+ import type { VoiceVisualizationProps } from '../voice-visualization/types';
10
+ import { useVoiceVisualization } from '../voice-visualization/useVoiceVisualization';
11
+ import { waveColors, wavePath } from '../voice-visualization/waveGeometry';
12
+
13
+ const themed =
14
+ (style?: DeepPartial<Theme>) =>
15
+ ({ children }: { children: React.ReactNode }) => (
16
+ <StreamTheme style={style}>{children}</StreamTheme>
17
+ );
18
+
19
+ const layout = (width: number, height: number) => ({
20
+ nativeEvent: { layout: { height, width, x: 0, y: 0 } },
21
+ });
22
+
23
+ const renderVisualization = (
24
+ props: Partial<VoiceVisualizationProps> = {},
25
+ style?: DeepPartial<Theme>,
26
+ visualStyle = GLOW_STYLE,
27
+ ) =>
28
+ renderHook(
29
+ (hookProps: VoiceVisualizationProps) => useVoiceVisualization(hookProps, visualStyle),
30
+ { initialProps: { state: 'idle', ...props }, wrapper: themed(style) },
31
+ );
32
+
33
+ const hueOf = (color: [number, number, number]) => rgbToHsv(color).hue;
34
+
35
+ describe('VoiceGlow and VoiceWave', () => {
36
+ afterEach(() => jest.restoreAllMocks());
37
+
38
+ it.each([
39
+ ['VoiceGlow', VoiceGlow],
40
+ ['VoiceWave', VoiceWave],
41
+ ])('%s names itself and its state, as on iOS and Android', (_, Component) => {
42
+ const { rerender } = render(<Component state='idle' />, { wrapper: themed() });
43
+
44
+ const root = screen.getByLabelText('Voice agent');
45
+ expect(root.props.accessibilityRole).toBe('image');
46
+ expect(root.props.accessibilityValue).toEqual({ text: 'Ready' });
47
+
48
+ rerender(<Component state='speaking' />);
49
+ expect(screen.getByLabelText('Voice agent').props.accessibilityValue).toEqual({
50
+ text: 'Speaking',
51
+ });
52
+ });
53
+
54
+ it('takes a label and value of its own', () => {
55
+ render(
56
+ <VoiceGlow
57
+ accessibilityLabel='Assistant'
58
+ accessibilityValue={{ text: 'Talking' }}
59
+ state='speaking'
60
+ />,
61
+ { wrapper: themed() },
62
+ );
63
+
64
+ expect(screen.getByLabelText('Assistant').props.accessibilityValue).toEqual({
65
+ text: 'Talking',
66
+ });
67
+ });
68
+
69
+ it.each([
70
+ ['VoiceGlow', VoiceGlow],
71
+ ['VoiceWave', VoiceWave],
72
+ ])('%s draws once it has a size', (_, Component) => {
73
+ render(<Component state='thinking' />, { wrapper: themed() });
74
+ expect(screen.queryByTestId('skia-canvas')).toBeNull();
75
+
76
+ fireEvent(screen.getByLabelText('Voice agent'), 'layout', layout(300, 200));
77
+
78
+ expect(screen.getByTestId('skia-canvas')).toBeTruthy();
79
+ expect(screen.getByTestId('voice-visualization-hit-area')).toBeTruthy();
80
+ });
81
+
82
+ it('lets a press on the shape reach a parent', () => {
83
+ const onPress = jest.fn();
84
+ render(
85
+ <Pressable onPress={onPress}>
86
+ <VoiceGlow state='idle' />
87
+ </Pressable>,
88
+ { wrapper: themed() },
89
+ );
90
+ fireEvent(screen.getByLabelText('Voice agent'), 'layout', layout(300, 300));
91
+
92
+ const hitArea = screen.getByTestId('voice-visualization-hit-area');
93
+ fireEvent(hitArea, 'touchEnd', { nativeEvent: { locationX: 96, locationY: 96 } });
94
+ fireEvent.press(hitArea);
95
+
96
+ expect(onPress).toHaveBeenCalledTimes(1);
97
+ });
98
+
99
+ it('compiles the glow shader once for every glow', () => {
100
+ // A fresh copy of the module, so a shader compiled by an earlier test doesn't hide a miss.
101
+ jest.isolateModules(() => {
102
+ const { Skia: freshSkia } = require('@shopify/react-native-skia');
103
+ const { getGlowEffect } = require('../voice-visualization/glowShader');
104
+ const make = jest.spyOn(freshSkia.RuntimeEffect, 'Make');
105
+
106
+ const first = getGlowEffect();
107
+ const second = getGlowEffect();
108
+
109
+ expect(make).toHaveBeenCalledTimes(1);
110
+ expect(second).toBe(first);
111
+ });
112
+ });
113
+
114
+ it('keeps the touch area as a real view, so taps on the shape land', () => {
115
+ render(<VoiceWave state='idle' />, { wrapper: themed() });
116
+ fireEvent(screen.getByLabelText('Voice agent'), 'layout', layout(300, 200));
117
+
118
+ expect(screen.getByTestId('voice-visualization-hit-area').props.collapsable).toBe(false);
119
+ });
120
+
121
+ it('reads translated state values', () => {
122
+ render(<VoiceGlow state='thinking' stateLabels={{ thinking: 'Denkt nach' }} />, {
123
+ wrapper: themed(),
124
+ });
125
+
126
+ expect(screen.getByLabelText('Voice agent').props.accessibilityValue).toEqual({
127
+ text: 'Denkt nach',
128
+ });
129
+ });
130
+ });
131
+
132
+ describe('voice visualization motion', () => {
133
+ afterEach(() => jest.restoreAllMocks());
134
+
135
+ it('uses its own colors first', () => {
136
+ const { result } = renderVisualization({ colors: ['#ff0000', 'nope', '#00ff00'] });
137
+
138
+ expect(result.current.driver.value.colors[0].hue).toBe(hueOf([1, 0, 0]));
139
+ expect(result.current.driver.value.colors[1].hue).toBe(hueOf([0, 1, 0]));
140
+ expect(result.current.driver.value.count).toBe(2);
141
+ });
142
+
143
+ it('falls back to the theme, then to the default accent', () => {
144
+ const fromTheme = renderVisualization(
145
+ { colors: [] },
146
+ {
147
+ voiceVisualization: { colors: ['#00ff00'] },
148
+ },
149
+ );
150
+ expect(fromTheme.result.current.driver.value.colors[0].hue).toBe(hueOf([0, 1, 0]));
151
+
152
+ const fallback = renderVisualization(
153
+ { colors: ['nope'] },
154
+ {
155
+ voiceVisualization: { colors: ['also-nope'] },
156
+ },
157
+ );
158
+ expect(fallback.result.current.driver.value.colors[0].hue).toBe(hueOf([0, 95 / 255, 1]));
159
+ });
160
+
161
+ it('draws for a dark backdrop', () => {
162
+ const light = renderVisualization({}, { voiceVisualization: { backdropColor: '#ffffff' } });
163
+ const dark = renderVisualization({}, { voiceVisualization: { backdropColor: '#101010' } });
164
+
165
+ expect(light.result.current.dark.value).toBe(0);
166
+ expect(dark.result.current.dark.value).toBe(1);
167
+ });
168
+
169
+ it('takes the wave’s smooth level response and the glow’s steady one', () => {
170
+ const glow = renderVisualization({}, undefined, GLOW_STYLE);
171
+ const wave = renderVisualization({}, undefined, WAVE_STYLE);
172
+
173
+ expect(glow.result.current.driver.value.levelResponse).toBe(GLOW_STYLE.levelResponse);
174
+ expect(wave.result.current.driver.value.levelResponse).toBe(WAVE_STYLE.levelResponse);
175
+ });
176
+
177
+ it('bounces on a tap, unless motion is reduced', async () => {
178
+ const motion = renderVisualization();
179
+ act(() => motion.result.current.tap(0.1, 0.1));
180
+ expect(motion.result.current.driver.value.tapAge).toBe(0);
181
+ expect(motion.result.current.driver.value.tapX).toBe(0.1);
182
+
183
+ jest.spyOn(AccessibilityInfo, 'isReduceMotionEnabled').mockResolvedValueOnce(true);
184
+ const reduced = renderVisualization();
185
+ await act(async () => {
186
+ await Promise.resolve();
187
+ });
188
+ act(() => reduced.result.current.tap(0.1, 0.1));
189
+ expect(reduced.result.current.driver.value.tapAge).toBeGreaterThan(0);
190
+ });
191
+
192
+ it('draws the wave as one stroke with its ends faded out', () => {
193
+ const { result } = renderVisualization({ state: 'speaking' }, undefined, WAVE_STYLE);
194
+ const d = result.current.driver.value;
195
+
196
+ const path = wavePath(d, 300, 200) as unknown as { commands: Array<[string, number, number]> };
197
+ expect(path.commands).toHaveLength(161);
198
+ expect(path.commands[0][0]).toBe('M');
199
+ expect(path.commands[0][1]).toBeCloseTo(150 - 0.42 * 200, 4);
200
+ expect(path.commands[160][1]).toBeCloseTo(150 + 0.42 * 200, 4);
201
+
202
+ const stops = waveColors(d, 0);
203
+ expect(stops).toHaveLength(13);
204
+ expect(stops[0][3]).toBe(0);
205
+ expect(stops[12][3]).toBe(0);
206
+ expect(stops[6][3]).toBeCloseTo(1, 4);
207
+ });
208
+ });
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { DeviceEventEmitter } from 'react-native';
3
3
 
4
- import { act, fireEvent, render, screen } from '@testing-library/react-native';
4
+ import { act, fireEvent, render, renderHook, screen } from '@testing-library/react-native';
5
5
 
6
6
  import * as aiComponents from '../index';
7
7
  import { AIDictation } from '../native/AIDictation';
@@ -162,5 +162,62 @@ describe('ai-components dictation', () => {
162
162
  state: 'idle',
163
163
  });
164
164
  });
165
+
166
+ it('streams the microphone level while listening and clears it afterwards', () => {
167
+ mockNativeModule = createNativeModule();
168
+ const levels: number[] = [];
169
+ const unsubscribe = AIDictation.addLevelListener(({ level }) => levels.push(level));
170
+ const { result, unmount } = renderHook(() => aiComponents.useDictationAudioLevel());
171
+ expect(result.current.value).toBeNull();
172
+
173
+ act(() => {
174
+ DeviceEventEmitter.emit('AIDictationOnState', { state: 'listening' });
175
+ DeviceEventEmitter.emit('AIDictationOnLevel', { level: 0.4 });
176
+ });
177
+ expect(levels).toEqual([0.4]);
178
+ expect(result.current.value).toBe(0.4);
179
+
180
+ act(() => {
181
+ DeviceEventEmitter.emit('AIDictationOnState', { state: 'idle' });
182
+ });
183
+ expect(result.current.value).toBeNull();
184
+
185
+ act(() => {
186
+ DeviceEventEmitter.emit('AIDictationOnLevel', { level: 0.7 });
187
+ DeviceEventEmitter.emit('AIDictationOnError', { code: 'X', message: 'boom' });
188
+ });
189
+ expect(result.current.value).toBeNull();
190
+
191
+ unsubscribe();
192
+ unmount();
193
+ act(() => {
194
+ DeviceEventEmitter.emit('AIDictationOnLevel', { level: 0.9 });
195
+ });
196
+ expect(levels).toEqual([0.4, 0.7]);
197
+ });
198
+
199
+ it('leaves the composer text alone for dictation started elsewhere', async () => {
200
+ mockNativeModule = createNativeModule();
201
+ const composer = renderComposer();
202
+ fireEvent.changeText(composer.getByPlaceholderText('Ask anything'), 'my draft');
203
+
204
+ const { result } = renderHook(() =>
205
+ aiComponents.useDictation(undefined, { source: 'voice-mode' }),
206
+ );
207
+ await act(async () => {
208
+ await result.current.start();
209
+ });
210
+ act(() => {
211
+ DeviceEventEmitter.emit('AIDictationOnState', { state: 'listening' });
212
+ DeviceEventEmitter.emit('AIDictationOnResult', { isFinal: true, text: 'spoken question' });
213
+ DeviceEventEmitter.emit('AIDictationOnState', { state: 'idle' });
214
+ });
215
+
216
+ expect(aiComponents.dictationStore.getLatestValue()).toMatchObject({
217
+ source: 'voice-mode',
218
+ transcript: 'spoken question',
219
+ });
220
+ expect(composer.getByPlaceholderText('Ask anything').props.value).toBe('my draft');
221
+ });
165
222
  });
166
223
  });