4.0 KB125 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {Platform} from '../platform';
9
10import {makeBrowserLikePlatformImpl} from './browserPlatformImpl';
11
12// important: this file should not try to import other code from 'isl',
13// since it will end up getting duplicated when bundling.
14
15/**
16 * This platform is used when spawned as a standalone webview from `sl web`.
17 * We pass messages to the rust side via `external.invoke`,
18 * with JSON serialized requests. Rust will respond back with JSON serialized responses.
19 * This lets us handle features like alerts, file dialogs, and opening external links
20 * which are not implemented in the webview itself.
21 */
22export const webviewPlatform: Platform = {
23 // just act like the browser platform by default, since the app use case is similar
24 ...makeBrowserLikePlatformImpl('webview'),
25
26 openExternalLink(url: string) {
27 invoke({cmd: 'openExternal', url});
28 },
29 confirm(message: string, details?: string): Promise<boolean> {
30 return request({cmd: 'confirm', message, details}).then(({ok}) => ok);
31 },
32 async chooseFile(title: string, multi: boolean): Promise<Array<File>> {
33 const response = await request({cmd: 'chooseFile', title, path: '', multi, mediaOnly: true});
34 const {files} = response;
35 if (!files) {
36 return [];
37 }
38 const result = files.map(value => b64toFile(value.base64Content, value.name));
39 return result;
40 },
41};
42
43function b64toFile(b64Data: string, filename: string, sliceSize = 512): File {
44 const byteCharacters = atob(b64Data);
45 const byteArrays = [];
46
47 for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
48 const slice = byteCharacters.slice(offset, offset + sliceSize);
49
50 const byteNumbers = new Array(slice.length);
51 for (let i = 0; i < slice.length; i++) {
52 byteNumbers[i] = slice.charCodeAt(i);
53 }
54
55 const byteArray = new Uint8Array(byteNumbers);
56 byteArrays.push(byteArray);
57 }
58
59 const blobParts = [new Blob(byteArrays)];
60 const file = new File(blobParts, filename);
61 return file;
62}
63
64window.islPlatform = webviewPlatform;
65
66/**
67 * Typed commands to communicate from the frontend with the Rust app hosting the webview.
68 * This should match the rust types used in webview-app.
69 */
70type ExternalWebviewCommandsInvoke =
71 | {cmd: 'openExternal'; url: string}
72 | {cmd: 'confirm'; message: string; details?: string}
73 | {cmd: 'chooseFile'; title: string; path: string; multi: boolean; mediaOnly: boolean};
74type ExternalWebviewCommandsResponse = (
75 | {cmd: 'confirm'; ok: boolean}
76 | {
77 cmd: 'chooseFile';
78 files: Array<{
79 name: string;
80 base64Content: string;
81 }>;
82 }
83) & {id: number};
84
85declare global {
86 interface Window {
87 islWebviewHandleResponse: (response: ExternalWebviewCommandsResponse) => void;
88 }
89}
90
91let nextId = 0;
92const callbacks: Array<(response: ExternalWebviewCommandsResponse) => void> = [];
93window.islWebviewHandleResponse = (response: ExternalWebviewCommandsResponse) => {
94 const cb = callbacks[response.id];
95 if (cb) {
96 cb(response);
97 delete callbacks[response.id];
98 }
99};
100
101declare const external: {
102 invoke(arg: string): Promise<void>;
103};
104
105function invoke(json: ExternalWebviewCommandsInvoke) {
106 external.invoke(JSON.stringify({...json, id: nextId++}));
107}
108
109function request<K extends ExternalWebviewCommandsInvoke['cmd']>(
110 json: ExternalWebviewCommandsInvoke & {cmd: K},
111): Promise<ExternalWebviewCommandsResponse & {cmd: K}> {
112 const id = nextId++;
113 let resolve: (value: ExternalWebviewCommandsResponse & {cmd: K}) => void;
114 const callback = (response: ExternalWebviewCommandsResponse) => {
115 resolve(response as ExternalWebviewCommandsResponse & {cmd: K});
116 };
117 const promise = new Promise<ExternalWebviewCommandsResponse & {cmd: K}>(res => {
118 resolve = res;
119 });
120 external.invoke(JSON.stringify({...json, id}));
121 callbacks[id] = callback;
122
123 return promise;
124}
125