@q9labsai/chalk-react-native 4.1.17 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (544) hide show
  1. package/README.md +134 -29
  2. package/android/build.gradle +35 -0
  3. package/android/compatibility/ModuleApiCompatibility.java +215 -0
  4. package/android/src/main/AndroidManifest.xml +2 -1
  5. package/android/src/main/java/ai/q9labs/chalk/reactnative/ChalkSpaceForegroundService.java +105 -31
  6. package/android/src/main/kotlin/ai/q9labs/chalk/reactnative/ChalkAndroidConnectionServicePackage.kt +2 -0
  7. package/android/src/main/kotlin/ai/q9labs/chalk/reactnative/ChalkAudioRouteModule.kt +137 -0
  8. package/android/src/main/kotlin/ai/q9labs/chalk/reactnative/ChalkCallServiceModule.kt +32 -0
  9. package/android/src/main/kotlin/ai/q9labs/chalk/reactnative/CommunicationDeviceListener.kt +23 -0
  10. package/android/src/test/kotlin/ai/q9labs/chalk/compatibility/UnsafeSignatures.kt +29 -0
  11. package/dist/android/call-service.d.ts +4 -0
  12. package/dist/android/call-service.d.ts.map +1 -0
  13. package/dist/avatar/FaceAvatar.d.ts +19 -0
  14. package/dist/avatar/FaceAvatar.d.ts.map +1 -0
  15. package/dist/avatar/tile-avatar-size.d.ts +3 -0
  16. package/dist/avatar/tile-avatar-size.d.ts.map +1 -0
  17. package/dist/chunk-GYVZSOGQ.js +30 -0
  18. package/dist/chunk-IHE6EA3T.js +25 -0
  19. package/dist/chunk-OIBT2FNM.js +1479 -0
  20. package/dist/client.d.ts +1 -5
  21. package/dist/client.d.ts.map +1 -1
  22. package/dist/client.js +185 -2
  23. package/dist/components/EmbeddedWhiteboard.d.ts.map +1 -1
  24. package/dist/components/native-media-visibility.d.ts +3 -2
  25. package/dist/components/native-media-visibility.d.ts.map +1 -1
  26. package/dist/components/useAndroidCallService.d.ts +8 -0
  27. package/dist/components/useAndroidCallService.d.ts.map +1 -0
  28. package/dist/dev.d.ts +10 -0
  29. package/dist/dev.d.ts.map +1 -0
  30. package/dist/dev.js +10 -0
  31. package/dist/dock/Dock.d.ts +23 -0
  32. package/dist/dock/Dock.d.ts.map +1 -0
  33. package/dist/dock/LeaveMenu.d.ts +12 -0
  34. package/dist/dock/LeaveMenu.d.ts.map +1 -0
  35. package/dist/dock/MoreSheet.d.ts +17 -0
  36. package/dist/dock/MoreSheet.d.ts.map +1 -0
  37. package/dist/dock/ReactionBurst.d.ts +8 -0
  38. package/dist/dock/ReactionBurst.d.ts.map +1 -0
  39. package/dist/dock/RoutePicker.d.ts +16 -0
  40. package/dist/dock/RoutePicker.d.ts.map +1 -0
  41. package/dist/dock/audio-route.d.ts +25 -0
  42. package/dist/dock/audio-route.d.ts.map +1 -0
  43. package/dist/error-YOPTEH5N.wav +0 -0
  44. package/dist/frame/CallFrame.d.ts +17 -0
  45. package/dist/frame/CallFrame.d.ts.map +1 -0
  46. package/dist/frame/Chalk.d.ts +40 -0
  47. package/dist/frame/Chalk.d.ts.map +1 -0
  48. package/dist/frame/back-handler.d.ts +6 -0
  49. package/dist/frame/back-handler.d.ts.map +1 -0
  50. package/dist/frame/dev-scenario.d.ts +28 -0
  51. package/dist/frame/dev-scenario.d.ts.map +1 -0
  52. package/dist/frame/device-layout.d.ts +16 -0
  53. package/dist/frame/device-layout.d.ts.map +1 -0
  54. package/dist/frame/measurement.d.ts +28 -0
  55. package/dist/frame/measurement.d.ts.map +1 -0
  56. package/dist/frame/portal.d.ts +24 -0
  57. package/dist/frame/portal.d.ts.map +1 -0
  58. package/dist/frame/preferences.d.ts +21 -0
  59. package/dist/frame/preferences.d.ts.map +1 -0
  60. package/dist/frame/regions.d.ts +25 -0
  61. package/dist/frame/regions.d.ts.map +1 -0
  62. package/dist/hand-DFWVXPH2.wav +0 -0
  63. package/dist/haptics/haptics.d.ts +12 -0
  64. package/dist/haptics/haptics.d.ts.map +1 -0
  65. package/dist/header/CallDetailsSheet.d.ts +18 -0
  66. package/dist/header/CallDetailsSheet.d.ts.map +1 -0
  67. package/dist/header/Header.d.ts +17 -0
  68. package/dist/header/Header.d.ts.map +1 -0
  69. package/dist/header/QrCode.d.ts +6 -0
  70. package/dist/header/QrCode.d.ts.map +1 -0
  71. package/dist/icons/Icon.d.ts +12 -0
  72. package/dist/icons/Icon.d.ts.map +1 -0
  73. package/dist/icons/icon-element.d.ts +24 -0
  74. package/dist/icons/icon-element.d.ts.map +1 -0
  75. package/dist/icons/icon-paths.d.ts +530 -0
  76. package/dist/icons/icon-paths.d.ts.map +1 -0
  77. package/dist/index.d.ts +6 -9
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +2956 -38
  80. package/dist/join-Z6RL5AHE.wav +0 -0
  81. package/dist/knock-4VBQSHK4.wav +0 -0
  82. package/dist/leave-MQLZOWQD.wav +0 -0
  83. package/dist/limbo/Limbo.d.ts +25 -0
  84. package/dist/limbo/Limbo.d.ts.map +1 -0
  85. package/dist/limbo/Veil.d.ts +10 -0
  86. package/dist/limbo/Veil.d.ts.map +1 -0
  87. package/dist/media/native-capture-error.d.ts +7 -0
  88. package/dist/media/native-capture-error.d.ts.map +1 -0
  89. package/dist/message-6Y6WJGTB.wav +0 -0
  90. package/dist/motion/MovingFrame.d.ts +36 -0
  91. package/dist/motion/MovingFrame.d.ts.map +1 -0
  92. package/dist/motion/motion.d.ts +82 -0
  93. package/dist/motion/motion.d.ts.map +1 -0
  94. package/dist/motion/presence.d.ts +6 -0
  95. package/dist/motion/presence.d.ts.map +1 -0
  96. package/dist/motion/press.d.ts +15 -0
  97. package/dist/motion/press.d.ts.map +1 -0
  98. package/dist/mute-H5SJFXVI.wav +0 -0
  99. package/dist/primitives/BottomSheet.d.ts +36 -0
  100. package/dist/primitives/BottomSheet.d.ts.map +1 -0
  101. package/dist/primitives/ChalkText.d.ts +13 -0
  102. package/dist/primitives/ChalkText.d.ts.map +1 -0
  103. package/dist/primitives/CopyButton.d.ts +11 -0
  104. package/dist/primitives/CopyButton.d.ts.map +1 -0
  105. package/dist/primitives/IconButton.d.ts +26 -0
  106. package/dist/primitives/IconButton.d.ts.map +1 -0
  107. package/dist/primitives/Menu.d.ts +39 -0
  108. package/dist/primitives/Menu.d.ts.map +1 -0
  109. package/dist/primitives/NameTag.d.ts +19 -0
  110. package/dist/primitives/NameTag.d.ts.map +1 -0
  111. package/dist/primitives/PillButton.d.ts +19 -0
  112. package/dist/primitives/PillButton.d.ts.map +1 -0
  113. package/dist/primitives/SegmentedControl.d.ts +17 -0
  114. package/dist/primitives/SegmentedControl.d.ts.map +1 -0
  115. package/dist/primitives/SheetGroup.d.ts +24 -0
  116. package/dist/primitives/SheetGroup.d.ts.map +1 -0
  117. package/dist/primitives/Switch.d.ts +9 -0
  118. package/dist/primitives/Switch.d.ts.map +1 -0
  119. package/dist/primitives/TileShell.d.ts +18 -0
  120. package/dist/primitives/TileShell.d.ts.map +1 -0
  121. package/dist/primitives/announce.d.ts +3 -0
  122. package/dist/primitives/announce.d.ts.map +1 -0
  123. package/dist/primitives/menu-placement.d.ts +32 -0
  124. package/dist/primitives/menu-placement.d.ts.map +1 -0
  125. package/dist/primitives/toast/Toast.d.ts +8 -0
  126. package/dist/primitives/toast/Toast.d.ts.map +1 -0
  127. package/dist/primitives/toast/ToastStack.d.ts +18 -0
  128. package/dist/primitives/toast/ToastStack.d.ts.map +1 -0
  129. package/dist/primitives/toast/toast-store.d.ts +39 -0
  130. package/dist/primitives/toast/toast-store.d.ts.map +1 -0
  131. package/dist/recording-Q4GB77Y4.wav +0 -0
  132. package/dist/sheets/people-chat/ChatNotices.d.ts +14 -0
  133. package/dist/sheets/people-chat/ChatNotices.d.ts.map +1 -0
  134. package/dist/sheets/people-chat/ChatTab.d.ts +6 -0
  135. package/dist/sheets/people-chat/ChatTab.d.ts.map +1 -0
  136. package/dist/sheets/people-chat/Composer.d.ts +7 -0
  137. package/dist/sheets/people-chat/Composer.d.ts.map +1 -0
  138. package/dist/sheets/people-chat/PeopleChatSheet.d.ts +17 -0
  139. package/dist/sheets/people-chat/PeopleChatSheet.d.ts.map +1 -0
  140. package/dist/sheets/people-chat/PeopleTab.d.ts +3 -0
  141. package/dist/sheets/people-chat/PeopleTab.d.ts.map +1 -0
  142. package/dist/sheets/people-chat/PhotoViewer.d.ts +19 -0
  143. package/dist/sheets/people-chat/PhotoViewer.d.ts.map +1 -0
  144. package/dist/sheets/people-chat/TextField.d.ts +4 -0
  145. package/dist/sheets/people-chat/TextField.d.ts.map +1 -0
  146. package/dist/sheets/people-chat/attachments.d.ts +33 -0
  147. package/dist/sheets/people-chat/attachments.d.ts.map +1 -0
  148. package/dist/sheets/people-chat/chat-model.d.ts +64 -0
  149. package/dist/sheets/people-chat/chat-model.d.ts.map +1 -0
  150. package/dist/sheets/people-chat/people-model.d.ts +43 -0
  151. package/dist/sheets/people-chat/people-model.d.ts.map +1 -0
  152. package/dist/sounds/sounds.d.ts +33 -0
  153. package/dist/sounds/sounds.d.ts.map +1 -0
  154. package/dist/space-client/create-native-space-client.d.ts +0 -4
  155. package/dist/space-client/create-native-space-client.d.ts.map +1 -1
  156. package/dist/space-client/random-uuid.d.ts +7 -0
  157. package/dist/space-client/random-uuid.d.ts.map +1 -0
  158. package/dist/stage/ContentTile.d.ts +18 -0
  159. package/dist/stage/ContentTile.d.ts.map +1 -0
  160. package/dist/stage/ParticipantTile.d.ts +18 -0
  161. package/dist/stage/ParticipantTile.d.ts.map +1 -0
  162. package/dist/stage/Stage.d.ts +14 -0
  163. package/dist/stage/Stage.d.ts.map +1 -0
  164. package/dist/stage/TileSheet.d.ts +12 -0
  165. package/dist/stage/TileSheet.d.ts.map +1 -0
  166. package/dist/stage/camera.d.ts +7 -0
  167. package/dist/stage/camera.d.ts.map +1 -0
  168. package/dist/stage/layout.d.ts +57 -0
  169. package/dist/stage/layout.d.ts.map +1 -0
  170. package/dist/stage/space-selector.d.ts +10 -0
  171. package/dist/stage/space-selector.d.ts.map +1 -0
  172. package/dist/stage/stage-view.d.ts +23 -0
  173. package/dist/stage/stage-view.d.ts.map +1 -0
  174. package/dist/status/CallOverlays.d.ts +17 -0
  175. package/dist/status/CallOverlays.d.ts.map +1 -0
  176. package/dist/status/ChalkArrival.d.ts +24 -0
  177. package/dist/status/ChalkArrival.d.ts.map +1 -0
  178. package/dist/status/StatusPage.d.ts +14 -0
  179. package/dist/status/StatusPage.d.ts.map +1 -0
  180. package/dist/status/call-screen.d.ts +20 -0
  181. package/dist/status/call-screen.d.ts.map +1 -0
  182. package/dist/test-VD2KAM4D.wav +0 -0
  183. package/dist/theme/theme.d.ts +15 -0
  184. package/dist/theme/theme.d.ts.map +1 -0
  185. package/dist/theme/tokens.d.ts +248 -0
  186. package/dist/theme/tokens.d.ts.map +1 -0
  187. package/dist/toasts/CallEventToasts.d.ts +21 -0
  188. package/dist/toasts/CallEventToasts.d.ts.map +1 -0
  189. package/dist/toasts/notices.d.ts +34 -0
  190. package/dist/toasts/notices.d.ts.map +1 -0
  191. package/dist/ui/ChalkUI.d.ts +12 -0
  192. package/dist/ui/ChalkUI.d.ts.map +1 -0
  193. package/dist/ui.d.ts +25 -0
  194. package/dist/ui.d.ts.map +1 -0
  195. package/dist/ui.js +77 -0
  196. package/dist/unmute-2XR2LICU.wav +0 -0
  197. package/embedded/chalk-whiteboard/asset-manifest.json +2 -2
  198. package/embedded/chalk-whiteboard/renderer.js +510 -510
  199. package/ios/ChalkAudioRouteModule.swift +93 -0
  200. package/ios/ChalkAudioRouteModuleBridge.m +9 -0
  201. package/package.json +46 -76
  202. package/src/android/call-service.ts +19 -0
  203. package/src/avatar/FaceAvatar.tsx +145 -0
  204. package/src/avatar/tile-avatar-size.ts +4 -0
  205. package/src/client.ts +1 -5
  206. package/src/components/EmbeddedWhiteboard.tsx +14 -71
  207. package/src/components/native-media-visibility.ts +1 -2
  208. package/src/components/useAndroidCallService.ts +41 -0
  209. package/src/dev.ts +9 -0
  210. package/src/dock/Dock.tsx +95 -0
  211. package/src/dock/LeaveMenu.tsx +21 -0
  212. package/src/dock/MoreSheet.tsx +233 -0
  213. package/src/dock/ReactionBurst.tsx +55 -0
  214. package/src/dock/RoutePicker.tsx +42 -0
  215. package/src/dock/audio-route.ts +69 -0
  216. package/src/frame/CallFrame.tsx +209 -0
  217. package/src/frame/Chalk.tsx +83 -0
  218. package/src/frame/back-handler.ts +19 -0
  219. package/src/frame/dev-scenario.ts +48 -0
  220. package/src/frame/device-layout.tsx +31 -0
  221. package/src/frame/measurement.ts +44 -0
  222. package/src/frame/portal.tsx +107 -0
  223. package/src/frame/preferences.tsx +48 -0
  224. package/src/frame/regions.test.ts +38 -0
  225. package/src/frame/regions.ts +60 -0
  226. package/src/haptics/haptics.ts +19 -0
  227. package/src/header/CallDetailsSheet.tsx +139 -0
  228. package/src/header/Header.tsx +107 -0
  229. package/src/header/QrCode.tsx +34 -0
  230. package/src/icons/Icon.tsx +35 -0
  231. package/src/icons/icon-element.ts +11 -0
  232. package/src/icons/icon-paths.ts +199 -0
  233. package/src/index.ts +6 -9
  234. package/src/limbo/Limbo.tsx +159 -0
  235. package/src/limbo/Veil.tsx +36 -0
  236. package/src/media/native-capture-error.test.ts +19 -0
  237. package/src/media/native-capture-error.ts +13 -0
  238. package/src/motion/MovingFrame.tsx +101 -0
  239. package/src/motion/motion.ts +33 -0
  240. package/src/motion/presence.ts +20 -0
  241. package/src/motion/press.ts +35 -0
  242. package/src/primitives/BottomSheet.tsx +211 -0
  243. package/src/primitives/ChalkText.tsx +23 -0
  244. package/src/primitives/CopyButton.tsx +78 -0
  245. package/src/primitives/IconButton.tsx +90 -0
  246. package/src/primitives/Menu.tsx +212 -0
  247. package/src/primitives/NameTag.tsx +68 -0
  248. package/src/primitives/PillButton.tsx +56 -0
  249. package/src/primitives/SegmentedControl.tsx +55 -0
  250. package/src/primitives/SheetGroup.tsx +72 -0
  251. package/src/primitives/Switch.tsx +39 -0
  252. package/src/primitives/TileShell.tsx +53 -0
  253. package/src/primitives/announce.ts +7 -0
  254. package/src/primitives/menu-placement.test.ts +50 -0
  255. package/src/primitives/menu-placement.ts +43 -0
  256. package/src/primitives/toast/Toast.tsx +50 -0
  257. package/src/primitives/toast/ToastStack.tsx +89 -0
  258. package/src/primitives/toast/toast-store.ts +86 -0
  259. package/src/sheets/people-chat/ChatNotices.tsx +69 -0
  260. package/src/sheets/people-chat/ChatTab.tsx +186 -0
  261. package/src/sheets/people-chat/Composer.tsx +182 -0
  262. package/src/sheets/people-chat/PeopleChatSheet.tsx +100 -0
  263. package/src/sheets/people-chat/PeopleTab.tsx +326 -0
  264. package/src/sheets/people-chat/PhotoViewer.tsx +132 -0
  265. package/src/sheets/people-chat/TextField.tsx +14 -0
  266. package/src/sheets/people-chat/attachments.tsx +183 -0
  267. package/src/sheets/people-chat/chat-model.ts +157 -0
  268. package/src/sheets/people-chat/people-chat-model.test.ts +98 -0
  269. package/src/sheets/people-chat/people-model.ts +68 -0
  270. package/src/sounds/assets/error.wav +0 -0
  271. package/src/sounds/assets/hand.wav +0 -0
  272. package/src/sounds/assets/join.wav +0 -0
  273. package/src/sounds/assets/knock.wav +0 -0
  274. package/src/sounds/assets/leave.wav +0 -0
  275. package/src/sounds/assets/message.wav +0 -0
  276. package/src/sounds/assets/mute.wav +0 -0
  277. package/src/sounds/assets/recording.wav +0 -0
  278. package/src/sounds/assets/test.wav +0 -0
  279. package/src/sounds/assets/unmute.wav +0 -0
  280. package/src/sounds/assets.d.ts +5 -0
  281. package/src/sounds/sounds.ts +50 -0
  282. package/src/space-client/create-native-space-client.ts +20 -181
  283. package/src/space-client/random-uuid.ts +17 -0
  284. package/src/space-client/whiteboard-watchdog.test.ts +23 -0
  285. package/src/stage/ContentTile.tsx +100 -0
  286. package/src/stage/ParticipantTile.tsx +140 -0
  287. package/src/stage/Stage.tsx +152 -0
  288. package/src/stage/TileSheet.tsx +116 -0
  289. package/src/stage/camera.ts +20 -0
  290. package/src/stage/layout.test.ts +77 -0
  291. package/src/stage/layout.ts +226 -0
  292. package/src/stage/space-selector.ts +31 -0
  293. package/src/stage/stage-view.tsx +68 -0
  294. package/src/status/CallOverlays.tsx +47 -0
  295. package/src/status/ChalkArrival.tsx +53 -0
  296. package/src/status/StatusPage.tsx +123 -0
  297. package/src/status/call-screen.test.ts +42 -0
  298. package/src/status/call-screen.ts +29 -0
  299. package/src/theme/theme.tsx +28 -0
  300. package/src/theme/tokens.ts +247 -0
  301. package/src/toasts/CallEventToasts.tsx +192 -0
  302. package/src/toasts/notices.test.ts +92 -0
  303. package/src/toasts/notices.ts +62 -0
  304. package/src/ui/ChalkUI.tsx +49 -0
  305. package/src/ui.ts +24 -0
  306. package/dist/android.d.ts +0 -2
  307. package/dist/android.d.ts.map +0 -1
  308. package/dist/android.js +0 -62
  309. package/dist/chunk-2OS7SSXE.js +0 -552
  310. package/dist/chunk-5DQ4KBRH.js +0 -690
  311. package/dist/chunk-ITLUHTVS.js +0 -5271
  312. package/dist/chunk-JB4LNWVP.js +0 -247
  313. package/dist/components/Chalk.d.ts +0 -79
  314. package/dist/components/Chalk.d.ts.map +0 -1
  315. package/dist/components/ChatMessageList.d.ts +0 -11
  316. package/dist/components/ChatMessageList.d.ts.map +0 -1
  317. package/dist/components/Entrance.d.ts +0 -22
  318. package/dist/components/Entrance.d.ts.map +0 -1
  319. package/dist/components/EntranceView.android.d.ts +0 -4
  320. package/dist/components/EntranceView.android.d.ts.map +0 -1
  321. package/dist/components/EntranceView.d.ts +0 -18
  322. package/dist/components/EntranceView.d.ts.map +0 -1
  323. package/dist/components/EntranceView.ios-pad.d.ts +0 -3
  324. package/dist/components/EntranceView.ios-pad.d.ts.map +0 -1
  325. package/dist/components/EntranceView.ios-phone.d.ts +0 -3
  326. package/dist/components/EntranceView.ios-phone.d.ts.map +0 -1
  327. package/dist/components/EntranceView.macos.d.ts +0 -4
  328. package/dist/components/EntranceView.macos.d.ts.map +0 -1
  329. package/dist/components/FaceAvatar.d.ts +0 -16
  330. package/dist/components/FaceAvatar.d.ts.map +0 -1
  331. package/dist/components/GradientSurface.d.ts +0 -14
  332. package/dist/components/GradientSurface.d.ts.map +0 -1
  333. package/dist/components/MediaView.d.ts +0 -14
  334. package/dist/components/MediaView.d.ts.map +0 -1
  335. package/dist/components/ReactionPicker.d.ts +0 -10
  336. package/dist/components/ReactionPicker.d.ts.map +0 -1
  337. package/dist/components/SpaceView.android.d.ts +0 -2
  338. package/dist/components/SpaceView.android.d.ts.map +0 -1
  339. package/dist/components/SpaceView.d.ts +0 -33
  340. package/dist/components/SpaceView.d.ts.map +0 -1
  341. package/dist/components/SpaceView.ios-pad.d.ts +0 -2
  342. package/dist/components/SpaceView.ios-pad.d.ts.map +0 -1
  343. package/dist/components/SpaceView.ios-phone.d.ts +0 -2
  344. package/dist/components/SpaceView.ios-phone.d.ts.map +0 -1
  345. package/dist/components/SpaceView.macos.d.ts +0 -2
  346. package/dist/components/SpaceView.macos.d.ts.map +0 -1
  347. package/dist/components/SpaceView.shared.d.ts +0 -3
  348. package/dist/components/SpaceView.shared.d.ts.map +0 -1
  349. package/dist/components/episode-diagnostic-render-observer.d.ts +0 -2
  350. package/dist/components/episode-diagnostic-render-observer.d.ts.map +0 -1
  351. package/dist/components/logo/Logo.d.ts +0 -11
  352. package/dist/components/logo/Logo.d.ts.map +0 -1
  353. package/dist/components/native-animation-controller.d.ts +0 -7
  354. package/dist/components/native-animation-controller.d.ts.map +0 -1
  355. package/dist/components/native-chat.d.ts +0 -8
  356. package/dist/components/native-chat.d.ts.map +0 -1
  357. package/dist/components/native-entrance/entrance-controller-store.d.ts +0 -34
  358. package/dist/components/native-entrance/entrance-controller-store.d.ts.map +0 -1
  359. package/dist/components/native-entrance/useEntranceController.d.ts +0 -19
  360. package/dist/components/native-entrance/useEntranceController.d.ts.map +0 -1
  361. package/dist/components/native-joining-loading-animation.d.ts +0 -9
  362. package/dist/components/native-joining-loading-animation.d.ts.map +0 -1
  363. package/dist/components/native-space-view/AppearanceSettings.d.ts +0 -2
  364. package/dist/components/native-space-view/AppearanceSettings.d.ts.map +0 -1
  365. package/dist/components/native-space-view/FeedbackSheet.d.ts +0 -12
  366. package/dist/components/native-space-view/FeedbackSheet.d.ts.map +0 -1
  367. package/dist/components/native-space-view/ParticipantActionSheet.d.ts +0 -9
  368. package/dist/components/native-space-view/ParticipantActionSheet.d.ts.map +0 -1
  369. package/dist/components/native-space-view/SettingsSheet.d.ts +0 -7
  370. package/dist/components/native-space-view/SettingsSheet.d.ts.map +0 -1
  371. package/dist/components/native-space-view/SpaceActionMenu.d.ts +0 -10
  372. package/dist/components/native-space-view/SpaceActionMenu.d.ts.map +0 -1
  373. package/dist/components/native-space-view/SpaceBottomDock.android.d.ts +0 -4
  374. package/dist/components/native-space-view/SpaceBottomDock.android.d.ts.map +0 -1
  375. package/dist/components/native-space-view/SpaceChatAttachment.d.ts +0 -8
  376. package/dist/components/native-space-view/SpaceChatAttachment.d.ts.map +0 -1
  377. package/dist/components/native-space-view/SpaceChatSheet.d.ts +0 -5
  378. package/dist/components/native-space-view/SpaceChatSheet.d.ts.map +0 -1
  379. package/dist/components/native-space-view/SpaceGrid.android.d.ts +0 -8
  380. package/dist/components/native-space-view/SpaceGrid.android.d.ts.map +0 -1
  381. package/dist/components/native-space-view/SpaceMoreSheet.d.ts +0 -7
  382. package/dist/components/native-space-view/SpaceMoreSheet.d.ts.map +0 -1
  383. package/dist/components/native-space-view/SpacePanelSheet.d.ts +0 -5
  384. package/dist/components/native-space-view/SpacePanelSheet.d.ts.map +0 -1
  385. package/dist/components/native-space-view/SpacePeopleSheet.d.ts +0 -5
  386. package/dist/components/native-space-view/SpacePeopleSheet.d.ts.map +0 -1
  387. package/dist/components/native-space-view/SpaceStage.android.d.ts +0 -29
  388. package/dist/components/native-space-view/SpaceStage.android.d.ts.map +0 -1
  389. package/dist/components/native-space-view/SpaceSurfacePrimitives.d.ts +0 -30
  390. package/dist/components/native-space-view/SpaceSurfacePrimitives.d.ts.map +0 -1
  391. package/dist/components/native-space-view/SpaceTopBar.android.d.ts +0 -11
  392. package/dist/components/native-space-view/SpaceTopBar.android.d.ts.map +0 -1
  393. package/dist/components/native-space-view/SpaceWhiteboardSurface.d.ts +0 -15
  394. package/dist/components/native-space-view/SpaceWhiteboardSurface.d.ts.map +0 -1
  395. package/dist/components/native-space-view/diagnostics.d.ts +0 -48
  396. package/dist/components/native-space-view/diagnostics.d.ts.map +0 -1
  397. package/dist/components/native-space-view/format-space-duration.d.ts +0 -2
  398. package/dist/components/native-space-view/format-space-duration.d.ts.map +0 -1
  399. package/dist/components/native-space-view/native-space-grid-keys.d.ts +0 -3
  400. package/dist/components/native-space-view/native-space-grid-keys.d.ts.map +0 -1
  401. package/dist/components/native-space-view/native-space-page-store.d.ts +0 -8
  402. package/dist/components/native-space-view/native-space-page-store.d.ts.map +0 -1
  403. package/dist/components/native-space-view/space-chat-attachments.d.ts +0 -15
  404. package/dist/components/native-space-view/space-chat-attachments.d.ts.map +0 -1
  405. package/dist/components/native-space-view/space-panel-layout.d.ts +0 -10
  406. package/dist/components/native-space-view/space-panel-layout.d.ts.map +0 -1
  407. package/dist/components/native-space-view/space-progressive-surface-helpers.d.ts +0 -16
  408. package/dist/components/native-space-view/space-progressive-surface-helpers.d.ts.map +0 -1
  409. package/dist/components/native-space-view/space-progressive-surface-types.d.ts +0 -3
  410. package/dist/components/native-space-view/space-progressive-surface-types.d.ts.map +0 -1
  411. package/dist/components/native-space-view/types.d.ts +0 -24
  412. package/dist/components/native-space-view/types.d.ts.map +0 -1
  413. package/dist/components/native-space-view/useSpaceViewController.contract.d.ts +0 -41
  414. package/dist/components/native-space-view/useSpaceViewController.contract.d.ts.map +0 -1
  415. package/dist/components/native-space-view/useSpaceViewController.d.ts +0 -156
  416. package/dist/components/native-space-view/useSpaceViewController.d.ts.map +0 -1
  417. package/dist/components/native-space-view/useSpaceViewDerived.d.ts +0 -12
  418. package/dist/components/native-space-view/useSpaceViewDerived.d.ts.map +0 -1
  419. package/dist/components/native-space-view/useSpaceViewPanels.d.ts +0 -70
  420. package/dist/components/native-space-view/useSpaceViewPanels.d.ts.map +0 -1
  421. package/dist/feedback/native-capture.d.ts +0 -9
  422. package/dist/feedback/native-capture.d.ts.map +0 -1
  423. package/dist/feedback/native-evidence.d.ts +0 -3
  424. package/dist/feedback/native-evidence.d.ts.map +0 -1
  425. package/dist/hooks/entrance-preview-store.d.ts +0 -16
  426. package/dist/hooks/entrance-preview-store.d.ts.map +0 -1
  427. package/dist/hooks/useEntrancePreview.d.ts +0 -7
  428. package/dist/hooks/useEntrancePreview.d.ts.map +0 -1
  429. package/dist/hooks/useLayout.d.ts +0 -16
  430. package/dist/hooks/useLayout.d.ts.map +0 -1
  431. package/dist/hooks/usePanels.d.ts +0 -12
  432. package/dist/hooks/usePanels.d.ts.map +0 -1
  433. package/dist/ios-pad.d.ts +0 -2
  434. package/dist/ios-pad.d.ts.map +0 -1
  435. package/dist/ios-pad.js +0 -62
  436. package/dist/ios-phone.d.ts +0 -2
  437. package/dist/ios-phone.d.ts.map +0 -1
  438. package/dist/ios-phone.js +0 -62
  439. package/dist/ios.d.ts +0 -2
  440. package/dist/ios.d.ts.map +0 -1
  441. package/dist/ios.js +0 -62
  442. package/dist/macos.d.ts +0 -2
  443. package/dist/macos.d.ts.map +0 -1
  444. package/dist/macos.js +0 -62
  445. package/dist/space-client/cloudflare-rtk-native.d.ts +0 -3
  446. package/dist/space-client/cloudflare-rtk-native.d.ts.map +0 -1
  447. package/dist/telemetry.d.ts +0 -63
  448. package/dist/telemetry.d.ts.map +0 -1
  449. package/dist/ui/appearance.d.ts +0 -246
  450. package/dist/ui/appearance.d.ts.map +0 -1
  451. package/dist/ui/appearance.js +0 -14
  452. package/dist/ui/native-appearance-context.d.ts +0 -15
  453. package/dist/ui/native-appearance-context.d.ts.map +0 -1
  454. package/dist/ui/native-theme.d.ts +0 -17
  455. package/dist/ui/native-theme.d.ts.map +0 -1
  456. package/dist/ui/native-types.d.ts +0 -38
  457. package/dist/ui/native-types.d.ts.map +0 -1
  458. package/dist/ui/participant-avatar.d.ts +0 -21
  459. package/dist/ui/participant-avatar.d.ts.map +0 -1
  460. package/dist/ui/theme-tokens.d.ts +0 -25
  461. package/dist/ui/theme-tokens.d.ts.map +0 -1
  462. package/dist/ui/theme.d.ts +0 -404
  463. package/dist/ui/theme.d.ts.map +0 -1
  464. package/dist/ui/theme.js +0 -8
  465. package/dist/utils/native-space-layout.d.ts +0 -42
  466. package/dist/utils/native-space-layout.d.ts.map +0 -1
  467. package/src/android.ts +0 -1
  468. package/src/components/Chalk.tsx +0 -363
  469. package/src/components/ChatMessageList.tsx +0 -209
  470. package/src/components/Entrance.tsx +0 -42
  471. package/src/components/EntranceView.android.tsx +0 -274
  472. package/src/components/EntranceView.ios-pad.tsx +0 -326
  473. package/src/components/EntranceView.ios-phone.tsx +0 -271
  474. package/src/components/EntranceView.macos.tsx +0 -278
  475. package/src/components/EntranceView.tsx +0 -38
  476. package/src/components/FaceAvatar.tsx +0 -120
  477. package/src/components/GradientSurface.tsx +0 -23
  478. package/src/components/MediaView.tsx +0 -76
  479. package/src/components/ReactionPicker.tsx +0 -98
  480. package/src/components/SpaceView.android.tsx +0 -1
  481. package/src/components/SpaceView.ios-pad.tsx +0 -1
  482. package/src/components/SpaceView.ios-phone.tsx +0 -1
  483. package/src/components/SpaceView.macos.tsx +0 -1
  484. package/src/components/SpaceView.shared.tsx +0 -98
  485. package/src/components/SpaceView.tsx +0 -54
  486. package/src/components/episode-diagnostic-render-observer.ts +0 -12
  487. package/src/components/logo/Logo.tsx +0 -309
  488. package/src/components/native-animation-controller.ts +0 -36
  489. package/src/components/native-chat.ts +0 -23
  490. package/src/components/native-entrance/entrance-controller-store.ts +0 -112
  491. package/src/components/native-entrance/useEntranceController.ts +0 -60
  492. package/src/components/native-joining-loading-animation.ts +0 -26
  493. package/src/components/native-space-view/AppearanceSettings.tsx +0 -114
  494. package/src/components/native-space-view/FeedbackSheet.tsx +0 -296
  495. package/src/components/native-space-view/ParticipantActionSheet.tsx +0 -99
  496. package/src/components/native-space-view/SettingsSheet.tsx +0 -164
  497. package/src/components/native-space-view/SpaceActionMenu.tsx +0 -109
  498. package/src/components/native-space-view/SpaceBottomDock.android.tsx +0 -115
  499. package/src/components/native-space-view/SpaceChatAttachment.tsx +0 -105
  500. package/src/components/native-space-view/SpaceChatSheet.tsx +0 -184
  501. package/src/components/native-space-view/SpaceGrid.android.tsx +0 -323
  502. package/src/components/native-space-view/SpaceMoreSheet.tsx +0 -190
  503. package/src/components/native-space-view/SpacePanelSheet.tsx +0 -45
  504. package/src/components/native-space-view/SpacePeopleSheet.tsx +0 -116
  505. package/src/components/native-space-view/SpaceStage.android.tsx +0 -372
  506. package/src/components/native-space-view/SpaceSurfacePrimitives.tsx +0 -75
  507. package/src/components/native-space-view/SpaceTopBar.android.tsx +0 -113
  508. package/src/components/native-space-view/SpaceWhiteboardSurface.tsx +0 -69
  509. package/src/components/native-space-view/diagnostics.ts +0 -84
  510. package/src/components/native-space-view/format-space-duration.ts +0 -3
  511. package/src/components/native-space-view/native-space-grid-keys.ts +0 -6
  512. package/src/components/native-space-view/native-space-page-store.ts +0 -41
  513. package/src/components/native-space-view/space-chat-attachments.test.ts +0 -26
  514. package/src/components/native-space-view/space-chat-attachments.ts +0 -37
  515. package/src/components/native-space-view/space-panel-layout.ts +0 -10
  516. package/src/components/native-space-view/space-progressive-surface-helpers.ts +0 -33
  517. package/src/components/native-space-view/space-progressive-surface-types.ts +0 -3
  518. package/src/components/native-space-view/types.ts +0 -25
  519. package/src/components/native-space-view/useSpaceViewController.contract.ts +0 -34
  520. package/src/components/native-space-view/useSpaceViewController.ts +0 -416
  521. package/src/components/native-space-view/useSpaceViewDerived.ts +0 -29
  522. package/src/components/native-space-view/useSpaceViewPanels.ts +0 -170
  523. package/src/feedback/native-capture.ts +0 -62
  524. package/src/feedback/native-evidence.ts +0 -37
  525. package/src/hooks/entrance-preview-store.ts +0 -99
  526. package/src/hooks/useEntrancePreview.ts +0 -26
  527. package/src/hooks/useLayout.ts +0 -37
  528. package/src/hooks/usePanels.ts +0 -30
  529. package/src/ios-pad.ts +0 -1
  530. package/src/ios-phone.ts +0 -1
  531. package/src/ios.ts +0 -1
  532. package/src/macos.ts +0 -1
  533. package/src/space-client/cloudflare-rtk-native.ts +0 -104
  534. package/src/telemetry.ts +0 -234
  535. package/src/types/hugeicons-direct.d.ts +0 -4
  536. package/src/types/react-native-safe-area-context.d.ts +0 -8
  537. package/src/ui/appearance.ts +0 -808
  538. package/src/ui/native-appearance-context.tsx +0 -74
  539. package/src/ui/native-theme.tsx +0 -54
  540. package/src/ui/native-types.ts +0 -40
  541. package/src/ui/participant-avatar.ts +0 -47
  542. package/src/ui/theme-tokens.ts +0 -41
  543. package/src/ui/theme.ts +0 -214
  544. package/src/utils/native-space-layout.ts +0 -135
package/dist/index.js CHANGED
@@ -1,54 +1,2972 @@
1
1
  import {
2
- Chalk,
3
- ChalkProvider,
4
- Entrance,
5
- Logo,
6
- buildChalkInviteLink,
7
- useCan,
8
- useChat,
9
- useConnection,
10
- useMedia,
11
- useParticipants,
12
- useReactions,
13
- useRecording,
14
- useSelf,
15
- useSpaceClient,
16
- useWhiteboard
17
- } from "./chunk-ITLUHTVS.js";
18
- import {
19
- THEME_PALETTES,
20
- THEME_TEXTURES,
21
- getThemeMode,
22
- isDarkThemePalette,
23
- resolveNativeAppearance
24
- } from "./chunk-5DQ4KBRH.js";
25
- import {
26
- Theme,
27
- createNativeTheme
28
- } from "./chunk-JB4LNWVP.js";
29
- import "./chunk-2OS7SSXE.js";
30
- import "./chunk-F3NPRVHO.js";
2
+ useDevScenario
3
+ } from "./chunk-IHE6EA3T.js";
31
4
  import {
32
5
  extractSpaceInviteFromLink,
33
6
  getClipboardInviteSuggestion,
34
7
  parseSpaceInviteLink
35
8
  } from "./chunk-4L5GXJ36.js";
9
+ import {
10
+ BottomSheet,
11
+ ChalkText,
12
+ CopyButton,
13
+ DeviceLayoutProvider,
14
+ FaceAvatar,
15
+ Icon,
16
+ IconButton,
17
+ Limbo,
18
+ Menu,
19
+ MenuItem,
20
+ MenuSection,
21
+ PillButton,
22
+ Portal,
23
+ PortalHost,
24
+ PortalProvider,
25
+ STAGE_GAP,
26
+ SegmentedControl,
27
+ SheetDragRegion,
28
+ SheetGroup,
29
+ SheetRow,
30
+ Switch,
31
+ ThemeProvider,
32
+ ToastProvider,
33
+ ToastStack,
34
+ announce,
35
+ chatOverlays,
36
+ durations,
37
+ easings,
38
+ elevations,
39
+ fileKindColors,
40
+ fixed,
41
+ fontFamilies,
42
+ frameRegions,
43
+ haptics,
44
+ pressScale,
45
+ radii,
46
+ sizes,
47
+ timings,
48
+ type,
49
+ useBackHandler,
50
+ useDeviceLayout,
51
+ usePortalCount,
52
+ usePressAnimation,
53
+ useTheme,
54
+ useToasts,
55
+ viewer
56
+ } from "./chunk-OIBT2FNM.js";
57
+ import {
58
+ useMeasuredRef
59
+ } from "./chunk-GYVZSOGQ.js";
60
+
61
+ // src/frame/Chalk.tsx
62
+ import { useEffect as useEffect20 } from "react";
63
+ import { StyleSheet as StyleSheet24 } from "react-native";
64
+ import { GestureHandlerRootView } from "react-native-gesture-handler";
65
+ import { SafeAreaProvider as SafeAreaProvider2 } from "react-native-safe-area-context";
66
+
67
+ // src/context/space-client-context.tsx
68
+ import { createContext, useContext, useMemo } from "react";
69
+ import { jsx } from "react/jsx-runtime";
70
+ var SpaceClientContext = createContext(null);
71
+ function ChalkProvider({ children, client }) {
72
+ const value = useMemo(() => client, [client]);
73
+ return /* @__PURE__ */ jsx(SpaceClientContext.Provider, { value, children });
74
+ }
75
+ function useSpaceClient() {
76
+ const client = useContext(SpaceClientContext);
77
+ if (!client) throw new Error("Chalk hooks must be used within a ChalkProvider.");
78
+ return client;
79
+ }
80
+
81
+ // src/sounds/sounds.ts
82
+ import { createAudioPlayer } from "expo-audio";
83
+ import error from "./error-YOPTEH5N.wav";
84
+ import hand from "./hand-DFWVXPH2.wav";
85
+ import join from "./join-Z6RL5AHE.wav";
86
+ import knock from "./knock-4VBQSHK4.wav";
87
+ import leave from "./leave-MQLZOWQD.wav";
88
+ import message from "./message-6Y6WJGTB.wav";
89
+ import mute from "./mute-H5SJFXVI.wav";
90
+ import recording from "./recording-Q4GB77Y4.wav";
91
+ import test from "./test-VD2KAM4D.wav";
92
+ import unmute from "./unmute-2XR2LICU.wav";
93
+ var sources = { join, leave, knock, message, hand, mute, unmute, recording, error, test };
94
+ var players = /* @__PURE__ */ new Map();
95
+ function playSound(name, enabled) {
96
+ if (!enabled && name !== "test") return;
97
+ let player = players.get(name);
98
+ if (!player) {
99
+ player = createAudioPlayer(sources[name], { keepAudioSessionActive: true });
100
+ players.set(name, player);
101
+ }
102
+ void player.seekTo(0);
103
+ player.play();
104
+ }
105
+ function releaseSounds() {
106
+ for (const player of players.values()) player.remove();
107
+ players.clear();
108
+ }
109
+
110
+ // src/stage/stage-view.tsx
111
+ import { createContext as createContext2, useContext as useContext2, useEffect, useMemo as useMemo2, useState } from "react";
112
+ import { jsx as jsx2 } from "react/jsx-runtime";
113
+ var StageViewContext = createContext2(null);
114
+ function StageViewProvider({ children }) {
115
+ const [layout, setLayout] = useState("spotlight");
116
+ const [pinned, setPinned] = useState(null);
117
+ const [mirror, setMirror] = useState(true);
118
+ const dev = useDevScenario();
119
+ useEffect(() => {
120
+ if (dev.layout) setLayout(dev.layout);
121
+ if (dev.pinned) setPinned(dev.pinned);
122
+ }, [dev]);
123
+ const value = useMemo2(
124
+ () => ({
125
+ layout,
126
+ pinned,
127
+ mirror,
128
+ setLayout: (next) => {
129
+ setLayout(next);
130
+ if (next === "grid") setPinned(null);
131
+ },
132
+ togglePin: (participantId) => {
133
+ if (layout === "grid") {
134
+ setLayout("spotlight");
135
+ setPinned(participantId);
136
+ return;
137
+ }
138
+ setPinned((current) => current === participantId ? null : participantId);
139
+ },
140
+ showContent: () => {
141
+ setLayout("spotlight");
142
+ setPinned(null);
143
+ },
144
+ setMirror
145
+ }),
146
+ [layout, mirror, pinned]
147
+ );
148
+ return /* @__PURE__ */ jsx2(StageViewContext.Provider, { value, children });
149
+ }
150
+ function useStageView() {
151
+ const view = useContext2(StageViewContext);
152
+ if (!view) throw new Error("useStageView must be used inside <Chalk>.");
153
+ return view;
154
+ }
155
+
156
+ // src/frame/CallFrame.tsx
157
+ import { useCallback as useCallback15, useEffect as useEffect19, useMemo as useMemo12, useRef as useRef13, useState as useState13 } from "react";
158
+ import { Pressable as Pressable15, StatusBar, StyleSheet as StyleSheet23, View as View22 } from "react-native";
159
+ import Animated12, { useAnimatedStyle as useAnimatedStyle7, useSharedValue as useSharedValue5, withTiming as withTiming7 } from "react-native-reanimated";
160
+
161
+ // src/components/useAndroidCallService.ts
162
+ import { useEffect as useEffect2 } from "react";
163
+ import { AppState } from "react-native";
164
+
165
+ // src/android/call-service.ts
166
+ import { NativeModules, Platform } from "react-native";
167
+ var nativeModule = NativeModules.ChalkCallService;
168
+ async function startAndroidCallService() {
169
+ if (Platform.OS !== "android" || !nativeModule) return false;
170
+ return nativeModule.start();
171
+ }
172
+ async function stopAndroidCallService() {
173
+ if (Platform.OS !== "android" || !nativeModule) return;
174
+ await nativeModule.stop();
175
+ }
176
+
177
+ // src/hooks/space-hooks.ts
178
+ import { useCallback, useSyncExternalStore } from "react";
179
+ function useSpaceSlice(slice) {
180
+ const client = useSpaceClient();
181
+ const subscribe = useCallback((listener) => client.subscribe(listener), [client]);
182
+ const getSnapshot = useCallback(() => client.getSnapshot()[slice], [client, slice]);
183
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
184
+ }
185
+ function useConnection() {
186
+ return useSpaceSlice("connection");
187
+ }
188
+ function useSelf() {
189
+ return useSpaceSlice("self");
190
+ }
191
+ function useParticipants() {
192
+ return useSpaceSlice("participants");
193
+ }
194
+ function useMedia() {
195
+ return useSpaceSlice("media");
196
+ }
197
+ function useChat() {
198
+ return useSpaceSlice("chat");
199
+ }
200
+ function useReactions() {
201
+ return useSpaceSlice("reactions");
202
+ }
203
+ function useRecording() {
204
+ return useSpaceSlice("recording");
205
+ }
206
+ function useWhiteboard() {
207
+ return useSpaceSlice("whiteboard");
208
+ }
209
+ function useCan(capability) {
210
+ return useSelf().can(capability);
211
+ }
212
+
213
+ // src/components/useAndroidCallService.ts
214
+ function useAndroidCallService() {
215
+ const connection = useConnection();
216
+ const media = useMedia();
217
+ const inCall = connection.status === "live" || connection.status === "reconnecting";
218
+ const microphoneOn = media.local.microphone.state === "enabled";
219
+ const cameraOn = media.local.camera.state === "enabled";
220
+ useEffect2(() => {
221
+ if (!inCall) return;
222
+ const start = () => {
223
+ startAndroidCallService().catch((error2) => {
224
+ console.warn("Failed to start the Android call service", error2);
225
+ });
226
+ };
227
+ start();
228
+ const subscription = AppState.addEventListener("change", (state) => {
229
+ if (state === "active") start();
230
+ });
231
+ return () => subscription.remove();
232
+ }, [inCall, microphoneOn, cameraOn]);
233
+ useEffect2(() => {
234
+ if (!inCall) return;
235
+ return () => {
236
+ stopAndroidCallService().catch((error2) => {
237
+ console.warn("Failed to stop the Android call service", error2);
238
+ });
239
+ };
240
+ }, [inCall]);
241
+ }
242
+
243
+ // src/dock/Dock.tsx
244
+ import { memo, useCallback as useCallback3, useEffect as useEffect4, useRef, useState as useState3 } from "react";
245
+ import { StyleSheet, View } from "react-native";
246
+
247
+ // src/dock/audio-route.ts
248
+ import { useCallback as useCallback2, useEffect as useEffect3, useState as useState2 } from "react";
249
+ import { NativeEventEmitter, NativeModules as NativeModules2, Platform as Platform2 } from "react-native";
250
+ var nativeModule2 = NativeModules2.ChalkAudioRouteModule;
251
+ var emitter = nativeModule2 ? new NativeEventEmitter(nativeModule2) : null;
252
+ var routeIcons = { earpiece: "earpiece", speaker: "vol", bluetooth: "buds", wired: "buds" };
253
+ function routeName(route) {
254
+ if (route.kind === "speaker") return "Speaker";
255
+ if (route.kind === "earpiece") return Platform2.OS === "ios" ? "iPhone" : "Phone";
256
+ return route.name;
257
+ }
258
+ function useAudioOutput() {
259
+ const scenario = useDevScenario().audioRoutes;
260
+ const [state, setState] = useState2(null);
261
+ const [scenarioSelected, setScenarioSelected] = useState2(null);
262
+ useEffect3(() => {
263
+ if (!nativeModule2 || !emitter) return;
264
+ let live = true;
265
+ nativeModule2.getRoutes().then((next) => live && setState(next)).catch(() => void 0);
266
+ const subscription = emitter.addListener(nativeModule2.eventName, (next) => setState(next));
267
+ return () => {
268
+ live = false;
269
+ subscription.remove();
270
+ };
271
+ }, []);
272
+ const select = useCallback2(async (id) => {
273
+ if (!nativeModule2) return;
274
+ setState(await nativeModule2.selectRoute(id));
275
+ }, []);
276
+ const routes = scenario ? { ...scenario, selected: scenarioSelected ?? scenario.selected } : state;
277
+ if (!routes) return null;
278
+ const current = routes.routes.find((route) => route.id === routes.selected) ?? routes.routes[0];
279
+ if (!current) return null;
280
+ return { ...routes, current, select: scenario ? async (id) => setScenarioSelected(id) : select };
281
+ }
282
+
283
+ // src/dock/RoutePicker.tsx
284
+ import { Platform as Platform3 } from "react-native";
285
+ import { jsx as jsx3 } from "react/jsx-runtime";
286
+ function RoutePicker({ open, onClose, onSelect, output, anchorRef }) {
287
+ const { colors } = useTheme();
288
+ if (Platform3.OS === "ios") {
289
+ return /* @__PURE__ */ jsx3(Menu, { accessibilityLabel: "Audio output", anchorRef, onClose, open, testID: "menu.route", children: output.routes.map((route) => /* @__PURE__ */ jsx3(MenuItem, { checked: route.id === output.selected, icon: routeIcons[route.kind], label: routeName(route), onPress: () => onSelect(route.id), testID: `menu.route.${route.kind}` }, route.id)) });
290
+ }
291
+ return /* @__PURE__ */ jsx3(BottomSheet, { accessibilityLabel: "Audio output", onClose, open, returnFocusRef: anchorRef, testID: "sheet.route", title: "Audio output", children: /* @__PURE__ */ jsx3(SheetGroup, { children: output.routes.map((route) => /* @__PURE__ */ jsx3(SheetRow, { end: route.id === output.selected ? /* @__PURE__ */ jsx3(Icon, { color: colors.text, name: "check" }) : null, icon: routeIcons[route.kind], label: routeName(route), onPress: () => onSelect(route.id), testID: `sheet.route.${route.kind}` }, route.id)) }) });
292
+ }
293
+
294
+ // src/dock/Dock.tsx
295
+ import { Fragment, jsx as jsx4, jsxs } from "react/jsx-runtime";
296
+ var Dock = memo(function Dock2({ arrangement, microphoneOn, cameraOn, onToggleMicrophone, onToggleCamera, onOpenMore, onLeave, moreOpen, leaveOpen, moreRef, leaveRef }) {
297
+ const size = arrangement === "pad" ? 44 : 52;
298
+ const dockStyle = [styles.dock, arrangement === "pad" ? styles.pad : arrangement === "landscape" ? styles.landscape : styles.portrait];
299
+ const dockRef = useMeasuredRef("frame.dock", dockStyle);
300
+ const output = useAudioOutput();
301
+ return /* @__PURE__ */ jsxs(View, { accessibilityLabel: "Call controls", accessibilityRole: "toolbar", pointerEvents: "box-none", ref: dockRef, style: dockStyle, testID: "frame.dock", children: [
302
+ /* @__PURE__ */ jsx4(IconButton, { accessibilityLabel: microphoneOn ? "Mute microphone" : "Unmute microphone", icon: microphoneOn ? "mic" : "micOff", off: !microphoneOn, onCanvas: true, onPress: onToggleMicrophone, size, testID: "dock.microphone" }),
303
+ /* @__PURE__ */ jsx4(IconButton, { accessibilityLabel: cameraOn ? "Turn off camera" : "Turn on camera", icon: cameraOn ? "cam" : "camOff", off: !cameraOn, onCanvas: true, onPress: onToggleCamera, size, testID: "dock.camera" }),
304
+ output ? /* @__PURE__ */ jsx4(AudioOutputButton, { captioned: arrangement === "portrait", output, size }) : null,
305
+ /* @__PURE__ */ jsx4(IconButton, { accessibilityLabel: "More options", expanded: moreOpen, icon: "more", onCanvas: true, onPress: onOpenMore, ref: moreRef, size, testID: "dock.more" }),
306
+ arrangement === "pad" ? /* @__PURE__ */ jsx4(View, { style: styles.gap }) : null,
307
+ /* @__PURE__ */ jsx4(IconButton, { accessibilityLabel: "Leave call", expanded: leaveOpen, icon: "hang", onPress: onLeave, ref: leaveRef, size, testID: "dock.leave", tone: "danger" })
308
+ ] });
309
+ });
310
+ function AudioOutputButton({ output, size, captioned }) {
311
+ const toasts = useToasts();
312
+ const ref = useRef(null);
313
+ const [open, setOpen] = useState3(false);
314
+ const devScenario = useDevScenario();
315
+ useEffect4(() => {
316
+ if (devScenario.sheet === "route" || devScenario.menu === "route") setOpen(true);
317
+ }, [devScenario]);
318
+ const name = routeName(output.current);
319
+ const choosing = output.routes.length > 2;
320
+ const other = output.routes.find((route) => route.id !== output.selected);
321
+ const select = useCallback3(
322
+ (id) => {
323
+ setOpen(false);
324
+ const route = output.routes.find((candidate) => candidate.id === id);
325
+ output.select(id).then(() => route && announce(`Audio output: ${routeName(route)}`)).catch(() => toasts.show({ kind: "error", dismissible: true, message: "Couldn't switch the audio output." }));
326
+ },
327
+ [output, toasts]
328
+ );
329
+ const press = useCallback3(() => {
330
+ if (choosing) setOpen((was) => !was);
331
+ else if (other) select(other.id);
332
+ }, [choosing, other, select]);
333
+ const hint = choosing || !other ? "Choose output" : output.current.kind === "speaker" ? `Switch to ${routeName(other)}` : "Switch to speaker";
334
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
335
+ /* @__PURE__ */ jsx4(IconButton, { accessibilityLabel: `Audio output: ${name}. ${hint}`, caption: captioned ? name : void 0, expanded: choosing ? open : void 0, icon: routeIcons[output.current.kind], onCanvas: true, onPress: press, ref, size, testID: "dock.route" }),
336
+ choosing ? /* @__PURE__ */ jsx4(RoutePicker, { anchorRef: ref, onClose: () => setOpen(false), onSelect: select, open, output }) : null
337
+ ] });
338
+ }
339
+ var styles = StyleSheet.create({
340
+ dock: { flex: 1, flexDirection: "row", alignItems: "center" },
341
+ portrait: { justifyContent: "space-between", paddingHorizontal: 20 },
342
+ landscape: { justifyContent: "center", gap: 14 },
343
+ pad: { justifyContent: "center", gap: 8 },
344
+ gap: { width: 8 }
345
+ });
346
+
347
+ // src/dock/LeaveMenu.tsx
348
+ import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
349
+ function LeaveMenu({ open, onClose, anchorRef, onLeave, onEnd }) {
350
+ return /* @__PURE__ */ jsxs2(Menu, { accessibilityLabel: "Leave", anchorRef, onClose, open, testID: "menu.leave", children: [
351
+ /* @__PURE__ */ jsx5(MenuItem, { label: "Leave call", onPress: onLeave, testID: "menu.leave.leave" }),
352
+ /* @__PURE__ */ jsx5(MenuItem, { danger: true, label: "End call for everyone", onPress: onEnd, testID: "menu.leave.end" })
353
+ ] });
354
+ }
355
+
356
+ // src/dock/MoreSheet.tsx
357
+ import { setStringAsync } from "expo-clipboard";
358
+ import { useCallback as useCallback5, useEffect as useEffect5, useRef as useRef2, useState as useState5 } from "react";
359
+ import { Platform as Platform4, Pressable, Share, StyleSheet as StyleSheet2, Text, View as View2 } from "react-native";
360
+ import Animated, { useAnimatedStyle } from "react-native-reanimated";
361
+
362
+ // src/frame/preferences.tsx
363
+ import { createContext as createContext3, useCallback as useCallback4, useContext as useContext3, useMemo as useMemo3, useState as useState4 } from "react";
364
+ import { jsx as jsx6 } from "react/jsx-runtime";
365
+ var PreferencesContext = createContext3(null);
366
+ function PreferencesProvider({ colorScheme, onColorSchemeChange, soundsEnabled, onSoundsEnabledChange, children }) {
367
+ const [ownScheme, setOwnScheme] = useState4("dark");
368
+ const [ownSounds, setOwnSounds] = useState4(true);
369
+ const setColorScheme = useCallback4(
370
+ (scheme) => {
371
+ setOwnScheme(scheme);
372
+ onColorSchemeChange?.(scheme);
373
+ },
374
+ [onColorSchemeChange]
375
+ );
376
+ const setSoundsEnabled = useCallback4(
377
+ (enabled) => {
378
+ setOwnSounds(enabled);
379
+ onSoundsEnabledChange?.(enabled);
380
+ },
381
+ [onSoundsEnabledChange]
382
+ );
383
+ const value = useMemo3(() => ({ colorScheme: colorScheme ?? ownScheme, setColorScheme, soundsEnabled: soundsEnabled ?? ownSounds, setSoundsEnabled }), [colorScheme, ownScheme, ownSounds, setColorScheme, setSoundsEnabled, soundsEnabled]);
384
+ return /* @__PURE__ */ jsx6(PreferencesContext.Provider, { value, children: children(value) });
385
+ }
386
+ function usePreferences() {
387
+ const preferences = useContext3(PreferencesContext);
388
+ if (!preferences) throw new Error("usePreferences must be used inside <Chalk>.");
389
+ return preferences;
390
+ }
391
+
392
+ // src/dock/MoreSheet.tsx
393
+ import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
394
+ var REACTIONS = [
395
+ ["\u{1F44D}", "thumbs up"],
396
+ ["\u2764\uFE0F", "heart"],
397
+ ["\u{1F602}", "laughter"],
398
+ ["\u{1F62E}", "surprise"],
399
+ ["\u{1F389}", "celebration"],
400
+ ["\u{1F622}", "sadness"]
401
+ ];
402
+ function MoreSheet({ open, onClose, onOpenPeople, onOpenChat, inviteLink, returnFocusRef }) {
403
+ const client = useSpaceClient();
404
+ const self = useSelf();
405
+ const people2 = useParticipants().roster.length;
406
+ const toasts = useToasts();
407
+ const { soundsEnabled } = usePreferences();
408
+ const canRaiseHand = useCan("raiseHand");
409
+ const canReact = useCan("sendReaction");
410
+ const fail = useCallback5((message2) => () => toasts.show({ kind: "error", dismissible: true, message: message2 }), [toasts]);
411
+ const toggleHand = useCallback5(() => {
412
+ onClose();
413
+ haptics.handRaise();
414
+ if (self.handRaised) {
415
+ client.participants.lowerHand().catch(fail("Couldn't lower your hand."));
416
+ return;
417
+ }
418
+ playSound("hand", soundsEnabled);
419
+ announce("You raised a hand");
420
+ client.participants.raiseHand().catch(fail("Couldn't raise your hand."));
421
+ }, [client, fail, onClose, self.handRaised, soundsEnabled]);
422
+ const react = useCallback5(
423
+ (emoji) => {
424
+ onClose();
425
+ client.reactions.send(emoji).catch(fail("Couldn't send your reaction."));
426
+ },
427
+ [client, fail, onClose]
428
+ );
429
+ return /* @__PURE__ */ jsxs3(BottomSheet, { accessibilityLabel: "More options", onClose, open, returnFocusRef, testID: "sheet.more", children: [
430
+ canRaiseHand || canReact ? /* @__PURE__ */ jsxs3(View2, { accessibilityLabel: "Reactions", accessibilityRole: "toolbar", style: styles2.reactions, children: [
431
+ canRaiseHand ? /* @__PURE__ */ jsx7(HandButton, { onPress: toggleHand, raised: self.handRaised }) : null,
432
+ canReact ? REACTIONS.map(([emoji, name], index) => /* @__PURE__ */ jsx7(EmojiButton, { emoji, name, onPress: react, testID: `more.reaction.${index}` }, emoji)) : null
433
+ ] }) : null,
434
+ /* @__PURE__ */ jsxs3(SheetGroup, { children: [
435
+ inviteLink ? /* @__PURE__ */ jsx7(CopyLinkRow, { link: inviteLink }) : null,
436
+ inviteLink ? /* @__PURE__ */ jsx7(SheetRow, { icon: "iosShare", label: "Share link", onPress: () => shareLink(inviteLink), testID: "more.share" }) : null,
437
+ /* @__PURE__ */ jsx7(RecordingRow, { onDone: onClose }),
438
+ /* @__PURE__ */ jsx7(ChatRow, { onPress: onOpenChat }),
439
+ /* @__PURE__ */ jsx7(SheetRow, { end: /* @__PURE__ */ jsx7(PeopleCount, { count: people2 }), icon: "users", label: "People", onPress: onOpenPeople, testID: "more.people" }),
440
+ /* @__PURE__ */ jsx7(LayoutRow, {})
441
+ ] })
442
+ ] });
443
+ }
444
+ function shareLink(link) {
445
+ Share.share(Platform4.OS === "ios" ? { url: link } : { message: link }).catch(() => void 0);
446
+ }
447
+ function HandButton({ raised, onPress }) {
448
+ const { colors } = useTheme();
449
+ const press = usePressAnimation("pill");
450
+ const foreground = raised ? fixed.handText : colors.text;
451
+ return /* @__PURE__ */ jsx7(Pressable, { accessibilityLabel: raised ? "Lower hand" : "Raise hand", accessibilityRole: "button", accessibilityState: { selected: raised }, onPress, onPressIn: press.onPressIn, onPressOut: press.onPressOut, testID: "more.hand", children: /* @__PURE__ */ jsxs3(Animated.View, { style: [styles2.hand, { backgroundColor: raised ? fixed.hand : colors.raised }, press.scaleStyle], children: [
452
+ /* @__PURE__ */ jsx7(Icon, { color: foreground, name: "hand" }),
453
+ /* @__PURE__ */ jsx7(ChalkText, { color: foreground, variant: "bodyStrong", children: raised ? "Lower hand" : "Raise hand" })
454
+ ] }) });
455
+ }
456
+ function EmojiButton({ emoji, name, onPress, testID }) {
457
+ const { colors } = useTheme();
458
+ const press = usePressAnimation("small");
459
+ const { down } = press;
460
+ const pressed = colors.hover;
461
+ const background = useAnimatedStyle(() => {
462
+ "worklet";
463
+ return { backgroundColor: down.value ? pressed : fixed.transparent };
464
+ });
465
+ return /* @__PURE__ */ jsx7(Pressable, { accessibilityLabel: `Send ${name}`, accessibilityRole: "button", onPress: () => onPress(emoji), onPressIn: press.onPressIn, onPressOut: press.onPressOut, style: styles2.emojiSlot, testID, children: /* @__PURE__ */ jsx7(Animated.View, { style: [styles2.emoji, background, press.scaleStyle], children: /* @__PURE__ */ jsx7(Text, { allowFontScaling: false, style: styles2.emojiText, children: emoji }) }) });
466
+ }
467
+ function CopyLinkRow({ link }) {
468
+ const [copied, setCopied] = useState5(false);
469
+ const reset = useRef2(null);
470
+ useEffect5(() => () => clearTimeout(reset.current ?? void 0), []);
471
+ const copy = useCallback5(async () => {
472
+ await setStringAsync(link);
473
+ setCopied(true);
474
+ announce("Link copied");
475
+ clearTimeout(reset.current ?? void 0);
476
+ reset.current = setTimeout(() => setCopied(false), durations.copyReset);
477
+ }, [link]);
478
+ return /* @__PURE__ */ jsx7(SheetRow, { icon: copied ? "check" : "copy", label: "Copy link", onPress: () => void copy(), testID: "more.copy" });
479
+ }
480
+ var LAYOUTS = [
481
+ { value: "spotlight", label: "Spotlight", testID: "more.layout.spotlight" },
482
+ { value: "grid", label: "Grid", testID: "more.layout.grid" }
483
+ ];
484
+ function LayoutRow() {
485
+ const { layout, setLayout } = useStageView();
486
+ return /* @__PURE__ */ jsx7(SheetRow, { end: /* @__PURE__ */ jsx7(SegmentedControl, { accessibilityLabel: "Layout", onChange: setLayout, options: LAYOUTS, testID: "more.layout.control", value: layout }), icon: "grid", label: "Layout", testID: "more.layout" });
487
+ }
488
+ function UnreadDot() {
489
+ const { colors } = useTheme();
490
+ const style = [styles2.unread, { backgroundColor: colors.info }];
491
+ const ref = useMeasuredRef("more.chat.unread", style);
492
+ return /* @__PURE__ */ jsx7(View2, { ref, style, testID: "more.chat.unread" });
493
+ }
494
+ function ChatRow({ onPress }) {
495
+ const { colors } = useTheme();
496
+ const unread = useChat().unreadCount > 0;
497
+ return /* @__PURE__ */ jsx7(SheetRow, { accessibilityLabel: unread ? "Chat, new messages" : "Chat", badge: unread ? /* @__PURE__ */ jsx7(UnreadDot, {}) : null, end: /* @__PURE__ */ jsx7(Icon, { color: colors.textMuted, name: "right" }), icon: "chat", label: "Chat", onPress, testID: "more.chat" });
498
+ }
499
+ function PeopleCount({ count }) {
500
+ const { colors } = useTheme();
501
+ return /* @__PURE__ */ jsxs3(View2, { style: styles2.end, children: [
502
+ /* @__PURE__ */ jsx7(ChalkText, { tabular: true, tone: "textMuted", variant: "phoneRow", children: count }),
503
+ /* @__PURE__ */ jsx7(Icon, { color: colors.textMuted, name: "right" })
504
+ ] });
505
+ }
506
+ function RecordingRow({ onDone }) {
507
+ const client = useSpaceClient();
508
+ const toasts = useToasts();
509
+ const { soundsEnabled } = usePreferences();
510
+ const { colors } = useTheme();
511
+ const canRecord = useCan("manageRecording");
512
+ const status = useRecording().current?.status;
513
+ if (!canRecord) return null;
514
+ const recording2 = status === "starting" || status === "recording";
515
+ const toggle = () => {
516
+ onDone();
517
+ if (recording2) {
518
+ client.recording.stop().then(() => {
519
+ toasts.show({ leading: /* @__PURE__ */ jsx7(Icon, { color: colors.textMuted, name: "rec" }), message: "Recording stopped" });
520
+ }).catch(() => toasts.show({ kind: "error", dismissible: true, message: "Couldn't stop recording." }));
521
+ return;
522
+ }
523
+ client.recording.start().then(() => {
524
+ playSound("recording", soundsEnabled);
525
+ toasts.show({ leading: /* @__PURE__ */ jsx7(Icon, { color: colors.dangerText, name: "rec" }), message: [{ strong: "Recording started." }, " Everyone in the call can see it."] });
526
+ }).catch(() => toasts.show({ kind: "error", dismissible: true, message: "Couldn't start recording." }));
527
+ };
528
+ return /* @__PURE__ */ jsx7(SheetRow, { icon: "rec", label: recording2 ? "Stop recording" : "Start recording", onPress: toggle, testID: "more.recording" });
529
+ }
530
+ var styles2 = StyleSheet2.create({
531
+ // `.rxrow`: wraps onto a second line on narrow phones.
532
+ // One line on every phone: the reactions narrow from 44 pt to 32 pt before they would wrap beside the hand.
533
+ reactions: { flexDirection: "row", alignItems: "center", gap: 2, paddingBottom: 12 },
534
+ hand: { flexDirection: "row", alignItems: "center", gap: 8, height: 44, paddingLeft: 12, paddingRight: 16, marginRight: 6, borderRadius: 999 },
535
+ emojiSlot: { flexBasis: 44, flexShrink: 1, minWidth: 32 },
536
+ emoji: { height: 44, borderRadius: 22, alignItems: "center", justifyContent: "center" },
537
+ emojiText: { fontSize: 22 },
538
+ end: { flexDirection: "row", alignItems: "center", gap: 14 },
539
+ unread: { width: 8, height: 8, borderRadius: 4 }
540
+ });
541
+
542
+ // src/header/CallDetailsSheet.tsx
543
+ import { setStringAsync as setStringAsync2 } from "expo-clipboard";
544
+ import { Platform as Platform5, Pressable as Pressable2, Share as Share2, StyleSheet as StyleSheet3, View as View4 } from "react-native";
545
+ import Animated2 from "react-native-reanimated";
546
+
547
+ // src/header/QrCode.tsx
548
+ import { memo as memo2, useMemo as useMemo4 } from "react";
549
+ import { View as View3 } from "react-native";
550
+ import Svg, { Path, Rect } from "react-native-svg";
551
+ import { encode } from "uqr";
552
+ import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
553
+ var QrCode = memo2(function QrCode2({ value, size }) {
554
+ const { path, modules } = useMemo4(() => {
555
+ const { data, size: count } = encode(value, { ecc: "M", border: 2 });
556
+ let d = "";
557
+ data.forEach((row, y) => {
558
+ let start = -1;
559
+ row.forEach((dark, x) => {
560
+ if (dark && start < 0) start = x;
561
+ if (start >= 0 && (!dark || x === row.length - 1)) {
562
+ const end = dark ? x + 1 : x;
563
+ d += `M${start} ${y}h${end - start}v1h${start - end}z`;
564
+ start = -1;
565
+ }
566
+ });
567
+ });
568
+ return { path: d, modules: count };
569
+ }, [value]);
570
+ return /* @__PURE__ */ jsx8(View3, { accessibilityLabel: `QR code for ${value}`, accessibilityRole: "image", accessible: true, style: { width: size, height: size, borderRadius: 8, overflow: "hidden" }, testID: "details.qr", children: /* @__PURE__ */ jsxs4(Svg, { height: size, viewBox: `0 0 ${modules} ${modules}`, width: size, children: [
571
+ /* @__PURE__ */ jsx8(Rect, { fill: fixed.qrLight, height: modules, width: modules, x: 0, y: 0 }),
572
+ /* @__PURE__ */ jsx8(Path, { d: path, fill: fixed.qrDark })
573
+ ] }) });
574
+ });
575
+
576
+ // src/header/CallDetailsSheet.tsx
577
+ import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
578
+ var DETAILS_TOP = 58;
579
+ var SCHEMES = [
580
+ { value: "dark", label: "Dark", testID: "details.appearance.dark" },
581
+ { value: "light", label: "Light", testID: "details.appearance.light" }
582
+ ];
583
+ function CallDetailsSheet({ open, onClose, callName, inviteLink, returnFocusRef }) {
584
+ const preferences = usePreferences();
585
+ const layout = useDeviceLayout();
586
+ const heightRatio = (layout.height - layout.insets.top - DETAILS_TOP) / layout.height;
587
+ return /* @__PURE__ */ jsxs5(BottomSheet, { accessibilityLabel: callName, heightRatio, inset: 14, onClose, open, returnFocusRef, testID: "sheet.details", variant: "popover", children: [
588
+ /* @__PURE__ */ jsxs5(View4, { style: styles3.heading, children: [
589
+ /* @__PURE__ */ jsx9(ChalkText, { accessibilityRole: "header", numberOfLines: 1, style: styles3.title, variant: "cardTitle", children: callName }),
590
+ /* @__PURE__ */ jsx9(IconButton, { accessibilityLabel: "Close", icon: "x", onPress: onClose, size: 36, testID: "details.close", tone: "ghost" })
591
+ ] }),
592
+ inviteLink ? /* @__PURE__ */ jsx9(ShareSection, { link: inviteLink }) : null,
593
+ /* @__PURE__ */ jsxs5(Section, { first: !inviteLink, children: [
594
+ /* @__PURE__ */ jsx9(MenuRow, { end: /* @__PURE__ */ jsx9(SegmentedControl, { accessibilityLabel: "Appearance", onChange: preferences.setColorScheme, options: SCHEMES, size: "compact", testID: "details.appearance", value: preferences.colorScheme }), label: "Appearance" }),
595
+ /* @__PURE__ */ jsx9(MenuRow, { end: /* @__PURE__ */ jsx9(Switch, { accessibilityLabel: "Sounds", onValueChange: preferences.setSoundsEnabled, testID: "details.sounds", value: preferences.soundsEnabled }), label: "Sounds" })
596
+ ] })
597
+ ] });
598
+ }
599
+ function ShareSection({ link }) {
600
+ const { colors } = useTheme();
601
+ return /* @__PURE__ */ jsxs5(Section, { first: true, children: [
602
+ /* @__PURE__ */ jsx9(MenuSection, { label: "Share" }),
603
+ /* @__PURE__ */ jsxs5(View4, { style: [styles3.link, { backgroundColor: colors.hover }], testID: "details.link", children: [
604
+ /* @__PURE__ */ jsx9(ChalkText, { numberOfLines: 1, selectable: true, style: styles3.linkText, variant: "bodyMedium", children: displayLink(link) }),
605
+ /* @__PURE__ */ jsxs5(View4, { style: styles3.linkButtons, children: [
606
+ /* @__PURE__ */ jsx9(CopyButton, { accessibilityLabel: "Copy link", announcement: "Link copied", onCopy: () => setStringAsync2(link).then(() => void 0), testID: "details.copy" }),
607
+ /* @__PURE__ */ jsx9(ShareButton, { link })
608
+ ] })
609
+ ] }),
610
+ /* @__PURE__ */ jsxs5(View4, { style: styles3.qr, children: [
611
+ /* @__PURE__ */ jsx9(QrCode, { size: 112, value: link }),
612
+ /* @__PURE__ */ jsx9(ChalkText, { style: styles3.qrText, tone: "textSoft", variant: "meta", children: "Scan with a phone camera to join this call." })
613
+ ] })
614
+ ] });
615
+ }
616
+ function displayLink(link) {
617
+ return link.replace(/^https?:\/\//u, "").replace(/[#?].*$/u, "");
618
+ }
619
+ function ShareButton({ link }) {
620
+ const { colors } = useTheme();
621
+ const press = usePressAnimation("small");
622
+ const share = () => {
623
+ Share2.share(Platform5.OS === "ios" ? { url: link } : { message: link }).catch(() => void 0);
624
+ };
625
+ return /* @__PURE__ */ jsx9(Pressable2, { accessibilityLabel: "Share link", accessibilityRole: "button", hitSlop: 4, onPress: share, onPressIn: press.onPressIn, onPressOut: press.onPressOut, testID: "details.share", children: ({ pressed }) => /* @__PURE__ */ jsx9(Animated2.View, { style: [styles3.round, pressed ? { backgroundColor: colors.pressed } : null, press.scaleStyle], children: /* @__PURE__ */ jsx9(Icon, { color: colors.text, name: "iosShare" }) }) });
626
+ }
627
+ function Section({ first = false, children }) {
628
+ const { colors } = useTheme();
629
+ return /* @__PURE__ */ jsx9(View4, { style: [styles3.section, first ? null : { borderTopWidth: 1, borderTopColor: colors.line }], children });
630
+ }
631
+ function MenuRow({ label, end }) {
632
+ return /* @__PURE__ */ jsxs5(View4, { style: styles3.row, children: [
633
+ /* @__PURE__ */ jsx9(ChalkText, { numberOfLines: 1, style: styles3.rowLabel, children: label }),
634
+ end
635
+ ] });
636
+ }
637
+ var styles3 = StyleSheet3.create({
638
+ heading: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 8, paddingTop: 2, paddingBottom: 4, paddingLeft: 10 },
639
+ title: { flexShrink: 1 },
640
+ section: { paddingTop: 4, paddingBottom: 8 },
641
+ link: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 8, height: 44, marginHorizontal: 6, paddingLeft: 14, paddingRight: 4, borderRadius: 999 },
642
+ linkText: { flexShrink: 1 },
643
+ linkButtons: { flexDirection: "row", gap: 2 },
644
+ round: { width: 36, height: 36, borderRadius: 18, alignItems: "center", justifyContent: "center" },
645
+ qr: { flexDirection: "row", alignItems: "center", gap: 14, marginTop: 10, marginHorizontal: 6, marginBottom: 2 },
646
+ qrText: { flex: 1 },
647
+ row: { flexDirection: "row", alignItems: "center", gap: 10, minHeight: sizes.menuRowPhone, paddingHorizontal: 10 },
648
+ rowLabel: { flex: 1 }
649
+ });
650
+
651
+ // src/header/Header.tsx
652
+ import { forwardRef, memo as memo3, useEffect as useEffect6, useState as useState6 } from "react";
653
+ import { Pressable as Pressable3, StyleSheet as StyleSheet4, View as View5 } from "react-native";
654
+ import Animated3 from "react-native-reanimated";
655
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
656
+ var Header = memo3(
657
+ forwardRef(function Header2({ callName, arrangement, onOpenDetails, detailsOpen }, ref) {
658
+ const { colors } = useTheme();
659
+ const press = usePressAnimation("pill");
660
+ const landscape = arrangement === "landscape";
661
+ const headerStyle = [styles4.header, arrangement === "pad" ? styles4.pad : landscape ? styles4.landscape : styles4.portrait];
662
+ const headerRef = useMeasuredRef("frame.header", headerStyle);
663
+ const nameStyle = [styles4.name, landscape ? [styles4.namePill, { backgroundColor: colors.raised, boxShadow: `0px 0px 0px 1px ${colors.lineStrong}` }] : null];
664
+ const nameRef = useMeasuredRef("header.name", nameStyle);
665
+ return /* @__PURE__ */ jsxs6(View5, { pointerEvents: "box-none", ref: headerRef, style: headerStyle, testID: "frame.header", children: [
666
+ /* @__PURE__ */ jsx10(Pressable3, { accessibilityLabel: `Call details, ${callName}`, accessibilityRole: "button", accessibilityState: { expanded: detailsOpen }, onPress: onOpenDetails, onPressIn: press.onPressIn, onPressOut: press.onPressOut, ref, style: styles4.nameButton, testID: "header.name", children: /* @__PURE__ */ jsxs6(Animated3.View, { ref: nameRef, style: [nameStyle, press.scaleStyle], children: [
667
+ /* @__PURE__ */ jsx10(ChalkText, { numberOfLines: 1, style: styles4.nameText, variant: "bodyStrong", children: callName }),
668
+ /* @__PURE__ */ jsx10(Icon, { color: colors.textMuted, name: "down", size: 16 })
669
+ ] }) }),
670
+ /* @__PURE__ */ jsxs6(View5, { style: styles4.status, children: [
671
+ /* @__PURE__ */ jsx10(RecordingDot, {}),
672
+ landscape ? null : /* @__PURE__ */ jsx10(CallTimer, {})
673
+ ] })
674
+ ] });
675
+ })
676
+ );
677
+ function RecordingDot() {
678
+ const { colors } = useTheme();
679
+ const status = useRecording().current?.status;
680
+ if (status !== "recording" && status !== "stopping") return null;
681
+ return /* @__PURE__ */ jsxs6(View5, { accessibilityLabel: "This call is being recorded", accessible: true, style: styles4.rec, testID: "header.recording", children: [
682
+ /* @__PURE__ */ jsx10(View5, { style: [styles4.recDot, { backgroundColor: colors.danger }] }),
683
+ /* @__PURE__ */ jsx10(ChalkText, { variant: "meta", children: "Rec" })
684
+ ] });
685
+ }
686
+ function CallTimer() {
687
+ const { episode } = useConnection();
688
+ const startedAt = episode?.startedAt ? Date.parse(episode.startedAt) : null;
689
+ const [now, setNow] = useState6(Date.now);
690
+ useEffect6(() => {
691
+ if (startedAt === null) return;
692
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
693
+ return () => clearInterval(timer);
694
+ }, [startedAt]);
695
+ if (startedAt === null) return null;
696
+ return /* @__PURE__ */ jsx10(ChalkText, { accessibilityLabel: `Call time ${formatElapsed(now - startedAt)}`, tabular: true, testID: "header.timer", tone: "textMuted", variant: "bodyMedium", children: formatElapsed(now - startedAt) });
697
+ }
698
+ function formatElapsed(milliseconds) {
699
+ const total = Math.max(0, Math.floor(milliseconds / 1e3));
700
+ const hours = Math.floor(total / 3600);
701
+ const minutes = Math.floor(total % 3600 / 60);
702
+ const seconds = String(total % 60).padStart(2, "0");
703
+ return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` : `${minutes}:${seconds}`;
704
+ }
705
+ var styles4 = StyleSheet4.create({
706
+ header: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 8 },
707
+ portrait: { paddingLeft: 14, paddingRight: 12 },
708
+ landscape: { paddingHorizontal: 8, justifyContent: "flex-start" },
709
+ pad: { paddingLeft: 24, paddingRight: 16 },
710
+ nameButton: { flexShrink: 1 },
711
+ // `.nameb`: 44 pt high, pulled 6 pt left so the text lines up with the header padding.
712
+ name: { flexDirection: "row", alignItems: "center", gap: 4, height: 44, paddingHorizontal: 6, marginLeft: -6, borderRadius: 999 },
713
+ // `.lnd .title`: a 40 pt pill. The mockup pulls `.nameb` 6 pt into the pill's 2 pt padding, so the
714
+ // text starts 2 pt in, and the name's own 6 pt plus the pill's 12 pt follow the chevron.
715
+ namePill: { height: 40, marginLeft: 0, paddingLeft: 2, paddingRight: 18 },
716
+ nameText: { flexShrink: 1 },
717
+ status: { flexDirection: "row", alignItems: "center", gap: 10 },
718
+ rec: { flexDirection: "row", alignItems: "center", gap: 6 },
719
+ recDot: { width: 8, height: 8, borderRadius: 4 }
720
+ });
721
+
722
+ // src/sheets/people-chat/ChatNotices.tsx
723
+ import { useEffect as useEffect7, useRef as useRef3 } from "react";
724
+
725
+ // src/sheets/people-chat/chat-model.ts
726
+ import { CHALK_CHAT_ATTACHMENT_LIMITS, CHALK_CHAT_ATTACHMENT_MIME_TYPES } from "@q9labsai/chalk-client";
727
+ var KIND_BY_MIME = {
728
+ "image/png": "image",
729
+ "image/jpeg": "image",
730
+ "image/gif": "image",
731
+ "image/webp": "image",
732
+ "application/pdf": "pdf",
733
+ "text/plain": "txt",
734
+ "application/msword": "doc",
735
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "doc",
736
+ "application/vnd.oasis.opendocument.text": "doc",
737
+ "application/vnd.ms-excel": "xls",
738
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xls",
739
+ "application/vnd.oasis.opendocument.spreadsheet": "xls",
740
+ "application/vnd.ms-powerpoint": "ppt",
741
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "ppt",
742
+ "application/vnd.oasis.opendocument.presentation": "ppt"
743
+ };
744
+ function fileKind(mimeType) {
745
+ return KIND_BY_MIME[mimeType] ?? "other";
746
+ }
747
+ var MIME_BY_EXTENSION = {
748
+ png: "image/png",
749
+ jpg: "image/jpeg",
750
+ jpeg: "image/jpeg",
751
+ gif: "image/gif",
752
+ webp: "image/webp",
753
+ pdf: "application/pdf",
754
+ txt: "text/plain",
755
+ doc: "application/msword",
756
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
757
+ odt: "application/vnd.oasis.opendocument.text",
758
+ xls: "application/vnd.ms-excel",
759
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
760
+ ods: "application/vnd.oasis.opendocument.spreadsheet",
761
+ ppt: "application/vnd.ms-powerpoint",
762
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
763
+ odp: "application/vnd.oasis.opendocument.presentation"
764
+ };
765
+ function mimeTypeOf(name, reported) {
766
+ if (reported) return reported;
767
+ const dot = name.lastIndexOf(".");
768
+ return dot > 0 && MIME_BY_EXTENSION[name.slice(dot + 1).toLowerCase()] || "application/octet-stream";
769
+ }
770
+ var FILE_BADGE = { pdf: "PDF", doc: "DOC", xls: "XLS", ppt: "PPT", txt: "TXT", other: "FILE" };
771
+ var MB = 1024 * 1024;
772
+ function formatSize(bytes) {
773
+ if (bytes < 1024) return `${bytes} B`;
774
+ if (bytes < MB) return `${Math.round(bytes / 1024)} KB`;
775
+ return `${(bytes / MB).toFixed(bytes < 10 * MB ? 1 : 0)} MB`;
776
+ }
777
+ function formatTime(iso) {
778
+ const date = new Date(iso);
779
+ return Number.isNaN(date.getTime()) ? "" : date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
780
+ }
781
+ function messageSummary(text, attachments) {
782
+ if (text) return text;
783
+ const count = attachments.length;
784
+ if (count === 0) return "";
785
+ const photos = attachments.filter((attachment) => fileKind(attachment.mimeType) === "image").length;
786
+ if (photos === count) return count > 1 ? `Sent ${count} photos` : "Sent a photo";
787
+ return count > 1 ? `Sent ${count} files` : "Sent a file";
788
+ }
789
+ function groupMessages(messages, pending, self, retried) {
790
+ const groups = [];
791
+ const add = (participantId, displayName, time, entry) => {
792
+ const last = groups[groups.length - 1];
793
+ if (last && last.participantId === participantId) {
794
+ groups[groups.length - 1] = { ...last, entries: [...last.entries, entry] };
795
+ return;
796
+ }
797
+ groups.push({ key: entry.key, participantId, displayName, mine: participantId === self.participantId, time, entries: [entry] });
798
+ };
799
+ for (const message2 of messages) {
800
+ add(message2.participantId, message2.displayName, formatTime(message2.createdAt), { key: message2.messageId, text: message2.text, attachments: message2.attachments, state: "sent", clientMessageId: null });
801
+ }
802
+ for (const send of pending) {
803
+ if (retried.has(send.clientMessageId)) continue;
804
+ add(self.participantId ?? "self", self.displayName ?? "You", "Now", { key: send.clientMessageId, text: send.text, attachments: send.attachments, state: send.status === "failed" ? "failed" : send.status === "queued" ? "queued" : "sending", clientMessageId: send.clientMessageId });
805
+ }
806
+ return groups;
807
+ }
808
+ var allowedMimeTypes = new Set(CHALK_CHAT_ATTACHMENT_MIME_TYPES);
809
+ function stagingError(facts, accepted) {
810
+ if (!allowedMimeTypes.has(facts.mimeType)) {
811
+ const dot = facts.name.lastIndexOf(".");
812
+ return dot > 0 && dot < facts.name.length - 1 ? `Can\u2019t send .${facts.name.slice(dot + 1).toLowerCase()} files` : "Can\u2019t send this type of file";
813
+ }
814
+ if (facts.size > CHALK_CHAT_ATTACHMENT_LIMITS.maximumByteLength) return "Over the 25 MB limit";
815
+ if (accepted >= CHALK_CHAT_ATTACHMENT_LIMITS.maximumPerMessage) return `Only ${CHALK_CHAT_ATTACHMENT_LIMITS.maximumPerMessage} files per message`;
816
+ return null;
817
+ }
818
+ function thumbnailLayout(count) {
819
+ return { shown: Math.min(count, 4), more: Math.max(0, count - 4) };
820
+ }
821
+
822
+ // src/sheets/people-chat/ChatNotices.tsx
823
+ import { jsx as jsx11 } from "react/jsx-runtime";
824
+ function ChatNotices({ chatVisible, sheetOpen, onOpenChat }) {
825
+ const { messages, status } = useChat();
826
+ const self = useSelf();
827
+ const toasts = useToasts();
828
+ const { soundsEnabled } = usePreferences();
829
+ const seen = useRef3(null);
830
+ const latest = useRef3({ chatVisible, sheetOpen, onOpenChat, soundsEnabled });
831
+ latest.current = { chatVisible, sheetOpen, onOpenChat, soundsEnabled };
832
+ useEffect7(() => {
833
+ if (status !== "ready") return;
834
+ const previous = seen.current;
835
+ seen.current = new Set(messages.map((message2) => message2.messageId));
836
+ if (!previous) return;
837
+ const fresh = messages.filter((message2) => !previous.has(message2.messageId) && message2.participantId !== self.participantId);
838
+ const now = latest.current;
839
+ for (const message2 of fresh) {
840
+ const text = messageSummary(message2.text, message2.attachments);
841
+ playSound("message", now.soundsEnabled);
842
+ if (now.chatVisible || now.sheetOpen) {
843
+ announce(`New message from ${message2.displayName}: ${text}`);
844
+ continue;
845
+ }
846
+ const id = toasts.show({
847
+ leading: /* @__PURE__ */ jsx11(FaceAvatar, { name: message2.displayName, size: 28 }),
848
+ message: [{ strong: firstName(message2.displayName) }, ` ${text}`],
849
+ actions: [
850
+ {
851
+ label: "Reply",
852
+ onPress: () => {
853
+ toasts.dismiss(id);
854
+ latest.current.onOpenChat();
855
+ },
856
+ testID: "toast.chat.reply"
857
+ }
858
+ ],
859
+ testID: "toast.chat"
860
+ });
861
+ }
862
+ }, [messages, self.participantId, status, toasts]);
863
+ return null;
864
+ }
865
+ function firstName(name) {
866
+ return name.trim().split(/\s+/)[0] ?? name;
867
+ }
868
+
869
+ // src/sheets/people-chat/PeopleChatSheet.tsx
870
+ import { useEffect as useEffect10 } from "react";
871
+ import { Pressable as Pressable9, StyleSheet as StyleSheet11, View as View11 } from "react-native";
872
+
873
+ // src/sheets/people-chat/ChatTab.tsx
874
+ import { memo as memo5, useCallback as useCallback7, useMemo as useMemo5, useState as useState9 } from "react";
875
+ import { FlatList, Pressable as Pressable7, StyleSheet as StyleSheet9, View as View9 } from "react-native";
876
+
877
+ // src/sheets/people-chat/attachments.tsx
878
+ import { memo as memo4, useEffect as useEffect8, useState as useState7 } from "react";
879
+ import { Image, Linking, Pressable as Pressable4, StyleSheet as StyleSheet5, View as View6 } from "react-native";
880
+ import { jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
881
+ var urls = /* @__PURE__ */ new Map();
882
+ function resolveUrl(client, attachment) {
883
+ let url = urls.get(attachment.attachmentId);
884
+ if (!url) {
885
+ url = client.chat.files.resolveUrl(attachment);
886
+ urls.set(attachment.attachmentId, url);
887
+ url.catch(() => urls.delete(attachment.attachmentId));
888
+ }
889
+ return url;
890
+ }
891
+ function useAttachmentSource(attachment) {
892
+ const client = useSpaceClient();
893
+ const [source, setSource] = useState7({ state: "loading" });
894
+ useEffect8(() => {
895
+ let live = true;
896
+ resolveUrl(client, attachment).then((uri) => {
897
+ Image.getSize(
898
+ uri,
899
+ (width, height) => live && setSource({ state: "ready", uri, aspect: width / height }),
900
+ () => live && setSource({ state: "ready", uri, aspect: 4 / 3 })
901
+ );
902
+ }).catch(() => live && setSource({ state: "failed" }));
903
+ return () => {
904
+ live = false;
905
+ };
906
+ }, [attachment, client]);
907
+ return source;
908
+ }
909
+ function useDownload() {
910
+ const client = useSpaceClient();
911
+ const toasts = useToasts();
912
+ return (attachment) => {
913
+ resolveUrl(client, attachment).then((uri) => Linking.openURL(uri)).catch(() => toasts.show({ kind: "error", dismissible: true, message: `Couldn't download ${attachment.fileName}.` }));
914
+ };
915
+ }
916
+ var Thumbnails = memo4(function Thumbnails2({ photos, disabled, onOpen }) {
917
+ const { shown, more } = thumbnailLayout(photos.length);
918
+ const count = photos.length;
919
+ return /* @__PURE__ */ jsx12(View6, { style: [styles5.grid, count > 1 ? styles5.gridMany : null], children: photos.slice(0, shown).map((photo, index) => /* @__PURE__ */ jsx12(
920
+ Thumbnail,
921
+ {
922
+ attachment: photo,
923
+ disabled,
924
+ label: `Open ${photo.fileName}${count > 1 ? `, ${index + 1} of ${count}` : ""}${index === 3 && more > 0 ? `, and ${more} more` : ""}`,
925
+ layout: count === 1 ? "one" : count === 3 && index === 2 ? "wide" : "square",
926
+ more: index === 3 ? more : 0,
927
+ onPress: () => onOpen(index)
928
+ },
929
+ photo.attachmentId
930
+ )) });
931
+ });
932
+ function Thumbnail({ attachment, layout, more, label, disabled, onPress }) {
933
+ const { colors } = useTheme();
934
+ const source = useAttachmentSource(attachment);
935
+ const aspectRatio = layout === "one" ? source.state === "ready" ? Math.min(1.78, Math.max(0.75, source.aspect)) : 4 / 3 : layout === "wide" ? 2 : 1;
936
+ const style = [styles5.thumb, layout === "one" ? styles5.thumbOne : layout === "wide" ? styles5.thumbWide : styles5.thumbSquare, { aspectRatio, backgroundColor: colors.raised }];
937
+ if (source.state !== "ready") {
938
+ return /* @__PURE__ */ jsx12(View6, { accessibilityLabel: source.state === "loading" ? `Loading ${attachment.fileName}` : `Couldn't load ${attachment.fileName}`, accessibilityRole: "image", accessible: true, style: [style, styles5.centered], children: /* @__PURE__ */ jsx12(Icon, { color: colors.textMuted, name: "img", size: 18 }) });
939
+ }
940
+ return /* @__PURE__ */ jsxs7(Pressable4, { accessibilityLabel: label, accessibilityRole: "imagebutton", accessibilityState: { disabled }, disabled, onPress, style, testID: `chat.photo.${attachment.attachmentId}`, children: [
941
+ /* @__PURE__ */ jsx12(Image, { source: { uri: source.uri }, style: StyleSheet5.absoluteFill }),
942
+ /* @__PURE__ */ jsx12(View6, { pointerEvents: "none", style: [StyleSheet5.absoluteFill, { borderRadius: layout === "one" ? 14 : 3, boxShadow: `inset 0px 0px 0px 1px ${colors.line}` }] }),
943
+ more > 0 ? /* @__PURE__ */ jsx12(View6, { style: [StyleSheet5.absoluteFill, styles5.centered, styles5.moreShade], children: /* @__PURE__ */ jsx12(ChalkText, { color: fixed.overlayText, style: styles5.moreText, variant: "sheetTitle", children: `+${more}` }) }) : null
944
+ ] });
945
+ }
946
+ function FileBadge({ mimeType, size = 36 }) {
947
+ const { colors } = useTheme();
948
+ const kind = fileKind(mimeType);
949
+ const box = { width: size, height: size, borderRadius: size === 36 ? 9 : 7 };
950
+ if (kind === "image") {
951
+ return /* @__PURE__ */ jsx12(View6, { style: [styles5.badge, box, { backgroundColor: colors.hover }], children: /* @__PURE__ */ jsx12(Icon, { color: colors.textMuted, name: "img", size: 18 }) });
952
+ }
953
+ const color = kind === "txt" ? fileKindColors.other : fileKindColors[kind];
954
+ return /* @__PURE__ */ jsx12(View6, { style: [styles5.badge, box, { backgroundColor: color }], children: /* @__PURE__ */ jsx12(ChalkText, { color: fixed.overlayText, style: [styles5.badgeText, size === 32 ? styles5.badgeTextSmall : null], variant: "caption", children: FILE_BADGE[kind] }) });
955
+ }
956
+ var FileCard = memo4(function FileCard2({ attachment, sent }) {
957
+ const { colors } = useTheme();
958
+ const download = useDownload();
959
+ return /* @__PURE__ */ jsxs7(View6, { style: [styles5.file, { backgroundColor: colors.raised, borderColor: colors.line }], testID: `chat.file.${attachment.attachmentId}`, children: [
960
+ /* @__PURE__ */ jsx12(FileBadge, { mimeType: attachment.mimeType }),
961
+ /* @__PURE__ */ jsxs7(View6, { style: styles5.fileText, children: [
962
+ /* @__PURE__ */ jsx12(ChalkText, { numberOfLines: 1, variant: "button", children: attachment.fileName }),
963
+ /* @__PURE__ */ jsx12(ChalkText, { style: styles5.fileSize, tone: "textMuted", variant: "label", children: formatSize(attachment.byteLength) })
964
+ ] }),
965
+ sent ? /* @__PURE__ */ jsx12(IconButton, { accessibilityLabel: `Download ${attachment.fileName}`, icon: "dl", onPress: () => download(attachment), size: 36, testID: `chat.download.${attachment.attachmentId}`, tone: "ghost" }) : null
966
+ ] });
967
+ });
968
+ var styles5 = StyleSheet5.create({
969
+ grid: { width: 240, maxWidth: "100%", borderRadius: 14, overflow: "hidden" },
970
+ gridMany: { flexDirection: "row", flexWrap: "wrap", gap: 2 },
971
+ thumb: { overflow: "hidden" },
972
+ thumbOne: { width: "100%", borderRadius: 14 },
973
+ thumbSquare: { width: 119, borderRadius: 3 },
974
+ thumbWide: { width: "100%", borderRadius: 3 },
975
+ centered: { alignItems: "center", justifyContent: "center" },
976
+ moreShade: { backgroundColor: chatOverlays.photoMore },
977
+ moreText: { fontSize: 20, lineHeight: 28 },
978
+ badge: { alignItems: "center", justifyContent: "center" },
979
+ // `.fi`: 10 pt bold capitals with 0.04 em tracking.
980
+ badgeText: { fontFamily: fontFamilies[700], fontSize: 10, lineHeight: 10, letterSpacing: 0.4 },
981
+ badgeTextSmall: { fontSize: 9, lineHeight: 9, letterSpacing: 0.36 },
982
+ file: { flexDirection: "row", alignItems: "center", gap: 10, width: 240, maxWidth: "100%", minHeight: 56, paddingTop: 8, paddingRight: 6, paddingBottom: 8, paddingLeft: 10, borderRadius: 14, borderWidth: 1 },
983
+ fileText: { flex: 1, minWidth: 0, gap: 1 },
984
+ fileSize: { fontFamily: fontFamilies[400], lineHeight: 15 }
985
+ });
986
+
987
+ // src/sheets/people-chat/Composer.tsx
988
+ import { CHALK_CHAT_ATTACHMENT_MIME_TYPES as CHALK_CHAT_ATTACHMENT_MIME_TYPES2 } from "@q9labsai/chalk-client";
989
+ import { getDocumentAsync } from "expo-document-picker";
990
+ import { File } from "expo-file-system";
991
+ import { useCallback as useCallback6, useRef as useRef4, useState as useState8 } from "react";
992
+ import { ActivityIndicator, Image as Image2, Pressable as Pressable5, ScrollView, StyleSheet as StyleSheet7, View as View7 } from "react-native";
993
+ import Animated4, { useAnimatedKeyboard, useAnimatedStyle as useAnimatedStyle2 } from "react-native-reanimated";
994
+
995
+ // src/sheets/people-chat/TextField.tsx
996
+ import { forwardRef as forwardRef2 } from "react";
997
+ import { StyleSheet as StyleSheet6, TextInput } from "react-native";
998
+ import { jsx as jsx13 } from "react/jsx-runtime";
999
+ var TextField = forwardRef2(function TextField2({ style, ...rest }, ref) {
1000
+ const { colors } = useTheme();
1001
+ return /* @__PURE__ */ jsx13(TextInput, { ...rest, cursorColor: colors.text, placeholderTextColor: colors.textMuted, ref, selectionColor: colors.textMuted, style: [styles6.input, { color: colors.text }, style] });
1002
+ });
1003
+ var styles6 = StyleSheet6.create({
1004
+ input: { flex: 1, minWidth: 0, height: "100%", padding: 0, fontFamily: type.body.fontFamily, fontSize: type.body.fontSize }
1005
+ });
1006
+
1007
+ // src/sheets/people-chat/Composer.tsx
1008
+ import { jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
1009
+ function Composer() {
1010
+ const client = useSpaceClient();
1011
+ const toasts = useToasts();
1012
+ const { colors } = useTheme();
1013
+ const layout = useDeviceLayout();
1014
+ const keyboard = useAnimatedKeyboard();
1015
+ const [text, setText] = useState8("");
1016
+ const [focused, setFocused] = useState8(false);
1017
+ const [staged, setStaged] = useState8([]);
1018
+ const nextId = useRef4(0);
1019
+ const update = useCallback6((id, change) => setStaged((files) => files.map((file) => file.id === id ? { ...file, ...change } : file)), []);
1020
+ const upload = useCallback6(
1021
+ (file) => {
1022
+ client.chat.files.upload(file.source).then((attachment) => update(file.id, { state: "ready", attachment })).catch((cause) => update(file.id, { state: "error", error: cause instanceof Error ? cause.message : "Couldn\u2019t upload" }));
1023
+ },
1024
+ [client, update]
1025
+ );
1026
+ const attach = useCallback6(async () => {
1027
+ const picked = await getDocumentAsync({ multiple: true, copyToCacheDirectory: true, type: [...CHALK_CHAT_ATTACHMENT_MIME_TYPES2] });
1028
+ if (picked.canceled) return;
1029
+ let accepted = staged.filter((file) => file.state !== "error").length;
1030
+ const added = [];
1031
+ for (const asset of picked.assets) {
1032
+ const mimeType = mimeTypeOf(asset.name, asset.mimeType);
1033
+ const size = asset.size ?? 0;
1034
+ const source = { name: asset.name, type: mimeType, size, arrayBuffer: () => new File(asset.uri).arrayBuffer() };
1035
+ const error2 = stagingError({ name: asset.name, mimeType, size }, accepted);
1036
+ if (!error2) accepted += 1;
1037
+ nextId.current += 1;
1038
+ added.push({ id: nextId.current, name: asset.name, size, mimeType, source, previewUri: fileKind(mimeType) === "image" ? asset.uri : null, state: error2 ? "error" : "uploading", attachment: null, error: error2 });
1039
+ }
1040
+ setStaged((files) => [...files, ...added]);
1041
+ added.filter((file) => file.state === "uploading").forEach(upload);
1042
+ const refused = added.filter((file) => file.state === "error").length;
1043
+ const ok = added.length - refused;
1044
+ announce(`${ok} file${ok === 1 ? "" : "s"} added${refused ? `, ${refused} can\u2019t be sent` : ""}`);
1045
+ }, [staged, upload]);
1046
+ const pick = useCallback6(() => {
1047
+ attach().catch(() => toasts.show({ kind: "error", dismissible: true, message: "Couldn't open your files." }));
1048
+ }, [attach, toasts]);
1049
+ const ready = staged.flatMap((file) => file.state === "ready" && file.attachment ? [file.attachment] : []);
1050
+ const uploading = staged.some((file) => file.state === "uploading");
1051
+ const message2 = text.trim();
1052
+ const canSend = !uploading && (message2.length > 0 || ready.length > 0);
1053
+ const send = useCallback6(() => {
1054
+ if (!canSend) return;
1055
+ const attachments = ready;
1056
+ setText("");
1057
+ setStaged([]);
1058
+ client.chat.send({ text: message2, attachments }).catch(() => void 0);
1059
+ }, [canSend, client, message2, ready]);
1060
+ const padStyle = useAnimatedStyle2(() => {
1061
+ "worklet";
1062
+ return { paddingBottom: keyboard.height.value > 0 ? 8 : layout.insets.bottom + 8 };
1063
+ });
1064
+ return /* @__PURE__ */ jsxs8(Animated4.View, { style: [styles7.composer, padStyle], testID: "chat.composer", children: [
1065
+ staged.length > 0 ? /* @__PURE__ */ jsx14(ScrollView, { accessibilityLabel: "Files to send", contentContainerStyle: styles7.stagedList, style: styles7.staged, testID: "chat.staged", children: staged.map((file) => /* @__PURE__ */ jsx14(StagedChip, { file, onRemove: () => setStaged((files) => files.filter((other) => other.id !== file.id)) }, file.id)) }) : null,
1066
+ /* @__PURE__ */ jsxs8(View7, { style: [styles7.field, { backgroundColor: colors.raised, borderColor: focused ? colors.text : colors.line }], children: [
1067
+ /* @__PURE__ */ jsx14(View7, { style: styles7.clip, children: /* @__PURE__ */ jsx14(IconButton, { accessibilityLabel: "Attach files", icon: "clip", onPress: pick, size: 36, testID: "chat.attach", tone: "ghost" }) }),
1068
+ /* @__PURE__ */ jsx14(
1069
+ TextField,
1070
+ {
1071
+ accessibilityLabel: "Message everyone",
1072
+ blurOnSubmit: false,
1073
+ onBlur: () => setFocused(false),
1074
+ onChangeText: setText,
1075
+ onFocus: () => setFocused(true),
1076
+ onSubmitEditing: send,
1077
+ placeholder: "Message everyone",
1078
+ returnKeyType: "send",
1079
+ submitBehavior: "submit",
1080
+ testID: "chat.input",
1081
+ value: text
1082
+ }
1083
+ ),
1084
+ /* @__PURE__ */ jsx14(SendButton, { disabled: !canSend, onPress: send })
1085
+ ] })
1086
+ ] });
1087
+ }
1088
+ function SendButton({ disabled, onPress }) {
1089
+ const { colors } = useTheme();
1090
+ const press = usePressAnimation("round");
1091
+ return /* @__PURE__ */ jsx14(Pressable5, { accessibilityLabel: "Send message", accessibilityRole: "button", accessibilityState: { disabled }, disabled, onPress, onPressIn: press.onPressIn, onPressOut: press.onPressOut, testID: "chat.send", children: /* @__PURE__ */ jsx14(Animated4.View, { style: [styles7.send, { backgroundColor: disabled ? colors.hover : colors.text }, disabled ? null : press.scaleStyle], children: /* @__PURE__ */ jsx14(Icon, { color: disabled ? colors.textMuted : colors.canvas, name: "up", size: 18 }) }) });
1092
+ }
1093
+ function StagedChip({ file, onRemove }) {
1094
+ const { colors } = useTheme();
1095
+ const failed = file.state === "error";
1096
+ const uploading = file.state === "uploading";
1097
+ return /* @__PURE__ */ jsxs8(View7, { accessible: false, style: [styles7.chip, { backgroundColor: colors.raised, borderColor: failed ? colors.danger : colors.line }], testID: `chat.staged.${file.id}`, children: [
1098
+ /* @__PURE__ */ jsxs8(View7, { style: [styles7.chipPreview, failed ? styles7.faded : null], children: [
1099
+ file.previewUri && !failed ? /* @__PURE__ */ jsx14(Image2, { source: { uri: file.previewUri }, style: styles7.chipImage }) : /* @__PURE__ */ jsx14(FileBadge, { mimeType: file.mimeType, size: 32 }),
1100
+ uploading ? /* @__PURE__ */ jsx14(View7, { style: [StyleSheet7.absoluteFill, styles7.chipShade], children: /* @__PURE__ */ jsx14(ActivityIndicator, { color: chatOverlays.ring, size: "small" }) }) : null
1101
+ ] }),
1102
+ /* @__PURE__ */ jsxs8(View7, { style: styles7.chipText, children: [
1103
+ /* @__PURE__ */ jsx14(ChalkText, { numberOfLines: 1, variant: "button", children: file.name }),
1104
+ /* @__PURE__ */ jsx14(ChalkText, { color: failed ? colors.dangerText : void 0, style: styles7.chipDetail, tone: "textMuted", variant: "label", children: failed ? file.error : uploading ? `Uploading, ${formatSize(file.size)}` : formatSize(file.size) })
1105
+ ] }),
1106
+ /* @__PURE__ */ jsx14(Pressable5, { accessibilityLabel: `Remove ${file.name}`, accessibilityRole: "button", hitSlop: 8, onPress: onRemove, style: ({ pressed }) => [styles7.remove, pressed ? { backgroundColor: colors.hover } : null], testID: `chat.staged.remove.${file.id}`, children: /* @__PURE__ */ jsx14(Icon, { color: colors.textMuted, name: "x", size: 16 }) })
1107
+ ] });
1108
+ }
1109
+ var styles7 = StyleSheet7.create({
1110
+ composer: { paddingTop: 8, paddingHorizontal: 12 },
1111
+ staged: { maxHeight: 196 },
1112
+ stagedList: { gap: 4, paddingBottom: 8 },
1113
+ field: { flexDirection: "row", alignItems: "center", height: 44, paddingHorizontal: 4, borderRadius: 999, borderWidth: 1 },
1114
+ clip: { marginRight: 2 },
1115
+ send: { width: 36, height: 36, borderRadius: 18, alignItems: "center", justifyContent: "center" },
1116
+ chip: { minHeight: 44, flexDirection: "row", alignItems: "center", gap: 10, paddingTop: 4, paddingRight: 4, paddingBottom: 4, paddingLeft: 6, borderRadius: 12, borderWidth: 1 },
1117
+ chipPreview: { width: 32, height: 32 },
1118
+ chipImage: { width: 32, height: 32, borderRadius: 7 },
1119
+ chipShade: { borderRadius: 7, backgroundColor: chatOverlays.uploading, alignItems: "center", justifyContent: "center" },
1120
+ faded: { opacity: 0.5 },
1121
+ chipText: { flex: 1, minWidth: 0, gap: 1 },
1122
+ chipDetail: { fontFamily: fontFamilies[400], lineHeight: 15 },
1123
+ remove: { width: 28, height: 28, borderRadius: 14, alignItems: "center", justifyContent: "center" }
1124
+ });
1125
+
1126
+ // src/sheets/people-chat/PhotoViewer.tsx
1127
+ import { useEffect as useEffect9, useRef as useRef5 } from "react";
1128
+ import { AccessibilityInfo, findNodeHandle, Image as Image3, Pressable as Pressable6, StyleSheet as StyleSheet8, View as View8 } from "react-native";
1129
+ import Animated5, { ReduceMotion, withTiming } from "react-native-reanimated";
1130
+ import { jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
1131
+ function PhotoViewer({ viewing, onChange, onClose }) {
1132
+ const layout = useDeviceLayout();
1133
+ const download = useDownload();
1134
+ const closeRef = useRef5(null);
1135
+ const { photos, index, from } = viewing;
1136
+ const photo = photos[index] ?? photos[0];
1137
+ const count = photos.length;
1138
+ useBackHandler(true, onClose);
1139
+ useEffect9(() => {
1140
+ const node = closeRef.current && findNodeHandle(closeRef.current);
1141
+ if (node) AccessibilityInfo.setAccessibilityFocus(node);
1142
+ }, []);
1143
+ const go = (step) => {
1144
+ const next = (index + step + count) % count;
1145
+ onChange(next);
1146
+ announce(`Image ${next + 1} of ${count}`);
1147
+ };
1148
+ if (!photo) return null;
1149
+ return /* @__PURE__ */ jsx15(Portal, { layer: "menus", children: /* @__PURE__ */ jsxs9(Animated5.View, { accessibilityLabel: "Image viewer", accessibilityViewIsModal: true, entering: fadeIn, exiting: fadeOut, onAccessibilityEscape: onClose, style: [StyleSheet8.absoluteFill, styles8.root], testID: "chat.viewer", children: [
1150
+ /* @__PURE__ */ jsxs9(View8, { style: [styles8.top, { paddingTop: Math.max(52, layout.insets.top), paddingLeft: 16 + layout.insets.left, paddingRight: 8 + layout.insets.right }], children: [
1151
+ /* @__PURE__ */ jsxs9(View8, { style: styles8.title, children: [
1152
+ /* @__PURE__ */ jsx15(ChalkText, { color: viewer.text, numberOfLines: 1, variant: "bodyStrong", children: photo.fileName }),
1153
+ /* @__PURE__ */ jsx15(ChalkText, { color: viewer.textMuted, style: styles8.detail, variant: "label", children: count > 1 ? `${from} \xB7 ${index + 1} of ${count}` : from })
1154
+ ] }),
1155
+ /* @__PURE__ */ jsx15(ViewerButton, { icon: "dl", label: `Download ${photo.fileName}`, onPress: () => download(photo), testID: "chat.viewer.download" }),
1156
+ /* @__PURE__ */ jsx15(ViewerButton, { icon: "x", label: "Close", buttonRef: closeRef, onPress: onClose, testID: "chat.viewer.close" })
1157
+ ] }),
1158
+ /* @__PURE__ */ jsxs9(Pressable6, { accessible: false, onPress: onClose, style: styles8.stage, children: [
1159
+ /* @__PURE__ */ jsx15(Photo, { attachment: photo }),
1160
+ count > 1 ? /* @__PURE__ */ jsxs9(View8, { style: styles8.arrows, children: [
1161
+ /* @__PURE__ */ jsx15(ViewerButton, { icon: "left", label: "Previous image", onPress: () => go(-1), testID: "chat.viewer.previous" }),
1162
+ /* @__PURE__ */ jsx15(ViewerButton, { icon: "right", label: "Next image", onPress: () => go(1), testID: "chat.viewer.next" })
1163
+ ] }) : null
1164
+ ] })
1165
+ ] }) });
1166
+ }
1167
+ function Photo({ attachment }) {
1168
+ const source = useAttachmentSource(attachment);
1169
+ return /* @__PURE__ */ jsx15(View8, { pointerEvents: "none", style: styles8.photoBox, children: source.state === "ready" ? /* @__PURE__ */ jsx15(Image3, { accessibilityLabel: attachment.fileName, resizeMode: "contain", source: { uri: source.uri }, style: styles8.photo }) : /* @__PURE__ */ jsx15(Icon, { color: viewer.textMuted, name: "img", size: 24 }) });
1170
+ }
1171
+ function ViewerButton({ icon, label, onPress, testID, buttonRef }) {
1172
+ const press = usePressAnimation("round");
1173
+ return /* @__PURE__ */ jsx15(Pressable6, { accessibilityLabel: label, accessibilityRole: "button", onPress, onPressIn: press.onPressIn, onPressOut: press.onPressOut, ref: buttonRef, testID, children: ({ pressed }) => /* @__PURE__ */ jsx15(Animated5.View, { style: [styles8.button, { backgroundColor: pressed ? viewer.buttonPressed : viewer.button }, press.scaleStyle], children: /* @__PURE__ */ jsx15(Icon, { color: viewer.text, name: icon, size: 20 }) }) });
1174
+ }
1175
+ var fade = { duration: durations.menu, easing: easings.easeOut, reduceMotion: ReduceMotion.System };
1176
+ var fadeIn = () => {
1177
+ "worklet";
1178
+ return { initialValues: { opacity: 0 }, animations: { opacity: withTiming(1, fade) } };
1179
+ };
1180
+ var fadeOut = () => {
1181
+ "worklet";
1182
+ return { initialValues: { opacity: 1 }, animations: { opacity: withTiming(0, fade) } };
1183
+ };
1184
+ var styles8 = StyleSheet8.create({
1185
+ root: { backgroundColor: viewer.backdrop },
1186
+ top: { flexDirection: "row", alignItems: "center", gap: 8, paddingBottom: 8 },
1187
+ title: { flex: 1, minWidth: 0 },
1188
+ detail: { fontFamily: fontFamilies[400] },
1189
+ stage: { flex: 1 },
1190
+ // `.phone .lbs img`: the full width, 108 pt kept free under it for the arrows.
1191
+ photoBox: { position: "absolute", top: 0, left: 0, right: 0, bottom: 108, alignItems: "center", justifyContent: "center" },
1192
+ photo: { width: "100%", height: "100%" },
1193
+ arrows: { position: "absolute", left: 0, right: 0, bottom: 40, flexDirection: "row", justifyContent: "center", gap: 32 },
1194
+ button: { width: 44, height: 44, borderRadius: 22, alignItems: "center", justifyContent: "center" }
1195
+ });
1196
+
1197
+ // src/sheets/people-chat/ChatTab.tsx
1198
+ import { Fragment as Fragment2, jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
1199
+ function ChatTab() {
1200
+ const client = useSpaceClient();
1201
+ const chat = useChat();
1202
+ const self = useSelf();
1203
+ const [retried, setRetried] = useState9(() => /* @__PURE__ */ new Set());
1204
+ const [viewing, setViewing] = useState9(null);
1205
+ const groups = useMemo5(() => groupMessages(chat.messages, chat.pendingSends, self, retried).slice().reverse(), [chat.messages, chat.pendingSends, retried, self]);
1206
+ const retry = useCallback7(
1207
+ (entry) => {
1208
+ const id = entry.clientMessageId;
1209
+ if (!id) return;
1210
+ setRetried((previous) => new Set(previous).add(id));
1211
+ client.chat.send({ text: entry.text, attachments: entry.attachments }).catch(() => void 0);
1212
+ },
1213
+ [client]
1214
+ );
1215
+ const loadOlder = useCallback7(() => {
1216
+ if (!chat.pagination.hasOlder || chat.status !== "ready") return;
1217
+ client.chat.loadOlder().catch(() => void 0);
1218
+ }, [chat.pagination.hasOlder, chat.status, client]);
1219
+ const renderItem = useCallback7(({ item }) => /* @__PURE__ */ jsx16(GroupRow, { group: item, onOpenPhotos: setViewing, onRetry: retry }), [retry]);
1220
+ return /* @__PURE__ */ jsxs10(Fragment2, { children: [
1221
+ groups.length === 0 ? /* @__PURE__ */ jsxs10(View9, { style: styles9.empty, testID: "chat.empty", children: [
1222
+ /* @__PURE__ */ jsx16(ChalkText, { variant: "bodyStrong", children: "No messages yet" }),
1223
+ /* @__PURE__ */ jsx16(ChalkText, { tone: "textMuted", variant: "meta", children: "Messages are seen by everyone in the call." })
1224
+ ] }) : /* @__PURE__ */ jsx16(
1225
+ FlatList,
1226
+ {
1227
+ contentContainerStyle: styles9.content,
1228
+ data: groups,
1229
+ initialNumToRender: 10,
1230
+ inverted: true,
1231
+ keyboardDismissMode: "interactive",
1232
+ keyboardShouldPersistTaps: "handled",
1233
+ keyExtractor: (group) => group.key,
1234
+ maxToRenderPerBatch: 10,
1235
+ onEndReached: loadOlder,
1236
+ onEndReachedThreshold: 0.5,
1237
+ renderItem,
1238
+ style: styles9.list,
1239
+ testID: "chat.list",
1240
+ windowSize: 9
1241
+ }
1242
+ ),
1243
+ /* @__PURE__ */ jsx16(Composer, {}),
1244
+ viewing ? /* @__PURE__ */ jsx16(PhotoViewer, { onChange: (index) => setViewing({ ...viewing, index }), onClose: () => setViewing(null), viewing }) : null
1245
+ ] });
1246
+ }
1247
+ var GroupRow = memo5(function GroupRow2({ group, onRetry, onOpenPhotos }) {
1248
+ const { mine } = group;
1249
+ return /* @__PURE__ */ jsxs10(View9, { style: [styles9.group, mine ? styles9.groupMine : null], testID: `chat.group.${group.key}`, children: [
1250
+ mine ? null : /* @__PURE__ */ jsx16(View9, { accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", children: /* @__PURE__ */ jsx16(FaceAvatar, { name: group.displayName, size: 28 }) }),
1251
+ /* @__PURE__ */ jsxs10(View9, { style: [styles9.column, mine ? styles9.columnMine : null], children: [
1252
+ /* @__PURE__ */ jsxs10(View9, { accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", style: styles9.header, children: [
1253
+ mine ? null : /* @__PURE__ */ jsx16(ChalkText, { numberOfLines: 1, style: styles9.name, variant: "label", children: group.displayName }),
1254
+ /* @__PURE__ */ jsx16(ChalkText, { style: styles9.time, tabular: true, tone: "textMuted", variant: "label", children: group.time })
1255
+ ] }),
1256
+ group.entries.map((entry) => /* @__PURE__ */ jsx16(Message, { entry, from: mine ? "You" : group.displayName, mine, onOpenPhotos: (photos, index) => onOpenPhotos({ photos, index, from: `${mine ? "You" : group.displayName}, ${group.time}` }), onRetry }, entry.key))
1257
+ ] })
1258
+ ] });
1259
+ });
1260
+ function Message({ entry, from, mine, onRetry, onOpenPhotos }) {
1261
+ const { colors } = useTheme();
1262
+ const photos = entry.attachments.filter((attachment) => fileKind(attachment.mimeType) === "image");
1263
+ const files = entry.attachments.filter((attachment) => fileKind(attachment.mimeType) !== "image");
1264
+ const sent = entry.state === "sent";
1265
+ const summary = messageSummary(entry.text, entry.attachments);
1266
+ const stateLabel = entry.state === "failed" ? ", couldn't send" : sent ? "" : ", sending";
1267
+ return /* @__PURE__ */ jsxs10(View9, { style: [styles9.message, mine ? styles9.messageMine : null], testID: `chat.message.${entry.key}`, children: [
1268
+ /* @__PURE__ */ jsxs10(View9, { accessibilityLabel: `${from}: ${summary}${stateLabel}`, accessible: photos.length === 0 && files.length === 0, style: [styles9.body, mine ? styles9.messageMine : null, sent ? null : styles9.pending], children: [
1269
+ entry.text ? /* @__PURE__ */ jsx16(View9, { style: [styles9.bubble, { backgroundColor: colors.raised }], children: /* @__PURE__ */ jsx16(ChalkText, { variant: "body", children: entry.text }) }) : null,
1270
+ photos.length > 0 ? /* @__PURE__ */ jsx16(Thumbnails, { disabled: !sent, onOpen: (index) => onOpenPhotos(photos, index), photos }) : null,
1271
+ files.map((file) => /* @__PURE__ */ jsx16(FileCard, { attachment: file, sent }, file.attachmentId))
1272
+ ] }),
1273
+ entry.state === "queued" || entry.state === "sending" ? /* @__PURE__ */ jsx16(ChalkText, { style: styles9.status, tone: "textMuted", variant: "caption", children: "Sending\u2026" }) : null,
1274
+ entry.state === "failed" ? /* @__PURE__ */ jsxs10(View9, { style: styles9.failed, children: [
1275
+ /* @__PURE__ */ jsx16(Icon, { color: colors.dangerText, name: "alert", size: 14 }),
1276
+ /* @__PURE__ */ jsx16(ChalkText, { color: colors.dangerText, style: styles9.failedText, variant: "label", children: "Couldn\u2019t send." }),
1277
+ /* @__PURE__ */ jsx16(Pressable7, { accessibilityLabel: "Retry sending", accessibilityRole: "button", hitSlop: 12, onPress: () => onRetry(entry), testID: `chat.retry.${entry.key}`, children: /* @__PURE__ */ jsx16(ChalkText, { style: styles9.retry, variant: "label", children: "Retry" }) })
1278
+ ] }) : null
1279
+ ] });
1280
+ }
1281
+ var styles9 = StyleSheet9.create({
1282
+ list: { flex: 1 },
1283
+ // `.cbody`: 8 pt around the messages.
1284
+ content: { padding: 8 },
1285
+ empty: { flex: 1, alignItems: "center", justifyContent: "center", gap: 4, padding: 24 },
1286
+ group: { flexDirection: "row", alignItems: "flex-start", gap: 8, marginTop: 12, marginHorizontal: 4 },
1287
+ groupMine: { flexDirection: "row-reverse" },
1288
+ column: { flexShrink: 1, minWidth: 0, maxWidth: 240, alignItems: "flex-start", gap: 3 },
1289
+ columnMine: { alignItems: "flex-end" },
1290
+ header: { flexDirection: "row", alignItems: "baseline", marginHorizontal: 4, marginBottom: 1 },
1291
+ name: { fontFamily: fontFamilies[600], marginRight: 6, flexShrink: 1 },
1292
+ time: { fontFamily: fontFamilies[400] },
1293
+ message: { maxWidth: "100%", alignItems: "flex-start", gap: 4 },
1294
+ messageMine: { alignItems: "flex-end" },
1295
+ body: { maxWidth: "100%", alignItems: "flex-start", gap: 4 },
1296
+ pending: { opacity: 0.55 },
1297
+ bubble: { borderRadius: 18, paddingVertical: 8, paddingHorizontal: 12 },
1298
+ status: { fontFamily: fontFamilies[400], marginHorizontal: 6 },
1299
+ failed: { flexDirection: "row", alignItems: "center", gap: 4, marginHorizontal: 6 },
1300
+ failedText: { fontFamily: fontFamilies[400] },
1301
+ // `.lnk`: the text colour, underlined, 2 pt by 4 pt of padding.
1302
+ retry: { fontFamily: fontFamilies[600], textDecorationLine: "underline", paddingVertical: 2, paddingHorizontal: 4 }
1303
+ });
1304
+
1305
+ // src/sheets/people-chat/PeopleTab.tsx
1306
+ import { memo as memo6, useCallback as useCallback8, useMemo as useMemo6, useRef as useRef6, useState as useState10 } from "react";
1307
+ import { FlatList as FlatList2, Pressable as Pressable8, StyleSheet as StyleSheet10, View as View10 } from "react-native";
1308
+ import Animated6 from "react-native-reanimated";
1309
+
1310
+ // src/sheets/people-chat/people-model.ts
1311
+ function peopleRows({ roster, waiting, handOrder, query }) {
1312
+ const q = query.trim().toLowerCase();
1313
+ const rows = [];
1314
+ const byId = new Map(roster.map((participant) => [participant.participantId, participant]));
1315
+ const raised = handOrder.filter((id) => byId.get(id)?.handRaised);
1316
+ const numbered = raised.length > 1;
1317
+ let headed = false;
1318
+ const headInTheCall = () => {
1319
+ if (headed) return;
1320
+ headed = true;
1321
+ rows.push({ kind: "section", key: "section.call", label: "In the call" });
1322
+ };
1323
+ if (waiting.length > 0) {
1324
+ rows.push({ kind: "section", key: "section.waiting", label: "Waiting" });
1325
+ for (const request of waiting) rows.push({ kind: "waiting", key: `waiting.${request.requestId}`, request });
1326
+ headInTheCall();
1327
+ }
1328
+ if (!q && numbered) {
1329
+ rows.push({ kind: "section", key: "section.hands", label: "Raised hands" });
1330
+ raised.forEach((id, index) => {
1331
+ const participant = byId.get(id);
1332
+ if (participant) rows.push({ kind: "hand", key: `hand.${id}`, participant, order: index + 1 });
1333
+ });
1334
+ headInTheCall();
1335
+ }
1336
+ const people2 = roster.filter((participant) => !q || participant.displayName.toLowerCase().includes(q));
1337
+ if (people2.length === 0) rows.push({ kind: "none", key: "none", query: query.trim() });
1338
+ for (const participant of people2) {
1339
+ const index = raised.indexOf(participant.participantId);
1340
+ rows.push({ kind: "person", key: `person.${participant.participantId}`, participant, handNumber: numbered && index >= 0 ? index + 1 : null });
1341
+ }
1342
+ return rows;
1343
+ }
1344
+ function nextHandOrder(previous, roster) {
1345
+ const raised = new Set(roster.filter((participant) => participant.handRaised).map((participant) => participant.participantId));
1346
+ const kept = previous.filter((id) => raised.has(id));
1347
+ const added = [...raised].filter((id) => !kept.includes(id));
1348
+ return added.length === 0 && kept.length === previous.length ? previous : [...kept, ...added];
1349
+ }
1350
+
1351
+ // src/sheets/people-chat/PeopleTab.tsx
1352
+ import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
1353
+ var SEARCH_FROM = 9;
1354
+ function PeopleTab() {
1355
+ const client = useSpaceClient();
1356
+ const toasts = useToasts();
1357
+ const { colors } = useTheme();
1358
+ const { roster, admissionQueue } = useParticipants();
1359
+ const self = useSelf();
1360
+ const canAdmit = useCan("manageAdmission");
1361
+ const canMute = useCan("muteOthers");
1362
+ const canRemove = useCan("removeParticipant");
1363
+ const canRename = useCan("renameSelf");
1364
+ const [query, setQuery] = useState10("");
1365
+ const [confirmMuteAll, setConfirmMuteAll] = useState10(false);
1366
+ const muteAllRef = useRef6(null);
1367
+ const handOrder = useRef6([]);
1368
+ handOrder.current = useMemo6(() => nextHandOrder(handOrder.current, roster), [roster]);
1369
+ const waiting = canAdmit ? admissionQueue : NONE;
1370
+ const rows = useMemo6(() => peopleRows({ roster, waiting, handOrder: handOrder.current, query }), [roster, waiting, query]);
1371
+ const failed = useCallback8(
1372
+ (what) => () => {
1373
+ toasts.show({ kind: "error", dismissible: true, message: `Couldn't ${what}.` });
1374
+ },
1375
+ [toasts]
1376
+ );
1377
+ const admit = useCallback8((request) => client.participants.admit(request.requestId).catch(failed(`let ${request.displayName} in`)), [client, failed]);
1378
+ const deny = useCallback8((request) => client.participants.deny(request.requestId).catch(failed(`deny ${request.displayName}`)), [client, failed]);
1379
+ const mute2 = useCallback8((participant) => client.participants.mute(participant.participantId).catch(failed(`mute ${participant.displayName}`)), [client, failed]);
1380
+ const remove = useCallback8((participant) => client.participants.remove(participant.participantId).catch(failed(`remove ${participant.displayName}`)), [client, failed]);
1381
+ const rename = useCallback8((name) => client.participants.renameSelf(name).catch(failed("change your name")), [client, failed]);
1382
+ const loud = useMemo6(() => roster.filter((participant) => participant.participantId !== self.participantId && participant.media.microphone === "active"), [roster, self.participantId]);
1383
+ const muteAll = useCallback8(() => {
1384
+ setConfirmMuteAll(false);
1385
+ Promise.all(loud.map((participant) => client.participants.mute(participant.participantId))).catch(failed("mute everyone"));
1386
+ }, [client, failed, loud]);
1387
+ const renderItem = useCallback8(
1388
+ ({ item }) => {
1389
+ switch (item.kind) {
1390
+ case "section":
1391
+ return /* @__PURE__ */ jsx17(SectionLabel, { label: item.label });
1392
+ case "waiting":
1393
+ return /* @__PURE__ */ jsx17(WaitingRow, { onAdmit: admit, onDeny: deny, request: item.request });
1394
+ case "hand":
1395
+ return /* @__PURE__ */ jsx17(HandRow, { isSelf: item.participant.participantId === self.participantId, order: item.order, participant: item.participant });
1396
+ case "none":
1397
+ return /* @__PURE__ */ jsx17(ChalkText, { style: styles10.none, tone: "textMuted", variant: "meta", children: item.query ? `No one in the call matches \u201C${item.query}\u201D.` : "No one else is in the call." });
1398
+ case "person": {
1399
+ const isSelf = item.participant.participantId === self.participantId;
1400
+ return /* @__PURE__ */ jsx17(PersonRow, { canMute: canMute && !isSelf, canRemove: canRemove && !isSelf, canRename: canRename && isSelf, handNumber: item.handNumber, isSelf, onMute: mute2, onRemove: remove, onRename: rename, participant: item.participant });
1401
+ }
1402
+ }
1403
+ },
1404
+ [admit, canMute, canRemove, canRename, deny, mute2, remove, rename, self.participantId]
1405
+ );
1406
+ const header = /* @__PURE__ */ jsxs11(View10, { children: [
1407
+ canMute ? /* @__PURE__ */ jsx17(View10, { style: styles10.controls, children: /* @__PURE__ */ jsx17(HostChip, { disabled: loud.length === 0, expanded: confirmMuteAll, buttonRef: muteAllRef, onPress: () => setConfirmMuteAll(true) }) }) : null,
1408
+ roster.length >= SEARCH_FROM ? /* @__PURE__ */ jsxs11(View10, { style: [styles10.search, { backgroundColor: colors.raised }], children: [
1409
+ /* @__PURE__ */ jsx17(Icon, { color: colors.textMuted, name: "search", size: 16 }),
1410
+ /* @__PURE__ */ jsx17(TextField, { accessibilityLabel: "Search people", autoCapitalize: "none", autoCorrect: false, onChangeText: setQuery, placeholder: "Search people", returnKeyType: "search", testID: "people.search", value: query })
1411
+ ] }) : null
1412
+ ] });
1413
+ return /* @__PURE__ */ jsxs11(Fragment3, { children: [
1414
+ /* @__PURE__ */ jsx17(FlatList2, { contentContainerStyle: styles10.content, data: rows, initialNumToRender: 12, keyboardShouldPersistTaps: "handled", keyExtractor: (row) => row.key, ListHeaderComponent: header, maxToRenderPerBatch: 12, renderItem, style: styles10.list, testID: "people.list", windowSize: 7 }),
1415
+ /* @__PURE__ */ jsx17(Menu, { accessibilityLabel: "Mute everyone", anchorRef: muteAllRef, minWidth: 280, onClose: () => setConfirmMuteAll(false), open: confirmMuteAll, testID: "people.mute-all.confirm", children: /* @__PURE__ */ jsxs11(View10, { style: styles10.confirm, children: [
1416
+ /* @__PURE__ */ jsx17(ChalkText, { variant: "cardTitle", children: "Mute everyone?" }),
1417
+ /* @__PURE__ */ jsx17(ChalkText, { style: styles10.confirmText, tone: "textSoft", variant: "meta", children: `${loud.length} ${loud.length === 1 ? "person" : "people"} will be muted. They can unmute themselves.` }),
1418
+ /* @__PURE__ */ jsxs11(View10, { style: styles10.confirmButtons, children: [
1419
+ /* @__PURE__ */ jsx17(PillButton, { label: "Cancel", onPress: () => setConfirmMuteAll(false), size: "sm", testID: "people.mute-all.cancel" }),
1420
+ /* @__PURE__ */ jsx17(PillButton, { label: "Mute everyone", onPress: muteAll, size: "sm", testID: "people.mute-all.confirm-button", variant: "primary" })
1421
+ ] })
1422
+ ] }) })
1423
+ ] });
1424
+ }
1425
+ var NONE = [];
1426
+ function HostChip({ disabled, expanded, onPress, buttonRef }) {
1427
+ const { colors } = useTheme();
1428
+ const press = usePressAnimation("pill");
1429
+ return /* @__PURE__ */ jsx17(Pressable8, { accessibilityHint: "Asks you to confirm", accessibilityLabel: "Mute everyone", accessibilityRole: "button", accessibilityState: { disabled, expanded }, disabled, onPress, onPressIn: press.onPressIn, onPressOut: press.onPressOut, ref: buttonRef, testID: "people.mute-all", children: ({ pressed }) => /* @__PURE__ */ jsxs11(Animated6.View, { style: [styles10.chip, { backgroundColor: pressed || expanded ? colors.hover : colors.raised }, disabled ? styles10.disabled : press.scaleStyle], children: [
1430
+ /* @__PURE__ */ jsx17(Icon, { color: colors.textMuted, name: "micOff", size: 16 }),
1431
+ /* @__PURE__ */ jsx17(ChalkText, { variant: "button", children: "Mute everyone" })
1432
+ ] }) });
1433
+ }
1434
+ var SectionLabel = memo6(function SectionLabel2({ label }) {
1435
+ return /* @__PURE__ */ jsx17(View10, { accessibilityRole: "header", accessible: true, style: styles10.section, children: /* @__PURE__ */ jsx17(ChalkText, { tone: "textMuted", variant: "section", children: label }) });
1436
+ });
1437
+ function Face({ name }) {
1438
+ return /* @__PURE__ */ jsx17(View10, { accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", children: /* @__PURE__ */ jsx17(FaceAvatar, { name, size: 32 }) });
1439
+ }
1440
+ var WaitingRow = memo6(function WaitingRow2({ request, onAdmit, onDeny }) {
1441
+ return /* @__PURE__ */ jsxs11(View10, { style: styles10.row, testID: `people.waiting.${request.participantId}`, children: [
1442
+ /* @__PURE__ */ jsx17(Face, { name: request.displayName }),
1443
+ /* @__PURE__ */ jsx17(ChalkText, { numberOfLines: 1, style: styles10.name, variant: "bodyMedium", children: request.displayName }),
1444
+ /* @__PURE__ */ jsx17(PillButton, { accessibilityLabel: `Deny ${request.displayName}`, label: "Deny", onPress: () => onDeny(request), size: "sm", testID: `people.deny.${request.participantId}` }),
1445
+ /* @__PURE__ */ jsx17(PillButton, { accessibilityLabel: `Let ${request.displayName} in`, label: "Let in", onPress: () => onAdmit(request), size: "sm", testID: `people.admit.${request.participantId}`, variant: "primary" })
1446
+ ] });
1447
+ });
1448
+ var HandRow = memo6(function HandRow2({ participant, order, isSelf }) {
1449
+ return /* @__PURE__ */ jsxs11(View10, { accessible: true, accessibilityLabel: `${participant.displayName}${isSelf ? " (You)" : ""}, hand raised, number ${order}`, style: [styles10.row, styles10.handRow], testID: `people.hand.${participant.participantId}`, children: [
1450
+ /* @__PURE__ */ jsx17(View10, { style: styles10.handNumber, children: /* @__PURE__ */ jsx17(ChalkText, { color: fixed.handText, variant: "section", children: order }) }),
1451
+ /* @__PURE__ */ jsx17(Face, { name: participant.displayName }),
1452
+ /* @__PURE__ */ jsxs11(ChalkText, { numberOfLines: 1, style: styles10.name, variant: "bodyMedium", children: [
1453
+ participant.displayName,
1454
+ isSelf ? /* @__PURE__ */ jsx17(ChalkText, { tone: "textMuted", variant: "meta", children: " (You)" }) : null
1455
+ ] })
1456
+ ] });
1457
+ });
1458
+ var PersonRow = memo6(function PersonRow2({ participant, isSelf, handNumber, canMute, canRemove, canRename, onMute, onRemove, onRename }) {
1459
+ const { colors } = useTheme();
1460
+ const [menuOpen, setMenuOpen] = useState10(false);
1461
+ const [renaming, setRenaming] = useState10(false);
1462
+ const [draft, setDraft] = useState10(participant.displayName);
1463
+ const optionsRef = useRef6(null);
1464
+ const host = participant.capabilities.includes("endEpisode");
1465
+ const micOff = participant.media.microphone === "inactive";
1466
+ const canMuteNow = canMute && participant.media.microphone === "active";
1467
+ const hasOptions = canRename || canMuteNow || canRemove;
1468
+ const save = () => {
1469
+ const name = draft.trim();
1470
+ setRenaming(false);
1471
+ if (name && name !== participant.displayName) onRename(name);
1472
+ };
1473
+ if (renaming) {
1474
+ return /* @__PURE__ */ jsxs11(View10, { style: styles10.row, testID: "people.rename", children: [
1475
+ /* @__PURE__ */ jsx17(Face, { name: participant.displayName }),
1476
+ /* @__PURE__ */ jsx17(View10, { style: [styles10.rename, { backgroundColor: colors.raised, boxShadow: `0px 0px 0px 2px ${colors.text}` }], children: /* @__PURE__ */ jsx17(TextField, { accessibilityLabel: "Your name", autoFocus: true, maxLength: 40, onChangeText: setDraft, onSubmitEditing: save, returnKeyType: "done", selectTextOnFocus: true, testID: "people.rename.input", value: draft }) }),
1477
+ /* @__PURE__ */ jsx17(PillButton, { label: "Save", onPress: save, size: "sm", testID: "people.rename.save", variant: "primary" })
1478
+ ] });
1479
+ }
1480
+ const status = [isSelf ? "You" : null, host ? "Host" : null, participant.handRaised ? `hand raised${handNumber ? `, number ${handNumber}` : ""}` : null, micOff ? "microphone off" : null].filter(Boolean).join(", ");
1481
+ return /* @__PURE__ */ jsxs11(View10, { style: styles10.row, testID: `people.row.${participant.participantId}`, children: [
1482
+ /* @__PURE__ */ jsx17(Face, { name: participant.displayName }),
1483
+ /* @__PURE__ */ jsxs11(ChalkText, { accessibilityLabel: status ? `${participant.displayName}, ${status}` : participant.displayName, numberOfLines: 1, style: styles10.name, variant: "bodyMedium", children: [
1484
+ participant.displayName,
1485
+ isSelf ? /* @__PURE__ */ jsx17(ChalkText, { tone: "textMuted", variant: "meta", children: " (You)" }) : null,
1486
+ host ? /* @__PURE__ */ jsx17(ChalkText, { tone: "textMuted", variant: "meta", children: " Host" }) : null
1487
+ ] }),
1488
+ participant.handRaised ? /* @__PURE__ */ jsxs11(View10, { accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", style: styles10.status, children: [
1489
+ /* @__PURE__ */ jsx17(Icon, { color: colors.warning, name: "hand", size: 16 }),
1490
+ handNumber ? /* @__PURE__ */ jsx17(ChalkText, { color: colors.warning, style: styles10.statusNumber, variant: "caption", children: handNumber }) : null
1491
+ ] }) : null,
1492
+ micOff ? /* @__PURE__ */ jsx17(View10, { accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", style: styles10.status, children: /* @__PURE__ */ jsx17(Icon, { color: colors.dangerText, name: "micOff", size: 16 }) }) : null,
1493
+ hasOptions ? /* @__PURE__ */ jsx17(IconButton, { accessibilityLabel: `Options for ${participant.displayName}`, expanded: menuOpen, icon: "more", onPress: () => setMenuOpen(true), ref: optionsRef, size: 36, testID: `people.options.${participant.participantId}`, tone: "ghost" }) : null,
1494
+ /* @__PURE__ */ jsxs11(Menu, { accessibilityLabel: "Person options", anchorRef: optionsRef, onClose: () => setMenuOpen(false), open: menuOpen, testID: "people.menu", children: [
1495
+ canRename ? /* @__PURE__ */ jsx17(
1496
+ MenuItem,
1497
+ {
1498
+ icon: "edit",
1499
+ label: "Edit name",
1500
+ onPress: () => {
1501
+ setMenuOpen(false);
1502
+ setDraft(participant.displayName);
1503
+ setRenaming(true);
1504
+ },
1505
+ testID: "people.menu.rename"
1506
+ }
1507
+ ) : null,
1508
+ canMuteNow ? /* @__PURE__ */ jsx17(
1509
+ MenuItem,
1510
+ {
1511
+ icon: "micOff",
1512
+ label: "Mute",
1513
+ onPress: () => {
1514
+ setMenuOpen(false);
1515
+ onMute(participant);
1516
+ },
1517
+ testID: "people.menu.mute"
1518
+ }
1519
+ ) : null,
1520
+ canRemove ? /* @__PURE__ */ jsx17(
1521
+ MenuItem,
1522
+ {
1523
+ danger: true,
1524
+ icon: "x",
1525
+ label: "Remove from call",
1526
+ onPress: () => {
1527
+ setMenuOpen(false);
1528
+ onRemove(participant);
1529
+ },
1530
+ testID: "people.menu.remove"
1531
+ }
1532
+ ) : null
1533
+ ] })
1534
+ ] });
1535
+ });
1536
+ var styles10 = StyleSheet10.create({
1537
+ list: { flex: 1 },
1538
+ content: { padding: 8, paddingBottom: 16 },
1539
+ controls: { flexDirection: "row", flexWrap: "wrap", gap: 6, marginHorizontal: 2, marginTop: 2, marginBottom: 8 },
1540
+ chip: { flexDirection: "row", alignItems: "center", gap: 6, height: 36, paddingLeft: 10, paddingRight: 12, borderRadius: 999 },
1541
+ disabled: { opacity: 0.45 },
1542
+ search: { flexDirection: "row", alignItems: "center", gap: 8, height: 40, marginHorizontal: 2, marginTop: 2, marginBottom: 8, paddingHorizontal: 14, borderRadius: 999 },
1543
+ section: { paddingTop: 10, paddingHorizontal: 8, paddingBottom: 6 },
1544
+ none: { paddingVertical: 16, paddingHorizontal: 8 },
1545
+ row: { flexDirection: "row", alignItems: "center", gap: 10, minHeight: 52, paddingLeft: 8, paddingRight: 4, borderRadius: 12 },
1546
+ handRow: { gap: 8 },
1547
+ name: { flex: 1 },
1548
+ handNumber: { width: 20, height: 20, borderRadius: 10, alignItems: "center", justifyContent: "center", backgroundColor: fixed.hand },
1549
+ status: { flexDirection: "row", alignItems: "center", justifyContent: "center" },
1550
+ statusNumber: { marginLeft: 1, fontWeight: "700" },
1551
+ rename: { flex: 1, flexDirection: "row", alignItems: "center", height: 36, paddingHorizontal: 12, borderRadius: 999 },
1552
+ confirm: { paddingTop: 8, paddingHorizontal: 8, paddingBottom: 4 },
1553
+ confirmText: { marginTop: 4, marginBottom: 10 },
1554
+ confirmButtons: { flexDirection: "row", justifyContent: "flex-end", gap: 8, marginTop: 4, marginHorizontal: 6 }
1555
+ });
1556
+
1557
+ // src/sheets/people-chat/PeopleChatSheet.tsx
1558
+ import { jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
1559
+ function PeopleChatSheet({ open, tab, onTabChange, onClose, returnFocusRef }) {
1560
+ const { colors } = useTheme();
1561
+ const { roster } = useParticipants();
1562
+ const chat = useChat();
1563
+ useMarkChatRead(open && tab === "chat");
1564
+ const unread = chat.unreadCount > 0 && tab !== "chat";
1565
+ return /* @__PURE__ */ jsxs12(BottomSheet, { accessibilityLabel: "People and chat", heightRatio: 0.78, onClose, open, returnFocusRef, testID: "sheet.people", variant: "card", children: [
1566
+ /* @__PURE__ */ jsx18(SheetDragRegion, { children: /* @__PURE__ */ jsxs12(View11, { accessibilityRole: "tablist", style: [styles11.tabs, { borderBottomColor: colors.line }], children: [
1567
+ /* @__PURE__ */ jsx18(Tab, { label: "People", count: roster.length, onPress: () => onTabChange("people"), selected: tab === "people", testID: "sheet.tab.people" }),
1568
+ /* @__PURE__ */ jsx18(Tab, { label: "Chat", onPress: () => onTabChange("chat"), selected: tab === "chat", testID: "sheet.tab.chat", unread }),
1569
+ /* @__PURE__ */ jsx18(View11, { style: styles11.close, children: /* @__PURE__ */ jsx18(IconButton, { accessibilityLabel: "Close", icon: "x", onPress: onClose, size: 44, testID: "sheet.people.close", tone: "ghost" }) })
1570
+ ] }) }),
1571
+ tab === "people" ? /* @__PURE__ */ jsx18(PeopleTab, {}) : /* @__PURE__ */ jsx18(ChatTab, {})
1572
+ ] });
1573
+ }
1574
+ function Tab({ label, selected, onPress, count, unread = false, testID }) {
1575
+ const { colors } = useTheme();
1576
+ const accessibilityLabel = [count === void 0 ? label : `${label}, ${count}`, unread ? "new messages" : null].filter(Boolean).join(", ");
1577
+ return /* @__PURE__ */ jsxs12(Pressable9, { accessibilityLabel, accessibilityRole: "tab", accessibilityState: { selected }, onPress, style: styles11.tab, testID, children: [
1578
+ /* @__PURE__ */ jsxs12(View11, { style: styles11.tabLabel, children: [
1579
+ /* @__PURE__ */ jsx18(ChalkText, { tone: selected ? "text" : "textMuted", variant: "bodyStrong", children: label }),
1580
+ count === void 0 ? null : /* @__PURE__ */ jsx18(ChalkText, { style: styles11.count, tabular: true, tone: "textMuted", variant: "bodyMedium", children: count })
1581
+ ] }),
1582
+ selected ? /* @__PURE__ */ jsx18(View11, { style: [styles11.underline, { backgroundColor: colors.text }] }) : null,
1583
+ unread ? /* @__PURE__ */ jsx18(View11, { style: [styles11.unread, { backgroundColor: colors.info }], testID: `${testID}.unread` }) : null
1584
+ ] });
1585
+ }
1586
+ function useMarkChatRead(reading) {
1587
+ const client = useSpaceClient();
1588
+ const { messages, unreadCount } = useChat();
1589
+ const last = messages[messages.length - 1]?.messageId;
1590
+ useEffect10(() => {
1591
+ if (!reading || !last || unreadCount === 0) return;
1592
+ client.chat.markRead(last).catch(() => void 0);
1593
+ }, [client, last, reading, unreadCount]);
1594
+ }
1595
+ var styles11 = StyleSheet11.create({
1596
+ // `.tabs`: 4 pt above, 8 right, 10 left, a hairline under it.
1597
+ tabs: { flexDirection: "row", alignItems: "center", gap: 4, paddingTop: 4, paddingRight: 8, paddingLeft: 10, borderBottomWidth: 1 },
1598
+ tab: { height: 46, justifyContent: "center", paddingHorizontal: 10 },
1599
+ tabLabel: { flexDirection: "row", alignItems: "center" },
1600
+ count: { marginLeft: 5 },
1601
+ underline: { position: "absolute", left: 10, right: 10, bottom: -1, height: 2, borderRadius: 2 },
1602
+ unread: { position: "absolute", right: 0, top: 13, width: 7, height: 7, borderRadius: 4 },
1603
+ close: { marginLeft: "auto" }
1604
+ });
1605
+
1606
+ // src/stage/Stage.tsx
1607
+ import { memo as memo11, useCallback as useCallback13, useMemo as useMemo11, useState as useState12 } from "react";
1608
+ import { Pressable as Pressable13, StyleSheet as StyleSheet18, View as View18 } from "react-native";
1609
+ import Animated9 from "react-native-reanimated";
1610
+
1611
+ // src/motion/MovingFrame.tsx
1612
+ import { useEffect as useEffect11, useRef as useRef7 } from "react";
1613
+ import { StyleSheet as StyleSheet12 } from "react-native";
1614
+ import Animated7, { runOnUI, useAnimatedStyle as useAnimatedStyle3, useReducedMotion, useSharedValue, withTiming as withTiming2 } from "react-native-reanimated";
1615
+ import { jsx as jsx19 } from "react/jsx-runtime";
1616
+ function MovingFrame({ frame, children, style, testID }) {
1617
+ const reduceMotion = useReducedMotion();
1618
+ const target = useSharedValue(frame);
1619
+ const from = useSharedValue(frame);
1620
+ const progress = useSharedValue(1);
1621
+ const mounted = useRef7(false);
1622
+ const { x, y, width, height } = frame;
1623
+ useEffect11(() => {
1624
+ if (!mounted.current) {
1625
+ mounted.current = true;
1626
+ return;
1627
+ }
1628
+ runOnUI((next, instant) => {
1629
+ "worklet";
1630
+ const t = progress.value;
1631
+ const was = target.value;
1632
+ const start = from.value;
1633
+ const shown = {
1634
+ x: was.x + (start.x - was.x) * (1 - t),
1635
+ y: was.y + (start.y - was.y) * (1 - t),
1636
+ width: was.width + (start.width - was.width) * (1 - t),
1637
+ height: was.height + (start.height - was.height) * (1 - t)
1638
+ };
1639
+ target.value = next;
1640
+ if (instant) {
1641
+ from.value = next;
1642
+ progress.value = 1;
1643
+ return;
1644
+ }
1645
+ from.value = shown;
1646
+ progress.value = 0;
1647
+ progress.value = withTiming2(1, timings.move);
1648
+ })({ x, y, width, height }, reduceMotion);
1649
+ }, [x, y, width, height, reduceMotion, from, progress, target]);
1650
+ const animatedStyle = useAnimatedStyle3(() => {
1651
+ "worklet";
1652
+ const next = target.value;
1653
+ const start = from.value;
1654
+ const rest = 1 - progress.value;
1655
+ const scaleX = next.width > 0 ? 1 + (start.width / next.width - 1) * rest : 1;
1656
+ const scaleY = next.height > 0 ? 1 + (start.height / next.height - 1) * rest : 1;
1657
+ return {
1658
+ left: next.x,
1659
+ top: next.y,
1660
+ width: next.width,
1661
+ height: next.height,
1662
+ transform: [{ translateX: (start.x - next.x) * rest }, { translateY: (start.y - next.y) * rest }, { scaleX }, { scaleY }]
1663
+ };
1664
+ });
1665
+ return /* @__PURE__ */ jsx19(Animated7.View, { style: [styles12.frame, style, animatedStyle], testID, children });
1666
+ }
1667
+ var styles12 = StyleSheet12.create({
1668
+ frame: { position: "absolute", transformOrigin: "left top" }
1669
+ });
1670
+
1671
+ // src/motion/presence.ts
1672
+ import { withTiming as withTiming3 } from "react-native-reanimated";
1673
+ var tileEntering = () => {
1674
+ "worklet";
1675
+ return {
1676
+ initialValues: { opacity: 0, transform: [{ scale: 0.97 }] },
1677
+ animations: { opacity: withTiming3(1, timings.enter), transform: [{ scale: withTiming3(1, timings.enter) }] }
1678
+ };
1679
+ };
1680
+ var tileLeaving = () => {
1681
+ "worklet";
1682
+ return {
1683
+ initialValues: { opacity: 1 },
1684
+ animations: { opacity: withTiming3(0, timings.leave) }
1685
+ };
1686
+ };
1687
+
1688
+ // src/stage/ContentTile.tsx
1689
+ import { memo as memo8, useMemo as useMemo8, useRef as useRef10 } from "react";
1690
+ import { Pressable as Pressable11, StyleSheet as StyleSheet14, View as View14 } from "react-native";
1691
+
1692
+ // src/components/EmbeddedWhiteboard.tsx
1693
+ import { ChalkWhiteboardController } from "@q9labsai/chalk-whiteboard/embedded";
1694
+ import { useCallback as useCallback9, useEffect as useEffect12, useMemo as useMemo7, useRef as useRef8, useState as useState11 } from "react";
1695
+ import { ActivityIndicator as ActivityIndicator2, Pressable as Pressable10, Text as Text2, useWindowDimensions, View as View12 } from "react-native";
1696
+ import WebView from "react-native-webview";
1697
+
1698
+ // src/whiteboard/embedded-whiteboard-assets.ts
1699
+ import { NativeModules as NativeModules3, Platform as Platform6 } from "react-native";
1700
+ var ANDROID_RENDERER_URL = "file:///android_asset/chalk-whiteboard/index.html";
1701
+ async function resolveEmbeddedWhiteboardRendererURL(override) {
1702
+ if (override) return requireLocalRendererURL(override);
1703
+ if (Platform6.OS === "android") return ANDROID_RENDERER_URL;
1704
+ const module = NativeModules3.ChalkWhiteboardAssets;
1705
+ if (!module?.rendererURL) throw new Error(`Chalk embedded whiteboard assets are unavailable on ${Platform6.OS}`);
1706
+ return requireLocalRendererURL(await module.rendererURL());
1707
+ }
1708
+ function rendererURLWithContext(rendererURL, context) {
1709
+ const separator = rendererURL.includes("?") ? "&" : "?";
1710
+ return `${rendererURL}${separator}journeyId=${encodeURIComponent(context.journeyId)}&rendererGeneration=${encodeURIComponent(context.rendererGeneration)}`;
1711
+ }
1712
+ function isEmbeddedWhiteboardNavigationAllowed(requestURL, rendererURL) {
1713
+ if (requestURL === "about:blank") return true;
1714
+ try {
1715
+ const renderer = new URL(rendererURL);
1716
+ const request = new URL(requestURL);
1717
+ if (renderer.protocol !== "file:" || request.protocol !== "file:" || request.host !== renderer.host) return false;
1718
+ const requestPath = decodeURIComponent(request.pathname);
1719
+ if (requestPath.split("/").includes("..")) return false;
1720
+ const rendererDirectory = renderer.pathname.slice(0, renderer.pathname.lastIndexOf("/") + 1);
1721
+ return request.pathname === renderer.pathname || request.pathname.startsWith(rendererDirectory);
1722
+ } catch {
1723
+ return false;
1724
+ }
1725
+ }
1726
+ function requireLocalRendererURL(value) {
1727
+ if (!value.startsWith("file://")) throw new Error("Chalk embedded whiteboard renderer must be a local file URL");
1728
+ return value;
1729
+ }
1730
+
1731
+ // src/whiteboard/embedded-whiteboard-viewport.ts
1732
+ function createEmbeddedWhiteboardViewport(width, height, scale) {
1733
+ if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0 || !Number.isFinite(scale) || scale <= 0) return null;
1734
+ return { width, height, scale };
1735
+ }
1736
+
1737
+ // src/components/EmbeddedWhiteboard.tsx
1738
+ import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
1739
+ var embeddedWhiteboardWebViewSecurityPolicy = Object.freeze({
1740
+ allowFileAccess: true,
1741
+ allowFileAccessFromFileURLs: false,
1742
+ allowUniversalAccessFromFileURLs: false,
1743
+ cacheEnabled: false,
1744
+ domStorageEnabled: false,
1745
+ javaScriptCanOpenWindowsAutomatically: false,
1746
+ javaScriptEnabled: true,
1747
+ mixedContentMode: "never",
1748
+ setSupportMultipleWindows: false,
1749
+ sharedCookiesEnabled: false,
1750
+ thirdPartyCookiesEnabled: false
1751
+ });
1752
+ function EmbeddedWhiteboard({ transport, journeyId, traceparent, tracestate, canDraw, canClear, theme: themeOverride, localParticipantColor, rendererURL: rendererURLOverride, style, testID, onMetric, onError, onUserExport }) {
1753
+ const theme = themeOverride ?? "light";
1754
+ const webViewRef = useRef8(null);
1755
+ const controllerRef = useRef8(null);
1756
+ const listenersRef = useRef8(/* @__PURE__ */ new Set());
1757
+ const onMetricRef = useRef8(onMetric);
1758
+ const onErrorRef = useRef8(onError);
1759
+ const onUserExportRef = useRef8(onUserExport);
1760
+ const [rendererURL, setRendererURL] = useState11(null);
1761
+ const [rendererGeneration, setRendererGeneration] = useState11(createRendererGeneration);
1762
+ const [loadAttempt, setLoadAttempt] = useState11(0);
1763
+ const [loadError, setLoadError] = useState11(null);
1764
+ const [compatibilityNotice, setCompatibilityNotice] = useState11(null);
1765
+ const [layout, setLayout] = useState11({ width: 0, height: 0 });
1766
+ const { scale } = useWindowDimensions();
1767
+ const viewport = useMemo7(() => createEmbeddedWhiteboardViewport(layout.width, layout.height, scale), [layout.height, layout.width, scale]);
1768
+ const latestViewportRef = useRef8(viewport);
1769
+ latestViewportRef.current = viewport;
1770
+ onMetricRef.current = onMetric;
1771
+ onErrorRef.current = onError;
1772
+ onUserExportRef.current = onUserExport;
1773
+ const rendererPort = useMemo7(
1774
+ () => ({
1775
+ postMessage: (message2) => webViewRef.current?.postMessage(message2),
1776
+ subscribe: (listener) => {
1777
+ listenersRef.current.add(listener);
1778
+ return () => listenersRef.current.delete(listener);
1779
+ }
1780
+ }),
1781
+ []
1782
+ );
1783
+ const handleControllerError = useCallback9((error2) => onErrorRef.current?.(error2), []);
1784
+ const handleControllerMetric = useCallback9((metric) => onMetricRef.current?.(metric), []);
1785
+ const handleControllerUserExport = useCallback9((value) => onUserExportRef.current?.(value), []);
1786
+ useEffect12(() => {
1787
+ let active = true;
1788
+ setLoadError(null);
1789
+ void resolveEmbeddedWhiteboardRendererURL(rendererURLOverride).then((url) => {
1790
+ if (active) setRendererURL(url);
1791
+ }).catch((cause) => {
1792
+ if (!active) return;
1793
+ const message2 = cause instanceof Error ? cause.message : "Chalk whiteboard assets are unavailable";
1794
+ setLoadError(message2);
1795
+ onError?.({ code: "renderer_asset_unavailable", message: message2, recoverable: false });
1796
+ });
1797
+ return () => {
1798
+ active = false;
1799
+ };
1800
+ }, [loadAttempt, onError, rendererURLOverride]);
1801
+ useEffect12(() => {
1802
+ setCompatibilityNotice(null);
1803
+ const controller = new ChalkWhiteboardController({
1804
+ renderer: rendererPort,
1805
+ transport,
1806
+ journeyId,
1807
+ ...traceparent ? { traceparent } : {},
1808
+ ...tracestate ? { tracestate } : {},
1809
+ canDraw,
1810
+ canClear,
1811
+ theme,
1812
+ ...localParticipantColor ? { localParticipantColor } : {},
1813
+ onMetric: handleControllerMetric,
1814
+ onError: handleControllerError,
1815
+ onCompatibilityChange: (state) => setCompatibilityNotice(state.message),
1816
+ onUserExport: handleControllerUserExport
1817
+ });
1818
+ controller.start();
1819
+ if (latestViewportRef.current) controller.setViewport(latestViewportRef.current);
1820
+ controllerRef.current = controller;
1821
+ return () => {
1822
+ if (controllerRef.current === controller) controllerRef.current = null;
1823
+ controller.stop();
1824
+ };
1825
+ }, [handleControllerError, handleControllerMetric, handleControllerUserExport, journeyId, localParticipantColor, rendererPort, theme, traceparent, tracestate, transport]);
1826
+ useEffect12(() => {
1827
+ controllerRef.current?.setCapabilities({ canDraw, canClear });
1828
+ }, [canClear, canDraw]);
1829
+ useEffect12(() => {
1830
+ if (viewport) controllerRef.current?.setViewport(viewport);
1831
+ }, [viewport]);
1832
+ const handleLayout = useCallback9((event) => {
1833
+ const next = { width: event.nativeEvent.layout.width, height: event.nativeEvent.layout.height };
1834
+ setLayout((current) => current.width === next.width && current.height === next.height ? current : next);
1835
+ }, []);
1836
+ const sourceURL = rendererURL ? rendererURLWithContext(rendererURL, { journeyId, rendererGeneration }) : null;
1837
+ const handleMessage = (event) => {
1838
+ const message2 = event.nativeEvent.data;
1839
+ listenersRef.current.forEach((listener) => listener(message2));
1840
+ };
1841
+ const allowNavigation = (request) => Boolean(rendererURL && isEmbeddedWhiteboardNavigationAllowed(request.url, rendererURL));
1842
+ const reloadRenderer = () => {
1843
+ controllerRef.current?.rendererReloaded();
1844
+ setRendererGeneration(createRendererGeneration());
1845
+ };
1846
+ const retryRenderer = () => {
1847
+ setLoadError(null);
1848
+ setLoadAttempt((attempt) => attempt + 1);
1849
+ reloadRenderer();
1850
+ };
1851
+ const handleRendererTermination = () => {
1852
+ handleControllerMetric({ name: "whiteboard.renderer.termination", value: 1 });
1853
+ handleControllerError({
1854
+ code: "renderer_terminated",
1855
+ message: "The whiteboard renderer stopped and is recovering.",
1856
+ recoverable: true
1857
+ });
1858
+ reloadRenderer();
1859
+ };
1860
+ if (loadError) {
1861
+ return /* @__PURE__ */ jsxs13(View12, { style: [{ flex: 1 }, style], testID, children: [
1862
+ /* @__PURE__ */ jsx20(Text2, { children: "Whiteboard unavailable" }),
1863
+ /* @__PURE__ */ jsx20(Text2, { children: loadError }),
1864
+ /* @__PURE__ */ jsx20(Pressable10, { accessibilityRole: "button", onPress: retryRenderer, children: /* @__PURE__ */ jsx20(Text2, { children: "Retry" }) })
1865
+ ] });
1866
+ }
1867
+ if (!sourceURL || !rendererURL) {
1868
+ return /* @__PURE__ */ jsxs13(View12, { style: [{ flex: 1 }, style], testID, children: [
1869
+ /* @__PURE__ */ jsx20(ActivityIndicator2, {}),
1870
+ /* @__PURE__ */ jsx20(Text2, { children: "Loading whiteboard\u2026" })
1871
+ ] });
1872
+ }
1873
+ return /* @__PURE__ */ jsxs13(View12, { onLayout: handleLayout, style: [{ flex: 1 }, style], testID, children: [
1874
+ /* @__PURE__ */ jsx20(
1875
+ WebView,
1876
+ {
1877
+ ref: webViewRef,
1878
+ ...embeddedWhiteboardWebViewSecurityPolicy,
1879
+ allowingReadAccessToURL: rendererURL.slice(0, rendererURL.lastIndexOf("/") + 1),
1880
+ mediaPlaybackRequiresUserAction: true,
1881
+ onContentProcessDidTerminate: handleRendererTermination,
1882
+ onError: (event) => {
1883
+ const message2 = event.nativeEvent.description || "The whiteboard renderer failed to load";
1884
+ setLoadError(message2);
1885
+ handleControllerError({ code: "renderer_load_failed", message: message2, recoverable: true });
1886
+ },
1887
+ onMessage: handleMessage,
1888
+ onRenderProcessGone: handleRendererTermination,
1889
+ onShouldStartLoadWithRequest: allowNavigation,
1890
+ originWhitelist: ["file://*"],
1891
+ pullToRefreshEnabled: false,
1892
+ scrollEnabled: false,
1893
+ source: { uri: sourceURL },
1894
+ style: { flex: 1 },
1895
+ webviewDebuggingEnabled: typeof __DEV__ !== "undefined" && __DEV__
1896
+ },
1897
+ rendererGeneration
1898
+ ),
1899
+ compatibilityNotice ? /* @__PURE__ */ jsx20(View12, { pointerEvents: "none", children: /* @__PURE__ */ jsx20(Text2, { children: compatibilityNotice }) }) : null
1900
+ ] });
1901
+ }
1902
+ function createRendererGeneration() {
1903
+ return globalThis.crypto?.randomUUID?.() ?? `whiteboard-${Date.now()}-${Math.random().toString(36).slice(2)}`;
1904
+ }
1905
+
1906
+ // src/media/native-webrtc.ts
1907
+ import { MediaStream, RTCView, mediaDevices } from "@cloudflare/react-native-webrtc";
1908
+ function createNativeMediaStream(track) {
1909
+ const stream = Reflect.construct(MediaStream, [[track]]);
1910
+ if (!(stream instanceof MediaStream)) throw new TypeError("React Native WebRTC did not create a media stream");
1911
+ return stream;
1912
+ }
1913
+
1914
+ // src/components/rtc-video-view-availability.ts
1915
+ function canRenderRtcVideoView(component) {
1916
+ return typeof component === "function" || typeof component === "object" && component !== null;
1917
+ }
1918
+
1919
+ // src/components/RtcVideoView.tsx
1920
+ import { jsx as jsx21 } from "react/jsx-runtime";
1921
+ function RtcVideoView(props) {
1922
+ if (!canRenderRtcVideoView(RTCView)) {
1923
+ return null;
1924
+ }
1925
+ const RtcView = RTCView;
1926
+ return /* @__PURE__ */ jsx21(RtcView, { ...props });
1927
+ }
1928
+
1929
+ // src/primitives/NameTag.tsx
1930
+ import { memo as memo7 } from "react";
1931
+ import { StyleSheet as StyleSheet13, View as View13 } from "react-native";
1932
+ import { jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
1933
+ var NameTag = memo7(function NameTag2({ name, muted = false, pinned = false, icon, you = false, handRaised = false, handOrder, testID }) {
1934
+ const tagRef = useMeasuredRef(testID, styles13.tag);
1935
+ return /* @__PURE__ */ jsx22(View13, { pointerEvents: "none", style: styles13.slot, children: /* @__PURE__ */ jsxs14(View13, { ref: tagRef, style: styles13.tag, testID, children: [
1936
+ pinned ? /* @__PURE__ */ jsx22(Icon, { color: fixed.overlayText, name: "pin", size: 14 }) : null,
1937
+ icon ? /* @__PURE__ */ jsx22(Icon, { color: fixed.overlayText, name: icon, size: 14 }) : null,
1938
+ /* @__PURE__ */ jsx22(ChalkText, { color: fixed.overlayText, numberOfLines: 1, style: styles13.name, variant: "label", children: name }),
1939
+ you ? /* @__PURE__ */ jsx22(ChalkText, { color: fixed.overlayText, style: styles13.you, variant: "label", children: "(You)" }) : null,
1940
+ muted ? /* @__PURE__ */ jsx22(Icon, { color: fixed.overlayDanger, name: "micOff", size: 14 }) : null,
1941
+ handRaised ? /* @__PURE__ */ jsxs14(View13, { style: styles13.hand, children: [
1942
+ /* @__PURE__ */ jsx22(Icon, { color: fixed.overlayWarning, name: "hand", size: 14 }),
1943
+ handOrder !== void 0 ? /* @__PURE__ */ jsx22(ChalkText, { color: fixed.overlayWarning, style: styles13.order, tabular: true, children: handOrder }) : null
1944
+ ] }) : null
1945
+ ] }) });
1946
+ });
1947
+ var INSET = 8;
1948
+ var HEIGHT = 24;
1949
+ var nameTagZone = INSET + HEIGHT;
1950
+ var styles13 = StyleSheet13.create({
1951
+ // `.tag`: 8 pt from the bottom-left corner and never wider than the tile less 16 pt.
1952
+ slot: { position: "absolute", left: INSET, right: INSET, bottom: INSET, flexDirection: "row", zIndex: 2 },
1953
+ tag: { flexShrink: 1, height: HEIGHT, paddingHorizontal: 8, borderRadius: 6, backgroundColor: fixed.overlay, flexDirection: "row", alignItems: "center", gap: 5 },
1954
+ name: { flexShrink: 1 },
1955
+ you: { flexShrink: 0 },
1956
+ hand: { flexDirection: "row", alignItems: "center", gap: 2 },
1957
+ order: { ...type.caption, fontFamily: fontFamilies[700] }
1958
+ });
1959
+
1960
+ // src/stage/space-selector.ts
1961
+ import { useCallback as useCallback10, useRef as useRef9, useSyncExternalStore as useSyncExternalStore2 } from "react";
1962
+ function useSpaceSelector(select, isEqual = Object.is) {
1963
+ const client = useSpaceClient();
1964
+ const cache = useRef9(null);
1965
+ const subscribe = useCallback10((listener) => client.subscribe(listener), [client]);
1966
+ const getSnapshot = useCallback10(() => {
1967
+ const snapshot = client.getSnapshot();
1968
+ const cached = cache.current;
1969
+ if (cached && cached.snapshot === snapshot && cached.select === select) return cached.value;
1970
+ const next = select(snapshot);
1971
+ const value = cached && cached.select === select && isEqual(cached.value, next) ? cached.value : next;
1972
+ cache.current = { snapshot, select, value };
1973
+ return value;
1974
+ }, [client, isEqual, select]);
1975
+ return useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
1976
+ }
1977
+ function shallowEqual(a, b) {
1978
+ const keys = Object.keys(a);
1979
+ if (keys.length !== Object.keys(b).length) return false;
1980
+ return keys.every((key) => Object.is(Reflect.get(a, key), Reflect.get(b, key)));
1981
+ }
1982
+
1983
+ // src/stage/ContentTile.tsx
1984
+ import { Fragment as Fragment4, jsx as jsx23, jsxs as jsxs15 } from "react/jsx-runtime";
1985
+ var ContentTile = memo8(function ContentTile2({ content, inner, mini, onShowInSpotlight }) {
1986
+ const { colors } = useTheme();
1987
+ const tileStyle = [styles14.tile, { backgroundColor: colors.tile }];
1988
+ const tileRef = useMeasuredRef("tile.content", tileStyle);
1989
+ return /* @__PURE__ */ jsx23(View14, { ref: tileRef, style: tileStyle, testID: "tile.content", children: content.kind === "screen" ? /* @__PURE__ */ jsx23(ScreenShare, { inner, mini, onShowInSpotlight, sharerId: content.sharerId }) : /* @__PURE__ */ jsx23(Board, { mini, onShowInSpotlight }) });
1990
+ });
1991
+ function ScreenShare({ sharerId, inner, mini, onShowInSpotlight }) {
1992
+ const { colors } = useTheme();
1993
+ const select = useMemo8(() => (snapshot) => snapshot.media.remote.find((media) => media.participantId === sharerId && media.source === "screen")?.track ?? null, [sharerId]);
1994
+ const track = useSpaceSelector(select);
1995
+ const selectName = useMemo8(() => (snapshot) => snapshot.participants.roster.find((person) => person.participantId === sharerId)?.displayName ?? "", [sharerId]);
1996
+ const sharerName = useSpaceSelector(selectName);
1997
+ const slot = useDevScenario().screenSlots?.[sharerId];
1998
+ const streamURL = useMemo8(() => track ? createNativeMediaStream(track).toURL() : null, [track]);
1999
+ const name = `${sharerName.split(" ")[0]}\u2019s screen`;
2000
+ return /* @__PURE__ */ jsxs15(Fragment4, { children: [
2001
+ /* @__PURE__ */ jsx23(View14, { style: [styles14.box, { left: inner.x, top: inner.y, width: inner.width, height: inner.height, backgroundColor: colors.tile }], testID: "tile.content.screen", children: slot ?? (streamURL ? /* @__PURE__ */ jsx23(RtcVideoView, { objectFit: "contain", streamURL, style: StyleSheet14.absoluteFill }) : null) }),
2002
+ mini ? /* @__PURE__ */ jsx23(Pressable11, { accessibilityLabel: `Show ${name} in spotlight`, accessibilityRole: "button", onPress: onShowInSpotlight, style: StyleSheet14.absoluteFill, testID: "tile.content.open" }) : null,
2003
+ /* @__PURE__ */ jsx23(NameTag, { icon: "share", name, testID: "tile.content.name" })
2004
+ ] });
2005
+ }
2006
+ var selectBoard = (snapshot) => snapshot.whiteboard.engine.status;
2007
+ function Board({ mini, onShowInSpotlight }) {
2008
+ const client = useSpaceClient();
2009
+ const { colors, scheme } = useTheme();
2010
+ const status = useSpaceSelector(selectBoard);
2011
+ const chipStyle = [styles14.viewOnly, { backgroundColor: colors.surface, borderColor: colors.lineStrong }];
2012
+ const chipRef = useMeasuredRef("tile.content.view-only", chipStyle);
2013
+ const transport = useMemo8(() => client.whiteboard.transport(), [client, status]);
2014
+ const journeyId = useRef10(`board-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`).current;
2015
+ return /* @__PURE__ */ jsxs15(Fragment4, { children: [
2016
+ transport ? /* @__PURE__ */ jsx23(EmbeddedWhiteboard, { canClear: false, canDraw: false, journeyId, style: StyleSheet14.absoluteFill, testID: "tile.content.board", theme: scheme, transport }) : null,
2017
+ mini ? /* @__PURE__ */ jsxs15(Fragment4, { children: [
2018
+ /* @__PURE__ */ jsx23(Pressable11, { accessibilityLabel: "Show the board in spotlight", accessibilityRole: "button", onPress: onShowInSpotlight, style: StyleSheet14.absoluteFill, testID: "tile.content.open" }),
2019
+ /* @__PURE__ */ jsx23(NameTag, { icon: "pen", name: "Board", testID: "tile.content.name" })
2020
+ ] }) : /* @__PURE__ */ jsxs15(View14, { accessibilityLiveRegion: "polite", accessibilityRole: "text", accessible: true, ref: chipRef, style: chipStyle, testID: "tile.content.view-only", children: [
2021
+ /* @__PURE__ */ jsx23(Icon, { color: colors.textMuted, name: "lock", size: 16 }),
2022
+ /* @__PURE__ */ jsx23(ChalkText, { tone: "textSoft", variant: "section", children: "View only" })
2023
+ ] })
2024
+ ] });
2025
+ }
2026
+ var styles14 = StyleSheet14.create({
2027
+ tile: { flex: 1, overflow: "hidden", borderRadius: radii.tilePhone },
2028
+ box: { position: "absolute", overflow: "hidden" },
2029
+ // `.vchip`: a 30 pt pill 12 pt in from the board's top-left corner, with a one-pixel ring.
2030
+ viewOnly: { position: "absolute", left: 12, top: 12, height: 30, paddingHorizontal: 12, borderRadius: radii.round, borderWidth: 1, flexDirection: "row", alignItems: "center", gap: 6, zIndex: 3 }
2031
+ });
2032
+
2033
+ // src/stage/layout.ts
2034
+ var SOFT = [0.8, 2];
2035
+ var HARD = [0.6, 2.4];
2036
+ var clamp = (value, min, max) => Math.max(min, Math.min(max, value));
2037
+ var area = (frame) => frame.width * frame.height;
2038
+ function contain(box, aspect) {
2039
+ if (!aspect) return box;
2040
+ let width = box.width;
2041
+ let height = width / aspect;
2042
+ if (height > box.height) {
2043
+ height = box.height;
2044
+ width = height * aspect;
2045
+ }
2046
+ return { x: box.x + (box.width - width) / 2, y: box.y + (box.height - height) / 2, width, height };
2047
+ }
2048
+ function justifyOptions(portrait) {
2049
+ return portrait ? { t: 1, minW: 88, minH: 72 } : { t: 1.5, minW: 100, minH: 64 };
2050
+ }
2051
+ function justifyPlan(box, units, options, firstRowTwo) {
2052
+ let best = null;
2053
+ for (let rows = 1; rows <= units; rows++) {
2054
+ const base = Math.floor(units / rows);
2055
+ const extra = units % rows;
2056
+ if (!base) break;
2057
+ const counts = Array.from({ length: rows }, (_, index) => base + (index < extra ? 1 : 0));
2058
+ if (firstRowTwo && (counts[0] ?? 0) < 2) continue;
2059
+ const height = (box.height - (rows - 1) * STAGE_GAP) / rows;
2060
+ if (height <= 0) break;
2061
+ let cost = 0;
2062
+ let minWidth = Infinity;
2063
+ for (const count of counts) {
2064
+ const width = (box.width - (count - 1) * STAGE_GAP) / count;
2065
+ const aspect = width / height;
2066
+ minWidth = Math.min(minWidth, width);
2067
+ const hard = aspect < HARD[0] ? Math.log(HARD[0] / aspect) : aspect > HARD[1] ? Math.log(aspect / HARD[1]) : 0;
2068
+ const soft = aspect < SOFT[0] ? Math.log(SOFT[0] / aspect) : aspect > SOFT[1] ? Math.log(aspect / SOFT[1]) : 0;
2069
+ cost += count * (hard * 60 + soft * 3 + Math.abs(Math.log(aspect / options.t)));
2070
+ }
2071
+ cost /= units;
2072
+ if (!best || cost < best.cost) best = { counts, height, cost, minWidth };
2073
+ }
2074
+ return best;
2075
+ }
2076
+ function justifiedRows(box, list, options) {
2077
+ const weight = (key) => key !== "more" && key.kind === "content" ? 2 : 1;
2078
+ const contentFirst = list[0]?.kind === "content";
2079
+ const total = list.length;
2080
+ let keys = list;
2081
+ let plan = null;
2082
+ for (let kept = total; kept >= 1; kept--) {
2083
+ keys = kept === total ? list : [...list.slice(0, kept - 1), "more"];
2084
+ plan = justifyPlan(
2085
+ box,
2086
+ keys.reduce((sum, key) => sum + weight(key), 0),
2087
+ options,
2088
+ contentFirst
2089
+ );
2090
+ if (plan && plan.minWidth >= options.minW && plan.height >= options.minH || kept <= (contentFirst ? 2 : 1)) break;
2091
+ }
2092
+ if (!plan) return { placed: [] };
2093
+ let height = plan.height;
2094
+ for (const count of plan.counts) {
2095
+ const width = (box.width - (count - 1) * STAGE_GAP) / count;
2096
+ if (width / height < HARD[0]) height = Math.min(height, width / HARD[0]);
2097
+ }
2098
+ const top = box.y + (box.height - plan.counts.length * height - (plan.counts.length - 1) * STAGE_GAP) / 2;
2099
+ const placed = [];
2100
+ let index = 0;
2101
+ plan.counts.forEach((count, row) => {
2102
+ let unit = (box.width - (count - 1) * STAGE_GAP) / count;
2103
+ if (unit / height > HARD[1]) unit = height * HARD[1];
2104
+ let x = box.x + (box.width - count * unit - (count - 1) * STAGE_GAP) / 2;
2105
+ let used = 0;
2106
+ while (used < count && index < keys.length) {
2107
+ const key = keys[index];
2108
+ if (key === void 0) break;
2109
+ const units = weight(key);
2110
+ const frame = { x, y: top + row * (height + STAGE_GAP), width: unit * units + STAGE_GAP * (units - 1), height };
2111
+ placed.push(key === "more" ? { key, frame, more: total - keys.length + 1 } : { key, frame });
2112
+ x += frame.width + STAGE_GAP;
2113
+ used += units;
2114
+ index++;
2115
+ }
2116
+ });
2117
+ return { placed };
2118
+ }
2119
+ function fillOne(box, aspect) {
2120
+ let width = box.width;
2121
+ let height = box.height;
2122
+ if (width / height > aspect.max) width = height * aspect.max;
2123
+ else if (width / height < aspect.min) height = width / aspect.min;
2124
+ if (aspect.cap && width > aspect.cap) {
2125
+ height *= aspect.cap / width;
2126
+ width = aspect.cap;
2127
+ }
2128
+ return { x: box.x + (box.width - width) / 2, y: box.y + (box.height - height) / 2, width, height };
2129
+ }
2130
+ function stageLayout(input) {
2131
+ const { stage, portrait, content } = input;
2132
+ if (input.layout === "grid") {
2133
+ const rows = justifiedRows(stage, content ? [{ kind: "content" }, ...people(input.order)] : people(input.order), justifyOptions(portrait));
2134
+ return toItems(rows.placed, content, { miniContent: true });
2135
+ }
2136
+ if (content && input.pinned !== null) return spotlightPeople(input, [{ kind: "content" }, ...people(input.order)]);
2137
+ return content ? spotlightContent(input, content) : spotlightPeople(input, people(input.order));
2138
+ }
2139
+ function people(order) {
2140
+ return order.map((id) => ({ kind: "person", id }));
2141
+ }
2142
+ function spotlightContent(input, content) {
2143
+ const { stage, portrait } = input;
2144
+ const peopleMin = portrait ? content.kind === "board" ? clamp(Math.round(stage.height * 0.17), 96, 120) : Math.round(stage.height * 0.3) : clamp(stage.height * 0.17, 104, 150);
2145
+ const contentMin = portrait ? 120 : clamp(stage.width * 0.15, 180, 260);
2146
+ const belowBox = contain({ ...stage, height: stage.height - peopleMin - STAGE_GAP }, content.aspect);
2147
+ const below = { tile: { ...stage, height: belowBox.height }, rest: { x: stage.x, y: stage.y + belowBox.height + STAGE_GAP, width: stage.width, height: stage.height - belowBox.height - STAGE_GAP }, area: area(belowBox) };
2148
+ const besideBox = contain({ ...stage, width: stage.width - contentMin - STAGE_GAP }, content.aspect);
2149
+ const beside = { tile: { ...stage, width: besideBox.width }, rest: { x: stage.x + besideBox.width + STAGE_GAP, y: stage.y, width: stage.width - besideBox.width - STAGE_GAP, height: stage.height }, area: area(besideBox) };
2150
+ const best = beside.area > below.area * 1.005 ? beside : below;
2151
+ const rows = justifiedRows(best.rest, people(input.order), justifyOptions(portrait));
2152
+ const tile = { kind: "content", frame: best.tile, inner: contain({ x: 0, y: 0, width: best.tile.width, height: best.tile.height }, content.aspect), mini: false };
2153
+ return [tile, ...toItems(rows.placed, content, { miniContent: true })];
2154
+ }
2155
+ function spotlightPeople(input, list) {
2156
+ const { stage, portrait, content, pinned, spot } = input;
2157
+ const ids = list.flatMap((slot) => slot.kind === "person" ? [slot.id] : []);
2158
+ const main = content ? pinned : pinned !== null && ids.includes(pinned) ? pinned : spot !== null && ids.includes(spot) ? spot : ids[0] ?? null;
2159
+ if (main === null) return [];
2160
+ const rest = list.filter((slot) => slot.kind !== "person" || slot.id !== main);
2161
+ const first = rest[0];
2162
+ if (!first) return [{ kind: "person", id: main, frame: fillOne(stage, { min: portrait ? 0.4 : 4 / 3, max: 16 / 9, cap: portrait ? 0 : stage.width * 0.56 }), big: false, float: false }];
2163
+ if (rest.length === 1) {
2164
+ const size = portrait ? { width: 96, height: 128 } : { width: 132, height: 96 };
2165
+ const y = portrait ? stage.y + 12 : stage.y + stage.height - size.height - 12;
2166
+ const floating = { x: stage.x + stage.width - size.width - 12, y, ...size };
2167
+ return [{ kind: "person", id: main, frame: stage, big: false, float: false }, ...toItems([{ key: first, frame: floating }], content, { miniContent: true, float: true })];
2168
+ }
2169
+ const peopleMin = portrait ? Math.round(stage.height * 0.3) : clamp(stage.height * 0.17, 104, 150);
2170
+ const sideMin = clamp(stage.width * 0.15, 180, 260);
2171
+ const off = (frame) => Math.abs(Math.log(frame.width / frame.height / (16 / 9)));
2172
+ const below = { main: { ...stage, height: stage.height - peopleMin - STAGE_GAP }, rest: { x: stage.x, y: stage.y + stage.height - peopleMin, width: stage.width, height: peopleMin } };
2173
+ const beside = { main: { ...stage, width: stage.width - sideMin - STAGE_GAP }, rest: { x: stage.x + stage.width - sideMin, y: stage.y, width: sideMin, height: stage.height } };
2174
+ const chosen = portrait || off(below.main) <= off(beside.main) ? below : beside;
2175
+ return [{ kind: "person", id: main, frame: chosen.main, big: true, float: false }, ...toItems(justifiedRows(chosen.rest, rest, justifyOptions(portrait)).placed, content, { miniContent: true })];
2176
+ }
2177
+ function toItems(placed, content, options) {
2178
+ return placed.map(({ key, frame, more }) => {
2179
+ if (key === "more") return { kind: "more", frame, count: more ?? 0 };
2180
+ if (key.kind === "person") return { kind: "person", id: key.id, frame, big: false, float: options.float ?? false };
2181
+ return { kind: "content", frame, inner: contain({ x: 0, y: 0, width: frame.width, height: frame.height }, content?.aspect ?? null), mini: options.miniContent };
2182
+ });
2183
+ }
2184
+ function nextStageOrder(previous, present, selfId, speaker) {
2185
+ const order = previous.order.filter((id) => present.includes(id));
2186
+ for (const id of present) if (!order.includes(id)) order.push(id);
2187
+ let spot = previous.spot !== null && present.includes(previous.spot) ? previous.spot : present.find((id) => id !== selfId) ?? present[0] ?? null;
2188
+ if (speaker !== null && speaker !== selfId && speaker !== spot && present.includes(speaker)) {
2189
+ const from = spot === null ? -1 : order.indexOf(spot);
2190
+ const to = order.indexOf(speaker);
2191
+ if (from >= 0 && to >= 0) [order[from], order[to]] = [speaker, order[from] ?? speaker];
2192
+ spot = speaker;
2193
+ }
2194
+ const unchanged = spot === previous.spot && order.length === previous.order.length && order.every((id, index) => id === previous.order[index]);
2195
+ return unchanged ? previous : { order, spot };
2196
+ }
2197
+
2198
+ // src/stage/ParticipantTile.tsx
2199
+ import { memo as memo10, useCallback as useCallback11, useEffect as useEffect14, useMemo as useMemo9 } from "react";
2200
+ import { Pressable as Pressable12, StyleSheet as StyleSheet16, View as View16 } from "react-native";
2201
+ import { useSharedValue as useSharedValue3, withTiming as withTiming5 } from "react-native-reanimated";
2202
+
2203
+ // src/avatar/tile-avatar-size.ts
2204
+ function tileAvatarSize(width, height) {
2205
+ return Math.round(Math.min(150, Math.max(32, Math.min(width, height) * 0.42)));
2206
+ }
2207
+
2208
+ // src/primitives/TileShell.tsx
2209
+ import { memo as memo9, useEffect as useEffect13 } from "react";
2210
+ import { StyleSheet as StyleSheet15, View as View15 } from "react-native";
2211
+ import Animated8, { useAnimatedStyle as useAnimatedStyle4, useSharedValue as useSharedValue2, withTiming as withTiming4 } from "react-native-reanimated";
2212
+ import { jsx as jsx24, jsxs as jsxs16 } from "react/jsx-runtime";
2213
+ var TileShell = memo9(function TileShell2({ children, speaking = false, cameraOn = false, level, testID }) {
2214
+ const { colors } = useTheme();
2215
+ const { phone } = useDeviceLayout();
2216
+ const radius = phone ? radii.tilePhone : radii.tile;
2217
+ const live = useSharedValue2(0);
2218
+ const showRing = speaking && cameraOn;
2219
+ useEffect13(() => {
2220
+ live.value = withTiming4(showRing ? 1 : 0, timings.scrim);
2221
+ }, [live, showRing]);
2222
+ const ringStyle = useAnimatedStyle4(() => {
2223
+ "worklet";
2224
+ return { opacity: live.value * (0.3 + 0.7 * (level ? level.value : 1)) };
2225
+ });
2226
+ return /* @__PURE__ */ jsxs16(View15, { style: [styles15.tile, { borderRadius: radius, backgroundColor: colors.tile }], testID, children: [
2227
+ children,
2228
+ /* @__PURE__ */ jsx24(Animated8.View, { pointerEvents: "none", style: [styles15.ring, { borderRadius: radius }, ringStyle], testID: testID ? `${testID}.ring` : void 0, children: /* @__PURE__ */ jsx24(View15, { style: [styles15.ringShade, { borderRadius: radius - 3 }] }) })
2229
+ ] });
2230
+ });
2231
+ var styles15 = StyleSheet15.create({
2232
+ tile: { flex: 1, overflow: "hidden" },
2233
+ // `inset 0 0 0 3px` white and `inset 0 0 0 4px` shade in the mockup: a 3 pt white band with a 1 pt shade inside it.
2234
+ ring: { ...StyleSheet15.absoluteFillObject, borderWidth: 3, borderColor: fixed.liveRing, zIndex: 5 },
2235
+ ringShade: { ...StyleSheet15.absoluteFillObject, borderWidth: 1, borderColor: fixed.liveRingShade }
2236
+ });
2237
+
2238
+ // src/stage/camera.ts
2239
+ var BACK = /back|rear|environment/i;
2240
+ function facingOf(device) {
2241
+ return device && BACK.test(device.label) ? "back" : "front";
2242
+ }
2243
+ function cameraFacing(media) {
2244
+ return facingOf(media.devices.cameras.find((camera) => camera.deviceId === media.selection.camera));
2245
+ }
2246
+ function otherCamera(media) {
2247
+ const current = cameraFacing(media);
2248
+ return media.devices.cameras.find((camera) => facingOf(camera) !== current) ?? null;
2249
+ }
2250
+
2251
+ // src/stage/ParticipantTile.tsx
2252
+ import { jsx as jsx25, jsxs as jsxs17 } from "react/jsx-runtime";
2253
+ var SELF_NAME_MIN_WIDTH = 120;
2254
+ function selectTile(id) {
2255
+ return (snapshot) => {
2256
+ const participant = snapshot.participants.roster.find((person) => person.participantId === id);
2257
+ const isSelf = snapshot.self.participantId === id;
2258
+ const { local, remote } = snapshot.media;
2259
+ const track = isSelf ? local.camera.state === "enabled" ? local.camera.track : null : participant?.media.camera === "active" ? remote.find((media) => media.participantId === id && media.source === "camera")?.track ?? null : null;
2260
+ return {
2261
+ name: participant?.displayName ?? snapshot.self.displayName ?? "",
2262
+ isSelf,
2263
+ muted: isSelf ? local.microphone.state !== "enabled" : participant?.media.microphone !== "active",
2264
+ handRaised: isSelf ? snapshot.self.handRaised : participant?.handRaised ?? false,
2265
+ speaking: participant?.presence.speaking ?? false,
2266
+ track,
2267
+ back: isSelf && cameraFacing(snapshot.media) === "back"
2268
+ };
2269
+ };
2270
+ }
2271
+ var ParticipantTile = memo10(function ParticipantTile2({ id, width, height, float, pinned, mirror, onOpen }) {
2272
+ const select = useMemo9(() => selectTile(id), [id]);
2273
+ const tile = useSpaceSelector(select, shallowEqual);
2274
+ const slot = useDevScenario().videoSlots?.[id];
2275
+ const level = useSharedValue3(0);
2276
+ useEffect14(() => {
2277
+ level.value = withTiming5(tile.speaking ? 0.6 : 0, timings.level);
2278
+ }, [level, tile.speaking]);
2279
+ const streamURL = useMemo9(() => tile.track ? createNativeMediaStream(tile.track).toURL() : null, [tile.track]);
2280
+ const video = tileVideo(slot, streamURL, tile.isSelf && !tile.back && mirror);
2281
+ const youOnly = tile.isSelf && (float || width < SELF_NAME_MIN_WIDTH);
2282
+ const name = youOnly ? "You" : tile.name;
2283
+ const you = tile.isSelf && !youOnly;
2284
+ const state = `${tile.muted ? ", microphone off" : ""}${tile.handRaised ? ", hand raised" : ""}`;
2285
+ const hitRef = useMeasuredRef(`tile.${id}.open`, StyleSheet16.absoluteFill);
2286
+ const label = `Options for ${tile.name}${tile.isSelf ? " (you)" : ""}${pinned ? ", pinned" : ""}${state}`;
2287
+ return /* @__PURE__ */ jsxs17(TileShell, { cameraOn: video !== null, level, speaking: tile.speaking, testID: `tile.${id}`, children: [
2288
+ video ?? /* @__PURE__ */ jsx25(View16, { style: styles16.center, children: /* @__PURE__ */ jsx25(FaceAvatar, { level, lookAround: true, name: tile.name, size: tileAvatarSize(width, height - nameTagZone), style: "bold" }) }),
2289
+ /* @__PURE__ */ jsx25(Pressable12, { accessibilityHint: "Opens a sheet", accessibilityLabel: label, accessibilityRole: "button", onPress: () => onOpen(id), ref: hitRef, style: StyleSheet16.absoluteFill, testID: `tile.${id}.open` }),
2290
+ /* @__PURE__ */ jsx25(NameTag, { handRaised: tile.handRaised, muted: tile.muted, name, pinned, testID: `tile.${id}.name`, you }),
2291
+ tile.isSelf && video !== null ? /* @__PURE__ */ jsx25(FlipCameraButton, { back: tile.back, float, testID: `tile.${id}.flip` }) : null
2292
+ ] });
2293
+ });
2294
+ function tileVideo(slot, streamURL, mirror) {
2295
+ if (slot !== void 0) return /* @__PURE__ */ jsx25(View16, { style: [StyleSheet16.absoluteFill, mirror ? styles16.mirrored : null], children: slot });
2296
+ if (streamURL) return /* @__PURE__ */ jsx25(RtcVideoView, { mirror, objectFit: "cover", streamURL, style: StyleSheet16.absoluteFill });
2297
+ return null;
2298
+ }
2299
+ var selectOtherCamera = (snapshot) => otherCamera(snapshot.media)?.deviceId ?? null;
2300
+ function FlipCameraButton({ back, float, testID }) {
2301
+ const client = useSpaceClient();
2302
+ const toasts = useToasts();
2303
+ const other = useSpaceSelector(selectOtherCamera);
2304
+ const flip = useCallback11(() => {
2305
+ if (other === null) return;
2306
+ client.media.selectCamera(other).catch(() => toasts.show({ kind: "error", dismissible: true, message: "Couldn't switch the camera." }));
2307
+ }, [client, other, toasts]);
2308
+ if (other === null) return null;
2309
+ return /* @__PURE__ */ jsx25(Pressable12, { accessibilityLabel: `Switch to the ${back ? "front" : "back"} camera`, accessibilityRole: "button", onPress: flip, style: [styles16.flip, float ? styles16.flipFloat : null], testID, children: ({ pressed }) => /* @__PURE__ */ jsx25(View16, { style: [styles16.flipKnob, { backgroundColor: pressed ? fixed.overlayPressed : fixed.overlay }, pressed ? styles16.flipPressed : null], children: /* @__PURE__ */ jsx25(Icon, { color: fixed.overlayText, name: "flip", size: 17 }) }) });
2310
+ }
2311
+ var styles16 = StyleSheet16.create({
2312
+ // Centred above the name tag, so a short strip tile never hides the face behind the tag.
2313
+ center: { ...StyleSheet16.absoluteFillObject, bottom: nameTagZone, alignItems: "center", justifyContent: "center" },
2314
+ mirrored: { transform: [{ scaleX: -1 }] },
2315
+ flip: { position: "absolute", top: 2, right: 2, width: 44, height: 44, alignItems: "center", justifyContent: "center", zIndex: 4 },
2316
+ flipFloat: { top: 0, right: 0 },
2317
+ flipKnob: { width: 32, height: 32, borderRadius: 16, alignItems: "center", justifyContent: "center" },
2318
+ flipPressed: { transform: [{ scale: pressScale.small }] }
2319
+ });
2320
+
2321
+ // src/stage/TileSheet.tsx
2322
+ import { useCallback as useCallback12, useMemo as useMemo10 } from "react";
2323
+ import { StyleSheet as StyleSheet17, View as View17 } from "react-native";
2324
+ import { jsx as jsx26, jsxs as jsxs18 } from "react/jsx-runtime";
2325
+ var closed = { name: "", isSelf: false, cameraOn: false, back: false, otherCamera: null, muted: false, canMute: false, canRemove: false };
2326
+ function TileSheet({ participantId, open, onClose }) {
2327
+ const client = useSpaceClient();
2328
+ const toasts = useToasts();
2329
+ const view = useStageView();
2330
+ const select = useMemo10(
2331
+ () => (snapshot) => {
2332
+ if (participantId === null) return closed;
2333
+ const person2 = snapshot.participants.roster.find((participant) => participant.participantId === participantId);
2334
+ const isSelf = snapshot.self.participantId === participantId;
2335
+ return {
2336
+ name: person2?.displayName ?? snapshot.self.displayName ?? "",
2337
+ isSelf,
2338
+ cameraOn: isSelf && snapshot.media.local.camera.state === "enabled",
2339
+ back: cameraFacing(snapshot.media) === "back",
2340
+ otherCamera: otherCamera(snapshot.media)?.deviceId ?? null,
2341
+ muted: person2?.media.microphone !== "active",
2342
+ canMute: !isSelf && snapshot.self.can("muteOthers"),
2343
+ canRemove: !isSelf && snapshot.self.can("removeParticipant")
2344
+ };
2345
+ },
2346
+ [participantId]
2347
+ );
2348
+ const person = useSpaceSelector(select, shallowEqual);
2349
+ const id = participantId ?? "";
2350
+ const failed = useCallback12((message2) => () => toasts.show({ kind: "error", dismissible: true, message: message2 }), [toasts]);
2351
+ const run = (action) => () => {
2352
+ onClose();
2353
+ action();
2354
+ };
2355
+ return /* @__PURE__ */ jsxs18(BottomSheet, { accessibilityLabel: `${person.name}${person.isSelf ? " (You)" : ""}`, onClose, open: open && participantId !== null, testID: "sheet.tile", children: [
2356
+ /* @__PURE__ */ jsxs18(View17, { style: styles17.header, children: [
2357
+ /* @__PURE__ */ jsx26(FaceAvatar, { name: person.name, size: 32 }),
2358
+ /* @__PURE__ */ jsx26(ChalkText, { numberOfLines: 1, style: styles17.name, variant: "sheetTitle", children: `${person.name}${person.isSelf ? " (You)" : ""}` })
2359
+ ] }),
2360
+ /* @__PURE__ */ jsxs18(SheetGroup, { children: [
2361
+ /* @__PURE__ */ jsx26(SheetRow, { icon: "pin", label: view.pinned === participantId ? "Unpin" : "Pin", onPress: run(() => view.togglePin(id)), testID: "tile-sheet.pin" }),
2362
+ person.cameraOn && person.otherCamera !== null ? /* @__PURE__ */ jsx26(
2363
+ SheetRow,
2364
+ {
2365
+ icon: "flip",
2366
+ label: `Switch to the ${person.back ? "front" : "back"} camera`,
2367
+ onPress: run(() => {
2368
+ if (person.otherCamera) client.media.selectCamera(person.otherCamera).then(() => announce(`Switched to the ${person.back ? "front" : "back"} camera`), failed("Couldn't switch the camera."));
2369
+ }),
2370
+ testID: "tile-sheet.flip"
2371
+ }
2372
+ ) : null,
2373
+ person.cameraOn && !person.back ? /* @__PURE__ */ jsx26(SheetRow, { end: /* @__PURE__ */ jsx26(Switch, { accessibilityLabel: "Mirror my video", onValueChange: view.setMirror, testID: "tile-sheet.mirror", value: view.mirror }), icon: "swap", label: "Mirror my video" }) : null,
2374
+ person.canMute && !person.muted ? /* @__PURE__ */ jsx26(SheetRow, { icon: "micOff", label: "Mute", onPress: run(() => client.participants.mute(id).catch(failed(`Couldn't mute ${person.name}.`))), testID: "tile-sheet.mute" }) : null,
2375
+ person.canRemove ? /* @__PURE__ */ jsx26(
2376
+ SheetRow,
2377
+ {
2378
+ danger: true,
2379
+ icon: "x",
2380
+ label: "Remove from call",
2381
+ onPress: run(
2382
+ () => client.participants.remove(id).then(() => announce(`${person.name} was removed`)).catch(failed(`Couldn't remove ${person.name}.`))
2383
+ ),
2384
+ testID: "tile-sheet.remove"
2385
+ }
2386
+ ) : null
2387
+ ] })
2388
+ ] });
2389
+ }
2390
+ var styles17 = StyleSheet17.create({
2391
+ // `.shd`: the person's face and name over the rows.
2392
+ header: { flexDirection: "row", alignItems: "center", gap: 12, paddingTop: 4, paddingHorizontal: 8, paddingBottom: 14 },
2393
+ name: { flexShrink: 1 }
2394
+ });
2395
+
2396
+ // src/stage/Stage.tsx
2397
+ import { jsx as jsx27, jsxs as jsxs19 } from "react/jsx-runtime";
2398
+ function selectStructure(snapshot) {
2399
+ const selfId = snapshot.self.participantId;
2400
+ const { roster } = snapshot.participants;
2401
+ const others = roster.filter((person) => person.participantId !== selfId).map((person) => person.participantId);
2402
+ const hasSelf = roster.some((person) => person.participantId === selfId);
2403
+ return {
2404
+ selfId,
2405
+ present: (hasSelf && selfId !== null ? [selfId, ...others] : others).join("\n"),
2406
+ speaker: roster.find((person) => person.presence.activeSpeaker)?.participantId ?? null,
2407
+ sharer: roster.find((person) => person.participantId !== selfId && person.media.screenShare === "active")?.participantId ?? null,
2408
+ board: snapshot.whiteboard.open
2409
+ };
2410
+ }
2411
+ var Stage = memo11(function Stage2({ frame, onOpenPeople }) {
2412
+ const structure = useSpaceSelector(selectStructure, shallowEqual);
2413
+ const view = useStageView();
2414
+ const { arrangement } = useDeviceLayout();
2415
+ const { shareAspect } = useDevScenario();
2416
+ const [sheet, setSheet] = useState12({ id: null, open: false });
2417
+ const present = useMemo11(() => structure.present ? structure.present.split("\n") : [], [structure.present]);
2418
+ const [order, setOrder] = useState12({ order: [], spot: null });
2419
+ const next = nextStageOrder(order, present, structure.selfId, structure.speaker);
2420
+ if (next !== order) setOrder(next);
2421
+ const content = useMemo11(() => structure.board ? { kind: "board" } : structure.sharer !== null ? { kind: "screen", sharerId: structure.sharer } : null, [structure.board, structure.sharer]);
2422
+ const pinned = view.pinned !== null && present.includes(view.pinned) ? view.pinned : null;
2423
+ const items = useMemo11(
2424
+ () => stageLayout({
2425
+ stage: frame,
2426
+ // The iPad uses the phone layout today.
2427
+ portrait: arrangement !== "landscape",
2428
+ layout: view.layout,
2429
+ order: next.order,
2430
+ spot: next.spot,
2431
+ pinned,
2432
+ content: content ? { kind: content.kind, aspect: content.kind === "board" ? null : shareAspect ?? 16 / 9 } : null
2433
+ }),
2434
+ [arrangement, content, frame, next, pinned, shareAspect, view.layout]
2435
+ );
2436
+ const openTile = useCallback13((id) => setSheet({ id, open: true }), []);
2437
+ const closeTile = useCallback13(() => setSheet((current) => ({ ...current, open: false })), []);
2438
+ const stageRef = useMeasuredRef("frame.stage", styles18.layer);
2439
+ return /* @__PURE__ */ jsxs19(View18, { pointerEvents: "box-none", ref: stageRef, style: styles18.layer, testID: "frame.stage", children: [
2440
+ items.map((item) => /* @__PURE__ */ jsx27(StageTile, { content, item, mirror: view.mirror, onOpenPeople, onOpenTile: openTile, onShowContent: view.showContent, peopleCount: present.length, pinned }, itemKey(item))),
2441
+ /* @__PURE__ */ jsx27(TileSheet, { onClose: closeTile, open: sheet.open, participantId: sheet.id })
2442
+ ] });
2443
+ });
2444
+ function itemKey(item) {
2445
+ return item.kind === "person" ? `person:${item.id}` : item.kind;
2446
+ }
2447
+ function StageTile({ item, content, pinned, mirror, peopleCount, onOpenTile, onOpenPeople, onShowContent }) {
2448
+ const { scheme } = useTheme();
2449
+ const float = item.kind === "person" && item.float;
2450
+ return /* @__PURE__ */ jsx27(MovingFrame, { frame: item.frame, style: float ? [styles18.float, { boxShadow: elevations[scheme].floatTile }] : null, children: /* @__PURE__ */ jsx27(Animated9.View, { entering: tileEntering, exiting: tileLeaving, style: styles18.fill, children: item.kind === "person" ? /* @__PURE__ */ jsx27(ParticipantTile, { float, height: item.frame.height, id: item.id, mirror, onOpen: onOpenTile, pinned: pinned === item.id, width: item.frame.width }) : item.kind === "content" && content ? /* @__PURE__ */ jsx27(ContentTile, { content, inner: item.inner, mini: item.mini, onShowInSpotlight: onShowContent }) : item.kind === "more" ? /* @__PURE__ */ jsx27(MoreTile, { count: item.count, onPress: onOpenPeople, peopleCount }) : null }) });
2451
+ }
2452
+ var MoreTile = memo11(function MoreTile2({ count, peopleCount, onPress }) {
2453
+ const { colors } = useTheme();
2454
+ const restStyle = [styles18.more, { backgroundColor: colors.raised }];
2455
+ const moreRef = useMeasuredRef("tile.more", restStyle);
2456
+ return /* @__PURE__ */ jsx27(Pressable13, { accessibilityLabel: `Show all ${peopleCount} people`, accessibilityRole: "button", onPress, ref: moreRef, style: ({ pressed }) => pressed ? [styles18.more, { backgroundColor: colors.hover }] : restStyle, testID: "tile.more", children: /* @__PURE__ */ jsx27(ChalkText, { style: styles18.moreText, children: `+${count}` }) });
2457
+ });
2458
+ var styles18 = StyleSheet18.create({
2459
+ fill: { flex: 1 },
2460
+ layer: StyleSheet18.absoluteFillObject,
2461
+ float: { zIndex: 12, borderRadius: radii.tilePhone },
2462
+ more: { flex: 1, borderRadius: radii.tilePhone, alignItems: "center", justifyContent: "center" },
2463
+ moreText: { fontFamily: fontFamilies[600], fontSize: 22, lineHeight: 22 * 1.4 }
2464
+ });
2465
+
2466
+ // src/status/CallOverlays.tsx
2467
+ import { useEffect as useEffect17 } from "react";
2468
+ import { StyleSheet as StyleSheet21, View as View20 } from "react-native";
2469
+
2470
+ // src/limbo/Veil.tsx
2471
+ import { useEffect as useEffect15 } from "react";
2472
+ import { StyleSheet as StyleSheet19 } from "react-native";
2473
+ import Animated10, { ReduceMotion as ReduceMotion2, useAnimatedStyle as useAnimatedStyle5, useSharedValue as useSharedValue4, withTiming as withTiming6 } from "react-native-reanimated";
2474
+ import { jsx as jsx28 } from "react/jsx-runtime";
2475
+ var OPACITY = { none: 0, limbo: 0.85, paused: 0.75 };
2476
+ var LIFT_FROM = 0.6;
2477
+ function Veil({ level }) {
2478
+ const { colors } = useTheme();
2479
+ const opacity = useSharedValue4(OPACITY[level]);
2480
+ useEffect15(() => {
2481
+ if (level !== "none") {
2482
+ opacity.value = OPACITY[level];
2483
+ return;
2484
+ }
2485
+ if (opacity.value === 0) return;
2486
+ opacity.value = LIFT_FROM;
2487
+ opacity.value = withTiming6(0, { duration: durations.veil, easing: easings.easeOut, reduceMotion: ReduceMotion2.System });
2488
+ }, [level, opacity]);
2489
+ const style = useAnimatedStyle5(() => {
2490
+ "worklet";
2491
+ return { opacity: opacity.value };
2492
+ });
2493
+ return /* @__PURE__ */ jsx28(Animated10.View, { pointerEvents: level === "none" ? "none" : "auto", style: [StyleSheet19.absoluteFill, { backgroundColor: colors.canvas }, style], testID: "limbo.veil" });
2494
+ }
2495
+
2496
+ // src/status/StatusPage.tsx
2497
+ import { forwardRef as forwardRef3, memo as memo12, useEffect as useEffect16, useRef as useRef11 } from "react";
2498
+ import { AccessibilityInfo as AccessibilityInfo2, findNodeHandle as findNodeHandle2, Pressable as Pressable14, StyleSheet as StyleSheet20, View as View19 } from "react-native";
2499
+ import Animated11, { useAnimatedStyle as useAnimatedStyle6 } from "react-native-reanimated";
2500
+ import { jsx as jsx29, jsxs as jsxs20 } from "react/jsx-runtime";
2501
+ function pageFor(outcome, callName) {
2502
+ switch (outcome) {
2503
+ case "left":
2504
+ return { title: `You left ${callName}`, actions: ["rejoin", "home"] };
2505
+ case "ended":
2506
+ return { title: "The call ended", actions: ["rejoin", "home"] };
2507
+ case "failed":
2508
+ return { title: `Couldn\u2019t connect to ${callName}`, detail: "Check your internet connection, then try again.", actions: ["rejoin", "home"], rejoinLabel: "Try again" };
2509
+ case "removed":
2510
+ return { title: `You were removed from ${callName}`, detail: "The host removed you from this call.", actions: ["home"] };
2511
+ case "exited":
2512
+ return { title: `You\u2019re no longer in ${callName}`, actions: ["rejoin", "home"] };
2513
+ case "full":
2514
+ return { title: "This call is full", detail: `${callName} has reached 100 people. Ask the host to make room, then try again.`, actions: ["rejoin", "home"], rejoinLabel: "Try again" };
2515
+ }
2516
+ }
2517
+ function StatusPage({ outcome, callName, onRejoin, onHome }) {
2518
+ const { colors } = useTheme();
2519
+ const page = pageFor(outcome, callName);
2520
+ const pageStyle = [styles19.page, { backgroundColor: colors.canvas }];
2521
+ const ref = useMeasuredRef("status.page", pageStyle);
2522
+ const firstAction = useRef11(null);
2523
+ useEffect16(() => {
2524
+ const node = firstAction.current && findNodeHandle2(firstAction.current);
2525
+ if (node) AccessibilityInfo2.setAccessibilityFocus(node);
2526
+ }, [outcome]);
2527
+ return /* @__PURE__ */ jsxs20(View19, { accessibilityViewIsModal: true, ref, style: pageStyle, testID: "status.page", children: [
2528
+ /* @__PURE__ */ jsx29(ChalkText, { accessibilityRole: "header", style: styles19.center, testID: "status.title", variant: "statusTitle", children: page.title }),
2529
+ page.detail ? /* @__PURE__ */ jsx29(ChalkText, { style: styles19.center, testID: "status.detail", tone: "textMuted", children: page.detail }) : null,
2530
+ /* @__PURE__ */ jsx29(View19, { style: styles19.actions, children: page.actions.map(
2531
+ (action, index) => action === "rejoin" ? /* @__PURE__ */ jsx29(StatusAction, { icon: "retry", label: page.rejoinLabel ?? "Rejoin", onPress: onRejoin, ref: index === 0 ? firstAction : void 0, testID: "status.rejoin" }, action) : /* @__PURE__ */ jsx29(StatusAction, { icon: "home", label: "Home", onPress: onHome, ref: index === 0 ? firstAction : void 0, testID: "status.home" }, action)
2532
+ ) })
2533
+ ] });
2534
+ }
2535
+ var StatusAction = memo12(
2536
+ forwardRef3(function StatusAction2({ icon, label, onPress, testID }, ref) {
2537
+ const { colors } = useTheme();
2538
+ const press = usePressAnimation("round");
2539
+ const { down } = press;
2540
+ const roundStyle = [styles19.round, { backgroundColor: colors.raised }];
2541
+ const roundRef = useMeasuredRef(`${testID}.button`, roundStyle);
2542
+ const raised = colors.raised;
2543
+ const pressed = colors.pressed;
2544
+ const background = useAnimatedStyle6(() => {
2545
+ "worklet";
2546
+ return { backgroundColor: down.value ? pressed : raised };
2547
+ });
2548
+ return /* @__PURE__ */ jsxs20(Pressable14, { accessibilityLabel: label, accessibilityRole: "button", onPress, onPressIn: press.onPressIn, onPressOut: press.onPressOut, ref, style: styles19.action, testID, children: [
2549
+ /* @__PURE__ */ jsx29(Animated11.View, { ref: roundRef, style: [roundStyle, background, press.scaleStyle], children: /* @__PURE__ */ jsx29(Icon, { color: colors.text, name: icon }) }),
2550
+ /* @__PURE__ */ jsx29(ChalkText, { style: styles19.actionLabel, testID: `${testID}.label`, tone: "textSoft", variant: "meta", children: label })
2551
+ ] });
2552
+ })
2553
+ );
2554
+ var styles19 = StyleSheet20.create({
2555
+ page: { ...StyleSheet20.absoluteFillObject, alignItems: "center", justifyContent: "center", gap: 12, padding: 24 },
2556
+ center: { textAlign: "center" },
2557
+ actions: { flexDirection: "row", gap: 28, marginTop: 20 },
2558
+ action: { alignItems: "center", gap: 8 },
2559
+ round: { width: 56, height: 56, borderRadius: 28, alignItems: "center", justifyContent: "center" },
2560
+ // `.act` is 13 pt at weight 500; the type scale has 13 pt only at 400 and 600.
2561
+ actionLabel: { fontFamily: fontFamilies[500] }
2562
+ });
2563
+
2564
+ // src/status/CallOverlays.tsx
2565
+ import { jsx as jsx30, jsxs as jsxs21 } from "react/jsx-runtime";
2566
+ function CallOverlays({ screen, callName, rejoining, onHold, onRejoin, onHome }) {
2567
+ const recording2 = useRecording().current?.status === "recording";
2568
+ const queuedMessages = useChat().pendingSends.filter((send) => send.status === "queued").length;
2569
+ useBackHandler(screen.kind === "status", onHome);
2570
+ const { clear } = useToasts();
2571
+ const clearsNotices = screen.kind === "status" || screen.kind === "joining";
2572
+ useEffect17(() => {
2573
+ if (clearsNotices) clear();
2574
+ }, [clear, clearsNotices]);
2575
+ const limbo = screen.kind === "joining" ? { kind: "joining", rejoining } : screen.kind === "reconnecting" ? { kind: "reconnecting", queuedMessages } : screen.kind === "call" && onHold ? { kind: "hold" } : null;
2576
+ const veil = limbo === null ? "none" : limbo.kind === "joining" ? "limbo" : "paused";
2577
+ return /* @__PURE__ */ jsxs21(View20, { pointerEvents: "box-none", style: StyleSheet21.absoluteFill, children: [
2578
+ /* @__PURE__ */ jsx30(Veil, { level: screen.kind === "status" ? "none" : veil }),
2579
+ limbo ? /* @__PURE__ */ jsx30(Limbo, { callName, recording: recording2, state: limbo }) : null,
2580
+ screen.kind === "status" ? /* @__PURE__ */ jsx30(StatusPage, { callName, onHome, onRejoin, outcome: screen.outcome }) : null
2581
+ ] });
2582
+ }
2583
+
2584
+ // src/status/ChalkArrival.tsx
2585
+ import { StyleSheet as StyleSheet22, View as View21 } from "react-native";
2586
+ import { SafeAreaProvider } from "react-native-safe-area-context";
2587
+ import { jsx as jsx31 } from "react/jsx-runtime";
2588
+ function ChalkArrival({ colorScheme = "dark", ...props }) {
2589
+ return /* @__PURE__ */ jsx31(SafeAreaProvider, { children: /* @__PURE__ */ jsx31(ThemeProvider, { colorScheme, children: /* @__PURE__ */ jsx31(ArrivalScreen, { ...props }) }) });
2590
+ }
2591
+ function ArrivalScreen({ state, callName, onCancel, onRetry, recording: recording2 = false }) {
2592
+ const { colors } = useTheme();
2593
+ useBackHandler(true, onCancel);
2594
+ return /* @__PURE__ */ jsx31(View21, { style: [styles20.screen, { backgroundColor: colors.canvas }], testID: "arrival.screen", children: state === "full" ? /* @__PURE__ */ jsx31(StatusPage, { callName, onHome: onCancel, onRejoin: onRetry, outcome: "full" }) : /* @__PURE__ */ jsx31(Limbo, { callName, recording: recording2, state: state === "knocking" ? { kind: "knocking", onCancel } : { kind: "refused", onAskAgain: onRetry } }) });
2595
+ }
2596
+ var styles20 = StyleSheet22.create({
2597
+ screen: { flex: 1 }
2598
+ });
2599
+
2600
+ // src/status/call-screen.ts
2601
+ function callScreen(connection, leftByChoice) {
2602
+ if (connection.episodeEnded || connection.lastError?.code === "episode.ended") return { kind: "status", outcome: "ended" };
2603
+ switch (connection.status) {
2604
+ case "idle":
2605
+ case "joining":
2606
+ return { kind: "joining" };
2607
+ case "reconnecting":
2608
+ return { kind: "reconnecting" };
2609
+ case "live":
2610
+ case "leaving":
2611
+ return { kind: "call" };
2612
+ case "left":
2613
+ if (leftByChoice) return { kind: "status", outcome: "left" };
2614
+ return { kind: "status", outcome: connection.removedByHost ? "removed" : "exited" };
2615
+ case "failed":
2616
+ return { kind: "status", outcome: "failed" };
2617
+ }
2618
+ }
2619
+
2620
+ // src/toasts/CallEventToasts.tsx
2621
+ import { SpaceClientError } from "@q9labsai/chalk-client";
2622
+ import { useCallback as useCallback14, useEffect as useEffect18, useRef as useRef12 } from "react";
2623
+ import { Linking as Linking2, Platform as Platform7 } from "react-native";
2624
+
2625
+ // src/toasts/notices.ts
2626
+ function callNotices(previous, next, ownMicrophoneChange) {
2627
+ if (previous.connection.status !== "live" || next.connection.status !== "live") return [];
2628
+ const self = next.self.participantId;
2629
+ const before = new Map(previous.participants.roster.map((participant) => [participant.participantId, participant]));
2630
+ const after = new Map(next.participants.roster.map((participant) => [participant.participantId, participant]));
2631
+ const notices = [];
2632
+ const departed = [...before.values()].filter((participant) => !after.has(participant.participantId) && participant.participantId !== self);
2633
+ const becameHost = !previous.self.can("endEpisode") && next.self.can("endEpisode");
2634
+ const departedHost = becameHost ? departed.find(isHost) : void 0;
2635
+ if (departedHost) notices.push({ kind: "newHost", previousHost: firstName2(departedHost.displayName) });
2636
+ for (const participant of after.values()) if (!before.has(participant.participantId) && participant.participantId !== self) notices.push({ kind: "joined", name: participant.displayName });
2637
+ for (const participant of departed) if (participant !== departedHost) notices.push({ kind: "left", name: participant.displayName });
2638
+ for (const participant of after.values()) {
2639
+ if (participant.participantId === self) continue;
2640
+ if (before.get(participant.participantId)?.media.screenShare === "active" && participant.media.screenShare !== "active") notices.push({ kind: "shareStopped", name: firstName2(participant.displayName) });
2641
+ }
2642
+ if (previous.media.local.microphone.state === "enabled" && next.media.local.microphone.state === "disabled" && !ownMicrophoneChange) {
2643
+ const muters = next.participants.roster.filter((participant) => participant.participantId !== self && participant.capabilities.includes("muteOthers"));
2644
+ notices.push({ kind: "mutedByHost", by: muters.length === 1 && muters[0] ? firstName2(muters[0].displayName) : null });
2645
+ }
2646
+ const known = new Set(previous.media.incomingRequests.map((request) => request.requestId));
2647
+ for (const request of next.media.incomingRequests) if (!known.has(request.requestId)) notices.push({ kind: "mediaRequest", request });
2648
+ const wasRecording = previous.recording.current?.status === "recording";
2649
+ const isRecording = next.recording.current?.status === "recording";
2650
+ if (!wasRecording && isRecording) notices.push({ kind: "recordingStarted" });
2651
+ if (wasRecording && !isRecording) notices.push({ kind: "recordingStopped" });
2652
+ return notices;
2653
+ }
2654
+ function isHost(participant) {
2655
+ return participant.capabilities.includes("endEpisode");
2656
+ }
2657
+ function firstName2(name) {
2658
+ return name.trim().split(/\s+/)[0] ?? name;
2659
+ }
2660
+
2661
+ // src/toasts/CallEventToasts.tsx
2662
+ import { jsx as jsx32 } from "react/jsx-runtime";
2663
+ function useCallEventToasts({ onOpenSettings, resumedAfterSettings }) {
2664
+ const client = useSpaceClient();
2665
+ const toasts = useToasts();
2666
+ const { colors } = useTheme();
2667
+ const { soundsEnabled } = usePreferences();
2668
+ const ownMicrophoneChange = useRef12(false);
2669
+ const latest = useRef12({ colors, soundsEnabled });
2670
+ latest.current = { colors, soundsEnabled };
2671
+ useEffect18(() => {
2672
+ let previous = client.getSnapshot();
2673
+ return client.subscribe(() => {
2674
+ const next = client.getSnapshot();
2675
+ if (next === previous) return;
2676
+ const notices = callNotices(previous, next, ownMicrophoneChange.current);
2677
+ if (previous.media.local.microphone.state !== next.media.local.microphone.state) ownMicrophoneChange.current = false;
2678
+ previous = next;
2679
+ for (const notice of notices) {
2680
+ let id = 0;
2681
+ const { input, sound } = noticeToast(notice, { client, toasts, colors: latest.current.colors, dismiss: () => toasts.dismiss(id) });
2682
+ id = toasts.show(input);
2683
+ if (sound) playSound(sound, latest.current.soundsEnabled);
2684
+ }
2685
+ });
2686
+ }, [client, toasts]);
2687
+ useResumedAfterSettings(resumedAfterSettings, toasts);
2688
+ const noteOwnMicrophoneChange = useCallback14(() => {
2689
+ ownMicrophoneChange.current = true;
2690
+ }, []);
2691
+ const reportMediaFailure = useCallback14(
2692
+ (permission, cause, retry) => {
2693
+ const code = cause instanceof SpaceClientError ? cause.code : null;
2694
+ const icon = permission === "microphone" ? "micOff" : "camOff";
2695
+ const leading = /* @__PURE__ */ jsx32(Icon, { color: latest.current.colors.dangerText, name: icon });
2696
+ playSound("error", latest.current.soundsEnabled);
2697
+ if (code === "media.permission_denied") {
2698
+ const id = toasts.show({
2699
+ kind: "error",
2700
+ leading,
2701
+ wrap: true,
2702
+ dismissible: true,
2703
+ testID: `toast.${permission}-denied`,
2704
+ message: [{ strong: `Chalk can\u2019t use your ${permission}.` }, Platform7.OS === "ios" ? ` Allow it in Settings, then turn your ${permission} back on.` : " Allow it in Settings."],
2705
+ actions: [
2706
+ {
2707
+ label: "Open Settings",
2708
+ testID: "toast.open-settings",
2709
+ onPress: () => {
2710
+ toasts.dismiss(id);
2711
+ onOpenSettings?.(permission);
2712
+ void Linking2.openSettings();
2713
+ }
2714
+ }
2715
+ ]
2716
+ });
2717
+ return;
2718
+ }
2719
+ if (code === "media.device_busy") {
2720
+ const id = toasts.show({
2721
+ kind: "error",
2722
+ leading,
2723
+ wrap: true,
2724
+ dismissible: true,
2725
+ testID: `toast.${permission}-busy`,
2726
+ message: `Another app is using your ${permission}`,
2727
+ actions: [
2728
+ {
2729
+ label: "Try again",
2730
+ testID: "toast.try-again",
2731
+ onPress: () => {
2732
+ toasts.dismiss(id);
2733
+ retry();
2734
+ }
2735
+ }
2736
+ ]
2737
+ });
2738
+ return;
2739
+ }
2740
+ toasts.show({ kind: "error", leading, dismissible: true, wrap: true, message: cause instanceof Error ? `Couldn\u2019t turn on your ${permission}. ${cause.message}` : `Couldn\u2019t turn on your ${permission}.` });
2741
+ },
2742
+ [onOpenSettings, toasts]
2743
+ );
2744
+ return { noteOwnMicrophoneChange, reportMediaFailure };
2745
+ }
2746
+ function useResumedAfterSettings(permission, toasts) {
2747
+ const client = useSpaceClient();
2748
+ const { colors } = useTheme();
2749
+ const shown = useRef12(false);
2750
+ useEffect18(() => {
2751
+ if (!permission) return;
2752
+ const check = () => {
2753
+ if (shown.current || client.getSnapshot().connection.status !== "live") return;
2754
+ shown.current = true;
2755
+ toasts.show({ leading: /* @__PURE__ */ jsx32(Icon, { color: colors.textMuted, name: "check" }), message: `You\u2019re back in the call. Chalk can use your ${permission} now.`, testID: "toast.resumed" });
2756
+ };
2757
+ check();
2758
+ return client.subscribe(check);
2759
+ }, [client, colors, permission, toasts]);
2760
+ }
2761
+ function noticeToast(notice, { client, toasts, colors, dismiss }) {
2762
+ const icon = (name, tone = "muted") => /* @__PURE__ */ jsx32(Icon, { color: tone === "danger" ? colors.dangerText : colors.textMuted, name });
2763
+ const avatar = (name) => /* @__PURE__ */ jsx32(FaceAvatar, { name, size: 28 });
2764
+ switch (notice.kind) {
2765
+ case "joined":
2766
+ return { input: { leading: avatar(notice.name), message: [{ strong: notice.name }, " joined"], testID: "toast.joined" }, sound: "join" };
2767
+ case "left":
2768
+ return { input: { leading: avatar(notice.name), message: [{ strong: notice.name }, " left"], testID: "toast.left" }, sound: "leave" };
2769
+ case "mutedByHost":
2770
+ return {
2771
+ input: notice.by ? { leading: avatar(notice.by), message: [{ strong: notice.by }, " muted you. Unmute when you\u2019re ready."], testID: "toast.muted-by-host" } : { leading: icon("micOff"), message: "You were muted. Unmute when you\u2019re ready.", testID: "toast.muted-by-host" },
2772
+ sound: "mute"
2773
+ };
2774
+ case "mediaRequest": {
2775
+ const { request } = notice;
2776
+ const who = request.actorDisplayName ? firstName2(request.actorDisplayName) : "The host";
2777
+ const unmute2 = request.kind === "unmute";
2778
+ const accept = () => {
2779
+ dismiss();
2780
+ client.media.acceptRequest(request.requestId).catch(() => toasts.show({ kind: "error", dismissible: true, message: unmute2 ? "Couldn\u2019t unmute you." : "Couldn\u2019t turn on your camera." }));
2781
+ };
2782
+ return {
2783
+ input: {
2784
+ leading: icon(unmute2 ? "mic" : "cam"),
2785
+ message: unmute2 ? `${who} asks you to unmute` : `${who} asks you to turn on your camera`,
2786
+ testID: "toast.media-request",
2787
+ actions: [{ label: unmute2 ? "Unmute" : "Turn on", variant: "primary", testID: "toast.accept-request", onPress: accept }]
2788
+ }
2789
+ };
2790
+ }
2791
+ case "newHost":
2792
+ return { input: { leading: icon("users"), message: [{ strong: notice.previousHost }, " left. You\u2019re now the host."], testID: "toast.new-host" }, sound: "leave" };
2793
+ case "shareStopped":
2794
+ return { input: { leading: icon("shareOff"), message: [{ strong: notice.name }, " stopped sharing"], testID: "toast.share-stopped" } };
2795
+ case "recordingStarted":
2796
+ return { input: { leading: icon("rec", "danger"), message: [{ strong: "Recording started." }, " Everyone in the call can see it."], testID: "toast.recording-started" }, sound: "recording" };
2797
+ case "recordingStopped":
2798
+ return { input: { leading: icon("rec"), message: "Recording stopped", testID: "toast.recording-stopped" } };
2799
+ }
2800
+ }
2801
+
2802
+ // src/frame/CallFrame.tsx
2803
+ import { jsx as jsx33, jsxs as jsxs22 } from "react/jsx-runtime";
2804
+ function CallFrame({ displayName, callName = "Call", inviteLink, onLeave, onOpenSettings, onRejoin, resumedAfterSettings }) {
2805
+ const client = useSpaceClient();
2806
+ const connection = useConnection();
2807
+ const media = useMedia();
2808
+ const { colors, scheme } = useTheme();
2809
+ const layout = useDeviceLayout();
2810
+ const { soundsEnabled } = usePreferences();
2811
+ const toasts = useToasts();
2812
+ const callEvents = useCallEventToasts({ onOpenSettings, resumedAfterSettings });
2813
+ const [leftByChoice, setLeftByChoice] = useState13(false);
2814
+ const [sheet, setSheet] = useState13(null);
2815
+ const moreRef = useRef13(null);
2816
+ const leaveRef = useRef13(null);
2817
+ const nameRef = useRef13(null);
2818
+ const canEnd = useCan("endEpisode");
2819
+ const [leaveMenuOpen, setLeaveMenuOpen] = useState13(false);
2820
+ const sheetCount = usePortalCount("sheets");
2821
+ const menuCount = usePortalCount("menus");
2822
+ const layered = sheetCount > 0 || menuCount > 0;
2823
+ const regions = useMemo12(() => frameRegions(layout.arrangement, layout.width, layout.height, layout.insets), [layout]);
2824
+ const screenStyle = [styles21.root, { backgroundColor: colors.canvas }];
2825
+ const screenRef = useMeasuredRef("frame.screen", screenStyle);
2826
+ const join2 = useCallback15(() => {
2827
+ setLeftByChoice(false);
2828
+ client.join({ displayName, microphone: false, camera: false }).catch(() => void 0);
2829
+ }, [client, displayName]);
2830
+ useEffect19(() => {
2831
+ if (client.getSnapshot().connection.status === "idle") join2();
2832
+ }, [client, join2]);
2833
+ useAndroidCallService();
2834
+ const microphoneOn = media.local.microphone.state === "enabled";
2835
+ const cameraOn = media.local.camera.state === "enabled";
2836
+ const { noteOwnMicrophoneChange, reportMediaFailure } = callEvents;
2837
+ const toggleMicrophone = useCallback15(() => {
2838
+ haptics.mute();
2839
+ playSound(microphoneOn ? "mute" : "unmute", soundsEnabled);
2840
+ if (microphoneOn) noteOwnMicrophoneChange();
2841
+ const turnOn = () => client.media.setMicrophoneEnabled(true).catch((cause) => reportMediaFailure("microphone", cause, turnOn));
2842
+ if (microphoneOn) client.media.setMicrophoneEnabled(false).catch(() => toasts.show({ kind: "error", dismissible: true, message: "Couldn\u2019t mute your microphone." }));
2843
+ else void turnOn();
2844
+ }, [client, microphoneOn, noteOwnMicrophoneChange, reportMediaFailure, soundsEnabled, toasts]);
2845
+ const toggleCamera = useCallback15(() => {
2846
+ const turnOn = () => client.media.setCameraEnabled(true).catch((cause) => reportMediaFailure("camera", cause, turnOn));
2847
+ if (cameraOn) client.media.setCameraEnabled(false).catch(() => toasts.show({ kind: "error", dismissible: true, message: "Couldn\u2019t turn off your camera." }));
2848
+ else void turnOn();
2849
+ }, [cameraOn, client, reportMediaFailure, toasts]);
2850
+ const exit = useCallback15(
2851
+ (how) => {
2852
+ setSheet(null);
2853
+ setLeaveMenuOpen(false);
2854
+ setLeftByChoice(true);
2855
+ (how === "end" ? client.endEpisode() : client.leave()).catch(() => {
2856
+ toasts.show({ kind: "error", dismissible: true, message: how === "end" ? "Couldn\u2019t end the call." : "Couldn\u2019t leave the call." });
2857
+ });
2858
+ },
2859
+ [client, toasts]
2860
+ );
2861
+ const pressLeave = useCallback15(() => {
2862
+ haptics.leave();
2863
+ if (canEnd) setLeaveMenuOpen((open) => !open);
2864
+ else exit("leave");
2865
+ }, [canEnd, exit]);
2866
+ const devScenario = useDevScenario();
2867
+ useEffect19(() => {
2868
+ if (devScenario.sheet && devScenario.sheet !== "route") setSheet(devScenario.sheet);
2869
+ if (devScenario.menu === "leave") setLeaveMenuOpen(true);
2870
+ }, [devScenario]);
2871
+ const chrome = useLandscapeChrome(layout.arrangement === "landscape" && sheet === null && !layered);
2872
+ const forced = devScenario.callState;
2873
+ const screen = forced === "hold" ? { kind: "call" } : forced === "left" || forced === "ended" || forced === "failed" || forced === "removed" || forced === "exited" ? { kind: "status", outcome: forced } : callScreen(connection, leftByChoice);
2874
+ return /* @__PURE__ */ jsxs22(View22, { ref: screenRef, style: screenStyle, testID: "frame.screen", children: [
2875
+ /* @__PURE__ */ jsx33(StatusBar, { barStyle: scheme === "dark" ? "light-content" : "dark-content" }),
2876
+ /* @__PURE__ */ jsxs22(View22, { accessibilityElementsHidden: layered, importantForAccessibility: layered ? "no-hide-descendants" : "auto", style: StyleSheet23.absoluteFill, children: [
2877
+ /* @__PURE__ */ jsx33(Stage, { frame: regions.stage, onOpenPeople: () => setSheet("people") }),
2878
+ chrome.hidden ? /* @__PURE__ */ jsx33(Pressable15, { accessibilityLabel: "Show call controls", onPress: chrome.show, style: StyleSheet23.absoluteFill, testID: "frame.show-controls" }) : null,
2879
+ /* @__PURE__ */ jsx33(Animated12.View, { pointerEvents: chrome.hidden ? "none" : "box-none", style: [styles21.region, frameStyle(regions.header), chrome.style], children: /* @__PURE__ */ jsx33(Header, { arrangement: layout.arrangement, callName, detailsOpen: sheet === "details", onOpenDetails: () => setSheet("details"), ref: nameRef }) }),
2880
+ /* @__PURE__ */ jsx33(Animated12.View, { pointerEvents: chrome.hidden ? "none" : "box-none", style: [styles21.region, frameStyle(regions.dock), chrome.style], children: /* @__PURE__ */ jsx33(
2881
+ Dock,
2882
+ {
2883
+ arrangement: layout.arrangement,
2884
+ cameraOn,
2885
+ microphoneOn,
2886
+ leaveOpen: canEnd ? leaveMenuOpen : void 0,
2887
+ leaveRef,
2888
+ moreOpen: sheet === "more",
2889
+ moreRef,
2890
+ onLeave: pressLeave,
2891
+ onOpenMore: () => setSheet("more"),
2892
+ onToggleCamera: toggleCamera,
2893
+ onToggleMicrophone: toggleMicrophone
2894
+ }
2895
+ ) })
2896
+ ] }),
2897
+ /* @__PURE__ */ jsxs22(View22, { pointerEvents: "box-none", style: StyleSheet23.absoluteFill, testID: "frame.sheets", children: [
2898
+ /* @__PURE__ */ jsx33(MoreSheet, { inviteLink, onClose: () => setSheet(null), onOpenChat: () => setSheet("chat"), onOpenPeople: () => setSheet("people"), open: sheet === "more", returnFocusRef: sheet === null ? moreRef : void 0 }),
2899
+ /* @__PURE__ */ jsx33(CallDetailsSheet, { callName, inviteLink, onClose: () => setSheet(null), open: sheet === "details", returnFocusRef: nameRef }),
2900
+ /* @__PURE__ */ jsx33(PeopleChatSheet, { onClose: () => setSheet(null), onTabChange: setSheet, open: sheet === "people" || sheet === "chat", returnFocusRef: sheet === null ? moreRef : void 0, tab: sheet === "chat" ? "chat" : "people" }),
2901
+ /* @__PURE__ */ jsx33(PortalHost, { layer: "sheets" })
2902
+ ] }),
2903
+ /* @__PURE__ */ jsx33(View22, { pointerEvents: "box-none", style: StyleSheet23.absoluteFill, testID: "frame.overlays", children: forced === "knocking" || forced === "refused" || forced === "full" ? /* @__PURE__ */ jsx33(ArrivalScreen, { callName, onCancel: () => onLeave?.(), onRetry: join2, state: forced }) : /* @__PURE__ */ jsx33(CallOverlays, { callName, onHold: forced === "hold", onHome: () => onLeave?.(), onRejoin, rejoining: resumedAfterSettings !== void 0, screen }) }),
2904
+ /* @__PURE__ */ jsxs22(View22, { pointerEvents: "box-none", style: StyleSheet23.absoluteFill, testID: "frame.menus", children: [
2905
+ /* @__PURE__ */ jsx33(LeaveMenu, { anchorRef: leaveRef, onClose: () => setLeaveMenuOpen(false), onEnd: () => exit("end"), onLeave: () => exit("leave"), open: leaveMenuOpen }),
2906
+ /* @__PURE__ */ jsx33(PortalHost, { layer: "menus" })
2907
+ ] }),
2908
+ /* @__PURE__ */ jsx33(ToastStack, {}),
2909
+ /* @__PURE__ */ jsx33(ChatNotices, { chatVisible: sheet === "chat", onOpenChat: () => setSheet("chat"), sheetOpen: sheet === "people" || sheet === "chat" })
2910
+ ] });
2911
+ }
2912
+ function useLandscapeChrome(fades) {
2913
+ const [hidden, setHidden] = useState13(false);
2914
+ const [shownAt, setShownAt] = useState13(0);
2915
+ const opacity = useSharedValue5(1);
2916
+ useEffect19(() => {
2917
+ opacity.value = withTiming7(hidden ? 0 : 1, timings.fade);
2918
+ }, [hidden, opacity]);
2919
+ useEffect19(() => {
2920
+ if (!fades) {
2921
+ setHidden(false);
2922
+ return;
2923
+ }
2924
+ const timer = setTimeout(() => setHidden(true), durations.landscapeHide);
2925
+ return () => clearTimeout(timer);
2926
+ }, [fades, shownAt]);
2927
+ const show = useCallback15(() => {
2928
+ setHidden(false);
2929
+ setShownAt(Date.now());
2930
+ }, []);
2931
+ const style = useAnimatedStyle7(() => {
2932
+ "worklet";
2933
+ return { opacity: opacity.value };
2934
+ });
2935
+ return { hidden, show, style };
2936
+ }
2937
+ function frameStyle(frame) {
2938
+ return { left: frame.x, top: frame.y, width: frame.width, height: frame.height };
2939
+ }
2940
+ var styles21 = StyleSheet23.create({
2941
+ root: { flex: 1 },
2942
+ region: { position: "absolute" }
2943
+ });
2944
+
2945
+ // src/frame/Chalk.tsx
2946
+ import { jsx as jsx34 } from "react/jsx-runtime";
2947
+ function Chalk({ client, colorScheme, onColorSchemeChange, soundsEnabled, onSoundsEnabledChange, ...frame }) {
2948
+ useEffect20(() => releaseSounds, []);
2949
+ return /* @__PURE__ */ jsx34(GestureHandlerRootView, { style: styles22.root, children: /* @__PURE__ */ jsx34(SafeAreaProvider2, { children: /* @__PURE__ */ jsx34(PreferencesProvider, { colorScheme, onColorSchemeChange, onSoundsEnabledChange, soundsEnabled, children: (preferences) => /* @__PURE__ */ jsx34(ThemeProvider, { colorScheme: preferences.colorScheme, children: /* @__PURE__ */ jsx34(DeviceLayoutProvider, { children: /* @__PURE__ */ jsx34(PortalProvider, { children: /* @__PURE__ */ jsx34(ToastProvider, { children: /* @__PURE__ */ jsx34(ChalkProvider, { client, children: /* @__PURE__ */ jsx34(StageViewProvider, { children: /* @__PURE__ */ jsx34(CallFrame, { ...frame }) }) }) }) }) }) }) }) }) });
2950
+ }
2951
+ var styles22 = StyleSheet24.create({
2952
+ root: { flex: 1 }
2953
+ });
2954
+
2955
+ // src/utils/build-chalk-invite-link.ts
2956
+ var PUBLIC_INVITE_ORIGIN = "https://chalkmeet.com";
2957
+ function buildChalkInviteLink(spaceSlug, spaceInviteToken, origin = PUBLIC_INVITE_ORIGIN) {
2958
+ const link = new URL(`/space/${encodeURIComponent(spaceSlug)}`, origin.replace(/\/+$/u, ""));
2959
+ link.hash = new URLSearchParams({ spaceInviteToken }).toString();
2960
+ return link.toString();
2961
+ }
36
2962
  export {
37
2963
  Chalk,
2964
+ ChalkArrival,
38
2965
  ChalkProvider,
39
- Entrance,
40
- Logo,
41
- THEME_PALETTES,
42
- THEME_TEXTURES,
43
- Theme,
44
2966
  buildChalkInviteLink,
45
- createNativeTheme,
46
2967
  extractSpaceInviteFromLink,
47
2968
  getClipboardInviteSuggestion,
48
- getThemeMode,
49
- isDarkThemePalette,
50
2969
  parseSpaceInviteLink,
51
- resolveNativeAppearance,
52
2970
  useCan,
53
2971
  useChat,
54
2972
  useConnection,