1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-30 20:39:46 +02:00

Compare commits

..

17 Commits

Author SHA1 Message Date
Laurent Cozic
6f9ba953a6 Desktop release v2.13.3 2023-10-23 18:23:18 +01:00
Laurent Cozic
5bd45abc10 Chore: Fix new item title 2023-10-23 18:23:05 +01:00
Laurent Cozic
c1a18bac6b Doc: Update Joplin Cloud storage info to 2, 30, 50 GB 2023-10-23 18:18:36 +01:00
Laurent Cozic
e45835ed9a Doc: Added news item about white hat hackers 2023-10-23 17:23:22 +01:00
Laurent Cozic
154619cc42 Chore: Fixed fetching user logic for Joplin Cloud 2023-10-23 16:58:50 +01:00
pedr
a77462f8ea Chore: All: Add property to Setting to control if file storage is used to save the configuration (#9121) 2023-10-23 16:58:20 +01:00
Joplin Bot
909776c666 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-23 12:23:23 +00:00
CptMeetKat
cd8b5388ec Desktop: Fixes #9108: Added Note Properties to Note menu bar items (#9119) 2023-10-23 13:05:06 +01:00
renovate[bot]
4ecc4816e9 Update dependency katex to v0.16.9 (#9117)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-23 00:20:13 +00:00
renovate[bot]
edda92c055 Update dependency react-select to v5.7.7 (#9109)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-22 13:53:37 +00:00
Henry Heino
03bd77c107 Desktop: Resolves #8749: Use plain text editor in safe mode (#8750) 2023-10-22 12:00:19 +01:00
Henry Heino
c2bfc526e7 Desktop: Fixes #9070: Fix external links in PDFs break Joplin (#9094) 2023-10-22 11:52:06 +01:00
Henry Heino
0e6891fd88 Desktop: Fixes #9104: Beta editor: Allow tab key to insert tabs at cursor rather than indent in some cases (#9107) 2023-10-22 11:51:54 +01:00
Henry Heino
d3744b0e6e Mobile: Fixes #9066: Improve list toggle logic (#9103) 2023-10-22 11:51:46 +01:00
Henry Heino
6b319f4738 Mobile: Fixes #9069: Fix writing UTF-8 data to a file replaces non-ASCII characters with ?s (#9076) 2023-10-22 11:51:31 +01:00
renovate[bot]
39c336a5d8 Update dependency @types/react to v18.2.24 (#9100)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2023-10-22 11:26:03 +01:00
renovate[bot]
1d4ea3d99f Update dependency react-native-dropdownalert to v5 (#9096)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2023-10-22 10:45:05 +01:00
46 changed files with 738 additions and 413 deletions

View File

@@ -242,6 +242,7 @@ 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
@@ -499,7 +500,8 @@ 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-rn.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
packages/app-mobile/utils/fs-driver/runOnDeviceTests.js
packages/app-mobile/utils/setupNotifications.js
packages/app-mobile/utils/shareHandler.js
packages/app-mobile/utils/types.js
@@ -533,6 +535,7 @@ 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
@@ -630,7 +633,6 @@ packages/lib/markdownUtils2.test.js
packages/lib/markupLanguageUtils.js
packages/lib/migrations/42.js
packages/lib/models/Alarm.js
packages/lib/models/BaseItem.test.js
packages/lib/models/BaseItem.js
packages/lib/models/Folder.sharing.test.js
packages/lib/models/Folder.test.js

6
.gitignore vendored
View File

@@ -224,6 +224,7 @@ 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
@@ -481,7 +482,8 @@ 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-rn.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
packages/app-mobile/utils/fs-driver/runOnDeviceTests.js
packages/app-mobile/utils/setupNotifications.js
packages/app-mobile/utils/shareHandler.js
packages/app-mobile/utils/types.js
@@ -515,6 +517,7 @@ 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
@@ -612,7 +615,6 @@ packages/lib/markdownUtils2.test.js
packages/lib/markupLanguageUtils.js
packages/lib/migrations/42.js
packages/lib/models/Alarm.js
packages/lib/models/BaseItem.test.js
packages/lib/models/BaseItem.js
packages/lib/models/Folder.sharing.test.js
packages/lib/models/Folder.test.js

View File

@@ -1 +0,0 @@
just testing

View File

@@ -1 +0,0 @@
just testing 2

View File

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

View File

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

View File

@@ -0,0 +1,56 @@
// 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

@@ -1,50 +0,0 @@
// 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 } from './utils/types';
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps, AllAssetsOptions, NoteBodyEditorRef } from './utils/types';
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
import CommandService from '@joplin/lib/services/CommandService';
import ToolbarButton from '../ToolbarButton/ToolbarButton';
@@ -45,6 +45,7 @@ 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';
@@ -60,7 +61,7 @@ function NoteEditor(props: NoteEditorProps) {
const [scrollWhenReady, setScrollWhenReady] = useState<ScrollOptions>(null);
const [isReadOnly, setIsReadOnly] = useState<boolean>(false);
const editorRef = useRef<any>();
const editorRef = useRef<NoteBodyEditorRef>();
const titleInputRef = useRef<any>();
const isMountedRef = useRef(true);
const noteSearchBarRef = useRef(null);
@@ -462,6 +463,8 @@ 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 { useEffect } from 'react';
import { FormNote, ScrollOptionTypes } from './types';
import { RefObject, useEffect } from 'react';
import { FormNote, NoteBodyEditorRef, 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,6 +12,8 @@ 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
@@ -19,16 +21,18 @@ interface HookDependencies {
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
dispatch: Function;
noteSearchBarRef: any;
editorRef: any;
editorRef: RefObject<NoteBodyEditorRef>;
titleInputRef: any;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
saveNoteAndWait: Function;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
setFormNote: Function;
setFormNote: SetFormNoteCallback;
}
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
function editorCommandRuntime(declaration: CommandDeclaration, editorRef: any, setFormNote: Function): CommandRuntime {
function editorCommandRuntime(
declaration: CommandDeclaration,
editorRef: RefObject<NoteBodyEditorRef>,
setFormNote: SetFormNoteCallback,
): CommandRuntime {
return {
execute: async (_context: CommandContext, ...args: any[]) => {
if (!editorRef.current) {

View File

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

View File

@@ -76,4 +76,49 @@ 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,12 +31,6 @@ 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.2",
"version": "2.13.3",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
@@ -121,7 +121,7 @@
"@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.5.4",
"@types/node": "18.17.19",
"@types/react": "18.2.23",
"@types/react": "18.2.24",
"@types/react-redux": "7.1.27",
"@types/styled-components": "5.1.28",
"electron": "25.9.0",
@@ -175,7 +175,7 @@
"react-datetime": "3.2.0",
"react-dom": "18.2.0",
"react-redux": "8.1.3",
"react-select": "5.7.5",
"react-select": "5.7.7",
"react-toggle-button": "2.2.0",
"react-tooltip": "4.5.1",
"redux": "4.2.1",

View File

@@ -12,8 +12,9 @@ export default function stateToWhenClauseContext(state: AppState, options: WhenC
...libStateToWhenClauseContext(state, options),
// UI elements
markdownEditorVisible: !!state.settings['editor.codeView'],
richTextEditorVisible: !state.settings['editor.codeView'],
markdownEditorVisible: !!state.settings['editor.codeView'] && !state.settings['isSafeMode'],
richTextEditorVisible: !state.settings['editor.codeView'] && !state.settings['isSafeMode'],
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

@@ -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": "4.5.1",
"react-native-dropdownalert": "5.1.0",
"react-native-exit-app": "2.0.0",
"react-native-file-viewer": "2.1.5",
"react-native-fingerprint-scanner": "6.0.0",
@@ -95,7 +95,7 @@
"@tsconfig/react-native": "2.0.2",
"@types/fs-extra": "11.0.2",
"@types/jest": "29.5.4",
"@types/react": "18.2.23",
"@types/react": "18.2.24",
"@types/react-native": "0.70.6",
"@types/react-redux": "7.1.27",
"@types/tar-stream": "2.2.3",

View File

@@ -97,7 +97,7 @@ SyncTargetRegistry.addClass(SyncTargetAmazonS3);
SyncTargetRegistry.addClass(SyncTargetJoplinServer);
SyncTargetRegistry.addClass(SyncTargetJoplinCloud);
import FsDriverRN from './utils/fs-driver-rn';
import FsDriverRN from './utils/fs-driver/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 } from '@joplin/lib/services/e2ee/ppkTestUtils';
import { runIntegrationTests as runRsaIntegrationTests } 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,6 +121,7 @@ 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';
@@ -749,7 +750,10 @@ 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 runIntegrationTests();
if (Setting.value('env') === 'dev') {
await runRsaIntegrationTests();
await runOnDeviceFsDriverTests();
}
reg.logger().info('Application initialized');
}
@@ -759,6 +763,7 @@ 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();
@@ -891,7 +896,11 @@ class AppComponent extends React.Component {
AlarmService.setInAppNotificationHandler(async (alarmId: string) => {
const alarm = await Alarm.load(alarmId);
const notification = await Alarm.makeNotification(alarm);
this.dropdownAlert_.alertWithType('info', notification.title, notification.body ? notification.body : '');
void this.dropdownAlert_({
type: 'info',
title: notification.title,
message: notification.body ? notification.body : '',
});
});
this.appStateChangeListener_ = RNAppState.addEventListener('change', this.onAppStateChange_);
@@ -1082,8 +1091,7 @@ class AppComponent extends React.Component {
<View style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
{ shouldShowMainContent && <AppNav screens={appNavInit} dispatch={this.props.dispatch} /> }
</View>
<DropdownAlert ref={(ref: any) => this.dropdownAlert_ = ref} tapToCloseEnabled={true} />
{ !shouldShowMainContent && <BiometricPopup
<DropdownAlert alert={(func: any) => (this.dropdownAlert_ = func)} /> { !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, { Encoding, DocumentFileDetail, openDocumentTree } from '@joplin/react-native-saf-x';
import RNSAF, { DocumentFileDetail, openDocumentTree } from '@joplin/react-native-saf-x';
import { Platform } from 'react-native';
import * as tar from 'tar-stream';
import { resolve } from 'path';
@@ -18,24 +18,63 @@ 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');
}
// Encoding can be either "utf8" or "base64"
public appendFile(path: string, content: any, encoding = 'base64') {
// 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);
if (isScopedUri(path)) {
return RNSAF.writeFile(path, content, { encoding: encoding as Encoding, append: true });
return RNSAF.writeFile(path, content, { encoding, append: true });
}
return RNFS.appendFile(path, content, encoding);
}
// Encoding can be either "utf8" or "base64"
public writeFile(path: string, content: any, encoding = 'base64') {
// Encoding can be either "utf8", "utf-8", or "base64"
public writeFile(path: string, content: any, rawEncoding = 'base64') {
const encoding = normalizeEncoding(rawEncoding);
if (isScopedUri(path)) {
return RNSAF.writeFile(path, content, { encoding: encoding as Encoding });
return RNSAF.writeFile(path, content, { encoding: 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);
@@ -195,10 +234,11 @@ export default class FsDriverRN extends FsDriverBase {
return null;
}
public readFile(path: string, encoding = 'utf8') {
if (encoding === 'Buffer') throw new Error('Raw buffer output not supported for FsDriverRN.readFile');
public readFile(path: string, rawEncoding = 'utf8') {
const encoding = normalizeEncoding(rawEncoding);
if (isScopedUri(path)) {
return RNSAF.readFile(path, { encoding: encoding as Encoding });
return RNSAF.readFile(path, { encoding: encoding });
}
return RNFS.readFile(path, encoding);
}
@@ -244,7 +284,9 @@ export default class FsDriverRN extends FsDriverBase {
}
}
public async readFileChunk(handle: any, length: number, encoding = 'base64') {
public async readFileChunk(handle: any, length: number, rawEncoding = 'base64') {
const encoding = normalizeEncoding(rawEncoding);
if (handle.offset + length > handle.stat.size) {
length = handle.stat.size - handle.offset;
}

View File

@@ -0,0 +1,249 @@
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-rn').default;
const FsDriverRN = require('./fs-driver/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,6 +20,7 @@ import { SearchState, EditorProps, EditorSettings } from '../types';
import { EditorEventType, SelectionRangeChangeEvent } from '../events';
import {
decreaseIndent, increaseIndent,
insertOrIncreaseIndent,
toggleBolded, toggleCode,
toggleItalicized, toggleMath,
} from './markdown/markdownCommands';
@@ -254,7 +255,7 @@ const createEditor = (
notifyLinkEditRequest();
return true;
}),
keyCommand('Tab', increaseIndent, true),
keyCommand('Tab', insertOrIncreaseIndent, true),
keyCommand('Shift-Tab', decreaseIndent, true),
...standardKeymap, ...historyKeymap, ...searchKeymap,

View File

@@ -1,5 +1,6 @@
import { EditorSelection } from '@codemirror/state';
import {
insertOrIncreaseIndent,
toggleBolded, toggleCode, toggleHeaderLevel, toggleItalicized, toggleMath, updateLink,
} from './markdownCommands';
import createTestEditor from '../testUtil/createTestEditor';
@@ -238,5 +239,41 @@ 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,6 +63,35 @@ 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,6 +12,7 @@ import {
toggleInlineFormatGlobally, toggleRegionFormatGlobally, toggleSelectedLinesStartWith,
isIndentationEquivalent, stripBlockquote, tabsToSpaces,
} from './markdownReformatter';
import intersectsSyntaxNode from '../util/isInSyntaxNode';
const startingSpaceRegex = /^(\s*)/;
@@ -183,8 +184,11 @@ export const toggleList = (listType: ListType): Command => {
const origFirstLineIndentation = firstLineIndentation;
const origContainerType = containerType;
// Grow [sel] to the smallest containing list
if (sel.empty) {
// 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() !== '') {
sel = growSelectionToNode(state, sel, [orderedListTag, unorderedListTag]);
computeSelectionProps();
}
@@ -420,6 +424,35 @@ 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

@@ -0,0 +1,32 @@
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,7 +18,7 @@
"@joplin/lib": "~2.13",
"@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.5.4",
"@types/react": "18.2.23",
"@types/react": "18.2.24",
"@types/react-redux": "7.1.27",
"@types/styled-components": "5.1.28",
"jest": "29.6.3",

View File

@@ -3,7 +3,7 @@ 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 migration44 from './services/database/migrations/44';
import { SqlQuery, Migration } from './services/database/types';
import addMigrationFile from './services/database/addMigrationFile';
@@ -127,7 +127,7 @@ INSERT INTO version (version) VALUES (1);
const migrations: Migration[] = [
migration42,
migration43,
migration44,
// migration44,
];
export interface TableField {

View File

@@ -48,7 +48,8 @@ describe('RotatingLogs', () => {
try {
dir = await createTempDir();
await createTestLogFile(dir);
const rotatingLogs: RotatingLogs = new RotatingLogs(dir, 1, 5000);
await msleep(100);
const rotatingLogs: RotatingLogs = new RotatingLogs(dir, 1, 100);
await rotatingLogs.cleanActiveLogFile();
await rotatingLogs.deleteNonActiveLogFiles();
const files = await readdir(dir);

View File

@@ -604,7 +604,7 @@ export default class Synchronizer {
} else {
// Note: in order to know the real updated_time value, we need to load the content. In theory we could
// rely on the file timestamp (in remote.updated_time) but in practice it's not accurate enough and
// can lead to conflicts (for example when the file timestamp is slightly ahead of its real
// can lead to conflicts (for example when the file timestamp is slightly ahead of it's real
// updated_time). updated_time is set and managed by clients so it's always accurate.
// Same situation below for updateLocal.
//
@@ -701,15 +701,7 @@ export default class Synchronizer {
logger.warn(`Uploading a large resource (resourceId: ${local.id}, size:${resource.size} bytes) which may tie up the sync process.`);
}
// We skip updating the blob if it hasn't
// been modified since the last sync. In
// that case, it means the resource metadata
// (title, filename, etc.) has been changed,
// but not the data blob.
const syncItem = await BaseItem.syncItem(syncTargetId, resource.id, { fields: ['sync_time', 'force_sync'] });
if (!syncItem || syncItem.sync_time < resource.blob_updated_time || syncItem.force_sync) {
await this.apiCall('put', remoteContentPath, null, { path: localResourceContentPath, source: 'file', shareId: resource.share_id });
}
await this.apiCall('put', remoteContentPath, null, { path: localResourceContentPath, source: 'file', shareId: resource.share_id });
} catch (error) {
if (isCannotSyncError(error)) {
await handleCannotSyncItem(ItemClass, syncTargetId, local, error.message);

View File

@@ -25,6 +25,10 @@ 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,22 +1,17 @@
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, syncTargetId, synchronizerStart, msleep } from '../testing/test-utils';
import BaseItem from './BaseItem';
import Folder from './Folder';
import Note from './Note';
const { setupDatabaseAndSynchronizer, switchClient } = require('../testing/test-utils.js');
const Folder = require('../models/Folder').default;
const Note = require('../models/Note').default;
describe('BaseItem', () => {
describe('models/BaseItem', () => {
beforeEach(async () => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
});
afterAll(async () => {
await afterAllCleanUp();
});
// This is to handle the case where a property is removed from a BaseItem table - in that case files in
// the sync target will still have the old property but we don't need it locally.
it('should ignore properties that are present in sync file but not in database when serialising', async () => {
it('should ignore properties that are present in sync file but not in database when serialising', (async () => {
const folder = await Folder.save({ title: 'folder1' });
let serialized = await Folder.serialize(folder);
@@ -25,9 +20,9 @@ describe('BaseItem', () => {
const unserialized = await Folder.unserialize(serialized);
expect('ignore_me' in unserialized).toBe(false);
});
}));
it('should not modify title when unserializing', async () => {
it('should not modify title when unserializing', (async () => {
const folder1 = await Folder.save({ title: '' });
const folder2 = await Folder.save({ title: 'folder1' });
@@ -40,9 +35,9 @@ describe('BaseItem', () => {
const unserialized2 = await Folder.unserialize(serialized2);
expect(unserialized2.title).toBe(folder2.title);
});
}));
it('should correctly unserialize note timestamps', async () => {
it('should correctly unserialize note timestamps', (async () => {
const folder = await Folder.save({ title: 'folder' });
const note = await Note.save({ title: 'note', parent_id: folder.id });
@@ -53,9 +48,9 @@ describe('BaseItem', () => {
expect(unserialized.updated_time).toEqual(note.updated_time);
expect(unserialized.user_created_time).toEqual(note.user_created_time);
expect(unserialized.user_updated_time).toEqual(note.user_updated_time);
});
}));
it('should serialize geolocation fields', async () => {
it('should serialize geolocation fields', (async () => {
const folder = await Folder.save({ title: 'folder' });
let note = await Note.save({ title: 'note', parent_id: folder.id });
note = await Note.load(note.id);
@@ -81,9 +76,9 @@ describe('BaseItem', () => {
expect(unserialized.latitude).toEqual(note.latitude);
expect(unserialized.longitude).toEqual(note.longitude);
expect(unserialized.altitude).toEqual(note.altitude);
});
}));
it('should serialize and unserialize notes', async () => {
it('should serialize and unserialize notes', (async () => {
const folder = await Folder.save({ title: 'folder' });
const note = await Note.save({ title: 'note', parent_id: folder.id });
await Note.save({
@@ -98,9 +93,9 @@ describe('BaseItem', () => {
const noteAfter = await Note.unserialize(serialized);
expect(noteAfter).toEqual(noteBefore);
});
}));
it('should serialize and unserialize properties that contain new lines', async () => {
it('should serialize and unserialize properties that contain new lines', (async () => {
const sourceUrl = `
https://joplinapp.org/ \\n
`;
@@ -112,9 +107,9 @@ https://joplinapp.org/ \\n
const noteAfter = await Note.unserialize(serialized);
expect(noteAfter).toEqual(noteBefore);
});
}));
it('should not serialize the note title and body', async () => {
it('should not serialize the note title and body', (async () => {
const note = await Note.save({ title: 'my note', body: `one line
two line
three line \\n no escape` });
@@ -126,27 +121,5 @@ three line \\n no escape` });
one line
two line
three line \\n no escape`)).toBe(0);
});
it('should update item sync item', async () => {
const note1 = await Note.save({ });
const syncTime = async (itemId: string) => {
const syncItem = await BaseItem.syncItem(syncTargetId(), itemId, { fields: ['sync_time'] });
return syncItem ? syncItem.sync_time : 0;
};
expect(await syncTime(note1.id)).toBe(0);
await synchronizerStart();
const newTime = await syncTime(note1.id);
expect(newTime).toBeLessThanOrEqual(Date.now());
// Check that it doesn't change if we sync again
await msleep(1);
await synchronizerStart();
expect(await syncTime(note1.id)).toBe(newTime);
});
}));
});

View File

@@ -1,5 +1,5 @@
import { ModelType, DeleteOptions } from '../BaseModel';
import { BaseItemEntity, DeletedItemEntity, NoteEntity, SyncItemEntity } from '../services/database/types';
import { BaseItemEntity, DeletedItemEntity, NoteEntity } from '../services/database/types';
import Setting from './Setting';
import BaseModel from '../BaseModel';
import time from '../time';
@@ -194,14 +194,6 @@ export default class BaseItem extends BaseModel {
return output;
}
public static async syncItem(syncTarget: number, itemId: string, options: LoadOptions = null): Promise<SyncItemEntity> {
options = {
fields: '*',
...options,
};
return await this.db().selectOne(`SELECT ${this.db().escapeFieldsToString(options.fields)} FROM sync_items WHERE sync_target = ? AND item_id = ?`, [syncTarget, itemId]);
}
public static async allSyncItems(syncTarget: number) {
const output = await this.db().selectAll('SELECT * FROM sync_items WHERE sync_target = ?', [syncTarget]);
return output;

View File

@@ -1,11 +1,10 @@
import { supportDir, setupDatabaseAndSynchronizer, switchClient, simulateReadOnlyShareEnv, expectThrow, createTempFile, msleep } from '../testing/test-utils';
import { supportDir, setupDatabaseAndSynchronizer, switchClient, simulateReadOnlyShareEnv, expectThrow, createTempFile } from '../testing/test-utils';
import Folder from '../models/Folder';
import Note from '../models/Note';
import Resource from '../models/Resource';
import shim from '../shim';
import { ErrorCode } from '../errors';
import { remove, pathExists } from 'fs-extra';
import { ResourceEntity } from '../services/database/types';
const testImagePath = `${supportDir}/photo.jpg`;
@@ -96,39 +95,6 @@ describe('models/Resource', () => {
expect(originalStat.size).toBe(newStat.size);
}));
it('should set the blob_updated_time property if the blob is updated', (async () => {
const note = await Note.save({});
await shim.attachFileToNote(note, testImagePath);
const resourceA: ResourceEntity = (await Resource.all())[0];
expect(resourceA.updated_time).toBe(resourceA.blob_updated_time);
await msleep(1);
await Resource.updateResourceBlobContent(resourceA.id, testImagePath);
const resourceB: ResourceEntity = (await Resource.all())[0];
expect(resourceB.updated_time).toBeGreaterThan(resourceA.updated_time);
expect(resourceB.blob_updated_time).toBeGreaterThan(resourceA.blob_updated_time);
}));
it('should NOT set the blob_updated_time property if the blob is NOT updated', (async () => {
const note = await Note.save({});
await shim.attachFileToNote(note, testImagePath);
const resourceA: ResourceEntity = (await Resource.all())[0];
await msleep(1);
// We only update the resource metadata - so the blob timestamp should
// not change
await Resource.save({ id: resourceA.id, title: 'new title' });
const resourceB: ResourceEntity = (await Resource.all())[0];
expect(resourceB.updated_time).toBeGreaterThan(resourceA.updated_time);
expect(resourceB.blob_updated_time).toBe(resourceA.blob_updated_time);
}));
it('should not allow modifying a read-only resource', async () => {
const { cleanup, resource } = await setupFolderNoteResourceReadOnly('123456789');
await expectThrow(async () => Resource.save({ id: resource.id, share_id: '123456789', title: 'cannot do this!' }), ErrorCode.IsReadOnly);

View File

@@ -15,7 +15,6 @@ import JoplinError from '../JoplinError';
import itemCanBeEncrypted from './utils/itemCanBeEncrypted';
import { getEncryptionEnabled } from '../services/synchronizer/syncInfoUtils';
import ShareService from '../services/share/ShareService';
import { SaveOptions } from './utils/types';
export default class Resource extends BaseItem {
@@ -373,15 +372,9 @@ export default class Resource extends BaseItem {
// We first save the resource metadata because this can throw, for
// example if modifying a resource that is read-only
const now = Date.now();
const result = await Resource.save({
id: resource.id,
size: fileStat.size,
updated_time: now,
blob_updated_time: now,
}, {
autoTimestamp: false,
});
// If the above call has succeeded, we save the data blob
@@ -449,18 +442,10 @@ export default class Resource extends BaseItem {
}, { changeSource: ItemChange.SOURCE_SYNC });
}
public static async save(o: ResourceEntity, options: SaveOptions = null): Promise<ResourceEntity> {
const resource = { ...o };
if (this.isNew(o, options)) {
const now = Date.now();
options = { ...options, autoTimestamp: false };
if (!resource.created_time) resource.created_time = now;
if (!resource.updated_time) resource.updated_time = now;
if (!resource.blob_updated_time) resource.blob_updated_time = now;
}
return await super.save(resource, options);
}
// 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,6 +303,7 @@ class Setting extends BaseModel {
};
public static autoSaveEnabled = true;
public static allowFileStorage = true;
private static metadata_: SettingItems = null;
private static keychainService_: any = null;
@@ -2055,7 +2056,7 @@ class Setting extends BaseModel {
}
private static canUseFileStorage(): boolean {
return !shim.mobilePlatform();
return this.allowFileStorage && !shim.mobilePlatform();
}
private static keyStorage(key: string): SettingStorage {

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.23",
"@types/react": "18.2.24",
"@types/uuid": "9.0.4",
"clean-html": "1.5.0",
"jest": "29.6.4",

View File

@@ -256,7 +256,6 @@ export interface ResourceLocalStateEntity {
'type_'?: number;
}
export interface ResourceEntity {
'blob_updated_time'?: number;
'created_time'?: number;
'encryption_applied'?: number;
'encryption_blob_encrypted'?: number;
@@ -468,7 +467,6 @@ export const databaseSchema: DatabaseTables = {
type_: { type: 'number' },
},
resources: {
blob_updated_time: { type: 'number' },
created_time: { type: 'number' },
encryption_applied: { type: 'number' },
encryption_blob_encrypted: { type: 'number' },

View File

@@ -1,9 +1,9 @@
import time from '../../time';
import shim from '../../shim';
import Setting from '../../models/Setting';
import { NoteEntity, ResourceEntity } from '../../services/database/types';
import { NoteEntity } from '../../services/database/types';
import { remoteNotesFoldersResources, remoteResources } from '../../testing/test-utils-synchronizer';
import { synchronizerStart, tempFilePath, resourceFetcher, supportDir, setupDatabaseAndSynchronizer, synchronizer, fileApi, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, checkThrowAsync, msleep } from '../../testing/test-utils';
import { synchronizerStart, tempFilePath, resourceFetcher, supportDir, setupDatabaseAndSynchronizer, synchronizer, fileApi, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, checkThrowAsync } from '../../testing/test-utils';
import Folder from '../../models/Folder';
import Note from '../../models/Note';
import Resource from '../../models/Resource';
@@ -27,7 +27,7 @@ describe('Synchronizer.resources', () => {
insideBeforeEach = false;
});
it('should sync resources', async () => {
it('should sync resources', (async () => {
while (insideBeforeEach) await time.msleep(500);
const folder1 = await Folder.save({ title: 'folder1' });
@@ -58,9 +58,9 @@ describe('Synchronizer.resources', () => {
const resourcePath1_2 = Resource.fullPath(resource1_2);
expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true);
});
}));
it('should handle resource download errors', async () => {
it('should handle resource download errors', (async () => {
while (insideBeforeEach) await time.msleep(500);
const folder1 = await Folder.save({ title: 'folder1' });
@@ -87,9 +87,9 @@ describe('Synchronizer.resources', () => {
const ls = await Resource.localState(resource1);
expect(ls.fetch_status).toBe(Resource.FETCH_STATUS_ERROR);
expect(ls.fetch_error).toBe('did not work');
});
}));
it('should set the resource file size if it is missing', async () => {
it('should set the resource file size if it is missing', (async () => {
while (insideBeforeEach) await time.msleep(500);
const folder1 = await Folder.save({ title: 'folder1' });
@@ -110,9 +110,9 @@ describe('Synchronizer.resources', () => {
await fetcher.waitForAllFinished();
r1 = await Resource.load(r1.id);
expect(r1.size).toBe(2720);
});
}));
it('should delete resources', async () => {
it('should delete resources', (async () => {
while (insideBeforeEach) await time.msleep(500);
const folder1 = await Folder.save({ title: 'folder1' });
@@ -142,9 +142,9 @@ describe('Synchronizer.resources', () => {
allResources = await Resource.all();
expect(allResources.length).toBe(0);
expect(await shim.fsDriver().exists(resourcePath1)).toBe(false);
});
}));
it('should encrypt resources', async () => {
it('should encrypt resources', (async () => {
setEncryptionEnabled(true);
const masterKey = await loadEncryptionMasterKey();
@@ -170,9 +170,9 @@ describe('Synchronizer.resources', () => {
const resourcePath1_2 = Resource.fullPath(resource1_2);
expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true);
});
}));
it('should sync resource blob changes', async () => {
it('should sync resource blob changes', (async () => {
const tempFile = tempFilePath('txt');
await shim.fsDriver().writeFile(tempFile, '1234', 'utf8');
const folder1 = await Folder.save({ title: 'folder1' });
@@ -204,9 +204,9 @@ describe('Synchronizer.resources', () => {
const resource1_1 = (await Resource.all())[0];
expect(resource1_1.size).toBe(newSize);
expect(await Resource.resourceBlobContent(resource1_1.id, 'utf8')).toBe('1234 MOD');
});
}));
it('should handle resource conflicts', async () => {
it('should handle resource conflicts', (async () => {
{
const tempFile = tempFilePath('txt');
await shim.fsDriver().writeFile(tempFile, '1234', 'utf8');
@@ -271,9 +271,9 @@ describe('Synchronizer.resources', () => {
expect(resourceConflictFolder).toBeTruthy();
expect(resourceConflictFolder.parent_id).toBeFalsy();
}
});
}));
it('should handle resource conflicts if a resource is changed locally but deleted remotely', async () => {
it('should handle resource conflicts if a resource is changed locally but deleted remotely', (async () => {
{
const tempFile = tempFilePath('txt');
await shim.fsDriver().writeFile(tempFile, '1234', 'utf8');
@@ -316,9 +316,9 @@ describe('Synchronizer.resources', () => {
expect(originalResource.id).not.toBe(conflictResource.id);
expect(conflictResource.title).toBe('modified resource');
}
});
}));
it('should not upload a resource if it has not been fetched yet', async () => {
it('should not upload a resource if it has not been fetched yet', (async () => {
// In some rare cases, the synchronizer might try to upload a resource even though it
// doesn't have the resource file. It can happen in this situation:
// - C1 create resource
@@ -350,9 +350,9 @@ describe('Synchronizer.resources', () => {
await BaseItem.saveSyncEnabled(ModelType.Resource, resource.id);
await synchronizerStart();
expect((await remoteResources()).length).toBe(1);
});
}));
it('should not download resources over the limit', async () => {
it('should not download resources over the limit', (async () => {
const note1 = await Note.save({ title: 'note' });
await shim.attachFileToNote(note1, `${supportDir}/photo.jpg`);
await synchronizer().start();
@@ -368,53 +368,6 @@ describe('Synchronizer.resources', () => {
expect(syncItems.length).toBe(2);
expect(syncItems[1].item_location).toBe(BaseItem.SYNC_ITEM_LOCATION_REMOTE);
expect(syncItems[1].sync_disabled).toBe(1);
});
it('should not upload blob if it has not changed', async () => {
const note = await Note.save({});
await shim.attachFileToNote(note, `${supportDir}/sample.txt`);
const resource: ResourceEntity = (await Resource.all())[0];
const resourcePath = `.resource/${resource.id}`;
await synchronizer().api().put(resourcePath, 'before upload');
expect(await synchronizer().api().get(resourcePath)).toBe('before upload');
await synchronizerStart();
expect(await synchronizer().api().get(resourcePath)).toBe('just testing');
// ----------------------------------------------------------------------
// Change metadata only and check that blob is not uploaded. To do this,
// we manually overwrite the data on the sync target, then sync. If the
// synchronizer doesn't upload the blob, this manually changed data
// should remain.
// ----------------------------------------------------------------------
await Resource.save({ id: resource.id, title: 'my new title' });
await synchronizer().api().put(resourcePath, 'check if changed');
await synchronizerStart();
expect(await synchronizer().api().get(resourcePath)).toBe('check if changed');
// ----------------------------------------------------------------------
// Now change the blob, and check that the remote item has been
// overwritten.
// ----------------------------------------------------------------------
await Resource.updateResourceBlobContent(resource.id, `${supportDir}/sample.txt`);
await synchronizerStart();
expect(await synchronizer().api().get(resourcePath)).toBe('just testing');
// ----------------------------------------------------------------------
// Change the blob, then change the metadata, and sync. Even though
// blob_updated_time is earlier than updated_time, it should still
// update everything on the sync target, because both times are after
// the item sync_time.
// ----------------------------------------------------------------------
await Resource.updateResourceBlobContent(resource.id, `${supportDir}/sample2.txt`);
await msleep(1);
await Resource.save({ id: resource.id, title: 'my new title 2' });
await synchronizerStart();
expect(await synchronizer().api().get(resourcePath)).toBe('just testing 2');
expect(await synchronizer().api().get(`${resource.id}.md`)).toContain('my new title 2');
});
}));
});

View File

@@ -142,12 +142,12 @@ const features = (): Record<FeatureId, PlanFeature> => {
basic: true,
pro: true,
teams: true,
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),
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),
},
publishNote: {
title: _('Publish notes to the internet'),

View File

@@ -25,7 +25,7 @@ const userFetcher = async () => {
const fileApi = await syncTarget.fileApi();
const api = fileApi.driver().api();
if (api.userId) {
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');

View File

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

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.8",
"katex": "0.16.9",
"markdown-it": "13.0.2",
"markdown-it-abbr": "1.0.4",
"markdown-it-anchor": "5.3.0",

View File

@@ -265,11 +265,6 @@
- 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))
@@ -461,11 +456,6 @@
- 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))
@@ -552,6 +542,15 @@ 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

@@ -0,0 +1,31 @@
---
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)

126
yarn.lock
View File

@@ -4666,7 +4666,7 @@ __metadata:
"@types/jest": 29.5.4
"@types/mustache": 4.2.3
"@types/node": 18.17.19
"@types/react": 18.2.23
"@types/react": 18.2.24
"@types/react-redux": 7.1.27
"@types/styled-components": 5.1.28
async-mutex: 0.4.0
@@ -4702,7 +4702,7 @@ __metadata:
react-datetime: 3.2.0
react-dom: 18.2.0
react-redux: 8.1.3
react-select: 5.7.5
react-select: 5.7.7
react-test-renderer: 18.2.0
react-toggle-button: 2.2.0
react-tooltip: 4.5.1
@@ -4754,7 +4754,7 @@ __metadata:
"@tsconfig/react-native": 2.0.2
"@types/fs-extra": 11.0.2
"@types/jest": 29.5.4
"@types/react": 18.2.23
"@types/react": 18.2.24
"@types/react-native": 0.70.6
"@types/react-redux": 7.1.27
"@types/tar-stream": 2.2.3
@@ -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: 4.5.1
react-native-dropdownalert: 5.1.0
react-native-exit-app: 2.0.0
react-native-file-viewer: 2.1.5
react-native-fingerprint-scanner: 6.0.0
@@ -4856,7 +4856,7 @@ __metadata:
"@replit/codemirror-vim": 6.0.14
"@testing-library/react-hooks": 8.0.1
"@types/jest": 29.5.4
"@types/react": 18.2.23
"@types/react": 18.2.24
"@types/react-redux": 7.1.27
"@types/styled-components": 5.1.28
jest: 29.6.3
@@ -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.23
"@types/react": 18.2.24
"@types/uuid": 9.0.4
async-mutex: 0.4.0
base-64: 1.0.0
@@ -5011,7 +5011,7 @@ __metadata:
"@joplin/lib": ~2.13
"@types/jest": 29.5.4
"@types/pdfjs-dist": 2.10.378
"@types/react": 18.2.23
"@types/react": 18.2.24
"@types/react-dom": 18.2.8
"@types/styled-components": 5.1.28
async-mutex: 0.4.0
@@ -5103,7 +5103,7 @@ __metadata:
jest: 29.6.4
jest-environment-jsdom: 29.6.4
json-stringify-safe: 5.0.1
katex: 0.16.8
katex: 0.16.9
markdown-it: 13.0.2
markdown-it-abbr: 1.0.4
markdown-it-anchor: 5.3.0
@@ -8405,14 +8405,14 @@ __metadata:
languageName: node
linkType: hard
"@types/react@npm:18.2.23":
version: 18.2.23
resolution: "@types/react@npm:18.2.23"
"@types/react@npm:18.2.24":
version: 18.2.24
resolution: "@types/react@npm:18.2.24"
dependencies:
"@types/prop-types": "*"
"@types/scheduler": "*"
csstype: ^3.0.2
checksum: efb9d1ed1940c0e7ba08a21ffba5e266d8dbbb8fe618cfb97bc902dfc96385fdd8189e3f7f64b4aa13134f8e61947d60560deb23be151253c3a97b0d070897ca
checksum: ea5d8204e71b1c9c6631f429a93f8e7be0614cdbdb464e92b3181bdccd8a7c45e30ded8b13da726684b6393f651317c36d54832e3d3cdea0da480a3f26268909
languageName: node
linkType: hard
@@ -10166,7 +10166,7 @@ __metadata:
languageName: node
linkType: hard
"asap@npm:^2.0.0, asap@npm:~2.0.3, asap@npm:~2.0.6":
"asap@npm:^2.0.0, asap@npm:~2.0.6":
version: 2.0.6
resolution: "asap@npm:2.0.6"
checksum: b296c92c4b969e973260e47523207cd5769abd27c245a68c26dc7a0fe8053c55bb04360237cb51cab1df52be939da77150ace99ad331fb7fb13b3423ed73ff3d
@@ -13074,13 +13074,6 @@ __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"
@@ -17072,21 +17065,6 @@ __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"
@@ -20609,7 +20587,7 @@ __metadata:
languageName: node
linkType: hard
"is-stream@npm:^1.0.1, is-stream@npm:^1.1.0":
"is-stream@npm:^1.1.0":
version: 1.1.0
resolution: "is-stream@npm:1.1.0"
checksum: 063c6bec9d5647aa6d42108d4c59723d2bd4ae42135a2d4db6eadbd49b7ea05b750fd69d279e5c7c45cf9da753ad2c00d8978be354d65aa9f6bb434969c6a2ae
@@ -20857,16 +20835,6 @@ __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"
@@ -22742,14 +22710,14 @@ __metadata:
languageName: node
linkType: hard
"katex@npm:0.16.8":
version: 0.16.8
resolution: "katex@npm:0.16.8"
"katex@npm:0.16.9":
version: 0.16.9
resolution: "katex@npm:0.16.9"
dependencies:
commander: ^8.3.0
bin:
katex: cli.js
checksum: 4e75b4786101cc5eca0404bb814b2985bec506846f9015e9bf00207a3af14215e341ee62b6e7af2455a1032f8244e47a754642f250eea43d7b8007146ac01fae
checksum: 861194dfd4d86505e657f688fb73048d46ac498edafce71199502a35b03c0ecc35ba930c631be79c4a09d90a0d23476673cd52f6bc367c7a161854d64005fa95
languageName: node
linkType: hard
@@ -23559,7 +23527,7 @@ __metadata:
languageName: node
linkType: hard
"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.3.1, loose-envify@npm:^1.4.0":
"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0":
version: 1.4.0
resolution: "loose-envify@npm:1.4.0"
dependencies:
@@ -26168,16 +26136,6 @@ __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"
@@ -28748,15 +28706,6 @@ __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"
@@ -28787,16 +28736,6 @@ __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"
@@ -29356,12 +29295,10 @@ __metadata:
languageName: node
linkType: hard
"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
"react-native-dropdownalert@npm:5.1.0":
version: 5.1.0
resolution: "react-native-dropdownalert@npm:5.1.0"
checksum: 595e409967a28e5305b7895407a801c6eb05091277eaa7362b30dce18213d695dcbf0811c69cbaa20f35ce9aa21f13d44235dd35717119e0020a3159bd06b0ef
languageName: node
linkType: hard
@@ -29816,9 +29753,9 @@ __metadata:
languageName: node
linkType: hard
"react-select@npm:5.7.5":
version: 5.7.5
resolution: "react-select@npm:5.7.5"
"react-select@npm:5.7.7":
version: 5.7.7
resolution: "react-select@npm:5.7.7"
dependencies:
"@babel/runtime": ^7.12.0
"@emotion/cache": ^11.4.0
@@ -29832,7 +29769,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: 88f2d94c4a6778df525a9fb5d7acac1bf34821f6efcfdc5927ec608f5f933cf3f47e1c4e4fd3b92d7b2ba1d91e44595d45ac4e2fd7528ba420086008ac5a81cf
checksum: 6fd0c211d377addba6e6762a614ae674936df39a3f46ec19fd06e7acae8d6cadeb93d4723b10e25eff1ff8235077bae9459f293936334d82b28fe5071081c057
languageName: node
linkType: hard
@@ -34704,13 +34641,6 @@ __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"
@@ -35873,7 +35803,7 @@ __metadata:
languageName: node
linkType: hard
"whatwg-fetch@npm:>=0.10.0, whatwg-fetch@npm:^3.0.0":
"whatwg-fetch@npm:^3.0.0":
version: 3.6.2
resolution: "whatwg-fetch@npm:3.6.2"
checksum: ee976b7249e7791edb0d0a62cd806b29006ad7ec3a3d89145921ad8c00a3a67e4be8f3fb3ec6bc7b58498724fd568d11aeeeea1f7827e7e1e5eae6c8a275afed