jazz-react-native 0.8.3
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/.eslintrc.cjs +24 -0
- package/.prettierrc.js +9 -0
- package/CHANGELOG.md +917 -0
- package/LICENSE.txt +19 -0
- package/README.md +10 -0
- package/dist/auth/DemoAuthMethod.d.ts +28 -0
- package/dist/auth/DemoAuthMethod.js +132 -0
- package/dist/auth/DemoAuthMethod.js.map +1 -0
- package/dist/auth/DemoAuthUI.d.ts +33 -0
- package/dist/auth/DemoAuthUI.js +203 -0
- package/dist/auth/DemoAuthUI.js.map +1 -0
- package/dist/auth/auth.d.ts +1 -0
- package/dist/auth/auth.js +2 -0
- package/dist/auth/auth.js.map +1 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +153 -0
- package/dist/index.js.map +1 -0
- package/dist/media.d.ts +21 -0
- package/dist/media.js +44 -0
- package/dist/media.js.map +1 -0
- package/dist/native-storage.d.ts +15 -0
- package/dist/native-storage.js +24 -0
- package/dist/native-storage.js.map +1 -0
- package/dist/provider.d.ts +46 -0
- package/dist/provider.js +130 -0
- package/dist/provider.js.map +1 -0
- package/package.json +47 -0
- package/src/auth/DemoAuthMethod.ts +219 -0
- package/src/auth/DemoAuthUI.tsx +289 -0
- package/src/auth/auth.ts +1 -0
- package/src/index.ts +275 -0
- package/src/media.tsx +68 -0
- package/src/native-storage.ts +35 -0
- package/src/provider.tsx +306 -0
- package/tsconfig.json +21 -0
@@ -0,0 +1,289 @@
|
|
1
|
+
import React, { useMemo, useState, useEffect } from "react";
|
2
|
+
import {
|
3
|
+
View,
|
4
|
+
Text,
|
5
|
+
TouchableOpacity,
|
6
|
+
TextInput,
|
7
|
+
StyleSheet,
|
8
|
+
} from "react-native";
|
9
|
+
import { AgentSecret } from "cojson";
|
10
|
+
import { Account, ID } from "jazz-tools";
|
11
|
+
import { RNDemoAuth } from "./DemoAuthMethod.js";
|
12
|
+
|
13
|
+
type DemoAuthState = (
|
14
|
+
| {
|
15
|
+
state: "uninitialized";
|
16
|
+
}
|
17
|
+
| {
|
18
|
+
state: "loading";
|
19
|
+
}
|
20
|
+
| {
|
21
|
+
state: "ready";
|
22
|
+
existingUsers: string[];
|
23
|
+
signUp: (username: string) => void;
|
24
|
+
logInAs: (existingUser: string) => void;
|
25
|
+
}
|
26
|
+
| {
|
27
|
+
state: "signedIn";
|
28
|
+
logOut: () => void;
|
29
|
+
}
|
30
|
+
) & {
|
31
|
+
errors: string[];
|
32
|
+
};
|
33
|
+
|
34
|
+
/** @category Auth Providers */
|
35
|
+
export function useDemoAuth({
|
36
|
+
seedAccounts,
|
37
|
+
}: {
|
38
|
+
seedAccounts?: {
|
39
|
+
[name: string]: { accountID: ID<Account>; accountSecret: AgentSecret };
|
40
|
+
};
|
41
|
+
} = {}) {
|
42
|
+
const [state, setState] = useState<DemoAuthState>({
|
43
|
+
state: "loading",
|
44
|
+
errors: [],
|
45
|
+
});
|
46
|
+
|
47
|
+
const [authMethod, setAuthMethod] = useState<RNDemoAuth | null>(null);
|
48
|
+
|
49
|
+
const authMethodPromise = useMemo(() => {
|
50
|
+
return RNDemoAuth.init(
|
51
|
+
{
|
52
|
+
onReady: async ({ signUp, getExistingUsers, logInAs }) => {
|
53
|
+
const existingUsers = await getExistingUsers();
|
54
|
+
setState({
|
55
|
+
state: "ready",
|
56
|
+
signUp,
|
57
|
+
existingUsers,
|
58
|
+
logInAs,
|
59
|
+
errors: [],
|
60
|
+
});
|
61
|
+
},
|
62
|
+
onSignedIn: ({ logOut }) => {
|
63
|
+
setState({ state: "signedIn", logOut, errors: [] });
|
64
|
+
},
|
65
|
+
onError: (error) => {
|
66
|
+
setState((current) => ({
|
67
|
+
...current,
|
68
|
+
errors: [...current.errors, error.toString()],
|
69
|
+
}));
|
70
|
+
},
|
71
|
+
},
|
72
|
+
seedAccounts,
|
73
|
+
);
|
74
|
+
}, [seedAccounts]);
|
75
|
+
|
76
|
+
useEffect(() => {
|
77
|
+
async function init() {
|
78
|
+
const auth = await authMethodPromise;
|
79
|
+
setAuthMethod(auth);
|
80
|
+
}
|
81
|
+
if (authMethod) return;
|
82
|
+
void init();
|
83
|
+
}, [seedAccounts]);
|
84
|
+
|
85
|
+
return [authMethod, state] as const;
|
86
|
+
}
|
87
|
+
|
88
|
+
export const DemoAuthBasicUI = ({
|
89
|
+
appName,
|
90
|
+
state,
|
91
|
+
}: {
|
92
|
+
appName: string;
|
93
|
+
state: DemoAuthState;
|
94
|
+
}) => {
|
95
|
+
const darkMode = false;
|
96
|
+
const [username, setUsername] = useState<string>("");
|
97
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
98
|
+
|
99
|
+
const handleSignUp = () => {
|
100
|
+
if (state.state !== "ready") return;
|
101
|
+
if (username.trim() === "") {
|
102
|
+
setErrorMessage("Display name is required");
|
103
|
+
} else {
|
104
|
+
setErrorMessage(null);
|
105
|
+
state.signUp(username);
|
106
|
+
}
|
107
|
+
};
|
108
|
+
|
109
|
+
return (
|
110
|
+
<View
|
111
|
+
style={[
|
112
|
+
styles.container,
|
113
|
+
darkMode ? styles.darkBackground : styles.lightBackground,
|
114
|
+
]}
|
115
|
+
>
|
116
|
+
{state.state === "loading" ? (
|
117
|
+
<Text style={styles.loadingText}>Loading...</Text>
|
118
|
+
) : state.state === "ready" ? (
|
119
|
+
<View style={styles.formContainer}>
|
120
|
+
<Text
|
121
|
+
style={[
|
122
|
+
styles.headerText,
|
123
|
+
darkMode ? styles.darkText : styles.lightText,
|
124
|
+
]}
|
125
|
+
>
|
126
|
+
{appName}
|
127
|
+
</Text>
|
128
|
+
|
129
|
+
{state.errors.map((error) => (
|
130
|
+
<Text key={error} style={styles.errorText}>
|
131
|
+
{error}
|
132
|
+
</Text>
|
133
|
+
))}
|
134
|
+
|
135
|
+
{errorMessage && (
|
136
|
+
<Text style={styles.errorText}>{errorMessage}</Text>
|
137
|
+
)}
|
138
|
+
|
139
|
+
<TextInput
|
140
|
+
placeholder="Display name"
|
141
|
+
value={username}
|
142
|
+
onChangeText={setUsername}
|
143
|
+
placeholderTextColor={darkMode ? "#fff" : "#000"}
|
144
|
+
style={[
|
145
|
+
styles.textInput,
|
146
|
+
darkMode ? styles.darkInput : styles.lightInput,
|
147
|
+
]}
|
148
|
+
/>
|
149
|
+
|
150
|
+
<TouchableOpacity
|
151
|
+
onPress={handleSignUp}
|
152
|
+
style={[
|
153
|
+
styles.button,
|
154
|
+
darkMode ? styles.darkButton : styles.lightButton,
|
155
|
+
]}
|
156
|
+
>
|
157
|
+
<Text
|
158
|
+
style={
|
159
|
+
darkMode
|
160
|
+
? styles.darkButtonText
|
161
|
+
: styles.lightButtonText
|
162
|
+
}
|
163
|
+
>
|
164
|
+
Sign Up as new account
|
165
|
+
</Text>
|
166
|
+
</TouchableOpacity>
|
167
|
+
|
168
|
+
<View style={styles.existingUsersContainer}>
|
169
|
+
{state.existingUsers.map((user) => (
|
170
|
+
<TouchableOpacity
|
171
|
+
key={user}
|
172
|
+
onPress={() => state.logInAs(user)}
|
173
|
+
style={[
|
174
|
+
styles.existingUserButton,
|
175
|
+
darkMode
|
176
|
+
? styles.darkUserButton
|
177
|
+
: styles.lightUserButton,
|
178
|
+
]}
|
179
|
+
>
|
180
|
+
<Text
|
181
|
+
style={
|
182
|
+
darkMode
|
183
|
+
? styles.darkText
|
184
|
+
: styles.lightText
|
185
|
+
}
|
186
|
+
>
|
187
|
+
Log In as "{user}"
|
188
|
+
</Text>
|
189
|
+
</TouchableOpacity>
|
190
|
+
))}
|
191
|
+
</View>
|
192
|
+
</View>
|
193
|
+
) : null}
|
194
|
+
</View>
|
195
|
+
);
|
196
|
+
};
|
197
|
+
|
198
|
+
const styles = StyleSheet.create({
|
199
|
+
container: {
|
200
|
+
flex: 1,
|
201
|
+
justifyContent: "center",
|
202
|
+
alignItems: "center",
|
203
|
+
padding: 20,
|
204
|
+
},
|
205
|
+
formContainer: {
|
206
|
+
width: "80%",
|
207
|
+
alignItems: "center",
|
208
|
+
justifyContent: "center",
|
209
|
+
},
|
210
|
+
headerText: {
|
211
|
+
fontSize: 24,
|
212
|
+
marginBottom: 20,
|
213
|
+
},
|
214
|
+
errorText: {
|
215
|
+
color: "red",
|
216
|
+
marginVertical: 5,
|
217
|
+
textAlign: "center",
|
218
|
+
},
|
219
|
+
textInput: {
|
220
|
+
borderWidth: 1,
|
221
|
+
padding: 10,
|
222
|
+
marginVertical: 10,
|
223
|
+
width: "100%",
|
224
|
+
borderRadius: 6,
|
225
|
+
},
|
226
|
+
darkInput: {
|
227
|
+
borderColor: "#444",
|
228
|
+
backgroundColor: "#000",
|
229
|
+
color: "#fff",
|
230
|
+
},
|
231
|
+
lightInput: {
|
232
|
+
borderColor: "#ddd",
|
233
|
+
backgroundColor: "#fff",
|
234
|
+
color: "#000",
|
235
|
+
},
|
236
|
+
button: {
|
237
|
+
paddingVertical: 15,
|
238
|
+
paddingHorizontal: 10,
|
239
|
+
borderRadius: 6,
|
240
|
+
width: "100%",
|
241
|
+
marginVertical: 10,
|
242
|
+
},
|
243
|
+
darkButton: {
|
244
|
+
backgroundColor: "#444",
|
245
|
+
},
|
246
|
+
lightButton: {
|
247
|
+
backgroundColor: "#ddd",
|
248
|
+
},
|
249
|
+
darkButtonText: {
|
250
|
+
color: "#fff",
|
251
|
+
textAlign: "center",
|
252
|
+
},
|
253
|
+
lightButtonText: {
|
254
|
+
color: "#000",
|
255
|
+
textAlign: "center",
|
256
|
+
},
|
257
|
+
existingUsersContainer: {
|
258
|
+
width: "100%",
|
259
|
+
marginTop: 20,
|
260
|
+
},
|
261
|
+
existingUserButton: {
|
262
|
+
paddingVertical: 15,
|
263
|
+
paddingHorizontal: 10,
|
264
|
+
borderRadius: 6,
|
265
|
+
marginVertical: 5,
|
266
|
+
},
|
267
|
+
darkUserButton: {
|
268
|
+
backgroundColor: "#222",
|
269
|
+
},
|
270
|
+
lightUserButton: {
|
271
|
+
backgroundColor: "#eee",
|
272
|
+
},
|
273
|
+
loadingText: {
|
274
|
+
fontSize: 18,
|
275
|
+
color: "#888",
|
276
|
+
},
|
277
|
+
darkText: {
|
278
|
+
color: "#fff",
|
279
|
+
},
|
280
|
+
lightText: {
|
281
|
+
color: "#000",
|
282
|
+
},
|
283
|
+
darkBackground: {
|
284
|
+
backgroundColor: "#000",
|
285
|
+
},
|
286
|
+
lightBackground: {
|
287
|
+
backgroundColor: "#fff",
|
288
|
+
},
|
289
|
+
});
|
package/src/auth/auth.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export * from "./DemoAuthUI.js";
|
package/src/index.ts
ADDED
@@ -0,0 +1,275 @@
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
3
|
+
import {
|
4
|
+
CoValue,
|
5
|
+
ID,
|
6
|
+
AgentID,
|
7
|
+
SessionID,
|
8
|
+
cojsonInternals,
|
9
|
+
InviteSecret,
|
10
|
+
Account,
|
11
|
+
CoValueClass,
|
12
|
+
CryptoProvider,
|
13
|
+
AuthMethod,
|
14
|
+
createJazzContext,
|
15
|
+
AnonymousJazzAgent,
|
16
|
+
} from "jazz-tools";
|
17
|
+
|
18
|
+
import { PureJSCrypto } from "jazz-tools/native";
|
19
|
+
import { RawAccountID } from "cojson";
|
20
|
+
import { createWebSocketPeer } from "cojson-transport-ws";
|
21
|
+
import { MMKV } from "react-native-mmkv";
|
22
|
+
import NetInfo from "@react-native-community/netinfo";
|
23
|
+
import * as Linking from "expo-linking";
|
24
|
+
|
25
|
+
export { RNDemoAuth } from "./auth/DemoAuthMethod.js";
|
26
|
+
|
27
|
+
import { NativeStorageContext } from "./native-storage.js";
|
28
|
+
|
29
|
+
/** @category Context Creation */
|
30
|
+
export type BrowserContext<Acc extends Account> = {
|
31
|
+
me: Acc;
|
32
|
+
logOut: () => void;
|
33
|
+
// TODO: Symbol.dispose?
|
34
|
+
done: () => void;
|
35
|
+
};
|
36
|
+
|
37
|
+
export type BrowserGuestContext = {
|
38
|
+
guest: AnonymousJazzAgent;
|
39
|
+
logOut: () => void;
|
40
|
+
done: () => void;
|
41
|
+
};
|
42
|
+
|
43
|
+
export type BrowserContextOptions<Acc extends Account> = {
|
44
|
+
auth: AuthMethod;
|
45
|
+
AccountSchema: CoValueClass<Acc> & {
|
46
|
+
fromNode: (typeof Account)["fromNode"];
|
47
|
+
};
|
48
|
+
} & BaseBrowserContextOptions;
|
49
|
+
|
50
|
+
export type BaseBrowserContextOptions = {
|
51
|
+
peer: `wss://${string}` | `ws://${string}`;
|
52
|
+
reconnectionTimeout?: number;
|
53
|
+
storage?: "indexedDB" | "singleTabOPFS";
|
54
|
+
crypto?: CryptoProvider;
|
55
|
+
};
|
56
|
+
|
57
|
+
/** @category Context Creation */
|
58
|
+
export async function createJazzRNContext<Acc extends Account>(
|
59
|
+
options: BrowserContextOptions<Acc>,
|
60
|
+
): Promise<BrowserContext<Acc>>;
|
61
|
+
export async function createJazzRNContext(
|
62
|
+
options: BaseBrowserContextOptions,
|
63
|
+
): Promise<BrowserGuestContext>;
|
64
|
+
export async function createJazzRNContext<Acc extends Account>(
|
65
|
+
options: BrowserContextOptions<Acc> | BaseBrowserContextOptions,
|
66
|
+
): Promise<BrowserContext<Acc> | BrowserGuestContext>;
|
67
|
+
export async function createJazzRNContext<Acc extends Account>(
|
68
|
+
options: BrowserContextOptions<Acc> | BaseBrowserContextOptions,
|
69
|
+
): Promise<BrowserContext<Acc> | BrowserGuestContext> {
|
70
|
+
const firstWsPeer = createWebSocketPeer({
|
71
|
+
websocket: new WebSocket(options.peer),
|
72
|
+
id: options.peer + "@" + new Date().toISOString(),
|
73
|
+
role: "server",
|
74
|
+
expectPings: true,
|
75
|
+
});
|
76
|
+
let shouldTryToReconnect = true;
|
77
|
+
|
78
|
+
let currentReconnectionTimeout = options.reconnectionTimeout || 500;
|
79
|
+
|
80
|
+
const unsubscribeNetworkChange = NetInfo.addEventListener((state) => {
|
81
|
+
if (state.isConnected) {
|
82
|
+
currentReconnectionTimeout = options.reconnectionTimeout || 500;
|
83
|
+
}
|
84
|
+
});
|
85
|
+
|
86
|
+
const context =
|
87
|
+
"auth" in options
|
88
|
+
? await createJazzContext({
|
89
|
+
AccountSchema: options.AccountSchema,
|
90
|
+
auth: options.auth,
|
91
|
+
crypto: await PureJSCrypto.create(),
|
92
|
+
peersToLoadFrom: [firstWsPeer],
|
93
|
+
sessionProvider: provideLockSession,
|
94
|
+
})
|
95
|
+
: await createJazzContext({
|
96
|
+
crypto: await PureJSCrypto.create(),
|
97
|
+
peersToLoadFrom: [firstWsPeer],
|
98
|
+
});
|
99
|
+
|
100
|
+
const node =
|
101
|
+
"account" in context
|
102
|
+
? context.account._raw.core.node
|
103
|
+
: context.agent.node;
|
104
|
+
|
105
|
+
async function websocketReconnectLoop() {
|
106
|
+
while (shouldTryToReconnect) {
|
107
|
+
if (
|
108
|
+
Object.keys(node.syncManager.peers).some((peerId) =>
|
109
|
+
peerId.includes(options.peer),
|
110
|
+
)
|
111
|
+
) {
|
112
|
+
// TODO: this might drain battery, use listeners instead
|
113
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
114
|
+
} else {
|
115
|
+
console.log(
|
116
|
+
"Websocket disconnected, trying to reconnect in " +
|
117
|
+
currentReconnectionTimeout +
|
118
|
+
"ms",
|
119
|
+
);
|
120
|
+
currentReconnectionTimeout = Math.min(
|
121
|
+
currentReconnectionTimeout * 2,
|
122
|
+
30000,
|
123
|
+
);
|
124
|
+
await new Promise<void>((resolve) => {
|
125
|
+
setTimeout(resolve, currentReconnectionTimeout);
|
126
|
+
const _unsubscribeNetworkChange = NetInfo.addEventListener(
|
127
|
+
(state) => {
|
128
|
+
if (state.isConnected) {
|
129
|
+
resolve();
|
130
|
+
_unsubscribeNetworkChange();
|
131
|
+
}
|
132
|
+
},
|
133
|
+
);
|
134
|
+
});
|
135
|
+
|
136
|
+
node.syncManager.addPeer(
|
137
|
+
createWebSocketPeer({
|
138
|
+
websocket: new WebSocket(options.peer),
|
139
|
+
id: options.peer + "@" + new Date().toISOString(),
|
140
|
+
role: "server",
|
141
|
+
}),
|
142
|
+
);
|
143
|
+
}
|
144
|
+
}
|
145
|
+
}
|
146
|
+
|
147
|
+
void websocketReconnectLoop();
|
148
|
+
|
149
|
+
return "account" in context
|
150
|
+
? {
|
151
|
+
me: context.account,
|
152
|
+
done: () => {
|
153
|
+
shouldTryToReconnect = false;
|
154
|
+
unsubscribeNetworkChange?.();
|
155
|
+
context.done();
|
156
|
+
},
|
157
|
+
logOut: () => {
|
158
|
+
context.logOut();
|
159
|
+
},
|
160
|
+
}
|
161
|
+
: {
|
162
|
+
guest: context.agent,
|
163
|
+
done: () => {
|
164
|
+
shouldTryToReconnect = false;
|
165
|
+
unsubscribeNetworkChange?.();
|
166
|
+
context.done();
|
167
|
+
},
|
168
|
+
logOut: () => {
|
169
|
+
context.logOut();
|
170
|
+
},
|
171
|
+
};
|
172
|
+
}
|
173
|
+
|
174
|
+
/** @category Auth Providers */
|
175
|
+
export type SessionProvider = (
|
176
|
+
accountID: ID<Account> | AgentID,
|
177
|
+
) => Promise<SessionID>;
|
178
|
+
|
179
|
+
export async function provideLockSession(
|
180
|
+
accountID: ID<Account> | AgentID,
|
181
|
+
crypto: CryptoProvider,
|
182
|
+
) {
|
183
|
+
const sessionDone = () => {};
|
184
|
+
|
185
|
+
const storage = NativeStorageContext.getInstance().getStorage();
|
186
|
+
|
187
|
+
const sessionID =
|
188
|
+
((await storage.get(accountID)) as SessionID) ||
|
189
|
+
crypto.newRandomSessionID(accountID as RawAccountID | AgentID);
|
190
|
+
await storage.set(accountID, sessionID);
|
191
|
+
|
192
|
+
return Promise.resolve({
|
193
|
+
sessionID,
|
194
|
+
sessionDone,
|
195
|
+
});
|
196
|
+
}
|
197
|
+
|
198
|
+
const window = {
|
199
|
+
location: {
|
200
|
+
href: "#",
|
201
|
+
},
|
202
|
+
history: {
|
203
|
+
replaceState: (a: any, b: any, c: any) => {},
|
204
|
+
},
|
205
|
+
};
|
206
|
+
|
207
|
+
/** @category Invite Links */
|
208
|
+
export function createInviteLink<C extends CoValue>(
|
209
|
+
value: C,
|
210
|
+
role: "reader" | "writer" | "admin",
|
211
|
+
{ baseURL, valueHint }: { baseURL?: string; valueHint?: string } = {},
|
212
|
+
): string {
|
213
|
+
const coValueCore = value._raw.core;
|
214
|
+
let currentCoValue = coValueCore;
|
215
|
+
|
216
|
+
while (currentCoValue.header.ruleset.type === "ownedByGroup") {
|
217
|
+
currentCoValue = currentCoValue.getGroup().core;
|
218
|
+
}
|
219
|
+
|
220
|
+
if (currentCoValue.header.ruleset.type !== "group") {
|
221
|
+
throw new Error("Can't create invite link for object without group");
|
222
|
+
}
|
223
|
+
|
224
|
+
const group = cojsonInternals.expectGroup(
|
225
|
+
currentCoValue.getCurrentContent(),
|
226
|
+
);
|
227
|
+
const inviteSecret = group.createInvite(role);
|
228
|
+
|
229
|
+
return `${baseURL}/invite/${valueHint ? valueHint + "/" : ""}${
|
230
|
+
value.id
|
231
|
+
}/${inviteSecret}`;
|
232
|
+
}
|
233
|
+
|
234
|
+
/** @category Invite Links */
|
235
|
+
export function parseInviteLink<C extends CoValue>(
|
236
|
+
inviteURL: string,
|
237
|
+
):
|
238
|
+
| {
|
239
|
+
valueID: ID<C>;
|
240
|
+
valueHint?: string;
|
241
|
+
inviteSecret: InviteSecret;
|
242
|
+
}
|
243
|
+
| undefined {
|
244
|
+
const url = Linking.parse(inviteURL);
|
245
|
+
const parts = url.path?.split("/");
|
246
|
+
|
247
|
+
if (!parts || parts[0] !== "invite") {
|
248
|
+
return undefined;
|
249
|
+
}
|
250
|
+
|
251
|
+
let valueHint: string | undefined;
|
252
|
+
let valueID: ID<C> | undefined;
|
253
|
+
let inviteSecret: InviteSecret | undefined;
|
254
|
+
|
255
|
+
if (parts.length === 4) {
|
256
|
+
valueHint = parts[1];
|
257
|
+
valueID = parts[2] as ID<C>;
|
258
|
+
inviteSecret = parts[3] as InviteSecret;
|
259
|
+
} else if (parts.length === 3) {
|
260
|
+
valueID = parts[1] as ID<C>;
|
261
|
+
inviteSecret = parts[2] as InviteSecret;
|
262
|
+
}
|
263
|
+
|
264
|
+
if (!valueID || !inviteSecret) {
|
265
|
+
return undefined;
|
266
|
+
}
|
267
|
+
|
268
|
+
return { valueID, inviteSecret, valueHint };
|
269
|
+
}
|
270
|
+
|
271
|
+
/////////
|
272
|
+
|
273
|
+
export * from "./provider.js";
|
274
|
+
export * from "./auth/auth.js";
|
275
|
+
export * from "./native-storage.js";
|
package/src/media.tsx
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
2
|
+
import { ImageDefinition } from "jazz-tools";
|
3
|
+
|
4
|
+
/** @category Media */
|
5
|
+
export function useProgressiveImg({
|
6
|
+
image,
|
7
|
+
maxWidth,
|
8
|
+
}: {
|
9
|
+
image: ImageDefinition | null | undefined;
|
10
|
+
maxWidth?: number;
|
11
|
+
}) {
|
12
|
+
const [current, setCurrent] = useState<
|
13
|
+
| { src?: string; res?: `${number}x${number}` | "placeholder" }
|
14
|
+
| undefined
|
15
|
+
>(undefined);
|
16
|
+
|
17
|
+
useEffect(() => {
|
18
|
+
let lastHighestRes: string | undefined;
|
19
|
+
if (!image) return;
|
20
|
+
const unsub = image.subscribe({}, (update) => {
|
21
|
+
const highestRes = update?.highestResAvailable({ maxWidth });
|
22
|
+
if (highestRes) {
|
23
|
+
if (highestRes.res !== lastHighestRes) {
|
24
|
+
lastHighestRes = highestRes.res;
|
25
|
+
const blob = highestRes.stream.toBlob();
|
26
|
+
if (blob) {
|
27
|
+
const blobURI = URL.createObjectURL(blob);
|
28
|
+
setCurrent({ src: blobURI, res: highestRes.res });
|
29
|
+
return () => {
|
30
|
+
setTimeout(() => URL.revokeObjectURL(blobURI), 200);
|
31
|
+
};
|
32
|
+
}
|
33
|
+
}
|
34
|
+
} else {
|
35
|
+
setCurrent({
|
36
|
+
src: update?.placeholderDataURL,
|
37
|
+
res: "placeholder",
|
38
|
+
});
|
39
|
+
}
|
40
|
+
});
|
41
|
+
|
42
|
+
return unsub;
|
43
|
+
}, [image?.id, maxWidth]);
|
44
|
+
|
45
|
+
return {
|
46
|
+
src: current?.src,
|
47
|
+
res: current?.res,
|
48
|
+
originalSize: image?.originalSize,
|
49
|
+
};
|
50
|
+
}
|
51
|
+
|
52
|
+
/** @category Media */
|
53
|
+
export function ProgressiveImg({
|
54
|
+
children,
|
55
|
+
image,
|
56
|
+
maxWidth,
|
57
|
+
}: {
|
58
|
+
children: (result: {
|
59
|
+
src: string | undefined;
|
60
|
+
res: `${number}x${number}` | "placeholder" | undefined;
|
61
|
+
originalSize: readonly [number, number] | undefined;
|
62
|
+
}) => React.ReactNode;
|
63
|
+
image: ImageDefinition | null | undefined;
|
64
|
+
maxWidth?: number;
|
65
|
+
}) {
|
66
|
+
const result = useProgressiveImg({ image, maxWidth });
|
67
|
+
return result && children(result);
|
68
|
+
}
|
@@ -0,0 +1,35 @@
|
|
1
|
+
export interface NativeStorage {
|
2
|
+
get(key: string): Promise<string | undefined>;
|
3
|
+
set(key: string, value: string): Promise<void>;
|
4
|
+
delete(key: string): Promise<void>;
|
5
|
+
clearAll(): Promise<void>;
|
6
|
+
}
|
7
|
+
|
8
|
+
export class NativeStorageContext {
|
9
|
+
private static instance: NativeStorageContext;
|
10
|
+
private storageInstance: NativeStorage | null = null;
|
11
|
+
|
12
|
+
private constructor() {}
|
13
|
+
|
14
|
+
public static getInstance(): NativeStorageContext {
|
15
|
+
if (!NativeStorageContext.instance) {
|
16
|
+
NativeStorageContext.instance = new NativeStorageContext();
|
17
|
+
}
|
18
|
+
return NativeStorageContext.instance;
|
19
|
+
}
|
20
|
+
|
21
|
+
public initialize(db: NativeStorage): void {
|
22
|
+
if (!this.storageInstance) {
|
23
|
+
this.storageInstance = db;
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
public getStorage(): NativeStorage {
|
28
|
+
if (!this.storageInstance) {
|
29
|
+
throw new Error("Storage instance is not initialized.");
|
30
|
+
}
|
31
|
+
return this.storageInstance;
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
export default NativeStorageContext;
|