diff --git a/packages/app-mobile/components/ScreenHeader/index.tsx b/packages/app-mobile/components/ScreenHeader/index.tsx index ea793a49a..b6132d001 100644 --- a/packages/app-mobile/components/ScreenHeader/index.tsx +++ b/packages/app-mobile/components/ScreenHeader/index.tsx @@ -34,12 +34,17 @@ const PADDING_V = 10; type OnSelectCallbackType=()=> void; type OnPressCallback=()=> void; -export interface MenuOptionType { +export type MenuOptionType = { onPress: OnPressCallback; isDivider?: boolean; title: string; disabled?: boolean; -} +}|{ + isDivider: true; + title?: undefined; + onPress?: undefined; + disabled?: false; +}; interface ScreenHeaderProps { selectedNoteIds: string[]; @@ -427,8 +432,10 @@ class ScreenHeaderComponent extends PureComponent { const allPluginViews = Object.values(this.props.plugins).map(plugin => Object.values(plugin.views)).flat(); - const allPanels = allPluginViews.filter(view => view.containerType === ContainerType.Panel); - if (allPanels.length === 0) return null; + const allVisiblePanels = allPluginViews.filter( + view => view.containerType === ContainerType.Panel && view.opened, + ); + if (allVisiblePanels.length === 0) return null; return ( implements B const readOnly = this.state.readOnly; const isDeleted = !!this.state.note.deleted_time; - const cacheKey = md5([isTodo, isSaved].join('_')); + const pluginCommands = pluginUtils.commandNamesFromViews(this.props.plugins, 'noteToolbar'); + + const cacheKey = md5([isTodo, isSaved, pluginCommands.join(',')].join('_')); if (!this.menuOptionsCache_) this.menuOptionsCache_ = {}; if (this.menuOptionsCache_[cacheKey]) return this.menuOptionsCache_[cacheKey]; @@ -1321,6 +1324,25 @@ class NoteScreenComponent extends BaseScreenComponent implements B disabled: readOnly, }); + if (pluginCommands.length) { + output.push({ isDivider: true }); + + const commandService = CommandService.instance(); + for (const commandName of pluginCommands) { + if (commandName === '-') { + output.push({ isDivider: true }); + } else { + output.push({ + title: commandService.description(commandName), + onPress: async () => { + void commandService.execute(commandName); + }, + disabled: !commandService.isEnabled(commandName), + }); + } + } + } + this.menuOptionsCache_ = {}; this.menuOptionsCache_[cacheKey] = output; diff --git a/packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.tsx b/packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.tsx index f5021eeaf..b27e03d53 100644 --- a/packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.tsx +++ b/packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.tsx @@ -3,8 +3,8 @@ import { PluginHtmlContents, PluginStates, ViewInfo } from '@joplin/lib/services import * as React from 'react'; import { Button, Portal, SegmentedButtons, Text } from 'react-native-paper'; import useViewInfos from './hooks/useViewInfos'; -import WebviewController, { ContainerType } from '@joplin/lib/services/plugins/WebviewController'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { ContainerType } from '@joplin/lib/services/plugins/WebviewController'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import PluginService from '@joplin/lib/services/plugins/PluginService'; import { connect } from 'react-redux'; import { AppState } from '../../../utils/types'; @@ -42,43 +42,38 @@ type ButtonInfo = { icon: string; }; -const useSelectedTabId = (buttonInfos: ButtonInfo[], viewInfoById: Record) => { +const useSelectedTabId = ( + buttonInfos: ButtonInfo[], + viewInfoById: Record, +) => { + const viewInfoByIdRef = useRef(viewInfoById); + viewInfoByIdRef.current = viewInfoById; + const getDefaultSelectedTabId = useCallback((): string|undefined => { const lastSelectedId = Setting.value('ui.lastSelectedPluginPanel'); - if (lastSelectedId && viewInfoById[lastSelectedId]) { + const lastSelectedInfo = viewInfoByIdRef.current[lastSelectedId]; + if (lastSelectedId && lastSelectedInfo && lastSelectedInfo.view.opened) { return lastSelectedId; } else { return buttonInfos[0]?.value; } - }, [buttonInfos, viewInfoById]); + }, [buttonInfos]); const [selectedTabId, setSelectedTabId] = useState(getDefaultSelectedTabId); useEffect(() => { - if (!selectedTabId) { + if (!selectedTabId || !viewInfoById[selectedTabId]?.view?.opened) { setSelectedTabId(getDefaultSelectedTabId()); } - }, [selectedTabId, getDefaultSelectedTabId]); + }, [selectedTabId, viewInfoById, getDefaultSelectedTabId]); useEffect(() => { - if (!selectedTabId) return () => {}; - - const info = viewInfoById[selectedTabId]; - - let controller: WebviewController|null = null; - if (info && info.view.opened) { - const plugin = PluginService.instance().pluginById(info.plugin.id); - controller = plugin.viewController(info.view.id) as WebviewController; - controller.setIsShownInModal(true); - } + if (!selectedTabId) return; + const info = viewInfoByIdRef.current[selectedTabId]; Setting.setValue('ui.lastSelectedPluginPanel', selectedTabId); AccessibilityInfo.announceForAccessibility(_('%s tab opened', getTabLabel(info))); - - return () => { - controller?.setIsShownInModal(false); - }; - }, [viewInfoById, selectedTabId]); + }, [selectedTabId]); return { setSelectedTabId, selectedTabId }; }; @@ -98,7 +93,7 @@ const PluginPanelViewer: React.FC = props => { const viewInfoById = useMemo(() => { const result: Record = {}; for (const info of viewInfos) { - result[`${info.plugin.id}--${info.view.id}`] = info; + result[info.view.id] = info; } return result; }, [viewInfos]); @@ -106,6 +101,7 @@ const PluginPanelViewer: React.FC = props => { const buttonInfos = useMemo(() => { return Object.entries(viewInfoById) .filter(([_id, info]) => info.view.containerType === ContainerType.Panel) + .filter(([_id, info]) => info.view.opened) .map(([id, info]) => { return { value: id, diff --git a/packages/lib/services/plugins/WebviewController.ts b/packages/lib/services/plugins/WebviewController.ts index a5e17a16e..87e729854 100644 --- a/packages/lib/services/plugins/WebviewController.ts +++ b/packages/lib/services/plugins/WebviewController.ts @@ -3,6 +3,7 @@ import shim from '../../shim'; import { ButtonSpec, DialogResult, ViewHandle } from './api/types'; const { toSystemSlashes } = require('../../path-utils'); import PostMessageService, { MessageParticipant } from '../PostMessageService'; +import { PluginViewState } from './reducer'; export enum ContainerType { Panel = 'panel', @@ -49,27 +50,28 @@ export default class WebviewController extends ViewController { private messageListener_: Function = null; private closeResponse_: CloseResponse = null; - // True if a **panel** is shown in a modal window. - private panelInModalMode_ = false; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied public constructor(handle: ViewHandle, pluginId: string, store: any, baseDir: string, containerType: ContainerType) { super(handle, pluginId, store); this.baseDir_ = toSystemSlashes(baseDir, 'linux'); + const view: PluginViewState = { + id: this.handle, + type: this.type, + containerType: containerType, + html: '', + scripts: [], + // Opened is used for dialogs and mobile panels (which are shown + // like dialogs): + opened: containerType === ContainerType.Panel, + buttons: null, + fitToContent: true, + }; + this.store.dispatch({ type: 'PLUGIN_VIEW_ADD', pluginId: pluginId, - view: { - id: this.handle, - type: this.type, - containerType: containerType, - html: '', - scripts: [], - opened: false, - buttons: null, - fitToContent: true, - }, + view, }); } @@ -147,39 +149,36 @@ export default class WebviewController extends ViewController { // Specific to panels // --------------------------------------------- + private showWithAppLayout() { + return this.containerType === ContainerType.Panel && !!this.store.getState().mainLayout; + } + public async show(show = true): Promise { - this.store.dispatch({ - type: 'MAIN_LAYOUT_SET_ITEM_PROP', - itemKey: this.handle, - propName: 'visible', - propValue: show, - }); + if (this.showWithAppLayout()) { + this.store.dispatch({ + type: 'MAIN_LAYOUT_SET_ITEM_PROP', + itemKey: this.handle, + propName: 'visible', + propValue: show, + }); + } else { + this.setStoreProp('opened', show); + } } public async hide(): Promise { return this.show(false); } - // This method allows us to determine whether a panel is shown in dialog mode, - // which is used on mobile. - public setIsShownInModal(shown: boolean) { - this.panelInModalMode_ = shown; - } - public get visible(): boolean { const appState = this.store.getState(); - if (this.panelInModalMode_) { - return true; + // Mobile: There is no appState.mainLayout + if (!this.showWithAppLayout()) { + return this.storeView.opened; } const mainLayout = appState.mainLayout; - - // Mobile: There is no appState.mainLayout - if (!mainLayout) { - return false; - } - const item = findItemByKey(mainLayout, this.handle); return item ? item.visible : false; }