Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import papi, { WebViewFactory } from '@papi/backend';
import type { OpenWebViewOptions, SavedWebViewDefinition, WebViewDefinition } from '@papi/core';
import type { CommentListWebViewController } from 'legacy-comment-manager';
import commentListWebView from './comment-list.web-view?inline';
import tailwindStyles from './tailwind.css?inline';
import {
COMMENT_LIST_PANEL_WEB_VIEW_TYPE,
resolveCommentListPanelProjectId,
} from './comment-list-panel.utils';
import { createCommentListWebViewController } from './comment-list-web-view-controller.util';

/** Options accepted when opening the fixed Column 3 Comment List panel. */
export interface CommentListPanelOptions extends OpenWebViewOptions {
projectId?: string;
}

/**
* Pending projectId consumed by {@link CommentListPanelWebViewFactory.getWebViewDefinition} after
* `reloadWebView`. Set via {@link setPendingCommentListPanelProjectId}, called by
* `openCommentListPanel` in `main.ts` immediately before it calls `reloadWebView`.
*
* Note: `undefined` doubles as the "no pending value" sentinel, so a pending `undefined` cannot
* clear an already-open panel's project — resolution falls back to the saved projectId. This is an
* accepted limitation; see `openCommentListPanel` in `main.ts`.
*/
let pendingProjectId: string | undefined;

/**
* Sets the projectId {@link CommentListPanelWebViewFactory.getWebViewDefinition} will consume (and
* clear) the next time it runs — used by `openCommentListPanel` to forward a project change through
* `reloadWebView`, which has no other way to pass extra data into `getWebView`.
*/
export function setPendingCommentListPanelProjectId(projectId: string | undefined): void {
pendingProjectId = projectId;
}

/**
* WebView Factory for the fixed Column 3 Comment List panel, with controller support so external
* callers can bring it to front and select a thread in it without a destructive reload (see
* `selectCommentThreadInPanel` in `main.ts`).
*/
export class CommentListPanelWebViewFactory extends WebViewFactory<
typeof COMMENT_LIST_PANEL_WEB_VIEW_TYPE
> {
constructor() {
super(COMMENT_LIST_PANEL_WEB_VIEW_TYPE);
}

override async getWebViewDefinition(
savedWebView: SavedWebViewDefinition,
openWebViewOptions: CommentListPanelOptions,
): Promise<WebViewDefinition | undefined> {
if (savedWebView.webViewType !== COMMENT_LIST_PANEL_WEB_VIEW_TYPE)
throw new Error(
`${COMMENT_LIST_PANEL_WEB_VIEW_TYPE} provider received request to provide a ${savedWebView.webViewType} web view`,
);

const projectId = resolveCommentListPanelProjectId(
pendingProjectId,
openWebViewOptions.projectId,
savedWebView.projectId,
);
pendingProjectId = undefined;

const title = await papi.localization.getLocalizedString({
localizeKey: '%webView_legacyCommentManager_commentListPanel_title%',
});

// Re-read every call so mode changes are picked up at open/replace/restore time.
const interfaceMode = await papi.settings.get('platform.interfaceMode');

return {
...savedWebView,
title,
projectId,
content: commentListWebView,
styles: tailwindStyles,
// In simple mode, force the comments panel to scroll group 0 so it stays verse-synced with
// the Scripture editor (which is also forced to 0 in simple mode). Power mode preserves the
// saved value. Without this, a persisted non-zero scroll group (e.g. set while in power
// mode) would survive into simple mode and detach the panel from the editor's navigation.
scrollGroupScrRef: interfaceMode === 'simple' ? 0 : savedWebView.scrollGroupScrRef,
};
}

override async createWebViewController(
webViewDefinition: WebViewDefinition,
webViewNonce: string,
): Promise<CommentListWebViewController> {
return createCommentListWebViewController(webViewDefinition, webViewNonce);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import papi, { logger } from '@papi/backend';
import type { WebViewDefinition } from '@papi/core';
import type {
CommentFilters,
CommentListWebViewController,
ScopeFilter,
} from 'legacy-comment-manager';
import { serialize } from 'platform-bible-utils';
import { CommentListWebViewMessage } from './comment-list-messages.model';

/**
* Builds the WebView Controller shared by both comment-list webview types. The editor-anchored list
* (`legacyCommentManager.commentList`) and the fixed Column 3 panel
* (`legacyCommentManager.commentListPanel`) render the exact same web view component and speak the
* same `selectThread`/`setFilters` message protocol, so one controller implementation serves both.
*/
export function createCommentListWebViewController(
webViewDefinition: WebViewDefinition,
webViewNonce: string,
): CommentListWebViewController {
const postToWebView = (message: CommentListWebViewMessage) =>
papi.webViewProviders.postMessageToWebView(webViewDefinition.id, webViewNonce, message);

return {
async selectThread(threadId: string): Promise<void> {
logger.debug(
`Comment List WebView Controller ${webViewDefinition.id} received request to selectThread ${threadId}`,
);
await postToWebView({ method: 'selectThread', threadId });
},
async setFilters(filters?: Partial<CommentFilters>, scopeFilter?: ScopeFilter): Promise<void> {
logger.debug(
`Comment List WebView Controller ${webViewDefinition.id} received setFilters ${serialize({ filters, scopeFilter })}`,
);
await postToWebView({ method: 'setFilters', filters, scopeFilter });
},
async dispose(): Promise<boolean> {
return true;
},
};
}
163 changes: 81 additions & 82 deletions extensions/src/legacy-comment-manager/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@ import type {
import { serialize } from 'platform-bible-utils';
import commentListWebView from './comment-list.web-view?inline';
import tailwindStyles from './tailwind.css?inline';
import { CommentListWebViewMessage } from './comment-list-messages.model';
import { SCOPE_FILTER_CURRENT_CHAPTER, UNFILTERED } from './comment-list-filters.model';
import {
LEGACY_COMMENT_USJ_PDPF_ID,
LegacyCommentManagerUsjProjectDataProviderEngineFactory,
} from './project-data-provider/legacy-comment-manager-usj-pdpef.model';
import { LEGACY_COMMENT_USJ_PROJECT_INTERFACES } from './project-data-provider/legacy-comment-manager-usj-pdpe.model';
import { COMMENT_LIST_PANEL_WEB_VIEW_TYPE } from './comment-list-panel.utils';
import { createCommentListWebViewController } from './comment-list-web-view-controller.util';
import {
COMMENT_LIST_PANEL_WEB_VIEW_TYPE,
resolveCommentListPanelProjectId,
} from './comment-list-panel.utils';
CommentListPanelOptions,
CommentListPanelWebViewFactory,
setPendingCommentListPanelProjectId,
} from './comment-list-panel-web-view.factory';

const commentListWebViewType = 'legacyCommentManager.commentList';
const commentListPanelWebViewType = COMMENT_LIST_PANEL_WEB_VIEW_TYPE;
Expand Down Expand Up @@ -118,31 +120,7 @@ class CommentListWebViewFactory extends WebViewFactory<typeof commentListWebView
webViewDefinition: WebViewDefinition,
webViewNonce: string,
): Promise<CommentListWebViewController> {
// Single message channel for this controller, closing over the id/nonce so the two methods
// can't drift on the plumbing.
const postToWebView = (message: CommentListWebViewMessage) =>
papi.webViewProviders.postMessageToWebView(webViewDefinition.id, webViewNonce, message);

return {
async selectThread(threadId: string): Promise<void> {
logger.debug(
`Comment List WebView Controller ${webViewDefinition.id} received request to selectThread ${threadId}`,
);
await postToWebView({ method: 'selectThread', threadId });
},
async setFilters(
filters?: Partial<CommentFilters>,
scopeFilter?: ScopeFilter,
): Promise<void> {
logger.debug(
`Comment List WebView Controller ${webViewDefinition.id} received setFilters ${serialize({ filters, scopeFilter })}`,
);
await postToWebView({ method: 'setFilters', filters, scopeFilter });
},
async dispose(): Promise<boolean> {
return true;
},
};
return createCommentListWebViewController(webViewDefinition, webViewNonce);
}
}

Expand All @@ -152,57 +130,7 @@ const commentListWebViewProvider: IWebViewProvider = new CommentListWebViewFacto

// #region Comment List Panel WebView (Column 3 fixed tab)

interface CommentListPanelOptions extends OpenWebViewOptions {
projectId?: string;
}

/**
* Pending projectId consumed by commentListPanelProvider.getWebView() after reloadWebView().
*
* Note: `undefined` doubles as the "no pending value" sentinel, so a pending `undefined` cannot
* clear an already-open panel's project — resolution falls back to the saved projectId. This is an
* accepted limitation; see {@link openCommentListPanel}.
*/
let currentCommentListPanelProjectId: string | undefined;

const commentListPanelProvider: IWebViewProvider = {
async getWebView(
savedWebView: SavedWebViewDefinition,
openWebViewOptions: CommentListPanelOptions,
): Promise<WebViewDefinition | undefined> {
if (savedWebView.webViewType !== commentListPanelWebViewType)
throw new Error(
`${commentListPanelWebViewType} provider received request to provide a ${savedWebView.webViewType} web view`,
);

const projectId = resolveCommentListPanelProjectId(
currentCommentListPanelProjectId,
openWebViewOptions.projectId,
savedWebView.projectId,
);
currentCommentListPanelProjectId = undefined;

const title = await papi.localization.getLocalizedString({
localizeKey: '%webView_legacyCommentManager_commentListPanel_title%',
});

// Re-read every call so mode changes are picked up at open/replace/restore time.
const interfaceMode = await papi.settings.get('platform.interfaceMode');

return {
...savedWebView,
title,
projectId,
content: commentListWebView,
styles: tailwindStyles,
// In simple mode, force the comments panel to scroll group 0 so it stays verse-synced with
// the Scripture editor (which is also forced to 0 in simple mode). Power mode preserves the
// saved value. Without this, a persisted non-zero scroll group (e.g. set while in power
// mode) would survive into simple mode and detach the panel from the editor's navigation.
scrollGroupScrRef: interfaceMode === 'simple' ? 0 : savedWebView.scrollGroupScrRef,
};
},
};
const commentListPanelWebViewProvider: IWebViewProvider = new CommentListPanelWebViewFactory();

/**
* Opens or updates the fixed Comment List Panel in Column 3 for the given project. If the panel is
Expand All @@ -227,7 +155,7 @@ async function openCommentListPanel(projectId: string | undefined): Promise<stri
);

if (existingId) {
currentCommentListPanelProjectId = projectId;
setPendingCommentListPanelProjectId(projectId);
return papi.webViews.reloadWebView(commentListPanelWebViewType, existingId, {
bringToFront: false, // Don't steal focus from the Scripture editor on project switch
});
Expand All @@ -238,6 +166,47 @@ async function openCommentListPanel(projectId: string | undefined): Promise<stri
return papi.webViews.openWebView(commentListPanelWebViewType, { type: 'tab' }, openOptions);
}

/**
* Selects/scrolls to a specific thread in the fixed Column 3 Comment List panel — without reloading
* the panel. Reloading (as {@link openCommentListPanel} does) remounts the panel's React root, which
* would discard any in-progress inline edit; this function assumes the panel is already showing the
* right project (it does not accept a `projectId`) and only changes which thread is selected and,
* optionally, which tab is in front.
*
* This implements the `legacyCommentManager.selectCommentThreadInPanel` command.
*
* @param threadId The ID of the thread to select and scroll to in the panel
* @param bringToFront Whether to also bring the panel's tab to the front
* @returns The webView ID of the panel, or `undefined` if it isn't open in the current layout
*/
async function selectCommentThreadInPanel(
threadId: string,
bringToFront: boolean,
): Promise<string | undefined> {
// Same existingId: '?' probe openCommentListPanel uses to find the singleton Column 3 panel.
const panelWebViewId = await papi.webViews.openWebView(
commentListPanelWebViewType,
{ type: 'tab' },
{ existingId: '?', createNewIfNotFound: false, bringToFront },
);
if (!panelWebViewId) {
throw new Error('Comment List Panel is not open in the current layout');
}

const panelController = await papi.webViews.getWebViewController(
commentListPanelWebViewType,
panelWebViewId,
);
if (!panelController) {
throw new Error(
`Could not get WebView Controller for comment list panel WebView ${panelWebViewId} to select thread ${threadId}`,
);
}

await panelController.selectThread(threadId);
return panelWebViewId;
}

// #endregion Comment List Panel WebView

/**
Expand Down Expand Up @@ -397,7 +366,7 @@ export async function activate(context: ExecutionActivationContext): Promise<voi

const commentListPanelWebViewProviderPromise = papi.webViewProviders.registerWebViewProvider(
commentListPanelWebViewType,
commentListPanelProvider,
commentListPanelWebViewProvider,
);

const openCommentListPanelPromise = papi.commands.registerCommand(
Expand All @@ -423,6 +392,35 @@ export async function activate(context: ExecutionActivationContext): Promise<voi
},
);

const selectCommentThreadInPanelPromise = papi.commands.registerCommand(
'legacyCommentManager.selectCommentThreadInPanel',
selectCommentThreadInPanel,
{
method: {
summary: 'Select a thread in the fixed Comment List panel in Column 3',
params: [
{
name: 'threadId',
required: true,
summary: 'The ID of the thread to select and scroll to in the panel',
schema: { type: 'string' },
},
{
name: 'bringToFront',
required: true,
summary: "Whether to also bring the panel's tab to the front",
schema: { type: 'boolean' },
},
],
result: {
name: 'return value',
summary: 'The webView ID of the panel',
schema: { type: 'string' },
},
},
},
);

// Subscribe to web view updates to clean up tracking when comment list is closed
const webViewUpdateUnsub = papi.webViews.onDidCloseWebView((event) => {
// Check if this was one of our tracked comment lists
Expand Down Expand Up @@ -519,6 +517,7 @@ export async function activate(context: ExecutionActivationContext): Promise<voi
await commentListPanelWebViewProviderPromise,
await openCommentListPromise,
await openCommentListPanelPromise,
await selectCommentThreadInPanelPromise,
await commentsUsjPdpefPromise,
webViewUpdateUnsub,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -590,9 +590,26 @@ declare module 'papi-shared-types' {
'legacyCommentManager.openCommentListPanel': (
projectId?: string | undefined,
) => Promise<string | undefined>;

/**
* Selects/scrolls to a specific thread in the fixed Column 3 Comment List panel, without
* reloading it. Optionally also brings the panel's tab to the front.
*
* @param threadId The ID of the thread to select and scroll to in the panel
* @param bringToFront Whether to also bring the panel's tab to the front. Pass `false` for
* background confirmation (e.g. after inserting a comment, so the user's current Column 3 tab
* isn't interrupted) or `true` for an explicit "show me this comment" navigation.
* @returns The webView ID of the panel, or `undefined` if it isn't open in the current layout
* @throws If the panel's WebView Controller cannot be obtained to apply the selection
*/
'legacyCommentManager.selectCommentThreadInPanel': (
threadId: string,
bringToFront: boolean,
) => Promise<string | undefined>;
}

export interface WebViewControllers {
'legacyCommentManager.commentList': CommentListWebViewController;
'legacyCommentManager.commentListPanel': CommentListWebViewController;
}
}
Loading
Loading