@q9labsai/chalk-react-native 4.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@q9labsai/chalk-react-native",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -150,15 +150,15 @@
150
150
  "registry": "https://registry.npmjs.org/"
151
151
  },
152
152
  "dependencies": {
153
- "@q9labsai/chalk-client": "^4.0.1",
154
- "@q9labsai/chalk-whiteboard": "^4.0.1",
155
- "@q9labsai/facehash": "^4.0.1"
153
+ "@q9labsai/chalk-client": "^4.1.0",
154
+ "@q9labsai/chalk-whiteboard": "^4.1.0",
155
+ "@q9labsai/facehash": "^4.1.0"
156
156
  },
157
157
  "devDependencies": {
158
- "@types/node": "^24.10.1",
159
- "@types/react": "^19.0.0",
158
+ "@types/node": "^26.2.0",
159
+ "@types/react": "^19.2.18",
160
160
  "react-native-webview": "13.16.1",
161
- "typescript": "^5.7.2"
161
+ "typescript": "^6.0.3"
162
162
  },
163
163
  "peerDependencies": {
164
164
  "@cloudflare/react-native-webrtc": "~125.0.1 || ~125.0.3",
@@ -13,6 +13,7 @@ const testingLibrary = createRequire(import.meta.url)(join(process.cwd(), "../re
13
13
  const { act, cleanup, render } = testingLibrary;
14
14
 
15
15
  const nativeClientFactory = vi.hoisted(() => vi.fn());
16
+ const spaceViewSpy = vi.hoisted(() => vi.fn(() => <div data-testid="space" />));
16
17
 
17
18
  vi.mock("react-native", () => ({
18
19
  StyleSheet: { create: <T,>(styles: T) => styles },
@@ -21,7 +22,7 @@ vi.mock("react-native", () => ({
21
22
  Pressable: ({ children }: { readonly children?: ReactNode }) => children,
22
23
  }));
23
24
  vi.mock("../space-client/create-native-space-client", () => ({ createNativeSpaceClient: nativeClientFactory }));
24
- vi.mock("./SpaceView", () => ({ SpaceView: () => <div data-testid="space" /> }));
25
+ vi.mock("./SpaceView", () => ({ SpaceView: spaceViewSpy }));
25
26
  vi.mock("./Entrance", () => ({ Entrance: () => <div data-testid="entrance" /> }));
26
27
 
27
28
  import { Chalk } from "./Chalk";
@@ -29,6 +30,7 @@ import { Chalk } from "./Chalk";
29
30
  afterEach(() => {
30
31
  cleanup();
31
32
  nativeClientFactory.mockReset();
33
+ spaceViewSpy.mockClear();
32
34
  });
33
35
 
34
36
  describe("Chalk", () => {
@@ -55,6 +57,14 @@ describe("Chalk", () => {
55
57
  expect(onJoined).not.toHaveBeenCalled();
56
58
  });
57
59
 
60
+ it("forwards the optional diagnostics action to the live Space", () => {
61
+ const client = createClient("live");
62
+ const onOpenDiagnostics = vi.fn();
63
+ render(<Chalk client={client.client} onOpenDiagnostics={onOpenDiagnostics} />);
64
+
65
+ expect(spaceViewSpy).toHaveBeenCalledWith(expect.objectContaining({ onOpenDiagnostics }), undefined);
66
+ });
67
+
58
68
  it("forwards canonical client events and cleans their subscriptions up", () => {
59
69
  const client = createClient("live");
60
70
  const handlers = new Map<string, (event: unknown) => void>();
@@ -64,6 +64,7 @@ type ChalkCommonProps = ChalkCallbacks & {
64
64
  readonly inviteLink?: string;
65
65
  readonly layout?: SpaceLayout;
66
66
  readonly onLayoutChange?: (layout: SpaceLayout) => void;
67
+ readonly onOpenDiagnostics?: () => void;
67
68
  };
68
69
 
69
70
  export type ChalkProps = ChalkCommonProps &
@@ -264,7 +265,19 @@ function SpaceExperience(props: ChalkProps): React.JSX.Element {
264
265
  </AppearanceLayer>
265
266
  );
266
267
 
267
- return <NativeSpaceView features={props.features} inviteLink={props.inviteLink} layout={props.layout} logoUrl={props.logoUrl} onLayoutChange={props.onLayoutChange} pickChatFiles={props.pickChatFiles} reconnecting={connection.status === "reconnecting"} spaceName={spaceName} />;
268
+ return (
269
+ <NativeSpaceView
270
+ features={props.features}
271
+ inviteLink={props.inviteLink}
272
+ layout={props.layout}
273
+ logoUrl={props.logoUrl}
274
+ onLayoutChange={props.onLayoutChange}
275
+ onOpenDiagnostics={props.onOpenDiagnostics}
276
+ pickChatFiles={props.pickChatFiles}
277
+ reconnecting={connection.status === "reconnecting"}
278
+ spaceName={spaceName}
279
+ />
280
+ );
268
281
  }
269
282
 
270
283
  function defaultJoinOptions(props: ChalkProps): Parameters<SpaceClient["join"]>[0] {
@@ -282,18 +295,31 @@ type NativeSpaceViewProps = {
282
295
  readonly layout?: SpaceLayout;
283
296
  readonly logoUrl?: string;
284
297
  readonly onLayoutChange?: (layout: SpaceLayout) => void;
298
+ readonly onOpenDiagnostics?: () => void;
285
299
  readonly pickChatFiles?: () => Promise<readonly ChatUploadFile[]>;
286
300
  readonly reconnecting: boolean;
287
301
  readonly spaceName: string;
288
302
  };
289
303
 
290
- function NativeSpaceView({ features, inviteLink, layout, logoUrl, onLayoutChange, pickChatFiles, reconnecting, spaceName }: NativeSpaceViewProps): React.JSX.Element {
304
+ function NativeSpaceView({ features, inviteLink, layout, logoUrl, onLayoutChange, onOpenDiagnostics, pickChatFiles, reconnecting, spaceName }: NativeSpaceViewProps): React.JSX.Element {
291
305
  const client = useSpaceClient();
292
306
  const theme = useNativeTheme();
293
307
 
294
308
  return (
295
309
  <View style={[styles.space, { backgroundColor: theme.colors.background }]}>
296
- <SpaceView features={features} inviteLink={inviteLink} layout={layout} logoUrl={logoUrl} onEndEpisode={() => client.endEpisode()} onLayoutChange={onLayoutChange} onLeave={() => client.leave()} pickChatFiles={pickChatFiles} reconnecting={reconnecting} spaceName={spaceName} />
310
+ <SpaceView
311
+ features={features}
312
+ inviteLink={inviteLink}
313
+ layout={layout}
314
+ logoUrl={logoUrl}
315
+ onEndEpisode={() => client.endEpisode()}
316
+ onLayoutChange={onLayoutChange}
317
+ onLeave={() => client.leave()}
318
+ onOpenDiagnostics={onOpenDiagnostics}
319
+ pickChatFiles={pickChatFiles}
320
+ reconnecting={reconnecting}
321
+ spaceName={spaceName}
322
+ />
297
323
  <NativeTextureOverlay />
298
324
  </View>
299
325
  );
@@ -67,13 +67,14 @@ describe("SpaceViewShared", () => {
67
67
  });
68
68
 
69
69
  it("passes reconnecting state and capability-gated controls to the native surface", () => {
70
- const props = { onLeave: vi.fn(), reconnecting: true, logoUrl: "https://cdn.example.test/logo.png", spaceName: "Design space" };
70
+ const props = { onLeave: vi.fn(), onOpenDiagnostics: vi.fn(), reconnecting: true, logoUrl: "https://cdn.example.test/logo.png", spaceName: "Design space" };
71
71
  const tree = SpaceViewShared(props);
72
72
 
73
73
  expect(state.useSpaceViewController).toHaveBeenCalledWith(props);
74
74
  expect(findText(tree)).toContain("Reconnecting… controls will resume shortly.");
75
75
  expect(findElement(tree, "SpaceTopBarAndroid")).toMatchObject({ props: { logoUrl: props.logoUrl, spaceName: props.spaceName } });
76
76
  expect(findElement(tree, "SpaceBottomDockAndroid")).toMatchObject({ props: { onOpenChat: expect.any(Function), unreadChatCount: 0 } });
77
+ expect(findElement(tree, "SpaceActionMenu")).toMatchObject({ props: { onOpenDiagnostics: props.onOpenDiagnostics } });
77
78
  });
78
79
 
79
80
  it("renders the whiteboard surface instead of a stage or grid when it is open", () => {
@@ -72,7 +72,7 @@ export function SpaceViewShared(props: SpaceViewProps): React.JSX.Element {
72
72
  simulatorMediaDisabled={controller.simulatorMediaDisabled}
73
73
  unreadChatCount={controller.chat.unreadCount}
74
74
  />
75
- <SpaceActionMenu controller={controller} />
75
+ <SpaceActionMenu controller={controller} onOpenDiagnostics={props.onOpenDiagnostics} />
76
76
  {controller.panel === "settings" ? <SettingsSheet controller={controller} isOpen onClose={controller.closePanel} /> : <SpacePanelSheet controller={controller} />}
77
77
  <ReactionPicker isOpen={controller.reactionPickerOpen} onClose={() => controller.setReactionPickerOpen(false)} onSelect={(reaction) => selectSpaceReaction(controller, reaction)} />
78
78
  </View>
@@ -29,6 +29,7 @@ export interface SpaceViewProps {
29
29
  features?: SpaceViewFeatures;
30
30
  onLeave: () => void | Promise<void>;
31
31
  onEndEpisode?: () => void | Promise<void>;
32
+ onOpenDiagnostics?: () => void;
32
33
  onDiagnosticsChange?: (snapshot: SpaceViewDiagnosticsSnapshot) => void;
33
34
  /** Selects files; the SDK owns bounds, upload, and send sequencing. */
34
35
  pickChatFiles?: () => Promise<readonly ChatUploadFile[]>;
@@ -59,6 +59,22 @@ describe("SpaceActionMenu", () => {
59
59
  expect(sendReaction).toHaveBeenCalledWith("🎉");
60
60
  });
61
61
 
62
+ it("shows diagnostics only when its open handler is provided", () => {
63
+ const setActionsOpen = vi.fn();
64
+ const onOpenDiagnostics = vi.fn();
65
+ const controller = actionController({ setActionsOpen });
66
+
67
+ expect(findText(SpaceActionMenu({ controller }))).not.toContain("Diagnostics");
68
+
69
+ const tree = SpaceActionMenu({ controller, onOpenDiagnostics });
70
+ const diagnostics = findElements(tree, "Pressable").find((element) => findText(element).length === 1 && findText(element).includes("Diagnostics"));
71
+ const onPress = diagnostics?.props?.onPress;
72
+ if (typeof onPress === "function") onPress();
73
+
74
+ expect(setActionsOpen).toHaveBeenCalledWith(false);
75
+ expect(onOpenDiagnostics).toHaveBeenCalledTimes(1);
76
+ });
77
+
62
78
  it("leaves chat and settings panel ownership to canonical mounted sheets", () => {
63
79
  expect(source).not.toContain("ChatPanel");
64
80
  expect(source).not.toContain("<SpacePanel");
@@ -7,7 +7,7 @@ import type { useSpaceViewController } from "./useSpaceViewController";
7
7
 
8
8
  type Controller = ReturnType<typeof useSpaceViewController>;
9
9
 
10
- export function SpaceActionMenu({ controller }: { readonly controller: Controller }): React.JSX.Element {
10
+ export function SpaceActionMenu({ controller, onOpenDiagnostics }: { readonly controller: Controller; readonly onOpenDiagnostics?: () => void }): React.JSX.Element {
11
11
  const theme = useNativeTheme();
12
12
  const actions = [
13
13
  ...(controller.canInvite ? [{ label: "Invite participants", onPress: controller.handleInviteParticipants }] : []),
@@ -28,6 +28,17 @@ export function SpaceActionMenu({ controller }: { readonly controller: Controlle
28
28
  ]
29
29
  : []),
30
30
  ...(controller.canSettings ? [{ label: "Settings", onPress: () => controller.openPanel("settings") }] : []),
31
+ ...(onOpenDiagnostics
32
+ ? [
33
+ {
34
+ label: "Diagnostics",
35
+ onPress: () => {
36
+ controller.setActionsOpen(false);
37
+ onOpenDiagnostics();
38
+ },
39
+ },
40
+ ]
41
+ : []),
31
42
  ...(["grid", "focus", "presentation"] as const).map((layout) => ({
32
43
  label: `${layout === "grid" ? "Grid" : layout === "focus" ? "Focus" : "Presentation"} layout${controller.layout.layout === layout ? " (selected)" : ""}`,
33
44
  onPress: () => {
@@ -11,6 +11,7 @@ test("the turnkey and entrance props use the ratified names", () => {
11
11
  readonly defaults?: { readonly microphone?: boolean; readonly camera?: boolean };
12
12
  readonly features?: object;
13
13
  readonly layout?: SpaceLayout;
14
+ readonly onOpenDiagnostics?: () => void;
14
15
  }>();
15
16
  expectTypeOf<EntranceProps>().toMatchTypeOf<{
16
17
  readonly defaults?: { readonly microphone?: boolean; readonly camera?: boolean };
@@ -1,3 +1,5 @@
1
+ /// <reference types="node" />
2
+
1
3
  import { createRequire } from "node:module";
2
4
  import { join } from "node:path";
3
5
  import type { ComponentType, ReactNode } from "react";