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