1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-02-13 19:42:36 +02:00

Merge branch 'dev' into release-2.9

This commit is contained in:
Laurent Cozic 2022-09-01 11:05:10 +01:00
commit 86fbf82d36
133 changed files with 4401 additions and 1355 deletions

View File

@ -6,6 +6,7 @@ _releases/
*.min.js
**/commands/index.ts
**/node_modules/
packages/generator-joplin/generators/app/templates/api/
Assets/
docs/
highlight.pack.js
@ -842,6 +843,12 @@ packages/app-mobile/components/BackButtonDialogBox.js.map
packages/app-mobile/components/CameraView.d.ts
packages/app-mobile/components/CameraView.js
packages/app-mobile/components/CameraView.js.map
packages/app-mobile/components/CustomButton.d.ts
packages/app-mobile/components/CustomButton.js
packages/app-mobile/components/CustomButton.js.map
packages/app-mobile/components/Dropdown.d.ts
packages/app-mobile/components/Dropdown.js
packages/app-mobile/components/Dropdown.js.map
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.d.ts
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js.map
@ -872,12 +879,9 @@ packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleTwice.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleTwice.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleTwice.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.togglingLists.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.togglingLists.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.togglingLists.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js.map
@ -905,6 +909,24 @@ packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.js.map
packages/app-mobile/components/NoteEditor/EditLinkDialog.d.ts
packages/app-mobile/components/NoteEditor/EditLinkDialog.js
packages/app-mobile/components/NoteEditor/EditLinkDialog.js.map
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.d.ts
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js.map
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.d.ts
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js.map
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.d.ts
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js.map
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.d.ts
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js.map
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.d.ts
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js.map
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.d.ts
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js.map
packages/app-mobile/components/NoteEditor/NoteEditor.d.ts
packages/app-mobile/components/NoteEditor/NoteEditor.js
packages/app-mobile/components/NoteEditor/NoteEditor.js.map
@ -917,9 +939,21 @@ packages/app-mobile/components/NoteEditor/SelectionFormatting.js.map
packages/app-mobile/components/NoteEditor/types.d.ts
packages/app-mobile/components/NoteEditor/types.js
packages/app-mobile/components/NoteEditor/types.js.map
packages/app-mobile/components/ScreenHeader.d.ts
packages/app-mobile/components/ScreenHeader.js
packages/app-mobile/components/ScreenHeader.js.map
packages/app-mobile/components/SelectDateTimeDialog.d.ts
packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SelectDateTimeDialog.js.map
packages/app-mobile/components/SideMenu.d.ts
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenu.js.map
packages/app-mobile/components/getResponsiveValue.d.ts
packages/app-mobile/components/getResponsiveValue.js
packages/app-mobile/components/getResponsiveValue.js.map
packages/app-mobile/components/getResponsiveValue.test.d.ts
packages/app-mobile/components/getResponsiveValue.test.js
packages/app-mobile/components/getResponsiveValue.test.js.map
packages/app-mobile/components/screens/ConfigScreen.d.ts
packages/app-mobile/components/screens/ConfigScreen.js
packages/app-mobile/components/screens/ConfigScreen.js.map
@ -1538,6 +1572,9 @@ packages/lib/services/interop/InteropService_Importer_Md_frontmatter.test.js.map
packages/lib/services/interop/InteropService_Importer_Raw.d.ts
packages/lib/services/interop/InteropService_Importer_Raw.js
packages/lib/services/interop/InteropService_Importer_Raw.js.map
packages/lib/services/interop/InteropService_Importer_Raw.test.d.ts
packages/lib/services/interop/InteropService_Importer_Raw.test.js
packages/lib/services/interop/InteropService_Importer_Raw.test.js.map
packages/lib/services/interop/types.d.ts
packages/lib/services/interop/types.js
packages/lib/services/interop/types.js.map
@ -1964,6 +2001,18 @@ packages/pdf-viewer/hooks/useIsFocused.js.map
packages/pdf-viewer/hooks/useIsVisible.d.ts
packages/pdf-viewer/hooks/useIsVisible.js
packages/pdf-viewer/hooks/useIsVisible.js.map
packages/pdf-viewer/hooks/usePdfData.d.ts
packages/pdf-viewer/hooks/usePdfData.js
packages/pdf-viewer/hooks/usePdfData.js.map
packages/pdf-viewer/hooks/useScaledSize.d.ts
packages/pdf-viewer/hooks/useScaledSize.js
packages/pdf-viewer/hooks/useScaledSize.js.map
packages/pdf-viewer/hooks/useScrollSaver.d.ts
packages/pdf-viewer/hooks/useScrollSaver.js
packages/pdf-viewer/hooks/useScrollSaver.js.map
packages/pdf-viewer/main.d.ts
packages/pdf-viewer/main.js
packages/pdf-viewer/main.js.map
packages/pdf-viewer/miniViewer.d.ts
packages/pdf-viewer/miniViewer.js
packages/pdf-viewer/miniViewer.js.map
@ -1973,6 +2022,9 @@ packages/pdf-viewer/pdfSource.js.map
packages/pdf-viewer/pdfSource.test.d.ts
packages/pdf-viewer/pdfSource.test.js
packages/pdf-viewer/pdfSource.test.js.map
packages/pdf-viewer/ui/ZoomControls.d.ts
packages/pdf-viewer/ui/ZoomControls.js
packages/pdf-viewer/ui/ZoomControls.js.map
packages/plugin-repo-cli/commands/updateRelease.d.ts
packages/plugin-repo-cli/commands/updateRelease.js
packages/plugin-repo-cli/commands/updateRelease.js.map

View File

@ -83,7 +83,9 @@ module.exports = {
// 'complexity': ['warn', { max: 10 }],
// Checks rules of Hooks
'react-hooks/rules-of-hooks': 'error',
'@seiyab/react-hooks/rules-of-hooks': 'error',
'@seiyab/react-hooks/exhaustive-deps': ['error', { 'ignoreThisDependency': 'props' }],
// Checks effect dependencies
// Disable because of this: https://github.com/facebook/react/issues/16265
// "react-hooks/exhaustive-deps": "warn",
@ -134,7 +136,10 @@ module.exports = {
'plugins': [
'react',
'@typescript-eslint',
'react-hooks',
// Need to use a fork of the official rules of hooks because of this bug:
// https://github.com/facebook/react/issues/16265
'@seiyab/eslint-plugin-react-hooks',
// 'react-hooks',
'import',
],
'overrides': [

66
.gitignore vendored
View File

@ -831,6 +831,12 @@ packages/app-mobile/components/BackButtonDialogBox.js.map
packages/app-mobile/components/CameraView.d.ts
packages/app-mobile/components/CameraView.js
packages/app-mobile/components/CameraView.js.map
packages/app-mobile/components/CustomButton.d.ts
packages/app-mobile/components/CustomButton.js
packages/app-mobile/components/CustomButton.js.map
packages/app-mobile/components/Dropdown.d.ts
packages/app-mobile/components/Dropdown.js
packages/app-mobile/components/Dropdown.js.map
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.d.ts
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js.map
@ -861,12 +867,9 @@ packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleTwice.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleTwice.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleTwice.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.togglingLists.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.togglingLists.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.togglingLists.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js.map
@ -894,6 +897,24 @@ packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.js.map
packages/app-mobile/components/NoteEditor/EditLinkDialog.d.ts
packages/app-mobile/components/NoteEditor/EditLinkDialog.js
packages/app-mobile/components/NoteEditor/EditLinkDialog.js.map
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.d.ts
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js.map
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.d.ts
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js.map
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.d.ts
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js.map
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.d.ts
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js.map
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.d.ts
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js.map
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.d.ts
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js.map
packages/app-mobile/components/NoteEditor/NoteEditor.d.ts
packages/app-mobile/components/NoteEditor/NoteEditor.js
packages/app-mobile/components/NoteEditor/NoteEditor.js.map
@ -906,9 +927,21 @@ packages/app-mobile/components/NoteEditor/SelectionFormatting.js.map
packages/app-mobile/components/NoteEditor/types.d.ts
packages/app-mobile/components/NoteEditor/types.js
packages/app-mobile/components/NoteEditor/types.js.map
packages/app-mobile/components/ScreenHeader.d.ts
packages/app-mobile/components/ScreenHeader.js
packages/app-mobile/components/ScreenHeader.js.map
packages/app-mobile/components/SelectDateTimeDialog.d.ts
packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SelectDateTimeDialog.js.map
packages/app-mobile/components/SideMenu.d.ts
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenu.js.map
packages/app-mobile/components/getResponsiveValue.d.ts
packages/app-mobile/components/getResponsiveValue.js
packages/app-mobile/components/getResponsiveValue.js.map
packages/app-mobile/components/getResponsiveValue.test.d.ts
packages/app-mobile/components/getResponsiveValue.test.js
packages/app-mobile/components/getResponsiveValue.test.js.map
packages/app-mobile/components/screens/ConfigScreen.d.ts
packages/app-mobile/components/screens/ConfigScreen.js
packages/app-mobile/components/screens/ConfigScreen.js.map
@ -1527,6 +1560,9 @@ packages/lib/services/interop/InteropService_Importer_Md_frontmatter.test.js.map
packages/lib/services/interop/InteropService_Importer_Raw.d.ts
packages/lib/services/interop/InteropService_Importer_Raw.js
packages/lib/services/interop/InteropService_Importer_Raw.js.map
packages/lib/services/interop/InteropService_Importer_Raw.test.d.ts
packages/lib/services/interop/InteropService_Importer_Raw.test.js
packages/lib/services/interop/InteropService_Importer_Raw.test.js.map
packages/lib/services/interop/types.d.ts
packages/lib/services/interop/types.js
packages/lib/services/interop/types.js.map
@ -1953,6 +1989,18 @@ packages/pdf-viewer/hooks/useIsFocused.js.map
packages/pdf-viewer/hooks/useIsVisible.d.ts
packages/pdf-viewer/hooks/useIsVisible.js
packages/pdf-viewer/hooks/useIsVisible.js.map
packages/pdf-viewer/hooks/usePdfData.d.ts
packages/pdf-viewer/hooks/usePdfData.js
packages/pdf-viewer/hooks/usePdfData.js.map
packages/pdf-viewer/hooks/useScaledSize.d.ts
packages/pdf-viewer/hooks/useScaledSize.js
packages/pdf-viewer/hooks/useScaledSize.js.map
packages/pdf-viewer/hooks/useScrollSaver.d.ts
packages/pdf-viewer/hooks/useScrollSaver.js
packages/pdf-viewer/hooks/useScrollSaver.js.map
packages/pdf-viewer/main.d.ts
packages/pdf-viewer/main.js
packages/pdf-viewer/main.js.map
packages/pdf-viewer/miniViewer.d.ts
packages/pdf-viewer/miniViewer.js
packages/pdf-viewer/miniViewer.js.map
@ -1962,6 +2010,9 @@ packages/pdf-viewer/pdfSource.js.map
packages/pdf-viewer/pdfSource.test.d.ts
packages/pdf-viewer/pdfSource.test.js
packages/pdf-viewer/pdfSource.test.js.map
packages/pdf-viewer/ui/ZoomControls.d.ts
packages/pdf-viewer/ui/ZoomControls.js
packages/pdf-viewer/ui/ZoomControls.js.map
packages/plugin-repo-cli/commands/updateRelease.d.ts
packages/plugin-repo-cli/commands/updateRelease.js
packages/plugin-repo-cli/commands/updateRelease.js.map
@ -2206,3 +2257,6 @@ packages/tools/website/utils/types.d.ts
packages/tools/website/utils/types.js
packages/tools/website/utils/types.js.map
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
packages/app-mobile/components/get-responsive-value.test.js
packages/app-mobile/components/get-responsive-value.test.js
packages/app-mobile/components/get-responsive-value.test.js

View File

@ -116,16 +116,21 @@
});
};
const applyPeriod = (period) => {
subscriptionPeriod = period;
$('.plan-group').removeClass(period === 'monthly' ? 'plan-prices-yearly' : 'plan-prices-monthly');
$('.plan-group').addClass('plan-prices-' + period);
$("#pay-" + period + '-radio').prop('checked', true);
}
$(() => {
$("input[name='pay-radio']").change(function() {
const period = $("input[type='radio'][name='pay-radio']:checked").val();
subscriptionPeriod = period;
$('.plan-group').removeClass(period === 'monthly' ? 'plan-prices-yearly' : 'plan-prices-monthly');
$('.plan-group').addClass('plan-prices-' + period);
applyPeriod(period);
});
setupBetaHandling(urlQuery);
applyPeriod('yearly');
});
</script>
</div>

View File

@ -12,8 +12,8 @@
"node": ">=16"
},
"scripts": {
"buildParallel": "yarn workspaces foreach --verbose --interlaced --parallel --jobs 2 run build && yarn run tsc",
"buildSequential": "yarn workspaces foreach --verbose --interlaced run build && yarn run tsc",
"buildParallel": "yarn workspaces foreach --verbose --interlaced --parallel --jobs 2 --topological run build && yarn run tsc",
"buildSequential": "yarn workspaces foreach --verbose --interlaced --topological run build && yarn run tsc",
"buildApiDoc": "yarn workspace joplin start apidoc ../../readme/api/references/rest_api.md",
"buildCommandIndex": "gulp buildCommandIndex",
"buildPluginDoc": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme './Assets/PluginDocTheme/' --readme './Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out ../joplin-website/docs/api/references/plugin_api packages/lib/services/plugins/api/",
@ -31,6 +31,7 @@
"linter-ci": "eslint --resolve-plugins-relative-to . --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
"linter-precommit": "eslint --resolve-plugins-relative-to . --fix --ext .js --ext .jsx --ext .ts --ext .tsx",
"linter": "eslint --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
"linter-interactive": "eslint-interactive --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
"postinstall": "gulp build",
"publishAll": "git pull && yarn run buildParallel && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
"releaseAndroid": "PATH=\"/usr/local/opt/openjdk@11/bin:$PATH\" node packages/tools/release-android.js",
@ -61,13 +62,14 @@
}
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.6.0",
"@typescript-eslint/parser": "^4.6.0",
"@seiyab/eslint-plugin-react-hooks": "^4.5.1-alpha.5",
"@typescript-eslint/eslint-plugin": "^5.33.1",
"@typescript-eslint/parser": "^5.33.1",
"cspell": "^5.20.0",
"eslint": "^7.6.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-react": "^7.18.0",
"eslint-plugin-react-hooks": "^2.4.0",
"eslint": "^8.22.0",
"eslint-interactive": "^10.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-react": "^7.30.1",
"fs-extra": "^8.1.0",
"glob": "^7.1.6",
"gulp": "^4.0.2",
@ -76,9 +78,10 @@
"lint-staged": "^9.2.1",
"madge": "^4.0.2",
"typedoc": "^0.17.8",
"typescript": "4.0.5"
"typescript": "4.7.4"
},
"dependencies": {
"@types/fs-extra": "^9.0.13",
"http-server": "^0.12.3",
"node-gyp": "^8.4.1",
"nodemon": "^2.0.9"

View File

@ -484,13 +484,19 @@ class ConfigScreenComponent extends React.Component<any, any> {
} else {
const paths = await bridge().showOpenDialog();
if (!paths || !paths.length) return;
const cmd = splitCmd(this.state.settings[key]);
cmd[0] = paths[0];
updateSettingValue(key, joinCmd(cmd));
if (md.subType === 'file_path') {
updateSettingValue(key, paths[0]);
} else {
const cmd = splitCmd(this.state.settings[key]);
cmd[0] = paths[0];
updateSettingValue(key, joinCmd(cmd));
}
}
};
const cmd = splitCmd(this.state.settings[key]);
const path = md.subType === 'file_path_and_args' ? cmd[0] : this.state.settings[key];
const argComp = md.subType !== 'file_path_and_args' ? null : (
<div style={{ ...rowStyle, marginBottom: 5 }}>
@ -526,7 +532,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
onChange={(event: any) => {
onPathChange(event);
}}
value={cmd[0]}
value={path}
spellCheck={false}
/>
<Button

View File

@ -101,6 +101,7 @@ export default function(props: Props) {
const pluginSettings = useMemo(() => {
return pluginService.unserializePluginSettings(props.value);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.value]);
const pluginItems = usePluginItems(pluginService.plugins, pluginSettings);
@ -167,6 +168,7 @@ export default function(props: Props) {
});
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [pluginSettings, props.onChange]);
const onToggle = useCallback((event: ItemEvent) => {
@ -178,6 +180,7 @@ export default function(props: Props) {
});
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [pluginSettings, props.onChange]);
const onInstall = useCallback(async () => {
@ -195,6 +198,7 @@ export default function(props: Props) {
});
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [pluginSettings, props.onChange]);
const onBrowsePlugins = useCallback(() => {
@ -203,6 +207,7 @@ export default function(props: Props) {
const onPluginSettingsChange = useCallback((event: OnPluginSettingChangeEvent) => {
props.onChange({ value: pluginService.serializePluginSettings(event.value) });
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
const onUpdate = useOnInstallHandler(setUpdatingPluginIds, pluginSettings, repoApi, onPluginSettingsChange, true);
@ -229,6 +234,7 @@ export default function(props: Props) {
const onSearchPluginSettingsChange = useCallback((event: any) => {
props.onChange({ value: pluginService.serializePluginSettings(event.value) });
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.onChange]);
function renderCells(items: PluginItem[]) {

View File

@ -60,6 +60,7 @@ export default function(props: Props) {
setSearchResultCount(r.length);
}
});
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.searchQuery]);
const onChange = useCallback((event: OnChangeEvent) => {
@ -70,6 +71,7 @@ export default function(props: Props) {
const onSearchButtonClick = useCallback(() => {
setSearchStarted(false);
props.onSearchQueryChange({ value: '' });
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
function installState(pluginId: string): InstallState {

View File

@ -57,5 +57,6 @@ export default function(setInstallingPluginIds: Function, pluginSettings: Plugin
});
if (installError) alert(_('Could not install plugin: %s', installError.message));
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [pluginSettings, onPluginSettingsChange]);
}

View File

@ -16,8 +16,10 @@ export default (props: Props) => {
globalKeydownHandlersRef.current.push(elementId);
return () => {
const idx = globalKeydownHandlersRef.current.findIndex(e => e === elementId);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
globalKeydownHandlersRef.current.splice(idx, 1);
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
const isTopDialog = () => {
@ -49,6 +51,7 @@ export default (props: Props) => {
} else if (event.keyCode === 27) {
props.onCancelButtonClick();
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.onOkButtonClick, props.onCancelButtonClick]);
useEffect(() => {

View File

@ -81,6 +81,7 @@ export default function(props: Props) {
return;
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [onClose, folderTitle, folderIcon, props.folderId, props.parentId]);
const onFolderTitleChange = useCallback((event: any) => {

View File

@ -39,6 +39,7 @@ export const ShortcutRecorder = ({ onSave, onReset, onCancel, onError, initialAc
onError({ recorderError });
setSaveAllowed(false);
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [accelerator]);
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {

View File

@ -14,21 +14,24 @@ export const declaration: CommandDeclaration = {
export const runtime = (): CommandRuntime => {
return {
execute: async (context: CommandContext, selectedLanguage: string = null, useSpellChecker: boolean = null) => {
selectedLanguage = selectedLanguage === null ? context.state.settings['spellChecker.language'] : selectedLanguage;
execute: async (context: CommandContext, selectedLanguages: string[] = null, useSpellChecker: boolean = null) => {
selectedLanguages = selectedLanguages === null ? context.state.settings['spellChecker.languages'] : selectedLanguages;
useSpellChecker = useSpellChecker === null ? context.state.settings['spellChecker.enabled'] : useSpellChecker;
const menuItems = SpellCheckerService.instance().spellCheckerConfigMenuItems(selectedLanguage, useSpellChecker);
const menuItems = SpellCheckerService.instance().spellCheckerConfigMenuItems(selectedLanguages, useSpellChecker);
const menu = Menu.buildFromTemplate(menuItems as any);
menu.popup(bridge().window());
},
mapStateToTitle(state: AppState): string {
if (!state.settings['spellChecker.enabled']) return null;
const language = state.settings['spellChecker.language'];
if (!language) return null;
const s = language.split('-');
return s[0];
const languages = state.settings['spellChecker.languages'];
if (languages.length === 0) return null;
const s: string[] = [];
languages.forEach((language: string) => {
s.push(language.split('-')[0]);
});
return s.join(', ');
},
};
};

View File

@ -38,6 +38,7 @@ export default function(props: Props) {
if ([MasterPasswordStatus.NotSet, MasterPasswordStatus.Invalid].includes(status)) return false;
if (mode === Mode.Reset) return false;
return true;
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [status]);
const onClose = useCallback(() => {
@ -84,6 +85,7 @@ export default function(props: Props) {
}
return;
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [currentPassword, password1, onClose, mode]);
const needToRepeatPassword = useMemo(() => {

View File

@ -122,7 +122,7 @@ interface Props {
pluginMenuItems: any[];
pluginMenus: any[];
['spellChecker.enabled']: boolean;
['spellChecker.language']: string;
['spellChecker.languages']: string[];
plugins: PluginStates;
customCss: string;
locale: string;
@ -192,12 +192,17 @@ function useMenuStates(menu: any, props: Props) {
clearTimeout(timeoutId);
timeoutId = null;
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [
props.menuItemProps,
props.layoutButtonSequence,
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
props['notes.sortOrder.field'],
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
props['folders.sortOrder.field'],
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
props['notes.sortOrder.reverse'],
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
props['folders.sortOrder.reverse'],
props.showNoteCounts,
props.uncompletedTodosOnTop,
@ -276,6 +281,7 @@ function useMenu(props: Props) {
}
void CommandService.instance().execute('hideModalMessage');
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.selectedFolderId]);
const onMenuItemClickRef = useRef(null);
@ -292,6 +298,7 @@ function useMenu(props: Props) {
(commandName: string) => onMenuItemClickRef.current(commandName),
props.locale
);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [commandNames, pluginCommandNames, props.locale]);
const switchProfileMenuItems: any[] = useSwitchProfileMenuItems(props.profileConfig, menuItemDic);
@ -471,7 +478,7 @@ function useMenu(props: Props) {
}
toolsItems = toolsItems.concat(toolsItemsAll);
toolsItems.push(SpellCheckerService.instance().spellCheckerConfigMenuItem(props['spellChecker.language'], props['spellChecker.enabled']));
toolsItems.push(SpellCheckerService.instance().spellCheckerConfigMenuItem(props['spellChecker.languages'], props['spellChecker.enabled']));
function _checkForUpdates() {
void checkForUpdates(false, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
@ -905,13 +912,16 @@ function useMenu(props: Props) {
clearTimeout(timeoutId);
timeoutId = null;
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [
props.routeName,
props.pluginMenuItems,
props.pluginMenus,
keymapLastChangeTime,
modulesLastChangeTime,
props['spellChecker.language'],
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
props['spellChecker.languages'],
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
props['spellChecker.enabled'],
props.customCss,
props.locale,
@ -973,7 +983,7 @@ const mapStateToProps = (state: AppState) => {
showCompletedTodos: state.settings.showCompletedTodos,
pluginMenuItems: stateUtils.selectArrayShallow({ array: pluginUtils.viewsByType(state.pluginService.plugins, 'menuItem') }, 'menuBar.pluginMenuItems'),
pluginMenus: stateUtils.selectArrayShallow({ array: pluginUtils.viewsByType(state.pluginService.plugins, 'menu') }, 'menuBar.pluginMenus'),
['spellChecker.language']: state.settings['spellChecker.language'],
['spellChecker.languages']: state.settings['spellChecker.languages'],
['spellChecker.enabled']: state.settings['spellChecker.enabled'],
plugins: state.pluginService.plugins,
customCss: state.customCss,

View File

@ -70,6 +70,7 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
useEffect(() => {
const strippedText: string = markupToHtml().stripMarkup(props.markupLanguage, props.text);
countElements(strippedText, setStrippedWords, setStrippedCharacters, setStrippedCharactersNoSpace, setStrippedLines);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.text]);
useEffect(() => {

View File

@ -259,6 +259,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
return commandOutput;
},
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.content, props.visiblePanes, addListItem, wrapSelectionWithStrings, setEditorPercentScroll, setViewerPercentScroll, resetScroll]);
const onEditorPaste = useCallback(async (event: any = null) => {
@ -565,6 +566,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
return () => {
document.head.removeChild(element);
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.themeId, props.contentMaxWidth]);
const webview_domReady = useCallback(() => {
@ -592,6 +594,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
} else {
props.onMessage(event);
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.onMessage, props.content, setEditorPercentScroll]);
useEffect(() => {
@ -616,6 +619,8 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
mapsToLine: true,
// Always using useCustomPdfViewer for now, we can add a new setting for it in future if we need to.
useCustomPdfViewer: true,
noteId: props.noteId,
vendorDir: bridge().vendorDir(),
}));
if (cancelled) return;
@ -635,6 +640,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
cancelled = true;
shim.clearTimeout(timeoutId);
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.content, props.contentKey, renderedBodyContentKey, props.contentMarkupLanguage, props.visiblePanes, props.resourceInfos, props.markupToHtml]);
useEffect(() => {
@ -660,6 +666,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
} else {
console.error('Trying to set HTML on an undefined webview ref');
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [renderedBody, webviewReady]);
useEffect(() => {
@ -683,6 +690,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
props.setLocalSearchResultCount(matches);
}
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.searchMarkers, previousSearchMarkers, props.setLocalSearchResultCount, props.content, previousContent, renderedBody, previousRenderedBody, renderedBody]);
const cellEditorStyle = useMemo(() => {
@ -835,6 +843,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
return () => {
bridge().window().webContents.off('context-menu', onContextMenu);
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.plugins]);
function renderEditor() {

View File

@ -219,9 +219,11 @@ function Editor(props: EditorProps, ref: any) {
cm.off('dragover', editor_drag);
cm.off('refresh', editor_resize);
cm.off('update', editor_update);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
editorParent.current.removeChild(cm.getWrapperElement());
setEditor(null);
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
useEffect(() => {
@ -234,36 +236,42 @@ function Editor(props: EditorProps, ref: any) {
}
editor.setOption('screenReaderLabel', props.value);
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.value]);
useEffect(() => {
if (editor) {
editor.setOption('theme', props.codeMirrorTheme);
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.codeMirrorTheme]);
useEffect(() => {
if (editor) {
editor.setOption('mode', props.mode);
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.mode]);
useEffect(() => {
if (editor) {
editor.setOption('readOnly', props.readOnly);
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.readOnly]);
useEffect(() => {
if (editor) {
editor.setOption('autoCloseBrackets', props.autoMatchBraces);
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.autoMatchBraces]);
useEffect(() => {
if (editor) {
editor.setOption('keyMap', props.keyMap ? props.keyMap : 'default');
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.keyMap]);
useEffect(() => {

View File

@ -7,6 +7,8 @@ import uuid from '@joplin/lib/uuid';
import { reg } from '@joplin/lib/registry';
const loadedPluginIdSet = new Set<string>();
export default function useExternalPlugins(CodeMirror: any, plugins: PluginStates) {
const [options, setOptions] = useState({});
@ -17,6 +19,10 @@ export default function useExternalPlugins(CodeMirror: any, plugins: PluginState
for (const contentScript of contentScripts) {
try {
if (loadedPluginIdSet.has(contentScript.id)) {
continue;
}
const mod = contentScript.module;
if (mod.codeMirrorResources) {
@ -64,11 +70,14 @@ export default function useExternalPlugins(CodeMirror: any, plugins: PluginState
if (mod.plugin) {
mod.plugin(CodeMirror);
}
loadedPluginIdSet.add(contentScript.id);
} catch (error) {
reg.logger().error(error.toString());
}
}
setOptions(newOptions);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [plugins]);
function addInlineCss(cssStrings: string[], id: string) {

View File

@ -184,5 +184,6 @@ export default function useKeymap(CodeMirror: any) {
setupEmacs();
setupVim();
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
}

View File

@ -94,6 +94,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
if (editorRef.current) {
scheduleOnScroll({ percent });
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [scheduleOnScroll]);
const setViewerPercentScroll = useCallback((percent: number) => {
@ -101,6 +102,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
webviewRef.current.wrappedInstance.send('setPercentScroll', percent);
scheduleOnScroll({ percent });
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [scheduleOnScroll]);
const editor_scroll = useCallback(() => {
@ -126,6 +128,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
lastResizeHeight_.current = NaN;
lastLinesHeight_.current = NaN;
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [setViewerPercentScroll]);
const resetScroll = useCallback(() => {
@ -134,6 +137,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
editorRef.current.setScrollPercent(0);
scrollTopIsUncertain_.current = false;
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
const editor_resize = useCallback((cm) => {
@ -152,6 +156,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
lastResizeHeight_.current = NaN;
lastLinesHeight_.current = NaN;
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
// When heights of lines are updated in CodeMirror, 'update' events are raised.
@ -173,6 +178,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
lastResizeHeight_.current = NaN;
lastLinesHeight_.current = NaN;
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
const getLineScrollPercent = useCallback(() => {
@ -183,6 +189,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
} else {
return scrollPercent_.current;
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
return {

View File

@ -280,6 +280,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
return true;
},
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [editor, props.contentMarkupLanguage, props.contentOriginalCss]);
// -----------------------------------------------------------------------------------------
@ -512,6 +513,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
// style and re-applying it on editorReady gives our styles precedence and prevents any flashing
//
// tl;dr: editorReady is used here because the css needs to be re-applied after TinyMCE init
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [editorReady, props.themeId]);
// -----------------------------------------------------------------------------------------
@ -680,6 +682,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
};
void loadEditor();
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [scriptLoaded]);
// -----------------------------------------------------------------------------------------
@ -829,6 +832,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
return () => {
cancelled = true;
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [editor, props.markupToHtml, props.allAssets, props.content, props.resourceInfos, props.contentKey]);
useEffect(() => {
@ -909,6 +913,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
return () => {
void execOnChangeEvent();
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
const onChangeHandlerTimeoutRef = useRef<any>(null);
@ -1091,6 +1096,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
console.warn('Error removing events', error);
}
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.onWillChange, props.onChange, props.contentMarkupLanguage, props.contentOriginalCss, editor]);
// -----------------------------------------------------------------------------------------

View File

@ -59,6 +59,7 @@ function NoteEditor(props: NoteEditorProps) {
const formNote_beforeLoad = useCallback(async (event: OnLoadEvent) => {
await saveNoteIfWillChange(event.formNote);
setShowRevisions(false);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
const formNote_afterLoad = useCallback(async () => {
@ -177,6 +178,7 @@ function NoteEditor(props: NoteEditorProps) {
id: formNote.id,
});
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.isProvisional, formNote.id]);
const previousNoteId = usePrevious(formNote.id);
@ -194,6 +196,7 @@ function NoteEditor(props: NoteEditorProps) {
});
void ResourceEditWatcher.instance().stopWatchingAll();
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [formNote.id, previousNoteId]);
const onFieldChange = useCallback((field: string, value: any, changeId = 0) => {
@ -238,6 +241,7 @@ function NoteEditor(props: NoteEditorProps) {
setFormNote(newNote);
scheduleSaveNote(newNote);
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [handleProvisionalFlag, formNote, isNewNote, titleHasBeenManuallyChanged]);
useWindowCommandHandler({
@ -288,6 +292,7 @@ function NoteEditor(props: NoteEditorProps) {
id: formNote.id,
status: 'saving',
});
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [formNote, handleProvisionalFlag]);
const onMessage = useMessageHandler(scrollWhenReady, setScrollWhenReady, editorRef, setLocalSearchResultCount, props.dispatch, formNote);
@ -302,6 +307,7 @@ function NoteEditor(props: NoteEditorProps) {
setFormNote(newFormNote);
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [formNote]);
const onNotePropertyChange = useCallback((event) => {
@ -317,6 +323,7 @@ function NoteEditor(props: NoteEditorProps) {
return newFormNote;
});
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
useEffect(() => {
@ -350,6 +357,7 @@ function NoteEditor(props: NoteEditorProps) {
noteId: formNoteRef.current.id,
percent: event.percent,
});
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.dispatch, formNote]);
function renderNoNotes(rootStyle: any) {
@ -413,6 +421,9 @@ function NoteEditor(props: NoteEditorProps) {
fontSize: Setting.value('style.editor.fontSize'),
contentMaxWidth: props.contentMaxWidth,
isSafeMode: props.isSafeMode,
// We need it to identify the context for which media is rendered.
// It is currently used to remember pdf scroll position for each attacments of each note uniquely.
noteId: props.noteId,
};
let editor = null;

View File

@ -75,6 +75,7 @@ export interface NoteBodyEditorProps {
fontSize: number;
contentMaxWidth: number;
isSafeMode: boolean;
noteId: string;
}
export interface FormNote {

View File

@ -51,5 +51,6 @@ export default function useDropHandler(dependencies: HookDependencies) {
},
});
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
}

View File

@ -138,6 +138,7 @@ export default function useFormNote(dependencies: HookDependencies) {
return () => {
cancelled = true;
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [prevSyncStarted, syncStarted, formNote]);
useEffect(() => {
@ -188,6 +189,7 @@ export default function useFormNote(dependencies: HookDependencies) {
return () => {
cancelled = true;
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [noteId, isProvisional, formNote]);
const onResourceChange = useCallback(async function(event: any = null) {

View File

@ -21,6 +21,8 @@ export interface MarkupToHtmlOptions {
bodyOnly?: boolean;
mapsToLine?: boolean;
useCustomPdfViewer?: boolean;
noteId?: string;
vendorDir?: string;
}
export default function useMarkupToHtml(deps: HookDependencies) {
@ -31,6 +33,7 @@ export default function useMarkupToHtml(deps: HookDependencies) {
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
customCss: customCss || '',
});
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [plugins, customCss]);
return useCallback(async (markupLanguage: number, md: string, options: MarkupToHtmlOptions = null): Promise<any> => {
@ -62,5 +65,6 @@ export default function useMarkupToHtml(deps: HookDependencies) {
}, options));
return result;
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [themeId, customCss, markupToHtml]);
}

View File

@ -56,5 +56,6 @@ export default function useMessageHandler(scrollWhenReady: any, setScrollWhenRea
await CommandService.instance().execute('openItem', msg);
// bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg));
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [dispatch, setLocalSearchResultCount, scrollWhenReady, formNote]);
}

View File

@ -8,5 +8,6 @@ export default function usePluginServiceRegistration(ref: any) {
return () => {
PlatformImplementation.instance().unregisterComponent('textEditor');
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
}

View File

@ -32,5 +32,6 @@ export default function useSearchMarkers(showLocalSearch: boolean, localSearchMa
output.keywords = highlightedWords;
return output;
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [highlightedWords, showLocalSearch, localSearchMarkerOptions, searches, selectedSearchId]);
}

View File

@ -96,5 +96,6 @@ export default function useWindowCommandHandler(dependencies: HookDependencies)
CommandService.instance().unregisterRuntime(command.declaration.name);
}
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [editorRef, setShowLocalSearch, noteSearchBarRef, titleInputRef]);
}

View File

@ -273,6 +273,7 @@ const NoteListComponent = (props: Props) => {
onTitleClick={noteItem_titleClick}
onContextMenu={itemContextMenu}
/>;
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [style, props.themeId, width, itemHeight, dragOverTargetNoteIndex, props.provisionalNoteIds, props.selectedNoteIds, props.watchedNoteFiles,
props.notes,
props.notesParentType,
@ -305,6 +306,7 @@ const NoteListComponent = (props: Props) => {
if (previousVisible !== props.visible) {
updateSizeState();
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [previousSelectedNoteIds,previousNotes, previousVisible, props.selectedNoteIds, props.notes]);
const scrollNoteIndex_ = (keyCode: any, ctrlKey: any, metaKey: any, noteIndex: any) => {
@ -439,6 +441,7 @@ const NoteListComponent = (props: Props) => {
return () => {
props.resizableLayoutEventEmitter.off('resize', resizableLayout_resize);
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.resizableLayoutEventEmitter]);
useEffect(() => {
@ -453,6 +456,7 @@ const NoteListComponent = (props: Props) => {
};
}, []);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
useEffect(() => {
// When a note list item is styled by userchrome.css, its height is reflected.
// Ref. https://github.com/laurent22/joplin/pull/6542

View File

@ -13,5 +13,6 @@ export default function useWindowResizeEvent(eventEmitter: any) {
window_resize.clear();
window.removeEventListener('resize', window_resize);
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
}

View File

@ -66,6 +66,7 @@ function useRestartOnDone(upgradeResult: SyncTargetUpgradeResult) {
if (upgradeResult.done && !upgradeResult.error) {
void restart();
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [upgradeResult.done]);
}

View File

@ -55,6 +55,7 @@ function SearchBar(props: Props) {
return () => {
debouncedSearch.clear();
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [query, searchStarted]);
const onExitSearch = useCallback(async (navigateAway = true) => {
@ -80,6 +81,7 @@ function SearchBar(props: Props) {
}
}
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.selectedNoteId]);
function onChange(event: any) {
@ -129,6 +131,7 @@ function SearchBar(props: Props) {
field: 'globalSearch',
});
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [onExitSearch, props.isFocused, searchStarted]);
useEffect(() => {
@ -150,6 +153,7 @@ function SearchBar(props: Props) {
}
void onExitSearch(true);
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
return (

View File

@ -140,6 +140,7 @@ function ShareFolderDialog(props: Props) {
useEffect(() => {
const s = props.shares.find(s => s.folder_id === props.folderId);
setShare(s);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.shares]);
useEffect(() => {

View File

@ -23,5 +23,6 @@ export default function useEffectDebugger(effectHook: any, dependencies: any, de
console.log('[use-effet-debugger] ', changedDeps);
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
useEffect(effectHook, dependencies);
}

View File

@ -23,5 +23,6 @@ export default function useImperativeHandleDebugger(ref: any, effectHook: any, d
console.log('[use-imperativeHandler-debugger] ', changedDeps);
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
useImperativeHandle(ref, effectHook, dependencies);
}

View File

@ -69,6 +69,7 @@ function UserWebview(props: Props, ref: any) {
useEffect(() => {
if (isReady && props.onReady) props.onReady();
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [isReady]);
function frameWindow() {

View File

@ -33,6 +33,7 @@ export default function(frameWindow: any, htmlHash: string, minWidth: number, mi
useEffect(() => {
updateContentSize(htmlHash);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [htmlHash]);
useEffect(() => {
@ -55,6 +56,7 @@ export default function(frameWindow: any, htmlHash: string, minWidth: number, mi
return () => {
clearInterval(updateFrameSizeIID);
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [fitToContent, isReady, minWidth, minHeight, htmlHash]);
return contentSize;

View File

@ -45,6 +45,7 @@ export default function(frameWindow: any, isReady: boolean, postMessage: Functio
hash: htmlHash,
html: html,
});
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [html, htmlHash, isReady]);
return loadedHtmlHash;

View File

@ -4,10 +4,12 @@ export default function(postMessage: Function, isReady: boolean, scripts: string
useEffect(() => {
if (!isReady) return;
postMessage('setScripts', { scripts: scripts });
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [scripts, isReady]);
useEffect(() => {
if (!isReady || !cssFilePath) return;
postMessage('setScript', { script: cssFilePath, key: 'themeCss' });
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [isReady, cssFilePath]);
}

View File

@ -43,8 +43,10 @@ export default function useViewIsReady(viewRef: any) {
return () => {
viewRef.current.removeEventListener('dom-ready', onIFrameReady);
viewRef.current.removeEventListener('load', onIFrameReady);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
viewRef.current.contentWindow.removeEventListener('message', onMessage);
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
return iframeReady && iframeContentReady;

View File

@ -10,6 +10,7 @@ export default function(frameWindow: any, isReady: boolean, pluginId: string, vi
return () => {
PostMessageService.instance().unregisterResponder(ResponderComponentType.UserWebview, viewId);
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [viewId]);
useEffect(() => {
@ -39,5 +40,6 @@ export default function(frameWindow: any, isReady: boolean, pluginId: string, vi
return () => {
frameWindow.removeEventListener('message', onMessage_);
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [frameWindow, isReady, pluginId, viewId]);
}

View File

@ -2,7 +2,6 @@
import SpellCheckerServiceDriverBase from '@joplin/lib/services/spellChecker/SpellCheckerServiceDriverBase';
import bridge from '../bridge';
import { languageCodeOnly, localesFromLanguageCode } from '@joplin/lib/locale';
import Logger from '@joplin/lib/Logger';
const logger = Logger.create('SpellCheckerServiceDriverNative');
@ -17,35 +16,17 @@ export default class SpellCheckerServiceDriverNative extends SpellCheckerService
return this.session().availableSpellCheckerLanguages;
}
// Language can be set to '' to disable spell-checking
public setLanguage(v: string) {
// Language can be set to [] to disable spell-checking
public setLanguages(v: string[]) {
// If we pass an empty array, it disables spell checking
// https://github.com/electron/electron/issues/25228
if (!v) {
if (v.length === 0) {
this.session().setSpellCheckerLanguages([]);
return;
}
// The below function will throw an error if the provided language is
// not supported, so we provide fallbacks.
// https://github.com/laurent22/joplin/issues/4146
const languagesToTry = [
v,
languageCodeOnly(v),
].concat(localesFromLanguageCode(languageCodeOnly(v), this.availableLanguages));
for (const toTry of languagesToTry) {
try {
this.session().setSpellCheckerLanguages([toTry]);
logger.info(`Set effective language from "${v}" to "${toTry}"`);
return;
} catch (error) {
logger.warn(`Failed to set language to "${toTry}". Will try the next one in this list: ${JSON.stringify(languagesToTry)}`);
logger.warn('Error was:', error);
}
}
logger.error(`Could not set language to: ${v}`);
this.session().setSpellCheckerLanguages(v);
logger.info(`Set effective languages to "${v}"`);
}
public get language(): string {

View File

@ -48,20 +48,6 @@ function convertJsx(paths) {
});
}
function build(path) {
chdir(path);
const result = spawnSync('yarn', ['run', 'build'], { shell: true });
if (result.status !== 0) {
const msg = [];
if (result.stdout) msg.push(result.stdout.toString());
if (result.stderr) msg.push(result.stderr.toString());
console.error(msg.join('\n'));
if (result.error) console.error(result.error);
process.exit(result.status);
}
}
module.exports = function() {
convertJsx([
`${__dirname}/../gui`,
@ -70,8 +56,6 @@ module.exports = function() {
`${__dirname}/../plugins`,
]);
build(`${__dirname}/../../pdf-viewer`);
const libContent = [
fs.readFileSync(`${basePath}/packages/lib/string-utils-common.js`, 'utf8'),
fs.readFileSync(`${basePath}/packages/lib/markJsUtils.js`, 'utf8'),

View File

@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.cozic.joplin"
android:installLocation="auto">
package="net.cozic.joplin"
android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
@ -8,7 +8,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!-- Make these features optional to enable Chromebooks -->
<!-- Make these features optional to enable Chromebooks -->
<!-- https://github.com/laurent22/joplin/issues/37 -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
@ -58,14 +58,19 @@
This is recommended in the React docs: https://reactnavigation.org/docs/deep-linking. In practice, "singleTask" and "singleTop" are
largely similar, but "singleTask" is more strict in preventing multiple instances of the app from being created if another app
explicitly requests it.
2022-08-12: Added `screenLayout` and `smallestScreenSize` to `android:configChanges`.
This prevents the application from being re-constructed on
screen orientation change/window resizes on some devices.
See https://github.com/laurent22/joplin/pull/6737.
-->
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@ -93,6 +98,6 @@
</activity>
<!-- /SHARE EXTENSION -->
</application>
</application>
</manifest>

View File

@ -0,0 +1,190 @@
//
// A button with a long-press action. Long-pressing the button displays a tooltip
//
const React = require('react');
import { ReactNode } from 'react';
import { themeStyle } from '@joplin/lib/theme';
import { Theme } from '@joplin/lib/themes/type';
import { useState, useMemo, useCallback, useRef } from 'react';
import { View, Text, Pressable, ViewStyle, PressableStateCallbackType, StyleProp, StyleSheet, LayoutChangeEvent, LayoutRectangle, Animated, AccessibilityState, AccessibilityRole } from 'react-native';
import { Menu, MenuOptions, MenuTrigger, renderers } from 'react-native-popup-menu';
type ButtonClickListener = ()=> void;
interface ButtonProps {
onPress: ButtonClickListener;
// Accessibility label and text shown in a tooltip
description?: string;
children: ReactNode;
themeId: number;
style?: ViewStyle;
pressedStyle?: ViewStyle;
contentStyle?: ViewStyle;
// Additional accessibility information. See View.accessibilityHint
accessibilityHint?: string;
// Role of the button. Defaults to 'button'.
accessibilityRole?: AccessibilityRole;
accessibilityState?: AccessibilityState;
disabled?: boolean;
}
const CustomButton = (props: ButtonProps) => {
const [tooltipVisible, setTooltipVisible] = useState(false);
const [buttonLayout, setButtonLayout] = useState<LayoutRectangle|null>(null);
const tooltipStyles = useTooltipStyles(props.themeId);
// See https://blog.logrocket.com/react-native-touchable-vs-pressable-components/
// for more about animating Pressable buttons.
const fadeAnim = useRef(new Animated.Value(1)).current;
const animationDuration = 100; // ms
const onPressIn = useCallback(() => {
// Fade out.
Animated.timing(fadeAnim, {
toValue: 0.5,
duration: animationDuration,
useNativeDriver: true,
}).start();
}, [fadeAnim]);
const onPressOut = useCallback(() => {
// Fade in.
Animated.timing(fadeAnim, {
toValue: 1,
duration: animationDuration,
useNativeDriver: true,
}).start();
setTooltipVisible(false);
}, [fadeAnim]);
const onLongPress = useCallback(() => {
setTooltipVisible(true);
}, []);
// Select different user-specified styles if selected/unselected.
const onStyleChange = useCallback((state: PressableStateCallbackType): StyleProp<ViewStyle> => {
let result = { ...props.style };
if (state.pressed) {
result = {
...result,
...props.pressedStyle,
};
}
return result;
}, [props.pressedStyle, props.style]);
const onButtonLayout = useCallback((event: LayoutChangeEvent) => {
const layoutEvt = event.nativeEvent.layout;
// Copy the layout event
setButtonLayout({ ...layoutEvt });
}, []);
const button = (
<Pressable
onPress={props.onPress}
onLongPress={onLongPress}
onPressIn={onPressIn}
onPressOut={onPressOut}
style={ onStyleChange }
disabled={ props.disabled ?? false }
onLayout={ onButtonLayout }
accessibilityLabel={props.description}
accessibilityHint={props.accessibilityHint}
accessibilityRole={props.accessibilityRole ?? 'button'}
accessibilityState={props.accessibilityState}
>
<Animated.View style={{
opacity: fadeAnim,
...props.contentStyle,
}}>
{ props.children }
</Animated.View>
</Pressable>
);
const tooltip = (
<View
// Any information given by the tooltip should also be provided via
// [accessibilityLabel]/[accessibilityHint]. As such, we can hide the tooltip
// from the screen reader.
// On Android:
importantForAccessibility='no-hide-descendants'
// On iOS:
accessibilityElementsHidden={true}
// Position the menu beneath the button so the tooltip appears in the
// correct location.
style={{
left: buttonLayout?.x,
top: buttonLayout?.y,
position: 'absolute',
zIndex: -1,
}}
>
<Menu
opened={tooltipVisible}
renderer={renderers.Popover}
rendererProps={{
preferredPlacement: 'bottom',
anchorStyle: tooltipStyles.anchor,
}}>
<MenuTrigger
// Don't show/hide when pressed (let the Pressable handle opening/closing)
disabled={true}
style={{
// Ensure that the trigger region has the same size as the button.
width: buttonLayout?.width ?? 0,
height: buttonLayout?.height ?? 0,
}}
/>
<MenuOptions
customStyles={{ optionsContainer: tooltipStyles.optionsContainer }}
>
<Text style={tooltipStyles.text}>
{props.description}
</Text>
</MenuOptions>
</Menu>
</View>
);
return (
<>
{props.description ? tooltip : null}
{button}
</>
);
};
const useTooltipStyles = (themeId: number) => {
return useMemo(() => {
const themeData: Theme = themeStyle(themeId);
return StyleSheet.create({
text: {
color: themeData.raisedColor,
padding: 4,
},
anchor: {
backgroundColor: themeData.raisedBackgroundColor,
},
optionsContainer: {
backgroundColor: themeData.raisedBackgroundColor,
},
});
}, [themeId]);
};
export default CustomButton;

View File

@ -1,31 +1,60 @@
const React = require('react');
const { TouchableOpacity, TouchableWithoutFeedback, Dimensions, Text, Modal, View } = require('react-native');
import { TouchableOpacity, TouchableWithoutFeedback, Dimensions, Text, Modal, View, LayoutRectangle, ViewStyle, TextStyle } from 'react-native';
import { Component } from 'react';
const { ItemList } = require('./ItemList.js');
class Dropdown extends React.Component {
constructor() {
super();
type ValueType = string;
export interface DropdownListItem {
label: string;
value: ValueType;
}
this.headerRef_ = null;
}
export type OnValueChangedListener = (newValue: ValueType)=> void;
UNSAFE_componentWillMount() {
this.setState({
interface DropdownProps {
listItemStyle?: ViewStyle;
itemListStyle?: ViewStyle;
itemWrapperStyle?: ViewStyle;
headerWrapperStyle?: ViewStyle;
headerStyle?: TextStyle;
itemStyle?: TextStyle;
disabled?: boolean;
labelTransform?: 'trim';
items: DropdownListItem[];
selectedValue: ValueType|null;
onValueChange?: OnValueChangedListener;
}
interface DropdownState {
headerSize: LayoutRectangle;
listVisible: boolean;
}
class Dropdown extends Component<DropdownProps, DropdownState> {
private headerRef: TouchableOpacity;
public constructor(props: DropdownProps) {
super(props);
this.headerRef = null;
this.state = {
headerSize: { x: 0, y: 0, width: 0, height: 0 },
listVisible: false,
});
};
}
updateHeaderCoordinates() {
private updateHeaderCoordinates() {
// https://stackoverflow.com/questions/30096038/react-native-getting-the-position-of-an-element
this.headerRef_.measure((fx, fy, width, height, px, py) => {
this.headerRef.measure((_fx, _fy, width, height, px, py) => {
this.setState({
headerSize: { x: px, y: py, width: width, height: height },
});
});
}
render() {
public render() {
const items = this.props.items;
const itemHeight = 60;
const windowHeight = Dimensions.get('window').height - 50;
@ -84,23 +113,26 @@ class Dropdown extends React.Component {
}
}
if (this.props.labelTransform && this.props.labelTransform === 'trim') headerLabel = headerLabel.trim();
if (this.props.labelTransform && this.props.labelTransform === 'trim') {
headerLabel = headerLabel.trim();
}
const closeList = () => {
this.setState({ listVisible: false });
};
const itemRenderer = item => {
const itemRenderer = (item: DropdownListItem) => {
const key = item.value.toString();
return (
<TouchableOpacity
style={itemWrapperStyle}
key={item.value}
key={key}
onPress={() => {
closeList();
if (this.props.onValueChange) this.props.onValueChange(item.value);
}}
>
<Text ellipsizeMode="tail" numberOfLines={1} style={itemStyle} key={item.value}>
<Text ellipsizeMode="tail" numberOfLines={1} style={itemStyle} key={key}>
{item.label}
</Text>
</TouchableOpacity>
@ -111,7 +143,7 @@ class Dropdown extends React.Component {
<View style={{ flex: 1, flexDirection: 'column' }}>
<TouchableOpacity
style={headerWrapperStyle}
ref={ref => (this.headerRef_ = ref)}
ref={ref => (this.headerRef = ref)}
disabled={this.props.disabled}
onPress={() => {
this.updateHeaderCoordinates();
@ -141,9 +173,7 @@ class Dropdown extends React.Component {
style={itemListStyle}
items={this.props.items}
itemHeight={itemHeight}
itemRenderer={item => {
return itemRenderer(item);
}}
itemRenderer={itemRenderer}
/>
</View>
</View>
@ -154,4 +184,5 @@ class Dropdown extends React.Component {
}
}
module.exports = { Dropdown };
export default Dropdown;
export { Dropdown };

View File

@ -36,5 +36,6 @@ export default function useOnResourceLongPress(onJoplinLinkClick: Function, dial
reg.logger().error('Could not handle link long press', e);
ToastAndroid.show('An error occurred, check log for details', ToastAndroid.SHORT);
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [onJoplinLinkClick]);
}

View File

@ -39,6 +39,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
const markupToHtml = useMemo(() => {
return markupLanguageUtils.newMarkupToHtml();
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [isFirstRender]);
// To address https://github.com/laurent22/joplin/issues/433
@ -82,7 +83,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
resources: noteResources,
codeTheme: theme.codeThemeCss,
postMessageSyntax: 'window.joplinPostMessage_',
enableLongPress: shim.mobilePlatform() === 'android', // On iOS, there's already a built-on open/share menu
enableLongPress: true,
};
// Whenever a resource state changes, for example when it goes from "not downloaded" to "downloaded", the "noteResources"
@ -202,6 +203,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
return () => {
cancelled = true;
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, effectDependencies);
return { source, injectedJs };

View File

@ -302,7 +302,10 @@ export function initCodeMirror(
decoratorExtension,
EditorView.lineWrapping,
EditorView.contentAttributes.of({ autocapitalize: 'sentence' }),
EditorView.contentAttributes.of({
autocapitalize: 'sentence',
spellcheck: settings.spellcheckEnabled ? 'true' : 'false',
}),
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
notifyDocChanged(viewUpdate);
notifySelectionChange(viewUpdate);
@ -381,10 +384,6 @@ export function initCodeMirror(
closeSearchPanel(editor);
}
},
setSpellcheckEnabled: (enabled: boolean) => {
editor.contentDOM.spellcheck = enabled;
notifySelectionFormattingChange();
},
// Formatting
toggleBolded: () => { toggleBolded(editor); },

View File

@ -1,13 +1,13 @@
import { markdown } from '@codemirror/lang-markdown';
import { GFM as GithubFlavoredMarkdownExt } from '@lezer/markdown';
import { indentUnit } from '@codemirror/language';
import { forceParsing, indentUnit } from '@codemirror/language';
import { SelectionRange, EditorSelection, EditorState } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { MarkdownMathExtension } from './markdownMathParser';
// Creates and returns a minimal editor with markdown extensions
const createEditor = (initialText: string, initialSelection: SelectionRange): EditorView => {
return new EditorView({
const editor = new EditorView({
doc: initialText,
selection: EditorSelection.create([initialSelection]),
extensions: [
@ -18,6 +18,9 @@ const createEditor = (initialText: string, initialSelection: SelectionRange): Ed
EditorState.tabSize.of(4),
],
});
forceParsing(editor);
return editor;
};
export default createEditor;

View File

@ -7,8 +7,6 @@ export interface CodeMirrorControl {
select(anchor: number, head: number): void;
insertText(text: string): void;
setSpellcheckEnabled(enabled: boolean): void;
// Toggle whether we're in a type of region.
toggleBolded(): void;
toggleItalicized(): void;

View File

@ -20,7 +20,7 @@ interface LinkDialogProps {
const EditLinkDialog = (props: LinkDialogProps) => {
// The content of the link selected in the editor (if any)
const editorLinkData = props.selectionState.linkData;
const editorLinkData = props.selectionState.linkData ?? {};
const [linkLabel, setLinkLabel] = useState('');
const [linkURL, setLinkURL] = useState('');

View File

@ -0,0 +1,365 @@
// A toolbar for the markdown editor.
const React = require('react');
import { Platform, StyleSheet, View } from 'react-native';
import { useMemo, useState, useCallback } from 'react';
// See https://oblador.github.io/react-native-vector-icons/ for a list of
// available icons.
const AntIcon = require('react-native-vector-icons/AntDesign').default;
const FontAwesomeIcon = require('react-native-vector-icons/FontAwesome5').default;
const MaterialIcon = require('react-native-vector-icons/MaterialIcons').default;
import { _ } from '@joplin/lib/locale';
import time from '@joplin/lib/time';
import { useEffect } from 'react';
import { Keyboard, ViewStyle } from 'react-native';
import { EditorControl, EditorSettings, ListType, SearchState } from '../types';
import SelectionFormatting from '../SelectionFormatting';
import { ButtonSpec, StyleSheetData } from './types';
import Toolbar from './Toolbar';
import { buttonSize } from './ToolbarButton';
import { Theme } from '@joplin/lib/themes/type';
type OnAttachCallback = ()=> void;
interface MarkdownToolbarProps {
editorControl: EditorControl;
selectionState: SelectionFormatting;
searchState: SearchState;
editorSettings: EditorSettings;
onAttach: OnAttachCallback;
style?: ViewStyle;
}
const MarkdownToolbar = (props: MarkdownToolbarProps) => {
const themeData = props.editorSettings.themeData;
const styles = useStyles(props.style, themeData);
const selState = props.selectionState;
const editorControl = props.editorControl;
const headerButtons: ButtonSpec[] = [];
for (let level = 1; level <= 5; level++) {
const active = selState.headerLevel === level;
let label;
if (!active) {
label = _('Create header level %d', level);
} else {
label = _('Remove level %d header', level);
}
headerButtons.push({
icon: `H${level}`,
description: label,
active,
// We only call addHeaderButton 5 times and in the same order, so
// the linter error is safe to ignore.
// eslint-disable-next-line @seiyab/react-hooks/rules-of-hooks
onPress: useCallback(() => {
editorControl.toggleHeaderLevel(level);
}, [editorControl, level]),
// Make it likely for the first three header buttons to show, less likely for
// the others.
priority: level < 3 ? 2 : 0,
});
}
const listButtons: ButtonSpec[] = [];
listButtons.push({
icon: (
<FontAwesomeIcon name="list-ul" style={styles.text}/>
),
description:
selState.inUnorderedList ? _('Remove unordered list') : _('Create unordered list'),
active: selState.inUnorderedList,
onPress: useCallback(() => {
editorControl.toggleList(ListType.UnorderedList);
}, [editorControl]),
priority: -2,
});
listButtons.push({
icon: (
<FontAwesomeIcon name="list-ol" style={styles.text}/>
),
description:
selState.inOrderedList ? _('Remove ordered list') : _('Create ordered list'),
active: selState.inOrderedList,
onPress: useCallback(() => {
editorControl.toggleList(ListType.OrderedList);
}, [editorControl]),
priority: -2,
});
listButtons.push({
icon: (
<FontAwesomeIcon name="tasks" style={styles.text}/>
),
description:
selState.inChecklist ? _('Remove task list') : _('Create task list'),
active: selState.inChecklist,
onPress: useCallback(() => {
editorControl.toggleList(ListType.CheckList);
}, [editorControl]),
priority: -2,
});
listButtons.push({
icon: (
<AntIcon name="indent-left" style={styles.text}/>
),
description: _('Decrease indent level'),
onPress: editorControl.decreaseIndent,
priority: -1,
});
listButtons.push({
icon: (
<AntIcon name="indent-right" style={styles.text}/>
),
description: _('Increase indent level'),
onPress: editorControl.increaseIndent,
priority: -1,
});
// Inline formatting
const inlineFormattingBtns: ButtonSpec[] = [];
inlineFormattingBtns.push({
icon: (
<FontAwesomeIcon name="bold" style={styles.text}/>
),
description:
selState.bolded ? _('Unbold') : _('Bold text'),
active: selState.bolded,
onPress: editorControl.toggleBolded,
priority: 3,
});
inlineFormattingBtns.push({
icon: (
<FontAwesomeIcon name="italic" style={styles.text}/>
),
description:
selState.italicized ? _('Unitalicize') : _('Italicize'),
active: selState.italicized,
onPress: editorControl.toggleItalicized,
priority: 2,
});
inlineFormattingBtns.push({
icon: '{;}',
description:
selState.inCode ? _('Remove code formatting') : _('Format as code'),
active: selState.inCode,
onPress: editorControl.toggleCode,
priority: 2,
});
if (props.editorSettings.katexEnabled) {
inlineFormattingBtns.push({
icon: '∑',
description:
selState.inMath ? _('Remove TeX region') : _('Create TeX region'),
active: selState.inMath,
onPress: editorControl.toggleMath,
priority: 1,
});
}
inlineFormattingBtns.push({
icon: (
<FontAwesomeIcon name="link" style={styles.text}/>
),
description:
selState.inLink ? _('Edit link') : _('Create link'),
active: selState.inLink,
onPress: editorControl.showLinkDialog,
priority: -3,
});
// Actions
const actionButtons: ButtonSpec[] = [];
actionButtons.push({
icon: (
<FontAwesomeIcon name="calendar-plus" style={styles.text}/>
),
description: _('Insert time'),
onPress: useCallback(() => {
editorControl.insertText(time.formatDateToLocal(new Date()));
}, [editorControl]),
});
const onDismissKeyboard = useCallback(() => {
// Keyboard.dismiss() doesn't dismiss the keyboard if it's editing the WebView.
Keyboard.dismiss();
// As such, dismiss the keyboard by sending a message to the View.
editorControl.hideKeyboard();
}, [editorControl]);
actionButtons.push({
icon: (
<MaterialIcon name="attachment" style={styles.text}/>
),
description: _('Attach'),
onPress: useCallback(() => {
onDismissKeyboard();
props.onAttach();
}, [props.onAttach, onDismissKeyboard]),
});
actionButtons.push({
icon: (
<MaterialIcon name="search" style={styles.text}/>
),
description: (
props.searchState.dialogVisible ? _('Close find and replace') : _('Find and replace')
),
active: props.searchState.dialogVisible,
onPress: useCallback(() => {
if (props.searchState.dialogVisible) {
editorControl.searchControl.hideSearch();
} else {
editorControl.searchControl.showSearch();
}
}, [editorControl, props.searchState.dialogVisible]),
priority: -3,
});
const [keyboardVisible, setKeyboardVisible] = useState(false);
const [hasSoftwareKeyboard, setHasSoftwareKeyboard] = useState(false);
useEffect(() => {
const showListener = Keyboard.addListener('keyboardDidShow', () => {
setKeyboardVisible(true);
setHasSoftwareKeyboard(true);
});
const hideListener = Keyboard.addListener('keyboardDidHide', () => {
setKeyboardVisible(false);
});
return (() => {
showListener.remove();
hideListener.remove();
});
});
actionButtons.push({
icon: (
<MaterialIcon name="keyboard-hide" style={styles.text}/>
),
description: _('Hide keyboard'),
disabled: !keyboardVisible,
visible: hasSoftwareKeyboard && Platform.OS === 'ios',
onPress: onDismissKeyboard,
priority: -3,
});
const styleData: StyleSheetData = {
styles: styles,
themeId: props.editorSettings.themeId,
};
return (
<>
<Toolbar
styleSheet={styleData}
buttons={[
{
title: _('Formatting'),
items: inlineFormattingBtns,
},
{
title: _('Headers'),
items: headerButtons,
},
{
title: _('Lists'),
items: listButtons,
},
{
title: _('Actions'),
items: actionButtons,
},
]}
/>
<View style={{
// The keyboard on iOS can overlap the markdown toolbar.
// Add additional padding to prevent this.
height: (
Platform.OS === 'ios' && keyboardVisible ? 16 : 0
),
}}/>
</>
);
};
const useStyles = (styleProps: any, theme: Theme) => {
return useMemo(() => {
return StyleSheet.create({
button: {
width: buttonSize,
height: buttonSize,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: theme.backgroundColor,
},
buttonDisabled: {
opacity: 0.5,
},
buttonDisabledContent: {
},
buttonActive: {
backgroundColor: theme.backgroundColor3,
color: theme.color3,
borderWidth: 1,
borderColor: theme.color3,
borderRadius: 6,
},
buttonActiveContent: {
color: theme.color3,
},
text: {
fontSize: 22,
color: theme.color,
},
toolbarRow: {
flex: 0,
flexDirection: 'row',
alignItems: 'baseline',
justifyContent: 'center',
// Add a small amount of additional padding for button borders
height: buttonSize + 6,
...styleProps,
},
toolbarContainer: {
maxHeight: '65%',
flexShrink: 1,
},
toolbarContent: {
flexGrow: 1,
justifyContent: 'center',
},
});
}, [styleProps, theme]);
};
export default MarkdownToolbar;

View File

@ -0,0 +1,34 @@
const React = require('react');
import { _ } from '@joplin/lib/locale';
import ToolbarButton from './ToolbarButton';
import { ButtonSpec, StyleSheetData } from './types';
const MaterialIcon = require('react-native-vector-icons/MaterialIcons').default;
type OnToggleOverflowCallback = ()=> void;
interface ToggleOverflowButtonProps {
overflowVisible: boolean;
onToggleOverflowVisible: OnToggleOverflowCallback;
styleSheet: StyleSheetData;
}
// Button that shows/hides the overflow menu.
const ToggleOverflowButton = (props: ToggleOverflowButtonProps) => {
const spec: ButtonSpec = {
icon: (
<MaterialIcon name="more-horiz" style={props.styleSheet.styles.text}/>
),
description:
props.overflowVisible ? _('Hide more actions') : _('Show more actions'),
active: props.overflowVisible,
onPress: props.onToggleOverflowVisible,
};
return (
<ToolbarButton
styleSheet={props.styleSheet}
spec={spec}
/>
);
};
export default ToggleOverflowButton;

View File

@ -0,0 +1,119 @@
const React = require('react');
import { _ } from '@joplin/lib/locale';
import { ReactElement, useCallback, useState } from 'react';
import { AccessibilityInfo, LayoutChangeEvent, ScrollView, View } from 'react-native';
import ToggleOverflowButton from './ToggleOverflowButton';
import ToolbarButton, { buttonSize } from './ToolbarButton';
import ToolbarOverflowRows from './ToolbarOverflowRows';
import { ButtonGroup, ButtonSpec, StyleSheetData } from './types';
interface ToolbarProps {
buttons: ButtonGroup[];
styleSheet: StyleSheetData;
}
// Displays a list of buttons with an overflow menu.
const Toolbar = (props: ToolbarProps) => {
const [overflowButtonsVisible, setOverflowPopupVisible] = useState(false);
const [maxButtonsEachSide, setMaxButtonsEachSide] = useState(0);
const allButtonSpecs = props.buttons.reduce((accumulator: ButtonSpec[], current: ButtonGroup) => {
const newItems: ButtonSpec[] = [];
for (const item of current.items) {
if (item.visible ?? true) {
newItems.push(item);
}
}
return accumulator.concat(...newItems);
}, []);
// Sort from highest priority to lowest
allButtonSpecs.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
const allButtonComponents: ReactElement[] = [];
let key = 0;
for (const spec of allButtonSpecs) {
key++;
allButtonComponents.push(
<ToolbarButton
key={key.toString()}
styleSheet={props.styleSheet}
spec={spec}
/>
);
}
const onContainerLayout = useCallback((event: LayoutChangeEvent) => {
const containerWidth = event.nativeEvent.layout.width;
const maxButtonsTotal = Math.floor(containerWidth / buttonSize);
setMaxButtonsEachSide(Math.floor(
Math.min((maxButtonsTotal - 1) / 2, allButtonSpecs.length / 2)
));
}, [allButtonSpecs.length]);
const onToggleOverflowVisible = useCallback(() => {
AccessibilityInfo.announceForAccessibility(
!overflowButtonsVisible
? _('Opened toolbar overflow menu')
: _('Closed toolbar overflow menu')
);
setOverflowPopupVisible(!overflowButtonsVisible);
}, [overflowButtonsVisible]);
const toggleOverflowButton = (
<ToggleOverflowButton
key={(++key).toString()}
styleSheet={props.styleSheet}
overflowVisible={overflowButtonsVisible}
onToggleOverflowVisible={onToggleOverflowVisible}
/>
);
const mainButtons: ReactElement[] = [];
if (maxButtonsEachSide < allButtonComponents.length) {
// We want the menu to look something like this:
// B I (…) 🔍 ⌨
// where (…) shows/hides overflow.
// Add from the left and right of [allButtonComponents] to ensure that
// the (…) button is in the center:
mainButtons.push(...allButtonComponents.slice(0, maxButtonsEachSide));
mainButtons.push(toggleOverflowButton);
mainButtons.push(...allButtonComponents.slice(-maxButtonsEachSide));
} else {
mainButtons.push(...allButtonComponents);
}
const styles = props.styleSheet.styles;
const mainButtonRow = (
<View style={styles.toolbarRow}>
{!overflowButtonsVisible ? mainButtons : null }
</View>
);
return (
<View
style={{
...styles.toolbarContainer,
// The number of buttons displayed is based on the width of the
// container. As such, we can't base the container's width on the
// size of its content.
width: '100%',
}}
onLayout={onContainerLayout}
>
<ScrollView>
<ToolbarOverflowRows
buttonGroups={props.buttons}
styleSheet={props.styleSheet}
visible={overflowButtonsVisible}
onToggleOverflow={onToggleOverflowVisible}
/>
{ !overflowButtonsVisible ? mainButtonRow : null }
</ScrollView>
</View>
);
};
export default Toolbar;

View File

@ -0,0 +1,64 @@
import React = require('react');
import { useCallback } from 'react';
import { Text, TextStyle } from 'react-native';
import { ButtonSpec, StyleSheetData } from './types';
import CustomButton from '../../CustomButton';
export const buttonSize = 54;
interface ToolbarButtonProps {
styleSheet: StyleSheetData;
style?: TextStyle;
spec: ButtonSpec;
onActionComplete?: ()=> void;
}
const ToolbarButton = ({ styleSheet, spec, onActionComplete, style }: ToolbarButtonProps) => {
const visible = spec.visible ?? true;
const disabled = (spec.disabled ?? false) && visible;
const styles = styleSheet.styles;
// Additional styles if activated
const activatedStyle = spec.active ? styles.buttonActive : {};
const activatedTextStyle = spec.active ? styles.buttonActiveContent : {};
const disabledStyle = disabled ? styles.buttonDisabled : {};
const disabledTextStyle = disabled ? styles.buttonDisabledContent : {};
let content;
if (typeof spec.icon === 'string') {
content = (
<Text style={{ ...styles.text, ...activatedTextStyle, ...disabledTextStyle }}>
{spec.icon}
</Text>
);
} else {
content = spec.icon;
}
const sourceOnPress = spec.onPress;
const onPress = useCallback(() => {
if (!disabled) {
sourceOnPress();
onActionComplete?.();
}
}, [disabled, sourceOnPress, onActionComplete]);
return (
<CustomButton
style={{
...styles.button, ...activatedStyle, ...disabledStyle, ...style,
...(!visible ? { opacity: 0 } : null),
}}
themeId={styleSheet.themeId}
onPress={onPress}
description={ spec.description }
accessibilityRole="button"
disabled={ disabled }
>
{ content }
</CustomButton>
);
};
export default ToolbarButton;

View File

@ -0,0 +1,122 @@
import { _ } from '@joplin/lib/locale';
import { ReactElement, useCallback, useState } from 'react';
import { LayoutChangeEvent, ScrollView, View } from 'react-native';
import ToggleOverflowButton from './ToggleOverflowButton';
import ToolbarButton, { buttonSize } from './ToolbarButton';
import { ButtonGroup, ButtonSpec, StyleSheetData } from './types';
const React = require('react');
type OnToggleOverflowCallback = ()=> void;
interface OverflowPopupProps {
buttonGroups: ButtonGroup[];
styleSheet: StyleSheetData;
visible: boolean;
// Should be created using useCallback
onToggleOverflow: OnToggleOverflowCallback;
}
// Contains buttons that overflow the available space.
// Displays all buttons in [props.buttonGroups] if [props.visible].
// Otherwise, displays nothing.
const ToolbarOverflowRows = (props: OverflowPopupProps) => {
const overflowRows: ReactElement[] = [];
let key = 0;
for (let i = 0; i < props.buttonGroups.length; i++) {
key++;
const row: ReactElement[] = [];
const group = props.buttonGroups[i];
for (let j = 0; j < group.items.length; j++) {
key++;
const buttonSpec = group.items[j];
row.push(
<ToolbarButton
key={key.toString()}
styleSheet={props.styleSheet}
spec={buttonSpec}
// After invoking this button's action, hide the overflow menu
onActionComplete={props.onToggleOverflow}
/>
);
// Show the "hide overflow" button if in the center of the last row
const isLastRow = i === props.buttonGroups.length - 1;
const isCenterOfRow = j + 1 === Math.floor(group.items.length / 2);
if (isLastRow && isCenterOfRow) {
row.push(
<ToggleOverflowButton
key={(++key).toString()}
styleSheet={props.styleSheet}
overflowVisible={true}
onToggleOverflowVisible={props.onToggleOverflow}
/>
);
}
}
overflowRows.push(
<View
key={key.toString()}
>
<ScrollView
horizontal={true}
contentContainerStyle={props.styleSheet.styles.toolbarContent}
>
{row}
</ScrollView>
</View>
);
}
const [hasSpaceForCloseBtn, setHasSpaceForCloseBtn] = useState(true);
const onContainerLayout = useCallback((event: LayoutChangeEvent) => {
if (props.buttonGroups.length === 0) {
return;
}
// Add 1 to account for the close button
const totalButtonCount = props.buttonGroups[0].items.length + 1;
const newWidth = event.nativeEvent.layout.width;
setHasSpaceForCloseBtn(newWidth > totalButtonCount * buttonSize);
}, [setHasSpaceForCloseBtn, props.buttonGroups]);
const closeButtonSpec: ButtonSpec = {
icon: '⨉',
description: _('Close'),
onPress: props.onToggleOverflow,
};
const closeButton = (
<ToolbarButton
styleSheet={props.styleSheet}
spec={closeButtonSpec}
style={{
position: 'absolute',
right: 0,
zIndex: 1,
}}
/>
);
if (!props.visible) {
return null;
}
return (
<View
style={{
height: props.buttonGroups.length * buttonSize,
flexDirection: 'column',
}}
onLayout={onContainerLayout}
>
{hasSpaceForCloseBtn ? closeButton : null}
{overflowRows}
</View>
);
};
export default ToolbarOverflowRows;

View File

@ -0,0 +1,35 @@
import { ReactElement } from 'react';
export type OnPressListener = ()=> void;
export interface ButtonSpec {
// Either text that will be shown in place of an icon or a component.
icon: string | ReactElement;
// Tooltip/accessibility label
description: string;
onPress: OnPressListener;
// Priority for showing the button in the main toolbar.
// Higher priority => more likely to be shown on the left of the toolbar
// Lower (negative) priority => more likely to be shown on the right side of the
// toolbar.
priority?: number;
// True if the button is connected to an enabled action.
// E.g. the cursor is in a header and the button is a header button.
active?: boolean;
disabled?: boolean;
visible?: boolean;
}
export interface ButtonGroup {
title: string;
items: ButtonSpec[];
}
export interface StyleSheetData {
themeId: number;
styles: any;
}

View File

@ -5,36 +5,36 @@ import EditLinkDialog from './EditLinkDialog';
import { defaultSearchState, SearchPanel } from './SearchPanel';
const React = require('react');
const { forwardRef, useImperativeHandle } = require('react');
const { useEffect, useMemo, useState, useCallback, useRef } = require('react');
import { forwardRef, RefObject, useImperativeHandle } from 'react';
import { useEffect, useMemo, useState, useCallback, useRef } from 'react';
const { WebView } = require('react-native-webview');
const { View } = require('react-native');
import { View, ViewStyle } from 'react-native';
const { editorFont } = require('../global-style');
import SelectionFormatting from './SelectionFormatting';
import {
EditorSettings,
EditorControl,
ChangeEvent, UndoRedoDepthChangeEvent, Selection, SelectionChangeEvent,
ListType,
SearchState,
EditorSettings, EditorControl,
ChangeEvent, UndoRedoDepthChangeEvent, Selection, SelectionChangeEvent, ListType, SearchState,
} from './types';
import { _ } from '@joplin/lib/locale';
import MarkdownToolbar from './MarkdownToolbar/MarkdownToolbar';
type ChangeEventHandler = (event: ChangeEvent)=> void;
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
type SelectionChangeEventHandler = (event: SelectionChangeEvent)=> void;
type OnAttachCallback = ()=> void;
interface Props {
themeId: number;
initialText: string;
initialSelection?: Selection;
style: any;
style: ViewStyle;
contentStyle?: ViewStyle;
onChange: ChangeEventHandler;
onSelectionChange: SelectionChangeEventHandler;
onUndoRedoDepthChange: UndoRedoDepthChangeHandler;
onAttach: OnAttachCallback;
}
function fontFamilyFromSettings() {
@ -106,6 +106,106 @@ function editorTheme(themeId: number) {
};
}
type OnInjectJSCallback = (js: string)=> void;
type OnSetVisibleCallback = (visible: boolean)=> void;
type OnSearchStateChangeCallback = (state: SearchState)=> void;
const useEditorControl = (
injectJS: OnInjectJSCallback, setLinkDialogVisible: OnSetVisibleCallback,
setSearchState: OnSearchStateChangeCallback, searchStateRef: RefObject<SearchState>
): EditorControl => {
return useMemo(() => {
return {
undo() {
injectJS('cm.undo();');
},
redo() {
injectJS('cm.redo();');
},
select(anchor: number, head: number) {
injectJS(
`cm.select(${JSON.stringify(anchor)}, ${JSON.stringify(head)});`
);
},
insertText(text: string) {
injectJS(`cm.insertText(${JSON.stringify(text)});`);
},
toggleBolded() {
injectJS('cm.toggleBolded();');
},
toggleItalicized() {
injectJS('cm.toggleItalicized();');
},
toggleList(listType: ListType) {
injectJS(`cm.toggleList(${JSON.stringify(listType)});`);
},
toggleCode() {
injectJS('cm.toggleCode();');
},
toggleMath() {
injectJS('cm.toggleMath();');
},
toggleHeaderLevel(level: number) {
injectJS(`cm.toggleHeaderLevel(${level});`);
},
increaseIndent() {
injectJS('cm.increaseIndent();');
},
decreaseIndent() {
injectJS('cm.decreaseIndent();');
},
updateLink(label: string, url: string) {
injectJS(`cm.updateLink(
${JSON.stringify(label)},
${JSON.stringify(url)}
);`);
},
scrollSelectionIntoView() {
injectJS('cm.scrollSelectionIntoView();');
},
showLinkDialog() {
setLinkDialogVisible(true);
},
hideLinkDialog() {
setLinkDialogVisible(false);
},
hideKeyboard() {
injectJS('document.activeElement?.blur();');
},
searchControl: {
findNext() {
injectJS('cm.searchControl.findNext();');
},
findPrevious() {
injectJS('cm.searchControl.findPrevious();');
},
replaceCurrent() {
injectJS('cm.searchControl.replaceCurrent();');
},
replaceAll() {
injectJS('cm.searchControl.replaceAll();');
},
setSearchState(state: SearchState) {
injectJS(`cm.searchControl.setSearchState(${JSON.stringify(state)})`);
setSearchState(state);
},
showSearch() {
setSearchState({
...searchStateRef.current,
dialogVisible: true,
});
},
hideSearch() {
setSearchState({
...searchStateRef.current,
dialogVisible: false,
});
},
},
};
}, [injectJS, searchStateRef, setLinkDialogVisible, setSearchState]);
};
function NoteEditor(props: Props, ref: any) {
const [source, setSource] = useState(undefined);
const webviewRef = useRef(null);
@ -115,8 +215,10 @@ function NoteEditor(props: Props, ref: any) {
` : '';
const editorSettings: EditorSettings = {
themeId: props.themeId,
themeData: editorTheme(props.themeId),
katexEnabled: Setting.value('markdown.plugin.katex') as boolean,
katexEnabled: Setting.value('markdown.plugin.katex'),
spellcheckEnabled: Setting.value('editor.mobile.spellcheckEnabled'),
};
const injectedJavaScript = `
@ -170,10 +272,19 @@ function NoteEditor(props: Props, ref: any) {
const css = useCss(props.themeId);
const html = useHtml(css);
const [selectionState, setSelectionState] = useState(new SelectionFormatting());
const [searchState, setSearchState] = useState(defaultSearchState);
const [linkDialogVisible, setLinkDialogVisible] = useState(false);
const [searchState, setSearchState] = useState(defaultSearchState);
// / Runs [js] in the context of the CodeMirror frame.
// Having a [searchStateRef] allows [editorControl] to not be re-created
// whenever [searchState] changes.
const searchStateRef = useRef(defaultSearchState);
// Keep the reference and the [searchState] in sync
useEffect(() => {
searchStateRef.current = searchState;
}, [searchState]);
// Runs [js] in the context of the CodeMirror frame.
const injectJS = (js: string) => {
webviewRef.current.injectJavaScript(`
try {
@ -187,99 +298,9 @@ function NoteEditor(props: Props, ref: any) {
true;`);
};
const editorControl: EditorControl = {
undo() {
injectJS('cm.undo();');
},
redo() {
injectJS('cm.redo();');
},
select(anchor: number, head: number) {
injectJS(
`cm.select(${JSON.stringify(anchor)}, ${JSON.stringify(head)});`
);
},
insertText(text: string) {
injectJS(`cm.insertText(${JSON.stringify(text)});`);
},
toggleBolded() {
injectJS('cm.toggleBolded();');
},
toggleItalicized() {
injectJS('cm.toggleItalicized();');
},
toggleList(listType: ListType) {
injectJS(`cm.toggleList(${JSON.stringify(listType)});`);
},
toggleCode() {
injectJS('cm.toggleCode();');
},
toggleMath() {
injectJS('cm.toggleMath();');
},
toggleHeaderLevel(level: number) {
injectJS(`cm.toggleHeaderLevel(${level});`);
},
increaseIndent() {
injectJS('cm.increaseIndent();');
},
decreaseIndent() {
injectJS('cm.decreaseIndent();');
},
updateLink(label: string, url: string) {
injectJS(`cm.updateLink(
${JSON.stringify(label)},
${JSON.stringify(url)}
);`);
},
scrollSelectionIntoView() {
injectJS('cm.scrollSelectionIntoView();');
},
showLinkDialog() {
setLinkDialogVisible(true);
},
hideLinkDialog() {
setLinkDialogVisible(false);
},
hideKeyboard() {
injectJS('document.activeElement?.blur();');
},
setSpellcheckEnabled(enabled: boolean) {
injectJS(`cm.setSpellcheckEnabled(${enabled ? 'true' : 'false'});`);
},
searchControl: {
findNext() {
injectJS('cm.searchControl.findNext();');
},
findPrevious() {
injectJS('cm.searchControl.findPrevious();');
},
replaceCurrent() {
injectJS('cm.searchControl.replaceCurrent();');
},
replaceAll() {
injectJS('cm.searchControl.replaceAll();');
},
setSearchState(state: SearchState) {
injectJS(`cm.searchControl.setSearchState(${JSON.stringify(state)})`);
setSearchState(state);
},
showSearch() {
const newSearchState: SearchState = Object.assign({}, searchState);
newSearchState.dialogVisible = true;
setSearchState(newSearchState);
},
hideSearch() {
const newSearchState: SearchState = Object.assign({}, searchState);
newSearchState.dialogVisible = false;
setSearchState(newSearchState);
},
},
};
const editorControl = useEditorControl(
injectJS, setLinkDialogVisible, setSearchState, searchStateRef
);
useImperativeHandle(ref, () => {
return editorControl;
@ -359,11 +380,12 @@ function NoteEditor(props: Props, ref: any) {
} else {
console.info('Unsupported CodeMirror message:', msg);
}
}, [props.onChange]);
}, [props.onSelectionChange, props.onUndoRedoDepthChange, props.onChange, editorControl]);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
const onError = useCallback(() => {
console.error('NoteEditor: webview error');
});
}, []);
// - `setSupportMultipleWindows` must be `true` for security reasons:
@ -384,7 +406,8 @@ function NoteEditor(props: Props, ref: any) {
<View style={{
flexGrow: 1,
flexShrink: 0,
minHeight: '40%',
minHeight: '30%',
...props.contentStyle,
}}>
<WebView
style={{
@ -410,6 +433,19 @@ function NoteEditor(props: Props, ref: any) {
searchControl={editorControl.searchControl}
searchState={searchState}
/>
<MarkdownToolbar
style={{
// Don't show the markdown toolbar if there isn't enough space
// for it:
flexShrink: 1,
}}
editorSettings={editorSettings}
editorControl={editorControl}
selectionState={selectionState}
searchState={searchState}
onAttach={props.onAttach}
/>
</View>
);
}

View File

@ -1,15 +1,14 @@
// Displays a find/replace dialog
const React = require('react');
const { StyleSheet } = require('react-native');
const { TextInput, View, Text, TouchableOpacity } = require('react-native');
const { useMemo, useState, useEffect } = require('react');
const MaterialCommunityIcon = require('react-native-vector-icons/MaterialCommunityIcons').default;
import { SearchControl, SearchState, EditorSettings } from './types';
import { _ } from '@joplin/lib/locale';
import { BackHandler } from 'react-native';
import { BackHandler, TextInput, View, Text, StyleSheet, ViewStyle } from 'react-native';
import { Theme } from '@joplin/lib/themes/type';
import CustomButton from '../CustomButton';
const buttonSize = 48;
@ -33,6 +32,7 @@ export interface SearchPanelProps {
interface ActionButtonProps {
styles: any;
themeId: number;
iconName: string;
title: string;
onPress: Callback;
@ -42,30 +42,32 @@ const ActionButton = (
props: ActionButtonProps
) => {
return (
<TouchableOpacity
<CustomButton
themeId={props.themeId}
style={props.styles.button}
onPress={props.onPress}
accessibilityLabel={props.title}
accessibilityRole='button'
description={props.title}
>
<MaterialCommunityIcon name={props.iconName} style={props.styles.buttonText}/>
</TouchableOpacity>
</CustomButton>
);
};
interface ToggleButtonProps {
styles: any;
themeId: number;
iconName: string;
title: string;
active: boolean;
onToggle: Callback;
}
const ToggleButton = (props: ToggleButtonProps) => {
const active = props.active;
return (
<TouchableOpacity
<CustomButton
themeId={props.themeId}
style={{
...props.styles.toggleButton,
...(active ? props.styles.toggleButtonActive : {}),
@ -75,20 +77,20 @@ const ToggleButton = (props: ToggleButtonProps) => {
accessibilityState={{
checked: props.active,
}}
accessibilityLabel={props.title}
description={props.title}
accessibilityRole='switch'
>
<MaterialCommunityIcon name={props.iconName} style={
active ? props.styles.activeButtonText : props.styles.buttonText
}/>
</TouchableOpacity>
</CustomButton>
);
};
const useStyles = (theme: Theme) => {
return useMemo(() => {
const buttonStyle = {
const buttonStyle: ViewStyle = {
width: buttonSize,
height: buttonSize,
backgroundColor: theme.backgroundColor4,
@ -136,8 +138,9 @@ const useStyles = (theme: Theme) => {
};
export const SearchPanel = (props: SearchPanelProps) => {
const placeholderColor = props.editorSettings.themeData.color3;
const styles = useStyles(props.editorSettings.themeData);
const theme = props.editorSettings.themeData;
const placeholderColor = theme.color3;
const styles = useStyles(theme);
const [showingAdvanced, setShowAdvanced] = useState(false);
@ -181,12 +184,14 @@ export const SearchPanel = (props: SearchPanelProps) => {
});
return () => backListener.remove();
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [state.dialogVisible]);
const themeId = props.editorSettings.themeId;
const closeButton = (
<ActionButton
themeId={themeId}
styles={styles}
iconName="close"
onPress={control.hideSearch}
@ -196,6 +201,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
const showDetailsButton = (
<ActionButton
themeId={themeId}
styles={styles}
iconName="menu-down"
onPress={() => setShowAdvanced(true)}
@ -205,6 +211,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
const hideDetailsButton = (
<ActionButton
themeId={themeId}
styles={styles}
iconName="menu-up"
onPress={() => setShowAdvanced(false)}
@ -254,6 +261,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
const toNextButton = (
<ActionButton
themeId={themeId}
styles={styles}
iconName="menu-right"
onPress={control.findNext}
@ -263,6 +271,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
const toPrevButton = (
<ActionButton
themeId={themeId}
styles={styles}
iconName="menu-left"
onPress={control.findPrevious}
@ -272,6 +281,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
const replaceButton = (
<ActionButton
themeId={themeId}
styles={styles}
iconName="swap-horizontal"
onPress={control.replaceCurrent}
@ -281,6 +291,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
const replaceAllButton = (
<ActionButton
themeId={themeId}
styles={styles}
iconName="reply-all"
onPress={control.replaceAll}
@ -290,6 +301,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
const regexpButton = (
<ToggleButton
themeId={themeId}
styles={styles}
iconName="regex"
onToggle={() => {
@ -304,6 +316,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
const caseSensitiveButton = (
<ToggleButton
themeId={themeId}
styles={styles}
iconName="format-letter-case"
onToggle={() => {

View File

@ -1,5 +1,6 @@
// Types related to the NoteEditor
import { Theme } from '@joplin/lib/themes/type';
import { CodeMirrorControl } from './CodeMirror/types';
// Controls for the entire editor (including dialogs)
@ -10,8 +11,14 @@ export interface EditorControl extends CodeMirrorControl {
}
export interface EditorSettings {
themeData: any;
// EditorSettings objects are deserialized within WebViews, where
// [themeStyle(themeId: number)] doesn't work. As such, we need both
// the [themeId] and [themeData].
themeId: number;
themeData: Theme;
katexEnabled: boolean;
spellcheckEnabled: boolean;
}
export interface ChangeEvent {

View File

@ -1,21 +1,25 @@
const React = require('react');
const { connect } = require('react-redux');
const { View, Text, StyleSheet, TouchableOpacity, Image, ScrollView, Dimensions } = require('react-native');
import { connect } from 'react-redux';
import { PureComponent, Component } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Image, ScrollView, Dimensions, ViewStyle } from 'react-native';
const Icon = require('react-native-vector-icons/Ionicons').default;
const { BackButtonService } = require('../services/back-button.js');
const NavService = require('@joplin/lib/services/NavService').default;
const { Menu, MenuOptions, MenuOption, MenuTrigger } = require('react-native-popup-menu');
const { _ } = require('@joplin/lib/locale');
const Setting = require('@joplin/lib/models/Setting').default;
const Note = require('@joplin/lib/models/Note').default;
const Folder = require('@joplin/lib/models/Folder').default;
import NavService from '@joplin/lib/services/NavService';
import { Menu, MenuOptions, MenuOption, MenuTrigger } from 'react-native-popup-menu';
import { _ } from '@joplin/lib/locale';
import Setting from '@joplin/lib/models/Setting';
import Note from '@joplin/lib/models/Note';
import Folder, { FolderEntityWithChildren } from '@joplin/lib/models/Folder';
const { themeStyle } = require('./global-style.js');
const { Dropdown } = require('./Dropdown.js');
import Dropdown, { DropdownListItem, OnValueChangedListener } from './Dropdown';
const { dialogs } = require('../utils/dialogs.js');
const DialogBox = require('react-native-dialogbox').default;
const { localSyncInfoFromState } = require('@joplin/lib/services/synchronizer/syncInfoUtils');
const { showMissingMasterKeyMessage } = require('@joplin/lib/services/e2ee/utils');
import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { showMissingMasterKeyMessage } from '@joplin/lib/services/e2ee/utils';
import { FolderEntity } from '@joplin/lib/services/database/types';
import { State } from '@joplin/lib/reducer';
import CustomButton from './CustomButton';
Icon.loadFont();
@ -25,20 +29,78 @@ Icon.loadFont();
// default height.
const PADDING_V = 10;
class ScreenHeaderComponent extends React.PureComponent {
constructor() {
super();
this.styles_ = {};
type OnSelectCallbackType=()=> void;
type OnPressCallback=()=> void;
interface NavButtonPressEvent {
// Name of the screen to navigate to
screen: string;
}
interface MenuOptionType {
onPress: OnPressCallback;
isDivider?: boolean;
title: string;
}
type DispatchCommandType=(event: { type: string })=> void;
interface ScreenHeaderProps {
selectedNoteIds: string[];
noteSelectionEnabled: boolean;
parentComponent: Component;
showUndoButton: boolean;
undoButtonDisabled?: boolean;
showRedoButton: boolean;
menuOptions: MenuOptionType[];
title?: string|null;
folders: FolderEntity[];
folderPickerOptions?: {
enabled: boolean;
selectedFolderId: string;
onValueChange: OnValueChangedListener;
mustSelect?: boolean;
};
dispatch: DispatchCommandType;
onUndoButtonPress: OnPressCallback;
onRedoButtonPress: OnPressCallback;
onSaveButtonPress: OnPressCallback;
sortButton_press?: OnPressCallback;
showSideMenuButton?: boolean;
showSearchButton?: boolean;
showContextMenuButton?: boolean;
showBackButton?: boolean;
saveButtonDisabled?: boolean;
showSaveButton?: boolean;
historyCanGoBack?: boolean;
showMissingMasterKeyMessage?: boolean;
hasDisabledSyncItems?: boolean;
shouldUpgradeSyncTarget?: boolean;
showShouldUpgradeSyncTargetMessage?: boolean;
}
interface ScreenHeaderState {
}
class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeaderState> {
private cachedStyles: any;
public dialogbox?: typeof DialogBox;
public constructor(props: ScreenHeaderProps) {
super(props);
this.cachedStyles = {};
}
styles() {
private styles() {
const themeId = Setting.value('theme');
if (this.styles_[themeId]) return this.styles_[themeId];
this.styles_ = {};
if (this.cachedStyles[themeId]) return this.cachedStyles[themeId];
this.cachedStyles = {};
const theme = themeStyle(themeId);
const styleObject = {
const styleObject: any = {
container: {
flexDirection: 'column',
backgroundColor: theme.backgroundColor2,
@ -147,15 +209,15 @@ class ScreenHeaderComponent extends React.PureComponent {
styleObject.saveButtonDisabled = Object.assign({}, styleObject.saveButton, { opacity: theme.disabledOpacity });
styleObject.iconButtonDisabled = Object.assign({}, styleObject.iconButton, { opacity: theme.disabledOpacity });
this.styles_[themeId] = StyleSheet.create(styleObject);
return this.styles_[themeId];
this.cachedStyles[themeId] = StyleSheet.create(styleObject);
return this.cachedStyles[themeId];
}
sideMenuButton_press() {
private sideMenuButton_press() {
this.props.dispatch({ type: 'SIDE_MENU_TOGGLE' });
}
async backButton_press() {
private async backButton_press() {
if (this.props.noteSelectionEnabled) {
this.props.dispatch({ type: 'NOTE_SELECTION_END' });
} else {
@ -163,15 +225,15 @@ class ScreenHeaderComponent extends React.PureComponent {
}
}
selectAllButton_press() {
private selectAllButton_press() {
this.props.dispatch({ type: 'NOTE_SELECT_ALL_TOGGLE' });
}
searchButton_press() {
NavService.go('Search');
private searchButton_press() {
void NavService.go('Search');
}
async duplicateButton_press() {
private async duplicateButton_press() {
const noteIds = this.props.selectedNoteIds;
// Duplicate all selected notes. ensureUniqueTitle is set to true to use the
@ -181,7 +243,7 @@ class ScreenHeaderComponent extends React.PureComponent {
this.props.dispatch({ type: 'NOTE_SELECTION_END' });
}
async deleteButton_press() {
private async deleteButton_press() {
// Dialog needs to be displayed as a child of the parent component, otherwise
// it won't be visible within the header component.
const noteIds = this.props.selectedNoteIds;
@ -196,25 +258,17 @@ class ScreenHeaderComponent extends React.PureComponent {
await Note.batchDelete(noteIds);
}
menu_select(value) {
private menu_select(value: OnSelectCallbackType) {
if (typeof value === 'function') {
value();
}
}
log_press() {
NavService.go('Log');
private warningBox_press(event: NavButtonPressEvent) {
void NavService.go(event.screen);
}
status_press() {
NavService.go('Status');
}
warningBox_press(event) {
NavService.go(event.screen);
}
renderWarningBox(screen, message) {
private renderWarningBox(screen: string, message: string) {
return (
<TouchableOpacity key={screen} style={this.styles().warningBox} onPress={() => this.warningBox_press({ screen: screen })} activeOpacity={0.8}>
<Text style={{ flex: 1 }}>{message}</Text>
@ -222,8 +276,9 @@ class ScreenHeaderComponent extends React.PureComponent {
);
}
render() {
function sideMenuButton(styles, onPress) {
public render() {
const themeId = Setting.value('theme');
function sideMenuButton(styles: any, onPress: OnPressCallback) {
return (
<TouchableOpacity
onPress={onPress}
@ -238,7 +293,7 @@ class ScreenHeaderComponent extends React.PureComponent {
);
}
function backButton(styles, onPress, disabled) {
function backButton(styles: any, onPress: OnPressCallback, disabled: boolean) {
return (
<TouchableOpacity
onPress={onPress}
@ -257,7 +312,9 @@ class ScreenHeaderComponent extends React.PureComponent {
);
}
function saveButton(styles, onPress, disabled, show) {
function saveButton(
styles: any, onPress: OnPressCallback, disabled: boolean, show: boolean
) {
if (!show) return null;
const icon = disabled ? <Icon name="md-checkmark" style={styles.savedButtonIcon} /> : <Image style={styles.saveButtonIcon} source={require('./SaveIcon.png')} />;
@ -276,26 +333,37 @@ class ScreenHeaderComponent extends React.PureComponent {
);
}
const renderTopButton = (options) => {
interface TopButtonOptions {
visible: boolean;
iconName: string;
disabled?: boolean;
description: string;
onPress: OnPressCallback;
}
const renderTopButton = (options: TopButtonOptions) => {
if (!options.visible) return null;
const icon = <Icon name={options.iconName} style={this.styles().topIcon} />;
const viewStyle = options.disabled ? this.styles().iconButtonDisabled : this.styles().iconButton;
return (
<TouchableOpacity
<CustomButton
onPress={options.onPress}
style={{ padding: 0 }}
themeId={themeId}
disabled={!!options.disabled}
accessibilityRole="button">
<View style={viewStyle}>{icon}</View>
</TouchableOpacity>
description={options.description}
contentStyle={viewStyle}
>
{icon}
</CustomButton>
);
};
const renderUndoButton = () => {
return renderTopButton({
iconName: 'arrow-undo-circle-sharp',
description: _('Undo'),
onPress: this.props.onUndoButtonPress,
visible: this.props.showUndoButton,
disabled: this.props.undoButtonDisabled,
@ -305,76 +373,77 @@ class ScreenHeaderComponent extends React.PureComponent {
const renderRedoButton = () => {
return renderTopButton({
iconName: 'arrow-redo-circle-sharp',
description: _('Redo'),
onPress: this.props.onRedoButtonPress,
visible: this.props.showRedoButton,
});
};
function selectAllButton(styles, onPress) {
function selectAllButton(styles: any, onPress: OnPressCallback) {
return (
<TouchableOpacity
<CustomButton
onPress={onPress}
accessibilityLabel={_('Select all')}
accessibilityRole="button">
<View style={styles.iconButton}>
<Icon name="md-checkmark-circle-outline" style={styles.topIcon} />
</View>
</TouchableOpacity>
themeId={themeId}
description={_('Select all')}
contentStyle={styles.iconButton}
>
<Icon name="md-checkmark-circle-outline" style={styles.topIcon} />
</CustomButton>
);
}
function searchButton(styles, onPress) {
function searchButton(styles: any, onPress: OnPressCallback) {
return (
<TouchableOpacity
<CustomButton
onPress={onPress}
accessibilityLabel={_('Search')}
accessibilityRole="button">
<View style={styles.iconButton}>
<Icon name="md-search" style={styles.topIcon} />
</View>
</TouchableOpacity>
description={_('Search')}
themeId={themeId}
contentStyle={styles.iconButton}
>
<Icon name="md-search" style={styles.topIcon} />
</CustomButton>
);
}
function deleteButton(styles, onPress, disabled) {
function deleteButton(styles: any, onPress: OnPressCallback, disabled: boolean) {
return (
<TouchableOpacity
<CustomButton
onPress={onPress}
disabled={disabled}
accessibilityLabel={_('Delete')}
themeId={themeId}
description={_('Delete')}
accessibilityHint={
disabled ? null : _('Delete selected notes')
}
accessibilityRole="button">
<View style={disabled ? styles.iconButtonDisabled : styles.iconButton}>
<Icon name="md-trash" style={styles.topIcon} />
</View>
</TouchableOpacity>
contentStyle={disabled ? styles.iconButtonDisabled : styles.iconButton}
>
<Icon name="md-trash" style={styles.topIcon} />
</CustomButton>
);
}
function duplicateButton(styles, onPress, disabled) {
function duplicateButton(styles: any, onPress: OnPressCallback, disabled: boolean) {
return (
<TouchableOpacity
<CustomButton
onPress={onPress}
disabled={disabled}
accessibilityLabel={_('Duplicate')}
themeId={themeId}
description={_('Duplicate')}
accessibilityHint={
disabled ? null : _('Duplicate selected notes')
}
accessibilityRole="button">
<View style={disabled ? styles.iconButtonDisabled : styles.iconButton}>
<Icon name="md-copy" style={styles.topIcon} />
</View>
</TouchableOpacity>
contentStyle={disabled ? styles.iconButtonDisabled : styles.iconButton}
>
<Icon name="md-copy" style={styles.topIcon} />
</CustomButton>
);
}
function sortButton(styles, onPress) {
function sortButton(styles: any, onPress: OnPressCallback) {
return (
<TouchableOpacity
onPress={onPress}
@ -423,13 +492,15 @@ class ScreenHeaderComponent extends React.PureComponent {
);
}
const createTitleComponent = (disabled) => {
const createTitleComponent = (disabled: boolean) => {
const themeId = Setting.value('theme');
const theme = themeStyle(themeId);
const folderPickerOptions = this.props.folderPickerOptions;
if (folderPickerOptions && folderPickerOptions.enabled) {
const addFolderChildren = (folders, pickerItems, indent) => {
const addFolderChildren = (
folders: FolderEntityWithChildren[], pickerItems: DropdownListItem[], indent: number
) => {
folders.sort((a, b) => {
const aTitle = a && a.title ? a.title : '';
const bTitle = b && b.title ? b.title : '';
@ -447,7 +518,7 @@ class ScreenHeaderComponent extends React.PureComponent {
return pickerItems;
};
const titlePickerItems = mustSelect => {
const titlePickerItems = (mustSelect: boolean) => {
const folders = this.props.folders.filter(f => f.id !== Folder.conflictFolderId());
let output = [];
if (mustSelect) output.push({ label: _('Move to notebook...'), value: null });
@ -459,7 +530,6 @@ class ScreenHeaderComponent extends React.PureComponent {
return (
<Dropdown
items={titlePickerItems(!!folderPickerOptions.mustSelect)}
itemHeight={35}
disabled={disabled}
labelTransform="trim"
selectedValue={'selectedFolderId' in folderPickerOptions ? folderPickerOptions.selectedFolderId : null}
@ -475,13 +545,13 @@ class ScreenHeaderComponent extends React.PureComponent {
color: theme.color,
fontSize: theme.fontSize,
}}
onValueChange={async (folderId, itemIndex) => {
onValueChange={async (folderId) => {
// If onValueChange is specified, use this as a callback, otherwise do the default
// which is to take the selectedNoteIds from the state and move them to the
// chosen folder.
if (folderPickerOptions.onValueChange) {
folderPickerOptions.onValueChange(folderId, itemIndex);
folderPickerOptions.onValueChange(folderId);
return;
}
@ -521,7 +591,7 @@ class ScreenHeaderComponent extends React.PureComponent {
let backButtonDisabled = !this.props.historyCanGoBack;
if (this.props.noteSelectionEnabled) backButtonDisabled = false;
const headerItemDisabled = !this.props.selectedNoteIds.length > 0;
const headerItemDisabled = !(this.props.selectedNoteIds.length > 0);
const titleComp = createTitleComponent(headerItemDisabled);
const sideMenuComp = !showSideMenuButton ? null : sideMenuButton(this.styles(), () => this.sideMenuButton_press());
@ -533,7 +603,10 @@ class ScreenHeaderComponent extends React.PureComponent {
const sortButtonComp = !this.props.noteSelectionEnabled && this.props.sortButton_press ? sortButton(this.styles(), () => this.props.sortButton_press()) : null;
const windowHeight = Dimensions.get('window').height - 50;
const contextMenuStyle = { paddingTop: PADDING_V, paddingBottom: PADDING_V };
const contextMenuStyle: ViewStyle = {
paddingTop: PADDING_V,
paddingBottom: PADDING_V,
};
// HACK: if this button is removed during selection mode, the header layout is broken, so for now just make it 1 pixel large (normally it should be hidden)
if (this.props.noteSelectionEnabled) contextMenuStyle.width = 1;
@ -555,8 +628,8 @@ class ScreenHeaderComponent extends React.PureComponent {
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{sideMenuComp}
{backButtonComp}
{renderUndoButton(this.styles())}
{renderRedoButton(this.styles())}
{renderUndoButton()}
{renderRedoButton()}
{saveButton(
this.styles(),
() => {
@ -575,20 +648,20 @@ class ScreenHeaderComponent extends React.PureComponent {
</View>
{warningComps}
<DialogBox
ref={dialogbox => {
ref={(dialogbox: typeof DialogBox) => {
this.dialogbox = dialogbox;
}}
/>
</View>
);
}
public static defaultProps: Partial<ScreenHeaderProps> = {
menuOptions: [],
};
}
ScreenHeaderComponent.defaultProps = {
menuOptions: [],
};
const ScreenHeader = connect(state => {
const ScreenHeader = connect((state: State) => {
const syncInfo = localSyncInfoFromState(state);
return {
@ -604,4 +677,5 @@ const ScreenHeader = connect(state => {
};
})(ScreenHeaderComponent);
module.exports = { ScreenHeader };
export default ScreenHeader;
export { ScreenHeader };

View File

@ -0,0 +1,22 @@
const { connect } = require('react-redux');
const SideMenu_ = require('react-native-side-menu-updated').default;
import { Dimensions } from 'react-native';
import { State } from '@joplin/lib/reducer';
class SideMenuComponent extends SideMenu_ {
onLayoutChange(e: any) {
const { width, height } = e.nativeEvent.layout;
const openMenuOffsetPercentage = this.props.openMenuOffset / Dimensions.get('window').width;
const openMenuOffset = width * openMenuOffsetPercentage;
const hiddenMenuOffset = width * this.state.hiddenMenuOffsetPercentage;
this.setState({ width, height, openMenuOffset, hiddenMenuOffset });
}
}
const SideMenu = connect((state: State) => {
return {
isOpen: state.showSideMenu,
};
})(SideMenuComponent);
export default SideMenu;

View File

@ -0,0 +1,63 @@
import getResponsiveValue, { ValueMap } from './getResponsiveValue';
import { Dimensions } from 'react-native';
type testCase = [ ValueMap, number[] ];
const testCases: testCase[] = [
// [ valueMap, outputs ]
[{ sm: 40 }, [40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40]],
[{ sm: 40, md: 70 }, [40, 40, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70]],
[{ sm: 40, md: 70, lg: 90 }, [40, 40, 70, 70, 70, 90, 90, 90, 90, 90, 90, 90, 90]],
[{ sm: 40, md: 70, lg: 90, xl: 110 }, [40, 40, 70, 70, 70, 90, 90, 90, 110, 110, 110, 110, 110]],
[{ sm: 40, md: 70, lg: 90, xl: 110, xxl: 130 }, [40, 40, 70, 70, 70, 90, 90, 90, 110, 110, 110, 130, 130]],
[{ md: 70 }, [70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70]],
[{ lg: 90 }, [90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90]],
[{ xl: 110 }, [110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110]],
[{ xxl: 130 }, [130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130]],
[{ sm: 10, lg: 50 }, [10, 10, 10, 10, 10, 50, 50, 50, 50, 50, 50, 50, 50]],
[{ sm: 10, xl: 50 }, [10, 10, 10, 10, 10, 10, 10, 10, 50, 50, 50, 50, 50]],
[{ sm: 10, xxl: 50 }, [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 50, 50]],
[{ md: 30, lg: 50 }, [30, 30, 30, 30, 30, 50, 50, 50, 50, 50, 50, 50, 50]],
[{ md: 30, xl: 50 }, [30, 30, 30, 30, 30, 30, 30, 30, 50, 50, 50, 50, 50]],
[{ md: 30, xxl: 50 }, [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 50, 50]],
[{ xl: 30, xxl: 50 }, [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 50, 50]],
];
describe('getResponsiveValue', () => {
test('Should throw exception if value map is an empty object', () => {
const input = {};
expect(() => getResponsiveValue(input)).toThrow('valueMap cannot be an empty object!');
});
test('Should return correct values', () => {
const mockReturnValues = [
{ width: 400 },
{ width: 480 },
{ width: 481 },
{ width: 600 },
{ width: 768 },
{ width: 769 },
{ width: 900 },
{ width: 1024 },
{ width: 1025 },
{ width: 1115 },
{ width: 1200 },
{ width: 1201 },
{ width: 1300 },
];
for (let i = 0; i < testCases.length; i++) {
const input = testCases[i][0];
for (let j = 0; j < mockReturnValues.length; j++) {
// Mock the device width values returned by Dimensions.get() using 'mockReturnValues' for better test coverage
Dimensions.get = jest.fn().mockReturnValue(mockReturnValues[j]);
const output: number = testCases[i][1][j];
expect(getResponsiveValue(input)).toBe(output);
}
}
});
});

View File

@ -0,0 +1,60 @@
// getResponsiveValue() returns the corresponding value for
// a particular device screen width based on a valueMap argument
// and breakpoints.
//
// Breakpoints:
// sm: < 481px
// md: >= 481px
// lg: >= 769px
// xl: >= 1025px
// xxl: >= 1201px
//
// Eg. [ 10, 15, 20, 25, 30 ] means { sm: 10, md: 15, lg: 20, xl: 25, xxl: 30 }
// [10] and [10, 15] are equivalent to { sm: 10 } and { sm: 10, md: 15 } respectively
//
// More Info: https://discourse.joplinapp.org/t/week-4-report/26117
import { Dimensions } from 'react-native';
export interface ValueMap {
// Value to use on small-width displays
sm?: number;
// Value to use on medium-width displays
md?: number;
// Value to use on large-width displays
lg?: number;
// Value to use on extra-large width displays
xl?: number;
// Value to use on extra-extra-large width displays
xxl?: number;
}
export default function getResponsiveValue(valueMap: ValueMap): number {
if (Object.keys(valueMap).length === 0) {
throw 'valueMap cannot be an empty object!';
}
const width = Dimensions.get('window').width;
let value: number;
const { sm, md, lg, xl, xxl } = valueMap;
// This handles cases where certain values are omitted
value = sm ?? md ?? lg ?? xl ?? xxl;
if (width >= 481) {
value = md ?? value;
}
if (width >= 769) {
value = lg ?? value;
}
if (width >= 1025) {
value = xl ?? value;
}
if (width >= 1201) {
value = xxl ?? value;
}
return value;
}

View File

@ -14,7 +14,7 @@ import { reg } from '@joplin/lib/registry';
import { State } from '@joplin/lib/reducer';
const VersionInfo = require('react-native-version-info').default;
const { connect } = require('react-redux');
const { ScreenHeader } = require('../screen-header.js');
import ScreenHeader from '../ScreenHeader';
const { _ } = require('@joplin/lib/locale');
const { BaseScreenComponent } = require('../base-screen.js');
const { Dropdown } = require('../Dropdown.js');
@ -461,7 +461,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
color: theme.color,
fontSize: theme.fontSize,
}}
onValueChange={(itemValue: any) => {
onValueChange={(itemValue: string) => {
updateSettingValue(key, itemValue);
}}
/>

View File

@ -26,7 +26,7 @@ import BaseModel from '@joplin/lib/BaseModel';
const { ActionButton } = require('../action-button.js');
const { fileExtension, safeFileExtension } = require('@joplin/lib/path-utils');
const mimeUtils = require('@joplin/lib/mime-utils.js').mime;
const { ScreenHeader } = require('../screen-header.js');
import ScreenHeader from '../ScreenHeader';
const NoteTagsDialog = require('./NoteTagsDialog');
import time from '@joplin/lib/time';
const { Checkbox } = require('../checkbox.js');
@ -867,6 +867,27 @@ class NoteScreenComponent extends BaseScreenComponent {
return output;
}
async showAttachMenu() {
const buttons = [];
// On iOS, it will show "local files", which means certain files saved from the browser
// and the iCloud files, but it doesn't include photos and images from the CameraRoll
//
// On Android, it will depend on the phone, but usually it will allow browing all files and photos.
buttons.push({ text: _('Attach file'), id: 'attachFile' });
// Disabled on Android because it doesn't work due to permission issues, but enabled on iOS
// because that's only way to browse photos from the camera roll.
if (Platform.OS === 'ios') buttons.push({ text: _('Attach photo'), id: 'attachPhoto' });
buttons.push({ text: _('Take photo'), id: 'takePhoto' });
const buttonId = await dialogs.pop(this, _('Choose an option'), buttons);
if (buttonId === 'takePhoto') this.takePhoto_onPress();
if (buttonId === 'attachFile') void this.attachFile_onPress();
if (buttonId === 'attachPhoto') void this.attachPhoto_onPress();
}
menuOptions() {
const note = this.state.note;
const isTodo = note && !!note.is_todo;
@ -891,26 +912,7 @@ class NoteScreenComponent extends BaseScreenComponent {
if (canAttachPicture) {
output.push({
title: _('Attach...'),
onPress: async () => {
const buttons = [];
// On iOS, it will show "local files", which means certain files saved from the browser
// and the iCloud files, but it doesn't include photos and images from the CameraRoll
//
// On Android, it will depend on the phone, but usually it will allow browing all files and photos.
buttons.push({ text: _('Attach file'), id: 'attachFile' });
// Disabled on Android because it doesn't work due to permission issues, but enabled on iOS
// because that's only way to browse photos from the camera roll.
if (Platform.OS === 'ios') buttons.push({ text: _('Attach photo'), id: 'attachPhoto' });
buttons.push({ text: _('Take photo'), id: 'takePhoto' });
const buttonId = await dialogs.pop(this, _('Choose an option'), buttons);
if (buttonId === 'takePhoto') this.takePhoto_onPress();
if (buttonId === 'attachFile') void this.attachFile_onPress();
if (buttonId === 'attachPhoto') void this.attachPhoto_onPress();
},
onPress: () => this.showAttachMenu(),
});
}
@ -1129,6 +1131,8 @@ class NoteScreenComponent extends BaseScreenComponent {
/>
);
} else {
const editorStyle = this.styles().bodyTextInput;
bodyComponent = <NoteEditor
ref={this.editorRef}
themeId={this.props.themeId}
@ -1137,7 +1141,17 @@ class NoteScreenComponent extends BaseScreenComponent {
onChange={this.onBodyChange}
onSelectionChange={this.body_selectionChange}
onUndoRedoDepthChange={this.onUndoRedoDepthChange}
style={this.styles().bodyTextInput}
onAttach={() => this.showAttachMenu()}
style={{
...editorStyle,
paddingLeft: 0,
paddingRight: 0,
}}
contentStyle={{
// Apply padding to the editor's content, but not the toolbar.
paddingLeft: editorStyle.paddingLeft,
paddingRight: editorStyle.paddingRight,
}}
/>;
}
}

View File

@ -5,7 +5,7 @@ const { View, Text, ScrollView } = require('react-native');
const { connect } = require('react-redux');
const { themeStyle } = require('../global-style.js');
const { ScreenHeader } = require('../screen-header.js');
import ScreenHeader from '../ScreenHeader';
function UpgradeSyncTargetScreen(props: any) {
const upgradeResult = useSyncTargetUpgrade();

View File

@ -2,7 +2,7 @@ const React = require('react');
const { View, Button, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView } = require('react-native');
const { connect } = require('react-redux');
const { ScreenHeader } = require('../screen-header.js');
const { ScreenHeader } = require('../ScreenHeader');
const { _ } = require('@joplin/lib/locale');
const { BaseScreenComponent } = require('../base-screen.js');
const DialogBox = require('react-native-dialogbox').default;

View File

@ -1,7 +1,7 @@
const React = require('react');
const { TextInput, TouchableOpacity, Linking, View, StyleSheet, Text, Button, ScrollView } = require('react-native');
const { connect } = require('react-redux');
const { ScreenHeader } = require('../screen-header.js');
import ScreenHeader from '../ScreenHeader';
const { themeStyle } = require('../global-style.js');
const DialogBox = require('react-native-dialogbox').default;
const { dialogs } = require('../../utils/dialogs.js');

View File

@ -4,7 +4,7 @@ const { View, TextInput, StyleSheet } = require('react-native');
const { connect } = require('react-redux');
const Folder = require('@joplin/lib/models/Folder').default;
const BaseModel = require('@joplin/lib/BaseModel').default;
const { ScreenHeader } = require('../screen-header.js');
const { ScreenHeader } = require('../ScreenHeader');
const { BaseScreenComponent } = require('../base-screen.js');
const { dialogs } = require('../../utils/dialogs.js');
const { themeStyle } = require('../global-style.js');

View File

@ -3,7 +3,7 @@ const React = require('react');
const { FlatList, View, Text, Button, StyleSheet, Platform } = require('react-native');
const { connect } = require('react-redux');
const { reg } = require('@joplin/lib/registry.js');
const { ScreenHeader } = require('../screen-header.js');
const { ScreenHeader } = require('../ScreenHeader');
const time = require('@joplin/lib/time').default;
const { themeStyle } = require('../global-style.js');
const Logger = require('@joplin/lib/Logger').default;

View File

@ -9,7 +9,7 @@ const Tag = require('@joplin/lib/models/Tag').default;
const Note = require('@joplin/lib/models/Note').default;
const Setting = require('@joplin/lib/models/Setting').default;
const { themeStyle } = require('../global-style.js');
const { ScreenHeader } = require('../screen-header.js');
const { ScreenHeader } = require('../ScreenHeader');
const { _ } = require('@joplin/lib/locale');
const { ActionButton } = require('../action-button.js');
const { dialogs } = require('../../utils/dialogs.js');

View File

@ -4,7 +4,7 @@ const { View } = require('react-native');
const { Button } = require('react-native');
const { WebView } = require('react-native-webview');
const { connect } = require('react-redux');
const { ScreenHeader } = require('../screen-header.js');
const { ScreenHeader } = require('../ScreenHeader');
const { reg } = require('@joplin/lib/registry.js');
const { _ } = require('@joplin/lib/locale');
const { BaseScreenComponent } = require('../base-screen.js');

View File

@ -2,7 +2,7 @@ const React = require('react');
const { StyleSheet, View, TextInput, FlatList, TouchableHighlight } = require('react-native');
const { connect } = require('react-redux');
const { ScreenHeader } = require('../screen-header.js');
const { ScreenHeader } = require('../ScreenHeader');
const Icon = require('react-native-vector-icons/Ionicons').default;
const { _ } = require('@joplin/lib/locale');
const Note = require('@joplin/lib/models/Note').default;

View File

@ -3,7 +3,7 @@ const React = require('react');
const { View, Text, Button, FlatList } = require('react-native');
const Setting = require('@joplin/lib/models/Setting').default;
const { connect } = require('react-redux');
const { ScreenHeader } = require('../screen-header.js');
const { ScreenHeader } = require('../ScreenHeader');
const ReportService = require('@joplin/lib/services/ReportService').default;
const { _ } = require('@joplin/lib/locale');
const { BaseScreenComponent } = require('../base-screen.js');

View File

@ -4,7 +4,7 @@ const { View, Text, FlatList, StyleSheet, TouchableOpacity } = require('react-na
const { connect } = require('react-redux');
const Tag = require('@joplin/lib/models/Tag').default;
const { themeStyle } = require('../global-style.js');
const { ScreenHeader } = require('../screen-header.js');
const { ScreenHeader } = require('../ScreenHeader');
const { _ } = require('@joplin/lib/locale');
const { BaseScreenComponent } = require('../base-screen.js');

View File

@ -1,12 +0,0 @@
const { connect } = require('react-redux');
const SideMenu_ = require('react-native-side-menu').default;
class SideMenuComponent extends SideMenu_ {}
const MySideMenu = connect(state => {
return {
isOpen: state.showSideMenu,
};
})(SideMenuComponent);
module.exports = { SideMenu: MySideMenu };

View File

@ -1,17 +1,19 @@
// Test configuration
// See https://jestjs.io/docs/configuration#testenvironment-string
module.exports = {
preset: 'react-native',
const config = {
preset: 'ts-jest',
// File extensions for imports, in order of precedence:
// prefer importing from .ts or .tsx to importing from .js
// files.
moduleFileExtensions: [
'moduleFileExtensions': [
'ts',
'tsx',
'js',
],
};
module.exports = config;
'transform': {
'\\.(ts|tsx)$': 'ts-jest',
},
testMatch: ['**/*.test.(ts|tsx)'],
testPathIgnorePatterns: ['<rootDir>/node_modules/'],
slowTestThreshold: 40,
};

View File

@ -9,11 +9,11 @@
"android": "react-native run-android",
"build": "gulp build",
"tsc": "tsc --project tsconfig.json",
"test": "jest",
"test-ci": "yarn test",
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json",
"clean": "node tools/clean.js",
"buildInjectedJs": "gulp buildInjectedJs",
"test": "jest",
"test-ci": "yarn test",
"watchInjectedJs": "gulp watchInjectedJs",
"postinstall": "jetify && yarn run build"
},
@ -50,12 +50,12 @@
"react-native-image-picker": "^2.3.4",
"react-native-image-resizer": "^1.3.0",
"react-native-modal-datetime-picker": "^9.0.0",
"react-native-popup-menu": "^0.10.0",
"react-native-popup-menu": "^0.15.13",
"react-native-quick-actions": "^0.3.13",
"react-native-rsa-native": "^2.0.4",
"react-native-securerandom": "^1.0.0-rc.0",
"react-native-share": "^7.2.1",
"react-native-side-menu": "^1.1.3",
"react-native-side-menu-updated": "^1.3.2",
"react-native-sqlite-storage": "^5.0.0",
"react-native-url-polyfill": "^1.3.0",
"react-native-vector-icons": "^7.1.0",
@ -89,21 +89,24 @@
"@codemirror/view": "^6.0.0",
"@joplin/tools": "~2.9",
"@lezer/highlight": "^1.0.0",
"@types/fs-extra": "^9.0.13",
"@types/jest": "^28.1.3",
"@types/react-native": "^0.64.4",
"@types/react-redux": "^7.1.24",
"babel-plugin-module-resolver": "^4.1.0",
"execa": "^4.0.0",
"fs-extra": "^8.1.0",
"gulp": "^4.0.2",
"jest": "^28.1.1",
"jest-environment-jsdom": "^28.1.1",
"jest-environment-jsdom": "^28.1.3",
"jetifier": "^1.6.5",
"jsdom": "^20.0.0",
"metro-react-native-babel-preset": "^0.66.2",
"nodemon": "^2.0.12",
"ts-jest": "^28.0.5",
"ts-loader": "^9.3.1",
"typescript": "^4.0.5",
"ts-node": "^10.9.1",
"typescript": "^4.7.4",
"uglify-js": "^3.13.10",
"webpack": "^5.74.0"
}

View File

@ -28,7 +28,8 @@ import SyncTargetJoplinServer from '@joplin/lib/SyncTargetJoplinServer';
import SyncTargetJoplinCloud from '@joplin/lib/SyncTargetJoplinCloud';
import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive';
const VersionInfo = require('react-native-version-info').default;
const { AppState, Keyboard, NativeModules, BackHandler, Animated, View, StatusBar, Linking, Platform } = require('react-native');
const { AppState, Keyboard, NativeModules, BackHandler, Animated, View, StatusBar, Linking, Platform, Dimensions } = require('react-native');
import getResponsiveValue from './components/getResponsiveValue';
import NetInfo from '@react-native-community/netinfo';
const DropdownAlert = require('react-native-dropdownalert').default;
const AlarmServiceDriver = require('./services/AlarmServiceDriver').default;
@ -64,7 +65,7 @@ const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js'
import EncryptionConfigScreen from './components/screens/encryption-config';
const { DropboxLoginScreen } = require('./components/screens/dropbox-login.js');
const { MenuContext } = require('react-native-popup-menu');
const { SideMenu } = require('./components/side-menu.js');
import SideMenu from './components/SideMenu';
const { SideMenuContent } = require('./components/side-menu-content.js');
const { SideMenuContentNote } = require('./components/side-menu-content-note.js');
const { DatabaseDriverReactNative } = require('./utils/database-driver-react-native');
@ -684,6 +685,7 @@ class AppComponent extends React.Component {
this.state = {
sideMenuContentOpacity: new Animated.Value(0),
sideMenuWidth: this.getSideMenuWidth(),
};
this.lastSyncStarted_ = defaultState.syncStarted;
@ -701,6 +703,8 @@ class AppComponent extends React.Component {
void this.handleShareData();
}
};
this.handleScreenWidthChange_ = this.handleScreenWidthChange_.bind(this);
}
// 2020-10-08: It seems the initialisation code is quite fragile in general and should be kept simple.
@ -769,6 +773,7 @@ class AppComponent extends React.Component {
});
AppState.addEventListener('change', this.onAppStateChange_);
this.unsubscribeScreenWidthChangeHandler_ = Dimensions.addEventListener('change', this.handleScreenWidthChange_);
await this.handleShareData();
@ -783,6 +788,12 @@ class AppComponent extends React.Component {
public componentWillUnmount() {
AppState.removeEventListener('change', this.onAppStateChange_);
Linking.removeEventListener('url', this.handleOpenURL_);
if (this.unsubscribeScreenWidthChangeHandler_) {
this.unsubscribeScreenWidthChangeHandler_.remove();
this.unsubscribeScreenWidthChangeHandler_ = null;
}
if (this.unsubscribeNetInfoHandler_) this.unsubscribeNetInfoHandler_();
}
@ -828,6 +839,10 @@ class AppComponent extends React.Component {
}
}
private async handleScreenWidthChange_() {
this.setState({ sideMenuWidth: this.getSideMenuWidth() });
}
public UNSAFE_componentWillReceiveProps(newProps: any) {
if (newProps.syncStarted !== this.lastSyncStarted_) {
if (!newProps.syncStarted) FoldersScreenUtils.refreshFolders();
@ -843,6 +858,18 @@ class AppComponent extends React.Component {
});
}
private getSideMenuWidth = () => {
const sideMenuWidth = getResponsiveValue({
sm: 250,
md: 260,
lg: 270,
xl: 280,
xxl: 290,
});
return sideMenuWidth;
};
public render() {
if (this.props.appState !== 'ready') return null;
const theme = themeStyle(this.props.themeId);
@ -872,6 +899,7 @@ class AppComponent extends React.Component {
Config: { screen: ConfigScreen },
};
// const statusBarStyle = theme.appearance === 'light-content';
const statusBarStyle = 'light-content';
@ -880,6 +908,7 @@ class AppComponent extends React.Component {
<SideMenu
menu={sideMenuContent}
edgeHitWidth={5}
openMenuOffset={this.state.sideMenuWidth}
menuPosition={menuPosition}
onChange={(isOpen: boolean) => this.sideMenu_change(isOpen)}
onSliding={(percent: number) => {

View File

@ -7,7 +7,9 @@ import { mkdirp, readFile, writeFile } from 'fs-extra';
import { dirname, extname, basename } from 'path';
const execa = require('execa');
import webpack from 'webpack';
// We need this to be transpiled to `const webpack = require('webpack')`.
// As such, do a namespace import. See https://www.typescriptlang.org/tsconfig#esModuleInterop
import * as webpack from 'webpack';
const rootDir = dirname(dirname(dirname(__dirname)));
const mobileDir = `${rootDir}/packages/app-mobile`;

View File

@ -5,12 +5,14 @@
"**/*.tsx",
],
"exclude": [
//Files that don't need transpilation
"**/node_modules",
// Files that don't need transpilation
"gulpfile.ts",
"tools/*.ts",
"**/*.test.ts",
"**/*.test.tsx",
"gulpfile.ts",
"tools/*.ts",
],
"compilerOptions": {
"types": ["jest", "node"]
}
}

View File

@ -107,10 +107,8 @@ export default class FsDriverRN extends FsDriverBase {
}
public async move(source: string, dest: string) {
if (isScopedUri(source) && isScopedUri(dest)) {
if (isScopedUri(source) || isScopedUri(dest)) {
await RNSAF.moveFile(source, dest, { replaceIfDestinationExists: true });
} else if (isScopedUri(source) || isScopedUri(dest)) {
throw new Error('Move between different storage types not supported');
}
return RNFS.moveFile(source, dest);
}
@ -191,11 +189,9 @@ export default class FsDriverRN extends FsDriverBase {
public async copy(source: string, dest: string) {
let retry = false;
try {
if (isScopedUri(source) && isScopedUri(dest)) {
if (isScopedUri(source) || isScopedUri(dest)) {
await RNSAF.copyFile(source, dest, { replaceIfDestinationExists: true });
return;
} else if (isScopedUri(source) || isScopedUri(dest)) {
throw new Error('Move between different storage types not supported');
}
await RNFS.copyFile(source, dest);
} catch (error) {
@ -204,7 +200,13 @@ export default class FsDriverRN extends FsDriverBase {
await this.unlink(dest);
}
if (retry) await RNFS.copyFile(source, dest);
if (retry) {
if (isScopedUri(source) || isScopedUri(dest)) {
await RNSAF.copyFile(source, dest, { replaceIfDestinationExists: true });
} else {
await RNFS.copyFile(source, dest);
}
}
}
public async unlink(path: string) {

View File

@ -11,7 +11,7 @@ class GeolocationReact {
speed: 0,
heading: 0,
accuracy: 20,
longitude: -3.4596633911132812,
longitude: -3.45966339111328,
altitude: 0,
latitude: 48.73219093634444,
},

View File

@ -9,5 +9,6 @@
"homepage_url": "<%= pluginHomepageUrl %>",
"repository_url": "<%= pluginRepositoryUrl %>",
"keywords": [],
"categories": []
"categories": [],
"screenshots": []
}

View File

@ -30,6 +30,7 @@ const userConfig = Object.assign({}, {
const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`;
const allPossibleCategories = ['appearance', 'developer tools', 'productivity', 'themes', 'integrations', 'viewer', 'search', 'tags', 'editor', 'files', 'personal knowledge management'];
const allPossibleScreenshotsType = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
const manifest = readManifest(manifestPath);
const pluginArchiveFilePath = path.resolve(publishDir, `${manifest.id}.jpl`);
const pluginInfoFilePath = path.resolve(publishDir, `${manifest.id}.json`);
@ -76,11 +77,29 @@ function validateCategories(categories) {
});
}
function validateScreenshots(screenshots) {
if (!screenshots) return null;
screenshots.forEach(screenshot => {
if (!screenshot.src) throw new Error('You must specify a src for each screenshot');
const screenshotType = screenshot.src.split('.').pop();
if (!allPossibleScreenshotsType.includes(screenshotType)) throw new Error(`${screenshotType} is not a valid screenshot type. Valid types are: \n${allPossibleScreenshotsType}\n`);
const screenshotPath = path.resolve(srcDir, screenshot.src);
// Max file size is 1MB
const fileMaxSize = 1024;
const fileSize = fs.statSync(screenshotPath).size / 1024;
if (fileSize > fileMaxSize) throw new Error(`Max screenshot file size is ${fileMaxSize}KB. ${screenshotPath} is ${fileSize}KB`);
});
}
function readManifest(manifestPath) {
const content = fs.readFileSync(manifestPath, 'utf8');
const output = JSON.parse(content);
if (!output.id) throw new Error(`Manifest plugin ID is not set in ${manifestPath}`);
validateCategories(output.categories);
validateScreenshots(output.screenshots);
return output;
}

View File

@ -848,6 +848,7 @@ export default class BaseApplication {
Setting.setValue('firstStart', 0);
} else {
Setting.applyDefaultMigrations();
Setting.applyUserSettingMigration();
}
setLocale(Setting.value('locale'));

View File

@ -118,6 +118,7 @@ export const useInputMasterPassword = (masterKeys: MasterKeyEntity[], activeMast
if (!(await masterPasswordIsValid(inputMasterPassword, masterKeys.find(mk => mk.id === activeMasterKeyId)))) {
alert('Password is invalid. Please try again.');
}
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [inputMasterPassword]);
const onMasterPasswordChange = useCallback((password: string) => {

View File

@ -14,5 +14,6 @@ export default function(effect: EffectFunction, dependencies: any[]) {
return () => {
event.cancelled = true;
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, dependencies);
}

View File

@ -27,7 +27,7 @@ function useElementSize(elementRef: any): Size {
// Initial size on mount
useEffect(() => {
updateSize();
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
useEventListener('resize', updateSize);

View File

@ -14,7 +14,7 @@ const { substrWithEllipsis } = require('../string-utils.js');
const logger = Logger.create('models/Folder');
interface FolderEntityWithChildren extends FolderEntity {
export interface FolderEntityWithChildren extends FolderEntity {
children?: FolderEntity[];
}
@ -544,6 +544,12 @@ export default class Folder extends BaseItem {
static async allAsTree(folders: FolderEntity[] = null, options: any = null) {
const all = folders ? folders : await this.all(options);
if (options && options.includeNotes) {
for (const folder of all) {
folder.notes = await Note.previews(folder.id);
}
}
// https://stackoverflow.com/a/49387427/561309
function getNestedChildren(models: FolderEntityWithChildren[], parentId: string) {
const nestedTreeStructure = [];
@ -609,7 +615,7 @@ export default class Folder extends BaseItem {
return output.join(' / ');
}
static buildTree(folders: FolderEntity[]) {
static buildTree(folders: FolderEntity[]): FolderEntityWithChildren[] {
const idToFolders: Record<string, any> = {};
for (let i = 0; i < folders.length; i++) {
idToFolders[folders[i].id] = Object.assign({}, folders[i]);

View File

@ -273,6 +273,31 @@ describe('models/Setting', function() {
expect(Setting.value('style.editor.contentMaxWidth')).toBe(600); // Changed
}));
it('should migrate to new setting', (async () => {
await Setting.reset();
Setting.setValue('spellChecker.language', 'fr-FR');
Setting.applyUserSettingMigration();
expect(Setting.value('spellChecker.languages')).toStrictEqual(['fr-FR']);
}));
it('should not override new setting, if it already set', (async () => {
await Setting.reset();
Setting.setValue('spellChecker.languages', ['fr-FR', 'en-US']);
Setting.setValue('spellChecker.language', 'fr-FR');
Setting.applyUserSettingMigration();
expect(Setting.value('spellChecker.languages')).toStrictEqual(['fr-FR', 'en-US']);
}));
it('should not set new setting, if old setting is not set', (async () => {
await Setting.reset();
expect(Setting.isSet('spellChecker.language')).toBe(false);
Setting.applyUserSettingMigration();
expect(Setting.isSet('spellChecker.languages')).toBe(false);
}));
it('should load sub-profile settings - 1', async () => {
await Setting.reset();

View File

@ -201,6 +201,22 @@ const defaultMigrations: DefaultMigration[] = [
},
];
// "UserSettingMigration" are used to migrate existing user setting to a new setting. With a way
// to transform existing value of the old setting to value and type of the new setting.
interface UserSettingMigration {
oldName: string;
newName: string;
transformValue: Function;
}
const userSettingMigration: UserSettingMigration[] = [
{
oldName: 'spellChecker.language',
newName: 'spellChecker.languages',
transformValue: (value: string) => { return [value]; },
},
];
class Setting extends BaseModel {
public static schemaUrl = 'https://joplinapp.org/schema/settings.json';
@ -1010,6 +1026,19 @@ class Setting extends BaseModel {
isGlobal: true,
},
// Enables/disables spellcheck in the mobile markdown beta editor.
'editor.mobile.spellcheckEnabled': {
value: true,
type: SettingItemType.Bool,
section: 'note',
public: true,
appTypes: [AppType.Mobile],
show: (settings: any) => settings['editor.beta'],
label: () => _('Enable spellcheck in the beta editor'),
storage: SettingStorage.File,
isGlobal: true,
},
newTodoFocus: {
value: 'title',
type: SettingItemType.String,
@ -1460,7 +1489,8 @@ class Setting extends BaseModel {
'camera.ratio': { value: '4:3', type: SettingItemType.String, public: false, appTypes: [AppType.Mobile] },
'spellChecker.enabled': { value: true, type: SettingItemType.Bool, isGlobal: true, storage: SettingStorage.File, public: false },
'spellChecker.language': { value: '', type: SettingItemType.String, isGlobal: true, storage: SettingStorage.File, public: false },
'spellChecker.language': { value: '', type: SettingItemType.String, isGlobal: true, storage: SettingStorage.File, public: false }, // Depreciated in favour of spellChecker.languages.
'spellChecker.languages': { value: [], type: SettingItemType.Array, isGlobal: true, storage: SettingStorage.File, public: false },
windowContentZoomFactor: {
value: 100,
@ -1608,6 +1638,16 @@ class Setting extends BaseModel {
this.setValue('lastSettingDefaultMigration', defaultMigrations.length - 1);
}
public static applyUserSettingMigration() {
// Function to translate existing user settings to new setting.
userSettingMigration.forEach(userMigration => {
if (!this.isSet(userMigration.newName) && this.isSet(userMigration.oldName)) {
this.setValue(userMigration.newName, userMigration.transformValue(this.value(userMigration.oldName)));
logger.info(`Migrating ${userMigration.oldName} to ${userMigration.newName}`);
}
});
}
public static featureFlagKeys(appType: AppType): string[] {
const keys = this.keys(false, appType);
return keys.filter(k => k.indexOf('featureFlag.') === 0);
@ -1669,7 +1709,7 @@ class Setting extends BaseModel {
}
public static isSet(key: string) {
return this.cache_.find(d => d.key === key);
return !!this.cache_.find(d => d.key === key);
}
static keyDescription(key: string, appType: AppType = null) {

View File

@ -52,6 +52,7 @@ interface StateResourceFetcher {
export interface State {
notes: any[];
noteSelectionEnabled?: boolean;
notesSource: string;
notesParentType: string;
folders: any[];

View File

@ -8,7 +8,7 @@ class Registry {
private syncTargets_: any = {};
private logger_: Logger = null;
private schedSyncCalls_: boolean[] = [];
private waitForReSyncCalls_: boolean[]= [];
private waitForReSyncCalls_: boolean[] = [];
private setupRecurrentCalls_: boolean[] = [];
private timerCallbackCalls_: boolean[] = [];
private showErrorMessageBoxHandler_: any;

View File

@ -131,11 +131,15 @@ export default class ExternalEditWatcher {
if (!noteContent) this.logger().warn(`ExternalEditWatcher: Could not re-read note - user might have purposely deleted note content: ${id}`);
}
this.logger().debug('ExternalEditWatcher: Updating note object.');
const updatedNote = await Note.unserializeForEdit(noteContent);
updatedNote.id = id;
updatedNote.parent_id = note.parent_id;
await Note.save(updatedNote);
this.eventEmitter_.emit('noteChange', { id: updatedNote.id, note: updatedNote });
} else {
this.logger().debug('ExternalEditWatcher: Skipping this event.');
}
this.skipNextChangeEvent_ = {};

Some files were not shown because too many files have changed in this diff Show More