addons/isl/src/platform/webviewPlatform.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {Platform} from '../platform';
b69ab319
b69ab3110import {makeBrowserLikePlatformImpl} from './browserPlatformImpl';
b69ab3111
b69ab3112// important: this file should not try to import other code from 'isl',
b69ab3113// since it will end up getting duplicated when bundling.
b69ab3114
b69ab3115/**
b69ab3116 * This platform is used when spawned as a standalone webview from `sl web`.
b69ab3117 * We pass messages to the rust side via `external.invoke`,
b69ab3118 * with JSON serialized requests. Rust will respond back with JSON serialized responses.
b69ab3119 * This lets us handle features like alerts, file dialogs, and opening external links
b69ab3120 * which are not implemented in the webview itself.
b69ab3121 */
b69ab3122export const webviewPlatform: Platform = {
b69ab3123 // just act like the browser platform by default, since the app use case is similar
b69ab3124 ...makeBrowserLikePlatformImpl('webview'),
b69ab3125
b69ab3126 openExternalLink(url: string) {
b69ab3127 invoke({cmd: 'openExternal', url});
b69ab3128 },
b69ab3129 confirm(message: string, details?: string): Promise<boolean> {
b69ab3130 return request({cmd: 'confirm', message, details}).then(({ok}) => ok);
b69ab3131 },
b69ab3132 async chooseFile(title: string, multi: boolean): Promise<Array<File>> {
b69ab3133 const response = await request({cmd: 'chooseFile', title, path: '', multi, mediaOnly: true});
b69ab3134 const {files} = response;
b69ab3135 if (!files) {
b69ab3136 return [];
b69ab3137 }
b69ab3138 const result = files.map(value => b64toFile(value.base64Content, value.name));
b69ab3139 return result;
b69ab3140 },
b69ab3141};
b69ab3142
b69ab3143function b64toFile(b64Data: string, filename: string, sliceSize = 512): File {
b69ab3144 const byteCharacters = atob(b64Data);
b69ab3145 const byteArrays = [];
b69ab3146
b69ab3147 for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
b69ab3148 const slice = byteCharacters.slice(offset, offset + sliceSize);
b69ab3149
b69ab3150 const byteNumbers = new Array(slice.length);
b69ab3151 for (let i = 0; i < slice.length; i++) {
b69ab3152 byteNumbers[i] = slice.charCodeAt(i);
b69ab3153 }
b69ab3154
b69ab3155 const byteArray = new Uint8Array(byteNumbers);
b69ab3156 byteArrays.push(byteArray);
b69ab3157 }
b69ab3158
b69ab3159 const blobParts = [new Blob(byteArrays)];
b69ab3160 const file = new File(blobParts, filename);
b69ab3161 return file;
b69ab3162}
b69ab3163
b69ab3164window.islPlatform = webviewPlatform;
b69ab3165
b69ab3166/**
b69ab3167 * Typed commands to communicate from the frontend with the Rust app hosting the webview.
b69ab3168 * This should match the rust types used in webview-app.
b69ab3169 */
b69ab3170type ExternalWebviewCommandsInvoke =
b69ab3171 | {cmd: 'openExternal'; url: string}
b69ab3172 | {cmd: 'confirm'; message: string; details?: string}
b69ab3173 | {cmd: 'chooseFile'; title: string; path: string; multi: boolean; mediaOnly: boolean};
b69ab3174type ExternalWebviewCommandsResponse = (
b69ab3175 | {cmd: 'confirm'; ok: boolean}
b69ab3176 | {
b69ab3177 cmd: 'chooseFile';
b69ab3178 files: Array<{
b69ab3179 name: string;
b69ab3180 base64Content: string;
b69ab3181 }>;
b69ab3182 }
b69ab3183) & {id: number};
b69ab3184
b69ab3185declare global {
b69ab3186 interface Window {
b69ab3187 islWebviewHandleResponse: (response: ExternalWebviewCommandsResponse) => void;
b69ab3188 }
b69ab3189}
b69ab3190
b69ab3191let nextId = 0;
b69ab3192const callbacks: Array<(response: ExternalWebviewCommandsResponse) => void> = [];
b69ab3193window.islWebviewHandleResponse = (response: ExternalWebviewCommandsResponse) => {
b69ab3194 const cb = callbacks[response.id];
b69ab3195 if (cb) {
b69ab3196 cb(response);
b69ab3197 delete callbacks[response.id];
b69ab3198 }
b69ab3199};
b69ab31100
b69ab31101declare const external: {
b69ab31102 invoke(arg: string): Promise<void>;
b69ab31103};
b69ab31104
b69ab31105function invoke(json: ExternalWebviewCommandsInvoke) {
b69ab31106 external.invoke(JSON.stringify({...json, id: nextId++}));
b69ab31107}
b69ab31108
b69ab31109function request<K extends ExternalWebviewCommandsInvoke['cmd']>(
b69ab31110 json: ExternalWebviewCommandsInvoke & {cmd: K},
b69ab31111): Promise<ExternalWebviewCommandsResponse & {cmd: K}> {
b69ab31112 const id = nextId++;
b69ab31113 let resolve: (value: ExternalWebviewCommandsResponse & {cmd: K}) => void;
b69ab31114 const callback = (response: ExternalWebviewCommandsResponse) => {
b69ab31115 resolve(response as ExternalWebviewCommandsResponse & {cmd: K});
b69ab31116 };
b69ab31117 const promise = new Promise<ExternalWebviewCommandsResponse & {cmd: K}>(res => {
b69ab31118 resolve = res;
b69ab31119 });
b69ab31120 external.invoke(JSON.stringify({...json, id}));
b69ab31121 callbacks[id] = callback;
b69ab31122
b69ab31123 return promise;
b69ab31124}