@q9labsai/chalk-react-native 4.1.14 → 4.1.16
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 +11 -0
- package/dist/android.js +5 -2
- package/dist/{chunk-MK6OO4V7.js → chunk-2OS7SSXE.js} +6 -1
- package/dist/{chunk-4NLA35HE.js → chunk-ITLUHTVS.js} +1126 -644
- package/dist/client.js +1 -1
- package/dist/components/Chalk.d.ts +2 -1
- package/dist/components/Chalk.d.ts.map +1 -1
- package/dist/components/EmbeddedWhiteboard.d.ts.map +1 -1
- package/dist/components/SpaceView.d.ts +2 -1
- package/dist/components/SpaceView.d.ts.map +1 -1
- package/dist/components/SpaceView.shared.d.ts.map +1 -1
- package/dist/components/native-space-view/FeedbackSheet.d.ts +12 -0
- package/dist/components/native-space-view/FeedbackSheet.d.ts.map +1 -0
- package/dist/components/native-space-view/SpaceActionMenu.d.ts +2 -1
- package/dist/components/native-space-view/SpaceActionMenu.d.ts.map +1 -1
- package/dist/components/native-space-view/SpaceChatAttachment.d.ts +8 -0
- package/dist/components/native-space-view/SpaceChatAttachment.d.ts.map +1 -0
- package/dist/components/native-space-view/SpaceChatSheet.d.ts.map +1 -1
- package/dist/components/native-space-view/SpaceMoreSheet.d.ts +2 -1
- package/dist/components/native-space-view/SpaceMoreSheet.d.ts.map +1 -1
- package/dist/components/native-space-view/SpaceTopBar.android.d.ts +3 -1
- package/dist/components/native-space-view/SpaceTopBar.android.d.ts.map +1 -1
- package/dist/components/native-space-view/screen-share-availability.d.ts +4 -1
- package/dist/components/native-space-view/screen-share-availability.d.ts.map +1 -1
- package/dist/components/native-space-view/space-chat-attachments.d.ts.map +1 -1
- package/dist/components/native-space-view/useSpaceViewController.d.ts +8 -2
- package/dist/components/native-space-view/useSpaceViewController.d.ts.map +1 -1
- package/dist/feedback/native-capture.d.ts +9 -0
- package/dist/feedback/native-capture.d.ts.map +1 -0
- package/dist/feedback/native-evidence.d.ts +3 -0
- package/dist/feedback/native-evidence.d.ts.map +1 -0
- package/dist/hooks/space-hooks.d.ts +1 -0
- package/dist/hooks/space-hooks.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/ios-pad.js +5 -2
- package/dist/ios-phone.js +5 -2
- package/dist/ios.js +5 -2
- package/dist/macos.js +5 -2
- package/dist/space-client/create-native-space-client.d.ts +2 -0
- package/dist/space-client/create-native-space-client.d.ts.map +1 -1
- package/dist/whiteboard/embedded-whiteboard-viewport.d.ts +3 -0
- package/dist/whiteboard/embedded-whiteboard-viewport.d.ts.map +1 -0
- package/embedded/chalk-whiteboard/asset-manifest.json +2 -2
- package/embedded/chalk-whiteboard/renderer.js +581 -581
- package/package.json +8 -6
- package/src/components/Chalk.tsx +6 -2
- package/src/components/EmbeddedWhiteboard.tsx +14 -5
- package/src/components/SpaceView.shared.tsx +58 -47
- package/src/components/SpaceView.tsx +2 -1
- package/src/components/native-space-view/FeedbackSheet.tsx +296 -0
- package/src/components/native-space-view/SpaceActionMenu.tsx +23 -3
- package/src/components/native-space-view/SpaceChatAttachment.tsx +105 -0
- package/src/components/native-space-view/SpaceChatSheet.tsx +14 -9
- package/src/components/native-space-view/SpaceMoreSheet.tsx +35 -6
- package/src/components/native-space-view/SpaceTopBar.android.tsx +16 -1
- package/src/components/native-space-view/screen-share-availability.test.ts +16 -0
- package/src/components/native-space-view/screen-share-availability.ts +4 -1
- package/src/components/native-space-view/space-chat-attachments.test.ts +26 -0
- package/src/components/native-space-view/space-chat-attachments.ts +2 -4
- package/src/components/native-space-view/useSpaceViewController.ts +28 -8
- package/src/feedback/native-capture.ts +62 -0
- package/src/feedback/native-evidence.ts +37 -0
- package/src/hooks/space-hooks.ts +4 -0
- package/src/index.ts +1 -1
- package/src/space-client/create-native-space-client.ts +7 -0
- package/src/whiteboard/embedded-whiteboard-viewport.test.ts +13 -0
- package/src/whiteboard/embedded-whiteboard-viewport.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@q9labsai/chalk-react-native",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.16",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -150,13 +150,14 @@
|
|
|
150
150
|
"registry": "https://registry.npmjs.org/"
|
|
151
151
|
},
|
|
152
152
|
"dependencies": {
|
|
153
|
-
"@q9labsai/chalk-client": "^4.1.
|
|
154
|
-
"@q9labsai/chalk-whiteboard": "^4.1.
|
|
155
|
-
"@q9labsai/facehash": "^4.1.
|
|
153
|
+
"@q9labsai/chalk-client": "^4.1.16",
|
|
154
|
+
"@q9labsai/chalk-whiteboard": "^4.1.16",
|
|
155
|
+
"@q9labsai/facehash": "^4.1.16"
|
|
156
156
|
},
|
|
157
157
|
"devDependencies": {
|
|
158
158
|
"@types/node": "^26.2.0",
|
|
159
159
|
"@types/react": "^19.2.18",
|
|
160
|
+
"react-native-view-shot": "4.0.3",
|
|
160
161
|
"react-native-webview": "13.16.1",
|
|
161
162
|
"typescript": "^6.0.3"
|
|
162
163
|
},
|
|
@@ -168,12 +169,13 @@
|
|
|
168
169
|
"react": "^18.0.0 || ^19.0.0",
|
|
169
170
|
"react-native": ">=0.77",
|
|
170
171
|
"react-native-svg": "^15.15.3",
|
|
172
|
+
"react-native-view-shot": "^4.0.3",
|
|
171
173
|
"react-native-webview": "^13.16.1"
|
|
172
174
|
},
|
|
173
175
|
"scripts": {
|
|
174
|
-
"build": "rm -rf ./dist && tsup ./src/index.ts ./src/client.ts ./src/android.ts ./src/ios.ts ./src/ios-phone.ts ./src/ios-pad.ts ./src/macos.ts ./src/runtime.ts ./src/clipboard.ts ./src/diagnostics.ts ./src/invites.ts ./src/ui/theme.ts ./src/ui/appearance.ts --format esm --out-dir ./dist --platform browser --external react --external react-native --external react-native-webview --external @q9labsai/chalk-whiteboard --external @cloudflare/react-native-webrtc --external @cloudflare/realtimekit-react-native && pnpm run build:types && pnpm run sync:whiteboard-assets",
|
|
176
|
+
"build": "rm -rf ./dist && tsup ./src/index.ts ./src/client.ts ./src/android.ts ./src/ios.ts ./src/ios-phone.ts ./src/ios-pad.ts ./src/macos.ts ./src/runtime.ts ./src/clipboard.ts ./src/diagnostics.ts ./src/invites.ts ./src/ui/theme.ts ./src/ui/appearance.ts --format esm --out-dir ./dist --platform browser --external react --external react-native --external react-native-webview --external react-native-view-shot --external @q9labsai/chalk-whiteboard --external @cloudflare/react-native-webrtc --external @cloudflare/realtimekit-react-native && pnpm run build:types && pnpm run sync:whiteboard-assets",
|
|
175
177
|
"build:types": "rm -f tsconfig.tsbuildinfo && tsc --build tsconfig.json",
|
|
176
|
-
"dev": "tsup ./src/index.ts ./src/client.ts ./src/android.ts ./src/ios.ts ./src/ios-phone.ts ./src/ios-pad.ts ./src/macos.ts ./src/runtime.ts ./src/clipboard.ts ./src/diagnostics.ts ./src/invites.ts ./src/ui/theme.ts ./src/ui/appearance.ts --format esm --out-dir ./dist --platform browser --external react --external react-native --external react-native-webview --external @q9labsai/chalk-whiteboard --external @cloudflare/react-native-webrtc --external @cloudflare/realtimekit-react-native --watch",
|
|
178
|
+
"dev": "tsup ./src/index.ts ./src/client.ts ./src/android.ts ./src/ios.ts ./src/ios-phone.ts ./src/ios-pad.ts ./src/macos.ts ./src/runtime.ts ./src/clipboard.ts ./src/diagnostics.ts ./src/invites.ts ./src/ui/theme.ts ./src/ui/appearance.ts --format esm --out-dir ./dist --platform browser --external react --external react-native --external react-native-webview --external react-native-view-shot --external @q9labsai/chalk-whiteboard --external @cloudflare/react-native-webrtc --external @cloudflare/realtimekit-react-native --watch",
|
|
177
179
|
"sync:whiteboard-assets": "node ./scripts/sync-whiteboard-assets.mjs",
|
|
178
180
|
"test": "pnpm exec vitest run",
|
|
179
181
|
"check-types": "tsc --project tsconfig.check-types.json --noEmit",
|
package/src/components/Chalk.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatUploadFile, ClientEventMap, GetAccess, SpaceClient } from "@q9labsai/chalk-client";
|
|
1
|
+
import type { ChatUploadFile, ClientEventMap, FeedbackEvidenceInput, GetAccess, SpaceClient } from "@q9labsai/chalk-client";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
3
3
|
import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
4
4
|
|
|
@@ -65,6 +65,7 @@ type ChalkCommonProps = ChalkCallbacks & {
|
|
|
65
65
|
readonly layout?: SpaceLayout;
|
|
66
66
|
readonly onLayoutChange?: (layout: SpaceLayout) => void;
|
|
67
67
|
readonly onOpenDiagnostics?: () => void;
|
|
68
|
+
readonly feedbackEvidence?: Partial<FeedbackEvidenceInput>;
|
|
68
69
|
};
|
|
69
70
|
|
|
70
71
|
export type ChalkProps = ChalkCommonProps &
|
|
@@ -273,6 +274,7 @@ function SpaceExperience(props: ChalkProps): React.JSX.Element {
|
|
|
273
274
|
logoUrl={props.logoUrl}
|
|
274
275
|
onLayoutChange={props.onLayoutChange}
|
|
275
276
|
onOpenDiagnostics={props.onOpenDiagnostics}
|
|
277
|
+
feedbackEvidence={props.feedbackEvidence}
|
|
276
278
|
pickChatFiles={props.pickChatFiles}
|
|
277
279
|
reconnecting={connection.status === "reconnecting"}
|
|
278
280
|
spaceName={spaceName}
|
|
@@ -296,12 +298,13 @@ type NativeSpaceViewProps = {
|
|
|
296
298
|
readonly logoUrl?: string;
|
|
297
299
|
readonly onLayoutChange?: (layout: SpaceLayout) => void;
|
|
298
300
|
readonly onOpenDiagnostics?: () => void;
|
|
301
|
+
readonly feedbackEvidence?: Partial<FeedbackEvidenceInput>;
|
|
299
302
|
readonly pickChatFiles?: () => Promise<readonly ChatUploadFile[]>;
|
|
300
303
|
readonly reconnecting: boolean;
|
|
301
304
|
readonly spaceName: string;
|
|
302
305
|
};
|
|
303
306
|
|
|
304
|
-
function NativeSpaceView({ features, inviteLink, layout, logoUrl, onLayoutChange, onOpenDiagnostics, pickChatFiles, reconnecting, spaceName }: NativeSpaceViewProps): React.JSX.Element {
|
|
307
|
+
function NativeSpaceView({ features, inviteLink, layout, logoUrl, onLayoutChange, onOpenDiagnostics, feedbackEvidence, pickChatFiles, reconnecting, spaceName }: NativeSpaceViewProps): React.JSX.Element {
|
|
305
308
|
const client = useSpaceClient();
|
|
306
309
|
const theme = useNativeTheme();
|
|
307
310
|
|
|
@@ -316,6 +319,7 @@ function NativeSpaceView({ features, inviteLink, layout, logoUrl, onLayoutChange
|
|
|
316
319
|
onLayoutChange={onLayoutChange}
|
|
317
320
|
onLeave={() => client.leave()}
|
|
318
321
|
onOpenDiagnostics={onOpenDiagnostics}
|
|
322
|
+
feedbackEvidence={feedbackEvidence}
|
|
319
323
|
pickChatFiles={pickChatFiles}
|
|
320
324
|
reconnecting={reconnecting}
|
|
321
325
|
spaceName={spaceName}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { ChalkWhiteboardController, type ChalkEmbeddedWhiteboardTransport, type ChalkEmbeddedWhiteboardViewport } from "@q9labsai/chalk-whiteboard/embedded";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { ActivityIndicator, Pressable, StyleSheet, Text, useWindowDimensions, View, type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
+
import { ActivityIndicator, Pressable, StyleSheet, Text, useWindowDimensions, View, type LayoutChangeEvent, type StyleProp, type ViewStyle } from "react-native";
|
|
4
4
|
import WebView, { type WebViewMessageEvent, type WebViewNavigation } from "react-native-webview";
|
|
5
5
|
|
|
6
6
|
import { useNativeTheme } from "../ui/native-theme";
|
|
7
7
|
import { isEmbeddedWhiteboardNavigationAllowed, rendererURLWithContext, resolveEmbeddedWhiteboardRendererURL } from "../whiteboard/embedded-whiteboard-assets";
|
|
8
|
+
import { createEmbeddedWhiteboardViewport } from "../whiteboard/embedded-whiteboard-viewport";
|
|
8
9
|
|
|
9
10
|
export interface EmbeddedWhiteboardProps {
|
|
10
11
|
readonly transport: ChalkEmbeddedWhiteboardTransport;
|
|
@@ -51,7 +52,11 @@ export function EmbeddedWhiteboard({ transport, journeyId, traceparent, tracesta
|
|
|
51
52
|
const [loadAttempt, setLoadAttempt] = useState(0);
|
|
52
53
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
53
54
|
const [compatibilityNotice, setCompatibilityNotice] = useState<string | null>(null);
|
|
54
|
-
const { width, height
|
|
55
|
+
const [layout, setLayout] = useState({ width: 0, height: 0 });
|
|
56
|
+
const { scale } = useWindowDimensions();
|
|
57
|
+
const viewport = useMemo<ChalkEmbeddedWhiteboardViewport | null>(() => createEmbeddedWhiteboardViewport(layout.width, layout.height, scale), [layout.height, layout.width, scale]);
|
|
58
|
+
const latestViewportRef = useRef(viewport);
|
|
59
|
+
latestViewportRef.current = viewport;
|
|
55
60
|
onMetricRef.current = onMetric;
|
|
56
61
|
onErrorRef.current = onError;
|
|
57
62
|
onUserExportRef.current = onUserExport;
|
|
@@ -106,6 +111,7 @@ export function EmbeddedWhiteboard({ transport, journeyId, traceparent, tracesta
|
|
|
106
111
|
onUserExport: handleControllerUserExport,
|
|
107
112
|
});
|
|
108
113
|
controller.start();
|
|
114
|
+
if (latestViewportRef.current) controller.setViewport(latestViewportRef.current);
|
|
109
115
|
controllerRef.current = controller;
|
|
110
116
|
return () => {
|
|
111
117
|
if (controllerRef.current === controller) controllerRef.current = null;
|
|
@@ -117,10 +123,13 @@ export function EmbeddedWhiteboard({ transport, journeyId, traceparent, tracesta
|
|
|
117
123
|
controllerRef.current?.setCapabilities({ canDraw, canClear });
|
|
118
124
|
}, [canClear, canDraw]);
|
|
119
125
|
|
|
120
|
-
const viewport = useMemo<ChalkEmbeddedWhiteboardViewport>(() => ({ width, height, scale }), [height, scale, width]);
|
|
121
126
|
useEffect(() => {
|
|
122
|
-
controllerRef.current?.setViewport(viewport);
|
|
127
|
+
if (viewport) controllerRef.current?.setViewport(viewport);
|
|
123
128
|
}, [viewport]);
|
|
129
|
+
const handleLayout = useCallback((event: LayoutChangeEvent): void => {
|
|
130
|
+
const next = { width: event.nativeEvent.layout.width, height: event.nativeEvent.layout.height };
|
|
131
|
+
setLayout((current) => (current.width === next.width && current.height === next.height ? current : next));
|
|
132
|
+
}, []);
|
|
124
133
|
|
|
125
134
|
const sourceURL = rendererURL ? rendererURLWithContext(rendererURL, { journeyId, rendererGeneration }) : null;
|
|
126
135
|
const handleMessage = (event: WebViewMessageEvent): void => {
|
|
@@ -168,7 +177,7 @@ export function EmbeddedWhiteboard({ transport, journeyId, traceparent, tracesta
|
|
|
168
177
|
}
|
|
169
178
|
|
|
170
179
|
return (
|
|
171
|
-
<View style={[styles.container, style]} testID={testID}>
|
|
180
|
+
<View onLayout={handleLayout} style={[styles.container, style]} testID={testID}>
|
|
172
181
|
<WebView
|
|
173
182
|
key={rendererGeneration}
|
|
174
183
|
ref={webViewRef}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
1
2
|
import { Alert, StyleSheet, Text, View } from "react-native";
|
|
2
3
|
|
|
4
|
+
import { useSpaceClient } from "../context/space-client-context";
|
|
3
5
|
import { Theme } from "../ui/theme";
|
|
4
6
|
import { useNativeTheme } from "../ui/native-theme";
|
|
5
7
|
import { getIosSimulatorMediaMessage } from "../utils/ios-simulator";
|
|
@@ -8,6 +10,8 @@ import type { SpaceViewProps } from "./SpaceView";
|
|
|
8
10
|
import { SpaceGridAndroid } from "./native-space-view/SpaceGrid.android";
|
|
9
11
|
import { SpaceBottomDockAndroid } from "./native-space-view/SpaceBottomDock.android";
|
|
10
12
|
import { SpaceActionMenu, selectSpaceReaction } from "./native-space-view/SpaceActionMenu";
|
|
13
|
+
import { FeedbackSheet } from "./native-space-view/FeedbackSheet";
|
|
14
|
+
import type { FeedbackCaptureSize } from "../feedback/native-capture";
|
|
11
15
|
import { SpaceStageAndroid } from "./native-space-view/SpaceStage.android";
|
|
12
16
|
import { SpaceTopBarAndroid } from "./native-space-view/SpaceTopBar.android";
|
|
13
17
|
import { SpaceWhiteboardSurface } from "./native-space-view/SpaceWhiteboardSurface";
|
|
@@ -17,7 +21,11 @@ import { useSpaceViewController } from "./native-space-view/useSpaceViewControll
|
|
|
17
21
|
|
|
18
22
|
export function SpaceViewShared(props: SpaceViewProps): React.JSX.Element {
|
|
19
23
|
const controller = useSpaceViewController(props);
|
|
24
|
+
const client = useSpaceClient();
|
|
20
25
|
const theme = useNativeTheme();
|
|
26
|
+
const captureTarget = useRef<View>(null);
|
|
27
|
+
const [captureSize, setCaptureSize] = useState<FeedbackCaptureSize>({ width: 0, height: 0 });
|
|
28
|
+
const [feedbackRequested, setFeedbackRequested] = useState(false);
|
|
21
29
|
const toggleMedia = (action: () => void) => {
|
|
22
30
|
if (controller.simulatorMediaDisabled) {
|
|
23
31
|
Alert.alert("Media unavailable", getIosSimulatorMediaMessage());
|
|
@@ -27,55 +35,58 @@ export function SpaceViewShared(props: SpaceViewProps): React.JSX.Element {
|
|
|
27
35
|
};
|
|
28
36
|
|
|
29
37
|
return (
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<
|
|
38
|
+
<>
|
|
39
|
+
<View collapsable={false} onLayout={(event) => setCaptureSize({ width: event.nativeEvent.layout.width, height: event.nativeEvent.layout.height })} ref={captureTarget} style={[styles.space, { backgroundColor: theme.colors.darkCanvas }]}>
|
|
40
|
+
<SpaceTopBarAndroid recording={controller.recording} formattedDuration={controller.formattedDuration} logoUrl={props.logoUrl} participantCount={controller.participantCount} spaceName={controller.spaceName} />
|
|
41
|
+
{props.reconnecting ? (
|
|
42
|
+
<View accessibilityLiveRegion="polite" style={[styles.recovery, { backgroundColor: theme.colors.primary }]}>
|
|
43
|
+
<Text style={[styles.recoveryText, { color: theme.colors.primaryForeground }]}>Reconnecting… controls will resume shortly.</Text>
|
|
44
|
+
</View>
|
|
45
|
+
) : null}
|
|
46
|
+
<View style={styles.stage}>
|
|
47
|
+
{controller.whiteboard.isOpen ? (
|
|
48
|
+
<SpaceWhiteboardSurface whiteboard={controller.whiteboard} />
|
|
49
|
+
) : controller.derived.isStageMode || controller.layout.layout !== "grid" ? (
|
|
50
|
+
<SpaceStageAndroid
|
|
51
|
+
activeReactions={controller.activeReactions}
|
|
52
|
+
handRaised={controller.handRaised}
|
|
53
|
+
isCompactViewport={controller.derived.isCompactViewport}
|
|
54
|
+
isMuted={controller.isMuted}
|
|
55
|
+
layoutMode={controller.layout.layout}
|
|
56
|
+
primaryContent={controller.derived.primaryContent}
|
|
57
|
+
raisedHandCount={controller.raisedHandCount}
|
|
58
|
+
screenShareTrack={controller.derived.screenShareTrack}
|
|
59
|
+
screenSharer={controller.derived.screenSharer}
|
|
60
|
+
selfName={controller.selfName}
|
|
61
|
+
stripParticipants={controller.derived.allParticipants}
|
|
62
|
+
whiteboard={{
|
|
63
|
+
isOpen: controller.whiteboard.isOpen,
|
|
64
|
+
canDraw: controller.whiteboard.canDraw,
|
|
65
|
+
elementCount: controller.whiteboard.elements.length,
|
|
66
|
+
participantCount: controller.whiteboard.openParticipants.length,
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
) : (
|
|
70
|
+
<SpaceGridAndroid gridPages={controller.derived.gridPages} participants={controller.derived.allParticipants} />
|
|
71
|
+
)}
|
|
35
72
|
</View>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
screenSharer={controller.derived.screenSharer}
|
|
51
|
-
selfName={controller.selfName}
|
|
52
|
-
stripParticipants={controller.derived.allParticipants}
|
|
53
|
-
whiteboard={{
|
|
54
|
-
isOpen: controller.whiteboard.isOpen,
|
|
55
|
-
canDraw: controller.whiteboard.canDraw,
|
|
56
|
-
elementCount: controller.whiteboard.elements.length,
|
|
57
|
-
participantCount: controller.whiteboard.openParticipants.length,
|
|
58
|
-
}}
|
|
59
|
-
/>
|
|
60
|
-
) : (
|
|
61
|
-
<SpaceGridAndroid gridPages={controller.derived.gridPages} participants={controller.derived.allParticipants} />
|
|
62
|
-
)}
|
|
73
|
+
<SpaceBottomDockAndroid
|
|
74
|
+
isCameraOff={controller.isCameraOff}
|
|
75
|
+
isMuted={controller.isMuted}
|
|
76
|
+
onLeave={controller.handleLeave}
|
|
77
|
+
onOpenChat={controller.canChat ? () => controller.openPanel("chat") : undefined}
|
|
78
|
+
onOpenMore={() => controller.setActionsOpen(true)}
|
|
79
|
+
onToggleAudio={() => toggleMedia(controller.toggleAudio)}
|
|
80
|
+
onToggleVideo={() => toggleMedia(controller.toggleVideo)}
|
|
81
|
+
simulatorMediaDisabled={controller.simulatorMediaDisabled}
|
|
82
|
+
unreadChatCount={controller.chat.unreadCount}
|
|
83
|
+
/>
|
|
84
|
+
<SpaceActionMenu controller={controller} onOpenDiagnostics={props.onOpenDiagnostics} onOpenFeedback={() => setFeedbackRequested(true)} />
|
|
85
|
+
{controller.panel === "settings" ? <SettingsSheet controller={controller} isOpen onClose={controller.closePanel} /> : <SpacePanelSheet controller={controller} />}
|
|
86
|
+
<ReactionPicker isOpen={controller.reactionPickerOpen} onClose={() => controller.setReactionPickerOpen(false)} onSelect={(reaction) => selectSpaceReaction(controller, reaction)} />
|
|
63
87
|
</View>
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
isMuted={controller.isMuted}
|
|
67
|
-
onLeave={controller.handleLeave}
|
|
68
|
-
onOpenChat={controller.canChat ? () => controller.openPanel("chat") : undefined}
|
|
69
|
-
onOpenMore={() => controller.setActionsOpen(true)}
|
|
70
|
-
onToggleAudio={() => toggleMedia(controller.toggleAudio)}
|
|
71
|
-
onToggleVideo={() => toggleMedia(controller.toggleVideo)}
|
|
72
|
-
simulatorMediaDisabled={controller.simulatorMediaDisabled}
|
|
73
|
-
unreadChatCount={controller.chat.unreadCount}
|
|
74
|
-
/>
|
|
75
|
-
<SpaceActionMenu controller={controller} onOpenDiagnostics={props.onOpenDiagnostics} />
|
|
76
|
-
{controller.panel === "settings" ? <SettingsSheet controller={controller} isOpen onClose={controller.closePanel} /> : <SpacePanelSheet controller={controller} />}
|
|
77
|
-
<ReactionPicker isOpen={controller.reactionPickerOpen} onClose={() => controller.setReactionPickerOpen(false)} onSelect={(reaction) => selectSpaceReaction(controller, reaction)} />
|
|
78
|
-
</View>
|
|
88
|
+
<FeedbackSheet client={client} captureSize={captureSize} captureTarget={captureTarget} feedbackEvidence={props.feedbackEvidence} onClose={() => setFeedbackRequested(false)} requested={feedbackRequested} />
|
|
89
|
+
</>
|
|
79
90
|
);
|
|
80
91
|
}
|
|
81
92
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatUploadFile } from "@q9labsai/chalk-client";
|
|
1
|
+
import type { ChatUploadFile, FeedbackEvidenceInput } from "@q9labsai/chalk-client";
|
|
2
2
|
import { resolvePlatformVariant } from "../platform/platform";
|
|
3
3
|
import type { Layout } from "../ui/native-types";
|
|
4
4
|
import { SpaceViewAndroid } from "./SpaceView.android";
|
|
@@ -30,6 +30,7 @@ export interface SpaceViewProps {
|
|
|
30
30
|
onLeave: () => void | Promise<void>;
|
|
31
31
|
onEndEpisode?: () => void | Promise<void>;
|
|
32
32
|
onOpenDiagnostics?: () => void;
|
|
33
|
+
feedbackEvidence?: Partial<FeedbackEvidenceInput>;
|
|
33
34
|
onDiagnosticsChange?: (snapshot: SpaceViewDiagnosticsSnapshot) => void;
|
|
34
35
|
/** Selects files; the SDK owns bounds, upload, and send sequencing. */
|
|
35
36
|
pickChatFiles?: () => Promise<readonly ChatUploadFile[]>;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import type { FeedbackCategory, FeedbackEvidenceInput, FeedbackPrepared, FeedbackScreenshotCapture, FeedbackScreenshotUnavailable, SpaceClient } from "@q9labsai/chalk-client";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { Image, KeyboardAvoidingView, Modal, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from "react-native";
|
|
4
|
+
|
|
5
|
+
import { useNativeTheme } from "../../ui/native-theme";
|
|
6
|
+
import { Theme } from "../../ui/theme";
|
|
7
|
+
import { createNativeFeedbackEvidence } from "../../feedback/native-evidence";
|
|
8
|
+
import { captureNativeFeedbackView, type FeedbackCaptureSize, type FeedbackCaptureTarget } from "../../feedback/native-capture";
|
|
9
|
+
|
|
10
|
+
type FeedbackScreenshot = FeedbackScreenshotCapture | FeedbackScreenshotUnavailable;
|
|
11
|
+
|
|
12
|
+
export interface FeedbackSheetProps {
|
|
13
|
+
readonly client: SpaceClient;
|
|
14
|
+
readonly requested: boolean;
|
|
15
|
+
readonly captureTarget: FeedbackCaptureTarget;
|
|
16
|
+
readonly captureSize: FeedbackCaptureSize;
|
|
17
|
+
readonly feedbackEvidence?: Partial<FeedbackEvidenceInput>;
|
|
18
|
+
readonly onClose: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const categories: readonly FeedbackCategory[] = ["bug", "feature_request", "other"];
|
|
22
|
+
|
|
23
|
+
export function FeedbackSheet({ client, requested, captureTarget, captureSize, feedbackEvidence, onClose }: FeedbackSheetProps): React.JSX.Element {
|
|
24
|
+
const theme = useNativeTheme();
|
|
25
|
+
const [visible, setVisible] = useState(false);
|
|
26
|
+
const [preparing, setPreparing] = useState(false);
|
|
27
|
+
const [sending, setSending] = useState(false);
|
|
28
|
+
const [category, setCategory] = useState<FeedbackCategory>("bug");
|
|
29
|
+
const [message, setMessage] = useState("");
|
|
30
|
+
const [screenshot, setScreenshot] = useState<FeedbackScreenshot>({ state: "unavailable", failure_code: "capture_failed" });
|
|
31
|
+
const [prepared, setPrepared] = useState<FeedbackPrepared | undefined>();
|
|
32
|
+
const [error, setError] = useState<string | undefined>();
|
|
33
|
+
const [receiptId, setReceiptId] = useState<string | undefined>();
|
|
34
|
+
const evidence = useMemo(() => ({ ...createNativeFeedbackEvidence(), ...feedbackEvidence }), [feedbackEvidence]);
|
|
35
|
+
const prepareScreenshot = useCallback((nextScreenshot: FeedbackScreenshot) => client.feedback.prepare({ evidence, screenshot_provider: () => nextScreenshot }), [client.feedback, evidence]);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!requested) {
|
|
39
|
+
setVisible(false);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let active = true;
|
|
44
|
+
setPreparing(true);
|
|
45
|
+
setSending(false);
|
|
46
|
+
setCategory("bug");
|
|
47
|
+
setMessage("");
|
|
48
|
+
setError(undefined);
|
|
49
|
+
setReceiptId(undefined);
|
|
50
|
+
setPrepared(undefined);
|
|
51
|
+
|
|
52
|
+
const prepare = async (): Promise<void> => {
|
|
53
|
+
let captured: FeedbackScreenshot;
|
|
54
|
+
try {
|
|
55
|
+
captured = await captureNativeFeedbackView(captureTarget, captureSize);
|
|
56
|
+
} catch {
|
|
57
|
+
captured = { state: "unavailable", failure_code: "capture_failed" };
|
|
58
|
+
}
|
|
59
|
+
if (!active) return;
|
|
60
|
+
setScreenshot(captured);
|
|
61
|
+
try {
|
|
62
|
+
const nextPrepared = await prepareScreenshot(captured);
|
|
63
|
+
if (!active) return;
|
|
64
|
+
setPrepared(nextPrepared);
|
|
65
|
+
} catch (cause) {
|
|
66
|
+
if (!active) return;
|
|
67
|
+
setError(errorMessage(cause));
|
|
68
|
+
} finally {
|
|
69
|
+
if (active) {
|
|
70
|
+
setPreparing(false);
|
|
71
|
+
setVisible(true);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
void prepare();
|
|
77
|
+
return () => {
|
|
78
|
+
active = false;
|
|
79
|
+
};
|
|
80
|
+
}, [captureSize, captureTarget, prepareScreenshot, requested]);
|
|
81
|
+
|
|
82
|
+
const close = useCallback(() => {
|
|
83
|
+
setVisible(false);
|
|
84
|
+
onClose();
|
|
85
|
+
}, [onClose]);
|
|
86
|
+
|
|
87
|
+
const refresh = useCallback(async () => {
|
|
88
|
+
if (preparing || sending) return;
|
|
89
|
+
setPreparing(true);
|
|
90
|
+
setPrepared(undefined);
|
|
91
|
+
setError(undefined);
|
|
92
|
+
let captured: FeedbackScreenshot;
|
|
93
|
+
try {
|
|
94
|
+
captured = await captureNativeFeedbackView(captureTarget, captureSize);
|
|
95
|
+
} catch {
|
|
96
|
+
captured = { state: "unavailable", failure_code: "capture_failed" };
|
|
97
|
+
}
|
|
98
|
+
setScreenshot(captured);
|
|
99
|
+
try {
|
|
100
|
+
setPrepared(await prepareScreenshot(captured));
|
|
101
|
+
} catch (cause) {
|
|
102
|
+
setError(errorMessage(cause));
|
|
103
|
+
} finally {
|
|
104
|
+
setPreparing(false);
|
|
105
|
+
}
|
|
106
|
+
}, [captureSize, captureTarget, prepareScreenshot, preparing, sending]);
|
|
107
|
+
|
|
108
|
+
const removeScreenshot = useCallback(async () => {
|
|
109
|
+
if (preparing || sending) return;
|
|
110
|
+
const removed: FeedbackScreenshot = { state: "removed" };
|
|
111
|
+
setPreparing(true);
|
|
112
|
+
setPrepared(undefined);
|
|
113
|
+
setScreenshot(removed);
|
|
114
|
+
setError(undefined);
|
|
115
|
+
try {
|
|
116
|
+
setPrepared(await prepareScreenshot(removed));
|
|
117
|
+
} catch (cause) {
|
|
118
|
+
setError(errorMessage(cause));
|
|
119
|
+
} finally {
|
|
120
|
+
setPreparing(false);
|
|
121
|
+
}
|
|
122
|
+
}, [prepareScreenshot, preparing, sending]);
|
|
123
|
+
|
|
124
|
+
const submit = useCallback(async () => {
|
|
125
|
+
if (sending || !message.trim()) return;
|
|
126
|
+
setSending(true);
|
|
127
|
+
setError(undefined);
|
|
128
|
+
try {
|
|
129
|
+
const request = prepared ?? (await prepareScreenshot(screenshot));
|
|
130
|
+
setPrepared(request);
|
|
131
|
+
const receipt = await request.send({ category, message });
|
|
132
|
+
setReceiptId(receipt.id);
|
|
133
|
+
} catch (cause) {
|
|
134
|
+
setError(errorMessage(cause));
|
|
135
|
+
} finally {
|
|
136
|
+
setSending(false);
|
|
137
|
+
}
|
|
138
|
+
}, [category, message, prepareScreenshot, prepared, screenshot, sending]);
|
|
139
|
+
|
|
140
|
+
const selectCategory = useCallback((nextCategory: FeedbackCategory) => {
|
|
141
|
+
setCategory(nextCategory);
|
|
142
|
+
setPrepared(undefined);
|
|
143
|
+
}, []);
|
|
144
|
+
|
|
145
|
+
const updateMessage = useCallback((nextMessage: string) => {
|
|
146
|
+
setMessage(nextMessage);
|
|
147
|
+
setPrepared(undefined);
|
|
148
|
+
}, []);
|
|
149
|
+
|
|
150
|
+
const screenshotUri = screenshotDataUri(screenshot);
|
|
151
|
+
const canSubmit = message.trim().length > 0 && !sending && !preparing && !receiptId;
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<Modal animationType="slide" onRequestClose={close} transparent visible={visible}>
|
|
155
|
+
<KeyboardAvoidingView behavior={Platform.OS === "ios" ? "padding" : "height"} style={styles.overlay}>
|
|
156
|
+
<Pressable accessibilityLabel="Close Feedback" onPress={close} style={styles.backdrop} />
|
|
157
|
+
<View style={[styles.sheet, { backgroundColor: theme.colors.background }]}>
|
|
158
|
+
<View style={styles.header}>
|
|
159
|
+
<View style={styles.headerCopy}>
|
|
160
|
+
<Text accessibilityRole="header" style={[styles.title, { color: theme.colors.foreground }]}>
|
|
161
|
+
Feedback
|
|
162
|
+
</Text>
|
|
163
|
+
<Text style={[styles.subtitle, { color: theme.colors.mutedForeground }]}>Tell Chalk what happened. We receive this report with safe diagnostic context.</Text>
|
|
164
|
+
</View>
|
|
165
|
+
<Pressable accessibilityLabel="Close Feedback" accessibilityRole="button" disabled={sending} onPress={close} style={styles.closeButton}>
|
|
166
|
+
<Text style={[styles.closeText, { color: theme.colors.foreground }]}>×</Text>
|
|
167
|
+
</Pressable>
|
|
168
|
+
</View>
|
|
169
|
+
|
|
170
|
+
{receiptId ? (
|
|
171
|
+
<View style={styles.success}>
|
|
172
|
+
<Text style={[styles.successTitle, { color: theme.colors.foreground }]}>Feedback sent to Chalk</Text>
|
|
173
|
+
<Text style={[styles.subtitle, { color: theme.colors.mutedForeground }]}>Thanks. Chalk received your report.</Text>
|
|
174
|
+
<Pressable accessibilityRole="button" onPress={close} style={[styles.primaryButton, { backgroundColor: theme.colors.primary }]}>
|
|
175
|
+
<Text style={[styles.primaryButtonText, { color: theme.colors.primaryForeground }]}>Done</Text>
|
|
176
|
+
</Pressable>
|
|
177
|
+
</View>
|
|
178
|
+
) : (
|
|
179
|
+
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
|
|
180
|
+
<Text style={[styles.label, { color: theme.colors.foreground }]}>Category</Text>
|
|
181
|
+
<View style={styles.categoryRow}>
|
|
182
|
+
{categories.map((option) => (
|
|
183
|
+
<Pressable
|
|
184
|
+
accessibilityRole="button"
|
|
185
|
+
accessibilityState={{ selected: category === option }}
|
|
186
|
+
key={option}
|
|
187
|
+
onPress={() => selectCategory(option)}
|
|
188
|
+
style={[styles.categoryButton, { borderColor: theme.colors.border }, category === option && { backgroundColor: theme.colors.primary, borderColor: theme.colors.primary }]}
|
|
189
|
+
>
|
|
190
|
+
<Text style={[styles.categoryText, { color: category === option ? theme.colors.primaryForeground : theme.colors.foreground }]}>{categoryLabel(option)}</Text>
|
|
191
|
+
</Pressable>
|
|
192
|
+
))}
|
|
193
|
+
</View>
|
|
194
|
+
|
|
195
|
+
<Text style={[styles.label, { color: theme.colors.foreground }]}>Message</Text>
|
|
196
|
+
<TextInput
|
|
197
|
+
accessibilityLabel="Feedback message"
|
|
198
|
+
editable={!sending}
|
|
199
|
+
maxLength={8_000}
|
|
200
|
+
multiline
|
|
201
|
+
onChangeText={updateMessage}
|
|
202
|
+
placeholder={category === "bug" ? "What went wrong?" : category === "feature_request" ? "What would help?" : "What should Chalk know?"}
|
|
203
|
+
placeholderTextColor={theme.colors.placeholder}
|
|
204
|
+
style={[styles.messageInput, { borderColor: theme.colors.border, color: theme.colors.foreground }]}
|
|
205
|
+
textAlignVertical="top"
|
|
206
|
+
value={message}
|
|
207
|
+
/>
|
|
208
|
+
|
|
209
|
+
<Text style={[styles.label, { color: theme.colors.foreground }]}>Screenshot</Text>
|
|
210
|
+
{screenshotUri ? (
|
|
211
|
+
<View style={styles.thumbnailFrame}>
|
|
212
|
+
<Image accessibilityLabel="Feedback screenshot preview" source={{ uri: screenshotUri }} style={styles.thumbnail} />
|
|
213
|
+
<View style={styles.thumbnailActions}>
|
|
214
|
+
<Pressable accessibilityRole="button" disabled={preparing || sending} onPress={() => void refresh()} style={styles.smallButton}>
|
|
215
|
+
<Text style={[styles.smallButtonText, { color: theme.colors.primary }]}>Refresh</Text>
|
|
216
|
+
</Pressable>
|
|
217
|
+
<Pressable accessibilityRole="button" disabled={preparing || sending} onPress={() => void removeScreenshot()} style={styles.smallButton}>
|
|
218
|
+
<Text style={[styles.smallButtonText, { color: theme.colors.error }]}>Remove</Text>
|
|
219
|
+
</Pressable>
|
|
220
|
+
</View>
|
|
221
|
+
</View>
|
|
222
|
+
) : (
|
|
223
|
+
<View style={[styles.unavailable, { borderColor: theme.colors.border }]}>
|
|
224
|
+
<Text style={[styles.unavailableText, { color: theme.colors.mutedForeground }]}>Screenshot unavailable. You can still send Feedback.</Text>
|
|
225
|
+
<Pressable accessibilityRole="button" disabled={preparing || sending} onPress={() => void refresh()} style={styles.smallButton}>
|
|
226
|
+
<Text style={[styles.smallButtonText, { color: theme.colors.primary }]}>Refresh</Text>
|
|
227
|
+
</Pressable>
|
|
228
|
+
</View>
|
|
229
|
+
)}
|
|
230
|
+
{screenshot.state === "partial" ? <Text style={[styles.notice, { color: theme.colors.mutedForeground }]}>Some protected content may be missing from this screenshot.</Text> : null}
|
|
231
|
+
{preparing ? <Text style={[styles.notice, { color: theme.colors.mutedForeground }]}>Preparing Feedback…</Text> : null}
|
|
232
|
+
{error ? (
|
|
233
|
+
<Text accessibilityLiveRegion="polite" style={[styles.error, { color: theme.colors.error }]}>
|
|
234
|
+
{error}
|
|
235
|
+
</Text>
|
|
236
|
+
) : null}
|
|
237
|
+
|
|
238
|
+
<Pressable accessibilityRole="button" disabled={!canSubmit} onPress={() => void submit()} style={[styles.primaryButton, { backgroundColor: theme.colors.primary }, !canSubmit && styles.disabledButton]}>
|
|
239
|
+
<Text style={[styles.primaryButtonText, { color: theme.colors.primaryForeground }]}>{sending ? "Sending…" : "Send Feedback"}</Text>
|
|
240
|
+
</Pressable>
|
|
241
|
+
<Text style={[styles.footer, { color: theme.colors.mutedForeground }]}>Chalk includes safe Space state, journey, and diagnostic details. It never includes credentials, names, or message content outside this report. There is no reply channel.</Text>
|
|
242
|
+
</ScrollView>
|
|
243
|
+
)}
|
|
244
|
+
</View>
|
|
245
|
+
</KeyboardAvoidingView>
|
|
246
|
+
</Modal>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function screenshotDataUri(screenshot: FeedbackScreenshot): string | undefined {
|
|
251
|
+
if (screenshot.state !== "captured" && screenshot.state !== "partial") return undefined;
|
|
252
|
+
return `data:${screenshot.mime_type};base64,${screenshot.data_base64}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function categoryLabel(category: FeedbackCategory): string {
|
|
256
|
+
if (category === "feature_request") return "Feature request";
|
|
257
|
+
if (category === "bug") return "Bug";
|
|
258
|
+
return "Other";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function errorMessage(cause: unknown): string {
|
|
262
|
+
return cause instanceof Error ? cause.message : "Chalk could not send Feedback.";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const styles = StyleSheet.create({
|
|
266
|
+
overlay: { flex: 1, justifyContent: "flex-end", backgroundColor: Theme.colors.darkOverlay55 },
|
|
267
|
+
backdrop: { ...StyleSheet.absoluteFillObject },
|
|
268
|
+
sheet: { maxHeight: "92%", borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: 24, paddingBottom: 36 },
|
|
269
|
+
header: { flexDirection: "row", alignItems: "flex-start", justifyContent: "space-between", gap: 16, marginBottom: 20 },
|
|
270
|
+
headerCopy: { flex: 1, gap: 4 },
|
|
271
|
+
title: { ...Theme.typography.heading },
|
|
272
|
+
subtitle: { ...Theme.typography.body },
|
|
273
|
+
closeButton: { alignItems: "center", borderColor: Theme.colors.border, borderRadius: Theme.radius.full, borderWidth: 1, height: 36, justifyContent: "center", width: 36 },
|
|
274
|
+
closeText: { fontSize: 24, lineHeight: 26 },
|
|
275
|
+
content: { gap: 12, paddingBottom: 8 },
|
|
276
|
+
label: { ...Theme.typography.label, marginTop: 4 },
|
|
277
|
+
categoryRow: { flexDirection: "row", flexWrap: "wrap", gap: 8 },
|
|
278
|
+
categoryButton: { borderRadius: 14, borderWidth: 1, paddingHorizontal: 13, paddingVertical: 10 },
|
|
279
|
+
categoryText: { fontSize: 14, fontWeight: "700" },
|
|
280
|
+
messageInput: { minHeight: 116, borderRadius: 14, borderWidth: 1, padding: 14, fontSize: 15, lineHeight: 21 },
|
|
281
|
+
thumbnailFrame: { borderRadius: 16, overflow: "hidden", borderWidth: 1, borderColor: Theme.colors.border },
|
|
282
|
+
thumbnail: { width: "100%", height: 170, backgroundColor: Theme.colors.surfaceMuted },
|
|
283
|
+
thumbnailActions: { flexDirection: "row", gap: 16, paddingHorizontal: 12, paddingVertical: 8 },
|
|
284
|
+
unavailable: { borderRadius: 14, borderWidth: 1, padding: 14, gap: 4 },
|
|
285
|
+
unavailableText: { ...Theme.typography.body },
|
|
286
|
+
smallButton: { alignSelf: "flex-start", paddingVertical: 6, paddingHorizontal: 2 },
|
|
287
|
+
smallButtonText: { fontSize: 14, fontWeight: "700" },
|
|
288
|
+
notice: { ...Theme.typography.meta },
|
|
289
|
+
error: { ...Theme.typography.meta, fontWeight: "700" },
|
|
290
|
+
primaryButton: { alignItems: "center", borderRadius: 14, justifyContent: "center", minHeight: 50, marginTop: 8, paddingHorizontal: 16 },
|
|
291
|
+
primaryButtonText: { fontSize: 16, fontWeight: "800" },
|
|
292
|
+
disabledButton: { opacity: 0.45 },
|
|
293
|
+
footer: { ...Theme.typography.meta, marginTop: 4 },
|
|
294
|
+
success: { gap: 12, paddingVertical: 28 },
|
|
295
|
+
successTitle: { ...Theme.typography.subheading },
|
|
296
|
+
});
|
|
@@ -7,9 +7,9 @@ import type { useSpaceViewController } from "./useSpaceViewController";
|
|
|
7
7
|
|
|
8
8
|
type Controller = ReturnType<typeof useSpaceViewController>;
|
|
9
9
|
|
|
10
|
-
export function SpaceActionMenu({ controller, onOpenDiagnostics }: { readonly controller: Controller; readonly onOpenDiagnostics?: () => void }): React.JSX.Element {
|
|
10
|
+
export function SpaceActionMenu({ controller, onOpenDiagnostics, onOpenFeedback }: { readonly controller: Controller; readonly onOpenDiagnostics?: () => void; readonly onOpenFeedback?: () => void }): React.JSX.Element {
|
|
11
11
|
const theme = useNativeTheme();
|
|
12
|
-
const actions = [
|
|
12
|
+
const actions: { label: string; onPress: () => void; disabled?: boolean }[] = [
|
|
13
13
|
...(controller.canInvite ? [{ label: "Invite participants", onPress: controller.handleInviteParticipants }] : []),
|
|
14
14
|
...(controller.canParticipants ? [{ label: "Participants", onPress: () => controller.openPanel("participants") }] : []),
|
|
15
15
|
...(controller.canChat ? [{ label: "Chat", onPress: () => controller.openPanel("chat") }] : []),
|
|
@@ -27,6 +27,15 @@ export function SpaceActionMenu({ controller, onOpenDiagnostics }: { readonly co
|
|
|
27
27
|
},
|
|
28
28
|
]
|
|
29
29
|
: []),
|
|
30
|
+
...(controller.canManageRecording
|
|
31
|
+
? [
|
|
32
|
+
{
|
|
33
|
+
label: controller.recording?.status === "recording" ? "Stop recording" : "Start recording",
|
|
34
|
+
disabled: !controller.canToggleRecording,
|
|
35
|
+
onPress: controller.toggleRecording,
|
|
36
|
+
},
|
|
37
|
+
]
|
|
38
|
+
: []),
|
|
30
39
|
...(controller.canSettings ? [{ label: "Settings", onPress: () => controller.openPanel("settings") }] : []),
|
|
31
40
|
...(onOpenDiagnostics
|
|
32
41
|
? [
|
|
@@ -39,6 +48,17 @@ export function SpaceActionMenu({ controller, onOpenDiagnostics }: { readonly co
|
|
|
39
48
|
},
|
|
40
49
|
]
|
|
41
50
|
: []),
|
|
51
|
+
...(onOpenFeedback
|
|
52
|
+
? [
|
|
53
|
+
{
|
|
54
|
+
label: "Feedback",
|
|
55
|
+
onPress: () => {
|
|
56
|
+
controller.setActionsOpen(false);
|
|
57
|
+
onOpenFeedback();
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
]
|
|
61
|
+
: []),
|
|
42
62
|
...(["grid", "focus", "presentation"] as const).map((layout) => ({
|
|
43
63
|
label: `${layout === "grid" ? "Grid" : layout === "focus" ? "Focus" : "Presentation"} layout${controller.layout.layout === layout ? " (selected)" : ""}`,
|
|
44
64
|
onPress: () => {
|
|
@@ -53,7 +73,7 @@ export function SpaceActionMenu({ controller, onOpenDiagnostics }: { readonly co
|
|
|
53
73
|
<View style={[styles.sheet, { backgroundColor: theme.colors.background }]}>
|
|
54
74
|
<Text style={[styles.title, { color: theme.colors.foreground }]}>Space actions</Text>
|
|
55
75
|
{actions.map((action) => (
|
|
56
|
-
<Pressable key={action.label} onPress={action.onPress} style={({ pressed }) => [styles.action, { backgroundColor: theme.colors.card }, pressed && styles.pressed]}>
|
|
76
|
+
<Pressable accessibilityRole="button" accessibilityState={{ disabled: action.disabled ?? false }} disabled={action.disabled} key={action.label} onPress={action.onPress} style={({ pressed }) => [styles.action, { backgroundColor: theme.colors.card }, pressed && styles.pressed]}>
|
|
57
77
|
<Text style={[styles.actionText, { color: theme.colors.foreground }]}>{action.label}</Text>
|
|
58
78
|
</Pressable>
|
|
59
79
|
))}
|