@magmamath/students-features 0.9.106 → 0.9.107-rc.10

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/dist/commonjs/features/voiceRecord/components/VoiceRecord.js +81 -0
  2. package/dist/commonjs/features/voiceRecord/components/VoiceRecord.js.map +1 -0
  3. package/dist/commonjs/features/voiceRecord/components/VoiceRecordButton.js +72 -0
  4. package/dist/commonjs/features/voiceRecord/components/VoiceRecordButton.js.map +1 -0
  5. package/dist/commonjs/features/voiceRecord/components/VoiceRecordDeleteButton.js +32 -0
  6. package/dist/commonjs/features/voiceRecord/components/VoiceRecordDeleteButton.js.map +1 -0
  7. package/dist/commonjs/features/voiceRecord/components/VoiceRecordDevider.js +27 -0
  8. package/dist/commonjs/features/voiceRecord/components/VoiceRecordDevider.js.map +1 -0
  9. package/dist/commonjs/features/voiceRecord/components/VoiceRecordTimer.js +49 -0
  10. package/dist/commonjs/features/voiceRecord/components/VoiceRecordTimer.js.map +1 -0
  11. package/dist/commonjs/features/voiceRecord/constants.js +18 -0
  12. package/dist/commonjs/features/voiceRecord/constants.js.map +1 -0
  13. package/dist/commonjs/features/voiceRecord/helpers.js +138 -0
  14. package/dist/commonjs/features/voiceRecord/helpers.js.map +1 -0
  15. package/dist/commonjs/features/voiceRecord/hooks/useVoiceRecorder.js +75 -0
  16. package/dist/commonjs/features/voiceRecord/hooks/useVoiceRecorder.js.map +1 -0
  17. package/dist/commonjs/features/voiceRecord/hooks/useVoiceRecorderAnimation.js +22 -0
  18. package/dist/commonjs/features/voiceRecord/hooks/useVoiceRecorderAnimation.js.map +1 -0
  19. package/dist/commonjs/features/voiceRecord/index.js +72 -0
  20. package/dist/commonjs/features/voiceRecord/index.js.map +1 -0
  21. package/dist/commonjs/features/voiceRecord/modal/VoiceRecordDeleteModal.js +93 -0
  22. package/dist/commonjs/features/voiceRecord/modal/VoiceRecordDeleteModal.js.map +1 -0
  23. package/dist/commonjs/features/voiceRecord/modal/VoiceRecordUndoModal.js +75 -0
  24. package/dist/commonjs/features/voiceRecord/modal/VoiceRecordUndoModal.js.map +1 -0
  25. package/dist/commonjs/features/voiceRecord/model/Recorder.model.js +37 -0
  26. package/dist/commonjs/features/voiceRecord/model/Recorder.model.js.map +1 -0
  27. package/dist/commonjs/features/voiceRecord/model/VoiceRecord.model.js +151 -0
  28. package/dist/commonjs/features/voiceRecord/model/VoiceRecord.model.js.map +1 -0
  29. package/dist/commonjs/features/voiceRecord/model/VoiceRecordApi.js +29 -0
  30. package/dist/commonjs/features/voiceRecord/model/VoiceRecordApi.js.map +1 -0
  31. package/dist/commonjs/features/voiceRecord/model/VoiceRecordCollection.js +30 -0
  32. package/dist/commonjs/features/voiceRecord/model/VoiceRecordCollection.js.map +1 -0
  33. package/dist/commonjs/features/voiceRecord/types.js +6 -0
  34. package/dist/commonjs/features/voiceRecord/types.js.map +1 -0
  35. package/dist/commonjs/index.js +12 -0
  36. package/dist/commonjs/index.js.map +1 -1
  37. package/dist/commonjs/shared/translation/localization/ca.json +13 -0
  38. package/dist/commonjs/shared/translation/localization/de.json +13 -0
  39. package/dist/commonjs/shared/translation/localization/en.json +13 -0
  40. package/dist/commonjs/shared/translation/localization/gb.json +13 -0
  41. package/dist/commonjs/shared/translation/localization/sct.json +13 -0
  42. package/dist/commonjs/shared/translation/localization/sw.json +13 -0
  43. package/dist/module/features/voiceRecord/components/VoiceRecord.js +73 -0
  44. package/dist/module/features/voiceRecord/components/VoiceRecord.js.map +1 -0
  45. package/dist/module/features/voiceRecord/components/VoiceRecordButton.js +66 -0
  46. package/dist/module/features/voiceRecord/components/VoiceRecordButton.js.map +1 -0
  47. package/dist/module/features/voiceRecord/components/VoiceRecordDeleteButton.js +26 -0
  48. package/dist/module/features/voiceRecord/components/VoiceRecordDeleteButton.js.map +1 -0
  49. package/dist/module/features/voiceRecord/components/VoiceRecordDevider.js +21 -0
  50. package/dist/module/features/voiceRecord/components/VoiceRecordDevider.js.map +1 -0
  51. package/dist/module/features/voiceRecord/components/VoiceRecordTimer.js +43 -0
  52. package/dist/module/features/voiceRecord/components/VoiceRecordTimer.js.map +1 -0
  53. package/dist/module/features/voiceRecord/constants.js +14 -0
  54. package/dist/module/features/voiceRecord/constants.js.map +1 -0
  55. package/dist/module/features/voiceRecord/helpers.js +127 -0
  56. package/dist/module/features/voiceRecord/helpers.js.map +1 -0
  57. package/dist/module/features/voiceRecord/hooks/useVoiceRecorder.js +70 -0
  58. package/dist/module/features/voiceRecord/hooks/useVoiceRecorder.js.map +1 -0
  59. package/dist/module/features/voiceRecord/hooks/useVoiceRecorderAnimation.js +17 -0
  60. package/dist/module/features/voiceRecord/hooks/useVoiceRecorderAnimation.js.map +1 -0
  61. package/dist/module/features/voiceRecord/index.js +9 -0
  62. package/dist/module/features/voiceRecord/index.js.map +1 -0
  63. package/dist/module/features/voiceRecord/modal/VoiceRecordDeleteModal.js +86 -0
  64. package/dist/module/features/voiceRecord/modal/VoiceRecordDeleteModal.js.map +1 -0
  65. package/dist/module/features/voiceRecord/modal/VoiceRecordUndoModal.js +69 -0
  66. package/dist/module/features/voiceRecord/modal/VoiceRecordUndoModal.js.map +1 -0
  67. package/dist/module/features/voiceRecord/model/Recorder.model.js +32 -0
  68. package/dist/module/features/voiceRecord/model/Recorder.model.js.map +1 -0
  69. package/dist/module/features/voiceRecord/model/VoiceRecord.model.js +146 -0
  70. package/dist/module/features/voiceRecord/model/VoiceRecord.model.js.map +1 -0
  71. package/dist/module/features/voiceRecord/model/VoiceRecordApi.js +24 -0
  72. package/dist/module/features/voiceRecord/model/VoiceRecordApi.js.map +1 -0
  73. package/dist/module/features/voiceRecord/model/VoiceRecordCollection.js +25 -0
  74. package/dist/module/features/voiceRecord/model/VoiceRecordCollection.js.map +1 -0
  75. package/dist/module/features/voiceRecord/types.js +4 -0
  76. package/dist/module/features/voiceRecord/types.js.map +1 -0
  77. package/dist/module/index.js +1 -0
  78. package/dist/module/index.js.map +1 -1
  79. package/dist/module/shared/translation/localization/ca.json +13 -0
  80. package/dist/module/shared/translation/localization/de.json +13 -0
  81. package/dist/module/shared/translation/localization/en.json +13 -0
  82. package/dist/module/shared/translation/localization/gb.json +13 -0
  83. package/dist/module/shared/translation/localization/sct.json +13 -0
  84. package/dist/module/shared/translation/localization/sw.json +13 -0
  85. package/dist/typescript/commonjs/features/voiceRecord/components/VoiceRecord.d.ts +10 -0
  86. package/dist/typescript/commonjs/features/voiceRecord/components/VoiceRecord.d.ts.map +1 -0
  87. package/dist/typescript/commonjs/features/voiceRecord/components/VoiceRecordButton.d.ts +11 -0
  88. package/dist/typescript/commonjs/features/voiceRecord/components/VoiceRecordButton.d.ts.map +1 -0
  89. package/dist/typescript/commonjs/features/voiceRecord/components/VoiceRecordDeleteButton.d.ts +7 -0
  90. package/dist/typescript/commonjs/features/voiceRecord/components/VoiceRecordDeleteButton.d.ts.map +1 -0
  91. package/dist/typescript/commonjs/features/voiceRecord/components/VoiceRecordDevider.d.ts +3 -0
  92. package/dist/typescript/commonjs/features/voiceRecord/components/VoiceRecordDevider.d.ts.map +1 -0
  93. package/dist/typescript/commonjs/features/voiceRecord/components/VoiceRecordTimer.d.ts +11 -0
  94. package/dist/typescript/commonjs/features/voiceRecord/components/VoiceRecordTimer.d.ts.map +1 -0
  95. package/dist/typescript/commonjs/features/voiceRecord/constants.d.ts +11 -0
  96. package/dist/typescript/commonjs/features/voiceRecord/constants.d.ts.map +1 -0
  97. package/dist/typescript/commonjs/features/voiceRecord/helpers.d.ts +29 -0
  98. package/dist/typescript/commonjs/features/voiceRecord/helpers.d.ts.map +1 -0
  99. package/dist/typescript/commonjs/features/voiceRecord/hooks/useVoiceRecorder.d.ts +5 -0
  100. package/dist/typescript/commonjs/features/voiceRecord/hooks/useVoiceRecorder.d.ts.map +1 -0
  101. package/dist/typescript/commonjs/features/voiceRecord/hooks/useVoiceRecorderAnimation.d.ts +6 -0
  102. package/dist/typescript/commonjs/features/voiceRecord/hooks/useVoiceRecorderAnimation.d.ts.map +1 -0
  103. package/dist/typescript/commonjs/features/voiceRecord/index.d.ts +7 -0
  104. package/dist/typescript/commonjs/features/voiceRecord/index.d.ts.map +1 -0
  105. package/dist/typescript/commonjs/features/voiceRecord/modal/VoiceRecordDeleteModal.d.ts +13 -0
  106. package/dist/typescript/commonjs/features/voiceRecord/modal/VoiceRecordDeleteModal.d.ts.map +1 -0
  107. package/dist/typescript/commonjs/features/voiceRecord/modal/VoiceRecordUndoModal.d.ts +13 -0
  108. package/dist/typescript/commonjs/features/voiceRecord/modal/VoiceRecordUndoModal.d.ts.map +1 -0
  109. package/dist/typescript/commonjs/features/voiceRecord/model/Recorder.model.d.ts +15 -0
  110. package/dist/typescript/commonjs/features/voiceRecord/model/Recorder.model.d.ts.map +1 -0
  111. package/dist/typescript/commonjs/features/voiceRecord/model/VoiceRecord.model.d.ts +39 -0
  112. package/dist/typescript/commonjs/features/voiceRecord/model/VoiceRecord.model.d.ts.map +1 -0
  113. package/dist/typescript/commonjs/features/voiceRecord/model/VoiceRecordApi.d.ts +5 -0
  114. package/dist/typescript/commonjs/features/voiceRecord/model/VoiceRecordApi.d.ts.map +1 -0
  115. package/dist/typescript/commonjs/features/voiceRecord/model/VoiceRecordCollection.d.ts +10 -0
  116. package/dist/typescript/commonjs/features/voiceRecord/model/VoiceRecordCollection.d.ts.map +1 -0
  117. package/dist/typescript/commonjs/features/voiceRecord/types.d.ts +43 -0
  118. package/dist/typescript/commonjs/features/voiceRecord/types.d.ts.map +1 -0
  119. package/dist/typescript/commonjs/index.d.ts +1 -0
  120. package/dist/typescript/commonjs/index.d.ts.map +1 -1
  121. package/dist/typescript/commonjs/shared/translation/constants.d.ts +78 -0
  122. package/dist/typescript/commonjs/shared/translation/constants.d.ts.map +1 -1
  123. package/dist/typescript/commonjs/shared/translation/model.d.ts +26 -0
  124. package/dist/typescript/commonjs/shared/translation/model.d.ts.map +1 -1
  125. package/dist/typescript/commonjs/types/common.types.d.ts +3 -0
  126. package/dist/typescript/commonjs/types/common.types.d.ts.map +1 -1
  127. package/dist/typescript/module/features/voiceRecord/components/VoiceRecord.d.ts +10 -0
  128. package/dist/typescript/module/features/voiceRecord/components/VoiceRecord.d.ts.map +1 -0
  129. package/dist/typescript/module/features/voiceRecord/components/VoiceRecordButton.d.ts +11 -0
  130. package/dist/typescript/module/features/voiceRecord/components/VoiceRecordButton.d.ts.map +1 -0
  131. package/dist/typescript/module/features/voiceRecord/components/VoiceRecordDeleteButton.d.ts +7 -0
  132. package/dist/typescript/module/features/voiceRecord/components/VoiceRecordDeleteButton.d.ts.map +1 -0
  133. package/dist/typescript/module/features/voiceRecord/components/VoiceRecordDevider.d.ts +3 -0
  134. package/dist/typescript/module/features/voiceRecord/components/VoiceRecordDevider.d.ts.map +1 -0
  135. package/dist/typescript/module/features/voiceRecord/components/VoiceRecordTimer.d.ts +11 -0
  136. package/dist/typescript/module/features/voiceRecord/components/VoiceRecordTimer.d.ts.map +1 -0
  137. package/dist/typescript/module/features/voiceRecord/constants.d.ts +11 -0
  138. package/dist/typescript/module/features/voiceRecord/constants.d.ts.map +1 -0
  139. package/dist/typescript/module/features/voiceRecord/helpers.d.ts +29 -0
  140. package/dist/typescript/module/features/voiceRecord/helpers.d.ts.map +1 -0
  141. package/dist/typescript/module/features/voiceRecord/hooks/useVoiceRecorder.d.ts +5 -0
  142. package/dist/typescript/module/features/voiceRecord/hooks/useVoiceRecorder.d.ts.map +1 -0
  143. package/dist/typescript/module/features/voiceRecord/hooks/useVoiceRecorderAnimation.d.ts +6 -0
  144. package/dist/typescript/module/features/voiceRecord/hooks/useVoiceRecorderAnimation.d.ts.map +1 -0
  145. package/dist/typescript/module/features/voiceRecord/index.d.ts +7 -0
  146. package/dist/typescript/module/features/voiceRecord/index.d.ts.map +1 -0
  147. package/dist/typescript/module/features/voiceRecord/modal/VoiceRecordDeleteModal.d.ts +13 -0
  148. package/dist/typescript/module/features/voiceRecord/modal/VoiceRecordDeleteModal.d.ts.map +1 -0
  149. package/dist/typescript/module/features/voiceRecord/modal/VoiceRecordUndoModal.d.ts +13 -0
  150. package/dist/typescript/module/features/voiceRecord/modal/VoiceRecordUndoModal.d.ts.map +1 -0
  151. package/dist/typescript/module/features/voiceRecord/model/Recorder.model.d.ts +15 -0
  152. package/dist/typescript/module/features/voiceRecord/model/Recorder.model.d.ts.map +1 -0
  153. package/dist/typescript/module/features/voiceRecord/model/VoiceRecord.model.d.ts +39 -0
  154. package/dist/typescript/module/features/voiceRecord/model/VoiceRecord.model.d.ts.map +1 -0
  155. package/dist/typescript/module/features/voiceRecord/model/VoiceRecordApi.d.ts +5 -0
  156. package/dist/typescript/module/features/voiceRecord/model/VoiceRecordApi.d.ts.map +1 -0
  157. package/dist/typescript/module/features/voiceRecord/model/VoiceRecordCollection.d.ts +10 -0
  158. package/dist/typescript/module/features/voiceRecord/model/VoiceRecordCollection.d.ts.map +1 -0
  159. package/dist/typescript/module/features/voiceRecord/types.d.ts +43 -0
  160. package/dist/typescript/module/features/voiceRecord/types.d.ts.map +1 -0
  161. package/dist/typescript/module/index.d.ts +1 -0
  162. package/dist/typescript/module/index.d.ts.map +1 -1
  163. package/dist/typescript/module/shared/translation/constants.d.ts +78 -0
  164. package/dist/typescript/module/shared/translation/constants.d.ts.map +1 -1
  165. package/dist/typescript/module/shared/translation/model.d.ts +26 -0
  166. package/dist/typescript/module/shared/translation/model.d.ts.map +1 -1
  167. package/dist/typescript/module/types/common.types.d.ts +3 -0
  168. package/dist/typescript/module/types/common.types.d.ts.map +1 -1
  169. package/package.json +9 -3
  170. package/src/features/voiceRecord/components/VoiceRecord.tsx +73 -0
  171. package/src/features/voiceRecord/components/VoiceRecordButton.tsx +86 -0
  172. package/src/features/voiceRecord/components/VoiceRecordDeleteButton.tsx +24 -0
  173. package/src/features/voiceRecord/components/VoiceRecordDevider.tsx +17 -0
  174. package/src/features/voiceRecord/components/VoiceRecordTimer.tsx +53 -0
  175. package/src/features/voiceRecord/constants.ts +10 -0
  176. package/src/features/voiceRecord/helpers.ts +167 -0
  177. package/src/features/voiceRecord/hooks/useVoiceRecorder.ts +85 -0
  178. package/src/features/voiceRecord/hooks/useVoiceRecorderAnimation.ts +19 -0
  179. package/src/features/voiceRecord/index.ts +6 -0
  180. package/src/features/voiceRecord/modal/VoiceRecordDeleteModal.tsx +106 -0
  181. package/src/features/voiceRecord/modal/VoiceRecordUndoModal.tsx +82 -0
  182. package/src/features/voiceRecord/model/Recorder.model.ts +40 -0
  183. package/src/features/voiceRecord/model/VoiceRecord.model.ts +178 -0
  184. package/src/features/voiceRecord/model/VoiceRecordApi.ts +25 -0
  185. package/src/features/voiceRecord/model/VoiceRecordCollection.ts +26 -0
  186. package/src/features/voiceRecord/types.ts +49 -0
  187. package/src/index.ts +1 -0
  188. package/src/shared/translation/localization/ca.json +13 -0
  189. package/src/shared/translation/localization/de.json +13 -0
  190. package/src/shared/translation/localization/en.json +13 -0
  191. package/src/shared/translation/localization/gb.json +13 -0
  192. package/src/shared/translation/localization/sct.json +13 -0
  193. package/src/shared/translation/localization/sw.json +13 -0
  194. package/src/types/common.types.ts +1 -0
  195. package/.editorconfig +0 -15
  196. package/.eslintignore +0 -2
  197. package/.eslintrc +0 -22
  198. package/.gitattributes +0 -3
  199. package/.github/actions/setup/action.yml +0 -37
  200. package/.github/workflows/ci.yml +0 -73
  201. package/.gitignore +0 -84
  202. package/.idea/.gitignore +0 -8
  203. package/.idea/codeStyles/Project.xml +0 -62
  204. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  205. package/.idea/copilot.data.migration.agent.xml +0 -6
  206. package/.idea/copilot.data.migration.ask.xml +0 -6
  207. package/.idea/copilot.data.migration.ask2agent.xml +0 -6
  208. package/.idea/copilot.data.migration.edit.xml +0 -6
  209. package/.idea/inspectionProfiles/Project_Default.xml +0 -8
  210. package/.idea/jsLinters/eslint.xml +0 -6
  211. package/.idea/modules.xml +0 -8
  212. package/.idea/prettier.xml +0 -7
  213. package/.idea/shelf/Uncommitted_changes_before_Checkout_at_30_12_2025,_11_06_[Changes]/shelved.patch +0 -18
  214. package/.idea/shelf/Uncommitted_changes_before_Checkout_at_30_12_2025,_11_07_[Changes]/shelved.patch +0 -18
  215. package/.idea/shelf/Uncommitted_changes_before_Checkout_at_30_12_2025__11_06__Changes_.xml +0 -4
  216. package/.idea/shelf/Uncommitted_changes_before_Checkout_at_30_12_2025__11_07__Changes_.xml +0 -4
  217. package/.idea/students-features.iml +0 -12
  218. package/.idea/vcs.xml +0 -6
  219. package/.idea/workspace.xml +0 -646
  220. package/.nvmrc +0 -1
  221. package/.prettierrc +0 -11
  222. package/.release-it.json +0 -17
  223. package/.watchmanconfig +0 -1
  224. package/.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs +0 -541
  225. package/.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs +0 -28
  226. package/.yarn/releases/yarn-3.6.1.cjs +0 -874
  227. package/babel.config.js +0 -5
  228. package/bob.config.js +0 -25
  229. package/example/.expo/README.md +0 -8
  230. package/example/.expo/devices.json +0 -3
  231. package/example/.expo/web/cache/production/images/favicon/favicon-24272cdaeff82cc5facdaccd982a6f05b60c4504704bbf94c19a6388659880bb-contain-transparent/favicon-48.png +0 -0
  232. package/example/app.json +0 -30
  233. package/example/assets/adaptive-icon.png +0 -0
  234. package/example/assets/favicon.png +0 -0
  235. package/example/assets/icon.png +0 -0
  236. package/example/assets/splash-icon.png +0 -0
  237. package/example/babel.config.js +0 -28
  238. package/example/index.js +0 -8
  239. package/example/metro.config.js +0 -18
  240. package/example/package.json +0 -25
  241. package/example/src/App.tsx +0 -8
  242. package/example/tsconfig.json +0 -6
  243. package/example/yarn.lock +0 -6552
  244. package/tsconfig.build.json +0 -4
  245. package/tsconfig.json +0 -34
  246. package/yarn.lock +0 -8623
@@ -0,0 +1,53 @@
1
+ import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'
2
+ import { COLORS, Typography } from '@magmamath/react-native-ui'
3
+ import React from 'react'
4
+ import { VoiceRecordModel } from '../model/VoiceRecord.model'
5
+ import { useUnit } from 'effector-react'
6
+ import { formatDuration } from '../helpers'
7
+
8
+ type VoiceRecordTimerProps = {
9
+ style?: StyleProp<ViewStyle>
10
+ model: VoiceRecordModel
11
+ recordingFileDurationMs?: number
12
+ }
13
+
14
+ export const VoiceRecordTimer = ({
15
+ style,
16
+ model,
17
+ recordingFileDurationMs,
18
+ }: VoiceRecordTimerProps) => {
19
+ const savedAudioFileDurationMs = useUnit(model.recorderModel.$audioFileDurationMs)
20
+
21
+ const displayDurationMs = recordingFileDurationMs
22
+ ? recordingFileDurationMs
23
+ : savedAudioFileDurationMs
24
+
25
+ return (
26
+ <View style={[styles.container, style]}>
27
+ <Typography style={styles.text}>
28
+ {displayDurationMs ? formatDuration(displayDurationMs) : '0:00'}
29
+ </Typography>
30
+ </View>
31
+ )
32
+ }
33
+
34
+ export default VoiceRecordTimer
35
+
36
+ const styles = StyleSheet.create({
37
+ container: {
38
+ backgroundColor: COLORS.NEUTRAL_1,
39
+ paddingHorizontal: 8,
40
+ paddingVertical: 2,
41
+ alignItems: 'center',
42
+ borderWidth: 0.5,
43
+ borderColor: COLORS.NEUTRAL_4,
44
+ borderRadius: 18,
45
+ },
46
+ text: {
47
+ color: COLORS.NEUTRAL_10,
48
+ fontSize: 13,
49
+ fontWeight: '600',
50
+ lineHeight: 18,
51
+ textTransform: 'uppercase',
52
+ },
53
+ })
@@ -0,0 +1,10 @@
1
+ export const VOICE_RECORDER_CONTAINER_SIZES = {
2
+ COLLAPSED: 61,
3
+ EXPANDED: 152,
4
+ }
5
+ export const VOICE_RECORDER_MAX_DURATION_MS = 300000 // 5 minutes
6
+ export enum VoiceRecorderState {
7
+ IDLE,
8
+ RECORDING,
9
+ PAUSED,
10
+ }
@@ -0,0 +1,167 @@
1
+ import { IS_MOBILE, IS_WEB } from '@magmamath/react-native-ui'
2
+ import {
3
+ AudioPlayer,
4
+ AudioRecorder,
5
+ createAudioPlayer,
6
+ getRecordingPermissionsAsync,
7
+ requestRecordingPermissionsAsync,
8
+ } from 'expo-audio'
9
+ import { AudioApi } from './types'
10
+
11
+ export const formatDuration = (ms: number): string => {
12
+ const totalSeconds = Math.floor(ms / 1000)
13
+ const minutes = Math.floor(totalSeconds / 60)
14
+ const seconds = totalSeconds % 60
15
+ return `${minutes}:${seconds.toString().padStart(2, '0')}`
16
+ }
17
+
18
+ export const ensureRecordingPermissions = async () => {
19
+ const { status } = await getRecordingPermissionsAsync()
20
+
21
+ if (status !== 'granted') {
22
+ const { granted } = await requestRecordingPermissionsAsync()
23
+ return granted
24
+ }
25
+
26
+ return true
27
+ }
28
+
29
+ const getAvailableInputsWeb = async () => {
30
+ if (!navigator.mediaDevices?.enumerateDevices) return []
31
+
32
+ const devices = await navigator.mediaDevices.enumerateDevices()
33
+ return devices.filter((d) => d.kind === 'audioinput')
34
+ }
35
+
36
+ export const getAvailableInputs = async (recorder: AudioRecorder) => {
37
+ if (IS_WEB) {
38
+ return await getAvailableInputsWeb()
39
+ }
40
+ if (IS_MOBILE) {
41
+ return await recorder.getAvailableInputs()
42
+ }
43
+ return []
44
+ }
45
+
46
+ export const addAudioInputsListenerWeb = (callback: (devices: MediaDeviceInfo[]) => void) => {
47
+ if (!IS_WEB || !navigator.mediaDevices?.addEventListener) {
48
+ return () => {}
49
+ }
50
+
51
+ const handleDeviceChange = async () => {
52
+ const devices = await getAvailableInputsWeb()
53
+ callback(devices)
54
+ }
55
+
56
+ handleDeviceChange()
57
+
58
+ navigator.mediaDevices.addEventListener('devicechange', handleDeviceChange)
59
+
60
+ return () => {
61
+ navigator.mediaDevices.removeEventListener('devicechange', handleDeviceChange)
62
+ }
63
+ }
64
+ /**
65
+ *
66
+ * @param fileUri
67
+ * @param externalPlayer - is optional expo-audio player, but should be provided
68
+ * because creating player is resource consuming operation
69
+ */
70
+ export const getAudioDurationMs = (
71
+ fileUri: string,
72
+ externalPlayer?: AudioPlayer,
73
+ ): Promise<number | undefined> => {
74
+ if (!fileUri) return Promise.resolve(undefined)
75
+
76
+ const player = externalPlayer ?? createAudioPlayer()
77
+ const isInternalPlayer = !externalPlayer
78
+
79
+ player.replace(fileUri)
80
+
81
+ return new Promise((resolve, reject) => {
82
+ const subscription = player.addListener('playbackStatusUpdate', () => {
83
+ if (player.duration !== undefined && player.duration > 0) {
84
+ subscription.remove()
85
+
86
+ // player.duration is in seconds (e.g., 1.234567)
87
+ const durationMs = Math.round(player.duration * 1000) // convert to milliseconds
88
+
89
+ if (isInternalPlayer) {
90
+ player.remove()
91
+ }
92
+
93
+ resolve(durationMs)
94
+ }
95
+ })
96
+
97
+ setTimeout(() => {
98
+ subscription.remove()
99
+
100
+ if (isInternalPlayer) {
101
+ player.remove()
102
+ }
103
+
104
+ reject(new Error('Timeout loading audio metadata'))
105
+ }, 3000)
106
+ })
107
+ }
108
+
109
+ type GenerateAudioFormDataParams = {
110
+ audioFileUri: string
111
+ durationMs?: number
112
+ fileName?: string
113
+ }
114
+
115
+ export const generateAudioFormData = async ({
116
+ audioFileUri,
117
+ durationMs,
118
+ fileName,
119
+ }: GenerateAudioFormDataParams): Promise<FormData> => {
120
+ //todo: add web or ios prefix to filename
121
+
122
+ const formData = new FormData()
123
+ const duration = durationMs?.toString() || '0'
124
+
125
+ if (IS_WEB) {
126
+ const response = await fetch(audioFileUri)
127
+ const blob = await response.blob()
128
+ const filename = fileName || 'solution_voice_record.webm'
129
+
130
+ formData.append('file', blob, filename)
131
+ formData.append('filename', filename)
132
+ formData.append('duration', duration)
133
+
134
+ return formData
135
+ }
136
+
137
+ const filename = fileName || audioFileUri.split('/').pop() || 'solution_voice_record.m4a'
138
+ const file = {
139
+ uri: audioFileUri,
140
+ type: 'audio/m4a',
141
+ name: filename,
142
+ } as any
143
+
144
+ formData.append('file', file, filename)
145
+ formData.append('filename', filename)
146
+ formData.append('duration', duration)
147
+
148
+ return formData
149
+ }
150
+
151
+ export const uploadAudioFileRequestGenerator = (audioFileFormData: FormData, api: AudioApi) => {
152
+ const controller = new AbortController()
153
+
154
+ return {
155
+ uploadAudio: () =>
156
+ api.uploadAudioFile(
157
+ {
158
+ audioFileFormData,
159
+ },
160
+ //@ts-ignore
161
+ {
162
+ signal: controller.signal,
163
+ },
164
+ ),
165
+ controller,
166
+ }
167
+ }
@@ -0,0 +1,85 @@
1
+ import {
2
+ RecordingPresets,
3
+ useAudioPlayer,
4
+ useAudioRecorder,
5
+ useAudioRecorderState,
6
+ } from 'expo-audio'
7
+ import { addAudioInputsListenerWeb, getAudioDurationMs, getAvailableInputs } from '../helpers'
8
+ import { VoiceRecordModel } from '../model/VoiceRecord.model'
9
+ import { useEffect } from 'react'
10
+ import { IS_WEB } from '@magmamath/react-native-ui'
11
+ import { VOICE_RECORDER_MAX_DURATION_MS, VoiceRecorderState } from '../constants'
12
+ import { useUnit } from 'effector-react'
13
+
14
+ export const useVoiceRecorder = (model: VoiceRecordModel) => {
15
+ const player = useAudioPlayer()
16
+ const audioFilePath = useUnit(model.$uri)
17
+
18
+ const recorder = useAudioRecorder(RecordingPresets.LOW_QUALITY, (status) => {
19
+ if (status.error) {
20
+ model.notification.error(`Recorder error: ${status.error}`)
21
+ return
22
+ }
23
+ if (status.isFinished && status.url) {
24
+ model.setUri(status.url)
25
+ }
26
+ })
27
+ const recorderState = useAudioRecorderState(recorder)
28
+
29
+ useEffect(() => {
30
+ if (
31
+ recorderState.durationMillis >= VOICE_RECORDER_MAX_DURATION_MS &&
32
+ recorderState.isRecording
33
+ ) {
34
+ model.recorderModel.stop()
35
+ }
36
+ }, [recorderState.durationMillis])
37
+
38
+ useEffect(() => {
39
+ if (!IS_WEB) {
40
+ //only once for iPad and listener for WEB
41
+ getAvailableInputs(recorder).then((inputs) => {
42
+ model.recorderModel.setAvailableInputs(inputs)
43
+ })
44
+ return
45
+ }
46
+
47
+ const removeListener = addAudioInputsListenerWeb((devices) => {
48
+ model.recorderModel.setAvailableInputs(devices)
49
+ })
50
+
51
+ return removeListener
52
+ }, [])
53
+
54
+ useEffect(() => {
55
+ if (audioFilePath) {
56
+ const getDuration = async () => {
57
+ const durationMs = await getAudioDurationMs(audioFilePath, player)
58
+ model.recorderModel.setAudioFileDurationMs(durationMs || 0)
59
+ }
60
+ getDuration()
61
+ }
62
+ }, [audioFilePath])
63
+
64
+ useEffect(() => {
65
+ if (recorderState.isRecording) {
66
+ model.recorderModel.setVoiceRecordState(VoiceRecorderState.RECORDING)
67
+ return
68
+ }
69
+ if (recorderState.durationMillis > 0) {
70
+ model.recorderModel.setVoiceRecordState(VoiceRecorderState.PAUSED)
71
+ return
72
+ }
73
+ model.recorderModel.setVoiceRecordState(VoiceRecorderState.IDLE)
74
+
75
+ return () => {
76
+ model.recorderModel.reset()
77
+ }
78
+ }, [recorderState])
79
+
80
+ useEffect(() => {
81
+ model.recorderModel.recorder = recorder
82
+ }, [recorder])
83
+
84
+ return { recorderState }
85
+ }
@@ -0,0 +1,19 @@
1
+ import { useAnimatedStyle, withTiming } from 'react-native-reanimated'
2
+ import { VOICE_RECORDER_CONTAINER_SIZES } from '../constants'
3
+
4
+ export const useVoiceRecorderAnimation = (isExpanded: boolean) => {
5
+ const containerAnimatedStyle = useAnimatedStyle(() => {
6
+ return {
7
+ height: withTiming(
8
+ isExpanded
9
+ ? VOICE_RECORDER_CONTAINER_SIZES.EXPANDED
10
+ : VOICE_RECORDER_CONTAINER_SIZES.COLLAPSED,
11
+ {
12
+ duration: 280,
13
+ },
14
+ ),
15
+ }
16
+ }, [isExpanded])
17
+
18
+ return { containerAnimatedStyle }
19
+ }
@@ -0,0 +1,6 @@
1
+ export * from './components/VoiceRecord'
2
+ export * from './model/VoiceRecord.model'
3
+ export * from './modal/VoiceRecordDeleteModal'
4
+ export * from './modal/VoiceRecordUndoModal'
5
+ export * from './constants'
6
+ export * from './types'
@@ -0,0 +1,106 @@
1
+ import React, { useEffect } from 'react'
2
+ import {
3
+ BaseModal,
4
+ Button,
5
+ ButtonColor,
6
+ ButtonSize,
7
+ ButtonVariant,
8
+ SPACING,
9
+ } from '@magmamath/react-native-ui'
10
+ import { View, StyleSheet } from 'react-native'
11
+ import { useUnit } from 'effector-react'
12
+ import { VoiceRecordModel } from '../model/VoiceRecord.model'
13
+ import { VoiceRecorderState } from '../constants'
14
+ import { useText } from '../../../shared/translation'
15
+
16
+ type VoiceRecordDeleteModalComponentProps = {
17
+ modal: {
18
+ closeModal: () => void
19
+ params: {
20
+ model: VoiceRecordModel
21
+ }
22
+ }
23
+ }
24
+
25
+ type FooterButtonsProps = {
26
+ onClose: () => void
27
+ onDelete: () => void
28
+ }
29
+
30
+ const FooterButtons = ({ onClose, onDelete }: FooterButtonsProps) => {
31
+ const t = useText()
32
+
33
+ return (
34
+ <View style={styles.footerContainer}>
35
+ <Button variant={ButtonVariant.TERTIARY} size={ButtonSize.LARGE} onPress={onClose}>
36
+ {t('voiceRecorder.cancel')}
37
+ </Button>
38
+ <Button
39
+ colorScheme={ButtonColor.RED}
40
+ variant={ButtonVariant.PRIMARY}
41
+ size={ButtonSize.LARGE}
42
+ onPress={onDelete}
43
+ >
44
+ {t('voiceRecorder.delete')}
45
+ </Button>
46
+ </View>
47
+ )
48
+ }
49
+
50
+ export const VoiceRecordDeleteModal = ({
51
+ modal: { closeModal, params },
52
+ }: VoiceRecordDeleteModalComponentProps) => {
53
+ const t = useText()
54
+ const { model } = params
55
+
56
+ const recorderState = useUnit(model.recorderModel.$voiceRecordState)
57
+ const { durationMillis } = model.recorderModel.recorder.getStatus()
58
+
59
+ const isRecordingInProgress =
60
+ (recorderState === VoiceRecorderState.PAUSED ||
61
+ recorderState === VoiceRecorderState.RECORDING) &&
62
+ durationMillis > 0
63
+
64
+ const handleDelete = async () => {
65
+ if (isRecordingInProgress) {
66
+ await model.recorderModel.recorder.stop()
67
+ }
68
+ model.deleteCurrentRecording()
69
+ closeModal()
70
+ }
71
+
72
+ useEffect(() => {
73
+ if (recorderState === VoiceRecorderState.RECORDING) {
74
+ model.recorderModel.recorder.pause()
75
+ }
76
+
77
+ return () => {
78
+ if (isRecordingInProgress && model.recorderModel.recorder.getStatus().durationMillis > 0) {
79
+ model.recorderModel.recorder.record()
80
+ }
81
+ }
82
+ }, [])
83
+
84
+ return (
85
+ <BaseModal
86
+ style={{
87
+ container: styles.modalContainer,
88
+ }}
89
+ header={t('voiceRecorder.deleteRecording')}
90
+ content={t('voiceRecorder.youWontBeAbleToRecover')}
91
+ footer={<FooterButtons onClose={closeModal} onDelete={handleDelete} />}
92
+ />
93
+ )
94
+ }
95
+
96
+ const styles = StyleSheet.create({
97
+ modalContainer: {
98
+ width: 600,
99
+ height: 268,
100
+ },
101
+ footerContainer: {
102
+ flexDirection: 'row',
103
+ alignItems: 'center',
104
+ gap: SPACING[200],
105
+ },
106
+ })
@@ -0,0 +1,82 @@
1
+ import React from 'react'
2
+ import {
3
+ BaseModal,
4
+ Button,
5
+ ButtonColor,
6
+ ButtonSize,
7
+ ButtonVariant,
8
+ SPACING,
9
+ } from '@magmamath/react-native-ui'
10
+ import { StyleSheet, View } from 'react-native'
11
+ import { useText } from '../../../shared/translation'
12
+ import { VoiceRecordModel } from '../model/VoiceRecord.model'
13
+
14
+ type VoiceRecordUndoModalProps = {
15
+ modal: {
16
+ closeModal: () => void
17
+ params: {
18
+ model: VoiceRecordModel
19
+ }
20
+ }
21
+ }
22
+
23
+ type FooterButtonsProps = {
24
+ onClose: () => void
25
+ onRedo: () => void
26
+ }
27
+
28
+ const FooterButtons = ({ onClose, onRedo }: FooterButtonsProps) => {
29
+ const t = useText()
30
+
31
+ return (
32
+ <View style={styles.footerContainer}>
33
+ <Button variant={ButtonVariant.TERTIARY} size={ButtonSize.LARGE} onPress={onClose}>
34
+ {t('voiceRecorder.cancel')}
35
+ </Button>
36
+ <Button
37
+ colorScheme={ButtonColor.BLUE}
38
+ variant={ButtonVariant.PRIMARY}
39
+ size={ButtonSize.LARGE}
40
+ onPress={onRedo}
41
+ >
42
+ {t('voiceRecorder.redo')}
43
+ </Button>
44
+ </View>
45
+ )
46
+ }
47
+
48
+ export const VoiceRecordUndoModal = ({
49
+ modal: { closeModal, params },
50
+ }: VoiceRecordUndoModalProps) => {
51
+ const t = useText()
52
+ const { model } = params
53
+
54
+ const handleRedo = () => {
55
+ model.deleteCurrentRecording({ notification: false })
56
+ closeModal()
57
+ model.initializeRecording()
58
+ }
59
+
60
+ return (
61
+ <BaseModal
62
+ style={{
63
+ container: styles.modalStyle,
64
+ }}
65
+ header={t('voiceRecorder.redoRecording')}
66
+ content={t('voiceRecorder.thisWillRemoveRecording')}
67
+ footer={<FooterButtons onClose={closeModal} onRedo={handleRedo} />}
68
+ />
69
+ )
70
+ }
71
+
72
+ const styles = StyleSheet.create({
73
+ modalStyle: {
74
+ width: 600,
75
+ height: 268,
76
+ },
77
+ footerContainer: {
78
+ flexDirection: 'row',
79
+ alignItems: 'center',
80
+ gap: SPACING[200],
81
+ },
82
+ })
@@ -0,0 +1,40 @@
1
+ import { createEffect, createEvent, restore } from 'effector'
2
+ import { AudioRecorder, setAudioModeAsync } from 'expo-audio'
3
+ import { VoiceRecorderState } from '../constants'
4
+ import { AvailableAudioInputs } from '../types'
5
+
6
+ const AUDIO_MODE_CONFIG = {
7
+ playsInSilentMode: true,
8
+ allowsRecording: true,
9
+ } as const
10
+
11
+ export class RecorderModel {
12
+ public recorder!: AudioRecorder
13
+
14
+ public readonly reset = createEvent()
15
+ public readonly setAudioFileDurationMs = createEvent<number>()
16
+ public readonly setVoiceRecordState = createEvent<VoiceRecorderState>()
17
+ public readonly setAvailableInputs = createEvent<AvailableAudioInputs>()
18
+
19
+ public readonly $audioFileDurationMs = restore(this.setAudioFileDurationMs, 0).reset(this.reset)
20
+ public readonly $voiceRecordState = restore(
21
+ this.setVoiceRecordState,
22
+ VoiceRecorderState.IDLE,
23
+ ).reset(this.reset)
24
+ public readonly $availableInputs = restore(this.setAvailableInputs, [])
25
+
26
+ public readonly start = createEffect(async () => {
27
+ try {
28
+ await setAudioModeAsync(AUDIO_MODE_CONFIG)
29
+ await this.recorder.prepareToRecordAsync()
30
+ this.recorder.record()
31
+ } catch (error) {
32
+ console.error('Error starting recording:', error)
33
+ throw error
34
+ }
35
+ })
36
+
37
+ public readonly stop = createEffect(async () => {
38
+ return this.recorder.stop()
39
+ })
40
+ }