jazz-react-native 0.8.3
Sign up to get free protection for your applications and to get access to all the features.
- 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;
|