@noya-app/noya-multiplayer-react 0.1.10 → 0.1.12

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.
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  ConnectionEvent,
3
3
  ExtendedPathKey,
4
+ MultiplayerUser,
4
5
  StateManager,
5
6
  } from "@noya-app/state-manager";
6
7
  import React, {
@@ -35,6 +36,16 @@ const darkTheme = {
35
36
  OBJECT_NAME_COLOR: "rgba(255,255,255,0.7)",
36
37
  } as any;
37
38
 
39
+ const styles = {
40
+ sectionInner: {
41
+ flex: "1 1 0",
42
+ overflowY: "auto",
43
+ overflowX: "hidden",
44
+ display: "flex",
45
+ flexDirection: "column",
46
+ },
47
+ } as const;
48
+
38
49
  function ToggleButton({
39
50
  showInspector,
40
51
  setShowInspector,
@@ -61,7 +72,11 @@ function ToggleButton({
61
72
  justifyContent: "center",
62
73
  padding: "2px 0",
63
74
  }}
64
- onClick={() => setShowInspector(!showInspector)}
75
+ onClick={(event) => {
76
+ event.stopPropagation();
77
+
78
+ setShowInspector(!showInspector);
79
+ }}
65
80
  >
66
81
  <span style={{ width: "12px", height: "12px" }}>
67
82
  {showInspector === (anchor === "right") ? (
@@ -108,6 +123,7 @@ function DisclosureSection({
108
123
  children,
109
124
  colorScheme,
110
125
  isFirst,
126
+ style,
111
127
  }: {
112
128
  open: boolean;
113
129
  setOpen?: (value: boolean) => void;
@@ -116,6 +132,7 @@ function DisclosureSection({
116
132
  children: React.ReactNode;
117
133
  colorScheme: "light" | "dark";
118
134
  isFirst?: boolean;
135
+ style?: CSSProperties;
119
136
  }) {
120
137
  const theme = colorScheme === "light" ? lightTheme : darkTheme;
121
138
 
@@ -128,6 +145,7 @@ function DisclosureSection({
128
145
  flex: open ? "1 1 0" : "0",
129
146
  display: "flex",
130
147
  flexDirection: "column",
148
+ ...style,
131
149
  }}
132
150
  >
133
151
  <div
@@ -160,6 +178,60 @@ function DisclosureSection({
160
178
  );
161
179
  }
162
180
 
181
+ function InspectorRow({
182
+ children,
183
+ colorScheme,
184
+ selected,
185
+ style,
186
+ variant,
187
+ }: {
188
+ children: React.ReactNode;
189
+ colorScheme: "light" | "dark";
190
+ selected?: boolean;
191
+ style?: CSSProperties;
192
+ variant?: "up" | "down";
193
+ }) {
194
+ const solidBorderColor =
195
+ colorScheme === "light" ? "rgb(223 223 223)" : "rgb(29 29 29)";
196
+
197
+ return (
198
+ <div
199
+ style={{
200
+ borderBottom: `1px solid ${solidBorderColor}`,
201
+ fontSize: "12px",
202
+ fontFamily: "Menlo, monospace",
203
+ padding: "2px 12px 1px",
204
+ display: "flex",
205
+ alignItems: "center",
206
+ background:
207
+ variant === "up"
208
+ ? "rgba(0,255,0,0.1)"
209
+ : variant === "down"
210
+ ? "rgba(255,0,0,0.1)"
211
+ : selected
212
+ ? "rgb(59 130 246 / 15%)"
213
+ : undefined,
214
+ // background:
215
+ // colorScheme === "light"
216
+ // ? "rgba(0,0,0,0.05)"
217
+ // : "rgba(255,255,255,0.05)",
218
+ ...style,
219
+ }}
220
+ >
221
+ <span
222
+ style={{
223
+ fontFamily: "Menlo, monospace",
224
+ fontSize: "11px",
225
+ borderRadius: 4,
226
+ display: "inline-block",
227
+ }}
228
+ >
229
+ {children}
230
+ </span>
231
+ </div>
232
+ );
233
+ }
234
+
163
235
  const HISTORY_ELEMENT_PREFIX = "noya-multiplayer-history-";
164
236
 
165
237
  type Anchor = "left" | "right";
@@ -170,6 +242,8 @@ export const StateInspector = memo(function StateInspector<
170
242
  >({
171
243
  state,
172
244
  connectionEvents,
245
+ connectedUsers,
246
+ userId,
173
247
  unstyled,
174
248
  colorScheme = "light",
175
249
  stateManager,
@@ -178,6 +252,8 @@ export const StateInspector = memo(function StateInspector<
178
252
  }: {
179
253
  state: S;
180
254
  connectionEvents?: ConnectionEvent<S>[];
255
+ connectedUsers?: MultiplayerUser[];
256
+ userId?: string;
181
257
  unstyled?: boolean;
182
258
  colorScheme?: "light" | "dark";
183
259
  stateManager: StateManager<S, M>;
@@ -204,6 +280,14 @@ export const StateInspector = memo(function StateInspector<
204
280
  "noya-multiplayer-react-show-history",
205
281
  false
206
282
  );
283
+ const [showUsers, setShowUsers] = useLocalStorageState(
284
+ "noya-multiplayer-react-show-users",
285
+ true
286
+ );
287
+ const [showData, setShowData] = useLocalStorageState(
288
+ "noya-multiplayer-react-show-data",
289
+ true
290
+ );
207
291
 
208
292
  useEffect(() => {
209
293
  trackEvents.set(showEvents);
@@ -230,7 +314,7 @@ export const StateInspector = memo(function StateInspector<
230
314
  if (!historyContainerRef.current) return;
231
315
 
232
316
  const historyEntry = historyContainerRef.current.querySelector(
233
- `#${HISTORY_ELEMENT_PREFIX}${historySnapshot.historyIndex - 1}`
317
+ `#${HISTORY_ELEMENT_PREFIX}${historySnapshot.historyIndex}`
234
318
  );
235
319
 
236
320
  if (historyEntry) {
@@ -242,8 +326,8 @@ export const StateInspector = memo(function StateInspector<
242
326
  }, [historySnapshot.historyIndex]);
243
327
 
244
328
  const theme = colorScheme === "light" ? lightTheme : darkTheme;
245
- const borderColor =
246
- colorScheme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)";
329
+ // const borderColor =
330
+ // colorScheme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)";
247
331
  const solidBorderColor =
248
332
  colorScheme === "light" ? "rgb(223 223 223)" : "rgb(29 29 29)";
249
333
 
@@ -300,12 +384,16 @@ export const StateInspector = memo(function StateInspector<
300
384
  ...props.style,
301
385
  }}
302
386
  >
303
- {/* <div style={{ flex: "0", fontSize: "12px" }}>User ID: {userId}</div> */}
304
387
  <DisclosureSection
305
- open
306
- title="Data"
307
- colorScheme={colorScheme}
308
388
  isFirst
389
+ open={showUsers}
390
+ setOpen={setShowUsers}
391
+ title="Users"
392
+ colorScheme={colorScheme}
393
+ style={{
394
+ flex: "0 0 auto",
395
+ maxHeight: "200px",
396
+ }}
309
397
  right={
310
398
  <ToggleButton
311
399
  showInspector={showInspector}
@@ -314,13 +402,41 @@ export const StateInspector = memo(function StateInspector<
314
402
  anchor={anchor}
315
403
  />
316
404
  }
405
+ >
406
+ <div style={{ ...styles.sectionInner, flex: "0 0 auto" }}>
407
+ {connectedUsers?.map((user) => (
408
+ <InspectorRow
409
+ key={user.id}
410
+ colorScheme={colorScheme}
411
+ variant={user.id === userId ? "up" : undefined}
412
+ >
413
+ {user.name} ({ellipsis(user.id.toString(), 8, "middle")})
414
+ </InspectorRow>
415
+ ))}
416
+ {!connectedUsers && (
417
+ <div
418
+ style={{
419
+ padding: "12px",
420
+ fontSize: "12px",
421
+ display: "flex",
422
+ flexDirection: "column",
423
+ gap: "4px",
424
+ }}
425
+ >
426
+ <span>No connected users</span>
427
+ </div>
428
+ )}
429
+ </div>
430
+ </DisclosureSection>
431
+ <DisclosureSection
432
+ title="Data"
433
+ colorScheme={colorScheme}
434
+ setOpen={setShowData}
435
+ open={showData}
317
436
  >
318
437
  <div
319
438
  style={{
320
- flex: "1 1 0",
321
- overflowY: "auto",
322
- display: "flex",
323
- flexDirection: "column",
439
+ ...styles.sectionInner,
324
440
  gap: "1px",
325
441
  padding: "1px 12px",
326
442
  }}
@@ -334,53 +450,42 @@ export const StateInspector = memo(function StateInspector<
334
450
  title="History"
335
451
  colorScheme={colorScheme}
336
452
  >
337
- <div
338
- ref={historyContainerRef}
339
- style={{
340
- flex: "1 1 0",
341
- overflowY: "auto",
342
- overflowX: "hidden",
343
- display: "flex",
344
- flexDirection: "column",
345
- }}
346
- >
347
- {historySnapshot.history.length < 50 && (
348
- <div
349
- id={`${HISTORY_ELEMENT_PREFIX}0`}
453
+ <div ref={historyContainerRef} style={styles.sectionInner}>
454
+ <div
455
+ id={`${HISTORY_ELEMENT_PREFIX}0`}
456
+ style={{
457
+ borderBottom: `1px solid ${solidBorderColor}`,
458
+ fontSize: "12px",
459
+ fontFamily: "Menlo, monospace",
460
+ padding: "2px 12px 1px",
461
+ display: "flex",
462
+ alignItems: "center",
463
+ background:
464
+ historySnapshot.historyIndex === 0
465
+ ? "rgb(59 130 246 / 15%)"
466
+ : undefined,
467
+ }}
468
+ >
469
+ <span
350
470
  style={{
351
- borderBottom: `1px solid ${solidBorderColor}`,
352
- fontSize: "12px",
353
471
  fontFamily: "Menlo, monospace",
354
- padding: "2px 12px 1px",
355
- display: "flex",
356
- alignItems: "center",
357
- background:
358
- historySnapshot.historyIndex === 0
359
- ? "rgb(59 130 246 / 15%)"
360
- : undefined,
472
+ fontSize: "11px",
473
+ borderRadius: 4,
474
+ display: "inline-block",
361
475
  }}
362
476
  >
363
- <span
364
- style={{
365
- fontFamily: "Menlo, monospace",
366
- fontSize: "11px",
367
- borderRadius: 4,
368
- display: "inline-block",
369
- }}
370
- >
371
- Initial state
372
- </span>
373
- </div>
374
- )}
375
- {historySnapshot.history.slice(-50).map((entry, index) => (
477
+ Initial state
478
+ </span>
479
+ </div>
480
+ {historySnapshot.history.map((entry, index) => (
376
481
  <div
377
- id={`noya-multiplayer-history-${index}`}
482
+ id={`${HISTORY_ELEMENT_PREFIX}${index + 1}`}
378
483
  key={index}
379
484
  style={{
380
485
  padding: "0px 12px 1px",
381
486
  borderBottom: `1px solid ${solidBorderColor}`,
382
487
  background:
383
- index === historySnapshot.historyIndex - 1
488
+ index + 1 === historySnapshot.historyIndex
384
489
  ? "rgb(59 130 246 / 15%)"
385
490
  : undefined,
386
491
  }}
@@ -434,86 +539,51 @@ export const StateInspector = memo(function StateInspector<
434
539
  title="Events"
435
540
  colorScheme={colorScheme}
436
541
  >
437
- <div
438
- ref={eventsContainerRef}
439
- style={{
440
- flex: "1 1 0",
441
- overflowY: "auto",
442
- display: "flex",
443
- flexDirection: "column",
444
- }}
445
- >
446
- {connectionEvents?.map((event, index) => (
447
- <div
448
- key={index}
449
- style={{
450
- borderBottom: `1px solid ${borderColor}`,
451
- }}
452
- >
453
- {event.type === "stateChange" ? (
454
- <div
455
- style={{
456
- padding: "2px 12px",
457
- background:
458
- colorScheme === "light"
459
- ? "rgba(0,0,0,0.05)"
460
- : "rgba(255,255,255,0.05)",
461
- display: "flex",
462
- alignItems: "center",
463
- }}
464
- >
465
- <span
466
- style={{
467
- fontFamily: "Menlo, monospace",
468
- fontSize: "11px",
469
- borderRadius: 4,
470
- display: "inline-block",
471
- }}
472
- >
473
- connection:{" "}
474
- <span style={{ fontStyle: "italic" }}>
475
- {event.state.toLowerCase()}
476
- </span>
477
- </span>
478
- </div>
479
- ) : (
480
- <div
481
- style={{
482
- padding: "0px 12px 1px",
483
- background:
484
- event.type === "send"
485
- ? "rgba(0,255,0,0.1)"
486
- : "rgba(255,0,0,0.1)",
542
+ <div ref={eventsContainerRef} style={styles.sectionInner}>
543
+ {connectionEvents?.map((event, index) =>
544
+ event.type === "stateChange" ? (
545
+ <InspectorRow key={index} colorScheme={colorScheme}>
546
+ connection:{" "}
547
+ <span style={{ fontStyle: "italic" }}>
548
+ {event.state.toLowerCase()}
549
+ </span>
550
+ </InspectorRow>
551
+ ) : (
552
+ <InspectorRow
553
+ key={index}
554
+ colorScheme={colorScheme}
555
+ variant={event.type === "send" ? "up" : "down"}
556
+ style={{
557
+ padding: "0px 12px 1px",
558
+ }}
559
+ >
560
+ <ObjectInspector
561
+ data={event.message}
562
+ theme={theme}
563
+ nodeRenderer={({
564
+ depth,
565
+ name,
566
+ data,
567
+ isNonenumerable,
568
+ expanded,
569
+ }: any) => {
570
+ const direction = event.type === "send" ? "up" : "down";
571
+
572
+ return depth === 0 ? (
573
+ <ObjectRootLabel direction={direction} data={data} />
574
+ ) : (
575
+ <ObjectLabel
576
+ direction={direction}
577
+ name={name}
578
+ data={data}
579
+ isNonenumerable={isNonenumerable}
580
+ />
581
+ );
487
582
  }}
488
- >
489
- <ObjectInspector
490
- data={event.message}
491
- theme={theme}
492
- nodeRenderer={({
493
- depth,
494
- name,
495
- data,
496
- isNonenumerable,
497
- expanded,
498
- }: any) => {
499
- const direction = event.type === "send" ? "up" : "down";
500
-
501
- return depth === 0 ? (
502
- <ObjectRootLabel direction={direction} data={data} />
503
- ) : (
504
- <ObjectLabel
505
- direction={direction}
506
- name={name}
507
- data={data}
508
- isNonenumerable={isNonenumerable}
509
- />
510
- );
511
- }}
512
- />
513
- </div>
514
- )}
515
- </div>
516
- ))}
583
+ />
584
+ </InspectorRow>
585
+ )
586
+ )}
517
587
  {!connectionEvents && (
518
588
  <div
519
589
  style={{
@@ -606,61 +676,6 @@ function Arrow({
606
676
  );
607
677
  }
608
678
 
609
- // <div className="flex py-px flex-col-reverse">
610
- // <button
611
- // type="button"
612
- // className={cx(
613
- // "px-4 py-2 cursor-pointer text-left",
614
- // 0 === historySnapshot.historyIndex && "bg-orange-500/25"
615
- // )}
616
- // onClick={() => {
617
- // // stateManager.goToHistoryIndex(0);
618
- // }}
619
- // >
620
- // <div className="flex justify-between gap-2">
621
- // <span>Initial State</span>
622
- // </div>
623
- // </button>
624
- // {historySnapshot.history.map((entry, index) => (
625
- // <button
626
- // key={index}
627
- // type="button"
628
- // className={cx(
629
- // "px-4 py-2 cursor-pointer text-left",
630
- // index === historySnapshot.historyIndex - 1 &&
631
- // "bg-orange-500/25"
632
- // )}
633
- // onClick={() => {
634
- // // stateManager.goToHistoryIndex(index + 1);
635
- // }}
636
- // >
637
- // <div className="flex justify-between gap-2">
638
- // <span>
639
- // {index + 1}. {entry.metadata.name}
640
- // </span>
641
- // <span>
642
- // {new Date(
643
- // entry.metadata.timestamp
644
- // ).toLocaleTimeString()}
645
- // </span>
646
- // </div>
647
- // <div className="font-mono text-xs">
648
- // {entry.redoPatches?.map((patch, index) => (
649
- // <pre key={index} className="max-w-full">
650
- // {patch.op === "remove"
651
- // ? `Delete ${pathToString(patch.path)}`
652
- // : `${pathToString(patch.path)} = ${JSON.stringify(
653
- // patch.value,
654
- // null,
655
- // 2
656
- // )}`}
657
- // </pre>
658
- // ))}
659
- // </div>
660
- // </button>
661
- // ))}
662
- // </div>
663
-
664
679
  function pathToString(extendedPath: ExtendedPathKey[]) {
665
680
  return extendedPath
666
681
  .map((key) =>
@@ -1,4 +1,8 @@
1
- import { ConnectionEvent, StateManager } from "@noya-app/state-manager";
1
+ import {
2
+ ConnectionEvent,
3
+ MultiplayerUser,
4
+ StateManager,
5
+ } from "@noya-app/state-manager";
2
6
  import React, { ComponentProps, useLayoutEffect } from "react";
3
7
  import { createRoot } from "react-dom/client";
4
8
 
@@ -14,6 +18,8 @@ function createPortalElement() {
14
18
  export function useStateInspector<S extends object, M>({
15
19
  state,
16
20
  connectionEvents,
21
+ connectedUsers,
22
+ userId,
17
23
  disabled = false,
18
24
  colorScheme,
19
25
  anchor,
@@ -21,6 +27,8 @@ export function useStateInspector<S extends object, M>({
21
27
  }: {
22
28
  state: S;
23
29
  connectionEvents?: ConnectionEvent<S>[];
30
+ connectedUsers?: MultiplayerUser[];
31
+ userId?: string;
24
32
  disabled: boolean;
25
33
  stateManager: StateManager<S, M>;
26
34
  } & Pick<ComponentProps<typeof StateInspector>, "colorScheme" | "anchor">) {
@@ -49,10 +57,21 @@ export function useStateInspector<S extends object, M>({
49
57
  <StateInspector
50
58
  state={state}
51
59
  connectionEvents={connectionEvents}
60
+ connectedUsers={connectedUsers}
61
+ userId={userId}
52
62
  colorScheme={colorScheme}
53
63
  anchor={anchor}
54
64
  stateManager={stateManager as StateManager<any, any>}
55
65
  />
56
66
  );
57
- }, [anchor, colorScheme, connectionEvents, root, state, stateManager]);
67
+ }, [
68
+ anchor,
69
+ colorScheme,
70
+ connectedUsers,
71
+ connectionEvents,
72
+ root,
73
+ state,
74
+ stateManager,
75
+ userId,
76
+ ]);
58
77
  }