@q9labsai/chalk-react-native 5.2.0 → 5.2.2

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 (48) hide show
  1. package/dist/{chunk-4L5GXJ36.js → chunk-BXDD5BE4.js} +23 -1
  2. package/dist/{chunk-MBZRE4US.js → chunk-PPQSOU35.js} +5 -5
  3. package/dist/client.js +5 -4
  4. package/dist/components/EmbeddedWhiteboard.d.ts +8 -4
  5. package/dist/frame/call-controls.d.ts +2 -0
  6. package/dist/frame/use-landscape-chrome.d.ts +8 -0
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +365 -101
  9. package/dist/invites.d.ts +5 -0
  10. package/dist/invites.js +3 -1
  11. package/dist/primitives/IconButton.d.ts +1 -0
  12. package/dist/primitives/NameTag.d.ts +2 -1
  13. package/dist/primitives/TileShell.d.ts +2 -1
  14. package/dist/stage/ParticipantTile.d.ts +3 -1
  15. package/dist/stage/TileSheet.d.ts +1 -1
  16. package/dist/stage/stage-view.d.ts +4 -0
  17. package/dist/status/StatusPage.d.ts +3 -1
  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 +5 -4
  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/status/CallOverlays.tsx +1 -1
  46. package/src/status/StatusPage.tsx +12 -4
  47. package/src/whiteboard/export-board-image.ts +21 -0
  48. package/src/whiteboard/native-board-session.ts +29 -0
package/dist/index.js CHANGED
@@ -4,8 +4,9 @@ import {
4
4
  import {
5
5
  extractSpaceInviteFromLink,
6
6
  getClipboardInviteSuggestion,
7
+ parseChalkInviteLink,
7
8
  parseSpaceInviteLink
8
- } from "./chunk-4L5GXJ36.js";
9
+ } from "./chunk-BXDD5BE4.js";
9
10
  import {
10
11
  BottomSheet,
11
12
  ChalkText,
@@ -53,13 +54,13 @@ import {
53
54
  useTheme,
54
55
  useToasts,
55
56
  viewer
56
- } from "./chunk-MBZRE4US.js";
57
+ } from "./chunk-PPQSOU35.js";
57
58
  import {
58
59
  useMeasuredRef
59
60
  } from "./chunk-GYVZSOGQ.js";
60
61
 
61
62
  // src/frame/Chalk.tsx
62
- import { useEffect as useEffect20 } from "react";
63
+ import { useEffect as useEffect23 } from "react";
63
64
  import { StyleSheet as StyleSheet24 } from "react-native";
64
65
  import { GestureHandlerRootView } from "react-native-gesture-handler";
65
66
  import { SafeAreaProvider as SafeAreaProvider2 } from "react-native-safe-area-context";
@@ -114,6 +115,8 @@ var StageViewContext = createContext2(null);
114
115
  function StageViewProvider({ children }) {
115
116
  const [layout, setLayout] = useState("spotlight");
116
117
  const [pinned, setPinned] = useState(null);
118
+ const [boardOpen, setBoardOpen] = useState(null);
119
+ const [following, setFollowing] = useState(null);
117
120
  const [mirror, setMirror] = useState(true);
118
121
  const dev = useDevScenario();
119
122
  useEffect(() => {
@@ -124,6 +127,10 @@ function StageViewProvider({ children }) {
124
127
  () => ({
125
128
  layout,
126
129
  pinned,
130
+ boardOpen,
131
+ setBoardOpen,
132
+ following,
133
+ setFollowing,
127
134
  mirror,
128
135
  setLayout: (next) => {
129
136
  setLayout(next);
@@ -143,7 +150,7 @@ function StageViewProvider({ children }) {
143
150
  },
144
151
  setMirror
145
152
  }),
146
- [layout, mirror, pinned]
153
+ [layout, mirror, pinned, following, boardOpen]
147
154
  );
148
155
  return /* @__PURE__ */ jsx2(StageViewContext.Provider, { value, children });
149
156
  }
@@ -154,7 +161,7 @@ function useStageView() {
154
161
  }
155
162
 
156
163
  // src/frame/CallFrame.tsx
157
- import { useCallback as useCallback15, useEffect as useEffect19, useMemo as useMemo12, useRef as useRef13, useState as useState13 } from "react";
164
+ import { useCallback as useCallback17, useEffect as useEffect22, useMemo as useMemo12, useRef as useRef13, useState as useState15 } from "react";
158
165
  import { Pressable as Pressable15, StatusBar, StyleSheet as StyleSheet23, View as View22 } from "react-native";
159
166
  import Animated12, { useAnimatedStyle as useAnimatedStyle7, useSharedValue as useSharedValue5, withTiming as withTiming7 } from "react-native-reanimated";
160
167
 
@@ -295,13 +302,27 @@ function RoutePicker({ open, onClose, onSelect, output, anchorRef }) {
295
302
  import { Fragment, jsx as jsx4, jsxs } from "react/jsx-runtime";
296
303
  var Dock = memo(function Dock2({ arrangement, microphoneOn, cameraOn, onToggleMicrophone, onToggleCamera, onOpenMore, onLeave, moreOpen, leaveOpen, moreRef, leaveRef }) {
297
304
  const live = useConnection().status === "live";
305
+ const { local } = useMedia();
298
306
  const size = arrangement === "pad" ? 44 : 52;
299
307
  const dockStyle = [styles.dock, arrangement === "pad" ? styles.pad : arrangement === "landscape" ? styles.landscape : styles.portrait];
300
308
  const dockRef = useMeasuredRef("frame.dock", dockStyle);
301
309
  const output = useAudioOutput();
302
310
  return /* @__PURE__ */ jsxs(View, { accessibilityLabel: "Call controls", accessibilityRole: "toolbar", pointerEvents: "box-none", ref: dockRef, style: dockStyle, testID: "frame.dock", children: [
303
- /* @__PURE__ */ jsx4(IconButton, { accessibilityLabel: microphoneOn ? "Mute microphone" : "Unmute microphone", icon: microphoneOn ? "mic" : "micOff", off: !microphoneOn, disabled: !live && !microphoneOn, onCanvas: true, onPress: onToggleMicrophone, size, testID: "dock.microphone" }),
304
- /* @__PURE__ */ jsx4(IconButton, { accessibilityLabel: cameraOn ? "Turn off camera" : "Turn on camera", icon: cameraOn ? "cam" : "camOff", off: !cameraOn, disabled: !live && !cameraOn, onCanvas: true, onPress: onToggleCamera, size, testID: "dock.camera" }),
311
+ /* @__PURE__ */ jsx4(
312
+ IconButton,
313
+ {
314
+ accessibilityLabel: microphoneOn ? "Mute microphone" : "Unmute microphone",
315
+ busy: local.microphone.state === "requesting",
316
+ icon: microphoneOn ? "mic" : "micOff",
317
+ off: !microphoneOn,
318
+ disabled: !live && !microphoneOn,
319
+ onCanvas: true,
320
+ onPress: onToggleMicrophone,
321
+ size,
322
+ testID: "dock.microphone"
323
+ }
324
+ ),
325
+ /* @__PURE__ */ jsx4(IconButton, { accessibilityLabel: cameraOn ? "Turn off camera" : "Turn on camera", busy: local.camera.state === "requesting", icon: cameraOn ? "cam" : "camOff", off: !cameraOn, disabled: !live && !cameraOn, onCanvas: true, onPress: onToggleCamera, size, testID: "dock.camera" }),
305
326
  output ? /* @__PURE__ */ jsx4(AudioOutputButton, { captioned: arrangement === "portrait", output, size }) : null,
306
327
  /* @__PURE__ */ jsx4(IconButton, { accessibilityLabel: "More options", expanded: moreOpen, icon: "more", onCanvas: true, onPress: onOpenMore, ref: moreRef, size, testID: "dock.more" }),
307
328
  arrangement === "pad" ? /* @__PURE__ */ jsx4(View, { style: styles.gap }) : null,
@@ -403,12 +424,14 @@ var REACTIONS = [
403
424
  function MoreSheet({ open, onClose, onOpenPeople, onOpenChat, inviteLink, returnFocusRef }) {
404
425
  const client = useSpaceClient();
405
426
  const self = useSelf();
427
+ const stage = useStageView();
406
428
  const people2 = useParticipants().roster.length;
407
429
  const toasts = useToasts();
408
430
  const { soundsEnabled } = usePreferences();
409
431
  const canRaiseHand = useCan("raiseHand");
410
432
  const live = useConnection().status === "live";
411
433
  const canReact = useCan("sendReaction") && live;
434
+ const canManageBoard = useCan("manageWhiteboard");
412
435
  const fail = useCallback5((message2) => () => toasts.show({ kind: "error", dismissible: true, message: message2 }), [toasts]);
413
436
  const toggleHand = useCallback5(() => {
414
437
  onClose();
@@ -436,6 +459,27 @@ function MoreSheet({ open, onClose, onOpenPeople, onOpenChat, inviteLink, return
436
459
  /* @__PURE__ */ jsxs3(SheetGroup, { children: [
437
460
  inviteLink ? /* @__PURE__ */ jsx7(CopyLinkRow, { link: inviteLink }) : null,
438
461
  inviteLink ? /* @__PURE__ */ jsx7(SheetRow, { icon: "iosShare", label: "Share link", onPress: () => shareLink(inviteLink), testID: "more.share" }) : null,
462
+ /* @__PURE__ */ jsx7(
463
+ SheetRow,
464
+ {
465
+ icon: "pen",
466
+ label: "Board",
467
+ onPress: () => {
468
+ onClose();
469
+ const transport = client.whiteboard.transport();
470
+ if (!transport) {
471
+ fail("The board is unavailable in this call.")();
472
+ return;
473
+ }
474
+ void transport.startSceneSubscription().then(async () => {
475
+ if (canManageBoard) await transport.setPresentation?.(true);
476
+ stage.setBoardOpen(true);
477
+ stage.showContent();
478
+ }).catch(fail("Couldn\u2019t open the board. Try again."));
479
+ },
480
+ testID: "more.board"
481
+ }
482
+ ),
439
483
  /* @__PURE__ */ jsx7(RecordingRow, { onDone: onClose }),
440
484
  /* @__PURE__ */ jsx7(ChatRow, { onPress: onOpenChat }),
441
485
  /* @__PURE__ */ jsx7(SheetRow, { end: /* @__PURE__ */ jsx7(PeopleCount, { count: people2 }), icon: "users", label: "People", onPress: onOpenPeople, testID: "more.people" }),
@@ -1606,10 +1650,33 @@ var styles11 = StyleSheet11.create({
1606
1650
  });
1607
1651
 
1608
1652
  // src/stage/Stage.tsx
1609
- import { memo as memo11, useCallback as useCallback13, useMemo as useMemo11, useState as useState12 } from "react";
1653
+ import { memo as memo11, useCallback as useCallback14, useEffect as useEffect16, useMemo as useMemo11, useState as useState13 } from "react";
1610
1654
  import { Pressable as Pressable13, StyleSheet as StyleSheet18, View as View18 } from "react-native";
1611
1655
  import Animated9 from "react-native-reanimated";
1612
1656
 
1657
+ // src/whiteboard/native-board-session.ts
1658
+ function ownNativeBoardSession(client, onError) {
1659
+ const transport = client.whiteboard.transport();
1660
+ if (!transport) return () => {
1661
+ };
1662
+ let started = false;
1663
+ let active = true;
1664
+ const observe = () => {
1665
+ if (started || client.getSnapshot().connection.status !== "live") return;
1666
+ started = true;
1667
+ void transport.startSceneSubscription().catch(() => {
1668
+ if (active) onError();
1669
+ });
1670
+ };
1671
+ const unsubscribe = client.subscribe(observe);
1672
+ observe();
1673
+ return () => {
1674
+ active = false;
1675
+ unsubscribe();
1676
+ if (started) void transport.stopSceneSubscription();
1677
+ };
1678
+ }
1679
+
1613
1680
  // src/motion/MovingFrame.tsx
1614
1681
  import { useEffect as useEffect11, useRef as useRef7 } from "react";
1615
1682
  import { StyleSheet as StyleSheet12 } from "react-native";
@@ -1688,13 +1755,37 @@ var tileLeaving = () => {
1688
1755
  };
1689
1756
 
1690
1757
  // src/stage/ContentTile.tsx
1691
- import { memo as memo8, useMemo as useMemo8, useRef as useRef10 } from "react";
1758
+ import { ChalkWhiteboardViewBounds } from "@q9labsai/chalk-client";
1759
+ import { memo as memo8, useCallback as useCallback11, useContext as useContext4, useEffect as useEffect13, useMemo as useMemo8, useRef as useRef10, useState as useState12 } from "react";
1692
1760
  import { Pressable as Pressable11, StyleSheet as StyleSheet14, View as View14 } from "react-native";
1693
1761
 
1694
1762
  // src/components/EmbeddedWhiteboard.tsx
1695
1763
  import { ChalkWhiteboardController } from "@q9labsai/chalk-whiteboard/embedded";
1696
1764
  import { useCallback as useCallback9, useEffect as useEffect12, useMemo as useMemo7, useRef as useRef8, useState as useState11 } from "react";
1697
1765
  import { ActivityIndicator as ActivityIndicator2, Pressable as Pressable10, Text as Text2, useWindowDimensions, View as View12 } from "react-native";
1766
+
1767
+ // src/whiteboard/export-board-image.ts
1768
+ import { setImageAsync } from "expo-clipboard";
1769
+ import { File as File2, Paths } from "expo-file-system";
1770
+ import { isAvailableAsync, shareAsync } from "expo-sharing";
1771
+ async function exportBoardImage(value) {
1772
+ if (value.format !== "png" || !value.dataURL.startsWith("data:image/png;base64,")) throw new Error("Board export is not a PNG image");
1773
+ const base64 = value.dataURL.slice("data:image/png;base64,".length);
1774
+ if (value.action === "copy") {
1775
+ await setImageAsync(base64);
1776
+ return;
1777
+ }
1778
+ if (!await isAvailableAsync()) throw new Error("Image sharing is unavailable on this device");
1779
+ const file = new File2(Paths.cache, `chalk-board-${Date.now()}.png`);
1780
+ try {
1781
+ file.write(base64, { encoding: "base64" });
1782
+ await shareAsync(file.uri, { mimeType: "image/png", UTI: "public.png", dialogTitle: "Save or share the board", anchor: { x: 16, y: 16, width: 1, height: 1 } });
1783
+ } finally {
1784
+ if (file.exists) file.delete();
1785
+ }
1786
+ }
1787
+
1788
+ // src/components/EmbeddedWhiteboard.tsx
1698
1789
  import WebView from "react-native-webview";
1699
1790
 
1700
1791
  // src/whiteboard/embedded-whiteboard-assets.ts
@@ -1743,7 +1834,7 @@ var embeddedWhiteboardWebViewSecurityPolicy = Object.freeze({
1743
1834
  allowFileAccessFromFileURLs: false,
1744
1835
  allowUniversalAccessFromFileURLs: false,
1745
1836
  cacheEnabled: false,
1746
- domStorageEnabled: false,
1837
+ domStorageEnabled: true,
1747
1838
  javaScriptCanOpenWindowsAutomatically: false,
1748
1839
  javaScriptEnabled: true,
1749
1840
  mixedContentMode: "never",
@@ -1751,11 +1842,41 @@ var embeddedWhiteboardWebViewSecurityPolicy = Object.freeze({
1751
1842
  sharedCookiesEnabled: false,
1752
1843
  thirdPartyCookiesEnabled: false
1753
1844
  });
1754
- function EmbeddedWhiteboard({ transport, journeyId, traceparent, tracestate, canDraw, canClear, theme: themeOverride, localParticipantColor, rendererURL: rendererURLOverride, style, testID, onMetric, onError, onUserExport }) {
1845
+ function EmbeddedWhiteboard({
1846
+ board,
1847
+ onFollow,
1848
+ onCloseBoard,
1849
+ transport,
1850
+ journeyId,
1851
+ traceparent,
1852
+ tracestate,
1853
+ canDraw,
1854
+ canClear,
1855
+ theme: themeOverride,
1856
+ localParticipantColor,
1857
+ rendererURL: rendererURLOverride,
1858
+ style,
1859
+ testID,
1860
+ onMetric,
1861
+ onError,
1862
+ onUserExport
1863
+ }) {
1864
+ const toasts = useToasts();
1865
+ const noticesRef = useRef8(/* @__PURE__ */ new Map());
1866
+ const clearNotices = useCallback9(() => {
1867
+ noticesRef.current.forEach((id) => toasts.dismiss(id));
1868
+ noticesRef.current.clear();
1869
+ }, [toasts]);
1755
1870
  const theme = themeOverride ?? "light";
1756
1871
  const webViewRef = useRef8(null);
1757
1872
  const controllerRef = useRef8(null);
1758
1873
  const listenersRef = useRef8(/* @__PURE__ */ new Set());
1874
+ const boardRef = useRef8(board);
1875
+ boardRef.current = board;
1876
+ const onFollowRef = useRef8(onFollow);
1877
+ onFollowRef.current = onFollow;
1878
+ const onCloseBoardRef = useRef8(onCloseBoard);
1879
+ onCloseBoardRef.current = onCloseBoard;
1759
1880
  const onMetricRef = useRef8(onMetric);
1760
1881
  const onErrorRef = useRef8(onError);
1761
1882
  const onUserExportRef = useRef8(onUserExport);
@@ -1784,7 +1905,13 @@ function EmbeddedWhiteboard({ transport, journeyId, traceparent, tracestate, can
1784
1905
  );
1785
1906
  const handleControllerError = useCallback9((error2) => onErrorRef.current?.(error2), []);
1786
1907
  const handleControllerMetric = useCallback9((metric) => onMetricRef.current?.(metric), []);
1787
- const handleControllerUserExport = useCallback9((value) => onUserExportRef.current?.(value), []);
1908
+ const handleControllerUserExport = useCallback9(async (value) => {
1909
+ if (onUserExportRef.current) {
1910
+ await onUserExportRef.current(value);
1911
+ return;
1912
+ }
1913
+ await exportBoardImage(value);
1914
+ }, []);
1788
1915
  useEffect12(() => {
1789
1916
  let active = true;
1790
1917
  setLoadError(null);
@@ -1808,6 +1935,41 @@ function EmbeddedWhiteboard({ transport, journeyId, traceparent, tracestate, can
1808
1935
  journeyId,
1809
1936
  ...traceparent ? { traceparent } : {},
1810
1937
  ...tracestate ? { tracestate } : {},
1938
+ board: boardRef.current,
1939
+ manageSceneSubscription: false,
1940
+ onNotice: (notice) => {
1941
+ if (!notice) {
1942
+ clearNotices();
1943
+ return;
1944
+ }
1945
+ const toastId = toasts.show({
1946
+ kind: notice.kind,
1947
+ message: notice.message,
1948
+ dismissible: notice.kind === "error",
1949
+ ...notice.actionLabel ? {
1950
+ actions: [
1951
+ {
1952
+ label: notice.actionLabel,
1953
+ onPress: () => {
1954
+ controller.performNoticeAction(notice.id);
1955
+ const id = noticesRef.current.get(notice.id);
1956
+ if (id !== void 0) toasts.dismiss(id);
1957
+ }
1958
+ }
1959
+ ]
1960
+ } : {}
1961
+ });
1962
+ noticesRef.current.set(notice.id, toastId);
1963
+ if (noticesRef.current.size > 8) {
1964
+ const oldest = noticesRef.current.entries().next().value;
1965
+ if (oldest) {
1966
+ toasts.dismiss(oldest[1]);
1967
+ noticesRef.current.delete(oldest[0]);
1968
+ }
1969
+ }
1970
+ },
1971
+ onFollow: (id) => onFollowRef.current?.(id),
1972
+ onCloseBoard: () => onCloseBoardRef.current?.(),
1811
1973
  canDraw,
1812
1974
  canClear,
1813
1975
  theme,
@@ -1823,8 +1985,12 @@ function EmbeddedWhiteboard({ transport, journeyId, traceparent, tracestate, can
1823
1985
  return () => {
1824
1986
  if (controllerRef.current === controller) controllerRef.current = null;
1825
1987
  controller.stop();
1988
+ clearNotices();
1826
1989
  };
1827
- }, [handleControllerError, handleControllerMetric, handleControllerUserExport, journeyId, localParticipantColor, rendererPort, theme, traceparent, tracestate, transport]);
1990
+ }, [clearNotices, toasts, handleControllerError, handleControllerMetric, handleControllerUserExport, journeyId, localParticipantColor, rendererPort, theme, traceparent, tracestate, transport]);
1991
+ useEffect12(() => {
1992
+ controllerRef.current?.setBoard(board);
1993
+ }, [board]);
1828
1994
  useEffect12(() => {
1829
1995
  controllerRef.current?.setCapabilities({ canDraw, canClear });
1830
1996
  }, [canClear, canDraw]);
@@ -1842,6 +2008,7 @@ function EmbeddedWhiteboard({ transport, journeyId, traceparent, tracestate, can
1842
2008
  };
1843
2009
  const allowNavigation = (request) => Boolean(rendererURL && isEmbeddedWhiteboardNavigationAllowed(request.url, rendererURL));
1844
2010
  const reloadRenderer = () => {
2011
+ clearNotices();
1845
2012
  controllerRef.current?.rendererReloaded();
1846
2013
  setRendererGeneration(createRendererGeneration());
1847
2014
  };
@@ -1928,16 +2095,21 @@ function RtcVideoView(props) {
1928
2095
  return /* @__PURE__ */ jsx21(RtcView, { ...props });
1929
2096
  }
1930
2097
 
2098
+ // src/frame/call-controls.ts
2099
+ import { createContext as createContext4 } from "react";
2100
+ var LandscapeControlsContext = createContext4(false);
2101
+
1931
2102
  // src/primitives/NameTag.tsx
1932
2103
  import { memo as memo7 } from "react";
1933
2104
  import { StyleSheet as StyleSheet13, View as View13 } from "react-native";
1934
2105
  import { jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
1935
- var NameTag = memo7(function NameTag2({ name, muted = false, pinned = false, icon, you = false, handRaised = false, handOrder, testID }) {
2106
+ var NameTag = memo7(function NameTag2({ name, muted = false, pinned = false, following = false, icon, you = false, handRaised = false, handOrder, testID }) {
1936
2107
  const tagRef = useMeasuredRef(testID, styles13.tag);
1937
2108
  return /* @__PURE__ */ jsx22(View13, { pointerEvents: "none", style: styles13.slot, children: /* @__PURE__ */ jsxs14(View13, { ref: tagRef, style: styles13.tag, testID, children: [
1938
2109
  pinned ? /* @__PURE__ */ jsx22(Icon, { color: fixed.overlayText, name: "pin", size: 14 }) : null,
1939
2110
  icon ? /* @__PURE__ */ jsx22(Icon, { color: fixed.overlayText, name: icon, size: 14 }) : null,
1940
2111
  /* @__PURE__ */ jsx22(ChalkText, { color: fixed.overlayText, numberOfLines: 1, style: styles13.name, variant: "label", children: name }),
2112
+ following ? /* @__PURE__ */ jsx22(ChalkText, { color: fixed.overlayText, variant: "label", children: "following" }) : null,
1941
2113
  you ? /* @__PURE__ */ jsx22(ChalkText, { color: fixed.overlayText, style: styles13.you, variant: "label", children: "(You)" }) : null,
1942
2114
  muted ? /* @__PURE__ */ jsx22(Icon, { color: fixed.overlayDanger, name: "micOff", size: 14 }) : null,
1943
2115
  handRaised ? /* @__PURE__ */ jsxs14(View13, { style: styles13.hand, children: [
@@ -2006,30 +2178,72 @@ function ScreenShare({ sharerId, inner, mini, onShowInSpotlight }) {
2006
2178
  ] });
2007
2179
  }
2008
2180
  var selectBoard = (snapshot) => snapshot.whiteboard.engine.status;
2181
+ var selectRoster = (snapshot) => snapshot.participants.roster;
2182
+ var selectSelfId = (snapshot) => snapshot.self.participantId;
2183
+ var selectConnection = (snapshot) => snapshot.connection.status;
2184
+ var selectAccess = (snapshot) => snapshot.whiteboard.access;
2185
+ var selectCanManage = (snapshot) => snapshot.self.can("manageWhiteboard");
2186
+ var selectSceneId = (snapshot) => snapshot.whiteboard.engine.sceneId;
2009
2187
  function Board({ mini, onShowInSpotlight }) {
2010
2188
  const client = useSpaceClient();
2011
2189
  const { colors, scheme } = useTheme();
2190
+ const view = useStageView();
2191
+ const { width, height } = useDeviceLayout();
2192
+ const landscapeControlsVisible = useContext4(LandscapeControlsContext);
2193
+ const phone = Math.min(width, height) < 600;
2012
2194
  const status = useSpaceSelector(selectBoard);
2013
- const chipStyle = [styles14.viewOnly, { backgroundColor: colors.surface, borderColor: colors.lineStrong }];
2014
- const chipRef = useMeasuredRef("tile.content.view-only", chipStyle);
2195
+ const roster = useSpaceSelector(selectRoster);
2196
+ const selfId = useSpaceSelector(selectSelfId);
2197
+ const connectionStatus = useSpaceSelector(selectConnection);
2198
+ const sceneId = useSpaceSelector(selectSceneId);
2199
+ const access = useSpaceSelector(selectAccess);
2200
+ const canManage = useSpaceSelector(selectCanManage);
2201
+ const toasts = useToasts();
2015
2202
  const transport = useMemo8(() => client.whiteboard.transport(), [client, status]);
2203
+ const [capabilities, setCapabilities] = useState12({ canDraw: false, canClear: false, viewsAvailable: false });
2204
+ useEffect13(() => transport?.subscribeSummary?.((summary) => setCapabilities({ canDraw: summary.canDraw, canClear: summary.canClear, viewsAvailable: transport.supportsViews === true })), [transport]);
2205
+ const board = useMemo8(
2206
+ () => ({
2207
+ primary: !mini,
2208
+ phone,
2209
+ landscapeControlsVisible,
2210
+ following: view.following,
2211
+ ownParticipantId: selfId,
2212
+ people: roster.map((person) => ({ id: person.participantId, displayName: person.displayName })),
2213
+ viewBounds: ChalkWhiteboardViewBounds,
2214
+ sceneId,
2215
+ sceneStatus: status,
2216
+ connectionStatus,
2217
+ viewsAvailable: capabilities.viewsAvailable,
2218
+ ...canManage && access.policy !== null && transport?.setDrawPolicy ? {
2219
+ whoCanDraw: {
2220
+ policy: access.policy,
2221
+ people: roster.filter((person) => person.participantId !== selfId && !person.capabilities.includes("manageWhiteboard")).map((person) => ({ id: person.participantId, name: person.displayName, picked: access.permissions.some((permission) => permission.participantId === person.participantId && permission.canDraw) }))
2222
+ }
2223
+ } : {},
2224
+ colors
2225
+ }),
2226
+ [mini, phone, landscapeControlsVisible, view.following, selfId, roster, sceneId, status, connectionStatus, capabilities.viewsAvailable, canManage, access, transport, colors]
2227
+ );
2228
+ const onClose = useCallback11(() => {
2229
+ void (async () => {
2230
+ if (canManage) await transport?.setPresentation?.(false);
2231
+ view.setBoardOpen(false);
2232
+ view.setFollowing(null);
2233
+ })().catch(() => toasts.show({ kind: "error", dismissible: true, message: "Couldn\u2019t close the board. Try again." }));
2234
+ }, [canManage, transport, toasts, view.setBoardOpen, view.setFollowing]);
2016
2235
  const journeyId = useRef10(`board-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`).current;
2017
2236
  return /* @__PURE__ */ jsxs15(Fragment4, { children: [
2018
- transport ? /* @__PURE__ */ jsx23(EmbeddedWhiteboard, { canClear: false, canDraw: false, journeyId, style: StyleSheet14.absoluteFill, testID: "tile.content.board", theme: scheme, transport }) : null,
2237
+ transport ? /* @__PURE__ */ jsx23(EmbeddedWhiteboard, { board, canClear: capabilities.canClear, canDraw: !mini && capabilities.canDraw, journeyId, onCloseBoard: onClose, onFollow: view.setFollowing, style: StyleSheet14.absoluteFill, testID: "tile.content.board", theme: scheme, transport }) : null,
2019
2238
  mini ? /* @__PURE__ */ jsxs15(Fragment4, { children: [
2020
2239
  /* @__PURE__ */ jsx23(Pressable11, { accessibilityLabel: "Show the board in spotlight", accessibilityRole: "button", onPress: onShowInSpotlight, style: StyleSheet14.absoluteFill, testID: "tile.content.open" }),
2021
2240
  /* @__PURE__ */ jsx23(NameTag, { icon: "pen", name: "Board", testID: "tile.content.name" })
2022
- ] }) : /* @__PURE__ */ jsxs15(View14, { accessibilityLiveRegion: "polite", accessibilityRole: "text", accessible: true, ref: chipRef, style: chipStyle, testID: "tile.content.view-only", children: [
2023
- /* @__PURE__ */ jsx23(Icon, { color: colors.textMuted, name: "lock", size: 16 }),
2024
- /* @__PURE__ */ jsx23(ChalkText, { tone: "textSoft", variant: "section", children: "View only" })
2025
- ] })
2241
+ ] }) : null
2026
2242
  ] });
2027
2243
  }
2028
2244
  var styles14 = StyleSheet14.create({
2029
2245
  tile: { flex: 1, overflow: "hidden", borderRadius: radii.tilePhone },
2030
- box: { position: "absolute", overflow: "hidden" },
2031
- // `.vchip`: a 30 pt pill 12 pt in from the board's top-left corner, with a one-pixel ring.
2032
- viewOnly: { position: "absolute", left: 12, top: 12, height: 30, paddingHorizontal: 12, borderRadius: radii.round, borderWidth: 1, flexDirection: "row", alignItems: "center", gap: 6, zIndex: 3 }
2246
+ box: { position: "absolute", overflow: "hidden" }
2033
2247
  });
2034
2248
 
2035
2249
  // src/stage/layout.ts
@@ -2198,7 +2412,7 @@ function nextStageOrder(previous, present, selfId, speaker) {
2198
2412
  }
2199
2413
 
2200
2414
  // src/stage/ParticipantTile.tsx
2201
- import { memo as memo10, useCallback as useCallback11, useEffect as useEffect14, useMemo as useMemo9 } from "react";
2415
+ import { memo as memo10, useCallback as useCallback12, useEffect as useEffect15, useMemo as useMemo9 } from "react";
2202
2416
  import { Pressable as Pressable12, StyleSheet as StyleSheet16, View as View16 } from "react-native";
2203
2417
  import { useSharedValue as useSharedValue3, withTiming as withTiming5 } from "react-native-reanimated";
2204
2418
 
@@ -2208,17 +2422,17 @@ function tileAvatarSize(width, height) {
2208
2422
  }
2209
2423
 
2210
2424
  // src/primitives/TileShell.tsx
2211
- import { memo as memo9, useEffect as useEffect13 } from "react";
2425
+ import { memo as memo9, useEffect as useEffect14 } from "react";
2212
2426
  import { StyleSheet as StyleSheet15, View as View15 } from "react-native";
2213
2427
  import Animated8, { useAnimatedStyle as useAnimatedStyle4, useSharedValue as useSharedValue2, withTiming as withTiming4 } from "react-native-reanimated";
2214
2428
  import { jsx as jsx24, jsxs as jsxs16 } from "react/jsx-runtime";
2215
- var TileShell = memo9(function TileShell2({ children, speaking = false, cameraOn = false, level, testID }) {
2429
+ var TileShell = memo9(function TileShell2({ children, selected = false, speaking = false, cameraOn = false, level, testID }) {
2216
2430
  const { colors } = useTheme();
2217
2431
  const { phone } = useDeviceLayout();
2218
2432
  const radius = phone ? radii.tilePhone : radii.tile;
2219
2433
  const live = useSharedValue2(0);
2220
2434
  const showRing = speaking && cameraOn;
2221
- useEffect13(() => {
2435
+ useEffect14(() => {
2222
2436
  live.value = withTiming4(showRing ? 1 : 0, timings.scrim);
2223
2437
  }, [live, showRing]);
2224
2438
  const ringStyle = useAnimatedStyle4(() => {
@@ -2227,6 +2441,7 @@ var TileShell = memo9(function TileShell2({ children, speaking = false, cameraOn
2227
2441
  });
2228
2442
  return /* @__PURE__ */ jsxs16(View15, { style: [styles15.tile, { borderRadius: radius, backgroundColor: colors.tile }], testID, children: [
2229
2443
  children,
2444
+ selected ? /* @__PURE__ */ jsx24(View15, { pointerEvents: "none", style: [StyleSheet15.absoluteFill, { borderRadius: radius, borderWidth: 2, borderColor: colors.text, zIndex: 6 }] }) : null,
2230
2445
  /* @__PURE__ */ jsx24(Animated8.View, { pointerEvents: "none", style: [styles15.ring, { borderRadius: radius }, ringStyle], testID: testID ? `${testID}.ring` : void 0, children: /* @__PURE__ */ jsx24(View15, { style: [styles15.ringShade, { borderRadius: radius - 3 }] }) })
2231
2446
  ] });
2232
2447
  });
@@ -2270,12 +2485,12 @@ function selectTile(id) {
2270
2485
  };
2271
2486
  };
2272
2487
  }
2273
- var ParticipantTile = memo10(function ParticipantTile2({ id, width, height, float, pinned, mirror, onOpen }) {
2488
+ var ParticipantTile = memo10(function ParticipantTile2({ id, width, height, float, pinned, following = false, mirror, onOpen, onOptions }) {
2274
2489
  const select = useMemo9(() => selectTile(id), [id]);
2275
2490
  const tile = useSpaceSelector(select, shallowEqual);
2276
2491
  const slot = useDevScenario().videoSlots?.[id];
2277
2492
  const level = useSharedValue3(0);
2278
- useEffect14(() => {
2493
+ useEffect15(() => {
2279
2494
  level.value = withTiming5(tile.speaking ? 0.6 : 0, timings.level);
2280
2495
  }, [level, tile.speaking]);
2281
2496
  const streamURL = useMemo9(() => tile.track ? createNativeMediaStream(tile.track).toURL() : null, [tile.track]);
@@ -2286,10 +2501,10 @@ var ParticipantTile = memo10(function ParticipantTile2({ id, width, height, floa
2286
2501
  const state = `${tile.muted ? ", microphone off" : ""}${tile.handRaised ? ", hand raised" : ""}`;
2287
2502
  const hitRef = useMeasuredRef(`tile.${id}.open`, StyleSheet16.absoluteFill);
2288
2503
  const label = `Options for ${tile.name}${tile.isSelf ? " (you)" : ""}${pinned ? ", pinned" : ""}${state}`;
2289
- return /* @__PURE__ */ jsxs17(TileShell, { cameraOn: video !== null, level, speaking: tile.speaking, testID: `tile.${id}`, children: [
2504
+ return /* @__PURE__ */ jsxs17(TileShell, { selected: following, cameraOn: video !== null, level, speaking: tile.speaking, testID: `tile.${id}`, children: [
2290
2505
  video ?? /* @__PURE__ */ jsx25(View16, { style: styles16.center, children: /* @__PURE__ */ jsx25(FaceAvatar, { level, lookAround: true, name: tile.name, size: tileAvatarSize(width, height - nameTagZone), style: "bold" }) }),
2291
- /* @__PURE__ */ jsx25(Pressable12, { accessibilityHint: "Opens a sheet", accessibilityLabel: label, accessibilityRole: "button", onPress: () => onOpen(id), ref: hitRef, style: StyleSheet16.absoluteFill, testID: `tile.${id}.open` }),
2292
- /* @__PURE__ */ jsx25(NameTag, { handRaised: tile.handRaised, muted: tile.muted, name, pinned, testID: `tile.${id}.name`, you }),
2506
+ /* @__PURE__ */ jsx25(Pressable12, { accessibilityHint: "Opens a sheet", accessibilityLabel: label, accessibilityRole: "button", onPress: () => onOpen(id), onLongPress: () => onOptions?.(id), ref: hitRef, style: StyleSheet16.absoluteFill, testID: `tile.${id}.open` }),
2507
+ /* @__PURE__ */ jsx25(NameTag, { following, handRaised: tile.handRaised, muted: tile.muted, name, pinned, testID: `tile.${id}.name`, you }),
2293
2508
  tile.isSelf && video !== null ? /* @__PURE__ */ jsx25(FlipCameraButton, { back: tile.back, float, testID: `tile.${id}.flip` }) : null
2294
2509
  ] });
2295
2510
  });
@@ -2303,7 +2518,7 @@ function FlipCameraButton({ back, float, testID }) {
2303
2518
  const client = useSpaceClient();
2304
2519
  const toasts = useToasts();
2305
2520
  const other = useSpaceSelector(selectOtherCamera);
2306
- const flip = useCallback11(() => {
2521
+ const flip = useCallback12(() => {
2307
2522
  if (other === null) return;
2308
2523
  client.media.selectCamera(other).catch(() => toasts.show({ kind: "error", dismissible: true, message: "Couldn't switch the camera." }));
2309
2524
  }, [client, other, toasts]);
@@ -2321,10 +2536,10 @@ var styles16 = StyleSheet16.create({
2321
2536
  });
2322
2537
 
2323
2538
  // src/stage/TileSheet.tsx
2324
- import { useCallback as useCallback12, useMemo as useMemo10 } from "react";
2539
+ import { useCallback as useCallback13, useMemo as useMemo10 } from "react";
2325
2540
  import { StyleSheet as StyleSheet17, View as View17 } from "react-native";
2326
2541
  import { jsx as jsx26, jsxs as jsxs18 } from "react/jsx-runtime";
2327
- var closed = { name: "", isSelf: false, cameraOn: false, back: false, otherCamera: null, muted: false, canMute: false, canRemove: false };
2542
+ var closed = { boardOpen: false, name: "", isSelf: false, cameraOn: false, back: false, otherCamera: null, muted: false, canMute: false, canRemove: false };
2328
2543
  function TileSheet({ participantId, open, onClose }) {
2329
2544
  const client = useSpaceClient();
2330
2545
  const toasts = useToasts();
@@ -2335,6 +2550,7 @@ function TileSheet({ participantId, open, onClose }) {
2335
2550
  const person2 = snapshot.participants.roster.find((participant) => participant.participantId === participantId);
2336
2551
  const isSelf = snapshot.self.participantId === participantId;
2337
2552
  return {
2553
+ boardOpen: snapshot.whiteboard.engine.presenting,
2338
2554
  name: person2?.displayName ?? snapshot.self.displayName ?? "",
2339
2555
  isSelf,
2340
2556
  cameraOn: isSelf && snapshot.media.local.camera.state === "enabled",
@@ -2348,8 +2564,10 @@ function TileSheet({ participantId, open, onClose }) {
2348
2564
  [participantId]
2349
2565
  );
2350
2566
  const person = useSpaceSelector(select, shallowEqual);
2567
+ const selectViewsAvailable = useCallback13(() => client.whiteboard.transport()?.supportsViews === true, [client]);
2568
+ const viewsAvailable = useSpaceSelector(selectViewsAvailable);
2351
2569
  const id = participantId ?? "";
2352
- const failed = useCallback12((message2) => () => toasts.show({ kind: "error", dismissible: true, message: message2 }), [toasts]);
2570
+ const failed = useCallback13((message2) => () => toasts.show({ kind: "error", dismissible: true, message: message2 }), [toasts]);
2353
2571
  const run = (action) => () => {
2354
2572
  onClose();
2355
2573
  action();
@@ -2360,6 +2578,18 @@ function TileSheet({ participantId, open, onClose }) {
2360
2578
  /* @__PURE__ */ jsx26(ChalkText, { numberOfLines: 1, style: styles17.name, variant: "sheetTitle", children: `${person.name}${person.isSelf ? " (You)" : ""}` })
2361
2579
  ] }),
2362
2580
  /* @__PURE__ */ jsxs18(SheetGroup, { children: [
2581
+ (view.boardOpen ?? person.boardOpen) && viewsAvailable && !person.isSelf ? /* @__PURE__ */ jsx26(
2582
+ SheetRow,
2583
+ {
2584
+ icon: "pin",
2585
+ label: view.following === participantId ? "Stop following" : "Follow",
2586
+ onPress: run(() => {
2587
+ view.showContent();
2588
+ view.setFollowing(view.following === participantId ? null : id);
2589
+ }),
2590
+ testID: "tile-sheet.follow"
2591
+ }
2592
+ ) : null,
2363
2593
  /* @__PURE__ */ jsx26(SheetRow, { icon: "pin", label: view.pinned === participantId ? "Unpin" : "Pin", onPress: run(() => view.togglePin(id)), testID: "tile-sheet.pin" }),
2364
2594
  person.cameraOn && person.otherCamera !== null ? /* @__PURE__ */ jsx26(
2365
2595
  SheetRow,
@@ -2411,20 +2641,33 @@ function selectStructure(snapshot) {
2411
2641
  present: (hasSelf && selfId !== null ? [selfId, ...others] : others).join("\n"),
2412
2642
  speaker: roster.find((person) => person.presence.activeSpeaker)?.participantId ?? null,
2413
2643
  sharer: roster.find((person) => person.participantId !== selfId && person.media.screenShare === "active")?.participantId ?? null,
2414
- board: snapshot.whiteboard.open
2644
+ board: snapshot.whiteboard.engine.presenting
2415
2645
  };
2416
2646
  }
2417
2647
  var Stage = memo11(function Stage2({ frame, onOpenPeople }) {
2648
+ const client = useSpaceClient();
2649
+ const toasts = useToasts();
2650
+ useEffect16(() => ownNativeBoardSession(client, () => toasts.show({ kind: "error", dismissible: true, message: "Couldn\u2019t open the board. Try again from More." })), [client, toasts]);
2418
2651
  const structure = useSpaceSelector(selectStructure, shallowEqual);
2652
+ const selectViewsAvailable = useCallback14(() => client.whiteboard.transport()?.supportsViews === true, [client]);
2653
+ const viewsAvailable = useSpaceSelector(selectViewsAvailable);
2419
2654
  const view = useStageView();
2420
2655
  const { arrangement } = useDeviceLayout();
2421
2656
  const { shareAspect } = useDevScenario();
2422
- const [sheet, setSheet] = useState12({ id: null, open: false });
2657
+ const [sheet, setSheet] = useState13({ id: null, open: false });
2658
+ useEffect16(() => {
2659
+ if (view.following && !structure.present.split("\n").includes(view.following)) view.setFollowing(null);
2660
+ }, [structure.present, view.following, view.setFollowing]);
2423
2661
  const present = useMemo11(() => structure.present ? structure.present.split("\n") : [], [structure.present]);
2424
- const [order, setOrder] = useState12({ order: [], spot: null });
2662
+ const [order, setOrder] = useState13({ order: [], spot: null });
2425
2663
  const next = nextStageOrder(order, present, structure.selfId, structure.speaker);
2426
2664
  if (next !== order) setOrder(next);
2427
- const content = useMemo11(() => structure.board ? { kind: "board" } : structure.sharer !== null ? { kind: "screen", sharerId: structure.sharer } : null, [structure.board, structure.sharer]);
2665
+ useEffect16(() => {
2666
+ view.setBoardOpen(null);
2667
+ view.setFollowing(null);
2668
+ }, [structure.board, view.setBoardOpen, view.setFollowing]);
2669
+ const boardVisible = view.boardOpen ?? structure.board;
2670
+ const content = useMemo11(() => boardVisible ? { kind: "board" } : structure.sharer !== null ? { kind: "screen", sharerId: structure.sharer } : null, [boardVisible, structure.sharer]);
2428
2671
  const pinned = view.pinned !== null && present.includes(view.pinned) ? view.pinned : null;
2429
2672
  const items = useMemo11(
2430
2673
  () => stageLayout({
@@ -2439,21 +2682,32 @@ var Stage = memo11(function Stage2({ frame, onOpenPeople }) {
2439
2682
  }),
2440
2683
  [arrangement, content, frame, next, pinned, shareAspect, view.layout]
2441
2684
  );
2442
- const openTile = useCallback13((id) => setSheet({ id, open: true }), []);
2443
- const closeTile = useCallback13(() => setSheet((current) => ({ ...current, open: false })), []);
2685
+ const followTile = useCallback14(
2686
+ (id) => {
2687
+ if (!boardVisible || !viewsAvailable || id === structure.selfId) {
2688
+ setSheet({ id, open: true });
2689
+ return;
2690
+ }
2691
+ view.showContent();
2692
+ view.setFollowing(view.following === id ? null : id);
2693
+ },
2694
+ [boardVisible, viewsAvailable, structure.selfId, view]
2695
+ );
2696
+ const openTile = useCallback14((id) => setSheet({ id, open: true }), []);
2697
+ const closeTile = useCallback14(() => setSheet((current) => ({ ...current, open: false })), []);
2444
2698
  const stageRef = useMeasuredRef("frame.stage", styles18.layer);
2445
2699
  return /* @__PURE__ */ jsxs19(View18, { pointerEvents: "box-none", ref: stageRef, style: styles18.layer, testID: "frame.stage", children: [
2446
- items.map((item) => /* @__PURE__ */ jsx27(StageTile, { content, item, mirror: view.mirror, onOpenPeople, onOpenTile: openTile, onShowContent: view.showContent, peopleCount: present.length, pinned }, itemKey(item))),
2700
+ items.map((item) => /* @__PURE__ */ jsx27(StageTile, { content, item, mirror: view.mirror, onOpenPeople, onOpenTile: followTile, onTileOptions: openTile, following: view.following, onShowContent: view.showContent, peopleCount: present.length, pinned }, itemKey(item))),
2447
2701
  /* @__PURE__ */ jsx27(TileSheet, { onClose: closeTile, open: sheet.open, participantId: sheet.id })
2448
2702
  ] });
2449
2703
  });
2450
2704
  function itemKey(item) {
2451
2705
  return item.kind === "person" ? `person:${item.id}` : item.kind;
2452
2706
  }
2453
- function StageTile({ item, content, pinned, mirror, peopleCount, onOpenTile, onOpenPeople, onShowContent }) {
2707
+ function StageTile({ item, content, pinned, mirror, peopleCount, onOpenTile, onOpenPeople, onShowContent, following, onTileOptions }) {
2454
2708
  const { scheme } = useTheme();
2455
2709
  const float = item.kind === "person" && item.float;
2456
- return /* @__PURE__ */ jsx27(MovingFrame, { frame: item.frame, style: float ? [styles18.float, { boxShadow: elevations[scheme].floatTile }] : null, children: /* @__PURE__ */ jsx27(Animated9.View, { entering: tileEntering, exiting: tileLeaving, style: styles18.fill, children: item.kind === "person" ? /* @__PURE__ */ jsx27(ParticipantTile, { float, height: item.frame.height, id: item.id, mirror, onOpen: onOpenTile, pinned: pinned === item.id, width: item.frame.width }) : item.kind === "content" && content ? /* @__PURE__ */ jsx27(ContentTile, { content, inner: item.inner, mini: item.mini, onShowInSpotlight: onShowContent }) : item.kind === "more" ? /* @__PURE__ */ jsx27(MoreTile, { count: item.count, onPress: onOpenPeople, peopleCount }) : null }) });
2710
+ return /* @__PURE__ */ jsx27(MovingFrame, { frame: item.frame, style: float ? [styles18.float, { boxShadow: elevations[scheme].floatTile }] : null, children: /* @__PURE__ */ jsx27(Animated9.View, { entering: tileEntering, exiting: tileLeaving, style: styles18.fill, children: item.kind === "person" ? /* @__PURE__ */ jsx27(ParticipantTile, { float, height: item.frame.height, id: item.id, mirror, onOpen: onOpenTile, onOptions: onTileOptions, following: following === item.id, pinned: pinned === item.id, width: item.frame.width }) : item.kind === "content" && content ? /* @__PURE__ */ jsx27(ContentTile, { content, inner: item.inner, mini: item.mini, onShowInSpotlight: onShowContent }) : item.kind === "more" ? /* @__PURE__ */ jsx27(MoreTile, { count: item.count, onPress: onOpenPeople, peopleCount }) : null }) });
2457
2711
  }
2458
2712
  var MoreTile = memo11(function MoreTile2({ count, peopleCount, onPress }) {
2459
2713
  const { colors } = useTheme();
@@ -2469,12 +2723,33 @@ var styles18 = StyleSheet18.create({
2469
2723
  moreText: { fontFamily: fontFamilies[600], fontSize: 22, lineHeight: 22 * 1.4 }
2470
2724
  });
2471
2725
 
2726
+ // src/frame/use-landscape-chrome.ts
2727
+ import { useCallback as useCallback15, useEffect as useEffect17, useState as useState14 } from "react";
2728
+ function useLandscapeChrome({ fades, boardVisible }) {
2729
+ const canHide = fades && !boardVisible;
2730
+ const [hidden, setHidden] = useState14(false);
2731
+ const [shownAt, setShownAt] = useState14(0);
2732
+ useEffect17(() => {
2733
+ if (!canHide) {
2734
+ setHidden(false);
2735
+ return;
2736
+ }
2737
+ const timer = setTimeout(() => setHidden(true), durations.landscapeHide);
2738
+ return () => clearTimeout(timer);
2739
+ }, [canHide, shownAt]);
2740
+ const show = useCallback15(() => {
2741
+ setHidden(false);
2742
+ setShownAt(Date.now());
2743
+ }, []);
2744
+ return { hidden: canHide && hidden, show };
2745
+ }
2746
+
2472
2747
  // src/status/CallOverlays.tsx
2473
- import { useEffect as useEffect17 } from "react";
2748
+ import { useEffect as useEffect20 } from "react";
2474
2749
  import { StyleSheet as StyleSheet21, View as View20 } from "react-native";
2475
2750
 
2476
2751
  // src/limbo/Veil.tsx
2477
- import { useEffect as useEffect15 } from "react";
2752
+ import { useEffect as useEffect18 } from "react";
2478
2753
  import { StyleSheet as StyleSheet19 } from "react-native";
2479
2754
  import Animated10, { ReduceMotion as ReduceMotion2, useAnimatedStyle as useAnimatedStyle5, useSharedValue as useSharedValue4, withTiming as withTiming6 } from "react-native-reanimated";
2480
2755
  import { jsx as jsx28 } from "react/jsx-runtime";
@@ -2483,7 +2758,7 @@ var LIFT_FROM = 0.6;
2483
2758
  function Veil({ level }) {
2484
2759
  const { colors } = useTheme();
2485
2760
  const opacity = useSharedValue4(OPACITY[level]);
2486
- useEffect15(() => {
2761
+ useEffect18(() => {
2487
2762
  if (level !== "none") {
2488
2763
  opacity.value = OPACITY[level];
2489
2764
  return;
@@ -2500,7 +2775,8 @@ function Veil({ level }) {
2500
2775
  }
2501
2776
 
2502
2777
  // src/status/StatusPage.tsx
2503
- import { forwardRef as forwardRef3, memo as memo12, useEffect as useEffect16, useRef as useRef11 } from "react";
2778
+ import { forwardRef as forwardRef3, memo as memo12, useEffect as useEffect19, useRef as useRef11 } from "react";
2779
+ import { formatSyncConnectDiagnostic, getConnectionFailureDetail } from "@q9labsai/chalk-client";
2504
2780
  import { AccessibilityInfo as AccessibilityInfo2, findNodeHandle as findNodeHandle2, Pressable as Pressable14, StyleSheet as StyleSheet20, View as View19 } from "react-native";
2505
2781
  import Animated11, { useAnimatedStyle as useAnimatedStyle6 } from "react-native-reanimated";
2506
2782
  import { jsx as jsx29, jsxs as jsxs20 } from "react/jsx-runtime";
@@ -2511,7 +2787,7 @@ function pageFor(outcome, callName) {
2511
2787
  case "ended":
2512
2788
  return { title: "The call ended", actions: ["rejoin", "home"] };
2513
2789
  case "failed":
2514
- return { title: `Couldn\u2019t connect to ${callName}`, detail: "Check your internet connection, then try again.", actions: ["rejoin", "home"], rejoinLabel: "Try again" };
2790
+ return { title: `Couldn\u2019t connect to ${callName}`, actions: ["rejoin", "home"], rejoinLabel: "Try again" };
2515
2791
  case "removed":
2516
2792
  return { title: `You were removed from ${callName}`, detail: "The host removed you from this call.", actions: ["home"] };
2517
2793
  case "exited":
@@ -2520,19 +2796,21 @@ function pageFor(outcome, callName) {
2520
2796
  return { title: "This call is full", detail: `${callName} has reached 100 people. Ask the host to make room, then try again.`, actions: ["rejoin", "home"], rejoinLabel: "Try again" };
2521
2797
  }
2522
2798
  }
2523
- function StatusPage({ outcome, callName, onRejoin, onHome }) {
2799
+ function StatusPage({ outcome, callName, failure, onRejoin, onHome }) {
2524
2800
  const { colors } = useTheme();
2525
2801
  const page = pageFor(outcome, callName);
2802
+ const detail = outcome === "failed" ? getConnectionFailureDetail({ code: failure?.code, sync: failure?.sync }) : page.detail;
2526
2803
  const pageStyle = [styles19.page, { backgroundColor: colors.canvas }];
2527
2804
  const ref = useMeasuredRef("status.page", pageStyle);
2528
2805
  const firstAction = useRef11(null);
2529
- useEffect16(() => {
2806
+ useEffect19(() => {
2530
2807
  const node = firstAction.current && findNodeHandle2(firstAction.current);
2531
2808
  if (node) AccessibilityInfo2.setAccessibilityFocus(node);
2532
2809
  }, [outcome]);
2533
2810
  return /* @__PURE__ */ jsxs20(View19, { accessibilityViewIsModal: true, ref, style: pageStyle, testID: "status.page", children: [
2534
2811
  /* @__PURE__ */ jsx29(ChalkText, { accessibilityRole: "header", style: styles19.center, testID: "status.title", variant: "statusTitle", children: page.title }),
2535
- page.detail ? /* @__PURE__ */ jsx29(ChalkText, { style: styles19.center, testID: "status.detail", tone: "textMuted", children: page.detail }) : null,
2812
+ detail ? /* @__PURE__ */ jsx29(ChalkText, { style: styles19.center, testID: "status.detail", tone: "textMuted", children: detail }) : null,
2813
+ outcome === "failed" && failure ? /* @__PURE__ */ jsx29(ChalkText, { style: styles19.center, testID: "status.reference", tone: "textMuted", variant: "caption", children: `Reference: ${failure.code}${failure.sync ? ` ${formatSyncConnectDiagnostic(failure.sync)}` : ""}` }) : null,
2536
2814
  /* @__PURE__ */ jsx29(View19, { style: styles19.actions, children: page.actions.map(
2537
2815
  (action, index) => action === "rejoin" ? /* @__PURE__ */ jsx29(StatusAction, { icon: "retry", label: page.rejoinLabel ?? "Rejoin", onPress: onRejoin, ref: index === 0 ? firstAction : void 0, testID: "status.rejoin" }, action) : /* @__PURE__ */ jsx29(StatusAction, { icon: "home", label: "Home", onPress: onHome, ref: index === 0 ? firstAction : void 0, testID: "status.home" }, action)
2538
2816
  ) })
@@ -2579,7 +2857,7 @@ function CallOverlays({ screen, callName, rejoining, onHold, onRejoin, onHome })
2579
2857
  useBackHandler(screen.kind === "status", onHome);
2580
2858
  const { clear } = useToasts();
2581
2859
  const clearsNotices = screen.kind === "status" || screen.kind === "joining";
2582
- useEffect17(() => {
2860
+ useEffect20(() => {
2583
2861
  if (clearsNotices) clear();
2584
2862
  }, [clear, clearsNotices]);
2585
2863
  const limbo = screen.kind === "joining" ? { kind: "joining", rejoining } : screen.kind === "call" && onHold ? { kind: "hold" } : null;
@@ -2588,7 +2866,7 @@ function CallOverlays({ screen, callName, rejoining, onHold, onRejoin, onHome })
2588
2866
  /* @__PURE__ */ jsx30(Veil, { level: screen.kind === "status" ? "none" : veil }),
2589
2867
  screen.kind === "reconnecting" || screen.kind === "call" && connection.syncHealth === "degraded" ? /* @__PURE__ */ jsx30(View20, { pointerEvents: "none", style: { position: "absolute", top: header.y + header.height + 4, left: layout.insets.left + 16, right: layout.insets.right + 16, alignItems: "center" }, testID: "connection.notice", children: /* @__PURE__ */ jsx30(ChalkText, { accessibilityLiveRegion: "polite", variant: "caption", style: { backgroundColor: colors.surface, borderRadius: 12, paddingHorizontal: 12, paddingVertical: 6 }, children: screen.kind === "reconnecting" ? `Reconnecting\u2026${queuedMessages ? ` ${queuedMessages} messages waiting to send.` : ""}` : "Connection is slow" }) }) : null,
2590
2868
  limbo ? /* @__PURE__ */ jsx30(Limbo, { callName, recording: recording2, state: limbo }) : null,
2591
- screen.kind === "status" ? /* @__PURE__ */ jsx30(StatusPage, { callName, onHome, onRejoin, outcome: screen.outcome }) : null
2869
+ screen.kind === "status" ? /* @__PURE__ */ jsx30(StatusPage, { callName, failure: connection.lastError, onHome, onRejoin, outcome: screen.outcome }) : null
2592
2870
  ] });
2593
2871
  }
2594
2872
 
@@ -2630,7 +2908,7 @@ function callScreen(connection, leftByChoice) {
2630
2908
 
2631
2909
  // src/toasts/CallEventToasts.tsx
2632
2910
  import { SpaceClientError } from "@q9labsai/chalk-client";
2633
- import { useCallback as useCallback14, useEffect as useEffect18, useRef as useRef12 } from "react";
2911
+ import { useCallback as useCallback16, useEffect as useEffect21, useRef as useRef12 } from "react";
2634
2912
  import { Linking as Linking2, Platform as Platform7 } from "react-native";
2635
2913
 
2636
2914
  // src/toasts/media-failure-copy.ts
@@ -2688,7 +2966,7 @@ function useCallEventToasts({ onOpenSettings, resumedAfterSettings }) {
2688
2966
  const ownMicrophoneChange = useRef12(false);
2689
2967
  const latest = useRef12({ colors, soundsEnabled });
2690
2968
  latest.current = { colors, soundsEnabled };
2691
- useEffect18(() => {
2969
+ useEffect21(() => {
2692
2970
  let previous = client.getSnapshot();
2693
2971
  return client.subscribe(() => {
2694
2972
  const next = client.getSnapshot();
@@ -2705,10 +2983,10 @@ function useCallEventToasts({ onOpenSettings, resumedAfterSettings }) {
2705
2983
  });
2706
2984
  }, [client, toasts]);
2707
2985
  useResumedAfterSettings(resumedAfterSettings, toasts);
2708
- const noteOwnMicrophoneChange = useCallback14(() => {
2986
+ const noteOwnMicrophoneChange = useCallback16(() => {
2709
2987
  ownMicrophoneChange.current = true;
2710
2988
  }, []);
2711
- const reportMediaFailure = useCallback14(
2989
+ const reportMediaFailure = useCallback16(
2712
2990
  function reportMediaFailure2(permission, cause, retry) {
2713
2991
  const code = cause instanceof SpaceClientError ? cause.code : null;
2714
2992
  const icon = permission === "microphone" ? "micOff" : "camOff";
@@ -2773,7 +3051,7 @@ function useResumedAfterSettings(permission, toasts) {
2773
3051
  const client = useSpaceClient();
2774
3052
  const { colors } = useTheme();
2775
3053
  const shown = useRef12(false);
2776
- useEffect18(() => {
3054
+ useEffect21(() => {
2777
3055
  if (!permission) return;
2778
3056
  const check = () => {
2779
3057
  if (shown.current || client.getSnapshot().connection.status !== "live") return;
@@ -2838,31 +3116,31 @@ function CallFrame({ displayName, callName = "Call", inviteLink, onLeave, onOpen
2838
3116
  const { soundsEnabled } = usePreferences();
2839
3117
  const toasts = useToasts();
2840
3118
  const callEvents = useCallEventToasts({ onOpenSettings, resumedAfterSettings });
2841
- const [leftByChoice, setLeftByChoice] = useState13(false);
2842
- const [sheet, setSheet] = useState13(null);
3119
+ const [leftByChoice, setLeftByChoice] = useState15(false);
3120
+ const [sheet, setSheet] = useState15(null);
2843
3121
  const moreRef = useRef13(null);
2844
3122
  const leaveRef = useRef13(null);
2845
3123
  const nameRef = useRef13(null);
2846
3124
  const canEnd = useCan("endEpisode");
2847
- const [leaveMenuOpen, setLeaveMenuOpen] = useState13(false);
3125
+ const [leaveMenuOpen, setLeaveMenuOpen] = useState15(false);
2848
3126
  const sheetCount = usePortalCount("sheets");
2849
3127
  const menuCount = usePortalCount("menus");
2850
3128
  const layered = sheetCount > 0 || menuCount > 0;
2851
3129
  const regions = useMemo12(() => frameRegions(layout.arrangement, layout.width, layout.height, layout.insets), [layout]);
2852
3130
  const screenStyle = [styles21.root, { backgroundColor: colors.canvas }];
2853
3131
  const screenRef = useMeasuredRef("frame.screen", screenStyle);
2854
- const join2 = useCallback15(() => {
3132
+ const join2 = useCallback17(() => {
2855
3133
  setLeftByChoice(false);
2856
3134
  client.join({ displayName, microphone: false, camera: false }).catch(() => void 0);
2857
3135
  }, [client, displayName]);
2858
- useEffect19(() => {
3136
+ useEffect22(() => {
2859
3137
  if (client.getSnapshot().connection.status === "idle") join2();
2860
3138
  }, [client, join2]);
2861
3139
  useAndroidCallService();
2862
3140
  const microphoneOn = media.local.microphone.state === "enabled";
2863
3141
  const cameraOn = media.local.camera.state === "enabled";
2864
3142
  const { noteOwnMicrophoneChange, reportMediaFailure } = callEvents;
2865
- const toggleMicrophone = useCallback15(() => {
3143
+ const toggleMicrophone = useCallback17(() => {
2866
3144
  haptics.mute();
2867
3145
  playSound(microphoneOn ? "mute" : "unmute", soundsEnabled);
2868
3146
  if (microphoneOn) noteOwnMicrophoneChange();
@@ -2870,12 +3148,12 @@ function CallFrame({ displayName, callName = "Call", inviteLink, onLeave, onOpen
2870
3148
  if (microphoneOn) client.media.setMicrophoneEnabled(false).catch(() => toasts.show({ kind: "error", dismissible: true, message: "Couldn\u2019t mute your microphone." }));
2871
3149
  else void turnOn().catch((cause) => reportMediaFailure("microphone", cause, turnOn));
2872
3150
  }, [client, microphoneOn, noteOwnMicrophoneChange, reportMediaFailure, soundsEnabled, toasts]);
2873
- const toggleCamera = useCallback15(() => {
3151
+ const toggleCamera = useCallback17(() => {
2874
3152
  const turnOn = () => client.media.setCameraEnabled(true);
2875
3153
  if (cameraOn) client.media.setCameraEnabled(false).catch(() => toasts.show({ kind: "error", dismissible: true, message: "Couldn\u2019t turn off your camera." }));
2876
3154
  else void turnOn().catch((cause) => reportMediaFailure("camera", cause, turnOn));
2877
3155
  }, [cameraOn, client, reportMediaFailure, toasts]);
2878
- const exit = useCallback15(
3156
+ const exit = useCallback17(
2879
3157
  (how) => {
2880
3158
  setSheet(null);
2881
3159
  setLeaveMenuOpen(false);
@@ -2886,26 +3164,36 @@ function CallFrame({ displayName, callName = "Call", inviteLink, onLeave, onOpen
2886
3164
  },
2887
3165
  [client, toasts]
2888
3166
  );
2889
- const pressLeave = useCallback15(() => {
3167
+ const pressLeave = useCallback17(() => {
2890
3168
  haptics.leave();
2891
3169
  if (canEnd) setLeaveMenuOpen((open) => !open);
2892
3170
  else exit("leave");
2893
3171
  }, [canEnd, exit]);
2894
3172
  const devScenario = useDevScenario();
2895
- useEffect19(() => {
3173
+ useEffect22(() => {
2896
3174
  if (devScenario.sheet && devScenario.sheet !== "route") setSheet(devScenario.sheet);
2897
3175
  if (devScenario.menu === "leave") setLeaveMenuOpen(true);
2898
3176
  }, [devScenario]);
2899
- const chrome = useLandscapeChrome(layout.arrangement === "landscape" && sheet === null && !layered);
3177
+ const sharedBoard = useWhiteboard().engine.presenting;
3178
+ const boardVisible = useStageView().boardOpen ?? sharedBoard;
3179
+ const chrome = useLandscapeChrome({ fades: layout.arrangement === "landscape" && sheet === null && !layered, boardVisible });
3180
+ const chromeOpacity = useSharedValue5(1);
3181
+ useEffect22(() => {
3182
+ chromeOpacity.value = withTiming7(chrome.hidden ? 0 : 1, timings.fade);
3183
+ }, [chrome.hidden, chromeOpacity]);
3184
+ const chromeStyle = useAnimatedStyle7(() => {
3185
+ "worklet";
3186
+ return { opacity: chromeOpacity.value };
3187
+ });
2900
3188
  const forced = devScenario.callState;
2901
3189
  const screen = forced === "hold" ? { kind: "call" } : forced === "left" || forced === "ended" || forced === "failed" || forced === "removed" || forced === "exited" ? { kind: "status", outcome: forced } : callScreen(connection, leftByChoice);
2902
3190
  return /* @__PURE__ */ jsxs22(View22, { ref: screenRef, style: screenStyle, testID: "frame.screen", children: [
2903
3191
  /* @__PURE__ */ jsx33(StatusBar, { barStyle: scheme === "dark" ? "light-content" : "dark-content" }),
2904
3192
  /* @__PURE__ */ jsxs22(View22, { accessibilityElementsHidden: layered, importantForAccessibility: layered ? "no-hide-descendants" : "auto", style: StyleSheet23.absoluteFill, children: [
2905
- /* @__PURE__ */ jsx33(Stage, { frame: regions.stage, onOpenPeople: () => setSheet("people") }),
3193
+ /* @__PURE__ */ jsx33(LandscapeControlsContext.Provider, { value: layout.arrangement === "landscape" && !chrome.hidden, children: /* @__PURE__ */ jsx33(Stage, { frame: regions.stage, onOpenPeople: () => setSheet("people") }) }),
2906
3194
  chrome.hidden ? /* @__PURE__ */ jsx33(Pressable15, { accessibilityLabel: "Show call controls", onPress: chrome.show, style: StyleSheet23.absoluteFill, testID: "frame.show-controls" }) : null,
2907
- /* @__PURE__ */ jsx33(Animated12.View, { pointerEvents: chrome.hidden ? "none" : "box-none", style: [styles21.region, frameStyle(regions.header), chrome.style], children: /* @__PURE__ */ jsx33(Header, { arrangement: layout.arrangement, callName, detailsOpen: sheet === "details", onOpenDetails: () => setSheet("details"), ref: nameRef }) }),
2908
- /* @__PURE__ */ jsx33(Animated12.View, { pointerEvents: chrome.hidden ? "none" : "box-none", style: [styles21.region, frameStyle(regions.dock), chrome.style], children: /* @__PURE__ */ jsx33(
3195
+ /* @__PURE__ */ jsx33(Animated12.View, { pointerEvents: chrome.hidden ? "none" : "box-none", style: [styles21.region, frameStyle(regions.header), chromeStyle], children: /* @__PURE__ */ jsx33(Header, { arrangement: layout.arrangement, callName, detailsOpen: sheet === "details", onOpenDetails: () => setSheet("details"), ref: nameRef }) }),
3196
+ /* @__PURE__ */ jsx33(Animated12.View, { pointerEvents: chrome.hidden ? "none" : "box-none", style: [styles21.region, frameStyle(regions.dock), chromeStyle], children: /* @__PURE__ */ jsx33(
2909
3197
  Dock,
2910
3198
  {
2911
3199
  arrangement: layout.arrangement,
@@ -2937,31 +3225,6 @@ function CallFrame({ displayName, callName = "Call", inviteLink, onLeave, onOpen
2937
3225
  /* @__PURE__ */ jsx33(ChatNotices, { chatVisible: sheet === "chat", onOpenChat: () => setSheet("chat"), sheetOpen: sheet === "people" || sheet === "chat" })
2938
3226
  ] });
2939
3227
  }
2940
- function useLandscapeChrome(fades) {
2941
- const [hidden, setHidden] = useState13(false);
2942
- const [shownAt, setShownAt] = useState13(0);
2943
- const opacity = useSharedValue5(1);
2944
- useEffect19(() => {
2945
- opacity.value = withTiming7(hidden ? 0 : 1, timings.fade);
2946
- }, [hidden, opacity]);
2947
- useEffect19(() => {
2948
- if (!fades) {
2949
- setHidden(false);
2950
- return;
2951
- }
2952
- const timer = setTimeout(() => setHidden(true), durations.landscapeHide);
2953
- return () => clearTimeout(timer);
2954
- }, [fades, shownAt]);
2955
- const show = useCallback15(() => {
2956
- setHidden(false);
2957
- setShownAt(Date.now());
2958
- }, []);
2959
- const style = useAnimatedStyle7(() => {
2960
- "worklet";
2961
- return { opacity: opacity.value };
2962
- });
2963
- return { hidden, show, style };
2964
- }
2965
3228
  function frameStyle(frame) {
2966
3229
  return { left: frame.x, top: frame.y, width: frame.width, height: frame.height };
2967
3230
  }
@@ -2973,7 +3236,7 @@ var styles21 = StyleSheet23.create({
2973
3236
  // src/frame/Chalk.tsx
2974
3237
  import { jsx as jsx34 } from "react/jsx-runtime";
2975
3238
  function Chalk({ client, colorScheme, onColorSchemeChange, soundsEnabled, onSoundsEnabledChange, ...frame }) {
2976
- useEffect20(() => releaseSounds, []);
3239
+ useEffect23(() => releaseSounds, []);
2977
3240
  return /* @__PURE__ */ jsx34(GestureHandlerRootView, { style: styles22.root, children: /* @__PURE__ */ jsx34(SafeAreaProvider2, { children: /* @__PURE__ */ jsx34(PreferencesProvider, { colorScheme, onColorSchemeChange, onSoundsEnabledChange, soundsEnabled, children: (preferences) => /* @__PURE__ */ jsx34(ThemeProvider, { colorScheme: preferences.colorScheme, children: /* @__PURE__ */ jsx34(DeviceLayoutProvider, { children: /* @__PURE__ */ jsx34(PortalProvider, { children: /* @__PURE__ */ jsx34(ToastProvider, { children: /* @__PURE__ */ jsx34(ChalkProvider, { client, children: /* @__PURE__ */ jsx34(StageViewProvider, { children: /* @__PURE__ */ jsx34(CallFrame, { ...frame }) }) }) }) }) }) }) }) }) });
2978
3241
  }
2979
3242
  var styles22 = StyleSheet24.create({
@@ -2994,6 +3257,7 @@ export {
2994
3257
  buildChalkInviteLink,
2995
3258
  extractSpaceInviteFromLink,
2996
3259
  getClipboardInviteSuggestion,
3260
+ parseChalkInviteLink,
2997
3261
  parseSpaceInviteLink,
2998
3262
  useCan,
2999
3263
  useChat,