@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,1155 @@
1
+ import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ TouchableOpacity,
7
+ TextInput,
8
+ FlatList,
9
+ ActivityIndicator,
10
+ Linking,
11
+ } from 'react-native';
12
+ import type { ViewStyle } from 'react-native';
13
+ import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons';
14
+
15
+ import { useLiveConfig, type PinnedPodcast } from '../context/LiveConfigContext';
16
+ import type { LiveTheme } from '../types';
17
+ import type { PodcastResult, EpisodeListItem, MusicTrackResult } from '../services/spacesService';
18
+
19
+ const SEARCH_DEBOUNCE_MS = 350;
20
+
21
+ /** Public Syra web base — the "Open in Syra" affordance links to `/podcasts/:id`. */
22
+ const SYRA_WEB_BASE_URL = 'https://syra.fm';
23
+
24
+ /**
25
+ * English source copy for every user-facing string in the picker. Hosts with an
26
+ * i18n layer (the Mention frontend) inject `t` via `useLiveConfig()` and these
27
+ * are never read; hosts without one (the standalone live-rooms app) fall back to
28
+ * these. Keys mirror the `agora.podcastStream.*` entries in the app locale files.
29
+ */
30
+ const DEFAULT_STRINGS: Record<string, string> = {
31
+ 'agora.podcastStream.disclaimer':
32
+ "You're responsible for having the rights to stream this audio into your room.",
33
+ 'agora.podcastStream.searchPlaceholder': 'Search podcasts',
34
+ 'agora.podcastStream.searchHint': 'Search for a podcast to stream',
35
+ 'agora.podcastStream.pinnedTitle': 'Stream my pinned podcast',
36
+ 'agora.podcastStream.emptyShows': 'No podcasts found',
37
+ 'agora.podcastStream.emptyEpisodes': 'No episodes found',
38
+ 'agora.podcastStream.errorShows': "Couldn't load podcasts. Check your connection and try again.",
39
+ 'agora.podcastStream.errorEpisodes': "Couldn't load episodes. Check your connection and try again.",
40
+ 'agora.podcastStream.retry': 'Retry',
41
+ 'agora.podcastStream.openInSyra': 'Open in Syra',
42
+ 'agora.podcastStream.openFailed': "Couldn't open Syra",
43
+ 'agora.podcastStream.addToQueue': 'Add to queue',
44
+ 'agora.podcastStream.removeFromQueue': 'Remove from queue',
45
+ 'agora.podcastStream.playQueue': 'Play episodes',
46
+ 'agora.podcastStream.clearQueue': 'Clear',
47
+ // Music (listening party). Streaming a full track into a room is a broadcast —
48
+ // the disclaimer makes the host's rights responsibility explicit.
49
+ 'agora.musicStream.disclaimer':
50
+ "You're responsible for having the rights to broadcast this music into your room.",
51
+ 'agora.musicStream.searchPlaceholder': 'Search music',
52
+ 'agora.musicStream.searchHint': 'Search for a track to play',
53
+ 'agora.musicStream.empty': 'No tracks found',
54
+ 'agora.musicStream.error': "Couldn't load music. Check your connection and try again.",
55
+ 'agora.musicStream.retry': 'Retry',
56
+ 'agora.musicStream.addToQueue': 'Add to queue',
57
+ 'agora.musicStream.removeFromQueue': 'Remove from queue',
58
+ 'agora.musicStream.playQueue': 'Play tracks',
59
+ 'agora.musicStream.clearQueue': 'Clear',
60
+ };
61
+
62
+ type TranslateFn = (key: string) => string;
63
+
64
+ type AvatarComponentType = React.ComponentType<{
65
+ size: number;
66
+ source?: string;
67
+ shape?: string;
68
+ style?: ViewStyle;
69
+ }>;
70
+
71
+ /** One episode selected for the up-next queue. The show id is always known here. */
72
+ export interface PodcastQueueSelection {
73
+ syraPodcastId: string;
74
+ episodeId: string;
75
+ }
76
+
77
+ interface PodcastStreamPickerProps {
78
+ onSelectEpisode: (syraPodcastId: string, episodeId: string) => void;
79
+ /**
80
+ * Start a multi-episode session: the first item plays immediately, the rest
81
+ * become the room's up-next queue. Optional — hosts that omit it get the
82
+ * single-tap "play this now" flow only, and the queue affordances (per-row "+"
83
+ * and the footer) are hidden.
84
+ */
85
+ onStartQueue?: (items: PodcastQueueSelection[]) => void;
86
+ }
87
+
88
+ function formatDuration(seconds?: number): string {
89
+ if (!seconds || seconds <= 0) return '';
90
+ const total = Math.floor(seconds);
91
+ const hours = Math.floor(total / 3600);
92
+ const minutes = Math.floor((total % 3600) / 60);
93
+ const secs = total % 60;
94
+ const paddedSecs = String(secs).padStart(2, '0');
95
+ if (hours > 0) {
96
+ return `${hours}:${String(minutes).padStart(2, '0')}:${paddedSecs}`;
97
+ }
98
+ return `${minutes}:${paddedSecs}`;
99
+ }
100
+
101
+ function formatDate(iso?: string): string {
102
+ if (!iso) return '';
103
+ const date = new Date(iso);
104
+ if (Number.isNaN(date.getTime())) return '';
105
+ return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
106
+ }
107
+
108
+ const ShowRow = memo(function ShowRow({
109
+ show,
110
+ theme,
111
+ AvatarComponent,
112
+ onSelect,
113
+ }: {
114
+ show: PodcastResult;
115
+ theme: LiveTheme;
116
+ AvatarComponent: AvatarComponentType;
117
+ onSelect: (show: PodcastResult) => void;
118
+ }) {
119
+ return (
120
+ <TouchableOpacity style={styles.row} activeOpacity={0.7} onPress={() => onSelect(show)}>
121
+ <AvatarComponent size={44} source={show.artworkUrl} shape="squircle" />
122
+ <View style={styles.rowText}>
123
+ <Text style={[styles.rowTitle, { color: theme.colors.text }]} numberOfLines={1}>
124
+ {show.title}
125
+ </Text>
126
+ {!!show.author && (
127
+ <Text style={[styles.rowSubtitle, { color: theme.colors.textSecondary }]} numberOfLines={1}>
128
+ {show.author}
129
+ </Text>
130
+ )}
131
+ </View>
132
+ <MaterialCommunityIcons name="chevron-right" size={22} color={theme.colors.textSecondary} />
133
+ </TouchableOpacity>
134
+ );
135
+ });
136
+
137
+ const EpisodeRow = memo(function EpisodeRow({
138
+ episode,
139
+ theme,
140
+ AvatarComponent,
141
+ onSelect,
142
+ queueEnabled,
143
+ isQueued,
144
+ onToggleQueue,
145
+ queueActionLabel,
146
+ }: {
147
+ episode: EpisodeListItem;
148
+ theme: LiveTheme;
149
+ AvatarComponent: AvatarComponentType;
150
+ onSelect: (episode: EpisodeListItem) => void;
151
+ queueEnabled: boolean;
152
+ isQueued: boolean;
153
+ onToggleQueue: (episode: EpisodeListItem) => void;
154
+ queueActionLabel: string;
155
+ }) {
156
+ const meta = [formatDuration(episode.durationSec), formatDate(episode.publishedAt)]
157
+ .filter(Boolean)
158
+ .join(' · ');
159
+
160
+ return (
161
+ <View style={styles.row}>
162
+ <TouchableOpacity style={styles.rowMain} activeOpacity={0.7} onPress={() => onSelect(episode)}>
163
+ <AvatarComponent size={44} source={episode.artworkUrl} shape="squircle" />
164
+ <View style={styles.rowText}>
165
+ <Text style={[styles.rowTitle, { color: theme.colors.text }]} numberOfLines={2}>
166
+ {episode.title}
167
+ </Text>
168
+ {!!meta && (
169
+ <Text style={[styles.rowSubtitle, { color: theme.colors.textSecondary }]} numberOfLines={1}>
170
+ {meta}
171
+ </Text>
172
+ )}
173
+ </View>
174
+ <MaterialCommunityIcons name="play-circle" size={26} color={theme.colors.primary} />
175
+ </TouchableOpacity>
176
+ {queueEnabled && (
177
+ <TouchableOpacity
178
+ style={styles.queueBtn}
179
+ activeOpacity={0.7}
180
+ hitSlop={8}
181
+ onPress={() => onToggleQueue(episode)}
182
+ accessibilityRole="button"
183
+ accessibilityLabel={queueActionLabel}
184
+ >
185
+ <MaterialCommunityIcons
186
+ name={isQueued ? 'check-circle' : 'plus-circle-outline'}
187
+ size={24}
188
+ color={isQueued ? theme.colors.primary : theme.colors.textSecondary}
189
+ />
190
+ </TouchableOpacity>
191
+ )}
192
+ </View>
193
+ );
194
+ });
195
+
196
+ /** Failed-request state (distinct from empty) with a Retry affordance. */
197
+ const ErrorState = memo(function ErrorState({
198
+ theme,
199
+ message,
200
+ retryLabel,
201
+ onRetry,
202
+ }: {
203
+ theme: LiveTheme;
204
+ message: string;
205
+ retryLabel: string;
206
+ onRetry: () => void;
207
+ }) {
208
+ return (
209
+ <View style={styles.stateBox}>
210
+ <MaterialCommunityIcons name="wifi-off" size={32} color={theme.colors.textSecondary} />
211
+ <Text style={[styles.stateText, { color: theme.colors.textSecondary }]}>{message}</Text>
212
+ <TouchableOpacity
213
+ style={[styles.retryBtn, { borderColor: theme.colors.border }]}
214
+ activeOpacity={0.7}
215
+ onPress={onRetry}
216
+ >
217
+ <MaterialCommunityIcons name="refresh" size={16} color={theme.colors.primary} />
218
+ <Text style={[styles.retryText, { color: theme.colors.primary }]}>{retryLabel}</Text>
219
+ </TouchableOpacity>
220
+ </View>
221
+ );
222
+ });
223
+
224
+ /**
225
+ * Self-contained two-level podcast picker for the live-room stream setup. Level 1
226
+ * is a debounced Syra show search; tapping a show drills into Level 2, its episode
227
+ * list. Tapping an episode reports `(syraPodcastId, episodeId)` to the parent — the
228
+ * picker owns NO room/stream logic. Ships inside @syra.fm/live, so it depends only
229
+ * on `useLiveConfig()` injection and never imports from a host app (`@/`).
230
+ *
231
+ * UX affordances (all backend-field-free): a host copyright disclaimer, an honest
232
+ * error+Retry state distinct from empty results, a one-tap "stream my pinned
233
+ * podcast" quick-start row (when the host exposes `getPinnedPodcast`), and an
234
+ * "Open in Syra" deep link on the episode-list header.
235
+ */
236
+ export function PodcastStreamPicker({ onSelectEpisode, onStartQueue }: PodcastStreamPickerProps) {
237
+ const { useTheme, roomsService, AvatarComponent, toast, t, getPinnedPodcast } = useLiveConfig();
238
+ const theme = useTheme();
239
+
240
+ const tr = useCallback<TranslateFn>(
241
+ (key) => (t ? t(key) : DEFAULT_STRINGS[key] ?? key),
242
+ [t],
243
+ );
244
+
245
+ // Up-next queue: only offered when the host wired the multi-episode start.
246
+ const queueEnabled = !!onStartQueue;
247
+ const [queued, setQueued] = useState<PodcastQueueSelection[]>([]);
248
+ const queuedIds = useMemo(() => new Set(queued.map((q) => q.episodeId)), [queued]);
249
+
250
+ // Level 1 — show search
251
+ const [query, setQuery] = useState('');
252
+ const [shows, setShows] = useState<PodcastResult[]>([]);
253
+ const [showsNextOffset, setShowsNextOffset] = useState(0);
254
+ const [showsHasMore, setShowsHasMore] = useState(false);
255
+ const [showsLoading, setShowsLoading] = useState(false);
256
+ const [showsLoadingMore, setShowsLoadingMore] = useState(false);
257
+ const [showsError, setShowsError] = useState(false);
258
+ const showsSeqRef = useRef(0);
259
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
260
+
261
+ // Pinned-podcast quick-start (only when the host exposes profile media).
262
+ const [pinnedPodcast, setPinnedPodcast] = useState<PinnedPodcast | null>(null);
263
+
264
+ // Level 2 — episode list
265
+ const [selectedShow, setSelectedShow] = useState<PodcastResult | null>(null);
266
+ const [episodes, setEpisodes] = useState<EpisodeListItem[]>([]);
267
+ const [episodesNextOffset, setEpisodesNextOffset] = useState(0);
268
+ const [episodesHasMore, setEpisodesHasMore] = useState(false);
269
+ const [episodesLoading, setEpisodesLoading] = useState(false);
270
+ const [episodesLoadingMore, setEpisodesLoadingMore] = useState(false);
271
+ const [episodesError, setEpisodesError] = useState(false);
272
+ const episodesSeqRef = useRef(0);
273
+
274
+ const runSearch = useCallback(
275
+ async (rawQuery: string, offset: number) => {
276
+ const trimmed = rawQuery.trim();
277
+ if (!trimmed) {
278
+ showsSeqRef.current += 1;
279
+ setShows([]);
280
+ setShowsHasMore(false);
281
+ setShowsNextOffset(0);
282
+ setShowsLoading(false);
283
+ setShowsLoadingMore(false);
284
+ setShowsError(false);
285
+ return;
286
+ }
287
+ const seq = offset === 0 ? (showsSeqRef.current += 1) : showsSeqRef.current;
288
+ if (offset === 0) {
289
+ setShowsLoading(true);
290
+ setShowsError(false);
291
+ } else {
292
+ setShowsLoadingMore(true);
293
+ }
294
+
295
+ const result = await roomsService.searchPodcasts(trimmed, offset);
296
+ if (seq !== showsSeqRef.current) return; // superseded by a newer query
297
+
298
+ if (!result.ok) {
299
+ // Only replace the list on a fresh search; a load-more failure keeps the
300
+ // rows the host already has and simply stops paginating.
301
+ if (offset === 0) {
302
+ setShows([]);
303
+ setShowsError(true);
304
+ }
305
+ setShowsHasMore(false);
306
+ setShowsLoading(false);
307
+ setShowsLoadingMore(false);
308
+ return;
309
+ }
310
+
311
+ setShows((prev) => (offset === 0 ? result.items : [...prev, ...result.items]));
312
+ setShowsHasMore(result.hasMore);
313
+ setShowsNextOffset(result.offset + result.items.length);
314
+ setShowsLoading(false);
315
+ setShowsLoadingMore(false);
316
+ },
317
+ [roomsService],
318
+ );
319
+
320
+ const handleQueryChange = useCallback(
321
+ (text: string) => {
322
+ setQuery(text);
323
+ if (debounceRef.current) clearTimeout(debounceRef.current);
324
+ // Show the spinner for the debounce window so a fresh query never briefly
325
+ // renders the "No podcasts found" empty state before the request fires.
326
+ if (text.trim()) {
327
+ setShowsLoading(true);
328
+ setShowsError(false);
329
+ }
330
+ debounceRef.current = setTimeout(() => {
331
+ runSearch(text, 0);
332
+ }, SEARCH_DEBOUNCE_MS);
333
+ },
334
+ [runSearch],
335
+ );
336
+
337
+ const handleClearQuery = useCallback(() => {
338
+ if (debounceRef.current) clearTimeout(debounceRef.current);
339
+ setQuery('');
340
+ runSearch('', 0);
341
+ }, [runSearch]);
342
+
343
+ const handleShowsEndReached = useCallback(() => {
344
+ if (showsLoading || showsLoadingMore || !showsHasMore) return;
345
+ runSearch(query, showsNextOffset);
346
+ }, [showsLoading, showsLoadingMore, showsHasMore, runSearch, query, showsNextOffset]);
347
+
348
+ const loadEpisodes = useCallback(
349
+ async (syraPodcastId: string, offset: number) => {
350
+ const seq = offset === 0 ? (episodesSeqRef.current += 1) : episodesSeqRef.current;
351
+ if (offset === 0) {
352
+ setEpisodesLoading(true);
353
+ setEpisodesError(false);
354
+ } else {
355
+ setEpisodesLoadingMore(true);
356
+ }
357
+
358
+ const result = await roomsService.getPodcastEpisodes(syraPodcastId, offset);
359
+ if (seq !== episodesSeqRef.current) return; // superseded by a newer show
360
+
361
+ if (!result.ok) {
362
+ if (offset === 0) {
363
+ setEpisodes([]);
364
+ setEpisodesError(true);
365
+ }
366
+ setEpisodesHasMore(false);
367
+ setEpisodesLoading(false);
368
+ setEpisodesLoadingMore(false);
369
+ return;
370
+ }
371
+
372
+ setEpisodes((prev) => (offset === 0 ? result.items : [...prev, ...result.items]));
373
+ setEpisodesHasMore(result.hasMore);
374
+ setEpisodesNextOffset(result.offset + result.items.length);
375
+ setEpisodesLoading(false);
376
+ setEpisodesLoadingMore(false);
377
+ },
378
+ [roomsService],
379
+ );
380
+
381
+ const handleSelectShow = useCallback(
382
+ (show: PodcastResult) => {
383
+ episodesSeqRef.current += 1;
384
+ setSelectedShow(show);
385
+ setEpisodes([]);
386
+ setEpisodesHasMore(false);
387
+ setEpisodesNextOffset(0);
388
+ setEpisodesError(false);
389
+ loadEpisodes(show.syraPodcastId, 0);
390
+ },
391
+ [loadEpisodes],
392
+ );
393
+
394
+ const handleSelectPinned = useCallback(() => {
395
+ if (!pinnedPodcast) return;
396
+ handleSelectShow({
397
+ syraPodcastId: pinnedPodcast.syraPodcastId,
398
+ title: pinnedPodcast.title ?? tr('agora.podcastStream.pinnedTitle'),
399
+ artworkUrl: pinnedPodcast.artworkUrl,
400
+ });
401
+ }, [pinnedPodcast, handleSelectShow, tr]);
402
+
403
+ const handleBack = useCallback(() => {
404
+ episodesSeqRef.current += 1;
405
+ setSelectedShow(null);
406
+ setEpisodes([]);
407
+ setEpisodesHasMore(false);
408
+ setEpisodesNextOffset(0);
409
+ setEpisodesLoading(false);
410
+ setEpisodesLoadingMore(false);
411
+ setEpisodesError(false);
412
+ }, []);
413
+
414
+ const handleEpisodesEndReached = useCallback(() => {
415
+ if (!selectedShow || episodesLoading || episodesLoadingMore || !episodesHasMore) return;
416
+ loadEpisodes(selectedShow.syraPodcastId, episodesNextOffset);
417
+ }, [selectedShow, episodesLoading, episodesLoadingMore, episodesHasMore, loadEpisodes, episodesNextOffset]);
418
+
419
+ const handleSelectEpisode = useCallback(
420
+ (episode: EpisodeListItem) => {
421
+ if (!selectedShow) return;
422
+ onSelectEpisode(selectedShow.syraPodcastId, episode.episodeId);
423
+ },
424
+ [selectedShow, onSelectEpisode],
425
+ );
426
+
427
+ // Toggle an episode in/out of the local up-next queue. Items keep their own
428
+ // show id, so a host can queue across multiple shows before launching.
429
+ const handleToggleQueue = useCallback(
430
+ (episode: EpisodeListItem) => {
431
+ if (!selectedShow) return;
432
+ const showId = selectedShow.syraPodcastId;
433
+ setQueued((prev) =>
434
+ prev.some((q) => q.episodeId === episode.episodeId)
435
+ ? prev.filter((q) => q.episodeId !== episode.episodeId)
436
+ : [...prev, { syraPodcastId: showId, episodeId: episode.episodeId }],
437
+ );
438
+ },
439
+ [selectedShow],
440
+ );
441
+
442
+ const handleClearQueue = useCallback(() => setQueued([]), []);
443
+
444
+ const handleStartQueue = useCallback(() => {
445
+ if (queued.length === 0) return;
446
+ onStartQueue?.(queued);
447
+ setQueued([]);
448
+ }, [queued, onStartQueue]);
449
+
450
+ const handleOpenInSyra = useCallback(() => {
451
+ if (!selectedShow) return;
452
+ Linking.openURL(`${SYRA_WEB_BASE_URL}/podcasts/${selectedShow.syraPodcastId}`).catch(() =>
453
+ toast.error(tr('agora.podcastStream.openFailed')),
454
+ );
455
+ }, [selectedShow, toast, tr]);
456
+
457
+ // Resolve the viewer's pinned podcast once on mount (host-provided; optional).
458
+ useEffect(() => {
459
+ if (!getPinnedPodcast) return;
460
+ let cancelled = false;
461
+ getPinnedPodcast().then(
462
+ (podcast) => {
463
+ if (!cancelled) setPinnedPodcast(podcast);
464
+ },
465
+ () => {
466
+ if (!cancelled) setPinnedPodcast(null);
467
+ },
468
+ );
469
+ return () => {
470
+ cancelled = true;
471
+ };
472
+ }, [getPinnedPodcast]);
473
+
474
+ // Cancel any pending debounced search when the picker unmounts.
475
+ useEffect(() => {
476
+ return () => {
477
+ if (debounceRef.current) clearTimeout(debounceRef.current);
478
+ };
479
+ }, []);
480
+
481
+ const hasQuery = query.trim().length > 0;
482
+ const showQueueFooter = queueEnabled && queued.length > 0;
483
+ const listContentStyle = showQueueFooter
484
+ ? [styles.listContent, styles.listContentWithFooter]
485
+ : styles.listContent;
486
+
487
+ // Floats over the bottom of either level; queued items persist across shows
488
+ // so the host can review/launch after adding from multiple shows.
489
+ const queueFooter = showQueueFooter ? (
490
+ <View style={[styles.queueFooter, { backgroundColor: theme.colors.background, borderTopColor: theme.colors.border }]}>
491
+ <TouchableOpacity
492
+ style={styles.queueClearBtn}
493
+ activeOpacity={0.7}
494
+ hitSlop={8}
495
+ onPress={handleClearQueue}
496
+ accessibilityRole="button"
497
+ >
498
+ <Text style={[styles.queueClearText, { color: theme.colors.textSecondary }]}>
499
+ {tr('agora.podcastStream.clearQueue')}
500
+ </Text>
501
+ </TouchableOpacity>
502
+ <TouchableOpacity
503
+ style={[styles.queuePlayBtn, { backgroundColor: theme.colors.primary }]}
504
+ activeOpacity={0.85}
505
+ onPress={handleStartQueue}
506
+ accessibilityRole="button"
507
+ >
508
+ <MaterialCommunityIcons name="play" size={18} color="#FFFFFF" />
509
+ <Text style={styles.queuePlayText}>
510
+ {tr('agora.podcastStream.playQueue')} ({queued.length})
511
+ </Text>
512
+ </TouchableOpacity>
513
+ </View>
514
+ ) : null;
515
+
516
+ if (selectedShow) {
517
+ return (
518
+ <View style={styles.container}>
519
+ <View style={styles.backHeader}>
520
+ <TouchableOpacity
521
+ style={styles.backHeaderMain}
522
+ activeOpacity={0.7}
523
+ onPress={handleBack}
524
+ hitSlop={8}
525
+ >
526
+ <MaterialCommunityIcons name="chevron-left" size={24} color={theme.colors.text} />
527
+ <Text style={[styles.backTitle, { color: theme.colors.text }]} numberOfLines={1}>
528
+ {selectedShow.title}
529
+ </Text>
530
+ </TouchableOpacity>
531
+ <TouchableOpacity
532
+ style={styles.openBtn}
533
+ activeOpacity={0.7}
534
+ onPress={handleOpenInSyra}
535
+ hitSlop={8}
536
+ accessibilityRole="link"
537
+ accessibilityLabel={tr('agora.podcastStream.openInSyra')}
538
+ >
539
+ <MaterialCommunityIcons name="open-in-new" size={20} color={theme.colors.textSecondary} />
540
+ </TouchableOpacity>
541
+ </View>
542
+
543
+ {episodesLoading ? (
544
+ <View style={styles.stateBox}>
545
+ <ActivityIndicator size="small" color={theme.colors.primary} />
546
+ </View>
547
+ ) : episodesError ? (
548
+ <ErrorState
549
+ theme={theme}
550
+ message={tr('agora.podcastStream.errorEpisodes')}
551
+ retryLabel={tr('agora.podcastStream.retry')}
552
+ onRetry={() => loadEpisodes(selectedShow.syraPodcastId, 0)}
553
+ />
554
+ ) : episodes.length === 0 ? (
555
+ <View style={styles.stateBox}>
556
+ <MaterialCommunityIcons name="playlist-remove" size={32} color={theme.colors.textSecondary} />
557
+ <Text style={[styles.stateText, { color: theme.colors.textSecondary }]}>
558
+ {tr('agora.podcastStream.emptyEpisodes')}
559
+ </Text>
560
+ </View>
561
+ ) : (
562
+ <FlatList
563
+ data={episodes}
564
+ keyExtractor={(item) => item.episodeId}
565
+ renderItem={({ item }) => (
566
+ <EpisodeRow
567
+ episode={item}
568
+ theme={theme}
569
+ AvatarComponent={AvatarComponent}
570
+ onSelect={handleSelectEpisode}
571
+ queueEnabled={queueEnabled}
572
+ isQueued={queuedIds.has(item.episodeId)}
573
+ onToggleQueue={handleToggleQueue}
574
+ queueActionLabel={tr(
575
+ queuedIds.has(item.episodeId)
576
+ ? 'agora.podcastStream.removeFromQueue'
577
+ : 'agora.podcastStream.addToQueue',
578
+ )}
579
+ />
580
+ )}
581
+ showsVerticalScrollIndicator={false}
582
+ keyboardShouldPersistTaps="handled"
583
+ onEndReached={handleEpisodesEndReached}
584
+ onEndReachedThreshold={0.4}
585
+ contentContainerStyle={listContentStyle}
586
+ ListFooterComponent={
587
+ episodesLoadingMore ? (
588
+ <View style={styles.footerLoader}>
589
+ <ActivityIndicator size="small" color={theme.colors.primary} />
590
+ </View>
591
+ ) : null
592
+ }
593
+ />
594
+ )}
595
+ {queueFooter}
596
+ </View>
597
+ );
598
+ }
599
+
600
+ return (
601
+ <View style={styles.container}>
602
+ <View style={styles.disclaimer}>
603
+ <MaterialCommunityIcons name="shield-alert-outline" size={15} color={theme.colors.textSecondary} />
604
+ <Text style={[styles.disclaimerText, { color: theme.colors.textSecondary }]}>
605
+ {tr('agora.podcastStream.disclaimer')}
606
+ </Text>
607
+ </View>
608
+
609
+ <View style={styles.searchWrap}>
610
+ <View style={[styles.searchRow, { backgroundColor: `${theme.colors.card}80`, borderColor: theme.colors.border }]}>
611
+ <MaterialCommunityIcons name="magnify" size={18} color={theme.colors.textSecondary} />
612
+ <TextInput
613
+ style={[styles.searchInput, { color: theme.colors.text }]}
614
+ placeholder={tr('agora.podcastStream.searchPlaceholder')}
615
+ placeholderTextColor={theme.colors.textSecondary}
616
+ value={query}
617
+ onChangeText={handleQueryChange}
618
+ autoCapitalize="none"
619
+ autoCorrect={false}
620
+ returnKeyType="search"
621
+ />
622
+ {query.length > 0 && (
623
+ <TouchableOpacity onPress={handleClearQuery} hitSlop={8}>
624
+ <MaterialCommunityIcons name="close-circle" size={18} color={theme.colors.textSecondary} />
625
+ </TouchableOpacity>
626
+ )}
627
+ </View>
628
+ </View>
629
+
630
+ {!hasQuery ? (
631
+ <View>
632
+ {pinnedPodcast && (
633
+ <TouchableOpacity
634
+ style={[styles.pinnedRow, { backgroundColor: `${theme.colors.primary}14`, borderColor: theme.colors.border }]}
635
+ activeOpacity={0.7}
636
+ onPress={handleSelectPinned}
637
+ >
638
+ <View style={[styles.pinnedIcon, { backgroundColor: `${theme.colors.primary}22` }]}>
639
+ <MaterialCommunityIcons name="flash" size={20} color={theme.colors.primary} />
640
+ </View>
641
+ <View style={styles.rowText}>
642
+ <Text style={[styles.rowTitle, { color: theme.colors.text }]} numberOfLines={1}>
643
+ {tr('agora.podcastStream.pinnedTitle')}
644
+ </Text>
645
+ {!!pinnedPodcast.title && (
646
+ <Text style={[styles.rowSubtitle, { color: theme.colors.textSecondary }]} numberOfLines={1}>
647
+ {pinnedPodcast.title}
648
+ </Text>
649
+ )}
650
+ </View>
651
+ <MaterialCommunityIcons name="chevron-right" size={22} color={theme.colors.textSecondary} />
652
+ </TouchableOpacity>
653
+ )}
654
+ <View style={styles.stateBox}>
655
+ <MaterialCommunityIcons name="podcast" size={32} color={theme.colors.textSecondary} />
656
+ <Text style={[styles.stateText, { color: theme.colors.textSecondary }]}>
657
+ {tr('agora.podcastStream.searchHint')}
658
+ </Text>
659
+ </View>
660
+ </View>
661
+ ) : showsError ? (
662
+ <ErrorState
663
+ theme={theme}
664
+ message={tr('agora.podcastStream.errorShows')}
665
+ retryLabel={tr('agora.podcastStream.retry')}
666
+ onRetry={() => runSearch(query, 0)}
667
+ />
668
+ ) : showsLoading ? (
669
+ <View style={styles.stateBox}>
670
+ <ActivityIndicator size="small" color={theme.colors.primary} />
671
+ </View>
672
+ ) : shows.length === 0 ? (
673
+ <View style={styles.stateBox}>
674
+ <MaterialCommunityIcons name="magnify-close" size={32} color={theme.colors.textSecondary} />
675
+ <Text style={[styles.stateText, { color: theme.colors.textSecondary }]}>
676
+ {tr('agora.podcastStream.emptyShows')}
677
+ </Text>
678
+ </View>
679
+ ) : (
680
+ <FlatList
681
+ data={shows}
682
+ keyExtractor={(item) => item.syraPodcastId}
683
+ renderItem={({ item }) => (
684
+ <ShowRow show={item} theme={theme} AvatarComponent={AvatarComponent} onSelect={handleSelectShow} />
685
+ )}
686
+ showsVerticalScrollIndicator={false}
687
+ keyboardShouldPersistTaps="handled"
688
+ onEndReached={handleShowsEndReached}
689
+ onEndReachedThreshold={0.4}
690
+ contentContainerStyle={listContentStyle}
691
+ ListFooterComponent={
692
+ showsLoadingMore ? (
693
+ <View style={styles.footerLoader}>
694
+ <ActivityIndicator size="small" color={theme.colors.primary} />
695
+ </View>
696
+ ) : null
697
+ }
698
+ />
699
+ )}
700
+ {queueFooter}
701
+ </View>
702
+ );
703
+ }
704
+
705
+ const MusicTrackRow = memo(function MusicTrackRow({
706
+ track,
707
+ theme,
708
+ AvatarComponent,
709
+ onSelect,
710
+ isQueued,
711
+ onToggleQueue,
712
+ queueActionLabel,
713
+ }: {
714
+ track: MusicTrackResult;
715
+ theme: LiveTheme;
716
+ AvatarComponent: AvatarComponentType;
717
+ onSelect: (track: MusicTrackResult) => void;
718
+ isQueued: boolean;
719
+ onToggleQueue: (track: MusicTrackResult) => void;
720
+ queueActionLabel: string;
721
+ }) {
722
+ const meta = [track.artist, formatDuration(track.durationSec)].filter(Boolean).join(' · ');
723
+
724
+ return (
725
+ <View style={styles.row}>
726
+ <TouchableOpacity style={styles.rowMain} activeOpacity={0.7} onPress={() => onSelect(track)}>
727
+ <AvatarComponent size={44} source={track.artworkUrl} shape="squircle" />
728
+ <View style={styles.rowText}>
729
+ <Text style={[styles.rowTitle, { color: theme.colors.text }]} numberOfLines={2}>
730
+ {track.title}
731
+ </Text>
732
+ {!!meta && (
733
+ <Text style={[styles.rowSubtitle, { color: theme.colors.textSecondary }]} numberOfLines={1}>
734
+ {meta}
735
+ </Text>
736
+ )}
737
+ </View>
738
+ <MaterialCommunityIcons name="play-circle" size={26} color={theme.colors.primary} />
739
+ </TouchableOpacity>
740
+ <TouchableOpacity
741
+ style={styles.queueBtn}
742
+ activeOpacity={0.7}
743
+ hitSlop={8}
744
+ onPress={() => onToggleQueue(track)}
745
+ accessibilityRole="button"
746
+ accessibilityLabel={queueActionLabel}
747
+ >
748
+ <MaterialCommunityIcons
749
+ name={isQueued ? 'check-circle' : 'plus-circle-outline'}
750
+ size={24}
751
+ color={isQueued ? theme.colors.primary : theme.colors.textSecondary}
752
+ />
753
+ </TouchableOpacity>
754
+ </View>
755
+ );
756
+ });
757
+
758
+ interface MusicPickerProps {
759
+ /** Stream a single track immediately. */
760
+ onSelectTrack: (trackId: string) => void;
761
+ /**
762
+ * Start a multi-track session: the first track plays immediately, the rest
763
+ * become the room's up-next queue. Optional — hosts that omit it get the
764
+ * single-tap "play this now" flow only, and the queue affordances are hidden.
765
+ */
766
+ onStartQueue?: (trackIds: string[]) => void;
767
+ }
768
+
769
+ /**
770
+ * Self-contained single-level music picker for the live-room "listening party".
771
+ * A debounced Syra catalog search; tapping a track reports its `trackId` to the
772
+ * parent (the picker owns NO room/stream logic and never sees an audio URL — the
773
+ * backend resolves the playable source server-side). Ships inside the shared
774
+ * package, so it depends only on `useLiveConfig()` injection and never imports
775
+ * from a host app (`@/`).
776
+ *
777
+ * Mirrors {@link PodcastStreamPicker}: a host copyright/broadcast disclaimer, an
778
+ * honest error+Retry state distinct from empty results, and an optional up-next
779
+ * queue (per-row "+" plus a floating "Play (N)" footer).
780
+ */
781
+ export function MusicPicker({ onSelectTrack, onStartQueue }: MusicPickerProps) {
782
+ const { useTheme, roomsService, AvatarComponent, t } = useLiveConfig();
783
+ const theme = useTheme();
784
+
785
+ const tr = useCallback<TranslateFn>((key) => (t ? t(key) : DEFAULT_STRINGS[key] ?? key), [t]);
786
+
787
+ const queueEnabled = !!onStartQueue;
788
+ const [queued, setQueued] = useState<string[]>([]);
789
+ const queuedIds = useMemo(() => new Set(queued), [queued]);
790
+
791
+ const [query, setQuery] = useState('');
792
+ const [tracks, setTracks] = useState<MusicTrackResult[]>([]);
793
+ const [nextOffset, setNextOffset] = useState(0);
794
+ const [hasMore, setHasMore] = useState(false);
795
+ const [loading, setLoading] = useState(false);
796
+ const [loadingMore, setLoadingMore] = useState(false);
797
+ const [error, setError] = useState(false);
798
+ const seqRef = useRef(0);
799
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
800
+
801
+ const runSearch = useCallback(
802
+ async (rawQuery: string, offset: number) => {
803
+ const trimmed = rawQuery.trim();
804
+ if (!trimmed) {
805
+ seqRef.current += 1;
806
+ setTracks([]);
807
+ setHasMore(false);
808
+ setNextOffset(0);
809
+ setLoading(false);
810
+ setLoadingMore(false);
811
+ setError(false);
812
+ return;
813
+ }
814
+ const seq = offset === 0 ? (seqRef.current += 1) : seqRef.current;
815
+ if (offset === 0) {
816
+ setLoading(true);
817
+ setError(false);
818
+ } else {
819
+ setLoadingMore(true);
820
+ }
821
+
822
+ const result = await roomsService.searchMusic(trimmed, offset);
823
+ if (seq !== seqRef.current) return; // superseded by a newer query
824
+
825
+ if (!result.ok) {
826
+ if (offset === 0) {
827
+ setTracks([]);
828
+ setError(true);
829
+ }
830
+ setHasMore(false);
831
+ setLoading(false);
832
+ setLoadingMore(false);
833
+ return;
834
+ }
835
+
836
+ setTracks((prev) => (offset === 0 ? result.items : [...prev, ...result.items]));
837
+ setHasMore(result.hasMore);
838
+ setNextOffset(result.offset + result.items.length);
839
+ setLoading(false);
840
+ setLoadingMore(false);
841
+ },
842
+ [roomsService],
843
+ );
844
+
845
+ const handleQueryChange = useCallback(
846
+ (text: string) => {
847
+ setQuery(text);
848
+ if (debounceRef.current) clearTimeout(debounceRef.current);
849
+ if (text.trim()) {
850
+ setLoading(true);
851
+ setError(false);
852
+ }
853
+ debounceRef.current = setTimeout(() => {
854
+ runSearch(text, 0);
855
+ }, SEARCH_DEBOUNCE_MS);
856
+ },
857
+ [runSearch],
858
+ );
859
+
860
+ const handleClearQuery = useCallback(() => {
861
+ if (debounceRef.current) clearTimeout(debounceRef.current);
862
+ setQuery('');
863
+ runSearch('', 0);
864
+ }, [runSearch]);
865
+
866
+ const handleEndReached = useCallback(() => {
867
+ if (loading || loadingMore || !hasMore) return;
868
+ runSearch(query, nextOffset);
869
+ }, [loading, loadingMore, hasMore, runSearch, query, nextOffset]);
870
+
871
+ const handleSelectTrack = useCallback(
872
+ (track: MusicTrackResult) => onSelectTrack(track.trackId),
873
+ [onSelectTrack],
874
+ );
875
+
876
+ const handleToggleQueue = useCallback((track: MusicTrackResult) => {
877
+ setQueued((prev) =>
878
+ prev.includes(track.trackId)
879
+ ? prev.filter((id) => id !== track.trackId)
880
+ : [...prev, track.trackId],
881
+ );
882
+ }, []);
883
+
884
+ const handleClearQueue = useCallback(() => setQueued([]), []);
885
+
886
+ const handleStartQueue = useCallback(() => {
887
+ if (queued.length === 0) return;
888
+ onStartQueue?.(queued);
889
+ setQueued([]);
890
+ }, [queued, onStartQueue]);
891
+
892
+ // Cancel any pending debounced search when the picker unmounts.
893
+ useEffect(() => {
894
+ return () => {
895
+ if (debounceRef.current) clearTimeout(debounceRef.current);
896
+ };
897
+ }, []);
898
+
899
+ const hasQuery = query.trim().length > 0;
900
+ const showQueueFooter = queueEnabled && queued.length > 0;
901
+ const listContentStyle = showQueueFooter
902
+ ? [styles.listContent, styles.listContentWithFooter]
903
+ : styles.listContent;
904
+
905
+ const queueFooter = showQueueFooter ? (
906
+ <View style={[styles.queueFooter, { backgroundColor: theme.colors.background, borderTopColor: theme.colors.border }]}>
907
+ <TouchableOpacity
908
+ style={styles.queueClearBtn}
909
+ activeOpacity={0.7}
910
+ hitSlop={8}
911
+ onPress={handleClearQueue}
912
+ accessibilityRole="button"
913
+ >
914
+ <Text style={[styles.queueClearText, { color: theme.colors.textSecondary }]}>
915
+ {tr('agora.musicStream.clearQueue')}
916
+ </Text>
917
+ </TouchableOpacity>
918
+ <TouchableOpacity
919
+ style={[styles.queuePlayBtn, { backgroundColor: theme.colors.primary }]}
920
+ activeOpacity={0.85}
921
+ onPress={handleStartQueue}
922
+ accessibilityRole="button"
923
+ >
924
+ <MaterialCommunityIcons name="play" size={18} color="#FFFFFF" />
925
+ <Text style={styles.queuePlayText}>
926
+ {tr('agora.musicStream.playQueue')} ({queued.length})
927
+ </Text>
928
+ </TouchableOpacity>
929
+ </View>
930
+ ) : null;
931
+
932
+ return (
933
+ <View style={styles.container}>
934
+ <View style={styles.disclaimer}>
935
+ <MaterialCommunityIcons name="shield-alert-outline" size={15} color={theme.colors.textSecondary} />
936
+ <Text style={[styles.disclaimerText, { color: theme.colors.textSecondary }]}>
937
+ {tr('agora.musicStream.disclaimer')}
938
+ </Text>
939
+ </View>
940
+
941
+ <View style={styles.searchWrap}>
942
+ <View style={[styles.searchRow, { backgroundColor: `${theme.colors.card}80`, borderColor: theme.colors.border }]}>
943
+ <MaterialCommunityIcons name="magnify" size={18} color={theme.colors.textSecondary} />
944
+ <TextInput
945
+ style={[styles.searchInput, { color: theme.colors.text }]}
946
+ placeholder={tr('agora.musicStream.searchPlaceholder')}
947
+ placeholderTextColor={theme.colors.textSecondary}
948
+ value={query}
949
+ onChangeText={handleQueryChange}
950
+ autoCapitalize="none"
951
+ autoCorrect={false}
952
+ returnKeyType="search"
953
+ />
954
+ {query.length > 0 && (
955
+ <TouchableOpacity onPress={handleClearQuery} hitSlop={8}>
956
+ <MaterialCommunityIcons name="close-circle" size={18} color={theme.colors.textSecondary} />
957
+ </TouchableOpacity>
958
+ )}
959
+ </View>
960
+ </View>
961
+
962
+ {!hasQuery ? (
963
+ <View style={styles.stateBox}>
964
+ <MaterialCommunityIcons name="music-note" size={32} color={theme.colors.textSecondary} />
965
+ <Text style={[styles.stateText, { color: theme.colors.textSecondary }]}>
966
+ {tr('agora.musicStream.searchHint')}
967
+ </Text>
968
+ </View>
969
+ ) : error ? (
970
+ <ErrorState
971
+ theme={theme}
972
+ message={tr('agora.musicStream.error')}
973
+ retryLabel={tr('agora.musicStream.retry')}
974
+ onRetry={() => runSearch(query, 0)}
975
+ />
976
+ ) : loading ? (
977
+ <View style={styles.stateBox}>
978
+ <ActivityIndicator size="small" color={theme.colors.primary} />
979
+ </View>
980
+ ) : tracks.length === 0 ? (
981
+ <View style={styles.stateBox}>
982
+ <MaterialCommunityIcons name="magnify-close" size={32} color={theme.colors.textSecondary} />
983
+ <Text style={[styles.stateText, { color: theme.colors.textSecondary }]}>
984
+ {tr('agora.musicStream.empty')}
985
+ </Text>
986
+ </View>
987
+ ) : (
988
+ <FlatList
989
+ data={tracks}
990
+ keyExtractor={(item) => item.trackId}
991
+ renderItem={({ item }) => (
992
+ <MusicTrackRow
993
+ track={item}
994
+ theme={theme}
995
+ AvatarComponent={AvatarComponent}
996
+ onSelect={handleSelectTrack}
997
+ isQueued={queuedIds.has(item.trackId)}
998
+ onToggleQueue={handleToggleQueue}
999
+ queueActionLabel={tr(
1000
+ queuedIds.has(item.trackId)
1001
+ ? 'agora.musicStream.removeFromQueue'
1002
+ : 'agora.musicStream.addToQueue',
1003
+ )}
1004
+ />
1005
+ )}
1006
+ showsVerticalScrollIndicator={false}
1007
+ keyboardShouldPersistTaps="handled"
1008
+ onEndReached={handleEndReached}
1009
+ onEndReachedThreshold={0.4}
1010
+ contentContainerStyle={listContentStyle}
1011
+ ListFooterComponent={
1012
+ loadingMore ? (
1013
+ <View style={styles.footerLoader}>
1014
+ <ActivityIndicator size="small" color={theme.colors.primary} />
1015
+ </View>
1016
+ ) : null
1017
+ }
1018
+ />
1019
+ )}
1020
+ {queueFooter}
1021
+ </View>
1022
+ );
1023
+ }
1024
+
1025
+ const styles = StyleSheet.create({
1026
+ container: { flex: 1 },
1027
+ disclaimer: {
1028
+ flexDirection: 'row',
1029
+ alignItems: 'flex-start',
1030
+ gap: 8,
1031
+ paddingHorizontal: 16,
1032
+ paddingTop: 12,
1033
+ },
1034
+ disclaimerText: { flex: 1, fontSize: 12, lineHeight: 16 },
1035
+ searchWrap: {
1036
+ paddingHorizontal: 16,
1037
+ paddingTop: 12,
1038
+ paddingBottom: 8,
1039
+ },
1040
+ searchRow: {
1041
+ flexDirection: 'row',
1042
+ alignItems: 'center',
1043
+ gap: 8,
1044
+ borderWidth: 1,
1045
+ borderRadius: 10,
1046
+ paddingHorizontal: 12,
1047
+ paddingVertical: 10,
1048
+ },
1049
+ searchInput: {
1050
+ flex: 1,
1051
+ fontSize: 15,
1052
+ padding: 0,
1053
+ },
1054
+ backHeader: {
1055
+ flexDirection: 'row',
1056
+ alignItems: 'center',
1057
+ paddingHorizontal: 12,
1058
+ paddingTop: 12,
1059
+ paddingBottom: 8,
1060
+ },
1061
+ backHeaderMain: {
1062
+ flex: 1,
1063
+ flexDirection: 'row',
1064
+ alignItems: 'center',
1065
+ gap: 2,
1066
+ },
1067
+ backTitle: {
1068
+ flex: 1,
1069
+ fontSize: 16,
1070
+ fontWeight: '600',
1071
+ },
1072
+ openBtn: { padding: 6, marginLeft: 4 },
1073
+ listContent: { paddingBottom: 24 },
1074
+ listContentWithFooter: { paddingBottom: 88 },
1075
+ row: {
1076
+ flexDirection: 'row',
1077
+ alignItems: 'center',
1078
+ gap: 12,
1079
+ paddingHorizontal: 16,
1080
+ paddingVertical: 10,
1081
+ },
1082
+ rowMain: {
1083
+ flex: 1,
1084
+ flexDirection: 'row',
1085
+ alignItems: 'center',
1086
+ gap: 12,
1087
+ },
1088
+ queueBtn: { padding: 4 },
1089
+ pinnedRow: {
1090
+ flexDirection: 'row',
1091
+ alignItems: 'center',
1092
+ gap: 12,
1093
+ marginHorizontal: 16,
1094
+ marginTop: 4,
1095
+ marginBottom: 8,
1096
+ paddingHorizontal: 12,
1097
+ paddingVertical: 10,
1098
+ borderWidth: 1,
1099
+ borderRadius: 12,
1100
+ },
1101
+ pinnedIcon: {
1102
+ width: 44,
1103
+ height: 44,
1104
+ borderRadius: 12,
1105
+ alignItems: 'center',
1106
+ justifyContent: 'center',
1107
+ },
1108
+ rowText: { flex: 1, gap: 2 },
1109
+ rowTitle: { fontSize: 15, fontWeight: '600' },
1110
+ rowSubtitle: { fontSize: 13 },
1111
+ stateBox: {
1112
+ alignItems: 'center',
1113
+ justifyContent: 'center',
1114
+ gap: 10,
1115
+ paddingVertical: 40,
1116
+ },
1117
+ stateText: { fontSize: 14, textAlign: 'center', paddingHorizontal: 32 },
1118
+ retryBtn: {
1119
+ flexDirection: 'row',
1120
+ alignItems: 'center',
1121
+ gap: 6,
1122
+ borderWidth: 1,
1123
+ borderRadius: 10,
1124
+ paddingHorizontal: 16,
1125
+ paddingVertical: 8,
1126
+ marginTop: 4,
1127
+ },
1128
+ retryText: { fontSize: 14, fontWeight: '600' },
1129
+ footerLoader: { paddingVertical: 16 },
1130
+ queueFooter: {
1131
+ position: 'absolute',
1132
+ left: 0,
1133
+ right: 0,
1134
+ bottom: 0,
1135
+ flexDirection: 'row',
1136
+ alignItems: 'center',
1137
+ gap: 12,
1138
+ paddingHorizontal: 16,
1139
+ paddingTop: 12,
1140
+ paddingBottom: 16,
1141
+ borderTopWidth: 1,
1142
+ },
1143
+ queueClearBtn: { paddingVertical: 8, paddingHorizontal: 4 },
1144
+ queueClearText: { fontSize: 14, fontWeight: '600' },
1145
+ queuePlayBtn: {
1146
+ flex: 1,
1147
+ flexDirection: 'row',
1148
+ alignItems: 'center',
1149
+ justifyContent: 'center',
1150
+ gap: 8,
1151
+ paddingVertical: 12,
1152
+ borderRadius: 12,
1153
+ },
1154
+ queuePlayText: { color: '#FFFFFF', fontSize: 15, fontWeight: '700' },
1155
+ });