stream-chat-react-native-core 9.10.2-beta.4 → 9.11.0-beta.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 (183) 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/version.json +1 -1
  53. package/lib/module/ai-components/README.md +50 -2
  54. package/lib/module/ai-components/index.js +24 -0
  55. package/lib/module/ai-components/index.js.map +1 -1
  56. package/lib/module/ai-components/message-composer/ComposerView.js +4 -2
  57. package/lib/module/ai-components/message-composer/ComposerView.js.map +1 -1
  58. package/lib/module/ai-components/message-composer/SpeechToTextButton.js +4 -1
  59. package/lib/module/ai-components/message-composer/SpeechToTextButton.js.map +1 -1
  60. package/lib/module/ai-components/native/AIDictation.js +6 -0
  61. package/lib/module/ai-components/native/AIDictation.js.map +1 -1
  62. package/lib/module/ai-components/store/dictation/store.js +4 -2
  63. package/lib/module/ai-components/store/dictation/store.js.map +1 -1
  64. package/lib/module/ai-components/store/dictation/useDictationState.js +4 -2
  65. package/lib/module/ai-components/store/dictation/useDictationState.js.map +1 -1
  66. package/lib/module/ai-components/store/index.js +22 -0
  67. package/lib/module/ai-components/store/index.js.map +1 -1
  68. package/lib/module/ai-components/theme.js +3 -0
  69. package/lib/module/ai-components/theme.js.map +1 -1
  70. package/lib/module/ai-components/transcription/useDictation.js +7 -3
  71. package/lib/module/ai-components/transcription/useDictation.js.map +1 -1
  72. package/lib/module/ai-components/transcription/useDictationAudioLevel.js +32 -0
  73. package/lib/module/ai-components/transcription/useDictationAudioLevel.js.map +1 -0
  74. package/lib/module/ai-components/voice-visualization/VoiceGlow.js +79 -0
  75. package/lib/module/ai-components/voice-visualization/VoiceGlow.js.map +1 -0
  76. package/lib/module/ai-components/voice-visualization/VoiceVisualizationView.js +147 -0
  77. package/lib/module/ai-components/voice-visualization/VoiceVisualizationView.js.map +1 -0
  78. package/lib/module/ai-components/voice-visualization/VoiceWave.js +58 -0
  79. package/lib/module/ai-components/voice-visualization/VoiceWave.js.map +1 -0
  80. package/lib/module/ai-components/voice-visualization/bounce.js +60 -0
  81. package/lib/module/ai-components/voice-visualization/bounce.js.map +1 -0
  82. package/lib/module/ai-components/voice-visualization/driver.js +208 -0
  83. package/lib/module/ai-components/voice-visualization/driver.js.map +1 -0
  84. package/lib/module/ai-components/voice-visualization/glowShader.js +218 -0
  85. package/lib/module/ai-components/voice-visualization/glowShader.js.map +1 -0
  86. package/lib/module/ai-components/voice-visualization/index.js +18 -0
  87. package/lib/module/ai-components/voice-visualization/index.js.map +1 -0
  88. package/lib/module/ai-components/voice-visualization/math.js +70 -0
  89. package/lib/module/ai-components/voice-visualization/math.js.map +1 -0
  90. package/lib/module/ai-components/voice-visualization/palette.js +47 -0
  91. package/lib/module/ai-components/voice-visualization/palette.js.map +1 -0
  92. package/lib/module/ai-components/voice-visualization/simplex.js +77 -0
  93. package/lib/module/ai-components/voice-visualization/simplex.js.map +1 -0
  94. package/lib/module/ai-components/voice-visualization/styles.js +33 -0
  95. package/lib/module/ai-components/voice-visualization/styles.js.map +1 -0
  96. package/lib/module/ai-components/voice-visualization/types.js +4 -0
  97. package/lib/module/ai-components/voice-visualization/types.js.map +1 -0
  98. package/lib/module/ai-components/voice-visualization/useReduceMotion.js +28 -0
  99. package/lib/module/ai-components/voice-visualization/useReduceMotion.js.map +1 -0
  100. package/lib/module/ai-components/voice-visualization/useVoiceVisualization.js +129 -0
  101. package/lib/module/ai-components/voice-visualization/useVoiceVisualization.js.map +1 -0
  102. package/lib/module/ai-components/voice-visualization/waveGeometry.js +124 -0
  103. package/lib/module/ai-components/voice-visualization/waveGeometry.js.map +1 -0
  104. package/lib/module/version.json +1 -1
  105. package/lib/typescript/ai-components/index.d.ts +3 -1
  106. package/lib/typescript/ai-components/index.d.ts.map +1 -1
  107. package/lib/typescript/ai-components/message-composer/ComposerView.d.ts.map +1 -1
  108. package/lib/typescript/ai-components/native/AIDictation.d.ts +6 -0
  109. package/lib/typescript/ai-components/native/AIDictation.d.ts.map +1 -1
  110. package/lib/typescript/ai-components/store/dictation/store.d.ts +7 -0
  111. package/lib/typescript/ai-components/store/dictation/store.d.ts.map +1 -1
  112. package/lib/typescript/ai-components/store/dictation/useDictationState.d.ts +1 -0
  113. package/lib/typescript/ai-components/store/dictation/useDictationState.d.ts.map +1 -1
  114. package/lib/typescript/ai-components/store/index.d.ts +2 -0
  115. package/lib/typescript/ai-components/store/index.d.ts.map +1 -1
  116. package/lib/typescript/ai-components/theme.d.ts +13 -0
  117. package/lib/typescript/ai-components/theme.d.ts.map +1 -1
  118. package/lib/typescript/ai-components/transcription/useDictation.d.ts +9 -1
  119. package/lib/typescript/ai-components/transcription/useDictation.d.ts.map +1 -1
  120. package/lib/typescript/ai-components/transcription/useDictationAudioLevel.d.ts +11 -0
  121. package/lib/typescript/ai-components/transcription/useDictationAudioLevel.d.ts.map +1 -0
  122. package/lib/typescript/ai-components/voice-visualization/VoiceGlow.d.ts +13 -0
  123. package/lib/typescript/ai-components/voice-visualization/VoiceGlow.d.ts.map +1 -0
  124. package/lib/typescript/ai-components/voice-visualization/VoiceVisualizationView.d.ts +26 -0
  125. package/lib/typescript/ai-components/voice-visualization/VoiceVisualizationView.d.ts.map +1 -0
  126. package/lib/typescript/ai-components/voice-visualization/VoiceWave.d.ts +13 -0
  127. package/lib/typescript/ai-components/voice-visualization/VoiceWave.d.ts.map +1 -0
  128. package/lib/typescript/ai-components/voice-visualization/bounce.d.ts +20 -0
  129. package/lib/typescript/ai-components/voice-visualization/bounce.d.ts.map +1 -0
  130. package/lib/typescript/ai-components/voice-visualization/driver.d.ts +96 -0
  131. package/lib/typescript/ai-components/voice-visualization/driver.d.ts.map +1 -0
  132. package/lib/typescript/ai-components/voice-visualization/glowShader.d.ts +7 -0
  133. package/lib/typescript/ai-components/voice-visualization/glowShader.d.ts.map +1 -0
  134. package/lib/typescript/ai-components/voice-visualization/index.d.ts +4 -0
  135. package/lib/typescript/ai-components/voice-visualization/index.d.ts.map +1 -0
  136. package/lib/typescript/ai-components/voice-visualization/math.d.ts +26 -0
  137. package/lib/typescript/ai-components/voice-visualization/math.d.ts.map +1 -0
  138. package/lib/typescript/ai-components/voice-visualization/palette.d.ts +18 -0
  139. package/lib/typescript/ai-components/voice-visualization/palette.d.ts.map +1 -0
  140. package/lib/typescript/ai-components/voice-visualization/simplex.d.ts +11 -0
  141. package/lib/typescript/ai-components/voice-visualization/simplex.d.ts.map +1 -0
  142. package/lib/typescript/ai-components/voice-visualization/styles.d.ts +24 -0
  143. package/lib/typescript/ai-components/voice-visualization/styles.d.ts.map +1 -0
  144. package/lib/typescript/ai-components/voice-visualization/types.d.ts +45 -0
  145. package/lib/typescript/ai-components/voice-visualization/types.d.ts.map +1 -0
  146. package/lib/typescript/ai-components/voice-visualization/useReduceMotion.d.ts +10 -0
  147. package/lib/typescript/ai-components/voice-visualization/useReduceMotion.d.ts.map +1 -0
  148. package/lib/typescript/ai-components/voice-visualization/useVoiceVisualization.d.ts +18 -0
  149. package/lib/typescript/ai-components/voice-visualization/useVoiceVisualization.d.ts.map +1 -0
  150. package/lib/typescript/ai-components/voice-visualization/waveGeometry.d.ts +25 -0
  151. package/lib/typescript/ai-components/voice-visualization/waveGeometry.d.ts.map +1 -0
  152. package/package.json +1 -1
  153. package/src/ai-components/README.md +50 -2
  154. package/src/ai-components/__tests__/VoiceVisualization.test.tsx +208 -0
  155. package/src/ai-components/__tests__/dictation.test.tsx +58 -1
  156. package/src/ai-components/__tests__/voiceDriver.test.ts +317 -0
  157. package/src/ai-components/__tests__/voicePalette.test.ts +124 -0
  158. package/src/ai-components/index.ts +3 -1
  159. package/src/ai-components/message-composer/ComposerView.tsx +6 -3
  160. package/src/ai-components/message-composer/SpeechToTextButton.tsx +5 -2
  161. package/src/ai-components/native/AIDictation.ts +11 -0
  162. package/src/ai-components/store/dictation/store.ts +9 -0
  163. package/src/ai-components/store/dictation/useDictationState.ts +2 -1
  164. package/src/ai-components/store/index.ts +2 -0
  165. package/src/ai-components/theme.ts +16 -0
  166. package/src/ai-components/transcription/useDictation.ts +16 -3
  167. package/src/ai-components/transcription/useDictationAudioLevel.ts +38 -0
  168. package/src/ai-components/voice-visualization/VoiceGlow.tsx +79 -0
  169. package/src/ai-components/voice-visualization/VoiceVisualizationView.tsx +179 -0
  170. package/src/ai-components/voice-visualization/VoiceWave.tsx +67 -0
  171. package/src/ai-components/voice-visualization/bounce.ts +61 -0
  172. package/src/ai-components/voice-visualization/driver.ts +240 -0
  173. package/src/ai-components/voice-visualization/glowShader.ts +227 -0
  174. package/src/ai-components/voice-visualization/index.ts +3 -0
  175. package/src/ai-components/voice-visualization/math.ts +73 -0
  176. package/src/ai-components/voice-visualization/palette.ts +59 -0
  177. package/src/ai-components/voice-visualization/simplex.ts +92 -0
  178. package/src/ai-components/voice-visualization/styles.ts +42 -0
  179. package/src/ai-components/voice-visualization/types.ts +44 -0
  180. package/src/ai-components/voice-visualization/useReduceMotion.ts +34 -0
  181. package/src/ai-components/voice-visualization/useVoiceVisualization.ts +153 -0
  182. package/src/ai-components/voice-visualization/waveGeometry.ts +137 -0
  183. package/src/version.json +1 -1
@@ -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
  });
@@ -0,0 +1,317 @@
1
+ import {
2
+ bounceScale,
3
+ createBounce,
4
+ easeOut,
5
+ scaleAt,
6
+ startBounce,
7
+ } from '../voice-visualization/bounce';
8
+ import {
9
+ advanceDriver,
10
+ createDriverState,
11
+ driverReact,
12
+ driverScale,
13
+ ease,
14
+ hoverDriver,
15
+ LEVEL_RESPONSE,
16
+ type LevelResponse,
17
+ NO_TAP,
18
+ tapDriver,
19
+ type VoiceDriverInput,
20
+ type VoiceDriverState,
21
+ } from '../voice-visualization/driver';
22
+ import type { Hsv, VoiceAgentState } from '../voice-visualization/types';
23
+
24
+ // Ported from stream-chat-android's VoiceVisualizationDriverTest and VoiceVisualizationBounceTest,
25
+ // so the platforms keep moving alike.
26
+
27
+ const blue: Hsv = { hue: 218 / 360, saturation: 1, value: 1 };
28
+ const red: Hsv = { hue: 0, saturation: 1, value: 1 };
29
+ const FRAME = 1 / 60;
30
+
31
+ const input = (
32
+ state: VoiceAgentState,
33
+ overrides: Partial<Omit<VoiceDriverInput, 'state'>> = {},
34
+ ): VoiceDriverInput => ({
35
+ audioLevel: null,
36
+ colors: [blue],
37
+ expressivity: 1,
38
+ reduceMotion: false,
39
+ state,
40
+ ...overrides,
41
+ });
42
+
43
+ /** A driver with the input it reads each frame, as the view keeps them. */
44
+ const createDriver = (initial: VoiceDriverInput, response?: LevelResponse) => {
45
+ const driver = { input: initial, state: createDriverState(initial, response) };
46
+ return {
47
+ ...driver,
48
+ advance: (seconds: number) => advanceDriver(driver.state, driver.input, seconds),
49
+ run: (seconds: number) => {
50
+ for (let i = 0; i < Math.floor(seconds / FRAME); i++) {
51
+ advanceDriver(driver.state, driver.input, FRAME);
52
+ }
53
+ },
54
+ setInput: (next: VoiceDriverInput) => {
55
+ driver.input = next;
56
+ },
57
+ get d(): VoiceDriverState {
58
+ return driver.state;
59
+ },
60
+ get input() {
61
+ return driver.input;
62
+ },
63
+ };
64
+ };
65
+
66
+ const expectNear = (actual: number, expected: number, tolerance: number) =>
67
+ expect(Math.abs(actual - expected)).toBeLessThanOrEqual(tolerance);
68
+
69
+ describe('voice visualization driver', () => {
70
+ it('starts settled on the state', () => {
71
+ const { d } = createDriver(input('speaking'));
72
+
73
+ expect(driverReact(d)).toBe(1);
74
+ expect(d.load).toBe(0);
75
+ expect(d.flow).toBe(0);
76
+ expect(d.bright).toBe(1);
77
+ expect(d.time).toBe(0);
78
+ expect(d.count).toBe(1);
79
+ expect(driverScale(d)).toBe(1);
80
+ expect(d.tapAge).toBe(NO_TAP);
81
+ expect(d.colors[0]).toEqual(blue);
82
+ expect(d.colors[4]).toEqual(blue);
83
+ });
84
+
85
+ it('does not share its colors with the caller’s palette', () => {
86
+ const palette = [{ ...blue }];
87
+ const driver = createDriver(input('idle', { colors: palette }));
88
+ driver.setInput(input('idle', { colors: [red] }));
89
+
90
+ driver.run(1);
91
+
92
+ expect(palette[0]).toEqual(blue);
93
+ });
94
+
95
+ it('eases toward the motion of a new state', () => {
96
+ const driver = createDriver(input('idle'));
97
+ driver.setInput(input('connecting'));
98
+
99
+ driver.advance(FRAME);
100
+ expectNear(driver.d.load, 0.08, 1e-4);
101
+ driver.run(2);
102
+ expectNear(driver.d.load, 1, 1e-3);
103
+ expectNear(driverReact(driver.d), 0, 1e-3);
104
+ });
105
+
106
+ it('eases the same at any frame rate', () => {
107
+ const at60 = createDriver(input('idle'));
108
+ const at120 = createDriver(input('idle'));
109
+ at60.setInput(input('speaking'));
110
+ at120.setInput(input('speaking'));
111
+
112
+ for (let i = 0; i < 30; i++) at60.advance(1 / 60);
113
+ for (let i = 0; i < 60; i++) at120.advance(1 / 120);
114
+
115
+ expectNear(driverReact(at60.d), driverReact(at120.d), 1e-4);
116
+ expectNear(at60.d.time, at120.d.time, 1e-2);
117
+ });
118
+
119
+ it('clamps a long frame', () => {
120
+ const driver = createDriver(input('idle'));
121
+
122
+ driver.advance(5);
123
+
124
+ expectNear(driver.d.time, 0.05 * 0.4, 1e-5);
125
+ });
126
+
127
+ it('turns the flow only while thinking', () => {
128
+ const driver = createDriver(input('speaking'));
129
+ driver.run(1);
130
+ expect(driver.d.flowSpin).toBe(0);
131
+
132
+ driver.setInput(input('thinking'));
133
+ driver.run(1);
134
+ expect(driver.d.flowSpin).toBeGreaterThan(0);
135
+ });
136
+
137
+ it('keeps the colors turning in every state', () => {
138
+ const driver = createDriver(input('idle'));
139
+
140
+ driver.run(1);
141
+
142
+ expect(driver.d.orbSpin).toBeGreaterThan(0);
143
+ });
144
+
145
+ it('scales amplitude and speed with expressivity', () => {
146
+ const lively = createDriver(input('speaking', { expressivity: 2 }));
147
+ const still = createDriver(input('speaking', { expressivity: 0 }));
148
+
149
+ expectNear(driverReact(lively.d), 1.85, 1e-5);
150
+ expectNear(driverReact(still.d), 0.15, 1e-5);
151
+ lively.advance(0.05);
152
+ expectNear(lively.d.time, 0.05 * 1.25 * 1.4, 1e-5);
153
+ });
154
+
155
+ it('slows time and ignores the pointer under reduced motion', () => {
156
+ const driver = createDriver(input('idle', { reduceMotion: true }));
157
+
158
+ tapDriver(driver.d, driver.input, 0.1, 0.1);
159
+ hoverDriver(driver.d, driver.input, 0.1, 0.1);
160
+ driver.advance(0.05);
161
+
162
+ expectNear(driver.d.time, 0.05 * 0.07 * 0.4, 1e-6);
163
+ expect(driver.d.tapAge).toBeGreaterThan(NO_TAP);
164
+ expect(driverScale(driver.d)).toBe(1);
165
+ expect(driver.d.hoverAmount).toBe(0);
166
+ });
167
+
168
+ it('stops hovering once motion is reduced', () => {
169
+ const driver = createDriver(input('idle'));
170
+ hoverDriver(driver.d, driver.input, 0.1, 0.1);
171
+ driver.run(1);
172
+
173
+ driver.setInput(input('idle', { reduceMotion: true }));
174
+ driver.run(1);
175
+
176
+ expectNear(driver.d.hoverAmount, 0, 1e-3);
177
+ });
178
+
179
+ it('ripples from a tap and bounces', () => {
180
+ const driver = createDriver(input('idle'));
181
+
182
+ tapDriver(driver.d, driver.input, 0.1, -0.2);
183
+ expect(driver.d.tapX).toBe(0.1);
184
+ expect(driver.d.tapY).toBe(-0.2);
185
+ expect(driver.d.tapAge).toBe(0);
186
+
187
+ driver.advance(0.05);
188
+ expectNear(driver.d.tapAge, 0.05, 1e-6);
189
+ expect(driverScale(driver.d)).toBeLessThan(1);
190
+ driver.run(0.5);
191
+ expect(driverScale(driver.d)).toBe(1);
192
+ });
193
+
194
+ it('eases hovering in and out', () => {
195
+ const driver = createDriver(input('idle'));
196
+
197
+ hoverDriver(driver.d, driver.input, 0.2, 0.1);
198
+ driver.run(1);
199
+ expectNear(driver.d.hoverAmount, 1, 1e-3);
200
+ expect(driver.d.hoverX).toBe(0.2);
201
+
202
+ hoverDriver(driver.d, driver.input, null, null);
203
+ driver.run(1);
204
+ expectNear(driver.d.hoverAmount, 0, 1e-3);
205
+ expect(driver.d.hoverX).toBe(0.2);
206
+ });
207
+
208
+ it('lifts the motion to a live audio level', () => {
209
+ const driver = createDriver(input('idle', { audioLevel: 0.8 }));
210
+
211
+ driver.run(2);
212
+ expectNear(driver.d.mic, 0.8, 1e-3);
213
+ expectNear(driver.d.voice, 1, 1e-3);
214
+ expectNear(driverReact(driver.d), 0.8, 1e-3);
215
+
216
+ driver.setInput(input('idle'));
217
+ driver.run(2);
218
+ expectNear(driver.d.voice, 0, 1e-3);
219
+ expectNear(driverReact(driver.d), 0.12, 1e-3);
220
+ });
221
+
222
+ const settled = (response: LevelResponse, audioLevel: number) => {
223
+ const driver = createDriver(input('idle', { audioLevel }), response);
224
+ driver.run(1);
225
+ return driver;
226
+ };
227
+
228
+ it('catches up with a louder level within a word when smooth', () => {
229
+ const driver = settled(LEVEL_RESPONSE.smooth, 0);
230
+
231
+ driver.setInput(input('idle', { audioLevel: 1 }));
232
+ driver.run(0.1);
233
+ expect(driver.d.mic).toBeGreaterThanOrEqual(0.5);
234
+ expect(driver.d.mic).toBeLessThanOrEqual(0.8);
235
+ driver.run(0.3);
236
+ expect(driver.d.mic).toBeGreaterThan(0.95);
237
+ });
238
+
239
+ it('lets a quieter level fall away over a pause, not a syllable, when smooth', () => {
240
+ const driver = settled(LEVEL_RESPONSE.smooth, 1);
241
+
242
+ driver.setInput(input('idle', { audioLevel: 0 }));
243
+ driver.run(0.1);
244
+ expect(driver.d.mic).toBeGreaterThan(0.7);
245
+ driver.run(0.9);
246
+ expect(driver.d.mic).toBeLessThan(0.1);
247
+ });
248
+
249
+ it('follows a quieter level within a syllable when steady', () => {
250
+ const driver = settled(LEVEL_RESPONSE.steady, 1);
251
+
252
+ driver.setInput(input('idle', { audioLevel: 0 }));
253
+ driver.run(0.1);
254
+ expect(driver.d.mic).toBeLessThan(0.1);
255
+ });
256
+
257
+ it('eases a hue the short way round', () => {
258
+ const driver = createDriver(
259
+ input('idle', { colors: [{ hue: 0.95, saturation: 1, value: 1 }] }),
260
+ );
261
+ driver.setInput(input('idle', { colors: [{ hue: 0.05, saturation: 1, value: 1 }] }));
262
+
263
+ driver.advance(FRAME);
264
+ expectNear(driver.d.colors[0].hue, 0.962, 1e-4);
265
+ driver.run(2);
266
+ expectNear(driver.d.colors[0].hue, 0.05, 1e-3);
267
+ });
268
+
269
+ it('repeats the first color in empty slots and crossfades the count', () => {
270
+ const driver = createDriver(input('idle', { colors: [blue, red] }));
271
+ expect(driver.d.colors[1]).toEqual(red);
272
+ expect(driver.d.colors[2]).toEqual(blue);
273
+
274
+ driver.setInput(input('idle', { colors: [blue, red, red] }));
275
+ driver.advance(FRAME);
276
+ expectNear(driver.d.count, 2.12, 1e-4);
277
+ });
278
+
279
+ it('moves the per-frame share at 60 Hz', () => {
280
+ expectNear(ease(0.08, 1 / 60), 0.08, 1e-5);
281
+ expect(ease(0.08, 0)).toBe(0);
282
+ });
283
+ });
284
+
285
+ describe('voice visualization bounce', () => {
286
+ it('follows the keyframes', () => {
287
+ expect(scaleAt(0)).toBe(1);
288
+ expectNear(scaleAt(0.15), 0.965, 1e-5);
289
+ expectNear(scaleAt(0.3), 0.93, 1e-5);
290
+ expectNear(scaleAt(0.62), 1.03, 1e-5);
291
+ expectNear(scaleAt(1), 1, 1e-5);
292
+ });
293
+
294
+ it('times them with the CSS ease-out', () => {
295
+ expectNear(easeOut(0), 0, 1e-4);
296
+ expectNear(easeOut(0.5), 0.684, 2e-3);
297
+ expectNear(easeOut(1), 1, 1e-4);
298
+ });
299
+
300
+ it('dips, overshoots and settles', () => {
301
+ const bounce = createBounce();
302
+ expect(bounceScale(bounce)).toBe(1);
303
+
304
+ startBounce(bounce);
305
+ const scales = Array.from({ length: 21 }, () => {
306
+ const scale = bounceScale(bounce);
307
+ bounce.age += 1 / 60;
308
+ return scale;
309
+ });
310
+
311
+ expectNear(scales[0], 1, 1e-9);
312
+ expectNear(Math.min(...scales), 0.93, 0.01);
313
+ expectNear(Math.max(...scales), 1.03, 0.01);
314
+ expect(scales.indexOf(Math.min(...scales))).toBeLessThan(scales.indexOf(Math.max(...scales)));
315
+ expect(bounceScale(bounce)).toBe(1);
316
+ });
317
+ });