@q9labsai/chalk-react-native 5.1.0 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/{chunk-4L5GXJ36.js → chunk-BXDD5BE4.js} +23 -1
  3. package/dist/{chunk-MBZRE4US.js → chunk-PPQSOU35.js} +5 -5
  4. package/dist/client.js +1 -0
  5. package/dist/components/EmbeddedWhiteboard.d.ts +8 -4
  6. package/dist/frame/call-controls.d.ts +2 -0
  7. package/dist/frame/use-landscape-chrome.d.ts +8 -0
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +358 -97
  10. package/dist/invites.d.ts +5 -0
  11. package/dist/invites.js +3 -1
  12. package/dist/primitives/IconButton.d.ts +1 -0
  13. package/dist/primitives/NameTag.d.ts +2 -1
  14. package/dist/primitives/TileShell.d.ts +2 -1
  15. package/dist/stage/ParticipantTile.d.ts +3 -1
  16. package/dist/stage/TileSheet.d.ts +1 -1
  17. package/dist/stage/stage-view.d.ts +4 -0
  18. package/dist/ui.js +1 -1
  19. package/dist/whiteboard/export-board-image.d.ts +6 -0
  20. package/dist/whiteboard/native-board-session.d.ts +15 -0
  21. package/embedded/chalk-whiteboard/Figtree-Regular.ttf +0 -0
  22. package/embedded/chalk-whiteboard/Figtree-SemiBold.ttf +0 -0
  23. package/embedded/chalk-whiteboard/asset-manifest.json +19 -4
  24. package/embedded/chalk-whiteboard/chalk.css +44 -0
  25. package/embedded/chalk-whiteboard/licenses/Figtree.txt +93 -0
  26. package/embedded/chalk-whiteboard/renderer.js +1669 -1418
  27. package/package.json +11 -6
  28. package/src/components/EmbeddedWhiteboard.tsx +90 -6
  29. package/src/dock/Dock.tsx +14 -3
  30. package/src/dock/MoreSheet.tsx +23 -0
  31. package/src/frame/CallFrame.tsx +20 -33
  32. package/src/frame/call-controls.ts +4 -0
  33. package/src/frame/use-landscape-chrome.ts +22 -0
  34. package/src/index.ts +2 -2
  35. package/src/invites.ts +32 -1
  36. package/src/primitives/IconButton.tsx +6 -5
  37. package/src/primitives/NameTag.tsx +7 -1
  38. package/src/primitives/TileShell.tsx +3 -1
  39. package/src/space-client/create-native-space-client.ts +1 -0
  40. package/src/stage/ContentTile.tsx +64 -19
  41. package/src/stage/ParticipantTile.tsx +6 -4
  42. package/src/stage/Stage.tsx +37 -6
  43. package/src/stage/TileSheet.tsx +17 -2
  44. package/src/stage/stage-view.tsx +11 -1
  45. package/src/whiteboard/export-board-image.ts +21 -0
  46. package/src/whiteboard/native-board-session.ts +29 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.2.0
4
+
5
+ ### confirmed-call-entry
6
+
7
+ Count a person in a call only after they have fully entered it. The built-in
8
+ browser and React Native Sync clients ask Sync for entry confirmation on the Sync
9
+ URL (`entry_confirmation=1`). When Sync's `welcome` grants it, the client sends
10
+ `entered` once Sync is live and media has started, so a failed connection no
11
+ longer leaves a ghost participant or an attendance record. Against an older Sync,
12
+ the client sends the previous hello and Sync confirms entry at hello as before.
13
+
14
+ `confirmEntered()` on a custom `createSyncClient` implementation is optional.
15
+ Implementing it, and building `V1SyncClient` with `entryConfirmation: true`, adds
16
+ the confirmed entry above. Without it, the client never asks for entry
17
+ confirmation and Sync confirms the entry at hello, as in 5.0.x. `V1SyncClient`
18
+ asks only when it is built with `entryConfirmation: true`.
19
+
20
+ When entry fails, the client cancels the pending admission through the new
21
+ `cancelEpisodeParticipantJoin` endpoint (`POST .../participants/:participant_id/cancel-join`),
22
+ and a retry keeps the same person. If Sync reports `entry timeout` because the
23
+ pending seat expired before entry, the connection fails as a retryable
24
+ `sync_start_failed` instead of rejected access, and the client does not reconnect
25
+ the expired seat with the same grant.
26
+
27
+ The admission contract declares the 409 `participant.not_active` response when
28
+ a retried admission names a participant that has already left. The public
29
+ `AdmitEpisodeParticipantError` union stays compatible with 5.1.0; this response
30
+ uses the existing `HttpClientError` channel, with its code in the response body.
31
+ The dashboard reads that code and retries once with a fresh join key.
32
+
33
+ Sync assigns each entry a participant-local generation and returns it in
34
+ `welcome`. The client resumes that generation through the `entry_generation`
35
+ WebSocket query, even after refreshing its credential. Confirmation advances
36
+ ownership only forward and voids older pending Leaves. Acceptance and completion
37
+ both fence a Leave against the confirmed generation, so reattaching the same
38
+ seat cannot let the previous mount remove it. Previous Sync ignores this query.
39
+ Legacy sockets receive a server generation internally without changing their
40
+ welcome frame, and still confirm at hello. Participants created before seat
41
+ storage acquire it on their first live hello. When recovery replaces the Sync
42
+ client, it confirms the replacement entry before reporting recovery complete.
43
+ Completed startup also survives reconnecting from an older Sync to an upgraded
44
+ one. Recovery can finish while a previous entry's Leave is pending, and an
45
+ uncertain confirmation reply checks that entry's generation before acknowledging it.
46
+ A claimed Leave blocks later entry
47
+ admission until its provider work settles. An entry admitted before that claim
48
+ defers the older provider work until confirmation or its two-minute startup
49
+ lease expires, preventing old cleanup from closing the new entry's media.
50
+
51
+ Rollout: apply the database migrations first, then upgrade Sync, then the API and
52
+ this client. This client also works with an older Sync, and published 5.0.x
53
+ clients keep hello confirmation with the new Sync.
54
+
3
55
  ## 5.1.0
4
56
 
5
57
  - Bound each Sync and whiteboard WebSocket opening attempt to the existing 30-second startup allowance, or to `syncStartupTimeoutMs` when set. `syncStartupTimeoutMs` accepts 1 to 60,000 ms, and each attempt gets at least 10 seconds. The whiteboard retries a timed-out attempt through the existing reconnect backoff, and so does Sync once the call is live, such as after a drop or a return to the foreground. A first join does not retry a Sync socket that never opens: the join fails when its startup wait of `syncStartupTimeoutMs` ends, and a setting below 10 seconds ends the join before the attempt's own 10-second minimum. The client's 10-second default does not apply to React Native.
@@ -1,4 +1,5 @@
1
1
  // src/invites.ts
2
+ import { parseInviteCode } from "@q9labsai/chalk-client/invites";
2
3
  var CHALK_INVITE_HOSTS = ["chalkmeet.com", "chalk.q9labs.ai"];
3
4
  var CHALK_CUSTOM_PROTOCOLS = /* @__PURE__ */ new Set(["chalk:", "ai.q9labs.chalk.mobile:"]);
4
5
  var SPACE_SLUG_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u;
@@ -27,6 +28,26 @@ function parseSpaceInviteLink(input) {
27
28
  return null;
28
29
  }
29
30
  }
31
+ function parseChalkInviteLink(input) {
32
+ const trimmed = input.trim();
33
+ const code = parseInviteCode(trimmed);
34
+ if (code) return { spaceInviteToken: code };
35
+ const legacy = parseSpaceInviteLink(trimmed);
36
+ if (legacy) return legacy;
37
+ const normalized = trimmed.includes("://") ? trimmed : CHALK_INVITE_HOSTS.some((host) => trimmed.startsWith(`${host}/`)) ? `https://${trimmed}` : null;
38
+ if (!normalized) return null;
39
+ try {
40
+ const parsed = new URL(normalized);
41
+ if (!CHALK_CUSTOM_PROTOCOLS.has(parsed.protocol) && !isAllowedWebInviteOrigin(parsed)) return null;
42
+ const pathSegments = parsed.pathname.split("/").filter(Boolean);
43
+ if (CHALK_CUSTOM_PROTOCOLS.has(parsed.protocol)) pathSegments.unshift(parsed.hostname);
44
+ if (pathSegments.length !== 1 || parsed.search || parsed.hash) return null;
45
+ const pathCode = parseInviteCode(decodeURIComponent(pathSegments[0] ?? ""));
46
+ return pathCode ? { spaceInviteToken: pathCode } : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
30
51
  function isChalkInviteHost(hostname) {
31
52
  return CHALK_INVITE_HOSTS.some((host) => host === hostname);
32
53
  }
@@ -48,11 +69,12 @@ function getClipboardInviteSuggestion(clipboardText, currentInput = "") {
48
69
  if (normalizedClipboardText === currentInput.trim()) {
49
70
  return null;
50
71
  }
51
- return parseSpaceInviteLink(normalizedClipboardText) ? normalizedClipboardText : null;
72
+ return parseChalkInviteLink(normalizedClipboardText) ? normalizedClipboardText : null;
52
73
  }
53
74
 
54
75
  export {
55
76
  parseSpaceInviteLink,
77
+ parseChalkInviteLink,
56
78
  extractSpaceInviteFromLink,
57
79
  getClipboardInviteSuggestion
58
80
  };
@@ -507,11 +507,11 @@ function usePressAnimation(family) {
507
507
 
508
508
  // src/primitives/IconButton.tsx
509
509
  import { forwardRef, memo as memo2 } from "react";
510
- import { Pressable, StyleSheet, View } from "react-native";
510
+ import { ActivityIndicator, Pressable, StyleSheet, View } from "react-native";
511
511
  import Animated, { useAnimatedStyle as useAnimatedStyle2 } from "react-native-reanimated";
512
512
  import { jsx as jsx5, jsxs } from "react/jsx-runtime";
513
513
  var IconButton = memo2(
514
- forwardRef(function IconButton2({ icon, accessibilityLabel, onPress, size = 44, tone = "raised", selected = false, off = false, caption, expanded, onCanvas = false, disabled = false, testID }, ref) {
514
+ forwardRef(function IconButton2({ icon, accessibilityLabel, onPress, size = 44, tone = "raised", selected = false, off = false, caption, expanded, onCanvas = false, disabled = false, busy = false, testID }, ref) {
515
515
  const { colors, elevation } = useTheme();
516
516
  const press = usePressAnimation("round");
517
517
  const base = selected ? colors.text : BASE[tone](colors);
@@ -530,8 +530,8 @@ var IconButton = memo2(
530
530
  {
531
531
  accessibilityLabel,
532
532
  accessibilityRole: "button",
533
- accessibilityState: { selected, disabled, expanded },
534
- disabled,
533
+ accessibilityState: { selected, disabled: disabled || busy, expanded, busy },
534
+ disabled: disabled || busy,
535
535
  hitSlop: size < 44 ? (44 - size) / 2 : void 0,
536
536
  onPress,
537
537
  onPressIn: press.onPressIn,
@@ -539,7 +539,7 @@ var IconButton = memo2(
539
539
  ref,
540
540
  testID,
541
541
  children: [
542
- /* @__PURE__ */ jsx5(Animated.View, { ref: roundRef, style: [roundStyle, background, press.scaleStyle], children: /* @__PURE__ */ jsx5(Icon, { color: iconColor, name: icon, size: tone === "ghost" ? 18 : 20 }) }),
542
+ /* @__PURE__ */ jsx5(Animated.View, { ref: roundRef, style: [roundStyle, background, press.scaleStyle], children: busy ? /* @__PURE__ */ jsx5(ActivityIndicator, { color: iconColor, size: "small" }) : /* @__PURE__ */ jsx5(Icon, { color: iconColor, name: icon, size: tone === "ghost" ? 18 : 20 }) }),
543
543
  caption ? /* @__PURE__ */ jsx5(View, { pointerEvents: "none", style: styles.captionBox, children: /* @__PURE__ */ jsx5(ChalkText, { numberOfLines: 1, tone: "textMuted", variant: "caption", children: caption }) }) : null
544
544
  ]
545
545
  }
package/dist/client.js CHANGED
@@ -146,6 +146,7 @@ function createSyncClient(syncURL, input, lifecycle, webSocket, storage, dialTim
146
146
  lifecycle,
147
147
  webSocket,
148
148
  dialTimeoutMs,
149
+ entryConfirmation: true,
149
150
  ...storage ? { pendingStore: new AsyncStorageV1PendingTargetStore({ scope, storage }) } : {}
150
151
  });
151
152
  }
@@ -1,6 +1,9 @@
1
- import { type ChalkEmbeddedWhiteboardTransport } from "@q9labsai/chalk-whiteboard/embedded";
1
+ import { type ChalkEmbeddedWhiteboardTransport, type ChalkEmbeddedBoardContext } from "@q9labsai/chalk-whiteboard/embedded";
2
2
  import { type StyleProp, type ViewStyle } from "react-native";
3
3
  export interface EmbeddedWhiteboardProps {
4
+ readonly board: ChalkEmbeddedBoardContext;
5
+ readonly onFollow?: (participantId: string | null) => void;
6
+ readonly onCloseBoard?: () => void;
4
7
  readonly transport: ChalkEmbeddedWhiteboardTransport;
5
8
  readonly journeyId: string;
6
9
  readonly traceparent?: string;
@@ -27,14 +30,15 @@ export interface EmbeddedWhiteboardProps {
27
30
  readonly format: "png" | "svg";
28
31
  readonly mimeType: string;
29
32
  readonly dataURL: string;
30
- }) => void;
33
+ readonly action?: "download" | "copy";
34
+ }) => void | Promise<void>;
31
35
  }
32
36
  export declare const embeddedWhiteboardWebViewSecurityPolicy: Readonly<{
33
37
  allowFileAccess: true;
34
38
  allowFileAccessFromFileURLs: false;
35
39
  allowUniversalAccessFromFileURLs: false;
36
40
  cacheEnabled: false;
37
- domStorageEnabled: false;
41
+ domStorageEnabled: true;
38
42
  javaScriptCanOpenWindowsAutomatically: false;
39
43
  javaScriptEnabled: true;
40
44
  mixedContentMode: "never";
@@ -42,4 +46,4 @@ export declare const embeddedWhiteboardWebViewSecurityPolicy: Readonly<{
42
46
  sharedCookiesEnabled: false;
43
47
  thirdPartyCookiesEnabled: false;
44
48
  }>;
45
- export declare function EmbeddedWhiteboard({ transport, journeyId, traceparent, tracestate, canDraw, canClear, theme: themeOverride, localParticipantColor, rendererURL: rendererURLOverride, style, testID, onMetric, onError, onUserExport }: EmbeddedWhiteboardProps): React.JSX.Element;
49
+ export declare function EmbeddedWhiteboard({ board, onFollow, onCloseBoard, transport, journeyId, traceparent, tracestate, canDraw, canClear, theme: themeOverride, localParticipantColor, rendererURL: rendererURLOverride, style, testID, onMetric, onError, onUserExport, }: EmbeddedWhiteboardProps): React.JSX.Element;
@@ -0,0 +1,2 @@
1
+ /** Landscape controls overlay the stage, so board tools need room above them. */
2
+ export declare const LandscapeControlsContext: import("react").Context<boolean>;
@@ -0,0 +1,8 @@
1
+ /** A visible board stays interactive instead of acquiring the tap-to-reveal overlay. */
2
+ export declare function useLandscapeChrome({ fades, boardVisible }: {
3
+ readonly fades: boolean;
4
+ readonly boardVisible: boolean;
5
+ }): {
6
+ hidden: boolean;
7
+ show: () => void;
8
+ };
package/dist/index.d.ts CHANGED
@@ -7,6 +7,6 @@ export type { ColorScheme, Palette } from "./theme/tokens";
7
7
  export { ChalkProvider, useSpaceClient } from "./context/space-client-context";
8
8
  export type { ChalkProviderProps } from "./context/space-client-context";
9
9
  export { useCan, useChat, useConnection, useMedia, useParticipants, useReactions, useRecording, useSelf, useWhiteboard } from "./hooks/space-hooks";
10
- export { extractSpaceInviteFromLink, getClipboardInviteSuggestion, parseSpaceInviteLink } from "./invites";
11
- export type { SpaceInviteLink } from "./invites";
10
+ export { extractSpaceInviteFromLink, getClipboardInviteSuggestion, parseChalkInviteLink, parseSpaceInviteLink } from "./invites";
11
+ export type { ChalkInviteLink, SpaceInviteLink } from "./invites";
12
12
  export { buildChalkInviteLink } from "./utils/build-chalk-invite-link";