mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 08:15:38 +00:00
fix: keep the composer's proofreading off the plugin store, and safe with a stand-in editor #1397
This commit is contained in:
@@ -194,21 +194,21 @@ export function createTextCheckExtension(options: () => TextCheckOptions) {
|
||||
|
||||
/** Put the checkers' issues on the editor, replacing what was there. */
|
||||
export function setTextIssues(editor: Editor, issues: TextIssueRange[]): void {
|
||||
if (editor.isDestroyed) return;
|
||||
if (editor.isDestroyed || !editor.state) return;
|
||||
const { tr } = editor.state;
|
||||
editor.view.dispatch(tr.setMeta(textCheckKey, { issues } satisfies Meta).setMeta('addToHistory', false));
|
||||
}
|
||||
|
||||
/** Mark the issue whose popup is open (or none). */
|
||||
export function setActiveIssue(editor: Editor, key: string | null): void {
|
||||
if (editor.isDestroyed) return;
|
||||
if (editor.isDestroyed || !editor.state) return;
|
||||
const { tr } = editor.state;
|
||||
editor.view.dispatch(tr.setMeta(textCheckKey, { active: key } satisfies Meta).setMeta('addToHistory', false));
|
||||
}
|
||||
|
||||
/** The issues drawn now, in document order, with their current ranges. */
|
||||
export function drawnIssues(state: EditorState): TextIssueRange[] {
|
||||
const set = textCheckKey.getState(state)?.decos;
|
||||
export function drawnIssues(state: EditorState | undefined): TextIssueRange[] {
|
||||
const set = state ? textCheckKey.getState(state)?.decos : undefined;
|
||||
if (!set) return [];
|
||||
return set.find().map((d) => {
|
||||
const spec = d.spec as IssueSpec;
|
||||
|
||||
@@ -18,7 +18,7 @@ import { actionsFor, subscribeActions } from "@/lib/plugin-sandbox/actions";
|
||||
import { all as allActivePlugins, subscribe as subscribeActivePlugins } from "@/lib/plugin-sandbox/registry";
|
||||
import { replaceTextareaRange, type PlainOwnOptions } from "@/lib/plugin-sandbox/composer-edit";
|
||||
import { usePolicyStore } from "@/stores/policy-store";
|
||||
import { usePluginStore } from "@/stores/plugin-store";
|
||||
import { knownPlugins, subscribeKnownPlugins } from "@/lib/plugin-sandbox/known-plugins";
|
||||
import type { InstalledPlugin } from "@/lib/plugin-types";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
import {
|
||||
@@ -48,7 +48,7 @@ export function policySettled(s: Pick<ReturnType<typeof usePolicyStore.getState>
|
||||
/** Plugins turned on that aren't running yet (still loading). */
|
||||
function startingPlugins(running: InstalledPlugin[]): InstalledPlugin[] {
|
||||
const up = new Set(running.map((p) => p.id));
|
||||
return usePluginStore.getState().plugins.filter((p) => p.enabled && p.status !== 'error' && p.status !== 'disabled' && !up.has(p.id));
|
||||
return knownPlugins().filter((p) => p.enabled && p.status !== 'error' && p.status !== 'disabled' && !up.has(p.id));
|
||||
}
|
||||
|
||||
// The per-composer switch outlives a remount (pop-out, layout switch).
|
||||
@@ -148,7 +148,7 @@ export function useProofreading({ composerId, sessionKey, editor, textareaRef, p
|
||||
subscribeCheckers(() => p.sync()),
|
||||
subscribeActions(() => p.gateChanged()),
|
||||
subscribeActivePlugins(() => p.gateChanged()),
|
||||
usePluginStore.subscribe(() => p.gateChanged()),
|
||||
subscribeKnownPlugins(() => p.gateChanged()),
|
||||
usePolicyStore.subscribe(() => p.gateChanged()),
|
||||
useSettingsStore.subscribe((s, prev) => {
|
||||
if (s.writingLanguage !== prev.writingLanguage) p.edited("now");
|
||||
@@ -205,7 +205,7 @@ export function useProofreading({ composerId, sessionKey, editor, textareaRef, p
|
||||
// Underlines in the rich editor.
|
||||
const drawnRef = useRef("");
|
||||
useEffect(() => {
|
||||
if (!editor || editor.isDestroyed || plainTextMode) return;
|
||||
if (!editor || editor.isDestroyed || !editor.state || plainTextMode) return;
|
||||
const ranges = issues.map((i) => ({ key: i.key, from: i.from, to: i.to, kind: i.kind, text: i.annotation.text }));
|
||||
const signature = ranges.map((r) => `${r.key}@${r.from}`).join(",");
|
||||
if (signature === drawnRef.current) return;
|
||||
@@ -217,7 +217,7 @@ export function useProofreading({ composerId, sessionKey, editor, textareaRef, p
|
||||
// and the list and the count follow.
|
||||
const [drawnKeys, setDrawnKeys] = useState<string[] | null>(null);
|
||||
useEffect(() => {
|
||||
if (!editor || editor.isDestroyed || plainTextMode) { setDrawnKeys(null); return; }
|
||||
if (!editor || editor.isDestroyed || !editor.state || plainTextMode) { setDrawnKeys(null); return; }
|
||||
const read = () => {
|
||||
const keys = drawnIssues(editor.state).map((r) => r.key);
|
||||
setDrawnKeys((prev) => (prev && prev.length === keys.length && prev.every((k, i) => k === keys[i]) ? prev : keys));
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { usePluginStore } from '@/stores/plugin-store';
|
||||
import { knownPlugins } from '@/lib/plugin-sandbox/known-plugins';
|
||||
import type { InstalledPlugin } from '@/lib/plugin-types';
|
||||
|
||||
// The composer's proofreading reads the installed plugins from here, so it
|
||||
// knows which signing plugin is still starting, without importing the store.
|
||||
|
||||
describe('known plugins', () => {
|
||||
it('follow the plugin store', () => {
|
||||
const smime = { id: 'smime', name: 'S/MIME', permissions: ['crypto:full'], enabled: true, status: 'enabled' } as unknown as InstalledPlugin;
|
||||
usePluginStore.setState({ plugins: [smime] });
|
||||
expect(knownPlugins()).toEqual([smime]);
|
||||
usePluginStore.setState({ plugins: [] });
|
||||
expect(knownPlugins()).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
// The user's installed plugins as the plugin store has them, for code that
|
||||
// needs the list without loading the store and everything it imports (the
|
||||
// composer's proofreading checks which plugins are turned on but not running
|
||||
// yet). The plugin store keeps it current.
|
||||
|
||||
import type { InstalledPlugin } from '../plugin-types';
|
||||
|
||||
let plugins: readonly InstalledPlugin[] = [];
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
export function setKnownPlugins(next: readonly InstalledPlugin[]): void {
|
||||
if (next === plugins) return;
|
||||
plugins = next;
|
||||
for (const l of listeners) {
|
||||
try { l(); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
export function knownPlugins(): readonly InstalledPlugin[] {
|
||||
return plugins;
|
||||
}
|
||||
|
||||
export function subscribeKnownPlugins(listener: () => void): () => void {
|
||||
listeners.add(listener);
|
||||
return () => { listeners.delete(listener); };
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Plugin store - manages installed plugins and lifecycle. Slot registrations
|
||||
// are owned by `lib/plugin-sandbox/registry` (per-iframe), not by the store.
|
||||
|
||||
import { setKnownPlugins } from '@/lib/plugin-sandbox/known-plugins';
|
||||
import { cancelConnectFor, forgetCredentials } from '@/lib/plugin-sandbox/connect-dialog';
|
||||
import { create } from 'zustand';
|
||||
import { persist, type PersistStorage, type StorageValue } from 'zustand/middleware';
|
||||
@@ -876,3 +877,7 @@ if (!IS_LITE) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// The list for code that shouldn't import this store (lib/plugin-sandbox/known-plugins.ts).
|
||||
setKnownPlugins(usePluginStore.getState().plugins);
|
||||
usePluginStore.subscribe((state) => setKnownPlugins(state.plugins));
|
||||
|
||||
Reference in New Issue
Block a user