@syra.fm/sdk 0.4.0 → 0.6.0

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 (317) hide show
  1. package/README.md +33 -4
  2. package/lib/commonjs/client.js +268 -0
  3. package/lib/commonjs/client.js.map +1 -0
  4. package/lib/commonjs/errors.js +22 -0
  5. package/lib/commonjs/errors.js.map +1 -0
  6. package/lib/commonjs/index.js +63 -0
  7. package/lib/commonjs/index.js.map +1 -0
  8. package/lib/commonjs/index.native.js +28 -0
  9. package/lib/commonjs/index.native.js.map +1 -0
  10. package/lib/commonjs/live/assets/icons/spaces-icon.js +42 -0
  11. package/lib/commonjs/live/assets/icons/spaces-icon.js.map +1 -0
  12. package/lib/commonjs/live/assets/icons/syra-icon.js +43 -0
  13. package/lib/commonjs/live/assets/icons/syra-icon.js.map +1 -0
  14. package/lib/commonjs/live/colors.js +54 -0
  15. package/lib/commonjs/live/colors.js.map +1 -0
  16. package/lib/commonjs/live/components/AnimatedPulse.js +38 -0
  17. package/lib/commonjs/live/components/AnimatedPulse.js.map +1 -0
  18. package/lib/commonjs/live/components/AnimatedSpeakerRing.js +90 -0
  19. package/lib/commonjs/live/components/AnimatedSpeakerRing.js.map +1 -0
  20. package/lib/commonjs/live/components/CreateRoomSheet.js +688 -0
  21. package/lib/commonjs/live/components/CreateRoomSheet.js.map +1 -0
  22. package/lib/commonjs/live/components/InsightsPanel.js +333 -0
  23. package/lib/commonjs/live/components/InsightsPanel.js.map +1 -0
  24. package/lib/commonjs/live/components/LiveRoomSheet.js +1196 -0
  25. package/lib/commonjs/live/components/LiveRoomSheet.js.map +1 -0
  26. package/lib/commonjs/live/components/MiniRoomBar.js +162 -0
  27. package/lib/commonjs/live/components/MiniRoomBar.js.map +1 -0
  28. package/lib/commonjs/live/components/PanelHeader.js +57 -0
  29. package/lib/commonjs/live/components/PanelHeader.js.map +1 -0
  30. package/lib/commonjs/live/components/PodcastStreamPicker.js +1190 -0
  31. package/lib/commonjs/live/components/PodcastStreamPicker.js.map +1 -0
  32. package/lib/commonjs/live/components/RecordingCard.js +192 -0
  33. package/lib/commonjs/live/components/RecordingCard.js.map +1 -0
  34. package/lib/commonjs/live/components/RecordingsPanel.js +305 -0
  35. package/lib/commonjs/live/components/RecordingsPanel.js.map +1 -0
  36. package/lib/commonjs/live/components/RoomCard.js +349 -0
  37. package/lib/commonjs/live/components/RoomCard.js.map +1 -0
  38. package/lib/commonjs/live/components/StreamConfigModal.js +656 -0
  39. package/lib/commonjs/live/components/StreamConfigModal.js.map +1 -0
  40. package/lib/commonjs/live/components/StreamConfigPanel.js +822 -0
  41. package/lib/commonjs/live/components/StreamConfigPanel.js.map +1 -0
  42. package/lib/commonjs/live/context/LiveConfigContext.js +46 -0
  43. package/lib/commonjs/live/context/LiveConfigContext.js.map +1 -0
  44. package/lib/commonjs/live/context/LiveRoomContext.js +195 -0
  45. package/lib/commonjs/live/context/LiveRoomContext.js.map +1 -0
  46. package/lib/commonjs/live/hooks/useActiveSpeakers.js +35 -0
  47. package/lib/commonjs/live/hooks/useActiveSpeakers.js.map +1 -0
  48. package/lib/commonjs/live/hooks/useRoomAudio.js +129 -0
  49. package/lib/commonjs/live/hooks/useRoomAudio.js.map +1 -0
  50. package/lib/commonjs/live/hooks/useRoomConnection.js +177 -0
  51. package/lib/commonjs/live/hooks/useRoomConnection.js.map +1 -0
  52. package/lib/commonjs/live/hooks/useRoomManager.js +37 -0
  53. package/lib/commonjs/live/hooks/useRoomManager.js.map +1 -0
  54. package/lib/commonjs/live/hooks/useRoomUsers.js +52 -0
  55. package/lib/commonjs/live/hooks/useRoomUsers.js.map +1 -0
  56. package/lib/commonjs/live/index.js +341 -0
  57. package/lib/commonjs/live/index.js.map +1 -0
  58. package/lib/commonjs/live/services/livekitService.js +20 -0
  59. package/lib/commonjs/live/services/livekitService.js.map +1 -0
  60. package/lib/commonjs/live/services/spaceSocketService.js +181 -0
  61. package/lib/commonjs/live/services/spaceSocketService.js.map +1 -0
  62. package/lib/commonjs/live/services/spacesService.js +642 -0
  63. package/lib/commonjs/live/services/spacesService.js.map +1 -0
  64. package/lib/commonjs/live/types.js +6 -0
  65. package/lib/commonjs/live/types.js.map +1 -0
  66. package/lib/commonjs/live/validation.js +238 -0
  67. package/lib/commonjs/live/validation.js.map +1 -0
  68. package/lib/commonjs/package.json +1 -0
  69. package/{dist/cjs → lib/commonjs}/schema.js +55 -47
  70. package/lib/commonjs/schema.js.map +1 -0
  71. package/lib/module/client.js +264 -0
  72. package/lib/module/client.js.map +1 -0
  73. package/lib/module/errors.js +17 -0
  74. package/lib/module/errors.js.map +1 -0
  75. package/lib/module/index.js +6 -0
  76. package/lib/module/index.js.map +1 -0
  77. package/lib/module/index.native.js +5 -0
  78. package/lib/module/index.native.js.map +1 -0
  79. package/lib/module/live/assets/icons/spaces-icon.js +34 -0
  80. package/lib/module/live/assets/icons/spaces-icon.js.map +1 -0
  81. package/lib/module/live/assets/icons/syra-icon.js +36 -0
  82. package/lib/module/live/assets/icons/syra-icon.js.map +1 -0
  83. package/lib/module/live/colors.js +49 -0
  84. package/lib/module/live/colors.js.map +1 -0
  85. package/lib/module/live/components/AnimatedPulse.js +33 -0
  86. package/lib/module/live/components/AnimatedPulse.js.map +1 -0
  87. package/lib/module/live/components/AnimatedSpeakerRing.js +85 -0
  88. package/lib/module/live/components/AnimatedSpeakerRing.js.map +1 -0
  89. package/lib/module/live/components/CreateRoomSheet.js +682 -0
  90. package/lib/module/live/components/CreateRoomSheet.js.map +1 -0
  91. package/lib/module/live/components/InsightsPanel.js +327 -0
  92. package/lib/module/live/components/InsightsPanel.js.map +1 -0
  93. package/lib/module/live/components/LiveRoomSheet.js +1190 -0
  94. package/lib/module/live/components/LiveRoomSheet.js.map +1 -0
  95. package/lib/module/live/components/MiniRoomBar.js +155 -0
  96. package/lib/module/live/components/MiniRoomBar.js.map +1 -0
  97. package/lib/module/live/components/PanelHeader.js +52 -0
  98. package/lib/module/live/components/PanelHeader.js.map +1 -0
  99. package/lib/module/live/components/PodcastStreamPicker.js +1183 -0
  100. package/lib/module/live/components/PodcastStreamPicker.js.map +1 -0
  101. package/lib/module/live/components/RecordingCard.js +187 -0
  102. package/lib/module/live/components/RecordingCard.js.map +1 -0
  103. package/lib/module/live/components/RecordingsPanel.js +299 -0
  104. package/lib/module/live/components/RecordingsPanel.js.map +1 -0
  105. package/lib/module/live/components/RoomCard.js +343 -0
  106. package/lib/module/live/components/RoomCard.js.map +1 -0
  107. package/lib/module/live/components/StreamConfigModal.js +650 -0
  108. package/lib/module/live/components/StreamConfigModal.js.map +1 -0
  109. package/lib/module/live/components/StreamConfigPanel.js +816 -0
  110. package/lib/module/live/components/StreamConfigPanel.js.map +1 -0
  111. package/lib/module/live/context/LiveConfigContext.js +40 -0
  112. package/lib/module/live/context/LiveConfigContext.js.map +1 -0
  113. package/lib/module/live/context/LiveRoomContext.js +189 -0
  114. package/lib/module/live/context/LiveRoomContext.js.map +1 -0
  115. package/lib/module/live/hooks/useActiveSpeakers.js +31 -0
  116. package/lib/module/live/hooks/useActiveSpeakers.js.map +1 -0
  117. package/lib/module/live/hooks/useRoomAudio.js +125 -0
  118. package/lib/module/live/hooks/useRoomAudio.js.map +1 -0
  119. package/lib/module/live/hooks/useRoomConnection.js +173 -0
  120. package/lib/module/live/hooks/useRoomConnection.js.map +1 -0
  121. package/lib/module/live/hooks/useRoomManager.js +32 -0
  122. package/lib/module/live/hooks/useRoomManager.js.map +1 -0
  123. package/lib/module/live/hooks/useRoomUsers.js +46 -0
  124. package/lib/module/live/hooks/useRoomUsers.js.map +1 -0
  125. package/lib/module/live/index.js +42 -0
  126. package/lib/module/live/index.js.map +1 -0
  127. package/lib/module/live/services/livekitService.js +16 -0
  128. package/lib/module/live/services/livekitService.js.map +1 -0
  129. package/lib/module/live/services/spaceSocketService.js +176 -0
  130. package/lib/module/live/services/spaceSocketService.js.map +1 -0
  131. package/lib/module/live/services/spacesService.js +639 -0
  132. package/lib/module/live/services/spacesService.js.map +1 -0
  133. package/lib/module/live/types.js +4 -0
  134. package/lib/module/live/types.js.map +1 -0
  135. package/lib/module/live/validation.js +229 -0
  136. package/lib/module/live/validation.js.map +1 -0
  137. package/lib/module/package.json +1 -0
  138. package/{dist/esm → lib/module}/schema.js +47 -39
  139. package/lib/module/schema.js.map +1 -0
  140. package/{dist/types → lib/typescript/commonjs}/client.d.ts +1 -0
  141. package/lib/typescript/commonjs/client.d.ts.map +1 -0
  142. package/{dist/types → lib/typescript/commonjs}/errors.d.ts +1 -0
  143. package/lib/typescript/commonjs/errors.d.ts.map +1 -0
  144. package/{dist/types → lib/typescript/commonjs}/index.d.ts +1 -0
  145. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  146. package/lib/typescript/commonjs/index.native.d.ts +3 -0
  147. package/lib/typescript/commonjs/index.native.d.ts.map +1 -0
  148. package/lib/typescript/commonjs/live/assets/icons/spaces-icon.d.ts +13 -0
  149. package/lib/typescript/commonjs/live/assets/icons/spaces-icon.d.ts.map +1 -0
  150. package/lib/typescript/commonjs/live/assets/icons/syra-icon.d.ts +13 -0
  151. package/lib/typescript/commonjs/live/assets/icons/syra-icon.d.ts.map +1 -0
  152. package/lib/typescript/commonjs/live/colors.d.ts +35 -0
  153. package/lib/typescript/commonjs/live/colors.d.ts.map +1 -0
  154. package/lib/typescript/commonjs/live/components/AnimatedPulse.d.ts +8 -0
  155. package/lib/typescript/commonjs/live/components/AnimatedPulse.d.ts.map +1 -0
  156. package/lib/typescript/commonjs/live/components/AnimatedSpeakerRing.d.ts +10 -0
  157. package/lib/typescript/commonjs/live/components/AnimatedSpeakerRing.d.ts.map +1 -0
  158. package/lib/typescript/commonjs/live/components/CreateRoomSheet.d.ts +26 -0
  159. package/lib/typescript/commonjs/live/components/CreateRoomSheet.d.ts.map +1 -0
  160. package/lib/typescript/commonjs/live/components/InsightsPanel.d.ts +11 -0
  161. package/lib/typescript/commonjs/live/components/InsightsPanel.d.ts.map +1 -0
  162. package/lib/typescript/commonjs/live/components/LiveRoomSheet.d.ts +11 -0
  163. package/lib/typescript/commonjs/live/components/LiveRoomSheet.d.ts.map +1 -0
  164. package/lib/typescript/commonjs/live/components/MiniRoomBar.d.ts +15 -0
  165. package/lib/typescript/commonjs/live/components/MiniRoomBar.d.ts.map +1 -0
  166. package/lib/typescript/commonjs/live/components/PanelHeader.d.ts +10 -0
  167. package/lib/typescript/commonjs/live/components/PanelHeader.d.ts.map +1 -0
  168. package/lib/typescript/commonjs/live/components/PodcastStreamPicker.d.ts +54 -0
  169. package/lib/typescript/commonjs/live/components/PodcastStreamPicker.d.ts.map +1 -0
  170. package/lib/typescript/commonjs/live/components/RecordingCard.d.ts +11 -0
  171. package/lib/typescript/commonjs/live/components/RecordingCard.d.ts.map +1 -0
  172. package/lib/typescript/commonjs/live/components/RecordingsPanel.d.ts +12 -0
  173. package/lib/typescript/commonjs/live/components/RecordingsPanel.d.ts.map +1 -0
  174. package/lib/typescript/commonjs/live/components/RoomCard.d.ts +38 -0
  175. package/lib/typescript/commonjs/live/components/RoomCard.d.ts.map +1 -0
  176. package/lib/typescript/commonjs/live/components/StreamConfigModal.d.ts +11 -0
  177. package/lib/typescript/commonjs/live/components/StreamConfigModal.d.ts.map +1 -0
  178. package/lib/typescript/commonjs/live/components/StreamConfigPanel.d.ts +13 -0
  179. package/lib/typescript/commonjs/live/components/StreamConfigPanel.d.ts.map +1 -0
  180. package/lib/typescript/commonjs/live/context/LiveConfigContext.d.ts +78 -0
  181. package/lib/typescript/commonjs/live/context/LiveConfigContext.d.ts.map +1 -0
  182. package/lib/typescript/commonjs/live/context/LiveRoomContext.d.ts +12 -0
  183. package/lib/typescript/commonjs/live/context/LiveRoomContext.d.ts.map +1 -0
  184. package/lib/typescript/commonjs/live/hooks/useActiveSpeakers.d.ts +3 -0
  185. package/lib/typescript/commonjs/live/hooks/useActiveSpeakers.d.ts.map +1 -0
  186. package/lib/typescript/commonjs/live/hooks/useRoomAudio.d.ts +16 -0
  187. package/lib/typescript/commonjs/live/hooks/useRoomAudio.d.ts.map +1 -0
  188. package/lib/typescript/commonjs/live/hooks/useRoomConnection.d.ts +29 -0
  189. package/lib/typescript/commonjs/live/hooks/useRoomConnection.d.ts.map +1 -0
  190. package/lib/typescript/commonjs/live/hooks/useRoomManager.d.ts +11 -0
  191. package/lib/typescript/commonjs/live/hooks/useRoomManager.d.ts.map +1 -0
  192. package/lib/typescript/commonjs/live/hooks/useRoomUsers.d.ts +5 -0
  193. package/lib/typescript/commonjs/live/hooks/useRoomUsers.d.ts.map +1 -0
  194. package/lib/typescript/commonjs/live/index.d.ts +25 -0
  195. package/lib/typescript/commonjs/live/index.d.ts.map +1 -0
  196. package/lib/typescript/commonjs/live/services/livekitService.d.ts +7 -0
  197. package/lib/typescript/commonjs/live/services/livekitService.d.ts.map +1 -0
  198. package/lib/typescript/commonjs/live/services/spaceSocketService.d.ts +69 -0
  199. package/lib/typescript/commonjs/live/services/spaceSocketService.d.ts.map +1 -0
  200. package/lib/typescript/commonjs/live/services/spacesService.d.ts +181 -0
  201. package/lib/typescript/commonjs/live/services/spacesService.d.ts.map +1 -0
  202. package/lib/typescript/commonjs/live/types.d.ts +100 -0
  203. package/lib/typescript/commonjs/live/types.d.ts.map +1 -0
  204. package/lib/typescript/commonjs/live/validation.d.ts +658 -0
  205. package/lib/typescript/commonjs/live/validation.d.ts.map +1 -0
  206. package/lib/typescript/commonjs/package.json +1 -0
  207. package/{dist/types → lib/typescript/commonjs}/schema.d.ts +1 -0
  208. package/lib/typescript/commonjs/schema.d.ts.map +1 -0
  209. package/lib/typescript/module/client.d.ts +141 -0
  210. package/lib/typescript/module/client.d.ts.map +1 -0
  211. package/lib/typescript/module/errors.d.ts +9 -0
  212. package/lib/typescript/module/errors.d.ts.map +1 -0
  213. package/lib/typescript/module/index.d.ts +6 -0
  214. package/lib/typescript/module/index.d.ts.map +1 -0
  215. package/lib/typescript/module/index.native.d.ts +3 -0
  216. package/lib/typescript/module/index.native.d.ts.map +1 -0
  217. package/lib/typescript/module/live/assets/icons/spaces-icon.d.ts +13 -0
  218. package/lib/typescript/module/live/assets/icons/spaces-icon.d.ts.map +1 -0
  219. package/lib/typescript/module/live/assets/icons/syra-icon.d.ts +13 -0
  220. package/lib/typescript/module/live/assets/icons/syra-icon.d.ts.map +1 -0
  221. package/lib/typescript/module/live/colors.d.ts +35 -0
  222. package/lib/typescript/module/live/colors.d.ts.map +1 -0
  223. package/lib/typescript/module/live/components/AnimatedPulse.d.ts +8 -0
  224. package/lib/typescript/module/live/components/AnimatedPulse.d.ts.map +1 -0
  225. package/lib/typescript/module/live/components/AnimatedSpeakerRing.d.ts +10 -0
  226. package/lib/typescript/module/live/components/AnimatedSpeakerRing.d.ts.map +1 -0
  227. package/lib/typescript/module/live/components/CreateRoomSheet.d.ts +26 -0
  228. package/lib/typescript/module/live/components/CreateRoomSheet.d.ts.map +1 -0
  229. package/lib/typescript/module/live/components/InsightsPanel.d.ts +11 -0
  230. package/lib/typescript/module/live/components/InsightsPanel.d.ts.map +1 -0
  231. package/lib/typescript/module/live/components/LiveRoomSheet.d.ts +11 -0
  232. package/lib/typescript/module/live/components/LiveRoomSheet.d.ts.map +1 -0
  233. package/lib/typescript/module/live/components/MiniRoomBar.d.ts +15 -0
  234. package/lib/typescript/module/live/components/MiniRoomBar.d.ts.map +1 -0
  235. package/lib/typescript/module/live/components/PanelHeader.d.ts +10 -0
  236. package/lib/typescript/module/live/components/PanelHeader.d.ts.map +1 -0
  237. package/lib/typescript/module/live/components/PodcastStreamPicker.d.ts +54 -0
  238. package/lib/typescript/module/live/components/PodcastStreamPicker.d.ts.map +1 -0
  239. package/lib/typescript/module/live/components/RecordingCard.d.ts +11 -0
  240. package/lib/typescript/module/live/components/RecordingCard.d.ts.map +1 -0
  241. package/lib/typescript/module/live/components/RecordingsPanel.d.ts +12 -0
  242. package/lib/typescript/module/live/components/RecordingsPanel.d.ts.map +1 -0
  243. package/lib/typescript/module/live/components/RoomCard.d.ts +38 -0
  244. package/lib/typescript/module/live/components/RoomCard.d.ts.map +1 -0
  245. package/lib/typescript/module/live/components/StreamConfigModal.d.ts +11 -0
  246. package/lib/typescript/module/live/components/StreamConfigModal.d.ts.map +1 -0
  247. package/lib/typescript/module/live/components/StreamConfigPanel.d.ts +13 -0
  248. package/lib/typescript/module/live/components/StreamConfigPanel.d.ts.map +1 -0
  249. package/lib/typescript/module/live/context/LiveConfigContext.d.ts +78 -0
  250. package/lib/typescript/module/live/context/LiveConfigContext.d.ts.map +1 -0
  251. package/lib/typescript/module/live/context/LiveRoomContext.d.ts +12 -0
  252. package/lib/typescript/module/live/context/LiveRoomContext.d.ts.map +1 -0
  253. package/lib/typescript/module/live/hooks/useActiveSpeakers.d.ts +3 -0
  254. package/lib/typescript/module/live/hooks/useActiveSpeakers.d.ts.map +1 -0
  255. package/lib/typescript/module/live/hooks/useRoomAudio.d.ts +16 -0
  256. package/lib/typescript/module/live/hooks/useRoomAudio.d.ts.map +1 -0
  257. package/lib/typescript/module/live/hooks/useRoomConnection.d.ts +29 -0
  258. package/lib/typescript/module/live/hooks/useRoomConnection.d.ts.map +1 -0
  259. package/lib/typescript/module/live/hooks/useRoomManager.d.ts +11 -0
  260. package/lib/typescript/module/live/hooks/useRoomManager.d.ts.map +1 -0
  261. package/lib/typescript/module/live/hooks/useRoomUsers.d.ts +5 -0
  262. package/lib/typescript/module/live/hooks/useRoomUsers.d.ts.map +1 -0
  263. package/lib/typescript/module/live/index.d.ts +25 -0
  264. package/lib/typescript/module/live/index.d.ts.map +1 -0
  265. package/lib/typescript/module/live/services/livekitService.d.ts +7 -0
  266. package/lib/typescript/module/live/services/livekitService.d.ts.map +1 -0
  267. package/lib/typescript/module/live/services/spaceSocketService.d.ts +69 -0
  268. package/lib/typescript/module/live/services/spaceSocketService.d.ts.map +1 -0
  269. package/lib/typescript/module/live/services/spacesService.d.ts +181 -0
  270. package/lib/typescript/module/live/services/spacesService.d.ts.map +1 -0
  271. package/lib/typescript/module/live/types.d.ts +100 -0
  272. package/lib/typescript/module/live/types.d.ts.map +1 -0
  273. package/lib/typescript/module/live/validation.d.ts +658 -0
  274. package/lib/typescript/module/live/validation.d.ts.map +1 -0
  275. package/lib/typescript/module/package.json +1 -0
  276. package/lib/typescript/module/schema.d.ts +1072 -0
  277. package/lib/typescript/module/schema.d.ts.map +1 -0
  278. package/package.json +123 -21
  279. package/src/index.native.ts +2 -0
  280. package/src/live/assets/icons/spaces-icon.tsx +15 -0
  281. package/src/live/assets/icons/syra-icon.tsx +13 -0
  282. package/src/live/assets/sounds/intro.mp3 +0 -0
  283. package/src/live/colors.ts +57 -0
  284. package/src/live/components/AnimatedPulse.tsx +52 -0
  285. package/src/live/components/AnimatedSpeakerRing.tsx +101 -0
  286. package/src/live/components/CreateRoomSheet.tsx +654 -0
  287. package/src/live/components/InsightsPanel.tsx +202 -0
  288. package/src/live/components/LiveRoomSheet.tsx +970 -0
  289. package/src/live/components/MiniRoomBar.tsx +167 -0
  290. package/src/live/components/PanelHeader.tsx +36 -0
  291. package/src/live/components/PodcastStreamPicker.tsx +1155 -0
  292. package/src/live/components/RecordingCard.tsx +154 -0
  293. package/src/live/components/RecordingsPanel.tsx +252 -0
  294. package/src/live/components/RoomCard.tsx +343 -0
  295. package/src/live/components/StreamConfigModal.tsx +600 -0
  296. package/src/live/components/StreamConfigPanel.tsx +729 -0
  297. package/src/live/context/LiveConfigContext.tsx +96 -0
  298. package/src/live/context/LiveRoomContext.tsx +235 -0
  299. package/src/live/hooks/useActiveSpeakers.ts +39 -0
  300. package/src/live/hooks/useRoomAudio.ts +103 -0
  301. package/src/live/hooks/useRoomConnection.ts +162 -0
  302. package/src/live/hooks/useRoomManager.ts +17 -0
  303. package/src/live/hooks/useRoomUsers.ts +52 -0
  304. package/src/live/index.ts +104 -0
  305. package/src/live/services/livekitService.ts +11 -0
  306. package/src/live/services/spaceSocketService.ts +136 -0
  307. package/src/live/services/spacesService.ts +692 -0
  308. package/src/live/types.ts +119 -0
  309. package/src/live/validation.ts +279 -0
  310. package/dist/cjs/client.js +0 -244
  311. package/dist/cjs/errors.js +0 -16
  312. package/dist/cjs/index.js +0 -15
  313. package/dist/cjs/package.json +0 -3
  314. package/dist/esm/client.js +0 -240
  315. package/dist/esm/errors.js +0 -12
  316. package/dist/esm/index.js +0 -3
  317. package/dist/esm/package.json +0 -3
@@ -0,0 +1,970 @@
1
+ import React, { useCallback, useEffect, useState, useMemo } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ Image,
6
+ StyleSheet,
7
+ TouchableOpacity,
8
+ ScrollView,
9
+ type ViewStyle,
10
+ } from 'react-native';
11
+ import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons';
12
+ import { useAuth } from '@oxyhq/services';
13
+
14
+ import { useLiveConfig } from '../context/LiveConfigContext';
15
+ import { MiniRoomBar } from './MiniRoomBar';
16
+ import { StreamConfigPanel } from './StreamConfigPanel';
17
+ import { InsightsPanel } from './InsightsPanel';
18
+ import { PanelHeader } from './PanelHeader';
19
+ import { AnimatedSpeakerRing } from './AnimatedSpeakerRing';
20
+ import { AnimatedPulse } from './AnimatedPulse';
21
+ import { useRoomConnection } from '../hooks/useRoomConnection';
22
+ import { useRoomAudio } from '../hooks/useRoomAudio';
23
+ import { useActiveSpeakers } from '../hooks/useActiveSpeakers';
24
+ import { useRoomUsers, getDisplayName, getAvatarUrl } from '../hooks/useRoomUsers';
25
+ import { LIVE_COLOR, LIVE_TINT_COLOR } from '../colors';
26
+ import type { RoomParticipant, Room, StreamInfo, UserEntity, LiveTheme } from '../types';
27
+
28
+ type ActivePanel = null | 'stream' | 'insights' | 'settings';
29
+
30
+ /**
31
+ * English source copy for the podcast host-control strings added to the stream
32
+ * card. Hosts with an i18n layer inject `t`; hosts without one fall back here.
33
+ * Keys mirror the `agora.podcastStream.*` entries in the app locale files.
34
+ */
35
+ const LIVE_ROOM_STRINGS: Record<string, string> = {
36
+ 'agora.podcastStream.skipNext': 'Skip to next episode',
37
+ 'agora.podcastStream.skipped': 'Skipped to the next episode',
38
+ 'agora.podcastStream.queueEnded': 'Podcast queue finished',
39
+ 'agora.podcastStream.skipFailed': "Couldn't skip to the next episode",
40
+ };
41
+
42
+ /** Format a second count as `m:ss` (or `h:mm:ss` when hours are relevant). */
43
+ function formatClock(totalSeconds: number, withHours: boolean): string {
44
+ const s = Math.max(0, Math.floor(totalSeconds));
45
+ const hours = Math.floor(s / 3600);
46
+ const minutes = Math.floor((s % 3600) / 60);
47
+ const secs = s % 60;
48
+ const pad = (n: number) => String(n).padStart(2, '0');
49
+ return withHours ? `${hours}:${pad(minutes)}:${pad(secs)}` : `${minutes}:${pad(secs)}`;
50
+ }
51
+
52
+ type AvatarComponentType = React.ComponentType<{ size: number; source?: string; shape?: string; style?: ViewStyle }>;
53
+ type CachedFileDownloadUrlSyncFn = (oxyServices: unknown, fileId: string, variant?: string) => string;
54
+
55
+ const RoleBadge = ({ role, theme }: { role: string; theme: LiveTheme }) => {
56
+ if (role === 'host') {
57
+ return (
58
+ <View style={[styles.roleBadge, { backgroundColor: theme.colors.primary }]}>
59
+ <Text style={styles.roleBadgeText}>Host</Text>
60
+ </View>
61
+ );
62
+ }
63
+ if (role === 'speaker') {
64
+ return (
65
+ <View style={[styles.roleBadge, { backgroundColor: theme.colors.backgroundSecondary }]}>
66
+ <Text style={[styles.roleBadgeText, { color: theme.colors.text }]}>Speaker</Text>
67
+ </View>
68
+ );
69
+ }
70
+ return null;
71
+ };
72
+
73
+ const SpeakerTile = ({
74
+ participant,
75
+ isCurrentUser,
76
+ isSpeaking,
77
+ theme,
78
+ userProfile,
79
+ oxyServices,
80
+ AvatarComponent,
81
+ getCachedFileDownloadUrlSync,
82
+ }: {
83
+ participant: RoomParticipant;
84
+ isCurrentUser: boolean;
85
+ isSpeaking: boolean;
86
+ theme: LiveTheme;
87
+ userProfile: UserEntity | undefined;
88
+ oxyServices: unknown;
89
+ AvatarComponent: AvatarComponentType;
90
+ getCachedFileDownloadUrlSync: CachedFileDownloadUrlSyncFn;
91
+ }) => {
92
+ const displayName = getDisplayName(userProfile, participant.userId, isCurrentUser);
93
+ const avatarUri = getAvatarUrl(userProfile, oxyServices, getCachedFileDownloadUrlSync);
94
+
95
+ return (
96
+ <View style={styles.speakerTile}>
97
+ <AnimatedSpeakerRing
98
+ isSpeaking={isSpeaking}
99
+ isMuted={participant.isMuted}
100
+ primaryColor={theme.colors.primary}
101
+ >
102
+ <AvatarComponent size={64} source={avatarUri} shape="squircle" />
103
+ {participant.isMuted && (
104
+ <View style={[styles.muteIndicator, { backgroundColor: LIVE_COLOR }]}>
105
+ <MaterialCommunityIcons name="microphone-off" size={12} color="#FFFFFF" />
106
+ </View>
107
+ )}
108
+ </AnimatedSpeakerRing>
109
+ <Text style={[styles.speakerName, { color: theme.colors.text }]} numberOfLines={1}>
110
+ {displayName}
111
+ </Text>
112
+ <RoleBadge role={participant.role} theme={theme} />
113
+ </View>
114
+ );
115
+ };
116
+
117
+ const ListenerAvatar = ({
118
+ participant,
119
+ userProfile,
120
+ oxyServices,
121
+ AvatarComponent,
122
+ getCachedFileDownloadUrlSync,
123
+ }: {
124
+ participant: RoomParticipant;
125
+ userProfile: UserEntity | undefined;
126
+ oxyServices: unknown;
127
+ AvatarComponent: AvatarComponentType;
128
+ getCachedFileDownloadUrlSync: CachedFileDownloadUrlSyncFn;
129
+ }) => {
130
+ return (
131
+ <View style={styles.listenerItem}>
132
+ <AvatarComponent size={40} source={getAvatarUrl(userProfile, oxyServices, getCachedFileDownloadUrlSync)} shape="squircle" />
133
+ </View>
134
+ );
135
+ };
136
+
137
+ const ConnectedSpeakerTile = ({ participant, isCurrentUser, isSpeaking, theme, oxyServices, AvatarComponent, getCachedFileDownloadUrlSync, useUserById }: {
138
+ participant: RoomParticipant; isCurrentUser: boolean; isSpeaking: boolean; theme: LiveTheme; oxyServices: unknown;
139
+ AvatarComponent: AvatarComponentType; getCachedFileDownloadUrlSync: CachedFileDownloadUrlSyncFn; useUserById: (id: string | undefined) => UserEntity | undefined;
140
+ }) => {
141
+ const userProfile = useUserById(participant.userId);
142
+ return <SpeakerTile participant={participant} isCurrentUser={isCurrentUser} isSpeaking={isSpeaking} theme={theme} userProfile={userProfile} oxyServices={oxyServices} AvatarComponent={AvatarComponent} getCachedFileDownloadUrlSync={getCachedFileDownloadUrlSync} />;
143
+ };
144
+
145
+ const ConnectedListenerAvatar = ({ participant, oxyServices, AvatarComponent, getCachedFileDownloadUrlSync, useUserById }: {
146
+ participant: RoomParticipant; oxyServices: unknown;
147
+ AvatarComponent: AvatarComponentType; getCachedFileDownloadUrlSync: CachedFileDownloadUrlSyncFn; useUserById: (id: string | undefined) => UserEntity | undefined;
148
+ }) => {
149
+ const userProfile = useUserById(participant.userId);
150
+ return <ListenerAvatar participant={participant} userProfile={userProfile} oxyServices={oxyServices} AvatarComponent={AvatarComponent} getCachedFileDownloadUrlSync={getCachedFileDownloadUrlSync} />;
151
+ };
152
+
153
+ const ConnectedRequestRow = ({ request, theme, oxyServices, onApprove, onDeny, AvatarComponent, getCachedFileDownloadUrlSync, useUserById }: {
154
+ request: { userId: string; requestedAt: string }; theme: LiveTheme; oxyServices: unknown;
155
+ onApprove: (userId: string) => void; onDeny: (userId: string) => void;
156
+ AvatarComponent: AvatarComponentType; getCachedFileDownloadUrlSync: CachedFileDownloadUrlSyncFn; useUserById: (id: string | undefined) => UserEntity | undefined;
157
+ }) => {
158
+ const userProfile = useUserById(request.userId);
159
+ const displayName = getDisplayName(userProfile, request.userId, false);
160
+ const avatarUri = getAvatarUrl(userProfile, oxyServices, getCachedFileDownloadUrlSync);
161
+
162
+ return (
163
+ <View style={[styles.requestRow, { backgroundColor: `${theme.colors.card}80`, borderColor: theme.colors.border }]}>
164
+ <AvatarComponent size={36} source={avatarUri} shape="squircle" />
165
+ <Text style={[styles.requestName, { color: theme.colors.text }]} numberOfLines={1}>
166
+ {displayName}
167
+ </Text>
168
+ <TouchableOpacity onPress={() => onApprove(request.userId)} style={[styles.approveBtn, { backgroundColor: theme.colors.primary }]}>
169
+ <MaterialCommunityIcons name="check" size={18} color="#FFFFFF" />
170
+ </TouchableOpacity>
171
+ <TouchableOpacity onPress={() => onDeny(request.userId)} style={[styles.denyBtn, { backgroundColor: theme.colors.backgroundSecondary }]}>
172
+ <MaterialCommunityIcons name="close" size={18} color={theme.colors.text} />
173
+ </TouchableOpacity>
174
+ </View>
175
+ );
176
+ };
177
+
178
+ interface LiveRoomSheetProps {
179
+ roomId: string;
180
+ isExpanded: boolean;
181
+ onCollapse: () => void;
182
+ onExpand: () => void;
183
+ onLeave: () => void;
184
+ }
185
+
186
+ export function LiveRoomSheet({ roomId, isExpanded, onCollapse, onExpand, onLeave }: LiveRoomSheetProps) {
187
+ const { useTheme, useUserById, AvatarComponent, roomsService, toast, getCachedFileDownloadUrl, getCachedFileDownloadUrlSync, onRoomChanged, t } = useLiveConfig();
188
+ const theme = useTheme();
189
+ const { user, oxyServices } = useAuth();
190
+ const [room, setRoom] = useState<Room | null>(null);
191
+
192
+ const tr = useCallback(
193
+ (key: string) => (t ? t(key) : LIVE_ROOM_STRINGS[key] ?? key),
194
+ [t],
195
+ );
196
+
197
+ useEffect(() => {
198
+ if (roomId) {
199
+ roomsService.getRoom(roomId).then(setRoom);
200
+ }
201
+ }, [roomId]);
202
+
203
+ const {
204
+ isConnected,
205
+ participants,
206
+ myRole,
207
+ isMuted,
208
+ speakerRequests,
209
+ activeStream,
210
+ isRecording,
211
+ join,
212
+ leave,
213
+ toggleMute,
214
+ requestToSpeak,
215
+ approveSpeaker,
216
+ denySpeaker,
217
+ isRoomEnded,
218
+ } = useRoomConnection({ roomId, enabled: !!roomId });
219
+
220
+ const isRoomLive = room?.status === 'live';
221
+ const { isLiveKitConnected, micPermissionDenied, livekitRoom } = useRoomAudio({
222
+ roomId,
223
+ isSpeaker: myRole === 'speaker' || myRole === 'host',
224
+ isMuted,
225
+ isConnected: isConnected && isRoomLive,
226
+ });
227
+
228
+ const activeSpeakerIds = useActiveSpeakers(livekitRoom);
229
+
230
+ useEffect(() => {
231
+ if (isConnected && roomId) {
232
+ join();
233
+ }
234
+ }, [isConnected, roomId, join]);
235
+
236
+ useEffect(() => {
237
+ if (isRoomEnded) {
238
+ toast('Room ended');
239
+ leave();
240
+ onLeave();
241
+ }
242
+ }, [isRoomEnded]);
243
+
244
+ const handleLeave = () => {
245
+ leave();
246
+ onLeave();
247
+ };
248
+
249
+ const handleStopRoom = async () => {
250
+ if (!roomId) return;
251
+ const success = await roomsService.stopRoom(roomId);
252
+ if (success) {
253
+ onRoomChanged?.(roomId);
254
+ leave();
255
+ onLeave();
256
+ } else {
257
+ toast.error('Failed to stop session');
258
+ }
259
+ };
260
+
261
+ const handleDeleteRoom = async () => {
262
+ if (!roomId) return;
263
+ const success = await roomsService.deleteRoom(roomId);
264
+ if (success) {
265
+ onRoomChanged?.(roomId);
266
+ leave();
267
+ onLeave();
268
+ toast.success('Room deleted');
269
+ } else {
270
+ toast.error('Failed to delete room');
271
+ }
272
+ };
273
+
274
+ const [startingRoom, setStartingRoom] = useState(false);
275
+ const handleStartRoom = async () => {
276
+ if (!roomId || startingRoom) return;
277
+ setStartingRoom(true);
278
+ try {
279
+ const success = await roomsService.startRoom(roomId);
280
+ if (success) {
281
+ const updated = await roomsService.getRoom(roomId);
282
+ if (updated) setRoom(updated);
283
+ onRoomChanged?.(roomId);
284
+ toast.success('Room is now live!');
285
+ } else {
286
+ toast.error('Failed to start room');
287
+ }
288
+ } catch {
289
+ toast.error('Failed to start room');
290
+ } finally {
291
+ setStartingRoom(false);
292
+ }
293
+ };
294
+
295
+ const handleStartRecording = async () => {
296
+ if (!roomId) return;
297
+ const success = await roomsService.startRecording(roomId);
298
+ if (success) {
299
+ toast.success('Recording started');
300
+ } else {
301
+ toast.error('Failed to start recording');
302
+ }
303
+ };
304
+
305
+ const handleStopRecording = async () => {
306
+ if (!roomId) return;
307
+ const success = await roomsService.stopRecording(roomId);
308
+ if (success) {
309
+ toast.success('Recording saved');
310
+ } else {
311
+ toast.error('Failed to stop recording');
312
+ }
313
+ };
314
+
315
+ const [activePanel, setActivePanel] = useState<ActivePanel>(null);
316
+ const [streamLoading, setStreamLoading] = useState(false);
317
+
318
+ const effectiveStream: StreamInfo | null = activeStream
319
+ ?? (room?.streamTitle || room?.activeStreamUrl
320
+ ? {
321
+ title: room.streamTitle ?? undefined,
322
+ image: room.streamImage ?? undefined,
323
+ description: room.streamDescription ?? undefined,
324
+ startedAt: room.streamStartedAt ?? undefined,
325
+ durationSec: room.streamDurationSec ?? undefined,
326
+ }
327
+ : null);
328
+
329
+ const [streamImageUrl, setStreamImageUrl] = useState<string | null>(null);
330
+ useEffect(() => {
331
+ if (effectiveStream?.image) {
332
+ getCachedFileDownloadUrl(oxyServices, effectiveStream.image).then(setStreamImageUrl);
333
+ } else {
334
+ setStreamImageUrl(null);
335
+ }
336
+ }, [effectiveStream?.image, oxyServices]);
337
+
338
+ // Stream progress — only for length-known streams (e.g. podcast episodes).
339
+ const streamStartedAt = effectiveStream?.startedAt;
340
+ const streamDurationSec = effectiveStream?.durationSec ?? 0;
341
+ const hasProgress = !!streamStartedAt && streamDurationSec > 0;
342
+
343
+ // A lightweight 1s tick that advances the elapsed readout while a length-known
344
+ // stream plays. Runs only when there's progress to show; cleared on unmount
345
+ // and whenever the stream clears.
346
+ const [nowMs, setNowMs] = useState(() => Date.now());
347
+ useEffect(() => {
348
+ if (!hasProgress) return;
349
+ setNowMs(Date.now());
350
+ const id = setInterval(() => setNowMs(Date.now()), 1000);
351
+ return () => clearInterval(id);
352
+ }, [hasProgress]);
353
+
354
+ const elapsedSec = useMemo(() => {
355
+ if (!hasProgress || !streamStartedAt) return 0;
356
+ const startMs = new Date(streamStartedAt).getTime();
357
+ if (Number.isNaN(startMs)) return 0;
358
+ return Math.min(Math.max(Math.floor((nowMs - startMs) / 1000), 0), streamDurationSec);
359
+ }, [hasProgress, streamStartedAt, nowMs, streamDurationSec]);
360
+
361
+ const progressPct = hasProgress ? Math.min(Math.max(elapsedSec / streamDurationSec, 0), 1) * 100 : 0;
362
+ const progressFillStyle: ViewStyle = { backgroundColor: theme.colors.primary, width: `${progressPct}%` };
363
+ const withHours = streamDurationSec >= 3600;
364
+
365
+ const handleStopStream = async () => {
366
+ if (!roomId || streamLoading) return;
367
+ setStreamLoading(true);
368
+ try {
369
+ const success = await roomsService.stopStream(roomId);
370
+ if (success) {
371
+ toast.success('Stream stopped');
372
+ } else {
373
+ toast.error('Failed to stop stream');
374
+ }
375
+ } catch {
376
+ toast.error('Failed to stop stream');
377
+ } finally {
378
+ setStreamLoading(false);
379
+ }
380
+ };
381
+
382
+ const [skipLoading, setSkipLoading] = useState(false);
383
+ const handleSkipNext = async () => {
384
+ if (!roomId || skipLoading) return;
385
+ setSkipLoading(true);
386
+ try {
387
+ const result = await roomsService.skipPodcastNext(roomId);
388
+ if (!result) {
389
+ toast.error(tr('agora.podcastStream.skipFailed'));
390
+ } else {
391
+ toast.success(tr(result.ended ? 'agora.podcastStream.queueEnded' : 'agora.podcastStream.skipped'));
392
+ const updated = await roomsService.getRoom(roomId);
393
+ if (updated) setRoom(updated);
394
+ }
395
+ } catch {
396
+ toast.error(tr('agora.podcastStream.skipFailed'));
397
+ } finally {
398
+ setSkipLoading(false);
399
+ }
400
+ };
401
+
402
+ const participantIds = useMemo(() => participants.map((p) => p.userId), [participants]);
403
+ useRoomUsers(participantIds);
404
+
405
+ const speakers = participants.filter(
406
+ (p) => p.role === 'host' || p.role === 'speaker'
407
+ );
408
+ const listeners = participants.filter((p) => p.role === 'listener');
409
+
410
+ const userId = user?.id;
411
+ const isHost = myRole === 'host' || (!!userId && room?.host === userId);
412
+ const canSpeak = myRole === 'host' || myRole === 'speaker';
413
+
414
+ if (!isExpanded) {
415
+ return (
416
+ <MiniRoomBar
417
+ title={room?.title || 'Room'}
418
+ participantCount={participants.length}
419
+ isMuted={isMuted}
420
+ canSpeak={canSpeak}
421
+ activeSpeakerCount={activeSpeakerIds.size}
422
+ onExpand={onExpand}
423
+ onToggleMute={toggleMute}
424
+ onLeave={handleLeave}
425
+ />
426
+ );
427
+ }
428
+
429
+ if (activePanel === 'stream') {
430
+ return (
431
+ <StreamConfigPanel
432
+ roomId={roomId}
433
+ roomStatus={room?.status}
434
+ initialStreamUrl={room?.activeStreamUrl ?? undefined}
435
+ initialRtmpUrl={room?.rtmpUrl ?? undefined}
436
+ initialStreamKey={room?.rtmpStreamKey ?? undefined}
437
+ onClose={() => setActivePanel(null)}
438
+ onStreamStarted={() => {
439
+ roomsService.getRoom(roomId).then(setRoom);
440
+ onRoomChanged?.(roomId);
441
+ setActivePanel(null);
442
+ }}
443
+ />
444
+ );
445
+ }
446
+
447
+ if (activePanel === 'insights') {
448
+ return (
449
+ <InsightsPanel
450
+ room={room}
451
+ participants={participants}
452
+ theme={theme}
453
+ onClose={() => setActivePanel(null)}
454
+ />
455
+ );
456
+ }
457
+
458
+ if (activePanel === 'settings') {
459
+ return (
460
+ <View style={styles.container}>
461
+ <PanelHeader title="Room Settings" theme={theme} onBack={() => setActivePanel(null)} />
462
+ <View style={styles.settingsContent}>
463
+ {isRoomLive && (
464
+ <View style={styles.settingsItem}>
465
+ <MaterialCommunityIcons
466
+ name={isRecording ? 'record-circle' : 'record-circle-outline'}
467
+ size={22}
468
+ color={isRecording ? '#FF0000' : theme.colors.text}
469
+ />
470
+ <View style={styles.settingsItemText}>
471
+ <Text style={[styles.settingsItemTitle, { color: theme.colors.text }]}>Recording</Text>
472
+ <Text style={[styles.settingsItemDesc, { color: theme.colors.textSecondary }]}>
473
+ {isRecording ? 'Currently recording (max 1h)' : 'Record room audio for replay'}
474
+ </Text>
475
+ </View>
476
+ <TouchableOpacity
477
+ onPress={isRecording ? handleStopRecording : handleStartRecording}
478
+ style={[styles.recordingToggle, {
479
+ backgroundColor: isRecording ? LIVE_COLOR : theme.colors.primary,
480
+ }]}
481
+ >
482
+ <Text style={{ color: '#FFFFFF', fontWeight: '600', fontSize: 13 }}>
483
+ {isRecording ? 'Stop' : 'Start'}
484
+ </Text>
485
+ </TouchableOpacity>
486
+ </View>
487
+ )}
488
+ <TouchableOpacity
489
+ style={styles.settingsItem}
490
+ onPress={handleDeleteRoom}
491
+ >
492
+ <MaterialCommunityIcons name="delete-outline" size={22} color={LIVE_COLOR} />
493
+ <View style={styles.settingsItemText}>
494
+ <Text style={[styles.settingsItemTitle, { color: LIVE_COLOR }]}>Delete Room</Text>
495
+ <Text style={[styles.settingsItemDesc, { color: theme.colors.textSecondary }]}>
496
+ Permanently remove this room
497
+ </Text>
498
+ </View>
499
+ </TouchableOpacity>
500
+ </View>
501
+ </View>
502
+ );
503
+ }
504
+
505
+ return (
506
+ <View style={styles.container}>
507
+ <View style={[styles.header, { borderBottomColor: `${theme.colors.border}80` }]}>
508
+ <TouchableOpacity onPress={onCollapse} style={styles.headerButton}>
509
+ <MaterialCommunityIcons name="chevron-down" size={24} color={theme.colors.text} />
510
+ </TouchableOpacity>
511
+
512
+ <View style={styles.headerCenter}>
513
+ {isRoomLive ? (
514
+ <View style={[styles.liveBadge, { backgroundColor: LIVE_COLOR }]}>
515
+ <AnimatedPulse />
516
+ <Text style={styles.liveText}>LIVE</Text>
517
+ </View>
518
+ ) : (
519
+ <View style={[styles.liveBadge, { backgroundColor: theme.colors.textSecondary }]}>
520
+ <Text style={styles.liveText}>SCHEDULED</Text>
521
+ </View>
522
+ )}
523
+ {isRecording && (
524
+ <View style={[styles.liveBadge, { backgroundColor: '#FF0000' }]}>
525
+ <AnimatedPulse />
526
+ <Text style={styles.liveText}>REC</Text>
527
+ </View>
528
+ )}
529
+ <Text style={[styles.listenerCount, { color: theme.colors.textSecondary }]}>
530
+ {participants.length} in room
531
+ </Text>
532
+ </View>
533
+
534
+ <View style={styles.headerRight}>
535
+ {isHost && isRoomLive && (
536
+ <TouchableOpacity onPress={handleStopRoom} style={styles.endButton}>
537
+ <Text style={styles.endButtonText}>End</Text>
538
+ </TouchableOpacity>
539
+ )}
540
+ {isHost && !isRoomLive && room?.status === 'scheduled' && (
541
+ <TouchableOpacity
542
+ onPress={handleStartRoom}
543
+ disabled={startingRoom}
544
+ style={[styles.endButton, { backgroundColor: theme.colors.primary }]}
545
+ >
546
+ <Text style={[styles.endButtonText, { color: '#FFFFFF' }]}>
547
+ {startingRoom ? 'Starting...' : 'Go Live'}
548
+ </Text>
549
+ </TouchableOpacity>
550
+ )}
551
+ </View>
552
+ </View>
553
+
554
+ <View style={styles.roomInfo}>
555
+ <Text style={[styles.roomTitle, { color: theme.colors.text }]} numberOfLines={2}>
556
+ {room?.title || 'Room'}
557
+ </Text>
558
+ {room?.topic && (
559
+ <Text style={[styles.roomTopic, { color: theme.colors.textSecondary }]}>
560
+ {room.topic}
561
+ </Text>
562
+ )}
563
+ </View>
564
+
565
+ {micPermissionDenied && canSpeak && (
566
+ <View style={[styles.micBanner, { backgroundColor: '#FFF3CD', borderColor: '#FFE69C' }]}>
567
+ <MaterialCommunityIcons name="microphone-off" size={18} color="#856404" />
568
+ <Text style={styles.micBannerText}>
569
+ Microphone access denied. Allow mic permission in your browser settings to speak.
570
+ </Text>
571
+ </View>
572
+ )}
573
+
574
+ {effectiveStream && (
575
+ <View style={[styles.streamCard, { backgroundColor: `${theme.colors.card}80`, borderColor: theme.colors.border }]}>
576
+ <View style={styles.streamCardRow}>
577
+ {streamImageUrl ? (
578
+ <Image source={{ uri: streamImageUrl }} style={styles.streamCardImage} />
579
+ ) : (
580
+ <View style={[styles.streamCardIconBox, { backgroundColor: '#E8F5E9' }]}>
581
+ <MaterialCommunityIcons name="radio" size={20} color="#2E7D32" />
582
+ </View>
583
+ )}
584
+ <View style={styles.streamCardContent}>
585
+ <Text style={[styles.streamCardTitle, { color: theme.colors.text }]} numberOfLines={1}>
586
+ {effectiveStream.title || 'Live Stream'}
587
+ </Text>
588
+ {effectiveStream.description ? (
589
+ <Text style={[styles.streamCardDesc, { color: theme.colors.textSecondary }]} numberOfLines={2}>
590
+ {effectiveStream.description}
591
+ </Text>
592
+ ) : null}
593
+ </View>
594
+ {isHost && (
595
+ <View style={styles.streamCardActions}>
596
+ {(room?.podcastQueue?.length ?? 0) > 0 && (
597
+ <TouchableOpacity
598
+ onPress={handleSkipNext}
599
+ disabled={skipLoading}
600
+ style={styles.streamCardAction}
601
+ accessibilityRole="button"
602
+ accessibilityLabel={tr('agora.podcastStream.skipNext')}
603
+ >
604
+ <MaterialCommunityIcons name="skip-next-circle" size={22} color={theme.colors.primary} />
605
+ </TouchableOpacity>
606
+ )}
607
+ <TouchableOpacity onPress={handleStopStream} disabled={streamLoading} style={styles.streamCardAction}>
608
+ <MaterialCommunityIcons name="close-circle" size={22} color="#C62828" />
609
+ </TouchableOpacity>
610
+ </View>
611
+ )}
612
+ </View>
613
+ {hasProgress && (
614
+ <View style={styles.streamProgress}>
615
+ <View style={[styles.progressTrack, { backgroundColor: theme.colors.border }]}>
616
+ <View style={[styles.progressFill, progressFillStyle]} />
617
+ </View>
618
+ <Text style={[styles.progressTime, { color: theme.colors.textSecondary }]}>
619
+ {formatClock(elapsedSec, withHours)} / {formatClock(streamDurationSec, withHours)}
620
+ </Text>
621
+ </View>
622
+ )}
623
+ </View>
624
+ )}
625
+
626
+ <ScrollView
627
+ style={{ flex: 1 }}
628
+ contentContainerStyle={styles.scrollContent}
629
+ showsVerticalScrollIndicator={false}
630
+ >
631
+ <View style={styles.speakerGrid}>
632
+ {speakers.map((p) => (
633
+ <ConnectedSpeakerTile
634
+ key={p.userId}
635
+ participant={p}
636
+ isCurrentUser={p.userId === userId}
637
+ isSpeaking={activeSpeakerIds.has(p.userId)}
638
+ theme={theme}
639
+ oxyServices={oxyServices}
640
+ AvatarComponent={AvatarComponent}
641
+ getCachedFileDownloadUrlSync={getCachedFileDownloadUrlSync}
642
+ useUserById={useUserById}
643
+ />
644
+ ))}
645
+ </View>
646
+
647
+ {listeners.length > 0 && (
648
+ <View style={styles.listenerSection}>
649
+ <Text style={[styles.listenerHeader, { color: theme.colors.textSecondary }]}>
650
+ Listeners ({listeners.length})
651
+ </Text>
652
+ <View style={styles.listenerGrid}>
653
+ {listeners.map((p) => (
654
+ <ConnectedListenerAvatar
655
+ key={p.userId}
656
+ participant={p}
657
+ oxyServices={oxyServices}
658
+ AvatarComponent={AvatarComponent}
659
+ getCachedFileDownloadUrlSync={getCachedFileDownloadUrlSync}
660
+ useUserById={useUserById}
661
+ />
662
+ ))}
663
+ </View>
664
+ </View>
665
+ )}
666
+
667
+ {isHost && speakerRequests.length > 0 && (
668
+ <View style={styles.requestsSection}>
669
+ <Text style={[styles.requestsHeader, { color: theme.colors.textSecondary }]}>
670
+ Requests to speak
671
+ </Text>
672
+ {speakerRequests.map((r) => (
673
+ <ConnectedRequestRow
674
+ key={r.userId}
675
+ request={r}
676
+ theme={theme}
677
+ oxyServices={oxyServices}
678
+ onApprove={approveSpeaker}
679
+ onDeny={denySpeaker}
680
+ AvatarComponent={AvatarComponent}
681
+ getCachedFileDownloadUrlSync={getCachedFileDownloadUrlSync}
682
+ useUserById={useUserById}
683
+ />
684
+ ))}
685
+ </View>
686
+ )}
687
+ </ScrollView>
688
+
689
+ <View
690
+ style={[
691
+ styles.controlBar,
692
+ { borderTopColor: theme.colors.border },
693
+ ]}
694
+ >
695
+ {canSpeak ? (
696
+ <TouchableOpacity style={styles.controlItem} onPress={toggleMute}>
697
+ <View
698
+ style={[
699
+ styles.controlCircle,
700
+ {
701
+ backgroundColor: isMuted
702
+ ? theme.colors.backgroundSecondary
703
+ : theme.colors.primary,
704
+ },
705
+ ]}
706
+ >
707
+ <MaterialCommunityIcons
708
+ name={isMuted ? 'microphone-off' : 'microphone'}
709
+ size={24}
710
+ color={isMuted ? theme.colors.text : '#FFFFFF'}
711
+ />
712
+ </View>
713
+ <Text style={[styles.controlLabel, { color: theme.colors.textSecondary }]}>
714
+ {isMuted ? 'Unmute' : 'Mute'}
715
+ </Text>
716
+ </TouchableOpacity>
717
+ ) : (
718
+ <TouchableOpacity style={styles.controlItem} onPress={requestToSpeak}>
719
+ <View
720
+ style={[
721
+ styles.controlCircle,
722
+ { backgroundColor: theme.colors.backgroundSecondary },
723
+ ]}
724
+ >
725
+ <MaterialCommunityIcons name="hand-back-left" size={24} color={theme.colors.text} />
726
+ </View>
727
+ <Text style={[styles.controlLabel, { color: theme.colors.textSecondary }]}>
728
+ Request
729
+ </Text>
730
+ </TouchableOpacity>
731
+ )}
732
+
733
+ <TouchableOpacity style={styles.controlItem} onPress={() => setActivePanel('insights')}>
734
+ <View
735
+ style={[
736
+ styles.controlCircle,
737
+ { backgroundColor: theme.colors.backgroundSecondary },
738
+ ]}
739
+ >
740
+ <MaterialCommunityIcons name="chart-box-outline" size={24} color={theme.colors.text} />
741
+ </View>
742
+ <Text style={[styles.controlLabel, { color: theme.colors.textSecondary }]}>
743
+ Insights
744
+ </Text>
745
+ </TouchableOpacity>
746
+
747
+ {isHost && (
748
+ <TouchableOpacity style={styles.controlItem} onPress={() => setActivePanel('stream')}>
749
+ <View style={[styles.controlCircle, { backgroundColor: theme.colors.backgroundSecondary }]}>
750
+ <MaterialCommunityIcons name="radio" size={24} color={theme.colors.text} />
751
+ </View>
752
+ <Text style={[styles.controlLabel, { color: theme.colors.textSecondary }]}>
753
+ Stream
754
+ </Text>
755
+ </TouchableOpacity>
756
+ )}
757
+
758
+ {isHost && (
759
+ <TouchableOpacity style={styles.controlItem} onPress={() => setActivePanel('settings')}>
760
+ <View style={[styles.controlCircle, { backgroundColor: theme.colors.backgroundSecondary }]}>
761
+ <MaterialCommunityIcons name="cog-outline" size={24} color={theme.colors.text} />
762
+ </View>
763
+ <Text style={[styles.controlLabel, { color: theme.colors.textSecondary }]}>
764
+ Settings
765
+ </Text>
766
+ </TouchableOpacity>
767
+ )}
768
+
769
+ <TouchableOpacity style={styles.controlItem} onPress={handleLeave}>
770
+ <View style={[styles.leaveCircle, { backgroundColor: LIVE_COLOR }]}>
771
+ <MaterialCommunityIcons name="exit-run" size={24} color="#FFFFFF" />
772
+ </View>
773
+ <Text style={[styles.controlLabel, { color: theme.colors.textSecondary }]}>
774
+ Leave
775
+ </Text>
776
+ </TouchableOpacity>
777
+ </View>
778
+ </View>
779
+ );
780
+ }
781
+
782
+ const styles = StyleSheet.create({
783
+ container: { flex: 1 },
784
+ header: {
785
+ flexDirection: 'row',
786
+ alignItems: 'center',
787
+ paddingHorizontal: 16,
788
+ paddingVertical: 12,
789
+ borderBottomWidth: 1,
790
+ },
791
+ headerButton: { padding: 4 },
792
+ headerCenter: {
793
+ flex: 1,
794
+ flexDirection: 'row',
795
+ alignItems: 'center',
796
+ justifyContent: 'center',
797
+ gap: 8,
798
+ },
799
+ headerRight: { minWidth: 40, alignItems: 'flex-end' },
800
+ liveBadge: {
801
+ flexDirection: 'row',
802
+ alignItems: 'center',
803
+ paddingHorizontal: 10,
804
+ paddingVertical: 4,
805
+ borderRadius: 12,
806
+ gap: 5,
807
+ },
808
+ liveText: { fontSize: 12, fontWeight: '700', color: '#FFFFFF' },
809
+ listenerCount: { fontSize: 14, fontWeight: '500' },
810
+ endButton: {
811
+ paddingHorizontal: 16,
812
+ paddingVertical: 6,
813
+ borderRadius: 16,
814
+ backgroundColor: LIVE_TINT_COLOR,
815
+ },
816
+ endButtonText: { fontSize: 15, fontWeight: '600', color: LIVE_COLOR },
817
+ roomInfo: { paddingHorizontal: 16, paddingTop: 16, paddingBottom: 8 },
818
+ roomTitle: { fontSize: 32, fontWeight: 'bold', lineHeight: 38 },
819
+ roomTopic: { fontSize: 14, marginTop: 4 },
820
+ micBanner: {
821
+ flexDirection: 'row',
822
+ alignItems: 'center',
823
+ gap: 8,
824
+ marginHorizontal: 16,
825
+ marginBottom: 4,
826
+ paddingHorizontal: 12,
827
+ paddingVertical: 10,
828
+ borderRadius: 8,
829
+ borderWidth: 1,
830
+ },
831
+ micBannerText: { flex: 1, fontSize: 13, fontWeight: '500', color: '#856404' },
832
+ scrollContent: { paddingBottom: 120 },
833
+ speakerGrid: {
834
+ flexDirection: 'row',
835
+ flexWrap: 'wrap',
836
+ paddingHorizontal: 12,
837
+ paddingTop: 16,
838
+ },
839
+ speakerTile: { width: '25%', alignItems: 'center', marginBottom: 20 },
840
+ muteIndicator: {
841
+ position: 'absolute',
842
+ bottom: 0,
843
+ right: 0,
844
+ width: 22,
845
+ height: 22,
846
+ borderRadius: 11,
847
+ alignItems: 'center',
848
+ justifyContent: 'center',
849
+ borderWidth: 2,
850
+ borderColor: '#FFFFFF',
851
+ },
852
+ speakerName: {
853
+ fontSize: 12,
854
+ fontWeight: '500',
855
+ marginTop: 6,
856
+ textAlign: 'center',
857
+ maxWidth: 80,
858
+ },
859
+ roleBadge: {
860
+ paddingHorizontal: 6,
861
+ paddingVertical: 2,
862
+ borderRadius: 4,
863
+ marginTop: 2,
864
+ },
865
+ roleBadgeText: { fontSize: 10, fontWeight: '600', color: '#FFFFFF' },
866
+ listenerSection: { paddingHorizontal: 16, marginTop: 8 },
867
+ listenerHeader: { fontSize: 14, fontWeight: '600', marginBottom: 12 },
868
+ listenerGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 12 },
869
+ listenerItem: { marginBottom: 4 },
870
+ requestsSection: { paddingHorizontal: 16, marginTop: 24 },
871
+ requestsHeader: { fontSize: 14, fontWeight: '600', marginBottom: 12 },
872
+ requestRow: {
873
+ flexDirection: 'row',
874
+ alignItems: 'center',
875
+ padding: 12,
876
+ borderRadius: 12,
877
+ borderWidth: 1,
878
+ marginBottom: 8,
879
+ gap: 10,
880
+ },
881
+ requestName: { flex: 1, fontSize: 14, fontWeight: '500' },
882
+ approveBtn: {
883
+ width: 32,
884
+ height: 32,
885
+ borderRadius: 16,
886
+ alignItems: 'center',
887
+ justifyContent: 'center',
888
+ },
889
+ denyBtn: {
890
+ width: 32,
891
+ height: 32,
892
+ borderRadius: 16,
893
+ alignItems: 'center',
894
+ justifyContent: 'center',
895
+ },
896
+ controlBar: {
897
+ flexDirection: 'row',
898
+ justifyContent: 'center',
899
+ alignItems: 'flex-start',
900
+ paddingTop: 12,
901
+ paddingBottom: 12,
902
+ paddingHorizontal: 16,
903
+ borderTopWidth: 1,
904
+ gap: 20,
905
+ },
906
+ controlItem: { alignItems: 'center', gap: 4 },
907
+ controlCircle: {
908
+ width: 52,
909
+ height: 52,
910
+ borderRadius: 26,
911
+ alignItems: 'center',
912
+ justifyContent: 'center',
913
+ },
914
+ leaveCircle: {
915
+ width: 52,
916
+ height: 52,
917
+ borderRadius: 26,
918
+ alignItems: 'center',
919
+ justifyContent: 'center',
920
+ },
921
+ controlLabel: { fontSize: 11, fontWeight: '500' },
922
+ streamCard: {
923
+ marginHorizontal: 16,
924
+ marginBottom: 4,
925
+ padding: 10,
926
+ borderRadius: 12,
927
+ borderWidth: 1,
928
+ gap: 8,
929
+ },
930
+ streamCardRow: {
931
+ flexDirection: 'row',
932
+ alignItems: 'center',
933
+ gap: 10,
934
+ },
935
+ streamCardImage: { width: 44, height: 44, borderRadius: 8 },
936
+ streamCardIconBox: {
937
+ width: 44,
938
+ height: 44,
939
+ borderRadius: 8,
940
+ alignItems: 'center',
941
+ justifyContent: 'center',
942
+ },
943
+ streamCardContent: { flex: 1, gap: 2 },
944
+ streamCardTitle: { fontSize: 14, fontWeight: '600' },
945
+ streamCardDesc: { fontSize: 12 },
946
+ streamCardActions: { flexDirection: 'row', alignItems: 'center', gap: 2 },
947
+ streamCardAction: { padding: 4 },
948
+ streamProgress: { gap: 4 },
949
+ progressTrack: { height: 4, borderRadius: 2, overflow: 'hidden' },
950
+ progressFill: { height: 4, borderRadius: 2 },
951
+ progressTime: { fontSize: 11, fontWeight: '500', textAlign: 'right' },
952
+ settingsContent: {
953
+ paddingHorizontal: 16,
954
+ paddingTop: 16,
955
+ },
956
+ settingsItem: {
957
+ flexDirection: 'row',
958
+ alignItems: 'center',
959
+ gap: 14,
960
+ paddingVertical: 14,
961
+ },
962
+ settingsItemText: { flex: 1 },
963
+ settingsItemTitle: { fontSize: 16, fontWeight: '600' },
964
+ settingsItemDesc: { fontSize: 13, marginTop: 2 },
965
+ recordingToggle: {
966
+ paddingHorizontal: 14,
967
+ paddingVertical: 6,
968
+ borderRadius: 14,
969
+ },
970
+ });