From 4f2827f5c774aac8b5d650dda80672b1dce9036e Mon Sep 17 00:00:00 2001 From: linonetwo Date: Sun, 3 Sep 2023 20:27:48 +0800 Subject: [PATCH] feat: basic implementation of fs sync adaptor --- .vscode/settings.json | 1 + plugins/package.json | 1 + plugins/pnpm-lock.yaml | 41 ++++ .../Startup/install-electron-ipc-cat.js | 16 -- .../Startup/install-electron-ipc-cat.js.meta | 4 - .../electron-ipc-cat.js.meta | 4 - .../electron-ipc-cat.ts | 2 - ...cadaptor.ts => file-system-syncadaptor.ts} | 177 +++++++----------- .../file-system-syncadaptor.ts.meta | 3 + .../fix-location-info.ts | 44 +---- ...info.js.meta => fix-location-info.ts.meta} | 0 .../ipc-syncadaptor.js.meta | 3 - .../WikiStorageService/descriptor.ts | 12 ++ .../WikiWebView/WikiStorageService/index.ts | 87 +++++++++ .../registerWikiStorageServiceOnWebView.ts | 7 + .../WikiWebView/WikiStorageService/types.ts | 9 + src/pages/WikiWebView/WikiViewer.tsx | 40 +--- src/pages/WikiWebView/useTiddlyWiki.ts | 4 +- src/pages/WikiWebView/useWindowMeta.ts | 11 ++ 19 files changed, 253 insertions(+), 213 deletions(-) delete mode 100644 plugins/src/expo-file-system-syncadaptor/Startup/install-electron-ipc-cat.js delete mode 100644 plugins/src/expo-file-system-syncadaptor/Startup/install-electron-ipc-cat.js.meta delete mode 100644 plugins/src/expo-file-system-syncadaptor/electron-ipc-cat.js.meta delete mode 100644 plugins/src/expo-file-system-syncadaptor/electron-ipc-cat.ts rename plugins/src/expo-file-system-syncadaptor/{ipc-syncadaptor.ts => file-system-syncadaptor.ts} (58%) create mode 100644 plugins/src/expo-file-system-syncadaptor/file-system-syncadaptor.ts.meta rename plugins/src/expo-file-system-syncadaptor/{fix-location-info.js.meta => fix-location-info.ts.meta} (100%) delete mode 100644 plugins/src/expo-file-system-syncadaptor/ipc-syncadaptor.js.meta create mode 100644 src/pages/WikiWebView/WikiStorageService/descriptor.ts create mode 100644 src/pages/WikiWebView/WikiStorageService/index.ts create mode 100644 src/pages/WikiWebView/WikiStorageService/registerWikiStorageServiceOnWebView.ts create mode 100644 src/pages/WikiWebView/WikiStorageService/types.ts create mode 100644 src/pages/WikiWebView/useWindowMeta.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 7f84be1..b0ccfb8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ ], "i18n-ally.keystyle": "nested", "cSpell.words": [ + "tidgi", "zustand" ] } \ No newline at end of file diff --git a/plugins/package.json b/plugins/package.json index e00d9ad..cb1e567 100755 --- a/plugins/package.json +++ b/plugins/package.json @@ -16,6 +16,7 @@ "dprint": "^0.40.2", "postcss": "^8.4.29", "rimraf": "^5.0.1", + "tw5-typed": "^0.3.9", "ts-node": "^10.9.1" }, "dependencies": { diff --git a/plugins/pnpm-lock.yaml b/plugins/pnpm-lock.yaml index d1bd9ef..99d8fdd 100644 --- a/plugins/pnpm-lock.yaml +++ b/plugins/pnpm-lock.yaml @@ -28,6 +28,9 @@ devDependencies: ts-node: specifier: ^10.9.1 version: 10.9.1(@types/node@20.5.9)(typescript@5.2.2) + tw5-typed: + specifier: ^0.3.9 + version: 0.3.9 packages: @@ -213,10 +216,30 @@ packages: /@tsconfig/node16@1.0.4: resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + /@types/codemirror@5.60.9: + resolution: {integrity: sha512-8RhLhlGo9bAkytFYKDzezorY2ojvGk+4xFEso/6Hc2oR1oE2P9lI+AEkbUW7cDlKcQAK5WJkJRBLTdjBE7xQPA==} + dependencies: + '@types/tern': 0.23.4 + dev: true + + /@types/echarts@4.9.18: + resolution: {integrity: sha512-Qav4M1i1qmPemMywMnDGIbvIBB/9pdrDKLI1dyMho4Yz/ldCB3ry2zGeH0UhAhgmaoPgwYrCDo8xd1UeByz+rw==} + dependencies: + '@types/zrender': 4.0.4 + dev: true + + /@types/estree@1.0.1: + resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} + dev: true + /@types/less@3.0.4: resolution: {integrity: sha512-djlMpTdDF+tLaqVpK/0DWGNIr7BFjN8ykDLkgS0sQGYYLop51imRRE3foTjl+dMAH1zFE8bMZAG0VbYPEcSgsA==} dev: false + /@types/node@18.17.14: + resolution: {integrity: sha512-ZE/5aB73CyGqgQULkLG87N9GnyGe5TcQjv34pwS8tfBs1IkCh0ASM69mydb2znqd6v0eX+9Ytvk6oQRqu8T1Vw==} + dev: true + /@types/node@20.5.9: resolution: {integrity: sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==} @@ -233,6 +256,16 @@ packages: '@types/node': 20.5.9 dev: false + /@types/tern@0.23.4: + resolution: {integrity: sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==} + dependencies: + '@types/estree': 1.0.1 + dev: true + + /@types/zrender@4.0.4: + resolution: {integrity: sha512-oLm5ULgmBZh2Fw3G0BbhKaES0nUkmxwC+j932+0hBX+/MrNCAkUy3vOfZ7Mhub1IuBe3ApCy6MzgTw0Atu2RwA==} + dev: true + /acorn-walk@8.2.0: resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} engines: {node: '>=0.4.0'} @@ -1951,6 +1984,14 @@ packages: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} dev: false + /tw5-typed@0.3.9: + resolution: {integrity: sha512-FypE+YbEcpi+1vDg3EZP0jYyPYZLoH5HEDIwBvrWaLa7VhVNcMSZHCuIevZAyEAPnPT94g7gSWI08aVsWLxD7Q==} + dependencies: + '@types/codemirror': 5.60.9 + '@types/echarts': 4.9.18 + '@types/node': 18.17.14 + dev: true + /type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} diff --git a/plugins/src/expo-file-system-syncadaptor/Startup/install-electron-ipc-cat.js b/plugins/src/expo-file-system-syncadaptor/Startup/install-electron-ipc-cat.js deleted file mode 100644 index 54e5c70..0000000 --- a/plugins/src/expo-file-system-syncadaptor/Startup/install-electron-ipc-cat.js +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -exports.name = 'install-electron-ipc-cat'; -exports.platforms = ['browser']; -exports.after = ['startup']; -exports.synchronous = true; -exports.startup = function() { - if ('service' in window && 'descriptors' in window.service && window.service.descriptors !== undefined) { - require('$:/plugins/linonetwo/expo-file-system-syncadaptor/Startup/electron-ipc-cat.js'); - // call setupSSE in `src/services/wiki/plugin/ipcSyncAdaptor/ipc-syncadaptor.ts` of TidGi-Desktop - if (typeof $tw !== 'undefined') { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - $tw.syncadaptor?.setupSSE(); - } - } -}; diff --git a/plugins/src/expo-file-system-syncadaptor/Startup/install-electron-ipc-cat.js.meta b/plugins/src/expo-file-system-syncadaptor/Startup/install-electron-ipc-cat.js.meta deleted file mode 100644 index 929defb..0000000 --- a/plugins/src/expo-file-system-syncadaptor/Startup/install-electron-ipc-cat.js.meta +++ /dev/null @@ -1,4 +0,0 @@ -creator: LinOnetwo -title: $:/plugins/linonetwo/expo-file-system-syncadaptor/Startup/install-electron-ipc-cat.js -type: application/javascript -module-type: startup \ No newline at end of file diff --git a/plugins/src/expo-file-system-syncadaptor/electron-ipc-cat.js.meta b/plugins/src/expo-file-system-syncadaptor/electron-ipc-cat.js.meta deleted file mode 100644 index 93b74c7..0000000 --- a/plugins/src/expo-file-system-syncadaptor/electron-ipc-cat.js.meta +++ /dev/null @@ -1,4 +0,0 @@ -creator: LinOnetwo -title: $:/plugins/linonetwo/expo-file-system-syncadaptor/Startup/electron-ipc-cat.js -type: application/javascript -module-type: library \ No newline at end of file diff --git a/plugins/src/expo-file-system-syncadaptor/electron-ipc-cat.ts b/plugins/src/expo-file-system-syncadaptor/electron-ipc-cat.ts deleted file mode 100644 index 7735b1a..0000000 --- a/plugins/src/expo-file-system-syncadaptor/electron-ipc-cat.ts +++ /dev/null @@ -1,2 +0,0 @@ -import 'electron-ipc-cat/fixContextIsolation'; -console.log('electron-ipc-cat/fixContextIsolation in $:/plugins/linonetwo/expo-file-system-syncadaptor'); diff --git a/plugins/src/expo-file-system-syncadaptor/ipc-syncadaptor.ts b/plugins/src/expo-file-system-syncadaptor/file-system-syncadaptor.ts similarity index 58% rename from plugins/src/expo-file-system-syncadaptor/ipc-syncadaptor.ts rename to plugins/src/expo-file-system-syncadaptor/file-system-syncadaptor.ts index 5a6459e..4edfd15 100644 --- a/plugins/src/expo-file-system-syncadaptor/ipc-syncadaptor.ts +++ b/plugins/src/expo-file-system-syncadaptor/file-system-syncadaptor.ts @@ -1,9 +1,7 @@ /* eslint-disable @typescript-eslint/strict-boolean-expressions */ /* eslint-disable unicorn/no-null */ -import type { IWikiServerStatusObject } from '@services/wiki/wikiWorker/ipcServerRoutes'; -import type { WindowMeta, WindowNames } from '@services/windows/WindowProperties'; -import debounce from 'lodash/debounce'; -import type { IChangedTiddlers, ITiddlerFields, Logger, Syncer, Tiddler, Wiki } from 'tiddlywiki'; +import type { ITiddlerFields, Logger, Syncer, Tiddler, Wiki } from 'tiddlywiki'; +import type { WikiStorageService } from '../../../src/pages/WikiWebView/WikiStorageService/index.ts'; type ISyncAdaptorGetStatusCallback = (error: Error | null, isLoggedIn?: boolean, username?: string, isReadOnly?: boolean, isAnonymous?: boolean) => void; type ISyncAdaptorGetTiddlersJSONCallback = (error: Error | null, tiddler?: Array>) => void; @@ -13,8 +11,16 @@ type ISyncAdaptorPutTiddlersCallback = (error: Error | null | string, etag?: { type ISyncAdaptorLoadTiddlerCallback = (error: Error | null, tiddler?: ITiddlerFields) => void; type ISyncAdaptorDeleteTiddlerCallback = (error: Error | null, adaptorInfo?: { bag?: string } | null) => void; -class TidGiIPCSyncAdaptor { - name = 'tidgi-ipc'; +declare global { + interface Window { + service?: { + wikiStorageService: WikiStorageService; + }; + } +} + +class TidGiMobileFileSystemSyncAdaptor { + name = 'tidgi-mobile-fs'; supportsLazyLoading = true; wiki: Wiki; hasStatus: boolean; @@ -23,65 +29,28 @@ class TidGiIPCSyncAdaptor { isAnonymous: boolean; isReadOnly: boolean; logoutIsAvailable: boolean; - wikiService: typeof window.service.wiki; - workspaceService: typeof window.service.workspace; - authService: typeof window.service.auth; + wikiStorageService: WikiStorageService; workspaceID: string; recipe?: string; constructor(options: { wiki: Wiki }) { + if (window.service?.wikiStorageService === undefined) { + throw new Error("TidGi-Mobile wikiStorageService is undefined, can't load wiki."); + } + this.wikiStorageService = window.service.wikiStorageService; + if (window.meta?.workspaceID === undefined) { + throw new Error("TidGi-Mobile workspaceID is undefined, can't load wiki."); + } + this.workspaceID = window.meta.workspaceID; this.wiki = options.wiki; - this.wikiService = window.service.wiki; - this.workspaceService = window.service.workspace; - this.authService = window.service.auth; this.hasStatus = false; this.isAnonymous = false; - this.logger = new $tw.utils.Logger('TidGiIPCSyncAdaptor'); + this.logger = new $tw.utils.Logger('TidGiMobileFileSystemSyncAdaptor'); this.isLoggedIn = false; this.isReadOnly = false; this.logoutIsAvailable = true; - this.workspaceID = (window.meta as WindowMeta[WindowNames.view]).workspaceID!; - if (window.observables?.wiki?.getWikiChangeObserver$ !== undefined) { - // if install-electron-ipc-cat is faster than us, just subscribe to the observable. Otherwise we normally will wait for it to call us here. - this.setupSSE(); - } - } - - /** - * This should be called after install-electron-ipc-cat, so this is called in `$:/plugins/linonetwo/expo-file-system-syncadaptor/Startup/install-electron-ipc-cat.js` - */ - setupSSE() { - if (window.observables?.wiki?.getWikiChangeObserver$ === undefined) { - console.error("getWikiChangeObserver$ is undefined in window.observables.wiki, can't subscribe to server changes."); - return; - } - const debouncedSync = debounce(() => { - if ($tw.syncer === undefined) { - console.error('Syncer is undefined in TidGiIPCSyncAdaptor. Abort the `syncFromServer` in `setupSSE debouncedSync`.'); - return; - } - $tw.syncer.syncFromServer(); - this.clearUpdatedTiddlers(); - }, 500); - this.logger.log('setupSSE'); - - // After SSE is enabled, we can disable polling and else things that related to syncer. (build up complexer behavior with syncer.) - this.configSyncer(); - - window.observables?.wiki?.getWikiChangeObserver$(this.workspaceID).subscribe((change: IChangedTiddlers) => { - // `$tw.syncer.syncFromServer` calling `this.getUpdatedTiddlers`, so we need to update `this.updatedTiddlers` before it do so. See `core/modules/syncer.js` in the core - Object.keys(change).forEach(title => { - if (!change[title]) { - return; - } - if (change[title].deleted && !this.recentUpdatedTiddlersFromClient.deletions.includes(title)) { - this.updatedTiddlers.deletions.push(title); - } else if (change[title].modified && !this.recentUpdatedTiddlersFromClient.modifications.includes(title)) { - this.updatedTiddlers.modifications.push(title); - } - }); - debouncedSync(); - }); + // React-Native don't have fs monitor, so no SSE on mobile + // this.setupSSE(); } updatedTiddlers: { deletions: string[]; modifications: string[] } = { @@ -121,7 +90,7 @@ class TidGiIPCSyncAdaptor { private configSyncer() { if ($tw.syncer === undefined) { - console.error('Syncer is undefined in TidGiIPCSyncAdaptor. Abort the configSyncer.'); + console.error('Syncer is undefined in TidGiMobileFileSystemSyncAdaptor. Abort the configSyncer.'); return; } $tw.syncer.pollTimerInterval = 2_147_483_647; @@ -152,16 +121,13 @@ class TidGiIPCSyncAdaptor { return tiddler?.fields?.revision; } - /* - Get the current status of the TiddlyWeb connection - */ + /** + * Get the current status of the TiddlyWeb connection + */ async getStatus(callback?: ISyncAdaptorGetStatusCallback) { this.logger.log('Getting status'); try { - const workspace = await this.workspaceService.get(this.workspaceID); - const userName = workspace === undefined ? '' : await this.authService.getUserName(workspace); - const statusResponse = await this.wikiService.callWikiIpcServerRoute(this.workspaceID, 'getStatus', userName); - const status = statusResponse?.data as IWikiServerStatusObject; + const status = await this.wikiStorageService.getStatus(); if (status === undefined) { throw new Error('No status returned from callWikiIpcServerRoute getStatus'); } @@ -181,31 +147,32 @@ class TidGiIPCSyncAdaptor { } } - /* - Get an array of skinny tiddler fields from the server - */ - async getSkinnyTiddlers(callback: ISyncAdaptorGetTiddlersJSONCallback) { - try { - this.logger.log('getSkinnyTiddlers'); - const tiddlersJSONResponse = await this.wikiService.callWikiIpcServerRoute( - this.workspaceID, - 'getTiddlersJSON', - '[all[tiddlers]] -[[$:/isEncrypted]] -[prefix[$:/temp/]] -[prefix[$:/status/]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] -[[$:/library/sjcl.js]] -[[$:/core]]', - ); + /** + * Get an array of skinny tiddler fields from the server + * But HTML wiki already have all skinny tiddlers, so omit this. If this is necessary, maybe need mobile-sync plugin provide this, and store in asyncStorage, then provided here. + */ + // async getSkinnyTiddlers(callback: ISyncAdaptorGetTiddlersJSONCallback) { + // try { + // this.logger.log('getSkinnyTiddlers'); + // const tiddlersJSONResponse = await this.wikiService.callWikiIpcServerRoute( + // this.workspaceID, + // 'getTiddlersJSON', + // '[all[tiddlers]] -[[$:/isEncrypted]] -[prefix[$:/temp/]] -[prefix[$:/status/]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] -[[$:/library/sjcl.js]] -[[$:/core]]', + // ); - // Process the tiddlers to make sure the revision is a string - const skinnyTiddlers = tiddlersJSONResponse?.data as Array> | undefined; - if (skinnyTiddlers === undefined) { - throw new Error('No tiddlers returned from callWikiIpcServerRoute getTiddlersJSON in getSkinnyTiddlers'); - } - this.logger.log('skinnyTiddlers.length', skinnyTiddlers.length); - // Invoke the callback with the skinny tiddlers - callback(null, skinnyTiddlers); - } catch (error) { - // eslint-disable-next-line n/no-callback-literal - callback?.(error as Error); - } - } + // // Process the tiddlers to make sure the revision is a string + // const skinnyTiddlers = tiddlersJSONResponse?.data as Array> | undefined; + // if (skinnyTiddlers === undefined) { + // throw new Error('No tiddlers returned from callWikiIpcServerRoute getTiddlersJSON in getSkinnyTiddlers'); + // } + // this.logger.log('skinnyTiddlers.length', skinnyTiddlers.length); + // // Invoke the callback with the skinny tiddlers + // callback(null, skinnyTiddlers); + // } catch (error) { + // // eslint-disable-next-line n/no-callback-literal + // callback?.(error as Error); + // } + // } /* Save a tiddler and invoke the callback with (err,adaptorInfo,revision) @@ -217,19 +184,12 @@ class TidGiIPCSyncAdaptor { } try { const title = tiddler.fields.title; - this.logger.log(`loadTiddler ${title}`); + this.logger.log(`saveTiddler ${title}`); this.addRecentUpdatedTiddlersFromClient('modifications', title); - const putTiddlerResponse = await this.wikiService.callWikiIpcServerRoute( - this.workspaceID, - 'putTiddler', + const etag = await this.wikiStorageService.saveTiddler( title, tiddler.fields, ); - if (putTiddlerResponse === undefined) { - throw new Error('saveTiddler returned undefined from callWikiIpcServerRoute putTiddler in saveTiddler'); - } - // Save the details of the new revision of the tiddler - const etag = putTiddlerResponse?.headers?.Etag; if (etag === undefined) { callback(new Error('Response from server is missing required `etag` header')); } else { @@ -253,15 +213,8 @@ class TidGiIPCSyncAdaptor { async loadTiddler(title: string, callback?: ISyncAdaptorLoadTiddlerCallback) { this.logger.log(`loadTiddler ${title}`); try { - const getTiddlerResponse = await this.wikiService.callWikiIpcServerRoute( - this.workspaceID, - 'getTiddler', - title, - ); - if (getTiddlerResponse?.data === undefined) { - throw new Error('getTiddler returned undefined from callWikiIpcServerRoute getTiddler in loadTiddler'); - } - callback?.(null, getTiddlerResponse.data as ITiddlerFields); + const tiddlerFields: ITiddlerFields = await this.wikiStorageService.loadTiddler(title); + callback?.(null, tiddlerFields); } catch (error) { // eslint-disable-next-line n/no-callback-literal callback?.(error as Error); @@ -287,13 +240,9 @@ class TidGiIPCSyncAdaptor { return; } this.addRecentUpdatedTiddlersFromClient('deletions', title); - const getTiddlerResponse = await this.wikiService.callWikiIpcServerRoute( - this.workspaceID, - 'deleteTiddler', - title, - ); + const deleted = await this.wikiStorageService.deleteTiddler(title); try { - if (getTiddlerResponse?.data === undefined) { + if (!deleted) { throw new Error('getTiddler returned undefined from callWikiIpcServerRoute getTiddler in loadTiddler'); } // Invoke the callback & return null adaptorInfo @@ -335,10 +284,10 @@ class TidGiIPCSyncAdaptor { if ($tw.browser && typeof window !== 'undefined') { const isInTidGi = typeof document !== 'undefined' && document?.location?.protocol?.startsWith('tidgi'); - const servicesExposed = Boolean(window.service?.wiki); - const hasWorkspaceIDinMeta = Boolean((window.meta as WindowMeta[WindowNames.view] | undefined)?.workspaceID); + const servicesExposed = Boolean(window.service?.wikiStorageService); + const hasWorkspaceIDinMeta = Boolean(window.meta?.workspaceID); if (isInTidGi && servicesExposed && hasWorkspaceIDinMeta) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - exports.adaptorClass = TidGiIPCSyncAdaptor; + exports.adaptorClass = TidGiMobileFileSystemSyncAdaptor; } } diff --git a/plugins/src/expo-file-system-syncadaptor/file-system-syncadaptor.ts.meta b/plugins/src/expo-file-system-syncadaptor/file-system-syncadaptor.ts.meta new file mode 100644 index 0000000..a5f05b0 --- /dev/null +++ b/plugins/src/expo-file-system-syncadaptor/file-system-syncadaptor.ts.meta @@ -0,0 +1,3 @@ +title: $:/plugins/linonetwo/expo-file-system-syncadaptor/file-system-syncadaptor.js +type: application/javascript +module-type: syncadaptor \ No newline at end of file diff --git a/plugins/src/expo-file-system-syncadaptor/fix-location-info.ts b/plugins/src/expo-file-system-syncadaptor/fix-location-info.ts index fdefd95..b397820 100644 --- a/plugins/src/expo-file-system-syncadaptor/fix-location-info.ts +++ b/plugins/src/expo-file-system-syncadaptor/fix-location-info.ts @@ -1,9 +1,12 @@ /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ /* eslint-disable @typescript-eslint/strict-boolean-expressions */ +import type { WindowMeta } from '../../../src/pages/WikiWebView/useWindowMeta'; -import { getTidGiAuthHeaderWithToken } from '@/constants/auth'; -import { getDefaultHTTPServerIP } from '@/constants/urls'; -import type { WindowMeta, WindowNames } from '@services/windows/WindowProperties'; +declare global { + interface Window { + meta?: WindowMeta; + } +} function getInfoTiddlerFields(updateInfoTiddlersCallback: (infos: Array<{ text: string; title: string }>) => void) { const mapBoolean = function(value: boolean) { @@ -13,43 +16,10 @@ function getInfoTiddlerFields(updateInfoTiddlersCallback: (infos: Array<{ text: // Basics if (!$tw.browser || typeof window === 'undefined') return infoTiddlerFields; const isInTidGi = typeof document !== 'undefined' && document?.location?.protocol?.startsWith('tidgi'); - const workspaceID = (window.meta as WindowMeta[WindowNames.view] | undefined)?.workspaceID; + const workspaceID = (window.meta)?.workspaceID; infoTiddlerFields.push({ title: '$:/info/tidgi', text: mapBoolean(isInTidGi) }); if (isInTidGi && workspaceID) { infoTiddlerFields.push({ title: '$:/info/tidgi/workspaceID', text: workspaceID }); - void window.service.workspace.get(workspaceID).then(async (workspace) => { - if (workspace === undefined) return; - const { - https = { enabled: false }, - port, - enableHTTPAPI, - tokenAuth, - authToken, - userName, - } = workspace; - const asyncInfoTiddlerFields: Array<{ text: string; title: string }> = []; - const setLocationProperty = function(name: string, value: string) { - asyncInfoTiddlerFields.push({ title: '$:/info/url/' + name, text: value }); - }; - const localHostUrl = await window.service.native.getLocalHostUrlWithActualInfo(getDefaultHTTPServerIP(port), workspaceID); - const urlObject = new URL(localHostUrl); - setLocationProperty('full', (localHostUrl).split('#')[0]); - setLocationProperty('host', urlObject.host); - setLocationProperty('hostname', urlObject.hostname); - setLocationProperty('protocol', https ? 'https' : 'http'); - setLocationProperty('port', urlObject.port); - setLocationProperty('pathname', urlObject.pathname); - setLocationProperty('search', urlObject.search); - setLocationProperty('origin', urlObject.origin); - - infoTiddlerFields.push({ title: '$:/info/tidgi/tokenAuth', text: mapBoolean(tokenAuth) }, { title: '$:/info/tidgi/enableHTTPAPI', text: mapBoolean(enableHTTPAPI) }); - if (tokenAuth) { - const fallbackUserName = await window.service.auth.get('userName'); - const tokenAuthHeader = `"${getTidGiAuthHeaderWithToken(authToken ?? '')}": "${userName || fallbackUserName || ''}"`; - asyncInfoTiddlerFields.push({ title: '$:/info/tidgi/tokenAuthHeader', text: tokenAuthHeader }); - } - updateInfoTiddlersCallback(asyncInfoTiddlerFields); - }); } return infoTiddlerFields; } diff --git a/plugins/src/expo-file-system-syncadaptor/fix-location-info.js.meta b/plugins/src/expo-file-system-syncadaptor/fix-location-info.ts.meta similarity index 100% rename from plugins/src/expo-file-system-syncadaptor/fix-location-info.js.meta rename to plugins/src/expo-file-system-syncadaptor/fix-location-info.ts.meta diff --git a/plugins/src/expo-file-system-syncadaptor/ipc-syncadaptor.js.meta b/plugins/src/expo-file-system-syncadaptor/ipc-syncadaptor.js.meta deleted file mode 100644 index 195d2e7..0000000 --- a/plugins/src/expo-file-system-syncadaptor/ipc-syncadaptor.js.meta +++ /dev/null @@ -1,3 +0,0 @@ -title: $:/plugins/linonetwo/expo-file-system-syncadaptor/ipc-syncadaptor.js -type: application/javascript -module-type: syncadaptor \ No newline at end of file diff --git a/src/pages/WikiWebView/WikiStorageService/descriptor.ts b/src/pages/WikiWebView/WikiStorageService/descriptor.ts new file mode 100644 index 0000000..6b49e6c --- /dev/null +++ b/src/pages/WikiWebView/WikiStorageService/descriptor.ts @@ -0,0 +1,12 @@ +import { ProxyPropertyType } from 'react-native-postmessage-cat'; +import type { ProxyDescriptor } from 'react-native-postmessage-cat/common'; + +export enum WikiStorageServiceChannel { + name = 'wiki-storage', +} +export const WikiStorageServiceIPCDescriptor: ProxyDescriptor = { + channel: WikiStorageServiceChannel.name, + properties: { + save: ProxyPropertyType.Function, + }, +}; diff --git a/src/pages/WikiWebView/WikiStorageService/index.ts b/src/pages/WikiWebView/WikiStorageService/index.ts new file mode 100644 index 0000000..7c0ef35 --- /dev/null +++ b/src/pages/WikiWebView/WikiStorageService/index.ts @@ -0,0 +1,87 @@ +/* eslint-disable @typescript-eslint/require-await */ +import omit from 'lodash/omit'; +import { useRegisterProxy } from 'react-native-postmessage-cat'; +import { ITiddlerFields } from 'tiddlywiki'; +import { useConfigStore } from '../../../store/config'; +import { WikiStorageServiceIPCDescriptor } from './descriptor'; +import { registerWikiStorageServiceOnWebView } from './registerWikiStorageServiceOnWebView'; +import { IWikiServerStatusObject } from './types'; + +/** + * Service that can be used to save/load wiki data + * + * - proxy by `src/pages/WikiWebView/WikiStorageService/registerWikiStorageServiceOnWebView.ts` to be `window.service.wikiStorageService` + * - then used in `plugins/src/expo-file-system-syncadaptor/file-system-syncadaptor.ts` inside webview + * + * Don't forget to register method in WikiStorageServiceIPCDescriptor. + * All methods must be async. + */ +export class WikiStorageService { + #id: string; + #configStore = useConfigStore; + constructor(id: string) { + this.#id = id; + } + + async getStatus(): Promise { + return { + anonymous: false, + read_only: false, + space: { + recipe: 'default', + }, + // tiddlywiki_version: '5.1.23', + username: this.#configStore.getState().userName, + }; + } + + /** + * Return the e-tag + */ + async saveTiddler(title: string, fields: ITiddlerFields): Promise { + const tiddlerFieldsToPut = omit(fields, ['fields', 'revision', '_is_skinny']) as Record; + // If this is a skinny tiddler, it means the client never got the full + // version of the tiddler to edit. So we must preserve whatever text + // already exists on the server, or else we'll inadvertently delete it. + // if (fields._is_skinny !== undefined) { + // const tiddler = this.wikiInstance.wiki.getTiddler(title); + // if (tiddler !== undefined) { + // tiddlerFieldsToPut.text = tiddler.fields.text; + // } + // } + // tiddlerFieldsToPut.title = title; + // this.wikiInstance.wiki.addTiddler(new this.wikiInstance.Tiddler(tiddlerFieldsToPut)); + const changeCount = '0' //this.wikiInstance.wiki.getChangeCount(title).toString(); + const Etag = `"default/${encodeURIComponent(title)}/${changeCount}:"`; + return Etag; + } + + async loadTiddler(title: string): Promise { + // const tiddler = this.wikiInstance.wiki.getTiddler(title); + // if (tiddler === undefined) { + // return { statusCode: 404, headers: { 'Content-Type': 'text/plain' }, data: `Tiddler "${title}" not exist` }; + // } + // // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + // const tiddlerFields = { ...tiddler.fields }; + + // // only add revision if it > 0 or exists + // // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + // if (this.wikiInstance.wiki.getChangeCount(title)) { + // tiddlerFields.revision = String(this.wikiInstance.wiki.getChangeCount(title)); + // } + // tiddlerFields.bag = 'default'; + // tiddlerFields.type = tiddlerFields.type ?? 'text/vnd.tiddlywiki'; + // return tiddlerFields; + return {} + } + + async deleteTiddler(title: string): Promise { + return true; + } +} + +export function useWikiStorageService(id: string) { + const wikiStorageService = new WikiStorageService(id); + const [webViewReference, onMessageReference] = useRegisterProxy(wikiStorageService, WikiStorageServiceIPCDescriptor); + return [webViewReference, onMessageReference, registerWikiStorageServiceOnWebView] as const; +} diff --git a/src/pages/WikiWebView/WikiStorageService/registerWikiStorageServiceOnWebView.ts b/src/pages/WikiWebView/WikiStorageService/registerWikiStorageServiceOnWebView.ts new file mode 100644 index 0000000..814097f --- /dev/null +++ b/src/pages/WikiWebView/WikiStorageService/registerWikiStorageServiceOnWebView.ts @@ -0,0 +1,7 @@ +import { WikiStorageServiceIPCDescriptor } from './descriptor'; + +export const registerWikiStorageServiceOnWebView = ` +const wikiStorageService = window.PostMessageCat(${JSON.stringify(WikiStorageServiceIPCDescriptor)}); +window.service = window.service || {}; +window.service.wikiStorageService = wikiStorageService; +`; diff --git a/src/pages/WikiWebView/WikiStorageService/types.ts b/src/pages/WikiWebView/WikiStorageService/types.ts new file mode 100644 index 0000000..cbc54e8 --- /dev/null +++ b/src/pages/WikiWebView/WikiStorageService/types.ts @@ -0,0 +1,9 @@ +export interface IWikiServerStatusObject { + anonymous: boolean; + read_only: boolean; + space: { + recipe: string; + }; + tiddlywiki_version?: string; + username: string; +} diff --git a/src/pages/WikiWebView/WikiViewer.tsx b/src/pages/WikiWebView/WikiViewer.tsx index ca8d521..121f33a 100644 --- a/src/pages/WikiWebView/WikiViewer.tsx +++ b/src/pages/WikiWebView/WikiViewer.tsx @@ -2,43 +2,20 @@ import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Text } from 'react-native-paper'; -import { ProxyPropertyType, useRegisterProxy, webviewPreloadedJS } from 'react-native-postmessage-cat'; -import type { ProxyDescriptor } from 'react-native-postmessage-cat/common'; +import { webviewPreloadedJS } from 'react-native-postmessage-cat'; import { WebView } from 'react-native-webview'; import { styled } from 'styled-components/native'; import { IWikiWorkspace } from '../../store/wiki'; import { useStreamChunksToWebView } from './useStreamChunksToWebView'; import { IHtmlContent, useTiddlyWiki } from './useTiddlyWiki'; import { useWikiWebViewNotification } from './useWikiWebViewNotification'; +import { useWikiStorageService } from './WikiStorageService'; const WebViewContainer = styled.View` flex: 2; height: 100%; `; -class WikiStorage { - save(data: string) { - console.log('Saved', data); - return true; - } -} -enum WikiStorageChannel { - name = 'wiki-storage', -} -export const WikiStorageIPCDescriptor: ProxyDescriptor = { - channel: WikiStorageChannel.name, - properties: { - save: ProxyPropertyType.Function, - }, -}; -const wikiStorage = new WikiStorage(); -const tryWikiStorage = ` -const wikiStorage = window.PostMessageCat(${JSON.stringify(WikiStorageIPCDescriptor)}); -wikiStorage.save('Hello World').then(console.log); -// play with it: window.wikiStorage.save('BBB').then(console.log) -window.wikiStorage = wikiStorage; -`; - export interface WikiViewerProps { wikiWorkspace: IWikiWorkspace; } @@ -55,15 +32,16 @@ export const WikiViewer = ({ wikiWorkspace }: WikiViewerProps) => { } return ( - + ); }; -function WebViewWithPreload({ htmlContent }: { htmlContent: IHtmlContent }) { +function WebViewWithPreload({ htmlContent, wikiWorkspace }: { htmlContent: IHtmlContent } & WikiViewerProps) { const { t } = useTranslation(); - const [webViewReference, onMessageReference] = useRegisterProxy(wikiStorage, WikiStorageIPCDescriptor); + const [loaded, setLoaded] = useState(false); + const [webViewReference, onMessageReference, registerWikiStorageServiceOnWebView] = useWikiStorageService(wikiWorkspace.id); const [webviewSideReceiver] = useStreamChunksToWebView(webViewReference, htmlContent, loaded); const preloadScript = useMemo(() => ` window.onerror = function(message, sourcefile, lineno, colno, error) { @@ -75,12 +53,12 @@ function WebViewWithPreload({ htmlContent }: { htmlContent: IHtmlContent }) { ${webviewPreloadedJS} - ${tryWikiStorage} + ${registerWikiStorageServiceOnWebView} ${webviewSideReceiver} true; // note: this is required, or you'll sometimes get silent failures - `, [webviewSideReceiver]); + `, [registerWikiStorageServiceOnWebView, webviewSideReceiver]); return ( { + onLoadEnd={() => { setLoaded(true); }} onMessage={onMessageReference.current} diff --git a/src/pages/WikiWebView/useTiddlyWiki.ts b/src/pages/WikiWebView/useTiddlyWiki.ts index 24f9ad5..7b13391 100644 --- a/src/pages/WikiWebView/useTiddlyWiki.ts +++ b/src/pages/WikiWebView/useTiddlyWiki.ts @@ -19,8 +19,8 @@ export function useTiddlyWiki(workspace: IWikiWorkspace) { const fetchHTML = async () => { try { setHtmlContent(null); - const html = await fs.readAsStringAsync(getWikiFilePath(workspace)); // 'file:///data/user/0/host.exp.exponent/cache/ExponentAsset-8568a405f924c561e7d18846ddc10c97.html'); - const tiddlerStoreScript = await fs.readAsStringAsync(getWikiTiddlerStorePath(workspace)); + const html = await fs.readAsStringAsync(getWikiFilePath(workspace)); // file:///data/user/0/host.exp.exponent/files/wiki/index.html or 'file:///data/user/0/host.exp.exponent/cache/ExponentAsset-8568a405f924c561e7d18846ddc10c97.html'); + const tiddlerStoreScript = await fs.readAsStringAsync(getWikiTiddlerStorePath(workspace)); // file:///data/user/0/host.exp.exponent/files/wiki/tiddlerStore.json setHtmlContent({ html, tiddlerStoreScript }); } catch (error) { console.error(error, (error as Error).stack); diff --git a/src/pages/WikiWebView/useWindowMeta.ts b/src/pages/WikiWebView/useWindowMeta.ts new file mode 100644 index 0000000..ed892e4 --- /dev/null +++ b/src/pages/WikiWebView/useWindowMeta.ts @@ -0,0 +1,11 @@ +import { IWikiWorkspace } from "../../store/wiki"; + +export interface WindowMeta { + workspaceID: string; +} + +export function useWindowMeta(workspace: IWikiWorkspace ) { + return ` + window.meta = + ` +} \ No newline at end of file