@comapeo/core-react 6.0.0 → 6.1.1

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.
@@ -29,6 +29,7 @@ const react_query_1 = require("@tanstack/react-query");
29
29
  const react_1 = require("react");
30
30
  const projects_js_1 = require("../lib/react-query/projects.js");
31
31
  const sync_js_1 = require("../lib/sync.js");
32
+ const urls_js_1 = require("../lib/urls.js");
32
33
  const client_js_1 = require("./client.js");
33
34
  /**
34
35
  * Retrieve the project settings for a project.
@@ -185,13 +186,14 @@ function useManyMembers({ projectId }) {
185
186
  */
186
187
  function useIconUrl({ projectId, iconId, ...mimeBasedOpts }) {
187
188
  const { data: projectApi } = useSingleProject({ projectId });
188
- const { data, error, isRefetching } = (0, react_query_1.useSuspenseQuery)((0, projects_js_1.iconUrlQueryOptions)({
189
- ...mimeBasedOpts,
190
- projectApi,
191
- projectId,
189
+ const { data: serverOrigin, error, isRefetching, } = useMediaServerOrigin({ projectApi });
190
+ const iconUrl = (0, urls_js_1.getIconUrl)({
191
+ serverOrigin,
192
192
  iconId,
193
- }));
194
- return { data, error, isRefetching };
193
+ projectId,
194
+ mimeBasedOpts,
195
+ });
196
+ return { data: iconUrl, error, isRefetching };
195
197
  }
196
198
  /**
197
199
  * Retrieve a URL that points to a desired blob resource.
@@ -246,10 +248,17 @@ function useIconUrl({ projectId, iconId, ...mimeBasedOpts }) {
246
248
  */
247
249
  function useAttachmentUrl({ projectId, blobId, }) {
248
250
  const { data: projectApi } = useSingleProject({ projectId });
249
- const { data, error, isRefetching } = (0, react_query_1.useSuspenseQuery)((0, projects_js_1.attachmentUrlQueryOptions)({
251
+ const { data: serverOrigin, error, isRefetching, } = useMediaServerOrigin({ projectApi });
252
+ const blobUrl = (0, urls_js_1.getBlobUrl)({ serverOrigin, projectId, blobId });
253
+ return { data: blobUrl, error, isRefetching };
254
+ }
255
+ /**
256
+ * @internal
257
+ * Hack to retrieve the media server origin (protocol + host).
258
+ */
259
+ function useMediaServerOrigin({ projectApi }) {
260
+ const { data, error, isRefetching } = (0, react_query_1.useSuspenseQuery)((0, projects_js_1.mediaServerOriginQueryOptions)({
250
261
  projectApi,
251
- projectId,
252
- blobId,
253
262
  }));
254
263
  return { data, error, isRefetching };
255
264
  }
@@ -1,4 +1,4 @@
1
- import type { BlobApi, IconApi } from '@comapeo/core' with { 'resolution-mode': 'import' };
1
+ import type { BlobApi } from '@comapeo/core' with { 'resolution-mode': 'import' };
2
2
  import type { MapeoClientApi, MapeoProjectApi } from '@comapeo/ipc' with { 'resolution-mode': 'import' };
3
3
  import type { ProjectSettings } from '@comapeo/schema' with { 'resolution-mode': 'import' };
4
4
  import { type QueryClient, type UnusedSkipTokenOptions } from '@tanstack/react-query';
@@ -19,27 +19,16 @@ export declare function getMemberByIdQueryKey({ projectId, deviceId, }: {
19
19
  projectId: string;
20
20
  deviceId: string;
21
21
  }): readonly ["@comapeo/core-react", "projects", string, "members", string];
22
- export declare function getIconUrlQueryKey({ projectId, iconId, ...mimeBasedOpts }: {
23
- projectId: string;
24
- iconId: string;
25
- } & (IconApi.BitmapOpts | IconApi.SvgOpts)): readonly ["@comapeo/core-react", "projects", string, "icons", string, {
26
- mimeType: Extract<import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).IconVariant["mimeType"], "image/png">;
27
- pixelDensity: Extract<import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).IconVariant, {
28
- mimeType: "image/png";
29
- }>["pixelDensity"];
30
- size: import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).ValidSizes;
31
- } | {
32
- mimeType: Extract<import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).IconVariant["mimeType"], "image/svg+xml">;
33
- size: import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).ValidSizes;
34
- }];
35
22
  export declare function getDocumentCreatedByQueryKey({ projectId, originalVersionId, }: {
36
23
  projectId: string;
37
24
  originalVersionId: string;
38
25
  }): readonly ["@comapeo/core-react", "projects", string, "document_created_by", string];
39
- export declare function getAttachmentUrlQueryKey({ projectId, blobId, }: {
40
- projectId: string;
41
- blobId: BlobApi.BlobId;
42
- }): readonly ["@comapeo/core-react", "projects", string, "attachments", import("@comapeo/core/dist/types.js", { with: { "resolution-mode": "import" } }).BlobId];
26
+ /**
27
+ * We call this within a project hook, because that's the only place the API is
28
+ * exposed right now, but it is the same for all projects, so no need for
29
+ * scoping the query key to the project
30
+ */
31
+ export declare function getMediaServerOriginQueryKey(): readonly ["@comapeo/core-react", "media_server_origin"];
43
32
  export declare function projectsQueryOptions({ clientApi, }: {
44
33
  clientApi: MapeoClientApi;
45
34
  }): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<import("@comapeo/core/dist/mapeo-manager.js", { with: { "resolution-mode": "import" } }).ListedProject[], Error, import("@comapeo/core/dist/mapeo-manager.js", { with: { "resolution-mode": "import" } }).ListedProject[], readonly ["@comapeo/core-react", "projects"]>, "queryFn"> & {
@@ -99,45 +88,6 @@ export declare function projectOwnRoleQueryOptions({ projectApi, projectId, }: {
99
88
  [dataTagErrorSymbol]: Error;
100
89
  };
101
90
  };
102
- export declare function iconUrlQueryOptions({ projectApi, projectId, iconId, ...mimeBasedOpts }: {
103
- projectApi: MapeoProjectApi;
104
- projectId: string;
105
- iconId: Parameters<MapeoProjectApi['$icons']['getIconUrl']>[0];
106
- } & (IconApi.BitmapOpts | IconApi.SvgOpts)): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<string, Error, string, readonly ["@comapeo/core-react", "projects", string, "icons", string, {
107
- mimeType: Extract<import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).IconVariant["mimeType"], "image/png">;
108
- pixelDensity: Extract<import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).IconVariant, {
109
- mimeType: "image/png";
110
- }>["pixelDensity"];
111
- size: import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).ValidSizes;
112
- } | {
113
- mimeType: Extract<import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).IconVariant["mimeType"], "image/svg+xml">;
114
- size: import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).ValidSizes;
115
- }]>, "queryFn"> & {
116
- queryFn?: import("@tanstack/react-query").QueryFunction<string, readonly ["@comapeo/core-react", "projects", string, "icons", string, {
117
- mimeType: Extract<import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).IconVariant["mimeType"], "image/png">;
118
- pixelDensity: Extract<import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).IconVariant, {
119
- mimeType: "image/png";
120
- }>["pixelDensity"];
121
- size: import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).ValidSizes;
122
- } | {
123
- mimeType: Extract<import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).IconVariant["mimeType"], "image/svg+xml">;
124
- size: import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).ValidSizes;
125
- }], never> | undefined;
126
- } & {
127
- queryKey: readonly ["@comapeo/core-react", "projects", string, "icons", string, {
128
- mimeType: Extract<import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).IconVariant["mimeType"], "image/png">;
129
- pixelDensity: Extract<import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).IconVariant, {
130
- mimeType: "image/png";
131
- }>["pixelDensity"];
132
- size: import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).ValidSizes;
133
- } | {
134
- mimeType: Extract<import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).IconVariant["mimeType"], "image/svg+xml">;
135
- size: import("@comapeo/core/dist/icon-api.js", { with: { "resolution-mode": "import" } }).ValidSizes;
136
- }] & {
137
- [dataTagSymbol]: string;
138
- [dataTagErrorSymbol]: Error;
139
- };
140
- };
141
91
  export declare function documentCreatedByQueryOptions({ projectApi, projectId, originalVersionId, }: {
142
92
  projectApi: MapeoProjectApi;
143
93
  projectId: string;
@@ -150,14 +100,12 @@ export declare function documentCreatedByQueryOptions({ projectApi, projectId, o
150
100
  [dataTagErrorSymbol]: Error;
151
101
  };
152
102
  };
153
- export declare function attachmentUrlQueryOptions({ projectApi, projectId, blobId, }: {
103
+ export declare function mediaServerOriginQueryOptions({ projectApi, }: {
154
104
  projectApi: MapeoProjectApi;
155
- projectId: string;
156
- blobId: BlobApi.BlobId;
157
- }): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<string, Error, string, readonly ["@comapeo/core-react", "projects", string, "attachments", import("@comapeo/core/dist/types.js", { with: { "resolution-mode": "import" } }).BlobId]>, "queryFn"> & {
158
- queryFn?: import("@tanstack/react-query").QueryFunction<string, readonly ["@comapeo/core-react", "projects", string, "attachments", import("@comapeo/core/dist/types.js", { with: { "resolution-mode": "import" } }).BlobId], never> | undefined;
105
+ }): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<string, Error, string, readonly ["@comapeo/core-react", "media_server_origin"]>, "queryFn"> & {
106
+ queryFn?: import("@tanstack/react-query").QueryFunction<string, readonly ["@comapeo/core-react", "media_server_origin"], never> | undefined;
159
107
  } & {
160
- queryKey: readonly ["@comapeo/core-react", "projects", string, "attachments", import("@comapeo/core/dist/types.js", { with: { "resolution-mode": "import" } }).BlobId] & {
108
+ queryKey: readonly ["@comapeo/core-react", "media_server_origin"] & {
161
109
  [dataTagSymbol]: string;
162
110
  [dataTagErrorSymbol]: Error;
163
111
  };
@@ -6,18 +6,16 @@ exports.getProjectSettingsQueryKey = getProjectSettingsQueryKey;
6
6
  exports.getProjectRoleQueryKey = getProjectRoleQueryKey;
7
7
  exports.getMembersQueryKey = getMembersQueryKey;
8
8
  exports.getMemberByIdQueryKey = getMemberByIdQueryKey;
9
- exports.getIconUrlQueryKey = getIconUrlQueryKey;
10
9
  exports.getDocumentCreatedByQueryKey = getDocumentCreatedByQueryKey;
11
- exports.getAttachmentUrlQueryKey = getAttachmentUrlQueryKey;
10
+ exports.getMediaServerOriginQueryKey = getMediaServerOriginQueryKey;
12
11
  exports.projectsQueryOptions = projectsQueryOptions;
13
12
  exports.projectByIdQueryOptions = projectByIdQueryOptions;
14
13
  exports.projectSettingsQueryOptions = projectSettingsQueryOptions;
15
14
  exports.projectMembersQueryOptions = projectMembersQueryOptions;
16
15
  exports.projectMemberByIdQueryOptions = projectMemberByIdQueryOptions;
17
16
  exports.projectOwnRoleQueryOptions = projectOwnRoleQueryOptions;
18
- exports.iconUrlQueryOptions = iconUrlQueryOptions;
19
17
  exports.documentCreatedByQueryOptions = documentCreatedByQueryOptions;
20
- exports.attachmentUrlQueryOptions = attachmentUrlQueryOptions;
18
+ exports.mediaServerOriginQueryOptions = mediaServerOriginQueryOptions;
21
19
  exports.addServerPeerMutationOptions = addServerPeerMutationOptions;
22
20
  exports.removeServerPeerMutationOptions = removeServerPeerMutationOptions;
23
21
  exports.createProjectMutationOptions = createProjectMutationOptions;
@@ -52,16 +50,6 @@ function getMembersQueryKey({ projectId }) {
52
50
  function getMemberByIdQueryKey({ projectId, deviceId, }) {
53
51
  return [shared_js_1.ROOT_QUERY_KEY, 'projects', projectId, 'members', deviceId];
54
52
  }
55
- function getIconUrlQueryKey({ projectId, iconId, ...mimeBasedOpts }) {
56
- return [
57
- shared_js_1.ROOT_QUERY_KEY,
58
- 'projects',
59
- projectId,
60
- 'icons',
61
- iconId,
62
- mimeBasedOpts,
63
- ];
64
- }
65
53
  function getDocumentCreatedByQueryKey({ projectId, originalVersionId, }) {
66
54
  return [
67
55
  shared_js_1.ROOT_QUERY_KEY,
@@ -71,8 +59,13 @@ function getDocumentCreatedByQueryKey({ projectId, originalVersionId, }) {
71
59
  originalVersionId,
72
60
  ];
73
61
  }
74
- function getAttachmentUrlQueryKey({ projectId, blobId, }) {
75
- return [shared_js_1.ROOT_QUERY_KEY, 'projects', projectId, 'attachments', blobId];
62
+ /**
63
+ * We call this within a project hook, because that's the only place the API is
64
+ * exposed right now, but it is the same for all projects, so no need for
65
+ * scoping the query key to the project
66
+ */
67
+ function getMediaServerOriginQueryKey() {
68
+ return [shared_js_1.ROOT_QUERY_KEY, 'media_server_origin'];
76
69
  }
77
70
  function projectsQueryOptions({ clientApi, }) {
78
71
  return (0, react_query_1.queryOptions)({
@@ -128,15 +121,6 @@ function projectOwnRoleQueryOptions({ projectApi, projectId, }) {
128
121
  },
129
122
  });
130
123
  }
131
- function iconUrlQueryOptions({ projectApi, projectId, iconId, ...mimeBasedOpts }) {
132
- return (0, react_query_1.queryOptions)({
133
- ...(0, shared_js_1.baseQueryOptions)(),
134
- queryKey: getIconUrlQueryKey({ ...mimeBasedOpts, projectId, iconId }),
135
- queryFn: async () => {
136
- return projectApi.$icons.getIconUrl(iconId, mimeBasedOpts);
137
- },
138
- });
139
- }
140
124
  function documentCreatedByQueryOptions({ projectApi, projectId, originalVersionId, }) {
141
125
  return (0, react_query_1.queryOptions)({
142
126
  ...(0, shared_js_1.baseQueryOptions)(),
@@ -147,16 +131,31 @@ function documentCreatedByQueryOptions({ projectApi, projectId, originalVersionI
147
131
  queryFn: async () => {
148
132
  return projectApi.$originalVersionIdToDeviceId(originalVersionId);
149
133
  },
134
+ staleTime: 'static',
135
+ gcTime: Infinity,
150
136
  });
151
137
  }
152
- function attachmentUrlQueryOptions({ projectApi, projectId, blobId, }) {
138
+ // Used as a placeholder so that we can read the server port from the $blobs.getUrl() method
139
+ const FAKE_BLOB_ID = {
140
+ type: 'photo',
141
+ variant: 'original',
142
+ name: 'name',
143
+ driveId: 'drive-id',
144
+ };
145
+ function mediaServerOriginQueryOptions({ projectApi, }) {
153
146
  return (0, react_query_1.queryOptions)({
154
147
  ...(0, shared_js_1.baseQueryOptions)(),
155
- queryKey: getAttachmentUrlQueryKey({ projectId, blobId }),
148
+ // HACK: The server doesn't yet expose a method to get its origin, so we use
149
+ // the existing $blobs.getUrl() to get the origin with a fake BlobId. The origin
150
+ // is the same regardless of the blobId, so it's not necessary to include it
151
+ // as a dep for the query key.
152
+ queryKey: getMediaServerOriginQueryKey(),
156
153
  queryFn: async () => {
157
- // TODO: Might need a refresh token? (similar to map style url)
158
- return projectApi.$blobs.getUrl(blobId);
154
+ const url = await projectApi.$blobs.getUrl(FAKE_BLOB_ID);
155
+ return new URL(url).origin;
159
156
  },
157
+ staleTime: 'static',
158
+ gcTime: Infinity,
160
159
  });
161
160
  }
162
161
  function addServerPeerMutationOptions({ projectApi, projectId, queryClient, }) {
@@ -0,0 +1,15 @@
1
+ import type { BlobApi, IconApi } from '@comapeo/core';
2
+ /**
3
+ * Get a url for a blob based on its BlobId
4
+ */
5
+ export declare function getBlobUrl({ serverOrigin, projectId, blobId, }: {
6
+ serverOrigin: string;
7
+ projectId: string;
8
+ blobId: BlobApi.BlobId;
9
+ }): string;
10
+ export declare function getIconUrl({ serverOrigin, iconId, projectId, mimeBasedOpts, }: {
11
+ serverOrigin: string;
12
+ iconId: string;
13
+ projectId: string;
14
+ mimeBasedOpts: IconApi.BitmapOpts | IconApi.SvgOpts;
15
+ }): string;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ // TODO: Move these into a separate "@comapeo/asset-server" module which can
3
+ // export them to be imported directly in a client.
4
+ //
5
+ // NB: The URL construction is fragile right now - it must match the
6
+ // implementation in the @comapeo/core fastify plugins
7
+ // [blobServerPlugin](https://github.com/digidem/comapeo-core/blob/main/src/fastify-plugins/blobs.js)
8
+ // and
9
+ // [iconServerPlugin](https://github.com/digidem/comapeo-core/blob/main/src/fastify-plugins/icons.js)
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.getBlobUrl = getBlobUrl;
12
+ exports.getIconUrl = getIconUrl;
13
+ const MIME_TO_EXTENSION = {
14
+ 'image/png': '.png',
15
+ 'image/svg+xml': '.svg',
16
+ };
17
+ /**
18
+ * Get a url for a blob based on its BlobId
19
+ */
20
+ function getBlobUrl({ serverOrigin, projectId, blobId, }) {
21
+ const { driveId, type, variant, name } = blobId;
22
+ return `${serverOrigin}/blobs/${projectId}/${driveId}/${type}/${variant}/${name}`;
23
+ }
24
+ function getIconUrl({ serverOrigin, iconId, projectId, mimeBasedOpts, }) {
25
+ const mimeExtension = MIME_TO_EXTENSION[mimeBasedOpts.mimeType];
26
+ const pixelDensity = mimeBasedOpts.mimeType === 'image/svg+xml' ||
27
+ // if the pixel density is 1, we can omit the density suffix in the resulting url
28
+ // and assume the pixel density is 1 for applicable mime types when using the url
29
+ mimeBasedOpts.pixelDensity === 1
30
+ ? undefined
31
+ : mimeBasedOpts.pixelDensity;
32
+ return (`${serverOrigin}/icons/${projectId}/` +
33
+ constructIconPath({
34
+ pixelDensity,
35
+ size: mimeBasedOpts.size,
36
+ extension: mimeExtension,
37
+ iconId,
38
+ }));
39
+ }
40
+ /**
41
+ * General purpose path builder for an icon
42
+ */
43
+ function constructIconPath({ size, pixelDensity, iconId, extension, }) {
44
+ if (iconId.length === 0 || size.length === 0 || extension.length === 0) {
45
+ throw new Error('iconId, size, and extension cannot be empty strings');
46
+ }
47
+ let result = `${iconId}/${size}`;
48
+ if (typeof pixelDensity === 'number') {
49
+ if (pixelDensity < 1) {
50
+ throw new Error('pixelDensity must be a positive number');
51
+ }
52
+ result += `@${pixelDensity}x`;
53
+ }
54
+ result += extension.startsWith('.') ? extension : '.' + extension;
55
+ return result;
56
+ }
@@ -1,7 +1,8 @@
1
1
  import { useMutation, useQueryClient, useSuspenseQuery, } from '@tanstack/react-query';
2
2
  import { useSyncExternalStore } from 'react';
3
- import { addServerPeerMutationOptions, attachmentUrlQueryOptions, connectSyncServersMutationOptions, createBlobMutationOptions, createProjectMutationOptions, disconnectSyncServersMutationOptions, documentCreatedByQueryOptions, exportGeoJSONMutationOptions, exportZipFileMutationOptions, iconUrlQueryOptions, importProjectConfigMutationOptions, leaveProjectMutationOptions, projectByIdQueryOptions, projectMemberByIdQueryOptions, projectMembersQueryOptions, projectOwnRoleQueryOptions, projectSettingsQueryOptions, projectsQueryOptions, removeServerPeerMutationOptions, setAutostopDataSyncTimeoutMutationOptions, startSyncMutationOptions, stopSyncMutationOptions, updateProjectSettingsMutationOptions, } from '../lib/react-query/projects.js';
3
+ import { addServerPeerMutationOptions, connectSyncServersMutationOptions, createBlobMutationOptions, createProjectMutationOptions, disconnectSyncServersMutationOptions, documentCreatedByQueryOptions, exportGeoJSONMutationOptions, exportZipFileMutationOptions, importProjectConfigMutationOptions, leaveProjectMutationOptions, mediaServerOriginQueryOptions, projectByIdQueryOptions, projectMemberByIdQueryOptions, projectMembersQueryOptions, projectOwnRoleQueryOptions, projectSettingsQueryOptions, projectsQueryOptions, removeServerPeerMutationOptions, setAutostopDataSyncTimeoutMutationOptions, startSyncMutationOptions, stopSyncMutationOptions, updateProjectSettingsMutationOptions, } from '../lib/react-query/projects.js';
4
4
  import { SyncStore } from '../lib/sync.js';
5
+ import { getBlobUrl, getIconUrl } from '../lib/urls.js';
5
6
  import { useClientApi } from './client.js';
6
7
  /**
7
8
  * Retrieve the project settings for a project.
@@ -158,13 +159,14 @@ export function useManyMembers({ projectId }) {
158
159
  */
159
160
  export function useIconUrl({ projectId, iconId, ...mimeBasedOpts }) {
160
161
  const { data: projectApi } = useSingleProject({ projectId });
161
- const { data, error, isRefetching } = useSuspenseQuery(iconUrlQueryOptions({
162
- ...mimeBasedOpts,
163
- projectApi,
164
- projectId,
162
+ const { data: serverOrigin, error, isRefetching, } = useMediaServerOrigin({ projectApi });
163
+ const iconUrl = getIconUrl({
164
+ serverOrigin,
165
165
  iconId,
166
- }));
167
- return { data, error, isRefetching };
166
+ projectId,
167
+ mimeBasedOpts,
168
+ });
169
+ return { data: iconUrl, error, isRefetching };
168
170
  }
169
171
  /**
170
172
  * Retrieve a URL that points to a desired blob resource.
@@ -219,10 +221,17 @@ export function useIconUrl({ projectId, iconId, ...mimeBasedOpts }) {
219
221
  */
220
222
  export function useAttachmentUrl({ projectId, blobId, }) {
221
223
  const { data: projectApi } = useSingleProject({ projectId });
222
- const { data, error, isRefetching } = useSuspenseQuery(attachmentUrlQueryOptions({
224
+ const { data: serverOrigin, error, isRefetching, } = useMediaServerOrigin({ projectApi });
225
+ const blobUrl = getBlobUrl({ serverOrigin, projectId, blobId });
226
+ return { data: blobUrl, error, isRefetching };
227
+ }
228
+ /**
229
+ * @internal
230
+ * Hack to retrieve the media server origin (protocol + host).
231
+ */
232
+ function useMediaServerOrigin({ projectApi }) {
233
+ const { data, error, isRefetching } = useSuspenseQuery(mediaServerOriginQueryOptions({
223
234
  projectApi,
224
- projectId,
225
- blobId,
226
235
  }));
227
236
  return { data, error, isRefetching };
228
237
  }
@@ -1,4 +1,4 @@
1
- import type { BlobApi, IconApi } from '@comapeo/core' with { 'resolution-mode': 'import' };
1
+ import type { BlobApi } from '@comapeo/core' with { 'resolution-mode': 'import' };
2
2
  import type { MapeoClientApi, MapeoProjectApi } from '@comapeo/ipc' with { 'resolution-mode': 'import' };
3
3
  import type { ProjectSettings } from '@comapeo/schema' with { 'resolution-mode': 'import' };
4
4
  import { type QueryClient, type UnusedSkipTokenOptions } from '@tanstack/react-query';
@@ -19,27 +19,16 @@ export declare function getMemberByIdQueryKey({ projectId, deviceId, }: {
19
19
  projectId: string;
20
20
  deviceId: string;
21
21
  }): readonly ["@comapeo/core-react", "projects", string, "members", string];
22
- export declare function getIconUrlQueryKey({ projectId, iconId, ...mimeBasedOpts }: {
23
- projectId: string;
24
- iconId: string;
25
- } & (IconApi.BitmapOpts | IconApi.SvgOpts)): readonly ["@comapeo/core-react", "projects", string, "icons", string, {
26
- mimeType: Extract<import("@comapeo/core/dist/icon-api.js").IconVariant["mimeType"], "image/png">;
27
- pixelDensity: Extract<import("@comapeo/core/dist/icon-api.js").IconVariant, {
28
- mimeType: "image/png";
29
- }>["pixelDensity"];
30
- size: import("@comapeo/core/dist/icon-api.js").ValidSizes;
31
- } | {
32
- mimeType: Extract<import("@comapeo/core/dist/icon-api.js").IconVariant["mimeType"], "image/svg+xml">;
33
- size: import("@comapeo/core/dist/icon-api.js").ValidSizes;
34
- }];
35
22
  export declare function getDocumentCreatedByQueryKey({ projectId, originalVersionId, }: {
36
23
  projectId: string;
37
24
  originalVersionId: string;
38
25
  }): readonly ["@comapeo/core-react", "projects", string, "document_created_by", string];
39
- export declare function getAttachmentUrlQueryKey({ projectId, blobId, }: {
40
- projectId: string;
41
- blobId: BlobApi.BlobId;
42
- }): readonly ["@comapeo/core-react", "projects", string, "attachments", import("@comapeo/core/dist/types.js").BlobId];
26
+ /**
27
+ * We call this within a project hook, because that's the only place the API is
28
+ * exposed right now, but it is the same for all projects, so no need for
29
+ * scoping the query key to the project
30
+ */
31
+ export declare function getMediaServerOriginQueryKey(): readonly ["@comapeo/core-react", "media_server_origin"];
43
32
  export declare function projectsQueryOptions({ clientApi, }: {
44
33
  clientApi: MapeoClientApi;
45
34
  }): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<import("@comapeo/core/dist/mapeo-manager.js").ListedProject[], Error, import("@comapeo/core/dist/mapeo-manager.js").ListedProject[], readonly ["@comapeo/core-react", "projects"]>, "queryFn"> & {
@@ -99,45 +88,6 @@ export declare function projectOwnRoleQueryOptions({ projectApi, projectId, }: {
99
88
  [dataTagErrorSymbol]: Error;
100
89
  };
101
90
  };
102
- export declare function iconUrlQueryOptions({ projectApi, projectId, iconId, ...mimeBasedOpts }: {
103
- projectApi: MapeoProjectApi;
104
- projectId: string;
105
- iconId: Parameters<MapeoProjectApi['$icons']['getIconUrl']>[0];
106
- } & (IconApi.BitmapOpts | IconApi.SvgOpts)): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<string, Error, string, readonly ["@comapeo/core-react", "projects", string, "icons", string, {
107
- mimeType: Extract<import("@comapeo/core/dist/icon-api.js").IconVariant["mimeType"], "image/png">;
108
- pixelDensity: Extract<import("@comapeo/core/dist/icon-api.js").IconVariant, {
109
- mimeType: "image/png";
110
- }>["pixelDensity"];
111
- size: import("@comapeo/core/dist/icon-api.js").ValidSizes;
112
- } | {
113
- mimeType: Extract<import("@comapeo/core/dist/icon-api.js").IconVariant["mimeType"], "image/svg+xml">;
114
- size: import("@comapeo/core/dist/icon-api.js").ValidSizes;
115
- }]>, "queryFn"> & {
116
- queryFn?: import("@tanstack/react-query").QueryFunction<string, readonly ["@comapeo/core-react", "projects", string, "icons", string, {
117
- mimeType: Extract<import("@comapeo/core/dist/icon-api.js").IconVariant["mimeType"], "image/png">;
118
- pixelDensity: Extract<import("@comapeo/core/dist/icon-api.js").IconVariant, {
119
- mimeType: "image/png";
120
- }>["pixelDensity"];
121
- size: import("@comapeo/core/dist/icon-api.js").ValidSizes;
122
- } | {
123
- mimeType: Extract<import("@comapeo/core/dist/icon-api.js").IconVariant["mimeType"], "image/svg+xml">;
124
- size: import("@comapeo/core/dist/icon-api.js").ValidSizes;
125
- }], never> | undefined;
126
- } & {
127
- queryKey: readonly ["@comapeo/core-react", "projects", string, "icons", string, {
128
- mimeType: Extract<import("@comapeo/core/dist/icon-api.js").IconVariant["mimeType"], "image/png">;
129
- pixelDensity: Extract<import("@comapeo/core/dist/icon-api.js").IconVariant, {
130
- mimeType: "image/png";
131
- }>["pixelDensity"];
132
- size: import("@comapeo/core/dist/icon-api.js").ValidSizes;
133
- } | {
134
- mimeType: Extract<import("@comapeo/core/dist/icon-api.js").IconVariant["mimeType"], "image/svg+xml">;
135
- size: import("@comapeo/core/dist/icon-api.js").ValidSizes;
136
- }] & {
137
- [dataTagSymbol]: string;
138
- [dataTagErrorSymbol]: Error;
139
- };
140
- };
141
91
  export declare function documentCreatedByQueryOptions({ projectApi, projectId, originalVersionId, }: {
142
92
  projectApi: MapeoProjectApi;
143
93
  projectId: string;
@@ -150,14 +100,12 @@ export declare function documentCreatedByQueryOptions({ projectApi, projectId, o
150
100
  [dataTagErrorSymbol]: Error;
151
101
  };
152
102
  };
153
- export declare function attachmentUrlQueryOptions({ projectApi, projectId, blobId, }: {
103
+ export declare function mediaServerOriginQueryOptions({ projectApi, }: {
154
104
  projectApi: MapeoProjectApi;
155
- projectId: string;
156
- blobId: BlobApi.BlobId;
157
- }): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<string, Error, string, readonly ["@comapeo/core-react", "projects", string, "attachments", import("@comapeo/core/dist/types.js").BlobId]>, "queryFn"> & {
158
- queryFn?: import("@tanstack/react-query").QueryFunction<string, readonly ["@comapeo/core-react", "projects", string, "attachments", import("@comapeo/core/dist/types.js").BlobId], never> | undefined;
105
+ }): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<string, Error, string, readonly ["@comapeo/core-react", "media_server_origin"]>, "queryFn"> & {
106
+ queryFn?: import("@tanstack/react-query").QueryFunction<string, readonly ["@comapeo/core-react", "media_server_origin"], never> | undefined;
159
107
  } & {
160
- queryKey: readonly ["@comapeo/core-react", "projects", string, "attachments", import("@comapeo/core/dist/types.js").BlobId] & {
108
+ queryKey: readonly ["@comapeo/core-react", "media_server_origin"] & {
161
109
  [dataTagSymbol]: string;
162
110
  [dataTagErrorSymbol]: Error;
163
111
  };
@@ -18,16 +18,6 @@ export function getMembersQueryKey({ projectId }) {
18
18
  export function getMemberByIdQueryKey({ projectId, deviceId, }) {
19
19
  return [ROOT_QUERY_KEY, 'projects', projectId, 'members', deviceId];
20
20
  }
21
- export function getIconUrlQueryKey({ projectId, iconId, ...mimeBasedOpts }) {
22
- return [
23
- ROOT_QUERY_KEY,
24
- 'projects',
25
- projectId,
26
- 'icons',
27
- iconId,
28
- mimeBasedOpts,
29
- ];
30
- }
31
21
  export function getDocumentCreatedByQueryKey({ projectId, originalVersionId, }) {
32
22
  return [
33
23
  ROOT_QUERY_KEY,
@@ -37,8 +27,13 @@ export function getDocumentCreatedByQueryKey({ projectId, originalVersionId, })
37
27
  originalVersionId,
38
28
  ];
39
29
  }
40
- export function getAttachmentUrlQueryKey({ projectId, blobId, }) {
41
- return [ROOT_QUERY_KEY, 'projects', projectId, 'attachments', blobId];
30
+ /**
31
+ * We call this within a project hook, because that's the only place the API is
32
+ * exposed right now, but it is the same for all projects, so no need for
33
+ * scoping the query key to the project
34
+ */
35
+ export function getMediaServerOriginQueryKey() {
36
+ return [ROOT_QUERY_KEY, 'media_server_origin'];
42
37
  }
43
38
  export function projectsQueryOptions({ clientApi, }) {
44
39
  return queryOptions({
@@ -94,15 +89,6 @@ export function projectOwnRoleQueryOptions({ projectApi, projectId, }) {
94
89
  },
95
90
  });
96
91
  }
97
- export function iconUrlQueryOptions({ projectApi, projectId, iconId, ...mimeBasedOpts }) {
98
- return queryOptions({
99
- ...baseQueryOptions(),
100
- queryKey: getIconUrlQueryKey({ ...mimeBasedOpts, projectId, iconId }),
101
- queryFn: async () => {
102
- return projectApi.$icons.getIconUrl(iconId, mimeBasedOpts);
103
- },
104
- });
105
- }
106
92
  export function documentCreatedByQueryOptions({ projectApi, projectId, originalVersionId, }) {
107
93
  return queryOptions({
108
94
  ...baseQueryOptions(),
@@ -113,16 +99,31 @@ export function documentCreatedByQueryOptions({ projectApi, projectId, originalV
113
99
  queryFn: async () => {
114
100
  return projectApi.$originalVersionIdToDeviceId(originalVersionId);
115
101
  },
102
+ staleTime: 'static',
103
+ gcTime: Infinity,
116
104
  });
117
105
  }
118
- export function attachmentUrlQueryOptions({ projectApi, projectId, blobId, }) {
106
+ // Used as a placeholder so that we can read the server port from the $blobs.getUrl() method
107
+ const FAKE_BLOB_ID = {
108
+ type: 'photo',
109
+ variant: 'original',
110
+ name: 'name',
111
+ driveId: 'drive-id',
112
+ };
113
+ export function mediaServerOriginQueryOptions({ projectApi, }) {
119
114
  return queryOptions({
120
115
  ...baseQueryOptions(),
121
- queryKey: getAttachmentUrlQueryKey({ projectId, blobId }),
116
+ // HACK: The server doesn't yet expose a method to get its origin, so we use
117
+ // the existing $blobs.getUrl() to get the origin with a fake BlobId. The origin
118
+ // is the same regardless of the blobId, so it's not necessary to include it
119
+ // as a dep for the query key.
120
+ queryKey: getMediaServerOriginQueryKey(),
122
121
  queryFn: async () => {
123
- // TODO: Might need a refresh token? (similar to map style url)
124
- return projectApi.$blobs.getUrl(blobId);
122
+ const url = await projectApi.$blobs.getUrl(FAKE_BLOB_ID);
123
+ return new URL(url).origin;
125
124
  },
125
+ staleTime: 'static',
126
+ gcTime: Infinity,
126
127
  });
127
128
  }
128
129
  export function addServerPeerMutationOptions({ projectApi, projectId, queryClient, }) {
@@ -0,0 +1,15 @@
1
+ import type { BlobApi, IconApi } from '@comapeo/core';
2
+ /**
3
+ * Get a url for a blob based on its BlobId
4
+ */
5
+ export declare function getBlobUrl({ serverOrigin, projectId, blobId, }: {
6
+ serverOrigin: string;
7
+ projectId: string;
8
+ blobId: BlobApi.BlobId;
9
+ }): string;
10
+ export declare function getIconUrl({ serverOrigin, iconId, projectId, mimeBasedOpts, }: {
11
+ serverOrigin: string;
12
+ iconId: string;
13
+ projectId: string;
14
+ mimeBasedOpts: IconApi.BitmapOpts | IconApi.SvgOpts;
15
+ }): string;
@@ -0,0 +1,52 @@
1
+ // TODO: Move these into a separate "@comapeo/asset-server" module which can
2
+ // export them to be imported directly in a client.
3
+ //
4
+ // NB: The URL construction is fragile right now - it must match the
5
+ // implementation in the @comapeo/core fastify plugins
6
+ // [blobServerPlugin](https://github.com/digidem/comapeo-core/blob/main/src/fastify-plugins/blobs.js)
7
+ // and
8
+ // [iconServerPlugin](https://github.com/digidem/comapeo-core/blob/main/src/fastify-plugins/icons.js)
9
+ const MIME_TO_EXTENSION = {
10
+ 'image/png': '.png',
11
+ 'image/svg+xml': '.svg',
12
+ };
13
+ /**
14
+ * Get a url for a blob based on its BlobId
15
+ */
16
+ export function getBlobUrl({ serverOrigin, projectId, blobId, }) {
17
+ const { driveId, type, variant, name } = blobId;
18
+ return `${serverOrigin}/blobs/${projectId}/${driveId}/${type}/${variant}/${name}`;
19
+ }
20
+ export function getIconUrl({ serverOrigin, iconId, projectId, mimeBasedOpts, }) {
21
+ const mimeExtension = MIME_TO_EXTENSION[mimeBasedOpts.mimeType];
22
+ const pixelDensity = mimeBasedOpts.mimeType === 'image/svg+xml' ||
23
+ // if the pixel density is 1, we can omit the density suffix in the resulting url
24
+ // and assume the pixel density is 1 for applicable mime types when using the url
25
+ mimeBasedOpts.pixelDensity === 1
26
+ ? undefined
27
+ : mimeBasedOpts.pixelDensity;
28
+ return (`${serverOrigin}/icons/${projectId}/` +
29
+ constructIconPath({
30
+ pixelDensity,
31
+ size: mimeBasedOpts.size,
32
+ extension: mimeExtension,
33
+ iconId,
34
+ }));
35
+ }
36
+ /**
37
+ * General purpose path builder for an icon
38
+ */
39
+ function constructIconPath({ size, pixelDensity, iconId, extension, }) {
40
+ if (iconId.length === 0 || size.length === 0 || extension.length === 0) {
41
+ throw new Error('iconId, size, and extension cannot be empty strings');
42
+ }
43
+ let result = `${iconId}/${size}`;
44
+ if (typeof pixelDensity === 'number') {
45
+ if (pixelDensity < 1) {
46
+ throw new Error('pixelDensity must be a positive number');
47
+ }
48
+ result += `@${pixelDensity}x`;
49
+ }
50
+ result += extension.startsWith('.') ? extension : '.' + extension;
51
+ return result;
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comapeo/core-react",
3
- "version": "6.0.0",
3
+ "version": "6.1.1",
4
4
  "description": "React wrapper for working with @comapeo/core",
5
5
  "repository": {
6
6
  "type": "git",