You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-09-02 20:46:21 +02:00
Compare commits
71 Commits
android-v3
...
android-v3
Author | SHA1 | Date | |
---|---|---|---|
|
6875fd271c | ||
|
a5c14c8d10 | ||
|
ddd18551eb | ||
|
612d72d765 | ||
|
2974465882 | ||
|
f7f4a50d35 | ||
|
f1e5ab8255 | ||
|
81993628ab | ||
|
0b3f6a268e | ||
|
a2069df3e0 | ||
|
1ad150c1bf | ||
|
41b251d67a | ||
|
2c40cec639 | ||
|
efb58c5f40 | ||
|
9d8cd1d707 | ||
|
591c458a4f | ||
|
f9b1a32ae7 | ||
|
1a195e23dd | ||
|
26ae3f853e | ||
|
e84e9a58e1 | ||
|
3b8da5023d | ||
|
548d41d0d4 | ||
|
d6c921249f | ||
|
e044c50b03 | ||
|
beec74d792 | ||
|
8b4e163b28 | ||
|
b61467097d | ||
|
447e4638d1 | ||
|
b831525b20 | ||
|
e05be832d5 | ||
|
64c9c3179f | ||
|
0ea61f26eb | ||
|
349fa426ea | ||
|
e3d5f0c9cf | ||
|
e63d545ed8 | ||
|
ab3058612d | ||
|
715abcce32 | ||
|
f165b3f870 | ||
|
8895d745e7 | ||
|
33a9b96a31 | ||
|
d1ac3d415e | ||
|
432fac8fda | ||
|
0f23882d47 | ||
|
693c0f22c8 | ||
|
e2db7a6b61 | ||
|
2a74f60812 | ||
|
2419291976 | ||
|
733845eb95 | ||
|
b3315aeb03 | ||
|
d88c522d96 | ||
|
c0cefc30f4 | ||
|
0dc3589661 | ||
|
f64c3d5484 | ||
|
5fceb5a3c9 | ||
|
916b3f6f69 | ||
|
0c4e8eeafc | ||
|
b27e0ff1f4 | ||
|
59ffb0f265 | ||
|
20b4fd85c1 | ||
|
fc2da05ba6 | ||
|
948ca605b0 | ||
|
eda2c69334 | ||
|
42ab9ecd95 | ||
|
5935c9c147 | ||
|
90640e590e | ||
|
75b8caf816 | ||
|
3ea403d004 | ||
|
058a559de4 | ||
|
ac43c62ce8 | ||
|
c4a7749f2a | ||
|
e6c09da639 |
@@ -374,6 +374,7 @@ packages/app-desktop/gui/NoteListItem/utils/useItemElement.js
|
|||||||
packages/app-desktop/gui/NoteListItem/utils/useItemEventHandlers.js
|
packages/app-desktop/gui/NoteListItem/utils/useItemEventHandlers.js
|
||||||
packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.js
|
packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.js
|
||||||
packages/app-desktop/gui/NoteListItem/utils/useRenderedNote.js
|
packages/app-desktop/gui/NoteListItem/utils/useRenderedNote.js
|
||||||
|
packages/app-desktop/gui/NoteListItem/utils/useRootElement.test.js
|
||||||
packages/app-desktop/gui/NoteListItem/utils/useRootElement.js
|
packages/app-desktop/gui/NoteListItem/utils/useRootElement.js
|
||||||
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
|
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
|
||||||
packages/app-desktop/gui/NotePropertiesDialog.js
|
packages/app-desktop/gui/NotePropertiesDialog.js
|
||||||
@@ -441,7 +442,6 @@ packages/app-desktop/gui/ToggleEditorsButton/ToggleEditorsButton.js
|
|||||||
packages/app-desktop/gui/ToggleEditorsButton/styles/index.js
|
packages/app-desktop/gui/ToggleEditorsButton/styles/index.js
|
||||||
packages/app-desktop/gui/ToolbarBase.js
|
packages/app-desktop/gui/ToolbarBase.js
|
||||||
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
|
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
|
||||||
packages/app-desktop/gui/ToolbarButton/styles/index.js
|
|
||||||
packages/app-desktop/gui/ToolbarSpace.js
|
packages/app-desktop/gui/ToolbarSpace.js
|
||||||
packages/app-desktop/gui/TrashNotification/TrashNotification.js
|
packages/app-desktop/gui/TrashNotification/TrashNotification.js
|
||||||
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
|
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
|
||||||
@@ -475,6 +475,7 @@ packages/app-desktop/integration-tests/models/NoteList.js
|
|||||||
packages/app-desktop/integration-tests/models/SettingsScreen.js
|
packages/app-desktop/integration-tests/models/SettingsScreen.js
|
||||||
packages/app-desktop/integration-tests/models/Sidebar.js
|
packages/app-desktop/integration-tests/models/Sidebar.js
|
||||||
packages/app-desktop/integration-tests/noteList.spec.js
|
packages/app-desktop/integration-tests/noteList.spec.js
|
||||||
|
packages/app-desktop/integration-tests/pluginApi.spec.js
|
||||||
packages/app-desktop/integration-tests/richTextEditor.spec.js
|
packages/app-desktop/integration-tests/richTextEditor.spec.js
|
||||||
packages/app-desktop/integration-tests/settings.spec.js
|
packages/app-desktop/integration-tests/settings.spec.js
|
||||||
packages/app-desktop/integration-tests/sidebar.spec.js
|
packages/app-desktop/integration-tests/sidebar.spec.js
|
||||||
@@ -786,6 +787,7 @@ packages/app-mobile/utils/shim-init-react/injectedJs.js
|
|||||||
packages/app-mobile/utils/shim-init-react/shimInitShared.js
|
packages/app-mobile/utils/shim-init-react/shimInitShared.js
|
||||||
packages/app-mobile/utils/testing/createMockReduxStore.js
|
packages/app-mobile/utils/testing/createMockReduxStore.js
|
||||||
packages/app-mobile/utils/testing/getWebViewDomById.js
|
packages/app-mobile/utils/testing/getWebViewDomById.js
|
||||||
|
packages/app-mobile/utils/testing/getWebViewWindowById.js
|
||||||
packages/app-mobile/utils/types.js
|
packages/app-mobile/utils/types.js
|
||||||
packages/app-mobile/web/serviceWorker.js
|
packages/app-mobile/web/serviceWorker.js
|
||||||
packages/default-plugins/build.js
|
packages/default-plugins/build.js
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -351,6 +351,7 @@ packages/app-desktop/gui/NoteListItem/utils/useItemElement.js
|
|||||||
packages/app-desktop/gui/NoteListItem/utils/useItemEventHandlers.js
|
packages/app-desktop/gui/NoteListItem/utils/useItemEventHandlers.js
|
||||||
packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.js
|
packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.js
|
||||||
packages/app-desktop/gui/NoteListItem/utils/useRenderedNote.js
|
packages/app-desktop/gui/NoteListItem/utils/useRenderedNote.js
|
||||||
|
packages/app-desktop/gui/NoteListItem/utils/useRootElement.test.js
|
||||||
packages/app-desktop/gui/NoteListItem/utils/useRootElement.js
|
packages/app-desktop/gui/NoteListItem/utils/useRootElement.js
|
||||||
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
|
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
|
||||||
packages/app-desktop/gui/NotePropertiesDialog.js
|
packages/app-desktop/gui/NotePropertiesDialog.js
|
||||||
@@ -418,7 +419,6 @@ packages/app-desktop/gui/ToggleEditorsButton/ToggleEditorsButton.js
|
|||||||
packages/app-desktop/gui/ToggleEditorsButton/styles/index.js
|
packages/app-desktop/gui/ToggleEditorsButton/styles/index.js
|
||||||
packages/app-desktop/gui/ToolbarBase.js
|
packages/app-desktop/gui/ToolbarBase.js
|
||||||
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
|
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
|
||||||
packages/app-desktop/gui/ToolbarButton/styles/index.js
|
|
||||||
packages/app-desktop/gui/ToolbarSpace.js
|
packages/app-desktop/gui/ToolbarSpace.js
|
||||||
packages/app-desktop/gui/TrashNotification/TrashNotification.js
|
packages/app-desktop/gui/TrashNotification/TrashNotification.js
|
||||||
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
|
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
|
||||||
@@ -452,6 +452,7 @@ packages/app-desktop/integration-tests/models/NoteList.js
|
|||||||
packages/app-desktop/integration-tests/models/SettingsScreen.js
|
packages/app-desktop/integration-tests/models/SettingsScreen.js
|
||||||
packages/app-desktop/integration-tests/models/Sidebar.js
|
packages/app-desktop/integration-tests/models/Sidebar.js
|
||||||
packages/app-desktop/integration-tests/noteList.spec.js
|
packages/app-desktop/integration-tests/noteList.spec.js
|
||||||
|
packages/app-desktop/integration-tests/pluginApi.spec.js
|
||||||
packages/app-desktop/integration-tests/richTextEditor.spec.js
|
packages/app-desktop/integration-tests/richTextEditor.spec.js
|
||||||
packages/app-desktop/integration-tests/settings.spec.js
|
packages/app-desktop/integration-tests/settings.spec.js
|
||||||
packages/app-desktop/integration-tests/sidebar.spec.js
|
packages/app-desktop/integration-tests/sidebar.spec.js
|
||||||
@@ -763,6 +764,7 @@ packages/app-mobile/utils/shim-init-react/injectedJs.js
|
|||||||
packages/app-mobile/utils/shim-init-react/shimInitShared.js
|
packages/app-mobile/utils/shim-init-react/shimInitShared.js
|
||||||
packages/app-mobile/utils/testing/createMockReduxStore.js
|
packages/app-mobile/utils/testing/createMockReduxStore.js
|
||||||
packages/app-mobile/utils/testing/getWebViewDomById.js
|
packages/app-mobile/utils/testing/getWebViewDomById.js
|
||||||
|
packages/app-mobile/utils/testing/getWebViewWindowById.js
|
||||||
packages/app-mobile/utils/types.js
|
packages/app-mobile/utils/types.js
|
||||||
packages/app-mobile/web/serviceWorker.js
|
packages/app-mobile/web/serviceWorker.js
|
||||||
packages/default-plugins/build.js
|
packages/default-plugins/build.js
|
||||||
|
874
.yarn/releases/yarn-3.6.4.cjs
vendored
874
.yarn/releases/yarn-3.6.4.cjs
vendored
File diff suppressed because one or more lines are too long
875
.yarn/releases/yarn-3.8.3.cjs
vendored
Executable file
875
.yarn/releases/yarn-3.8.3.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@ plugins:
|
|||||||
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||||
spec: "@yarnpkg/plugin-workspace-tools"
|
spec: "@yarnpkg/plugin-workspace-tools"
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-3.6.4.cjs
|
yarnPath: .yarn/releases/yarn-3.8.3.cjs
|
||||||
|
|
||||||
logFilters:
|
logFilters:
|
||||||
|
|
||||||
|
BIN
Assets/WebsiteAssets/images/sponsors/CasinoReviews.png
Normal file
BIN
Assets/WebsiteAssets/images/sponsors/CasinoReviews.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
@@ -31,7 +31,7 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
|
|||||||
# Sponsors
|
# Sponsors
|
||||||
|
|
||||||
<!-- SPONSORS-ORG -->
|
<!-- SPONSORS-ORG -->
|
||||||
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://buyyoutubviews.com"><img title="BYTV" width="256" src="https://joplinapp.org/images/sponsors/BYTV.png"/></a> <a href="https://www.famegear.com"><img title="Famegear" width="256" src="https://joplinapp.org/images/sponsors/Famegear.png"/></a>
|
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://buyyoutubviews.com"><img title="BYTV" width="256" src="https://joplinapp.org/images/sponsors/BYTV.png"/></a> <a href="https://www.famegear.com"><img title="Famegear" width="256" src="https://joplinapp.org/images/sponsors/Famegear.png"/></a> <a href="https://casinoreviews.net"><img title="Casino Reviews" width="256" src="https://joplinapp.org/images/sponsors/CasinoReviews.png"/></a>
|
||||||
<!-- SPONSORS-ORG -->
|
<!-- SPONSORS-ORG -->
|
||||||
|
|
||||||
* * *
|
* * *
|
||||||
|
26
package.json
26
package.json
@@ -72,38 +72,38 @@
|
|||||||
"@crowdin/cli": "3",
|
"@crowdin/cli": "3",
|
||||||
"@joplin/utils": "~2.12",
|
"@joplin/utils": "~2.12",
|
||||||
"@seiyab/eslint-plugin-react-hooks": "4.5.1-beta.0",
|
"@seiyab/eslint-plugin-react-hooks": "4.5.1-beta.0",
|
||||||
"@typescript-eslint/eslint-plugin": "6.8.0",
|
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||||
"@typescript-eslint/parser": "6.8.0",
|
"@typescript-eslint/parser": "6.21.0",
|
||||||
"cspell": "5.21.2",
|
"cspell": "5.21.2",
|
||||||
"eslint": "8.52.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-interactive": "10.8.0",
|
"eslint-interactive": "10.8.0",
|
||||||
"eslint-plugin-import": "2.28.1",
|
"eslint-plugin-import": "2.29.1",
|
||||||
"eslint-plugin-jest": "27.4.3",
|
"eslint-plugin-jest": "27.9.0",
|
||||||
"eslint-plugin-promise": "6.1.1",
|
"eslint-plugin-promise": "6.2.0",
|
||||||
"eslint-plugin-react": "7.33.2",
|
"eslint-plugin-react": "7.34.3",
|
||||||
"execa": "5.1.1",
|
"execa": "5.1.1",
|
||||||
"fs-extra": "11.2.0",
|
"fs-extra": "11.2.0",
|
||||||
"glob": "10.4.2",
|
"glob": "10.4.5",
|
||||||
"gulp": "4.0.2",
|
"gulp": "4.0.2",
|
||||||
"husky": "3.1.0",
|
"husky": "3.1.0",
|
||||||
"lerna": "3.22.1",
|
"lerna": "3.22.1",
|
||||||
"lint-staged": "15.2.7",
|
"lint-staged": "15.2.7",
|
||||||
"madge": "6.1.0",
|
"madge": "6.1.0",
|
||||||
"npm-package-json-lint": "7.1.0",
|
"npm-package-json-lint": "7.1.0",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.4.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/fs-extra": "11.0.4",
|
"@types/fs-extra": "11.0.4",
|
||||||
"eslint-plugin-github": "4.10.1",
|
"eslint-plugin-github": "4.10.2",
|
||||||
"http-server": "14.1.1",
|
"http-server": "14.1.1",
|
||||||
"node-gyp": "9.4.1",
|
"node-gyp": "9.4.1",
|
||||||
"nodemon": "3.0.3"
|
"nodemon": "3.1.7"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@3.6.4",
|
"packageManager": "yarn@3.8.3",
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
|
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
|
||||||
"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",
|
"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": "patch:eslint@8.52.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
|
"eslint": "patch:eslint@8.57.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",
|
||||||
"nanoid": "patch:nanoid@npm%3A3.3.7#./.yarn/patches/nanoid-npm-3.3.7-98824ba130.patch",
|
"nanoid": "patch:nanoid@npm%3A3.3.7#./.yarn/patches/nanoid-npm-3.3.7-98824ba130.patch",
|
||||||
"pdfjs-dist": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
|
"pdfjs-dist": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
|
||||||
|
@@ -72,12 +72,12 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@joplin/tools": "~3.1",
|
"@joplin/tools": "~3.1",
|
||||||
"@types/fs-extra": "11.0.4",
|
"@types/fs-extra": "11.0.4",
|
||||||
"@types/jest": "29.5.8",
|
"@types/jest": "29.5.12",
|
||||||
"@types/node": "18.19.39",
|
"@types/node": "18.19.39",
|
||||||
"@types/proper-lockfile": "^4.1.2",
|
"@types/proper-lockfile": "^4.1.2",
|
||||||
"gulp": "4.0.2",
|
"gulp": "4.0.2",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"temp": "0.9.4",
|
"temp": "0.9.4",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.4.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -352,4 +352,12 @@ describe('MdToHtml', () => {
|
|||||||
expect(html).toContain('Inline</span>');
|
expect(html).toContain('Inline</span>');
|
||||||
expect(html).toContain('Block</span>');
|
expect(html).toContain('Block</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should sanitize KaTeX errors', async () => {
|
||||||
|
const markdown = '$\\a<svg>$';
|
||||||
|
const renderResult = await newTestMdToHtml().render(markdown, null, { bodyOnly: true });
|
||||||
|
|
||||||
|
// Should not contain the HTML in unsanitized form
|
||||||
|
expect(renderResult.html).not.toContain('<svg>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -5,7 +5,7 @@ import type ShimType from '@joplin/lib/shim';
|
|||||||
const shim: typeof ShimType = require('@joplin/lib/shim').default;
|
const shim: typeof ShimType = require('@joplin/lib/shim').default;
|
||||||
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||||
|
|
||||||
import { BrowserWindow, Tray, screen } from 'electron';
|
import { BrowserWindow, Tray, WebContents, screen } from 'electron';
|
||||||
import bridge from './bridge';
|
import bridge from './bridge';
|
||||||
const url = require('url');
|
const url = require('url');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
@@ -232,8 +232,9 @@ export default class ElectronAppWrapper {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addWindowEventHandlers = (webContents: WebContents) => {
|
||||||
// will-frame-navigate is fired by clicking on a link within the BrowserWindow.
|
// will-frame-navigate is fired by clicking on a link within the BrowserWindow.
|
||||||
this.win_.webContents.on('will-frame-navigate', event => {
|
webContents.on('will-frame-navigate', event => {
|
||||||
// If the link changes the URL of the browser window,
|
// If the link changes the URL of the browser window,
|
||||||
if (event.isMainFrame) {
|
if (event.isMainFrame) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -241,6 +242,26 @@ export default class ElectronAppWrapper {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Override calls to window.open and links with target="_blank": Open most in a browser instead
|
||||||
|
// of Electron:
|
||||||
|
webContents.setWindowOpenHandler((event) => {
|
||||||
|
if (event.url === 'about:blank') {
|
||||||
|
// Script-controlled pages: Used for opening notes in new windows
|
||||||
|
return {
|
||||||
|
action: 'allow',
|
||||||
|
};
|
||||||
|
} else if (event.url.match(/^https?:\/\//)) {
|
||||||
|
void bridge().openExternal(event.url);
|
||||||
|
}
|
||||||
|
return { action: 'deny' };
|
||||||
|
});
|
||||||
|
|
||||||
|
webContents.on('did-create-window', (event) => {
|
||||||
|
addWindowEventHandlers(event.webContents);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
addWindowEventHandlers(this.win_.webContents);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
this.win_.on('close', (event: any) => {
|
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)
|
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)
|
||||||
|
@@ -127,6 +127,12 @@ class Application extends BaseApplication {
|
|||||||
bridge().setLocale(Setting.value('locale'));
|
bridge().setLocale(Setting.value('locale'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'renderer.fileUrls' || action.type === 'SETTING_UPDATE_ALL') {
|
||||||
|
bridge().electronApp().getCustomProtocolHandler().setMediaAccessEnabled(
|
||||||
|
Setting.value('renderer.fileUrls'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'showTrayIcon' || action.type === 'SETTING_UPDATE_ALL') {
|
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'showTrayIcon' || action.type === 'SETTING_UPDATE_ALL') {
|
||||||
this.updateTray();
|
this.updateTray();
|
||||||
}
|
}
|
||||||
|
@@ -234,7 +234,7 @@ export default function(props: Props) {
|
|||||||
return (
|
return (
|
||||||
<CellFooter>
|
<CellFooter>
|
||||||
<NeedUpgradeMessage>
|
<NeedUpgradeMessage>
|
||||||
{PluginService.instance().describeIncompatibility(props.manifest)}
|
{PluginService.instance().describeIncompatibility(item.manifest)}
|
||||||
</NeedUpgradeMessage>
|
</NeedUpgradeMessage>
|
||||||
</CellFooter>
|
</CellFooter>
|
||||||
);
|
);
|
||||||
|
@@ -378,6 +378,8 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
|||||||
katexEnabled: Setting.value('markdown.plugin.katex'),
|
katexEnabled: Setting.value('markdown.plugin.katex'),
|
||||||
themeData: {
|
themeData: {
|
||||||
...styles.globalTheme,
|
...styles.globalTheme,
|
||||||
|
marginLeft: 0,
|
||||||
|
marginRight: 0,
|
||||||
monospaceFont: Setting.value('style.editor.monospaceFontFamily'),
|
monospaceFont: Setting.value('style.editor.monospaceFontFamily'),
|
||||||
},
|
},
|
||||||
automatchBraces: Setting.value('editor.autoMatchingBraces'),
|
automatchBraces: Setting.value('editor.autoMatchingBraces'),
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
|
|
||||||
import { RefObject, useMemo } from 'react';
|
import { RefObject, useMemo } from 'react';
|
||||||
import { CommandValue } from '../../../utils/types';
|
import { CommandValue, DropCommandValue } from '../../../utils/types';
|
||||||
import { commandAttachFileToBody } from '../../../utils/resourceHandling';
|
import { commandAttachFileToBody } from '../../../utils/resourceHandling';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import dialogs from '../../../../dialogs';
|
import dialogs from '../../../../dialogs';
|
||||||
import { EditorCommandType } from '@joplin/editor/types';
|
import { EditorCommandType, UserEventSource } from '@joplin/editor/types';
|
||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||||
import { MarkupLanguage } from '@joplin/renderer';
|
import { MarkupLanguage } from '@joplin/renderer';
|
||||||
@@ -38,13 +38,22 @@ const useEditorCommands = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
dropItems: async (cmd: DropCommandValue) => {
|
||||||
dropItems: async (cmd: any) => {
|
let pos = cmd.pos && editorRef.current.editor.posAtCoords({ x: cmd.pos.clientX, y: cmd.pos.clientY });
|
||||||
if (cmd.type === 'notes') {
|
if (cmd.type === 'notes') {
|
||||||
editorRef.current.insertText(cmd.markdownTags.join('\n'));
|
const text = cmd.markdownTags.join('\n');
|
||||||
|
if ((pos ?? null) !== null) {
|
||||||
|
editorRef.current.select(pos, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
editorRef.current.insertText(text, UserEventSource.Drop);
|
||||||
} else if (cmd.type === 'files') {
|
} else if (cmd.type === 'files') {
|
||||||
const pos = props.selectionRange.from;
|
pos ??= props.selectionRange.from;
|
||||||
const newBody = await commandAttachFileToBody(props.editorContent, cmd.paths, { createFileURL: !!cmd.createFileURL, position: pos, markupLanguage: props.contentMarkupLanguage });
|
const newBody = await commandAttachFileToBody(props.editorContent, cmd.paths, {
|
||||||
|
createFileURL: !!cmd.createFileURL,
|
||||||
|
position: pos,
|
||||||
|
markupLanguage: props.contentMarkupLanguage,
|
||||||
|
});
|
||||||
editorRef.current.updateBody(newBody);
|
editorRef.current.updateBody(newBody);
|
||||||
} else {
|
} else {
|
||||||
logger.warn('CodeMirror: unsupported drop item: ', cmd);
|
logger.warn('CodeMirror: unsupported drop item: ', cmd);
|
||||||
|
@@ -218,15 +218,6 @@ function NoteEditor(props: NoteEditorProps) {
|
|||||||
}
|
}
|
||||||
}, [handleProvisionalFlag, formNote, setFormNote, isNewNote, titleHasBeenManuallyChanged, scheduleNoteListResort, scheduleSaveNote]);
|
}, [handleProvisionalFlag, formNote, setFormNote, isNewNote, titleHasBeenManuallyChanged, scheduleNoteListResort, scheduleSaveNote]);
|
||||||
|
|
||||||
useWindowCommandHandler({
|
|
||||||
dispatch: props.dispatch,
|
|
||||||
setShowLocalSearch,
|
|
||||||
noteSearchBarRef,
|
|
||||||
editorRef,
|
|
||||||
titleInputRef,
|
|
||||||
setFormNote,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onDrop = useDropHandler({ editorRef });
|
const onDrop = useDropHandler({ editorRef });
|
||||||
|
|
||||||
const onBodyChange = useCallback((event: OnChangeEvent) => onFieldChange('body', event.content, event.changeId), [onFieldChange]);
|
const onBodyChange = useCallback((event: OnChangeEvent) => onFieldChange('body', event.content, event.changeId), [onFieldChange]);
|
||||||
@@ -234,6 +225,15 @@ function NoteEditor(props: NoteEditorProps) {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
const onTitleChange = useCallback((event: any) => onFieldChange('title', event.target.value), [onFieldChange]);
|
const onTitleChange = useCallback((event: any) => onFieldChange('title', event.target.value), [onFieldChange]);
|
||||||
|
|
||||||
|
useWindowCommandHandler({
|
||||||
|
dispatch: props.dispatch,
|
||||||
|
setShowLocalSearch,
|
||||||
|
noteSearchBarRef,
|
||||||
|
editorRef,
|
||||||
|
titleInputRef,
|
||||||
|
onBodyChange,
|
||||||
|
});
|
||||||
|
|
||||||
// const onTitleKeydown = useCallback((event:any) => {
|
// const onTitleKeydown = useCallback((event:any) => {
|
||||||
// const keyCode = event.keyCode;
|
// const keyCode = event.keyCode;
|
||||||
|
|
||||||
|
@@ -5,30 +5,6 @@ import { ChangeEvent, useCallback, useRef } from 'react';
|
|||||||
import NoteToolbar from '../../NoteToolbar/NoteToolbar';
|
import NoteToolbar from '../../NoteToolbar/NoteToolbar';
|
||||||
import { buildStyle } from '@joplin/lib/theme';
|
import { buildStyle } from '@joplin/lib/theme';
|
||||||
import time from '@joplin/lib/time';
|
import time from '@joplin/lib/time';
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const StyledRoot = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
padding-left: ${props => props.theme.editorPaddingLeft}px;
|
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const InfoGroup = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
|
||||||
border-top: 1px solid ${props => props.theme.dividerColor};
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
themeId: number;
|
themeId: number;
|
||||||
@@ -130,7 +106,7 @@ export default function NoteTitleBar(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRoot>
|
<div className='note-title-wrapper'>
|
||||||
<input
|
<input
|
||||||
className="title-input"
|
className="title-input"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -144,10 +120,10 @@ export default function NoteTitleBar(props: Props) {
|
|||||||
onBlur={onTitleBlur}
|
onBlur={onTitleBlur}
|
||||||
value={props.noteTitle}
|
value={props.noteTitle}
|
||||||
/>
|
/>
|
||||||
<InfoGroup>
|
<div className='note-title-info-group'>
|
||||||
{renderTitleBarDate()}
|
{renderTitleBarDate()}
|
||||||
{renderNoteToolbar()}
|
{renderNoteToolbar()}
|
||||||
</InfoGroup>
|
</div>
|
||||||
</StyledRoot>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -38,7 +38,6 @@ const incompatiblePluginIds = [
|
|||||||
'ylc395.noteLinkSystem',
|
'ylc395.noteLinkSystem',
|
||||||
'outline',
|
'outline',
|
||||||
'joplin.plugin.cmoptions',
|
'joplin.plugin.cmoptions',
|
||||||
'plugin.calebjohn.MathMode',
|
|
||||||
'com.ckant.joplin-plugin-better-code-blocks',
|
'com.ckant.joplin-plugin-better-code-blocks',
|
||||||
// cSpell:enable
|
// cSpell:enable
|
||||||
];
|
];
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
|
||||||
@use "./styles/warning-banner.scss";
|
@use "./styles/warning-banner.scss";
|
||||||
@use "./styles/warning-banner-link.scss";
|
@use "./styles/warning-banner-link.scss";
|
||||||
|
@use "./styles/note-title-info-group.scss";
|
||||||
|
@use "./styles/note-title-wrapper.scss";
|
||||||
|
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
.note-title-info-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
border-top: 1px solid var(--joplin-divider-color);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
.note-title-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: var(--joplin-editor-padding-left);
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
@@ -252,3 +252,19 @@ export interface CommandValue {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
value?: any; // For TinyMCE only
|
value?: any; // For TinyMCE only
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DropCommandBase = {
|
||||||
|
pos: {
|
||||||
|
clientX: number;
|
||||||
|
clientY: number;
|
||||||
|
}|undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DropCommandValue = ({
|
||||||
|
type: 'notes';
|
||||||
|
markdownTags: string[];
|
||||||
|
}|{
|
||||||
|
type: 'files';
|
||||||
|
paths: string[];
|
||||||
|
createFileURL: boolean;
|
||||||
|
}) & DropCommandBase;
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import Note from '@joplin/lib/models/Note';
|
import Note from '@joplin/lib/models/Note';
|
||||||
import { DragEvent as ReactDragEvent } from 'react';
|
import { DragEvent as ReactDragEvent } from 'react';
|
||||||
|
import { DropCommandValue } from './types';
|
||||||
|
|
||||||
interface HookDependencies {
|
interface HookDependencies {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
@@ -19,6 +20,11 @@ export default function useDropHandler(dependencies: HookDependencies): DropHand
|
|||||||
const dt = event.dataTransfer;
|
const dt = event.dataTransfer;
|
||||||
const createFileURL = event.altKey;
|
const createFileURL = event.altKey;
|
||||||
|
|
||||||
|
const eventPosition = {
|
||||||
|
clientX: event.clientX,
|
||||||
|
clientY: event.clientY,
|
||||||
|
};
|
||||||
|
|
||||||
if (dt.types.indexOf('text/x-jop-note-ids') >= 0) {
|
if (dt.types.indexOf('text/x-jop-note-ids') >= 0) {
|
||||||
const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids'));
|
const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids'));
|
||||||
|
|
||||||
@@ -29,12 +35,15 @@ export default function useDropHandler(dependencies: HookDependencies): DropHand
|
|||||||
noteMarkdownTags.push(Note.markdownTag(note));
|
noteMarkdownTags.push(Note.markdownTag(note));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const props: DropCommandValue = {
|
||||||
|
type: 'notes',
|
||||||
|
pos: eventPosition,
|
||||||
|
markdownTags: noteMarkdownTags,
|
||||||
|
};
|
||||||
|
|
||||||
editorRef.current.execCommand({
|
editorRef.current.execCommand({
|
||||||
name: 'dropItems',
|
name: 'dropItems',
|
||||||
value: {
|
value: props,
|
||||||
type: 'notes',
|
|
||||||
markdownTags: noteMarkdownTags,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
void dropNotes();
|
void dropNotes();
|
||||||
@@ -51,13 +60,16 @@ export default function useDropHandler(dependencies: HookDependencies): DropHand
|
|||||||
paths.push(file.path);
|
paths.push(file.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
editorRef.current.execCommand({
|
const props: DropCommandValue = {
|
||||||
name: 'dropItems',
|
|
||||||
value: {
|
|
||||||
type: 'files',
|
type: 'files',
|
||||||
|
pos: eventPosition,
|
||||||
paths: paths,
|
paths: paths,
|
||||||
createFileURL: createFileURL,
|
createFileURL: createFileURL,
|
||||||
},
|
};
|
||||||
|
|
||||||
|
editorRef.current.execCommand({
|
||||||
|
name: 'dropItems',
|
||||||
|
value: props,
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { RefObject, useEffect } from 'react';
|
import { RefObject, useEffect } from 'react';
|
||||||
import { FormNote, NoteBodyEditorRef, ScrollOptionTypes } from './types';
|
import { NoteBodyEditorRef, OnChangeEvent, ScrollOptionTypes } from './types';
|
||||||
import editorCommandDeclarations, { enabledCondition } from '../editorCommandDeclarations';
|
import editorCommandDeclarations, { enabledCondition } from '../editorCommandDeclarations';
|
||||||
import CommandService, { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService';
|
import CommandService, { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService';
|
||||||
import time from '@joplin/lib/time';
|
import time from '@joplin/lib/time';
|
||||||
@@ -12,7 +12,7 @@ const commandsWithDependencies = [
|
|||||||
require('../commands/pasteAsText'),
|
require('../commands/pasteAsText'),
|
||||||
];
|
];
|
||||||
|
|
||||||
type SetFormNoteCallback = (callback: (prev: FormNote)=> FormNote)=> void;
|
type OnBodyChange = (event: OnChangeEvent)=> void;
|
||||||
|
|
||||||
interface HookDependencies {
|
interface HookDependencies {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||||
@@ -23,13 +23,13 @@ interface HookDependencies {
|
|||||||
noteSearchBarRef: any;
|
noteSearchBarRef: any;
|
||||||
editorRef: RefObject<NoteBodyEditorRef>;
|
editorRef: RefObject<NoteBodyEditorRef>;
|
||||||
titleInputRef: RefObject<HTMLInputElement>;
|
titleInputRef: RefObject<HTMLInputElement>;
|
||||||
setFormNote: SetFormNoteCallback;
|
onBodyChange: OnBodyChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
function editorCommandRuntime(
|
function editorCommandRuntime(
|
||||||
declaration: CommandDeclaration,
|
declaration: CommandDeclaration,
|
||||||
editorRef: RefObject<NoteBodyEditorRef>,
|
editorRef: RefObject<NoteBodyEditorRef>,
|
||||||
setFormNote: SetFormNoteCallback,
|
onBodyChange: OnBodyChange,
|
||||||
): CommandRuntime {
|
): CommandRuntime {
|
||||||
return {
|
return {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
@@ -55,9 +55,7 @@ function editorCommandRuntime(
|
|||||||
value: args[0],
|
value: args[0],
|
||||||
});
|
});
|
||||||
} else if (declaration.name === 'editor.setText') {
|
} else if (declaration.name === 'editor.setText') {
|
||||||
setFormNote((prev: FormNote) => {
|
onBodyChange({ content: args[0], changeId: 0 });
|
||||||
return { ...prev, body: args[0] };
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
return editorRef.current.execCommand({
|
return editorRef.current.execCommand({
|
||||||
name: declaration.name,
|
name: declaration.name,
|
||||||
@@ -78,11 +76,11 @@ function editorCommandRuntime(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function useWindowCommandHandler(dependencies: HookDependencies) {
|
export default function useWindowCommandHandler(dependencies: HookDependencies) {
|
||||||
const { setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef, setFormNote } = dependencies;
|
const { setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef, onBodyChange } = dependencies;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
for (const declaration of editorCommandDeclarations) {
|
for (const declaration of editorCommandDeclarations) {
|
||||||
CommandService.instance().registerRuntime(declaration.name, editorCommandRuntime(declaration, editorRef, setFormNote));
|
CommandService.instance().registerRuntime(declaration.name, editorCommandRuntime(declaration, editorRef, onBodyChange));
|
||||||
}
|
}
|
||||||
|
|
||||||
const dependencies = {
|
const dependencies = {
|
||||||
@@ -105,5 +103,5 @@ export default function useWindowCommandHandler(dependencies: HookDependencies)
|
|||||||
CommandService.instance().unregisterRuntime(command.declaration.name);
|
CommandService.instance().unregisterRuntime(command.declaration.name);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [editorRef, setShowLocalSearch, noteSearchBarRef, titleInputRef, setFormNote]);
|
}, [editorRef, setShowLocalSearch, noteSearchBarRef, titleInputRef, onBodyChange]);
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useMemo, useRef, useEffect } from 'react';
|
import { useMemo, useRef, useEffect, useCallback } from 'react';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
|
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
|
||||||
import { Props } from './utils/types';
|
import { Props } from './utils/types';
|
||||||
@@ -275,6 +275,12 @@ const NoteList = (props: Props) => {
|
|||||||
return output;
|
return output;
|
||||||
}, [listRenderer.flow]);
|
}, [listRenderer.flow]);
|
||||||
|
|
||||||
|
const onContainerContextMenu = useCallback((event: React.MouseEvent) => {
|
||||||
|
const isFromKeyboard = event.button === -1;
|
||||||
|
if (event.isDefaultPrevented() || !isFromKeyboard) return;
|
||||||
|
onItemContextMenu({ itemId: activeNoteId });
|
||||||
|
}, [onItemContextMenu, activeNoteId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role='listbox'
|
role='listbox'
|
||||||
@@ -293,6 +299,7 @@ const NoteList = (props: Props) => {
|
|||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
onKeyUp={onKeyUp}
|
onKeyUp={onKeyUp}
|
||||||
onDrop={onDrop}
|
onDrop={onDrop}
|
||||||
|
onContextMenu={onContainerContextMenu}
|
||||||
>
|
>
|
||||||
{renderEmptyList()}
|
{renderEmptyList()}
|
||||||
{renderFiller('top', topFillerStyle)}
|
{renderFiller('top', topFillerStyle)}
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import * as React from 'react';
|
||||||
import Folder from '@joplin/lib/models/Folder';
|
import Folder from '@joplin/lib/models/Folder';
|
||||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||||
@@ -6,6 +7,13 @@ import { Dispatch } from 'redux';
|
|||||||
import bridge from '../../../services/bridge';
|
import bridge from '../../../services/bridge';
|
||||||
import NoteListUtils from '../../utils/NoteListUtils';
|
import NoteListUtils from '../../utils/NoteListUtils';
|
||||||
|
|
||||||
|
interface CustomContextMenuEvent {
|
||||||
|
itemId: string;
|
||||||
|
currentTarget?: undefined;
|
||||||
|
preventDefault?: undefined;
|
||||||
|
}
|
||||||
|
type ContextMenuEvent = React.MouseEvent|CustomContextMenuEvent;
|
||||||
|
|
||||||
const useOnContextMenu = (
|
const useOnContextMenu = (
|
||||||
selectedNoteIds: string[],
|
selectedNoteIds: string[],
|
||||||
selectedFolderId: string,
|
selectedFolderId: string,
|
||||||
@@ -15,10 +23,14 @@ const useOnContextMenu = (
|
|||||||
plugins: PluginStates,
|
plugins: PluginStates,
|
||||||
customCss: string,
|
customCss: string,
|
||||||
) => {
|
) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
return useCallback((event: ContextMenuEvent) => {
|
||||||
return useCallback((event: any) => {
|
let currentNoteId = event.currentTarget?.getAttribute('data-id');
|
||||||
const currentNoteId = event.currentTarget.getAttribute('data-id');
|
if ('itemId' in event) {
|
||||||
|
currentNoteId = event.itemId;
|
||||||
|
}
|
||||||
|
|
||||||
if (!currentNoteId) return;
|
if (!currentNoteId) return;
|
||||||
|
event.preventDefault?.();
|
||||||
|
|
||||||
let noteIds = [];
|
let noteIds = [];
|
||||||
if (selectedNoteIds.indexOf(currentNoteId) < 0) {
|
if (selectedNoteIds.indexOf(currentNoteId) < 0) {
|
||||||
|
@@ -0,0 +1,55 @@
|
|||||||
|
import { act, renderHook } from '@testing-library/react-hooks';
|
||||||
|
import useRootElement from './useRootElement';
|
||||||
|
|
||||||
|
describe('useRootElement', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers({ advanceTimers: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find an element with a matching ID', async () => {
|
||||||
|
const testElement = document.createElement('div');
|
||||||
|
testElement.id = 'test-element-id';
|
||||||
|
document.body.appendChild(testElement);
|
||||||
|
|
||||||
|
const { result } = renderHook(useRootElement, {
|
||||||
|
initialProps: testElement.id,
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
await jest.advanceTimersByTimeAsync(100);
|
||||||
|
});
|
||||||
|
expect(result.current).toBe(testElement);
|
||||||
|
|
||||||
|
testElement.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should redo the element search when the elementId prop changes', async () => {
|
||||||
|
const testElement = document.createElement('div');
|
||||||
|
document.body.appendChild(testElement);
|
||||||
|
|
||||||
|
const { rerender, result } = renderHook(useRootElement, {
|
||||||
|
initialProps: 'some-id-here',
|
||||||
|
});
|
||||||
|
await jest.advanceTimersByTimeAsync(100);
|
||||||
|
expect(result.current).toBe(null);
|
||||||
|
|
||||||
|
// Searching for another non-existent ID: Should not match
|
||||||
|
rerender('updated-id');
|
||||||
|
await jest.advanceTimersByTimeAsync(100);
|
||||||
|
expect(result.current).toBe(null);
|
||||||
|
|
||||||
|
// Should not match the first element if its ID is set to the original (search
|
||||||
|
// should be cancelled).
|
||||||
|
testElement.id = 'some-id-here';
|
||||||
|
await jest.advanceTimersByTimeAsync(100);
|
||||||
|
expect(result.current).toBe(null);
|
||||||
|
|
||||||
|
// Should match if the element ID changes to the updated ID.
|
||||||
|
await act(async () => {
|
||||||
|
testElement.id = 'updated-id';
|
||||||
|
await jest.advanceTimersByTimeAsync(100);
|
||||||
|
});
|
||||||
|
expect(result.current).toBe(testElement);
|
||||||
|
|
||||||
|
testElement.remove();
|
||||||
|
});
|
||||||
|
});
|
@@ -6,7 +6,7 @@ const useRootElement = (elementId: string) => {
|
|||||||
const [rootElement, setRootElement] = useState<HTMLDivElement>(null);
|
const [rootElement, setRootElement] = useState<HTMLDivElement>(null);
|
||||||
|
|
||||||
useAsyncEffect(async (event) => {
|
useAsyncEffect(async (event) => {
|
||||||
const element = await waitForElement(document, elementId);
|
const element = await waitForElement(document, elementId, event);
|
||||||
if (event.cancelled) return;
|
if (event.cancelled) return;
|
||||||
setRootElement(element);
|
setRootElement(element);
|
||||||
}, [document, elementId]);
|
}, [document, elementId]);
|
||||||
|
@@ -29,6 +29,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
|||||||
private webviewRef_: React.RefObject<HTMLIFrameElement>;
|
private webviewRef_: React.RefObject<HTMLIFrameElement>;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
private webviewListeners_: any = null;
|
private webviewListeners_: any = null;
|
||||||
|
|
||||||
private removePluginAssetsCallback_: RemovePluginAssetsCallback|null = null;
|
private removePluginAssetsCallback_: RemovePluginAssetsCallback|null = null;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
@@ -110,7 +111,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
|||||||
window.addEventListener('message', this.webview_message);
|
window.addEventListener('message', this.webview_message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroyWebview() {
|
private destroyWebview() {
|
||||||
const wv = this.webviewRef_.current;
|
const wv = this.webviewRef_.current;
|
||||||
if (!wv || !this.initialized_) return;
|
if (!wv || !this.initialized_) return;
|
||||||
|
|
||||||
@@ -194,14 +195,13 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
public setHtml(html: string, options: SetHtmlOptions) {
|
public setHtml(html: string, options: SetHtmlOptions) {
|
||||||
|
const protocolHandler = bridge().electronApp().getCustomProtocolHandler();
|
||||||
|
|
||||||
// Grant & remove asset access.
|
// Grant & remove asset access.
|
||||||
if (options.pluginAssets) {
|
if (options.pluginAssets) {
|
||||||
this.removePluginAssetsCallback_?.();
|
this.removePluginAssetsCallback_?.();
|
||||||
|
|
||||||
const protocolHandler = bridge().electronApp().getCustomProtocolHandler();
|
|
||||||
|
|
||||||
const pluginAssetPaths: string[] = options.pluginAssets.map((asset) => asset.path);
|
const pluginAssetPaths: string[] = options.pluginAssets.map((asset) => asset.path);
|
||||||
const assetAccesses = pluginAssetPaths.map(
|
const assetAccesses = pluginAssetPaths.map(
|
||||||
path => protocolHandler.allowReadAccessToFile(path),
|
path => protocolHandler.allowReadAccessToFile(path),
|
||||||
@@ -216,7 +216,10 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.send('setHtml', html, options);
|
this.send('setHtml', html, {
|
||||||
|
...options,
|
||||||
|
mediaAccessKey: protocolHandler.getMediaAccessKey(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||||
import { StyledIconSpan, StyledIconI } from './styles';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
readonly themeId: number;
|
readonly themeId: number;
|
||||||
@@ -36,8 +35,12 @@ export default function ToolbarButton(props: Props) {
|
|||||||
let icon = null;
|
let icon = null;
|
||||||
const iconName = getProp(props, 'iconName');
|
const iconName = getProp(props, 'iconName');
|
||||||
if (iconName) {
|
if (iconName) {
|
||||||
const IconClass = isFontAwesomeIcon(iconName) ? StyledIconI : StyledIconSpan;
|
const iconProps: React.HTMLProps<HTMLDivElement> = {
|
||||||
icon = <IconClass className={iconName} aria-label='' hasTitle={!!title} role='img'/>;
|
'aria-label': '',
|
||||||
|
role: 'img',
|
||||||
|
className: `toolbar-icon ${title ? '-has-title' : ''} ${iconName}`,
|
||||||
|
};
|
||||||
|
icon = isFontAwesomeIcon(iconName) ? <i {...iconProps} /> : <span {...iconProps} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep this for legacy compatibility but for consistency we should use "disabled" prop
|
// Keep this for legacy compatibility but for consistency we should use "disabled" prop
|
||||||
|
@@ -1,19 +0,0 @@
|
|||||||
import { ThemeStyle } from '@joplin/lib/theme';
|
|
||||||
|
|
||||||
const styled = require('styled-components').default;
|
|
||||||
const { css } = require('styled-components');
|
|
||||||
|
|
||||||
interface IconProps {
|
|
||||||
readonly theme: ThemeStyle;
|
|
||||||
readonly hasTitle: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const iconStyle = css<IconProps>`
|
|
||||||
font-size: ${(props: IconProps) => props.theme.toolbarIconSize}px;
|
|
||||||
color: ${(props: IconProps) => props.theme.color3};
|
|
||||||
margin-right: ${(props: IconProps) => props.hasTitle ? 5 : 0}px;
|
|
||||||
pointer-events: none; /* Need this to get button tooltip to work */
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const StyledIconI = styled.i`${iconStyle}`;
|
|
||||||
export const StyledIconSpan = styled.span`${iconStyle}`;
|
|
@@ -377,6 +377,20 @@
|
|||||||
contentElement.scrollTop = scrollTop;
|
contentElement.scrollTop = scrollTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rewriteFileUrls = (accessKey) => {
|
||||||
|
if (!accessKey) return;
|
||||||
|
|
||||||
|
// To allow accessing local files from the viewer's non-file URL, file:// URLs are re-written
|
||||||
|
// to joplin-content:// URLs:
|
||||||
|
const mediaElements = document.querySelectorAll('video[src], audio[src], source[src], img[src]');
|
||||||
|
for (const element of mediaElements) {
|
||||||
|
if (element.src?.startsWith('file:')) {
|
||||||
|
const newUrl = element.src.replace(/^file:\/\//, 'joplin-content://file-media/');
|
||||||
|
element.src = `${newUrl}?access-key=${accessKey}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
ipc.setHtml = (event) => {
|
ipc.setHtml = (event) => {
|
||||||
const html = event.html;
|
const html = event.html;
|
||||||
|
|
||||||
@@ -388,6 +402,10 @@
|
|||||||
|
|
||||||
contentElement.innerHTML = html;
|
contentElement.innerHTML = html;
|
||||||
|
|
||||||
|
if (html.includes('file://')) {
|
||||||
|
rewriteFileUrls(event.options.mediaAccessKey);
|
||||||
|
}
|
||||||
|
|
||||||
scrollmap.create(event.options.markupLineCount);
|
scrollmap.create(event.options.markupLineCount);
|
||||||
if (typeof event.options.percent !== 'number') {
|
if (typeof event.options.percent !== 'number') {
|
||||||
restorePercentScroll(); // First, a quick treatment is applied.
|
restorePercentScroll(); // First, a quick treatment is applied.
|
||||||
@@ -733,6 +751,13 @@
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
document.addEventListener('click', webviewLib.logEnabledEventHandler(e => {
|
document.addEventListener('click', webviewLib.logEnabledEventHandler(e => {
|
||||||
|
// Links should all have custom click handlers. Allowing Electron to load custom links
|
||||||
|
// can cause security issues, particularly if these links have the same domain as the
|
||||||
|
// top-level page.
|
||||||
|
if (e.target.hasAttribute('href')) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelectorAll('.media-pdf').forEach(element => {
|
document.querySelectorAll('.media-pdf').forEach(element => {
|
||||||
if(!!element.contentWindow){
|
if(!!element.contentWindow){
|
||||||
element.contentWindow.postMessage({
|
element.contentWindow.postMessage({
|
||||||
|
4
packages/app-desktop/gui/styles/dialog-anchor-node.scss
Normal file
4
packages/app-desktop/gui/styles/dialog-anchor-node.scss
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
.dialog-anchor-node {
|
||||||
|
display: none;
|
||||||
|
}
|
@@ -5,4 +5,7 @@
|
|||||||
@use './flat-button.scss';
|
@use './flat-button.scss';
|
||||||
@use './help-text.scss';
|
@use './help-text.scss';
|
||||||
@use './toolbar-button.scss';
|
@use './toolbar-button.scss';
|
||||||
|
@use './toolbar-icon.scss';
|
||||||
@use './editor-toolbar.scss';
|
@use './editor-toolbar.scss';
|
||||||
|
@use './user-webview-dialog-container.scss';
|
||||||
|
@use './dialog-anchor-node.scss';
|
||||||
|
11
packages/app-desktop/gui/styles/toolbar-icon.scss
Normal file
11
packages/app-desktop/gui/styles/toolbar-icon.scss
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
.toolbar-icon {
|
||||||
|
font-size: var(--joplin-toolbar-icon-size);
|
||||||
|
color: var(--joplin-color3);
|
||||||
|
margin-right: 0px;
|
||||||
|
pointer-events: none; /* Need this to get button tooltip to work */
|
||||||
|
|
||||||
|
&.-has-title {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
.user-webview-dialog-container {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
@@ -136,7 +136,8 @@ test.describe('main', () => {
|
|||||||
expect(fullSize[0] / resizedSize[0]).toBeCloseTo(fullSize[1] / resizedSize[1]);
|
expect(fullSize[0] / resizedSize[0]).toBeCloseTo(fullSize[1] / resizedSize[1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clicking on an external link should try to launch a browser', async ({ electronApp, mainWindow }) => {
|
for (const target of ['', '_blank']) {
|
||||||
|
test(`clicking on an external link with target=${JSON.stringify(target)} should try to launch a browser`, async ({ electronApp, mainWindow }) => {
|
||||||
const mainScreen = new MainScreen(mainWindow);
|
const mainScreen = new MainScreen(mainWindow);
|
||||||
await mainScreen.waitFor();
|
await mainScreen.waitFor();
|
||||||
|
|
||||||
@@ -154,7 +155,7 @@ test.describe('main', () => {
|
|||||||
const testLinkTitle = 'This is a test link!';
|
const testLinkTitle = 'This is a test link!';
|
||||||
const linkHref = 'https://joplinapp.org/';
|
const linkHref = 'https://joplinapp.org/';
|
||||||
|
|
||||||
await mainWindow.evaluate(({ testLinkTitle, linkHref }) => {
|
await mainWindow.evaluate(({ testLinkTitle, linkHref, target }) => {
|
||||||
const testLink = document.createElement('a');
|
const testLink = document.createElement('a');
|
||||||
testLink.textContent = testLinkTitle;
|
testLink.textContent = testLinkTitle;
|
||||||
testLink.onclick = () => {
|
testLink.onclick = () => {
|
||||||
@@ -170,9 +171,12 @@ test.describe('main', () => {
|
|||||||
testLink.style.position = 'fixed';
|
testLink.style.position = 'fixed';
|
||||||
testLink.style.top = '0';
|
testLink.style.top = '0';
|
||||||
testLink.style.left = '0';
|
testLink.style.left = '0';
|
||||||
|
if (target) {
|
||||||
|
testLink.target = target;
|
||||||
|
}
|
||||||
|
|
||||||
document.body.appendChild(testLink);
|
document.body.appendChild(testLink);
|
||||||
}, { testLinkTitle, linkHref });
|
}, { testLinkTitle, linkHref, target });
|
||||||
|
|
||||||
const testLink = mainWindow.getByText(testLinkTitle);
|
const testLink = mainWindow.getByText(testLinkTitle);
|
||||||
await expect(testLink).toBeVisible();
|
await expect(testLink).toBeVisible();
|
||||||
@@ -180,6 +184,7 @@ test.describe('main', () => {
|
|||||||
|
|
||||||
expect(await nextExternalUrlPromise).toBe(linkHref);
|
expect(await nextExternalUrlPromise).toBe(linkHref);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
test('should start in safe mode if profile-dir/force-safe-mode-on-next-start exists', async ({ profileDirectory }) => {
|
test('should start in safe mode if profile-dir/force-safe-mode-on-next-start exists', async ({ profileDirectory }) => {
|
||||||
await writeFile(join(profileDirectory, 'force-safe-mode-on-next-start'), 'true', 'utf8');
|
await writeFile(join(profileDirectory, 'force-safe-mode-on-next-start'), 'true', 'utf8');
|
||||||
|
@@ -30,4 +30,16 @@ export default class GoToAnything {
|
|||||||
public async expectToBeOpen() {
|
public async expectToBeOpen() {
|
||||||
await expect(this.containerLocator).toBeAttached();
|
await expect(this.containerLocator).toBeAttached();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async runCommand(electronApp: ElectronApplication, command: string) {
|
||||||
|
if (!command.startsWith(':')) {
|
||||||
|
command = `:${command}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.open(electronApp);
|
||||||
|
await this.inputLocator.fill(command);
|
||||||
|
await this.containerLocator.locator('.match-highlight').first().waitFor();
|
||||||
|
await this.inputLocator.press('Enter');
|
||||||
|
await this.expectToBeClosed();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
import { Locator, Page } from '@playwright/test';
|
import { Locator, Page } from '@playwright/test';
|
||||||
|
import { expect } from '../util/test';
|
||||||
|
|
||||||
export default class NoteEditorPage {
|
export default class NoteEditorPage {
|
||||||
public readonly codeMirrorEditor: Locator;
|
public readonly codeMirrorEditor: Locator;
|
||||||
@@ -31,6 +32,31 @@ export default class NoteEditorPage {
|
|||||||
return this.containerLocator.getByRole('button', { name: title });
|
return this.containerLocator.getByRole('button', { name: title });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async contentLocator() {
|
||||||
|
const richTextBody = this.getRichTextFrameLocator().locator('body');
|
||||||
|
const markdownEditor = this.codeMirrorEditor;
|
||||||
|
|
||||||
|
// Work around an issue where .or doesn't work with frameLocators.
|
||||||
|
// See https://github.com/microsoft/playwright/issues/27688#issuecomment-1771403495
|
||||||
|
await Promise.race([
|
||||||
|
richTextBody.waitFor({ state: 'visible' }).catch(()=>{}),
|
||||||
|
markdownEditor.waitFor({ state: 'visible' }).catch(()=>{}),
|
||||||
|
]);
|
||||||
|
if (await richTextBody.isVisible()) {
|
||||||
|
return richTextBody;
|
||||||
|
} else {
|
||||||
|
return markdownEditor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async expectToHaveText(content: string) {
|
||||||
|
// expect(...).toHaveText can fail in the Rich Text Editor (perhaps due to frame locators).
|
||||||
|
// Using expect.poll refreshes the locator on each attempt, which seems to prevent flakiness.
|
||||||
|
await expect.poll(
|
||||||
|
async () => (await this.contentLocator()).textContent(),
|
||||||
|
).toBe(content);
|
||||||
|
}
|
||||||
|
|
||||||
public getNoteViewerFrameLocator() {
|
public getNoteViewerFrameLocator() {
|
||||||
// The note viewer can change content when the note re-renders. As such,
|
// The note viewer can change content when the note re-renders. As such,
|
||||||
// a new locator needs to be created after re-renders (and this can't be a
|
// a new locator needs to be created after re-renders (and this can't be a
|
||||||
@@ -38,7 +64,7 @@ export default class NoteEditorPage {
|
|||||||
return this.noteViewerContainer.frameLocator(':scope');
|
return this.noteViewerContainer.frameLocator(':scope');
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTinyMCEFrameLocator() {
|
public getRichTextFrameLocator() {
|
||||||
// We use frameLocator(':scope') to convert the richTextEditor Locator into
|
// We use frameLocator(':scope') to convert the richTextEditor Locator into
|
||||||
// a FrameLocator. (:scope selects the locator itself).
|
// a FrameLocator. (:scope selects the locator itself).
|
||||||
// https://playwright.dev/docs/api/class-framelocator
|
// https://playwright.dev/docs/api/class-framelocator
|
||||||
@@ -53,4 +79,10 @@ export default class NoteEditorPage {
|
|||||||
await this.noteTitleInput.waitFor();
|
await this.noteTitleInput.waitFor();
|
||||||
await this.toggleEditorsButton.waitFor();
|
await this.toggleEditorsButton.waitFor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async goBack() {
|
||||||
|
const backButton = this.toolbarButtonLocator('Back');
|
||||||
|
await expect(backButton).not.toBeDisabled();
|
||||||
|
await backButton.click();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
32
packages/app-desktop/integration-tests/pluginApi.spec.ts
Normal file
32
packages/app-desktop/integration-tests/pluginApi.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
import { test } from './util/test';
|
||||||
|
import MainScreen from './models/MainScreen';
|
||||||
|
|
||||||
|
test.describe('pluginApi', () => {
|
||||||
|
for (const richTextEditor of [false, true]) {
|
||||||
|
test(`the editor.setText command should update the current note (use RTE: ${richTextEditor})`, async ({ startAppWithPlugins }) => {
|
||||||
|
const { app, mainWindow } = await startAppWithPlugins(['resources/test-plugins/execCommand.js']);
|
||||||
|
const mainScreen = new MainScreen(mainWindow);
|
||||||
|
await mainScreen.createNewNote('First note');
|
||||||
|
const editor = mainScreen.noteEditor;
|
||||||
|
|
||||||
|
await editor.focusCodeMirrorEditor();
|
||||||
|
await mainWindow.keyboard.type('This content should be overwritten.');
|
||||||
|
|
||||||
|
if (richTextEditor) {
|
||||||
|
await editor.toggleEditorsButton.click();
|
||||||
|
await editor.richTextEditor.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
await mainScreen.goToAnything.runCommand(app, 'testUpdateEditorText');
|
||||||
|
await editor.expectToHaveText('PASS');
|
||||||
|
|
||||||
|
// Should still have the same text after switching notes:
|
||||||
|
await mainScreen.createNewNote('Second note');
|
||||||
|
await editor.goBack();
|
||||||
|
|
||||||
|
await editor.expectToHaveText('PASS');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@@ -0,0 +1,31 @@
|
|||||||
|
// Allows referencing the Joplin global:
|
||||||
|
/* eslint-disable no-undef */
|
||||||
|
|
||||||
|
// Allows the `joplin-manifest` block comment:
|
||||||
|
/* eslint-disable multiline-comment-style */
|
||||||
|
|
||||||
|
/* joplin-manifest:
|
||||||
|
{
|
||||||
|
"id": "org.joplinapp.plugins.example.execCommand",
|
||||||
|
"manifest_version": 1,
|
||||||
|
"app_min_version": "3.1",
|
||||||
|
"name": "JS Bundle test",
|
||||||
|
"description": "JS Bundle Test plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "",
|
||||||
|
"homepage_url": "https://joplinapp.org"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
joplin.plugins.register({
|
||||||
|
onStart: async function() {
|
||||||
|
await joplin.commands.register({
|
||||||
|
name: 'testUpdateEditorText',
|
||||||
|
label: 'Test setting the editor\'s text with editor.setText',
|
||||||
|
iconName: 'fas fa-drum',
|
||||||
|
execute: async () => {
|
||||||
|
await joplin.commands.execute('editor.setText', 'PASS');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
@@ -38,7 +38,7 @@ test.describe('richTextEditor', () => {
|
|||||||
await editor.richTextEditor.waitFor();
|
await editor.richTextEditor.waitFor();
|
||||||
|
|
||||||
// Edit the note to cause the original content to update
|
// Edit the note to cause the original content to update
|
||||||
await editor.getTinyMCEFrameLocator().locator('a').click();
|
await editor.getRichTextFrameLocator().locator('a').click();
|
||||||
await mainWindow.keyboard.type('Test...');
|
await mainWindow.keyboard.type('Test...');
|
||||||
|
|
||||||
await editor.toggleEditorsButton.click();
|
await editor.toggleEditorsButton.click();
|
||||||
@@ -70,7 +70,7 @@ test.describe('richTextEditor', () => {
|
|||||||
|
|
||||||
// Click on the attached file URL
|
// Click on the attached file URL
|
||||||
const openPathResult = waitForNextOpenPath(electronApp);
|
const openPathResult = waitForNextOpenPath(electronApp);
|
||||||
const targetLink = editor.getTinyMCEFrameLocator().getByRole('link', { name: basename(pathToAttach) });
|
const targetLink = editor.getRichTextFrameLocator().getByRole('link', { name: basename(pathToAttach) });
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
await targetLink.click({ modifiers: ['Meta'] });
|
await targetLink.click({ modifiers: ['Meta'] });
|
||||||
} else {
|
} else {
|
||||||
|
@@ -6,10 +6,12 @@ import createStartupArgs from './createStartupArgs';
|
|||||||
import firstNonDevToolsWindow from './firstNonDevToolsWindow';
|
import firstNonDevToolsWindow from './firstNonDevToolsWindow';
|
||||||
|
|
||||||
|
|
||||||
|
type StartWithPluginsResult = { app: ElectronApplication; mainWindow: Page };
|
||||||
|
|
||||||
type JoplinFixtures = {
|
type JoplinFixtures = {
|
||||||
profileDirectory: string;
|
profileDirectory: string;
|
||||||
electronApp: ElectronApplication;
|
electronApp: ElectronApplication;
|
||||||
|
startAppWithPlugins: (pluginPaths: string[])=> Promise<StartWithPluginsResult>;
|
||||||
startupPluginsLoaded: Promise<void>;
|
startupPluginsLoaded: Promise<void>;
|
||||||
mainWindow: Page;
|
mainWindow: Page;
|
||||||
};
|
};
|
||||||
@@ -17,6 +19,20 @@ type JoplinFixtures = {
|
|||||||
// A custom fixture that loads an electron app. See
|
// A custom fixture that loads an electron app. See
|
||||||
// https://playwright.dev/docs/test-fixtures
|
// https://playwright.dev/docs/test-fixtures
|
||||||
|
|
||||||
|
const getAndResizeMainWindow = async (electronApp: ElectronApplication) => {
|
||||||
|
const mainWindow = await firstNonDevToolsWindow(electronApp);
|
||||||
|
|
||||||
|
// Setting the viewport size helps keep test environments consistent.
|
||||||
|
await mainWindow.setViewportSize({
|
||||||
|
width: 1200,
|
||||||
|
height: 800,
|
||||||
|
});
|
||||||
|
|
||||||
|
return mainWindow;
|
||||||
|
};
|
||||||
|
|
||||||
|
const testDir = dirname(__dirname);
|
||||||
|
|
||||||
export const test = base.extend<JoplinFixtures>({
|
export const test = base.extend<JoplinFixtures>({
|
||||||
// Playwright fails if we don't use the object destructuring
|
// Playwright fails if we don't use the object destructuring
|
||||||
// pattern in the first argument.
|
// pattern in the first argument.
|
||||||
@@ -25,7 +41,7 @@ export const test = base.extend<JoplinFixtures>({
|
|||||||
//
|
//
|
||||||
// eslint-disable-next-line no-empty-pattern
|
// eslint-disable-next-line no-empty-pattern
|
||||||
profileDirectory: async ({ }, use) => {
|
profileDirectory: async ({ }, use) => {
|
||||||
const profilePath = resolve(join(dirname(__dirname), 'test-profile'));
|
const profilePath = resolve(join(testDir, 'test-profile'));
|
||||||
const profileSubdir = join(profilePath, uuid.createNano());
|
const profileSubdir = join(profilePath, uuid.createNano());
|
||||||
await mkdirp(profileSubdir);
|
await mkdirp(profileSubdir);
|
||||||
|
|
||||||
@@ -44,6 +60,34 @@ export const test = base.extend<JoplinFixtures>({
|
|||||||
await electronApp.close();
|
await electronApp.close();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
startAppWithPlugins: async ({ profileDirectory }, use) => {
|
||||||
|
const startupArgs = createStartupArgs(profileDirectory);
|
||||||
|
let electronApp: ElectronApplication;
|
||||||
|
|
||||||
|
await use(async (pluginPaths: string[]) => {
|
||||||
|
if (electronApp) {
|
||||||
|
throw new Error('Electron app already created');
|
||||||
|
}
|
||||||
|
electronApp = await electron.launch({
|
||||||
|
args: [
|
||||||
|
...startupArgs,
|
||||||
|
'--dev-plugins',
|
||||||
|
pluginPaths.map(path => resolve(testDir, path)).join(','),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
app: electronApp,
|
||||||
|
mainWindow: await getAndResizeMainWindow(electronApp),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (electronApp) {
|
||||||
|
await electronApp.firstWindow();
|
||||||
|
await electronApp.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
startupPluginsLoaded: async ({ electronApp }, use) => {
|
startupPluginsLoaded: async ({ electronApp }, use) => {
|
||||||
const startupPluginsLoadedPromise = electronApp.evaluate(({ ipcMain }) => {
|
const startupPluginsLoadedPromise = electronApp.evaluate(({ ipcMain }) => {
|
||||||
return new Promise<void>(resolve => {
|
return new Promise<void>(resolve => {
|
||||||
@@ -55,15 +99,7 @@ export const test = base.extend<JoplinFixtures>({
|
|||||||
},
|
},
|
||||||
|
|
||||||
mainWindow: async ({ electronApp }, use) => {
|
mainWindow: async ({ electronApp }, use) => {
|
||||||
const mainWindow = await firstNonDevToolsWindow(electronApp);
|
await use(await getAndResizeMainWindow(electronApp));
|
||||||
|
|
||||||
// Setting the viewport size helps keep test environments consistent.
|
|
||||||
await mainWindow.setViewportSize({
|
|
||||||
width: 1200,
|
|
||||||
height: 800,
|
|
||||||
});
|
|
||||||
|
|
||||||
await use(mainWindow);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -24,9 +24,9 @@ jest.mock('@electron/remote', () => {
|
|||||||
|
|
||||||
// Import after mocking problematic libraries
|
// Import after mocking problematic libraries
|
||||||
const { afterEachCleanUp, afterAllCleanUp } = require('@joplin/lib/testing/test-utils.js');
|
const { afterEachCleanUp, afterAllCleanUp } = require('@joplin/lib/testing/test-utils.js');
|
||||||
|
const React = require('react');
|
||||||
|
|
||||||
|
shimInit({ nodeSqlite: sqlite3, React });
|
||||||
shimInit({ nodeSqlite: sqlite3 });
|
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await afterEachCleanUp();
|
await afterEachCleanUp();
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@joplin/app-desktop",
|
"name": "@joplin/app-desktop",
|
||||||
"version": "3.1.16",
|
"version": "3.1.21",
|
||||||
"description": "Joplin for Desktop",
|
"description": "Joplin for Desktop",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -124,12 +124,12 @@
|
|||||||
"homepage": "https://github.com/laurent22/joplin#readme",
|
"homepage": "https://github.com/laurent22/joplin#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"7zip-bin": "5.2.0",
|
"7zip-bin": "5.2.0",
|
||||||
"@electron/rebuild": "3.3.0",
|
"@electron/rebuild": "3.6.0",
|
||||||
"@joplin/default-plugins": "~3.1",
|
"@joplin/default-plugins": "~3.1",
|
||||||
"@joplin/tools": "~3.1",
|
"@joplin/tools": "~3.1",
|
||||||
"@playwright/test": "1.44.1",
|
"@playwright/test": "1.44.1",
|
||||||
"@testing-library/react-hooks": "8.0.1",
|
"@testing-library/react-hooks": "8.0.1",
|
||||||
"@types/jest": "29.5.8",
|
"@types/jest": "29.5.12",
|
||||||
"@types/node": "18.19.39",
|
"@types/node": "18.19.39",
|
||||||
"@types/react": "18.3.3",
|
"@types/react": "18.3.3",
|
||||||
"@types/react-dom": "18.3.0",
|
"@types/react-dom": "18.3.0",
|
||||||
@@ -139,19 +139,19 @@
|
|||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"electron": "29.4.5",
|
"electron": "29.4.5",
|
||||||
"electron-builder": "24.13.3",
|
"electron-builder": "24.13.3",
|
||||||
"glob": "10.4.2",
|
"glob": "10.4.5",
|
||||||
"gulp": "4.0.2",
|
"gulp": "4.0.2",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-environment-jsdom": "29.7.0",
|
"jest-environment-jsdom": "29.7.0",
|
||||||
"js-sha512": "0.9.0",
|
"js-sha512": "0.9.0",
|
||||||
"nan": "2.19.0",
|
"nan": "2.19.0",
|
||||||
"react-test-renderer": "18.3.1",
|
"react-test-renderer": "18.3.1",
|
||||||
"ts-jest": "29.1.1",
|
"ts-jest": "29.1.5",
|
||||||
"ts-node": "10.9.2",
|
"ts-node": "10.9.2",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.4.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron/notarize": "2.1.0",
|
"@electron/notarize": "2.3.2",
|
||||||
"@electron/remote": "2.1.2",
|
"@electron/remote": "2.1.2",
|
||||||
"@fortawesome/fontawesome-free": "5.15.4",
|
"@fortawesome/fontawesome-free": "5.15.4",
|
||||||
"@joeattardi/emoji-button": "4.6.4",
|
"@joeattardi/emoji-button": "4.6.4",
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
"@joplin/lib": "~3.1",
|
"@joplin/lib": "~3.1",
|
||||||
"@joplin/renderer": "~3.1",
|
"@joplin/renderer": "~3.1",
|
||||||
"@joplin/utils": "~3.1",
|
"@joplin/utils": "~3.1",
|
||||||
"@sentry/electron": "4.17.0",
|
"@sentry/electron": "4.24.0",
|
||||||
"@types/mustache": "4.2.5",
|
"@types/mustache": "4.2.5",
|
||||||
"async-mutex": "0.5.0",
|
"async-mutex": "0.5.0",
|
||||||
"codemirror": "5.65.9",
|
"codemirror": "5.65.9",
|
||||||
|
@@ -543,11 +543,22 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
|||||||
const resultId = getResultId(item);
|
const resultId = getResultId(item);
|
||||||
const isSelected = resultId === this.state.selectedItemId;
|
const isSelected = resultId === this.state.selectedItemId;
|
||||||
const rowStyle = isSelected ? style.rowSelected : style.row;
|
const rowStyle = isSelected ? style.rowSelected : style.row;
|
||||||
|
|
||||||
|
const wrapKeywordMatches = (unescapedContent: string) => {
|
||||||
|
return surroundKeywords(
|
||||||
|
this.state.keywords,
|
||||||
|
unescapedContent,
|
||||||
|
`<span class="match-highlight" style="font-weight: bold; color: ${theme.searchMarkerColor}; background-color: ${theme.searchMarkerBackgroundColor}">`,
|
||||||
|
'</span>',
|
||||||
|
{ escapeHtml: true },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const titleHtml = item.fragments
|
const titleHtml = item.fragments
|
||||||
? `<span style="font-weight: bold; color: ${theme.color};">${item.title}</span>`
|
? `<span style="font-weight: bold; color: ${theme.color};">${item.title}</span>`
|
||||||
: surroundKeywords(this.state.keywords, item.title, `<span style="font-weight: bold; color: ${theme.searchMarkerColor}; background-color: ${theme.searchMarkerBackgroundColor}">`, '</span>', { escapeHtml: true });
|
: wrapKeywordMatches(item.title);
|
||||||
|
|
||||||
const fragmentsHtml = !item.fragments ? null : surroundKeywords(this.state.keywords, item.fragments, `<span style="color: ${theme.searchMarkerColor}; background-color: ${theme.searchMarkerBackgroundColor}">`, '</span>', { escapeHtml: true });
|
const fragmentsHtml = !item.fragments ? null : wrapKeywordMatches(item.fragments);
|
||||||
|
|
||||||
const folderIcon = <i style={{ fontSize: theme.fontSize, marginRight: 2 }} className="fa fa-book" role='img' aria-label={_('Notebook')} />;
|
const folderIcon = <i style={{ fontSize: theme.fontSize, marginRight: 2 }} className="fa fa-book" role='img' aria-label={_('Notebook')} />;
|
||||||
const pathComp = !item.path ? null : <div style={style.rowPath}>{folderIcon} {item.path}</div>;
|
const pathComp = !item.path ? null : <div style={style.rowPath}>{folderIcon} {item.path}</div>;
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useRef, useImperativeHandle, forwardRef, useEffect } from 'react';
|
import { useRef, useImperativeHandle, forwardRef, useEffect, useMemo } from 'react';
|
||||||
import useViewIsReady from './hooks/useViewIsReady';
|
import useViewIsReady from './hooks/useViewIsReady';
|
||||||
import useThemeCss from './hooks/useThemeCss';
|
import useThemeCss from './hooks/useThemeCss';
|
||||||
import useContentSize from './hooks/useContentSize';
|
import useContentSize from './hooks/useContentSize';
|
||||||
@@ -8,14 +8,10 @@ import useHtmlLoader from './hooks/useHtmlLoader';
|
|||||||
import useWebviewToPluginMessages from './hooks/useWebviewToPluginMessages';
|
import useWebviewToPluginMessages from './hooks/useWebviewToPluginMessages';
|
||||||
import useScriptLoader from './hooks/useScriptLoader';
|
import useScriptLoader from './hooks/useScriptLoader';
|
||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
import styled from 'styled-components';
|
|
||||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||||
|
|
||||||
const logger = Logger.create('UserWebview');
|
const logger = Logger.create('UserWebview');
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
type StyleProps = any;
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
html: string;
|
html: string;
|
||||||
scripts: string[];
|
scripts: string[];
|
||||||
@@ -36,15 +32,6 @@ export interface Props {
|
|||||||
onReady?: Function;
|
onReady?: Function;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledFrame = styled.iframe<{ fitToContent: boolean; borderBottom: boolean }>`
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
width: ${(props: StyleProps) => props.fitToContent ? `${props.width}px` : '100%'};
|
|
||||||
height: ${(props: StyleProps) => props.fitToContent ? `${props.height}px` : '100%'};
|
|
||||||
border: none;
|
|
||||||
border-bottom: ${(props: StyleProps) => props.borderBottom ? `1px solid ${props.theme.dividerColor}` : 'none'};
|
|
||||||
`;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
function serializeForm(form: any) {
|
function serializeForm(form: any) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
@@ -153,15 +140,18 @@ function UserWebview(props: Props, ref: any) {
|
|||||||
cssFilePath,
|
cssFilePath,
|
||||||
);
|
);
|
||||||
|
|
||||||
return <StyledFrame
|
const style = useMemo(() => ({
|
||||||
|
'--content-width': `${contentSize.width}px`,
|
||||||
|
'--content-height': `${contentSize.height}px`,
|
||||||
|
} as React.CSSProperties), [contentSize.width, contentSize.height]);
|
||||||
|
|
||||||
|
return <iframe
|
||||||
id={props.viewId}
|
id={props.viewId}
|
||||||
width={contentSize.width}
|
style={style}
|
||||||
height={contentSize.height}
|
className={`plugin-user-webview ${props.fitToContent ? '-fit-to-content' : ''} ${props.borderBottom ? '-border-bottom' : ''}`}
|
||||||
fitToContent={props.fitToContent}
|
|
||||||
ref={viewRef}
|
ref={viewRef}
|
||||||
src="services/plugins/UserWebviewIndex.html"
|
src="services/plugins/UserWebviewIndex.html"
|
||||||
borderBottom={props.borderBottom}
|
></iframe>;
|
||||||
></StyledFrame>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default forwardRef(UserWebview);
|
export default forwardRef(UserWebview);
|
||||||
|
@@ -7,18 +7,12 @@ import UserWebview, { Props as UserWebviewProps } from './UserWebview';
|
|||||||
import UserWebviewDialogButtonBar from './UserWebviewDialogButtonBar';
|
import UserWebviewDialogButtonBar from './UserWebviewDialogButtonBar';
|
||||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||||
import Dialog from '../../gui/Dialog';
|
import Dialog from '../../gui/Dialog';
|
||||||
const styled = require('styled-components').default;
|
|
||||||
|
|
||||||
interface Props extends UserWebviewProps {
|
interface Props extends UserWebviewProps {
|
||||||
buttons: ButtonSpec[];
|
buttons: ButtonSpec[];
|
||||||
fitToContent: boolean;
|
fitToContent: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserWebViewWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function defaultButtons(): ButtonSpec[] {
|
function defaultButtons(): ButtonSpec[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -84,7 +78,7 @@ export default function UserWebviewDialog(props: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog className={`user-webview-dialog ${props.fitToContent ? '-fit' : ''}`}>
|
<Dialog className={`user-webview-dialog ${props.fitToContent ? '-fit' : ''}`}>
|
||||||
<UserWebViewWrapper>
|
<div className='user-dialog-wrapper'>
|
||||||
<UserWebview
|
<UserWebview
|
||||||
ref={webviewRef}
|
ref={webviewRef}
|
||||||
html={props.html}
|
html={props.html}
|
||||||
@@ -98,7 +92,7 @@ export default function UserWebviewDialog(props: Props) {
|
|||||||
onDismiss={onDismiss}
|
onDismiss={onDismiss}
|
||||||
onReady={onReady}
|
onReady={onReady}
|
||||||
/>
|
/>
|
||||||
</UserWebViewWrapper>
|
</div>
|
||||||
<UserWebviewDialogButtonBar buttons={buttons}/>
|
<UserWebviewDialogButtonBar buttons={buttons}/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
@@ -5,20 +5,10 @@ import { ButtonSpec } from '@joplin/lib/services/plugins/api/types';
|
|||||||
const styled = require('styled-components').default;
|
const styled = require('styled-components').default;
|
||||||
const { space } = require('styled-system');
|
const { space } = require('styled-system');
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
type StyleProps = any;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
buttons: ButtonSpec[];
|
buttons: ButtonSpec[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledRoot = styled.div`
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding-top: ${(props: StyleProps) => props.theme.mainPadding}px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledButton = styled(Button)`${space}`;
|
const StyledButton = styled(Button)`${space}`;
|
||||||
|
|
||||||
@@ -48,8 +38,8 @@ export default function UserWebviewDialogButtonBar(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRoot>
|
<div className='user-dialog-button-bar'>
|
||||||
{renderButtons()}
|
{renderButtons()}
|
||||||
</StyledRoot>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -51,6 +51,7 @@ const webviewApi = {
|
|||||||
|
|
||||||
docReady(() => {
|
docReady(() => {
|
||||||
const rootElement = document.createElement('div');
|
const rootElement = document.createElement('div');
|
||||||
|
rootElement.setAttribute('id', 'joplin-plugin-content-root');
|
||||||
document.getElementsByTagName('body')[0].appendChild(rootElement);
|
document.getElementsByTagName('body')[0].appendChild(rootElement);
|
||||||
|
|
||||||
const contentElement = document.createElement('div');
|
const contentElement = document.createElement('div');
|
||||||
|
3
packages/app-desktop/services/plugins/styles/index.scss
Normal file
3
packages/app-desktop/services/plugins/styles/index.scss
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@use './plugin-user-webview.scss';
|
||||||
|
@use './user-dialog-wrapper.scss';
|
||||||
|
@use './user-dialog-button-bar.scss';
|
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
.plugin-user-webview {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&.-border-bottom {
|
||||||
|
border-bottom: 1px solid var(--joplin-divider-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-fit-to-content {
|
||||||
|
width: var(--content-width);
|
||||||
|
height: var(--content-height);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
.user-dialog-button-bar {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: var(--joplin-main-padding);
|
||||||
|
}
|
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
.user-dialog-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
@@ -11,6 +11,7 @@
|
|||||||
@use 'gui/UpdateNotification/style.scss' as update-notification;
|
@use 'gui/UpdateNotification/style.scss' as update-notification;
|
||||||
@use 'gui/TrashNotification/style.scss' as trash-notification;
|
@use 'gui/TrashNotification/style.scss' as trash-notification;
|
||||||
@use 'gui/Sidebar/style.scss' as sidebar-styles;
|
@use 'gui/Sidebar/style.scss' as sidebar-styles;
|
||||||
@use 'gui/styles/index.scss';
|
@use 'gui/NoteEditor/style.scss' as note-editor-styles;
|
||||||
@use 'gui/NoteEditor/style.scss';
|
@use 'services/plugins/styles/index.scss' as plugins-styles;
|
||||||
|
@use 'gui/styles/index.scss' as gui-styles;
|
||||||
@use 'main.scss' as main;
|
@use 'main.scss' as main;
|
||||||
|
@@ -16,9 +16,13 @@ if [[ $NEED_COMPILING == 1 ]]; then
|
|||||||
echo "Copying from: $PLUGIN_PATH"
|
echo "Copying from: $PLUGIN_PATH"
|
||||||
echo "To: $TEMP_PLUGIN_PATH"
|
echo "To: $TEMP_PLUGIN_PATH"
|
||||||
|
|
||||||
rsync -a --delete "$PLUGIN_PATH/" "$TEMP_PLUGIN_PATH/"
|
rsync -a --exclude "cache/" --exclude "node_modules" --delete "$PLUGIN_PATH/" "$TEMP_PLUGIN_PATH/"
|
||||||
|
|
||||||
NODE_OPTIONS=--openssl-legacy-provider npm install --prefix="$TEMP_PLUGIN_PATH" && yarn start --dev-plugins "$TEMP_PLUGIN_PATH"
|
cd "$TEMP_PLUGIN_PATH/"
|
||||||
|
NODE_OPTIONS=--openssl-legacy-provider npm install
|
||||||
|
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
yarn start --dev-plugins "$TEMP_PLUGIN_PATH"
|
||||||
else
|
else
|
||||||
yarn start --dev-plugins "$PLUGIN_PATH"
|
yarn start --dev-plugins "$PLUGIN_PATH"
|
||||||
fi
|
fi
|
||||||
|
@@ -42,21 +42,29 @@ const setUpProtocolHandler = () => {
|
|||||||
return { protocolHandler, onRequestListener };
|
return { protocolHandler, onRequestListener };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface ExpectBlockedOptions {
|
||||||
|
host?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Although none of the paths in this test suite point to real files, file paths must be in
|
// Although none of the paths in this test suite point to real files, file paths must be in
|
||||||
// a certain format on Windows to avoid invalid path exceptions.
|
// a certain format on Windows to avoid invalid path exceptions.
|
||||||
const toPlatformPath = (path: string) => process.platform === 'win32' ? `C:/${path}` : path;
|
const toPlatformPath = (path: string) => process.platform === 'win32' ? `C:/${path}` : path;
|
||||||
|
|
||||||
const expectPathToBeBlocked = async (onRequestListener: ProtocolHandler, filePath: string) => {
|
const toAccessUrl = (path: string, { host = 'note-viewer' }: ExpectBlockedOptions = {}) => {
|
||||||
const url = `joplin-content://note-viewer/${toPlatformPath(filePath)}`;
|
return `joplin-content://${host}/${toPlatformPath(path)}`;
|
||||||
|
|
||||||
await expect(
|
|
||||||
async () => await onRequestListener(new Request(url)),
|
|
||||||
).rejects.toThrowError('Read access not granted for URL');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const expectPathToBeUnblocked = async (onRequestListener: ProtocolHandler, filePath: string) => {
|
const expectPathToBeBlocked = async (onRequestListener: ProtocolHandler, filePath: string, options?: ExpectBlockedOptions) => {
|
||||||
|
const url = toAccessUrl(filePath, options);
|
||||||
|
await expect(
|
||||||
|
async () => await onRequestListener(new Request(url)),
|
||||||
|
).rejects.toThrow(/Read access not granted for URL|Invalid or missing media access key|Media access denied/);
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectPathToBeUnblocked = async (onRequestListener: ProtocolHandler, filePath: string, options?: ExpectBlockedOptions) => {
|
||||||
|
const url = toAccessUrl(filePath, options);
|
||||||
const handleRequestResult = await onRequestListener(
|
const handleRequestResult = await onRequestListener(
|
||||||
new Request(`joplin-content://note-viewer/${toPlatformPath(filePath)}`),
|
new Request(url),
|
||||||
);
|
);
|
||||||
expect(handleRequestResult.body).toBeTruthy();
|
expect(handleRequestResult.body).toBeTruthy();
|
||||||
};
|
};
|
||||||
@@ -107,6 +115,34 @@ describe('handleCustomProtocols', () => {
|
|||||||
await expectPathToBeBlocked(onRequestListener, '/test/path/a.txt');
|
await expectPathToBeBlocked(onRequestListener, '/test/path/a.txt');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should only allow access to file-media/ URLs when given the correct access key', async () => {
|
||||||
|
const { protocolHandler, onRequestListener } = setUpProtocolHandler();
|
||||||
|
const expectBlocked = (path: string) => {
|
||||||
|
return expectPathToBeBlocked(onRequestListener, path, { host: 'file-media' });
|
||||||
|
};
|
||||||
|
const expectUnblocked = (path: string) => {
|
||||||
|
return expectPathToBeUnblocked(onRequestListener, path, { host: 'file-media' });
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchMock.mockImplementation(async (_url: string) => {
|
||||||
|
return new Response('', { headers: { 'Content-Type': 'image/jpeg' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const testPath = join(supportDir, 'photo.jpg');
|
||||||
|
await expectBlocked(testPath);
|
||||||
|
await expectBlocked(`${testPath}?access-key=wrongKey`);
|
||||||
|
await expectBlocked(`${testPath}?access-key=false`);
|
||||||
|
|
||||||
|
protocolHandler.setMediaAccessEnabled(true);
|
||||||
|
const key = protocolHandler.getMediaAccessKey();
|
||||||
|
await expectUnblocked(`${testPath}?access-key=${key}`);
|
||||||
|
await expectBlocked(`${testPath}?access-key=null`);
|
||||||
|
protocolHandler.setMediaAccessEnabled(false);
|
||||||
|
|
||||||
|
await expectBlocked(`${testPath}?access-key=${key}`);
|
||||||
|
});
|
||||||
|
|
||||||
test('should allow requesting part of a file', async () => {
|
test('should allow requesting part of a file', async () => {
|
||||||
const { protocolHandler, onRequestListener } = setUpProtocolHandler();
|
const { protocolHandler, onRequestListener } = setUpProtocolHandler();
|
||||||
|
|
||||||
|
@@ -7,10 +7,20 @@ import { LoggerWrapper } from '@joplin/utils/Logger';
|
|||||||
import * as fs from 'fs-extra';
|
import * as fs from 'fs-extra';
|
||||||
import { createReadStream } from 'fs';
|
import { createReadStream } from 'fs';
|
||||||
import { fromFilename } from '@joplin/lib/mime-utils';
|
import { fromFilename } from '@joplin/lib/mime-utils';
|
||||||
|
import { createSecureRandom } from '@joplin/lib/uuid';
|
||||||
|
|
||||||
|
export interface AccessController {
|
||||||
|
remove(): void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CustomProtocolHandler {
|
export interface CustomProtocolHandler {
|
||||||
|
// note-viewer/ URLs
|
||||||
allowReadAccessToDirectory(path: string): void;
|
allowReadAccessToDirectory(path: string): void;
|
||||||
allowReadAccessToFile(path: string): { remove(): void };
|
allowReadAccessToFile(path: string): AccessController;
|
||||||
|
|
||||||
|
// file-media/ URLs
|
||||||
|
setMediaAccessEnabled(enabled: boolean): void;
|
||||||
|
getMediaAccessKey(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -125,8 +135,16 @@ const handleRangeRequest = async (request: Request, targetPath: string) => {
|
|||||||
// TODO: Use Logger.create (doesn't work for now because Logger is only initialized
|
// TODO: Use Logger.create (doesn't work for now because Logger is only initialized
|
||||||
// in the main process.)
|
// in the main process.)
|
||||||
const handleCustomProtocols = (logger: LoggerWrapper): CustomProtocolHandler => {
|
const handleCustomProtocols = (logger: LoggerWrapper): CustomProtocolHandler => {
|
||||||
|
logger = {
|
||||||
|
...logger,
|
||||||
|
debug: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allow-listed files/directories for joplin-content://note-viewer/
|
||||||
const readableDirectories: string[] = [];
|
const readableDirectories: string[] = [];
|
||||||
const readableFiles = new Map<string, number>();
|
const readableFiles = new Map<string, number>();
|
||||||
|
// Access for joplin-content://file-media/
|
||||||
|
let mediaAccessKey: string|false = false;
|
||||||
|
|
||||||
// See also the protocol.handle example: https://www.electronjs.org/docs/latest/api/protocol#protocolhandlescheme-handler
|
// See also the protocol.handle example: https://www.electronjs.org/docs/latest/api/protocol#protocolhandlescheme-handler
|
||||||
protocol.handle(contentProtocolName, async request => {
|
protocol.handle(contentProtocolName, async request => {
|
||||||
@@ -142,10 +160,9 @@ const handleCustomProtocols = (logger: LoggerWrapper): CustomProtocolHandler =>
|
|||||||
|
|
||||||
pathname = resolve(appBundleDirectory, pathname);
|
pathname = resolve(appBundleDirectory, pathname);
|
||||||
|
|
||||||
const allowedHosts = ['note-viewer'];
|
|
||||||
|
|
||||||
let canRead = false;
|
let canRead = false;
|
||||||
if (allowedHosts.includes(host)) {
|
let mediaOnly = true;
|
||||||
|
if (host === 'note-viewer') {
|
||||||
if (readableFiles.has(pathname)) {
|
if (readableFiles.has(pathname)) {
|
||||||
canRead = true;
|
canRead = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -156,6 +173,20 @@ const handleCustomProtocols = (logger: LoggerWrapper): CustomProtocolHandler =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mediaOnly = false;
|
||||||
|
} else if (host === 'file-media') {
|
||||||
|
if (!mediaAccessKey) {
|
||||||
|
throw new Error('Media access denied. This must be enabled with .setMediaAccessEnabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
canRead = true;
|
||||||
|
mediaOnly = true;
|
||||||
|
|
||||||
|
const accessKey = url.searchParams.get('access-key');
|
||||||
|
if (accessKey !== mediaAccessKey) {
|
||||||
|
throw new Error(`Invalid or missing media access key (was ${accessKey}). An allow-listed ?access-key= parameter must be provided.`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Invalid URL ${request.url}`);
|
throw new Error(`Invalid URL ${request.url}`);
|
||||||
}
|
}
|
||||||
@@ -168,12 +199,26 @@ const handleCustomProtocols = (logger: LoggerWrapper): CustomProtocolHandler =>
|
|||||||
logger.debug('protocol handler: Fetch file URL', asFileUrl);
|
logger.debug('protocol handler: Fetch file URL', asFileUrl);
|
||||||
|
|
||||||
const rangeHeader = request.headers.get('Range');
|
const rangeHeader = request.headers.get('Range');
|
||||||
|
let response;
|
||||||
if (!rangeHeader) {
|
if (!rangeHeader) {
|
||||||
const response = await net.fetch(asFileUrl);
|
response = await net.fetch(asFileUrl);
|
||||||
return response;
|
|
||||||
} else {
|
} else {
|
||||||
return handleRangeRequest(request, pathname);
|
response = await handleRangeRequest(request, pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mediaOnly) {
|
||||||
|
// Tells the browser to avoid MIME confusion attacks. See
|
||||||
|
// https://blog.mozilla.org/security/2016/08/26/mitigating-mime-confusion-attacks-in-firefox/
|
||||||
|
response.headers.set('X-Content-Type-Options', 'nosniff');
|
||||||
|
|
||||||
|
// This is an extra check to prevent loading text/html and arbitrary non-media content from the URL.
|
||||||
|
const contentType = response.headers.get('Content-Type');
|
||||||
|
if (!contentType || !contentType.match(/^(image|video|audio)\//)) {
|
||||||
|
throw new Error(`Attempted to access non-media file from ${request.url}, which is media-only. Content type was ${contentType}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
});
|
});
|
||||||
|
|
||||||
const appBundleDirectory = dirname(dirname(__dirname));
|
const appBundleDirectory = dirname(dirname(__dirname));
|
||||||
@@ -205,6 +250,18 @@ const handleCustomProtocols = (logger: LoggerWrapper): CustomProtocolHandler =>
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
setMediaAccessEnabled: (enabled: boolean) => {
|
||||||
|
if (enabled) {
|
||||||
|
mediaAccessKey ||= createSecureRandom();
|
||||||
|
} else {
|
||||||
|
mediaAccessKey = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Allows access to all local media files, provided a matching ?access-key=<key> is added
|
||||||
|
// to the request URL.
|
||||||
|
getMediaAccessKey: () => {
|
||||||
|
return mediaAccessKey || null;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -79,8 +79,8 @@ android {
|
|||||||
applicationId "net.cozic.joplin"
|
applicationId "net.cozic.joplin"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 2097752
|
versionCode 2097755
|
||||||
versionName "3.1.4"
|
versionName "3.1.7"
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||||
}
|
}
|
||||||
|
@@ -93,7 +93,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
|||||||
}, [dom]);
|
}, [dom]);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- HACK: Allow wrapper testing logic to access the DOM.
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- HACK: Allow wrapper testing logic to access the DOM.
|
||||||
const additionalProps: any = { document: dom?.window?.document };
|
const additionalProps: any = { window: dom?.window };
|
||||||
return (
|
return (
|
||||||
<View style={props.style} testID={props.testID} {...additionalProps}/>
|
<View style={props.style} testID={props.testID} {...additionalProps}/>
|
||||||
);
|
);
|
||||||
|
@@ -569,6 +569,7 @@ function NoteEditor(props: Props, ref: any) {
|
|||||||
}}>
|
}}>
|
||||||
<ExtendedWebView
|
<ExtendedWebView
|
||||||
webviewInstanceId='NoteEditor'
|
webviewInstanceId='NoteEditor'
|
||||||
|
testID='NoteEditor'
|
||||||
scrollEnabled={true}
|
scrollEnabled={true}
|
||||||
ref={webviewRef}
|
ref={webviewRef}
|
||||||
html={html}
|
html={html}
|
||||||
|
@@ -139,6 +139,7 @@ const MenuComponent: React.FC<Props> = props => {
|
|||||||
style={styles.menuContentScroller}
|
style={styles.menuContentScroller}
|
||||||
aria-modal={true}
|
aria-modal={true}
|
||||||
accessibilityViewIsModal={true}
|
accessibilityViewIsModal={true}
|
||||||
|
testID={`menu-content-${refocusCounter ? 'refocusing' : ''}`}
|
||||||
>{menuOptionComponents}</ScrollView>
|
>{menuOptionComponents}</ScrollView>
|
||||||
</MenuOptions>
|
</MenuOptions>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
@@ -100,6 +100,9 @@ const FloatingActionButton = (props: ActionButtonProps) => {
|
|||||||
onStateChange={onMenuToggled}
|
onStateChange={onMenuToggled}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
onPress={props.mainButton?.onPress ?? defaultOnPress}
|
onPress={props.mainButton?.onPress ?? defaultOnPress}
|
||||||
|
// The long press delay is too short by default (and we don't use the long press event). See https://github.com/laurent22/joplin/issues/11183.
|
||||||
|
// Increase to a large value:
|
||||||
|
delayLongPress={10_000}
|
||||||
visible={true}
|
visible={true}
|
||||||
/>;
|
/>;
|
||||||
const mainMenu = isWeb ? (
|
const mainMenu = isWeb ? (
|
||||||
|
@@ -30,6 +30,7 @@ export type ThemeStyle = BaseTheme & typeof baseStyle & {
|
|||||||
headerStyle: TextStyle;
|
headerStyle: TextStyle;
|
||||||
headerWrapperStyle: ViewStyle;
|
headerWrapperStyle: ViewStyle;
|
||||||
rootStyle: ViewStyle;
|
rootStyle: ViewStyle;
|
||||||
|
hiddenRootStyle: ViewStyle;
|
||||||
keyboardAppearance: 'light'|'dark';
|
keyboardAppearance: 'light'|'dark';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87,6 +88,11 @@ function extraStyles(theme: BaseTheme) {
|
|||||||
backgroundColor: theme.backgroundColor,
|
backgroundColor: theme.backgroundColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hiddenRootStyle: ViewStyle = {
|
||||||
|
...rootStyle,
|
||||||
|
flex: 0.001, // This is a bit of a hack but it seems to work fine - it makes the component invisible but without unmounting it
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
marginRight: baseStyle.margin,
|
marginRight: baseStyle.margin,
|
||||||
marginLeft: baseStyle.margin,
|
marginLeft: baseStyle.margin,
|
||||||
@@ -101,6 +107,7 @@ function extraStyles(theme: BaseTheme) {
|
|||||||
headerStyle,
|
headerStyle,
|
||||||
headerWrapperStyle,
|
headerWrapperStyle,
|
||||||
rootStyle,
|
rootStyle,
|
||||||
|
hiddenRootStyle,
|
||||||
|
|
||||||
keyboardAppearance: theme.appearance,
|
keyboardAppearance: theme.appearance,
|
||||||
color5: theme.color5 ?? theme.backgroundColor4,
|
color5: theme.color5 ?? theme.backgroundColor4,
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { describe, it, beforeEach } from '@jest/globals';
|
import { describe, it, beforeEach } from '@jest/globals';
|
||||||
import { fireEvent, render, screen, userEvent, waitFor } from '@testing-library/react-native';
|
import { act, fireEvent, render, screen, userEvent, waitFor } from '@testing-library/react-native';
|
||||||
import '@testing-library/jest-native/extend-expect';
|
import '@testing-library/jest-native/extend-expect';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import NoteScreen from './Note';
|
import NoteScreen from './Note';
|
||||||
import { MenuProvider } from 'react-native-popup-menu';
|
import { MenuProvider } from 'react-native-popup-menu';
|
||||||
import { runWithFakeTimers, setupDatabaseAndSynchronizer, switchClient, simulateReadOnlyShareEnv } from '@joplin/lib/testing/test-utils';
|
import { setupDatabaseAndSynchronizer, switchClient, simulateReadOnlyShareEnv, supportDir, synchronizerStart, resourceFetcher, runWithFakeTimers } from '@joplin/lib/testing/test-utils';
|
||||||
|
import { waitFor as waitForWithRealTimers } from '@joplin/lib/testing/test-utils';
|
||||||
import Note from '@joplin/lib/models/Note';
|
import Note from '@joplin/lib/models/Note';
|
||||||
import { AppState } from '../../utils/types';
|
import { AppState } from '../../utils/types';
|
||||||
import { Store } from 'redux';
|
import { Store } from 'redux';
|
||||||
@@ -24,6 +25,10 @@ import { getDisplayParentId } from '@joplin/lib/services/trash';
|
|||||||
import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly';
|
import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly';
|
||||||
import { LayoutChangeEvent } from 'react-native';
|
import { LayoutChangeEvent } from 'react-native';
|
||||||
import shim from '@joplin/lib/shim';
|
import shim from '@joplin/lib/shim';
|
||||||
|
import getWebViewWindowById from '../../utils/testing/getWebViewWindowById';
|
||||||
|
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||||
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
import Resource from '@joplin/lib/models/Resource';
|
||||||
|
|
||||||
interface WrapperProps {
|
interface WrapperProps {
|
||||||
}
|
}
|
||||||
@@ -44,12 +49,29 @@ const getNoteViewerDom = async () => {
|
|||||||
return await getWebViewDomById('NoteBodyViewer');
|
return await getWebViewDomById('NoteBodyViewer');
|
||||||
};
|
};
|
||||||
|
|
||||||
const openNewNote = async (noteProperties: NoteEntity) => {
|
const getNoteEditorControl = async () => {
|
||||||
const note = await Note.save({
|
const noteEditor = await getWebViewWindowById('NoteEditor');
|
||||||
parent_id: (await Folder.defaultFolder()).id,
|
const getEditorControl = () => {
|
||||||
...noteProperties,
|
if ('cm' in noteEditor.window && noteEditor.window.cm) {
|
||||||
|
return noteEditor.window.cm as CodeMirrorControl;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
await waitFor(async () => {
|
||||||
|
expect(getEditorControl()).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
return getEditorControl();
|
||||||
|
};
|
||||||
|
|
||||||
|
const waitForNoteToMatch = async (noteId: string, note: Partial<NoteEntity>) => {
|
||||||
|
await act(() => waitForWithRealTimers(async () => {
|
||||||
|
const loadedNote = await Note.load(noteId);
|
||||||
|
expect(loadedNote).toMatchObject(note);
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const openExistingNote = async (noteId: string) => {
|
||||||
|
const note = await Note.load(noteId);
|
||||||
const displayParentId = getDisplayParentId(note, await Folder.load(note.parent_id));
|
const displayParentId = getDisplayParentId(note, await Folder.load(note.parent_id));
|
||||||
|
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
@@ -62,6 +84,17 @@ const openNewNote = async (noteProperties: NoteEntity) => {
|
|||||||
id: note.id,
|
id: note.id,
|
||||||
folderId: displayParentId,
|
folderId: displayParentId,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openNewNote = async (noteProperties: NoteEntity) => {
|
||||||
|
const note = await Note.save({
|
||||||
|
parent_id: (await Folder.defaultFolder()).id,
|
||||||
|
...noteProperties,
|
||||||
|
});
|
||||||
|
|
||||||
|
await openExistingNote(note.id);
|
||||||
|
await waitForNoteToMatch(note.id, { parent_id: note.parent_id, title: note.title, body: note.body });
|
||||||
|
|
||||||
return note.id;
|
return note.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -80,11 +113,33 @@ const openNoteActionsMenu = async () => {
|
|||||||
cursor = cursor.parent;
|
cursor = cursor.parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
await runWithFakeTimers(() => userEvent.press(actionMenuButton));
|
// Wrap in act(...) -- this tells the test library that component state is intended to update (prevents
|
||||||
|
// warnings).
|
||||||
|
await act(async () => {
|
||||||
|
await runWithFakeTimers(async () => {
|
||||||
|
await userEvent.press(actionMenuButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
// State can update until the menu content is marked as in the process of refocusing (part of the
|
||||||
|
// menu transition).
|
||||||
|
await waitFor(async () => {
|
||||||
|
expect(await screen.findByTestId('menu-content-refocusing')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Note', () => {
|
const openEditor = async () => {
|
||||||
|
const editButton = await screen.findByLabelText('Edit');
|
||||||
|
|
||||||
|
fireEvent.press(editButton);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByLabelText('Edit')).toBeNull();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('screens/Note', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
await setupDatabaseAndSynchronizer(1);
|
||||||
await setupDatabaseAndSynchronizer(0);
|
await setupDatabaseAndSynchronizer(0);
|
||||||
await switchClient(0);
|
await switchClient(0);
|
||||||
|
|
||||||
@@ -113,19 +168,62 @@ describe('Note', () => {
|
|||||||
|
|
||||||
it('changing the note title input should update the note\'s title', async () => {
|
it('changing the note title input should update the note\'s title', async () => {
|
||||||
const noteId = await openNewNote({ title: 'Change me!', body: 'Unchanged body' });
|
const noteId = await openNewNote({ title: 'Change me!', body: 'Unchanged body' });
|
||||||
|
|
||||||
render(<WrappedNoteScreen />);
|
render(<WrappedNoteScreen />);
|
||||||
|
|
||||||
const titleInput = await screen.findByDisplayValue('Change me!');
|
const titleInput = await screen.findByDisplayValue('Change me!');
|
||||||
// We need to use fake timers while using userEvent to avoid warnings:
|
|
||||||
await runWithFakeTimers(async () => {
|
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
await user.clear(titleInput);
|
await user.clear(titleInput);
|
||||||
await user.type(titleInput, 'New title');
|
await user.type(titleInput, 'New title');
|
||||||
|
|
||||||
|
await waitForNoteToMatch(noteId, { title: 'New title', body: 'Unchanged body' });
|
||||||
|
|
||||||
|
// Use fake timers to allow advancing timers without pausing the test
|
||||||
|
await runWithFakeTimers(async () => {
|
||||||
|
let expectedTitle = 'New title';
|
||||||
|
for (let i = 0; i <= 10; i++) {
|
||||||
|
for (const chunk of ['!', ' test', '!!!', ' Testing']) {
|
||||||
|
jest.advanceTimersByTime(i % 5);
|
||||||
|
await user.type(titleInput, chunk);
|
||||||
|
expectedTitle += chunk;
|
||||||
|
|
||||||
|
// Don't verify after each input event -- this allows the save action queue to fill.
|
||||||
|
if (i % 4 === 0) {
|
||||||
|
await waitForNoteToMatch(noteId, { title: expectedTitle });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await waitForNoteToMatch(noteId, { title: expectedTitle });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(async () => {
|
it('changing the note body in the editor should update the note\'s body', async () => {
|
||||||
expect(await Note.load(noteId)).toMatchObject({ title: 'New title', body: 'Unchanged body' });
|
const defaultBody = 'Change me!';
|
||||||
});
|
const noteId = await openNewNote({ title: 'Unchanged title', body: defaultBody });
|
||||||
|
|
||||||
|
const noteScreen = render(<WrappedNoteScreen />);
|
||||||
|
await act(async () => await runWithFakeTimers(async () => {
|
||||||
|
await openEditor();
|
||||||
|
const editor = await getNoteEditorControl();
|
||||||
|
editor.select(defaultBody.length, defaultBody.length);
|
||||||
|
|
||||||
|
editor.insertText(' Testing!!!');
|
||||||
|
await waitForNoteToMatch(noteId, { body: 'Change me! Testing!!!' });
|
||||||
|
|
||||||
|
editor.insertText(' This is a test.');
|
||||||
|
await waitForNoteToMatch(noteId, { body: 'Change me! Testing!!! This is a test.' });
|
||||||
|
|
||||||
|
// should also save changes made shortly before unmounting
|
||||||
|
editor.insertText(' Test!');
|
||||||
|
|
||||||
|
// TODO: Decreasing this below 100 causes the test to fail.
|
||||||
|
// See issue #11125.
|
||||||
|
await jest.advanceTimersByTimeAsync(450);
|
||||||
|
|
||||||
|
noteScreen.unmount();
|
||||||
|
await waitForNoteToMatch(noteId, { body: 'Change me! Testing!!! This is a test. Test!' });
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('pressing "delete" should move the note to the trash', async () => {
|
it('pressing "delete" should move the note to the trash', async () => {
|
||||||
@@ -189,4 +287,48 @@ describe('Note', () => {
|
|||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
'auto',
|
||||||
|
'manual',
|
||||||
|
])('should correctly auto-download or not auto-download resources in %j mode', async (downloadMode) => {
|
||||||
|
let note = await Note.save({ title: 'Note 1', parent_id: (await Folder.defaultFolder()).id });
|
||||||
|
note = await shim.attachFileToNote(note, `${supportDir}/photo.jpg`);
|
||||||
|
|
||||||
|
await synchronizerStart();
|
||||||
|
await switchClient(1);
|
||||||
|
Setting.setValue('sync.resourceDownloadMode', downloadMode);
|
||||||
|
await synchronizerStart();
|
||||||
|
|
||||||
|
// Before opening the note, the resource should not be marked for download
|
||||||
|
const allResources = await Resource.all();
|
||||||
|
expect(allResources.length).toBe(1);
|
||||||
|
const resource = allResources[0];
|
||||||
|
expect(await Resource.localState(resource)).toMatchObject({ fetch_status: Resource.FETCH_STATUS_IDLE });
|
||||||
|
|
||||||
|
await openExistingNote(note.id);
|
||||||
|
|
||||||
|
render(<WrappedNoteScreen />);
|
||||||
|
|
||||||
|
// Note should render
|
||||||
|
const titleInput = await screen.findByDisplayValue('Note 1');
|
||||||
|
expect(titleInput).toBeVisible();
|
||||||
|
|
||||||
|
// Wrap in act() -- the component may update in the background during this.
|
||||||
|
await act(async () => {
|
||||||
|
await resourceFetcher().waitForAllFinished();
|
||||||
|
|
||||||
|
// After opening the note, the resource should be marked for download only in automatic mode
|
||||||
|
if (downloadMode === 'auto') {
|
||||||
|
await waitFor(async () => {
|
||||||
|
expect(await Resource.localState(resource.id)).toMatchObject({ fetch_status: Resource.FETCH_STATUS_DONE });
|
||||||
|
});
|
||||||
|
} else if (downloadMode === 'manual') {
|
||||||
|
// In manual mode, should not mark for download
|
||||||
|
expect(await Resource.localState(resource)).toMatchObject({ fetch_status: Resource.FETCH_STATUS_IDLE });
|
||||||
|
} else {
|
||||||
|
throw new Error(`Should not be testing downloadMode: ${downloadMode}.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -493,11 +493,6 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
|||||||
this.undoRedoService_ = new UndoRedoService();
|
this.undoRedoService_ = new UndoRedoService();
|
||||||
this.undoRedoService_.on('stackChange', this.undoRedoService_stackChange);
|
this.undoRedoService_.on('stackChange', this.undoRedoService_stackChange);
|
||||||
|
|
||||||
if (this.state.note && this.state.note.body && Setting.value('sync.resourceDownloadMode') === 'auto') {
|
|
||||||
const resourceIds = await Note.linkedResourceIds(this.state.note.body);
|
|
||||||
await ResourceFetcher.instance().markForDownload(resourceIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Although it is async, we don't wait for the answer so that if permission
|
// Although it is async, we don't wait for the answer so that if permission
|
||||||
// has already been granted, it doesn't slow down opening the note. If it hasn't
|
// has already been granted, it doesn't slow down opening the note. If it hasn't
|
||||||
// been granted, the popup will open anyway.
|
// been granted, the popup will open anyway.
|
||||||
@@ -509,8 +504,12 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
|||||||
void ResourceFetcher.instance().markForDownload(event.resourceId);
|
void ResourceFetcher.instance().markForDownload(event.resourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
public async markAllAttachedResourcesForDownload() {
|
||||||
public componentDidUpdate(prevProps: any, prevState: any) {
|
const resourceIds = await Note.linkedResourceIds(this.state.note.body);
|
||||||
|
await ResourceFetcher.instance().markForDownload(resourceIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: Props, prevState: State) {
|
||||||
if (this.doFocusUpdate_) {
|
if (this.doFocusUpdate_) {
|
||||||
this.doFocusUpdate_ = false;
|
this.doFocusUpdate_ = false;
|
||||||
this.scheduleFocusUpdate();
|
this.scheduleFocusUpdate();
|
||||||
@@ -528,6 +527,11 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
|||||||
void promptRestoreAutosave((drawingData: string) => {
|
void promptRestoreAutosave((drawingData: string) => {
|
||||||
void this.attachNewDrawing(drawingData);
|
void this.attachNewDrawing(drawingData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle automatic resource downloading
|
||||||
|
if (this.state.note?.body && Setting.value('sync.resourceDownloadMode') === 'auto') {
|
||||||
|
void this.markAllAttachedResourcesForDownload();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable opening/closing the side menu with touch gestures
|
// Disable opening/closing the side menu with touch gestures
|
||||||
|
@@ -167,6 +167,8 @@ class NotesScreenComponent extends BaseScreenComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (source === props.notesSource) return;
|
if (source === props.notesSource) return;
|
||||||
|
// For now, search refresh is handled by the search screen.
|
||||||
|
if (props.notesParentType === 'Search') return;
|
||||||
|
|
||||||
let notes: NoteEntity[] = [];
|
let notes: NoteEntity[] = [];
|
||||||
if (props.notesParentType === 'Folder') {
|
if (props.notesParentType === 'Folder') {
|
||||||
@@ -234,14 +236,7 @@ class NotesScreenComponent extends BaseScreenComponent<Props, State> {
|
|||||||
const parent = this.parentItem();
|
const parent = this.parentItem();
|
||||||
const theme = themeStyle(this.props.themeId);
|
const theme = themeStyle(this.props.themeId);
|
||||||
|
|
||||||
const rootStyle = {
|
const rootStyle = this.props.visible ? theme.rootStyle : theme.hiddenRootStyle;
|
||||||
flex: 1,
|
|
||||||
backgroundColor: theme.backgroundColor,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!this.props.visible) {
|
|
||||||
rootStyle.flex = 0.001; // This is a bit of a hack but it seems to work fine - it makes the component invisible but without unmounting it
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = parent ? parent.title : null;
|
const title = parent ? parent.title : null;
|
||||||
if (!parent) {
|
if (!parent) {
|
||||||
|
@@ -10,6 +10,7 @@ import { Dispatch } from 'redux';
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import IconButton from '../../IconButton';
|
import IconButton from '../../IconButton';
|
||||||
import SearchResults from './SearchResults';
|
import SearchResults from './SearchResults';
|
||||||
|
import AccessibleView from '../../accessibility/AccessibleView';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
themeId: number;
|
themeId: number;
|
||||||
@@ -21,7 +22,7 @@ interface Props {
|
|||||||
ftsEnabled: number;
|
ftsEnabled: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = (theme: ThemeStyle) => {
|
const useStyles = (theme: ThemeStyle, visible: boolean) => {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
body: {
|
body: {
|
||||||
@@ -46,13 +47,14 @@ const useStyles = (theme: ThemeStyle) => {
|
|||||||
paddingRight: theme.marginRight,
|
paddingRight: theme.marginRight,
|
||||||
backgroundColor: theme.backgroundColor,
|
backgroundColor: theme.backgroundColor,
|
||||||
},
|
},
|
||||||
|
rootStyle: visible ? theme.rootStyle : theme.hiddenRootStyle,
|
||||||
});
|
});
|
||||||
}, [theme]);
|
}, [theme, visible]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SearchScreenComponent: React.FC<Props> = props => {
|
const SearchScreenComponent: React.FC<Props> = props => {
|
||||||
const theme = themeStyle(props.themeId);
|
const theme = themeStyle(props.themeId);
|
||||||
const styles = useStyles(theme);
|
const styles = useStyles(theme, props.visible);
|
||||||
|
|
||||||
const [query, setQuery] = useState(props.query);
|
const [query, setQuery] = useState(props.query);
|
||||||
|
|
||||||
@@ -79,7 +81,7 @@ const SearchScreenComponent: React.FC<Props> = props => {
|
|||||||
}, [props.dispatch]);
|
}, [props.dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={theme.rootStyle}>
|
<AccessibleView style={styles.rootStyle} inert={!props.visible}>
|
||||||
<ScreenHeader
|
<ScreenHeader
|
||||||
title={_('Search')}
|
title={_('Search')}
|
||||||
folderPickerOptions={{
|
folderPickerOptions={{
|
||||||
@@ -115,7 +117,7 @@ const SearchScreenComponent: React.FC<Props> = props => {
|
|||||||
onHighlightedWordsChange={onHighlightedWordsChange}
|
onHighlightedWordsChange={onHighlightedWordsChange}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</AccessibleView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -404,6 +404,7 @@
|
|||||||
);
|
);
|
||||||
inputPaths = (
|
inputPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Joplin/Pods-Joplin-resources.sh",
|
"${PODS_ROOT}/Target Support Files/Pods-Joplin/Pods-Joplin-resources.sh",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/RNDeviceInfo/RNDeviceInfoPrivacyInfo.bundle",
|
||||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
|
||||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
|
||||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf",
|
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf",
|
||||||
@@ -427,6 +428,7 @@
|
|||||||
);
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNDeviceInfoPrivacyInfo.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf",
|
||||||
@@ -503,13 +505,13 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 124;
|
CURRENT_PROJECT_VERSION = 127;
|
||||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Joplin/Info.plist;
|
INFOPLIST_FILE = Joplin/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 13.1.3;
|
MARKETING_VERSION = 13.1.6;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
@@ -534,12 +536,12 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 124;
|
CURRENT_PROJECT_VERSION = 127;
|
||||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||||
INFOPLIST_FILE = Joplin/Info.plist;
|
INFOPLIST_FILE = Joplin/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 13.1.3;
|
MARKETING_VERSION = 13.1.6;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
@@ -724,14 +726,14 @@
|
|||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 124;
|
CURRENT_PROJECT_VERSION = 127;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||||
MARKETING_VERSION = 13.1.3;
|
MARKETING_VERSION = 13.1.6;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
@@ -762,14 +764,14 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 124;
|
CURRENT_PROJECT_VERSION = 127;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||||
MARKETING_VERSION = 13.1.3;
|
MARKETING_VERSION = 13.1.6;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
@@ -949,7 +949,7 @@ PODS:
|
|||||||
- React-Core
|
- React-Core
|
||||||
- react-native-camera/RN (4.2.1):
|
- react-native-camera/RN (4.2.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-document-picker (9.1.1):
|
- react-native-document-picker (9.3.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-fingerprint-scanner (6.0.0):
|
- react-native-fingerprint-scanner (6.0.0):
|
||||||
- React
|
- React
|
||||||
@@ -997,15 +997,15 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/bridging
|
- ReactCommon/turbomodule/bridging
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- Yoga
|
- Yoga
|
||||||
- react-native-image-resizer (3.0.9):
|
- react-native-image-resizer (3.0.10):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-netinfo (11.3.1):
|
- react-native-netinfo (11.3.2):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-rsa-native (2.0.5):
|
- react-native-rsa-native (2.0.5):
|
||||||
- React
|
- React
|
||||||
- react-native-saf-x (3.1.0):
|
- react-native-saf-x (3.1.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-safe-area-context (4.10.1):
|
- react-native-safe-area-context (4.10.7):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-slider (4.4.4):
|
- react-native-slider (4.4.4):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
@@ -1032,7 +1032,7 @@ PODS:
|
|||||||
- React-Core
|
- React-Core
|
||||||
- react-native-version-info (1.1.1):
|
- react-native-version-info (1.1.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-webview (13.8.6):
|
- react-native-webview (13.10.4):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
@@ -1284,13 +1284,13 @@ PODS:
|
|||||||
- React-utils (= 0.74.1)
|
- React-utils (= 0.74.1)
|
||||||
- rn-fetch-blob (0.12.0):
|
- rn-fetch-blob (0.12.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNCClipboard (1.13.2):
|
- RNCClipboard (1.14.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNCPushNotificationIOS (1.11.0):
|
- RNCPushNotificationIOS (1.11.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNDateTimePicker (8.0.0):
|
- RNDateTimePicker (8.0.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNDeviceInfo (10.13.1):
|
- RNDeviceInfo (10.14.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNExitApp (2.0.0):
|
- RNExitApp (2.0.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
@@ -1298,13 +1298,13 @@ PODS:
|
|||||||
- React-Core
|
- React-Core
|
||||||
- RNFS (2.20.0):
|
- RNFS (2.20.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNLocalize (3.0.6):
|
- RNLocalize (3.1.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNQuickAction (0.3.13):
|
- RNQuickAction (0.3.13):
|
||||||
- React
|
- React
|
||||||
- RNSecureRandom (1.0.1):
|
- RNSecureRandom (1.0.1):
|
||||||
- React
|
- React
|
||||||
- RNShare (10.0.2):
|
- RNShare (10.2.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNVectorIcons (10.1.0):
|
- RNVectorIcons (10.1.0):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
@@ -1327,11 +1327,11 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/bridging
|
- ReactCommon/turbomodule/bridging
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- Yoga
|
- Yoga
|
||||||
- RNZipArchive (6.1.0):
|
- RNZipArchive (6.1.2):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNZipArchive/Core (= 6.1.0)
|
- RNZipArchive/Core (= 6.1.2)
|
||||||
- SSZipArchive (~> 2.2)
|
- SSZipArchive (~> 2.2)
|
||||||
- RNZipArchive/Core (6.1.0):
|
- RNZipArchive/Core (6.1.2):
|
||||||
- React-Core
|
- React-Core
|
||||||
- SSZipArchive (~> 2.2)
|
- SSZipArchive (~> 2.2)
|
||||||
- SocketRocket (0.7.0)
|
- SocketRocket (0.7.0)
|
||||||
@@ -1643,20 +1643,20 @@ SPEC CHECKSUMS:
|
|||||||
React-Mapbuffer: 11029dcd47c5c9e057a4092ab9c2a8d10a496a33
|
React-Mapbuffer: 11029dcd47c5c9e057a4092ab9c2a8d10a496a33
|
||||||
react-native-alarm-notification: 43183613222c563c071f2c726624f9f6f06e605d
|
react-native-alarm-notification: 43183613222c563c071f2c726624f9f6f06e605d
|
||||||
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
|
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
|
||||||
react-native-document-picker: 3599b238843369026201d2ef466df53f77ae0452
|
react-native-document-picker: 5b97e24a7f1a1e4a50a72c540a043f32d29a70a2
|
||||||
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
|
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
|
||||||
react-native-geolocation: fe0562c94eb0b6334f266aea717448dfd9b08cd0
|
react-native-geolocation: fe0562c94eb0b6334f266aea717448dfd9b08cd0
|
||||||
react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06
|
react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06
|
||||||
react-native-image-picker: d3a65af2538ac5407e5329e50f057fb2456f15f8
|
react-native-image-picker: d3a65af2538ac5407e5329e50f057fb2456f15f8
|
||||||
react-native-image-resizer: 669454edae94399b11e49c840e4da14482302293
|
react-native-image-resizer: fd0c333eca55147bd55c5e054cac95dcd0da6814
|
||||||
react-native-netinfo: bdb108d340cdb41875c9ced535977cac6d2ff321
|
react-native-netinfo: 076df4f9b07f6670acf4ce9a75aac8d34c2e2ccc
|
||||||
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
|
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
|
||||||
react-native-saf-x: 7dfb7e614512c82dba2dea3401509e1c44f3d1f9
|
react-native-saf-x: 7dfb7e614512c82dba2dea3401509e1c44f3d1f9
|
||||||
react-native-safe-area-context: dcab599c527c2d7de2d76507a523d20a0b83823d
|
react-native-safe-area-context: 422017db8bcabbada9ad607d010996c56713234c
|
||||||
react-native-slider: 03f213d3ffbf919b16298c7896c1b60101d8e137
|
react-native-slider: 03f213d3ffbf919b16298c7896c1b60101d8e137
|
||||||
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
|
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
|
||||||
react-native-version-info: a106f23009ac0db4ee00de39574eb546682579b9
|
react-native-version-info: a106f23009ac0db4ee00de39574eb546682579b9
|
||||||
react-native-webview: 05bae3a03a1e4f59568dfc05286c0ebf8954106c
|
react-native-webview: 596fb33d67a3cde5a74bf1f6b4c28d3543477fdd
|
||||||
React-nativeconfig: b0073a590774e8b35192fead188a36d1dca23dec
|
React-nativeconfig: b0073a590774e8b35192fead188a36d1dca23dec
|
||||||
React-NativeModulesApple: df46ff3e3de5b842b30b4ca8a6caae6d7c8ab09f
|
React-NativeModulesApple: df46ff3e3de5b842b30b4ca8a6caae6d7c8ab09f
|
||||||
React-perflogger: 3d31e0d1e8ad891e43a09ac70b7b17a79773003a
|
React-perflogger: 3d31e0d1e8ad891e43a09ac70b7b17a79773003a
|
||||||
@@ -1681,19 +1681,19 @@ SPEC CHECKSUMS:
|
|||||||
React-utils: 3285151c9d1e3a28a9586571fc81d521678c196d
|
React-utils: 3285151c9d1e3a28a9586571fc81d521678c196d
|
||||||
ReactCommon: f42444e384d82ab89184aed5d6f3142748b54768
|
ReactCommon: f42444e384d82ab89184aed5d6f3142748b54768
|
||||||
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
||||||
RNCClipboard: 60fed4b71560d7bfe40e9d35dea9762b024da86d
|
RNCClipboard: 0a720adef5ec193aa0e3de24c3977222c7e52a37
|
||||||
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
|
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
|
||||||
RNDateTimePicker: cd42eda5f315fc320f0b359413bd598957f7e601
|
RNDateTimePicker: b6a9b35a785ecbe12b4e7d6de5439d0aa4614146
|
||||||
RNDeviceInfo: 4f9c7cfd6b9db1b05eb919620a001cf35b536423
|
RNDeviceInfo: 59344c19152c4b2b32283005f9737c5c64b42fba
|
||||||
RNExitApp: 00036cabe7bacbb413d276d5520bf74ba39afa6a
|
RNExitApp: 00036cabe7bacbb413d276d5520bf74ba39afa6a
|
||||||
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
|
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
|
||||||
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
||||||
RNLocalize: 4222a3756cdbe2dc9a5bdf445765a4d2572107cb
|
RNLocalize: e8694475db034bf601e17bd3dfa8986565e769eb
|
||||||
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
|
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
|
||||||
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
|
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
|
||||||
RNShare: 859ff710211285676b0bcedd156c12437ea1d564
|
RNShare: 0fad69ae2d71de9d1f7b9a43acf876886a6cb99c
|
||||||
RNVectorIcons: 2a2f79274248390b80684ea3c4400bd374a15c90
|
RNVectorIcons: 2a2f79274248390b80684ea3c4400bd374a15c90
|
||||||
RNZipArchive: ef9451b849c45a29509bf44e65b788829ab07801
|
RNZipArchive: 6d736ee4e286dbbd9d81206b7a4da355596ca04a
|
||||||
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
|
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
|
||||||
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
|
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
|
||||||
Yoga: 348f8b538c3ed4423eb58a8e5730feec50bce372
|
Yoga: 348f8b538c3ed4423eb58a8e5730feec50bce372
|
||||||
|
@@ -6,10 +6,10 @@
|
|||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
<array>
|
<array>
|
||||||
<string>C617.1</string>
|
<string>35F9.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -22,10 +22,10 @@
|
|||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
<array>
|
<array>
|
||||||
<string>35F9.1</string>
|
<string>C617.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
<array>
|
<array>
|
||||||
|
<string>85F4.1</string>
|
||||||
<string>E174.1</string>
|
<string>E174.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
|
@@ -49,7 +49,7 @@
|
|||||||
"react-native-camera": "4.2.1",
|
"react-native-camera": "4.2.1",
|
||||||
"react-native-device-info": "10.14.0",
|
"react-native-device-info": "10.14.0",
|
||||||
"react-native-dialogbox": "0.6.10",
|
"react-native-dialogbox": "0.6.10",
|
||||||
"react-native-document-picker": "9.2.0",
|
"react-native-document-picker": "9.3.0",
|
||||||
"react-native-dropdownalert": "5.1.0",
|
"react-native-dropdownalert": "5.1.0",
|
||||||
"react-native-exit-app": "2.0.0",
|
"react-native-exit-app": "2.0.0",
|
||||||
"react-native-file-viewer": "2.1.5",
|
"react-native-file-viewer": "2.1.5",
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
"react-native-popup-menu": "0.16.1",
|
"react-native-popup-menu": "0.16.1",
|
||||||
"react-native-quick-actions": "0.3.13",
|
"react-native-quick-actions": "0.3.13",
|
||||||
"react-native-rsa-native": "2.0.5",
|
"react-native-rsa-native": "2.0.5",
|
||||||
"react-native-safe-area-context": "4.10.5",
|
"react-native-safe-area-context": "4.10.7",
|
||||||
"react-native-securerandom": "1.0.1",
|
"react-native-securerandom": "1.0.1",
|
||||||
"react-native-share": "10.2.1",
|
"react-native-share": "10.2.1",
|
||||||
"react-native-sqlite-storage": "6.0.1",
|
"react-native-sqlite-storage": "6.0.1",
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
"react-redux": "8.1.3",
|
"react-redux": "8.1.3",
|
||||||
"redux": "4.2.1",
|
"redux": "4.2.1",
|
||||||
"rn-fetch-blob": "0.12.0",
|
"rn-fetch-blob": "0.12.0",
|
||||||
"stream": "0.0.2",
|
"stream": "0.0.3",
|
||||||
"stream-browserify": "3.0.0",
|
"stream-browserify": "3.0.0",
|
||||||
"string-natural-compare": "3.0.1",
|
"string-natural-compare": "3.0.1",
|
||||||
"tar-stream": "3.1.7",
|
"tar-stream": "3.1.7",
|
||||||
@@ -90,18 +90,18 @@
|
|||||||
"@babel/runtime": "7.24.7",
|
"@babel/runtime": "7.24.7",
|
||||||
"@joplin/tools": "~3.1",
|
"@joplin/tools": "~3.1",
|
||||||
"@js-draw/material-icons": "1.20.3",
|
"@js-draw/material-icons": "1.20.3",
|
||||||
"@react-native/babel-preset": "0.74.84",
|
"@react-native/babel-preset": "0.74.85",
|
||||||
"@react-native/metro-config": "0.74.84",
|
"@react-native/metro-config": "0.74.85",
|
||||||
"@sqlite.org/sqlite-wasm": "3.46.0-build2",
|
"@sqlite.org/sqlite-wasm": "3.46.0-build2",
|
||||||
"@testing-library/jest-native": "5.4.3",
|
"@testing-library/jest-native": "5.4.3",
|
||||||
"@testing-library/react-native": "12.3.3",
|
"@testing-library/react-native": "12.3.3",
|
||||||
"@tsconfig/react-native": "2.0.2",
|
"@tsconfig/react-native": "2.0.2",
|
||||||
"@types/fs-extra": "11.0.4",
|
"@types/fs-extra": "11.0.4",
|
||||||
"@types/jest": "29.5.8",
|
"@types/jest": "29.5.12",
|
||||||
"@types/react": "18.3.3",
|
"@types/react": "18.3.3",
|
||||||
"@types/react-native": "0.70.6",
|
"@types/react-native": "0.70.6",
|
||||||
"@types/react-redux": "7.1.33",
|
"@types/react-redux": "7.1.33",
|
||||||
"@types/serviceworker": "0.0.88",
|
"@types/serviceworker": "0.0.89",
|
||||||
"@types/tar-stream": "3.1.3",
|
"@types/tar-stream": "3.1.3",
|
||||||
"babel-jest": "29.7.0",
|
"babel-jest": "29.7.0",
|
||||||
"babel-loader": "9.1.3",
|
"babel-loader": "9.1.3",
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
"jetifier": "2.0.0",
|
"jetifier": "2.0.0",
|
||||||
"js-draw": "1.20.3",
|
"js-draw": "1.20.3",
|
||||||
"jsdom": "24.1.0",
|
"jsdom": "24.1.0",
|
||||||
"nodemon": "3.0.3",
|
"nodemon": "3.1.7",
|
||||||
"punycode": "2.3.1",
|
"punycode": "2.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native-web": "0.19.12",
|
"react-native-web": "0.19.12",
|
||||||
@@ -122,10 +122,10 @@
|
|||||||
"sharp": "0.33.4",
|
"sharp": "0.33.4",
|
||||||
"sqlite3": "5.1.6",
|
"sqlite3": "5.1.6",
|
||||||
"timers-browserify": "2.0.12",
|
"timers-browserify": "2.0.12",
|
||||||
"ts-jest": "29.1.1",
|
"ts-jest": "29.1.5",
|
||||||
"ts-loader": "9.5.1",
|
"ts-loader": "9.5.1",
|
||||||
"ts-node": "10.9.2",
|
"ts-node": "10.9.2",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.4.5",
|
||||||
"uglify-js": "3.17.4",
|
"uglify-js": "3.17.4",
|
||||||
"url-loader": "4.1.1",
|
"url-loader": "4.1.1",
|
||||||
"webpack": "5.84.0",
|
"webpack": "5.84.0",
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
hash:"d7216e17c44aad8a0b1c3cf2e01a0135", files: {
|
hash:"addedbac5508e231800fe0f97c326075", files: {
|
||||||
'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },
|
'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },
|
||||||
'highlight.js/atom-one-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' },
|
'highlight.js/atom-one-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' },
|
||||||
'katex/fonts/KaTeX_AMS-Regular.woff2': { data: require('./katex/fonts/KaTeX_AMS-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
|
'katex/fonts/KaTeX_AMS-Regular.woff2': { data: require('./katex/fonts/KaTeX_AMS-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
|
||||||
|
@@ -1 +1 @@
|
|||||||
module.exports = {"hash":"d7216e17c44aad8a0b1c3cf2e01a0135","files":["highlight.js/atom-one-dark-reasonable.css","highlight.js/atom-one-light.css","katex/fonts/KaTeX_AMS-Regular.woff2","katex/fonts/KaTeX_Caligraphic-Bold.woff2","katex/fonts/KaTeX_Caligraphic-Regular.woff2","katex/fonts/KaTeX_Fraktur-Bold.woff2","katex/fonts/KaTeX_Fraktur-Regular.woff2","katex/fonts/KaTeX_Main-Bold.woff2","katex/fonts/KaTeX_Main-BoldItalic.woff2","katex/fonts/KaTeX_Main-Italic.woff2","katex/fonts/KaTeX_Main-Regular.woff2","katex/fonts/KaTeX_Math-BoldItalic.woff2","katex/fonts/KaTeX_Math-Italic.woff2","katex/fonts/KaTeX_SansSerif-Bold.woff2","katex/fonts/KaTeX_SansSerif-Italic.woff2","katex/fonts/KaTeX_SansSerif-Regular.woff2","katex/fonts/KaTeX_Script-Regular.woff2","katex/fonts/KaTeX_Size1-Regular.woff2","katex/fonts/KaTeX_Size2-Regular.woff2","katex/fonts/KaTeX_Size3-Regular.woff2","katex/fonts/KaTeX_Size4-Regular.woff2","katex/fonts/KaTeX_Typewriter-Regular.woff2","katex/katex.css","mermaid/mermaid.min.js","mermaid/mermaid_render.js"]}
|
module.exports = {"hash":"addedbac5508e231800fe0f97c326075","files":["highlight.js/atom-one-dark-reasonable.css","highlight.js/atom-one-light.css","katex/fonts/KaTeX_AMS-Regular.woff2","katex/fonts/KaTeX_Caligraphic-Bold.woff2","katex/fonts/KaTeX_Caligraphic-Regular.woff2","katex/fonts/KaTeX_Fraktur-Bold.woff2","katex/fonts/KaTeX_Fraktur-Regular.woff2","katex/fonts/KaTeX_Main-Bold.woff2","katex/fonts/KaTeX_Main-BoldItalic.woff2","katex/fonts/KaTeX_Main-Italic.woff2","katex/fonts/KaTeX_Main-Regular.woff2","katex/fonts/KaTeX_Math-BoldItalic.woff2","katex/fonts/KaTeX_Math-Italic.woff2","katex/fonts/KaTeX_SansSerif-Bold.woff2","katex/fonts/KaTeX_SansSerif-Italic.woff2","katex/fonts/KaTeX_SansSerif-Regular.woff2","katex/fonts/KaTeX_Script-Regular.woff2","katex/fonts/KaTeX_Size1-Regular.woff2","katex/fonts/KaTeX_Size2-Regular.woff2","katex/fonts/KaTeX_Size3-Regular.woff2","katex/fonts/KaTeX_Size4-Regular.woff2","katex/fonts/KaTeX_Typewriter-Regular.woff2","katex/katex.css","mermaid/mermaid.min.js","mermaid/mermaid_render.js"]}
|
File diff suppressed because one or more lines are too long
@@ -308,6 +308,10 @@ const appReducer = (state = appDefaultState, action: any) => {
|
|||||||
|
|
||||||
newState.selectedNoteHash = '';
|
newState.selectedNoteHash = '';
|
||||||
|
|
||||||
|
if (action.routeName === 'Search') {
|
||||||
|
newState.notesParentType = 'Search';
|
||||||
|
}
|
||||||
|
|
||||||
if ('noteId' in action) {
|
if ('noteId' in action) {
|
||||||
newState.selectedNoteIds = action.noteId ? [action.noteId] : [];
|
newState.selectedNoteIds = action.noteId ? [action.noteId] : [];
|
||||||
}
|
}
|
||||||
@@ -344,6 +348,8 @@ const appReducer = (state = appDefaultState, action: any) => {
|
|||||||
|
|
||||||
newState.route = action;
|
newState.route = action;
|
||||||
newState.historyCanGoBack = !!navHistory.length;
|
newState.historyCanGoBack = !!navHistory.length;
|
||||||
|
|
||||||
|
logger.debug('Navigated to route:', newState.route?.routeName, 'with notesParentType:', newState.notesParentType);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@@ -3,15 +3,18 @@ const execa = require('execa');
|
|||||||
module.exports = async function() {
|
module.exports = async function() {
|
||||||
if (process.platform !== 'darwin') return Promise.resolve();
|
if (process.platform !== 'darwin') return Promise.resolve();
|
||||||
|
|
||||||
if (!process.env.RUN_POD_INSTALL) {
|
// 2024-10-11: Seems running `pod install` is not so slow anymore, and at least not the
|
||||||
// We almost never need to run `pod install` either because it has
|
// bottleneck when running `yarn install` so we should run it every time.
|
||||||
// already been done, or because we are not building the iOS app, yet
|
|
||||||
// it's taking most of the build time (3 min out of the 5 min needed to
|
// if (!process.env.RUN_POD_INSTALL) {
|
||||||
// build the entire monorepo). If it needs to be ran, XCode will tell us
|
// // We almost never need to run `pod install` either because it has
|
||||||
// anyway.
|
// // already been done, or because we are not building the iOS app, yet
|
||||||
console.warn('**Not** running `pod install` - set `RUN_POD_INSTALL` to `1` to do so');
|
// // it's taking most of the build time (3 min out of the 5 min needed to
|
||||||
return Promise.resolve();
|
// // build the entire monorepo). If it needs to be ran, XCode will tell us
|
||||||
}
|
// // anyway.
|
||||||
|
// console.warn('**Not** running `pod install` - set `RUN_POD_INSTALL` to `1` to do so');
|
||||||
|
// return Promise.resolve();
|
||||||
|
// }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const promise = execa('pod', ['install'], { cwd: `${__dirname}/../ios` });
|
const promise = execa('pod', ['install'], { cwd: `${__dirname}/../ios` });
|
||||||
|
@@ -1,15 +1,7 @@
|
|||||||
import { screen, waitFor } from '@testing-library/react-native';
|
import getWebViewWindowById from './getWebViewWindowById';
|
||||||
|
|
||||||
const getWebViewDomById = async (id: string): Promise<Document> => {
|
const getWebViewDomById = async (id: string): Promise<Document> => {
|
||||||
const webviewContent = await screen.findByTestId(id);
|
return (await getWebViewWindowById(id)).document;
|
||||||
expect(webviewContent).toBeVisible();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(!!webviewContent.props.document).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return the composite ExtendedWebView component
|
|
||||||
// See https://callstack.github.io/react-native-testing-library/docs/advanced/testing-env#tree-navigation
|
|
||||||
return webviewContent.props.document;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getWebViewDomById;
|
export default getWebViewDomById;
|
||||||
|
15
packages/app-mobile/utils/testing/getWebViewWindowById.ts
Normal file
15
packages/app-mobile/utils/testing/getWebViewWindowById.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { screen, waitFor } from '@testing-library/react-native';
|
||||||
|
|
||||||
|
const getWebViewWindowById = async (id: string): Promise<Window> => {
|
||||||
|
const webviewContent = await screen.findByTestId(id);
|
||||||
|
expect(webviewContent).toBeVisible();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(!!webviewContent.props.window).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const webviewWindow = webviewContent.props.window;
|
||||||
|
return webviewWindow;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getWebViewWindowById;
|
@@ -15,7 +15,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/yargs": "17.0.32",
|
"@types/yargs": "17.0.32",
|
||||||
"ts-node": "10.9.2",
|
"ts-node": "10.9.2",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.4.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@joplin/utils": "~3.1",
|
"@joplin/utils": "~3.1",
|
||||||
|
@@ -6,6 +6,7 @@ import { classHighlighter } from '@lezer/highlight';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
EditorView, drawSelection, highlightSpecialChars, ViewUpdate, Command, rectangularSelection,
|
EditorView, drawSelection, highlightSpecialChars, ViewUpdate, Command, rectangularSelection,
|
||||||
|
dropCursor,
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { history, undoDepth, redoDepth, standardKeymap } from '@codemirror/commands';
|
import { history, undoDepth, redoDepth, standardKeymap } from '@codemirror/commands';
|
||||||
|
|
||||||
@@ -253,6 +254,8 @@ const createEditor = (
|
|||||||
|
|
||||||
// Apply styles to entire lines (block-display decorations)
|
// Apply styles to entire lines (block-display decorations)
|
||||||
decoratorExtension,
|
decoratorExtension,
|
||||||
|
dropCursor(),
|
||||||
|
|
||||||
biDirectionalTextExtension,
|
biDirectionalTextExtension,
|
||||||
|
|
||||||
// Adds additional CSS classes to tokens (the default CSS classes are
|
// Adds additional CSS classes to tokens (the default CSS classes are
|
||||||
|
@@ -84,6 +84,18 @@ const tableDelimiterDecoration = Decoration.line({
|
|||||||
attributes: { class: 'cm-tableDelimiter' },
|
attributes: { class: 'cm-tableDelimiter' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const orderedListDecoration = Decoration.line({
|
||||||
|
attributes: { class: 'cm-orderedList' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const unorderedListDecoration = Decoration.line({
|
||||||
|
attributes: { class: 'cm-unorderedList' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const listItemDecoration = Decoration.line({
|
||||||
|
attributes: { class: 'cm-listItem' },
|
||||||
|
});
|
||||||
|
|
||||||
const horizontalRuleDecoration = Decoration.mark({
|
const horizontalRuleDecoration = Decoration.mark({
|
||||||
attributes: { class: 'cm-hr' },
|
attributes: { class: 'cm-hr' },
|
||||||
});
|
});
|
||||||
@@ -97,6 +109,10 @@ const nodeNameToLineDecoration: Record<string, Decoration> = {
|
|||||||
'CodeBlock': codeBlockDecoration,
|
'CodeBlock': codeBlockDecoration,
|
||||||
'BlockMath': mathBlockDecoration,
|
'BlockMath': mathBlockDecoration,
|
||||||
'Blockquote': blockQuoteDecoration,
|
'Blockquote': blockQuoteDecoration,
|
||||||
|
'OrderedList': orderedListDecoration,
|
||||||
|
'BulletList': unorderedListDecoration,
|
||||||
|
|
||||||
|
'ListItem': listItemDecoration,
|
||||||
|
|
||||||
'SetextHeading1': header1LineDecoration,
|
'SetextHeading1': header1LineDecoration,
|
||||||
'ATXHeading1': header1LineDecoration,
|
'ATXHeading1': header1LineDecoration,
|
||||||
@@ -122,6 +138,14 @@ const nodeNameToMarkDecoration: Record<string, Decoration> = {
|
|||||||
'TaskMarker': taskMarkerDecoration,
|
'TaskMarker': taskMarkerDecoration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const multilineNodes = {
|
||||||
|
'FencedCode': true,
|
||||||
|
'CodeBlock': true,
|
||||||
|
'BlockMath': true,
|
||||||
|
'Blockquote': true,
|
||||||
|
'OrderedList': true,
|
||||||
|
'BulletList': true,
|
||||||
|
};
|
||||||
|
|
||||||
type DecorationDescription = { pos: number; length: number; decoration: Decoration };
|
type DecorationDescription = { pos: number; length: number; decoration: Decoration };
|
||||||
|
|
||||||
@@ -179,8 +203,8 @@ const computeDecorations = (view: EditorView) => {
|
|||||||
addDecorationToRange(viewFrom, viewTo, decoration);
|
addDecorationToRange(viewFrom, viewTo, decoration);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only block decorations will have differing first and last lines
|
// Only certain block decorations will have differing first and last lines
|
||||||
if (blockDecorated) {
|
if (blockDecorated && multilineNodes.hasOwnProperty(node.name)) {
|
||||||
// Allow different styles for the first, last lines in a block.
|
// Allow different styles for the first, last lines in a block.
|
||||||
if (viewFrom === node.from) {
|
if (viewFrom === node.from) {
|
||||||
addDecorationToLines(viewFrom, viewFrom, regionStartDecoration);
|
addDecorationToLines(viewFrom, viewFrom, regionStartDecoration);
|
||||||
|
@@ -232,4 +232,19 @@ describe('markdownCommands.toggleList', () => {
|
|||||||
);
|
);
|
||||||
expect(editor.state.selection.main.from).toBe(preSubListText.length);
|
expect(editor.state.selection.main.from).toBe(preSubListText.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not treat a list of IP addresses as a numbered list', async () => {
|
||||||
|
const initialDocText = '192.168.1.1. This\n127.0.0.1. is\n0.0.0.0. a list';
|
||||||
|
|
||||||
|
const editor = await createTestEditor(
|
||||||
|
initialDocText,
|
||||||
|
EditorSelection.range(0, initialDocText.length),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
toggleList(ListType.UnorderedList)(editor);
|
||||||
|
expect(editor.state.doc.toString()).toBe(
|
||||||
|
'- 192.168.1.1. This\n- 127.0.0.1. is\n- 0.0.0.0. a list',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -132,9 +132,9 @@ export const toggleList = (listType: ListType): Command => {
|
|||||||
// RegExps for different list types. The regular expressions MUST
|
// RegExps for different list types. The regular expressions MUST
|
||||||
// be mutually exclusive.
|
// be mutually exclusive.
|
||||||
// `(?!\[[ xX]+\])` means "not followed by [x] or [ ]".
|
// `(?!\[[ xX]+\])` means "not followed by [x] or [ ]".
|
||||||
const bulletedRegex = /^\s*([-*])\s(?!\[[ xX]+\])/;
|
const bulletedRegex = /^\s*([-*])\s(?!\[[ xX]+\]\s)/;
|
||||||
const checklistRegex = /^\s*[-*]\s\[[ xX]+\]\s?/;
|
const checklistRegex = /^\s*[-*]\s\[[ xX]+\]\s/;
|
||||||
const numberedRegex = /^\s*\d+\.\s?/;
|
const numberedRegex = /^\s*\d+\.\s/;
|
||||||
|
|
||||||
const listRegexes: Record<ListType, RegExp> = {
|
const listRegexes: Record<ListType, RegExp> = {
|
||||||
[ListType.OrderedList]: numberedRegex,
|
[ListType.OrderedList]: numberedRegex,
|
||||||
|
10
packages/editor/CodeMirror/theme.ts
vendored
10
packages/editor/CodeMirror/theme.ts
vendored
@@ -79,6 +79,10 @@ const createTheme = (theme: EditorTheme): Extension[] => {
|
|||||||
// be at least this specific.
|
// be at least this specific.
|
||||||
const selectionBackgroundSelector = '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground';
|
const selectionBackgroundSelector = '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground';
|
||||||
|
|
||||||
|
// Matches the editor only when there are no gutters (e.g. line numbers) added by
|
||||||
|
// plugins
|
||||||
|
const editorNoGuttersSelector = '&:not(:has(> .cm-scroller > .cm-gutters))';
|
||||||
|
|
||||||
const baseHeadingStyle = {
|
const baseHeadingStyle = {
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
fontFamily: theme.fontFamily,
|
fontFamily: theme.fontFamily,
|
||||||
@@ -180,6 +184,12 @@ const createTheme = (theme: EditorTheme): Extension[] => {
|
|||||||
marginRight: 'auto',
|
marginRight: 'auto',
|
||||||
} : undefined,
|
} : undefined,
|
||||||
|
|
||||||
|
// Allows editor content to be left-aligned with the toolbar on desktop.
|
||||||
|
// See https://github.com/laurent22/joplin/issues/11279
|
||||||
|
[`${editorNoGuttersSelector} .cm-line`]: theme.isDesktop ? {
|
||||||
|
paddingLeft: 0,
|
||||||
|
} : undefined,
|
||||||
|
|
||||||
// Override the default URL style when the URL is within a link
|
// Override the default URL style when the URL is within a link
|
||||||
'& .tok-url.tok-link, & .tok-link.tok-meta, & .tok-link.tok-string': {
|
'& .tok-url.tok-link, & .tok-link.tok-meta, & .tok-link.tok-string': {
|
||||||
opacity: 0.6,
|
opacity: 0.6,
|
||||||
|
@@ -16,30 +16,30 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@joplin/lib": "~3.1",
|
"@joplin/lib": "~3.1",
|
||||||
"@testing-library/react-hooks": "8.0.1",
|
"@testing-library/react-hooks": "8.0.1",
|
||||||
"@types/jest": "29.5.8",
|
"@types/jest": "29.5.12",
|
||||||
"@types/react": "18.3.3",
|
"@types/react": "18.3.3",
|
||||||
"@types/react-redux": "7.1.33",
|
"@types/react-redux": "7.1.33",
|
||||||
"@types/styled-components": "5.1.32",
|
"@types/styled-components": "5.1.32",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-environment-jsdom": "29.7.0",
|
"jest-environment-jsdom": "29.7.0",
|
||||||
"ts-jest": "29.1.1",
|
"ts-jest": "29.1.5",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.4.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "6.18.0",
|
"@codemirror/autocomplete": "6.13.0",
|
||||||
"@codemirror/commands": "6.6.1",
|
"@codemirror/commands": "6.3.3",
|
||||||
"@codemirror/lang-html": "6.4.9",
|
"@codemirror/lang-html": "6.4.8",
|
||||||
"@codemirror/lang-markdown": "6.2.5",
|
"@codemirror/lang-markdown": "6.2.4",
|
||||||
"@codemirror/language": "6.10.2",
|
"@codemirror/language": "6.10.1",
|
||||||
"@codemirror/language-data": "6.3.1",
|
"@codemirror/language-data": "6.3.1",
|
||||||
"@codemirror/legacy-modes": "6.4.1",
|
"@codemirror/legacy-modes": "6.3.3",
|
||||||
"@codemirror/lint": "6.8.1",
|
"@codemirror/lint": "6.5.0",
|
||||||
"@codemirror/search": "6.5.6",
|
"@codemirror/search": "6.5.6",
|
||||||
"@codemirror/state": "6.4.1",
|
"@codemirror/state": "6.4.1",
|
||||||
"@codemirror/view": "6.33.0",
|
"@codemirror/view": "6.26.3",
|
||||||
"@lezer/common": "1.2.1",
|
"@lezer/common": "1.2.1",
|
||||||
"@lezer/highlight": "1.2.1",
|
"@lezer/highlight": "1.2.0",
|
||||||
"@lezer/markdown": "1.3.1",
|
"@lezer/markdown": "1.2.0",
|
||||||
"@replit/codemirror-vim": "6.2.0"
|
"@replit/codemirror-vim": "6.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -90,6 +90,7 @@ export interface ContentScriptData {
|
|||||||
// Intended to correspond with https://codemirror.net/docs/ref/#state.Transaction%5EuserEvent
|
// Intended to correspond with https://codemirror.net/docs/ref/#state.Transaction%5EuserEvent
|
||||||
export enum UserEventSource {
|
export enum UserEventSource {
|
||||||
Paste = 'input.paste',
|
Paste = 'input.paste',
|
||||||
|
Drop = 'input.drop',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorControl {
|
export interface EditorControl {
|
||||||
|
@@ -45,16 +45,16 @@
|
|||||||
"entities": "2.2.0"
|
"entities": "2.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "29.5.8",
|
"@types/jest": "29.5.12",
|
||||||
"@types/node": "18.19.39",
|
"@types/node": "18.19.39",
|
||||||
"@typescript-eslint/eslint-plugin": "6.8.0",
|
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||||
"@typescript-eslint/parser": "6.8.0",
|
"@typescript-eslint/parser": "6.21.0",
|
||||||
"coveralls": "3.1.1",
|
"coveralls": "3.1.1",
|
||||||
"eslint": "8.52.0",
|
"eslint": "8.57.0",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"prettier": "3.0.3",
|
"prettier": "3.3.2",
|
||||||
"ts-jest": "29.1.1",
|
"ts-jest": "29.1.5",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.4.5"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "ts-jest",
|
"preset": "ts-jest",
|
||||||
|
@@ -7,13 +7,15 @@ export const isInsideContainer = (node: any, className: string): boolean => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface CancelEvent { cancelled: boolean }
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
export const waitForElement = async (parent: any, id: string): Promise<any> => {
|
export const waitForElement = async (parent: any, id: string, cancelEvent?: CancelEvent): Promise<any> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const iid = setInterval(() => {
|
const iid = setInterval(() => {
|
||||||
try {
|
try {
|
||||||
const element = parent.getElementById(id);
|
const element = parent.getElementById(id);
|
||||||
if (element) {
|
if (element || cancelEvent?.cancelled) {
|
||||||
clearInterval(iid);
|
clearInterval(iid);
|
||||||
resolve(element);
|
resolve(element);
|
||||||
}
|
}
|
||||||
|
@@ -926,6 +926,18 @@ const builtInMetadata = (Setting: typeof SettingType) => {
|
|||||||
'markdown.plugin.insert': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ++insert++ syntax')}${wysiwygYes}` },
|
'markdown.plugin.insert': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ++insert++ syntax')}${wysiwygYes}` },
|
||||||
'markdown.plugin.multitable': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable multimarkdown table extension')}${wysiwygNo}` },
|
'markdown.plugin.multitable': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable multimarkdown table extension')}${wysiwygNo}` },
|
||||||
|
|
||||||
|
// For now, applies only to the Markdown viewer
|
||||||
|
'renderer.fileUrls': {
|
||||||
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
|
value: false,
|
||||||
|
type: SettingItemType.Bool,
|
||||||
|
section: 'markdownPlugins',
|
||||||
|
public: true,
|
||||||
|
appTypes: [AppType.Desktop],
|
||||||
|
label: () => `${_('Enable file:// URLs for images and videos')}${wysiwygYes}`,
|
||||||
|
},
|
||||||
|
|
||||||
// Tray icon (called AppIndicator) doesn't work in Ubuntu
|
// Tray icon (called AppIndicator) doesn't work in Ubuntu
|
||||||
// http://www.webupd8.org/2017/04/fix-appindicator-not-working-for.html
|
// http://www.webupd8.org/2017/04/fix-appindicator-not-working-for.html
|
||||||
// Might be fixed in Electron 18.x but no non-beta release yet. So for now
|
// Might be fixed in Electron 18.x but no non-beta release yet. So for now
|
||||||
@@ -1127,7 +1139,7 @@ const builtInMetadata = (Setting: typeof SettingType) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
autoUpdateEnabled: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'application', public: false, appTypes: [AppType.Desktop], label: () => _('Automatically check for updates') },
|
autoUpdateEnabled: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'application', public: platform !== 'linux', appTypes: [AppType.Desktop], label: () => _('Automatically check for updates') },
|
||||||
'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Desktop], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/help/about/prereleases') },
|
'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Desktop], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/help/about/prereleases') },
|
||||||
|
|
||||||
'autoUploadCrashDumps': {
|
'autoUploadCrashDumps': {
|
||||||
@@ -1576,6 +1588,20 @@ const builtInMetadata = (Setting: typeof SettingType) => {
|
|||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'featureFlag.linuxKeychain': {
|
||||||
|
value: false,
|
||||||
|
type: SettingItemType.Bool,
|
||||||
|
public: true,
|
||||||
|
storage: SettingStorage.File,
|
||||||
|
appTypes: [AppType.Desktop],
|
||||||
|
label: () => 'Enable keychain support',
|
||||||
|
description: () => 'This is an experimental setting to enable keychain support on Linux',
|
||||||
|
show: () => shim.isLinux(),
|
||||||
|
section: 'general',
|
||||||
|
isGlobal: true,
|
||||||
|
advanced: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
// 'featureFlag.syncAccurateTimestamps': {
|
// 'featureFlag.syncAccurateTimestamps': {
|
||||||
// value: false,
|
// value: false,
|
||||||
|
@@ -18,7 +18,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/react-hooks": "8.0.1",
|
"@testing-library/react-hooks": "8.0.1",
|
||||||
"@types/fs-extra": "11.0.4",
|
"@types/fs-extra": "11.0.4",
|
||||||
"@types/jest": "29.5.8",
|
"@types/jest": "29.5.12",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/markdown-it": "13.0.8",
|
"@types/markdown-it": "13.0.8",
|
||||||
"@types/mustache": "4.2.5",
|
"@types/mustache": "4.2.5",
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"react-test-renderer": "18.3.1",
|
"react-test-renderer": "18.3.1",
|
||||||
"sharp": "0.33.4",
|
"sharp": "0.33.4",
|
||||||
"tesseract.js": "5.1.0",
|
"tesseract.js": "5.1.0",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.4.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/css-tools": "4.3.3",
|
"@adobe/css-tools": "4.3.3",
|
||||||
|
@@ -34,6 +34,13 @@ export async function loadKeychainServiceAndSettings(keychainServiceDrivers: Key
|
|||||||
Setting.setKeychainService(KeychainService.instance());
|
Setting.setKeychainService(KeychainService.instance());
|
||||||
await Setting.load();
|
await Setting.load();
|
||||||
|
|
||||||
|
// Using Linux with the keychain has been observed to cause all secure settings to be lost
|
||||||
|
// on Fedora 40 + GNOME. (This may have been related to running multiple Joplin instances).
|
||||||
|
// For now, make saving to the keychain opt-in until more feedback is received.
|
||||||
|
if (shim.isLinux() && !Setting.value('featureFlag.linuxKeychain')) {
|
||||||
|
KeychainService.instance().readOnly = true;
|
||||||
|
}
|
||||||
|
|
||||||
// This is part of the migration to the new sync target info. It needs to be
|
// This is part of the migration to the new sync target info. It needs to be
|
||||||
// set as early as possible since it's used to tell if E2EE is enabled, it
|
// set as early as possible since it's used to tell if E2EE is enabled, it
|
||||||
// contains the master keys, etc. Once it has been set, it becomes a noop
|
// contains the master keys, etc. Once it has been set, it becomes a noop
|
||||||
|
@@ -4,7 +4,7 @@ import Logger from '@joplin/utils/Logger';
|
|||||||
import KvStore from '../KvStore';
|
import KvStore from '../KvStore';
|
||||||
import Setting from '../../models/Setting';
|
import Setting from '../../models/Setting';
|
||||||
|
|
||||||
const logger = Logger.create('KeychainServiceDriver.node');
|
const logger = Logger.create('KeychainServiceDriver.electron');
|
||||||
|
|
||||||
const canUseSafeStorage = () => {
|
const canUseSafeStorage = () => {
|
||||||
return !!shim.electronBridge?.()?.safeStorage?.isEncryptionAvailable();
|
return !!shim.electronBridge?.()?.safeStorage?.isEncryptionAvailable();
|
||||||
|
@@ -147,6 +147,20 @@ export default class JoplinSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Gets setting values (only applies to setting you registered from your plugin)
|
||||||
|
*/
|
||||||
|
public async values(keys: string[] | string): Promise<Record<string, unknown>> {
|
||||||
|
if (typeof keys === 'string') keys = [keys];
|
||||||
|
const output: Record<string, unknown> = {};
|
||||||
|
for (const key of keys) {
|
||||||
|
output[key] = Setting.value(getPluginNamespacedSettingKey(this.plugin_.id, key));
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use joplin.settings.values()
|
||||||
|
*
|
||||||
* Gets a setting value (only applies to setting you registered from your plugin)
|
* Gets a setting value (only applies to setting you registered from your plugin)
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
|
@@ -286,6 +286,7 @@ async function switchClient(id: number, options: any = null) {
|
|||||||
BaseItem.encryptionService_ = encryptionServices_[id];
|
BaseItem.encryptionService_ = encryptionServices_[id];
|
||||||
Resource.encryptionService_ = encryptionServices_[id];
|
Resource.encryptionService_ = encryptionServices_[id];
|
||||||
BaseItem.revisionService_ = revisionServices_[id];
|
BaseItem.revisionService_ = revisionServices_[id];
|
||||||
|
ResourceFetcher.instance_ = resourceFetchers_[id];
|
||||||
|
|
||||||
await Setting.reset();
|
await Setting.reset();
|
||||||
Setting.settingFilename = settingFilename(id);
|
Setting.settingFilename = settingFilename(id);
|
||||||
@@ -1092,12 +1093,41 @@ export const mockMobilePlatform = (platform: string) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Waits for callback to not throw. Similar to react-native-testing-library's waitFor, but works better
|
||||||
|
// with Joplin's mix of real and fake Jest timers.
|
||||||
|
const realSetTimeout = setTimeout;
|
||||||
|
export const waitFor = async (callback: ()=> Promise<void>) => {
|
||||||
|
const timeout = 10_000;
|
||||||
|
const startTime = performance.now();
|
||||||
|
let passed = false;
|
||||||
|
let lastError: Error|null = null;
|
||||||
|
|
||||||
|
while (!passed && performance.now() - startTime < timeout) {
|
||||||
|
try {
|
||||||
|
await callback();
|
||||||
|
passed = true;
|
||||||
|
lastError = null;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
|
||||||
|
await new Promise<void>(resolve => {
|
||||||
|
realSetTimeout(() => resolve(), 10);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const runWithFakeTimers = async (callback: ()=> Promise<void>) => {
|
export const runWithFakeTimers = async (callback: ()=> Promise<void>) => {
|
||||||
if (typeof jest === 'undefined') {
|
if (typeof jest === 'undefined') {
|
||||||
throw new Error('Fake timers are only supported in jest.');
|
throw new Error('Fake timers are only supported in jest.');
|
||||||
}
|
}
|
||||||
|
|
||||||
jest.useFakeTimers();
|
// advanceTimers: Needed by Joplin's database driver
|
||||||
|
jest.useFakeTimers({ advanceTimers: true });
|
||||||
|
|
||||||
// The shim.setTimeout and similar functions need to be changed to
|
// The shim.setTimeout and similar functions need to be changed to
|
||||||
// use fake timers.
|
// use fake timers.
|
||||||
|
@@ -2,7 +2,7 @@ import replaceUnsupportedCharacters from './replaceUnsupportedCharacters';
|
|||||||
|
|
||||||
describe('replaceUnsupportedCharacters', () => {
|
describe('replaceUnsupportedCharacters', () => {
|
||||||
test('should replace NULL characters', () => {
|
test('should replace NULL characters', () => {
|
||||||
expect(replaceUnsupportedCharacters('Test\x00...')).toBe('Test�...');
|
expect(replaceUnsupportedCharacters('Test\x00...')).toBe('Test\uFFFD...');
|
||||||
expect(replaceUnsupportedCharacters('\x00Test\x00...')).toBe('�Test�...');
|
expect(replaceUnsupportedCharacters('\x00Test\x00...')).toBe('\uFFFDTest\uFFFD...');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,9 +1,13 @@
|
|||||||
|
import Logger from '@joplin/utils/Logger';
|
||||||
import { _ } from './locale';
|
import { _ } from './locale';
|
||||||
import Setting from './models/Setting';
|
import Setting from './models/Setting';
|
||||||
import { reg } from './registry';
|
import { reg } from './registry';
|
||||||
|
import KeychainService from './services/keychain/KeychainService';
|
||||||
import { Plugins } from './services/plugins/PluginService';
|
import { Plugins } from './services/plugins/PluginService';
|
||||||
import shim from './shim';
|
import shim from './shim';
|
||||||
|
|
||||||
|
const logger = Logger.create('versionInfo');
|
||||||
|
|
||||||
export interface PackageInfo {
|
export interface PackageInfo {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
@@ -70,15 +74,21 @@ export default function versionInfo(packageInfo: PackageInfo, plugins: Plugins)
|
|||||||
copyrightText.replace('YYYY', `${now.getFullYear()}`),
|
copyrightText.replace('YYYY', `${now.getFullYear()}`),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let keychainSupported = false;
|
||||||
|
try {
|
||||||
|
// To allow old keys to be read, certain apps allow read-only keychain access:
|
||||||
|
keychainSupported = Setting.value('keychain.supported') >= 1 && !KeychainService.instance().readOnly;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to determine if keychain is supported', error);
|
||||||
|
}
|
||||||
|
|
||||||
const body = [
|
const body = [
|
||||||
_('%s %s (%s, %s)', p.name, p.version, Setting.value('env'), shim.platformName()),
|
_('%s %s (%s, %s)', p.name, p.version, Setting.value('env'), shim.platformName()),
|
||||||
'',
|
'',
|
||||||
_('Client ID: %s', Setting.value('clientId')),
|
_('Client ID: %s', Setting.value('clientId')),
|
||||||
_('Sync Version: %s', Setting.value('syncVersion')),
|
_('Sync Version: %s', Setting.value('syncVersion')),
|
||||||
_('Profile Version: %s', reg.db().version()),
|
_('Profile Version: %s', reg.db().version()),
|
||||||
// The portable app temporarily supports read-only keychain access (but disallows
|
_('Keychain Supported: %s', keychainSupported ? _('Yes') : _('No')),
|
||||||
// write).
|
|
||||||
_('Keychain Supported: %s', (Setting.value('keychain.supported') >= 1 && !shim.isPortable()) ? _('Yes') : _('No')),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (gitInfo) {
|
if (gitInfo) {
|
||||||
|
@@ -19,7 +19,7 @@
|
|||||||
"author": "Joplin",
|
"author": "Joplin",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "29.5.8",
|
"@types/jest": "29.5.12",
|
||||||
"@types/pdfjs-dist": "2.10.378",
|
"@types/pdfjs-dist": "2.10.378",
|
||||||
"@types/react": "18.3.3",
|
"@types/react": "18.3.3",
|
||||||
"@types/react-dom": "18.3.0",
|
"@types/react-dom": "18.3.0",
|
||||||
@@ -29,9 +29,9 @@
|
|||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-environment-jsdom": "29.7.0",
|
"jest-environment-jsdom": "29.7.0",
|
||||||
"style-loader": "3.3.4",
|
"style-loader": "3.3.4",
|
||||||
"ts-jest": "29.1.1",
|
"ts-jest": "29.1.5",
|
||||||
"ts-loader": "9.5.1",
|
"ts-loader": "9.5.1",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.4.5",
|
||||||
"webpack": "5.74.0",
|
"webpack": "5.74.0",
|
||||||
"webpack-cli": "4.10.0"
|
"webpack-cli": "4.10.0"
|
||||||
},
|
},
|
||||||
|
@@ -29,11 +29,11 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/fs-extra": "11.0.4",
|
"@types/fs-extra": "11.0.4",
|
||||||
"@types/jest": "29.5.8",
|
"@types/jest": "29.5.12",
|
||||||
"@types/node": "18.19.39",
|
"@types/node": "18.19.39",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"source-map-loader": "4.0.2",
|
"source-map-loader": "4.0.2",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.4.5",
|
||||||
"webpack": "5.65.0",
|
"webpack": "5.65.0",
|
||||||
"webpack-cli": "4.10.0"
|
"webpack-cli": "4.10.0"
|
||||||
},
|
},
|
||||||
|
@@ -48,7 +48,7 @@
|
|||||||
"@types/react-native": "0.64.19",
|
"@types/react-native": "0.64.19",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-native": "0.70.6",
|
"react-native": "0.70.6",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.4.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "*",
|
"react": "*",
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user