Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create WebViewProvider API, allowing webviews to persist across refreshes #225

Merged
merged 18 commits into from
Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b439b05
Changed addWebView command to request
tjcouch-sil May 26, 2023
a3448b2
Started working on web view provider service
tjcouch-sil May 26, 2023
54e1e71
Merge remote-tracking branch 'origin/main' into 177-web-view-provider
tjcouch-sil Jun 2, 2023
dcd861a
Finished first pass on web view provider service, other cleanup
tjcouch-sil Jun 2, 2023
02ae9c7
Refactored docking layout types to support save/load concepts
tjcouch-sil Jun 2, 2023
6e237b2
Get webview definition from web view provider instead of passing in w…
tjcouch-sil Jun 6, 2023
207be26
Moved registering web view provider to web view service
tjcouch-sil Jun 6, 2023
2620d51
Implemented existingId addWebView option for persisting webview
tjcouch-sil Jun 6, 2023
c2e557a
Removed content and styles from onDidAddWebView events
tjcouch-sil Jun 6, 2023
2cb8083
Updated papi.d.ts to match removing content and styles from onDidAddW…
tjcouch-sil Jun 6, 2023
1d231e1
Persist web views between reloads
tjcouch-sil Jun 7, 2023
67e7809
Added existingId: '*' that looks for a webview with same type and any id
tjcouch-sil Jun 7, 2023
fe9ec11
Updated rc-dock to 3.2.18 and re-wrote patch
tjcouch-sil Jun 8, 2023
ac77bb7
Changed load/save panel function names to load/save tab
tjcouch-sil Jun 12, 2023
dbb49ed
Renamed many things, reworked a couple things, changed '*' to '?' on …
tjcouch-sil Jun 12, 2023
71b8e41
Added JSDocs explaining how to use webview stuff
tjcouch-sil Jun 13, 2023
2a4e79a
Merge remote-tracking branch 'origin/main' into 177-web-view-provider
tjcouch-sil Jun 13, 2023
1d3f825
Fix test, code review
tjcouch-sil Jun 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 68 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,
WebViewDefinitionSerialized,
} 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,37 @@ const greetingsDataProviderEngine = {
},
};

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

const peopleWebViewProvider: IWebViewProvider = {
async getWebView(
serializedWebView: WebViewDefinitionSerialized,
): Promise<WebViewDefinition | undefined> {
if (serializedWebView.webViewType !== peopleWebViewType)
throw new Error(
`${peopleWebViewType} provider received request to provide a ${serializedWebView.webViewType} web view`,
);
return {
...serializedWebView,
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 +101,46 @@ 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;
}

const peopleWebViewId = await papi.webViews.addWebView(
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.addWebView('hello-someone.people-viewer', {
type: 'float',
});
print(`New People webview id: ${webViewId}`);
});
}

if (document.readyState === 'loading')
Expand Down
84 changes: 69 additions & 15 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 type {
WebViewContentType,
WebViewDefinition,
WebViewDefinitionSerialized,
} from 'shared/data/web-view.model';
import { 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,57 @@ logger.info('Hello world is importing!');

const unsubscribers: UnsubscriberAsync[] = [];

const htmlWebViewType = 'hello-world.html';

const htmlWebViewProvider: IWebViewProvider = {
async getWebView(
serializedWebView: WebViewDefinitionSerialized,
): Promise<WebViewDefinition | undefined> {
if (serializedWebView.webViewType !== htmlWebViewType)
throw new Error(
`${htmlWebViewType} provider received request to provide a ${serializedWebView.webViewType} web view`,
);
return {
...serializedWebView,
title: 'Hello World HTML',
contentType: 'html' as WebViewContentType.HTML,
content: helloWorldHtmlWebView,
};
},
};

const reactWebViewType = 'hello-world.react';

const reactWebViewProvider: IWebViewProvider = {
async getWebView(
serializedWebView: WebViewDefinitionSerialized,
): Promise<WebViewDefinition | undefined> {
if (serializedWebView.webViewType !== reactWebViewType)
throw new Error(
`${reactWebViewType} provider received request to provide a ${serializedWebView.webViewType} web view`,
);
return {
...serializedWebView,
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 +85,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.addWebView(htmlWebViewType, undefined, { existingId: '*' });
papi.webViews.addWebView(reactWebViewType, undefined, { existingId: '*' });

const greetingsDataProvider = await papi.dataProvider.get<GreetingsDataProvider>(
'hello-someone.greetings',
Expand All @@ -59,8 +106,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