@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.
- package/README.md +134 -29
- package/android/build.gradle +35 -0
- package/android/compatibility/ModuleApiCompatibility.java +215 -0
- package/android/src/main/AndroidManifest.xml +2 -1
- package/android/src/main/java/ai/q9labs/chalk/reactnative/ChalkSpaceForegroundService.java +105 -31
- package/android/src/main/kotlin/ai/q9labs/chalk/reactnative/ChalkAndroidConnectionServicePackage.kt +2 -0
- package/android/src/main/kotlin/ai/q9labs/chalk/reactnative/ChalkAudioRouteModule.kt +137 -0
- package/android/src/main/kotlin/ai/q9labs/chalk/reactnative/ChalkCallServiceModule.kt +32 -0
- package/android/src/main/kotlin/ai/q9labs/chalk/reactnative/CommunicationDeviceListener.kt +23 -0
- package/android/src/test/kotlin/ai/q9labs/chalk/compatibility/UnsafeSignatures.kt +29 -0
- package/dist/android/call-service.d.ts +4 -0
- package/dist/android/call-service.d.ts.map +1 -0
- package/dist/avatar/FaceAvatar.d.ts +19 -0
- package/dist/avatar/FaceAvatar.d.ts.map +1 -0
- package/dist/avatar/tile-avatar-size.d.ts +3 -0
- package/dist/avatar/tile-avatar-size.d.ts.map +1 -0
- package/dist/chunk-GYVZSOGQ.js +30 -0
- package/dist/chunk-IHE6EA3T.js +25 -0
- package/dist/chunk-OIBT2FNM.js +1479 -0
- package/dist/client.d.ts +1 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +185 -2
- package/dist/components/EmbeddedWhiteboard.d.ts.map +1 -1
- package/dist/components/native-media-visibility.d.ts +3 -2
- package/dist/components/native-media-visibility.d.ts.map +1 -1
- package/dist/components/useAndroidCallService.d.ts +8 -0
- package/dist/components/useAndroidCallService.d.ts.map +1 -0
- package/dist/dev.d.ts +10 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +10 -0
- package/dist/dock/Dock.d.ts +23 -0
- package/dist/dock/Dock.d.ts.map +1 -0
- package/dist/dock/LeaveMenu.d.ts +12 -0
- package/dist/dock/LeaveMenu.d.ts.map +1 -0
- package/dist/dock/MoreSheet.d.ts +17 -0
- package/dist/dock/MoreSheet.d.ts.map +1 -0
- package/dist/dock/ReactionBurst.d.ts +8 -0
- package/dist/dock/ReactionBurst.d.ts.map +1 -0
- package/dist/dock/RoutePicker.d.ts +16 -0
- package/dist/dock/RoutePicker.d.ts.map +1 -0
- package/dist/dock/audio-route.d.ts +25 -0
- package/dist/dock/audio-route.d.ts.map +1 -0
- package/dist/error-YOPTEH5N.wav +0 -0
- package/dist/frame/CallFrame.d.ts +17 -0
- package/dist/frame/CallFrame.d.ts.map +1 -0
- package/dist/frame/Chalk.d.ts +40 -0
- package/dist/frame/Chalk.d.ts.map +1 -0
- package/dist/frame/back-handler.d.ts +6 -0
- package/dist/frame/back-handler.d.ts.map +1 -0
- package/dist/frame/dev-scenario.d.ts +28 -0
- package/dist/frame/dev-scenario.d.ts.map +1 -0
- package/dist/frame/device-layout.d.ts +16 -0
- package/dist/frame/device-layout.d.ts.map +1 -0
- package/dist/frame/measurement.d.ts +28 -0
- package/dist/frame/measurement.d.ts.map +1 -0
- package/dist/frame/portal.d.ts +24 -0
- package/dist/frame/portal.d.ts.map +1 -0
- package/dist/frame/preferences.d.ts +21 -0
- package/dist/frame/preferences.d.ts.map +1 -0
- package/dist/frame/regions.d.ts +25 -0
- package/dist/frame/regions.d.ts.map +1 -0
- package/dist/hand-DFWVXPH2.wav +0 -0
- package/dist/haptics/haptics.d.ts +12 -0
- package/dist/haptics/haptics.d.ts.map +1 -0
- package/dist/header/CallDetailsSheet.d.ts +18 -0
- package/dist/header/CallDetailsSheet.d.ts.map +1 -0
- package/dist/header/Header.d.ts +17 -0
- package/dist/header/Header.d.ts.map +1 -0
- package/dist/header/QrCode.d.ts +6 -0
- package/dist/header/QrCode.d.ts.map +1 -0
- package/dist/icons/Icon.d.ts +12 -0
- package/dist/icons/Icon.d.ts.map +1 -0
- package/dist/icons/icon-element.d.ts +24 -0
- package/dist/icons/icon-element.d.ts.map +1 -0
- package/dist/icons/icon-paths.d.ts +530 -0
- package/dist/icons/icon-paths.d.ts.map +1 -0
- package/dist/index.d.ts +6 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2956 -38
- package/dist/join-Z6RL5AHE.wav +0 -0
- package/dist/knock-4VBQSHK4.wav +0 -0
- package/dist/leave-MQLZOWQD.wav +0 -0
- package/dist/limbo/Limbo.d.ts +25 -0
- package/dist/limbo/Limbo.d.ts.map +1 -0
- package/dist/limbo/Veil.d.ts +10 -0
- package/dist/limbo/Veil.d.ts.map +1 -0
- package/dist/media/native-capture-error.d.ts +7 -0
- package/dist/media/native-capture-error.d.ts.map +1 -0
- package/dist/message-6Y6WJGTB.wav +0 -0
- package/dist/motion/MovingFrame.d.ts +36 -0
- package/dist/motion/MovingFrame.d.ts.map +1 -0
- package/dist/motion/motion.d.ts +82 -0
- package/dist/motion/motion.d.ts.map +1 -0
- package/dist/motion/presence.d.ts +6 -0
- package/dist/motion/presence.d.ts.map +1 -0
- package/dist/motion/press.d.ts +15 -0
- package/dist/motion/press.d.ts.map +1 -0
- package/dist/mute-H5SJFXVI.wav +0 -0
- package/dist/primitives/BottomSheet.d.ts +36 -0
- package/dist/primitives/BottomSheet.d.ts.map +1 -0
- package/dist/primitives/ChalkText.d.ts +13 -0
- package/dist/primitives/ChalkText.d.ts.map +1 -0
- package/dist/primitives/CopyButton.d.ts +11 -0
- package/dist/primitives/CopyButton.d.ts.map +1 -0
- package/dist/primitives/IconButton.d.ts +26 -0
- package/dist/primitives/IconButton.d.ts.map +1 -0
- package/dist/primitives/Menu.d.ts +39 -0
- package/dist/primitives/Menu.d.ts.map +1 -0
- package/dist/primitives/NameTag.d.ts +19 -0
- package/dist/primitives/NameTag.d.ts.map +1 -0
- package/dist/primitives/PillButton.d.ts +19 -0
- package/dist/primitives/PillButton.d.ts.map +1 -0
- package/dist/primitives/SegmentedControl.d.ts +17 -0
- package/dist/primitives/SegmentedControl.d.ts.map +1 -0
- package/dist/primitives/SheetGroup.d.ts +24 -0
- package/dist/primitives/SheetGroup.d.ts.map +1 -0
- package/dist/primitives/Switch.d.ts +9 -0
- package/dist/primitives/Switch.d.ts.map +1 -0
- package/dist/primitives/TileShell.d.ts +18 -0
- package/dist/primitives/TileShell.d.ts.map +1 -0
- package/dist/primitives/announce.d.ts +3 -0
- package/dist/primitives/announce.d.ts.map +1 -0
- package/dist/primitives/menu-placement.d.ts +32 -0
- package/dist/primitives/menu-placement.d.ts.map +1 -0
- package/dist/primitives/toast/Toast.d.ts +8 -0
- package/dist/primitives/toast/Toast.d.ts.map +1 -0
- package/dist/primitives/toast/ToastStack.d.ts +18 -0
- package/dist/primitives/toast/ToastStack.d.ts.map +1 -0
- package/dist/primitives/toast/toast-store.d.ts +39 -0
- package/dist/primitives/toast/toast-store.d.ts.map +1 -0
- package/dist/recording-Q4GB77Y4.wav +0 -0
- package/dist/sheets/people-chat/ChatNotices.d.ts +14 -0
- package/dist/sheets/people-chat/ChatNotices.d.ts.map +1 -0
- package/dist/sheets/people-chat/ChatTab.d.ts +6 -0
- package/dist/sheets/people-chat/ChatTab.d.ts.map +1 -0
- package/dist/sheets/people-chat/Composer.d.ts +7 -0
- package/dist/sheets/people-chat/Composer.d.ts.map +1 -0
- package/dist/sheets/people-chat/PeopleChatSheet.d.ts +17 -0
- package/dist/sheets/people-chat/PeopleChatSheet.d.ts.map +1 -0
- package/dist/sheets/people-chat/PeopleTab.d.ts +3 -0
- package/dist/sheets/people-chat/PeopleTab.d.ts.map +1 -0
- package/dist/sheets/people-chat/PhotoViewer.d.ts +19 -0
- package/dist/sheets/people-chat/PhotoViewer.d.ts.map +1 -0
- package/dist/sheets/people-chat/TextField.d.ts +4 -0
- package/dist/sheets/people-chat/TextField.d.ts.map +1 -0
- package/dist/sheets/people-chat/attachments.d.ts +33 -0
- package/dist/sheets/people-chat/attachments.d.ts.map +1 -0
- package/dist/sheets/people-chat/chat-model.d.ts +64 -0
- package/dist/sheets/people-chat/chat-model.d.ts.map +1 -0
- package/dist/sheets/people-chat/people-model.d.ts +43 -0
- package/dist/sheets/people-chat/people-model.d.ts.map +1 -0
- package/dist/sounds/sounds.d.ts +33 -0
- package/dist/sounds/sounds.d.ts.map +1 -0
- package/dist/space-client/create-native-space-client.d.ts +0 -4
- package/dist/space-client/create-native-space-client.d.ts.map +1 -1
- package/dist/space-client/random-uuid.d.ts +7 -0
- package/dist/space-client/random-uuid.d.ts.map +1 -0
- package/dist/stage/ContentTile.d.ts +18 -0
- package/dist/stage/ContentTile.d.ts.map +1 -0
- package/dist/stage/ParticipantTile.d.ts +18 -0
- package/dist/stage/ParticipantTile.d.ts.map +1 -0
- package/dist/stage/Stage.d.ts +14 -0
- package/dist/stage/Stage.d.ts.map +1 -0
- package/dist/stage/TileSheet.d.ts +12 -0
- package/dist/stage/TileSheet.d.ts.map +1 -0
- package/dist/stage/camera.d.ts +7 -0
- package/dist/stage/camera.d.ts.map +1 -0
- package/dist/stage/layout.d.ts +57 -0
- package/dist/stage/layout.d.ts.map +1 -0
- package/dist/stage/space-selector.d.ts +10 -0
- package/dist/stage/space-selector.d.ts.map +1 -0
- package/dist/stage/stage-view.d.ts +23 -0
- package/dist/stage/stage-view.d.ts.map +1 -0
- package/dist/status/CallOverlays.d.ts +17 -0
- package/dist/status/CallOverlays.d.ts.map +1 -0
- package/dist/status/ChalkArrival.d.ts +24 -0
- package/dist/status/ChalkArrival.d.ts.map +1 -0
- package/dist/status/StatusPage.d.ts +14 -0
- package/dist/status/StatusPage.d.ts.map +1 -0
- package/dist/status/call-screen.d.ts +20 -0
- package/dist/status/call-screen.d.ts.map +1 -0
- package/dist/test-VD2KAM4D.wav +0 -0
- package/dist/theme/theme.d.ts +15 -0
- package/dist/theme/theme.d.ts.map +1 -0
- package/dist/theme/tokens.d.ts +248 -0
- package/dist/theme/tokens.d.ts.map +1 -0
- package/dist/toasts/CallEventToasts.d.ts +21 -0
- package/dist/toasts/CallEventToasts.d.ts.map +1 -0
- package/dist/toasts/notices.d.ts +34 -0
- package/dist/toasts/notices.d.ts.map +1 -0
- package/dist/ui/ChalkUI.d.ts +12 -0
- package/dist/ui/ChalkUI.d.ts.map +1 -0
- package/dist/ui.d.ts +25 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +77 -0
- package/dist/unmute-2XR2LICU.wav +0 -0
- package/embedded/chalk-whiteboard/asset-manifest.json +2 -2
- package/embedded/chalk-whiteboard/renderer.js +510 -510
- package/ios/ChalkAudioRouteModule.swift +93 -0
- package/ios/ChalkAudioRouteModuleBridge.m +9 -0
- package/package.json +46 -76
- package/src/android/call-service.ts +19 -0
- package/src/avatar/FaceAvatar.tsx +145 -0
- package/src/avatar/tile-avatar-size.ts +4 -0
- package/src/client.ts +1 -5
- package/src/components/EmbeddedWhiteboard.tsx +14 -71
- package/src/components/native-media-visibility.ts +1 -2
- package/src/components/useAndroidCallService.ts +41 -0
- package/src/dev.ts +9 -0
- package/src/dock/Dock.tsx +95 -0
- package/src/dock/LeaveMenu.tsx +21 -0
- package/src/dock/MoreSheet.tsx +233 -0
- package/src/dock/ReactionBurst.tsx +55 -0
- package/src/dock/RoutePicker.tsx +42 -0
- package/src/dock/audio-route.ts +69 -0
- package/src/frame/CallFrame.tsx +209 -0
- package/src/frame/Chalk.tsx +83 -0
- package/src/frame/back-handler.ts +19 -0
- package/src/frame/dev-scenario.ts +48 -0
- package/src/frame/device-layout.tsx +31 -0
- package/src/frame/measurement.ts +44 -0
- package/src/frame/portal.tsx +107 -0
- package/src/frame/preferences.tsx +48 -0
- package/src/frame/regions.test.ts +38 -0
- package/src/frame/regions.ts +60 -0
- package/src/haptics/haptics.ts +19 -0
- package/src/header/CallDetailsSheet.tsx +139 -0
- package/src/header/Header.tsx +107 -0
- package/src/header/QrCode.tsx +34 -0
- package/src/icons/Icon.tsx +35 -0
- package/src/icons/icon-element.ts +11 -0
- package/src/icons/icon-paths.ts +199 -0
- package/src/index.ts +6 -9
- package/src/limbo/Limbo.tsx +159 -0
- package/src/limbo/Veil.tsx +36 -0
- package/src/media/native-capture-error.test.ts +19 -0
- package/src/media/native-capture-error.ts +13 -0
- package/src/motion/MovingFrame.tsx +101 -0
- package/src/motion/motion.ts +33 -0
- package/src/motion/presence.ts +20 -0
- package/src/motion/press.ts +35 -0
- package/src/primitives/BottomSheet.tsx +211 -0
- package/src/primitives/ChalkText.tsx +23 -0
- package/src/primitives/CopyButton.tsx +78 -0
- package/src/primitives/IconButton.tsx +90 -0
- package/src/primitives/Menu.tsx +212 -0
- package/src/primitives/NameTag.tsx +68 -0
- package/src/primitives/PillButton.tsx +56 -0
- package/src/primitives/SegmentedControl.tsx +55 -0
- package/src/primitives/SheetGroup.tsx +72 -0
- package/src/primitives/Switch.tsx +39 -0
- package/src/primitives/TileShell.tsx +53 -0
- package/src/primitives/announce.ts +7 -0
- package/src/primitives/menu-placement.test.ts +50 -0
- package/src/primitives/menu-placement.ts +43 -0
- package/src/primitives/toast/Toast.tsx +50 -0
- package/src/primitives/toast/ToastStack.tsx +89 -0
- package/src/primitives/toast/toast-store.ts +86 -0
- package/src/sheets/people-chat/ChatNotices.tsx +69 -0
- package/src/sheets/people-chat/ChatTab.tsx +186 -0
- package/src/sheets/people-chat/Composer.tsx +182 -0
- package/src/sheets/people-chat/PeopleChatSheet.tsx +100 -0
- package/src/sheets/people-chat/PeopleTab.tsx +326 -0
- package/src/sheets/people-chat/PhotoViewer.tsx +132 -0
- package/src/sheets/people-chat/TextField.tsx +14 -0
- package/src/sheets/people-chat/attachments.tsx +183 -0
- package/src/sheets/people-chat/chat-model.ts +157 -0
- package/src/sheets/people-chat/people-chat-model.test.ts +98 -0
- package/src/sheets/people-chat/people-model.ts +68 -0
- package/src/sounds/assets/error.wav +0 -0
- package/src/sounds/assets/hand.wav +0 -0
- package/src/sounds/assets/join.wav +0 -0
- package/src/sounds/assets/knock.wav +0 -0
- package/src/sounds/assets/leave.wav +0 -0
- package/src/sounds/assets/message.wav +0 -0
- package/src/sounds/assets/mute.wav +0 -0
- package/src/sounds/assets/recording.wav +0 -0
- package/src/sounds/assets/test.wav +0 -0
- package/src/sounds/assets/unmute.wav +0 -0
- package/src/sounds/assets.d.ts +5 -0
- package/src/sounds/sounds.ts +50 -0
- package/src/space-client/create-native-space-client.ts +20 -181
- package/src/space-client/random-uuid.ts +17 -0
- package/src/space-client/whiteboard-watchdog.test.ts +23 -0
- package/src/stage/ContentTile.tsx +100 -0
- package/src/stage/ParticipantTile.tsx +140 -0
- package/src/stage/Stage.tsx +152 -0
- package/src/stage/TileSheet.tsx +116 -0
- package/src/stage/camera.ts +20 -0
- package/src/stage/layout.test.ts +77 -0
- package/src/stage/layout.ts +226 -0
- package/src/stage/space-selector.ts +31 -0
- package/src/stage/stage-view.tsx +68 -0
- package/src/status/CallOverlays.tsx +47 -0
- package/src/status/ChalkArrival.tsx +53 -0
- package/src/status/StatusPage.tsx +123 -0
- package/src/status/call-screen.test.ts +42 -0
- package/src/status/call-screen.ts +29 -0
- package/src/theme/theme.tsx +28 -0
- package/src/theme/tokens.ts +247 -0
- package/src/toasts/CallEventToasts.tsx +192 -0
- package/src/toasts/notices.test.ts +92 -0
- package/src/toasts/notices.ts +62 -0
- package/src/ui/ChalkUI.tsx +49 -0
- package/src/ui.ts +24 -0
- package/dist/android.d.ts +0 -2
- package/dist/android.d.ts.map +0 -1
- package/dist/android.js +0 -62
- package/dist/chunk-2OS7SSXE.js +0 -552
- package/dist/chunk-5DQ4KBRH.js +0 -690
- package/dist/chunk-ITLUHTVS.js +0 -5271
- package/dist/chunk-JB4LNWVP.js +0 -247
- package/dist/components/Chalk.d.ts +0 -79
- package/dist/components/Chalk.d.ts.map +0 -1
- package/dist/components/ChatMessageList.d.ts +0 -11
- package/dist/components/ChatMessageList.d.ts.map +0 -1
- package/dist/components/Entrance.d.ts +0 -22
- package/dist/components/Entrance.d.ts.map +0 -1
- package/dist/components/EntranceView.android.d.ts +0 -4
- package/dist/components/EntranceView.android.d.ts.map +0 -1
- package/dist/components/EntranceView.d.ts +0 -18
- package/dist/components/EntranceView.d.ts.map +0 -1
- package/dist/components/EntranceView.ios-pad.d.ts +0 -3
- package/dist/components/EntranceView.ios-pad.d.ts.map +0 -1
- package/dist/components/EntranceView.ios-phone.d.ts +0 -3
- package/dist/components/EntranceView.ios-phone.d.ts.map +0 -1
- package/dist/components/EntranceView.macos.d.ts +0 -4
- package/dist/components/EntranceView.macos.d.ts.map +0 -1
- package/dist/components/FaceAvatar.d.ts +0 -16
- package/dist/components/FaceAvatar.d.ts.map +0 -1
- package/dist/components/GradientSurface.d.ts +0 -14
- package/dist/components/GradientSurface.d.ts.map +0 -1
- package/dist/components/MediaView.d.ts +0 -14
- package/dist/components/MediaView.d.ts.map +0 -1
- package/dist/components/ReactionPicker.d.ts +0 -10
- package/dist/components/ReactionPicker.d.ts.map +0 -1
- package/dist/components/SpaceView.android.d.ts +0 -2
- package/dist/components/SpaceView.android.d.ts.map +0 -1
- package/dist/components/SpaceView.d.ts +0 -33
- package/dist/components/SpaceView.d.ts.map +0 -1
- package/dist/components/SpaceView.ios-pad.d.ts +0 -2
- package/dist/components/SpaceView.ios-pad.d.ts.map +0 -1
- package/dist/components/SpaceView.ios-phone.d.ts +0 -2
- package/dist/components/SpaceView.ios-phone.d.ts.map +0 -1
- package/dist/components/SpaceView.macos.d.ts +0 -2
- package/dist/components/SpaceView.macos.d.ts.map +0 -1
- package/dist/components/SpaceView.shared.d.ts +0 -3
- package/dist/components/SpaceView.shared.d.ts.map +0 -1
- package/dist/components/episode-diagnostic-render-observer.d.ts +0 -2
- package/dist/components/episode-diagnostic-render-observer.d.ts.map +0 -1
- package/dist/components/logo/Logo.d.ts +0 -11
- package/dist/components/logo/Logo.d.ts.map +0 -1
- package/dist/components/native-animation-controller.d.ts +0 -7
- package/dist/components/native-animation-controller.d.ts.map +0 -1
- package/dist/components/native-chat.d.ts +0 -8
- package/dist/components/native-chat.d.ts.map +0 -1
- package/dist/components/native-entrance/entrance-controller-store.d.ts +0 -34
- package/dist/components/native-entrance/entrance-controller-store.d.ts.map +0 -1
- package/dist/components/native-entrance/useEntranceController.d.ts +0 -19
- package/dist/components/native-entrance/useEntranceController.d.ts.map +0 -1
- package/dist/components/native-joining-loading-animation.d.ts +0 -9
- package/dist/components/native-joining-loading-animation.d.ts.map +0 -1
- package/dist/components/native-space-view/AppearanceSettings.d.ts +0 -2
- package/dist/components/native-space-view/AppearanceSettings.d.ts.map +0 -1
- package/dist/components/native-space-view/FeedbackSheet.d.ts +0 -12
- package/dist/components/native-space-view/FeedbackSheet.d.ts.map +0 -1
- package/dist/components/native-space-view/ParticipantActionSheet.d.ts +0 -9
- package/dist/components/native-space-view/ParticipantActionSheet.d.ts.map +0 -1
- package/dist/components/native-space-view/SettingsSheet.d.ts +0 -7
- package/dist/components/native-space-view/SettingsSheet.d.ts.map +0 -1
- package/dist/components/native-space-view/SpaceActionMenu.d.ts +0 -10
- package/dist/components/native-space-view/SpaceActionMenu.d.ts.map +0 -1
- package/dist/components/native-space-view/SpaceBottomDock.android.d.ts +0 -4
- package/dist/components/native-space-view/SpaceBottomDock.android.d.ts.map +0 -1
- package/dist/components/native-space-view/SpaceChatAttachment.d.ts +0 -8
- package/dist/components/native-space-view/SpaceChatAttachment.d.ts.map +0 -1
- package/dist/components/native-space-view/SpaceChatSheet.d.ts +0 -5
- package/dist/components/native-space-view/SpaceChatSheet.d.ts.map +0 -1
- package/dist/components/native-space-view/SpaceGrid.android.d.ts +0 -8
- package/dist/components/native-space-view/SpaceGrid.android.d.ts.map +0 -1
- package/dist/components/native-space-view/SpaceMoreSheet.d.ts +0 -7
- package/dist/components/native-space-view/SpaceMoreSheet.d.ts.map +0 -1
- package/dist/components/native-space-view/SpacePanelSheet.d.ts +0 -5
- package/dist/components/native-space-view/SpacePanelSheet.d.ts.map +0 -1
- package/dist/components/native-space-view/SpacePeopleSheet.d.ts +0 -5
- package/dist/components/native-space-view/SpacePeopleSheet.d.ts.map +0 -1
- package/dist/components/native-space-view/SpaceStage.android.d.ts +0 -29
- package/dist/components/native-space-view/SpaceStage.android.d.ts.map +0 -1
- package/dist/components/native-space-view/SpaceSurfacePrimitives.d.ts +0 -30
- package/dist/components/native-space-view/SpaceSurfacePrimitives.d.ts.map +0 -1
- package/dist/components/native-space-view/SpaceTopBar.android.d.ts +0 -11
- package/dist/components/native-space-view/SpaceTopBar.android.d.ts.map +0 -1
- package/dist/components/native-space-view/SpaceWhiteboardSurface.d.ts +0 -15
- package/dist/components/native-space-view/SpaceWhiteboardSurface.d.ts.map +0 -1
- package/dist/components/native-space-view/diagnostics.d.ts +0 -48
- package/dist/components/native-space-view/diagnostics.d.ts.map +0 -1
- package/dist/components/native-space-view/format-space-duration.d.ts +0 -2
- package/dist/components/native-space-view/format-space-duration.d.ts.map +0 -1
- package/dist/components/native-space-view/native-space-grid-keys.d.ts +0 -3
- package/dist/components/native-space-view/native-space-grid-keys.d.ts.map +0 -1
- package/dist/components/native-space-view/native-space-page-store.d.ts +0 -8
- package/dist/components/native-space-view/native-space-page-store.d.ts.map +0 -1
- package/dist/components/native-space-view/space-chat-attachments.d.ts +0 -15
- package/dist/components/native-space-view/space-chat-attachments.d.ts.map +0 -1
- package/dist/components/native-space-view/space-panel-layout.d.ts +0 -10
- package/dist/components/native-space-view/space-panel-layout.d.ts.map +0 -1
- package/dist/components/native-space-view/space-progressive-surface-helpers.d.ts +0 -16
- package/dist/components/native-space-view/space-progressive-surface-helpers.d.ts.map +0 -1
- package/dist/components/native-space-view/space-progressive-surface-types.d.ts +0 -3
- package/dist/components/native-space-view/space-progressive-surface-types.d.ts.map +0 -1
- package/dist/components/native-space-view/types.d.ts +0 -24
- package/dist/components/native-space-view/types.d.ts.map +0 -1
- package/dist/components/native-space-view/useSpaceViewController.contract.d.ts +0 -41
- package/dist/components/native-space-view/useSpaceViewController.contract.d.ts.map +0 -1
- package/dist/components/native-space-view/useSpaceViewController.d.ts +0 -156
- package/dist/components/native-space-view/useSpaceViewController.d.ts.map +0 -1
- package/dist/components/native-space-view/useSpaceViewDerived.d.ts +0 -12
- package/dist/components/native-space-view/useSpaceViewDerived.d.ts.map +0 -1
- package/dist/components/native-space-view/useSpaceViewPanels.d.ts +0 -70
- package/dist/components/native-space-view/useSpaceViewPanels.d.ts.map +0 -1
- package/dist/feedback/native-capture.d.ts +0 -9
- package/dist/feedback/native-capture.d.ts.map +0 -1
- package/dist/feedback/native-evidence.d.ts +0 -3
- package/dist/feedback/native-evidence.d.ts.map +0 -1
- package/dist/hooks/entrance-preview-store.d.ts +0 -16
- package/dist/hooks/entrance-preview-store.d.ts.map +0 -1
- package/dist/hooks/useEntrancePreview.d.ts +0 -7
- package/dist/hooks/useEntrancePreview.d.ts.map +0 -1
- package/dist/hooks/useLayout.d.ts +0 -16
- package/dist/hooks/useLayout.d.ts.map +0 -1
- package/dist/hooks/usePanels.d.ts +0 -12
- package/dist/hooks/usePanels.d.ts.map +0 -1
- package/dist/ios-pad.d.ts +0 -2
- package/dist/ios-pad.d.ts.map +0 -1
- package/dist/ios-pad.js +0 -62
- package/dist/ios-phone.d.ts +0 -2
- package/dist/ios-phone.d.ts.map +0 -1
- package/dist/ios-phone.js +0 -62
- package/dist/ios.d.ts +0 -2
- package/dist/ios.d.ts.map +0 -1
- package/dist/ios.js +0 -62
- package/dist/macos.d.ts +0 -2
- package/dist/macos.d.ts.map +0 -1
- package/dist/macos.js +0 -62
- package/dist/space-client/cloudflare-rtk-native.d.ts +0 -3
- package/dist/space-client/cloudflare-rtk-native.d.ts.map +0 -1
- package/dist/telemetry.d.ts +0 -63
- package/dist/telemetry.d.ts.map +0 -1
- package/dist/ui/appearance.d.ts +0 -246
- package/dist/ui/appearance.d.ts.map +0 -1
- package/dist/ui/appearance.js +0 -14
- package/dist/ui/native-appearance-context.d.ts +0 -15
- package/dist/ui/native-appearance-context.d.ts.map +0 -1
- package/dist/ui/native-theme.d.ts +0 -17
- package/dist/ui/native-theme.d.ts.map +0 -1
- package/dist/ui/native-types.d.ts +0 -38
- package/dist/ui/native-types.d.ts.map +0 -1
- package/dist/ui/participant-avatar.d.ts +0 -21
- package/dist/ui/participant-avatar.d.ts.map +0 -1
- package/dist/ui/theme-tokens.d.ts +0 -25
- package/dist/ui/theme-tokens.d.ts.map +0 -1
- package/dist/ui/theme.d.ts +0 -404
- package/dist/ui/theme.d.ts.map +0 -1
- package/dist/ui/theme.js +0 -8
- package/dist/utils/native-space-layout.d.ts +0 -42
- package/dist/utils/native-space-layout.d.ts.map +0 -1
- package/src/android.ts +0 -1
- package/src/components/Chalk.tsx +0 -363
- package/src/components/ChatMessageList.tsx +0 -209
- package/src/components/Entrance.tsx +0 -42
- package/src/components/EntranceView.android.tsx +0 -274
- package/src/components/EntranceView.ios-pad.tsx +0 -326
- package/src/components/EntranceView.ios-phone.tsx +0 -271
- package/src/components/EntranceView.macos.tsx +0 -278
- package/src/components/EntranceView.tsx +0 -38
- package/src/components/FaceAvatar.tsx +0 -120
- package/src/components/GradientSurface.tsx +0 -23
- package/src/components/MediaView.tsx +0 -76
- package/src/components/ReactionPicker.tsx +0 -98
- package/src/components/SpaceView.android.tsx +0 -1
- package/src/components/SpaceView.ios-pad.tsx +0 -1
- package/src/components/SpaceView.ios-phone.tsx +0 -1
- package/src/components/SpaceView.macos.tsx +0 -1
- package/src/components/SpaceView.shared.tsx +0 -98
- package/src/components/SpaceView.tsx +0 -54
- package/src/components/episode-diagnostic-render-observer.ts +0 -12
- package/src/components/logo/Logo.tsx +0 -309
- package/src/components/native-animation-controller.ts +0 -36
- package/src/components/native-chat.ts +0 -23
- package/src/components/native-entrance/entrance-controller-store.ts +0 -112
- package/src/components/native-entrance/useEntranceController.ts +0 -60
- package/src/components/native-joining-loading-animation.ts +0 -26
- package/src/components/native-space-view/AppearanceSettings.tsx +0 -114
- package/src/components/native-space-view/FeedbackSheet.tsx +0 -296
- package/src/components/native-space-view/ParticipantActionSheet.tsx +0 -99
- package/src/components/native-space-view/SettingsSheet.tsx +0 -164
- package/src/components/native-space-view/SpaceActionMenu.tsx +0 -109
- package/src/components/native-space-view/SpaceBottomDock.android.tsx +0 -115
- package/src/components/native-space-view/SpaceChatAttachment.tsx +0 -105
- package/src/components/native-space-view/SpaceChatSheet.tsx +0 -184
- package/src/components/native-space-view/SpaceGrid.android.tsx +0 -323
- package/src/components/native-space-view/SpaceMoreSheet.tsx +0 -190
- package/src/components/native-space-view/SpacePanelSheet.tsx +0 -45
- package/src/components/native-space-view/SpacePeopleSheet.tsx +0 -116
- package/src/components/native-space-view/SpaceStage.android.tsx +0 -372
- package/src/components/native-space-view/SpaceSurfacePrimitives.tsx +0 -75
- package/src/components/native-space-view/SpaceTopBar.android.tsx +0 -113
- package/src/components/native-space-view/SpaceWhiteboardSurface.tsx +0 -69
- package/src/components/native-space-view/diagnostics.ts +0 -84
- package/src/components/native-space-view/format-space-duration.ts +0 -3
- package/src/components/native-space-view/native-space-grid-keys.ts +0 -6
- package/src/components/native-space-view/native-space-page-store.ts +0 -41
- package/src/components/native-space-view/space-chat-attachments.test.ts +0 -26
- package/src/components/native-space-view/space-chat-attachments.ts +0 -37
- package/src/components/native-space-view/space-panel-layout.ts +0 -10
- package/src/components/native-space-view/space-progressive-surface-helpers.ts +0 -33
- package/src/components/native-space-view/space-progressive-surface-types.ts +0 -3
- package/src/components/native-space-view/types.ts +0 -25
- package/src/components/native-space-view/useSpaceViewController.contract.ts +0 -34
- package/src/components/native-space-view/useSpaceViewController.ts +0 -416
- package/src/components/native-space-view/useSpaceViewDerived.ts +0 -29
- package/src/components/native-space-view/useSpaceViewPanels.ts +0 -170
- package/src/feedback/native-capture.ts +0 -62
- package/src/feedback/native-evidence.ts +0 -37
- package/src/hooks/entrance-preview-store.ts +0 -99
- package/src/hooks/useEntrancePreview.ts +0 -26
- package/src/hooks/useLayout.ts +0 -37
- package/src/hooks/usePanels.ts +0 -30
- package/src/ios-pad.ts +0 -1
- package/src/ios-phone.ts +0 -1
- package/src/ios.ts +0 -1
- package/src/macos.ts +0 -1
- package/src/space-client/cloudflare-rtk-native.ts +0 -104
- package/src/telemetry.ts +0 -234
- package/src/types/hugeicons-direct.d.ts +0 -4
- package/src/types/react-native-safe-area-context.d.ts +0 -8
- package/src/ui/appearance.ts +0 -808
- package/src/ui/native-appearance-context.tsx +0 -74
- package/src/ui/native-theme.tsx +0 -54
- package/src/ui/native-types.ts +0 -40
- package/src/ui/participant-avatar.ts +0 -47
- package/src/ui/theme-tokens.ts +0 -41
- package/src/ui/theme.ts +0 -214
- package/src/utils/native-space-layout.ts +0 -135
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { ChatAttachment } from "@q9labsai/chalk-client";
|
|
2
|
+
import { useEffect, useRef, type RefObject } from "react";
|
|
3
|
+
import { AccessibilityInfo, findNodeHandle, Image, Pressable, StyleSheet, View } from "react-native";
|
|
4
|
+
import Animated, { ReduceMotion, withTiming, type EntryExitAnimationFunction } from "react-native-reanimated";
|
|
5
|
+
import { useBackHandler } from "../../frame/back-handler";
|
|
6
|
+
import { useDeviceLayout } from "../../frame/device-layout";
|
|
7
|
+
import { Portal } from "../../frame/portal";
|
|
8
|
+
import { Icon, type IconName } from "../../icons/Icon";
|
|
9
|
+
import { usePressAnimation } from "../../motion/press";
|
|
10
|
+
import { announce } from "../../primitives/announce";
|
|
11
|
+
import { ChalkText } from "../../primitives/ChalkText";
|
|
12
|
+
import { durations, easings, fontFamilies, viewer } from "../../theme/tokens";
|
|
13
|
+
import { useAttachmentSource, useDownload } from "./attachments";
|
|
14
|
+
|
|
15
|
+
export type ViewedPhotos = {
|
|
16
|
+
readonly photos: readonly ChatAttachment[];
|
|
17
|
+
readonly index: number;
|
|
18
|
+
/** "You" or the sender's name, then the time: the viewer's second line. */
|
|
19
|
+
readonly from: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type PhotoViewerProps = {
|
|
23
|
+
readonly viewing: ViewedPhotos;
|
|
24
|
+
readonly onChange: (index: number) => void;
|
|
25
|
+
readonly onClose: () => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The photo viewer (`.lbw`): dark in both themes, above the sheets. The name, sender and place in
|
|
30
|
+
* the set sit at the top with Download and Close; Previous and Next sit at the bottom.
|
|
31
|
+
*/
|
|
32
|
+
export function PhotoViewer({ viewing, onChange, onClose }: PhotoViewerProps): React.JSX.Element | null {
|
|
33
|
+
const layout = useDeviceLayout();
|
|
34
|
+
const download = useDownload();
|
|
35
|
+
const closeRef = useRef<View>(null);
|
|
36
|
+
const { photos, index, from } = viewing;
|
|
37
|
+
const photo = photos[index] ?? photos[0];
|
|
38
|
+
const count = photos.length;
|
|
39
|
+
useBackHandler(true, onClose);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const node = closeRef.current && findNodeHandle(closeRef.current);
|
|
42
|
+
if (node) AccessibilityInfo.setAccessibilityFocus(node);
|
|
43
|
+
}, []);
|
|
44
|
+
const go = (step: number) => {
|
|
45
|
+
const next = (index + step + count) % count;
|
|
46
|
+
onChange(next);
|
|
47
|
+
announce(`Image ${next + 1} of ${count}`);
|
|
48
|
+
};
|
|
49
|
+
if (!photo) return null;
|
|
50
|
+
return (
|
|
51
|
+
<Portal layer="menus">
|
|
52
|
+
<Animated.View accessibilityLabel="Image viewer" accessibilityViewIsModal entering={fadeIn} exiting={fadeOut} onAccessibilityEscape={onClose} style={[StyleSheet.absoluteFill, styles.root]} testID="chat.viewer">
|
|
53
|
+
<View style={[styles.top, { paddingTop: Math.max(52, layout.insets.top), paddingLeft: 16 + layout.insets.left, paddingRight: 8 + layout.insets.right }]}>
|
|
54
|
+
<View style={styles.title}>
|
|
55
|
+
<ChalkText color={viewer.text} numberOfLines={1} variant="bodyStrong">
|
|
56
|
+
{photo.fileName}
|
|
57
|
+
</ChalkText>
|
|
58
|
+
<ChalkText color={viewer.textMuted} style={styles.detail} variant="label">
|
|
59
|
+
{count > 1 ? `${from} · ${index + 1} of ${count}` : from}
|
|
60
|
+
</ChalkText>
|
|
61
|
+
</View>
|
|
62
|
+
<ViewerButton icon="dl" label={`Download ${photo.fileName}`} onPress={() => download(photo)} testID="chat.viewer.download" />
|
|
63
|
+
<ViewerButton icon="x" label="Close" buttonRef={closeRef} onPress={onClose} testID="chat.viewer.close" />
|
|
64
|
+
</View>
|
|
65
|
+
<Pressable accessible={false} onPress={onClose} style={styles.stage}>
|
|
66
|
+
<Photo attachment={photo} />
|
|
67
|
+
{count > 1 ? (
|
|
68
|
+
<View style={styles.arrows}>
|
|
69
|
+
<ViewerButton icon="left" label="Previous image" onPress={() => go(-1)} testID="chat.viewer.previous" />
|
|
70
|
+
<ViewerButton icon="right" label="Next image" onPress={() => go(1)} testID="chat.viewer.next" />
|
|
71
|
+
</View>
|
|
72
|
+
) : null}
|
|
73
|
+
</Pressable>
|
|
74
|
+
</Animated.View>
|
|
75
|
+
</Portal>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function Photo({ attachment }: { readonly attachment: ChatAttachment }): React.JSX.Element {
|
|
80
|
+
const source = useAttachmentSource(attachment);
|
|
81
|
+
return (
|
|
82
|
+
<View pointerEvents="none" style={styles.photoBox}>
|
|
83
|
+
{source.state === "ready" ? <Image accessibilityLabel={attachment.fileName} resizeMode="contain" source={{ uri: source.uri }} style={styles.photo} /> : <Icon color={viewer.textMuted} name="img" size={24} />}
|
|
84
|
+
</View>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type ViewerButtonProps = {
|
|
89
|
+
readonly icon: IconName;
|
|
90
|
+
readonly label: string;
|
|
91
|
+
readonly onPress: () => void;
|
|
92
|
+
readonly testID: string;
|
|
93
|
+
readonly buttonRef?: RefObject<View | null>;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** `.lbb` on the phone: 44 pt, a faint white disc that brightens while pressed. */
|
|
97
|
+
function ViewerButton({ icon, label, onPress, testID, buttonRef }: ViewerButtonProps): React.JSX.Element {
|
|
98
|
+
const press = usePressAnimation("round");
|
|
99
|
+
return (
|
|
100
|
+
<Pressable accessibilityLabel={label} accessibilityRole="button" onPress={onPress} onPressIn={press.onPressIn} onPressOut={press.onPressOut} ref={buttonRef} testID={testID}>
|
|
101
|
+
{({ pressed }) => (
|
|
102
|
+
<Animated.View style={[styles.button, { backgroundColor: pressed ? viewer.buttonPressed : viewer.button }, press.scaleStyle]}>
|
|
103
|
+
<Icon color={viewer.text} name={icon} size={20} />
|
|
104
|
+
</Animated.View>
|
|
105
|
+
)}
|
|
106
|
+
</Pressable>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// `.lbw`: opacity over 180 ms, ease-out.
|
|
111
|
+
const fade = { duration: durations.menu, easing: easings.easeOut, reduceMotion: ReduceMotion.System } as const;
|
|
112
|
+
const fadeIn: EntryExitAnimationFunction = () => {
|
|
113
|
+
"worklet";
|
|
114
|
+
return { initialValues: { opacity: 0 }, animations: { opacity: withTiming(1, fade) } };
|
|
115
|
+
};
|
|
116
|
+
const fadeOut: EntryExitAnimationFunction = () => {
|
|
117
|
+
"worklet";
|
|
118
|
+
return { initialValues: { opacity: 1 }, animations: { opacity: withTiming(0, fade) } };
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const styles = StyleSheet.create({
|
|
122
|
+
root: { backgroundColor: viewer.backdrop },
|
|
123
|
+
top: { flexDirection: "row", alignItems: "center", gap: 8, paddingBottom: 8 },
|
|
124
|
+
title: { flex: 1, minWidth: 0 },
|
|
125
|
+
detail: { fontFamily: fontFamilies[400] },
|
|
126
|
+
stage: { flex: 1 },
|
|
127
|
+
// `.phone .lbs img`: the full width, 108 pt kept free under it for the arrows.
|
|
128
|
+
photoBox: { position: "absolute", top: 0, left: 0, right: 0, bottom: 108, alignItems: "center", justifyContent: "center" },
|
|
129
|
+
photo: { width: "100%", height: "100%" },
|
|
130
|
+
arrows: { position: "absolute", left: 0, right: 0, bottom: 40, flexDirection: "row", justifyContent: "center", gap: 32 },
|
|
131
|
+
button: { width: 44, height: 44, borderRadius: 22, alignItems: "center", justifyContent: "center" },
|
|
132
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import { StyleSheet, TextInput, type TextInputProps } from "react-native";
|
|
3
|
+
import { useTheme } from "../../theme/theme";
|
|
4
|
+
import { type } from "../../theme/tokens";
|
|
5
|
+
|
|
6
|
+
/** The text input inside a field (`input` in the mockup): the body type, no chrome of its own. */
|
|
7
|
+
export const TextField = forwardRef<TextInput, TextInputProps>(function TextField({ style, ...rest }, ref) {
|
|
8
|
+
const { colors } = useTheme();
|
|
9
|
+
return <TextInput {...rest} cursorColor={colors.text} placeholderTextColor={colors.textMuted} ref={ref} selectionColor={colors.textMuted} style={[styles.input, { color: colors.text }, style]} />;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const styles = StyleSheet.create({
|
|
13
|
+
input: { flex: 1, minWidth: 0, height: "100%", padding: 0, fontFamily: type.body.fontFamily, fontSize: type.body.fontSize },
|
|
14
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { ChatAttachment, SpaceClient } from "@q9labsai/chalk-client";
|
|
2
|
+
import { memo, useEffect, useState } from "react";
|
|
3
|
+
import { Image, Linking, Pressable, StyleSheet, View } from "react-native";
|
|
4
|
+
import { useSpaceClient } from "../../context/space-client-context";
|
|
5
|
+
import { Icon } from "../../icons/Icon";
|
|
6
|
+
import { ChalkText } from "../../primitives/ChalkText";
|
|
7
|
+
import { IconButton } from "../../primitives/IconButton";
|
|
8
|
+
import { useToasts } from "../../primitives/toast/ToastStack";
|
|
9
|
+
import { useTheme } from "../../theme/theme";
|
|
10
|
+
import { chatOverlays, fileKindColors, fixed, fontFamilies } from "../../theme/tokens";
|
|
11
|
+
import { FILE_BADGE, fileKind, formatSize, thumbnailLayout } from "./chat-model";
|
|
12
|
+
|
|
13
|
+
/** Download links for this session, so a photo scrolled back into view doesn't ask the server again. */
|
|
14
|
+
const urls = new Map<string, Promise<string>>();
|
|
15
|
+
|
|
16
|
+
function resolveUrl(client: SpaceClient, attachment: ChatAttachment): Promise<string> {
|
|
17
|
+
let url = urls.get(attachment.attachmentId);
|
|
18
|
+
if (!url) {
|
|
19
|
+
url = client.chat.files.resolveUrl(attachment);
|
|
20
|
+
urls.set(attachment.attachmentId, url);
|
|
21
|
+
url.catch(() => urls.delete(attachment.attachmentId));
|
|
22
|
+
}
|
|
23
|
+
return url;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type AttachmentSource = { readonly state: "loading" } | { readonly state: "ready"; readonly uri: string; readonly aspect: number } | { readonly state: "failed" };
|
|
27
|
+
|
|
28
|
+
/** The download link of a photo and its shape, once both are known. */
|
|
29
|
+
export function useAttachmentSource(attachment: ChatAttachment): AttachmentSource {
|
|
30
|
+
const client = useSpaceClient();
|
|
31
|
+
const [source, setSource] = useState<AttachmentSource>({ state: "loading" });
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
let live = true;
|
|
34
|
+
resolveUrl(client, attachment)
|
|
35
|
+
.then((uri) => {
|
|
36
|
+
Image.getSize(
|
|
37
|
+
uri,
|
|
38
|
+
(width, height) => live && setSource({ state: "ready", uri, aspect: width / height }),
|
|
39
|
+
() => live && setSource({ state: "ready", uri, aspect: 4 / 3 }),
|
|
40
|
+
);
|
|
41
|
+
})
|
|
42
|
+
.catch(() => live && setSource({ state: "failed" }));
|
|
43
|
+
return () => {
|
|
44
|
+
live = false;
|
|
45
|
+
};
|
|
46
|
+
}, [attachment, client]);
|
|
47
|
+
return source;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Opens a file in the system viewer, which offers saving and sharing. */
|
|
51
|
+
export function useDownload(): (attachment: ChatAttachment) => void {
|
|
52
|
+
const client = useSpaceClient();
|
|
53
|
+
const toasts = useToasts();
|
|
54
|
+
return (attachment) => {
|
|
55
|
+
resolveUrl(client, attachment)
|
|
56
|
+
.then((uri) => Linking.openURL(uri))
|
|
57
|
+
.catch(() => toasts.show({ kind: "error", dismissible: true, message: `Couldn't download ${attachment.fileName}.` }));
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type ThumbnailsProps = {
|
|
62
|
+
readonly photos: readonly ChatAttachment[];
|
|
63
|
+
readonly disabled: boolean;
|
|
64
|
+
readonly onOpen: (index: number) => void;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** A message's photos (`.ths`): one at its own shape, or a grid of up to four with "+N" on the last. */
|
|
68
|
+
export const Thumbnails = memo(function Thumbnails({ photos, disabled, onOpen }: ThumbnailsProps): React.JSX.Element {
|
|
69
|
+
const { shown, more } = thumbnailLayout(photos.length);
|
|
70
|
+
const count = photos.length;
|
|
71
|
+
return (
|
|
72
|
+
<View style={[styles.grid, count > 1 ? styles.gridMany : null]}>
|
|
73
|
+
{photos.slice(0, shown).map((photo, index) => (
|
|
74
|
+
<Thumbnail
|
|
75
|
+
attachment={photo}
|
|
76
|
+
disabled={disabled}
|
|
77
|
+
key={photo.attachmentId}
|
|
78
|
+
label={`Open ${photo.fileName}${count > 1 ? `, ${index + 1} of ${count}` : ""}${index === 3 && more > 0 ? `, and ${more} more` : ""}`}
|
|
79
|
+
layout={count === 1 ? "one" : count === 3 && index === 2 ? "wide" : "square"}
|
|
80
|
+
more={index === 3 ? more : 0}
|
|
81
|
+
onPress={() => onOpen(index)}
|
|
82
|
+
/>
|
|
83
|
+
))}
|
|
84
|
+
</View>
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
type ThumbnailProps = {
|
|
89
|
+
readonly attachment: ChatAttachment;
|
|
90
|
+
readonly layout: "one" | "square" | "wide";
|
|
91
|
+
readonly more: number;
|
|
92
|
+
readonly label: string;
|
|
93
|
+
readonly disabled: boolean;
|
|
94
|
+
readonly onPress: () => void;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function Thumbnail({ attachment, layout, more, label, disabled, onPress }: ThumbnailProps): React.JSX.Element {
|
|
98
|
+
const { colors } = useTheme();
|
|
99
|
+
const source = useAttachmentSource(attachment);
|
|
100
|
+
const aspectRatio = layout === "one" ? (source.state === "ready" ? Math.min(1.78, Math.max(0.75, source.aspect)) : 4 / 3) : layout === "wide" ? 2 : 1;
|
|
101
|
+
const style = [styles.thumb, layout === "one" ? styles.thumbOne : layout === "wide" ? styles.thumbWide : styles.thumbSquare, { aspectRatio, backgroundColor: colors.raised }];
|
|
102
|
+
if (source.state !== "ready") {
|
|
103
|
+
return (
|
|
104
|
+
<View accessibilityLabel={source.state === "loading" ? `Loading ${attachment.fileName}` : `Couldn't load ${attachment.fileName}`} accessibilityRole="image" accessible style={[style, styles.centered]}>
|
|
105
|
+
<Icon color={colors.textMuted} name="img" size={18} />
|
|
106
|
+
</View>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return (
|
|
110
|
+
<Pressable accessibilityLabel={label} accessibilityRole="imagebutton" accessibilityState={{ disabled }} disabled={disabled} onPress={onPress} style={style} testID={`chat.photo.${attachment.attachmentId}`}>
|
|
111
|
+
<Image source={{ uri: source.uri }} style={StyleSheet.absoluteFill} />
|
|
112
|
+
<View pointerEvents="none" style={[StyleSheet.absoluteFill, { borderRadius: layout === "one" ? 14 : 3, boxShadow: `inset 0px 0px 0px 1px ${colors.line}` }]} />
|
|
113
|
+
{more > 0 ? (
|
|
114
|
+
<View style={[StyleSheet.absoluteFill, styles.centered, styles.moreShade]}>
|
|
115
|
+
<ChalkText color={fixed.overlayText} style={styles.moreText} variant="sheetTitle">
|
|
116
|
+
{`+${more}`}
|
|
117
|
+
</ChalkText>
|
|
118
|
+
</View>
|
|
119
|
+
) : null}
|
|
120
|
+
</Pressable>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A file's coloured badge (`.fi`): its kind in two to four letters, or a photo icon. */
|
|
125
|
+
export function FileBadge({ mimeType, size = 36 }: { readonly mimeType: string; readonly size?: 32 | 36 }): React.JSX.Element {
|
|
126
|
+
const { colors } = useTheme();
|
|
127
|
+
const kind = fileKind(mimeType);
|
|
128
|
+
const box = { width: size, height: size, borderRadius: size === 36 ? 9 : 7 };
|
|
129
|
+
if (kind === "image") {
|
|
130
|
+
return (
|
|
131
|
+
<View style={[styles.badge, box, { backgroundColor: colors.hover }]}>
|
|
132
|
+
<Icon color={colors.textMuted} name="img" size={18} />
|
|
133
|
+
</View>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const color = kind === "txt" ? fileKindColors.other : fileKindColors[kind];
|
|
137
|
+
return (
|
|
138
|
+
<View style={[styles.badge, box, { backgroundColor: color }]}>
|
|
139
|
+
<ChalkText color={fixed.overlayText} style={[styles.badgeText, size === 32 ? styles.badgeTextSmall : null]} variant="caption">
|
|
140
|
+
{FILE_BADGE[kind]}
|
|
141
|
+
</ChalkText>
|
|
142
|
+
</View>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** A file in a message (`.fcard`) with its name, size and a download button. */
|
|
147
|
+
export const FileCard = memo(function FileCard({ attachment, sent }: { readonly attachment: ChatAttachment; readonly sent: boolean }): React.JSX.Element {
|
|
148
|
+
const { colors } = useTheme();
|
|
149
|
+
const download = useDownload();
|
|
150
|
+
return (
|
|
151
|
+
<View style={[styles.file, { backgroundColor: colors.raised, borderColor: colors.line }]} testID={`chat.file.${attachment.attachmentId}`}>
|
|
152
|
+
<FileBadge mimeType={attachment.mimeType} />
|
|
153
|
+
<View style={styles.fileText}>
|
|
154
|
+
<ChalkText numberOfLines={1} variant="button">
|
|
155
|
+
{attachment.fileName}
|
|
156
|
+
</ChalkText>
|
|
157
|
+
<ChalkText style={styles.fileSize} tone="textMuted" variant="label">
|
|
158
|
+
{formatSize(attachment.byteLength)}
|
|
159
|
+
</ChalkText>
|
|
160
|
+
</View>
|
|
161
|
+
{sent ? <IconButton accessibilityLabel={`Download ${attachment.fileName}`} icon="dl" onPress={() => download(attachment)} size={36} testID={`chat.download.${attachment.attachmentId}`} tone="ghost" /> : null}
|
|
162
|
+
</View>
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const styles = StyleSheet.create({
|
|
167
|
+
grid: { width: 240, maxWidth: "100%", borderRadius: 14, overflow: "hidden" },
|
|
168
|
+
gridMany: { flexDirection: "row", flexWrap: "wrap", gap: 2 },
|
|
169
|
+
thumb: { overflow: "hidden" },
|
|
170
|
+
thumbOne: { width: "100%", borderRadius: 14 },
|
|
171
|
+
thumbSquare: { width: 119, borderRadius: 3 },
|
|
172
|
+
thumbWide: { width: "100%", borderRadius: 3 },
|
|
173
|
+
centered: { alignItems: "center", justifyContent: "center" },
|
|
174
|
+
moreShade: { backgroundColor: chatOverlays.photoMore },
|
|
175
|
+
moreText: { fontSize: 20, lineHeight: 28 },
|
|
176
|
+
badge: { alignItems: "center", justifyContent: "center" },
|
|
177
|
+
// `.fi`: 10 pt bold capitals with 0.04 em tracking.
|
|
178
|
+
badgeText: { fontFamily: fontFamilies[700], fontSize: 10, lineHeight: 10, letterSpacing: 0.4 },
|
|
179
|
+
badgeTextSmall: { fontSize: 9, lineHeight: 9, letterSpacing: 0.36 },
|
|
180
|
+
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 },
|
|
181
|
+
fileText: { flex: 1, minWidth: 0, gap: 1 },
|
|
182
|
+
fileSize: { fontFamily: fontFamilies[400], lineHeight: 15 },
|
|
183
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { CHALK_CHAT_ATTACHMENT_LIMITS, CHALK_CHAT_ATTACHMENT_MIME_TYPES, type ChatAttachment, type ChatMessage, type ChatUploadFile, type PendingChatSend } from "@q9labsai/chalk-client";
|
|
2
|
+
|
|
3
|
+
export type FileKind = "image" | "pdf" | "doc" | "xls" | "ppt" | "txt" | "other";
|
|
4
|
+
|
|
5
|
+
const KIND_BY_MIME: Readonly<Record<string, FileKind>> = {
|
|
6
|
+
"image/png": "image",
|
|
7
|
+
"image/jpeg": "image",
|
|
8
|
+
"image/gif": "image",
|
|
9
|
+
"image/webp": "image",
|
|
10
|
+
"application/pdf": "pdf",
|
|
11
|
+
"text/plain": "txt",
|
|
12
|
+
"application/msword": "doc",
|
|
13
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "doc",
|
|
14
|
+
"application/vnd.oasis.opendocument.text": "doc",
|
|
15
|
+
"application/vnd.ms-excel": "xls",
|
|
16
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xls",
|
|
17
|
+
"application/vnd.oasis.opendocument.spreadsheet": "xls",
|
|
18
|
+
"application/vnd.ms-powerpoint": "ppt",
|
|
19
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "ppt",
|
|
20
|
+
"application/vnd.oasis.opendocument.presentation": "ppt",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function fileKind(mimeType: string): FileKind {
|
|
24
|
+
return KIND_BY_MIME[mimeType] ?? "other";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MIME_BY_EXTENSION: Readonly<Record<string, string>> = {
|
|
28
|
+
png: "image/png",
|
|
29
|
+
jpg: "image/jpeg",
|
|
30
|
+
jpeg: "image/jpeg",
|
|
31
|
+
gif: "image/gif",
|
|
32
|
+
webp: "image/webp",
|
|
33
|
+
pdf: "application/pdf",
|
|
34
|
+
txt: "text/plain",
|
|
35
|
+
doc: "application/msword",
|
|
36
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
37
|
+
odt: "application/vnd.oasis.opendocument.text",
|
|
38
|
+
xls: "application/vnd.ms-excel",
|
|
39
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
40
|
+
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
|
41
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
42
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
43
|
+
odp: "application/vnd.oasis.opendocument.presentation",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** The MIME type a picked file reports, or the one its extension implies when the picker reports none. */
|
|
47
|
+
export function mimeTypeOf(name: string, reported: string | undefined): string {
|
|
48
|
+
if (reported) return reported;
|
|
49
|
+
const dot = name.lastIndexOf(".");
|
|
50
|
+
return (dot > 0 && MIME_BY_EXTENSION[name.slice(dot + 1).toLowerCase()]) || "application/octet-stream";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The two or three letters on a file badge (`.fi`). */
|
|
54
|
+
export const FILE_BADGE: Readonly<Record<Exclude<FileKind, "image">, string>> = { pdf: "PDF", doc: "DOC", xls: "XLS", ppt: "PPT", txt: "TXT", other: "FILE" };
|
|
55
|
+
|
|
56
|
+
const MB = 1024 * 1024;
|
|
57
|
+
|
|
58
|
+
export function formatSize(bytes: number): string {
|
|
59
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
60
|
+
if (bytes < MB) return `${Math.round(bytes / 1024)} KB`;
|
|
61
|
+
return `${(bytes / MB).toFixed(bytes < 10 * MB ? 1 : 0)} MB`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** What a message row shows for a sent, waiting or failed message. */
|
|
65
|
+
export type EntryState = "sent" | "queued" | "sending" | "failed";
|
|
66
|
+
|
|
67
|
+
export type ChatEntry = {
|
|
68
|
+
readonly key: string;
|
|
69
|
+
readonly text: string;
|
|
70
|
+
readonly attachments: readonly ChatAttachment[];
|
|
71
|
+
readonly state: EntryState;
|
|
72
|
+
/** Set on pending sends, so a failed one can be sent again. */
|
|
73
|
+
readonly clientMessageId: string | null;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type MessageGroup = {
|
|
77
|
+
readonly key: string;
|
|
78
|
+
readonly participantId: string;
|
|
79
|
+
readonly displayName: string;
|
|
80
|
+
readonly mine: boolean;
|
|
81
|
+
readonly time: string;
|
|
82
|
+
readonly entries: readonly ChatEntry[];
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export function formatTime(iso: string): string {
|
|
86
|
+
const date = new Date(iso);
|
|
87
|
+
return Number.isNaN(date.getTime()) ? "" : date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Whether a message with only attachments should read as "Sent a photo" and so on, for announcements and labels. */
|
|
91
|
+
export function messageSummary(text: string, attachments: readonly ChatAttachment[]): string {
|
|
92
|
+
if (text) return text;
|
|
93
|
+
const count = attachments.length;
|
|
94
|
+
if (count === 0) return "";
|
|
95
|
+
const photos = attachments.filter((attachment) => fileKind(attachment.mimeType) === "image").length;
|
|
96
|
+
if (photos === count) return count > 1 ? `Sent ${count} photos` : "Sent a photo";
|
|
97
|
+
return count > 1 ? `Sent ${count} files` : "Sent a file";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Turns the client's confirmed messages and your waiting sends into rows, oldest first: consecutive
|
|
102
|
+
* messages from one person share a header (`.mg`). A send you retried leaves its failed copy in the
|
|
103
|
+
* client, so `retried` hides those.
|
|
104
|
+
*/
|
|
105
|
+
export function groupMessages(messages: readonly ChatMessage[], pending: readonly PendingChatSend[], self: { readonly participantId: string | null; readonly displayName: string | null }, retried: ReadonlySet<string>): readonly MessageGroup[] {
|
|
106
|
+
const groups: MessageGroup[] = [];
|
|
107
|
+
const add = (participantId: string, displayName: string, time: string, entry: ChatEntry) => {
|
|
108
|
+
const last = groups[groups.length - 1];
|
|
109
|
+
if (last && last.participantId === participantId) {
|
|
110
|
+
groups[groups.length - 1] = { ...last, entries: [...last.entries, entry] };
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
groups.push({ key: entry.key, participantId, displayName, mine: participantId === self.participantId, time, entries: [entry] });
|
|
114
|
+
};
|
|
115
|
+
for (const message of messages) {
|
|
116
|
+
add(message.participantId, message.displayName, formatTime(message.createdAt), { key: message.messageId, text: message.text, attachments: message.attachments, state: "sent", clientMessageId: null });
|
|
117
|
+
}
|
|
118
|
+
for (const send of pending) {
|
|
119
|
+
if (retried.has(send.clientMessageId)) continue;
|
|
120
|
+
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 });
|
|
121
|
+
}
|
|
122
|
+
return groups;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A file the person picked, on its way to being sent. */
|
|
126
|
+
export type StagedFile = {
|
|
127
|
+
readonly id: number;
|
|
128
|
+
readonly name: string;
|
|
129
|
+
readonly size: number;
|
|
130
|
+
readonly mimeType: string;
|
|
131
|
+
readonly source: ChatUploadFile;
|
|
132
|
+
/** The local photo to show in the chip, before it's uploaded. */
|
|
133
|
+
readonly previewUri: string | null;
|
|
134
|
+
readonly state: "uploading" | "ready" | "error";
|
|
135
|
+
readonly attachment: ChatAttachment | null;
|
|
136
|
+
readonly error: string | null;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export type UploadFacts = { readonly name: string; readonly mimeType: string; readonly size: number };
|
|
140
|
+
|
|
141
|
+
const allowedMimeTypes = new Set<string>(CHALK_CHAT_ATTACHMENT_MIME_TYPES);
|
|
142
|
+
|
|
143
|
+
/** The reason a file can't be sent, or null. `accepted` counts the files already accepted in this message. */
|
|
144
|
+
export function stagingError(facts: UploadFacts, accepted: number): string | null {
|
|
145
|
+
if (!allowedMimeTypes.has(facts.mimeType)) {
|
|
146
|
+
const dot = facts.name.lastIndexOf(".");
|
|
147
|
+
return dot > 0 && dot < facts.name.length - 1 ? `Can’t send .${facts.name.slice(dot + 1).toLowerCase()} files` : "Can’t send this type of file";
|
|
148
|
+
}
|
|
149
|
+
if (facts.size > CHALK_CHAT_ATTACHMENT_LIMITS.maximumByteLength) return "Over the 25 MB limit";
|
|
150
|
+
if (accepted >= CHALK_CHAT_ATTACHMENT_LIMITS.maximumPerMessage) return `Only ${CHALK_CHAT_ATTACHMENT_LIMITS.maximumPerMessage} files per message`;
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** How many photos of a message the thumbnail grid draws, and how many are left over for "+N". */
|
|
155
|
+
export function thumbnailLayout(count: number): { readonly shown: number; readonly more: number } {
|
|
156
|
+
return { shown: Math.min(count, 4), more: Math.max(0, count - 4) };
|
|
157
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { ChatMessage, Participant, PendingChatSend } from "@q9labsai/chalk-client";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { groupMessages, messageSummary, mimeTypeOf, stagingError } from "./chat-model";
|
|
4
|
+
import { nextHandOrder, peopleRows } from "./people-model";
|
|
5
|
+
|
|
6
|
+
function person(participantId: string, displayName: string, handRaised = false): Participant {
|
|
7
|
+
return {
|
|
8
|
+
participantId,
|
|
9
|
+
displayName,
|
|
10
|
+
role: "participant",
|
|
11
|
+
eligibleRoles: [],
|
|
12
|
+
capabilities: [],
|
|
13
|
+
handRaised,
|
|
14
|
+
media: { microphone: "active", camera: "inactive", screenShare: "inactive" },
|
|
15
|
+
presence: { state: "connected", speaking: false, activeSpeaker: false },
|
|
16
|
+
} as const satisfies Participant;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function message(messageId: string, participantId: string, text: string): ChatMessage {
|
|
20
|
+
return { messageId, clientMessageId: `c-${messageId}`, sequence: messageId, participantId, displayName: participantId, text, createdAt: "2026-10-18T10:00:00Z", attachments: [] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const photo = { attachmentId: "a", fileName: "IMG_0412.jpg", mimeType: "image/jpeg", byteLength: 10 } as const;
|
|
24
|
+
const pdf = { attachmentId: "b", fileName: "Q4 plan.pdf", mimeType: "application/pdf", byteLength: 10 } as const;
|
|
25
|
+
|
|
26
|
+
describe("peopleRows", () => {
|
|
27
|
+
const roster = [person("me", "Hasan"), person("n", "Nora", true), person("s", "Sam", true), person("k", "Kai")];
|
|
28
|
+
|
|
29
|
+
it("puts the raised-hands queue in the order hands went up, above everyone, once two hands are up", () => {
|
|
30
|
+
const rows = peopleRows({ roster, waiting: [], handOrder: ["s", "n"], query: "" });
|
|
31
|
+
expect(rows.map((row) => row.key)).toEqual(["section.hands", "hand.s", "hand.n", "section.call", "person.me", "person.n", "person.s", "person.k"]);
|
|
32
|
+
expect(rows.find((row) => row.key === "person.n")).toMatchObject({ handNumber: 2 });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("numbers nothing and shows no queue for a single raised hand", () => {
|
|
36
|
+
const rows = peopleRows({ roster: [person("me", "Hasan"), person("n", "Nora", true)], waiting: [], handOrder: ["n"], query: "" });
|
|
37
|
+
expect(rows.map((row) => row.kind)).toEqual(["person", "person"]);
|
|
38
|
+
expect(rows[1]).toMatchObject({ handNumber: null });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("filters by name and says when no one matches", () => {
|
|
42
|
+
expect(peopleRows({ roster, waiting: [], handOrder: ["s", "n"], query: "no" }).map((row) => row.key)).toEqual(["person.n"]);
|
|
43
|
+
expect(peopleRows({ roster, waiting: [], handOrder: [], query: "zed" })).toEqual([{ kind: "none", key: "none", query: "zed" }]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("keeps the first-raised hand at the front as others raise and lower", () => {
|
|
47
|
+
const first = nextHandOrder([], [person("n", "Nora", true)]);
|
|
48
|
+
const second = nextHandOrder(first, [person("n", "Nora", true), person("s", "Sam", true)]);
|
|
49
|
+
expect(second).toEqual(["n", "s"]);
|
|
50
|
+
expect(nextHandOrder(second, [person("n", "Nora"), person("s", "Sam", true)])).toEqual(["s"]);
|
|
51
|
+
expect(nextHandOrder(second, [person("n", "Nora", true), person("s", "Sam", true)])).toBe(second);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("groupMessages", () => {
|
|
56
|
+
const self = { participantId: "me", displayName: "Hasan" };
|
|
57
|
+
|
|
58
|
+
it("groups consecutive messages from one person and marks yours", () => {
|
|
59
|
+
const groups = groupMessages([message("1", "n", "Hi"), message("2", "n", "Ready?"), message("3", "me", "Yes")], [], self, new Set());
|
|
60
|
+
expect(groups.map((group) => [group.participantId, group.mine, group.entries.length])).toEqual([
|
|
61
|
+
["n", false, 2],
|
|
62
|
+
["me", true, 1],
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("adds your waiting sends after the confirmed messages and hides ones you retried", () => {
|
|
67
|
+
const failed: PendingChatSend = { clientMessageId: "p1", text: "Lost", attachments: [], status: "failed", error: null };
|
|
68
|
+
const sending: PendingChatSend = { clientMessageId: "p2", text: "Again", attachments: [], status: "sending", error: null };
|
|
69
|
+
const groups = groupMessages([message("1", "me", "First")], [failed, sending], self, new Set(["p1"]));
|
|
70
|
+
expect(groups).toHaveLength(1);
|
|
71
|
+
expect(groups[0]?.entries.map((entry) => [entry.text, entry.state])).toEqual([
|
|
72
|
+
["First", "sent"],
|
|
73
|
+
["Again", "sending"],
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("chat files", () => {
|
|
79
|
+
it("says what a message with only attachments sent", () => {
|
|
80
|
+
expect(messageSummary("", [photo])).toBe("Sent a photo");
|
|
81
|
+
expect(messageSummary("", [photo, photo])).toBe("Sent 2 photos");
|
|
82
|
+
expect(messageSummary("", [photo, pdf])).toBe("Sent 2 files");
|
|
83
|
+
expect(messageSummary("Look", [photo])).toBe("Look");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("refuses unknown types, files over 25 MB and a sixth file", () => {
|
|
87
|
+
expect(stagingError({ name: "Demo.mov", mimeType: "video/quicktime", size: 10 }, 0)).toBe("Can’t send .mov files");
|
|
88
|
+
expect(stagingError({ name: "Site photos.pdf", mimeType: "application/pdf", size: 31 * 1024 * 1024 }, 0)).toBe("Over the 25 MB limit");
|
|
89
|
+
expect(stagingError({ name: "Agenda.pdf", mimeType: "application/pdf", size: 10 }, 5)).toBe("Only 5 files per message");
|
|
90
|
+
expect(stagingError({ name: "Agenda.pdf", mimeType: "application/pdf", size: 10 }, 4)).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("falls back to the extension when the picker reports no type", () => {
|
|
94
|
+
expect(mimeTypeOf("Budget 2027.XLSX", undefined)).toBe("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
|
95
|
+
expect(mimeTypeOf("notes", undefined)).toBe("application/octet-stream");
|
|
96
|
+
expect(mimeTypeOf("a.png", "image/png")).toBe("image/png");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { AdmissionRequest, Participant } from "@q9labsai/chalk-client";
|
|
2
|
+
|
|
3
|
+
export type PeopleRow =
|
|
4
|
+
| { readonly kind: "section"; readonly key: string; readonly label: string }
|
|
5
|
+
| { readonly kind: "waiting"; readonly key: string; readonly request: AdmissionRequest }
|
|
6
|
+
| { readonly kind: "hand"; readonly key: string; readonly participant: Participant; readonly order: number }
|
|
7
|
+
| { readonly kind: "person"; readonly key: string; readonly participant: Participant; /** Place in the raised-hands queue; set only while two or more hands are up. */ readonly handNumber: number | null }
|
|
8
|
+
| { readonly kind: "none"; readonly key: string; readonly query: string };
|
|
9
|
+
|
|
10
|
+
export type PeopleInput = {
|
|
11
|
+
readonly roster: readonly Participant[];
|
|
12
|
+
/** People waiting to join; empty for anyone who can't let people in. */
|
|
13
|
+
readonly waiting: readonly AdmissionRequest[];
|
|
14
|
+
/** Participant ids with a raised hand, the one raised first at the front. */
|
|
15
|
+
readonly handOrder: readonly string[];
|
|
16
|
+
readonly query: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The People list, top to bottom: people waiting, the raised-hands queue (only once two hands are up,
|
|
21
|
+
* since one hand needs no order), then everyone in the call. A search hides the queue.
|
|
22
|
+
*/
|
|
23
|
+
export function peopleRows({ roster, waiting, handOrder, query }: PeopleInput): readonly PeopleRow[] {
|
|
24
|
+
const q = query.trim().toLowerCase();
|
|
25
|
+
const rows: PeopleRow[] = [];
|
|
26
|
+
const byId = new Map(roster.map((participant) => [participant.participantId, participant]));
|
|
27
|
+
const raised = handOrder.filter((id) => byId.get(id)?.handRaised);
|
|
28
|
+
const numbered = raised.length > 1;
|
|
29
|
+
let headed = false;
|
|
30
|
+
const headInTheCall = () => {
|
|
31
|
+
if (headed) return;
|
|
32
|
+
headed = true;
|
|
33
|
+
rows.push({ kind: "section", key: "section.call", label: "In the call" });
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (waiting.length > 0) {
|
|
37
|
+
rows.push({ kind: "section", key: "section.waiting", label: "Waiting" });
|
|
38
|
+
for (const request of waiting) rows.push({ kind: "waiting", key: `waiting.${request.requestId}`, request });
|
|
39
|
+
headInTheCall();
|
|
40
|
+
}
|
|
41
|
+
if (!q && numbered) {
|
|
42
|
+
rows.push({ kind: "section", key: "section.hands", label: "Raised hands" });
|
|
43
|
+
raised.forEach((id, index) => {
|
|
44
|
+
const participant = byId.get(id);
|
|
45
|
+
if (participant) rows.push({ kind: "hand", key: `hand.${id}`, participant, order: index + 1 });
|
|
46
|
+
});
|
|
47
|
+
headInTheCall();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const people = roster.filter((participant) => !q || participant.displayName.toLowerCase().includes(q));
|
|
51
|
+
if (people.length === 0) rows.push({ kind: "none", key: "none", query: query.trim() });
|
|
52
|
+
for (const participant of people) {
|
|
53
|
+
const index = raised.indexOf(participant.participantId);
|
|
54
|
+
rows.push({ kind: "person", key: `person.${participant.participantId}`, participant, handNumber: numbered && index >= 0 ? index + 1 : null });
|
|
55
|
+
}
|
|
56
|
+
return rows;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Who raised a hand first. The client only says whether a hand is up, so this remembers the order
|
|
61
|
+
* the lists saw hands go up in; people who join with a hand already raised count from when they appear.
|
|
62
|
+
*/
|
|
63
|
+
export function nextHandOrder(previous: readonly string[], roster: readonly Participant[]): readonly string[] {
|
|
64
|
+
const raised = new Set(roster.filter((participant) => participant.handRaised).map((participant) => participant.participantId));
|
|
65
|
+
const kept = previous.filter((id) => raised.has(id));
|
|
66
|
+
const added = [...raised].filter((id) => !kept.includes(id));
|
|
67
|
+
return added.length === 0 && kept.length === previous.length ? previous : [...kept, ...added];
|
|
68
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|