@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,53 @@
|
|
|
1
|
+
import { memo, useEffect, type ReactNode } from "react";
|
|
2
|
+
import { StyleSheet, View } from "react-native";
|
|
3
|
+
import Animated, { useAnimatedStyle, useSharedValue, withTiming, type SharedValue } from "react-native-reanimated";
|
|
4
|
+
import { useDeviceLayout } from "../frame/device-layout";
|
|
5
|
+
import { timings } from "../motion/motion";
|
|
6
|
+
import { useTheme } from "../theme/theme";
|
|
7
|
+
import { fixed, radii } from "../theme/tokens";
|
|
8
|
+
|
|
9
|
+
export type TileShellProps = {
|
|
10
|
+
readonly children: ReactNode;
|
|
11
|
+
/** The live ring shows only on a speaking person whose camera is on; a camera-off face reacts instead. */
|
|
12
|
+
readonly speaking?: boolean;
|
|
13
|
+
readonly cameraOn?: boolean;
|
|
14
|
+
/** Speaking level from 0 to 1; the ring's opacity follows it (0.3 + 0.7 × level). */
|
|
15
|
+
readonly level?: SharedValue<number>;
|
|
16
|
+
readonly testID?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A stage tile's frame (`.tile`): tile surface, 12 pt radius (14 pt on a phone), clipped
|
|
21
|
+
* content, and the live ring drawn over the video. The stage fills it with video or an
|
|
22
|
+
* avatar and a `NameTag`.
|
|
23
|
+
*/
|
|
24
|
+
export const TileShell = memo(function TileShell({ children, speaking = false, cameraOn = false, level, testID }: TileShellProps): React.JSX.Element {
|
|
25
|
+
const { colors } = useTheme();
|
|
26
|
+
const { phone } = useDeviceLayout();
|
|
27
|
+
const radius = phone ? radii.tilePhone : radii.tile;
|
|
28
|
+
const live = useSharedValue(0);
|
|
29
|
+
const showRing = speaking && cameraOn;
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
live.value = withTiming(showRing ? 1 : 0, timings.scrim);
|
|
32
|
+
}, [live, showRing]);
|
|
33
|
+
const ringStyle = useAnimatedStyle(() => {
|
|
34
|
+
"worklet";
|
|
35
|
+
return { opacity: live.value * (0.3 + 0.7 * (level ? level.value : 1)) };
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<View style={[styles.tile, { borderRadius: radius, backgroundColor: colors.tile }]} testID={testID}>
|
|
40
|
+
{children}
|
|
41
|
+
<Animated.View pointerEvents="none" style={[styles.ring, { borderRadius: radius }, ringStyle]} testID={testID ? `${testID}.ring` : undefined}>
|
|
42
|
+
<View style={[styles.ringShade, { borderRadius: radius - 3 }]} />
|
|
43
|
+
</Animated.View>
|
|
44
|
+
</View>
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const styles = StyleSheet.create({
|
|
49
|
+
tile: { flex: 1, overflow: "hidden" },
|
|
50
|
+
// `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.
|
|
51
|
+
ring: { ...StyleSheet.absoluteFillObject, borderWidth: 3, borderColor: fixed.liveRing, zIndex: 5 },
|
|
52
|
+
ringShade: { ...StyleSheet.absoluteFillObject, borderWidth: 1, borderColor: fixed.liveRingShade },
|
|
53
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { AccessibilityInfo, Platform } from "react-native";
|
|
2
|
+
|
|
3
|
+
/** Speaks a message through VoiceOver or TalkBack, like the mockup's polite live region. Queued on iOS so it doesn't cut off the current utterance. */
|
|
4
|
+
export function announce(message: string): void {
|
|
5
|
+
if (Platform.OS === "ios") AccessibilityInfo.announceForAccessibilityWithOptions(message, { queue: true });
|
|
6
|
+
else AccessibilityInfo.announceForAccessibility(message);
|
|
7
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { placeMenu } from "./menu-placement";
|
|
3
|
+
|
|
4
|
+
const iphone = { width: 393, height: 852 };
|
|
5
|
+
const iphoneInsets = { top: 59, bottom: 34, left: 0, right: 0 };
|
|
6
|
+
const menu = { width: 250, height: 200 };
|
|
7
|
+
|
|
8
|
+
describe("placeMenu", () => {
|
|
9
|
+
it("opens below a trigger near the top instead of covering the status bar and Dynamic Island", () => {
|
|
10
|
+
const placement = placeMenu({ x: 337, y: 66, width: 44, height: 44 }, menu, iphone, iphoneInsets);
|
|
11
|
+
expect(placement.side).toBe("below");
|
|
12
|
+
expect(placement.y).toBe(66 + 44 + 6);
|
|
13
|
+
expect(placement.y).toBeGreaterThanOrEqual(iphoneInsets.top + 8);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("flips above only when there is more room above", () => {
|
|
17
|
+
const placement = placeMenu({ x: 20, y: 700, width: 44, height: 44 }, menu, iphone, iphoneInsets);
|
|
18
|
+
expect(placement.side).toBe("above");
|
|
19
|
+
expect(placement.y).toBe(700 - 6 - 200);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("stays below when the menu fits below even if there is more room above", () => {
|
|
23
|
+
const placement = placeMenu({ x: 20, y: 500, width: 44, height: 44 }, menu, iphone, iphoneInsets);
|
|
24
|
+
expect(placement.side).toBe("below");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("shifts sideways to stay inside the screen edges", () => {
|
|
28
|
+
expect(placeMenu({ x: 349, y: 300, width: 44, height: 44 }, menu, iphone, iphoneInsets).x).toBe(393 - 8 - 250);
|
|
29
|
+
expect(placeMenu({ x: 0, y: 300, width: 44, height: 44 }, menu, iphone, iphoneInsets).x).toBe(8);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("respects landscape side insets", () => {
|
|
33
|
+
const landscape = { width: 852, height: 393 };
|
|
34
|
+
const placement = placeMenu({ x: 0, y: 100, width: 44, height: 44 }, menu, landscape, { top: 0, bottom: 21, left: 59, right: 59 });
|
|
35
|
+
expect(placement.x).toBe(59 + 8);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("never covers the home indicator or the keyboard", () => {
|
|
39
|
+
const placement = placeMenu({ x: 100, y: 760, width: 44, height: 44 }, { width: 250, height: 120 }, iphone, iphoneInsets);
|
|
40
|
+
expect(placement.y + 120).toBeLessThanOrEqual(852 - 34 - 8);
|
|
41
|
+
const withKeyboard = placeMenu({ x: 100, y: 420, width: 44, height: 44 }, { width: 250, height: 120 }, iphone, iphoneInsets, 336);
|
|
42
|
+
expect(withKeyboard.y + 120).toBeLessThanOrEqual(852 - 336 - 8);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("caps a menu taller than the safe area and lets it scroll", () => {
|
|
46
|
+
const placement = placeMenu({ x: 100, y: 300, width: 44, height: 44 }, { width: 250, height: 2000 }, iphone, iphoneInsets);
|
|
47
|
+
expect(placement.maxHeight).toBe(852 - 34 - 8 - (59 + 8));
|
|
48
|
+
expect(placement.y).toBe(59 + 8);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type Rect = { readonly x: number; readonly y: number; readonly width: number; readonly height: number };
|
|
2
|
+
export type Insets = { readonly top: number; readonly bottom: number; readonly left: number; readonly right: number };
|
|
3
|
+
|
|
4
|
+
export type MenuPlacement = {
|
|
5
|
+
readonly x: number;
|
|
6
|
+
readonly y: number;
|
|
7
|
+
/** Set when the menu is taller than the room inside the safe area; the menu then scrolls. */
|
|
8
|
+
readonly maxHeight: number | null;
|
|
9
|
+
readonly side: "below" | "above";
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Gap between the trigger and the menu. */
|
|
13
|
+
const GAP = 6;
|
|
14
|
+
/** Distance kept from the safe area's edges. */
|
|
15
|
+
const MARGIN = 8;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Places a menu next to its trigger, as the mockup's `fitBox` does: below the trigger unless
|
|
19
|
+
* there's more room above, shifted sideways to stay on screen, and always inside the safe area
|
|
20
|
+
* (clear of the status bar, the Dynamic Island, the home indicator and the keyboard).
|
|
21
|
+
*/
|
|
22
|
+
export function placeMenu(trigger: Rect, menu: { readonly width: number; readonly height: number }, screen: { readonly width: number; readonly height: number }, insets: Insets, keyboardHeight = 0): MenuPlacement {
|
|
23
|
+
const top = insets.top + MARGIN;
|
|
24
|
+
const bottom = screen.height - Math.max(insets.bottom, keyboardHeight) - MARGIN;
|
|
25
|
+
const left = insets.left + MARGIN;
|
|
26
|
+
const right = screen.width - insets.right - MARGIN;
|
|
27
|
+
const maxHeight = menu.height > bottom - top ? bottom - top : null;
|
|
28
|
+
const height = maxHeight ?? menu.height;
|
|
29
|
+
const roomBelow = bottom - (trigger.y + trigger.height + GAP);
|
|
30
|
+
const roomAbove = trigger.y - GAP - top;
|
|
31
|
+
const below = height <= roomBelow || roomBelow >= roomAbove;
|
|
32
|
+
const y = below ? trigger.y + trigger.height + GAP : trigger.y - GAP - height;
|
|
33
|
+
return {
|
|
34
|
+
x: clamp(trigger.x + trigger.width / 2 - menu.width / 2, left, right - menu.width),
|
|
35
|
+
y: clamp(y, top, bottom - height),
|
|
36
|
+
maxHeight,
|
|
37
|
+
side: below ? "below" : "above",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function clamp(value: number, min: number, max: number): number {
|
|
42
|
+
return Math.max(min, Math.min(max, value));
|
|
43
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { memo } from "react";
|
|
2
|
+
import { StyleSheet, Text, View } from "react-native";
|
|
3
|
+
import { useMeasuredRef } from "../../frame/measurement";
|
|
4
|
+
import { useTheme } from "../../theme/theme";
|
|
5
|
+
import { fontFamilies } from "../../theme/tokens";
|
|
6
|
+
import { ChalkText } from "../ChalkText";
|
|
7
|
+
import { IconButton } from "../IconButton";
|
|
8
|
+
import { PillButton } from "../PillButton";
|
|
9
|
+
import type { Toast as ToastModel } from "./toast-store";
|
|
10
|
+
|
|
11
|
+
export type ToastProps = {
|
|
12
|
+
readonly toast: ToastModel;
|
|
13
|
+
readonly onDismiss: () => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** One event notice (`.toast`). Every notice in the call uses this look. */
|
|
17
|
+
export const Toast = memo(function Toast({ toast, onDismiss }: ToastProps): React.JSX.Element {
|
|
18
|
+
const { colors, elevation } = useTheme();
|
|
19
|
+
const parts = typeof toast.message === "string" ? [toast.message] : toast.message;
|
|
20
|
+
const testID = toast.testID ?? `toast.${toast.id}`;
|
|
21
|
+
const toastStyle = [styles.toast, { backgroundColor: colors.popover, borderColor: colors.line, boxShadow: elevation.toast }];
|
|
22
|
+
const ref = useMeasuredRef<View>(testID, toastStyle);
|
|
23
|
+
return (
|
|
24
|
+
<View accessibilityLiveRegion={toast.kind === "error" ? "assertive" : "polite"} accessibilityRole={toast.kind === "error" ? "alert" : "summary"} ref={ref} style={toastStyle} testID={testID}>
|
|
25
|
+
{toast.leading}
|
|
26
|
+
<ChalkText numberOfLines={toast.wrap ? undefined : 1} style={[styles.message, toast.wrap ? styles.wrapped : null]}>
|
|
27
|
+
{parts.map((part, index) =>
|
|
28
|
+
typeof part === "string" ? (
|
|
29
|
+
part
|
|
30
|
+
) : (
|
|
31
|
+
<Text key={index} style={styles.strong}>
|
|
32
|
+
{part.strong}
|
|
33
|
+
</Text>
|
|
34
|
+
),
|
|
35
|
+
)}
|
|
36
|
+
</ChalkText>
|
|
37
|
+
{toast.actions?.map((action) => (
|
|
38
|
+
<PillButton key={action.label} label={action.label} onPress={action.onPress} testID={action.testID} variant={action.variant ?? "secondary"} />
|
|
39
|
+
))}
|
|
40
|
+
{toast.dismissible ? <IconButton accessibilityLabel="Dismiss" icon="x" onPress={onDismiss} size={36} tone="ghost" /> : null}
|
|
41
|
+
</View>
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const styles = StyleSheet.create({
|
|
46
|
+
toast: { flexDirection: "row", alignItems: "center", gap: 10, minHeight: 48, paddingVertical: 6, paddingRight: 6, paddingLeft: 14, borderWidth: 1, borderRadius: 12 },
|
|
47
|
+
message: { flexShrink: 1, marginRight: 4 },
|
|
48
|
+
wrapped: { lineHeight: 14 * 1.35, paddingVertical: 4 },
|
|
49
|
+
strong: { fontFamily: fontFamilies[600] },
|
|
50
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo, useState, useSyncExternalStore, type ReactNode } from "react";
|
|
2
|
+
import { StyleSheet, View } from "react-native";
|
|
3
|
+
import Animated, { useReducedMotion, withTiming, type EntryExitAnimationFunction } from "react-native-reanimated";
|
|
4
|
+
import { useDeviceLayout } from "../../frame/device-layout";
|
|
5
|
+
import { usePortalCount } from "../../frame/portal";
|
|
6
|
+
import { timings } from "../../motion/motion";
|
|
7
|
+
import { Toast } from "./Toast";
|
|
8
|
+
import { createToastStore, type ToastStore } from "./toast-store";
|
|
9
|
+
|
|
10
|
+
const ToastContext = createContext<ToastStore | null>(null);
|
|
11
|
+
|
|
12
|
+
/** `bottom` is the height the stack keeps above the safe area; null means the call dock's clearance. */
|
|
13
|
+
type ToastClearance = { readonly bottom: number | null; readonly setBottom: (bottom: number | null) => void };
|
|
14
|
+
const ToastClearanceContext = createContext<ToastClearance | null>(null);
|
|
15
|
+
|
|
16
|
+
export function ToastProvider({ children }: { readonly children: ReactNode }): React.JSX.Element {
|
|
17
|
+
const [store] = useState(createToastStore);
|
|
18
|
+
const [bottom, setBottom] = useState<number | null>(null);
|
|
19
|
+
const clearance = useMemo<ToastClearance>(() => ({ bottom, setBottom }), [bottom]);
|
|
20
|
+
return (
|
|
21
|
+
<ToastContext.Provider value={store}>
|
|
22
|
+
<ToastClearanceContext.Provider value={clearance}>{children}</ToastClearanceContext.Provider>
|
|
23
|
+
</ToastContext.Provider>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Shows and dismisses event notices: `const toasts = useToasts(); toasts.show({ message: [{ strong: "Nora" }, " joined"] })`. */
|
|
28
|
+
export function useToasts(): Pick<ToastStore, "show" | "dismiss" | "clear"> {
|
|
29
|
+
const store = useContext(ToastContext);
|
|
30
|
+
if (!store) throw new Error("useToasts must be used inside <Chalk>.");
|
|
31
|
+
return store;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Lifts the notices above a screen's own bottom controls, such as Home's buttons, which are taller
|
|
36
|
+
* than the call dock. Call it with null when the screen unmounts.
|
|
37
|
+
*/
|
|
38
|
+
export function useToastClearance(): (bottom: number | null) => void {
|
|
39
|
+
const clearance = useContext(ToastClearanceContext);
|
|
40
|
+
if (!clearance) throw new Error("useToastClearance must be used inside <Chalk>.");
|
|
41
|
+
return clearance.setBottom;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The stack of notices, centred above the dock. While a phone sheet is up, the stack moves to
|
|
46
|
+
* the top of the screen with the newest notice nearest the top edge, as in the mockup.
|
|
47
|
+
*/
|
|
48
|
+
export function ToastStack(): React.JSX.Element {
|
|
49
|
+
const store = useContext(ToastContext);
|
|
50
|
+
const clearance = useContext(ToastClearanceContext);
|
|
51
|
+
if (!store || !clearance) throw new Error("ToastStack must be inside <Chalk>.");
|
|
52
|
+
const toasts = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
53
|
+
const layout = useDeviceLayout();
|
|
54
|
+
const sheetUp = usePortalCount("sheets") > 0;
|
|
55
|
+
const reduceMotion = useReducedMotion();
|
|
56
|
+
const atTop = layout.phone && sheetUp;
|
|
57
|
+
const offset = layout.arrangement === "pad" ? 88 : layout.arrangement === "landscape" ? 70 : 76;
|
|
58
|
+
const position = atTop ? { top: layout.insets.top + 8, flexDirection: "column-reverse" as const } : { bottom: layout.insets.bottom + (clearance.bottom ?? offset) };
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<View pointerEvents="box-none" style={[styles.stack, position]} testID="frame.toasts">
|
|
62
|
+
{toasts.map((toast) => (
|
|
63
|
+
<Animated.View entering={reduceMotion ? undefined : toastEntering} exiting={reduceMotion ? undefined : toastExiting} key={toast.id} style={{ maxWidth: layout.width - 32 }}>
|
|
64
|
+
<Toast onDismiss={() => store.dismiss(toast.id)} toast={toast} />
|
|
65
|
+
</Animated.View>
|
|
66
|
+
))}
|
|
67
|
+
</View>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const toastEntering: EntryExitAnimationFunction = () => {
|
|
72
|
+
"worklet";
|
|
73
|
+
return {
|
|
74
|
+
initialValues: { opacity: 0, transform: [{ translateY: 8 }] },
|
|
75
|
+
animations: { opacity: withTiming(1, timings.toastIn), transform: [{ translateY: withTiming(0, timings.toastIn) }] },
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const toastExiting: EntryExitAnimationFunction = () => {
|
|
80
|
+
"worklet";
|
|
81
|
+
return {
|
|
82
|
+
initialValues: { opacity: 1, transform: [{ translateY: 0 }] },
|
|
83
|
+
animations: { opacity: withTiming(0, timings.toastOut), transform: [{ translateY: withTiming(4, timings.toastOut) }] },
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const styles = StyleSheet.create({
|
|
88
|
+
stack: { position: "absolute", left: 0, right: 0, alignItems: "center", gap: 8 },
|
|
89
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { durations } from "../../theme/tokens";
|
|
3
|
+
import { announce } from "../announce";
|
|
4
|
+
|
|
5
|
+
/** `info` leaves on its own after 5 s; `error` and `knock` stay until acted on or dismissed. */
|
|
6
|
+
export type ToastKind = "info" | "error" | "knock";
|
|
7
|
+
|
|
8
|
+
/** A message is plain text with optional bold runs, such as a person's name. */
|
|
9
|
+
export type MessagePart = string | { readonly strong: string };
|
|
10
|
+
|
|
11
|
+
export type ToastAction = {
|
|
12
|
+
readonly label: string;
|
|
13
|
+
readonly onPress: () => void;
|
|
14
|
+
readonly variant?: "primary" | "secondary";
|
|
15
|
+
readonly testID?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ToastInput = {
|
|
19
|
+
readonly kind?: ToastKind;
|
|
20
|
+
/** An icon or a small avatar in front of the message. */
|
|
21
|
+
readonly leading?: ReactNode;
|
|
22
|
+
readonly message: string | readonly MessagePart[];
|
|
23
|
+
readonly actions?: readonly ToastAction[];
|
|
24
|
+
/** Adds the close button; errors usually have one. */
|
|
25
|
+
readonly dismissible?: boolean;
|
|
26
|
+
/** Lets a long message wrap instead of trimming it to one line. */
|
|
27
|
+
readonly wrap?: boolean;
|
|
28
|
+
readonly testID?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type Toast = ToastInput & { readonly id: number; readonly kind: ToastKind };
|
|
32
|
+
|
|
33
|
+
export type ToastStore = {
|
|
34
|
+
readonly show: (input: ToastInput) => number;
|
|
35
|
+
readonly dismiss: (id: number) => void;
|
|
36
|
+
readonly clear: () => void;
|
|
37
|
+
readonly subscribe: (listener: () => void) => () => void;
|
|
38
|
+
readonly getSnapshot: () => readonly Toast[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** At most three notices show at once; a fourth pushes out the oldest. */
|
|
42
|
+
const MAX_VISIBLE = 3;
|
|
43
|
+
|
|
44
|
+
export function messageText(message: ToastInput["message"]): string {
|
|
45
|
+
return typeof message === "string" ? message : message.map((part) => (typeof part === "string" ? part : part.strong)).join("");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createToastStore(): ToastStore {
|
|
49
|
+
let toasts: readonly Toast[] = [];
|
|
50
|
+
let nextId = 0;
|
|
51
|
+
const timers = new Map<number, ReturnType<typeof setTimeout>>();
|
|
52
|
+
const listeners = new Set<() => void>();
|
|
53
|
+
const publish = (next: readonly Toast[]) => {
|
|
54
|
+
toasts = next;
|
|
55
|
+
for (const listener of listeners) listener();
|
|
56
|
+
};
|
|
57
|
+
const dismiss = (id: number) => {
|
|
58
|
+
clearTimeout(timers.get(id));
|
|
59
|
+
timers.delete(id);
|
|
60
|
+
if (toasts.some((toast) => toast.id === id)) publish(toasts.filter((toast) => toast.id !== id));
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
show(input) {
|
|
64
|
+
const toast: Toast = { ...input, id: ++nextId, kind: input.kind ?? "info" };
|
|
65
|
+
const kept = toasts.slice(Math.max(0, toasts.length - (MAX_VISIBLE - 1)));
|
|
66
|
+
for (const dropped of toasts.slice(0, toasts.length - kept.length)) dismiss(dropped.id);
|
|
67
|
+
publish([...kept, toast]);
|
|
68
|
+
if (toast.kind === "info")
|
|
69
|
+
timers.set(
|
|
70
|
+
toast.id,
|
|
71
|
+
setTimeout(() => dismiss(toast.id), durations.toastInfo),
|
|
72
|
+
);
|
|
73
|
+
announce(messageText(toast.message));
|
|
74
|
+
return toast.id;
|
|
75
|
+
},
|
|
76
|
+
dismiss,
|
|
77
|
+
clear() {
|
|
78
|
+
for (const toast of toasts) dismiss(toast.id);
|
|
79
|
+
},
|
|
80
|
+
subscribe(listener) {
|
|
81
|
+
listeners.add(listener);
|
|
82
|
+
return () => listeners.delete(listener);
|
|
83
|
+
},
|
|
84
|
+
getSnapshot: () => toasts,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { FaceAvatar } from "../../avatar/FaceAvatar";
|
|
3
|
+
import { usePreferences } from "../../frame/preferences";
|
|
4
|
+
import { useChat, useSelf } from "../../hooks/space-hooks";
|
|
5
|
+
import { announce } from "../../primitives/announce";
|
|
6
|
+
import { useToasts } from "../../primitives/toast/ToastStack";
|
|
7
|
+
import { playSound } from "../../sounds/sounds";
|
|
8
|
+
import { messageSummary } from "./chat-model";
|
|
9
|
+
|
|
10
|
+
export type ChatNoticesProps = {
|
|
11
|
+
/** True while the Chat tab is in view; new messages then only sound and are announced. */
|
|
12
|
+
readonly chatVisible: boolean;
|
|
13
|
+
/** True while the People and Chat sheet is open on either tab; the Chat tab's dot shows instead of a toast. */
|
|
14
|
+
readonly sheetOpen: boolean;
|
|
15
|
+
readonly onOpenChat: () => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* New messages from others (F4): the message sound and an announcement, and while the sheet is
|
|
20
|
+
* closed a notice with the sender's first name, the message and Reply. Messages already in the
|
|
21
|
+
* history when the call opens stay quiet.
|
|
22
|
+
*/
|
|
23
|
+
export function ChatNotices({ chatVisible, sheetOpen, onOpenChat }: ChatNoticesProps): null {
|
|
24
|
+
const { messages, status } = useChat();
|
|
25
|
+
const self = useSelf();
|
|
26
|
+
const toasts = useToasts();
|
|
27
|
+
const { soundsEnabled } = usePreferences();
|
|
28
|
+
const seen = useRef<ReadonlySet<string> | null>(null);
|
|
29
|
+
const latest = useRef({ chatVisible, sheetOpen, onOpenChat, soundsEnabled });
|
|
30
|
+
latest.current = { chatVisible, sheetOpen, onOpenChat, soundsEnabled };
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (status !== "ready") return;
|
|
34
|
+
const previous = seen.current;
|
|
35
|
+
seen.current = new Set(messages.map((message) => message.messageId));
|
|
36
|
+
if (!previous) return;
|
|
37
|
+
const fresh = messages.filter((message) => !previous.has(message.messageId) && message.participantId !== self.participantId);
|
|
38
|
+
const now = latest.current;
|
|
39
|
+
for (const message of fresh) {
|
|
40
|
+
const text = messageSummary(message.text, message.attachments);
|
|
41
|
+
playSound("message", now.soundsEnabled);
|
|
42
|
+
if (now.chatVisible || now.sheetOpen) {
|
|
43
|
+
announce(`New message from ${message.displayName}: ${text}`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// The notice announces itself.
|
|
47
|
+
const id = toasts.show({
|
|
48
|
+
leading: <FaceAvatar name={message.displayName} size={28} />,
|
|
49
|
+
message: [{ strong: firstName(message.displayName) }, ` ${text}`],
|
|
50
|
+
actions: [
|
|
51
|
+
{
|
|
52
|
+
label: "Reply",
|
|
53
|
+
onPress: () => {
|
|
54
|
+
toasts.dismiss(id);
|
|
55
|
+
latest.current.onOpenChat();
|
|
56
|
+
},
|
|
57
|
+
testID: "toast.chat.reply",
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
testID: "toast.chat",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}, [messages, self.participantId, status, toasts]);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function firstName(name: string): string {
|
|
68
|
+
return name.trim().split(/\s+/)[0] ?? name;
|
|
69
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { ChatAttachment } from "@q9labsai/chalk-client";
|
|
2
|
+
import { memo, useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { FlatList, Pressable, StyleSheet, View, type ListRenderItemInfo } from "react-native";
|
|
4
|
+
import { FaceAvatar } from "../../avatar/FaceAvatar";
|
|
5
|
+
import { useSpaceClient } from "../../context/space-client-context";
|
|
6
|
+
import { useChat, useSelf } from "../../hooks/space-hooks";
|
|
7
|
+
import { Icon } from "../../icons/Icon";
|
|
8
|
+
import { ChalkText } from "../../primitives/ChalkText";
|
|
9
|
+
import { useTheme } from "../../theme/theme";
|
|
10
|
+
import { fontFamilies } from "../../theme/tokens";
|
|
11
|
+
import { FileCard, Thumbnails } from "./attachments";
|
|
12
|
+
import { fileKind, groupMessages, messageSummary, type ChatEntry, type MessageGroup } from "./chat-model";
|
|
13
|
+
import { Composer } from "./Composer";
|
|
14
|
+
import { PhotoViewer, type ViewedPhotos } from "./PhotoViewer";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The Chat tab: the call's messages, newest at the bottom, and the composer under them. The list
|
|
18
|
+
* is inverted, so it opens at the newest message and stays there as new ones arrive.
|
|
19
|
+
*/
|
|
20
|
+
export function ChatTab(): React.JSX.Element {
|
|
21
|
+
const client = useSpaceClient();
|
|
22
|
+
const chat = useChat();
|
|
23
|
+
const self = useSelf();
|
|
24
|
+
const [retried, setRetried] = useState<ReadonlySet<string>>(() => new Set());
|
|
25
|
+
const [viewing, setViewing] = useState<ViewedPhotos | null>(null);
|
|
26
|
+
const groups = useMemo(() => groupMessages(chat.messages, chat.pendingSends, self, retried).slice().reverse(), [chat.messages, chat.pendingSends, retried, self]);
|
|
27
|
+
|
|
28
|
+
const retry = useCallback(
|
|
29
|
+
(entry: ChatEntry) => {
|
|
30
|
+
const id = entry.clientMessageId;
|
|
31
|
+
if (!id) return;
|
|
32
|
+
setRetried((previous) => new Set(previous).add(id));
|
|
33
|
+
// The new attempt shows up as its own pending send, failed again if it fails.
|
|
34
|
+
client.chat.send({ text: entry.text, attachments: entry.attachments }).catch(() => undefined);
|
|
35
|
+
},
|
|
36
|
+
[client],
|
|
37
|
+
);
|
|
38
|
+
const loadOlder = useCallback(() => {
|
|
39
|
+
if (!chat.pagination.hasOlder || chat.status !== "ready") return;
|
|
40
|
+
// Older history is a nicety; failing to load it leaves the list as it is.
|
|
41
|
+
client.chat.loadOlder().catch(() => undefined);
|
|
42
|
+
}, [chat.pagination.hasOlder, chat.status, client]);
|
|
43
|
+
|
|
44
|
+
const renderItem = useCallback(({ item }: ListRenderItemInfo<MessageGroup>) => <GroupRow group={item} onOpenPhotos={setViewing} onRetry={retry} />, [retry]);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
{groups.length === 0 ? (
|
|
49
|
+
<View style={styles.empty} testID="chat.empty">
|
|
50
|
+
<ChalkText variant="bodyStrong">No messages yet</ChalkText>
|
|
51
|
+
<ChalkText tone="textMuted" variant="meta">
|
|
52
|
+
Messages are seen by everyone in the call.
|
|
53
|
+
</ChalkText>
|
|
54
|
+
</View>
|
|
55
|
+
) : (
|
|
56
|
+
<FlatList
|
|
57
|
+
contentContainerStyle={styles.content}
|
|
58
|
+
data={groups}
|
|
59
|
+
initialNumToRender={10}
|
|
60
|
+
inverted
|
|
61
|
+
keyboardDismissMode="interactive"
|
|
62
|
+
keyboardShouldPersistTaps="handled"
|
|
63
|
+
keyExtractor={(group) => group.key}
|
|
64
|
+
maxToRenderPerBatch={10}
|
|
65
|
+
onEndReached={loadOlder}
|
|
66
|
+
onEndReachedThreshold={0.5}
|
|
67
|
+
renderItem={renderItem}
|
|
68
|
+
style={styles.list}
|
|
69
|
+
testID="chat.list"
|
|
70
|
+
windowSize={9}
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
<Composer />
|
|
74
|
+
{viewing ? <PhotoViewer onChange={(index) => setViewing({ ...viewing, index })} onClose={() => setViewing(null)} viewing={viewing} /> : null}
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type GroupRowProps = {
|
|
80
|
+
readonly group: MessageGroup;
|
|
81
|
+
readonly onRetry: (entry: ChatEntry) => void;
|
|
82
|
+
readonly onOpenPhotos: (viewing: ViewedPhotos) => void;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** One person's run of messages (`.mg`): their face, name and time once, then each message. Yours sit on the right without a face. */
|
|
86
|
+
const GroupRow = memo(function GroupRow({ group, onRetry, onOpenPhotos }: GroupRowProps): React.JSX.Element {
|
|
87
|
+
const { mine } = group;
|
|
88
|
+
return (
|
|
89
|
+
<View style={[styles.group, mine ? styles.groupMine : null]} testID={`chat.group.${group.key}`}>
|
|
90
|
+
{mine ? null : (
|
|
91
|
+
<View accessibilityElementsHidden importantForAccessibility="no-hide-descendants">
|
|
92
|
+
<FaceAvatar name={group.displayName} size={28} />
|
|
93
|
+
</View>
|
|
94
|
+
)}
|
|
95
|
+
<View style={[styles.column, mine ? styles.columnMine : null]}>
|
|
96
|
+
<View accessibilityElementsHidden importantForAccessibility="no-hide-descendants" style={styles.header}>
|
|
97
|
+
{mine ? null : (
|
|
98
|
+
<ChalkText numberOfLines={1} style={styles.name} variant="label">
|
|
99
|
+
{group.displayName}
|
|
100
|
+
</ChalkText>
|
|
101
|
+
)}
|
|
102
|
+
<ChalkText style={styles.time} tabular tone="textMuted" variant="label">
|
|
103
|
+
{group.time}
|
|
104
|
+
</ChalkText>
|
|
105
|
+
</View>
|
|
106
|
+
{group.entries.map((entry) => (
|
|
107
|
+
<Message entry={entry} from={mine ? "You" : group.displayName} key={entry.key} mine={mine} onOpenPhotos={(photos, index) => onOpenPhotos({ photos, index, from: `${mine ? "You" : group.displayName}, ${group.time}` })} onRetry={onRetry} />
|
|
108
|
+
))}
|
|
109
|
+
</View>
|
|
110
|
+
</View>
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
type MessageProps = {
|
|
115
|
+
readonly entry: ChatEntry;
|
|
116
|
+
readonly from: string;
|
|
117
|
+
readonly mine: boolean;
|
|
118
|
+
readonly onRetry: (entry: ChatEntry) => void;
|
|
119
|
+
readonly onOpenPhotos: (photos: readonly ChatAttachment[], index: number) => void;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
function Message({ entry, from, mine, onRetry, onOpenPhotos }: MessageProps): React.JSX.Element {
|
|
123
|
+
const { colors } = useTheme();
|
|
124
|
+
const photos = entry.attachments.filter((attachment) => fileKind(attachment.mimeType) === "image");
|
|
125
|
+
const files = entry.attachments.filter((attachment) => fileKind(attachment.mimeType) !== "image");
|
|
126
|
+
const sent = entry.state === "sent";
|
|
127
|
+
const summary = messageSummary(entry.text, entry.attachments);
|
|
128
|
+
const stateLabel = entry.state === "failed" ? ", couldn't send" : sent ? "" : ", sending";
|
|
129
|
+
return (
|
|
130
|
+
<View style={[styles.message, mine ? styles.messageMine : null]} testID={`chat.message.${entry.key}`}>
|
|
131
|
+
<View accessibilityLabel={`${from}: ${summary}${stateLabel}`} accessible={photos.length === 0 && files.length === 0} style={[styles.body, mine ? styles.messageMine : null, sent ? null : styles.pending]}>
|
|
132
|
+
{entry.text ? (
|
|
133
|
+
<View style={[styles.bubble, { backgroundColor: colors.raised }]}>
|
|
134
|
+
<ChalkText variant="body">{entry.text}</ChalkText>
|
|
135
|
+
</View>
|
|
136
|
+
) : null}
|
|
137
|
+
{photos.length > 0 ? <Thumbnails disabled={!sent} onOpen={(index) => onOpenPhotos(photos, index)} photos={photos} /> : null}
|
|
138
|
+
{files.map((file) => (
|
|
139
|
+
<FileCard attachment={file} key={file.attachmentId} sent={sent} />
|
|
140
|
+
))}
|
|
141
|
+
</View>
|
|
142
|
+
{entry.state === "queued" || entry.state === "sending" ? (
|
|
143
|
+
<ChalkText style={styles.status} tone="textMuted" variant="caption">
|
|
144
|
+
Sending…
|
|
145
|
+
</ChalkText>
|
|
146
|
+
) : null}
|
|
147
|
+
{entry.state === "failed" ? (
|
|
148
|
+
<View style={styles.failed}>
|
|
149
|
+
<Icon color={colors.dangerText} name="alert" size={14} />
|
|
150
|
+
<ChalkText color={colors.dangerText} style={styles.failedText} variant="label">
|
|
151
|
+
Couldn’t send.
|
|
152
|
+
</ChalkText>
|
|
153
|
+
<Pressable accessibilityLabel="Retry sending" accessibilityRole="button" hitSlop={12} onPress={() => onRetry(entry)} testID={`chat.retry.${entry.key}`}>
|
|
154
|
+
<ChalkText style={styles.retry} variant="label">
|
|
155
|
+
Retry
|
|
156
|
+
</ChalkText>
|
|
157
|
+
</Pressable>
|
|
158
|
+
</View>
|
|
159
|
+
) : null}
|
|
160
|
+
</View>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const styles = StyleSheet.create({
|
|
165
|
+
list: { flex: 1 },
|
|
166
|
+
// `.cbody`: 8 pt around the messages.
|
|
167
|
+
content: { padding: 8 },
|
|
168
|
+
empty: { flex: 1, alignItems: "center", justifyContent: "center", gap: 4, padding: 24 },
|
|
169
|
+
group: { flexDirection: "row", alignItems: "flex-start", gap: 8, marginTop: 12, marginHorizontal: 4 },
|
|
170
|
+
groupMine: { flexDirection: "row-reverse" },
|
|
171
|
+
column: { flexShrink: 1, minWidth: 0, maxWidth: 240, alignItems: "flex-start", gap: 3 },
|
|
172
|
+
columnMine: { alignItems: "flex-end" },
|
|
173
|
+
header: { flexDirection: "row", alignItems: "baseline", marginHorizontal: 4, marginBottom: 1 },
|
|
174
|
+
name: { fontFamily: fontFamilies[600], marginRight: 6, flexShrink: 1 },
|
|
175
|
+
time: { fontFamily: fontFamilies[400] },
|
|
176
|
+
message: { maxWidth: "100%", alignItems: "flex-start", gap: 4 },
|
|
177
|
+
messageMine: { alignItems: "flex-end" },
|
|
178
|
+
body: { maxWidth: "100%", alignItems: "flex-start", gap: 4 },
|
|
179
|
+
pending: { opacity: 0.55 },
|
|
180
|
+
bubble: { borderRadius: 18, paddingVertical: 8, paddingHorizontal: 12 },
|
|
181
|
+
status: { fontFamily: fontFamilies[400], marginHorizontal: 6 },
|
|
182
|
+
failed: { flexDirection: "row", alignItems: "center", gap: 4, marginHorizontal: 6 },
|
|
183
|
+
failedText: { fontFamily: fontFamilies[400] },
|
|
184
|
+
// `.lnk`: the text colour, underlined, 2 pt by 4 pt of padding.
|
|
185
|
+
retry: { fontFamily: fontFamilies[600], textDecorationLine: "underline", paddingVertical: 2, paddingHorizontal: 4 },
|
|
186
|
+
});
|