1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-02-01 07:49:31 +02:00

Compare commits

..

4 Commits

Author SHA1 Message Date
Laurent Cozic
306773ec8d update 2023-10-19 17:09:03 +01:00
Laurent Cozic
d0f2ff6929 update 2023-10-19 12:24:03 +01:00
Laurent Cozic
c19f9eb705 update 2023-10-18 17:54:29 +01:00
Laurent Cozic
8b811111d6 init 2023-10-17 17:27:41 +01:00
67 changed files with 530 additions and 1250 deletions

View File

@@ -242,7 +242,6 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands.js
@@ -500,8 +499,7 @@ packages/app-mobile/utils/autodetectTheme.js
packages/app-mobile/utils/checkPermissions.js
packages/app-mobile/utils/createRootStyle.js
packages/app-mobile/utils/debounce.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
packages/app-mobile/utils/fs-driver/runOnDeviceTests.js
packages/app-mobile/utils/fs-driver-rn.js
packages/app-mobile/utils/setupNotifications.js
packages/app-mobile/utils/shareHandler.js
packages/app-mobile/utils/types.js
@@ -535,7 +533,6 @@ packages/editor/CodeMirror/testUtil/createTestEditor.js
packages/editor/CodeMirror/testUtil/forceFullParse.js
packages/editor/CodeMirror/testUtil/loadLanguages.js
packages/editor/CodeMirror/theme.js
packages/editor/CodeMirror/util/isInSyntaxNode.js
packages/editor/SelectionFormatting.js
packages/editor/events.js
packages/editor/types.js
@@ -708,10 +705,6 @@ packages/lib/services/commands/isEditorCommand.js
packages/lib/services/commands/propsHaveChanged.js
packages/lib/services/commands/stateToWhenClauseContext.js
packages/lib/services/contextkey/contextkey.js
packages/lib/services/database/addMigrationFile.js
packages/lib/services/database/migrations/42.js
packages/lib/services/database/migrations/43.js
packages/lib/services/database/migrations/44.js
packages/lib/services/database/types.js
packages/lib/services/debug/populateDatabase.js
packages/lib/services/e2ee/EncryptionService.test.js

9
.gitignore vendored
View File

@@ -224,7 +224,6 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands.js
@@ -482,8 +481,7 @@ packages/app-mobile/utils/autodetectTheme.js
packages/app-mobile/utils/checkPermissions.js
packages/app-mobile/utils/createRootStyle.js
packages/app-mobile/utils/debounce.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
packages/app-mobile/utils/fs-driver/runOnDeviceTests.js
packages/app-mobile/utils/fs-driver-rn.js
packages/app-mobile/utils/setupNotifications.js
packages/app-mobile/utils/shareHandler.js
packages/app-mobile/utils/types.js
@@ -517,7 +515,6 @@ packages/editor/CodeMirror/testUtil/createTestEditor.js
packages/editor/CodeMirror/testUtil/forceFullParse.js
packages/editor/CodeMirror/testUtil/loadLanguages.js
packages/editor/CodeMirror/theme.js
packages/editor/CodeMirror/util/isInSyntaxNode.js
packages/editor/SelectionFormatting.js
packages/editor/events.js
packages/editor/types.js
@@ -690,10 +687,6 @@ packages/lib/services/commands/isEditorCommand.js
packages/lib/services/commands/propsHaveChanged.js
packages/lib/services/commands/stateToWhenClauseContext.js
packages/lib/services/contextkey/contextkey.js
packages/lib/services/database/addMigrationFile.js
packages/lib/services/database/migrations/42.js
packages/lib/services/database/migrations/43.js
packages/lib/services/database/migrations/44.js
packages/lib/services/database/types.js
packages/lib/services/debug/populateDatabase.js
packages/lib/services/e2ee/EncryptionService.test.js

View File

@@ -9,7 +9,7 @@
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230825-share-permissions.png" alt=""></p>
<h2>Email to Note<a name="email-to-note" href="#email-to-note" class="heading-anchor">🔗</a></h2>
<p>Joplin Cloud Pro and Teams also now include the Email to Note feature, allowing you to conveniently store your emails within Joplin Cloud. By simply forwarding your emails to your Joplin Cloud address, you can transform them into notes. The email's subject will serve as the note title, while the body of the email will be the note's content. These notes will be organized within a notebook named &quot;Inbox.&quot;</p>
<p>More information in the <a href="https://joplinapp.org/email_to_note/">Email to Note documentation</a>.</p>
<p>More information in the <a href="https://joplinapp.org/email%5C_to%5C_note/">Email to Note documentation</a>.</p>
<h2>Choose to resize an image or not<a name="choose-to-resize-an-image-or-not" href="#choose-to-resize-an-image-or-not" class="heading-anchor">🔗</a></h2>
<p>By default, when you add a large image, Joplin will ask you if you would like to shrink it down or not. With this new release, you now have the option to always ask, to always resize, or to never resize the image, giving you more flexibility and reducing the number of prompts in the app.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230825-resize-note.png" alt=""></p>

View File

@@ -22,11 +22,11 @@ Three types of applications are available: for **desktop** (Windows, macOS and L
Operating System | Download
---|---
Windows (32 and 64-bit) | <a href='https://objects.joplinusercontent.com/v2.12.19/Joplin-Setup-2.12.19.exe?source=JoplinWebsite&type=New'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a>
macOS | <a href='https://objects.joplinusercontent.com/v2.12.19/Joplin-2.12.19.dmg?source=JoplinWebsite&type=New'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a>
Linux | <a href='https://objects.joplinusercontent.com/v2.12.19/Joplin-2.12.19.AppImage?source=JoplinWebsite&type=New'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a>
Windows (32 and 64-bit) | <a href='https://objects.joplinusercontent.com/v2.12.18/Joplin-Setup-2.12.18.exe?source=JoplinWebsite&type=New'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a>
macOS | <a href='https://objects.joplinusercontent.com/v2.12.18/Joplin-2.12.18.dmg?source=JoplinWebsite&type=New'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a>
Linux | <a href='https://objects.joplinusercontent.com/v2.12.18/Joplin-2.12.18.AppImage?source=JoplinWebsite&type=New'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a>
**On Windows**, you may also use the <a href='https://objects.joplinusercontent.com/v2.12.19/JoplinPortable.exe?source=JoplinWebsite&type=New'>Portable version</a>. The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
**On Windows**, you may also use the <a href='https://objects.joplinusercontent.com/v2.12.18/JoplinPortable.exe?source=JoplinWebsite&type=New'>Portable version</a>. The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
**On Linux**, the recommended way is to use the following installation script as it will handle the desktop icon too:

View File

@@ -4,7 +4,6 @@ import shim from '@joplin/lib/shim';
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
import { BrowserWindow, Tray, screen } from 'electron';
import bridge from './bridge';
const url = require('url');
const path = require('path');
const { dirname } = require('@joplin/lib/path-utils');
@@ -143,15 +142,6 @@ export default class ElectronAppWrapper {
}, 3000);
}
// will-frame-navigate is fired by clicking on a link within the BrowserWindow.
this.win_.webContents.on('will-frame-navigate', event => {
// If the link changes the URL of the browser window,
if (event.isMainFrame) {
event.preventDefault();
void bridge().openExternal(event.url);
}
});
this.win_.on('close', (event: any) => {
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)
// otherwise the window is simply hidden, and will be re-open once the app is "activated" (which happens when the

View File

@@ -740,9 +740,7 @@ class MainScreenComponent extends React.Component<Props, State> {
editor: () => {
let bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE';
if (this.props.isSafeMode) {
bodyEditor = 'PlainText';
} else if (this.props.settingEditorCodeView && this.props.enableBetaMarkdownEditor) {
if (this.props.settingEditorCodeView && this.props.enableBetaMarkdownEditor) {
bodyEditor = 'CodeMirror6';
}
return <NoteEditor key={key} bodyEditor={bodyEditor} />;

View File

@@ -817,7 +817,6 @@ function useMenu(props: Props) {
menuItemDic.setTags,
menuItemDic.showShareNoteDialog,
separator(),
menuItemDic.showNoteProperties,
menuItemDic.showNoteContentProperties,
],
},

View File

@@ -1,56 +0,0 @@
// Used in safe mode
import * as React from 'react';
import { ForwardedRef } from 'react';
import { useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
import { NoteBodyEditorProps, NoteBodyEditorRef } from '../../utils/types';
const PlainEditor = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditorRef>) => {
const editorRef = useRef<HTMLTextAreaElement>();
useImperativeHandle(ref, () => {
return {
content: () => editorRef.current?.value ?? '',
resetScroll: () => {
editorRef.current.scrollTop = 0;
},
scrollTo: () => {
// Not supported
},
supportsCommand: _name => {
return false;
},
execCommand: async _command => {
// Not supported
},
};
}, []);
useEffect(() => {
if (!editorRef.current) return;
if (editorRef.current.value !== props.content) {
editorRef.current.value = props.content;
}
}, [props.content]);
const onChange = useCallback((event: any) => {
props.onChange({ changeId: null, content: event.target.value });
}, [props.onChange]);
return (
<div style={props.style}>
<textarea
ref={editorRef}
style={{ width: '100%', height: '100%' }}
defaultValue={props.content}
onChange={onChange}
/>
</div>
);
};
export default forwardRef(PlainEditor);

View File

@@ -0,0 +1,50 @@
// Kept only for reference
import * as React from 'react';
import { useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
export interface OnChangeEvent {
changeId: number,
content: any,
}
interface PlainEditorProps {
style: any,
onChange(event: OnChangeEvent): void,
onWillChange(event:any): void,
markupToHtml: Function,
disabled: boolean,
}
const PlainEditor = (props:PlainEditorProps, ref:any) => {
const editorRef = useRef<any>();
useImperativeHandle(ref, () => {
return {
content: () => '',
};
}, []);
useEffect(() => {
if (!editorRef.current) return;
editorRef.current.value = props.defaultEditorState.value;
}, [props.defaultEditorState]);
const onChange = useCallback((event:any) => {
props.onChange({ changeId: null, content: event.target.value });
}, [props.onWillChange, props.onChange]);
return (
<div style={props.style}>
<textarea
ref={editorRef}
style={{ width: '100%', height: '100%' }}
defaultValue={props.defaultEditorState.value}
onChange={onChange}
/>;
</div>
);
};
export default forwardRef(PlainEditor);

View File

@@ -14,7 +14,7 @@ import useFormNote, { OnLoadEvent } from './utils/useFormNote';
import useEffectiveNoteId from './utils/useEffectiveNoteId';
import useFolder from './utils/useFolder';
import styles_ from './styles';
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps, AllAssetsOptions, NoteBodyEditorRef } from './utils/types';
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps, AllAssetsOptions } from './utils/types';
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
import CommandService from '@joplin/lib/services/CommandService';
import ToolbarButton from '../ToolbarButton/ToolbarButton';
@@ -45,7 +45,6 @@ import { ModelType } from '@joplin/lib/BaseModel';
import BaseItem from '@joplin/lib/models/BaseItem';
import { ErrorCode } from '@joplin/lib/errors';
import ItemChange from '@joplin/lib/models/ItemChange';
import PlainEditor from './NoteBody/PlainEditor/PlainEditor';
import CodeMirror6 from './NoteBody/CodeMirror/v6/CodeMirror';
import CodeMirror5 from './NoteBody/CodeMirror/v5/CodeMirror';
@@ -61,7 +60,7 @@ function NoteEditor(props: NoteEditorProps) {
const [scrollWhenReady, setScrollWhenReady] = useState<ScrollOptions>(null);
const [isReadOnly, setIsReadOnly] = useState<boolean>(false);
const editorRef = useRef<NoteBodyEditorRef>();
const editorRef = useRef<any>();
const titleInputRef = useRef<any>();
const isMountedRef = useRef(true);
const noteSearchBarRef = useRef(null);
@@ -463,8 +462,6 @@ function NoteEditor(props: NoteEditorProps) {
if (props.bodyEditor === 'TinyMCE') {
editor = <TinyMCE {...editorProps}/>;
} else if (props.bodyEditor === 'PlainText') {
editor = <PlainEditor {...editorProps}/>;
} else if (props.bodyEditor === 'CodeMirror') {
editor = <CodeMirror5 {...editorProps}/>;
} else if (props.bodyEditor === 'CodeMirror6') {

View File

@@ -1,5 +1,5 @@
import { RefObject, useEffect } from 'react';
import { FormNote, NoteBodyEditorRef, ScrollOptionTypes } from './types';
import { useEffect } from 'react';
import { FormNote, ScrollOptionTypes } from './types';
import editorCommandDeclarations, { enabledCondition } from '../editorCommandDeclarations';
import CommandService, { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService';
import time from '@joplin/lib/time';
@@ -12,8 +12,6 @@ const commandsWithDependencies = [
require('../commands/pasteAsText'),
];
type SetFormNoteCallback = (callback: (prev: FormNote)=> FormNote)=> void;
interface HookDependencies {
formNote: FormNote;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
@@ -21,18 +19,16 @@ interface HookDependencies {
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
dispatch: Function;
noteSearchBarRef: any;
editorRef: RefObject<NoteBodyEditorRef>;
editorRef: any;
titleInputRef: any;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
saveNoteAndWait: Function;
setFormNote: SetFormNoteCallback;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
setFormNote: Function;
}
function editorCommandRuntime(
declaration: CommandDeclaration,
editorRef: RefObject<NoteBodyEditorRef>,
setFormNote: SetFormNoteCallback,
): CommandRuntime {
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
function editorCommandRuntime(declaration: CommandDeclaration, editorRef: any, setFormNote: Function): CommandRuntime {
return {
execute: async (_context: CommandContext, ...args: any[]) => {
if (!editorRef.current) {

View File

@@ -67,6 +67,5 @@ export default function() {
'switchProfile2',
'switchProfile3',
'pasteAsText',
'showNoteProperties',
];
}

View File

@@ -76,49 +76,4 @@ test.describe('main', () => {
await expect(mainScreen.noteListContainer.locator('[title^="Toggle sort order"]')).not.toBeVisible();
});
test('clicking on an external link should try to launch a browser', async ({ electronApp, mainWindow }) => {
const mainScreen = new MainScreen(mainWindow);
await mainScreen.waitFor();
// Mock openExternal
const nextExternalUrlPromise = electronApp.evaluate(({ shell }) => {
return new Promise<string>(resolve => {
const openExternal = async (url: string) => {
resolve(url);
};
shell.openExternal = openExternal;
});
});
// Create a test link
const testLinkTitle = 'This is a test link!';
const linkHref = 'https://joplinapp.org/';
await mainWindow.evaluate(({ testLinkTitle, linkHref }) => {
const testLink = document.createElement('a');
testLink.textContent = testLinkTitle;
testLink.onclick = () => {
// We need to navigate by setting location.href -- clicking on a link
// directly within the main window (i.e. not in a PDF viewer) doesn't
// navigate.
location.href = linkHref;
};
testLink.href = '#';
// Display on top of everything
testLink.style.zIndex = '99999';
testLink.style.position = 'fixed';
testLink.style.top = '0';
testLink.style.left = '0';
document.body.appendChild(testLink);
}, { testLinkTitle, linkHref });
const testLink = mainWindow.getByText(testLinkTitle);
await expect(testLink).toBeVisible();
await testLink.click({ noWaitAfter: true });
expect(await nextExternalUrlPromise).toBe(linkHref);
});
});

View File

@@ -31,6 +31,12 @@ const React = require('react');
const nodeSqlite = require('sqlite3');
const initLib = require('@joplin/lib/initLib').default;
// Security: If we attempt to navigate away from the root HTML page, it's likely because
// of an improperly sanitized link. Prevent this by closing the window before we can
// navigate away.
window.onbeforeunload = () => {
window.close();
};
if (bridge().env() === 'dev') {
const newConsole = function(oldConsole) {

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.13.3",
"version": "2.13.2",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
@@ -121,10 +121,10 @@
"@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.5.4",
"@types/node": "18.17.19",
"@types/react": "18.2.24",
"@types/react-redux": "7.1.27",
"@types/react": "18.2.23",
"@types/react-redux": "7.1.26",
"@types/styled-components": "5.1.28",
"electron": "25.9.0",
"electron": "25.8.1",
"electron-builder": "24.4.0",
"glob": "10.3.10",
"gulp": "4.0.2",
@@ -149,7 +149,7 @@
"@joplin/lib": "~2.13",
"@joplin/renderer": "~2.13",
"@joplin/utils": "~2.13",
"@types/mustache": "4.2.3",
"@types/mustache": "4.2.2",
"async-mutex": "0.4.0",
"codemirror": "5.65.9",
"color": "3.2.1",
@@ -174,8 +174,8 @@
"react": "18.2.0",
"react-datetime": "3.2.0",
"react-dom": "18.2.0",
"react-redux": "8.1.3",
"react-select": "5.7.7",
"react-redux": "8.1.2",
"react-select": "5.7.5",
"react-toggle-button": "2.2.0",
"react-tooltip": "4.5.1",
"redux": "4.2.1",

View File

@@ -12,9 +12,8 @@ export default function stateToWhenClauseContext(state: AppState, options: WhenC
...libStateToWhenClauseContext(state, options),
// UI elements
markdownEditorVisible: !!state.settings['editor.codeView'] && !state.settings['isSafeMode'],
richTextEditorVisible: !state.settings['editor.codeView'] && !state.settings['isSafeMode'],
markdownEditorVisible: !!state.settings['editor.codeView'],
richTextEditorVisible: !state.settings['editor.codeView'],
markdownEditorPaneVisible: state.settings['editor.codeView'] && state.noteVisiblePanes.includes('editor'),
markdownViewerPaneVisible: state.settings['editor.codeView'] && state.noteVisiblePanes.includes('viewer'),
modalDialogVisible: !!Object.keys(state.visibleDialogs).length,

View File

@@ -284,7 +284,7 @@ PODS:
- React-Core
- react-native-fingerprint-scanner (6.0.0):
- React
- react-native-geolocation (3.1.0):
- react-native-geolocation (3.0.6):
- React-Core
- react-native-get-random-values (1.9.0):
- React-Core
@@ -306,7 +306,7 @@ PODS:
- React-Core
- react-native-version-info (1.1.1):
- React-Core
- react-native-webview (13.6.0):
- react-native-webview (13.5.1):
- React-Core
- React-perflogger (0.71.10)
- React-RCTActionSheet (0.71.10):
@@ -667,7 +667,7 @@ SPEC CHECKSUMS:
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
react-native-document-picker: 2b8f18667caee73a96708a82b284a4f40b30a156
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
react-native-geolocation: ef66fb798d96284c6043f0b16c15d9d1d4955db4
react-native-geolocation: 0f7fe8a4c2de477e278b0365cce27d089a8c5903
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
react-native-image-picker: 3269f75c251cdcd61ab51b911dd30d6fff8c6169
react-native-image-resizer: 681f7607418b97c084ba2d0999b153b103040d8a
@@ -678,7 +678,7 @@ SPEC CHECKSUMS:
react-native-slider: 1cdd6ba29675df21f30544253bf7351d3c2d68c4
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
react-native-version-info: a106f23009ac0db4ee00de39574eb546682579b9
react-native-webview: 669ae162965f629a8d6a4bdd3b99a304d36ef1f2
react-native-webview: 8baa0f5c6d336d6ba488e942bcadea5bf51f050a
React-perflogger: 217095464d5c4bb70df0742fa86bf2a363693468
React-RCTActionSheet: 8deae9b85a4cbc6a2243618ea62a374880a2c614
React-RCTAnimation: 59c62353a8b59ce206044786c5d30e4754bffa64

View File

@@ -27,7 +27,7 @@
"@joplin/utils": "~2.13",
"@react-native-community/clipboard": "1.5.1",
"@react-native-community/datetimepicker": "7.5.0",
"@react-native-community/geolocation": "3.1.0",
"@react-native-community/geolocation": "3.0.6",
"@react-native-community/netinfo": "9.4.1",
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-community/slider": "4.4.3",
@@ -49,7 +49,7 @@
"react-native-device-info": "10.9.0",
"react-native-dialogbox": "0.6.10",
"react-native-document-picker": "9.0.1",
"react-native-dropdownalert": "5.1.0",
"react-native-dropdownalert": "4.5.1",
"react-native-exit-app": "2.0.0",
"react-native-file-viewer": "2.1.5",
"react-native-fingerprint-scanner": "6.0.0",
@@ -71,9 +71,9 @@
"react-native-vector-icons": "10.0.0",
"react-native-version-info": "1.1.1",
"react-native-vosk": "0.1.12",
"react-native-webview": "13.6.0",
"react-native-webview": "13.5.1",
"react-native-zip-archive": "6.1.0",
"react-redux": "8.1.3",
"react-redux": "8.1.2",
"redux": "4.2.1",
"rn-fetch-blob": "0.12.0",
"stream": "0.0.2",
@@ -95,9 +95,9 @@
"@tsconfig/react-native": "2.0.2",
"@types/fs-extra": "11.0.2",
"@types/jest": "29.5.4",
"@types/react": "18.2.24",
"@types/react": "18.2.23",
"@types/react-native": "0.70.6",
"@types/react-redux": "7.1.27",
"@types/react-redux": "7.1.26",
"@types/tar-stream": "2.2.3",
"babel-jest": "29.6.4",
"babel-plugin-module-resolver": "4.1.0",

View File

@@ -97,7 +97,7 @@ SyncTargetRegistry.addClass(SyncTargetAmazonS3);
SyncTargetRegistry.addClass(SyncTargetJoplinServer);
SyncTargetRegistry.addClass(SyncTargetJoplinCloud);
import FsDriverRN from './utils/fs-driver/fs-driver-rn';
import FsDriverRN from './utils/fs-driver-rn';
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import MigrationService from '@joplin/lib/services/MigrationService';
@@ -109,7 +109,7 @@ import { loadMasterKeysFromSettings, migrateMasterPassword } from '@joplin/lib/s
import SyncTargetNone from '@joplin/lib/SyncTargetNone';
import { setRSA } from '@joplin/lib/services/e2ee/ppk';
import RSA from './services/e2ee/RSA.react-native';
import { runIntegrationTests as runRsaIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils';
import { runIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils';
import { Theme, ThemeAppearance } from '@joplin/lib/themes/type';
import { AppState } from './utils/types';
import ProfileSwitcher from './components/ProfileSwitcher/ProfileSwitcher';
@@ -121,7 +121,6 @@ import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetche
import { ReactNode } from 'react';
import { parseShareCache } from '@joplin/lib/services/share/reducer';
import autodetectTheme, { onSystemColorSchemeChange } from './utils/autodetectTheme';
import runOnDeviceFsDriverTests from './utils/fs-driver/runOnDeviceTests';
type SideMenuPosition = 'left' | 'right';
@@ -750,10 +749,7 @@ async function initialize(dispatch: Function) {
// call will throw an error, alerting us of the issue. Otherwise it will
// just print some messages in the console.
// ----------------------------------------------------------------------------
if (Setting.value('env') === 'dev') {
await runRsaIntegrationTests();
await runOnDeviceFsDriverTests();
}
if (Setting.value('env') === 'dev') await runIntegrationTests();
reg.logger().info('Application initialized');
}
@@ -763,7 +759,6 @@ class AppComponent extends React.Component {
private urlOpenListener_: EmitterSubscription|null = null;
private appStateChangeListener_: NativeEventSubscription|null = null;
private themeChangeListener_: NativeEventSubscription|null = null;
private dropdownAlert_ = (_data: any) => new Promise<any>(res => res);
public constructor() {
super();
@@ -896,11 +891,7 @@ class AppComponent extends React.Component {
AlarmService.setInAppNotificationHandler(async (alarmId: string) => {
const alarm = await Alarm.load(alarmId);
const notification = await Alarm.makeNotification(alarm);
void this.dropdownAlert_({
type: 'info',
title: notification.title,
message: notification.body ? notification.body : '',
});
this.dropdownAlert_.alertWithType('info', notification.title, notification.body ? notification.body : '');
});
this.appStateChangeListener_ = RNAppState.addEventListener('change', this.onAppStateChange_);
@@ -1091,7 +1082,8 @@ class AppComponent extends React.Component {
<View style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
{ shouldShowMainContent && <AppNav screens={appNavInit} dispatch={this.props.dispatch} /> }
</View>
<DropdownAlert alert={(func: any) => (this.dropdownAlert_ = func)} /> { !shouldShowMainContent && <BiometricPopup
<DropdownAlert ref={(ref: any) => this.dropdownAlert_ = ref} tapToCloseEnabled={true} />
{ !shouldShowMainContent && <BiometricPopup
dispatch={this.props.dispatch}
themeId={this.props.themeId}
sensorInfo={this.state.sensorInfo}

View File

@@ -3,7 +3,7 @@ const RNFetchBlob = require('rn-fetch-blob').default;
import * as RNFS from 'react-native-fs';
const DocumentPicker = require('react-native-document-picker').default;
import { openDocument } from '@joplin/react-native-saf-x';
import RNSAF, { DocumentFileDetail, openDocumentTree } from '@joplin/react-native-saf-x';
import RNSAF, { Encoding, DocumentFileDetail, openDocumentTree } from '@joplin/react-native-saf-x';
import { Platform } from 'react-native';
import * as tar from 'tar-stream';
import { resolve } from 'path';
@@ -18,63 +18,24 @@ function isScopedUri(path: string) {
return path.includes(ANDROID_URI_PREFIX);
}
// Encodings supported by rn-fetch-blob, RNSAF, and
// RNFS.
// See also
// - https://github.com/itinance/react-native-fs#readfilefilepath-string-encoding-string-promisestring
// - https://github.com/joltup/rn-fetch-blob/blob/cf9e8843599de92031df2660d5a1da18491fa3c0/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java#L1049
export enum SupportedEncoding {
Utf8 = 'utf8',
Ascii = 'ascii',
Base64 = 'base64',
}
const supportedEncodings = Object.values<string>(SupportedEncoding);
// Converts some encodings specifiers that work with NodeJS into encodings
// that work with RNSAF, RNFetchBlob.fs, and RNFS.
//
// Throws if an encoding can't be normalized.
const normalizeEncoding = (encoding: string): SupportedEncoding => {
encoding = encoding.toLowerCase();
// rn-fetch-blob and RNSAF require the exact string "utf8", but NodeJS (and thus
// fs-driver-node) support variants on this like "UtF-8" and "utf-8". Convert them:
if (encoding === 'utf-8') {
encoding = 'utf8';
}
if (!supportedEncodings.includes(encoding)) {
throw new Error(`Unsupported encoding: ${encoding}.`);
}
return encoding as SupportedEncoding;
};
export default class FsDriverRN extends FsDriverBase {
public appendFileSync() {
throw new Error('Not implemented');
}
// Requires that the file already exists.
// TODO: Update for compatibility with fs-driver-node's appendFile (which does not
// require that the file exists).
public appendFile(path: string, content: any, rawEncoding = 'base64') {
const encoding = normalizeEncoding(rawEncoding);
// Encoding can be either "utf8" or "base64"
public appendFile(path: string, content: any, encoding = 'base64') {
if (isScopedUri(path)) {
return RNSAF.writeFile(path, content, { encoding, append: true });
return RNSAF.writeFile(path, content, { encoding: encoding as Encoding, append: true });
}
return RNFS.appendFile(path, content, encoding);
}
// Encoding can be either "utf8", "utf-8", or "base64"
public writeFile(path: string, content: any, rawEncoding = 'base64') {
const encoding = normalizeEncoding(rawEncoding);
// Encoding can be either "utf8" or "base64"
public writeFile(path: string, content: any, encoding = 'base64') {
if (isScopedUri(path)) {
return RNSAF.writeFile(path, content, { encoding: encoding });
return RNSAF.writeFile(path, content, { encoding: encoding as Encoding });
}
// We need to use rn-fetch-blob here due to this bug:
// https://github.com/itinance/react-native-fs/issues/700
return RNFetchBlob.fs.writeFile(path, content, encoding);
@@ -234,11 +195,10 @@ export default class FsDriverRN extends FsDriverBase {
return null;
}
public readFile(path: string, rawEncoding = 'utf8') {
const encoding = normalizeEncoding(rawEncoding);
public readFile(path: string, encoding = 'utf8') {
if (encoding === 'Buffer') throw new Error('Raw buffer output not supported for FsDriverRN.readFile');
if (isScopedUri(path)) {
return RNSAF.readFile(path, { encoding: encoding });
return RNSAF.readFile(path, { encoding: encoding as Encoding });
}
return RNFS.readFile(path, encoding);
}
@@ -284,9 +244,7 @@ export default class FsDriverRN extends FsDriverBase {
}
}
public async readFileChunk(handle: any, length: number, rawEncoding = 'base64') {
const encoding = normalizeEncoding(rawEncoding);
public async readFileChunk(handle: any, length: number, encoding = 'base64') {
if (handle.offset + length > handle.stat.size) {
length = handle.stat.size - handle.offset;
}

View File

@@ -1,249 +0,0 @@
import Setting from '@joplin/lib/models/Setting';
import shim from '@joplin/lib/shim';
import uuid from '@joplin/lib/uuid';
import { join } from 'path';
import FsDriverBase from '@joplin/lib/fs-driver-base';
import Logger from '@joplin/utils/Logger';
import { Buffer } from 'buffer';
const logger = Logger.create('fs-driver-tests');
const expectToBe = async <T> (actual: T, expected: T) => {
if (actual !== expected) {
throw new Error(`Integration test failure: ${actual} was expected to be ${expected}`);
}
};
const testExpect = async () => {
// Verify that expect is working
await expectToBe(1, 1);
await expectToBe(true, true);
let failed = false;
try {
await expectToBe('a', 'test');
failed = true;
} catch (_error) {
failed = false;
}
if (failed) {
throw new Error('expectToBe should throw when given non-equal inputs');
}
};
const testAppendFile = async (tempDir: string) => {
logger.info('Testing fsDriver.appendFile...');
const targetFile = join(tempDir, uuid.createNano());
const fsDriver: FsDriverBase = shim.fsDriver();
// For fs-driver-rn's appendFile to work, we first need to create the file.
// TODO: This is different from the requirements of fs-driver-node.
await fsDriver.writeFile(targetFile, '');
const firstChunk = 'A 𝓊𝓃𝒾𝒸𝓸𝒹𝓮 test\n...';
await fsDriver.appendFile(targetFile, firstChunk, 'utf-8');
await expectToBe(await fsDriver.readFile(targetFile), firstChunk);
const secondChunk = '▪️ More unicode ▪️';
await fsDriver.appendFile(targetFile, secondChunk, 'utf8');
await expectToBe(await fsDriver.readFile(targetFile), firstChunk + secondChunk);
const thirdChunk = 'ASCII';
await fsDriver.appendFile(targetFile, thirdChunk, 'ascii');
await expectToBe(await fsDriver.readFile(targetFile), firstChunk + secondChunk + thirdChunk);
const lastChunk = 'Test...';
await fsDriver.appendFile(
targetFile, Buffer.from(lastChunk, 'utf8').toString('base64'), 'base64',
);
await expectToBe(
await fsDriver.readFile(targetFile), firstChunk + secondChunk + thirdChunk + lastChunk,
);
// Should throw if given an invalid encoding
let didThrow = false;
try {
await fsDriver.appendFile(targetFile, 'test', 'bad-encoding');
} catch (_error) {
didThrow = true;
}
await expectToBe(didThrow, true);
};
const testReadWriteFileUtf8 = async (tempDir: string) => {
logger.info('Testing fsDriver.writeFile and fsDriver.readFile with utf-8...');
const filePath = join(tempDir, uuid.createNano());
const testStrings = [
// ASCII
'test',
// Special characters
'𝐴 𝒕𝐞𝑺𝒕',
// Emojis
'✅ Test. 🕳️',
];
const testEncodings = ['utf-8', 'utf8', 'UtF-8'];
// Use the same file for all tests to test overwriting
for (const encoding of testEncodings) {
for (const testString of testStrings) {
const fsDriver: FsDriverBase = shim.fsDriver();
await fsDriver.writeFile(filePath, testString, encoding);
const fileData = await fsDriver.readFile(filePath, encoding);
await expectToBe(fileData, testString);
}
}
};
const testReadFileChunkUtf8 = async (tempDir: string) => {
logger.info('Testing fsDriver.readFileChunk...');
const filePath = join(tempDir, `${uuid.createNano()}.txt`);
const fsDriver: FsDriverBase = shim.fsDriver();
// 🕳️ is 7 bytes when utf-8 encoded
// à,á,â, and ã are each 2 bytes
const expectedFileContent = '01234567\nàáâã\n🕳️🕳️🕳️\ntēst...';
await fsDriver.writeFile(filePath, expectedFileContent, 'utf8');
const testEncodings = ['utf-8', 'utf8', 'UtF-8'];
for (const encoding of testEncodings) {
const handle = await fsDriver.open(filePath, 'r');
await expectToBe(
await fsDriver.readFileChunk(handle, 8, encoding), '01234567',
);
await expectToBe(
await fsDriver.readFileChunk(handle, 1, encoding), '\n',
);
await expectToBe(
await fsDriver.readFileChunk(handle, 8, encoding), 'àáâã',
);
await expectToBe(
await fsDriver.readFileChunk(handle, 8, encoding), '\n🕳️',
);
await expectToBe(
await fsDriver.readFileChunk(handle, 15, encoding), '🕳️🕳️\n',
);
// A 0 length should return null and not advance
await expectToBe(
await fsDriver.readFileChunk(handle, 0, encoding), null,
);
// Reading a different encoding (then switching back to the original)
// should be supported
await expectToBe(
await fsDriver.readFileChunk(handle, 3, 'base64'),
Buffer.from('tē', 'utf-8').toString('base64'),
);
await expectToBe(
await fsDriver.readFileChunk(handle, 100, encoding), 'st...',
);
// Should not be able to read past the end
await expectToBe(
await fsDriver.readFileChunk(handle, 10, encoding), null,
);
await expectToBe(
await fsDriver.readFileChunk(handle, 1, encoding), null,
);
await fsDriver.close(filePath);
}
};
const testTarCreate = async (tempDir: string) => {
logger.info('Testing fsDriver.tarCreate...');
const directoryToPack = join(tempDir, uuid.createNano());
const fsDriver: FsDriverBase = shim.fsDriver();
// Add test files to the directory
const fileContents: Record<string, string> = {};
// small utf-8 encoded files
for (let i = 0; i < 10; i ++) {
const testFilePath = join(directoryToPack, uuid.createNano());
const fileContent = `✅ Testing... ä ✅ File #${i}`;
await fsDriver.writeFile(testFilePath, fileContent, 'utf-8');
fileContents[testFilePath] = fileContent;
}
// larger utf-8 encoded files
for (let i = 0; i < 3; i ++) {
const testFilePath = join(directoryToPack, uuid.createNano());
let fileContent = `✅ Testing... ä ✅ File #${i}`;
for (let j = 0; j < 8; j ++) {
fileContent += fileContent;
}
await fsDriver.writeFile(testFilePath, fileContent, 'utf-8');
fileContents[testFilePath] = fileContent;
}
// Pack the files
const pathsToTar = Object.keys(fileContents);
const tarOutputPath = join(tempDir, 'test-tar.tar');
await fsDriver.tarCreate({
cwd: tempDir,
file: tarOutputPath,
}, pathsToTar);
// Read the tar file as utf-8 and search for the written file contents
// (which should work).
const rawTarData: string = await fsDriver.readFile(tarOutputPath, 'utf8');
for (const fileContent of Object.values(fileContents)) {
await expectToBe(rawTarData.includes(fileContent), true);
}
};
// In the past, some fs-driver functionality has worked correctly on some devices and not others.
// As such, we need to be able to run some tests on-device.
const runOnDeviceTests = async () => {
const tempDir = join(Setting.value('tempDir'), uuid.createNano());
if (await shim.fsDriver().exists(tempDir)) {
await shim.fsDriver().remove(tempDir);
}
try {
await testExpect();
await testAppendFile(tempDir);
await testReadWriteFileUtf8(tempDir);
await testReadFileChunkUtf8(tempDir);
await testTarCreate(tempDir);
} catch (error) {
const errorMessage = `On-device testing failed with an exception: ${error}.`;
logger.error(errorMessage, error);
alert(errorMessage);
} finally {
await shim.fsDriver().remove(tempDir);
}
};
export default runOnDeviceTests;

View File

@@ -3,7 +3,7 @@ const { GeolocationReact } = require('./geolocation-react.js');
const PoorManIntervals = require('@joplin/lib/PoorManIntervals').default;
const RNFetchBlob = require('rn-fetch-blob').default;
const { generateSecureRandom } = require('react-native-securerandom');
const FsDriverRN = require('./fs-driver/fs-driver-rn').default;
const FsDriverRN = require('./fs-driver-rn').default;
const { Buffer } = require('buffer');
const { Linking, Platform } = require('react-native');
const mimeUtils = require('@joplin/lib/mime-utils.js').mime;

View File

@@ -20,7 +20,6 @@ import { SearchState, EditorProps, EditorSettings } from '../types';
import { EditorEventType, SelectionRangeChangeEvent } from '../events';
import {
decreaseIndent, increaseIndent,
insertOrIncreaseIndent,
toggleBolded, toggleCode,
toggleItalicized, toggleMath,
} from './markdown/markdownCommands';
@@ -255,7 +254,7 @@ const createEditor = (
notifyLinkEditRequest();
return true;
}),
keyCommand('Tab', insertOrIncreaseIndent, true),
keyCommand('Tab', increaseIndent, true),
keyCommand('Shift-Tab', decreaseIndent, true),
...standardKeymap, ...historyKeymap, ...searchKeymap,

View File

@@ -1,6 +1,5 @@
import { EditorSelection } from '@codemirror/state';
import {
insertOrIncreaseIndent,
toggleBolded, toggleCode, toggleHeaderLevel, toggleItalicized, toggleMath, updateLink,
} from './markdownCommands';
import createTestEditor from '../testUtil/createTestEditor';
@@ -239,41 +238,5 @@ describe('markdownCommands', () => {
expect(sel.from).toBe('> Testing...> \n> \n'.length);
expect(sel.to).toBe(editor.state.doc.length);
});
it('insertOrIncreaseIndent should indent when text is selected', async () => {
const initialText = '> Testing...\n> Test.';
const editor = await createTestEditor(
initialText,
EditorSelection.range(0, initialText.length),
['Blockquote'],
);
insertOrIncreaseIndent(editor);
expect(editor.state.doc.toString()).toBe('> \tTesting...\n> \tTest.');
});
it('insertOrIncreaseIndent should insert tabs when selection is empty, in a paragraph', async () => {
const initialText = 'This is a test\nof indentation.';
const editor = await createTestEditor(
initialText,
EditorSelection.cursor(initialText.length),
[],
);
insertOrIncreaseIndent(editor);
const finalText = editor.state.doc.toString();
// Should add tab character at the cursor
expect(finalText).toBe('This is a test\nof indentation.\t');
// Should move the selection after the tab
expect(editor.state.selection.ranges).toHaveLength(1);
expect(editor.state.selection.main).toMatchObject({
from: finalText.length,
to: finalText.length,
});
});
});

View File

@@ -63,35 +63,6 @@ describe('markdownCommands.toggleList', () => {
);
});
it('should not toggle a the full list when the cursor is on a blank line', async () => {
const checklistStartText = [
'# Test',
'',
'- [ ] This',
'- [ ] is',
'',
].join('\n');
const checklistEndText = [
'- [ ] a',
'- [ ] test',
].join('\n');
const editor = await createTestEditor(
`${checklistStartText}\n${checklistEndText}`,
// Place the cursor on the blank line between the checklist
// regions
EditorSelection.cursor(unorderedListText.length + 1),
['BulletList', 'ATXHeading1'],
);
// Should create a checkbox on the blank line
toggleList(ListType.CheckList)(editor);
expect(editor.state.doc.toString()).toBe(
`${checklistStartText}- [ ] \n${checklistEndText}`,
);
});
// it('should correctly replace an unordered list with a checklist', async () => {
// const editor = await createEditor(

View File

@@ -12,7 +12,6 @@ import {
toggleInlineFormatGlobally, toggleRegionFormatGlobally, toggleSelectedLinesStartWith,
isIndentationEquivalent, stripBlockquote, tabsToSpaces,
} from './markdownReformatter';
import intersectsSyntaxNode from '../util/isInSyntaxNode';
const startingSpaceRegex = /^(\s*)/;
@@ -184,11 +183,8 @@ export const toggleList = (listType: ListType): Command => {
const origFirstLineIndentation = firstLineIndentation;
const origContainerType = containerType;
// Grow `sel` to the smallest containing list, unless the
// cursor is on an empty line, in which case, the user
// probably wants to add a list item (and not select the entire
// list).
if (sel.empty && fromLine.text.trim() !== '') {
// Grow [sel] to the smallest containing list
if (sel.empty) {
sel = growSelectionToNode(state, sel, [orderedListTag, unorderedListTag]);
computeSelectionProps();
}
@@ -424,35 +420,6 @@ export const increaseIndent: Command = (view: EditorView): boolean => {
return true;
};
// Like `increaseIndent`, but may insert tabs, rather than
// indenting, in some instances.
export const insertOrIncreaseIndent: Command = (view: EditorView): boolean => {
const selection = view.state.selection;
const mainSelection = selection.main;
if (selection.ranges.length !== 1 || !mainSelection.empty) {
return increaseIndent(view);
}
if (intersectsSyntaxNode(view.state, mainSelection, 'ListItem')) {
return increaseIndent(view);
}
const indentUnit = indentString(view.state, getIndentUnit(view.state));
view.dispatch(view.state.changeByRange(selection => {
return {
// Move the selection to after the inserted text
range: EditorSelection.cursor(selection.from + indentUnit.length),
changes: {
from: selection.from,
insert: indentUnit,
},
};
}));
return true;
};
export const decreaseIndent: Command = (view: EditorView): boolean => {
const matchEmpty = true;
const changes = toggleSelectedLinesStartWith(

View File

@@ -1,32 +0,0 @@
import { syntaxTree } from '@codemirror/language';
import { EditorState } from '@codemirror/state';
interface Range {
from: number;
to: number;
}
const intersectsSyntaxNode = (state: EditorState, range: Range, nodeName: string) => {
let foundNode = false;
syntaxTree(state).iterate({
from: range.from,
to: range.to,
enter: node => {
if (node.name === nodeName) {
foundNode = true;
// Skip children
return false;
}
// Search children if we haven't found a matching node yet.
return !foundNode;
},
});
return foundNode;
};
export default intersectsSyntaxNode;

View File

@@ -18,8 +18,8 @@
"@joplin/lib": "~2.13",
"@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.5.4",
"@types/react": "18.2.24",
"@types/react-redux": "7.1.27",
"@types/react": "18.2.23",
"@types/react-redux": "7.1.26",
"@types/styled-components": "5.1.28",
"jest": "29.6.3",
"jest-environment-jsdom": "29.6.3",

View File

@@ -16,7 +16,7 @@
],
"devDependencies": {
"standard": "17.1.0",
"tap": "16.3.9"
"tap": "16.3.8"
},
"gitHead": "eb4b0e64eab40a51b0895d3a40a9d8c3cb7b1b14"
}

View File

@@ -1,11 +1,6 @@
import Resource from './models/Resource';
import shim from './shim';
import Database from './database';
import migration42 from './services/database/migrations/42';
import migration43 from './services/database/migrations/43';
// import migration44 from './services/database/migrations/44';
import { SqlQuery, Migration } from './services/database/types';
import addMigrationFile from './services/database/addMigrationFile';
import Database, { SqlQuery } from './database';
const { promiseChain } = require('./promise-utils.js');
const { sprintf } = require('sprintf-js');
@@ -124,12 +119,6 @@ CREATE TABLE version (
INSERT INTO version (version) VALUES (1);
`;
const migrations: Migration[] = [
migration42,
migration43,
// migration44,
];
export interface TableField {
name: string;
type: number;
@@ -345,11 +334,16 @@ export default class JoplinDatabase extends Database {
});
}
public addMigrationFile(num: number) {
const timestamp = Date.now();
return { sql: 'INSERT INTO migrations (number, created_time, updated_time) VALUES (?, ?, ?)', params: [num, timestamp, timestamp] };
}
public async upgradeDatabase(fromVersion: number) {
// INSTRUCTIONS TO UPGRADE THE DATABASE:
//
// 1. Add the migration to lib/services/database/migrations.
// 2. Import the migration and add it to the `migrations` array above.
// 1. Add the new version number to the existingDatabaseVersions array
// 2. Add the upgrade logic to the "switch (targetVersion)" statement below
// IMPORTANT:
//
@@ -360,9 +354,7 @@ export default class JoplinDatabase extends Database {
// must be set in the synchronizer too.
// Note: v16 and v17 don't do anything. They were used to debug an issue.
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41];
for (let i = 0; i < migrations.length; i++) existingDatabaseVersions.push(existingDatabaseVersions[existingDatabaseVersions.length - 1] + 1);
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43];
let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion);
@@ -386,7 +378,7 @@ export default class JoplinDatabase extends Database {
const targetVersion = existingDatabaseVersions[currentVersionIndex + 1];
this.logger().info(`Converting database to version ${targetVersion}`);
let queries: (SqlQuery|string)[] = [];
let queries: any[] = [];
if (targetVersion === 1) {
queries = this.wrapQueries(this.sqlStringToLines(structureSql));
@@ -665,7 +657,7 @@ export default class JoplinDatabase extends Database {
queries.push(this.sqlStringToLines(newTableSql)[0]);
queries.push('ALTER TABLE resources ADD COLUMN `size` INT NOT NULL DEFAULT -1');
queries.push(addMigrationFile(20));
queries.push(this.addMigrationFile(20));
}
if (targetVersion === 21) {
@@ -725,7 +717,7 @@ export default class JoplinDatabase extends Database {
}
if (targetVersion === 27) {
queries.push(addMigrationFile(27));
queries.push(this.addMigrationFile(27));
}
if (targetVersion === 28) {
@@ -780,7 +772,7 @@ export default class JoplinDatabase extends Database {
queries.push('ALTER TABLE tags ADD COLUMN parent_id TEXT NOT NULL DEFAULT ""');
// Drop the tag note count view, instead compute note count on the fly
// queries.push('DROP VIEW tags_with_note_count');
// queries.push(addMigrationFile(31));
// queries.push(this.addMigrationFile(31));
}
if (targetVersion === 32) {
@@ -875,7 +867,7 @@ export default class JoplinDatabase extends Database {
CREATE TRIGGER notes_after_insert AFTER INSERT ON notes_normalized BEGIN
INSERT INTO notes_fts(docid, ${tableFields}) SELECT rowid, ${tableFields} FROM notes_normalized WHERE new.rowid = notes_normalized.rowid;
END;`);
queries.push(addMigrationFile(33));
queries.push(this.addMigrationFile(33));
}
if (targetVersion === 34) {
@@ -886,7 +878,7 @@ export default class JoplinDatabase extends Database {
if (targetVersion === 35) {
queries.push('ALTER TABLE notes_normalized ADD COLUMN todo_due INT NOT NULL DEFAULT 0');
queries.push('CREATE INDEX notes_normalized_todo_due ON notes_normalized (todo_due)');
queries.push(addMigrationFile(35));
queries.push(this.addMigrationFile(35));
}
if (targetVersion === 36) {
@@ -921,11 +913,15 @@ export default class JoplinDatabase extends Database {
queries.push('ALTER TABLE `folders` ADD COLUMN icon TEXT NOT NULL DEFAULT ""');
}
if (targetVersion > 41) {
const migration = migrations[targetVersion - 42];
if (!migration) throw new Error(`No such migration: ${targetVersion}`);
const migrationQueries = migration();
queries = queries.concat(migrationQueries);
if (targetVersion === 42) {
queries.push(this.addMigrationFile(42));
}
if (targetVersion === 43) {
queries.push('ALTER TABLE `notes` ADD COLUMN `user_data` TEXT NOT NULL DEFAULT ""');
queries.push('ALTER TABLE `tags` ADD COLUMN `user_data` TEXT NOT NULL DEFAULT ""');
queries.push('ALTER TABLE `folders` ADD COLUMN `user_data` TEXT NOT NULL DEFAULT ""');
queries.push('ALTER TABLE `resources` ADD COLUMN `user_data` TEXT NOT NULL DEFAULT ""');
}
const updateVersionQuery = { sql: 'UPDATE version SET version = ?', params: [targetVersion] };

View File

@@ -48,7 +48,7 @@ type ImageObject = {
naturalHeight?: number;
};
export function getImageSizes(element: Document, forceAbsoluteUrls = false) {
export function getImageSizes(element: HTMLElement, forceAbsoluteUrls = false) {
const output: Record<string, ImageObject[]> = {};
const images = element.getElementsByTagName('img');

View File

@@ -1,10 +1,18 @@
import Logger from '@joplin/utils/Logger';
import time from './time';
import shim from './shim';
import { SqlParams, SqlQuery, StringOrSqlQuery } from './services/database/types';
const Mutex = require('async-mutex').Mutex;
type SqlParams = any[];
export interface SqlQuery {
sql: string;
params?: SqlParams;
}
type StringOrSqlQuery = string | SqlQuery;
export type Row = Record<string, any>;
export default class Database {

View File

@@ -25,10 +25,6 @@ export default class FsDriverBase {
throw new Error('Not implemented');
}
public async appendFile(_path: string, _content: string, _encoding = 'base64'): Promise<any> {
throw new Error('Not implemented');
}
public async copy(_source: string, _dest: string) {
throw new Error('Not implemented');
}

View File

@@ -1,5 +1,5 @@
import BaseModel from '../BaseModel';
import { SqlQuery } from '../services/database/types';
import { SqlQuery } from '../database';
import BaseItem from './BaseItem';
// - If is_associated = 1, note_resources indicates which note_id is currently associated with the given resource_id

View File

@@ -442,10 +442,4 @@ export default class Resource extends BaseItem {
}, { changeSource: ItemChange.SOURCE_SYNC });
}
// public static async save(o: ResourceEntity, options: SaveOptions = null): Promise<ResourceEntity> {
// const resource:ResourceEntity = await super.save(o, options);
// if (resource.updated_time) resource.bl
// return resource;
// }
}

View File

@@ -303,7 +303,6 @@ class Setting extends BaseModel {
};
public static autoSaveEnabled = true;
public static allowFileStorage = true;
private static metadata_: SettingItems = null;
private static keychainService_: any = null;
@@ -2056,7 +2055,7 @@ class Setting extends BaseModel {
}
private static canUseFileStorage(): boolean {
return this.allowFileStorage && !shim.mobilePlatform();
return !shim.mobilePlatform();
}
private static keyStorage(key: string): SettingStorage {

View File

@@ -1,4 +1,3 @@
import Logger from '@joplin/utils/Logger';
import { ModelType } from '../../BaseModel';
import { ErrorCode } from '../../errors';
import JoplinError from '../../JoplinError';
@@ -6,8 +5,6 @@ import { State as ShareState } from '../../services/share/reducer';
import ItemChange from '../ItemChange';
import Setting from '../Setting';
const logger = Logger.create('models/utils/readOnly');
export interface ItemSlice {
id?: string;
share_id: string;
@@ -46,18 +43,6 @@ export const checkIfItemCanBeChanged = (itemType: ModelType, changeSource: numbe
export const checkIfItemCanBeAddedToFolder = async (itemType: ModelType, Folder: any, changeSource: number, shareState: ShareState, parentId: string) => {
if (needsReadOnlyChecks(itemType, changeSource, shareState) && parentId) {
const parentFolder = await Folder.load(parentId, { fields: ['id', 'share_id'] });
if (!parentFolder) {
// Historically it's always been possible to set the parent_id of a
// note to a folder that does not exist - this is to support
// synchronisation, where items are downloaded in random order. It
// is not ideal to skip the check here, but if for some reason the
// folder turns out to be read-only the issue will be resolved
// during sync.
logger.warn('checkIfItemCanBeAddedToFolder: Trying to add an item to a folder that does not exist - skipping check');
return;
}
if (itemIsReadOnlySync(itemType, changeSource, parentFolder, Setting.value('sync.userId'), shareState)) {
throw new JoplinError('Cannot add an item as a child of a read-only item', ErrorCode.IsReadOnly);
}

View File

@@ -1,4 +1,4 @@
import { SqlQuery } from '../../services/database/types';
import { SqlQuery } from '../../database';
export enum PaginationOrderDir {
ASC = 'ASC',

View File

@@ -21,7 +21,7 @@
"@types/js-yaml": "4.0.6",
"@types/node": "18.17.19",
"@types/node-rsa": "1.1.2",
"@types/react": "18.2.24",
"@types/react": "18.2.23",
"@types/uuid": "9.0.4",
"clean-html": "1.5.0",
"jest": "29.6.4",

View File

@@ -1,4 +0,0 @@
export default (num: number) => {
const timestamp = Date.now();
return { sql: 'INSERT INTO migrations (number, created_time, updated_time) VALUES (?, ?, ?)', params: [num, timestamp, timestamp] };
};

View File

@@ -1,8 +0,0 @@
import addMigrationFile from '../addMigrationFile';
import { SqlQuery } from '../types';
export default (): (SqlQuery|string)[] => {
return [
addMigrationFile(42),
];
};

View File

@@ -1,10 +0,0 @@
import { SqlQuery } from '../types';
export default (): (SqlQuery|string)[] => {
return [
'ALTER TABLE `notes` ADD COLUMN `user_data` TEXT NOT NULL DEFAULT ""',
'ALTER TABLE `tags` ADD COLUMN `user_data` TEXT NOT NULL DEFAULT ""',
'ALTER TABLE `folders` ADD COLUMN `user_data` TEXT NOT NULL DEFAULT ""',
'ALTER TABLE `resources` ADD COLUMN `user_data` TEXT NOT NULL DEFAULT ""',
];
};

View File

@@ -1,8 +0,0 @@
import { SqlQuery } from '../types';
export default (): (SqlQuery|string)[] => {
return [
'ALTER TABLE `resources` ADD COLUMN blob_updated_time INT NOT NULL DEFAULT 0',
'UPDATE `resources` SET blob_updated_time = updated_time',
];
};

View File

@@ -15,17 +15,6 @@ export interface BaseItemEntity {
created_time?: number;
}
export type SqlParams = any[];
export interface SqlQuery {
sql: string;
params?: SqlParams;
}
export type StringOrSqlQuery = string | SqlQuery;
export type Migration = () => (SqlQuery|string)[];
export enum FolderIconType {
Emoji = 1,
DataUrl = 2,
@@ -150,7 +139,6 @@ export interface FolderEntity {
'title'?: string;
'updated_time'?: number;
'user_created_time'?: number;
'user_data'?: string;
'user_updated_time'?: number;
'type_'?: number;
}
@@ -271,7 +259,6 @@ export interface ResourceEntity {
'title'?: string;
'updated_time'?: number;
'user_created_time'?: number;
'user_data'?: string;
'user_updated_time'?: number;
'type_'?: number;
}
@@ -332,7 +319,6 @@ export interface TagEntity {
'title'?: string;
'updated_time'?: number;
'user_created_time'?: number;
'user_data'?: string;
'user_updated_time'?: number;
'type_'?: number;
}
@@ -366,7 +352,6 @@ export const databaseSchema: DatabaseTables = {
title: { type: 'string' },
updated_time: { type: 'number' },
user_created_time: { type: 'number' },
user_data: { type: 'string' },
user_updated_time: { type: 'number' },
type_: { type: 'number' },
},
@@ -380,7 +365,6 @@ export const databaseSchema: DatabaseTables = {
title: { type: 'string' },
updated_time: { type: 'number' },
user_created_time: { type: 'number' },
user_data: { type: 'string' },
user_updated_time: { type: 'number' },
type_: { type: 'number' },
},
@@ -482,7 +466,6 @@ export const databaseSchema: DatabaseTables = {
title: { type: 'string' },
updated_time: { type: 'number' },
user_created_time: { type: 'number' },
user_data: { type: 'string' },
user_updated_time: { type: 'number' },
type_: { type: 'number' },
},

View File

@@ -72,8 +72,8 @@ export default class RepositoryApi {
// https://github.com/joplin/plugins
// https://api.github.com/repos/joplin/plugins/releases
this.githubApiUrl_ = this.baseUrl_.replace(/^(https:\/\/)(github\.com\/)(.*)$/, '$1api.$2repos/$3');
const defaultContentBaseUrl = this.isLocalRepo ? this.baseUrl_ : `${this.baseUrl_.replace(/github\.com/, 'raw.githubusercontent.com')}/master`;
this.contentBaseUrl_ = this.isLocalRepo ? defaultContentBaseUrl : await findWorkingGitHubUrl(defaultContentBaseUrl);
const defaultContentBaseUrl = `${this.baseUrl_.replace(/github\.com/, 'raw.githubusercontent.com')}/master`;
this.contentBaseUrl_ = await findWorkingGitHubUrl(defaultContentBaseUrl);
this.isUsingDefaultContentUrl_ = this.contentBaseUrl_ === defaultContentBaseUrl;

View File

@@ -3,7 +3,7 @@
import { Size } from './types';
// AUTO-GENERATED by generate-database-type
type ListRendererDatabaseDependency = 'folder.created_time' | 'folder.encryption_applied' | 'folder.encryption_cipher_text' | 'folder.icon' | 'folder.id' | 'folder.is_shared' | 'folder.master_key_id' | 'folder.parent_id' | 'folder.share_id' | 'folder.title' | 'folder.updated_time' | 'folder.user_created_time' | 'folder.user_data' | 'folder.user_updated_time' | 'folder.type_' | 'note.altitude' | 'note.application_data' | 'note.author' | 'note.body' | 'note.conflict_original_id' | 'note.created_time' | 'note.encryption_applied' | 'note.encryption_cipher_text' | 'note.id' | 'note.is_conflict' | 'note.is_shared' | 'note.is_todo' | 'note.latitude' | 'note.longitude' | 'note.markup_language' | 'note.master_key_id' | 'note.order' | 'note.parent_id' | 'note.share_id' | 'note.source' | 'note.source_application' | 'note.source_url' | 'note.title' | 'note.todo_completed' | 'note.todo_due' | 'note.updated_time' | 'note.user_created_time' | 'note.user_data' | 'note.user_updated_time' | 'note.type_';
type ListRendererDatabaseDependency = 'folder.created_time' | 'folder.encryption_applied' | 'folder.encryption_cipher_text' | 'folder.icon' | 'folder.id' | 'folder.is_shared' | 'folder.master_key_id' | 'folder.parent_id' | 'folder.share_id' | 'folder.title' | 'folder.updated_time' | 'folder.user_created_time' | 'folder.user_updated_time' | 'folder.type_' | 'note.altitude' | 'note.application_data' | 'note.author' | 'note.body' | 'note.conflict_original_id' | 'note.created_time' | 'note.encryption_applied' | 'note.encryption_cipher_text' | 'note.id' | 'note.is_conflict' | 'note.is_shared' | 'note.is_todo' | 'note.latitude' | 'note.longitude' | 'note.markup_language' | 'note.master_key_id' | 'note.order' | 'note.parent_id' | 'note.share_id' | 'note.source' | 'note.source_application' | 'note.source_url' | 'note.title' | 'note.todo_completed' | 'note.todo_due' | 'note.updated_time' | 'note.user_created_time' | 'note.user_data' | 'note.user_updated_time' | 'note.type_';
// AUTO-GENERATED by generate-database-type
export enum ItemFlow {

View File

@@ -27,7 +27,6 @@ const { fileExtension, safeFileExtension, safeFilename, filename } = require('..
const { MarkupToHtml } = require('@joplin/renderer');
const { ErrorNotFound } = require('../utils/errors');
import { fileUriToPath } from '@joplin/utils/url';
import { NoteEntity } from '../../database/types';
const logger = Logger.create('routes/notes');
@@ -39,31 +38,7 @@ function htmlToMdParser() {
return htmlToMdParser_;
}
type RequestNote = {
id?: any;
parent_id?: string;
title: string;
body?: string;
latitude?: number;
longitude?: number;
altitude?: number;
author?: string;
source_url?: string;
is_todo?: number;
todo_due?: number;
todo_completed?: number;
user_updated_time?: number;
user_created_time?: number;
markup_language?: number;
body_html: string;
base_url?: string;
convert_to: string;
anchor_names?: any[];
image_sizes?: object;
stylesheets: any;
};
async function requestNoteToNote(requestNote: RequestNote): Promise<NoteEntity> {
async function requestNoteToNote(requestNote: any) {
const output: any = {
title: requestNote.title ? requestNote.title : '',
body: requestNote.body ? requestNote.body : '',
@@ -362,34 +337,6 @@ async function attachImageFromDataUrl(note: any, imageDataUrl: string, cropRect:
return await shim.attachFileToNote(note, tempFilePath);
}
export const extractNoteFromHTML = async (requestNote: RequestNote, requestId: number, imageSizes: any) => {
const note = await requestNoteToNote(requestNote);
const mediaUrls = extractMediaUrls(note.markup_language, note.body);
logger.info(`Request (${requestId}): Downloading media files: ${mediaUrls.length}`);
const mediaFiles = await downloadMediaFiles(mediaUrls); // , allowFileProtocolImages);
logger.info(`Request (${requestId}): Creating resources from paths: ${Object.getOwnPropertyNames(mediaFiles).length}`);
const resources = await createResourcesFromPaths(mediaFiles);
await removeTempFiles(resources);
note.body = replaceUrlsByResources(note.markup_language, note.body, resources, imageSizes);
logger.info(`Request (${requestId}): Saving note...`);
const saveOptions = defaultSaveOptions('POST', note.id);
saveOptions.autoTimestamp = false; // No auto-timestamp because user may have provided them
const timestamp = Date.now();
note.updated_time = timestamp;
note.created_time = timestamp;
if (!('user_updated_time' in note)) note.user_updated_time = timestamp;
if (!('user_created_time' in note)) note.user_created_time = timestamp;
return { note, saveOptions, resources };
};
export default async function(request: Request, id: string = null, link: string = null) {
if (request.method === 'GET') {
if (link && link === 'tags') {
@@ -421,9 +368,31 @@ export default async function(request: Request, id: string = null, link: string
logger.info('Images:', imageSizes);
const extracted = await extractNoteFromHTML(requestNote, requestId, imageSizes);
let note: any = await requestNoteToNote(requestNote);
let note = await Note.save(extracted.note, extracted.saveOptions);
const mediaUrls = extractMediaUrls(note.markup_language, note.body);
logger.info(`Request (${requestId}): Downloading media files: ${mediaUrls.length}`);
let result = await downloadMediaFiles(mediaUrls); // , allowFileProtocolImages);
logger.info(`Request (${requestId}): Creating resources from paths: ${Object.getOwnPropertyNames(result).length}`);
result = await createResourcesFromPaths(result);
await removeTempFiles(result);
note.body = replaceUrlsByResources(note.markup_language, note.body, result, imageSizes);
logger.info(`Request (${requestId}): Saving note...`);
const saveOptions = defaultSaveOptions('POST', note.id);
saveOptions.autoTimestamp = false; // No auto-timestamp because user may have provided them
const timestamp = Date.now();
note.updated_time = timestamp;
note.created_time = timestamp;
if (!('user_updated_time' in note)) note.user_updated_time = timestamp;
if (!('user_created_time' in note)) note.user_created_time = timestamp;
note = await Note.save(note, saveOptions);
if (requestNote.tags) {
const tagTitles = requestNote.tags.split(',');

View File

@@ -1,8 +1,8 @@
import { SqlQuery } from '../../database';
import JoplinDatabase from '../../JoplinDatabase';
import BaseItem from '../../models/BaseItem';
import Setting from '../../models/Setting';
import SyncTargetRegistry from '../../SyncTargetRegistry';
import { SqlQuery } from '../database/types';
async function clearSyncContext() {
const syncTargetIds = SyncTargetRegistry.allIds();

View File

@@ -142,12 +142,12 @@ const features = (): Record<FeatureId, PlanFeature> => {
basic: true,
pro: true,
teams: true,
basicInfo: _('%d GB storage space', 2),
proInfo: _('%d GB storage space', 30),
teamsInfo: _('%d GB storage space', 50),
basicInfoShort: _('%d GB', 2),
proInfoShort: _('%d GB', 30),
teamsInfoShort: _('%d GB', 50),
basicInfo: _('%d GB storage space', 1),
proInfo: _('%d GB storage space', 10),
teamsInfo: _('%d GB storage space', 10),
basicInfoShort: _('%d GB', 1),
proInfoShort: _('%d GB', 10),
teamsInfoShort: _('%d GB', 10),
},
publishNote: {
title: _('Publish notes to the internet'),

View File

@@ -25,13 +25,6 @@ const userFetcher = async () => {
const fileApi = await syncTarget.fileApi();
const api = fileApi.driver().api();
if (!api.userId) {
// That can happen if we don't have a session yet or if it has been
// cleared
logger.info('Skipping fetching user because user ID is not available');
return;
}
const owner: UserApiResponse = await api.exec('GET', `api/users/${api.userId}`);
logger.info('Got user:', owner);

View File

@@ -21,8 +21,8 @@
"devDependencies": {
"@types/jest": "29.5.4",
"@types/pdfjs-dist": "2.10.378",
"@types/react": "18.2.24",
"@types/react-dom": "18.2.8",
"@types/react": "18.2.23",
"@types/react-dom": "18.2.7",
"@types/styled-components": "5.1.28",
"babel-jest": "29.6.4",
"css-loader": "6.8.1",

View File

@@ -35,7 +35,7 @@
"highlight.js": "11.8.0",
"html-entities": "1.4.0",
"json-stringify-safe": "5.0.1",
"katex": "0.16.9",
"katex": "0.16.8",
"markdown-it": "13.0.2",
"markdown-it-abbr": "1.0.4",
"markdown-it-anchor": "5.3.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/server",
"version": "2.13.2",
"version": "2.13.1",
"private": true,
"scripts": {
"start-dev": "yarn run build && JOPLIN_IS_TESTING=1 nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",
@@ -69,9 +69,9 @@
"@types/jsdom": "21.1.3",
"@types/koa": "2.13.9",
"@types/markdown-it": "12.2.3",
"@types/mustache": "4.2.3",
"@types/mustache": "4.2.2",
"@types/nodemailer": "6.4.11",
"@types/yargs": "17.0.26",
"@types/yargs": "17.0.25",
"@types/zxcvbn": "4.4.2",
"gulp": "4.0.2",
"jest": "29.6.4",

View File

@@ -6,7 +6,7 @@ import { md5 } from '../utils/crypto';
import { ErrorResyncRequired } from '../utils/errors';
import { Day, formatDateTime } from '../utils/time';
import BaseModel, { SaveOptions } from './BaseModel';
import { PaginatedResults } from './utils/pagination';
import { PaginatedResults, Pagination, PaginationOrderDir } from './utils/pagination';
const logger = Logger.create('ChangeModel');
@@ -88,44 +88,7 @@ export default class ChangeModel extends BaseModel<Change> {
};
}
// private changesForUserQuery(userId: Uuid, count: boolean): Knex.QueryBuilder {
// // When need to get:
// //
// // - All the CREATE and DELETE changes associated with the user
// // - All the UPDATE changes that applies to items associated with the
// // user.
// //
// // UPDATE changes do not have the user_id set because they are specific
// // to the item, not to a particular user.
// const query = this
// .db('changes')
// .where(function() {
// void this.whereRaw('((type = ? OR type = ?) AND user_id = ?)', [ChangeType.Create, ChangeType.Delete, userId])
// // Need to use a RAW query here because Knex has a "not a
// // bug" bug that makes it go into infinite loop in some
// // contexts, possibly only when running inside Jest (didn't
// // test outside).
// // https://github.com/knex/knex/issues/1851
// .orWhereRaw('type = ? AND item_id IN (SELECT item_id FROM user_items WHERE user_id = ?)', [ChangeType.Update, userId]);
// });
// if (count) {
// void query.countDistinct('id', { as: 'total' });
// } else {
// void query.select([
// 'id',
// 'item_id',
// 'item_name',
// 'type',
// 'updated_time',
// ]);
// }
// return query;
// }
public async changesForUserQuery(userId: Uuid, fromCounter: number, limit: number, doCountQuery: boolean): Promise<Change[]> {
private changesForUserQuery(userId: Uuid, count: boolean): Knex.QueryBuilder {
// When need to get:
//
// - All the CREATE and DELETE changes associated with the user
@@ -135,125 +98,61 @@ export default class ChangeModel extends BaseModel<Change> {
// UPDATE changes do not have the user_id set because they are specific
// to the item, not to a particular user.
// This used to be just one query but it kept getting slower and slower
// as the `changes` table grew. So it is now split into two queries
// merged by a UNION ALL.
const query = this
.db('changes')
.where(function() {
void this.whereRaw('((type = ? OR type = ?) AND user_id = ?)', [ChangeType.Create, ChangeType.Delete, userId])
// Need to use a RAW query here because Knex has a "not a
// bug" bug that makes it go into infinite loop in some
// contexts, possibly only when running inside Jest (didn't
// test outside).
// https://github.com/knex/knex/issues/1851
.orWhereRaw('type = ? AND item_id IN (SELECT item_id FROM user_items WHERE user_id = ?)', [ChangeType.Update, userId]);
});
const fields = [
'id',
'item_id',
'item_name',
'type',
'updated_time',
'counter',
];
const fieldsSql = `"${fields.join('", "')}"`;
const subQuery1 = `
SELECT ${fieldsSql}
FROM "changes"
WHERE counter > ?
AND (type = ? OR type = ?)
AND user_id = ?
ORDER BY "counter" ASC
${doCountQuery ? '' : 'LIMIT ?'}
`;
const subParams1 = [
fromCounter,
ChangeType.Create,
ChangeType.Delete,
userId,
];
if (!doCountQuery) subParams1.push(limit);
const subQuery2 = `
SELECT ${fieldsSql}
FROM "changes"
WHERE counter > ?
AND type = ?
AND item_id IN (SELECT item_id FROM user_items WHERE user_id = ?)
ORDER BY "counter" ASC
${doCountQuery ? '' : 'LIMIT ?'}
`;
const subParams2 = [
fromCounter,
ChangeType.Update,
userId,
];
if (!doCountQuery) subParams2.push(limit);
let query: Knex.Raw<any> = null;
const finalParams = subParams1.concat(subParams2);
if (!doCountQuery) {
finalParams.push(limit);
query = this.db.raw(`
SELECT ${fieldsSql} FROM (${subQuery1}) as sub1
UNION ALL
SELECT ${fieldsSql} FROM (${subQuery2}) as sub2
ORDER BY counter ASC
LIMIT ?
`, finalParams);
if (count) {
void query.countDistinct('id', { as: 'total' });
} else {
query = this.db.raw(`
SELECT count(*) as total
FROM (
(${subQuery1})
UNION ALL
(${subQuery2})
) AS merged
`, finalParams);
void query.select([
'id',
'item_id',
'item_name',
'type',
'updated_time',
]);
}
const results = await query;
// Because it's a raw query, we need to handle the results manually:
// Postgres returns an object with a "rows" property, while SQLite
// returns the rows directly;
const output: Change[] = results.rows ? results.rows : results;
// This property is present only for the purpose of ordering the results
// and can be removed afterwards.
for (const change of output) delete change.counter;
return output;
return query;
}
// public async allByUser(userId: Uuid, pagination: Pagination = null): Promise<PaginatedDeltaChanges> {
// pagination = {
// page: 1,
// limit: 100,
// order: [{ by: 'counter', dir: PaginationOrderDir.ASC }],
// ...pagination,
// };
public async allByUser(userId: Uuid, pagination: Pagination = null): Promise<PaginatedDeltaChanges> {
pagination = {
page: 1,
limit: 100,
order: [{ by: 'counter', dir: PaginationOrderDir.ASC }],
...pagination,
};
// const query = this.changesForUserQuery(userId, false);
// const countQuery = this.changesForUserQuery(userId, true);
// const itemCount = (await countQuery.first()).total;
const query = this.changesForUserQuery(userId, false);
const countQuery = this.changesForUserQuery(userId, true);
const itemCount = (await countQuery.first()).total;
// void query
// .orderBy(pagination.order[0].by, pagination.order[0].dir)
// .offset((pagination.page - 1) * pagination.limit)
// .limit(pagination.limit) as any[];
void query
.orderBy(pagination.order[0].by, pagination.order[0].dir)
.offset((pagination.page - 1) * pagination.limit)
.limit(pagination.limit) as any[];
// const changes = await query;
const changes = await query;
// return {
// items: changes,
// // If we have changes, we return the ID of the latest changes from which delta sync can resume.
// // If there's no change, we return the previous cursor.
// cursor: changes.length ? changes[changes.length - 1].id : pagination.cursor,
// has_more: changes.length >= pagination.limit,
// page_count: itemCount !== null ? Math.ceil(itemCount / pagination.limit) : undefined,
// };
// }
return {
items: changes,
// If we have changes, we return the ID of the latest changes from which delta sync can resume.
// If there's no change, we return the previous cursor.
cursor: changes.length ? changes[changes.length - 1].id : pagination.cursor,
has_more: changes.length >= pagination.limit,
page_count: itemCount !== null ? Math.ceil(itemCount / pagination.limit) : undefined,
};
}
public async delta(userId: Uuid, pagination: ChangePagination = null): Promise<PaginatedDeltaChanges> {
pagination = {
@@ -268,12 +167,18 @@ export default class ChangeModel extends BaseModel<Change> {
if (!changeAtCursor) throw new ErrorResyncRequired();
}
const changes = await this.changesForUserQuery(
userId,
changeAtCursor ? changeAtCursor.counter : -1,
pagination.limit,
false,
);
const query = this.changesForUserQuery(userId, false);
// If a cursor was provided, apply it to the query.
if (changeAtCursor) {
void query.where('counter', '>', changeAtCursor.counter);
}
void query
.orderBy('counter', 'asc')
.limit(pagination.limit) as any[];
const changes: Change[] = await query;
const items: Item[] = await this.db('items').select('id', 'jop_updated_time').whereIn('items.id', changes.map(c => c.item_id));

View File

@@ -428,14 +428,9 @@ describe('UserModel', () => {
test('should throw an error if the password being saved seems to be hashed', async () => {
const passwordSimilarToHash = '$2a$10';
const user = await models().user().save({
email: 'test@example.com',
password: '111111',
});
const error = await checkThrowAsync(async () => await models().user().save({ password: passwordSimilarToHash }));
const error = await checkThrowAsync(async () => await models().user().save({ id: user.id, password: passwordSimilarToHash }));
expect(error.message).toBe(`Unable to save user because password already seems to be hashed. User id: ${user.id}`);
expect(error.message).toBe('Unable to save user because password already seems to be hashed. User id: undefined');
expect(error instanceof ErrorBadRequest).toBe(true);
});

View File

@@ -640,9 +640,6 @@ export default class UserModel extends BaseModel<User> {
if (user.password) {
if (isHashedPassword(user.password)) {
if (!isNew) {
// We have this check because if an existing user is loaded,
// then saved again, the "password" field will be hashed a
// second time, and we don't want this.
throw new ErrorBadRequest(`Unable to save user because password already seems to be hashed. User id: ${user.id}`);
} else {
// OK - We allow supplying an already hashed password for

View File

@@ -1,62 +1,52 @@
// Disabled for now
import { beforeAllDb, afterAllTests, beforeEachDb, createItemTree, createUserAndSession, parseHtml } from '../../utils/testing/testUtils';
import { execRequest } from '../../utils/testing/apiUtils';
describe('index_changes', () => {
it('should pass', () => {
expect(true).toBe(true);
beforeAll(async () => {
await beforeAllDb('index_changes');
});
afterAll(async () => {
await afterAllTests();
});
beforeEach(async () => {
await beforeEachDb();
});
test('should list changes', async () => {
const { user: user1, session: session1 } = await createUserAndSession(1, true);
const items: any = {};
for (let i = 1; i <= 150; i++) {
items[(`${i}`).padStart(32, '0')] = {};
}
await createItemTree(user1.id, '', items);
// Just some basic tests to check that we're seeing at least the first
// and last item of each page.
{
const response: string = await execRequest(session1.id, 'GET', 'changes');
const navLinks = parseHtml(response).querySelectorAll('.pagination-link');
expect(response.includes('00000000000000000000000000000150.md')).toBe(true);
expect(response.includes('00000000000000000000000000000051.md')).toBe(true);
expect(navLinks.length).toBe(2);
expect(navLinks[0].getAttribute('class')).toContain('is-current');
expect(navLinks[1].getAttribute('class')).not.toContain('is-current');
}
{
const response: string = await execRequest(session1.id, 'GET', 'changes', null, { query: { page: 2 } });
const navLinks = parseHtml(response).querySelectorAll('.pagination-link');
expect(response.includes('00000000000000000000000000000050.md')).toBe(true);
expect(response.includes('00000000000000000000000000000001.md')).toBe(true);
expect(navLinks.length).toBe(2);
expect(navLinks[0].getAttribute('class')).not.toContain('is-current');
expect(navLinks[1].getAttribute('class')).toContain('is-current');
}
});
});
// import { beforeAllDb, afterAllTests, beforeEachDb, createItemTree, createUserAndSession, parseHtml } from '../../utils/testing/testUtils';
// import { execRequest } from '../../utils/testing/apiUtils';
// describe('index_changes', () => {
// beforeAll(async () => {
// await beforeAllDb('index_changes');
// });
// afterAll(async () => {
// await afterAllTests();
// });
// beforeEach(async () => {
// await beforeEachDb();
// });
// test('should list changes', async () => {
// const { user: user1, session: session1 } = await createUserAndSession(1, true);
// const items: any = {};
// for (let i = 1; i <= 150; i++) {
// items[(`${i}`).padStart(32, '0')] = {};
// }
// await createItemTree(user1.id, '', items);
// // Just some basic tests to check that we're seeing at least the first
// // and last item of each page.
// {
// const response: string = await execRequest(session1.id, 'GET', 'changes');
// const navLinks = parseHtml(response).querySelectorAll('.pagination-link');
// expect(response.includes('00000000000000000000000000000150.md')).toBe(true);
// expect(response.includes('00000000000000000000000000000051.md')).toBe(true);
// expect(navLinks.length).toBe(2);
// expect(navLinks[0].getAttribute('class')).toContain('is-current');
// expect(navLinks[1].getAttribute('class')).not.toContain('is-current');
// }
// {
// const response: string = await execRequest(session1.id, 'GET', 'changes', null, { query: { page: 2 } });
// const navLinks = parseHtml(response).querySelectorAll('.pagination-link');
// expect(response.includes('00000000000000000000000000000050.md')).toBe(true);
// expect(response.includes('00000000000000000000000000000001.md')).toBe(true);
// expect(navLinks.length).toBe(2);
// expect(navLinks[0].getAttribute('class')).not.toContain('is-current');
// expect(navLinks[1].getAttribute('class')).toContain('is-current');
// }
// });
// });

View File

@@ -2,74 +2,69 @@ import { SubPath } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
// import { changeTypeToString } from '../../services/database/types';
// import { PaginationOrderDir } from '../../models/utils/pagination';
// import { formatDateTime } from '../../utils/time';
// import defaultView from '../../utils/defaultView';
// import { View } from '../../services/MustacheService';
// import { makeTablePagination, Table, Row, makeTableView } from '../../utils/views/table';
// import config, { showItemUrls } from '../../config';
import { changeTypeToString } from '../../services/database/types';
import { PaginationOrderDir } from '../../models/utils/pagination';
import { formatDateTime } from '../../utils/time';
import defaultView from '../../utils/defaultView';
import { View } from '../../services/MustacheService';
import { makeTablePagination, Table, Row, makeTableView } from '../../utils/views/table';
import config, { showItemUrls } from '../../config';
import { ErrorForbidden } from '../../utils/errors';
const router = new Router(RouteType.Web);
router.get('changes', async (_path: SubPath, _ctx: AppContext) => {
// We disable this because it is too slow to retrieve all the changes and
// could easily lock a database. If we need a way to inspect the log there
// would have to be a different, more efficient way to do it.
throw new ErrorForbidden('Disabled');
router.get('changes', async (_path: SubPath, ctx: AppContext) => {
if (!ctx.joplin.owner.is_admin) throw new ErrorForbidden();
// if (!ctx.joplin.owner.is_admin) throw new ErrorForbidden();
const pagination = makeTablePagination(ctx.query, 'updated_time', PaginationOrderDir.DESC);
const paginatedChanges = await ctx.joplin.models.change().allByUser(ctx.joplin.owner.id, pagination);
const items = await ctx.joplin.models.item().loadByIds(paginatedChanges.items.map(i => i.item_id), { fields: ['id'] });
// const pagination = makeTablePagination(ctx.query, 'updated_time', PaginationOrderDir.DESC);
// const paginatedChanges = await ctx.joplin.models.change().allByUser(ctx.joplin.owner.id, pagination);
// const items = await ctx.joplin.models.item().loadByIds(paginatedChanges.items.map(i => i.item_id), { fields: ['id'] });
const table: Table = {
baseUrl: ctx.joplin.models.change().changeUrl(),
requestQuery: ctx.query,
pageCount: paginatedChanges.page_count,
pagination,
headers: [
{
name: 'item_name',
label: 'Name',
stretch: true,
},
{
name: 'type',
label: 'Type',
},
{
name: 'updated_time',
label: 'Timestamp',
},
],
rows: paginatedChanges.items.map(change => {
const row: Row = {
items: [
{
value: change.item_name,
stretch: true,
url: showItemUrls(config()) ? (items.find(i => i.id === change.item_id) ? ctx.joplin.models.item().itemContentUrl(change.item_id) : '') : null,
},
{
value: changeTypeToString(change.type),
},
{
value: formatDateTime(change.updated_time),
},
],
};
// const table: Table = {
// baseUrl: ctx.joplin.models.change().changeUrl(),
// requestQuery: ctx.query,
// pageCount: paginatedChanges.page_count,
// pagination,
// headers: [
// {
// name: 'item_name',
// label: 'Name',
// stretch: true,
// },
// {
// name: 'type',
// label: 'Type',
// },
// {
// name: 'updated_time',
// label: 'Timestamp',
// },
// ],
// rows: paginatedChanges.items.map(change => {
// const row: Row = {
// items: [
// {
// value: change.item_name,
// stretch: true,
// url: showItemUrls(config()) ? (items.find(i => i.id === change.item_id) ? ctx.joplin.models.item().itemContentUrl(change.item_id) : '') : null,
// },
// {
// value: changeTypeToString(change.type),
// },
// {
// value: formatDateTime(change.updated_time),
// },
// ],
// };
return row;
}),
};
// return row;
// }),
// };
// const view: View = defaultView('changes', 'Log');
// view.content.changeTable = makeTableView(table),
// view.cssFiles = ['index/changes'];
// return view;
const view: View = defaultView('changes', 'Log');
view.content.changeTable = makeTableView(table),
view.cssFiles = ['index/changes'];
return view;
});
export default router;

View File

@@ -9,7 +9,7 @@ import { makeUrl, SubPath, UrlType } from '../utils/routeUtils';
import MarkdownIt = require('markdown-it');
import { headerAnchor } from '@joplin/renderer';
import { _ } from '@joplin/lib/locale';
import { adminDashboardUrl, adminEmailsUrl, adminTasksUrl, adminUserDeletionsUrl, adminUsersUrl, homeUrl, itemsUrl } from '../utils/urlUtils';
import { adminDashboardUrl, adminEmailsUrl, adminTasksUrl, adminUserDeletionsUrl, adminUsersUrl, changesUrl, homeUrl, itemsUrl } from '../utils/urlUtils';
import { MenuItem, setSelectedMenu } from '../utils/views/menu';
export interface RenderOptions {
@@ -150,6 +150,10 @@ export default class MustacheService {
title: _('Items'),
url: itemsUrl(),
},
{
title: _('Logs'),
url: changesUrl(),
},
{
title: _('Admin'),
url: adminDashboardUrl(),

View File

@@ -46,11 +46,11 @@
"@rmp135/sql-ts": "1.18.0",
"@types/fs-extra": "11.0.2",
"@types/jest": "29.5.4",
"@types/markdown-it": "13.0.2",
"@types/mustache": "4.2.3",
"@types/markdown-it": "13.0.1",
"@types/mustache": "4.2.2",
"@types/node": "18.17.19",
"@types/node-fetch": "2.6.6",
"@types/yargs": "17.0.26",
"@types/yargs": "17.0.25",
"gettext-extractor": "3.8.0",
"gulp": "4.0.2",
"html-entities": "1.4.0",

View File

@@ -1,10 +1,5 @@
# Joplin changelog
## [v2.12.19](https://github.com/laurent22/joplin/releases/tag/v2.12.19) - 2023-10-21T09:39:18Z
- Security: Update Electron to 25.9.0 ([#9049](https://github.com/laurent22/joplin/issues/9049) by Henry Heino)
- Fixed: Fixed issues related to sharing notes on read-only notebooks ([afaa2a7](https://github.com/laurent22/joplin/commit/afaa2a7))
## [v2.13.2](https://github.com/laurent22/joplin/releases/tag/v2.13.2) (Pre-release) - 2023-10-06T17:00:07Z
- New: Add new beta Markdown editor based on CodeMirror 6 ([#8793](https://github.com/laurent22/joplin/issues/8793) by Henry Heino)
@@ -265,6 +260,11 @@
- Fixed: Drag-dropping notes to top or bottom, in custom sort, is finicky ([#7777](https://github.com/laurent22/joplin/issues/7777)) ([#7776](https://github.com/laurent22/joplin/issues/7776) by Tao Klerks)
- Fixed: Linux notebook display bug ([#7897](https://github.com/laurent22/joplin/issues/7897)) ([#7506](https://github.com/laurent22/joplin/issues/7506) by Arun Kumar)
## [v2.10.8](https://github.com/laurent22/joplin/releases/tag/v2.10.8) (Pre-release) - 2023-02-26T12:53:55Z
- Improved: Note background does not change when theme automatically updated via system ([d1e545a](https://github.com/laurent22/joplin/commit/d1e545a))
- Fixed: Fixed clipping certain pages that contain images within links ([92cf5ab](https://github.com/laurent22/joplin/commit/92cf5ab))
## [v2.10.7](https://github.com/laurent22/joplin/releases/tag/v2.10.7) (Pre-release) - 2023-02-24T10:56:20Z
- New: Add a link to twitter inside the help menu ([#7796](https://github.com/laurent22/joplin/issues/7796) by [@pedr](https://github.com/pedr))
@@ -456,6 +456,11 @@
- Fixed: Prevent certain errors from stopping the revision service ([#5531](https://github.com/laurent22/joplin/issues/5531))
- Fixed: Note export could fail in some cases (regression) ([#6203](https://github.com/laurent22/joplin/issues/6203))
## [v2.7.13](https://github.com/laurent22/joplin/releases/tag/v2.7.13) - 2022-02-24T17:42:12Z
- Fixed: Fixed search marker background color in Markdown editor ([440618e](https://github.com/laurent22/joplin/commit/440618e))
- Updated translations
## [v2.7.12](https://github.com/laurent22/joplin/releases/tag/v2.7.12) (Pre-release) - 2022-02-14T15:06:14Z
- Fixed: Exported JEX notebook should not contain share metadata ([#6129](https://github.com/laurent22/joplin/issues/6129))
@@ -542,15 +547,6 @@ Important: If you use custom notebook icons and sync with the mobile app, make s
- New: Added detailed tooltip for 'Toggle Sort Order Field' button ([#5854](https://github.com/laurent22/joplin/issues/5854) by Kenichi Kobayashi)
- Fixed (Regression): Scroll positions are preserved ([#5826](https://github.com/laurent22/joplin/issues/5826)) ([#5708](https://github.com/laurent22/joplin/issues/5708) by Kenichi Kobayashi)
## [v2.6.9](https://github.com/laurent22/joplin/releases/tag/v2.6.9) - 2021-12-17T11:57:32Z
- Update translations
## [v2.6.7](https://github.com/laurent22/joplin/releases/tag/v2.6.7) (Pre-release) - 2021-12-16T10:47:23Z
- New: Added detailed tooltip for 'Toggle Sort Order Field' button ([#5854](https://github.com/laurent22/joplin/issues/5854) by Kenichi Kobayashi)
- Fixed (Regression): Scroll positions are preserved ([#5826](https://github.com/laurent22/joplin/issues/5826)) ([#5708](https://github.com/laurent22/joplin/issues/5708) by Kenichi Kobayashi)
## [v2.6.6](https://github.com/laurent22/joplin/releases/tag/v2.6.6) (Pre-release) - 2021-12-13T12:31:43Z
- Improved: Changed note sort buttons to 3px radius ([#5771](https://github.com/laurent22/joplin/issues/5771) by [@Daeraxa](https://github.com/Daeraxa))

View File

@@ -1,11 +1,5 @@
# Joplin Server Changelog
## [server-v2.13.2](https://github.com/laurent22/joplin/releases/tag/server-v2.13.2) - 2023-10-19T19:49:44Z
- Improved: Significantly improve sync performances, especially when there are many changes (5986710)
- Improved: Updated packages compare-versions (v6.1.0), dayjs (v1.11.10), follow-redirects (v1.15.3), glob (v10.3.6), katex (v0.16.8), markdown-it (v13.0.2), node-mocks-http (v1.13.0), nodemailer (v6.9.5), nodemon (v3.0.1), react, sass (v1.66.1), sharp (v0.32.6), sprintf-js (v1.1.3), tar (v6.2.0), uuid (v9.0.1)
- Fixed: Fixed publishing logo (01f37df)
## [server-v2.13.1](https://github.com/laurent22/joplin/releases/tag/server-v2.13.1) - 2023-09-20T15:15:32Z
- New: Add Joplin Server and Joplin Cloud favicons (1b00445)

View File

@@ -743,14 +743,6 @@
"created_at": "2023-09-28T16:40:13Z",
"repoId": 79162682,
"pullRequestNo": 8980
},
{
"name": "PiotrNarel",
"id": 22528145,
"comment_id": 1771460591,
"created_at": "2023-10-19T17:55:59Z",
"repoId": 79162682,
"pullRequestNo": 9095
}
]
}

View File

@@ -10,7 +10,7 @@ Your download of <span class="downloaded-filename">Joplin</span> is in progress.
Access your notes on Windows, macOS or Linux.
<!-- DESKTOP-DOWNLOAD-LINKS --><a href='https://objects.joplinusercontent.com/v2.12.19/Joplin-Setup-2.12.19.exe?source=JoplinWebsite&type=New'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a> <a href='https://objects.joplinusercontent.com/v2.12.19/Joplin-2.12.19.dmg?source=JoplinWebsite&type=New'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a> <a href='https://objects.joplinusercontent.com/v2.12.19/Joplin-2.12.19.AppImage?source=JoplinWebsite&type=New'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a><!-- DESKTOP-DOWNLOAD-LINKS -->
<!-- DESKTOP-DOWNLOAD-LINKS --><a href='https://objects.joplinusercontent.com/v2.12.18/Joplin-Setup-2.12.18.exe?source=JoplinWebsite&type=New'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a> <a href='https://objects.joplinusercontent.com/v2.12.18/Joplin-2.12.18.dmg?source=JoplinWebsite&type=New'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a> <a href='https://objects.joplinusercontent.com/v2.12.18/Joplin-2.12.18.AppImage?source=JoplinWebsite&type=New'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a><!-- DESKTOP-DOWNLOAD-LINKS -->
</div>

View File

@@ -1,31 +0,0 @@
---
tweet: Working in the shadows with white-hat hackers
---
# Working in the shadows with white-hat hackers
The majority of Joplin's development is carried out in the public domain. This includes the discussion of issues on GitHub, as well as the submission of pull requests and related discussions. The transparency of these processes allows for collaborative problem-solving and shared insights.
However, there is one aspect that operates behind closed doors, and for good reason: addressing cybersecurity vulnerabilities. It is imperative that these issues remain undisclosed until they have been resolved. Once a solution is implemented, it is usually accompanied by discreet commits and a message in the changelog to signify the progress made.
Typically, the process begins with an email from a security researcher. They provide valuable insights, such as a specially crafted note that triggers a bug, or an API call, along with an explanation of how the application's security can be circumvented. We examine the vulnerability, create a fix, and create automated test units to prevent any accidental reintroduction of the vulnerability in future code updates. An example of such a commit is: [9e90d9016daf79b5414646a93fd369aedb035071](https://github.com/laurent22/joplin/commit/9e90d9016daf79b5414646a93fd369aedb035071)
We then share our fix with the researcher for validation. Additionally, we often apply the fix to previous versions of Joplin, depending on the severity of the vulnerability.
The contribution of security researchers in this regard is immeasurable. They employ their ingenuity to identify inventive methods of bypassing existing security measures and often discover subtle flaws in the code that might otherwise go unnoticed.
We would like to express our sincere gratitude to the security researchers who have assisted us throughout the years in identifying and rectifying security vulnerabilities!
- [@Alise](https://github.com/a1ise)
- @hexodotsh
- [@ly1g3](https://github.com/ly1g3)
- [@maple3142](https://twitter.com/maple3142)
- Ademar Nowasky Junior
- [Benjamin Harris](mailto:ben@mayhem.sg)
- [Javier Olmedo](https://github.com/JavierOlmedo)
- [Jubair Rehman Yousafzai](https://twitter.com/newfolderj)
- lin@UCCU Hacker
- [personalizedrefrigerator](https://github.com/personalizedrefrigerator)
- [Phil Holbrook](https://twitter.com/fhlipZero)
- [RyotaK](https://ryotak.net/)
- [Yaniv Nizry](https://twitter.com/YNizry)

View File

@@ -32,14 +32,6 @@ Additionally, every few minutes, the client is going to poll the server and down
- `packages/lib/*Api.ts`: The `file-api-driver` will call some low-level API to perform its operations. For example `file-api-driver-local` will use the `fs` package to read/write files, `file-api-driver-amazon-s3` will use the AWS API to work with S3. In some cases however such a low-level API is not available - in that case, we usually create an `*Api.ts` file, which is used by the file API driver to perform its operations. For example, there is a `JoplinServerApi.ts`, which is used to connect to Joplin Server.
- In general, each object in the database is represented by a `BaseModel` class. Then each object than can be synced is represented by a `BaseItem` class that inherits from `BaseModel`. This class is where many sync-related utilities can be found such as `itemsThatNeedSync()` or methods that encrypt items so that they can be uploaded when E2EE is enabled.
- The state of each item is saved to the `sync_items` table. There is saved in particular the `sync_time` property which tells when the item was last synced. It is then used to decide what needs to be synced or not. Additional sync-related properties include `sync_disabled`, which is used in the rare case an item cannot be synced at all - for example if blocked by Dropbox for being "restricted content" (copyrighted), or is over the limit on Joplin Cloud. Each entry in `sync_items` is scoped to a sync target (`sync_target` property), so theoretically it's possible to sync the same items to multiple sync targets.
## Testing
By default, the test units synchronise with an in-memory sync target, which is fast and is usually enough to verify most behaviours. The test units however can be configured to sync with a specific sync target, such as the file system, Nextcloud, Joplin Server, etc. To do so, modify `packages/lib/testing/test-utils.ts` and change `setSyncTargetName()` to the relevant sync target. You may also need to add or modify the relevant files in `~/joplin-credentials/*`. See the `initFileApi()` method in `test-utils.ts` for more details.
## See also
- [Synchronisation lock](https://github.com/laurent22/joplin/blob/dev/readme/spec/sync_lock.md)

238
yarn.lock
View File

@@ -4664,10 +4664,10 @@ __metadata:
"@playwright/test": 1.38.1
"@testing-library/react-hooks": 8.0.1
"@types/jest": 29.5.4
"@types/mustache": 4.2.3
"@types/mustache": 4.2.2
"@types/node": 18.17.19
"@types/react": 18.2.24
"@types/react-redux": 7.1.27
"@types/react": 18.2.23
"@types/react-redux": 7.1.26
"@types/styled-components": 5.1.28
async-mutex: 0.4.0
codemirror: 5.65.9
@@ -4675,7 +4675,7 @@ __metadata:
compare-versions: 6.1.0
countable: 3.0.1
debounce: 1.2.1
electron: 25.9.0
electron: 25.8.1
electron-builder: 24.4.0
electron-window-state: 5.0.3
formatcoords: 1.1.3
@@ -4701,8 +4701,8 @@ __metadata:
react: 18.2.0
react-datetime: 3.2.0
react-dom: 18.2.0
react-redux: 8.1.3
react-select: 5.7.7
react-redux: 8.1.2
react-select: 5.7.5
react-test-renderer: 18.2.0
react-toggle-button: 2.2.0
react-tooltip: 4.5.1
@@ -4745,7 +4745,7 @@ __metadata:
"@lezer/highlight": 1.1.4
"@react-native-community/clipboard": 1.5.1
"@react-native-community/datetimepicker": 7.5.0
"@react-native-community/geolocation": 3.1.0
"@react-native-community/geolocation": 3.0.6
"@react-native-community/netinfo": 9.4.1
"@react-native-community/push-notification-ios": 1.11.0
"@react-native-community/slider": 4.4.3
@@ -4754,9 +4754,9 @@ __metadata:
"@tsconfig/react-native": 2.0.2
"@types/fs-extra": 11.0.2
"@types/jest": 29.5.4
"@types/react": 18.2.24
"@types/react": 18.2.23
"@types/react-native": 0.70.6
"@types/react-redux": 7.1.27
"@types/react-redux": 7.1.26
"@types/tar-stream": 2.2.3
assert-browserify: 2.0.0
babel-jest: 29.6.4
@@ -4789,7 +4789,7 @@ __metadata:
react-native-device-info: 10.9.0
react-native-dialogbox: 0.6.10
react-native-document-picker: 9.0.1
react-native-dropdownalert: 5.1.0
react-native-dropdownalert: 4.5.1
react-native-exit-app: 2.0.0
react-native-file-viewer: 2.1.5
react-native-fingerprint-scanner: 6.0.0
@@ -4811,9 +4811,9 @@ __metadata:
react-native-vector-icons: 10.0.0
react-native-version-info: 1.1.1
react-native-vosk: 0.1.12
react-native-webview: 13.6.0
react-native-webview: 13.5.1
react-native-zip-archive: 6.1.0
react-redux: 8.1.3
react-redux: 8.1.2
react-test-renderer: 18.2.0
redux: 4.2.1
rn-fetch-blob: 0.12.0
@@ -4856,8 +4856,8 @@ __metadata:
"@replit/codemirror-vim": 6.0.14
"@testing-library/react-hooks": 8.0.1
"@types/jest": 29.5.4
"@types/react": 18.2.24
"@types/react-redux": 7.1.27
"@types/react": 18.2.23
"@types/react-redux": 7.1.26
"@types/styled-components": 5.1.28
jest: 29.6.3
jest-environment-jsdom: 29.6.3
@@ -4892,7 +4892,7 @@ __metadata:
resolution: "@joplin/fork-sax@workspace:packages/fork-sax"
dependencies:
standard: 17.1.0
tap: 16.3.9
tap: 16.3.8
languageName: unknown
linkType: soft
@@ -4939,7 +4939,7 @@ __metadata:
"@types/nanoid": 3.0.0
"@types/node": 18.17.19
"@types/node-rsa": 1.1.2
"@types/react": 18.2.24
"@types/react": 18.2.23
"@types/uuid": 9.0.4
async-mutex: 0.4.0
base-64: 1.0.0
@@ -5011,8 +5011,8 @@ __metadata:
"@joplin/lib": ~2.13
"@types/jest": 29.5.4
"@types/pdfjs-dist": 2.10.378
"@types/react": 18.2.24
"@types/react-dom": 18.2.8
"@types/react": 18.2.23
"@types/react-dom": 18.2.7
"@types/styled-components": 5.1.28
async-mutex: 0.4.0
babel-jest: 29.6.4
@@ -5103,7 +5103,7 @@ __metadata:
jest: 29.6.4
jest-environment-jsdom: 29.6.4
json-stringify-safe: 5.0.1
katex: 0.16.9
katex: 0.16.8
markdown-it: 13.0.2
markdown-it-abbr: 1.0.4
markdown-it-anchor: 5.3.0
@@ -5144,10 +5144,10 @@ __metadata:
"@types/jsdom": 21.1.3
"@types/koa": 2.13.9
"@types/markdown-it": 12.2.3
"@types/mustache": 4.2.3
"@types/mustache": 4.2.2
"@types/nodemailer": 6.4.11
"@types/uuid": 9.0.4
"@types/yargs": 17.0.26
"@types/yargs": 17.0.25
"@types/zxcvbn": 4.4.2
bcryptjs: 2.4.3
bulma: 0.9.4
@@ -5198,11 +5198,11 @@ __metadata:
"@rmp135/sql-ts": 1.18.0
"@types/fs-extra": 11.0.2
"@types/jest": 29.5.4
"@types/markdown-it": 13.0.2
"@types/mustache": 4.2.3
"@types/markdown-it": 13.0.1
"@types/mustache": 4.2.2
"@types/node": 18.17.19
"@types/node-fetch": 2.6.6
"@types/yargs": 17.0.26
"@types/yargs": 17.0.25
compare-versions: 6.1.0
dayjs: 1.11.10
execa: 4.1.0
@@ -7207,13 +7207,13 @@ __metadata:
languageName: node
linkType: hard
"@react-native-community/geolocation@npm:3.1.0":
version: 3.1.0
resolution: "@react-native-community/geolocation@npm:3.1.0"
"@react-native-community/geolocation@npm:3.0.6":
version: 3.0.6
resolution: "@react-native-community/geolocation@npm:3.0.6"
peerDependencies:
react: "*"
react-native: "*"
checksum: defc42ed11d0a3cc697d5db56c17cc19af2ca35b32674dc7323b40a458f5066b051191e5dc58c367f2643a0ed70b47e8eb2ac87ab1566e642ca0cbab1a1f09a9
checksum: 813df03599c064639bd5fe90922ab80d20d6224deffc8c6bfb02b7ade3d20f80e1ef6ac19c5d4394e901cca9a9fdb36eceda334a0ea6213e47c3c066921d09a9
languageName: node
linkType: hard
@@ -8157,13 +8157,13 @@ __metadata:
languageName: node
linkType: hard
"@types/markdown-it@npm:13.0.2":
version: 13.0.2
resolution: "@types/markdown-it@npm:13.0.2"
"@types/markdown-it@npm:13.0.1":
version: 13.0.1
resolution: "@types/markdown-it@npm:13.0.1"
dependencies:
"@types/linkify-it": "*"
"@types/mdurl": "*"
checksum: fe1f6a12ee8ad2246359376431a30d22c9b603e63e93e3e27d6920840934b9764034679a4d0b01ec54b0693c8d5c42012ec34715cba4f5b0736b8a4b66db4c74
checksum: 184d383ac21903a9e6be1639cde2b0cc082d0366b423fd8a69d0f37d9d1d36338f66611226ba4ef1da6148f370a62e08f688e8147ead43d429d6ff213c38c062
languageName: node
linkType: hard
@@ -8211,10 +8211,10 @@ __metadata:
languageName: node
linkType: hard
"@types/mustache@npm:4.2.3":
version: 4.2.3
resolution: "@types/mustache@npm:4.2.3"
checksum: c2c7cf749a84a622648c7088fb10350b84ea935145514b6c51d17e808a4b4972fb137273339f4d93160a3c496a3943dab3be93251d74c185730daa300a299e2f
"@types/mustache@npm:4.2.2":
version: 4.2.2
resolution: "@types/mustache@npm:4.2.2"
checksum: 1fa67a519f4302c96615524be4c8248067da02ca047bae9d4c4bb79977135ac7c15dcc388e7c70b8a817b9497004d5ca5c77a155dcb096bea16d53d4cdbe75d2
languageName: node
linkType: hard
@@ -8346,12 +8346,12 @@ __metadata:
languageName: node
linkType: hard
"@types/react-dom@npm:18.2.8":
version: 18.2.8
resolution: "@types/react-dom@npm:18.2.8"
"@types/react-dom@npm:18.2.7":
version: 18.2.7
resolution: "@types/react-dom@npm:18.2.7"
dependencies:
"@types/react": "*"
checksum: d36264631028d021b73cd9e015f10b95c4959ae1ce8f7a7419f318d1f05b1d063e6afffcd2a349a6bccd64ccc9ee9d2d976e1f0437643f0e7db621fa035bca65
checksum: e02ea908289a7ad26053308248d2b87f6aeafd73d0e2de2a3d435947bcea0422599016ffd1c3e38ff36c42f5e1c87c7417f05b0a157e48649e4a02f21727d54f
languageName: node
linkType: hard
@@ -8373,15 +8373,15 @@ __metadata:
languageName: node
linkType: hard
"@types/react-redux@npm:7.1.27":
version: 7.1.27
resolution: "@types/react-redux@npm:7.1.27"
"@types/react-redux@npm:7.1.26":
version: 7.1.26
resolution: "@types/react-redux@npm:7.1.26"
dependencies:
"@types/hoist-non-react-statics": ^3.3.0
"@types/react": "*"
hoist-non-react-statics: ^3.3.0
redux: ^4.0.0
checksum: 38fcc56f013e81e9a3125fd75acdacb4cdb5f9fe49402330b4783923f236d2d12ccdd2240ffa42e5bbb75900acd55393c00e0ca5dd6cab91a7b7e39e74ac62b4
checksum: 7f299f15ca8790c2e2683ad776ea4cbd5c38247f7a9fbddbc64ff4b235883238c772dc0e2687e328ba690d3b0c6a51b026c5fad719183595df87a91a1060094c
languageName: node
linkType: hard
@@ -8405,14 +8405,14 @@ __metadata:
languageName: node
linkType: hard
"@types/react@npm:18.2.24":
version: 18.2.24
resolution: "@types/react@npm:18.2.24"
"@types/react@npm:18.2.23":
version: 18.2.23
resolution: "@types/react@npm:18.2.23"
dependencies:
"@types/prop-types": "*"
"@types/scheduler": "*"
csstype: ^3.0.2
checksum: ea5d8204e71b1c9c6631f429a93f8e7be0614cdbdb464e92b3181bdccd8a7c45e30ded8b13da726684b6393f651317c36d54832e3d3cdea0da480a3f26268909
checksum: efb9d1ed1940c0e7ba08a21ffba5e266d8dbbb8fe618cfb97bc902dfc96385fdd8189e3f7f64b4aa13134f8e61947d60560deb23be151253c3a97b0d070897ca
languageName: node
linkType: hard
@@ -8543,12 +8543,12 @@ __metadata:
languageName: node
linkType: hard
"@types/yargs@npm:17.0.26":
version: 17.0.26
resolution: "@types/yargs@npm:17.0.26"
"@types/yargs@npm:17.0.25":
version: 17.0.25
resolution: "@types/yargs@npm:17.0.25"
dependencies:
"@types/yargs-parser": "*"
checksum: 26611969674f4972080c3b22239d4579eaadc5287f95f7802f893c4a9bb292c141467bd70f1e66eb834486c63a23c4f10032618b3d2e7b1ddc05051d08db4078
checksum: ef57926de514f5eb0a182167a63930bd7d2eb33d89b6041760f690d50b2019d7901b30c33ab7d03b3fa66a5004f0f81e36186d8f9e5e583a27b9ce331d2a5276
languageName: node
linkType: hard
@@ -10166,7 +10166,7 @@ __metadata:
languageName: node
linkType: hard
"asap@npm:^2.0.0, asap@npm:~2.0.6":
"asap@npm:^2.0.0, asap@npm:~2.0.3, asap@npm:~2.0.6":
version: 2.0.6
resolution: "asap@npm:2.0.6"
checksum: b296c92c4b969e973260e47523207cd5769abd27c245a68c26dc7a0fe8053c55bb04360237cb51cab1df52be939da77150ace99ad331fb7fb13b3423ed73ff3d
@@ -13074,6 +13074,13 @@ __metadata:
languageName: node
linkType: hard
"core-js@npm:^1.0.0":
version: 1.2.7
resolution: "core-js@npm:1.2.7"
checksum: 0b76371bfa98708351cde580f9287e2360d2209920e738ae950ae74ad08639a2e063541020bf666c28778956fc356ed9fe56d962129c88a87a6a4a0612526c75
languageName: node
linkType: hard
"core-util-is@npm:1.0.2":
version: 1.0.2
resolution: "core-util-is@npm:1.0.2"
@@ -15341,16 +15348,16 @@ __metadata:
languageName: node
linkType: hard
"electron@npm:25.9.0":
version: 25.9.0
resolution: "electron@npm:25.9.0"
"electron@npm:25.8.1":
version: 25.8.1
resolution: "electron@npm:25.8.1"
dependencies:
"@electron/get": ^2.0.0
"@types/node": ^18.11.18
extract-zip: ^2.0.1
bin:
electron: cli.js
checksum: 7d9bccf0af89ba7af1f904a58fdc623e6cade95e02956d4ccf6d998dadd2bac3d2a4f782a3d8a0f8bd92bdfc0d40c7a3b9b8af6bf50f104d9298c3ac4c642823
checksum: 3305f0d3e3d68d8921533b4fd42003812778bd90d1884b2baa859b3a7d900354298e63298fbfbfcac9267223e0d0d3de584137aec52b764081f3c68bf4b09efc
languageName: node
linkType: hard
@@ -17065,6 +17072,21 @@ __metadata:
languageName: node
linkType: hard
"fbjs@npm:^0.8.9":
version: 0.8.18
resolution: "fbjs@npm:0.8.18"
dependencies:
core-js: ^1.0.0
isomorphic-fetch: ^2.1.1
loose-envify: ^1.0.0
object-assign: ^4.1.0
promise: ^7.1.1
setimmediate: ^1.0.5
ua-parser-js: ^0.7.30
checksum: 668731b946a765908c9cbe51d5160f973abb78004b3d122587c3e930e3e1ddcc0ce2b17f2a8637dc9d733e149aa580f8d3035a35cc2d3bc78b78f1b19aab90e2
languageName: node
linkType: hard
"fd-slicer@npm:~1.1.0":
version: 1.1.0
resolution: "fd-slicer@npm:1.1.0"
@@ -20587,7 +20609,7 @@ __metadata:
languageName: node
linkType: hard
"is-stream@npm:^1.1.0":
"is-stream@npm:^1.0.1, is-stream@npm:^1.1.0":
version: 1.1.0
resolution: "is-stream@npm:1.1.0"
checksum: 063c6bec9d5647aa6d42108d4c59723d2bd4ae42135a2d4db6eadbd49b7ea05b750fd69d279e5c7c45cf9da753ad2c00d8978be354d65aa9f6bb434969c6a2ae
@@ -20835,6 +20857,16 @@ __metadata:
languageName: node
linkType: hard
"isomorphic-fetch@npm:^2.1.1":
version: 2.2.1
resolution: "isomorphic-fetch@npm:2.2.1"
dependencies:
node-fetch: ^1.0.1
whatwg-fetch: ">=0.10.0"
checksum: bb5daa7c3785d6742f4379a81e55b549a469503f7c9bf9411b48592e86632cf5e8fe8ea878dba185c0f33eb7c510c23abdeb55aebfdf5d3c70f031ced68c5424
languageName: node
linkType: hard
"isstream@npm:~0.1.2":
version: 0.1.2
resolution: "isstream@npm:0.1.2"
@@ -22710,14 +22742,14 @@ __metadata:
languageName: node
linkType: hard
"katex@npm:0.16.9":
version: 0.16.9
resolution: "katex@npm:0.16.9"
"katex@npm:0.16.8":
version: 0.16.8
resolution: "katex@npm:0.16.8"
dependencies:
commander: ^8.3.0
bin:
katex: cli.js
checksum: 861194dfd4d86505e657f688fb73048d46ac498edafce71199502a35b03c0ecc35ba930c631be79c4a09d90a0d23476673cd52f6bc367c7a161854d64005fa95
checksum: 4e75b4786101cc5eca0404bb814b2985bec506846f9015e9bf00207a3af14215e341ee62b6e7af2455a1032f8244e47a754642f250eea43d7b8007146ac01fae
languageName: node
linkType: hard
@@ -23527,7 +23559,7 @@ __metadata:
languageName: node
linkType: hard
"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0":
"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.3.1, loose-envify@npm:^1.4.0":
version: 1.4.0
resolution: "loose-envify@npm:1.4.0"
dependencies:
@@ -26136,6 +26168,16 @@ __metadata:
languageName: node
linkType: hard
"node-fetch@npm:^1.0.1":
version: 1.7.3
resolution: "node-fetch@npm:1.7.3"
dependencies:
encoding: ^0.1.11
is-stream: ^1.0.1
checksum: 3bb0528c05d541316ebe52770d71ee25a6dce334df4231fd55df41a644143e07f068637488c18a5b0c43f05041dbd3346752f9e19b50df50569a802484544d5b
languageName: node
linkType: hard
"node-fetch@npm:^2.2.0, node-fetch@npm:^2.5.0, node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1":
version: 2.6.6
resolution: "node-fetch@npm:2.6.6"
@@ -28706,6 +28748,15 @@ __metadata:
languageName: node
linkType: hard
"promise@npm:^7.1.1":
version: 7.3.1
resolution: "promise@npm:7.3.1"
dependencies:
asap: ~2.0.3
checksum: 475bb069130179fbd27ed2ab45f26d8862376a137a57314cf53310bdd85cc986a826fd585829be97ebc0aaf10e9d8e68be1bfe5a4a0364144b1f9eedfa940cf1
languageName: node
linkType: hard
"prompts@npm:^2.0.1, prompts@npm:^2.4.0":
version: 2.4.2
resolution: "prompts@npm:2.4.2"
@@ -28736,6 +28787,16 @@ __metadata:
languageName: node
linkType: hard
"prop-types@npm:15.5.10":
version: 15.5.10
resolution: "prop-types@npm:15.5.10"
dependencies:
fbjs: ^0.8.9
loose-envify: ^1.3.1
checksum: 3e928ad5afa5124d52a341a706170628e7b0caa9340515782be6a767261e6eb0e473116188bb8efbe9d9b62cb3c9501c71bf4ab7d34f2507294ee34c90de6964
languageName: node
linkType: hard
"prop-types@npm:^15.5.7, prop-types@npm:^15.5.8, prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2":
version: 15.7.2
resolution: "prop-types@npm:15.7.2"
@@ -29295,10 +29356,12 @@ __metadata:
languageName: node
linkType: hard
"react-native-dropdownalert@npm:5.1.0":
version: 5.1.0
resolution: "react-native-dropdownalert@npm:5.1.0"
checksum: 595e409967a28e5305b7895407a801c6eb05091277eaa7362b30dce18213d695dcbf0811c69cbaa20f35ce9aa21f13d44235dd35717119e0020a3159bd06b0ef
"react-native-dropdownalert@npm:4.5.1":
version: 4.5.1
resolution: "react-native-dropdownalert@npm:4.5.1"
dependencies:
prop-types: 15.5.10
checksum: 16346105f130f1aefe8ed4c9171524ce931f2a924c6fa95b41291f99a5613a7c4fb9a3f75b19ef280ac8d47f8ba13ebadf596174d83a92cbbdd00c278c2e2b9f
languageName: node
linkType: hard
@@ -29542,16 +29605,16 @@ __metadata:
languageName: node
linkType: hard
"react-native-webview@npm:13.6.0":
version: 13.6.0
resolution: "react-native-webview@npm:13.6.0"
"react-native-webview@npm:13.5.1":
version: 13.5.1
resolution: "react-native-webview@npm:13.5.1"
dependencies:
escape-string-regexp: 2.0.0
invariant: 2.2.4
peerDependencies:
react: "*"
react-native: "*"
checksum: f7220fb18dcf5e9631674831ac8beb3bc4b99863f812676c25394a3cce487975b05c8fc795b0ee4ff1ead43da304ddea104f5e6683ea3f4c3a135b14ae193069
checksum: f7536d0832c401d75c6f92bb997daeb7fe9de82471bf50bbc895421b82cd355da476dc393b186e9256d5759c345f5c97f2f8185fd079d3cb6fc174ca8ee70ba5
languageName: node
linkType: hard
@@ -29714,9 +29777,9 @@ __metadata:
languageName: node
linkType: hard
"react-redux@npm:8.1.3":
version: 8.1.3
resolution: "react-redux@npm:8.1.3"
"react-redux@npm:8.1.2":
version: 8.1.2
resolution: "react-redux@npm:8.1.2"
dependencies:
"@babel/runtime": ^7.12.1
"@types/hoist-non-react-statics": ^3.3.1
@@ -29742,7 +29805,7 @@ __metadata:
optional: true
redux:
optional: true
checksum: 192ea6f6053148ec80a4148ec607bc259403b937e515f616a1104ca5ab357e97e98b8245ed505a17afee67a72341d4a559eaca9607968b4a422aa9b44ba7eb89
checksum: 4d5976b0f721e4148475871fcabce2fee875cc7f70f9a292f3370d63b38aa1dd474eb303c073c5555f3e69fc732f3bac05303def60304775deb28361e3f4b7cc
languageName: node
linkType: hard
@@ -29753,9 +29816,9 @@ __metadata:
languageName: node
linkType: hard
"react-select@npm:5.7.7":
version: 5.7.7
resolution: "react-select@npm:5.7.7"
"react-select@npm:5.7.5":
version: 5.7.5
resolution: "react-select@npm:5.7.5"
dependencies:
"@babel/runtime": ^7.12.0
"@emotion/cache": ^11.4.0
@@ -29769,7 +29832,7 @@ __metadata:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 6fd0c211d377addba6e6762a614ae674936df39a3f46ec19fd06e7acae8d6cadeb93d4723b10e25eff1ff8235077bae9459f293936334d82b28fe5071081c057
checksum: 88f2d94c4a6778df525a9fb5d7acac1bf34821f6efcfdc5927ec608f5f933cf3f47e1c4e4fd3b92d7b2ba1d91e44595d45ac4e2fd7528ba420086008ac5a81cf
languageName: node
linkType: hard
@@ -33242,9 +33305,9 @@ __metadata:
languageName: node
linkType: hard
"tap@npm:16.3.9":
version: 16.3.9
resolution: "tap@npm:16.3.9"
"tap@npm:16.3.8":
version: 16.3.8
resolution: "tap@npm:16.3.8"
dependencies:
"@isaacs/import-jsx": ^4.0.1
"@types/react": ^17.0.52
@@ -33288,7 +33351,7 @@ __metadata:
optional: true
bin:
tap: bin/run.js
checksum: 5d2f671681ad6199fd7a1abc48f1b2010e77fd5a757413d9c12e78fb5b841ae4639ee4913fd40c8bf7de7af5e7bab0453c8b7bb6174fd1e1d76948a248712e56
checksum: b63e064f1ea20aa4cbe8cd40fbe780def9757b637caaae8ee24d96b184d8627421045dd56168b21715f6ebff77e88db774cda0b80af113ae33432641aefcbb58
languageName: node
linkType: hard
@@ -34641,6 +34704,13 @@ __metadata:
languageName: node
linkType: hard
"ua-parser-js@npm:^0.7.30":
version: 0.7.31
resolution: "ua-parser-js@npm:0.7.31"
checksum: e2f8324a83d1715601576af85b2b6c03890699aaa7272950fc77ea925c70c5e4f75060ae147dc92124e49f7f0e3d6dd2b0a91e7f40d267e92df8894be967ba8b
languageName: node
linkType: hard
"uc.micro@npm:^1.0.1, uc.micro@npm:^1.0.5":
version: 1.0.6
resolution: "uc.micro@npm:1.0.6"
@@ -35803,7 +35873,7 @@ __metadata:
languageName: node
linkType: hard
"whatwg-fetch@npm:^3.0.0":
"whatwg-fetch@npm:>=0.10.0, whatwg-fetch@npm:^3.0.0":
version: 3.6.2
resolution: "whatwg-fetch@npm:3.6.2"
checksum: ee976b7249e7791edb0d0a62cd806b29006ad7ec3a3d89145921ad8c00a3a67e4be8f3fb3ec6bc7b58498724fd568d11aeeeea1f7827e7e1e5eae6c8a275afed