@noya-app/noya-multiplayer-react 0.1.42 → 0.1.44

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,10 +1,20 @@
1
- import { GetAtPath, ObservableOptions, PathKey } from "@noya-app/observable";
2
1
  import {
2
+ GetAtPath,
3
+ Observable,
4
+ ObservableOptions,
5
+ PathKey,
6
+ } from "@noya-app/observable";
7
+ import {
8
+ AIManager,
3
9
  AssetManager,
10
+ ConnectedUsersManager,
11
+ EphemeralUserDataManager,
4
12
  MultiplayerStateManager,
13
+ SecretManager,
5
14
  StateManagerOptions,
6
15
  Static,
7
16
  TSchema,
17
+ WorkflowManager,
8
18
  } from "@noya-app/state-manager";
9
19
  import React, {
10
20
  createContext,
@@ -16,14 +26,21 @@ import React, {
16
26
  import { useNoyaState, UseNoyaStateOptions } from "./noyaApp";
17
27
  import { useObservable } from "./useObservable";
18
28
 
19
- interface NoyaStateProviderProps<S, M, E>
29
+ interface NoyaStateProviderProps<S, M, E extends object>
20
30
  extends Omit<UseNoyaStateOptions<S, M, E>, "schema" | "mergeHistoryEntries"> {
21
31
  children: React.ReactNode;
22
32
  initialState?: S;
33
+ fallback?: React.ReactNode;
23
34
  }
24
35
 
25
36
  const AnyNoyaStateContext = createContext<
26
- | { ms: MultiplayerStateManager<any, any>; assetManager: AssetManager }
37
+ | {
38
+ ms: MultiplayerStateManager<any, any>;
39
+ assetManager: AssetManager;
40
+ workflowManager: WorkflowManager;
41
+ secretManager: SecretManager;
42
+ aiManager: AIManager;
43
+ }
27
44
  | undefined
28
45
  >(undefined);
29
46
 
@@ -64,7 +81,91 @@ export function useAssetManager() {
64
81
  );
65
82
  }
66
83
 
67
- export function createNoyaContext<Schema extends TSchema, M = void, E = void>({
84
+ export function useAIManager() {
85
+ const { aiManager } = useAnyNoyaStateContext();
86
+ return aiManager;
87
+ }
88
+
89
+ export function useSecretManager() {
90
+ const { secretManager } = useAnyNoyaStateContext();
91
+ return secretManager;
92
+ }
93
+
94
+ export function useSecrets() {
95
+ const { secretManager } = useAnyNoyaStateContext();
96
+ return useObservable(secretManager.secrets$);
97
+ }
98
+
99
+ export function useSecret(name: string) {
100
+ const { secretManager } = useAnyNoyaStateContext();
101
+
102
+ return useObservable(
103
+ secretManager.secrets$,
104
+ useCallback((secrets) => secrets.find((s) => s.name === name), [name])
105
+ );
106
+ }
107
+
108
+ export function useIsInitialized() {
109
+ const { ms, secretManager, assetManager } = useAnyNoyaStateContext();
110
+ const isInitializedObservable = useMemo(
111
+ () =>
112
+ Observable.combine(
113
+ [
114
+ ms.isInitialized$,
115
+ secretManager.isInitialized$,
116
+ assetManager.isInitialized$,
117
+ ],
118
+ (values) => values.every((v) => v)
119
+ ),
120
+ [ms, secretManager, assetManager]
121
+ );
122
+
123
+ return useObservable(isInitializedObservable);
124
+ }
125
+
126
+ export function useWorkflowManager() {
127
+ const { workflowManager } = useAnyNoyaStateContext();
128
+ return workflowManager;
129
+ }
130
+
131
+ export const ConnectedUsersContext = createContext<
132
+ ConnectedUsersManager | undefined
133
+ >(undefined);
134
+
135
+ export function useConnectedUsersManager() {
136
+ return useContext(ConnectedUsersContext);
137
+ }
138
+
139
+ export const AnyEphemeralUserDataManagerContext = createContext<
140
+ EphemeralUserDataManager<object> | undefined
141
+ >(undefined);
142
+
143
+ export function useAnyEphemeralUserData() {
144
+ return useContext(AnyEphemeralUserDataManagerContext);
145
+ }
146
+
147
+ export function useCurrentUserId() {
148
+ const connectedUsersManager = useConnectedUsersManager();
149
+ return useObservable(connectedUsersManager?.currentUserId$);
150
+ }
151
+
152
+ export const FallbackUntilInitialized = ({
153
+ children,
154
+ fallback,
155
+ }: {
156
+ children: React.ReactNode;
157
+ fallback?: React.ReactNode;
158
+ }) => {
159
+ const isInitialized = useIsInitialized();
160
+
161
+ return isInitialized ? children : fallback ?? null;
162
+ };
163
+
164
+ export function createNoyaContext<
165
+ Schema extends TSchema,
166
+ M = void,
167
+ E extends object = object,
168
+ >({
68
169
  schema,
69
170
  mergeHistoryEntries,
70
171
  }: {
@@ -80,35 +181,71 @@ export function createNoyaContext<Schema extends TSchema, M = void, E = void>({
80
181
  | {
81
182
  ms: MultiplayerStateManager<S, M>;
82
183
  assetManager: AssetManager;
184
+ secretManager: SecretManager;
83
185
  }
84
186
  | undefined
85
187
  >;
86
188
 
189
+ const EphemeralUserDataManagerContext =
190
+ AnyEphemeralUserDataManagerContext as React.Context<
191
+ EphemeralUserDataManager<E> | undefined
192
+ >;
193
+
87
194
  function NoyaStateProvider({
88
195
  children,
89
196
  initialState,
90
197
  ...options
91
198
  }: NoyaStateProviderProps<S, M, E>) {
92
- const [, , { multiplayerStateManager, assetManager }] = useNoyaState(
93
- initialState,
199
+ const [
200
+ ,
201
+ ,
94
202
  {
95
- ...(options as any),
96
- schema,
97
- mergeHistoryEntries,
98
- }
99
- );
203
+ multiplayerStateManager,
204
+ assetManager,
205
+ workflowManager,
206
+ secretManager,
207
+ aiManager,
208
+ ephemeralUserDataManager,
209
+ connectedUsersManager,
210
+ },
211
+ ] = useNoyaState<S, M, E>(initialState, {
212
+ ...(options as any),
213
+ schema,
214
+ mergeHistoryEntries,
215
+ });
100
216
 
101
217
  const value = useMemo(
102
218
  () => ({
103
219
  ms: multiplayerStateManager as MultiplayerStateManager<any, any>,
104
220
  assetManager,
221
+ workflowManager,
222
+ secretManager,
223
+ aiManager,
105
224
  }),
106
- [multiplayerStateManager, assetManager]
225
+ [
226
+ multiplayerStateManager,
227
+ assetManager,
228
+ workflowManager,
229
+ secretManager,
230
+ aiManager,
231
+ ]
107
232
  );
108
233
 
109
234
  return (
110
235
  <AnyNoyaStateContext.Provider value={value}>
111
- {children}
236
+ <ConnectedUsersContext.Provider value={connectedUsersManager}>
237
+ <EphemeralUserDataManagerContext.Provider
238
+ value={ephemeralUserDataManager}
239
+ >
240
+ {options.fallback !== undefined ? (
241
+ <FallbackUntilInitialized fallback={options.fallback}>
242
+ {children}
243
+ </FallbackUntilInitialized>
244
+ ) : (
245
+ children
246
+ )}
247
+ </EphemeralUserDataManagerContext.Provider>
248
+ </ConnectedUsersContext.Provider>
112
249
  </AnyNoyaStateContext.Provider>
113
250
  );
114
251
  }
@@ -220,6 +357,18 @@ export function createNoyaContext<Schema extends TSchema, M = void, E = void>({
220
357
  return ms;
221
358
  }
222
359
 
360
+ function useEphemeralUserDataManager() {
361
+ const ephemeralUserData = useContext(EphemeralUserDataManagerContext);
362
+
363
+ if (!ephemeralUserData) {
364
+ throw new Error(
365
+ "useEphemeralUserData must be used within a NoyaStateProvider"
366
+ );
367
+ }
368
+
369
+ return ephemeralUserData;
370
+ }
371
+
223
372
  return {
224
373
  Context: NoyaStateContext,
225
374
  Provider: NoyaStateProvider,
@@ -227,6 +376,7 @@ export function createNoyaContext<Schema extends TSchema, M = void, E = void>({
227
376
  useSetValue,
228
377
  useValueState,
229
378
  useStateManager,
379
+ useEphemeralUserDataManager,
230
380
  // useAssets,
231
381
  // useAssetManager,
232
382
  // useAsset,
@@ -1,132 +1,92 @@
1
1
  "use client";
2
2
 
3
- import { EphemeralUserData, MultiplayerUser } from "@noya-app/state-manager";
4
- import React, { memo, useEffect, useMemo, useState } from "react";
5
- import { getAvatarStyle } from "../avatarStyle";
3
+ import { memoGeneric } from "@noya-app/react-utils";
4
+ import {
5
+ ConnectedUsersManager,
6
+ EphemeralUserDataManager,
7
+ MultiplayerUser,
8
+ } from "@noya-app/state-manager";
9
+ import React, { useEffect, useState } from "react";
6
10
  import { useObservable } from "../useObservable";
7
-
8
11
  type Point = { x: number; y: number };
9
12
 
10
13
  export type UserPointerData = { pointer?: Point };
11
14
 
12
- export type UserPointerProps<E extends UserPointerData> = {
15
+ export type RenderUserPointerProps = {
16
+ userId: string;
17
+ name?: string;
18
+ visible: boolean;
19
+ point: Point;
20
+ };
21
+
22
+ export type RenderUserPointer = {
23
+ renderUserPointer: (options: RenderUserPointerProps) => React.ReactNode;
24
+ };
25
+
26
+ type InternalUserPointerProps<E extends UserPointerData> = {
13
27
  user: MultiplayerUser;
14
- ephemeralUserData: EphemeralUserData<E>;
28
+ ephemeralUserDataManager: EphemeralUserDataManager<E>;
15
29
  hideAfter?: number;
16
- };
30
+ } & RenderUserPointer;
17
31
 
18
32
  function shouldShow(hideAfter: number, updatedAt: number) {
19
33
  return !!updatedAt && Date.now() - updatedAt < hideAfter;
20
34
  }
21
35
 
22
- const UserPointer_ = function UserPointer<E extends UserPointerData>({
36
+ const UserPointerInternal = memoGeneric(function UserPointerInternal<
37
+ E extends UserPointerData,
38
+ >({
23
39
  user,
24
- ephemeralUserData,
40
+ ephemeralUserDataManager,
25
41
  hideAfter = 5000,
26
- }: UserPointerProps<E>) {
27
- const metadata = useObservable(ephemeralUserData.metadata$, [
42
+ renderUserPointer,
43
+ }: InternalUserPointerProps<E>) {
44
+ const metadata = useObservable(ephemeralUserDataManager.metadata$, [
45
+ user.id,
46
+ ] as const);
47
+ const data = useObservable(ephemeralUserDataManager.data$, [
28
48
  user.id,
29
49
  ] as const);
30
- const data = useObservable(ephemeralUserData.data$, [user.id] as const);
31
- const avatarStyle = useMemo(() => getAvatarStyle(user.id), [user.id]);
32
50
 
33
51
  const [, setForceUpdate] = useState(0);
34
52
 
35
53
  const updatedAt = metadata?.updatedAt ?? 0;
54
+ const show = shouldShow(hideAfter, updatedAt);
36
55
 
37
56
  useEffect(() => {
38
- if (!shouldShow(hideAfter, updatedAt)) return;
57
+ if (!show) return;
39
58
 
40
59
  const timeoutId = setTimeout(() => {
41
60
  setForceUpdate((prev) => prev + 1);
42
61
  }, hideAfter);
43
62
 
44
63
  return () => clearTimeout(timeoutId);
45
- }, [hideAfter, updatedAt]);
46
-
47
- const show = shouldShow(hideAfter, updatedAt);
48
-
49
- if (!data || !data.pointer) return null;
50
-
51
- return (
52
- <UserPointerContainer key={user.id} point={data.pointer} show={show}>
53
- <UserPointerIcon color={avatarStyle.backgroundColor} />
54
- <UserNameTag background={avatarStyle.backgroundColor}>
55
- {user.name}
56
- </UserNameTag>
57
- </UserPointerContainer>
58
- );
59
- };
64
+ }, [hideAfter, show, updatedAt]);
60
65
 
61
- export const UserPointer = memo(UserPointer_) as typeof UserPointer_;
66
+ if (!data?.pointer) return null;
62
67
 
63
- export const UserPointerContainer = memo(function UserPointerContainer({
64
- point,
65
- children,
66
- show = true,
67
- }: {
68
- point: Point;
69
- children: React.ReactNode;
70
- show?: boolean;
71
- }) {
72
- return (
73
- <div
74
- style={{
75
- position: "absolute",
76
- top: 0,
77
- left: 0,
78
- transform: `translate(${Math.round(point.x)}px, ${Math.round(
79
- point.y
80
- )}px)`,
81
- opacity: show ? 1 : 0,
82
- transition: "transform 0.1s, opacity 0.2s",
83
- }}
84
- >
85
- {children}
86
- </div>
87
- );
88
- });
89
-
90
- export const UserNameTag = memo(function UserNameTag({
91
- background,
92
- children,
93
- }: {
94
- background: string;
95
- children: React.ReactNode;
96
- }) {
97
- return (
98
- <span
99
- style={{
100
- position: "relative",
101
- top: "8px",
102
- left: "15px",
103
- fontSize: 11,
104
- lineHeight: "1",
105
- fontWeight: 500,
106
- boxShadow: "0 1px 1px rgba(0,0,0,0.1)",
107
- padding: "2px 4px 3px",
108
- pointerEvents: "none",
109
- letterSpacing: "-0.3px",
110
- outline: `1px solid rgba(255,255,255,0.3)`,
111
- outlineOffset: "-1px",
112
- background,
113
- color: "white",
114
- }}
115
- >
116
- {children}
117
- </span>
118
- );
68
+ return renderUserPointer({
69
+ userId: user.id,
70
+ name: user.name,
71
+ point: data.pointer,
72
+ visible: show,
73
+ });
119
74
  });
120
75
 
121
76
  export type UserPointersOverlayProps<E extends UserPointerData> = {
122
- connectedUsers: MultiplayerUser[];
123
- ephemeralUserData: EphemeralUserData<E>;
124
- };
77
+ connectedUsersManager: ConnectedUsersManager;
78
+ ephemeralUserDataManager: EphemeralUserDataManager<E>;
79
+ } & RenderUserPointer;
125
80
 
126
- export const UserPointersOverlay = memo(function UserPointers<
81
+ export const UserPointersOverlay = memoGeneric(function UserPointers<
127
82
  E extends UserPointerData,
128
- >({ connectedUsers, ephemeralUserData }: UserPointersOverlayProps<E>) {
129
- const currentUserId = useObservable(ephemeralUserData.currentUserId$);
83
+ >({
84
+ connectedUsersManager,
85
+ ephemeralUserDataManager,
86
+ renderUserPointer,
87
+ }: UserPointersOverlayProps<E>) {
88
+ const currentUserId = useObservable(ephemeralUserDataManager.currentUserId$);
89
+ const connectedUsers = useObservable(connectedUsersManager.connectedUsers$);
130
90
 
131
91
  return (
132
92
  <>
@@ -134,49 +94,16 @@ export const UserPointersOverlay = memo(function UserPointers<
134
94
  if (user.id === currentUserId) return null;
135
95
 
136
96
  return (
137
- <UserPointer
97
+ <UserPointerInternal
138
98
  key={user.id}
139
99
  user={user}
140
- ephemeralUserData={
141
- ephemeralUserData as unknown as EphemeralUserData<E>
100
+ ephemeralUserDataManager={
101
+ ephemeralUserDataManager as unknown as EphemeralUserDataManager<UserPointerData>
142
102
  }
103
+ renderUserPointer={renderUserPointer}
143
104
  />
144
105
  );
145
106
  })}
146
107
  </>
147
108
  );
148
109
  });
149
-
150
- export const UserPointerIcon = memo(function CursorIcon({
151
- color = "black",
152
- }: {
153
- color?: string;
154
- }) {
155
- return (
156
- <svg
157
- width="15"
158
- height="15"
159
- viewBox="0 0 15 15"
160
- fill="none"
161
- xmlns="http://www.w3.org/2000/svg"
162
- style={{
163
- position: "absolute",
164
- filter: "drop-shadow(0 1px 1px rgba(0,0,0,0.1))",
165
- // filter: "drop-shadow(0 1px 1px rgba(0,0,0,0.6))",
166
- }}
167
- >
168
- <path
169
- fillRule="evenodd"
170
- clipRule="evenodd"
171
- d="M3.29227 0.0451998C3.47033 -0.0361222 3.67946 -0.00606629 3.8274 0.122107L12.8587 7.94648C13.0134 8.08054 13.0708 8.29538 13.0035 8.48873C12.9362 8.68208 12.7578 8.81488 12.5533 8.8239L9.21887 8.97096L11.1504 13.2149C11.2648 13.4662 11.1538 13.7626 10.9026 13.877L8.75024 14.8575C8.499 14.972 8.20255 14.8611 8.08802 14.6099L6.15339 10.3665L3.86279 12.7817C3.72196 12.9302 3.50487 12.9779 3.31479 12.9021C3.1247 12.8263 3 12.6423 3 12.4376V0.500008C3 0.304264 3.11422 0.126522 3.29227 0.0451998Z"
172
- fill={color}
173
- />
174
- <path
175
- fillRule="evenodd"
176
- clipRule="evenodd"
177
- d="M3.29227 0.048984C3.47033 -0.032338 3.67946 -0.00228211 3.8274 0.125891L12.8587 7.95026C13.0134 8.08432 13.0708 8.29916 13.0035 8.49251C12.9362 8.68586 12.7578 8.81866 12.5533 8.82768L9.21887 8.97474L11.1504 13.2187C11.2648 13.47 11.1538 13.7664 10.9026 13.8808L8.75024 14.8613C8.499 14.9758 8.20255 14.8649 8.08802 14.6137L6.15339 10.3703L3.86279 12.7855C3.72196 12.934 3.50487 12.9817 3.31479 12.9059C3.1247 12.8301 3 12.6461 3 12.4414V0.503792C3 0.308048 3.11422 0.130306 3.29227 0.048984ZM4 1.59852V11.1877L5.93799 9.14425C6.05238 9.02363 6.21924 8.96776 6.38319 8.99516C6.54715 9.02256 6.68677 9.12965 6.75573 9.2809L8.79056 13.7441L10.0332 13.178L8.00195 8.71497C7.93313 8.56376 7.94391 8.38824 8.03072 8.24659C8.11753 8.10494 8.26903 8.01566 8.435 8.00834L11.2549 7.88397L4 1.59852Z"
178
- fill="rgba(255,255,255,0.5)"
179
- />
180
- </svg>
181
- );
182
- });