diff --git a/.eslintignore b/.eslintignore
index 0cbe191010d..79764cf70da 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -641,6 +641,7 @@ packages/app-mobile/components/NoteEditor/ImageEditor/isEditableResource.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/applyTemplateToEditor.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.test.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.js
+packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/polyfills.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/startAutosaveLoop.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/types.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/watchEditorForTemplateChanges.js
@@ -703,6 +704,7 @@ packages/app-mobile/components/plugins/dialogs/hooks/useViewInfos.js
packages/app-mobile/components/plugins/dialogs/hooks/useWebViewSetup.js
packages/app-mobile/components/plugins/types.js
packages/app-mobile/components/plugins/utils/createOnLogHandler.js
+packages/app-mobile/components/plugins/utils/useOnDevPluginsUpdated.js
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
packages/app-mobile/components/screens/ConfigScreen/JoplinCloudConfig.js
diff --git a/.gitignore b/.gitignore
index 83f939737d4..18586771d0d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -616,6 +616,7 @@ packages/app-mobile/components/NoteEditor/ImageEditor/isEditableResource.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/applyTemplateToEditor.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.test.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.js
+packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/polyfills.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/startAutosaveLoop.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/types.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/watchEditorForTemplateChanges.js
@@ -678,6 +679,7 @@ packages/app-mobile/components/plugins/dialogs/hooks/useViewInfos.js
packages/app-mobile/components/plugins/dialogs/hooks/useWebViewSetup.js
packages/app-mobile/components/plugins/types.js
packages/app-mobile/components/plugins/utils/createOnLogHandler.js
+packages/app-mobile/components/plugins/utils/useOnDevPluginsUpdated.js
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
packages/app-mobile/components/screens/ConfigScreen/JoplinCloudConfig.js
diff --git a/.yarn/patches/app-builder-lib-npm-24.13.3-86a66c0bf3.patch b/.yarn/patches/app-builder-lib-npm-24.13.3-86a66c0bf3.patch
index 310ffc5f6b0..eaf90825797 100644
--- a/.yarn/patches/app-builder-lib-npm-24.13.3-86a66c0bf3.patch
+++ b/.yarn/patches/app-builder-lib-npm-24.13.3-86a66c0bf3.patch
@@ -1,7 +1,15 @@
-# This patch prevents the installer from considering itself as a running instance of Joplin.
+# This patch's goal is to work around an issue in the NSIS uninstaller on Windows:
+# - For future uninstallers, this patch backports an upstream commit that changes how
+# running copies of the app are found.
+# - See https://github.com/electron-userland/electron-builder/pull/8133
+# - If an existing uninstaller fails, gives an option to continue with the installation
+# despite the failure.
+# - Updates "uninstall failed" error messages to state that uninstallation failed (rather
+# than incorrectly stating that the issue was with closing the app).
+#
# See https://github.com/laurent22/joplin/pull/11541
diff --git a/templates/nsis/include/allowOnlyOneInstallerInstance.nsh b/templates/nsis/include/allowOnlyOneInstallerInstance.nsh
-index fe5d45c730f36c9fe8d8cfea12e242e501b67139..af2ce5c90ac910b079e24992519bffe33d57668a 100644
+index fe5d45c730f36c9fe8d8cfea12e242e501b67139..97b27fce6798e30e3e631221435f09b3579e77c3 100644
--- a/templates/nsis/include/allowOnlyOneInstallerInstance.nsh
+++ b/templates/nsis/include/allowOnlyOneInstallerInstance.nsh
@@ -42,7 +42,7 @@
@@ -9,7 +17,74 @@ index fe5d45c730f36c9fe8d8cfea12e242e501b67139..af2ce5c90ac910b079e24992519bffe3
!else
# find process owned by current user
- nsExec::Exec `%SYSTEMROOT%\System32\cmd.exe /c tasklist /FI "USERNAME eq %USERNAME%" /FI "IMAGENAME eq ${_FILE}" /FO csv | %SYSTEMROOT%\System32\find.exe "${_FILE}"`
-+ nsExec::Exec `%SYSTEMROOT%\System32\cmd.exe /c tasklist /FI "USERNAME eq %USERNAME%" /FI "PID ne $pid" /FI "IMAGENAME eq ${_FILE}" /FO csv | %SYSTEMROOT%\System32\find.exe "${_FILE}"`
++ nsExec::Exec `"$SYSDIR\cmd.exe" /c tasklist /FI "USERNAME eq %USERNAME%" /FI "IMAGENAME eq ${_FILE}" /FO csv | "$SYSDIR\find.exe" "${_FILE}"`
Pop ${_ERR}
!endif
!macroend
+@@ -73,7 +73,7 @@
+ !ifdef INSTALL_MODE_PER_ALL_USERS
+ nsExec::Exec `taskkill /im "${APP_EXECUTABLE_FILENAME}" /fi "PID ne $pid"`
+ !else
+- nsExec::Exec `%SYSTEMROOT%\System32\cmd.exe /c taskkill /im "${APP_EXECUTABLE_FILENAME}" /fi "PID ne $pid" /fi "USERNAME eq %USERNAME%"`
++ nsExec::Exec `"$SYSDIR\cmd.exe" /c taskkill /im "${APP_EXECUTABLE_FILENAME}" /fi "PID ne $pid" /fi "USERNAME eq %USERNAME%"`
+ !endif
+ # to ensure that files are not "in-use"
+ Sleep 300
+@@ -91,7 +91,7 @@
+ !ifdef INSTALL_MODE_PER_ALL_USERS
+ nsExec::Exec `taskkill /f /im "${APP_EXECUTABLE_FILENAME}" /fi "PID ne $pid"`
+ !else
+- nsExec::Exec `%SYSTEMROOT%\System32\cmd.exe /c taskkill /f /im "${APP_EXECUTABLE_FILENAME}" /fi "PID ne $pid" /fi "USERNAME eq %USERNAME%"`
++ nsExec::Exec `"$SYSDIR\cmd.exe" /c taskkill /f /im "${APP_EXECUTABLE_FILENAME}" /fi "PID ne $pid" /fi "USERNAME eq %USERNAME%"`
+ !endif
+ !insertmacro FIND_PROCESS "${APP_EXECUTABLE_FILENAME}" $R0
+ ${If} $R0 == 0
+diff --git a/templates/nsis/include/installUtil.nsh b/templates/nsis/include/installUtil.nsh
+index 47367741632726ba0886ac516461dbe98b7aea58..675965762375925a505ca6d8bbb67507ef696c2e 100644
+--- a/templates/nsis/include/installUtil.nsh
++++ b/templates/nsis/include/installUtil.nsh
+@@ -126,10 +126,11 @@ Function handleUninstallResult
+ Return
+
+ ${if} $R0 != 0
+- MessageBox MB_OK|MB_ICONEXCLAMATION "$(uninstallFailed): $R0"
++ # MessageBox MB_OK|MB_ICONEXCLAMATION "$(uninstallFailed): $R0"
+ DetailPrint `Uninstall was not successful. Uninstaller error code: $R0.`
+- SetErrorLevel 2
+- Quit
++ DetailPrint `Continuing anyway. See https://github.com/laurent22/joplin/pull/11612.`
++ # SetErrorLevel 2
++ # Quit
+ ${endif}
+ FunctionEnd
+
+@@ -216,11 +217,13 @@ Function uninstallOldVersion
+ IntOp $R5 $R5 + 1
+
+ ${if} $R5 > 5
+- MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeClosed)" /SD IDCANCEL IDRETRY OneMoreAttempt
+- Return
++ MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeUninstalled)" /SD IDCANCEL IDRETRY ContinueWithoutUninstall
++ Abort ; Exit early
++ ContinueWithoutUninstall:
++ Return
+ ${endIf}
+
+- OneMoreAttempt:
++# OneMoreAttempt: ; Commented out because unused
+ ExecWait '"$uninstallerFileNameTemp" /S /KEEP_APP_DATA $0 _?=$installationDir' $R0
+ ifErrors TryInPlace CheckResult
+
+diff --git a/templates/nsis/messages.yml b/templates/nsis/messages.yml
+index a1c2847fa48d79f835b30b48e999ccaf3c818657..6884c18d1e77dbd6be114401d23cf5caf3e0dd94 100644
+--- a/templates/nsis/messages.yml
++++ b/templates/nsis/messages.yml
+@@ -235,3 +235,8 @@ uninstallFailed:
+ sv: Det gick inte att avinstallera gamla programfiler. Försök att köra installationsprogrammet igen.
+ uk: Не вдалось видалити старі файли застосунку. Будь ласка, спробуйте запустити встановлювач знов.
+ zh_TW: 無法俺安裝舊的應用程式檔案。 請嘗試再次執行安裝程式。
++
++
++appCannotBeUninstalled:
++ en: "The old version of ${PRODUCT_NAME} could not be removed. \nClick Retry to skip this step."
++
diff --git a/Assets/JoplinLetterBlue.svg b/Assets/JoplinLetterBlue.svg
deleted file mode 100644
index 02a9369957c..00000000000
--- a/Assets/JoplinLetterBlue.svg
+++ /dev/null
@@ -1,77 +0,0 @@
-
-
diff --git a/Dockerfile.server b/Dockerfile.server
index 8dff0e0008b..076da5859ad 100644
--- a/Dockerfile.server
+++ b/Dockerfile.server
@@ -2,11 +2,11 @@
# Build stage
# =============================================================================
-FROM node:18-bullseye AS builder
+FROM node:18 AS builder
RUN apt-get update \
&& apt-get install -y \
- python tini \
+ python3 tini \
&& rm -rf /var/lib/apt/lists/*
# Enables Yarn
@@ -56,7 +56,7 @@ RUN BUILD_SEQUENCIAL=1 yarn install --inline-builds \
# from a smaller base image.
# =============================================================================
-FROM node:18-bullseye-slim
+FROM node:18-slim
ARG user=joplin
RUN useradd --create-home --shell /bin/bash $user
diff --git a/packages/app-cli/app/main.js b/packages/app-cli/app/main.js
index 2b4f38d6f30..ab3e31c8e6c 100644
--- a/packages/app-cli/app/main.js
+++ b/packages/app-cli/app/main.js
@@ -1,4 +1,4 @@
-#!/usr/bin/env -S NODE_OPTIONS=--no-deprecation node
+#!/usr/bin/env node
// Use njstrace to find out what Node.js might be spending time on
// var njstrace = require('njstrace').inject();
diff --git a/packages/app-cli/tests/support/onenote/hyperlink_marker_as_first_character.zip b/packages/app-cli/tests/support/onenote/hyperlink_marker_as_first_character.zip
new file mode 100644
index 00000000000..7352fd91d10
Binary files /dev/null and b/packages/app-cli/tests/support/onenote/hyperlink_marker_as_first_character.zip differ
diff --git a/packages/app-cli/tests/support/onenote/remove_hyperlink_on_title.zip b/packages/app-cli/tests/support/onenote/remove_hyperlink_on_title.zip
new file mode 100644
index 00000000000..fedd8123a00
Binary files /dev/null and b/packages/app-cli/tests/support/onenote/remove_hyperlink_on_title.zip differ
diff --git a/packages/app-cli/tests/support/test_notes/md/sample-malformed-uri.md b/packages/app-cli/tests/support/test_notes/md/sample-malformed-uri.md
new file mode 100644
index 00000000000..b626bf22bf3
--- /dev/null
+++ b/packages/app-cli/tests/support/test_notes/md/sample-malformed-uri.md
@@ -0,0 +1 @@
+![malformed link](https://malformed_uri/%E0%A4%A.jpg)
diff --git a/packages/app-desktop/bridge.ts b/packages/app-desktop/bridge.ts
index f6f31e0361a..12941a4071b 100644
--- a/packages/app-desktop/bridge.ts
+++ b/packages/app-desktop/bridge.ts
@@ -1,5 +1,5 @@
import ElectronAppWrapper from './ElectronAppWrapper';
-import shim from '@joplin/lib/shim';
+import shim, { MessageBoxType } from '@joplin/lib/shim';
import { _, setLocale } from '@joplin/lib/locale';
import { BrowserWindow, nativeTheme, nativeImage, shell, dialog, MessageBoxSyncOptions, safeStorage } from 'electron';
import { dirname, toSystemSlashes } from '@joplin/lib/path-utils';
@@ -384,9 +384,14 @@ export class Bridge {
/* returns the index of the clicked button */
public showMessageBox(message: string, options: MessageDialogOptions = {}) {
+ const defaultButtons = [_('OK')];
+ if (options.type !== MessageBoxType.Error && options.type !== MessageBoxType.Info) {
+ defaultButtons.push(_('Cancel'));
+ }
+
const result = this.showMessageBox_(this.activeWindow(), { type: 'question',
message: message,
- buttons: [_('OK'), _('Cancel')], ...options });
+ buttons: defaultButtons, ...options });
return result;
}
diff --git a/packages/app-desktop/commands/copyDevCommand.ts b/packages/app-desktop/commands/copyDevCommand.ts
index e103ed61f26..d7bcb998b8f 100644
--- a/packages/app-desktop/commands/copyDevCommand.ts
+++ b/packages/app-desktop/commands/copyDevCommand.ts
@@ -1,5 +1,6 @@
import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
+import shim, { MessageBoxType } from '@joplin/lib/shim';
const app = require('@electron/remote').app;
const { clipboard } = require('electron');
@@ -14,7 +15,7 @@ export const runtime = (): CommandRuntime => {
const appPath = app.getPath('exe');
const cmd = `${appPath} --env dev`;
clipboard.writeText(cmd);
- alert(`The dev mode command has been copied to clipboard:\n\n${cmd}`);
+ await shim.showMessageBox(`The dev mode command has been copied to clipboard:\n\n${cmd}`, { type: MessageBoxType.Info });
},
};
};
diff --git a/packages/app-desktop/commands/restoreNoteRevision.ts b/packages/app-desktop/commands/restoreNoteRevision.ts
index ea7cc86d7c0..c80870643a1 100644
--- a/packages/app-desktop/commands/restoreNoteRevision.ts
+++ b/packages/app-desktop/commands/restoreNoteRevision.ts
@@ -1,5 +1,6 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import RevisionService from '@joplin/lib/services/RevisionService';
+import shim, { MessageBoxType } from '@joplin/lib/shim';
export const declaration: CommandDeclaration = {
name: 'restoreNoteRevision',
@@ -11,9 +12,9 @@ export const runtime = (): CommandRuntime => {
execute: async (_context: CommandContext, noteId: string, reverseRevIndex = 0) => {
try {
const note = await RevisionService.instance().restoreNoteById(noteId, reverseRevIndex);
- alert(RevisionService.instance().restoreSuccessMessage(note));
+ await shim.showMessageBox(RevisionService.instance().restoreSuccessMessage(note), { type: MessageBoxType.Info });
} catch (error) {
- alert(error.message);
+ await shim.showErrorDialog(error.message);
}
},
};
diff --git a/packages/app-desktop/gui/ClipperConfigScreen.tsx b/packages/app-desktop/gui/ClipperConfigScreen.tsx
index 6fd1dfa36dd..a6d0c6c4e5b 100644
--- a/packages/app-desktop/gui/ClipperConfigScreen.tsx
+++ b/packages/app-desktop/gui/ClipperConfigScreen.tsx
@@ -9,6 +9,7 @@ import ClipperServer from '@joplin/lib/ClipperServer';
import Setting from '@joplin/lib/models/Setting';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import { AppState } from '../app.reducer';
+import shim, { MessageBoxType } from '@joplin/lib/shim';
class ClipperConfigScreenComponent extends React.Component {
public constructor() {
@@ -30,7 +31,7 @@ class ClipperConfigScreenComponent extends React.Component {
private copyToken_click() {
clipboard.writeText(this.props.apiToken);
- alert(_('Token has been copied to the clipboard!'));
+ void shim.showMessageBox(_('Token has been copied to the clipboard!'), { type: MessageBoxType.Info });
}
private renewToken_click() {
diff --git a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx
index 25b00bc4b80..3a5f9783b4d 100644
--- a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx
+++ b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx
@@ -19,6 +19,7 @@ import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/conf
import MacOSMissingPasswordHelpLink from './controls/MissingPasswordHelpLink';
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
import SettingComponent, { UpdateSettingValueEvent } from './controls/SettingComponent';
+import shim from '@joplin/lib/shim';
interface Font {
@@ -144,7 +145,7 @@ class ConfigScreenComponent extends React.Component {
screenName = section.name;
if (this.hasChanges()) {
- const ok = confirm(_('This will open a new screen. Save your current changes?'));
+ const ok = await shim.showConfirmationDialog(_('This will open a new screen. Save your current changes?'));
if (ok) {
await shared.saveSettings(this);
}
diff --git a/packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx b/packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx
index f045c7e1549..03b2c729ee5 100644
--- a/packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx
+++ b/packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx
@@ -3,7 +3,7 @@ import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
import time from '@joplin/lib/time';
-import shim from '@joplin/lib/shim';
+import shim, { MessageBoxType } from '@joplin/lib/shim';
import dialogs from '../dialogs';
import { decryptedStatText, determineKeyPassword, dontReencryptData, enableEncryptionConfirmationMessages, onSavePasswordClick, onToggleEnabledClick, reencryptData, upgradeMasterKey, useInputPasswords, useNeedMasterPassword, usePasswordChecker, useStats, useToggleShowDisabledMasterKeys } from '@joplin/lib/components/EncryptionConfigScreen/utils';
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
@@ -47,7 +47,7 @@ const EncryptionConfigScreen = (props: Props) => {
const onUpgradeMasterKey = useCallback(async (mk: MasterKeyEntity) => {
const password = determineKeyPassword(mk.id, masterPasswordKeys, props.masterPassword, props.passwords);
const result = await upgradeMasterKey(mk, password);
- alert(result);
+ await shim.showMessageBox(result, { type: MessageBoxType.Info });
}, [props.passwords, masterPasswordKeys, props.masterPassword]);
const renderNeedUpgradeSection = () => {
diff --git a/packages/app-desktop/gui/MasterPasswordDialog/Dialog.tsx b/packages/app-desktop/gui/MasterPasswordDialog/Dialog.tsx
index 95ac261840b..1573bbc2460 100644
--- a/packages/app-desktop/gui/MasterPasswordDialog/Dialog.tsx
+++ b/packages/app-desktop/gui/MasterPasswordDialog/Dialog.tsx
@@ -11,6 +11,7 @@ import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import KvStore from '@joplin/lib/services/KvStore';
import ShareService from '@joplin/lib/services/share/ShareService';
import LabelledPasswordInput from '../PasswordInput/LabelledPasswordInput';
+import shim from '@joplin/lib/shim';
interface Props {
themeId: number;
@@ -80,7 +81,7 @@ export default function(props: Props) {
void reg.waitForSyncFinishedThenSync();
onClose();
} catch (error) {
- alert(error.message);
+ void shim.showErrorDialog(error.message);
} finally {
setUpdatingPassword(false);
}
diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx
index 8e4f59197af..5b1bd0e683f 100644
--- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx
+++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx
@@ -48,7 +48,10 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef(null);
+ const rootRef = useRef(null);
+ rootRef.current = editorRoot;
+
const webviewRef = useRef(null);
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
const props_onChangeRef = useRef(null);
@@ -410,6 +413,8 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef {
+ if (!editorRoot) return () => {};
+
const theme = themeStyle(props.themeId);
// Selection in dark mode is hard to see so make it brighter.
@@ -431,10 +436,11 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef {
- document.head.removeChild(element);
+ ownerDoc.head.removeChild(element);
};
- // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
- }, [props.themeId, props.contentMaxWidth]);
+ }, [props.themeId, props.contentMaxWidth, props.fontSize, editorRoot]);
const webview_domReady = useCallback(() => {
setWebviewReady(true);
@@ -774,7 +784,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef
-
+
{props.noteToolbar}
diff --git a/packages/app-desktop/gui/NoteRevisionViewer.tsx b/packages/app-desktop/gui/NoteRevisionViewer.tsx
index a58f8963bff..dbdde2c305e 100644
--- a/packages/app-desktop/gui/NoteRevisionViewer.tsx
+++ b/packages/app-desktop/gui/NoteRevisionViewer.tsx
@@ -17,6 +17,7 @@ const urlUtils = require('@joplin/lib/urlUtils');
const ReactTooltip = require('react-tooltip');
const { connect } = require('react-redux');
import shared from '@joplin/lib/components/shared/note-screen-shared';
+import shim, { MessageBoxType } from '@joplin/lib/shim';
interface Props {
themeId: number;
@@ -97,7 +98,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
this.setState({ restoring: true });
await RevisionService.instance().importRevisionNote(this.state.note);
this.setState({ restoring: false });
- alert(RevisionService.instance().restoreSuccessMessage(this.state.note));
+ await shim.showMessageBox(RevisionService.instance().restoreSuccessMessage(this.state.note), { type: MessageBoxType.Info });
}
private backButton_click() {
diff --git a/packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.tsx b/packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.tsx
index e3eb8b159d4..df76c33edce 100644
--- a/packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.tsx
+++ b/packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.tsx
@@ -18,6 +18,7 @@ import { connect } from 'react-redux';
import { reg } from '@joplin/lib/registry';
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
import { ChangeEvent, Dropdown, DropdownOptions, DropdownVariant } from '../Dropdown/Dropdown';
+import shim from '@joplin/lib/shim';
const logger = Logger.create('ShareFolderDialog');
@@ -242,13 +243,13 @@ function ShareFolderDialog(props: Props) {
}
async function recipient_delete(event: RecipientDeleteEvent) {
- if (!confirm(_('Delete this invitation? The recipient will no longer have access to this shared notebook.'))) return;
+ if (!await shim.showConfirmationDialog(_('Delete this invitation? The recipient will no longer have access to this shared notebook.'))) return;
try {
await ShareService.instance().deleteShareRecipient(event.shareUserId);
} catch (error) {
logger.error(error);
- alert(_('The recipient could not be removed from the list. Please try again.\n\nThe error was: "%s"', error.message));
+ await shim.showErrorDialog(_('The recipient could not be removed from the list. Please try again.\n\nThe error was: "%s"', error.message));
}
await ShareService.instance().refreshShareUsers(share.id);
@@ -290,7 +291,7 @@ function ShareFolderDialog(props: Props) {
});
await ShareService.instance().setPermissions(share.id, shareUserId, permissionsFromString(value));
} catch (error) {
- alert(`Could not set permissions: ${error.message}`);
+ void shim.showErrorDialog(`Could not set permissions: ${error.message}`);
logger.error(error);
} finally {
setRecipientsBeingUpdated(prev => {
@@ -383,7 +384,9 @@ function ShareFolderDialog(props: Props) {
async function buttonRow_click(event: ClickEvent) {
if (event.buttonName === 'unshare') {
- if (!confirm(_('Unshare this notebook? The recipients will no longer have access to its content.'))) return;
+ if (!await shim.showConfirmationDialog(_('Unshare this notebook? The recipients will no longer have access to its content.'))) {
+ return;
+ }
await ShareService.instance().unshareFolder(props.folderId);
void synchronize();
}
diff --git a/packages/app-desktop/gui/ShareNoteDialog.tsx b/packages/app-desktop/gui/ShareNoteDialog.tsx
index e09427c5a19..7f9e4c7232e 100644
--- a/packages/app-desktop/gui/ShareNoteDialog.tsx
+++ b/packages/app-desktop/gui/ShareNoteDialog.tsx
@@ -16,6 +16,7 @@ import { connect } from 'react-redux';
import { AppState } from '../app.reducer';
import { getEncryptionEnabled } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
+import shim from '@joplin/lib/shim';
const { clipboard } = require('electron');
interface Props {
@@ -146,7 +147,7 @@ export function ShareNoteDialog(props: Props) {
reg.logger().error('ShareNoteDialog: Cannot publish note:', error);
setSharesState('idle');
- alert(JoplinServerApi.connectionErrorMessage(error));
+ void shim.showErrorDialog(JoplinServerApi.connectionErrorMessage(error));
}
break;
diff --git a/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx b/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx
index f9fac470e21..bbdb3b37650 100644
--- a/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx
+++ b/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx
@@ -31,6 +31,7 @@ import HeaderItem from '../listItemComponents/HeaderItem';
import AllNotesItem from '../listItemComponents/AllNotesItem';
import ListItemWrapper from '../listItemComponents/ListItemWrapper';
import { focus } from '@joplin/lib/utils/focusHandler';
+import shim from '@joplin/lib/shim';
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
@@ -309,7 +310,7 @@ const useOnRenderItem = (props: Props) => {
}
} catch (error) {
logger.error(error);
- alert(error.message);
+ await shim.showErrorDialog(error.message);
}
}, []);
diff --git a/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.ts
index 7b8ac1e2b5b..6d964b296cb 100644
--- a/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.ts
+++ b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.ts
@@ -2,6 +2,7 @@ import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/
import { _ } from '@joplin/lib/locale';
import ShareService from '@joplin/lib/services/share/ShareService';
import Logger from '@joplin/utils/Logger';
+import shim from '@joplin/lib/shim';
const logger = Logger.create('leaveSharedFolder');
@@ -13,7 +14,7 @@ export const declaration: CommandDeclaration = {
export const runtime = (): CommandRuntime => {
return {
execute: async (_context: CommandContext, folderId: string = null) => {
- const answer = confirm(_('This will remove the notebook from your collection and you will no longer have access to its content. Do you wish to continue?'));
+ const answer = await shim.showConfirmationDialog(_('This will remove the notebook from your collection and you will no longer have access to its content. Do you wish to continue?'));
if (!answer) return;
try {
@@ -28,7 +29,7 @@ export const runtime = (): CommandRuntime => {
await ShareService.instance().leaveSharedFolder(folderId, share.user.id);
} catch (error) {
logger.error(error);
- alert(_('Error: %s', error.message));
+ await shim.showErrorDialog(_('Error: %s', error.message));
}
},
enabledCondition: 'joplinServerConnected && folderIsShareRootAndNotOwnedByUser',
diff --git a/packages/app-desktop/package.json b/packages/app-desktop/package.json
index 1b74568ddff..cfc6bf514a7 100644
--- a/packages/app-desktop/package.json
+++ b/packages/app-desktop/package.json
@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
- "version": "3.2.7",
+ "version": "3.2.10",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
diff --git a/packages/app-mobile/android/app/build.gradle b/packages/app-mobile/android/app/build.gradle
index 85944326ed6..360d8b599f5 100644
--- a/packages/app-mobile/android/app/build.gradle
+++ b/packages/app-mobile/android/app/build.gradle
@@ -79,8 +79,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 2097760
- versionName "3.2.4"
+ versionCode 2097761
+ versionName "3.2.5"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}
diff --git a/packages/app-mobile/commands/openItem.ts b/packages/app-mobile/commands/openItem.ts
index 65e4812b48d..33b55aa501c 100644
--- a/packages/app-mobile/commands/openItem.ts
+++ b/packages/app-mobile/commands/openItem.ts
@@ -8,6 +8,7 @@ import BaseItem from '@joplin/lib/models/BaseItem';
import { BaseItemEntity } from '@joplin/lib/services/database/types';
import { ModelType } from '@joplin/lib/BaseModel';
import showResource from './util/showResource';
+import { isCallbackUrl, parseCallbackUrl } from '@joplin/lib/callbackUrlUtils';
const logger = Logger.create('openItemCommand');
@@ -15,32 +16,48 @@ export const declaration: CommandDeclaration = {
name: 'openItem',
};
+const openItemById = async (itemId: string, hash?: string) => {
+ logger.info(`Navigating to item ${itemId}`);
+ const item: BaseItemEntity = await BaseItem.loadItemById(itemId);
+
+ if (item.type_ === ModelType.Note) {
+ await goToNote(itemId, hash);
+ } else if (item.type_ === ModelType.Resource) {
+ await showResource(item);
+ } else {
+ throw new Error(`Unsupported item type for links: ${item.type_}`);
+ }
+};
+
export const runtime = (): CommandRuntime => {
return {
execute: async (_context: CommandContext, link: string) => {
if (!link) throw new Error('Link cannot be empty');
- if (link.startsWith('joplin://') || link.startsWith(':/')) {
- const parsedUrl = parseResourceUrl(link);
- if (parsedUrl) {
- const { itemId, hash } = parsedUrl;
+ try {
+ if (link.startsWith('joplin://') || link.startsWith(':/')) {
+ const parsedResourceUrl = parseResourceUrl(link);
+ const parsedCallbackUrl = isCallbackUrl(link) ? parseCallbackUrl(link) : null;
- logger.info(`Navigating to item ${itemId}`);
- const item: BaseItemEntity = await BaseItem.loadItemById(itemId);
- if (item.type_ === ModelType.Note) {
- await goToNote(itemId, hash);
- } else if (item.type_ === ModelType.Resource) {
- await showResource(item);
+ if (parsedResourceUrl) {
+ const { itemId, hash } = parsedResourceUrl;
+ await openItemById(itemId, hash);
+ } else if (parsedCallbackUrl) {
+ const id = parsedCallbackUrl.params.id;
+ if (!id) {
+ throw new Error('Missing item ID');
+ }
+ await openItemById(id);
} else {
- logger.error('Unsupported item type for links:', item.type_);
+ throw new Error('Unsupported link format.');
}
+ } else if (urlProtocol(link)) {
+ shim.openUrl(link);
} else {
- logger.error(`Invalid Joplin link: ${link}`);
+ throw new Error('Unsupported protocol');
}
- } else if (urlProtocol(link)) {
- shim.openUrl(link);
- } else {
- const errorMessage = _('Unsupported link or message: %s', link);
+ } catch (error) {
+ const errorMessage = _('Unsupported link or message: %s.\nError: %s', link, error);
logger.error(errorMessage);
await shim.showErrorDialog(errorMessage);
}
diff --git a/packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.ts b/packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.ts
index 29d89a1b6dd..a5acab20a91 100644
--- a/packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.ts
+++ b/packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.ts
@@ -7,6 +7,8 @@ import watchEditorForTemplateChanges from './watchEditorForTemplateChanges';
import { ImageEditorCallbacks, ImageEditorControl, LocalizedStrings } from './types';
import startAutosaveLoop from './startAutosaveLoop';
import WebViewToRNMessenger from '../../../../utils/ipc/WebViewToRNMessenger';
+import './polyfills';
+
const restoreToolbarState = (toolbar: AbstractToolbar, state: string) => {
if (state) {
diff --git a/packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/polyfills.ts b/packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/polyfills.ts
new file mode 100644
index 00000000000..ee4d188bcd0
--- /dev/null
+++ b/packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/polyfills.ts
@@ -0,0 +1,11 @@
+// .replaceChildren is not supported in Chromium 83, which is the default for Android 11
+// (unless auto-updated from the Google Play store).
+HTMLElement.prototype.replaceChildren ??= function(this: HTMLElement, ...nodes: Node[]) {
+ while (this.children.length) {
+ this.children[0].remove();
+ }
+
+ for (const node of nodes) {
+ this.appendChild(node);
+ }
+};
diff --git a/packages/app-mobile/components/plugins/PluginRunnerWebView.tsx b/packages/app-mobile/components/plugins/PluginRunnerWebView.tsx
index 14a72c5deb3..c8edf4343fe 100644
--- a/packages/app-mobile/components/plugins/PluginRunnerWebView.tsx
+++ b/packages/app-mobile/components/plugins/PluginRunnerWebView.tsx
@@ -15,6 +15,7 @@ import { AppState } from '../../utils/types';
import usePrevious from '@joplin/lib/hooks/usePrevious';
import PlatformImplementation from '../../services/plugins/PlatformImplementation';
import AccessibleView from '../accessibility/AccessibleView';
+import useOnDevPluginsUpdated from './utils/useOnDevPluginsUpdated';
const logger = Logger.create('PluginRunnerWebView');
@@ -29,20 +30,33 @@ const usePlugins = (
pluginRunner: PluginRunner,
webviewLoaded: boolean,
pluginSettings: PluginSettings,
+ pluginSupportEnabled: boolean,
+ devPluginPath: string,
) => {
const store = useStore();
const lastPluginRunner = usePrevious(pluginRunner);
+ const [reloadCounter, setReloadCounter] = useState(0);
// Only set reloadAll to true here -- this ensures that all plugins are reloaded,
// even if loadPlugins is cancelled and re-run.
const reloadAllRef = useRef(false);
reloadAllRef.current ||= pluginRunner !== lastPluginRunner;
+ useOnDevPluginsUpdated(async (pluginId: string) => {
+ logger.info(`Dev plugin ${pluginId} updated. Reloading...`);
+ await PluginService.instance().unloadPlugin(pluginId);
+ setReloadCounter(counter => counter + 1);
+ }, devPluginPath, pluginSupportEnabled);
+
useAsyncEffect(async (event) => {
if (!webviewLoaded) {
return;
}
+ if (reloadCounter > 0) {
+ logger.debug('Reloading with counter set to', reloadCounter);
+ }
+
await loadPlugins({
pluginRunner,
pluginSettings,
@@ -56,7 +70,7 @@ const usePlugins = (
if (!event.cancelled) {
reloadAllRef.current = false;
}
- }, [pluginRunner, store, webviewLoaded, pluginSettings]);
+ }, [pluginRunner, store, webviewLoaded, pluginSettings, reloadCounter]);
};
const useUnloadPluginsOnGlobalDisable = (
@@ -79,6 +93,7 @@ interface Props {
serializedPluginSettings: SerializedPluginSettings;
pluginSupportEnabled: boolean;
pluginStates: PluginStates;
+ devPluginPath: string;
pluginHtmlContents: PluginHtmlContents;
themeId: number;
}
@@ -98,7 +113,7 @@ const PluginRunnerWebViewComponent: React.FC = props => {
}, [webviewReloadCounter]);
const pluginSettings = usePluginSettings(props.serializedPluginSettings);
- usePlugins(pluginRunner, webviewLoaded, pluginSettings);
+ usePlugins(pluginRunner, webviewLoaded, pluginSettings, props.pluginSupportEnabled, props.devPluginPath);
useUnloadPluginsOnGlobalDisable(props.pluginStates, props.pluginSupportEnabled);
const onLoadStart = useCallback(() => {
@@ -183,6 +198,7 @@ export default connect((state: AppState) => {
const result: Props = {
serializedPluginSettings: state.settings['plugins.states'],
pluginSupportEnabled: state.settings['plugins.pluginSupportEnabled'],
+ devPluginPath: state.settings['plugins.devPluginPaths'],
pluginStates: state.pluginService.plugins,
pluginHtmlContents: state.pluginService.pluginHtmlContents,
themeId: state.settings.theme,
diff --git a/packages/app-mobile/components/plugins/utils/useOnDevPluginsUpdated.ts b/packages/app-mobile/components/plugins/utils/useOnDevPluginsUpdated.ts
new file mode 100644
index 00000000000..f1ad164bdd0
--- /dev/null
+++ b/packages/app-mobile/components/plugins/utils/useOnDevPluginsUpdated.ts
@@ -0,0 +1,60 @@
+import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
+import shim from '@joplin/lib/shim';
+import time from '@joplin/lib/time';
+import { basename, join } from 'path';
+import { useRef } from 'react';
+
+type OnDevPluginChange = (id: string)=> void;
+
+const useOnDevPluginsUpdated = (onDevPluginChange: OnDevPluginChange, devPluginPath: string, pluginSupportEnabled: boolean) => {
+ const onDevPluginChangeRef = useRef(onDevPluginChange);
+ onDevPluginChangeRef.current = onDevPluginChange;
+ const isFirstUpdateRef = useRef(true);
+
+ useAsyncEffect(async (event) => {
+ if (!devPluginPath || !pluginSupportEnabled) return;
+
+ const itemToLastModTime = new Map();
+
+ // publishPath should point to the publish/ subfolder of a plugin's development
+ // directory.
+ const checkPluginChange = async (pluginPublishPath: string) => {
+ const dirStats = await shim.fsDriver().readDirStats(pluginPublishPath);
+ let hasChange = false;
+ let changedPluginId = '';
+ for (const item of dirStats) {
+ if (item.path.endsWith('.jpl')) {
+ const lastModTime = itemToLastModTime.get(item.path);
+ const modTime = item.mtime.getTime();
+ if (lastModTime === undefined || lastModTime < modTime) {
+ itemToLastModTime.set(item.path, modTime);
+ hasChange = true;
+ changedPluginId = basename(item.path, '.jpl');
+ break;
+ }
+ }
+ }
+
+ if (hasChange) {
+ if (isFirstUpdateRef.current) {
+ // Avoid sending an event the first time the hook is called. The first iteration
+ // collects initial timestamp information. In that case, hasChange
+ // will always be true, even with no plugin reload.
+ isFirstUpdateRef.current = false;
+ } else {
+ onDevPluginChangeRef.current(changedPluginId);
+ }
+ }
+ };
+
+ while (!event.cancelled) {
+ const publishFolder = join(devPluginPath, 'publish');
+ await checkPluginChange(publishFolder);
+
+ const pollingIntervalSeconds = 5;
+ await time.sleep(pollingIntervalSeconds);
+ }
+ }, [devPluginPath, pluginSupportEnabled]);
+};
+
+export default useOnDevPluginsUpdated;
diff --git a/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx b/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx
index 4539d43cca3..1c849479033 100644
--- a/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx
+++ b/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { Platform, Linking, View, Switch, ScrollView, Text, TouchableOpacity, Alert, PermissionsAndroid, Dimensions, AccessibilityInfo } from 'react-native';
+import { Platform, Linking, View, ScrollView, Text, TouchableOpacity, Alert, PermissionsAndroid, Dimensions, AccessibilityInfo } from 'react-native';
import Setting, { AppType, SettingMetadataSection } from '@joplin/lib/models/Setting';
import NavService from '@joplin/lib/services/NavService';
import SearchEngine from '@joplin/lib/services/search/SearchEngine';
@@ -12,7 +12,6 @@ import { connect } from 'react-redux';
import ScreenHeader from '../../ScreenHeader';
import { _ } from '@joplin/lib/locale';
import BaseScreenComponent from '../../base-screen';
-import { themeStyle } from '../../global-style';
import * as shared from '@joplin/lib/components/shared/config/config-shared';
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
import biometricAuthenticate from '../../biometrics/biometricAuthenticate';
@@ -36,6 +35,8 @@ import EnablePluginSupportPage from './plugins/EnablePluginSupportPage';
import getVersionInfoText from '../../../utils/getVersionInfoText';
import JoplinCloudConfig, { emailToNoteDescription, emailToNoteLabel } from './JoplinCloudConfig';
import shim from '@joplin/lib/shim';
+import SettingsToggle from './SettingsToggle';
+import { UpdateSettingValueCallback } from './types';
interface ConfigScreenState {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@@ -673,22 +674,16 @@ class ConfigScreenComponent extends BaseScreenComponent
-
-
- {label}
-
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied */}
- void updateSettingValue(key, value)} />
-
- {descriptionComp}
-
- );
+ private renderToggle(key: string, label: string, value: unknown, updateSettingValue: UpdateSettingValueCallback) {
+ return ;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
diff --git a/packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.tsx b/packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.tsx
index 675216b09ab..1bd64af243a 100644
--- a/packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.tsx
+++ b/packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.tsx
@@ -3,18 +3,23 @@ import * as React from 'react';
import shim from '@joplin/lib/shim';
import { FunctionComponent, useCallback, useEffect, useState } from 'react';
import { ConfigScreenStyles } from './configScreenStyles';
-import { View, Text } from 'react-native';
+import { View, Text, StyleSheet } from 'react-native';
import Setting, { SettingItem } from '@joplin/lib/models/Setting';
import { openDocumentTree } from '@joplin/react-native-saf-x';
import { UpdateSettingValueCallback } from './types';
import { reg } from '@joplin/lib/registry';
import type FsDriverWeb from '../../../utils/fs-driver/fs-driver-rn.web';
-import { TouchableRipple } from 'react-native-paper';
+import { IconButton, TouchableRipple } from 'react-native-paper';
+import { _ } from '@joplin/lib/locale';
+
+type Mode = 'read'|'readwrite';
interface Props {
+ themeId: number;
styles: ConfigScreenStyles;
settingMetadata: SettingItem;
- mode: 'read'|'readwrite';
+ mode: Mode;
+ description: React.ReactNode|null;
updateSettingValue: UpdateSettingValueCallback;
}
@@ -23,30 +28,28 @@ type ExtendedSelf = (typeof window.self) & {
};
declare const self: ExtendedSelf;
-const FileSystemPathSelector: FunctionComponent = props => {
+const useFileSystemPath = (settingId: string, updateSettingValue: UpdateSettingValueCallback, accessMode: Mode) => {
const [fileSystemPath, setFileSystemPath] = useState('');
- const settingId = props.settingMetadata.key;
-
useEffect(() => {
setFileSystemPath(Setting.value(settingId));
}, [settingId]);
- const selectDirectoryButtonPress = useCallback(async () => {
+ const showDirectoryPicker = useCallback(async () => {
if (shim.mobilePlatform() === 'web') {
// Directory picker IDs can't include certain characters.
const pickerId = `setting-${settingId}`.replace(/[^a-zA-Z]/g, '_');
- const handle = await self.showDirectoryPicker({ id: pickerId, mode: props.mode });
+ const handle = await self.showDirectoryPicker({ id: pickerId, mode: accessMode });
const fsDriver = shim.fsDriver() as FsDriverWeb;
- const uri = await fsDriver.mountExternalDirectory(handle, pickerId, props.mode);
- await props.updateSettingValue(settingId, uri);
+ const uri = await fsDriver.mountExternalDirectory(handle, pickerId, accessMode);
+ await updateSettingValue(settingId, uri);
setFileSystemPath(uri);
} else {
try {
const doc = await openDocumentTree(true);
if (doc?.uri) {
setFileSystemPath(doc.uri);
- await props.updateSettingValue(settingId, doc.uri);
+ await updateSettingValue(settingId, doc.uri);
} else {
throw new Error('User cancelled operation');
}
@@ -54,32 +57,78 @@ const FileSystemPathSelector: FunctionComponent = props => {
reg.logger().info('Didn\'t pick sync dir: ', e);
}
}
- }, [props.updateSettingValue, settingId, props.mode]);
+ }, [updateSettingValue, settingId, accessMode]);
+
+ const clearPath = useCallback(() => {
+ setFileSystemPath('');
+ void updateSettingValue(settingId, '');
+ }, [updateSettingValue, settingId]);
// Supported on Android and some versions of Chrome
const supported = shim.fsDriver().isUsingAndroidSAF() || (shim.mobilePlatform() === 'web' && 'showDirectoryPicker' in self);
- if (!supported) {
- return null;
- }
+
+ return { clearPath, showDirectoryPicker, fileSystemPath, supported };
+};
+
+const pathSelectorStyles = StyleSheet.create({
+ innerContainer: {
+ paddingTop: 0,
+ paddingBottom: 0,
+ paddingLeft: 0,
+ paddingRight: 0,
+ },
+ mainButton: {
+ flexGrow: 1,
+ flexShrink: 1,
+ paddingHorizontal: 16,
+ paddingVertical: 22,
+ margin: 0,
+ },
+ buttonContent: {
+ flexDirection: 'row',
+ },
+});
+
+const FileSystemPathSelector: FunctionComponent = props => {
+ const settingId = props.settingMetadata.key;
+ const { clearPath, showDirectoryPicker, fileSystemPath, supported } = useFileSystemPath(settingId, props.updateSettingValue, props.mode);
const styleSheet = props.styles.styleSheet;
- return (
+ const clearButton = (
+
+ );
+
+ const containerStyles = props.styles.getContainerStyle(!!props.description);
+
+ const control =
-
+
{props.settingMetadata.label()}
-
+
{fileSystemPath}
- );
+ {fileSystemPath ? clearButton : null}
+ ;
+
+ if (!supported) return null;
+
+ return
+ {control}
+ {props.description}
+ ;
};
export default FileSystemPathSelector;
diff --git a/packages/app-mobile/components/screens/ConfigScreen/SettingComponent.tsx b/packages/app-mobile/components/screens/ConfigScreen/SettingComponent.tsx
index 74cd66e2b06..748ffdaace1 100644
--- a/packages/app-mobile/components/screens/ConfigScreen/SettingComponent.tsx
+++ b/packages/app-mobile/components/screens/ConfigScreen/SettingComponent.tsx
@@ -38,7 +38,7 @@ const SettingComponent: React.FunctionComponent = props => {
const styleSheet = props.styles.styleSheet;
const descriptionComp = !settingDescription ? null : {settingDescription};
- const containerStyle = props.styles.getContainerStyle(!!settingDescription);
+ const containerStyles = props.styles.getContainerStyle(!!settingDescription);
const labelId = useId();
@@ -49,8 +49,8 @@ const SettingComponent: React.FunctionComponent = props => {
const label = md.label();
return (
-
-
+
+
{label}
@@ -125,17 +125,19 @@ const SettingComponent: React.FunctionComponent = props => {
if (['sync.2.path', 'plugins.devPluginPaths'].includes(md.key) && (shim.fsDriver().isUsingAndroidSAF() || shim.mobilePlatform() === 'web')) {
return (
);
}
return (
-
-
+
+
{md.label()}
diff --git a/packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.tsx b/packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.tsx
index a9c4f38cf2b..ebace0634b5 100644
--- a/packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.tsx
+++ b/packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.tsx
@@ -24,9 +24,11 @@ const SettingsToggle: FunctionComponent = props => {
const theme = themeStyle(props.themeId);
const styleSheet = props.styles.styleSheet;
+ const containerStyles = props.styles.getContainerStyle(!!props.description);
+
return (
-
-
+
+
{props.label}
diff --git a/packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.ts b/packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.ts
index 2ddff9b9b9f..b243c7c091d 100644
--- a/packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.ts
+++ b/packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.ts
@@ -6,8 +6,11 @@ type SidebarButtonStyle = ViewStyle & { height: number };
export interface ConfigScreenStyleSheet {
body: ViewStyle;
+ settingOuterContainer: ViewStyle;
+ settingOuterContainerNoBorder: ViewStyle;
settingContainer: ViewStyle;
settingContainerNoBottomBorder: ViewStyle;
+
headerWrapperStyle: ViewStyle;
headerTextStyle: TextStyle;
@@ -39,12 +42,17 @@ export interface ConfigScreenStyleSheet {
settingControl: TextStyle;
}
+interface ContainerStyles {
+ outerContainer: ViewStyle;
+ innerContainer: ViewStyle;
+}
+
export interface ConfigScreenStyles {
styleSheet: ConfigScreenStyleSheet;
selectedSectionButtonColor: string;
keyboardAppearance: 'default'|'light'|'dark';
- getContainerStyle(hasDescription: boolean): ViewStyle;
+ getContainerStyle(hasDescription: boolean): ContainerStyles;
}
const configScreenStyles = (themeId: number): ConfigScreenStyles => {
@@ -107,6 +115,14 @@ const configScreenStyles = (themeId: number): ConfigScreenStyles => {
justifyContent: 'flex-start',
flexDirection: 'column',
},
+ settingOuterContainer: {
+ flexDirection: 'column',
+ borderBottomWidth: 1,
+ borderBottomColor: theme.dividerColor,
+ },
+ settingOuterContainerNoBorder: {
+ flexDirection: 'column',
+ },
settingContainer: settingContainerStyle,
settingContainerNoBottomBorder: {
...settingContainerStyle,
@@ -229,7 +245,9 @@ const configScreenStyles = (themeId: number): ConfigScreenStyles => {
selectedSectionButtonColor: theme.selectedColor,
keyboardAppearance: theme.keyboardAppearance,
getContainerStyle: (hasDescription) => {
- return !hasDescription ? styleSheet.settingContainer : styleSheet.settingContainerNoBottomBorder;
+ const outerContainer = hasDescription ? styleSheet.settingOuterContainer : styleSheet.settingOuterContainerNoBorder;
+ const innerContainer = hasDescription ? styleSheet.settingContainerNoBottomBorder : styleSheet.settingContainer;
+ return { outerContainer, innerContainer };
},
};
};
diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx
index 9002609d350..cd4628030ca 100644
--- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx
+++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx
@@ -92,12 +92,20 @@ const PluginChips: React.FC = props => {
return {_('Installed')};
};
+ const renderDevChip = () => {
+ if (!item.devMode) {
+ return null;
+ }
+ return {_('Dev')};
+ };
+
return
{renderIncompatibleChip()}
{renderInstalledChip()}
{renderErrorsChip()}
{renderBuiltInChip()}
{renderUpdatableChip()}
+ {renderDevChip()}
{renderDisabledChip()}
;
};
diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx
index ae6869e7353..dce8550d201 100644
--- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx
+++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx
@@ -203,7 +203,7 @@ const PluginInfoModalContent: React.FC = props => {
item={item}
type={ButtonType.Delete}
onPress={props.pluginCallbacks.onDelete}
- disabled={item.builtIn || (item?.deleted ?? true)}
+ disabled={item.builtIn || item.devMode || (item?.deleted ?? true)}
title={item?.deleted ? _('Deleted') : _('Delete')}
/>
);
diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.tsx
index 1b46bf0fec7..0d59ac68b99 100644
--- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.tsx
+++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.tsx
@@ -91,7 +91,7 @@ const PluginUploadButton: React.FC = props => {
}, [props.pluginSettings, props.updatePluginStates]);
return (
-
+
Promise;
+export type UpdateSettingValueCallback = (key: string, value: any)=> void|Promise;
export interface PluginStatusRecord {
[pluginId: string]: boolean;
diff --git a/packages/app-mobile/components/screens/Note/Note.test.tsx b/packages/app-mobile/components/screens/Note/Note.test.tsx
index cdd7f996734..3a1e209453f 100644
--- a/packages/app-mobile/components/screens/Note/Note.test.tsx
+++ b/packages/app-mobile/components/screens/Note/Note.test.tsx
@@ -29,6 +29,8 @@ import TestProviderStack from '../../testing/TestProviderStack';
import setupGlobalStore from '../../../utils/testing/setupGlobalStore';
import CommandService from '@joplin/lib/services/CommandService';
+jest.retryTimes(2);
+
interface WrapperProps {
}
diff --git a/packages/app-mobile/ios/Joplin.xcodeproj/project.pbxproj b/packages/app-mobile/ios/Joplin.xcodeproj/project.pbxproj
index b4302026c23..01bd11c5c27 100644
--- a/packages/app-mobile/ios/Joplin.xcodeproj/project.pbxproj
+++ b/packages/app-mobile/ios/Joplin.xcodeproj/project.pbxproj
@@ -535,13 +535,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
- CURRENT_PROJECT_VERSION = 130;
+ CURRENT_PROJECT_VERSION = 132;
DEVELOPMENT_TEAM = A9BXAFS6CT;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
- MARKETING_VERSION = 13.2.2;
+ MARKETING_VERSION = 13.2.4;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -567,12 +567,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
- CURRENT_PROJECT_VERSION = 130;
+ CURRENT_PROJECT_VERSION = 132;
DEVELOPMENT_TEAM = A9BXAFS6CT;
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
- MARKETING_VERSION = 13.2.2;
+ MARKETING_VERSION = 13.2.4;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -758,14 +758,14 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 130;
+ CURRENT_PROJECT_VERSION = 132;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
- MARKETING_VERSION = 13.2.2;
+ MARKETING_VERSION = 13.2.4;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
@@ -797,14 +797,14 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 130;
+ CURRENT_PROJECT_VERSION = 132;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
- MARKETING_VERSION = 13.2.2;
+ MARKETING_VERSION = 13.2.4;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
"$(inherited)",
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/Contents.json b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/Contents.json
index b4aeea0a7b5..523aa8211b7 100755
--- a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/Contents.json
+++ b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/Contents.json
@@ -1,37 +1,116 @@
{
- "images" : [
- {
- "filename" : "ios_marketing1024x1024.png",
- "idiom" : "universal",
- "platform" : "ios",
- "size" : "1024x1024"
- },
- {
- "appearances" : [
- {
- "appearance" : "luminosity",
- "value" : "dark"
- }
- ],
- "filename" : "ios_marketing_dark1024x1024.png",
- "idiom" : "universal",
- "platform" : "ios",
- "size" : "1024x1024"
- },
- {
- "appearances" : [
- {
- "appearance" : "luminosity",
- "value" : "tinted"
- }
- ],
- "idiom" : "universal",
- "platform" : "ios",
- "size" : "1024x1024"
+ "images": [
+ {
+ "filename": "ios_marketing1024x1024.png",
+ "idiom": "ios-marketing",
+ "size": "1024x1024",
+ "scale": "1x"
+ },
+ {
+ "filename": "iphone_notification20x20@2x.png",
+ "idiom": "iphone",
+ "size": "20x20",
+ "scale": "2x"
+ },
+ {
+ "filename": "iphone_notification20x20@3x.png",
+ "idiom": "iphone",
+ "size": "20x20",
+ "scale": "3x"
+ },
+ {
+ "filename": "iphone_settings29x29@2x.png",
+ "idiom": "iphone",
+ "size": "29x29",
+ "scale": "2x"
+ },
+ {
+ "filename": "iphone_settings29x29@3x.png",
+ "idiom": "iphone",
+ "size": "29x29",
+ "scale": "3x"
+ },
+ {
+ "filename": "iphone_spotlight40x40@2x.png",
+ "idiom": "iphone",
+ "size": "40x40",
+ "scale": "2x"
+ },
+ {
+ "filename": "iphone_spotlight40x40@3x.png",
+ "idiom": "iphone",
+ "size": "40x40",
+ "scale": "3x"
+ },
+ {
+ "filename": "iphone_app60x60@2x.png",
+ "idiom": "iphone",
+ "size": "60x60",
+ "scale": "2x"
+ },
+ {
+ "filename": "iphone_app60x60@3x.png",
+ "idiom": "iphone",
+ "size": "60x60",
+ "scale": "3x"
+ },
+ {
+ "filename": "ipad_notification20x20.png",
+ "idiom": "ipad",
+ "size": "20x20",
+ "scale": "1x"
+ },
+ {
+ "filename": "ipad_notification20x20@2x.png",
+ "idiom": "ipad",
+ "size": "20x20",
+ "scale": "2x"
+ },
+ {
+ "filename": "ipad_settings29x29.png",
+ "idiom": "ipad",
+ "size": "29x29",
+ "scale": "1x"
+ },
+ {
+ "filename": "ipad_settings29x29@2x.png",
+ "idiom": "ipad",
+ "size": "29x29",
+ "scale": "2x"
+ },
+ {
+ "filename": "ipad_spotlight40x40.png",
+ "idiom": "ipad",
+ "size": "40x40",
+ "scale": "1x"
+ },
+ {
+ "filename": "ipad_spotlight40x40@2x.png",
+ "idiom": "ipad",
+ "size": "40x40",
+ "scale": "2x"
+ },
+ {
+ "filename": "ipad_app76x76.png",
+ "idiom": "ipad",
+ "size": "76x76",
+ "scale": "1x"
+ },
+ {
+ "filename": "ipad_app76x76@2x.png",
+ "idiom": "ipad",
+ "size": "76x76",
+ "scale": "2x"
+ },
+ {
+ "filename": "ipad_pro_app83.5x83.5@2x.png",
+ "idiom": "ipad",
+ "size": "83.5x83.5",
+ "scale": "2x"
}
],
- "info" : {
- "author" : "xcode",
- "version" : 1
+ "info": {
+ "version": 1,
+ "author": "xcode"
}
-}
+}
\ No newline at end of file
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ios_marketing1024x1024.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ios_marketing1024x1024.png
old mode 100644
new mode 100755
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ios_marketing_dark1024x1024.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ios_marketing_dark1024x1024.png
deleted file mode 100644
index 543f02ed5a4..00000000000
Binary files a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ios_marketing_dark1024x1024.png and /dev/null differ
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_app76x76.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_app76x76.png
new file mode 100755
index 00000000000..f7556cf2475
Binary files /dev/null and b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_app76x76.png differ
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_app76x76@2x.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_app76x76@2x.png
new file mode 100755
index 00000000000..bae6a354aee
Binary files /dev/null and b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_app76x76@2x.png differ
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_notification20x20.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_notification20x20.png
new file mode 100755
index 00000000000..5c818e8470e
Binary files /dev/null and b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_notification20x20.png differ
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_notification20x20@2x.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_notification20x20@2x.png
new file mode 100755
index 00000000000..c4b9e4c9e18
Binary files /dev/null and b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_notification20x20@2x.png differ
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_pro_app83.5x83.5@2x.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_pro_app83.5x83.5@2x.png
new file mode 100755
index 00000000000..9ea41ab0a17
Binary files /dev/null and b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_pro_app83.5x83.5@2x.png differ
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_settings29x29.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_settings29x29.png
new file mode 100755
index 00000000000..957a5b4d203
Binary files /dev/null and b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_settings29x29.png differ
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_settings29x29@2x.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_settings29x29@2x.png
new file mode 100755
index 00000000000..bed468976e4
Binary files /dev/null and b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_settings29x29@2x.png differ
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_spotlight40x40.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_spotlight40x40.png
new file mode 100755
index 00000000000..c4b9e4c9e18
Binary files /dev/null and b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_spotlight40x40.png differ
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_spotlight40x40@2x.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_spotlight40x40@2x.png
new file mode 100755
index 00000000000..2650ef5a5e3
Binary files /dev/null and b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ipad_spotlight40x40@2x.png differ
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_app60x60@2x.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_app60x60@2x.png
new file mode 100755
index 00000000000..2241e825657
Binary files /dev/null and b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_app60x60@2x.png differ
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_app60x60@3x.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_app60x60@3x.png
new file mode 100755
index 00000000000..fb2f9c48fdd
Binary files /dev/null and b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_app60x60@3x.png differ
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_notification20x20@2x.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_notification20x20@2x.png
new file mode 100755
index 00000000000..c4b9e4c9e18
Binary files /dev/null and b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_notification20x20@2x.png differ
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_notification20x20@3x.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_notification20x20@3x.png
new file mode 100755
index 00000000000..6d053189d72
Binary files /dev/null and b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_notification20x20@3x.png differ
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_settings29x29@2x.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_settings29x29@2x.png
new file mode 100755
index 00000000000..bed468976e4
Binary files /dev/null and b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_settings29x29@2x.png differ
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_settings29x29@3x.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_settings29x29@3x.png
new file mode 100755
index 00000000000..ae72ab5e076
Binary files /dev/null and b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_settings29x29@3x.png differ
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_spotlight40x40@2x.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_spotlight40x40@2x.png
new file mode 100755
index 00000000000..2650ef5a5e3
Binary files /dev/null and b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_spotlight40x40@2x.png differ
diff --git a/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_spotlight40x40@3x.png b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_spotlight40x40@3x.png
new file mode 100755
index 00000000000..2241e825657
Binary files /dev/null and b/packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/iphone_spotlight40x40@3x.png differ
diff --git a/packages/app-mobile/ios/Podfile.lock b/packages/app-mobile/ios/Podfile.lock
index f4ef39d9211..235701c7b8f 100644
--- a/packages/app-mobile/ios/Podfile.lock
+++ b/packages/app-mobile/ios/Podfile.lock
@@ -1337,7 +1337,7 @@ PODS:
- React-utils (= 0.74.1)
- rn-fetch-blob (0.12.0):
- React-Core
- - RNCClipboard (1.14.1):
+ - RNCClipboard (1.14.2):
- React-Core
- RNCPushNotificationIOS (1.11.0):
- React-Core
@@ -1758,7 +1758,7 @@ SPEC CHECKSUMS:
React-utils: 3285151c9d1e3a28a9586571fc81d521678c196d
ReactCommon: f42444e384d82ab89184aed5d6f3142748b54768
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
- RNCClipboard: 0a720adef5ec193aa0e3de24c3977222c7e52a37
+ RNCClipboard: 5e503962f0719ace8f7fdfe9c60282b526305c85
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
RNDateTimePicker: 40ffda97d071a98a10fdca4fa97e3977102ccd14
RNDeviceInfo: 59344c19152c4b2b32283005f9737c5c64b42fba
diff --git a/packages/app-mobile/tools/buildInjectedJs/BundledFile.ts b/packages/app-mobile/tools/buildInjectedJs/BundledFile.ts
index 690744668a8..b99e38ac4d6 100644
--- a/packages/app-mobile/tools/buildInjectedJs/BundledFile.ts
+++ b/packages/app-mobile/tools/buildInjectedJs/BundledFile.ts
@@ -58,18 +58,18 @@ export default class BundledFile {
// Some libraries don't work with older browsers/WebViews.
// Because Babel transpilation can be slow, we only transpile
// these libraries.
- // For now, it's just Replit's CodeMirror-vim library. This library
- // uses `a?.b` syntax, which seems to be unsupported in iOS 12 Safari.
- const moduleNeedsTranspilation = !!(/.*node_modules.*replit.*\.[mc]?js$/.exec(value));
+ const moduleNeedsTranspilation = !!(
+ // Replit's CodeMirror-vim library uses a?.b syntax which seems to be unsupported in iOS 12 Safari.
+ /.*node_modules.*replit.*\.[mc]?js$/.exec(value) ||
+ // js-draw uses a ??= b syntax, which is unsupported in old Android WebView versions
+ /.*node_modules.*js-draw.*\.[mc]?js$/.exec(value)
+ );
if (isModuleFile && !moduleNeedsTranspilation) {
return false;
}
const isJsFile = !!(/\.[cm]?js$/.exec(value));
- if (isJsFile) {
- console.log('Compiling with Babel:', value);
- }
return isJsFile;
},
use: {
diff --git a/packages/default-plugins/pluginRepositories.json b/packages/default-plugins/pluginRepositories.json
index c868a7fdd17..932f84fba58 100644
--- a/packages/default-plugins/pluginRepositories.json
+++ b/packages/default-plugins/pluginRepositories.json
@@ -7,6 +7,6 @@
"io.github.personalizedrefrigerator.js-draw": {
"cloneUrl": "https://github.com/personalizedrefrigerator/joplin-plugin-freehand-drawing.git",
"branch": "main",
- "commit": "3e7eac96d10218728120ce81bee2eeffd5f8fdbb"
+ "commit": "9724793b4a6fb83346ff4f7c639af1e352bd7937"
}
}
diff --git a/packages/lib/Synchronizer.ts b/packages/lib/Synchronizer.ts
index afef7ac7574..22b19680174 100644
--- a/packages/lib/Synchronizer.ts
+++ b/packages/lib/Synchronizer.ts
@@ -337,8 +337,10 @@ export default class Synchronizer {
const hasActiveExclusiveLock = await hasActiveLock(locks, currentDate, this.lockHandler().lockTtl, LockType.Exclusive);
if (hasActiveExclusiveLock) return 'hasExclusiveLock';
- const hasActiveSyncLock = await hasActiveLock(locks, currentDate, this.lockHandler().lockTtl, LockType.Sync, this.lockClientType(), this.clientId_);
- if (!hasActiveSyncLock) return 'syncLockGone';
+ if (this.lockHandler().enabled) {
+ const hasActiveSyncLock = await hasActiveLock(locks, currentDate, this.lockHandler().lockTtl, LockType.Sync, this.lockClientType(), this.clientId_);
+ if (!hasActiveSyncLock) return 'syncLockGone';
+ }
return '';
}
diff --git a/packages/lib/models/settings/builtInMetadata.ts b/packages/lib/models/settings/builtInMetadata.ts
index 1d27109d7ba..7c9a3b8508c 100644
--- a/packages/lib/models/settings/builtInMetadata.ts
+++ b/packages/lib/models/settings/builtInMetadata.ts
@@ -929,9 +929,23 @@ const builtInMetadata = (Setting: typeof SettingType) => {
section: 'plugins',
public: true,
advanced: true,
- appTypes: [AppType.Desktop],
+ appTypes: [AppType.Desktop, AppType.Mobile],
+ // For now, development plugins are only enabled on desktop & web.
+ show: (settings) => {
+ if (shim.isElectron()) return true;
+ if (shim.mobilePlatform() !== 'web') return false;
+
+ const pluginSupportEnabled = settings['plugins.pluginSupportEnabled'];
+ return !!pluginSupportEnabled;
+ },
label: () => 'Development plugins',
- description: () => 'You may add multiple plugin paths, each separated by a comma. You will need to restart the application for the changes to take effect.',
+ description: () => {
+ if (shim.mobilePlatform()) {
+ return 'The path to a plugin\'s development directory. When the plugin is rebuilt, Joplin reloads the plugin automatically.';
+ } else {
+ return 'You may add multiple plugin paths, each separated by a comma. You will need to restart the application for the changes to take effect.';
+ }
+ },
storage: SettingStorage.File,
},
diff --git a/packages/lib/services/CommandService.ts b/packages/lib/services/CommandService.ts
index 5a1fba54e27..7fa9b026b74 100644
--- a/packages/lib/services/CommandService.ts
+++ b/packages/lib/services/CommandService.ts
@@ -209,6 +209,10 @@ export default class CommandService extends BaseService {
};
}
+ public unregisterDeclaration(name: string) {
+ delete this.commands_[name];
+ }
+
public registerRuntime(commandName: string, runtime: CommandRuntime, allowMultiple = false): RegisteredRuntime {
if (typeof commandName !== 'string') throw new Error(`Command name must be a string. Got: ${JSON.stringify(commandName)}`);
diff --git a/packages/lib/services/commands/ToolbarButtonUtils.ts b/packages/lib/services/commands/ToolbarButtonUtils.ts
index 554bb8b8832..a86a5766c84 100644
--- a/packages/lib/services/commands/ToolbarButtonUtils.ts
+++ b/packages/lib/services/commands/ToolbarButtonUtils.ts
@@ -48,22 +48,24 @@ export default class ToolbarButtonUtils {
private commandToToolbarButton(commandName: string, whenClauseContext: WhenClauseContext): ToolbarButtonInfo {
const newEnabled = this.service.isEnabled(commandName, whenClauseContext);
const newTitle = this.service.title(commandName);
+ const newIcon = this.service.iconName(commandName);
+ const newLabel = this.service.label(commandName);
if (
this.toolbarButtonCache_[commandName] &&
this.toolbarButtonCache_[commandName].info.enabled === newEnabled &&
- this.toolbarButtonCache_[commandName].info.title === newTitle
+ this.toolbarButtonCache_[commandName].info.title === newTitle &&
+ this.toolbarButtonCache_[commandName].info.iconName === newIcon &&
+ this.toolbarButtonCache_[commandName].info.tooltip === newLabel
) {
return this.toolbarButtonCache_[commandName].info;
}
- const command = this.service.commandByName(commandName, { runtimeMustBeRegistered: true });
-
const output: ToolbarButtonInfo = {
type: 'button',
name: commandName,
- tooltip: this.service.label(commandName),
- iconName: command.declaration.iconName,
+ tooltip: newLabel,
+ iconName: newIcon,
enabled: newEnabled,
onClick: async () => {
await this.service.execute(commandName);
diff --git a/packages/lib/services/interop/InteropService_Importer_Md.test.ts b/packages/lib/services/interop/InteropService_Importer_Md.test.ts
index e6fde6685c8..7b0f80f3710 100644
--- a/packages/lib/services/interop/InteropService_Importer_Md.test.ts
+++ b/packages/lib/services/interop/InteropService_Importer_Md.test.ts
@@ -195,4 +195,13 @@ describe('InteropService_Importer_Md', () => {
// The invalid image is imported as-is
expect(resource.title).toBe('invalid-image.jpg');
});
+
+ it('should not fail to import file that contains a malformed URI', async () => {
+ // The first implicit test is that the below call doesn't throw due to the malformed URI
+ const note = await importNote(`${supportDir}/test_notes/md/sample-malformed-uri.md`);
+ const itemIds = Note.linkedItemIds(note.body);
+ expect(itemIds.length).toBe(0);
+ // The malformed link is imported as-is
+ expect(note.body).toContain('![malformed link](https://malformed_uri/%E0%A4%A.jpg)');
+ });
});
diff --git a/packages/lib/services/interop/InteropService_Importer_Md.ts b/packages/lib/services/interop/InteropService_Importer_Md.ts
index 22ef8cf1f0a..892d0b744a6 100644
--- a/packages/lib/services/interop/InteropService_Importer_Md.ts
+++ b/packages/lib/services/interop/InteropService_Importer_Md.ts
@@ -110,11 +110,18 @@ export default class InteropService_Importer_Md extends InteropService_Importer_
const htmlLinks = htmlUtils.extractFileUrls(md);
const fileLinks = unique(markdownLinks.concat(htmlLinks));
for (const encodedLink of fileLinks) {
- const link = decodeURI(encodedLink);
+ let link = '';
+ try {
+ link = decodeURI(encodedLink);
+ } catch (error) {
+ // If the URI cannot be decoded, leave it as it is.
+ continue;
+ }
if (isDataUrl(link)) {
// Just leave it as it is. We could potentially import
// it as a resource but for now that's good enough.
+ continue;
} else {
// Handle anchor links appropriately
const trimmedLink = this.trimAnchorLink(link);
diff --git a/packages/lib/services/interop/InteropService_Importer_OneNote.test.ts b/packages/lib/services/interop/InteropService_Importer_OneNote.test.ts
index ce722377b4e..b63a8faa278 100644
--- a/packages/lib/services/interop/InteropService_Importer_OneNote.test.ts
+++ b/packages/lib/services/interop/InteropService_Importer_OneNote.test.ts
@@ -176,4 +176,35 @@ describe('InteropService_Importer_OneNote', () => {
BaseModel.setIdGenerator(originalIdGenerator);
});
+
+ skipIfNotCI('should remove hyperlink from title', async () => {
+ let idx = 0;
+ const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
+ const notes = await importNote(`${supportDir}/onenote/remove_hyperlink_on_title.zip`);
+
+ for (const note of notes) {
+ expect(note.body).toMatchSnapshot(note.title);
+ }
+ BaseModel.setIdGenerator(originalIdGenerator);
+ });
+
+ skipIfNotCI('should group link parts even if they have different css styles', async () => {
+ const notes = await importNote(`${supportDir}/onenote/remove_hyperlink_on_title.zip`);
+
+ const noteToTest = notes.find(n => n.title === 'Tips from a Pro Using Trees for Dramatic Landscape Photography');
+
+ expect(noteToTest).toBeTruthy();
+ expect(noteToTest.body.includes('Tips from a Pro: Using Trees for Dramatic Landscape Photography')).toBe(true);
+ });
+
+ skipIfNotCI('should render links properly by ignoring wrongly set indices when the first character is a hyperlink marker', async () => {
+ let idx = 0;
+ const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
+ const notes = await importNote(`${supportDir}/onenote/hyperlink_marker_as_first_character.zip`);
+
+ for (const note of notes) {
+ expect(note.body).toMatchSnapshot(note.title);
+ }
+ BaseModel.setIdGenerator(originalIdGenerator);
+ });
});
diff --git a/packages/lib/services/interop/__snapshots__/InteropService_Importer_OneNote.test.js.snap b/packages/lib/services/interop/__snapshots__/InteropService_Importer_OneNote.test.js.snap
index a70037a0b3e..c0fe8f27389 100644
--- a/packages/lib/services/interop/__snapshots__/InteropService_Importer_OneNote.test.js.snap
+++ b/packages/lib/services/interop/__snapshots__/InteropService_Importer_OneNote.test.js.snap
@@ -766,3 +766,404 @@ exports[`InteropService_Importer_OneNote should import a simple OneNote notebook