Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mobile: Plugin support: Simplify reporting plugin issues #10319

Merged
4 changes: 4 additions & 0 deletions .eslintignore
Expand Up @@ -488,6 +488,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
Expand Down Expand Up @@ -584,6 +585,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
Expand Down Expand Up @@ -1040,6 +1042,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
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Expand Up @@ -468,6 +468,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
Expand Down Expand Up @@ -564,6 +565,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
Expand Down Expand Up @@ -1020,6 +1022,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
Expand Down
83 changes: 83 additions & 0 deletions 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> = props => {
const styles = useStyles(props.themeId, props.containerStyle);

const closeButton = (
<View style={styles.closeButtonContainer}>
<IconButton
icon='close'
accessibilityLabel={_('Close')}
onPress={props.onDismiss}
/>
</View>
);

return (
<Modal
visible={props.visible}
onDismiss={props.onDismiss}
onRequestClose={props.onDismiss}
animationType='fade'
transparent={true}
>
<Surface style={styles.dialog} elevation={1}>
{closeButton}
{props.children}
</Surface>
</Modal>
);
};

export default DismissibleDialog;
@@ -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> = props => {
const aboutPlugin = (
<View style={styles.aboutPluginContainer}>
<Text variant='titleLarge'>{props.item.manifest.name}</Text>
<Text variant='bodyLarge'>{props.item.manifest.author ? _('by %s', props.item.manifest.author) : ''}</Text>
<Text style={styles.descriptionText}>{props.item.manifest.description ?? _('No description')}</Text>
</View>
);

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 = (
<List.Item
left={props => <List.Icon {...props} icon='bug'/>}
title={_('Report an issue')}
onPress={onReportIssuePress}
/>
);

const onReportFraudulentPress = useCallback(() => {
void Linking.openURL('https://github.com/laurent22/joplin/security/advisories/new');
}, []);

return (
<Portal>
<DismissibleDialog
themeId={props.themeId}
visible={true}
onDismiss={props.onModalDismiss}
>
<ScrollView>
{aboutPlugin}
<List.Item
left={props => <List.Icon {...props} icon='web'/>}
title={_('About')}
onPress={onAboutPress}
/>
{ reportIssueUrl ? reportIssueButton : null }
</ScrollView>
<Button
icon='shield-bug'
style={styles.fraudulentPluginButton}
onPress={onReportFraudulentPress}
>{_('Report fraudulent plugin')}</Button>
</DismissibleDialog>
</Portal>
);
};

const PluginInfoButton: React.FC<Props> = props => {
const [showInfoModal, setShowInfoModal] = useState(false);
const onInfoButtonPress = useCallback(() => {
setShowInfoModal(true);
}, []);

const onModalDismiss = useCallback(() => {
setShowInfoModal(false);
props.onModalDismiss?.();
}, [props.onModalDismiss]);

return (
<>
{showInfoModal ? <PluginInfoModal {...props} onModalDismiss={onModalDismiss} /> : null}
<IconButton
size={props.size}
icon='information'
onPress={onInfoButtonPress}
/>
</>
);
};

export default PluginInfoButton;
Expand Up @@ -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,
Expand All @@ -21,18 +22,19 @@ export enum UpdateState {
}

interface Props {
themeId: number;
item: PluginItem;
isCompatible: boolean;

hasErrors?: boolean;
installState?: InstallState;
updateState?: UpdateState;

onAboutPress?: PluginCallback;
onInstall?: PluginCallback;
onUpdate?: PluginCallback;
onDelete?: PluginCallback;
onToggle?: PluginCallback;
onAboutPress?: PluginCallback;
onShowPluginLog?: PluginCallback;
}

Expand Down Expand Up @@ -113,7 +115,7 @@ const PluginBox: React.FC<Props> = props => {
);
const disableButton = <ActionButton item={item} onPress={props.onToggle} title={_('Disable')}/>;
const enableButton = <ActionButton item={item} onPress={props.onToggle} title={_('Enable')}/>;
const aboutButton = <ActionButton icon='web' item={item} onPress={props.onAboutPress} title={_('About')}/>;
const aboutButton = <ActionButton item={item} onPress={props.onAboutPress} icon='web' title={_('About')}/>;

const renderErrorsChip = () => {
if (!props.hasErrors) return null;
Expand Down Expand Up @@ -165,6 +167,13 @@ const PluginBox: React.FC<Props> = 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 <PluginInfoButton {...buttonProps} themeId={props.themeId} item={props.item}/>;
};

const updateStateIsIdle = props.updateState !== UpdateState.Idle;

const titleComponent = <>
Expand All @@ -177,6 +186,7 @@ const PluginBox: React.FC<Props> = props => {
titleStyle={styles.title}
subtitle={manifest.description}
left={PluginIcon}
right={renderRightEdgeButton}
/>
<Card.Content>
<View style={{ flexDirection: 'row' }}>
Expand Down
Expand Up @@ -98,6 +98,7 @@ const PluginStates: React.FC<Props> = props => {
installedPluginCards.push(
<PluginToggle
key={`plugin-${key}`}
themeId={props.themeId}
pluginId={plugin.id}
styles={props.styles}
pluginSettings={props.pluginSettings}
Expand Down
Expand Up @@ -11,6 +11,7 @@ import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';

interface Props {
pluginId: string;
themeId: number;
styles: ConfigScreenStyles;
pluginSettings: string;
updatablePluginIds: Record<string, boolean>;
Expand Down Expand Up @@ -91,6 +92,7 @@ const PluginToggle: React.FC<Props> = props => {

return (
<PluginBox
themeId={props.themeId}
item={pluginItem}
isCompatible={isCompatible}
hasErrors={plugin.hasErrors}
Expand Down
Expand Up @@ -10,8 +10,8 @@ import PluginBox, { InstallState } from './PluginBox';
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
import useInstallHandler from '@joplin/lib/components/shared/config/plugins/useOnInstallHandler';
import { OnPluginSettingChangeEvent, PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
import onOpenWebsiteForPluginPress from './utils/openWebsiteForPlugin';
import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';
import openWebsiteForPlugin from './utils/openWebsiteForPlugin';

interface Props {
themeId: number;
Expand Down Expand Up @@ -90,15 +90,16 @@ const PluginSearch: React.FC<Props> = props => {

return (
<PluginBox
themeId={props.themeId}
key={manifest.id}
item={item.item}
installState={item.installState}
isCompatible={PluginService.instance().isCompatible(manifest)}
onInstall={installPlugin}
onAboutPress={onOpenWebsiteForPluginPress}
onAboutPress={openWebsiteForPlugin}
/>
);
}, [installPlugin]);
}, [installPlugin, props.themeId]);

return (
<View style={{ flexDirection: 'column' }}>
Expand Down