@dxos/plugin-wnfs 0.6.14-main.f49f251

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.
Files changed (87) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +15 -0
  3. package/dist/lib/browser/FileCard-HJIZDWJF.mjs +39 -0
  4. package/dist/lib/browser/FileCard-HJIZDWJF.mjs.map +7 -0
  5. package/dist/lib/browser/FileMain-WEWTX7QB.mjs +27 -0
  6. package/dist/lib/browser/FileMain-WEWTX7QB.mjs.map +7 -0
  7. package/dist/lib/browser/FileSection-BEUGJ7EI.mjs +24 -0
  8. package/dist/lib/browser/FileSection-BEUGJ7EI.mjs.map +7 -0
  9. package/dist/lib/browser/FileSlide-NYBGZEZS.mjs +21 -0
  10. package/dist/lib/browser/FileSlide-NYBGZEZS.mjs.map +7 -0
  11. package/dist/lib/browser/chunk-2I63TM7M.mjs +27 -0
  12. package/dist/lib/browser/chunk-2I63TM7M.mjs.map +7 -0
  13. package/dist/lib/browser/chunk-STEG7B5Y.mjs +17 -0
  14. package/dist/lib/browser/chunk-STEG7B5Y.mjs.map +7 -0
  15. package/dist/lib/browser/chunk-VI24OYJB.mjs +30 -0
  16. package/dist/lib/browser/chunk-VI24OYJB.mjs.map +7 -0
  17. package/dist/lib/browser/index.mjs +517 -0
  18. package/dist/lib/browser/index.mjs.map +7 -0
  19. package/dist/lib/browser/meta.json +1 -0
  20. package/dist/lib/browser/meta.mjs +9 -0
  21. package/dist/lib/browser/meta.mjs.map +7 -0
  22. package/dist/lib/browser/types/index.mjs +10 -0
  23. package/dist/lib/browser/types/index.mjs.map +7 -0
  24. package/dist/types/src/WnfsPlugin.d.ts +4 -0
  25. package/dist/types/src/WnfsPlugin.d.ts.map +1 -0
  26. package/dist/types/src/blockstore.d.ts +19 -0
  27. package/dist/types/src/blockstore.d.ts.map +1 -0
  28. package/dist/types/src/common.d.ts +11 -0
  29. package/dist/types/src/common.d.ts.map +1 -0
  30. package/dist/types/src/components/FileCard.d.ts +9 -0
  31. package/dist/types/src/components/FileCard.d.ts.map +1 -0
  32. package/dist/types/src/components/FileMain.d.ts +7 -0
  33. package/dist/types/src/components/FileMain.d.ts.map +1 -0
  34. package/dist/types/src/components/FilePreview.d.ts +11 -0
  35. package/dist/types/src/components/FilePreview.d.ts.map +1 -0
  36. package/dist/types/src/components/FileSection.d.ts +8 -0
  37. package/dist/types/src/components/FileSection.d.ts.map +1 -0
  38. package/dist/types/src/components/FileSlide.d.ts +8 -0
  39. package/dist/types/src/components/FileSlide.d.ts.map +1 -0
  40. package/dist/types/src/components/index.d.ts +14 -0
  41. package/dist/types/src/components/index.d.ts.map +1 -0
  42. package/dist/types/src/extensions/image.d.ts +9 -0
  43. package/dist/types/src/extensions/image.d.ts.map +1 -0
  44. package/dist/types/src/extensions/image.stories.d.ts +10 -0
  45. package/dist/types/src/extensions/image.stories.d.ts.map +1 -0
  46. package/dist/types/src/extensions/index.d.ts +2 -0
  47. package/dist/types/src/extensions/index.d.ts.map +1 -0
  48. package/dist/types/src/index.d.ts +4 -0
  49. package/dist/types/src/index.d.ts.map +1 -0
  50. package/dist/types/src/load.d.ts +8 -0
  51. package/dist/types/src/load.d.ts.map +1 -0
  52. package/dist/types/src/meta.d.ts +10 -0
  53. package/dist/types/src/meta.d.ts.map +1 -0
  54. package/dist/types/src/translations.d.ts +11 -0
  55. package/dist/types/src/translations.d.ts.map +1 -0
  56. package/dist/types/src/types/file.d.ts +20 -0
  57. package/dist/types/src/types/file.d.ts.map +1 -0
  58. package/dist/types/src/types/index.d.ts +3 -0
  59. package/dist/types/src/types/index.d.ts.map +1 -0
  60. package/dist/types/src/types/types.d.ts +8 -0
  61. package/dist/types/src/types/types.d.ts.map +1 -0
  62. package/dist/types/src/upload.d.ts +10 -0
  63. package/dist/types/src/upload.d.ts.map +1 -0
  64. package/dist/types/src/wnfs-url.d.ts +2 -0
  65. package/dist/types/src/wnfs-url.d.ts.map +1 -0
  66. package/package.json +82 -0
  67. package/src/WnfsPlugin.tsx +102 -0
  68. package/src/blockstore.ts +96 -0
  69. package/src/common.ts +84 -0
  70. package/src/components/FileCard.tsx +45 -0
  71. package/src/components/FileMain.tsx +28 -0
  72. package/src/components/FilePreview.tsx +23 -0
  73. package/src/components/FileSection.tsx +20 -0
  74. package/src/components/FileSlide.tsx +20 -0
  75. package/src/components/index.ts +11 -0
  76. package/src/extensions/image.stories.tsx +142 -0
  77. package/src/extensions/image.ts +244 -0
  78. package/src/extensions/index.ts +5 -0
  79. package/src/index.ts +9 -0
  80. package/src/load.ts +63 -0
  81. package/src/meta.tsx +15 -0
  82. package/src/translations.ts +17 -0
  83. package/src/types/file.ts +13 -0
  84. package/src/types/index.ts +6 -0
  85. package/src/types/types.ts +27 -0
  86. package/src/upload.ts +46 -0
  87. package/src/wnfs-url.ts +7 -0
@@ -0,0 +1,244 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import { type EditorState, type Extension, StateField, type Range, StateEffect } from '@codemirror/state';
7
+ import { Decoration, EditorView, WidgetType, ViewPlugin, type ViewUpdate } from '@codemirror/view';
8
+ import type { Blockstore } from 'interface-blockstore';
9
+ import type { PrivateDirectory, PrivateForest } from 'wnfs';
10
+
11
+ import { log } from '@dxos/log';
12
+ import { type Space } from '@dxos/react-client/echo';
13
+
14
+ import { store } from '../common';
15
+ import { loadWnfs } from '../load';
16
+
17
+ export type ImageOptions = {
18
+ blockstore: Blockstore;
19
+ space: Space;
20
+ };
21
+
22
+ export const image = (options: ImageOptions): Extension[] => {
23
+ return [stateField(options), viewPlugin(options)];
24
+ };
25
+
26
+ // GLOBAL STATE
27
+
28
+ /** Indexed by space id */
29
+ const loadedWnfsInstances: Record<string, { directory: PrivateDirectory; forest: PrivateForest }> = {};
30
+
31
+ // EFFECT + EXTENSIONS
32
+
33
+ const stateEffect = StateEffect.define<{
34
+ decorations: Range<Decoration>[];
35
+ from: number;
36
+ to: number;
37
+ }>({});
38
+
39
+ const stateField = (options: ImageOptions) =>
40
+ StateField.define({
41
+ create: () => {
42
+ return Decoration.none;
43
+ },
44
+ update: (value, tr) => {
45
+ for (const effect of tr.effects) {
46
+ if (effect.is(stateEffect)) {
47
+ return value.map(tr.changes).update({
48
+ filterFrom: effect.value.from,
49
+ filterTo: effect.value.to,
50
+ filter: () => false,
51
+ add: effect.value.decorations,
52
+ });
53
+ }
54
+ }
55
+
56
+ return value;
57
+ },
58
+ provide: (f) => {
59
+ return EditorView.decorations.from(f);
60
+ },
61
+ });
62
+
63
+ const viewPlugin = (options: ImageOptions) =>
64
+ ViewPlugin.define((view: EditorView) => {
65
+ // Initial decoration set on document load
66
+ buildDecorations({
67
+ from: 0,
68
+ to: view.state.doc.length,
69
+ state: view.state,
70
+ options,
71
+ })
72
+ .then((decorations) => {
73
+ view.dispatch({
74
+ effects: stateEffect.of({
75
+ decorations,
76
+ from: 0,
77
+ to: view.state.doc.length,
78
+ }),
79
+ });
80
+ })
81
+ .catch((err) => {
82
+ throw new Error(err);
83
+ });
84
+
85
+ const update = (update: ViewUpdate) => {
86
+ if (!update.docChanged) {
87
+ return;
88
+ }
89
+
90
+ const { from, to } = changedRange(update);
91
+ buildDecorations({
92
+ from,
93
+ to,
94
+ state: view.state,
95
+ options,
96
+ })
97
+ .then((decorations) => {
98
+ view.dispatch({
99
+ effects: stateEffect.of({
100
+ decorations,
101
+ from,
102
+ to,
103
+ }),
104
+ });
105
+ })
106
+ .catch((err) => {
107
+ log.catch(err);
108
+ });
109
+ };
110
+
111
+ return { update };
112
+ });
113
+
114
+ // 🛠️
115
+
116
+ const changedRange = (update: ViewUpdate) => {
117
+ // Find range of changes and cursor changes.
118
+ const cursor = update.state.selection.main.head;
119
+ const oldCursor = update.changes.mapPos(update.startState.selection.main.head);
120
+
121
+ let from = Math.min(cursor, oldCursor);
122
+ let to = Math.max(cursor, oldCursor);
123
+
124
+ update.changes.iterChangedRanges((fromA, toA, fromB, toB) => {
125
+ from = Math.min(from, fromB);
126
+ to = Math.max(to, toB);
127
+ });
128
+
129
+ // Expand to cover lines.
130
+ from = update.state.doc.lineAt(from).from;
131
+ to = update.state.doc.lineAt(to).to;
132
+
133
+ // Fin
134
+ return { from, to };
135
+ };
136
+
137
+ const buildDecorations = async ({
138
+ from,
139
+ to,
140
+ state,
141
+ options,
142
+ }: {
143
+ from: number;
144
+ to: number;
145
+ state: EditorState;
146
+ options: ImageOptions;
147
+ }) => {
148
+ const cursor = state.selection.main.head;
149
+ let nodes: { from: number; hide: boolean; to: number; url: string }[] = [];
150
+
151
+ syntaxTree(state).iterate({
152
+ enter: (node) => {
153
+ if (node.name === 'Image') {
154
+ const urlNode = node.node.getChild('URL');
155
+ if (urlNode) {
156
+ const hide = state.readOnly || cursor < node.from || cursor > node.to;
157
+ const url = state.sliceDoc(urlNode.from, urlNode.to);
158
+
159
+ if (url.startsWith('wnfs://')) {
160
+ nodes.push({
161
+ from: node.from,
162
+ to: node.to,
163
+ hide,
164
+ url,
165
+ });
166
+ }
167
+ }
168
+ }
169
+ },
170
+ from,
171
+ to,
172
+ });
173
+
174
+ // Nodes must be sorted by `from`
175
+ nodes = nodes.sort((a, b) => {
176
+ return a.from - b.from;
177
+ });
178
+
179
+ const decorations = await nodes.reduce(
180
+ async (acc, node) => {
181
+ const array = await acc;
182
+ const path = node.url
183
+ .replace(/^wnfs:\/\//, '')
184
+ .split('/')
185
+ .map((p) => decodeURIComponent(p));
186
+
187
+ // Cannot load images from other spaces
188
+ const spaceIdFromUrl = path[1];
189
+ if (spaceIdFromUrl !== options.space.properties.id) {
190
+ return array;
191
+ }
192
+
193
+ const cacheKey = options.space.properties.wnfs_private_forest_cid;
194
+
195
+ const { directory, forest } = loadedWnfsInstances[cacheKey]
196
+ ? loadedWnfsInstances[cacheKey]
197
+ : await loadWnfs(options.space, options.blockstore);
198
+
199
+ loadedWnfsInstances[cacheKey] = {
200
+ directory,
201
+ forest,
202
+ };
203
+
204
+ const wnfsStore = store(options.blockstore);
205
+
206
+ // Load image contents into memory blob
207
+ const { result } = await directory.read(path, true, forest, wnfsStore);
208
+ const blob = new Blob([result]);
209
+ const blobUrl = URL.createObjectURL(blob);
210
+
211
+ // Create decoration
212
+ return [
213
+ ...array,
214
+ Decoration.replace({
215
+ block: true, // Prevent cursor from entering.
216
+ widget: new WnfsImageWidget(blobUrl),
217
+ }).range(node.hide ? node.from : node.to, node.to),
218
+ ];
219
+ },
220
+ Promise.resolve([] as Range<Decoration>[]),
221
+ );
222
+
223
+ return decorations;
224
+ };
225
+
226
+ class WnfsImageWidget extends WidgetType {
227
+ constructor(readonly _url: string) {
228
+ super();
229
+ }
230
+
231
+ override eq(other: this) {
232
+ return this._url === (other as any as WnfsImageWidget)._url;
233
+ }
234
+
235
+ override toDOM(view: EditorView) {
236
+ const img = document.createElement('img');
237
+ img.setAttribute('loading', 'lazy');
238
+ img.setAttribute('src', this._url);
239
+ img.setAttribute('class', 'cm-image');
240
+ // Images are hidden until successfully loaded to avoid flickering effects.
241
+ img.onload = () => img.classList.add('cm-loaded-image');
242
+ return img;
243
+ }
244
+ }
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './image';
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { WnfsPlugin } from './WnfsPlugin';
6
+
7
+ export default WnfsPlugin;
8
+
9
+ export * from './WnfsPlugin';
package/src/load.ts ADDED
@@ -0,0 +1,63 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import type { Blockstore } from 'interface-blockstore';
6
+ import { CID } from 'multiformats';
7
+ import * as Uint8Arrays from 'uint8arrays';
8
+ import { AccessKey, PrivateForest, PrivateDirectory, PrivateNode } from 'wnfs';
9
+
10
+ import { type Space } from '@dxos/react-client/echo';
11
+
12
+ import { Rng, store } from './common';
13
+
14
+ //
15
+ // LOAD
16
+ //
17
+
18
+ export const loadWnfs = async (space: Space, blockstore: Blockstore) => {
19
+ const exists =
20
+ space.properties.wnfs_access_key !== undefined && space.properties.wnfs_private_forest_cid !== undefined;
21
+
22
+ return exists ? await loadWnfsDir(blockstore, space) : await createWnfsDir(blockstore, space);
23
+ };
24
+
25
+ //
26
+ // PRIVATE
27
+ //
28
+
29
+ const createWnfsDir = async (blockstore: Blockstore, space: Space) => {
30
+ const wnfsStore = store(blockstore);
31
+ const rng = new Rng();
32
+ const forest = new PrivateForest(rng);
33
+
34
+ const dirResult = await PrivateDirectory.newAndStore(forest.emptyName(), new Date(), forest, wnfsStore, rng);
35
+ const dir = dirResult.rootDir as PrivateDirectory;
36
+
37
+ const [accessKeyRaw, newForest]: [AccessKey, PrivateForest] = await dir
38
+ .asNode()
39
+ .store(dirResult.forest as PrivateForest, wnfsStore, rng);
40
+
41
+ const cidBytes = await newForest.store(wnfsStore);
42
+
43
+ space.properties.wnfs_access_key = Uint8Arrays.toString(accessKeyRaw.toBytes(), 'base64');
44
+ space.properties.wnfs_private_forest_cid = CID.decode(cidBytes).toString();
45
+
46
+ return {
47
+ directory: dir,
48
+ forest: newForest,
49
+ };
50
+ };
51
+
52
+ const loadWnfsDir = async (blockstore: Blockstore, space: Space) => {
53
+ const wnfsStore = store(blockstore);
54
+
55
+ const accessKey = AccessKey.fromBytes(Uint8Arrays.fromString(space.properties.wnfs_access_key, 'base64'));
56
+ const forest = await PrivateForest.load(CID.parse(space.properties.wnfs_private_forest_cid).bytes, wnfsStore);
57
+ const node: PrivateNode = await PrivateNode.load(accessKey, forest, wnfsStore);
58
+
59
+ return {
60
+ directory: node.asDir(),
61
+ forest,
62
+ };
63
+ };
package/src/meta.tsx ADDED
@@ -0,0 +1,15 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type PluginMeta } from '@dxos/app-framework';
6
+
7
+ export const WNFS_PLUGIN = 'dxos.org/plugin/WNFS';
8
+
9
+ export default {
10
+ id: WNFS_PLUGIN,
11
+ name: 'WNFS',
12
+ description: 'Upload & view files with WNFS.',
13
+ tags: ['experimental'],
14
+ icon: 'ph--file-cloud--regular',
15
+ } satisfies PluginMeta;
@@ -0,0 +1,17 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { WNFS_PLUGIN } from './meta';
6
+
7
+ export default [
8
+ {
9
+ 'en-US': {
10
+ [WNFS_PLUGIN]: {
11
+ 'plugin name': 'Files',
12
+ 'file title placeholder': 'New file',
13
+ 'delete object label': 'Delete',
14
+ },
15
+ },
16
+ },
17
+ ];
@@ -0,0 +1,13 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { S, TypedObject } from '@dxos/echo-schema';
6
+
7
+ export class FileType extends TypedObject({ typename: 'dxos.org/type/File', version: '0.1.0' })({
8
+ filename: S.String,
9
+ type: S.String,
10
+ timestamp: S.optional(S.String),
11
+ name: S.optional(S.String),
12
+ cid: S.optional(S.String),
13
+ }) {}
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './file';
6
+ export * from './types';
@@ -0,0 +1,27 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import {
6
+ type FileManagerProvides,
7
+ type MetadataRecordsProvides,
8
+ type SurfaceProvides,
9
+ type TranslationsProvides,
10
+ } from '@dxos/app-framework';
11
+ import { type SchemaProvides } from '@dxos/plugin-client';
12
+ import { type MarkdownExtensionProvides } from '@dxos/plugin-markdown/types';
13
+
14
+ import { WNFS_PLUGIN } from '../meta';
15
+
16
+ const WNFS_ACTION = `${WNFS_PLUGIN}/action`;
17
+
18
+ export enum WnfsAction {
19
+ CREATE = `${WNFS_ACTION}/create`,
20
+ }
21
+
22
+ export type WnfsPluginProvides = FileManagerProvides &
23
+ SurfaceProvides &
24
+ MetadataRecordsProvides &
25
+ TranslationsProvides &
26
+ SchemaProvides &
27
+ MarkdownExtensionProvides;
package/src/upload.ts ADDED
@@ -0,0 +1,46 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import type { Blockstore } from 'interface-blockstore';
6
+ import { CID } from 'multiformats';
7
+ import * as Raw from 'multiformats/codecs/raw';
8
+ import { sha256 } from 'multiformats/hashes/sha2';
9
+
10
+ import type { Space } from '@dxos/client/echo';
11
+ import { log } from '@dxos/log';
12
+
13
+ import { Rng, filePath, store } from './common';
14
+ import { loadWnfs } from './load';
15
+ import { wnfsUrl } from './wnfs-url';
16
+
17
+ export const upload = async ({ blockstore, file, space }: { blockstore: Blockstore; file: File; space: Space }) => {
18
+ const { directory, forest } = await loadWnfs(space, blockstore);
19
+ const wnfsStore = store(blockstore);
20
+
21
+ const bytes = new Uint8Array(await file.arrayBuffer());
22
+ const hash = await sha256.digest(bytes);
23
+ const cid = CID.create(1, Raw.code, hash);
24
+
25
+ const path = filePath(cid.toString(), space);
26
+
27
+ // Given the root directory of the private file system,
28
+ // write the file contents to `path` in said directory.
29
+ const result = await directory.write(path, true, bytes, new Date(), forest, wnfsStore, new Rng());
30
+
31
+ // Given the new immutable instance of the root directory after writing to it,
32
+ // **store** the changes in the "dark forest" and the associated (block) store.
33
+ const [_, updatedForest] = await result.rootDir.store(result.forest, wnfsStore, new Rng());
34
+ const cidBytes = await updatedForest.store(wnfsStore);
35
+
36
+ // Update the forest pointer on the associated space.
37
+ space.properties.wnfs_private_forest_cid = CID.decode(cidBytes).toString();
38
+
39
+ // Generate `wnfs://` URL & return the info.
40
+ const info = {
41
+ url: wnfsUrl(path),
42
+ };
43
+
44
+ log('upload', { file, info });
45
+ return info;
46
+ };
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export const wnfsUrl = (filePath: string[]) => {
6
+ return 'wnfs://' + filePath.map((f) => encodeURIComponent(f)).join('/');
7
+ };