diff --git a/.yarn/patches/react-native-animation-fix/react-native-npm-0.71.10-f9c32562d8.patch b/.yarn/patches/react-native-animation-fix/react-native-npm-0.71.10-f9c32562d8.patch new file mode 100644 index 000000000..f3abb4278 --- /dev/null +++ b/.yarn/patches/react-native-animation-fix/react-native-npm-0.71.10-f9c32562d8.patch @@ -0,0 +1,25 @@ +diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java +index 0f52b73c61625db2a3081c0950b6bdd2b06e3d40..b0fc3de4be0b3a26b638683613c63c783c2739bb 100644 +--- a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java ++++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.java +@@ -38,7 +38,7 @@ import com.facebook.react.uimanager.common.ViewUtil; + import java.util.ArrayList; + import java.util.List; + import java.util.Queue; +-import java.util.concurrent.ConcurrentLinkedQueue; ++import java.util.concurrent.LinkedBlockingQueue; + import java.util.concurrent.atomic.AtomicReference; + + /** +@@ -151,7 +151,10 @@ public class NativeAnimatedModule extends NativeAnimatedModuleSpec + } + + private class ConcurrentOperationQueue { +- private final Queue mQueue = new ConcurrentLinkedQueue<>(); ++ // Patch: Use LinkedBlockingQueue instead of ConcurrentLinkedQueue. ++ // In some versions of Android, ConcurrentLinkedQueue is known to drop ++ // items, causing crashing. See https://github.com/laurent22/joplin/issues/8425 ++ private final Queue mQueue = new LinkedBlockingQueue<>(); + @Nullable private UIThreadOperation mPeekedOperation = null; + + @AnyThread diff --git a/.yarn/patches/react-native-animation-fix/react-native-reanimated-npm-3.3.0-fb4272741c.patch b/.yarn/patches/react-native-animation-fix/react-native-reanimated-npm-3.3.0-fb4272741c.patch new file mode 100644 index 000000000..24a97b696 --- /dev/null +++ b/.yarn/patches/react-native-animation-fix/react-native-reanimated-npm-3.3.0-fb4272741c.patch @@ -0,0 +1,27 @@ +diff --git a/android/src/main/java/com/swmansion/reanimated/NodesManager.java b/android/src/main/java/com/swmansion/reanimated/NodesManager.java +index e974f8eb827a35be4d7e5fa9b096af9387c595dd..bc9e5ff566c9484274e8eacefc88327a5ff30def 100644 +--- a/android/src/main/java/com/swmansion/reanimated/NodesManager.java ++++ b/android/src/main/java/com/swmansion/reanimated/NodesManager.java +@@ -34,7 +34,7 @@ import java.util.List; + import java.util.Map; + import java.util.Queue; + import java.util.Set; +-import java.util.concurrent.ConcurrentLinkedQueue; ++import java.util.concurrent.LinkedBlockingQueue; + import java.util.concurrent.Semaphore; + import java.util.concurrent.TimeUnit; + import java.util.concurrent.atomic.AtomicBoolean; +@@ -80,7 +80,12 @@ public class NodesManager implements EventDispatcherListener { + private ReactApplicationContext mReactApplicationContext; + private RCTEventEmitter mCustomEventHandler; + private List mFrameCallbacks = new ArrayList<>(); +- private ConcurrentLinkedQueue mEventQueue = new ConcurrentLinkedQueue<>(); ++ ++ // Patch: On some versions of Android, ConcurrentLinkedQueue is known to ++ // drop items. LinkedBlockingQueue is a potentially-slower alternative that ++ // should not drop items. ++ // See https://github.com/laurent22/joplin/issues/8425 ++ private LinkedBlockingQueue mEventQueue = new LinkedBlockingQueue<>(); + public double currentFrameTimeMs; + public Set uiProps = Collections.emptySet(); + public Set nativeProps = Collections.emptySet(); diff --git a/package.json b/package.json index f410eb5c3..a93e590de 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "@types/fs-extra": "11.0.1", "eslint-plugin-github": "4.8.0", "http-server": "14.1.1", - "node-gyp": "9.3.1", + "node-gyp": "9.4.0", "nodemon": "2.0.22" }, "packageManager": "yarn@3.5.0", @@ -99,6 +99,8 @@ "react-native-vosk@0.1.12": "patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch", "eslint@8.39.0": "patch:eslint@npm%3A8.39.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch", "eslint@^8.13.0": "patch:eslint@npm%3A8.39.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch", - "app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch" + "app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch", + "react-native@0.71.10": "patch:react-native@npm%3A0.71.10#./.yarn/patches/react-native-animation-fix/react-native-npm-0.71.10-f9c32562d8.patch", + "react-native-reanimated@3.3.0": "patch:react-native-reanimated@npm%3A3.3.0#./.yarn/patches/react-native-animation-fix/react-native-reanimated-npm-3.3.0-fb4272741c.patch" } } diff --git a/packages/app-cli/tests/html_to_md/bold_italic_with_spaces.md b/packages/app-cli/tests/html_to_md/bold_italic_with_spaces.md index 1a4bc23e4..e7d328d7b 100644 --- a/packages/app-cli/tests/html_to_md/bold_italic_with_spaces.md +++ b/packages/app-cli/tests/html_to_md/bold_italic_with_spaces.md @@ -1 +1 @@ -   **A test...** Test \ No newline at end of file +   **A test...** Test \ No newline at end of file diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index 943283d62..9c95c3bf3 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -66,8 +66,7 @@ import syncDebugLog from '@joplin/lib/services/synchronizer/syncDebugLog'; import eventManager from '@joplin/lib/eventManager'; import path = require('path'); import { checkPreInstalledDefaultPlugins, installDefaultPlugins, setSettingsForDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils'; -// import { runIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils'; -import { initializeInboxFetcher, inboxFetcher } from '@joplin/lib/utils/inboxFetcher'; +import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetcher'; const pluginClasses = [ require('./plugins/GotoAnything').default, @@ -488,8 +487,8 @@ class Application extends BaseApplication { shim.setInterval(() => { runAutoUpdateCheck(); }, 12 * 60 * 60 * 1000); } - initializeInboxFetcher(); - shim.setInterval(() => { void inboxFetcher(); }, 1000 * 60 * 60); + initializeUserFetcher(); + shim.setInterval(() => { void userFetcher(); }, 1000 * 60 * 60); this.updateTray(); diff --git a/packages/app-desktop/gui/JoplinCloudConfigScreen.tsx b/packages/app-desktop/gui/JoplinCloudConfigScreen.tsx index 2e1886dca..a5dc002ca 100644 --- a/packages/app-desktop/gui/JoplinCloudConfigScreen.tsx +++ b/packages/app-desktop/gui/JoplinCloudConfigScreen.tsx @@ -25,7 +25,7 @@ const JoplinCloudConfigScreen = (props: JoplinCloudConfigScreenProps) => { const mapStateToProps = (state: AppState) => { return { - inboxEmail: state.settings['emailToNote.inboxEmail'], + inboxEmail: state.settings['sync.10.inboxEmail'], }; }; diff --git a/packages/app-desktop/gui/MainScreen/commands/deleteFolder.ts b/packages/app-desktop/gui/MainScreen/commands/deleteFolder.ts index dbfd21ec9..899031009 100644 --- a/packages/app-desktop/gui/MainScreen/commands/deleteFolder.ts +++ b/packages/app-desktop/gui/MainScreen/commands/deleteFolder.ts @@ -18,7 +18,7 @@ export const runtime = (): CommandRuntime => { if (!folder) throw new Error(`No such folder: ${folderId}`); let deleteMessage = _('Delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.', substrWithEllipsis(folder.title, 0, 32)); - if (folderId === context.state.settings['emailToNote.inboxJopId']) { + if (folderId === context.state.settings['sync.10.inboxId']) { deleteMessage = _('Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that\'s recently been sent to it may be lost.'); } diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx index 7aefd0c19..40ea310ac 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx @@ -599,6 +599,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { joplinInsert: { inline: 'ins', remove: 'all' }, joplinSub: { inline: 'sub', remove: 'all' }, joplinSup: { inline: 'sup', remove: 'all' }, + code: { inline: 'code', remove: 'all', attributes: { spellcheck: false } }, }, setup: (editor: Editor) => { editor.addCommand('joplinAttach', () => { @@ -697,7 +698,17 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { setEditorReady(true); }); + const preprocessContent = () => { + // Disable spellcheck for all inline code blocks. + const codeElements = editor.dom.doc.querySelectorAll('code.inline-code'); + for (const code of codeElements) { + code.setAttribute('spellcheck', 'false'); + } + }; + editor.on('SetContent', () => { + preprocessContent(); + props_onMessage.current({ channel: 'noteRenderComplete' }); }); }, @@ -714,18 +725,10 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { // Set the initial content and load the plugin CSS and JS files // ----------------------------------------------------------------------------------------- - const loadDocumentAssets = (editor: any, pluginAssets: any[]) => { - // Note: The way files are cached is not correct because it assumes there's only one version - // of each file. However, when the theme change, a new CSS file, specific to the theme, is - // created. That file should not be loaded on top of the previous one, but as a replacement. - // Otherwise it would do this: - // - Try to load CSS for theme 1 => OK - // - Try to load CSS for theme 2 => OK - // - Try to load CSS for theme 1 => Skip because the file is in cache. As a result, theme 2 - // incorrectly stay. - // The fix would be to make allAssets() return a name and a version for each asset. Then the loading - // code would check this and either append the CSS or replace. + const documentCssElements: Record = {}; + const documentScriptElements: Record = {}; + const loadDocumentAssets = (editor: any, pluginAssets: any[]) => { const theme = themeStyle(props.themeId); let docHead_: any = null; @@ -736,49 +739,72 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { return docHead_; } - const cssFiles = [ + const allCssFiles = [ `${bridge().vendorDir()}/lib/@fortawesome/fontawesome-free/css/all.min.css`, `gui/note-viewer/pluginAssets/highlight.js/${theme.codeThemeCss}`, ].concat( pluginAssets .filter((a: any) => a.mime === 'text/css') .map((a: any) => a.path) - ).filter((path: string) => !loadedCssFiles_.includes(path)); + ); - const jsFiles = [].concat( + const allJsFiles = [].concat( pluginAssets .filter((a: any) => a.mime === 'application/javascript') .map((a: any) => a.path) - ).filter((path: string) => !loadedJsFiles_.includes(path)); + ); - for (const cssFile of cssFiles) loadedCssFiles_.push(cssFile); - for (const jsFile of jsFiles) loadedJsFiles_.push(jsFile); + + // Remove all previously loaded files that aren't in the assets this time. + // Note: This is important to ensure that we properly change themes. + // See https://github.com/laurent22/joplin/issues/8520 + for (const cssFile of loadedCssFiles_) { + if (!allCssFiles.includes(cssFile)) { + documentCssElements[cssFile]?.remove(); + delete documentCssElements[cssFile]; + } + } + + for (const jsFile of loadedJsFiles_) { + if (!allJsFiles.includes(jsFile)) { + documentScriptElements[jsFile]?.remove(); + delete documentScriptElements[jsFile]; + } + } + + const newCssFiles = allCssFiles.filter((path: string) => !loadedCssFiles_.includes(path)); + const newJsFiles = allJsFiles.filter((path: string) => !loadedJsFiles_.includes(path)); + + loadedCssFiles_ = allCssFiles; + loadedJsFiles_ = allJsFiles; // console.info('loadDocumentAssets: files to load', cssFiles, jsFiles); - if (cssFiles.length) { - for (const cssFile of cssFiles) { - const script = editor.dom.create('link', { + if (newCssFiles.length) { + for (const cssFile of newCssFiles) { + const style = editor.dom.create('link', { rel: 'stylesheet', type: 'text/css', href: cssFile, class: 'jop-tinymce-css', }); - docHead().appendChild(script); + documentCssElements[cssFile] = style; + docHead().appendChild(style); } } - if (jsFiles.length) { + if (newJsFiles.length) { const editorElementId = editor.dom.uniqueId(); - for (const jsFile of jsFiles) { + for (const jsFile of newJsFiles) { const script = editor.dom.create('script', { id: editorElementId, type: 'text/javascript', src: jsFile, }); + documentScriptElements[jsFile] = script; docHead().appendChild(script); } } diff --git a/packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.tsx b/packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.tsx index 4e31ec9ef..059e7dcd2 100644 --- a/packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.tsx +++ b/packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.tsx @@ -96,7 +96,7 @@ interface Props { onClose(): void; shares: StateShare[]; shareUsers: Record; - isJoplinCloud: boolean; + canUseSharePermissions: boolean; } interface RecipientDeleteEvent { @@ -261,7 +261,7 @@ function ShareFolderDialog(props: Props) { function renderAddRecipient() { const disabled = shareState !== ShareState.Idle; - const dropdown = !props.isJoplinCloud ? null : ; + const dropdown = !props.canUseSharePermissions ? null : ; return ( @@ -306,7 +306,7 @@ function ShareFolderDialog(props: Props) { const permission = shareUser.can_write ? 'can_read_and_write' : 'can_read'; const enabled = !recipientsBeingUpdated[shareUser.id]; - const dropdown = !props.isJoplinCloud ? null : recipient_permissionChange(shareUser.id, event.value)}/>; + const dropdown = !props.canUseSharePermissions ? null : recipient_permissionChange(shareUser.id, event.value)}/>; return ( @@ -407,7 +407,7 @@ const mapStateToProps = (state: State) => { return { shares: state.shareService.shares, shareUsers: state.shareService.shareUsers, - isJoplinCloud: state.settings['sync.target'] === 10, + canUseSharePermissions: state.settings['sync.target'] === 10 && state.settings['sync.10.canUseSharePermissions'], }; }; diff --git a/packages/app-desktop/package.json b/packages/app-desktop/package.json index e020f16ca..92f5245a9 100644 --- a/packages/app-desktop/package.json +++ b/packages/app-desktop/package.json @@ -1,6 +1,6 @@ { "name": "@joplin/app-desktop", - "version": "2.12.8", + "version": "2.12.9", "description": "Joplin for Desktop", "main": "main.js", "private": true, @@ -161,7 +161,7 @@ "react": "18.2.0", "react-datetime": "3.2.0", "react-dom": "18.2.0", - "react-redux": "8.0.7", + "react-redux": "8.1.1", "react-select": "5.7.3", "react-toggle-button": "2.2.0", "react-tooltip": "4.5.1", diff --git a/packages/app-desktop/runForTesting.sh b/packages/app-desktop/runForTesting.sh index 0275b2c9c..f9eabc233 100755 --- a/packages/app-desktop/runForTesting.sh +++ b/packages/app-desktop/runForTesting.sh @@ -31,6 +31,12 @@ # ./runForTesting.sh 1 createUsers,createData,reset,sync && ./runForTesting.sh 1a reset,sync && ./runForTesting.sh 1 # ./runForTesting.sh 1a +# ---------------------------------------------------------------------------------- +# Team accounts: +# ---------------------------------------------------------------------------------- + +# ./runForTesting.sh 1 createTeams,createData,resetTeam,sync && ./runForTesting.sh 2 resetTeam,sync && ./runForTesting.sh 1 + set -e SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" @@ -54,6 +60,16 @@ if [ "$USER_NUM" = "1b" ]; then USER_PROFILE_NUM=1b fi +if [ "$USER_NUM" = "2a" ]; then + USER_NUM=2 + USER_PROFILE_NUM=2a +fi + +if [ "$USER_NUM" = "2b" ]; then + USER_NUM=2 + USER_PROFILE_NUM=2b +fi + COMMANDS=($(echo $2 | tr "," "\n")) PROFILE_DIR=~/.config/joplindev-desktop-$USER_PROFILE_NUM SYNC_TARGET=10 @@ -74,6 +90,10 @@ do curl --data '{"action": "createUserDeletions"}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug + elif [[ $CMD == "createTeams" ]]; then + + curl --data '{"action": "createTeams"}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug + elif [[ $CMD == "createData" ]]; then echo 'mkbook "shared"' >> "$CMD_FILE" @@ -96,6 +116,16 @@ do echo "config sync.$SYNC_TARGET.path http://api.joplincloud.local:22300" >> "$CMD_FILE" echo "config sync.$SYNC_TARGET.userContentPath http://joplinusercontent.local:22300" >> "$CMD_FILE" fi + + elif [[ $CMD == "resetTeam" ]]; then + + USER_EMAIL="teamuser1-$USER_NUM@example.com" + rm -rf "$PROFILE_DIR" + + echo "config keychain.supported 0" >> "$CMD_FILE" + echo "config sync.target $SYNC_TARGET" >> "$CMD_FILE" + echo "config sync.$SYNC_TARGET.username $USER_EMAIL" >> "$CMD_FILE" + echo "config sync.$SYNC_TARGET.password 111111" >> "$CMD_FILE" elif [[ $CMD == "e2ee" ]]; then diff --git a/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx b/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx index a6544d49b..aabff19f3 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx @@ -362,13 +362,13 @@ class ConfigScreenComponent extends BaseScreenComponent { {_('Email to note')} - {this.props.settings['emailToNote.inboxEmail']} + {this.props.settings['sync.10.inboxEmail']} { this.renderButton( - 'emailToNote.inboxEmail', + 'sync.10.inboxEmail', _('Copy to clipboard'), - () => Clipboard.setString(this.props.settings['emailToNote.inboxEmail']), + () => Clipboard.setString(this.props.settings['sync.10.inboxEmail']), { description } ) } diff --git a/packages/app-mobile/components/side-menu-content.tsx b/packages/app-mobile/components/side-menu-content.tsx index 917bdd95c..282b0b2c0 100644 --- a/packages/app-mobile/components/side-menu-content.tsx +++ b/packages/app-mobile/components/side-menu-content.tsx @@ -528,6 +528,6 @@ export default connect((state: AppState) => { isOnMobileData: state.isOnMobileData, syncOnlyOverWifi: state.settings['sync.mobileWifiOnly'], profileConfig: state.profileConfig, - inboxJopId: state.settings['emailToNote.inboxJopId'], + inboxJopId: state.settings['sync.10.inboxId'], }; })(SideMenuContentComponent); diff --git a/packages/app-mobile/ios/Podfile.lock b/packages/app-mobile/ios/Podfile.lock index cfbf3773e..7d6a07207 100644 --- a/packages/app-mobile/ios/Podfile.lock +++ b/packages/app-mobile/ios/Podfile.lock @@ -467,7 +467,7 @@ PODS: - React-Core - RNDateTimePicker (7.1.0): - React-Core - - RNDeviceInfo (10.6.0): + - RNDeviceInfo (10.6.1): - React-Core - RNExitApp (1.1.0): - React @@ -850,7 +850,7 @@ SPEC CHECKSUMS: RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495 RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8 RNDateTimePicker: 7ecd54a97fc3749f38c3c89a171f6cbd52f3c142 - RNDeviceInfo: 475a4c447168d0ad4c807e48ef5e0963a0f4eb1b + RNDeviceInfo: ab292735ad4fccc5f2aec0c773f7a7f03c7073ae RNExitApp: c4e052df2568b43bec8a37c7cd61194d4cfee2c3 RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592 RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 diff --git a/packages/app-mobile/package.json b/packages/app-mobile/package.json index 52d24a6ec..cbd547c9c 100644 --- a/packages/app-mobile/package.json +++ b/packages/app-mobile/package.json @@ -53,11 +53,11 @@ "react-native-file-viewer": "2.1.5", "react-native-fingerprint-scanner": "6.0.0", "react-native-fs": "2.20.0", - "react-native-gesture-handler": "2.11.0", + "react-native-gesture-handler": "2.12.0", "react-native-get-random-values": "1.9.0", "react-native-image-picker": "5.4.2", "react-native-image-resizer": "1.4.5", - "react-native-localize": "3.0.1", + "react-native-localize": "3.0.2", "react-native-modal-datetime-picker": "15.0.1", "react-native-paper": "5.8.0", "react-native-popup-menu": "0.16.1", @@ -74,7 +74,7 @@ "react-native-vosk": "0.1.12", "react-native-webview": "12.4.0", "react-native-zip-archive": "6.0.9", - "react-redux": "8.0.7", + "react-redux": "8.1.1", "redux": "4.2.1", "rn-fetch-blob": "0.12.0", "stream": "0.0.2", diff --git a/packages/app-mobile/root.tsx b/packages/app-mobile/root.tsx index 5f458dd81..5e19f07ba 100644 --- a/packages/app-mobile/root.tsx +++ b/packages/app-mobile/root.tsx @@ -117,7 +117,7 @@ import sensorInfo, { SensorInfo } from './components/biometrics/sensorInfo'; import { getCurrentProfile } from '@joplin/lib/services/profileConfig'; import { getDatabaseName, getProfilesRootDir, getResourceDir, setDispatch } from './services/profiles'; import { ReactNode } from 'react'; -import { initializeInboxFetcher, inboxFetcher } from '@joplin/lib/utils/inboxFetcher'; +import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetcher'; import { parseShareCache } from '@joplin/lib/services/share/reducer'; import autodetectTheme, { onSystemColorSchemeChange } from './utils/autodetectTheme'; @@ -665,8 +665,8 @@ async function initialize(dispatch: Function) { reg.setupRecurrentSync(); - initializeInboxFetcher(); - PoorManIntervals.setInterval(() => { void inboxFetcher(); }, 1000 * 60 * 60); + initializeUserFetcher(); + PoorManIntervals.setInterval(() => { void userFetcher(); }, 1000 * 60 * 60); PoorManIntervals.setTimeout(() => { void AlarmService.garbageCollect(); diff --git a/packages/lib/BaseModel.ts b/packages/lib/BaseModel.ts index 374c08bcb..c3ceaf7e1 100644 --- a/packages/lib/BaseModel.ts +++ b/packages/lib/BaseModel.ts @@ -38,6 +38,8 @@ export interface DeleteOptions { // sync, we don't need to track the deletion, because the operation doesn't // need to applied again on next sync. trackDeleted?: boolean; + + disableReadOnlyCheck?: boolean; } class BaseModel { diff --git a/packages/lib/models/BaseItem.ts b/packages/lib/models/BaseItem.ts index bddf0224f..0cedc6757 100644 --- a/packages/lib/models/BaseItem.ts +++ b/packages/lib/models/BaseItem.ts @@ -1,5 +1,5 @@ import { ModelType, DeleteOptions } from '../BaseModel'; -import { BaseItemEntity, NoteEntity } from '../services/database/types'; +import { BaseItemEntity, DeletedItemEntity, NoteEntity } from '../services/database/types'; import Setting from './Setting'; import BaseModel from '../BaseModel'; import time from '../time'; @@ -285,7 +285,7 @@ export default class BaseItem extends BaseModel { }); } - if (needsReadOnlyChecks(this.modelType(), options.changeSource, this.syncShareCache)) { + if (needsReadOnlyChecks(this.modelType(), options.changeSource, this.syncShareCache, options.disableReadOnlyCheck)) { const previousItems = await this.loadItemsByTypeAndIds(this.modelType(), ids, { fields: ['share_id', 'id'] }); checkIfItemsCanBeChanged(this.modelType(), options.changeSource, previousItems, this.syncShareCache); } @@ -321,7 +321,7 @@ export default class BaseItem extends BaseModel { // - Client 1 syncs with target 2 only => the note is *not* deleted from target 2 because no information // that it was previously deleted exist (deleted_items entry has been deleted). // The solution would be to permanently store the list of deleted items on each client. - public static deletedItems(syncTarget: number) { + public static deletedItems(syncTarget: number): Promise { return this.db().selectAll('SELECT * FROM deleted_items WHERE sync_target = ?', [syncTarget]); } diff --git a/packages/lib/models/Folder.ts b/packages/lib/models/Folder.ts index 7047e0ea2..8b55f6340 100644 --- a/packages/lib/models/Folder.ts +++ b/packages/lib/models/Folder.ts @@ -80,6 +80,21 @@ export default class Folder extends BaseItem { return this.db().exec(query); } + public static async deleteAllByShareId(shareId: string, deleteOptions: DeleteOptions = null) { + const tableNameToClasses: Record = { + 'folders': Folder, + 'notes': Note, + 'resources': Resource, + }; + + for (const tableName of ['folders', 'notes', 'resources']) { + const ItemClass = tableNameToClasses[tableName]; + const rows = await this.db().selectAll(`SELECT id FROM ${tableName} WHERE share_id = ?`, [shareId]); + const ids: string[] = rows.map(r => r.id); + await ItemClass.batchDelete(ids, deleteOptions); + } + } + public static async delete(folderId: string, options: DeleteOptions = null) { options = { deleteChildren: true, @@ -90,12 +105,16 @@ export default class Folder extends BaseItem { if (!folder) return; // noop if (options.deleteChildren) { + const childrenDeleteOptions: DeleteOptions = { + disableReadOnlyCheck: options.disableReadOnlyCheck, + }; + const noteIds = await Folder.noteIds(folderId); - await Note.batchDelete(noteIds); + await Note.batchDelete(noteIds, childrenDeleteOptions); const subFolderIds = await Folder.subFolderIds(folderId); for (let i = 0; i < subFolderIds.length; i++) { - await Folder.delete(subFolderIds[i]); + await Folder.delete(subFolderIds[i], childrenDeleteOptions); } } @@ -762,7 +781,14 @@ export default class Folder extends BaseItem { syncDebugLog.info('Folder Save:', o); - const savedFolder: FolderEntity = await super.save(o, options); + let savedFolder: FolderEntity = await super.save(o, options); + + // Ensures that any folder added to the state has all the required + // properties, in particular "share_id" and "parent_id', which are + // required in various parts of the code. + if (!('share_id' in savedFolder) || !('parent_id' in savedFolder)) { + savedFolder = await this.load(savedFolder.id); + } this.dispatch({ type: 'FOLDER_UPDATE_ONE', diff --git a/packages/lib/models/ItemChange.ts b/packages/lib/models/ItemChange.ts index e98a570b9..9cde119dc 100644 --- a/packages/lib/models/ItemChange.ts +++ b/packages/lib/models/ItemChange.ts @@ -21,6 +21,7 @@ export default class ItemChange extends BaseModel { public static SOURCE_UNSPECIFIED = 1; public static SOURCE_SYNC = 2; public static SOURCE_DECRYPTION = 2; // CAREFUL - SAME ID AS SOURCE_SYNC! + public static SOURCE_SHARE_SERVICE = 4; public static tableName() { return 'item_changes'; diff --git a/packages/lib/models/Note.ts b/packages/lib/models/Note.ts index 19db52727..5ca444418 100644 --- a/packages/lib/models/Note.ts +++ b/packages/lib/models/Note.ts @@ -1,4 +1,4 @@ -import BaseModel, { ModelType } from '../BaseModel'; +import BaseModel, { DeleteOptions, ModelType } from '../BaseModel'; import BaseItem from './BaseItem'; import ItemChange from './ItemChange'; import Setting from './Setting'; @@ -744,7 +744,7 @@ export default class Note extends BaseItem { return note; } - public static async batchDelete(ids: string[], options: any = null) { + public static async batchDelete(ids: string[], options: DeleteOptions = null) { ids = ids.slice(); while (ids.length) { diff --git a/packages/lib/models/Setting.ts b/packages/lib/models/Setting.ts index f8c5cceac..028154102 100644 --- a/packages/lib/models/Setting.ts +++ b/packages/lib/models/Setting.ts @@ -717,6 +717,12 @@ class Setting extends BaseModel { secure: true, }, + 'sync.10.inboxEmail': { value: '', type: SettingItemType.String, public: false }, + + 'sync.10.inboxId': { value: '', type: SettingItemType.String, public: false }, + + 'sync.10.canUseSharePermissions': { value: false, type: SettingItemType.Bool, public: false }, + 'sync.5.syncTargets': { value: {}, type: SettingItemType.Object, public: false }, 'sync.resourceDownloadMode': { @@ -1714,10 +1720,6 @@ class Setting extends BaseModel { label: () => _('Voice typing language files (URL)'), section: 'note', }, - - 'emailToNote.inboxEmail': { value: '', type: SettingItemType.String, public: false }, - - 'emailToNote.inboxJopId': { value: '', type: SettingItemType.String, public: false }, }; this.metadata_ = { ...this.buildInMetadata_ }; diff --git a/packages/lib/models/utils/readOnly.ts b/packages/lib/models/utils/readOnly.ts index 7cc3b02ac..fe28a8bff 100644 --- a/packages/lib/models/utils/readOnly.ts +++ b/packages/lib/models/utils/readOnly.ts @@ -13,7 +13,8 @@ export interface ItemSlice { // This function can be called to wrap any read-only-related code. It should be // fast and allows an early exit for cases that don't apply, for example if not // synchronising with Joplin Cloud or if not sharing any notebook. -export const needsReadOnlyChecks = (itemType: ModelType, changeSource: number, shareState: ShareState) => { +export const needsReadOnlyChecks = (itemType: ModelType, changeSource: number, shareState: ShareState, disableReadOnlyCheck = false) => { + if (disableReadOnlyCheck) return false; if (Setting.value('sync.target') !== 10) return false; if (changeSource === ItemChange.SOURCE_SYNC) return false; if (!Setting.value('sync.userId')) return false; diff --git a/packages/lib/models/utils/types.ts b/packages/lib/models/utils/types.ts index bb25c3218..eac98bd9b 100644 --- a/packages/lib/models/utils/types.ts +++ b/packages/lib/models/utils/types.ts @@ -33,4 +33,5 @@ export interface SaveOptions { ignoreProvisionalFlag?: boolean; dispatchUpdateAction?: boolean; changeSource?: number; + disableReadOnlyCheck?: boolean; } diff --git a/packages/lib/services/share/ShareService.test.ts b/packages/lib/services/share/ShareService.test.ts index 8243d9635..2d1e4d222 100644 --- a/packages/lib/services/share/ShareService.test.ts +++ b/packages/lib/services/share/ShareService.test.ts @@ -1,5 +1,5 @@ import Note from '../../models/Note'; -import { encryptionService, loadEncryptionMasterKey, msleep, resourceService, setupDatabaseAndSynchronizer, supportDir, switchClient } from '../../testing/test-utils'; +import { createFolderTree, encryptionService, loadEncryptionMasterKey, msleep, resourceService, setupDatabaseAndSynchronizer, simulateReadOnlyShareEnv, supportDir, switchClient } from '../../testing/test-utils'; import ShareService from './ShareService'; import reducer, { defaultState } from '../../reducer'; import { createStore } from 'redux'; @@ -15,6 +15,9 @@ import shim from '../../shim'; import Resource from '../../models/Resource'; import { readFile } from 'fs-extra'; import BaseItem from '../../models/BaseItem'; +import ResourceService from '../ResourceService'; +import Setting from '../../models/Setting'; +import { ModelType } from '../../BaseModel'; interface TestShareFolderServiceOptions { master_key_id?: string; @@ -239,5 +242,40 @@ describe('ShareService', () => { Logger.globalLogger.setLevel(previousLogLevel); }); + it('should leave a shared folder', async () => { + const folder1 = await createFolderTree('', [ + { + title: 'folder 1', + children: [ + { + title: 'note 1', + }, + { + title: 'note 2', + }, + ], + }, + ]); + + const resourceService = new ResourceService(); + await Folder.save({ id: folder1.id, share_id: '123456789' }); + await Folder.updateAllShareIds(resourceService); + + const cleanup = simulateReadOnlyShareEnv('123456789'); + + const shareService = testShareFolderService(); + await shareService.leaveSharedFolder(folder1.id, 'somethingrandom'); + + expect(await Folder.count()).toBe(0); + expect(await Note.count()).toBe(0); + + const deletedItems = await BaseItem.deletedItems(Setting.value('sync.target')); + + expect(deletedItems.length).toBe(1); + expect(deletedItems[0].item_type).toBe(ModelType.Folder); + expect(deletedItems[0].item_id).toBe(folder1.id); + + cleanup(); + }); }); diff --git a/packages/lib/services/share/ShareService.ts b/packages/lib/services/share/ShareService.ts index eb9a40e51..b4e3cd38e 100644 --- a/packages/lib/services/share/ShareService.ts +++ b/packages/lib/services/share/ShareService.ts @@ -173,14 +173,21 @@ export default class ShareService { // This is when a share recipient decides to leave the shared folder. // - // In that case, we should only delete the folder but none of its children. - // Deleting the folder tells the server that we want to leave the share. The - // server will then proceed to delete all associated user_items. So - // eventually all the notebook content will also be deleted for the current - // user. + // In that case we delete the root folder. Deleting the folder tells the + // server that we want to leave the share. // - // We don't delete the children here because that would delete them for the - // other share participants too. + // We also immediately delete the children, but we do not sync the changes + // otherwise it would delete the items for other users too. + // + // If we do not delete them now it would also cause all kind of issues with + // read-only shares, because the read-only status will be lost after the + // deletion of the root folder, which means various services may modify the + // data. The changes will then be rejected by the sync target and cause + // conflicts. + // + // We do not need to sync the children deletion, because the server will + // take care of deleting all associated user_items. So eventually all the + // notebook content will also be deleted for the current user. // // If `folderShareUserId` is provided, the function will check that the user // does not own the share. It would be an error to leave such a folder @@ -191,7 +198,14 @@ export default class ShareService { if (folderShareUserId === userId) throw new Error('Cannot leave own notebook'); } - await Folder.delete(folderId, { deleteChildren: false }); + const folder = await Folder.load(folderId); + + // We call this to make sure all items are correctly linked before we + // call deleteAllByShareId() + await Folder.updateAllShareIds(ResourceService.instance()); + + await Folder.delete(folderId, { deleteChildren: false, disableReadOnlyCheck: true }); + await Folder.deleteAllByShareId(folder.share_id, { disableReadOnlyCheck: true, trackDeleted: false }); } // Finds any folder that is associated with a share, but the user no longer diff --git a/packages/lib/services/share/reducer.ts b/packages/lib/services/share/reducer.ts index 8a5f622f2..d6e707fc1 100644 --- a/packages/lib/services/share/reducer.ts +++ b/packages/lib/services/share/reducer.ts @@ -94,6 +94,11 @@ export function isSharedFolderOwner(state: RootState, folderId: string): boolean } export function isRootSharedFolder(folder: FolderEntity): boolean { + if (!('share_id' in folder) || !('parent_id' in folder)) { + logger.warn('Calling isRootSharedFolder without specifying share_id and parent_id:', folder); + return false; + } + return !!folder.share_id && !folder.parent_id; } diff --git a/packages/lib/utils/inboxFetcher.ts b/packages/lib/utils/inboxFetcher.ts deleted file mode 100644 index 071d2c371..000000000 --- a/packages/lib/utils/inboxFetcher.ts +++ /dev/null @@ -1,30 +0,0 @@ -import SyncTargetRegistry from '../SyncTargetRegistry'; -import eventManager from '../eventManager'; -import Setting from '../models/Setting'; -import { reg } from '../registry'; - -export const inboxFetcher = async () => { - - if (Setting.value('sync.target') !== SyncTargetRegistry.nameToId('joplinCloud')) { - return; - } - - const syncTarget = reg.syncTarget(); - const fileApi = await syncTarget.fileApi(); - const api = fileApi.driver().api(); - - const owner = await api.exec('GET', `api/users/${api.userId}`); - - if (owner.inbox) { - Setting.setValue('emailToNote.inboxJopId', owner.inbox.jop_id); - } - - if (owner.inbox_email) { - Setting.setValue('emailToNote.inboxEmail', owner.inbox_email); - } -}; - -// Listen to the event only once -export const initializeInboxFetcher = () => { - eventManager.once('sessionEstablished', inboxFetcher); -}; diff --git a/packages/lib/utils/userFetcher.ts b/packages/lib/utils/userFetcher.ts new file mode 100644 index 000000000..884ec0e7e --- /dev/null +++ b/packages/lib/utils/userFetcher.ts @@ -0,0 +1,41 @@ +import SyncTargetRegistry from '../SyncTargetRegistry'; +import eventManager from '../eventManager'; +import Setting from '../models/Setting'; +import { reg } from '../registry'; +import Logger from '../Logger'; + +const logger = Logger.create('userFetcher'); + +interface UserApiResponse { + inbox?: { + jop_id: string; + }; + inbox_email?: string; + can_use_share_permissions?: number; +} + +const userFetcher = async () => { + + if (Setting.value('sync.target') !== SyncTargetRegistry.nameToId('joplinCloud')) { + return; + } + + const syncTarget = reg.syncTarget(); + const fileApi = await syncTarget.fileApi(); + const api = fileApi.driver().api(); + + const owner: UserApiResponse = await api.exec('GET', `api/users/${api.userId}`); + + logger.info('Got user:', owner); + + Setting.setValue('sync.10.inboxId', owner.inbox ? owner.inbox.jop_id : ''); + Setting.setValue('sync.10.inboxEmail', owner.inbox_email ? owner.inbox_email : ''); + Setting.setValue('sync.10.canUseSharePermissions', !!owner.can_use_share_permissions); +}; + +// Listen to the event only once +export const initializeUserFetcher = () => { + eventManager.once('sessionEstablished', userFetcher); +}; + +export default userFetcher; diff --git a/packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js b/packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js index 91d6b3e7b..0dfa29d60 100644 --- a/packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js +++ b/packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js @@ -1,6 +1,6 @@ const utils = require('../utils'); -const glob = require('glob'); const rootDir = utils.rootDir(); +const { globSync } = require('@joplin/utils/fs'); module.exports = { src: '', @@ -11,7 +11,7 @@ module.exports = { // // https://github.com/isaacs/node-glob/issues/371 - const tsFiles = glob.sync('{**/*.ts,**/*.tsx}', { + const tsFiles = globSync('{**/*.ts,**/*.tsx}', { cwd: rootDir, ignore: [ '**/.git/**', @@ -32,7 +32,6 @@ module.exports = { 'packages/app-desktop/dist/**', 'packages/app-mobile/android/**', 'packages/app-mobile/ios/**', - // 'packages/fork-htmlparser2/**', 'packages/fork-sax/**', 'packages/lib/plugin_types/**', 'packages/server/**', @@ -52,29 +51,12 @@ module.exports = { return `${s.join('.')}.js`; }); - // const ignoredMapFiles = tsFiles.map(f => { - // const s = f.split('.'); - // s.pop(); - // return `${s.join('.')}.js.map`; - // }); - - // const ignoredDefFiles = tsFiles.map(f => { - // const s = f.split('.'); - // s.pop(); - // return `${s.join('.')}.d.ts`; - // }); - - // const ignoredFiles = ignoredJsFiles.concat(ignoredMapFiles).concat(ignoredDefFiles); - const ignoredFiles = ignoredJsFiles; - ignoredFiles.sort(); - const regex = /(# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD)[\s\S]*(# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD)/; - const replacement = `$1\n${ignoredFiles.join('\n')}\n$2`; + const replacement = `$1\n${ignoredJsFiles.join('\n')}\n$2`; await Promise.all([ utils.replaceFileText(`${rootDir}/.gitignore`, regex, replacement), utils.replaceFileText(`${rootDir}/.eslintignore`, regex, replacement), - // utils.replaceFileText(`${rootDir}/.ignore`, regex, replacement), ]); }, }; diff --git a/packages/tools/package.json b/packages/tools/package.json index 6126dd0aa..1d85ee659 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -27,7 +27,7 @@ "dayjs": "1.11.9", "execa": "4.1.0", "fs-extra": "11.1.1", - "gettext-parser": "6.0.0", + "gettext-parser": "7.0.1", "glob": "10.2.7", "markdown-it": "13.0.1", "md5-file": "5.0.0", diff --git a/packages/turndown/src/commonmark-rules.js b/packages/turndown/src/commonmark-rules.js index d6eb8e560..5a60c64d8 100644 --- a/packages/turndown/src/commonmark-rules.js +++ b/packages/turndown/src/commonmark-rules.js @@ -1,4 +1,4 @@ -import { repeat, isCodeBlockSpecialCase1, isCodeBlockSpecialCase2, isCodeBlock, getStyleProp } from './utilities' +import { repeat, isCodeBlockSpecialCase1, isCodeBlockSpecialCase2, isCodeBlock, getStyleProp, htmlEscapeLeadingNonbreakingSpace } from './utilities' const Entities = require('html-entities').AllHtmlEntities; const htmlentities = (new Entities()).encode; @@ -25,6 +25,13 @@ rules.paragraph = { filter: 'p', replacement: function (content) { + // If the line starts with a nonbreaking space, replace it. By default, the + // markdown renderer removes leading non-HTML-escaped nonbreaking spaces. However, + // because the space is nonbreaking, we want to keep it. + // \u00A0 is a nonbreaking space. + const leadingNonbreakingSpace = /^\u{00A0}/ug; + content = content.replace(leadingNonbreakingSpace, ' '); + return '\n\n' + content + '\n\n' } } diff --git a/packages/turndown/src/turndown.js b/packages/turndown/src/turndown.js index 59d5b3c1b..52ec44a1f 100644 --- a/packages/turndown/src/turndown.js +++ b/packages/turndown/src/turndown.js @@ -36,7 +36,6 @@ export default function TurndownService (options) { linkReferenceStyle: 'full', anchorNames: [], br: ' ', - nonbreakingSpace: ' ', disableEscapeContent: false, preformattedCode: false, blankReplacement: function (content, node) { @@ -216,15 +215,10 @@ function replacementForNode (node) { var whitespace = node.flankingWhitespace if (whitespace.leading || whitespace.trailing) content = content.trim() - const replaceNonbreakingSpaces = space => { - // \u{00A0} is a nonbreaking space - return space.replace(/\u{00A0}/ug, this.options.nonbreakingSpace); - }; - return ( - replaceNonbreakingSpaces(whitespace.leading) + - replaceNonbreakingSpaces(rule.replacement(content, node, this.options)) + - replaceNonbreakingSpaces(whitespace.trailing) + whitespace.leading + + rule.replacement(content, node, this.options) + + whitespace.trailing ) } diff --git a/packages/utils/fs.ts b/packages/utils/fs.ts new file mode 100644 index 000000000..a707362e8 --- /dev/null +++ b/packages/utils/fs.ts @@ -0,0 +1,12 @@ +/* eslint-disable import/prefer-default-export */ + +import { GlobOptionsWithFileTypesFalse, sync } from 'glob'; + +// Wraps glob.sync but with good default options so that it works across +// platforms and with consistent sorting. +export const globSync = (pattern: string | string[], options: GlobOptionsWithFileTypesFalse) => { + let output = sync(pattern, options); + output = output.map(f => f.replace(/\\/g, '/')); + output.sort(); + return output; +}; diff --git a/packages/utils/package.json b/packages/utils/package.json index c622e6833..2c9d6f0d6 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -5,7 +5,8 @@ "repository": "https://github.com/laurent22/joplin/tree/dev/packages/utils", "exports": { ".": "./dist/index.js", - "./net": "./dist/net.js" + "./net": "./dist/net.js", + "./fs": "./dist/fs.js" }, "publishConfig": { "access": "public" @@ -21,6 +22,7 @@ "dependencies": { "execa": "5.1.1", "fs-extra": "11.1.1", + "glob": "10.2.7", "node-fetch": "2.6.7" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 8011ac202..03cb26901 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3966,13 +3966,6 @@ __metadata: languageName: node linkType: hard -"@gar/promisify@npm:^1.1.3": - version: 1.1.3 - resolution: "@gar/promisify@npm:1.1.3" - checksum: 4059f790e2d07bf3c3ff3e0fec0daa8144fe35c1f6e0111c9921bd32106adaa97a4ab096ad7dab1e28ee6a9060083c4d1a4ada42a7f5f3f7a96b8812e2b757c1 - languageName: node - linkType: hard - "@github/browserslist-config@npm:^1.0.0": version: 1.0.0 resolution: "@github/browserslist-config@npm:1.0.0" @@ -4487,7 +4480,7 @@ __metadata: react: 18.2.0 react-datetime: 3.2.0 react-dom: 18.2.0 - react-redux: 8.0.7 + react-redux: 8.1.1 react-select: 5.7.3 react-test-renderer: 18.2.0 react-toggle-button: 2.2.0 @@ -4590,11 +4583,11 @@ __metadata: react-native-file-viewer: 2.1.5 react-native-fingerprint-scanner: 6.0.0 react-native-fs: 2.20.0 - react-native-gesture-handler: 2.11.0 + react-native-gesture-handler: 2.12.0 react-native-get-random-values: 1.9.0 react-native-image-picker: 5.4.2 react-native-image-resizer: 1.4.5 - react-native-localize: 3.0.1 + react-native-localize: 3.0.2 react-native-modal-datetime-picker: 15.0.1 react-native-paper: 5.8.0 react-native-popup-menu: 0.16.1 @@ -4611,7 +4604,7 @@ __metadata: react-native-vosk: 0.1.12 react-native-webview: 12.4.0 react-native-zip-archive: 6.0.9 - react-redux: 8.0.7 + react-redux: 8.1.1 react-test-renderer: 18.2.0 redux: 4.2.1 rn-fetch-blob: 0.12.0 @@ -4970,7 +4963,7 @@ __metadata: execa: 4.1.0 fs-extra: 11.1.1 gettext-extractor: 3.7.2 - gettext-parser: 6.0.0 + gettext-parser: 7.0.1 glob: 10.2.7 gulp: 4.0.2 html-entities: 1.4.0 @@ -5042,6 +5035,7 @@ __metadata: "@types/node-fetch": 2.6.3 execa: 5.1.1 fs-extra: 11.1.1 + glob: 10.2.7 jest: 29.5.0 node-fetch: 2.6.7 ts-jest: 29.1.0 @@ -6172,16 +6166,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/fs@npm:^2.1.0": - version: 2.1.2 - resolution: "@npmcli/fs@npm:2.1.2" - dependencies: - "@gar/promisify": ^1.1.3 - semver: ^7.3.5 - checksum: 405074965e72d4c9d728931b64d2d38e6ea12066d4fad651ac253d175e413c06fe4350970c783db0d749181da8fe49c42d3880bd1cbc12cd68e3a7964d820225 - languageName: node - linkType: hard - "@npmcli/fs@npm:^3.1.0": version: 3.1.0 resolution: "@npmcli/fs@npm:3.1.0" @@ -6229,16 +6213,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/move-file@npm:^2.0.0": - version: 2.0.1 - resolution: "@npmcli/move-file@npm:2.0.1" - dependencies: - mkdirp: ^1.0.4 - rimraf: ^3.0.2 - checksum: 52dc02259d98da517fae4cb3a0a3850227bdae4939dda1980b788a7670636ca2b4a01b58df03dd5f65c1e3cb70c50fa8ce5762b582b3f499ec30ee5ce1fd9380 - languageName: node - linkType: hard - "@npmcli/node-gyp@npm:^3.0.0": version: 3.0.0 resolution: "@npmcli/node-gyp@npm:3.0.0" @@ -11098,32 +11072,6 @@ __metadata: languageName: node linkType: hard -"cacache@npm:^16.1.0": - version: 16.1.3 - resolution: "cacache@npm:16.1.3" - dependencies: - "@npmcli/fs": ^2.1.0 - "@npmcli/move-file": ^2.0.0 - chownr: ^2.0.0 - fs-minipass: ^2.1.0 - glob: ^8.0.1 - infer-owner: ^1.0.4 - lru-cache: ^7.7.1 - minipass: ^3.1.6 - minipass-collect: ^1.0.2 - minipass-flush: ^1.0.5 - minipass-pipeline: ^1.2.4 - mkdirp: ^1.0.4 - p-map: ^4.0.0 - promise-inflight: ^1.0.1 - rimraf: ^3.0.2 - ssri: ^9.0.0 - tar: ^6.1.11 - unique-filename: ^2.0.0 - checksum: d91409e6e57d7d9a3a25e5dcc589c84e75b178ae8ea7de05cbf6b783f77a5fae938f6e8fda6f5257ed70000be27a681e1e44829251bfffe4c10216002f8f14e6 - languageName: node - linkType: hard - "cacache@npm:^17.0.0": version: 17.1.3 resolution: "cacache@npm:17.1.3" @@ -12440,6 +12388,13 @@ __metadata: languageName: node linkType: hard +"content-type@npm:^1.0.5": + version: 1.0.5 + resolution: "content-type@npm:1.0.5" + checksum: 566271e0a251642254cde0f845f9dd4f9856e52d988f4eb0d0dcffbb7a1f8ec98de7a5215fc628f3bce30fe2fb6fd2bc064b562d721658c59b544e2d34ea2766 + languageName: node + linkType: hard + "conventional-changelog-angular@npm:^5.0.3": version: 5.0.13 resolution: "conventional-changelog-angular@npm:5.0.13" @@ -17274,7 +17229,7 @@ __metadata: languageName: node linkType: hard -"fs-minipass@npm:^2.0.0, fs-minipass@npm:^2.1.0": +"fs-minipass@npm:^2.0.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" dependencies: @@ -17707,15 +17662,15 @@ __metadata: languageName: node linkType: hard -"gettext-parser@npm:6.0.0": - version: 6.0.0 - resolution: "gettext-parser@npm:6.0.0" +"gettext-parser@npm:7.0.1": + version: 7.0.1 + resolution: "gettext-parser@npm:7.0.1" dependencies: - content-type: ^1.0.4 + content-type: ^1.0.5 encoding: ^0.1.13 - readable-stream: ^4.1.0 + readable-stream: ^4.3.0 safe-buffer: ^5.2.1 - checksum: e6938755d914e886c780a71e58390af0a5ea5698aab2777654733da4c4b96e02fcc9d40c50df0987e4d4a12bd6b99291a268805edb8c99a83a639606ab73e40e + checksum: 251174700e4c7a4b9d868acf3b0e233211724a40f779bd9869357f782b004abd1f0a587f6c9f4219fd24f468bbfc18ca654cdd6e375f19610bd99f1587046ffa languageName: node linkType: hard @@ -17961,20 +17916,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.0.1": - version: 8.0.1 - resolution: "glob@npm:8.0.1" - dependencies: - fs.realpath: ^1.0.0 - inflight: ^1.0.4 - inherits: 2 - minimatch: ^5.0.1 - once: ^1.3.0 - path-is-absolute: ^1.0.0 - checksum: 7ac782f3ef1c08005884447479e68ceb0ad56997eb2003e1e9aefae71bad3cb48eb7c49190d3d6736f2ffcd8af4985d53a46831b3d5e0052cc5756822a38b61a - languageName: node - linkType: hard - "glob@npm:^8.0.3": version: 8.0.3 resolution: "glob@npm:8.0.3" @@ -22605,30 +22546,6 @@ __metadata: languageName: node linkType: hard -"make-fetch-happen@npm:^10.0.3": - version: 10.2.1 - resolution: "make-fetch-happen@npm:10.2.1" - dependencies: - agentkeepalive: ^4.2.1 - cacache: ^16.1.0 - http-cache-semantics: ^4.1.0 - http-proxy-agent: ^5.0.0 - https-proxy-agent: ^5.0.0 - is-lambda: ^1.0.1 - lru-cache: ^7.7.1 - minipass: ^3.1.6 - minipass-collect: ^1.0.2 - minipass-fetch: ^2.0.3 - minipass-flush: ^1.0.5 - minipass-pipeline: ^1.2.4 - negotiator: ^0.6.3 - promise-retry: ^2.0.1 - socks-proxy-agent: ^7.0.0 - ssri: ^9.0.0 - checksum: 2332eb9a8ec96f1ffeeea56ccefabcb4193693597b132cd110734d50f2928842e22b84cfa1508e921b8385cdfd06dda9ad68645fed62b50fff629a580f5fb72c - languageName: node - linkType: hard - "make-fetch-happen@npm:^11.0.0, make-fetch-happen@npm:^11.0.1, make-fetch-happen@npm:^11.0.3, make-fetch-happen@npm:^11.1.1": version: 11.1.1 resolution: "make-fetch-happen@npm:11.1.1" @@ -24030,21 +23947,6 @@ __metadata: languageName: node linkType: hard -"minipass-fetch@npm:^2.0.3": - version: 2.1.2 - resolution: "minipass-fetch@npm:2.1.2" - dependencies: - encoding: ^0.1.13 - minipass: ^3.1.6 - minipass-sized: ^1.0.3 - minizlib: ^2.1.2 - dependenciesMeta: - encoding: - optional: true - checksum: 3f216be79164e915fc91210cea1850e488793c740534985da017a4cbc7a5ff50506956d0f73bb0cb60e4fe91be08b6b61ef35101706d3ef5da2c8709b5f08f91 - languageName: node - linkType: hard - "minipass-fetch@npm:^3.0.0": version: 3.0.3 resolution: "minipass-fetch@npm:3.0.3" @@ -24790,27 +24692,7 @@ __metadata: languageName: node linkType: hard -"node-gyp@npm:9.3.1": - version: 9.3.1 - resolution: "node-gyp@npm:9.3.1" - dependencies: - env-paths: ^2.2.0 - glob: ^7.1.4 - graceful-fs: ^4.2.6 - make-fetch-happen: ^10.0.3 - nopt: ^6.0.0 - npmlog: ^6.0.0 - rimraf: ^3.0.2 - semver: ^7.3.5 - tar: ^6.1.2 - which: ^2.0.2 - bin: - node-gyp: bin/node-gyp.js - checksum: b860e9976fa645ca0789c69e25387401b4396b93c8375489b5151a6c55cf2640a3b6183c212b38625ef7c508994930b72198338e3d09b9d7ade5acc4aaf51ea7 - languageName: node - linkType: hard - -"node-gyp@npm:^9.0.0, node-gyp@npm:latest": +"node-gyp@npm:9.4.0, node-gyp@npm:^9.0.0, node-gyp@npm:latest": version: 9.4.0 resolution: "node-gyp@npm:9.4.0" dependencies: @@ -27952,9 +27834,9 @@ __metadata: languageName: node linkType: hard -"react-native-gesture-handler@npm:2.11.0": - version: 2.11.0 - resolution: "react-native-gesture-handler@npm:2.11.0" +"react-native-gesture-handler@npm:2.12.0": + version: 2.12.0 + resolution: "react-native-gesture-handler@npm:2.12.0" dependencies: "@egjs/hammerjs": ^2.0.17 hoist-non-react-statics: ^3.3.0 @@ -27964,7 +27846,7 @@ __metadata: peerDependencies: react: "*" react-native: "*" - checksum: 2f1e5ab3e96473c034f90e7aecce7d66fca992b791317b16a951928a42487598d3d3237e6e711bcf3cdb065f5b37415d3a8f8872f9df17993c614b852e350cd5 + checksum: 5147357b3212e269d0b8003e1be9e0993d0770f4880f1a8f52d2d61512c4569c48ec7b866d0a9ee44038785c29e0f84fbb99fd18a8e09552a25910c71602d788 languageName: node linkType: hard @@ -28012,9 +27894,9 @@ __metadata: languageName: node linkType: hard -"react-native-localize@npm:3.0.1": - version: 3.0.1 - resolution: "react-native-localize@npm:3.0.1" +"react-native-localize@npm:3.0.2": + version: 3.0.2 + resolution: "react-native-localize@npm:3.0.2" peerDependencies: react: ">=18.1.0" react-native: ">=0.70.0" @@ -28022,7 +27904,7 @@ __metadata: peerDependenciesMeta: react-native-macos: optional: true - checksum: f8703405ea5cf3fcc04fbfb255853832d730e6bedfc4a758254888bd4fb0b5edfa17350dff0b51e5cf804c00819bba1be86c05c496cc2a1d1a1a005a19bb0b9d + checksum: 46abd6046dfb1fb5426f9e51971894e893556646206b7fa387c5eb9394a3f3b9ddce190bc94ffc9c51c65326f60bbad57219d8330b4a1dcfaf46dd2eb4ef43ae languageName: node linkType: hard @@ -28089,6 +27971,27 @@ __metadata: languageName: node linkType: hard +"react-native-reanimated@patch:react-native-reanimated@npm%3A3.3.0#./.yarn/patches/react-native-animation-fix/react-native-reanimated-npm-3.3.0-fb4272741c.patch::locator=root%40workspace%3A.": + version: 3.3.0 + resolution: "react-native-reanimated@patch:react-native-reanimated@npm%3A3.3.0#./.yarn/patches/react-native-animation-fix/react-native-reanimated-npm-3.3.0-fb4272741c.patch::version=3.3.0&hash=13daee&locator=root%40workspace%3A." + dependencies: + "@babel/plugin-transform-object-assign": ^7.16.7 + "@babel/preset-typescript": ^7.16.7 + convert-source-map: ^2.0.0 + invariant: ^2.2.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + "@babel/plugin-proposal-nullish-coalescing-operator": ^7.0.0-0 + "@babel/plugin-proposal-optional-chaining": ^7.0.0-0 + "@babel/plugin-transform-arrow-functions": ^7.0.0-0 + "@babel/plugin-transform-shorthand-properties": ^7.0.0-0 + "@babel/plugin-transform-template-literals": ^7.0.0-0 + react: "*" + react-native: "*" + checksum: 5d73a35e694ab6c37b5f12530e1acdac822f2cceef7d04dcc060cc4c9221f51cf6bb68ed16737b25e79086de9dca6cd322819c6d04d668c9b97526408e97db68 + languageName: node + linkType: hard + "react-native-rsa-native@npm:2.0.5": version: 2.0.5 resolution: "react-native-rsa-native@npm:2.0.5" @@ -28299,6 +28202,52 @@ __metadata: languageName: node linkType: hard +"react-native@patch:react-native@npm%3A0.71.10#./.yarn/patches/react-native-animation-fix/react-native-npm-0.71.10-f9c32562d8.patch::locator=root%40workspace%3A.": + version: 0.71.10 + resolution: "react-native@patch:react-native@npm%3A0.71.10#./.yarn/patches/react-native-animation-fix/react-native-npm-0.71.10-f9c32562d8.patch::version=0.71.10&hash=d4015e&locator=root%40workspace%3A." + dependencies: + "@jest/create-cache-key-function": ^29.2.1 + "@react-native-community/cli": 10.2.2 + "@react-native-community/cli-platform-android": 10.2.0 + "@react-native-community/cli-platform-ios": 10.2.1 + "@react-native/assets": 1.0.0 + "@react-native/normalize-color": 2.1.0 + "@react-native/polyfills": 2.0.0 + abort-controller: ^3.0.0 + anser: ^1.4.9 + base64-js: ^1.1.2 + deprecated-react-native-prop-types: ^3.0.1 + event-target-shim: ^5.0.1 + invariant: ^2.2.4 + jest-environment-node: ^29.2.1 + jsc-android: ^250231.0.0 + memoize-one: ^5.0.0 + metro-react-native-babel-transformer: 0.73.9 + metro-runtime: 0.73.9 + metro-source-map: 0.73.9 + mkdirp: ^0.5.1 + nullthrows: ^1.1.1 + pretty-format: ^26.5.2 + promise: ^8.3.0 + react-devtools-core: ^4.26.1 + react-native-codegen: ^0.71.5 + react-native-gradle-plugin: ^0.71.19 + react-refresh: ^0.4.0 + react-shallow-renderer: ^16.15.0 + regenerator-runtime: ^0.13.2 + scheduler: ^0.23.0 + stacktrace-parser: ^0.1.3 + use-sync-external-store: ^1.0.0 + whatwg-fetch: ^3.0.0 + ws: ^6.2.2 + peerDependencies: + react: 18.2.0 + bin: + react-native: cli.js + checksum: c8c3cd4abfe4031f2c91baa6b26e2a43421dc3a3b221fec408d67c788e477b90dc4c0b206c1a24f82d5d7dae319b5da8ce6a472e346b48936813001d8276a279 + languageName: node + linkType: hard + "react-reconciler@npm:^0.26.2": version: 0.26.2 resolution: "react-reconciler@npm:0.26.2" @@ -28312,9 +28261,9 @@ __metadata: languageName: node linkType: hard -"react-redux@npm:8.0.7": - version: 8.0.7 - resolution: "react-redux@npm:8.0.7" +"react-redux@npm:8.1.1": + version: 8.1.1 + resolution: "react-redux@npm:8.1.1" dependencies: "@babel/runtime": ^7.12.1 "@types/hoist-non-react-statics": ^3.3.1 @@ -28323,7 +28272,6 @@ __metadata: react-is: ^18.0.0 use-sync-external-store: ^1.0.0 peerDependencies: - "@reduxjs/toolkit": ^1 || ^2.0.0-beta.0 "@types/react": ^16.8 || ^17.0 || ^18.0 "@types/react-dom": ^16.8 || ^17.0 || ^18.0 react: ^16.8 || ^17.0 || ^18.0 @@ -28331,8 +28279,6 @@ __metadata: react-native: ">=0.59" redux: ^4 || ^5.0.0-beta.0 peerDependenciesMeta: - "@reduxjs/toolkit": - optional: true "@types/react": optional: true "@types/react-dom": @@ -28343,7 +28289,7 @@ __metadata: optional: true redux: optional: true - checksum: d903aa79b12154258fd76b8e61fcf56f72123f69c31033b262805646371e23822cd0cd11d24194cda1e03569a7b1bf86e935cd57a9bab4523186804ed2742fac + checksum: 370676330727764d78f35e9c5a0ed0591d79482fe9b70fffcab4aa6bcccc6194e4f1ebd818b4b390351dea5557e70d3bd4d95d7a0ac9baa1f45d6bf2230ee713 languageName: node linkType: hard @@ -28645,15 +28591,16 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^4.1.0": - version: 4.2.0 - resolution: "readable-stream@npm:4.2.0" +"readable-stream@npm:^4.3.0": + version: 4.4.2 + resolution: "readable-stream@npm:4.4.2" dependencies: abort-controller: ^3.0.0 buffer: ^6.0.3 events: ^3.3.0 process: ^0.11.10 - checksum: aa8447f781e6df90af78f6b0b9b9a77da2816dcf6c8220e7021c4de36e04f8129fed7ead81eac0baad2f42098209f9e7d7cd43169e1c156efcd2613828a37439 + string_decoder: ^1.3.0 + checksum: 6f4063763dbdb52658d22d3f49ca976420e1fbe16bbd241f744383715845350b196a2f08b8d6330f8e219153dff34b140aeefd6296da828e1041a7eab1f20d5e languageName: node linkType: hard @@ -29545,7 +29492,7 @@ __metadata: lerna: 3.22.1 lint-staged: 13.2.3 madge: 6.1.0 - node-gyp: 9.3.1 + node-gyp: 9.4.0 nodemon: 2.0.22 npm-package-json-lint: 6.4.0 typescript: 5.0.2 @@ -30837,15 +30784,6 @@ __metadata: languageName: node linkType: hard -"ssri@npm:^9.0.0": - version: 9.0.1 - resolution: "ssri@npm:9.0.1" - dependencies: - minipass: ^3.1.1 - checksum: fb58f5e46b6923ae67b87ad5ef1c5ab6d427a17db0bead84570c2df3cd50b4ceb880ebdba2d60726588272890bae842a744e1ecce5bd2a2a582fccd5068309eb - languageName: node - linkType: hard - "stack-trace@npm:0.0.10": version: 0.0.10 resolution: "stack-trace@npm:0.0.10" @@ -31238,7 +31176,7 @@ __metadata: languageName: node linkType: hard -"string_decoder@npm:^1.1.1": +"string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0": version: 1.3.0 resolution: "string_decoder@npm:1.3.0" dependencies: @@ -33264,15 +33202,6 @@ __metadata: languageName: node linkType: hard -"unique-filename@npm:^2.0.0": - version: 2.0.1 - resolution: "unique-filename@npm:2.0.1" - dependencies: - unique-slug: ^3.0.0 - checksum: 807acf3381aff319086b64dc7125a9a37c09c44af7620bd4f7f3247fcd5565660ac12d8b80534dcbfd067e6fe88a67e621386dd796a8af828d1337a8420a255f - languageName: node - linkType: hard - "unique-filename@npm:^3.0.0": version: 3.0.0 resolution: "unique-filename@npm:3.0.0" @@ -33291,15 +33220,6 @@ __metadata: languageName: node linkType: hard -"unique-slug@npm:^3.0.0": - version: 3.0.0 - resolution: "unique-slug@npm:3.0.0" - dependencies: - imurmurhash: ^0.1.4 - checksum: 49f8d915ba7f0101801b922062ee46b7953256c93ceca74303bd8e6413ae10aa7e8216556b54dc5382895e8221d04f1efaf75f945c2e4a515b4139f77aa6640c - languageName: node - linkType: hard - "unique-slug@npm:^4.0.0": version: 4.0.0 resolution: "unique-slug@npm:4.0.0"