@@ -319,6 +357,8 @@ export class AppletMain extends LitElement {
Embed WAL:
diff --git a/example/ui/src/elements/post-detail.ts b/example/ui/src/elements/post-detail.ts
index f3782878..9325aba1 100644
--- a/example/ui/src/elements/post-detail.ts
+++ b/example/ui/src/elements/post-detail.ts
@@ -28,7 +28,7 @@ import './edit-post.js';
import { PostsStore } from '../posts-store.js';
import { postsStoreContext } from '../context.js';
import { Post } from '../types.js';
-import { WAL, weaveUrlFromWal } from '@theweave/api';
+import { WAL, WeaveClient, weaveUrlFromWal } from '@theweave/api';
/**
* @element post-detail
@@ -41,6 +41,9 @@ export class PostDetail extends LitElement {
@property(hashProperty('post-hash'))
postHash!: ActionHash;
+ @property()
+ weaveClient!: WeaveClient;
+
/**
* @internal
*/
@@ -126,6 +129,12 @@ export class PostDetail extends LitElement {
+
this.weaveClient.requestClose()}
+ >Close Window (only works if open in separate Window)
`;
}
diff --git a/example/ui/src/example-applet.ts b/example/ui/src/example-applet.ts
index 49dbe262..ba181102 100644
--- a/example/ui/src/example-applet.ts
+++ b/example/ui/src/example-applet.ts
@@ -42,7 +42,14 @@ export class ExampleApplet extends LitElement {
peerStatusUnsubscribe: UnsubscribeFunction | undefined;
+ onBeforeUnloadUnsubscribe: UnsubscribeFunction | undefined;
+
firstUpdated() {
+ this.onBeforeUnloadUnsubscribe = this.weaveClient.onBeforeUnload(async () => {
+ console.log('Unloading in 10 seconds');
+ await new Promise((resolve) => setTimeout(resolve, 10000));
+ console.log('Unloading now.');
+ });
// To test whether applet iframe properly gets removed after disabling applet.
// setInterval(() => {
// console.log('Hello from the example applet iframe.');
@@ -117,6 +124,7 @@ export class ExampleApplet extends LitElement {
diff --git a/iframes/applet-iframe/src/index.ts b/iframes/applet-iframe/src/index.ts
index c79f9358..30efcd88 100644
--- a/iframes/applet-iframe/src/index.ts
+++ b/iframes/applet-iframe/src/index.ts
@@ -40,6 +40,11 @@ import {
import { readable } from '@holochain-open-dev/stores';
import { toOriginalCaseB64 } from '@theweave/utils';
+type CallbackWithId = {
+ id: number;
+ callback: () => any;
+};
+
declare global {
interface Window {
__WEAVE_API__: WeaveServices;
@@ -49,6 +54,7 @@ declare global {
__WEAVE_APPLET_ID__: AppletId;
__WEAVE_PROTOCOL_VERSION__: string;
__MOSS_VERSION__: string;
+ __WEAVE_ON_BEFORE_UNLOAD_CALLBACKS__: Array
| undefined;
}
interface WindowEventMap {
@@ -60,11 +66,40 @@ const weaveApi: WeaveServices = {
mossVersion: () => {
return window.__MOSS_VERSION__;
},
+
onPeerStatusUpdate: (callback: (payload: PeerStatusUpdate) => any) => {
const listener = (e: CustomEvent) => callback(e.detail);
window.addEventListener('peer-status-update', listener);
return () => window.removeEventListener('peer-status-update', listener);
},
+
+ onBeforeUnload: (callback: () => void) => {
+ // registers a callback on the window object that will be called before
+ // the iframe gets unloaded
+ const existingCallbacks = window.__WEAVE_ON_BEFORE_UNLOAD_CALLBACKS__ || [];
+ let newCallbackId = 0;
+ const existingCallbackIds = existingCallbacks.map((callbackWithId) => callbackWithId.id);
+ if (existingCallbackIds && existingCallbackIds.length > 0) {
+ // every new callback gets a new id in increasing manner
+ const highestId = existingCallbackIds.sort((a, b) => b - a)[0];
+ newCallbackId = highestId + 1;
+ }
+
+ existingCallbacks.push({ id: newCallbackId, callback });
+
+ window.__WEAVE_ON_BEFORE_UNLOAD_CALLBACKS__ = existingCallbacks;
+
+ const unlisten = () => {
+ const allCallbacks = window.__WEAVE_ON_BEFORE_UNLOAD_CALLBACKS__ || [];
+ window.__WEAVE_ON_BEFORE_UNLOAD_CALLBACKS__ = allCallbacks.filter(
+ (callbackWithId) => callbackWithId.id !== newCallbackId,
+ );
+ };
+
+ // We return an unlistener function which removes the callback from the list of callbacks
+ return unlisten;
+ },
+
openAppletMain: async (appletHash: EntryHash): Promise =>
postMessage({
type: 'open-view',
@@ -190,6 +225,22 @@ const weaveApi: WeaveServices = {
window.__WEAVE_API__ = weaveApi;
window.__WEAVE_APPLET_SERVICES__ = new AppletServices();
+ // message handler for ParentToApplet messages
+ // This one is registered early here for any type of iframe
+ // to be able to respond also in case of page refreshes in short time
+ // intervals. Otherwise the message handler may not be registered in time
+ // when the on-before-unload message is sent to the iframe and Moss
+ // is waiting for a response and will never get one.
+ window.addEventListener('message', async (m: MessageEvent) => {
+ try {
+ const result = await handleEventMessage(m.data);
+ m.ports[0].postMessage({ type: 'success', result });
+ } catch (e) {
+ console.error('Failed to send postMessage to cross-group-view', e);
+ m.ports[0]?.postMessage({ type: 'error', error: (e as any).message });
+ }
+ });
+
const [_, view] = await Promise.all([fetchLocalStorage(), getRenderView()]);
if (!view) {
@@ -233,7 +284,7 @@ const weaveApi: WeaveServices = {
const appletHash = window.__WEAVE_APPLET_HASH__;
- // message handler for ParentToApplet messages - Only added for applet main-view
+ // message handler for ParentToApplet messages
window.addEventListener('message', async (m: MessageEvent) => {
try {
const result = await handleMessage(appletClient, appletHash, m.data);
@@ -329,6 +380,19 @@ async function fetchLocalStorage() {
);
}
+const handleEventMessage = async (message: ParentToAppletMessage) => {
+ switch (message.type) {
+ case 'on-before-unload':
+ const allCallbacks = window.__WEAVE_ON_BEFORE_UNLOAD_CALLBACKS__ || [];
+ await Promise.all(
+ allCallbacks.map(async (callbackWithId) => await callbackWithId.callback()),
+ );
+ return;
+ default:
+ return;
+ }
+};
+
const handleMessage = async (
appletClient: AppClient,
appletHash: AppletHash,
@@ -364,6 +428,10 @@ const handleMessage = async (
}),
);
break;
+ case 'on-before-unload': {
+ // This case is handled in handleEventMessage
+ return;
+ }
default:
throw new Error(
`Unknown ParentToAppletMessage type: '${(message as any).type}'. Message: ${message}`,
diff --git a/libs/api/src/api.ts b/libs/api/src/api.ts
index 43cdd65c..7a28b17b 100644
--- a/libs/api/src/api.ts
+++ b/libs/api/src/api.ts
@@ -225,6 +225,18 @@ export interface WeaveServices {
* @returns
*/
onPeerStatusUpdate: (callback: (payload: PeerStatusUpdate) => any) => UnsubscribeFunction;
+ /**
+ * Event listener allowing to register a callback that will get executed before the
+ * applet gets reloaded, for example to save intermediate user input (e.g. commit
+ * the most recent changes of a document to the source chain).
+ *
+ * If this callback takes too long, users may be offered to force reload, thereby
+ * ignoring/cancelling the pending callback.
+ *
+ * @param callback Callback that gets called before the Applet gets reloaded
+ * @returns
+ */
+ onBeforeUnload: (callback: () => void) => UnsubscribeFunction;
/**
* Open the main view of the specified Applet
* @param appletHash
@@ -369,6 +381,10 @@ export class WeaveClient implements WeaveServices {
return window.__WEAVE_API__.onPeerStatusUpdate(callback);
};
+ onBeforeUnload = (callback: () => any): UnsubscribeFunction => {
+ return window.__WEAVE_API__.onBeforeUnload(callback);
+ };
+
openAppletMain = async (appletHash: EntryHash): Promise =>
window.__WEAVE_API__.openAppletMain(appletHash);
diff --git a/libs/api/src/types.ts b/libs/api/src/types.ts
index b4c6788c..41a28a80 100644
--- a/libs/api/src/types.ts
+++ b/libs/api/src/types.ts
@@ -293,6 +293,9 @@ export type ParentToAppletMessage =
| {
type: 'peer-status-update';
payload: PeerStatusUpdate;
+ }
+ | {
+ type: 'on-before-unload';
};
export type AppletToParentMessage = {
diff --git a/package.json b/package.json
index 9700315c..62089269 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "org.lightningrodlabs.moss-0.13",
- "version": "0.13.0-gamma.2",
+ "version": "0.13.0-gamma.3",
"private": true,
"description": "Moss (0.13)",
"main": "./out/main/index.js",
@@ -22,6 +22,7 @@
"applet-dev": "yarn check:binaries && concurrently \"yarn workspace @theweave/api build:watch\" \"yarn workspace @theweave/elements build:watch\" \"UI_PORT=8888 yarn workspace example-applet start\" \"electron-vite dev -- --dev-config weave.dev.config.ts --agent-idx 1\" \"sleep 10 && electron-vite dev -- --dev-config weave.dev.config.ts --agent-idx 2 --sync-time 10000\"",
"applet-dev-1": "yarn check:binaries && concurrently \"yarn workspace @theweave/api build:watch\" \"yarn workspace @theweave/elements build:watch\" \"yarn workspace @theweave/attachments build:watch\" \"UI_PORT=8888 yarn workspace example-applet start\" \"electron-vite dev -- --dev-config weave.dev.config.ts --agent-idx 1\"",
"applet-dev-3": "yarn check:binaries && concurrently \"yarn workspace @theweave/api build:watch\" \"yarn workspace @theweave/elements build:watch\" \"UI_PORT=8888 yarn workspace example-applet start\" \"electron-vite dev -- --dev-config weave.dev.config.ts --agent-idx 1\" \"sleep 5 && electron-vite dev -- --dev-config weave.dev.config.ts --agent-idx 2 --sync-time 10000\" \"sleep 5 && electron-vite dev -- --dev-config weave.dev.config.ts --agent-idx 3 --sync-time 10000\"",
+ "applet-dev-example-1": "yarn check:binaries && concurrently \"yarn workspace @theweave/api build:watch\" \"yarn workspace @theweave/elements build:watch\" \"UI_PORT=8888 yarn workspace example-applet start\" \"electron-vite dev -- --dev-config weave.dev.config.example.ts --agent-idx 1\"",
"applet-dev-example": "yarn check:binaries && concurrently \"yarn workspace @theweave/api build:watch\" \"yarn workspace @theweave/elements build:watch\" \"UI_PORT=8888 yarn workspace example-applet start\" \"electron-vite dev -- --dev-config weave.dev.config.example.ts --agent-idx 1\" \"sleep 10 && electron-vite dev -- --dev-config weave.dev.config.example.ts --agent-idx 2\"",
"dev": "yarn check:binaries && concurrently \"yarn workspace @theweave/api build:watch\" \"yarn workspace @theweave/elements build:watch\" \"UI_PORT=8888 yarn workspace example-applet start\" \"electron-vite dev -- --dev-config weave.dev.config.ts --agent-idx 1\"",
"dev:electron": "electron-vite dev",
diff --git a/src/main/index.ts b/src/main/index.ts
index 6522dd56..65027258 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -221,23 +221,25 @@ if (app.isPackaged) {
// This event will always be triggered in the first instance, no matter with which profile
// it is being run. On Linux and Windows it is also how deeplinks get in.
app.on('second-instance', (_event, argv, _cwd, additionalData: any) => {
- console.log('second-instance event triggered. argv: ', argv);
- console.log('additionalData: ', additionalData);
- if (process.platform !== 'darwin') {
- console.log('Option 3');
-
- // deeplink case
- const url = argv.pop();
- if (SPLASH_SCREEN_WINDOW) {
- CACHED_DEEP_LINK = url;
- SPLASH_SCREEN_WINDOW.show();
- } else if (MAIN_WINDOW) {
- console.log('RECEIVED DEEP LINK: url', argv, url);
- // main window is already open
- createOrShowMainWindow();
- emitToWindow(MAIN_WINDOW, 'deep-link-received', url);
- } else {
- CACHED_DEEP_LINK = url;
+ if (!isAppQuitting) {
+ console.log('second-instance event triggered. argv: ', argv);
+ console.log('additionalData: ', additionalData);
+ if (process.platform !== 'darwin') {
+ console.log('Option 3');
+
+ // deeplink case
+ const url = argv.pop();
+ if (SPLASH_SCREEN_WINDOW) {
+ CACHED_DEEP_LINK = url;
+ SPLASH_SCREEN_WINDOW.show();
+ } else if (MAIN_WINDOW) {
+ console.log('RECEIVED DEEP LINK: url', argv, url);
+ // main window is already open
+ createOrShowMainWindow();
+ emitToWindow(MAIN_WINDOW, 'deep-link-received', url);
+ } else {
+ CACHED_DEEP_LINK = url;
+ }
}
}
});
@@ -289,22 +291,17 @@ setupLogs(WE_EMITTER, WE_FILE_SYSTEM, RUN_OPTIONS.printHolochainLogs);
protocol.registerSchemesAsPrivileged([
{
- scheme: 'moss',
- privileges: { standard: true, supportFetchAPI: true, secure: true, stream: true },
- },
-]);
-
-protocol.registerSchemesAsPrivileged([
- {
- scheme: 'default-app',
+ scheme: 'applet',
privileges: { standard: true, supportFetchAPI: true, secure: true, stream: true },
},
-]);
-
-protocol.registerSchemesAsPrivileged([
{
- scheme: 'applet',
- privileges: { standard: true, supportFetchAPI: true, secure: true, stream: true },
+ scheme: 'moss',
+ privileges: {
+ standard: true,
+ secure: true,
+ stream: true,
+ supportFetchAPI: true,
+ },
},
]);
@@ -339,6 +336,10 @@ let UPDATE_AVAILABLE:
// icons
const SYSTRAY_ICON_DEFAULT = nativeImage.createFromPath(path.join(ICONS_DIRECTORY, '32x32@2x.png'));
+const SYSTRAY_ICON_QUITTING = nativeImage.createFromPath(
+ path.join(ICONS_DIRECTORY, 'transparent32x32@2x.png'),
+);
+
const SYSTRAY_ICON_HIGH = nativeImage.createFromPath(
path.join(ICONS_DIRECTORY, 'icon_priority_high_32x32@2x.png'),
);
@@ -825,6 +826,7 @@ app.whenReady().then(async () => {
icon: notificationIcon,
})
.on('click', () => {
+ console.log('Clicked on OS notification');
createOrShowMainWindow();
emitToWindow(MAIN_WINDOW!, 'switch-to-applet', appletId);
SYSTRAY_ICON_STATE = undefined;
@@ -897,7 +899,17 @@ app.whenReady().then(async () => {
return;
}
const newWalWindow = createWalWindow();
+ // on-before-unload (added here for searchability of event-related code)
+ // This event is forwarded to the window in order to discern in the
+ // onbeforeunload callback between reloading and closing of the window
newWalWindow.on('close', () => {
+ // on-before-unload
+ // closing may be prevented by the beforeunload event listener in the window
+ // the first time. The window should however be hidden already anyway.
+ newWalWindow.hide();
+ emitToWindow(newWalWindow, 'window-closing', null);
+ });
+ newWalWindow.on('closed', () => {
delete WAL_WINDOWS[src];
});
WAL_WINDOWS[src] = {
@@ -923,13 +935,15 @@ app.whenReady().then(async () => {
return undefined;
},
);
+ ipcMain.handle('close-main-window', () => {
+ if (MAIN_WINDOW) MAIN_WINDOW.close();
+ });
ipcMain.handle('close-window', (e) => {
const walAndWindowInfo = Object.entries(WAL_WINDOWS).find(
([_src, window]) => window.window.webContents.id === e.sender.id,
);
if (walAndWindowInfo) {
walAndWindowInfo[1].window.close();
- delete WAL_WINDOWS[walAndWindowInfo[0]];
}
});
ipcMain.handle('focus-main-window', (): void => {
@@ -1160,6 +1174,33 @@ app.whenReady().then(async () => {
return appInfo;
},
);
+ ipcMain.handle(
+ 'fetch-and-validate-happ-or-webhapp',
+ async (_e, url: string): Promise => {
+ const response = await net.fetch(url);
+ const byteArray = Array.from(new Uint8Array(await response.arrayBuffer()));
+ const { happSha256, webhappSha256, uiSha256 } =
+ await rustUtils.validateHappOrWebhapp(byteArray);
+ if (uiSha256) {
+ if (!webhappSha256) throw Error('Ui sha256 defined but not webhapp sha256.');
+ return {
+ type: 'webhapp',
+ sha256: webhappSha256,
+ happ: {
+ sha256: happSha256,
+ },
+ ui: {
+ sha256: uiSha256,
+ },
+ };
+ } else {
+ return {
+ type: 'happ',
+ sha256: happSha256,
+ };
+ }
+ },
+ );
ipcMain.handle('validate-happ-or-webhapp', async (_e, bytes: number[]): Promise => {
const { happSha256, webhappSha256, uiSha256 } = await rustUtils.validateHappOrWebhapp(bytes);
if (uiSha256) {
@@ -1572,7 +1613,36 @@ app.on('activate', () => {
});
app.on('before-quit', () => {
+ if (!isAppQuitting) {
+ // If the quitting process takes longer than 15 seconds, force quit.
+ setTimeout(() => {
+ WE_EMITTER.emitMossError('FORCE QUITTING. Quitting Moss took longer than 15 seconds.');
+ // ignore beforeunload of all windows
+ MAIN_WINDOW?.webContents.on('will-prevent-unload', (e) => {
+ e.preventDefault();
+ });
+ MAIN_WINDOW?.close();
+ Object.values(WAL_WINDOWS).forEach((windowInfo) => {
+ const walWindow = windowInfo.window;
+ if (walWindow) {
+ walWindow.webContents.on('will-prevent-unload', (e) => {
+ e.preventDefault();
+ });
+ walWindow.webContents.close();
+ }
+ });
+ }, 15000);
+ }
isAppQuitting = true;
+ // on-before-unload
+ // This is to discern in the beforeunload listener between a reaload
+ // and a window close
+ if (MAIN_WINDOW) MAIN_WINDOW.hide();
+ if (SYSTRAY) {
+ SYSTRAY.setImage(SYSTRAY_ICON_QUITTING);
+ SYSTRAY.setContextMenu(Menu.buildFromTemplate([]));
+ }
+ if (MAIN_WINDOW) emitToWindow(MAIN_WINDOW, 'window-closing', null);
});
app.on('quit', () => {
diff --git a/src/preload/admin.ts b/src/preload/admin.ts
index b2013a63..1f97dfb4 100644
--- a/src/preload/admin.ts
+++ b/src/preload/admin.ts
@@ -41,6 +41,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.on('deep-link-received', callback),
onSwitchToApplet: (callback: (e: Electron.IpcRendererEvent, payload: AppletId) => any) =>
ipcRenderer.on('switch-to-applet', callback),
+ onWindowClosing: (callback: (e: Electron.IpcRendererEvent) => any) =>
+ ipcRenderer.on('window-closing', callback),
onZomeCallSigned: (
callback: (
e: Electron.IpcRendererEvent,
@@ -51,6 +53,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
},
) => any,
) => ipcRenderer.on('zome-call-signed', callback),
+ closeMainWindow: () => ipcRenderer.invoke('close-main-window'),
openApp: (appId: string) => ipcRenderer.invoke('open-app', appId),
openAppStore: () => ipcRenderer.invoke('open-appstore'),
openWalWindow: (iframeSrc: string, appletId: AppletId, wal: WAL) =>
@@ -141,6 +144,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
),
uninstallApplet: (appId: string) => ipcRenderer.invoke('uninstall-applet', appId),
dumpNetworkStats: () => ipcRenderer.invoke('dump-network-stats'),
+ fetchAndValidateHappOrWebhapp: (url: string) =>
+ ipcRenderer.invoke('fetch-and-validate-happ-or-webhapp', url),
validateHappOrWebhapp: (bytes: number[]) => ipcRenderer.invoke('validate-happ-or-webhapp', bytes),
});
diff --git a/src/preload/walwindow.ts b/src/preload/walwindow.ts
index cc60d494..4e6fb1b5 100644
--- a/src/preload/walwindow.ts
+++ b/src/preload/walwindow.ts
@@ -12,6 +12,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
focusMainWindow: () => ipcRenderer.invoke('focus-main-window'),
focusMyWindow: () => ipcRenderer.invoke('focus-my-window'),
getMySrc: () => ipcRenderer.invoke('get-my-src'),
+ onWindowClosing: (callback: (e: Electron.IpcRendererEvent) => any) =>
+ ipcRenderer.on('window-closing', callback),
selectScreenOrWindow: () => ipcRenderer.invoke('select-screen-or-window'),
setMyIcon: (icon: string) => ipcRenderer.invoke('set-my-icon', icon),
setMyTitle: (title: string) => ipcRenderer.invoke('set-my-title', title),
diff --git a/src/renderer/index.html b/src/renderer/index.html
index 4ebb23ba..b74a947e 100644
--- a/src/renderer/index.html
+++ b/src/renderer/index.html
@@ -64,6 +64,15 @@
+
+
+