@noya-app/noya-multiplayer-react 0.1.70 → 0.1.71

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.
@@ -44,6 +44,7 @@ export type StateInspectorAnchor =
44
44
  export type StateInspectorOptions = {
45
45
  colorScheme?: "light" | "dark";
46
46
  anchor?: StateInspectorAnchor;
47
+ advanced?: boolean;
47
48
  };
48
49
 
49
50
  export const StateInspector = memoGeneric(function StateInspector<
@@ -56,6 +57,7 @@ export const StateInspector = memoGeneric(function StateInspector<
56
57
  colorScheme = "light",
57
58
  anchor = "right",
58
59
  unstyled = false,
60
+ advanced = false,
59
61
  ...props
60
62
  }: {
61
63
  noyaManager: NoyaManager<S, M, E, MenuT>;
@@ -65,7 +67,7 @@ export const StateInspector = memoGeneric(function StateInspector<
65
67
  const {
66
68
  multiplayerStateManager,
67
69
  assetManager,
68
- connectedUsersManager,
70
+ userManager,
69
71
  secretManager,
70
72
  ephemeralUserDataManager,
71
73
  connectionEventManager,
@@ -159,8 +161,8 @@ export const StateInspector = memoGeneric(function StateInspector<
159
161
  const assetsInitialized = useObservable(assetManager.isInitialized$);
160
162
  const secrets = useObservable(secretManager.secrets$);
161
163
  const secretsInitialized = useObservable(secretManager.isInitialized$);
162
- const connectedUsers = useObservable(connectedUsersManager.connectedUsers$);
163
- const userId = useObservable(connectedUsersManager.currentUserId$);
164
+ const connectedUsers = useObservable(userManager.connectedUsers$);
165
+ const userId = useObservable(userManager.currentUserId$);
164
166
  const state = useObservable(multiplayerStateManager.optimisticState$);
165
167
  const inputsInitialized = useObservable(ioManager.inputsInitialized$);
166
168
  const outputTransformsInitialized = useObservable(
@@ -263,11 +265,12 @@ export const StateInspector = memoGeneric(function StateInspector<
263
265
  }
264
266
  >
265
267
  <StateInspectorDisclosureRowInner style={{ flex: "0 0 auto" }}>
266
- {connectedUsers?.map((user) => (
268
+ {connectedUsers?.map((user, index, array) => (
267
269
  <StateInspectorRow
268
270
  key={user.id}
269
271
  colorScheme={colorScheme}
270
272
  variant={user.id === userId ? "up" : undefined}
273
+ bordered={index !== array.length - 1}
271
274
  >
272
275
  {user.image && (
273
276
  <img
@@ -447,231 +450,241 @@ export const StateInspector = memoGeneric(function StateInspector<
447
450
  ))}
448
451
  </StateInspectorDisclosureRowInner>
449
452
  </StateInspectorDisclosureSection>
450
- <StateInspectorDisclosureSection
451
- title={
452
- <StateInspectorTitleLabel>
453
- Resources ({resources.length})
454
- <ColoredDot type={resourcesInitialized ? "success" : "error"} />
455
- </StateInspectorTitleLabel>
456
- }
457
- colorScheme={colorScheme}
458
- open={showResources}
459
- setOpen={setShowResources}
460
- right={
461
- <span style={{ display: "flex", gap: "10px" }}>
462
- <StateInspectorButton
463
- theme={theme}
464
- onClick={async () => {
465
- const ok = confirm(
466
- "Are you sure you want to delete all resources?"
467
- );
468
- if (!ok) return;
469
- await resourceManager.deleteResource(
470
- resources.map((resource) => ({ id: resource.id }))
471
- );
472
- }}
473
- >
474
- Delete all
475
- </StateInspectorButton>
476
- <StateInspectorButton
477
- theme={theme}
478
- onClick={async () => {
479
- const path = prompt("Enter directory path") || uuid();
480
- await resourceManager.createResource({
481
- type: "directory",
482
- path,
483
- });
484
- }}
485
- >
486
- Create Directory
487
- </StateInspectorButton>
453
+ {advanced && (
454
+ <StateInspectorDisclosureSection
455
+ title={
456
+ <StateInspectorTitleLabel>
457
+ Resources ({resources.length})
458
+ <ColoredDot type={resourcesInitialized ? "success" : "error"} />
459
+ </StateInspectorTitleLabel>
460
+ }
461
+ colorScheme={colorScheme}
462
+ open={showResources}
463
+ setOpen={setShowResources}
464
+ right={
465
+ <span style={{ display: "flex", gap: "10px" }}>
466
+ <StateInspectorButton
467
+ theme={theme}
468
+ onClick={async () => {
469
+ const ok = confirm(
470
+ "Are you sure you want to delete all resources?"
471
+ );
472
+ if (!ok) return;
473
+ await resourceManager.deleteResource(
474
+ resources.map((resource) => ({ id: resource.id }))
475
+ );
476
+ }}
477
+ >
478
+ Delete all
479
+ </StateInspectorButton>
480
+ <StateInspectorButton
481
+ theme={theme}
482
+ onClick={async () => {
483
+ const path = prompt("Enter directory path") || uuid();
484
+ await resourceManager.createResource({
485
+ type: "directory",
486
+ path,
487
+ });
488
+ }}
489
+ >
490
+ Create Directory
491
+ </StateInspectorButton>
492
+ <StateInspectorButton
493
+ theme={theme}
494
+ onClick={async () => {
495
+ const file = await uploadFile();
496
+
497
+ resourceManager.createResource({
498
+ type: "asset",
499
+ path: file.name || uuid(),
500
+ asset: {
501
+ content: Base64.encode(await file.arrayBuffer()),
502
+ contentType: file.type,
503
+ encoding: "base64",
504
+ },
505
+ });
506
+ }}
507
+ >
508
+ Create Asset
509
+ </StateInspectorButton>
510
+ </span>
511
+ }
512
+ >
513
+ <StateInspectorDisclosureRowInner>
514
+ {resources?.map((resource) => (
515
+ <StateInspectorRow key={resource.id} colorScheme={colorScheme}>
516
+ <ObjectInspector
517
+ name={resource.path}
518
+ data={resource}
519
+ theme={theme}
520
+ />
521
+ <div style={{ display: "flex", gap: "4px" }}>
522
+ <StateInspectorButton
523
+ theme={theme}
524
+ onClick={() =>
525
+ resourceManager.deleteResource({ id: resource.id })
526
+ }
527
+ >
528
+ Delete
529
+ </StateInspectorButton>
530
+ <StateInspectorButton
531
+ theme={theme}
532
+ onClick={() => {
533
+ const path = prompt("Enter new path", resource.path);
534
+ if (!path) return;
535
+ resourceManager.updateResource({ id: resource.id, path });
536
+ }}
537
+ >
538
+ Rename
539
+ </StateInspectorButton>
540
+ </div>
541
+ </StateInspectorRow>
542
+ ))}
543
+ </StateInspectorDisclosureRowInner>
544
+ </StateInspectorDisclosureSection>
545
+ )}
546
+ {advanced && (
547
+ <StateInspectorDisclosureSection
548
+ title={
549
+ <StateInspectorTitleLabel>
550
+ Secrets
551
+ <ColoredDot type={secretsInitialized ? "success" : "error"} />
552
+ </StateInspectorTitleLabel>
553
+ }
554
+ colorScheme={colorScheme}
555
+ open={showSecrets}
556
+ setOpen={setShowSecrets}
557
+ right={
488
558
  <StateInspectorButton
489
559
  theme={theme}
490
- onClick={async () => {
491
- const file = await uploadFile();
492
-
493
- resourceManager.createResource({
494
- type: "asset",
495
- path: file.name || uuid(),
496
- asset: {
497
- content: Base64.encode(await file.arrayBuffer()),
498
- contentType: file.type,
499
- encoding: "base64",
500
- },
501
- });
560
+ onClick={() => {
561
+ const name = prompt("Enter secret name");
562
+ if (!name) return;
563
+ const value = prompt("Enter secret value");
564
+ if (!value) return;
565
+ secretManager.createSecret(null, name, value);
502
566
  }}
503
567
  >
504
- Create Asset
568
+ Create
505
569
  </StateInspectorButton>
506
- </span>
507
- }
508
- >
509
- <StateInspectorDisclosureRowInner>
510
- {resources?.map((resource) => (
511
- <StateInspectorRow key={resource.id} colorScheme={colorScheme}>
512
- <ObjectInspector
513
- name={resource.path}
514
- data={resource}
515
- theme={theme}
516
- />
517
- <div style={{ display: "flex", gap: "4px" }}>
570
+ }
571
+ >
572
+ <StateInspectorDisclosureRowInner>
573
+ {secrets.map((secret) => (
574
+ <StateInspectorRow key={secret.id} colorScheme={colorScheme}>
575
+ <ObjectInspector data={secret} theme={theme} />
518
576
  <StateInspectorButton
519
577
  theme={theme}
520
- onClick={() =>
521
- resourceManager.deleteResource({ id: resource.id })
522
- }
578
+ onClick={() => secretManager.deleteSecret(secret.id)}
523
579
  >
524
580
  Delete
525
581
  </StateInspectorButton>
526
- <StateInspectorButton
527
- theme={theme}
528
- onClick={() => {
529
- const path = prompt("Enter new path", resource.path);
530
- if (!path) return;
531
- resourceManager.updateResource({ id: resource.id, path });
532
- }}
533
- >
534
- Rename
535
- </StateInspectorButton>
536
- </div>
537
- </StateInspectorRow>
538
- ))}
539
- </StateInspectorDisclosureRowInner>
540
- </StateInspectorDisclosureSection>
541
- <StateInspectorDisclosureSection
542
- title={
543
- <StateInspectorTitleLabel>
544
- Secrets
545
- <ColoredDot type={secretsInitialized ? "success" : "error"} />
546
- </StateInspectorTitleLabel>
547
- }
548
- colorScheme={colorScheme}
549
- open={showSecrets}
550
- setOpen={setShowSecrets}
551
- right={
552
- <StateInspectorButton
553
- theme={theme}
554
- onClick={() => {
555
- const name = prompt("Enter secret name");
556
- if (!name) return;
557
- const value = prompt("Enter secret value");
558
- if (!value) return;
559
- secretManager.createSecret(null, name, value);
560
- }}
561
- >
562
- Create
563
- </StateInspectorButton>
564
- }
565
- >
566
- <StateInspectorDisclosureRowInner>
567
- {secrets.map((secret) => (
568
- <StateInspectorRow key={secret.id} colorScheme={colorScheme}>
569
- <ObjectInspector data={secret} theme={theme} />
570
- <StateInspectorButton
571
- theme={theme}
572
- onClick={() => secretManager.deleteSecret(secret.id)}
582
+ </StateInspectorRow>
583
+ ))}
584
+ </StateInspectorDisclosureRowInner>
585
+ </StateInspectorDisclosureSection>
586
+ )}
587
+ {advanced && (
588
+ <StateInspectorDisclosureSection
589
+ open={showInputs}
590
+ setOpen={setShowInputs}
591
+ title={
592
+ <StateInspectorTitleLabel>
593
+ Inputs
594
+ <ColoredDot type={inputsInitialized ? "success" : "error"} />
595
+ </StateInspectorTitleLabel>
596
+ }
597
+ colorScheme={colorScheme}
598
+ >
599
+ <StateInspectorDisclosureRowInner>
600
+ {inputs?.map((input: Input) => (
601
+ <StateInspectorRow key={input.id} colorScheme={colorScheme}>
602
+ <ObjectInspector data={input} theme={theme} />
603
+ </StateInspectorRow>
604
+ ))}
605
+ {!inputs?.length && (
606
+ <div
607
+ style={{
608
+ padding: "12px",
609
+ fontSize: "12px",
610
+ display: "flex",
611
+ flexDirection: "column",
612
+ gap: "4px",
613
+ }}
573
614
  >
574
- Delete
575
- </StateInspectorButton>
576
- </StateInspectorRow>
577
- ))}
578
- </StateInspectorDisclosureRowInner>
579
- </StateInspectorDisclosureSection>{" "}
580
- <StateInspectorDisclosureSection
581
- open={showInputs}
582
- setOpen={setShowInputs}
583
- title={
584
- <StateInspectorTitleLabel>
585
- Inputs
586
- <ColoredDot type={inputsInitialized ? "success" : "error"} />
587
- </StateInspectorTitleLabel>
588
- }
589
- colorScheme={colorScheme}
590
- >
591
- <StateInspectorDisclosureRowInner>
592
- {inputs?.map((input: Input) => (
593
- <StateInspectorRow key={input.id} colorScheme={colorScheme}>
594
- <ObjectInspector data={input} theme={theme} />
595
- </StateInspectorRow>
596
- ))}
597
- {!inputs?.length && (
598
- <div
599
- style={{
600
- padding: "12px",
601
- fontSize: "12px",
602
- display: "flex",
603
- flexDirection: "column",
604
- gap: "4px",
605
- }}
606
- >
607
- <span>No inputs</span>
608
- </div>
609
- )}
610
- </StateInspectorDisclosureRowInner>
611
- </StateInspectorDisclosureSection>
612
- <StateInspectorDisclosureSection
613
- open={showOutputTransforms}
614
- setOpen={setShowOutputTransforms}
615
- title={
616
- <StateInspectorTitleLabel>
617
- Output Transforms
618
- <ColoredDot
619
- type={outputTransformsInitialized ? "success" : "error"}
620
- />
621
- </StateInspectorTitleLabel>
622
- }
623
- colorScheme={colorScheme}
624
- >
625
- <StateInspectorDisclosureRowInner>
626
- {outputTransforms?.map((transform: OutputTransform) => (
627
- <StateInspectorRow key={transform.id} colorScheme={colorScheme}>
628
- <ObjectInspector data={transform} theme={theme} />
629
- </StateInspectorRow>
630
- ))}
631
- {!outputTransforms?.length && (
632
- <div
633
- style={{
634
- padding: "12px",
635
- fontSize: "12px",
636
- display: "flex",
637
- flexDirection: "column",
638
- gap: "4px",
639
- }}
640
- >
641
- <span>No output transforms</span>
642
- </div>
643
- )}
644
- </StateInspectorDisclosureRowInner>
645
- </StateInspectorDisclosureSection>
646
- <StateInspectorDisclosureSection
647
- title="Tasks"
648
- colorScheme={colorScheme}
649
- open={showTasks}
650
- setOpen={setShowTasks}
651
- >
652
- <StateInspectorDisclosureRowInner>
653
- {tasks?.map((task) => (
654
- <StateInspectorRow
655
- key={task.id}
656
- colorScheme={colorScheme}
657
- style={{
658
- backgroundColor:
659
- task.status === "done"
660
- ? "rgba(0,255,0,0.2)"
661
- : task.status === "error"
662
- ? "rgba(255,0,0,0.2)"
663
- : undefined,
664
- }}
665
- >
666
- <ObjectInspector
667
- name={task.name}
668
- data={task.payload}
669
- theme={theme}
615
+ <span>No inputs</span>
616
+ </div>
617
+ )}
618
+ </StateInspectorDisclosureRowInner>
619
+ </StateInspectorDisclosureSection>
620
+ )}
621
+ {advanced && (
622
+ <StateInspectorDisclosureSection
623
+ open={showOutputTransforms}
624
+ setOpen={setShowOutputTransforms}
625
+ title={
626
+ <StateInspectorTitleLabel>
627
+ Output Transforms
628
+ <ColoredDot
629
+ type={outputTransformsInitialized ? "success" : "error"}
670
630
  />
671
- </StateInspectorRow>
672
- ))}
673
- </StateInspectorDisclosureRowInner>
674
- </StateInspectorDisclosureSection>
631
+ </StateInspectorTitleLabel>
632
+ }
633
+ colorScheme={colorScheme}
634
+ >
635
+ <StateInspectorDisclosureRowInner>
636
+ {outputTransforms?.map((transform: OutputTransform) => (
637
+ <StateInspectorRow key={transform.id} colorScheme={colorScheme}>
638
+ <ObjectInspector data={transform} theme={theme} />
639
+ </StateInspectorRow>
640
+ ))}
641
+ {!outputTransforms?.length && (
642
+ <div
643
+ style={{
644
+ padding: "12px",
645
+ fontSize: "12px",
646
+ display: "flex",
647
+ flexDirection: "column",
648
+ gap: "4px",
649
+ }}
650
+ >
651
+ <span>No output transforms</span>
652
+ </div>
653
+ )}
654
+ </StateInspectorDisclosureRowInner>
655
+ </StateInspectorDisclosureSection>
656
+ )}
657
+ {advanced && (
658
+ <StateInspectorDisclosureSection
659
+ title="Tasks"
660
+ colorScheme={colorScheme}
661
+ open={showTasks}
662
+ setOpen={setShowTasks}
663
+ >
664
+ <StateInspectorDisclosureRowInner>
665
+ {tasks?.map((task) => (
666
+ <StateInspectorRow
667
+ key={task.id}
668
+ colorScheme={colorScheme}
669
+ style={{
670
+ backgroundColor:
671
+ task.status === "done"
672
+ ? "rgba(0,255,0,0.2)"
673
+ : task.status === "error"
674
+ ? "rgba(255,0,0,0.2)"
675
+ : undefined,
676
+ }}
677
+ >
678
+ <ObjectInspector
679
+ name={task.name}
680
+ data={task.payload}
681
+ theme={theme}
682
+ />
683
+ </StateInspectorRow>
684
+ ))}
685
+ </StateInspectorDisclosureRowInner>
686
+ </StateInspectorDisclosureSection>
687
+ )}
675
688
  <StateInspectorDisclosureSection
676
689
  open={showEphemeral}
677
690
  setOpen={setShowEphemeral}
@@ -691,13 +704,15 @@ export const StateInspector = memoGeneric(function StateInspector<
691
704
  ))}
692
705
  </StateInspectorDisclosureRowInner>
693
706
  </StateInspectorDisclosureSection>
694
- <ActivityEventsSection
695
- colorScheme={colorScheme}
696
- activityEventsContainerRef={activityEventsContainerRef}
697
- showEvents={showActivityEvents}
698
- setShowEvents={setShowActivityEvents}
699
- activityEventsManager={activityEventsManager}
700
- />
707
+ {advanced && (
708
+ <ActivityEventsSection
709
+ colorScheme={colorScheme}
710
+ activityEventsContainerRef={activityEventsContainerRef}
711
+ showEvents={showActivityEvents}
712
+ setShowEvents={setShowActivityEvents}
713
+ activityEventsManager={activityEventsManager}
714
+ />
715
+ )}
701
716
  <EventsSection<S>
702
717
  connectionEvents={connectionEvents}
703
718
  colorScheme={colorScheme}
@@ -8,12 +8,14 @@ export function StateInspectorRow({
8
8
  selected,
9
9
  style,
10
10
  variant,
11
+ bordered = true,
11
12
  }: {
12
13
  children: React.ReactNode;
13
14
  colorScheme: "light" | "dark";
14
15
  selected?: boolean;
15
16
  style?: CSSProperties;
16
17
  variant?: "up" | "down";
18
+ bordered?: boolean;
17
19
  }) {
18
20
  const solidBorderColor =
19
21
  colorScheme === "light" ? "rgb(223 223 223)" : "rgb(29 29 29)";
@@ -21,7 +23,7 @@ export function StateInspectorRow({
21
23
  return (
22
24
  <div
23
25
  style={{
24
- borderBottom: `1px solid ${solidBorderColor}`,
26
+ borderBottom: bordered ? `1px solid ${solidBorderColor}` : undefined,
25
27
  fontSize: "12px",
26
28
  fontFamily: "Menlo, monospace",
27
29
  padding: "2px 12px 1px",
@@ -38,12 +38,16 @@ export function useStateInspector<
38
38
  >({
39
39
  noyaManager,
40
40
  disabled = false,
41
+ advanced = false,
41
42
  colorScheme,
42
43
  anchor,
43
44
  }: {
44
45
  noyaManager: NoyaManager<S, M, E, MenuT>;
45
- disabled: boolean;
46
- } & Pick<ComponentProps<typeof StateInspector>, "colorScheme" | "anchor">) {
46
+ disabled?: boolean;
47
+ advanced?: boolean;
48
+ colorScheme?: ComponentProps<typeof StateInspector>["colorScheme"];
49
+ anchor?: ComponentProps<typeof StateInspector>["anchor"];
50
+ }) {
47
51
  const [root, setRoot] = React.useState<Root | null>(null);
48
52
 
49
53
  useIsomorphicLayoutEffect(() => {
@@ -64,7 +68,8 @@ export function useStateInspector<
64
68
  colorScheme={colorScheme}
65
69
  anchor={anchor}
66
70
  noyaManager={noyaManager}
71
+ advanced={advanced}
67
72
  />
68
73
  );
69
- }, [noyaManager, anchor, colorScheme, root]);
74
+ }, [noyaManager, anchor, colorScheme, root, advanced]);
70
75
  }
package/src/noyaApp.ts CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  TypeGuard,
15
15
  webSocketSync,
16
16
  } from "@noya-app/state-manager";
17
- import { useMemo, useState } from "react";
17
+ import { useEffect, useMemo, useState } from "react";
18
18
  import { useBindNoyaManager, UseMultiplayerStateOptions } from "./hooks";
19
19
  import { StateInspectorOptions } from "./inspector/StateInspector";
20
20
 
@@ -28,6 +28,8 @@ export type AppData<State> = {
28
28
  viewType: AppViewType;
29
29
  initialState: State;
30
30
  inspector: boolean | StateInspectorOptions;
31
+ userId?: string;
32
+ isDemo: boolean;
31
33
  };
32
34
 
33
35
  export function createDefaultAppData<State>(
@@ -38,6 +40,7 @@ export function createDefaultAppData<State>(
38
40
  viewType: "editable",
39
41
  initialState,
40
42
  inspector: false,
43
+ isDemo: false,
41
44
  };
42
45
  }
43
46
 
@@ -93,6 +96,7 @@ export function getAppData<State>(
93
96
  }
94
97
 
95
98
  appData.initialState = enforceSchema(appData.initialState, schema) as State;
99
+ appData.isDemo = appData.isDemo ?? false;
96
100
 
97
101
  return appData as AppData<State>;
98
102
  }
@@ -138,6 +142,7 @@ export type UseNoyaStateResult<
138
142
  ReturnType<typeof useBindNoyaManager<S, M, E, MenuT>>[2] & {
139
143
  theme: AppTheme;
140
144
  viewType: AppViewType;
145
+ isDemo: boolean;
141
146
  },
142
147
  ];
143
148
 
@@ -192,6 +197,8 @@ export function useNoyaStateInternal<
192
197
  initialState: noyaAppInitialState,
193
198
  theme,
194
199
  viewType,
200
+ userId,
201
+ isDemo,
195
202
  },
196
203
  ] = useState(() => {
197
204
  return getAppData(initialState, options?.schema, {
@@ -211,6 +218,13 @@ export function useNoyaStateInternal<
211
218
  () => new NoyaManager<S, M, E, MenuT>(noyaAppInitialState as S, options)
212
219
  );
213
220
 
221
+ useEffect(() => {
222
+ if (!userId) return;
223
+
224
+ noyaManager.ephemeralUserDataManager.setCurrentUserId(userId);
225
+ noyaManager.userManager.setCurrentUserId(userId);
226
+ }, [noyaManager, userId]);
227
+
214
228
  const result = useBindNoyaManager<S, M, E, MenuT>(noyaManager, {
215
229
  inspector: options?.inspector ?? inspector,
216
230
  sync: options?.sync ?? sync,
@@ -219,8 +233,8 @@ export function useNoyaStateInternal<
219
233
  const [, , extras] = result;
220
234
 
221
235
  const mergedExtras = useMemo(() => {
222
- return { ...extras, theme, viewType } as const;
223
- }, [extras, theme, viewType]);
236
+ return { ...extras, theme, viewType, isDemo } as const;
237
+ }, [extras, theme, viewType, isDemo]);
224
238
 
225
239
  return [result[0], result[1], mergedExtras] as UseNoyaStateResult<
226
240
  O["schema"] extends TSchema ? Static<O["schema"]> : S,