Skip to content

Commit

Permalink
Add communication to web views (Web View Controllers and `postMessage…
Browse files Browse the repository at this point in the history
…ToWebView`) (#1300)
  • Loading branch information
tjcouch-sil authored Nov 13, 2024
2 parents 384f30a + 2a5a503 commit 033d973
Show file tree
Hide file tree
Showing 26 changed files with 2,577 additions and 975 deletions.
132 changes: 93 additions & 39 deletions extensions/src/hello-world/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import papi, { logger } from '@papi/backend';
import papi, { logger, WebViewFactory } from '@papi/backend';
import type {
ExecutionActivationContext,
WebViewContentType,
Expand All @@ -8,7 +8,7 @@ import type {
GetWebViewOptions,
} from '@papi/core';
import { PlatformEventEmitter } from 'platform-bible-utils';
import type { HelloWorldEvent } from 'hello-world';
import type { HelloWorldEvent, HelloWorldProjectWebViewController } from 'hello-world';
import helloWorldReactWebView from './web-views/hello-world.web-view?inline';
import helloWorldReactWebViewStyles from './web-views/hello-world.web-view.scss?inline';
import helloWorldReactWebView2 from './web-views/hello-world-2.web-view?inline';
Expand All @@ -18,6 +18,7 @@ import HelloWorldProjectDataProviderEngineFactory from './models/hello-world-pro
import helloWorldProjectWebView from './web-views/hello-world-project/hello-world-project.web-view?inline';
import helloWorldProjectWebViewStyles from './web-views/hello-world-project/hello-world-project.web-view.scss?inline';
import helloWorldProjectViewerWebView from './web-views/hello-world-project/hello-world-project-viewer.web-view?inline';
import tailwindStyles from './tailwind.css?inline';
import { HTML_COLOR_NAMES } from './util';
import { HELLO_WORLD_PROJECT_INTERFACES } from './models/hello-world-project-data-provider-engine.model';
import { checkDetails, createHelloCheck } from './checks';
Expand Down Expand Up @@ -93,18 +94,26 @@ const reactWebView2Provider: IWebViewProviderWithType = {

// #region Hello World Project Web View, Command, etc.

interface HelloWorldProjectOptions extends GetWebViewOptions {
/** The project ID this viewer should focus on */
projectId?: string;
}

const HELLO_WORLD_PROJECT_WEB_VIEW_TYPE = 'helloWorld.projectWebView';
/** Simple web view provider that provides helloWorld project web views when papi requests them */
const helloWorldProjectWebViewProvider: IWebViewProviderWithType = {
webViewType: 'helloWorld.projectWebView',
async getWebView(
class HelloWorldProjectWebViewFactory extends WebViewFactory<
typeof HELLO_WORLD_PROJECT_WEB_VIEW_TYPE
> {
constructor() {
super(HELLO_WORLD_PROJECT_WEB_VIEW_TYPE);
}

// No need to use `this` and implementing an abstract method so can't be static
// eslint-disable-next-line class-methods-use-this
async getWebViewDefinition(
savedWebView: SavedWebViewDefinition,
getWebViewOptions: HelloWorldProjectViewerOptions,
getWebViewOptions: HelloWorldProjectOptions,
): Promise<WebViewDefinition | undefined> {
if (savedWebView.webViewType !== this.webViewType)
throw new Error(
`${this.webViewType} provider received request to provide a ${savedWebView.webViewType} web view`,
);

const projectId = getWebViewOptions.projectId || savedWebView.projectId || undefined;
return {
title: projectId
Expand All @@ -119,13 +128,38 @@ const helloWorldProjectWebViewProvider: IWebViewProviderWithType = {
styles: helloWorldProjectWebViewStyles,
projectId,
};
},
};
}

interface HelloWorldProjectViewerOptions extends GetWebViewOptions {
projectId: string | undefined;
// No need to use `this` and implementing an abstract method so can't be static
// eslint-disable-next-line class-methods-use-this
async createWebViewController(
webViewDefinition: WebViewDefinition,
webViewNonce: string,
): Promise<HelloWorldProjectWebViewController> {
return {
async focusName(name) {
try {
logger.info(
`Hello World Project Web View Controller ${webViewDefinition.id} received request to focus ${name}`,
);
await papi.webViewProviders.postMessageToWebView(webViewDefinition.id, webViewNonce, {
method: 'focusName',
name,
});
return true;
} catch (e) {
logger.warn(
`Hello World Project Web View Controller ${webViewDefinition.id} threw while running focusName! ${e}`,
);
return false;
}
},
};
}
}

const helloWorldProjectWebViewProvider = new HelloWorldProjectWebViewFactory();

/**
* Function to prompt for a project and open it in the hello world project web view. Registered as a
* command handler.
Expand All @@ -143,8 +177,12 @@ async function openHelloWorldProjectWebView(
}
if (!projectIdForWebView) return undefined;

const options: HelloWorldProjectViewerOptions = { projectId: projectIdForWebView };
return papi.webViews.getWebView(helloWorldProjectWebViewProvider.webViewType, undefined, options);
const options: HelloWorldProjectOptions = { projectId: projectIdForWebView };
return papi.webViews.openWebView(
helloWorldProjectWebViewProvider.webViewType,
undefined,
options,
);
}

// #endregion
Expand All @@ -157,6 +195,11 @@ function selectProjectToDelete(): Promise<string | undefined> {
});
}

interface HelloWorldProjectViewerOptions extends HelloWorldProjectOptions {
/** The id of the web view that opened this viewer if opened from a web view */
callerWebViewId?: string;
}

/**
* Simple web view provider that provides helloWorld project viewer web views when papi requests
* them
Expand Down Expand Up @@ -184,8 +227,12 @@ const helloWorldProjectViewerProvider: IWebViewProviderWithType = {
: 'Hello World Project Viewer',
...savedWebView,
content: helloWorldProjectViewerWebView,
styles: helloWorldProjectWebViewStyles,
styles: tailwindStyles,
projectId,
state: {
...savedWebView.state,
callerWebViewId: getWebViewOptions.callerWebViewId || savedWebView.state?.callerWebViewId,
},
};
},
};
Expand Down Expand Up @@ -275,7 +322,7 @@ export async function activate(context: ExecutionActivationContext): Promise<voi
async (webViewId) => {
let projectId: string | undefined;
if (webViewId) {
const webViewDefinition = await papi.webViews.getSavedWebViewDefinition(webViewId);
const webViewDefinition = await papi.webViews.getOpenWebViewDefinition(webViewId);
projectId = webViewDefinition?.projectId;
}
const projectIdToDelete = projectId ?? (await selectProjectToDelete());
Expand All @@ -293,7 +340,7 @@ export async function activate(context: ExecutionActivationContext): Promise<voi
async (webViewId) => {
let projectId: string | undefined;
if (webViewId) {
const webViewDefinition = await papi.webViews.getSavedWebViewDefinition(webViewId);
const webViewDefinition = await papi.webViews.getOpenWebViewDefinition(webViewId);
projectId = webViewDefinition?.projectId;
}
const projectIdForWebView =
Expand All @@ -306,10 +353,13 @@ export async function activate(context: ExecutionActivationContext): Promise<voi

if (!projectIdForWebView) return undefined;

const options: HelloWorldProjectViewerOptions = { projectId: projectIdForWebView };
return papi.webViews.getWebView(
const options: HelloWorldProjectViewerOptions = {
projectId: projectIdForWebView,
callerWebViewId: webViewId,
};
return papi.webViews.openWebView(
helloWorldProjectViewerProvider.webViewType,
{ type: 'float', position: 'center' },
{ type: 'float', position: 'center', floatSize: { width: 480, height: 320 } },
options,
);
},
Expand All @@ -330,27 +380,27 @@ export async function activate(context: ExecutionActivationContext): Promise<voi
async (newValue) => HTML_COLOR_NAMES.includes(newValue),
);

const helloWorldProjectWebViewProviderPromise = papi.webViewProviders.register(
const helloWorldProjectWebViewProviderPromise = papi.webViewProviders.registerWebViewProvider(
helloWorldProjectWebViewProvider.webViewType,
helloWorldProjectWebViewProvider,
);

const helloWorldProjectViewerProviderPromise = papi.webViewProviders.register(
const helloWorldProjectViewerProviderPromise = papi.webViewProviders.registerWebViewProvider(
helloWorldProjectViewerProvider.webViewType,
helloWorldProjectViewerProvider,
);

const htmlWebViewProviderPromise = papi.webViewProviders.register(
const htmlWebViewProviderPromise = papi.webViewProviders.registerWebViewProvider(
htmlWebViewProvider.webViewType,
htmlWebViewProvider,
);

const reactWebViewProviderPromise = papi.webViewProviders.register(
const reactWebViewProviderPromise = papi.webViewProviders.registerWebViewProvider(
reactWebViewProvider.webViewType,
reactWebViewProvider,
);

const reactWebView2ProviderPromise = papi.webViewProviders.register(
const reactWebView2ProviderPromise = papi.webViewProviders.registerWebViewProvider(
reactWebView2Provider.webViewType,
reactWebView2Provider,
);
Expand All @@ -369,17 +419,6 @@ export async function activate(context: ExecutionActivationContext): Promise<voi
.fetch('https://www.example.com')
.catch((e) => logger.error(`Could not get data from example.com! Reason: ${e}`));

const peopleDataProvider = await papi.dataProviders.get('helloSomeone.people');
if (peopleDataProvider) {
// Test subscribing to a data provider
const unsubGreetings = await peopleDataProvider.subscribeGreeting(
'Bill',
(billGreeting: string | undefined) => logger.debug(`Bill's greeting: ${billGreeting}`),
);

context.registrations.add(unsubGreetings);
}

const checkPromise = papi.commands.sendCommand(
'platformScripture.registerCheck',
checkDetails,
Expand Down Expand Up @@ -417,5 +456,20 @@ export async function activate(context: ExecutionActivationContext): Promise<voi
papi.webViews.getWebView(reactWebViewProvider.webViewType, undefined, { existingId: '?' });
papi.webViews.getWebView(reactWebView2Provider.webViewType, undefined, { existingId: '?' });

try {
const peopleDataProvider = await papi.dataProviders.get('helloSomeone.people');
if (peopleDataProvider) {
// Test subscribing to a data provider
const unsubGreetings = await peopleDataProvider.subscribeGreeting(
'Bill',
(billGreeting: string | undefined) => logger.debug(`Bill's greeting: ${billGreeting}`),
);

context.registrations.add(unsubGreetings);
}
} catch (e) {
logger.error(`Hello world error! Could not get people data provider ${e}`);
}

logger.info('Hello World is finished activating!');
}
28 changes: 25 additions & 3 deletions extensions/src/hello-world/src/types/hello-world.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
declare module 'hello-world' {
// @ts-ignore: TS2307 - Cannot find module '@papi/core' or its corresponding type declarations
import type { DataProviderDataType, MandatoryProjectDataTypes } from '@papi/core';
import type {
DataProviderDataType,
MandatoryProjectDataTypes,
NetworkableObject,
// @ts-ignore: TS2307 - Cannot find module '@papi/core' or its corresponding type declarations
} from '@papi/core';
import type { IBaseProjectDataProvider } from 'papi-shared-types';

export type HelloWorldProjectDataTypes = MandatoryProjectDataTypes & {
Expand Down Expand Up @@ -37,6 +41,16 @@ declare module 'hello-world' {
times: number;
};

export type HelloWorldProjectWebViewController = NetworkableObject<{
/**
* Attempts to focus a specific name in the web view
*
* @returns `true` if the name is in the project associated with this web view; `false`
* otherwise
*/
focusName: (name: string) => Promise<boolean>;
}>;

/** All html color names according to https://htmlcolorcodes.com/color-names/ */
type HTMLColorNames =
| 'IndianRed'
Expand Down Expand Up @@ -183,7 +197,11 @@ declare module 'hello-world' {
}

declare module 'papi-shared-types' {
import type { IHelloWorldProjectDataProvider, HTMLColorNames } from 'hello-world';
import type {
IHelloWorldProjectDataProvider,
HelloWorldProjectWebViewController,
HTMLColorNames,
} from 'hello-world';

export interface CommandHandlers {
'helloWorld.helloWorld': () => string;
Expand Down Expand Up @@ -252,4 +270,8 @@ declare module 'papi-shared-types' {
*/
'helloWorld.headerColor': HTMLColorNames;
}

export interface WebViewControllers {
'helloWorld.projectWebView': HelloWorldProjectWebViewController;
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,55 @@
import { WebViewProps } from '@papi/core';
import { useProjectData, useProjectSetting } from '@papi/frontend/react';
import { useProjectData, useProjectSetting, useWebViewController } from '@papi/frontend/react';
import { Button } from 'platform-bible-react';
import { CSSProperties, useMemo } from 'react';

const namesDefault: string[] = [];

globalThis.webViewComponent = function HelloWorldProjectViewer({ projectId }: WebViewProps) {
globalThis.webViewComponent = function HelloWorldProjectViewer({
projectId,
useWebViewState,
}: WebViewProps) {
const [callerWebViewId] = useWebViewState<string | undefined>('callerWebViewId', undefined);

const callerWebViewController = useWebViewController(
'helloWorld.projectWebView',
callerWebViewId,
);

const [names] = useProjectData('helloWorld', projectId).Names(undefined, namesDefault);

const [headerSize] = useProjectSetting(projectId, 'helloWorld.headerSize', 15);

const [headerColor] = useProjectSetting(projectId, 'helloWorld.headerColor', 'Black');

const headerStyle = useMemo<CSSProperties>(
() => ({ fontSize: `${headerSize}pt`, color: headerColor }),
[headerSize, headerColor],
);
const headerStyle = useMemo<CSSProperties>(() => {
const colorPropertyName = callerWebViewController ? 'backgroundColor' : 'color';
return { fontSize: `${headerSize}pt`, [colorPropertyName]: headerColor };
}, [callerWebViewController, headerSize, headerColor]);

return (
<div className="top">
{names.map((name) => (
<div style={headerStyle}>Hello, {name}!</div>
))}
<div className="tw-m-3 [&>*]:tw-mb-3">
{callerWebViewController && (
<div>Click a name to focus it on the Hello World Project web view!</div>
)}
{names.map((name) => {
const textContent = `Hello, ${name}!`;
return callerWebViewController ? (
<div key={name}>
<Button
className="tw-text-foreground"
style={headerStyle}
onClick={() => callerWebViewController?.focusName(name)}
>
{textContent}
</Button>
</div>
) : (
<div key={name} style={headerStyle}>
{textContent}
</div>
);
})}
</div>
);
};
Loading

0 comments on commit 033d973

Please sign in to comment.