plugins: add orbit.modal() for plugin dialogs
This commit is contained in:
parent
6f2015b73c
commit
e28b8370a4
4 changed files with 59 additions and 12 deletions
|
|
@ -42,6 +42,12 @@ Orbit.plugin('orbit-demo', (orbit, log) => {
|
|||
run: (args) => orbit.irc.say('\u{1F3B2} ' + (1 + Math.floor(Math.random() * Math.max(2, +args[0] || 6)))),
|
||||
});
|
||||
|
||||
// Register a keyboard shortcut: mod+shift+d fires a notification.
|
||||
orbit.addShortcut('mod+shift+d', () => orbit.notify('Demo plugin', 'shortcut fired'));
|
||||
// Register a keyboard shortcut: mod+shift+d opens a modal dialog.
|
||||
orbit.addShortcut('mod+shift+d', () => {
|
||||
const close = orbit.modal(
|
||||
() => orbit.html`<p style="padding:.5rem 0">Hello from the demo plugin.
|
||||
<button class="upbtn upbtn--primary" onClick=${() => close()}>Close</button></p>`,
|
||||
{ title: 'Demo plugin' },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { Avatar } from '../Avatar';
|
|||
import { SettingsModal } from '../settings/SettingsModal';
|
||||
import { QuickSwitcher } from '../QuickSwitcher';
|
||||
import { Shortcuts } from '../Shortcuts';
|
||||
import { usePluginRegistry } from '../../plugins/registry';
|
||||
import { PluginBoundary } from '../PluginBoundary';
|
||||
|
||||
function Modal({ title, onClose, children, wide }: { title: string; onClose: () => void; children: ReactNode; wide?: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -289,15 +291,32 @@ function ReportModal() {
|
|||
);
|
||||
}
|
||||
|
||||
// A modal opened by a plugin via orbit.modal(): the core owns the shell, the
|
||||
// plugin owns the body (rendered inside its own error boundary).
|
||||
function PluginModal() {
|
||||
const spec = usePluginRegistry((s) => s.modal);
|
||||
const close = usePluginRegistry((s) => s.closeModal);
|
||||
if (!spec) return null;
|
||||
return (
|
||||
<Modal title={spec.title || ''} wide={spec.wide} onClose={close}>
|
||||
<PluginBoundary render={spec.render} label="modal" />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function Modals() {
|
||||
const modal = useChat((s) => s.modal);
|
||||
if (modal === 'join') return <JoinDialog />;
|
||||
if (modal === 'settings') return <SettingsModal />;
|
||||
if (modal === 'explore') return <ExploreModal />;
|
||||
if (modal === 'friends') return <FriendsModal />;
|
||||
if (modal === 'chanadmin') return <ChanAdminModal />;
|
||||
if (modal === 'report') return <ReportModal />;
|
||||
if (modal === 'switcher') return <QuickSwitcher />;
|
||||
if (modal === 'shortcuts') return <Shortcuts />;
|
||||
return null;
|
||||
return (
|
||||
<>
|
||||
{modal === 'join' && <JoinDialog />}
|
||||
{modal === 'settings' && <SettingsModal />}
|
||||
{modal === 'explore' && <ExploreModal />}
|
||||
{modal === 'friends' && <FriendsModal />}
|
||||
{modal === 'chanadmin' && <ChanAdminModal />}
|
||||
{modal === 'report' && <ReportModal />}
|
||||
{modal === 'switcher' && <QuickSwitcher />}
|
||||
{modal === 'shortcuts' && <Shortcuts />}
|
||||
<PluginModal />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const THEMES: Theme[] = ['light', 'dark', 'orbit', 'orbit-dark', 'yomirc', 'yomi
|
|||
|
||||
// Plugin API contract version. Bumped on a breaking change to the surface below
|
||||
// so plugins can guard (e.g. `if (Orbit.apiVersion < 2) …`). Still experimental.
|
||||
const API_VERSION = 4;
|
||||
const API_VERSION = 5;
|
||||
|
||||
const registered = new Map<string, OrbitPluginApi>();
|
||||
|
||||
|
|
@ -108,6 +108,9 @@ export interface OrbitPluginApi {
|
|||
/** Register a global keyboard shortcut, e.g. "mod+shift+k" (mod = Cmd/Ctrl).
|
||||
* Runs in the chat view; built-in shortcuts take priority. */
|
||||
addShortcut: (combo: string, run: (e: KeyboardEvent) => void) => () => void;
|
||||
/** Open a modal dialog: `render` fills the body, the core supplies the backdrop,
|
||||
* title bar and close button. Returns a function that closes it. */
|
||||
modal: (render: () => ReactNode, opts?: { title?: string; wide?: boolean }) => () => void;
|
||||
}
|
||||
|
||||
function makeApi(name: string): OrbitPluginApi {
|
||||
|
|
@ -171,6 +174,7 @@ function makeApi(name: string): OrbitPluginApi {
|
|||
addCommand: (cmd, spec) => usePluginRegistry.getState().addCommand(name, cmd, spec.run, spec.help),
|
||||
notify: (title, body) => pluginNotify(title, body),
|
||||
addShortcut: (combo, run) => usePluginRegistry.getState().addShortcut(name, combo, run),
|
||||
modal: (render, opts) => usePluginRegistry.getState().openModal({ plugin: name, render, title: opts?.title, wide: opts?.wide }),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,6 +77,15 @@ export interface PluginShortcut {
|
|||
run: (e: KeyboardEvent) => void;
|
||||
}
|
||||
|
||||
// A plugin-opened modal dialog. `render` fills the body; the core supplies the
|
||||
// backdrop, title bar and close button. Only one is shown at a time.
|
||||
export interface PluginModalSpec {
|
||||
plugin: string;
|
||||
render: () => ReactNode;
|
||||
title?: string;
|
||||
wide?: boolean;
|
||||
}
|
||||
|
||||
const IS_MAC = typeof navigator !== 'undefined' && /mac/i.test(navigator.platform || '');
|
||||
// True when a keydown event matches a "mod+shift+k"-style combo.
|
||||
export function matchShortcut(e: KeyboardEvent, combo: string): boolean {
|
||||
|
|
@ -102,6 +111,7 @@ interface RegistryState {
|
|||
messageFilters: PluginFilter[];
|
||||
commands: PluginCommand[];
|
||||
shortcuts: PluginShortcut[];
|
||||
modal: PluginModalSpec | null;
|
||||
addUi: (slot: UiSlot, plugin: string, render: () => ReactNode, meta?: PluginUi['meta']) => () => void;
|
||||
addDecorator: (plugin: string, render: (m: MessageInfo) => ReactNode) => () => void;
|
||||
addAction: (plugin: string, render: (m: MessageInfo) => ReactNode) => () => void;
|
||||
|
|
@ -109,6 +119,8 @@ interface RegistryState {
|
|||
addMessageFilter: (plugin: string, fn: (m: FilterableMessage) => boolean) => () => void;
|
||||
addCommand: (plugin: string, name: string, run: PluginCommand['run'], help?: string) => () => void;
|
||||
addShortcut: (plugin: string, combo: string, run: PluginShortcut['run']) => () => void;
|
||||
openModal: (spec: PluginModalSpec) => () => void;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
export const usePluginRegistry = create<RegistryState>((set) => ({
|
||||
|
|
@ -119,6 +131,7 @@ export const usePluginRegistry = create<RegistryState>((set) => ({
|
|||
messageFilters: [],
|
||||
commands: [],
|
||||
shortcuts: [],
|
||||
modal: null,
|
||||
addUi: (slot, plugin, render, meta) => {
|
||||
const id = `${plugin}:${slot}:${Math.random().toString(36).slice(2, 8)}`;
|
||||
set((s) => ({ ui: [...s.ui, { id, plugin, slot, render, meta }] }));
|
||||
|
|
@ -155,4 +168,9 @@ export const usePluginRegistry = create<RegistryState>((set) => ({
|
|||
set((s) => ({ shortcuts: [...s.shortcuts, { id, plugin, combo, run }] }));
|
||||
return () => set((s) => ({ shortcuts: s.shortcuts.filter((k) => k.id !== id) }));
|
||||
},
|
||||
openModal: (spec) => {
|
||||
set({ modal: spec });
|
||||
return () => set((s) => (s.modal === spec ? { modal: null } : {}));
|
||||
},
|
||||
closeModal: () => set({ modal: null }),
|
||||
}));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue