Skip to content

Commit

Permalink
add logic to emit onBeforeUnload on Moss full page reload and when qu…
Browse files Browse the repository at this point in the history
…itting the app
  • Loading branch information
matthme committed Oct 19, 2024
1 parent a145098 commit 619dc39
Show file tree
Hide file tree
Showing 12 changed files with 249 additions and 74 deletions.
11 changes: 10 additions & 1 deletion example/ui/src/elements/post-detail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -41,6 +41,9 @@ export class PostDetail extends LitElement {
@property(hashProperty('post-hash'))
postHash!: ActionHash;

@property()
weaveClient!: WeaveClient;

/**
* @internal
*/
Expand Down Expand Up @@ -126,6 +129,12 @@ export class PostDetail extends LitElement {
</sl-card>
<attachments-card .wal=${weaveUrlFromWal(this.WAL!, false)}></attachments-card>
</div>
<sl-button
style="margin-top: 20px;"
variant="danger"
@click=${() => this.weaveClient.requestClose()}
>Close Window (only works if open in separate Window)</sl-button
>
`;
}

Expand Down
1 change: 1 addition & 0 deletions example/ui/src/example-applet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export class ExampleApplet extends LitElement {
<attachments-context .store=${this.attachmentsStore}>
<post-detail
.postHash=${this.weaveClient.renderInfo.view.wal.hrl[1]}
.weaveClient=${this.weaveClient}
></post-detail>
</attachments-context>
</posts-context>
Expand Down
35 changes: 17 additions & 18 deletions iframes/applet-iframe/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,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<any>) => {
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) {
Expand Down Expand Up @@ -325,17 +341,6 @@ const weaveApi: WeaveServices = {
),
);

// message handler for ParentToApplet messages - Only events are handled in the cross-group view
window.addEventListener('message', async (m: MessageEvent<any>) => {
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 });
}
});

window.__WEAVE_RENDER_INFO__ = {
type: 'cross-applet-view',
view: view.view,
Expand Down Expand Up @@ -378,7 +383,6 @@ async function fetchLocalStorage() {
const handleEventMessage = async (message: ParentToAppletMessage) => {
switch (message.type) {
case 'on-before-unload':
console.log('@applet-iframe: got on-before-unload event in cross-group-view');
const allCallbacks = window.__WEAVE_ON_BEFORE_UNLOAD_CALLBACKS__ || [];
await Promise.all(
allCallbacks.map(async (callbackWithId) => await callbackWithId.callback()),
Expand Down Expand Up @@ -425,12 +429,7 @@ const handleMessage = async (
);
break;
case 'on-before-unload': {
// Call all registered callbacks
console.log('@applet-iframe: got on-before-unload event');
const allCallbacks = window.__WEAVE_ON_BEFORE_UNLOAD_CALLBACKS__ || [];
await Promise.all(
allCallbacks.map(async (callbackWithId) => await callbackWithId.callback()),
);
// This case is handled in handleEventMessage
return;
}
default:
Expand Down
117 changes: 95 additions & 22 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
});
Expand Down Expand Up @@ -288,14 +290,19 @@ const WE_EMITTER = new WeEmitter();
setupLogs(WE_EMITTER, WE_FILE_SYSTEM, RUN_OPTIONS.printHolochainLogs);

protocol.registerSchemesAsPrivileged([
{
scheme: 'moss',
privileges: { standard: true, secure: true, stream: true, supportFetchAPI: true },
},
{
scheme: 'applet',
privileges: { standard: true, supportFetchAPI: true, secure: true, stream: true },
},
{
scheme: 'moss',
privileges: {
standard: true,
secure: true,
stream: true,
supportFetchAPI: true,
},
},
]);

let WE_RUST_HANDLER: WeRustHandler | undefined;
Expand Down Expand Up @@ -329,6 +336,10 @@ let UPDATE_AVAILABLE:

// icons
const SYSTRAY_ICON_DEFAULT = nativeImage.createFromPath(path.join(ICONS_DIRECTORY, '[email protected]'));
const SYSTRAY_ICON_QUITTING = nativeImage.createFromPath(
path.join(ICONS_DIRECTORY, '[email protected]'),
);

const SYSTRAY_ICON_HIGH = nativeImage.createFromPath(
path.join(ICONS_DIRECTORY, '[email protected]'),
);
Expand Down Expand Up @@ -815,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;
Expand Down Expand Up @@ -897,6 +909,9 @@ app.whenReady().then(async () => {
newWalWindow.hide();
emitToWindow(newWalWindow, 'window-closing', null);
});
newWalWindow.on('closed', () => {
delete WAL_WINDOWS[src];
});
WAL_WINDOWS[src] = {
window: newWalWindow,
appletId,
Expand All @@ -920,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 => {
Expand Down Expand Up @@ -1157,6 +1174,33 @@ app.whenReady().then(async () => {
return appInfo;
},
);
ipcMain.handle(
'fetch-and-validate-happ-or-webhapp',
async (_e, url: string): Promise<AppHashes> => {
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<AppHashes> => {
const { happSha256, webhappSha256, uiSha256 } = await rustUtils.validateHappOrWebhapp(bytes);
if (uiSha256) {
Expand Down Expand Up @@ -1569,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', () => {
Expand Down
5 changes: 5 additions & 0 deletions src/preload/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) =>
Expand Down Expand Up @@ -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),
});

Expand Down
9 changes: 9 additions & 0 deletions src/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@
<script>
window['__HC_LAUNCHER_ENV__'] = {};
</script>
<!-- code to make on-before-unload working: -->
<script>
window.electronAPI.onWindowClosing(() => {
// This code is required to discern in the window.onbeforeunload callback
// whether the onbeforunload event is coming from a page reload or from a
// page close
window.__WINDOW_CLOSING__ = true;
});
</script>
<script type="module">
import '@shoelace-style/shoelace/dist/themes/light.css';
import '@fontsource/aileron';
Expand Down
6 changes: 6 additions & 0 deletions src/renderer/src/electron-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ declare global {
},
) => any,
) => any;
closeMainWindow: () => Promise<void>;
openApp: (appId: string) => Promise<void>;
openWalWindow: (iframeSrc: string, appletId: AppletId, wal: WAL) => Promise<void>;
getAllAppAssetsInfos: () => Promise<
Expand Down Expand Up @@ -101,6 +102,7 @@ declare global {
) => Promise<void>;
uninstallApplet: (appId: string) => Promise<void>;
dumpNetworkStats: () => Promise<void>;
fetchAndValidateHappOrWebhapp: (url: string) => Promise<AppHashes>;
validateHappOrWebhapp: (bytes: number[]) => Promise<AppHashes>;
};
__ZOME_CALL_LOGGING_ENABLED__: boolean;
Expand Down Expand Up @@ -191,6 +193,10 @@ export async function selectScreenOrWindow(): Promise<string> {
return window.electronAPI.selectScreenOrWindow();
}

export async function fetchAndValidateHappOrWebhapp(url: string) {
return window.electronAPI.fetchAndValidateHappOrWebhapp(url);
}

export async function validateHappOrWebhapp(bytes: number[]) {
return window.electronAPI.validateHappOrWebhapp(bytes);
}
Expand Down
Loading

0 comments on commit 619dc39

Please sign in to comment.