Skip to content

Commit

Permalink
Create WebViewProvider API, allowing webviews to persist across refre…
Browse files Browse the repository at this point in the history
…shes (#225)
  • Loading branch information
tjcouch-sil authored Jun 13, 2023
2 parents e32552e + 1d3f825 commit ba1dee3
Show file tree
Hide file tree
Showing 28 changed files with 1,874 additions and 411 deletions.
82 changes: 70 additions & 12 deletions extensions/lib/hello-someone/hello-someone.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import papi from 'papi';
import type { WebViewContentType } from 'shared/data/web-view.model';
import type {
WebViewContentType,
WebViewDefinition,
SavedWebViewDefinition,
} from 'shared/data/web-view.model';
import { UnsubscriberAsync } from 'shared/utils/papi-util';
import type IDataProvider from 'shared/models/data-provider.interface';
import type { IWebViewProvider } from 'shared/models/web-view-provider.model';
import type { ExecutionActivationContext } from 'extension-host/extension-types/extension-activation-context.model';
// @ts-expect-error ts(1192) this file has no default export; the text is exported by rollup
import helloSomeoneHtmlWebView from './hello-someone.web-view.ejs';

Expand Down Expand Up @@ -45,22 +51,38 @@ const greetingsDataProviderEngine = {
},
};

export async function activate() {
const peopleWebViewType = 'hello-someone.people-viewer';
const peopleWebViewIdKey = 'people-web-view-id';

/**
* Simple web view provider that provides People web views when papi requests them
*/
const peopleWebViewProvider: IWebViewProvider = {
async getWebView(savedWebView: SavedWebViewDefinition): Promise<WebViewDefinition | undefined> {
if (savedWebView.webViewType !== peopleWebViewType)
throw new Error(
`${peopleWebViewType} provider received request to provide a ${savedWebView.webViewType} web view`,
);
return {
...savedWebView,
title: 'People',
contentType: 'html' as WebViewContentType.HTML,
content: helloSomeoneHtmlWebView,
};
},
};

export async function activate(context: ExecutionActivationContext) {
logger.info('Hello Someone is activating!');

const greetingsDataProviderPromise = papi.dataProvider.registerEngine(
'hello-someone.greetings',
greetingsDataProviderEngine,
);

await papi.webViews.addWebView(
{
id: 'Hello Someone',
title: 'Hello Someone HTML',
contentType: 'html' as WebViewContentType.HTML,
content: helloSomeoneHtmlWebView,
},
{ type: 'panel', direction: 'top' },
const peopleWebViewProviderPromise = papi.webViews.registerWebViewProvider(
peopleWebViewType,
peopleWebViewProvider,
);

const unsubPromises: Promise<UnsubscriberAsync>[] = [
Expand All @@ -80,11 +102,47 @@ export async function activate() {
),
];

// For now, let's just make things easy and await the data provider promise at the end so we don't hold everything else up
// Create a webview or get the existing webview if ours already exists
// Note: here, we are storing a created webview's id when we create it, and using that id on
// `existingId` to look specifically for the webview that we previously created if we have ever
// created one in a previous session. This means that, if someone else creates a people web view,
// it will be distinct from this one. We are creating our own web view here. See `hello-world.ts`
// for an example of getting any webview with the specified `webViewType`

// Get existing webview id if we previously created a webview for this type
let existingPeopleWebViewId: string | undefined;
try {
existingPeopleWebViewId = await papi.storage.readUserData(
context.executionToken,
peopleWebViewIdKey,
);
} catch (e) {
existingPeopleWebViewId = undefined;
}

// Get the existing web view if one exists or create a new one
const peopleWebViewId = await papi.webViews.getWebView(
peopleWebViewType,
{ type: 'panel', direction: 'top' },
{ existingId: existingPeopleWebViewId },
);

// Save newly acquired webview id
await papi.storage.writeUserData(
context.executionToken,
peopleWebViewIdKey,
peopleWebViewId || '',
);

// For now, let's just make things easy and await the registration promises at the end so we don't hold everything else up
const greetingsDataProvider = await greetingsDataProviderPromise;
const peopleWebViewProviderResolved = await peopleWebViewProviderPromise;

const combinedUnsubscriber: UnsubscriberAsync = papi.util.aggregateUnsubscriberAsyncs(
(await Promise.all(unsubPromises)).concat([greetingsDataProvider.dispose]),
(await Promise.all(unsubPromises)).concat([
greetingsDataProvider.dispose,
peopleWebViewProviderResolved.dispose,
]),
);
logger.info('Hello Someone is finished activating!');
return combinedUnsubscriber;
Expand Down
11 changes: 11 additions & 0 deletions extensions/lib/hello-someone/hello-someone.web-view.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
<div id="any-greetings-update-count">Any Greetings Updates: 0</div>
<div id="bill-any-greetings-update-count">Any Greetings Updates (via Bill): 0</div>
<div id="bill-greetings-update-count">Bill Greetings Updates: 0</div>
<br />
<div><button id="new-web-view-button" type="button">Create a new People WebView!</button></div>
<script>
// Enable webview debugging
console.debug('Debug Hello Someone WebView');
Expand Down Expand Up @@ -121,6 +123,15 @@
`Bill Greetings Updates: ${billGreetingsUpdateCount}`,
);
});
// Attach handler for new-web-view-button
const newWebViewButton = document.getElementById('new-web-view-button');
newWebViewButton.addEventListener('click', async () => {
const webViewId = await papi.webViews.getWebView('hello-someone.people-viewer', {
type: 'float',
});
print(`New People webview id: ${webViewId}`);
});
}
if (document.readyState === 'loading')
Expand Down
88 changes: 72 additions & 16 deletions extensions/lib/hello-world/hello-world.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import papi from 'papi';
import { UnsubscriberAsync } from 'shared/utils/papi-util';
import type { WebViewContentType } from 'shared/data/web-view.model';
import { GreetingsDataProvider } from '@extensions/hello-someone/hello-someone';
import type {
WebViewContentType,
WebViewDefinition,
SavedWebViewDefinition,
} from 'shared/data/web-view.model';
import type { GreetingsDataProvider } from '@extensions/hello-someone/hello-someone';
import type { IWebViewProvider } from 'shared/models/web-view-provider.model';
// @ts-expect-error ts(1192) this file has no default export; the text is exported by rollup
import helloWorldReactWebView from './hello-world.web-view';
import helloWorldReactWebViewStyles from './hello-world.web-view.scss?inline';
Expand All @@ -14,9 +19,59 @@ logger.info('Hello world is importing!');

const unsubscribers: UnsubscriberAsync[] = [];

const htmlWebViewType = 'hello-world.html';

/**
* Simple web view provider that provides sample html web views when papi requests them
*/
const htmlWebViewProvider: IWebViewProvider = {
async getWebView(savedWebView: SavedWebViewDefinition): Promise<WebViewDefinition | undefined> {
if (savedWebView.webViewType !== htmlWebViewType)
throw new Error(
`${htmlWebViewType} provider received request to provide a ${savedWebView.webViewType} web view`,
);
return {
...savedWebView,
title: 'Hello World HTML',
contentType: 'html' as WebViewContentType.HTML,
content: helloWorldHtmlWebView,
};
},
};

const reactWebViewType = 'hello-world.react';

/**
* Simple web view provider that provides React web views when papi requests them
*/
const reactWebViewProvider: IWebViewProvider = {
async getWebView(savedWebView: SavedWebViewDefinition): Promise<WebViewDefinition | undefined> {
if (savedWebView.webViewType !== reactWebViewType)
throw new Error(
`${reactWebViewType} provider received request to provide a ${savedWebView.webViewType} web view`,
);
return {
...savedWebView,
title: 'Hello World React',
content: helloWorldReactWebView,
styles: helloWorldReactWebViewStyles,
};
},
};

export async function activate(): Promise<UnsubscriberAsync> {
logger.info('Hello world is activating!');

const htmlWebViewProviderPromise = papi.webViews.registerWebViewProvider(
htmlWebViewType,
htmlWebViewProvider,
);

const reactWebViewProviderPromise = papi.webViews.registerWebViewProvider(
reactWebViewType,
reactWebViewProvider,
);

const unsubPromises: Promise<UnsubscriberAsync>[] = [
papi.commands.registerCommand('hello-world.hello-world', () => {
return 'Hello world!';
Expand All @@ -32,19 +87,13 @@ export async function activate(): Promise<UnsubscriberAsync> {
.then((scr) => logger.info(scr.text.replace(/\n/g, '')))
.catch((e) => logger.error(`Could not get Scripture from bible-api! Reason: ${e}`));

papi.webViews.addWebView({
id: 'Hello World HTML',
title: 'Hello World HTML',
contentType: 'html' as WebViewContentType.HTML,
content: helloWorldHtmlWebView,
});

await papi.webViews.addWebView({
id: 'Hello World React',
title: 'Hello World React',
content: helloWorldReactWebView,
styles: helloWorldReactWebViewStyles,
});
// Create webviews or get an existing webview if one already exists for this type
// Note: here, we are using `existingId: '?'` to indicate we do not want to create a new webview
// if one already exists. The webview that already exists could have been created by anyone
// anywhere; it just has to match `webViewType`. See `hello-someone.ts` for an example of keeping
// an existing webview that was specifically created by `hello-someone`.
papi.webViews.getWebView(htmlWebViewType, undefined, { existingId: '?' });
papi.webViews.getWebView(reactWebViewType, undefined, { existingId: '?' });

const greetingsDataProvider = await papi.dataProvider.get<GreetingsDataProvider>(
'hello-someone.greetings',
Expand All @@ -59,8 +108,15 @@ export async function activate(): Promise<UnsubscriberAsync> {
unsubscribers.push(unsubGreetings);
}

// For now, let's just make things easy and await the registration promises at the end so we don't hold everything else up
const htmlWebViewProviderResolved = await htmlWebViewProviderPromise;
const reactWebViewProviderResolved = await reactWebViewProviderPromise;

const combinedUnsubscriber: UnsubscriberAsync = papi.util.aggregateUnsubscriberAsyncs(
await Promise.all(unsubPromises),
(await Promise.all(unsubPromises)).concat([
htmlWebViewProviderResolved.dispose,
reactWebViewProviderResolved.dispose,
]),
);
logger.info('Hello World is finished activating!');
return combinedUnsubscriber;
Expand Down
Loading

0 comments on commit ba1dee3

Please sign in to comment.