From aec77b543ce32ddbd640d4d916ddf4cc97b5033e Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Thu, 25 Apr 2024 06:02:10 -0700 Subject: [PATCH] Mobile: Plugin support: Simplify reporting plugin issues (#10319) --- .eslintignore | 4 + .gitignore | 4 + .../components/DismissibleDialog.tsx | 83 +++++++++++++ .../plugins/PluginBox/PluginInfoButton.tsx | 115 ++++++++++++++++++ .../ConfigScreen/plugins/PluginBox/index.tsx | 14 ++- .../ConfigScreen/plugins/PluginStates.tsx | 1 + .../ConfigScreen/plugins/PluginToggle.tsx | 2 + .../ConfigScreen/plugins/SearchPlugins.tsx | 7 +- .../dialogs/PluginPanelViewer.tsx | 73 +++-------- .../utils/getPluginIssueReportUrl.test.ts | 17 +++ .../plugins/utils/getPluginIssueReportUrl.ts | 31 +++++ 11 files changed, 289 insertions(+), 62 deletions(-) create mode 100644 packages/app-mobile/components/DismissibleDialog.tsx create mode 100644 packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginInfoButton.tsx create mode 100644 packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.ts create mode 100644 packages/lib/services/plugins/utils/getPluginIssueReportUrl.ts diff --git a/.eslintignore b/.eslintignore index 3539b92b2..fe54f8247 100644 --- a/.eslintignore +++ b/.eslintignore @@ -489,6 +489,7 @@ packages/app-mobile/components/ActionButton.js packages/app-mobile/components/BackButtonDialogBox.js packages/app-mobile/components/CameraView.js packages/app-mobile/components/CustomButton.js +packages/app-mobile/components/DismissibleDialog.js packages/app-mobile/components/Dropdown.test.js packages/app-mobile/components/Dropdown.js packages/app-mobile/components/ExtendedWebView.js @@ -585,6 +586,7 @@ packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/ActionButton.js +packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginInfoButton.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.test.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js @@ -1045,6 +1047,8 @@ packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.js packages/lib/services/plugins/reducer.js packages/lib/services/plugins/utils/createViewHandle.js packages/lib/services/plugins/utils/executeSandboxCall.js +packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js +packages/lib/services/plugins/utils/getPluginIssueReportUrl.js packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js packages/lib/services/plugins/utils/getPluginSettingKeyPrefix.js packages/lib/services/plugins/utils/getPluginSettingValue.js diff --git a/.gitignore b/.gitignore index 626e6f23d..4e2b893f5 100644 --- a/.gitignore +++ b/.gitignore @@ -469,6 +469,7 @@ packages/app-mobile/components/ActionButton.js packages/app-mobile/components/BackButtonDialogBox.js packages/app-mobile/components/CameraView.js packages/app-mobile/components/CustomButton.js +packages/app-mobile/components/DismissibleDialog.js packages/app-mobile/components/Dropdown.test.js packages/app-mobile/components/Dropdown.js packages/app-mobile/components/ExtendedWebView.js @@ -565,6 +566,7 @@ packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/ActionButton.js +packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginInfoButton.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.test.js packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js @@ -1025,6 +1027,8 @@ packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.js packages/lib/services/plugins/reducer.js packages/lib/services/plugins/utils/createViewHandle.js packages/lib/services/plugins/utils/executeSandboxCall.js +packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js +packages/lib/services/plugins/utils/getPluginIssueReportUrl.js packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js packages/lib/services/plugins/utils/getPluginSettingKeyPrefix.js packages/lib/services/plugins/utils/getPluginSettingValue.js diff --git a/packages/app-mobile/components/DismissibleDialog.tsx b/packages/app-mobile/components/DismissibleDialog.tsx new file mode 100644 index 000000000..15b96e147 --- /dev/null +++ b/packages/app-mobile/components/DismissibleDialog.tsx @@ -0,0 +1,83 @@ +import * as React from 'react'; +import { useMemo } from 'react'; +import { StyleSheet, View, ViewStyle, useWindowDimensions } from 'react-native'; +import { IconButton, Surface } from 'react-native-paper'; +import { themeStyle } from './global-style'; +import Modal from './Modal'; +import { _ } from '@joplin/lib/locale'; + +interface Props { + themeId: number; + visible: boolean; + onDismiss: ()=> void; + containerStyle?: ViewStyle; + children: React.ReactNode; +} + +const useStyles = (themeId: number, containerStyle: ViewStyle) => { + const windowSize = useWindowDimensions(); + + return useMemo(() => { + const theme = themeStyle(themeId); + + return StyleSheet.create({ + webView: { + backgroundColor: 'transparent', + display: 'flex', + }, + webViewContainer: { + flexGrow: 1, + flexShrink: 1, + }, + closeButtonContainer: { + flexDirection: 'row', + justifyContent: 'flex-end', + }, + dialog: { + backgroundColor: theme.backgroundColor, + borderRadius: 12, + padding: 10, + + height: windowSize.height * 0.9, + width: windowSize.width * 0.97, + + // Center + marginLeft: 'auto', + marginRight: 'auto', + + ...containerStyle, + }, + }); + }, [themeId, windowSize.width, windowSize.height, containerStyle]); +}; + +const DismissibleDialog: React.FC = props => { + const styles = useStyles(props.themeId, props.containerStyle); + + const closeButton = ( + + + + ); + + return ( + + + {closeButton} + {props.children} + + + ); +}; + +export default DismissibleDialog; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginInfoButton.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginInfoButton.tsx new file mode 100644 index 000000000..9bcfcac36 --- /dev/null +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginInfoButton.tsx @@ -0,0 +1,115 @@ +import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; +import * as React from 'react'; +import { _ } from '@joplin/lib/locale'; +import { useCallback, useMemo, useState } from 'react'; +import { Button, IconButton, List, Portal, Text } from 'react-native-paper'; +import getPluginIssueReportUrl from '@joplin/lib/services/plugins/utils/getPluginIssueReportUrl'; +import { Linking, ScrollView, StyleSheet, View } from 'react-native'; +import DismissibleDialog from '../../../../DismissibleDialog'; +import openWebsiteForPlugin from '../utils/openWebsiteForPlugin'; + +interface Props { + themeId: number; + size: number; + item: PluginItem; + onModalDismiss?: ()=> void; +} + +const styles = StyleSheet.create({ + aboutPluginContainer: { + paddingLeft: 10, + paddingRight: 10, + paddingBottom: 10, + }, + descriptionText: { + marginTop: 5, + marginBottom: 5, + }, + fraudulentPluginButton: { + opacity: 0.6, + }, +}); + +const PluginInfoModal: React.FC = props => { + const aboutPlugin = ( + + {props.item.manifest.name} + {props.item.manifest.author ? _('by %s', props.item.manifest.author) : ''} + {props.item.manifest.description ?? _('No description')} + + ); + + const onAboutPress = useCallback(() => { + void openWebsiteForPlugin({ item: props.item }); + }, [props.item]); + + const reportIssueUrl = useMemo(() => { + return getPluginIssueReportUrl(props.item.manifest); + }, [props.item]); + + const onReportIssuePress = useCallback(() => { + void Linking.openURL(reportIssueUrl); + }, [reportIssueUrl]); + + const reportIssueButton = ( + } + title={_('Report an issue')} + onPress={onReportIssuePress} + /> + ); + + const onReportFraudulentPress = useCallback(() => { + void Linking.openURL('https://github.com/laurent22/joplin/security/advisories/new'); + }, []); + + return ( + + + + {aboutPlugin} + } + title={_('About')} + onPress={onAboutPress} + /> + { reportIssueUrl ? reportIssueButton : null } + + + + + ); +}; + +const PluginInfoButton: React.FC = props => { + const [showInfoModal, setShowInfoModal] = useState(false); + const onInfoButtonPress = useCallback(() => { + setShowInfoModal(true); + }, []); + + const onModalDismiss = useCallback(() => { + setShowInfoModal(false); + props.onModalDismiss?.(); + }, [props.onModalDismiss]); + + return ( + <> + {showInfoModal ? : null} + + + ); +}; + +export default PluginInfoButton; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx index 120494a8a..c6b9b82d3 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx @@ -6,6 +6,7 @@ import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types'; import shim from '@joplin/lib/shim'; import PluginService from '@joplin/lib/services/plugins/PluginService'; import ActionButton, { PluginCallback } from './ActionButton'; +import PluginInfoButton from './PluginInfoButton'; export enum InstallState { NotInstalled, @@ -21,6 +22,7 @@ export enum UpdateState { } interface Props { + themeId: number; item: PluginItem; isCompatible: boolean; @@ -28,11 +30,11 @@ interface Props { installState?: InstallState; updateState?: UpdateState; + onAboutPress?: PluginCallback; onInstall?: PluginCallback; onUpdate?: PluginCallback; onDelete?: PluginCallback; onToggle?: PluginCallback; - onAboutPress?: PluginCallback; onShowPluginLog?: PluginCallback; } @@ -113,7 +115,7 @@ const PluginBox: React.FC = props => { ); const disableButton = ; const enableButton = ; - const aboutButton = ; + const aboutButton = ; const renderErrorsChip = () => { if (!props.hasErrors) return null; @@ -165,6 +167,13 @@ const PluginBox: React.FC = props => { ); }; + const renderRightEdgeButton = (buttonProps: { size: number }) => { + // If .onAboutPress is given (e.g. when searching), there's another way to get information + // about the plugin. In this case, we don't show the right-side information link. + if (props.onAboutPress) return null; + return ; + }; + const updateStateIsIdle = props.updateState !== UpdateState.Idle; const titleComponent = <> @@ -177,6 +186,7 @@ const PluginBox: React.FC = props => { titleStyle={styles.title} subtitle={manifest.description} left={PluginIcon} + right={renderRightEdgeButton} /> diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx index 138ccfeff..d2953f3b6 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.tsx @@ -98,6 +98,7 @@ const PluginStates: React.FC = props => { installedPluginCards.push( ; @@ -91,6 +92,7 @@ const PluginToggle: React.FC = props => { return ( = props => { return ( ); - }, [installPlugin]); + }, [installPlugin, props.themeId]); return ( diff --git a/packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.tsx b/packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.tsx index 8bf89f8a7..f5021eeaf 100644 --- a/packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.tsx +++ b/packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.tsx @@ -1,7 +1,7 @@ import { PluginHtmlContents, PluginStates, ViewInfo } from '@joplin/lib/services/plugins/reducer'; import * as React from 'react'; -import { Button, IconButton, Portal, SegmentedButtons, Text } from 'react-native-paper'; +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'; @@ -9,13 +9,11 @@ import PluginService from '@joplin/lib/services/plugins/PluginService'; import { connect } from 'react-redux'; import { AppState } from '../../../utils/types'; import PluginUserWebView from './PluginUserWebView'; -import { View, useWindowDimensions, StyleSheet, AccessibilityInfo } from 'react-native'; +import { View, StyleSheet, AccessibilityInfo } from 'react-native'; import { _ } from '@joplin/lib/locale'; -import { Theme } from '@joplin/lib/themes/type'; -import { themeStyle } from '@joplin/lib/theme'; import Setting from '@joplin/lib/models/Setting'; import { Dispatch } from 'redux'; -import Modal from '../../../components/Modal'; +import DismissibleDialog from '../../../components/DismissibleDialog'; interface Props { themeId: number; @@ -27,40 +25,16 @@ interface Props { } -const useStyles = (themeId: number) => { - const windowSize = useWindowDimensions(); - - return useMemo(() => { - const theme: Theme = themeStyle(themeId); - - return StyleSheet.create({ - webView: { - backgroundColor: 'transparent', - display: 'flex', - }, - webViewContainer: { - flexGrow: 1, - flexShrink: 1, - }, - closeButtonContainer: { - flexDirection: 'row', - justifyContent: 'flex-end', - }, - dialog: { - backgroundColor: theme.backgroundColor, - borderRadius: 12, - padding: 10, - - height: windowSize.height * 0.9, - width: windowSize.width * 0.97, - - // Center - marginLeft: 'auto', - marginRight: 'auto', - }, - }); - }, [themeId, windowSize.width, windowSize.height]); -}; +const styles = StyleSheet.create({ + webView: { + backgroundColor: 'transparent', + display: 'flex', + }, + webViewContainer: { + flexGrow: 1, + flexShrink: 1, + }, +}); type ButtonInfo = { value: string; @@ -141,7 +115,6 @@ const PluginPanelViewer: React.FC = props => { }); }, [viewInfoById]); - const styles = useStyles(props.themeId); const { selectedTabId, setSelectedTabId } = useSelectedTabId(buttonInfos, viewInfoById); const viewInfo = viewInfoById[selectedTabId]; @@ -194,30 +167,16 @@ const PluginPanelViewer: React.FC = props => { }); }, [props.dispatch]); - const closeButton = ( - - - - ); - return ( - - {closeButton} {renderTabContent()} {renderTabSelector()} - + ); }; diff --git a/packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.ts b/packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.ts new file mode 100644 index 000000000..9f71da641 --- /dev/null +++ b/packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.ts @@ -0,0 +1,17 @@ +import getPluginIssueReportUrl from './getPluginIssueReportUrl'; + +describe('getPluginIssueReportUrl', () => { + test.each([ + [{ repository_url: 'http://github.com/laurent22/joplin' }, 'https://github.com/laurent22/joplin/issues'], + [{ repository_url: 'https://www.github.com/laurent22/joplin' }, 'https://github.com/laurent22/joplin/issues'], + [{ repository_url: 'https://www.github.com/laurent22/joplin.git' }, 'https://github.com/laurent22/joplin/issues'], + [{ homepage_url: 'https://www.github.com/laurent22/joplin' }, 'https://github.com/laurent22/joplin/issues'], + + [{ homepage_url: 'https://gitlab.com/laurent22/joplin' }, 'https://gitlab.com/laurent22/joplin/-/issues'], + [{ homepage_url: 'https://www.gitlab.com/laurent22/joplin' }, 'https://gitlab.com/laurent22/joplin/-/issues'], + + [{ homepage_url: 'https://example.com/laurent22/joplin' }, null], + ])('should return the issue URL (case %#)', async (manifest, expectedUrl) => { + expect(getPluginIssueReportUrl(manifest)).toBe(expectedUrl); + }); +}); diff --git a/packages/lib/services/plugins/utils/getPluginIssueReportUrl.ts b/packages/lib/services/plugins/utils/getPluginIssueReportUrl.ts new file mode 100644 index 000000000..45054af8b --- /dev/null +++ b/packages/lib/services/plugins/utils/getPluginIssueReportUrl.ts @@ -0,0 +1,31 @@ +import { PluginManifest } from './types'; + +type ManifestSlice = Pick; +const getPluginIssueReportUrl = (pluginManifest: ManifestSlice): string|null => { + const githubUrlExp = /^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/?]+)/; + const gitlabUrlExp = /^https?:\/\/(?:www\.)?gitlab\.com\/([^/]+)\/([^/]+)/; + + let githubUrlMatch = null; + let gitlabUrlMatch = null; + const urls = [pluginManifest.repository_url, pluginManifest.homepage_url].filter(url => !!url); + + for (const url of urls) { + githubUrlMatch ??= githubUrlExp.exec(url); + gitlabUrlMatch ??= gitlabUrlExp.exec(url); + } + + if (githubUrlMatch) { + const organization = githubUrlMatch[1]; + // Some plugins include a trailing .git after the repository name + const project = githubUrlMatch[2].replace(/\.git$/, ''); + return `https://github.com/${organization}/${project}/issues`; + } else if (gitlabUrlMatch) { + const organization = gitlabUrlMatch[1]; + const project = gitlabUrlMatch[2]; + return `https://gitlab.com/${organization}/${project}/-/issues`; + } + + return null; +}; + +export default getPluginIssueReportUrl;