@dxos/plugin-wnfs 0.7.5-main.9d26e3a → 0.7.5-main.b19bfc8

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 (139) hide show
  1. package/dist/lib/browser/FileContainer-JP3ZCHVO.mjs +94 -0
  2. package/dist/lib/browser/FileContainer-JP3ZCHVO.mjs.map +7 -0
  3. package/dist/lib/browser/blockstore-55OY65BY.mjs +191 -0
  4. package/dist/lib/browser/blockstore-55OY65BY.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-3ERFJ7W2.mjs +16 -0
  6. package/dist/lib/browser/chunk-3ERFJ7W2.mjs.map +7 -0
  7. package/dist/lib/browser/chunk-EELFT4QP.mjs +15 -0
  8. package/dist/lib/browser/chunk-EELFT4QP.mjs.map +7 -0
  9. package/dist/lib/browser/chunk-KFKLBRAQ.mjs +16 -0
  10. package/dist/lib/browser/chunk-KFKLBRAQ.mjs.map +7 -0
  11. package/dist/lib/browser/chunk-RYOWC7TX.mjs +60 -0
  12. package/dist/lib/browser/chunk-RYOWC7TX.mjs.map +7 -0
  13. package/dist/lib/browser/chunk-SCQWJNHD.mjs +157 -0
  14. package/dist/lib/browser/chunk-SCQWJNHD.mjs.map +7 -0
  15. package/dist/lib/browser/file-uploader-3DF5T3EZ.mjs +29 -0
  16. package/dist/lib/browser/file-uploader-3DF5T3EZ.mjs.map +7 -0
  17. package/dist/lib/browser/index.mjs +99 -486
  18. package/dist/lib/browser/index.mjs.map +4 -4
  19. package/dist/lib/browser/intent-resolver-6IX7MROZ.mjs +49 -0
  20. package/dist/lib/browser/intent-resolver-6IX7MROZ.mjs.map +7 -0
  21. package/dist/lib/browser/markdown-MGMT4KWF.mjs +198 -0
  22. package/dist/lib/browser/markdown-MGMT4KWF.mjs.map +7 -0
  23. package/dist/lib/browser/meta.json +1 -1
  24. package/dist/lib/browser/react-surface-DPJ6WGOE.mjs +81 -0
  25. package/dist/lib/browser/react-surface-DPJ6WGOE.mjs.map +7 -0
  26. package/dist/lib/browser/types/index.mjs +2 -2
  27. package/dist/types/src/WnfsPlugin.d.ts +1 -3
  28. package/dist/types/src/WnfsPlugin.d.ts.map +1 -1
  29. package/dist/types/src/blockstore.d.ts +4 -1
  30. package/dist/types/src/blockstore.d.ts.map +1 -1
  31. package/dist/types/src/capabilities/blockstore.d.ts +4 -0
  32. package/dist/types/src/capabilities/blockstore.d.ts.map +1 -0
  33. package/dist/types/src/capabilities/capabilities.d.ts +12 -0
  34. package/dist/types/src/capabilities/capabilities.d.ts.map +1 -0
  35. package/dist/types/src/capabilities/file-uploader.d.ts +4 -0
  36. package/dist/types/src/capabilities/file-uploader.d.ts.map +1 -0
  37. package/dist/types/src/capabilities/index.d.ts +7 -0
  38. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  39. package/dist/types/src/capabilities/intent-resolver.d.ts +4 -0
  40. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -0
  41. package/dist/types/src/capabilities/markdown.d.ts +4 -0
  42. package/dist/types/src/capabilities/markdown.d.ts.map +1 -0
  43. package/dist/types/src/capabilities/react-surface.d.ts +4 -0
  44. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  45. package/dist/types/src/components/FileContainer.d.ts +7 -0
  46. package/dist/types/src/components/FileContainer.d.ts.map +1 -0
  47. package/dist/types/src/components/FileInput.d.ts +7 -0
  48. package/dist/types/src/components/FileInput.d.ts.map +1 -0
  49. package/dist/types/src/components/FilePreview.d.ts +1 -2
  50. package/dist/types/src/components/FilePreview.d.ts.map +1 -1
  51. package/dist/types/src/components/index.d.ts +4 -11
  52. package/dist/types/src/components/index.d.ts.map +1 -1
  53. package/dist/types/src/extensions/image.d.ts +5 -0
  54. package/dist/types/src/extensions/image.d.ts.map +1 -1
  55. package/dist/types/src/extensions/image.stories.d.ts +2 -3
  56. package/dist/types/src/extensions/image.stories.d.ts.map +1 -1
  57. package/dist/types/src/helpers/common.d.ts.map +1 -0
  58. package/dist/types/src/helpers/index.d.ts +5 -0
  59. package/dist/types/src/helpers/index.d.ts.map +1 -0
  60. package/dist/types/src/helpers/load.d.ts +13 -0
  61. package/dist/types/src/helpers/load.d.ts.map +1 -0
  62. package/dist/types/src/helpers/upload.d.ts +15 -0
  63. package/dist/types/src/helpers/upload.d.ts.map +1 -0
  64. package/dist/types/src/helpers/wnfs-url.d.ts +11 -0
  65. package/dist/types/src/helpers/wnfs-url.d.ts.map +1 -0
  66. package/dist/types/src/index.d.ts +2 -2
  67. package/dist/types/src/index.d.ts.map +1 -1
  68. package/dist/types/src/meta.d.ts +2 -4
  69. package/dist/types/src/meta.d.ts.map +1 -1
  70. package/dist/types/src/translations.d.ts +15 -1
  71. package/dist/types/src/translations.d.ts.map +1 -1
  72. package/dist/types/src/types/file.d.ts +5 -7
  73. package/dist/types/src/types/file.d.ts.map +1 -1
  74. package/dist/types/src/types/types.d.ts +49 -6
  75. package/dist/types/src/types/types.d.ts.map +1 -1
  76. package/dist/types/tsconfig.tsbuildinfo +1 -1
  77. package/package.json +29 -26
  78. package/src/WnfsPlugin.tsx +83 -85
  79. package/src/blockstore.ts +143 -21
  80. package/src/capabilities/blockstore.ts +18 -0
  81. package/src/capabilities/capabilities.ts +18 -0
  82. package/src/capabilities/file-uploader.ts +24 -0
  83. package/src/capabilities/index.ts +13 -0
  84. package/src/capabilities/intent-resolver.ts +32 -0
  85. package/src/capabilities/markdown.ts +25 -0
  86. package/src/capabilities/react-surface.tsx +41 -0
  87. package/src/components/FileContainer.tsx +46 -0
  88. package/src/components/FileInput.tsx +46 -0
  89. package/src/components/index.ts +3 -5
  90. package/src/extensions/image.stories.tsx +26 -68
  91. package/src/extensions/image.tsx +203 -0
  92. package/src/{common.ts → helpers/common.ts} +1 -1
  93. package/src/helpers/index.ts +8 -0
  94. package/src/{load.ts → helpers/load.ts} +38 -9
  95. package/src/{upload.ts → helpers/upload.ts} +18 -4
  96. package/src/helpers/wnfs-url.ts +36 -0
  97. package/src/index.ts +2 -4
  98. package/src/meta.ts +2 -3
  99. package/src/translations.ts +5 -0
  100. package/src/types/file.ts +2 -3
  101. package/src/types/types.ts +30 -19
  102. package/dist/lib/browser/FileCard-HJIZDWJF.mjs +0 -39
  103. package/dist/lib/browser/FileCard-HJIZDWJF.mjs.map +0 -7
  104. package/dist/lib/browser/FileMain-WEWTX7QB.mjs +0 -27
  105. package/dist/lib/browser/FileMain-WEWTX7QB.mjs.map +0 -7
  106. package/dist/lib/browser/FileSection-BEUGJ7EI.mjs +0 -24
  107. package/dist/lib/browser/FileSection-BEUGJ7EI.mjs.map +0 -7
  108. package/dist/lib/browser/FileSlide-NYBGZEZS.mjs +0 -21
  109. package/dist/lib/browser/FileSlide-NYBGZEZS.mjs.map +0 -7
  110. package/dist/lib/browser/chunk-2I63TM7M.mjs +0 -27
  111. package/dist/lib/browser/chunk-2I63TM7M.mjs.map +0 -7
  112. package/dist/lib/browser/chunk-JDSKIDDR.mjs +0 -30
  113. package/dist/lib/browser/chunk-JDSKIDDR.mjs.map +0 -7
  114. package/dist/lib/browser/chunk-VL3H7P4F.mjs +0 -18
  115. package/dist/lib/browser/chunk-VL3H7P4F.mjs.map +0 -7
  116. package/dist/lib/browser/meta.mjs +0 -9
  117. package/dist/lib/browser/meta.mjs.map +0 -7
  118. package/dist/types/src/common.d.ts.map +0 -1
  119. package/dist/types/src/components/FileCard.d.ts +0 -9
  120. package/dist/types/src/components/FileCard.d.ts.map +0 -1
  121. package/dist/types/src/components/FileMain.d.ts +0 -7
  122. package/dist/types/src/components/FileMain.d.ts.map +0 -1
  123. package/dist/types/src/components/FileSection.d.ts +0 -8
  124. package/dist/types/src/components/FileSection.d.ts.map +0 -1
  125. package/dist/types/src/components/FileSlide.d.ts +0 -8
  126. package/dist/types/src/components/FileSlide.d.ts.map +0 -1
  127. package/dist/types/src/load.d.ts +0 -8
  128. package/dist/types/src/load.d.ts.map +0 -1
  129. package/dist/types/src/upload.d.ts +0 -10
  130. package/dist/types/src/upload.d.ts.map +0 -1
  131. package/dist/types/src/wnfs-url.d.ts +0 -2
  132. package/dist/types/src/wnfs-url.d.ts.map +0 -1
  133. package/src/components/FileCard.tsx +0 -45
  134. package/src/components/FileMain.tsx +0 -28
  135. package/src/components/FileSection.tsx +0 -20
  136. package/src/components/FileSlide.tsx +0 -20
  137. package/src/extensions/image.ts +0 -244
  138. package/src/wnfs-url.ts +0 -7
  139. /package/dist/types/src/{common.d.ts → helpers/common.d.ts} +0 -0
@@ -0,0 +1,41 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback } from 'react';
6
+
7
+ import { contributes, Capabilities, createSurface } from '@dxos/app-framework';
8
+ import { type S } from '@dxos/echo-schema';
9
+ import { findAnnotation } from '@dxos/effect';
10
+ import { type InputProps } from '@dxos/react-ui-form';
11
+
12
+ import { FileContainer, FileInput } from '../components';
13
+ import { WNFS_PLUGIN } from '../meta';
14
+ import { FileType, WnfsAction } from '../types';
15
+
16
+ export default () =>
17
+ contributes(Capabilities.ReactSurface, [
18
+ createSurface({
19
+ id: `${WNFS_PLUGIN}/article`,
20
+ role: ['article', 'section', 'slide'],
21
+ filter: (data): data is { subject: FileType } => data.subject instanceof FileType,
22
+ component: ({ data, role }) => <FileContainer role={role} file={data.subject} />,
23
+ }),
24
+ createSurface({
25
+ id: `${WNFS_PLUGIN}/create-form`,
26
+ role: 'form-input',
27
+ filter: (data): data is { prop: string; schema: S.Schema.Any } => {
28
+ const annotation = findAnnotation<boolean>((data.schema as S.Schema.All).ast, WnfsAction.UploadAnnotationId);
29
+ return !!annotation;
30
+ },
31
+ component: ({ data: { prop, schema }, ...props }) => {
32
+ const inputProps = props as unknown as InputProps;
33
+ const handleChange = useCallback(
34
+ (file: File) => inputProps.onValueChange?.('object', file),
35
+ [prop, inputProps.onValueChange],
36
+ );
37
+
38
+ return <FileInput schema={schema} onChange={handleChange} />;
39
+ },
40
+ }),
41
+ ]);
@@ -0,0 +1,46 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import React, { useEffect, useState } from 'react';
6
+
7
+ import { useCapability } from '@dxos/app-framework';
8
+ import { invariant } from '@dxos/invariant';
9
+ import { getSpace } from '@dxos/react-client/echo';
10
+ import { StackItem } from '@dxos/react-ui-stack';
11
+
12
+ import { FilePreview } from './FilePreview';
13
+ import { WnfsCapabilities } from '../capabilities';
14
+ import { filePath, getBlobUrl, loadWnfs, wnfsUrl } from '../helpers';
15
+ import { type FileType } from '../types';
16
+
17
+ const FileContainer = ({ file, role }: { file: FileType; role: string }) => {
18
+ const blockstore = useCapability(WnfsCapabilities.Blockstore);
19
+ const instances = useCapability(WnfsCapabilities.Instances);
20
+ const [blobUrl, setBlobUrl] = useState<string>();
21
+
22
+ useEffect(() => {
23
+ const timeout = setTimeout(async () => {
24
+ const space = getSpace(file);
25
+ invariant(space);
26
+ const { directory, forest } = await loadWnfs({ blockstore, instances, space });
27
+ const path = filePath(file.cid.toString(), space);
28
+ const url = await getBlobUrl({ wnfsUrl: wnfsUrl(path), blockstore, directory, forest });
29
+ setBlobUrl(url);
30
+ });
31
+
32
+ return () => clearTimeout(timeout);
33
+ }, [file]);
34
+
35
+ if (!blobUrl) {
36
+ return null;
37
+ }
38
+
39
+ return (
40
+ <StackItem.Content role={role} toolbar={false}>
41
+ <FilePreview type={file.type} url={blobUrl} />
42
+ </StackItem.Content>
43
+ );
44
+ };
45
+
46
+ export default FileContainer;
@@ -0,0 +1,46 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback } from 'react';
6
+ import { useDropzone } from 'react-dropzone';
7
+
8
+ import { findAnnotation, type S } from '@dxos/effect';
9
+ import { useTranslation } from '@dxos/react-ui';
10
+ import { mx } from '@dxos/react-ui-theme';
11
+
12
+ import { WNFS_PLUGIN } from '../meta';
13
+ import { WnfsAction } from '../types';
14
+
15
+ export type FileInputProps = {
16
+ schema: S.Schema.Any;
17
+ onChange: (file: File) => void;
18
+ };
19
+
20
+ export const FileInput = ({ schema, onChange }: FileInputProps) => {
21
+ const { t } = useTranslation(WNFS_PLUGIN);
22
+ const accept = findAnnotation<Record<string, string[]>>(schema.ast, WnfsAction.UploadAnnotationId);
23
+
24
+ const onDropAccepted = useCallback((files: File[]) => onChange?.(files[0]), [onChange]);
25
+
26
+ const { acceptedFiles, getRootProps, getInputProps, isFocused, isDragAccept, isDragReject } = useDropzone({
27
+ multiple: false,
28
+ accept,
29
+ onDropAccepted,
30
+ });
31
+
32
+ return (
33
+ <div
34
+ {...getRootProps()}
35
+ className={mx(
36
+ 'flex flex-col items-center p-8 border border-separator',
37
+ isFocused && 'focus-ring',
38
+ isDragAccept && 'bg-attention',
39
+ isDragReject && 'border-error',
40
+ )}
41
+ >
42
+ <input {...getInputProps()} />
43
+ {acceptedFiles[0] ? <p>{acceptedFiles[0].name}</p> : <p>{t('file input placeholder')}</p>}
44
+ </div>
45
+ );
46
+ };
@@ -4,8 +4,6 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- // Lazily load components for content surfaces.
8
- export const FileCard = React.lazy(() => import('./FileCard'));
9
- export const FileMain = React.lazy(() => import('./FileMain'));
10
- export const FileSection = React.lazy(() => import('./FileSection'));
11
- export const FileSlide = React.lazy(() => import('./FileSlide'));
7
+ export const FileContainer = React.lazy(() => import('./FileContainer'));
8
+
9
+ export * from './FileInput';
@@ -7,6 +7,7 @@ import '@dxos-theme';
7
7
  import type { Blockstore } from 'interface-blockstore';
8
8
  import React, { useEffect, useState } from 'react';
9
9
 
10
+ import { faker } from '@dxos/random';
10
11
  import { useSpace } from '@dxos/react-client/echo';
11
12
  import { withClientProvider } from '@dxos/react-client/testing';
12
13
  import { useThemeContext } from '@dxos/react-ui';
@@ -15,26 +16,27 @@ import {
15
16
  createMarkdownExtensions,
16
17
  createThemeExtensions,
17
18
  decorateMarkdown,
19
+ processEditorPayload,
18
20
  useTextEditor,
19
21
  } from '@dxos/react-ui-editor';
20
22
  import { withTheme, withLayout } from '@dxos/storybook-utils';
21
23
 
22
24
  import { image } from './image';
23
25
  import { create as createBlockstore } from '../blockstore';
24
- import { filePath, store } from '../common';
25
- import { loadWnfs } from '../load';
26
- import { upload } from '../upload';
26
+ import { upload } from '../helpers';
27
+
28
+ faker.seed(1);
29
+ const initialValue = faker.lorem.paragraphs(100);
30
+ const instances = {};
27
31
 
28
32
  const Story = () => {
33
+ const { themeMode } = useThemeContext();
29
34
  const space = useSpace();
30
-
31
35
  const [blockstore, setBlockstore] = useState<Blockstore>();
32
- const [amountOfUploads, setAmountOfUploads] = useState<number>(0);
33
- const [images, setImages] = useState<{ url: string }[]>([]);
34
36
 
35
37
  useEffect(() => {
36
38
  if (space) {
37
- console.log('SPACE ID', space.properties.id);
39
+ console.log('SPACE ID', space.id);
38
40
  }
39
41
 
40
42
  const create = async () => {
@@ -47,43 +49,29 @@ const Story = () => {
47
49
  create().catch(console.error);
48
50
  }, [space]);
49
51
 
50
- useEffect(() => {
51
- if (!space || !blockstore) {
52
- return;
53
- }
54
-
55
- (async () => {
56
- const { directory, forest } = await loadWnfs(space, blockstore);
57
- const parentPath = ['spaces', space.properties.id, 'files'];
58
- const parentNode = await directory.getNode(parentPath, true, forest, store(blockstore));
59
- if (!parentNode) {
60
- return;
61
- }
62
-
63
- const { result } = await directory.ls(parentPath, true, forest, store(blockstore));
64
-
65
- const images = await Promise.all(
66
- result.map(async (r: any) => {
67
- const path = filePath(r.name, space);
68
- const reading = await directory.read(path, true, forest, store(blockstore));
69
- const bytes = reading.result;
70
- const blob = new Blob([bytes]);
71
- const url = URL.createObjectURL(blob);
72
- return { url };
73
- }),
74
- );
75
-
76
- setImages(images);
77
- })().catch(console.error);
78
- }, [space, blockstore, amountOfUploads]);
52
+ const { parentRef, view, focusAttributes } = useTextEditor(
53
+ () => ({
54
+ initialValue,
55
+ extensions: [
56
+ createBasicExtensions(),
57
+ createMarkdownExtensions({ themeMode }),
58
+ createThemeExtensions({ themeMode, syntaxHighlighting: true }),
59
+ blockstore && space ? [image({ blockstore, instances, space })] : [],
60
+ decorateMarkdown(),
61
+ ],
62
+ }),
63
+ [themeMode, blockstore, space],
64
+ );
79
65
 
80
66
  const onFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
81
67
  const file = event.target.files?.[0];
82
68
  if (file) {
83
69
  if (blockstore) {
84
70
  if (space) {
85
- await upload({ blockstore, file, space });
86
- setAmountOfUploads(amountOfUploads + 1);
71
+ const info = await upload({ blockstore, file, space });
72
+ if (info && view) {
73
+ processEditorPayload(view, { type: 'image', data: info.url });
74
+ }
87
75
  } else {
88
76
  console.warn('Space is undefined.');
89
77
  }
@@ -93,42 +81,12 @@ const Story = () => {
93
81
  }
94
82
  };
95
83
 
96
- const { themeMode } = useThemeContext();
97
- const { parentRef, focusAttributes } = useTextEditor(
98
- () => ({
99
- initialValue: '',
100
- extensions: [
101
- createBasicExtensions(),
102
- createMarkdownExtensions({ themeMode }),
103
- createThemeExtensions({ themeMode, syntaxHighlighting: true }),
104
- blockstore && space ? [image({ blockstore, space })] : [],
105
- decorateMarkdown(),
106
- ],
107
- }),
108
- [themeMode],
109
- );
110
-
111
- const renderedImages = images.map((image, index) => (
112
- <div key={index} className='mt-1'>
113
- <img src={image.url} width='300' />
114
- </div>
115
- ));
116
-
117
84
  return (
118
85
  <>
119
86
  <div className='mb-2'>
120
87
  <input type='file' accept='image/*' onChange={onFileChange} />
121
88
  </div>
122
89
  <div className='w-[50rem]' ref={parentRef} {...focusAttributes} />
123
- <div>
124
- {images.length ? (
125
- renderedImages
126
- ) : (
127
- <p>
128
- <strong className='text-green-600'>Select an image file to add to WNFS and render it.</strong>
129
- </p>
130
- )}
131
- </div>
132
90
  </>
133
91
  );
134
92
  };
@@ -0,0 +1,203 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import { type EditorState, type Extension, StateField, type Range } from '@codemirror/state';
7
+ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
8
+ import type { Blockstore } from 'interface-blockstore';
9
+ import React from 'react';
10
+ import { createRoot } from 'react-dom/client';
11
+
12
+ import { type Space } from '@dxos/react-client/echo';
13
+ import { Status, ThemeProvider } from '@dxos/react-ui';
14
+ import { focusField } from '@dxos/react-ui-editor';
15
+ import { defaultTx } from '@dxos/react-ui-theme';
16
+ import { type MaybePromise } from '@dxos/util';
17
+
18
+ import { type WnfsCapabilities } from '../capabilities';
19
+ import { getBlobUrl, getPathFromUrl, loadWnfs } from '../helpers';
20
+
21
+ const WAIT_UNTIL_LOADER = 1500;
22
+
23
+ export type ImageOptions = {
24
+ blockstore: Blockstore;
25
+ instances: WnfsCapabilities.Instances;
26
+ space: Space;
27
+ };
28
+
29
+ /**
30
+ * Create WNFS image decorations.
31
+ */
32
+ export const image = (options: ImageOptions): Extension[] => {
33
+ const blobUrlCache: Record<string, string> = {};
34
+ const preloaded = new Set<string>();
35
+
36
+ const preload = (url: string) => {
37
+ if (!preloaded.has(url)) {
38
+ const img = document.createElement('img');
39
+ img.src = url;
40
+ preloaded.add(url);
41
+ }
42
+ };
43
+
44
+ return [
45
+ StateField.define({
46
+ create: (state) => {
47
+ return Decoration.set(
48
+ buildDecorations({ from: 0, to: state.doc.length, state, blobUrlCache, preload, options }),
49
+ );
50
+ },
51
+ update: (value, tr) => {
52
+ if (!tr.docChanged && !tr.selection) {
53
+ return value;
54
+ }
55
+
56
+ // Find range of changes and cursor changes.
57
+ const cursor = tr.state.selection.main.head;
58
+ const oldCursor = tr.changes.mapPos(tr.startState.selection.main.head);
59
+ let from = Math.min(cursor, oldCursor);
60
+ let to = Math.max(cursor, oldCursor);
61
+ tr.changes.iterChangedRanges((fromA, toA, fromB, toB) => {
62
+ from = Math.min(from, fromB);
63
+ to = Math.max(to, toB);
64
+ });
65
+
66
+ // Expand to cover lines.
67
+ from = tr.state.doc.lineAt(from).from;
68
+ to = tr.state.doc.lineAt(to).to;
69
+
70
+ return value.map(tr.changes).update({
71
+ filterFrom: from,
72
+ filterTo: to,
73
+ filter: () => false,
74
+ add: buildDecorations({ from, to, state: tr.state, blobUrlCache, preload, options }),
75
+ });
76
+ },
77
+ provide: (field) => EditorView.decorations.from(field),
78
+ }),
79
+ ];
80
+ };
81
+
82
+ const buildDecorations = ({
83
+ from,
84
+ to,
85
+ state,
86
+ blobUrlCache,
87
+ preload,
88
+ options: { blockstore, instances, space },
89
+ }: {
90
+ from: number;
91
+ to: number;
92
+ state: EditorState;
93
+ blobUrlCache: Record<string, string>;
94
+ preload: (url: string) => void;
95
+ options: ImageOptions;
96
+ }) => {
97
+ const decorations: Range<Decoration>[] = [];
98
+ const cursor = state.selection.main.head;
99
+ syntaxTree(state).iterate({
100
+ enter: (node) => {
101
+ if (node.name !== 'Image') {
102
+ return;
103
+ }
104
+
105
+ const urlNode = node.node.getChild('URL');
106
+ if (!urlNode) {
107
+ return;
108
+ }
109
+
110
+ const hide = state.readOnly || cursor < node.from || cursor > node.to;
111
+ const wnfsUrl = state.sliceDoc(urlNode.from, urlNode.to);
112
+ if (!wnfsUrl.startsWith('wnfs://')) {
113
+ return;
114
+ }
115
+
116
+ const path = getPathFromUrl(wnfsUrl);
117
+
118
+ // Cannot load images from other spaces
119
+ const spaceId = path[1];
120
+ if (spaceId !== space.id) {
121
+ return;
122
+ }
123
+
124
+ // Load image contents into memory blob
125
+ const blobUrlPromise = (async () => {
126
+ const { directory, forest } = await loadWnfs({ blockstore, instances, space });
127
+ const url = await getBlobUrl({ wnfsUrl, blockstore, directory, forest });
128
+ blobUrlCache[path.join('/')] = url;
129
+ preload(url);
130
+ return url;
131
+ })();
132
+
133
+ decorations.push(
134
+ Decoration.replace({
135
+ block: true, // Prevent cursor from entering.
136
+ widget: new WnfsImageWidget(wnfsUrl, blobUrlCache[path.join('/')] ?? blobUrlPromise),
137
+ }).range(hide ? node.from : node.to, node.to),
138
+ );
139
+ },
140
+ from,
141
+ to,
142
+ });
143
+
144
+ return decorations;
145
+ };
146
+
147
+ const createImg = (view: EditorView, url: string) => {
148
+ const img = document.createElement('img');
149
+ img.setAttribute('src', url);
150
+ img.setAttribute('class', 'cm-image-with-loader');
151
+ // If focused, hide image until successfully loaded to avoid flickering effects.
152
+ if (view.state.field(focusField)) {
153
+ img.onload = () => img.classList.add('cm-loaded-image');
154
+ } else {
155
+ img.classList.add('cm-loaded-image');
156
+ }
157
+ return img;
158
+ };
159
+
160
+ class WnfsImageWidget extends WidgetType {
161
+ constructor(
162
+ readonly _wnfsUrl: string,
163
+ readonly _url: MaybePromise<string>,
164
+ ) {
165
+ super();
166
+ }
167
+
168
+ override eq(other: this) {
169
+ return this._wnfsUrl === (other as any as WnfsImageWidget)._wnfsUrl;
170
+ }
171
+
172
+ override toDOM(view: EditorView) {
173
+ if (typeof this._url === 'string') {
174
+ return createImg(view, this._url);
175
+ }
176
+
177
+ const widget = document.createElement('div');
178
+ widget.className = 'cm-image-wrapper';
179
+ const loader = document.createElement('div');
180
+ loader.className = 'mx-auto transition-opacity';
181
+ let loaderAdded = false;
182
+
183
+ const timeout = setTimeout(() => {
184
+ const root = createRoot(loader);
185
+ root.render(
186
+ <ThemeProvider tx={defaultTx}>
187
+ <Status indeterminate />
188
+ </ThemeProvider>,
189
+ );
190
+ widget.appendChild(loader);
191
+ loaderAdded = true;
192
+ }, WAIT_UNTIL_LOADER);
193
+
194
+ void this._url.then((url) => {
195
+ clearTimeout(timeout);
196
+ const img = createImg(view, url);
197
+ loaderAdded && widget.removeChild(loader);
198
+ widget.appendChild(img);
199
+ });
200
+
201
+ return widget;
202
+ }
203
+ }
@@ -15,7 +15,7 @@ import { type Space } from '@dxos/react-client/echo';
15
15
  //
16
16
 
17
17
  export const filePath = (fileName: string, space: Space) => {
18
- return ['spaces', space.properties.id, 'files', fileName];
18
+ return ['spaces', space.id, 'files', fileName];
19
19
  };
20
20
 
21
21
  //
@@ -0,0 +1,8 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './common';
6
+ export * from './load';
7
+ export * from './upload';
8
+ export * from './wnfs-url';
@@ -10,16 +10,40 @@ import { AccessKey, PrivateForest, PrivateDirectory, PrivateNode } from 'wnfs';
10
10
  import { type Space } from '@dxos/react-client/echo';
11
11
 
12
12
  import { Rng, store } from './common';
13
+ import { type WnfsCapabilities } from '../capabilities';
13
14
 
14
15
  //
15
16
  // LOAD
16
17
  //
17
18
 
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);
19
+ export const loadWnfs = async ({
20
+ space,
21
+ blockstore,
22
+ instances,
23
+ }: {
24
+ space: Space;
25
+ blockstore: Blockstore;
26
+ instances?: WnfsCapabilities.Instances;
27
+ }) => {
28
+ // TODO(wittjosiah): Remove.
29
+ // Delete old properties if they exist.
30
+ if (space.properties.wnfs_access_key !== undefined && space.properties.wnfs_private_forest_cid !== undefined) {
31
+ delete space.properties.wnfs_access_key;
32
+ delete space.properties.wnfs_private_forest_cid;
33
+ }
34
+
35
+ const cacheKey = space.properties.wnfs?.privateForestCid;
36
+ if (instances?.[cacheKey]) {
37
+ return instances[cacheKey];
38
+ }
39
+
40
+ const exists = !!space.properties.wnfs;
41
+ const instance = exists ? await loadWnfsDir(blockstore, space) : await createWnfsDir(blockstore, space);
42
+ if (instances) {
43
+ instances[cacheKey] = instance;
44
+ }
45
+
46
+ return instance;
23
47
  };
24
48
 
25
49
  //
@@ -40,8 +64,10 @@ const createWnfsDir = async (blockstore: Blockstore, space: Space) => {
40
64
 
41
65
  const cidBytes = await newForest.store(wnfsStore);
42
66
 
43
- space.properties.wnfs_access_key = Uint8Arrays.toString(accessKeyRaw.toBytes(), 'base64');
44
- space.properties.wnfs_private_forest_cid = CID.decode(cidBytes).toString();
67
+ space.properties.wnfs = {
68
+ accessKey: Uint8Arrays.toString(accessKeyRaw.toBytes(), 'base64'),
69
+ privateForestCid: CID.decode(cidBytes).toString(),
70
+ };
45
71
 
46
72
  return {
47
73
  directory: dir,
@@ -52,8 +78,11 @@ const createWnfsDir = async (blockstore: Blockstore, space: Space) => {
52
78
  const loadWnfsDir = async (blockstore: Blockstore, space: Space) => {
53
79
  const wnfsStore = store(blockstore);
54
80
 
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);
81
+ const accessKey = AccessKey.fromBytes(Uint8Arrays.fromString(space.properties.wnfs.accessKey, 'base64'));
82
+ const forest: PrivateForest = await PrivateForest.load(
83
+ CID.parse(space.properties.wnfs.privateForestCid).bytes,
84
+ wnfsStore,
85
+ );
57
86
  const node: PrivateNode = await PrivateNode.load(accessKey, forest, wnfsStore);
58
87
 
59
88
  return {
@@ -13,9 +13,20 @@ import { log } from '@dxos/log';
13
13
  import { Rng, filePath, store } from './common';
14
14
  import { loadWnfs } from './load';
15
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);
16
+ import { type WnfsCapabilities } from '../capabilities';
17
+
18
+ export const upload = async ({
19
+ file,
20
+ blockstore,
21
+ instances,
22
+ space,
23
+ }: {
24
+ file: File;
25
+ blockstore: Blockstore;
26
+ instances?: WnfsCapabilities.Instances;
27
+ space: Space;
28
+ }) => {
29
+ const { directory, forest } = await loadWnfs({ blockstore, instances, space });
19
30
  const wnfsStore = store(blockstore);
20
31
 
21
32
  const bytes = new Uint8Array(await file.arrayBuffer());
@@ -34,11 +45,14 @@ export const upload = async ({ blockstore, file, space }: { blockstore: Blocksto
34
45
  const cidBytes = await updatedForest.store(wnfsStore);
35
46
 
36
47
  // Update the forest pointer on the associated space.
37
- space.properties.wnfs_private_forest_cid = CID.decode(cidBytes).toString();
48
+ space.properties.wnfs.privateForestCid = CID.decode(cidBytes).toString();
38
49
 
39
50
  // Generate `wnfs://` URL & return the info.
40
51
  const info = {
52
+ name: file.name,
53
+ type: file.type,
41
54
  url: wnfsUrl(path),
55
+ cid: cid.toString(),
42
56
  };
43
57
 
44
58
  log('upload', { file, info });
@@ -0,0 +1,36 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import type { Blockstore } from 'interface-blockstore';
6
+ import { type PrivateDirectory, type PrivateForest } from 'wnfs';
7
+
8
+ import { store } from './common';
9
+
10
+ export const wnfsUrl = (filePath: string[]) => {
11
+ return 'wnfs://' + filePath.map((f) => encodeURIComponent(f)).join('/');
12
+ };
13
+
14
+ export const getPathFromUrl = (wnfsUrl: string) =>
15
+ wnfsUrl
16
+ .replace(/^wnfs:\/\//, '')
17
+ .split('/')
18
+ .map((p) => decodeURIComponent(p));
19
+
20
+ export const getBlobUrl = async ({
21
+ wnfsUrl,
22
+ blockstore,
23
+ directory,
24
+ forest,
25
+ }: {
26
+ wnfsUrl: string;
27
+ blockstore: Blockstore;
28
+ directory: PrivateDirectory;
29
+ forest: PrivateForest;
30
+ }) => {
31
+ const path = getPathFromUrl(wnfsUrl);
32
+ const wnfsStore = store(blockstore);
33
+ const { result } = await directory.read(path, true, forest, wnfsStore);
34
+ const blob = new Blob([result]);
35
+ return URL.createObjectURL(blob);
36
+ };
package/src/index.ts CHANGED
@@ -2,8 +2,6 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { WnfsPlugin } from './WnfsPlugin';
6
-
7
- export default WnfsPlugin;
8
-
5
+ export { WnfsCapabilities } from './capabilities';
6
+ export * from './meta';
9
7
  export * from './WnfsPlugin';