@noya-app/noya-multiplayer-react 0.1.21 → 0.1.23

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.
package/src/noyaApp.ts ADDED
@@ -0,0 +1,185 @@
1
+ /* eslint-disable no-restricted-globals */
2
+ import {
3
+ createValue,
4
+ localStorageSync,
5
+ MultiplayerUser,
6
+ SyncAdapter,
7
+ TSchema,
8
+ webSocketSync,
9
+ } from "@noya-app/state-manager";
10
+ import { useEffect, useMemo, useState } from "react";
11
+ import { useMultiplayerState, UseMultiplayerStateOptions } from "./hooks";
12
+ import { StateInspectorOptions } from "./inspector/StateInspector";
13
+
14
+ export type AppViewType = "editable" | "readOnly" | "preview";
15
+
16
+ export type AppTheme = "light" | "dark";
17
+
18
+ export type AppData<State> = {
19
+ multiplayerUrl?: string;
20
+ theme: AppTheme;
21
+ viewType: AppViewType;
22
+ initialState: State;
23
+ inspector: boolean | StateInspectorOptions;
24
+ };
25
+
26
+ export function createDefaultAppData<State>(
27
+ initialState: State
28
+ ): AppData<State> {
29
+ return {
30
+ theme: "light",
31
+ viewType: "editable",
32
+ initialState,
33
+ inspector: false,
34
+ };
35
+ }
36
+
37
+ export function getAppData<State>(): AppData<State> | undefined;
38
+ export function getAppData<State>(initialState: State): AppData<State>;
39
+ export function getAppData<State>(
40
+ initialState?: State
41
+ ): AppData<State> | undefined {
42
+ try {
43
+ const urlHash = (window.location.hash || "").replace(/^#/, "");
44
+ const params = new URLSearchParams(urlHash);
45
+ const data = params.get("data");
46
+ if (!data) {
47
+ return initialState ? createDefaultAppData(initialState) : undefined;
48
+ }
49
+ const parsed = JSON.parse(data);
50
+ if (!parsed.initialState) {
51
+ parsed.initialState = initialState;
52
+ }
53
+ return parsed;
54
+ } catch (e) {
55
+ return initialState ? createDefaultAppData(initialState) : undefined;
56
+ }
57
+ }
58
+
59
+ type AppDataOptions = {
60
+ schema?: TSchema;
61
+ };
62
+
63
+ /**
64
+ * Get data from host app. Fall back to default data.
65
+ */
66
+ export function useAppData<S extends object>(
67
+ initialState: S,
68
+ { schema }: AppDataOptions = {}
69
+ ) {
70
+ const appData = useMemo(() => {
71
+ return getAppData(
72
+ schema ? createValue(schema, initialState) : initialState
73
+ );
74
+ }, [initialState, schema]);
75
+
76
+ return appData;
77
+ }
78
+
79
+ export type IframeToHostMessage =
80
+ | {
81
+ type: "application.setMenuItems";
82
+ payload: {
83
+ menuItems: any[];
84
+ };
85
+ }
86
+ | {
87
+ type: "multiplayer.setUsers";
88
+ payload: {
89
+ users: any[];
90
+ };
91
+ };
92
+
93
+ export type HostToIframeMessage = {
94
+ type: "application.selectMenuItem";
95
+ payload: {
96
+ value: string;
97
+ };
98
+ };
99
+
100
+ export function useBroadcastConnectedUsers(connectedUsers: MultiplayerUser[]) {
101
+ useEffect(() => {
102
+ if (!isEmbeddedApp()) return;
103
+
104
+ const message: IframeToHostMessage = {
105
+ type: "multiplayer.setUsers",
106
+ payload: { users: connectedUsers },
107
+ };
108
+
109
+ parent?.postMessage(message, "*");
110
+ }, [connectedUsers]);
111
+ }
112
+
113
+ export function useBroadcastMenuItems<T>(
114
+ menuItems: any[],
115
+ handleSelectMenuItem: (value: T) => void
116
+ ) {
117
+ useEffect(() => {
118
+ if (!isEmbeddedApp()) return;
119
+
120
+ const message: IframeToHostMessage = {
121
+ type: "application.setMenuItems",
122
+ payload: { menuItems },
123
+ };
124
+
125
+ parent?.postMessage(message, "*");
126
+
127
+ const listener = (event: MessageEvent<HostToIframeMessage>) => {
128
+ if (event.data.type === "application.selectMenuItem") {
129
+ handleSelectMenuItem(event.data.payload.value as T);
130
+ }
131
+ };
132
+
133
+ window.addEventListener("message", listener);
134
+
135
+ return () => {
136
+ window.removeEventListener("message", listener);
137
+ };
138
+ }, [handleSelectMenuItem, menuItems]);
139
+ }
140
+
141
+ export function isEmbeddedApp() {
142
+ return window.self !== window.top;
143
+ }
144
+
145
+ export function useNoyaAppState<S extends object, M = void, E = void>(
146
+ initialState: S | (() => S),
147
+ options: UseMultiplayerStateOptions<S, M, E> & {
148
+ localStorageKey?: string;
149
+ }
150
+ ) {
151
+ // Resolve state to a value
152
+ const [state] = useState(initialState);
153
+
154
+ const {
155
+ multiplayerUrl,
156
+ inspector,
157
+ initialState: noyaAppInitialState,
158
+ theme,
159
+ viewType,
160
+ } = useAppData(state, {
161
+ schema: options.schema,
162
+ });
163
+
164
+ const sync = useMemo((): SyncAdapter<S, M, E> => {
165
+ return multiplayerUrl
166
+ ? webSocketSync(multiplayerUrl)
167
+ : localStorageSync({ key: options.localStorageKey || "noya-app" });
168
+ }, [multiplayerUrl, options.localStorageKey]);
169
+
170
+ const result = useMultiplayerState(noyaAppInitialState, {
171
+ ...options,
172
+ inspector: options.inspector ?? inspector,
173
+ sync,
174
+ });
175
+
176
+ const [, , extras] = result;
177
+
178
+ useBroadcastConnectedUsers(extras.connectedUsers);
179
+
180
+ const mergedExtras = useMemo(() => {
181
+ return { ...extras, theme, viewType } as const;
182
+ }, [extras, theme, viewType]);
183
+
184
+ return [result[0], result[1], mergedExtras] as const;
185
+ }