You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-24 20:19:10 +02:00
Compare commits
26 Commits
android-v1
...
ios_releas
Author | SHA1 | Date | |
---|---|---|---|
|
c3854ec350 | ||
|
c2c737541d | ||
|
5b295d5f6f | ||
|
0be8cdf760 | ||
|
545940f545 | ||
|
d58f39823a | ||
|
f9fb1b8a81 | ||
|
56ded0062a | ||
|
83b29d7c51 | ||
|
1ec0746263 | ||
|
568d11bddf | ||
|
97b25ac99d | ||
|
7f1d3d8a5d | ||
|
fde201fbe9 | ||
|
694a1b4ede | ||
|
20d126b39d | ||
|
8ca9c3092a | ||
|
d2771029a3 | ||
|
4128c53fcf | ||
|
a14410b28c | ||
|
d1f8520e6e | ||
|
d76746b8e4 | ||
|
89d173b460 | ||
|
81aba8b8b0 | ||
|
f48697572d | ||
|
e61e8b7b94 |
@@ -803,6 +803,9 @@ packages/app-mobile/components/screens/Note.js.map
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.d.ts
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js.map
|
||||
packages/app-mobile/root.d.ts
|
||||
packages/app-mobile/root.js
|
||||
packages/app-mobile/root.js.map
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.d.ts
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.js.map
|
||||
@@ -1457,6 +1460,9 @@ packages/server/src/controllers/index/HomeController.js.map
|
||||
packages/server/src/controllers/index/LoginController.d.ts
|
||||
packages/server/src/controllers/index/LoginController.js
|
||||
packages/server/src/controllers/index/LoginController.js.map
|
||||
packages/server/src/controllers/index/NotificationController.d.ts
|
||||
packages/server/src/controllers/index/NotificationController.js
|
||||
packages/server/src/controllers/index/NotificationController.js.map
|
||||
packages/server/src/controllers/index/ProfileController.d.ts
|
||||
packages/server/src/controllers/index/ProfileController.js
|
||||
packages/server/src/controllers/index/ProfileController.js.map
|
||||
@@ -1466,9 +1472,21 @@ packages/server/src/controllers/index/UserController.js.map
|
||||
packages/server/src/db.d.ts
|
||||
packages/server/src/db.js
|
||||
packages/server/src/db.js.map
|
||||
packages/server/src/middleware/notificationHandler.d.ts
|
||||
packages/server/src/middleware/notificationHandler.js
|
||||
packages/server/src/middleware/notificationHandler.js.map
|
||||
packages/server/src/middleware/ownerHandler.d.ts
|
||||
packages/server/src/middleware/ownerHandler.js
|
||||
packages/server/src/middleware/ownerHandler.js.map
|
||||
packages/server/src/middleware/routeHandler.d.ts
|
||||
packages/server/src/middleware/routeHandler.js
|
||||
packages/server/src/middleware/routeHandler.js.map
|
||||
packages/server/src/migrations/20190913171451_create.d.ts
|
||||
packages/server/src/migrations/20190913171451_create.js
|
||||
packages/server/src/migrations/20190913171451_create.js.map
|
||||
packages/server/src/migrations/20203012152842_notifications.d.ts
|
||||
packages/server/src/migrations/20203012152842_notifications.js
|
||||
packages/server/src/migrations/20203012152842_notifications.js.map
|
||||
packages/server/src/models/ApiClientModel.d.ts
|
||||
packages/server/src/models/ApiClientModel.js
|
||||
packages/server/src/models/ApiClientModel.js.map
|
||||
@@ -1487,6 +1505,12 @@ packages/server/src/models/FileModel.js.map
|
||||
packages/server/src/models/FileModel.test.d.ts
|
||||
packages/server/src/models/FileModel.test.js
|
||||
packages/server/src/models/FileModel.test.js.map
|
||||
packages/server/src/models/NotificationModel.d.ts
|
||||
packages/server/src/models/NotificationModel.js
|
||||
packages/server/src/models/NotificationModel.js.map
|
||||
packages/server/src/models/NotificationModel.test.d.ts
|
||||
packages/server/src/models/NotificationModel.test.js
|
||||
packages/server/src/models/NotificationModel.test.js.map
|
||||
packages/server/src/models/PermissionModel.d.ts
|
||||
packages/server/src/models/PermissionModel.js
|
||||
packages/server/src/models/PermissionModel.js.map
|
||||
@@ -1532,6 +1556,9 @@ packages/server/src/routes/index/login.js.map
|
||||
packages/server/src/routes/index/logout.d.ts
|
||||
packages/server/src/routes/index/logout.js
|
||||
packages/server/src/routes/index/logout.js.map
|
||||
packages/server/src/routes/index/notifications.d.ts
|
||||
packages/server/src/routes/index/notifications.js
|
||||
packages/server/src/routes/index/notifications.js.map
|
||||
packages/server/src/routes/index/profile.d.ts
|
||||
packages/server/src/routes/index/profile.js
|
||||
packages/server/src/routes/index/profile.js.map
|
||||
|
@@ -65,7 +65,12 @@ module.exports = {
|
||||
'no-var': ['error'],
|
||||
'no-new-func': ['error'],
|
||||
'import/prefer-default-export': ['error'],
|
||||
'import/first': ['error'],
|
||||
|
||||
// This rule should not be enabled since it matters in what order
|
||||
// imports are done, in particular in relation to the shim.setReact
|
||||
// call, which should be done first, but this rule might move it down.
|
||||
// 'import/first': ['error'],
|
||||
|
||||
'no-array-constructor': ['error'],
|
||||
'radix': ['error'],
|
||||
|
||||
|
27
.gitignore
vendored
27
.gitignore
vendored
@@ -792,6 +792,9 @@ packages/app-mobile/components/screens/Note.js.map
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.d.ts
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js.map
|
||||
packages/app-mobile/root.d.ts
|
||||
packages/app-mobile/root.js
|
||||
packages/app-mobile/root.js.map
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.d.ts
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.js.map
|
||||
@@ -1446,6 +1449,9 @@ packages/server/src/controllers/index/HomeController.js.map
|
||||
packages/server/src/controllers/index/LoginController.d.ts
|
||||
packages/server/src/controllers/index/LoginController.js
|
||||
packages/server/src/controllers/index/LoginController.js.map
|
||||
packages/server/src/controllers/index/NotificationController.d.ts
|
||||
packages/server/src/controllers/index/NotificationController.js
|
||||
packages/server/src/controllers/index/NotificationController.js.map
|
||||
packages/server/src/controllers/index/ProfileController.d.ts
|
||||
packages/server/src/controllers/index/ProfileController.js
|
||||
packages/server/src/controllers/index/ProfileController.js.map
|
||||
@@ -1455,9 +1461,21 @@ packages/server/src/controllers/index/UserController.js.map
|
||||
packages/server/src/db.d.ts
|
||||
packages/server/src/db.js
|
||||
packages/server/src/db.js.map
|
||||
packages/server/src/middleware/notificationHandler.d.ts
|
||||
packages/server/src/middleware/notificationHandler.js
|
||||
packages/server/src/middleware/notificationHandler.js.map
|
||||
packages/server/src/middleware/ownerHandler.d.ts
|
||||
packages/server/src/middleware/ownerHandler.js
|
||||
packages/server/src/middleware/ownerHandler.js.map
|
||||
packages/server/src/middleware/routeHandler.d.ts
|
||||
packages/server/src/middleware/routeHandler.js
|
||||
packages/server/src/middleware/routeHandler.js.map
|
||||
packages/server/src/migrations/20190913171451_create.d.ts
|
||||
packages/server/src/migrations/20190913171451_create.js
|
||||
packages/server/src/migrations/20190913171451_create.js.map
|
||||
packages/server/src/migrations/20203012152842_notifications.d.ts
|
||||
packages/server/src/migrations/20203012152842_notifications.js
|
||||
packages/server/src/migrations/20203012152842_notifications.js.map
|
||||
packages/server/src/models/ApiClientModel.d.ts
|
||||
packages/server/src/models/ApiClientModel.js
|
||||
packages/server/src/models/ApiClientModel.js.map
|
||||
@@ -1476,6 +1494,12 @@ packages/server/src/models/FileModel.js.map
|
||||
packages/server/src/models/FileModel.test.d.ts
|
||||
packages/server/src/models/FileModel.test.js
|
||||
packages/server/src/models/FileModel.test.js.map
|
||||
packages/server/src/models/NotificationModel.d.ts
|
||||
packages/server/src/models/NotificationModel.js
|
||||
packages/server/src/models/NotificationModel.js.map
|
||||
packages/server/src/models/NotificationModel.test.d.ts
|
||||
packages/server/src/models/NotificationModel.test.js
|
||||
packages/server/src/models/NotificationModel.test.js.map
|
||||
packages/server/src/models/PermissionModel.d.ts
|
||||
packages/server/src/models/PermissionModel.js
|
||||
packages/server/src/models/PermissionModel.js.map
|
||||
@@ -1521,6 +1545,9 @@ packages/server/src/routes/index/login.js.map
|
||||
packages/server/src/routes/index/logout.d.ts
|
||||
packages/server/src/routes/index/logout.js
|
||||
packages/server/src/routes/index/logout.js.map
|
||||
packages/server/src/routes/index/notifications.d.ts
|
||||
packages/server/src/routes/index/notifications.js
|
||||
packages/server/src/routes/index/notifications.js.map
|
||||
packages/server/src/routes/index/profile.d.ts
|
||||
packages/server/src/routes/index/profile.js
|
||||
packages/server/src/routes/index/profile.js.map
|
||||
|
20
README.md
20
README.md
@@ -109,6 +109,8 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
|
||||
- [Sync Lock spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/sync_lock.md)
|
||||
- [Plugin Architecture spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/plugins.md)
|
||||
- [Search Sorting spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/search_sorting.md)
|
||||
- [Server: File URL Format](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_file_url_format.md)
|
||||
- [Server: Delta Sync](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_delta_sync.md)
|
||||
|
||||
- Google Summer of Code 2020
|
||||
|
||||
@@ -444,15 +446,15 @@ Current translations:
|
||||
 | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Hrvoje Mandić](mailto:trbuhom@net.hr) | 26%
|
||||
 | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Lukas Helebrandt](mailto:lukas@aiya.cz) | 93%
|
||||
 | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | Mustafa Al-Dailemi (dailemi@hotmail.com)Language-Team: | 77%
|
||||
 | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Ettore Atalan](mailto:atalanttore@users.noreply.github.com) | 96%
|
||||
 | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Ettore Atalan](mailto:atalanttore@users.noreply.github.com) | 95%
|
||||
 | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 61%
|
||||
 | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
|
||||
 | English (United States of America) | [en_US](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_US.po) | | 100%
|
||||
 | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Mario Campo](mailto:mario.campo@gmail.com) | 93%
|
||||
 | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Mario Campo](mailto:mario.campo@gmail.com) | 99%
|
||||
 | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 35%
|
||||
 | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | | 94%
|
||||
 | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 93%
|
||||
 | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 41%
|
||||
 | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 99%
|
||||
 | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 40%
|
||||
 | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [Fathy AR](mailto:16875937+fathyar@users.noreply.github.com) | 85%
|
||||
 | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Alessandro Bernardello](mailto:mailfilledwithspam@gmail.com) | 96%
|
||||
 | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 32%
|
||||
@@ -467,14 +469,14 @@ Current translations:
|
||||
 | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 66%
|
||||
 | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 49%
|
||||
 | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 79%
|
||||
 | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 92%
|
||||
 | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 99%
|
||||
 | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 89%
|
||||
 | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 96%
|
||||
 | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 67%
|
||||
 | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [WhiredPlanck](mailto:fungdaat31@outlook.com) | 97%
|
||||
 | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Yaoze Ye](mailto:yaozeye@yahoo.co.jp) | 89%
|
||||
 | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 97%
|
||||
 | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 97%
|
||||
 | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [WhiredPlanck](mailto:fungdaat31@outlook.com) | 99%
|
||||
 | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Yaoze Ye](mailto:yaozeye@yahoo.co.jp) | 95%
|
||||
 | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 96%
|
||||
 | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 96%
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
|
||||
# Contributors
|
||||
|
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import joplin from 'api';
|
||||
import { ToolbarButtonLocation } from 'api/types';
|
||||
|
||||
const uslug = require('uslug');
|
||||
|
||||
@@ -91,6 +92,18 @@ joplin.plugins.register({
|
||||
updateTocView();
|
||||
});
|
||||
|
||||
await joplin.commands.register({
|
||||
name: 'toggleToc',
|
||||
label: 'Toggle TOC',
|
||||
iconName: 'fas fa-drum',
|
||||
execute: async () => {
|
||||
const isVisible = await (panels as any).visible(view);
|
||||
(panels as any).show(view, !isVisible);
|
||||
},
|
||||
});
|
||||
|
||||
await joplin.views.toolbarButtons.create('toggleToc', 'toggleToc', ToolbarButtonLocation.NoteToolbar);
|
||||
|
||||
updateTocView();
|
||||
},
|
||||
});
|
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Joplin Web Clipper [DEV]",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1",
|
||||
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
|
||||
"homepage_url": "https://joplinapp.org",
|
||||
"content_security_policy": "script-src 'self'; object-src 'self'",
|
||||
|
@@ -22,6 +22,9 @@ import { LayoutItem } from './gui/ResizableLayout/utils/types';
|
||||
import stateToWhenClauseContext from './services/commands/stateToWhenClauseContext';
|
||||
import ResourceService from '@joplin/lib/services/ResourceService';
|
||||
import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher';
|
||||
import produce from 'immer';
|
||||
import iterateItems from './gui/ResizableLayout/utils/iterateItems';
|
||||
import validateLayout from './gui/ResizableLayout/utils/validateLayout';
|
||||
|
||||
const { FoldersScreenUtils } = require('@joplin/lib/folders-screen-utils.js');
|
||||
const MasterKey = require('@joplin/lib/models/MasterKey');
|
||||
@@ -247,6 +250,29 @@ class Application extends BaseApplication {
|
||||
};
|
||||
break;
|
||||
|
||||
case 'MAIN_LAYOUT_SET_ITEM_PROP':
|
||||
|
||||
{
|
||||
let newLayout = produce(state.mainLayout, (draftLayout: LayoutItem) => {
|
||||
iterateItems(draftLayout, (_itemIndex: number, item: LayoutItem, _parent: LayoutItem) => {
|
||||
if (item.key === action.itemKey) {
|
||||
(item as any)[action.propName] = action.propValue;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
if (newLayout !== state.mainLayout) newLayout = validateLayout(newLayout);
|
||||
|
||||
newState = {
|
||||
...state,
|
||||
mainLayout: newLayout,
|
||||
};
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'NOTE_FILE_WATCHER_ADD':
|
||||
|
||||
if (newState.watchedNoteFiles.indexOf(action.id) < 0) {
|
||||
|
@@ -19,7 +19,7 @@ const taboverride = require('taboverride');
|
||||
const { reg } = require('@joplin/lib/registry.js');
|
||||
const BaseItem = require('@joplin/lib/models/BaseItem');
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
// const { clipboard } = require('electron');
|
||||
const { clipboard } = require('electron');
|
||||
const supportedLocales = require('./supportedLocales');
|
||||
|
||||
function markupRenderOptions(override: any = null) {
|
||||
@@ -1015,19 +1015,22 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyDown(_event: any) {
|
||||
// It seems "paste as text" is now handled automatically by
|
||||
// either Chrome, Electron and/or TinyMCE so the code below
|
||||
// should not longer be necessary. Also fixes
|
||||
function onKeyDown(event: any) {
|
||||
// It seems "paste as text" is handled automatically by
|
||||
// on Windows so the code below so we need to run the below
|
||||
// code only on macOS (and maybe Linux). If we were to run
|
||||
// this on Windows we would have this double-paste issue:
|
||||
// https://github.com/laurent22/joplin/issues/4243
|
||||
|
||||
// Handle "paste as text". Note that when pressing CtrlOrCmd+Shift+V it's going
|
||||
// to trigger the "keydown" event but not the "paste" event, so it's ok to process
|
||||
// it here and we don't need to do anything special in onPaste
|
||||
// if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.code === 'KeyV') {
|
||||
// const pastedText = clipboard.readText();
|
||||
// if (pastedText) editor.insertContent(pastedText);
|
||||
// }
|
||||
if (!shim.isWindows()) {
|
||||
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.code === 'KeyV') {
|
||||
const pastedText = clipboard.readText();
|
||||
if (pastedText) editor.insertContent(pastedText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor.on('keyup', onKeyUp);
|
||||
|
@@ -16,8 +16,4 @@ export default function findItemByKey(layout: LayoutItem, key: string): LayoutIt
|
||||
}
|
||||
|
||||
return recurseFind(layout);
|
||||
|
||||
// const output = recurseFind(layout);
|
||||
// if (!output) throw new Error(`Could not find item "${key}"`);
|
||||
// return output;
|
||||
}
|
||||
|
@@ -2,6 +2,8 @@ import { LayoutItem } from './types';
|
||||
|
||||
type ItemItemCallback = (itemIndex: number, item: LayoutItem, parent: LayoutItem)=> boolean;
|
||||
|
||||
// Callback should return `true` if iteration should continue, or `false` if it
|
||||
// should stop
|
||||
export default function iterateItems(layout: LayoutItem, callback: ItemItemCallback) {
|
||||
const result = callback(0, layout, null);
|
||||
if (result === false) return;
|
||||
|
@@ -4,5 +4,5 @@
|
||||
# It could be used to develop plugins too.
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
PLUGIN_PATH="$SCRIPT_DIR/../app-cli/tests/support/plugins/register_command"
|
||||
PLUGIN_PATH="$SCRIPT_DIR/../app-cli/tests/support/plugins/toc"
|
||||
npm i --prefix="$PLUGIN_PATH" && npm start -- --dev-plugins "$PLUGIN_PATH"
|
@@ -138,8 +138,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097616
|
||||
versionName "1.6.2"
|
||||
versionCode 2097618
|
||||
versionName "1.6.4"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@
|
||||
// So there's basically still a one way flux: React => SQLite => Redux => React
|
||||
|
||||
import { LogBox, AppRegistry } from 'react-native';
|
||||
const { Root } = require('./root.js');
|
||||
const Root = require('./root').default;
|
||||
|
||||
// Seems JavaScript developers love adding warnings everywhere, even when these warnings can't be fixed
|
||||
// or don't really matter. Because we want important warnings to actually be fixed, we disable
|
||||
|
@@ -338,13 +338,13 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 58;
|
||||
CURRENT_PROJECT_VERSION = 59;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 10.6.0;
|
||||
MARKETING_VERSION = 10.6.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -365,12 +365,12 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 58;
|
||||
CURRENT_PROJECT_VERSION = 59;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 10.6.0;
|
||||
MARKETING_VERSION = 10.6.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
@@ -1,24 +1,43 @@
|
||||
import setUpQuickActions from './setUpQuickActions';
|
||||
import PluginAssetsLoader from './PluginAssetsLoader';
|
||||
|
||||
const React = require('react');
|
||||
const { AppState, Keyboard, NativeModules, BackHandler, Animated, View, StatusBar } = require('react-native');
|
||||
const shim = require('@joplin/lib/shim').default;
|
||||
import shim from '@joplin/lib/shim';
|
||||
shim.setReact(React);
|
||||
|
||||
import setUpQuickActions from './setUpQuickActions';
|
||||
import PluginAssetsLoader from './PluginAssetsLoader';
|
||||
import AlarmService from '@joplin/lib/services/AlarmService';
|
||||
import Alarm from '@joplin/lib/models/Alarm';
|
||||
import time from '@joplin/lib/time';
|
||||
import Logger, { TargetType } from '@joplin/lib/Logger';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import BaseService from '@joplin/lib/services/BaseService';
|
||||
import ResourceService from '@joplin/lib/services/ResourceService';
|
||||
import KvStore from '@joplin/lib/services/KvStore';
|
||||
import NoteScreen from './components/screens/Note';
|
||||
import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
|
||||
import reducer from '@joplin/lib/reducer';
|
||||
import ShareExtension from './utils/ShareExtension';
|
||||
import handleShared from './utils/shareHandler';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import { loadKeychainServiceAndSettings } from '@joplin/lib/services/SettingUtils';
|
||||
import KeychainServiceDriverMobile from '@joplin/lib/services/keychain/KeychainServiceDriver.mobile';
|
||||
import { setLocale, closestSupportedLocale, defaultLocale } from '@joplin/lib/locale';
|
||||
import SyncTargetJoplinServer from '@joplin/lib/SyncTargetJoplinServer';
|
||||
|
||||
const { AppState, Keyboard, NativeModules, BackHandler, Animated, View, StatusBar } = require('react-native');
|
||||
|
||||
const DropdownAlert = require('react-native-dropdownalert').default;
|
||||
const AlarmServiceDriver = require('./services/AlarmServiceDriver').default;
|
||||
const SafeAreaView = require('./components/SafeAreaView');
|
||||
const { connect, Provider } = require('react-redux');
|
||||
const { BackButtonService } = require('./services/back-button.js');
|
||||
const NavService = require('@joplin/lib/services/NavService.js');
|
||||
const AlarmService = require('@joplin/lib/services/AlarmService.js').default;
|
||||
const AlarmServiceDriver = require('./services/AlarmServiceDriver').default;
|
||||
const Alarm = require('@joplin/lib/models/Alarm').default;
|
||||
const { createStore, applyMiddleware } = require('redux');
|
||||
const reduxSharedMiddleware = require('@joplin/lib/components/shared/reduxSharedMiddleware');
|
||||
const { shimInit } = require('./utils/shim-init-react.js');
|
||||
const time = require('@joplin/lib/time').default;
|
||||
const { AppNav } = require('./components/app-nav.js');
|
||||
const Logger = require('@joplin/lib/Logger').default;
|
||||
const Note = require('@joplin/lib/models/Note.js');
|
||||
const Folder = require('@joplin/lib/models/Folder.js');
|
||||
const BaseSyncTarget = require('@joplin/lib/BaseSyncTarget.js');
|
||||
@@ -29,16 +48,11 @@ const NoteTag = require('@joplin/lib/models/NoteTag.js');
|
||||
const BaseItem = require('@joplin/lib/models/BaseItem.js');
|
||||
const MasterKey = require('@joplin/lib/models/MasterKey.js');
|
||||
const Revision = require('@joplin/lib/models/Revision.js');
|
||||
const BaseModel = require('@joplin/lib/BaseModel').default;
|
||||
const BaseService = require('@joplin/lib/services/BaseService').default;
|
||||
const ResourceService = require('@joplin/lib/services/ResourceService').default;
|
||||
const RevisionService = require('@joplin/lib/services/RevisionService');
|
||||
const KvStore = require('@joplin/lib/services/KvStore').default;
|
||||
const { JoplinDatabase } = require('@joplin/lib/joplin-database.js');
|
||||
const { Database } = require('@joplin/lib/database.js');
|
||||
const { NotesScreen } = require('./components/screens/notes.js');
|
||||
const { TagsScreen } = require('./components/screens/tags.js');
|
||||
const NoteScreen = require('./components/screens/Note').default;
|
||||
const { ConfigScreen } = require('./components/screens/config.js');
|
||||
const { FolderScreen } = require('./components/screens/folder.js');
|
||||
const { LogScreen } = require('./components/screens/log.js');
|
||||
@@ -47,31 +61,18 @@ const { SearchScreen } = require('./components/screens/search.js');
|
||||
const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js');
|
||||
const { EncryptionConfigScreen } = require('./components/screens/encryption-config.js');
|
||||
const { DropboxLoginScreen } = require('./components/screens/dropbox-login.js');
|
||||
const UpgradeSyncTargetScreen = require('./components/screens/UpgradeSyncTargetScreen').default;
|
||||
const Setting = require('@joplin/lib/models/Setting').default;
|
||||
const { MenuContext } = require('react-native-popup-menu');
|
||||
const { SideMenu } = require('./components/side-menu.js');
|
||||
const { SideMenuContent } = require('./components/side-menu-content.js');
|
||||
const { SideMenuContentNote } = require('./components/side-menu-content-note.js');
|
||||
const { DatabaseDriverReactNative } = require('./utils/database-driver-react-native');
|
||||
const { reg } = require('@joplin/lib/registry.js');
|
||||
const { setLocale, closestSupportedLocale, defaultLocale } = require('@joplin/lib/locale');
|
||||
const RNFetchBlob = require('rn-fetch-blob').default;
|
||||
const PoorManIntervals = require('@joplin/lib/PoorManIntervals').default;
|
||||
const reducer = require('@joplin/lib/reducer').default;
|
||||
const { defaultState } = require('@joplin/lib/reducer');
|
||||
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local.js');
|
||||
const DropdownAlert = require('react-native-dropdownalert').default;
|
||||
const ShareExtension = require('./utils/ShareExtension.js').default;
|
||||
const handleShared = require('./utils/shareHandler').default;
|
||||
const ResourceFetcher = require('@joplin/lib/services/ResourceFetcher');
|
||||
const SearchEngine = require('@joplin/lib/services/searchengine/SearchEngine');
|
||||
const WelcomeUtils = require('@joplin/lib/WelcomeUtils');
|
||||
const { themeStyle } = require('./components/global-style.js');
|
||||
const uuid = require('@joplin/lib/uuid').default;
|
||||
|
||||
const { loadKeychainServiceAndSettings } = require('@joplin/lib/services/SettingUtils');
|
||||
const KeychainServiceDriverMobile = require('@joplin/lib/services/keychain/KeychainServiceDriver.mobile').default;
|
||||
|
||||
const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry.js');
|
||||
const SyncTargetOneDrive = require('@joplin/lib/SyncTargetOneDrive.js');
|
||||
@@ -87,15 +88,16 @@ SyncTargetRegistry.addClass(SyncTargetWebDAV);
|
||||
SyncTargetRegistry.addClass(SyncTargetDropbox);
|
||||
SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||
SyncTargetRegistry.addClass(SyncTargetAmazonS3);
|
||||
SyncTargetRegistry.addClass(SyncTargetJoplinServer);
|
||||
|
||||
const FsDriverRN = require('./utils/fs-driver-rn.js').FsDriverRN;
|
||||
const DecryptionWorker = require('@joplin/lib/services/DecryptionWorker');
|
||||
const EncryptionService = require('@joplin/lib/services/EncryptionService');
|
||||
const MigrationService = require('@joplin/lib/services/MigrationService');
|
||||
|
||||
let storeDispatch = function() {};
|
||||
let storeDispatch = function(_action: any) {};
|
||||
|
||||
const logReducerAction = function(action) {
|
||||
const logReducerAction = function(action: any) {
|
||||
if (['SIDE_MENU_OPEN_PERCENT', 'SYNC_REPORT_UPDATE'].indexOf(action.type) >= 0) return;
|
||||
|
||||
const msg = [action.type];
|
||||
@@ -104,7 +106,7 @@ const logReducerAction = function(action) {
|
||||
// reg.logger().debug('Reducer action', msg.join(', '));
|
||||
};
|
||||
|
||||
const generalMiddleware = store => next => async (action) => {
|
||||
const generalMiddleware = (store: any) => (next: any) => async (action: any) => {
|
||||
logReducerAction(action);
|
||||
PoorManIntervals.update(); // This function needs to be called regularly so put it here
|
||||
|
||||
@@ -167,9 +169,9 @@ const generalMiddleware = store => next => async (action) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const navHistory = [];
|
||||
const navHistory: any[] = [];
|
||||
|
||||
function historyCanGoBackTo(route) {
|
||||
function historyCanGoBackTo(route: any) {
|
||||
if (route.routeName === 'Note') return false;
|
||||
if (route.routeName === 'Folder') return false;
|
||||
|
||||
@@ -194,13 +196,14 @@ const appDefaultState = Object.assign({}, defaultState, {
|
||||
noteSideMenuOptions: null,
|
||||
});
|
||||
|
||||
const appReducer = (state = appDefaultState, action) => {
|
||||
const appReducer = (state = appDefaultState, action: any) => {
|
||||
let newState = state;
|
||||
let historyGoingBack = false;
|
||||
|
||||
try {
|
||||
switch (action.type) {
|
||||
|
||||
// @ts-ignore
|
||||
case 'NAV_BACK':
|
||||
|
||||
{
|
||||
@@ -224,7 +227,7 @@ const appReducer = (state = appDefaultState, action) => {
|
||||
{
|
||||
const currentRoute = state.route;
|
||||
|
||||
if (!historyGoingBack && historyCanGoBackTo(currentRoute, action)) {
|
||||
if (!historyGoingBack && historyCanGoBackTo(currentRoute)) {
|
||||
// If the route *name* is the same (even if the other parameters are different), we
|
||||
// overwrite the last route in the history with the current one. If the route name
|
||||
// is different, we push a new history entry.
|
||||
@@ -368,7 +371,7 @@ const appReducer = (state = appDefaultState, action) => {
|
||||
const store = createStore(appReducer, applyMiddleware(generalMiddleware));
|
||||
storeDispatch = store.dispatch;
|
||||
|
||||
function resourceFetcher_downloadComplete(event) {
|
||||
function resourceFetcher_downloadComplete(event: any) {
|
||||
if (event.encrypted) {
|
||||
DecryptionWorker.instance().scheduleStart();
|
||||
}
|
||||
@@ -378,9 +381,10 @@ function decryptionWorker_resourceMetadataButNotBlobDecrypted() {
|
||||
ResourceFetcher.instance().scheduleAutoAddResources();
|
||||
}
|
||||
|
||||
async function initialize(dispatch) {
|
||||
async function initialize(dispatch: Function) {
|
||||
shimInit();
|
||||
|
||||
// @ts-ignore
|
||||
Setting.setConstant('env', __DEV__ ? 'dev' : 'prod');
|
||||
Setting.setConstant('appId', 'net.cozic.joplin-mobile');
|
||||
Setting.setConstant('appType', 'mobile');
|
||||
@@ -391,18 +395,18 @@ async function initialize(dispatch) {
|
||||
await logDatabase.exec(Logger.databaseCreateTableSql());
|
||||
|
||||
const mainLogger = new Logger();
|
||||
mainLogger.addTarget('database', { database: logDatabase, source: 'm' });
|
||||
mainLogger.addTarget(TargetType.Database, { database: logDatabase, source: 'm' });
|
||||
mainLogger.setLevel(Logger.LEVEL_INFO);
|
||||
|
||||
if (Setting.value('env') == 'dev') {
|
||||
mainLogger.addTarget('console');
|
||||
mainLogger.addTarget(TargetType.Console);
|
||||
mainLogger.setLevel(Logger.LEVEL_DEBUG);
|
||||
}
|
||||
|
||||
Logger.initializeGlobalLogger(mainLogger);
|
||||
|
||||
reg.setLogger(mainLogger);
|
||||
reg.setShowErrorMessageBoxHandler((message) => { alert(message); });
|
||||
reg.setShowErrorMessageBoxHandler((message: string) => { alert(message); });
|
||||
|
||||
BaseService.logger_ = mainLogger;
|
||||
// require('@joplin/lib/ntpDate').setLogger(reg.logger());
|
||||
@@ -411,9 +415,9 @@ async function initialize(dispatch) {
|
||||
reg.logger().info(`Starting application ${Setting.value('appId')} (${Setting.value('env')})`);
|
||||
|
||||
const dbLogger = new Logger();
|
||||
dbLogger.addTarget('database', { database: logDatabase, source: 'm' });
|
||||
dbLogger.addTarget(TargetType.Database, { database: logDatabase, source: 'm' });
|
||||
if (Setting.value('env') == 'dev') {
|
||||
dbLogger.addTarget('console');
|
||||
dbLogger.addTarget(TargetType.Console);
|
||||
dbLogger.setLevel(Logger.LEVEL_INFO); // Set to LEVEL_DEBUG for full SQL queries
|
||||
} else {
|
||||
dbLogger.setLevel(Logger.LEVEL_INFO);
|
||||
@@ -452,7 +456,7 @@ async function initialize(dispatch) {
|
||||
if (Setting.value('env') == 'prod') {
|
||||
await db.open({ name: 'joplin.sqlite' });
|
||||
} else {
|
||||
await db.open({ name: 'joplin-76.sqlite' });
|
||||
await db.open({ name: 'joplin-100.sqlite' });
|
||||
|
||||
// await db.clearForTesting();
|
||||
}
|
||||
@@ -562,7 +566,7 @@ async function initialize(dispatch) {
|
||||
reg.setupRecurrentSync();
|
||||
|
||||
PoorManIntervals.setTimeout(() => {
|
||||
AlarmService.garbageCollect();
|
||||
void AlarmService.garbageCollect();
|
||||
}, 1000 * 60 * 60);
|
||||
|
||||
ResourceService.runInBackground();
|
||||
@@ -584,7 +588,7 @@ async function initialize(dispatch) {
|
||||
reg.scheduleSync(1000).then(() => {
|
||||
// Wait for the first sync before updating the notifications, since synchronisation
|
||||
// might change the notifications.
|
||||
AlarmService.updateAllNotifications();
|
||||
void AlarmService.updateAllNotifications();
|
||||
|
||||
DecryptionWorker.instance().scheduleStart();
|
||||
});
|
||||
@@ -654,7 +658,7 @@ class AppComponent extends React.Component {
|
||||
|
||||
BackButtonService.initialize(this.backButtonHandler_);
|
||||
|
||||
AlarmService.setInAppNotificationHandler(async (alarmId) => {
|
||||
AlarmService.setInAppNotificationHandler(async (alarmId: string) => {
|
||||
const alarm = await Alarm.load(alarmId);
|
||||
const notification = await Alarm.makeNotification(alarm);
|
||||
this.dropdownAlert_.alertWithType('info', notification.title, notification.body ? notification.body : '');
|
||||
@@ -666,7 +670,7 @@ class AppComponent extends React.Component {
|
||||
if (sharedData) {
|
||||
reg.logger().info('Received shared data');
|
||||
if (this.props.selectedFolderId) {
|
||||
handleShared(sharedData, this.props.selectedFolderId, this.props.dispatch);
|
||||
await handleShared(sharedData, this.props.selectedFolderId, this.props.dispatch);
|
||||
} else {
|
||||
reg.logger.info('Cannot handle share - default folder id is not set');
|
||||
}
|
||||
@@ -677,7 +681,7 @@ class AppComponent extends React.Component {
|
||||
AppState.removeEventListener('change', this.onAppStateChange_);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps: any) {
|
||||
if (this.props.showSideMenu !== prevProps.showSideMenu) {
|
||||
Animated.timing(this.state.sideMenuContentOpacity, {
|
||||
toValue: this.props.showSideMenu ? 0.5 : 0,
|
||||
@@ -707,14 +711,14 @@ class AppComponent extends React.Component {
|
||||
return false;
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
UNSAFE_componentWillReceiveProps(newProps: any) {
|
||||
if (newProps.syncStarted != this.lastSyncStarted_) {
|
||||
if (!newProps.syncStarted) FoldersScreenUtils.refreshFolders();
|
||||
this.lastSyncStarted_ = newProps.syncStarted;
|
||||
}
|
||||
}
|
||||
|
||||
sideMenu_change(isOpen) {
|
||||
sideMenu_change(isOpen: boolean) {
|
||||
// Make sure showSideMenu property of state is updated
|
||||
// when the menu is open/closed.
|
||||
this.props.dispatch({
|
||||
@@ -759,8 +763,8 @@ class AppComponent extends React.Component {
|
||||
menu={sideMenuContent}
|
||||
edgeHitWidth={5}
|
||||
menuPosition={menuPosition}
|
||||
onChange={(isOpen) => this.sideMenu_change(isOpen)}
|
||||
onSliding={(percent) => {
|
||||
onChange={(isOpen: boolean) => this.sideMenu_change(isOpen)}
|
||||
onSliding={(percent: number) => {
|
||||
this.props.dispatch({
|
||||
type: 'SIDE_MENU_OPEN_PERCENT',
|
||||
value: percent,
|
||||
@@ -773,7 +777,7 @@ class AppComponent extends React.Component {
|
||||
<View style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
|
||||
<AppNav screens={appNavInit} />
|
||||
</View>
|
||||
<DropdownAlert ref={ref => this.dropdownAlert_ = ref} tapToCloseEnabled={true} />
|
||||
<DropdownAlert ref={(ref: any) => this.dropdownAlert_ = ref} tapToCloseEnabled={true} />
|
||||
<Animated.View pointerEvents='none' style={{ position: 'absolute', backgroundColor: 'black', opacity: this.state.sideMenuContentOpacity, width: '100%', height: '120%' }}/>
|
||||
</SafeAreaView>
|
||||
</MenuContext>
|
||||
@@ -783,7 +787,7 @@ class AppComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const mapStateToProps = (state: any) => {
|
||||
return {
|
||||
historyCanGoBack: state.historyCanGoBack,
|
||||
showSideMenu: state.showSideMenu,
|
||||
@@ -799,7 +803,7 @@ const mapStateToProps = (state) => {
|
||||
|
||||
const App = connect(mapStateToProps)(AppComponent);
|
||||
|
||||
class Root extends React.Component {
|
||||
export default class Root extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
@@ -808,5 +812,3 @@ class Root extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Root };
|
@@ -24,9 +24,9 @@ export default async (sharedData: SharedData, folderId: string, dispatch: Functi
|
||||
// below will do nothing (because routeName wouldn't change)
|
||||
// Then we wait a bit for the state to be set correctly, and
|
||||
// finally we go to the new note.
|
||||
await dispatch({ type: 'NAV_BACK' });
|
||||
dispatch({ type: 'NAV_BACK' });
|
||||
|
||||
await dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
|
||||
const newNote = await Note.save({
|
||||
parent_id: folderId,
|
||||
|
@@ -30,4 +30,16 @@ export default class JoplinViewsPanels {
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
show(handle: ViewHandle, show?: boolean): Promise<void>;
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(handle: ViewHandle): Promise<void>;
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
2
packages/generator-joplin/package-lock.json
generated
2
packages/generator-joplin/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "generator-joplin",
|
||||
"version": "1.5.3",
|
||||
"version": "1.6.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -66,7 +66,7 @@ class BaseModel {
|
||||
public static TYPE_SMART_FILTER = ModelType.SmartFilter;
|
||||
public static TYPE_COMMAND = ModelType.Command;
|
||||
|
||||
protected static dispatch: Function = function() {};
|
||||
public static dispatch: Function = function() {};
|
||||
private static saveMutexes_: any = {};
|
||||
|
||||
private static db_: any;
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -47,15 +47,15 @@ stats['ca'] = {"percentDone":89};
|
||||
stats['hr_HR'] = {"percentDone":26};
|
||||
stats['cs_CZ'] = {"percentDone":93};
|
||||
stats['da_DK'] = {"percentDone":77};
|
||||
stats['de_DE'] = {"percentDone":96};
|
||||
stats['de_DE'] = {"percentDone":95};
|
||||
stats['et_EE'] = {"percentDone":61};
|
||||
stats['en_GB'] = {"percentDone":100};
|
||||
stats['en_US'] = {"percentDone":100};
|
||||
stats['es_ES'] = {"percentDone":93};
|
||||
stats['es_ES'] = {"percentDone":99};
|
||||
stats['eo'] = {"percentDone":35};
|
||||
stats['fi_FI'] = {"percentDone":94};
|
||||
stats['fr_FR'] = {"percentDone":93};
|
||||
stats['gl_ES'] = {"percentDone":41};
|
||||
stats['fr_FR'] = {"percentDone":99};
|
||||
stats['gl_ES'] = {"percentDone":40};
|
||||
stats['id_ID'] = {"percentDone":85};
|
||||
stats['it_IT'] = {"percentDone":96};
|
||||
stats['nl_BE'] = {"percentDone":32};
|
||||
@@ -70,12 +70,12 @@ stats['sl_SI'] = {"percentDone":40};
|
||||
stats['sv'] = {"percentDone":66};
|
||||
stats['th_TH'] = {"percentDone":49};
|
||||
stats['vi'] = {"percentDone":79};
|
||||
stats['tr_TR'] = {"percentDone":92};
|
||||
stats['tr_TR'] = {"percentDone":99};
|
||||
stats['el_GR'] = {"percentDone":89};
|
||||
stats['ru_RU'] = {"percentDone":96};
|
||||
stats['sr_RS'] = {"percentDone":67};
|
||||
stats['zh_CN'] = {"percentDone":97};
|
||||
stats['zh_TW'] = {"percentDone":89};
|
||||
stats['ja_JP'] = {"percentDone":97};
|
||||
stats['ko'] = {"percentDone":97};
|
||||
stats['zh_CN'] = {"percentDone":99};
|
||||
stats['zh_TW'] = {"percentDone":95};
|
||||
stats['ja_JP'] = {"percentDone":96};
|
||||
stats['ko'] = {"percentDone":96};
|
||||
module.exports = { locales: locales, stats: stats };
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -141,7 +141,7 @@ class Note extends BaseItem {
|
||||
useAbsolutePaths: false,
|
||||
}, options);
|
||||
|
||||
this.logger().debug('replaceResourceInternalToExternalLinks', 'options:', options, 'body:', body);
|
||||
// this.logger().debug('replaceResourceInternalToExternalLinks', 'options:', options, 'body:', body);
|
||||
|
||||
const resourceIds = await this.linkedResourceIds(body);
|
||||
const Resource = this.getClass('Resource');
|
||||
@@ -161,7 +161,7 @@ class Note extends BaseItem {
|
||||
body = body.replace(new RegExp(`:/${id}`, 'gi'), markdownUtils.escapeLinkUrl(resourcePath));
|
||||
}
|
||||
|
||||
this.logger().debug('replaceResourceInternalToExternalLinks result', body);
|
||||
// this.logger().debug('replaceResourceInternalToExternalLinks result', body);
|
||||
|
||||
return body;
|
||||
}
|
||||
@@ -194,7 +194,7 @@ class Note extends BaseItem {
|
||||
|
||||
pathsToTry = temp;
|
||||
|
||||
this.logger().debug('replaceResourceExternalToInternalLinks', 'options:', options, 'pathsToTry:', pathsToTry);
|
||||
// this.logger().debug('replaceResourceExternalToInternalLinks', 'options:', options, 'pathsToTry:', pathsToTry);
|
||||
|
||||
for (const basePath of pathsToTry) {
|
||||
const reStrings = [
|
||||
|
@@ -313,6 +313,17 @@ class Resource extends BaseItem {
|
||||
return r ? r.total : 0;
|
||||
}
|
||||
|
||||
static async createdLocallyCount() {
|
||||
const r = await this.db().selectOne(`
|
||||
SELECT count(*) as total
|
||||
FROM resources
|
||||
WHERE id NOT IN
|
||||
(SELECT resource_id FROM resource_local_states)
|
||||
`);
|
||||
|
||||
return r ? r.total : 0;
|
||||
}
|
||||
|
||||
static fetchStatusToLabel(status) {
|
||||
if (status === Resource.FETCH_STATUS_IDLE) return _('Not downloaded');
|
||||
if (status === Resource.FETCH_STATUS_STARTED) return _('Downloading');
|
||||
|
@@ -239,7 +239,7 @@ class Revision extends BaseItem {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'revision_encrypted') {
|
||||
this.logger().info(`Aborted deletion of old revisions for item ${rev.item_id} because one of the revisions is still encrypted`, error);
|
||||
this.logger().info(`Aborted deletion of old revisions for item "${rev.item_id}" (rev "${rev.id}") because one of the revisions is still encrypted`, error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
|
@@ -6,14 +6,14 @@ export default class ViewController {
|
||||
private pluginId_: string;
|
||||
private store_: any;
|
||||
|
||||
constructor(handle: ViewHandle, pluginId: string, store: any) {
|
||||
public constructor(handle: ViewHandle, pluginId: string, store: any) {
|
||||
this.handle_ = handle;
|
||||
this.pluginId_ = pluginId;
|
||||
this.store_ = store;
|
||||
}
|
||||
|
||||
protected get storeView(): any {
|
||||
return this.store_.pluginService.plugins[this.pluginId_].views[this.handle];
|
||||
return this.store_.getState().pluginService.plugins[this.pluginId_].views[this.handle];
|
||||
}
|
||||
|
||||
protected get store(): any {
|
||||
|
@@ -17,13 +17,33 @@ interface CloseResponse {
|
||||
reject: Function;
|
||||
}
|
||||
|
||||
// TODO: Copied from:
|
||||
// packages/app-desktop/gui/ResizableLayout/utils/findItemByKey.ts
|
||||
function findItemByKey(layout: any, key: string): any {
|
||||
if (!layout) throw new Error('Layout cannot be null');
|
||||
|
||||
function recurseFind(item: any): any {
|
||||
if (item.key === key) return item;
|
||||
|
||||
if (item.children) {
|
||||
for (const child of item.children) {
|
||||
const found = recurseFind(child);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return recurseFind(layout);
|
||||
}
|
||||
|
||||
export default class WebviewController extends ViewController {
|
||||
|
||||
private baseDir_: string;
|
||||
private messageListener_: Function = null;
|
||||
private closeResponse_: CloseResponse = null;
|
||||
|
||||
constructor(id: string, pluginId: string, store: any, baseDir: string, containerType: ContainerType) {
|
||||
public constructor(id: string, pluginId: string, store: any, baseDir: string, containerType: ContainerType) {
|
||||
super(id, pluginId, store);
|
||||
this.baseDir_ = toSystemSlashes(baseDir, 'linux');
|
||||
|
||||
@@ -91,6 +111,29 @@ export default class WebviewController extends ViewController {
|
||||
this.messageListener_ = callback;
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// Specific to panels
|
||||
// ---------------------------------------------
|
||||
|
||||
public async show(show: boolean = true): Promise<void> {
|
||||
this.store.dispatch({
|
||||
type: 'MAIN_LAYOUT_SET_ITEM_PROP',
|
||||
itemKey: this.handle,
|
||||
propName: 'visible',
|
||||
propValue: show,
|
||||
});
|
||||
}
|
||||
|
||||
public async hide(): Promise<void> {
|
||||
return this.show(false);
|
||||
}
|
||||
|
||||
public get visible(): boolean {
|
||||
const mainLayout = this.store.getState().mainLayout;
|
||||
const item = findItemByKey(mainLayout, this.handle);
|
||||
return item ? item.visible : false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// Specific to dialogs
|
||||
// ---------------------------------------------
|
||||
|
@@ -17,7 +17,7 @@ export default class JoplinViewsPanels {
|
||||
private store: any;
|
||||
private plugin: Plugin;
|
||||
|
||||
constructor(plugin: Plugin, store: any) {
|
||||
public constructor(plugin: Plugin, store: any) {
|
||||
this.store = store;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export default class JoplinViewsPanels {
|
||||
/**
|
||||
* Creates a new panel
|
||||
*/
|
||||
async create(id: string): Promise<ViewHandle> {
|
||||
public async create(id: string): Promise<ViewHandle> {
|
||||
if (!id) {
|
||||
this.plugin.deprecationNotice('1.5', 'Creating a view without an ID is deprecated. To fix it, change your call to `joplin.views.panels.create("my-unique-id")`');
|
||||
id = `${this.plugin.viewCount}`;
|
||||
@@ -44,22 +44,43 @@ export default class JoplinViewsPanels {
|
||||
/**
|
||||
* Sets the panel webview HTML
|
||||
*/
|
||||
async setHtml(handle: ViewHandle, html: string) {
|
||||
public async setHtml(handle: ViewHandle, html: string) {
|
||||
return this.controller(handle).html = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds and loads a new JS or CSS files into the panel.
|
||||
*/
|
||||
async addScript(handle: ViewHandle, scriptPath: string) {
|
||||
public async addScript(handle: ViewHandle, scriptPath: string) {
|
||||
return this.controller(handle).addScript(scriptPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a message is sent from the webview (using postMessage).
|
||||
*/
|
||||
async onMessage(handle: ViewHandle, callback: Function) {
|
||||
public async onMessage(handle: ViewHandle, callback: Function) {
|
||||
return this.controller(handle).onMessage(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the panel
|
||||
*/
|
||||
public async show(handle: ViewHandle, show: boolean = true): Promise<void> {
|
||||
await this.controller(handle).show(show);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
public async hide(handle: ViewHandle): Promise<void> {
|
||||
await this.show(handle, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
public async visible(handle: ViewHandle): Promise<boolean> {
|
||||
return this.controller(handle).visible;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -187,8 +187,10 @@ class ReportService {
|
||||
if (status === Resource.FETCH_STATUS_DONE) {
|
||||
const downloadedButEncryptedBlobCount = await Resource.downloadedButEncryptedBlobCount();
|
||||
const downloadedCount = await Resource.downloadStatusCounts(Resource.FETCH_STATUS_DONE);
|
||||
const createdLocallyCount = await Resource.createdLocallyCount();
|
||||
section.body.push(_('%s: %d', _('Downloaded and decrypted'), downloadedCount - downloadedButEncryptedBlobCount));
|
||||
section.body.push(_('%s: %d', _('Downloaded and encrypted'), downloadedButEncryptedBlobCount));
|
||||
section.body.push(_('%s: %d', _('Created locally'), createdLocallyCount));
|
||||
} else {
|
||||
const count = await Resource.downloadStatusCounts(status);
|
||||
section.body.push(_('%s: %d', Resource.fetchStatusToLabel(status), count));
|
||||
|
76
packages/server/package-lock.json
generated
76
packages/server/package-lock.json
generated
@@ -1262,6 +1262,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/highlight.js": {
|
||||
"version": "9.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.12.4.tgz",
|
||||
"integrity": "sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/http-assert": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.1.tgz",
|
||||
@@ -1331,6 +1337,29 @@
|
||||
"@types/koa": "*"
|
||||
}
|
||||
},
|
||||
"@types/linkify-it": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.0.tgz",
|
||||
"integrity": "sha512-x9OaQQTb1N2hPZ/LWJsqushexDvz7NgzuZxiRmZio44WPuolTZNHDBCrOxCzRVOMwamJRO2dWax5NbygOf1OTQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/markdown-it": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.0.0.tgz",
|
||||
"integrity": "sha512-+RJNprPSIcEUBzj3nx8WYwRsDdAKF6/dG932OleYKbTqBSJ7VvZK0JbPKeEpIYxoniUhgvgyZjO4vlCd4mFTdw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/highlight.js": "^9.7.0",
|
||||
"@types/linkify-it": "*",
|
||||
"@types/mdurl": "*"
|
||||
}
|
||||
},
|
||||
"@types/mdurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz",
|
||||
"integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mime": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
|
||||
@@ -2652,6 +2681,11 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"entities": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
|
||||
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
|
||||
},
|
||||
"error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
@@ -5562,6 +5596,14 @@
|
||||
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
|
||||
"dev": true
|
||||
},
|
||||
"linkify-it": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz",
|
||||
"integrity": "sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==",
|
||||
"requires": {
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
@@ -5632,6 +5674,30 @@
|
||||
"object-visit": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "12.0.4",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.0.4.tgz",
|
||||
"integrity": "sha512-34RwOXZT8kyuOJy25oJNJoulO8L0bTHYWXcdZBYZqFnjIy3NgjeoM3FmPXIOFQ26/lSHYMr8oc62B6adxXcb3Q==",
|
||||
"requires": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "~2.1.0",
|
||||
"linkify-it": "^3.0.1",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"mdurl": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
||||
"integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
|
||||
},
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -7177,11 +7243,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
|
||||
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
|
||||
},
|
||||
"sqlite3": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.1.0.tgz",
|
||||
@@ -7583,6 +7644,11 @@
|
||||
"integrity": "sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ==",
|
||||
"dev": true
|
||||
},
|
||||
"uc.micro": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.12.2",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.12.2.tgz",
|
||||
|
@@ -23,6 +23,7 @@
|
||||
"html-entities": "^1.3.1",
|
||||
"knex": "^0.19.4",
|
||||
"koa": "^2.8.1",
|
||||
"markdown-it": "^12.0.4",
|
||||
"mustache": "^3.1.0",
|
||||
"nanoid": "^2.1.1",
|
||||
"nodemon": "^2.0.6",
|
||||
@@ -37,6 +38,7 @@
|
||||
"@types/fs-extra": "^8.0.0",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/koa": "^2.0.49",
|
||||
"@types/markdown-it": "^12.0.0",
|
||||
"@types/mustache": "^0.8.32",
|
||||
"@types/yargs": "^13.0.2",
|
||||
"jest": "^26.6.3",
|
||||
|
@@ -1,10 +1,9 @@
|
||||
// Allows displaying error stack traces with TypeScript file paths
|
||||
require('source-map-support').install();
|
||||
|
||||
import * as Koa from 'koa';
|
||||
import routes from './routes/routes';
|
||||
import { ErrorNotFound } from './utils/errors';
|
||||
import * as fs from 'fs-extra';
|
||||
import { argv } from 'yargs';
|
||||
import { routeResponseFormat, findMatchingRoute, Response, RouteResponseFormat, MatchedRoute } from './utils/routeUtils';
|
||||
import Logger, { LoggerWrapper, TargetType } from '@joplin/lib/Logger';
|
||||
import config, { initConfig, baseUrl } from './config';
|
||||
import configDev from './config-dev';
|
||||
@@ -14,9 +13,16 @@ import { createDb, dropDb } from './tools/dbTools';
|
||||
import { dropTables, connectDb, disconnectDb, migrateDb, waitForConnection } from './db';
|
||||
import modelFactory from './models/factory';
|
||||
import controllerFactory from './controllers/factory';
|
||||
import { AppContext, Config } from './utils/types';
|
||||
import { AppContext, Config, Env } from './utils/types';
|
||||
import FsDriverNode from '@joplin/lib/fs-driver-node';
|
||||
import mustacheService, { isView, View } from './services/MustacheService';
|
||||
import routeHandler from './middleware/routeHandler';
|
||||
import notificationHandler from './middleware/notificationHandler';
|
||||
import ownerHandler from './middleware/ownerHandler';
|
||||
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
||||
shimInit();
|
||||
|
||||
const env: Env = argv.env as Env || Env.Prod;
|
||||
|
||||
interface Configs {
|
||||
[name: string]: Config;
|
||||
@@ -28,13 +34,6 @@ const configs: Configs = {
|
||||
buildTypes: configBuildTypes,
|
||||
};
|
||||
|
||||
require('source-map-support').install();
|
||||
|
||||
const env: string = argv.env as string || 'prod';
|
||||
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
||||
shimInit();
|
||||
|
||||
let appLogger_: LoggerWrapper = null;
|
||||
|
||||
function appLogger(): LoggerWrapper {
|
||||
@@ -46,60 +45,13 @@ function appLogger(): LoggerWrapper {
|
||||
|
||||
const app = new Koa();
|
||||
|
||||
app.use(async (ctx: Koa.Context) => {
|
||||
appLogger().info(`${ctx.request.method} ${ctx.path}`);
|
||||
|
||||
const match: MatchedRoute = null;
|
||||
|
||||
try {
|
||||
const match = findMatchingRoute(ctx.path, routes);
|
||||
|
||||
if (match) {
|
||||
const responseObject = await match.route.exec(match.subPath, ctx);
|
||||
|
||||
if (responseObject instanceof Response) {
|
||||
ctx.response = responseObject.response;
|
||||
} else if (isView(responseObject)) {
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = await mustacheService.renderView(responseObject);
|
||||
} else {
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = responseObject;
|
||||
}
|
||||
} else {
|
||||
throw new ErrorNotFound();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.httpCode >= 400 && error.httpCode < 500) {
|
||||
appLogger().error(`${error.httpCode}: ` + `${ctx.request.method} ${ctx.path}` + ` : ${error.message}`);
|
||||
} else {
|
||||
appLogger().error(error);
|
||||
}
|
||||
|
||||
ctx.response.status = error.httpCode ? error.httpCode : 500;
|
||||
|
||||
const responseFormat = routeResponseFormat(match, ctx.path);
|
||||
|
||||
if (responseFormat === RouteResponseFormat.Html) {
|
||||
ctx.response.set('Content-Type', 'text/html');
|
||||
const view: View = {
|
||||
name: 'error',
|
||||
path: 'index/error',
|
||||
content: {
|
||||
error,
|
||||
stack: env === 'dev' ? error.stack : '',
|
||||
},
|
||||
};
|
||||
ctx.response.body = await mustacheService.renderView(view);
|
||||
} else { // JSON
|
||||
ctx.response.set('Content-Type', 'application/json');
|
||||
const r: any = { error: error.message };
|
||||
if (env === 'dev' && error.stack) r.stack = error.stack;
|
||||
if (error.code) r.code = error.code;
|
||||
ctx.response.body = r;
|
||||
}
|
||||
}
|
||||
});
|
||||
// Note: the order of middlewares is important. For example, ownerHandler
|
||||
// loads the user, which is then used by notificationHandler. And finally
|
||||
// routeHandler uses data from both previous middlewares. It would be good to
|
||||
// layout these dependencies in code but not clear how to do this.
|
||||
app.use(ownerHandler);
|
||||
app.use(notificationHandler);
|
||||
app.use(routeHandler);
|
||||
|
||||
async function main() {
|
||||
const configObject: Config = configs[env];
|
||||
@@ -151,9 +103,11 @@ async function main() {
|
||||
delete connectionCheckLogInfo.connection;
|
||||
|
||||
appLogger().info('Connection check:', connectionCheckLogInfo);
|
||||
appContext.env = env;
|
||||
appContext.db = connectionCheck.connection;
|
||||
appContext.models = modelFactory(appContext.db, baseUrl());
|
||||
appContext.controllers = controllerFactory(appContext.models);
|
||||
appContext.appLogger = appLogger;
|
||||
|
||||
appLogger().info('Migrating database...');
|
||||
await migrateDb(appContext.db);
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { Session, User } from '../../db';
|
||||
import { checkPassword } from '../../utils/auth';
|
||||
import { Session } from '../../db';
|
||||
import { ErrorForbidden } from '../../utils/errors';
|
||||
import uuidgen from '../../utils/uuidgen';
|
||||
import BaseController from '../BaseController';
|
||||
@@ -8,12 +7,10 @@ export default class SessionController extends BaseController {
|
||||
|
||||
public async authenticate(email: string, password: string): Promise<Session> {
|
||||
const userModel = this.models.user();
|
||||
const user: User = await userModel.loadByEmail(email);
|
||||
const user = await userModel.login(email, password);
|
||||
if (!user) throw new ErrorForbidden('Invalid username or password');
|
||||
if (!checkPassword(password, user.password)) throw new ErrorForbidden('Invalid username or password');
|
||||
const session: Session = { id: uuidgen(), user_id: user.id };
|
||||
const sessionModel = this.models.session();
|
||||
return sessionModel.save(session, { isNew: true });
|
||||
return this.models.session().save(session, { isNew: true });
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import IndexHomeController from './index/HomeController';
|
||||
import IndexProfileController from './index/ProfileController';
|
||||
import IndexUserController from './index/UserController';
|
||||
import IndexFileController from './index/FileController';
|
||||
import IndexNotificationController from './index/NotificationController';
|
||||
|
||||
export class Controllers {
|
||||
|
||||
@@ -53,6 +54,10 @@ export class Controllers {
|
||||
return new IndexFileController(this.models_);
|
||||
}
|
||||
|
||||
public indexNotifications() {
|
||||
return new IndexNotificationController(this.models_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default function(models: Models) {
|
||||
|
@@ -35,11 +35,12 @@ export default class FileController extends BaseController {
|
||||
const parent: File = await fileModel.load(parentTemp.id);
|
||||
const paginatedFiles = await fileModel.childrens(parent.id, pagination);
|
||||
const pageCount = Math.ceil((await fileModel.childrenCount(parent.id)) / pagination.limit);
|
||||
|
||||
const parentBaseUrl = await fileModel.fileUrl(parent.id);
|
||||
const paginationLinks = createPaginationLinks(pagination.page, pageCount, setQueryParameters(parentBaseUrl, { ...baseUrlQuery, 'page': 'PAGE_NUMBER' }));
|
||||
|
||||
async function fileToViewItem(file: File): Promise<any> {
|
||||
const filePath = await fileModel.itemFullPath(file);
|
||||
async function fileToViewItem(file: File, fileFullPaths: Record<string, string>): Promise<any> {
|
||||
const filePath = fileFullPaths[file.id];
|
||||
|
||||
let url = `${baseUrl()}/files/${filePath}`;
|
||||
if (!file.is_directory) {
|
||||
@@ -60,26 +61,30 @@ export default class FileController extends BaseController {
|
||||
|
||||
const files: any[] = [];
|
||||
|
||||
const fileFullPaths = await fileModel.itemFullPaths(paginatedFiles.items);
|
||||
|
||||
if (parent.id !== root.id) {
|
||||
const p = await fileModel.load(parent.parent_id);
|
||||
files.push({
|
||||
...await fileToViewItem(p),
|
||||
...await fileToViewItem(p, await fileModel.itemFullPaths([p])),
|
||||
icon: 'fas fa-arrow-left',
|
||||
name: '..',
|
||||
});
|
||||
}
|
||||
|
||||
for (const file of paginatedFiles.items) {
|
||||
files.push(await fileToViewItem(file));
|
||||
files.push(await fileToViewItem(file, fileFullPaths));
|
||||
}
|
||||
|
||||
const view: View = defaultView('files', owner);
|
||||
const view: View = defaultView('files');
|
||||
view.content.paginatedFiles = { ...paginatedFiles, items: files };
|
||||
view.content.paginationLinks = paginationLinks;
|
||||
view.content.postUrl = `${baseUrl()}/files`;
|
||||
view.content.parentId = parent.id;
|
||||
view.cssFiles = ['index/files'];
|
||||
view.partials.push('pagination');
|
||||
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
|
@@ -5,8 +5,8 @@ import defaultView from '../../utils/defaultView';
|
||||
export default class HomeController extends BaseController {
|
||||
|
||||
public async getIndex(sessionId: string): Promise<View> {
|
||||
const owner = await this.initSession(sessionId);
|
||||
return defaultView('home', owner);
|
||||
await this.initSession(sessionId);
|
||||
return defaultView('home');
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,23 @@
|
||||
import BaseController from '../BaseController';
|
||||
import { Notification } from '../../db';
|
||||
import { ErrorNotFound } from '../../utils/errors';
|
||||
|
||||
export default class NotificationController extends BaseController {
|
||||
|
||||
public async patchOne(sessionId: string, notificationId: string, notification: Notification): Promise<void> {
|
||||
const owner = await this.initSession(sessionId);
|
||||
const model = this.models.notification({ userId: owner.id });
|
||||
const existingNotification = await model.load(notificationId);
|
||||
if (!existingNotification) throw new ErrorNotFound();
|
||||
|
||||
|
||||
console.info('aaaaaaa', notification);
|
||||
const toSave: Notification = {};
|
||||
if ('read' in notification) toSave.read = notification.read;
|
||||
if (!Object.keys(toSave).length) return;
|
||||
|
||||
toSave.id = notificationId;
|
||||
await model.save(toSave);
|
||||
}
|
||||
|
||||
}
|
@@ -17,7 +17,7 @@ export default class ProfileController extends BaseController {
|
||||
public async getIndex(sessionId: string, user: User = null, error: any = null): Promise<View> {
|
||||
const owner = await this.initSession(sessionId);
|
||||
|
||||
const view: View = defaultView('profile', owner);
|
||||
const view: View = defaultView('profile');
|
||||
view.content.user = user ? user : owner;
|
||||
view.content.error = error;
|
||||
view.partials.push('errorBanner');
|
||||
|
@@ -7,11 +7,11 @@ import { baseUrl } from '../../config';
|
||||
export default class UserController extends BaseController {
|
||||
|
||||
public async getIndex(sessionId: string): Promise<View> {
|
||||
const owner = await this.initSession(sessionId);
|
||||
const owner = await this.initSession(sessionId, true);
|
||||
const userModel = this.models.user({ userId: owner.id });
|
||||
const users = await userModel.all();
|
||||
|
||||
const view: View = defaultView('users', owner);
|
||||
const view: View = defaultView('users');
|
||||
view.content.users = users;
|
||||
return view;
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export default class UserController extends BaseController {
|
||||
user = userIdOrString as User;
|
||||
}
|
||||
|
||||
const view: View = defaultView('user', owner);
|
||||
const view: View = defaultView('user');
|
||||
view.content.user = user;
|
||||
view.content.isNew = isNew;
|
||||
view.content.buttonTitle = isNew ? 'Create user' : 'Update profile';
|
||||
|
@@ -19,6 +19,9 @@ const logger = Logger.create('db');
|
||||
const migrationDir = `${__dirname}/migrations`;
|
||||
const sqliteDbDir = pathUtils.dirname(__dirname);
|
||||
|
||||
export const defaultAdminEmail = 'admin@localhost';
|
||||
export const defaultAdminPassword = 'admin';
|
||||
|
||||
export type DbConnection = Knex;
|
||||
|
||||
export interface DbConfigConnection {
|
||||
@@ -128,6 +131,17 @@ export async function dropTables(db: DbConnection): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function truncateTables(db: DbConnection): Promise<void> {
|
||||
for (const tableName of allTableNames()) {
|
||||
try {
|
||||
await db(tableName).truncate();
|
||||
} catch (error) {
|
||||
if (isNoSuchTableError(error)) continue;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isNoSuchTableError(error: any): boolean {
|
||||
if (error) {
|
||||
// Postgres error: 42P01: undefined_table
|
||||
@@ -181,6 +195,11 @@ export enum ItemAddressingType {
|
||||
Path,
|
||||
}
|
||||
|
||||
export enum NotificationLevel {
|
||||
Important = 10,
|
||||
Normal = 20,
|
||||
}
|
||||
|
||||
export enum ItemType {
|
||||
File = 1,
|
||||
User,
|
||||
@@ -261,6 +280,15 @@ export interface ApiClient extends WithDates, WithUuid {
|
||||
secret?: string;
|
||||
}
|
||||
|
||||
export interface Notification extends WithDates, WithUuid {
|
||||
owner_id?: Uuid;
|
||||
level?: NotificationLevel;
|
||||
key?: string;
|
||||
message?: string;
|
||||
read?: number;
|
||||
canBeDismissed?: number;
|
||||
}
|
||||
|
||||
export const databaseSchema: DatabaseTables = {
|
||||
users: {
|
||||
id: { type: 'string' },
|
||||
@@ -320,5 +348,16 @@ export const databaseSchema: DatabaseTables = {
|
||||
updated_time: { type: 'string' },
|
||||
created_time: { type: 'string' },
|
||||
},
|
||||
notifications: {
|
||||
id: { type: 'string' },
|
||||
owner_id: { type: 'string' },
|
||||
level: { type: 'number' },
|
||||
key: { type: 'string' },
|
||||
message: { type: 'string' },
|
||||
read: { type: 'number' },
|
||||
canBeDismissed: { type: 'number' },
|
||||
updated_time: { type: 'string' },
|
||||
created_time: { type: 'string' },
|
||||
},
|
||||
};
|
||||
// AUTO-GENERATED-TYPES
|
||||
|
51
packages/server/src/middleware/notificationHandler.ts
Normal file
51
packages/server/src/middleware/notificationHandler.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { AppContext, KoaNext, NotificationView } from '../utils/types';
|
||||
import { isApiRequest } from '../utils/requestUtils';
|
||||
import { defaultAdminEmail, defaultAdminPassword, NotificationLevel } from '../db';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import * as MarkdownIt from 'markdown-it';
|
||||
|
||||
const logger = Logger.create('notificationHandler');
|
||||
|
||||
export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
|
||||
ctx.notifications = [];
|
||||
|
||||
try {
|
||||
if (isApiRequest(ctx)) return next();
|
||||
|
||||
const user = ctx.owner;
|
||||
if (!user) return next();
|
||||
|
||||
const notificationModel = ctx.models.notification({ userId: user.id });
|
||||
|
||||
if (user.is_admin) {
|
||||
const defaultAdmin = await ctx.models.user().login(defaultAdminEmail, defaultAdminPassword);
|
||||
|
||||
if (defaultAdmin) {
|
||||
await notificationModel.add(
|
||||
'change_admin_password',
|
||||
NotificationLevel.Important,
|
||||
_('The default admin password is insecure and has not been changed! [Change it now](%s)', await ctx.models.user().profileUrl())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const markdownIt = new MarkdownIt();
|
||||
const notifications = await notificationModel.allUnreadByUserId(user.id);
|
||||
const views: NotificationView[] = [];
|
||||
for (const n of notifications) {
|
||||
views.push({
|
||||
id: n.id,
|
||||
messageHtml: markdownIt.render(n.message),
|
||||
level: n.level === NotificationLevel.Important ? 'warning' : 'info',
|
||||
closeUrl: notificationModel.closeUrl(n.id),
|
||||
});
|
||||
}
|
||||
|
||||
ctx.notifications = views;
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
17
packages/server/src/middleware/ownerHandler.ts
Normal file
17
packages/server/src/middleware/ownerHandler.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { AppContext, KoaNext } from '../utils/types';
|
||||
import { isApiRequest, contextSessionId } from '../utils/requestUtils';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
|
||||
const logger = Logger.create('loggedInUserHandler');
|
||||
|
||||
export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
|
||||
try {
|
||||
if (isApiRequest(ctx)) return next();
|
||||
const sessionId = contextSessionId(ctx);
|
||||
ctx.owner = await ctx.models.session().sessionUser(sessionId);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
63
packages/server/src/middleware/routeHandler.ts
Normal file
63
packages/server/src/middleware/routeHandler.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import routes from '../routes/routes';
|
||||
import { ErrorNotFound } from '../utils/errors';
|
||||
import { routeResponseFormat, findMatchingRoute, Response, RouteResponseFormat, MatchedRoute } from '../utils/routeUtils';
|
||||
import { AppContext, Env } from '../utils/types';
|
||||
import mustacheService, { isView, View } from '../services/MustacheService';
|
||||
|
||||
export default async function(ctx: AppContext) {
|
||||
ctx.appLogger().info(`${ctx.request.method} ${ctx.path}`);
|
||||
|
||||
const match: MatchedRoute = null;
|
||||
|
||||
try {
|
||||
const match = findMatchingRoute(ctx.path, routes);
|
||||
|
||||
if (match) {
|
||||
const responseObject = await match.route.exec(match.subPath, ctx);
|
||||
|
||||
if (responseObject instanceof Response) {
|
||||
ctx.response = responseObject.response;
|
||||
} else if (isView(responseObject)) {
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = await mustacheService.renderView(responseObject, {
|
||||
notifications: ctx.notifications || [],
|
||||
owner: ctx.owner,
|
||||
});
|
||||
} else {
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = responseObject;
|
||||
}
|
||||
} else {
|
||||
throw new ErrorNotFound();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.httpCode >= 400 && error.httpCode < 500) {
|
||||
ctx.appLogger().error(`${error.httpCode}: ` + `${ctx.request.method} ${ctx.path}` + ` : ${error.message}`);
|
||||
} else {
|
||||
ctx.appLogger().error(error);
|
||||
}
|
||||
|
||||
ctx.response.status = error.httpCode ? error.httpCode : 500;
|
||||
|
||||
const responseFormat = routeResponseFormat(match, ctx.path);
|
||||
|
||||
if (responseFormat === RouteResponseFormat.Html) {
|
||||
ctx.response.set('Content-Type', 'text/html');
|
||||
const view: View = {
|
||||
name: 'error',
|
||||
path: 'index/error',
|
||||
content: {
|
||||
error,
|
||||
stack: ctx.env === Env.Dev ? error.stack : '',
|
||||
},
|
||||
};
|
||||
ctx.response.body = await mustacheService.renderView(view);
|
||||
} else { // JSON
|
||||
ctx.response.set('Content-Type', 'application/json');
|
||||
const r: any = { error: error.message };
|
||||
if (ctx.env === Env.Dev && error.stack) r.stack = error.stack;
|
||||
if (error.code) r.code = error.code;
|
||||
ctx.response.body = r;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
import * as Knex from 'knex';
|
||||
import { DbConnection } from '../db';
|
||||
import { DbConnection, defaultAdminEmail, defaultAdminPassword } from '../db';
|
||||
import { hashPassword } from '../utils/auth';
|
||||
import uuidgen from '../utils/uuidgen';
|
||||
|
||||
@@ -97,8 +97,8 @@ export async function up(db: DbConnection): Promise<any> {
|
||||
|
||||
await db('users').insert({
|
||||
id: adminId,
|
||||
email: 'admin@localhost',
|
||||
password: hashPassword('admin'),
|
||||
email: defaultAdminEmail,
|
||||
password: hashPassword(defaultAdminPassword),
|
||||
full_name: 'Admin',
|
||||
is_admin: 1,
|
||||
updated_time: now,
|
||||
|
@@ -0,0 +1,24 @@
|
||||
import * as Knex from 'knex';
|
||||
import { DbConnection } from '../db';
|
||||
|
||||
export async function up(db: DbConnection): Promise<any> {
|
||||
await db.schema.createTable('notifications', function(table: Knex.CreateTableBuilder) {
|
||||
table.string('id', 32).unique().primary().notNullable();
|
||||
table.string('owner_id', 32).notNullable();
|
||||
table.integer('level').notNullable();
|
||||
table.text('key', 'string').notNullable();
|
||||
table.text('message', 'mediumtext').notNullable();
|
||||
table.integer('read').defaultTo(0).notNullable();
|
||||
table.integer('canBeDismissed').defaultTo(1).notNullable();
|
||||
table.bigInteger('updated_time').notNullable();
|
||||
table.bigInteger('created_time').notNullable();
|
||||
});
|
||||
|
||||
await db.schema.alterTable('notifications', function(table: Knex.CreateTableBuilder) {
|
||||
table.unique(['owner_id', 'key']);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(db: DbConnection): Promise<any> {
|
||||
await db.schema.dropTable('notifications');
|
||||
}
|
@@ -1,11 +1,11 @@
|
||||
import { WithDates, WithUuid, File, User, Session, Permission, databaseSchema, ApiClient, DbConnection, Change, ItemType, ChangeType } from '../db';
|
||||
import { WithDates, WithUuid, File, User, Session, Permission, databaseSchema, ApiClient, DbConnection, Change, ItemType, ChangeType, Notification } from '../db';
|
||||
import TransactionHandler from '../utils/TransactionHandler';
|
||||
import uuidgen from '../utils/uuidgen';
|
||||
import { ErrorUnprocessableEntity, ErrorBadRequest } from '../utils/errors';
|
||||
import { Models } from './factory';
|
||||
|
||||
export type AnyItemType = File | User | Session | Permission | ApiClient | Change;
|
||||
export type AnyItemTypes = File[] | User[] | Session[] | Permission[] | ApiClient[] | Change[];
|
||||
export type AnyItemType = File | User | Session | Permission | ApiClient | Change | Notification;
|
||||
export type AnyItemTypes = File[] | User[] | Session[] | Permission[] | ApiClient[] | Change[] | Notification[];
|
||||
|
||||
export interface ModelOptions {
|
||||
userId?: string;
|
||||
|
@@ -62,6 +62,36 @@ export default class FileModel extends BaseModel {
|
||||
return null; // Not a special dir
|
||||
}
|
||||
|
||||
public async itemFullPaths(items: File[]): Promise<Record<string, string>> {
|
||||
const output: Record<string, string> = {};
|
||||
|
||||
const itemCache: Record<string, File> = {};
|
||||
|
||||
await this.withTransaction(async () => {
|
||||
for (const item of items) {
|
||||
const segments: string[] = [];
|
||||
let current: File = item;
|
||||
|
||||
while (current) {
|
||||
if (current.is_root) break;
|
||||
segments.splice(0, 0, current.name);
|
||||
|
||||
if (current.parent_id) {
|
||||
const id = current.parent_id;
|
||||
current = itemCache[id] ? itemCache[id] : await this.load(id);
|
||||
itemCache[id] = current;
|
||||
} else {
|
||||
current = null;
|
||||
}
|
||||
}
|
||||
|
||||
output[item.id] = segments.length ? (`root:/${segments.join('/')}:`) : 'root';
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public async itemFullPath(item: File): Promise<string> {
|
||||
const segments: string[] = [];
|
||||
while (item) {
|
||||
@@ -348,8 +378,8 @@ export default class FileModel extends BaseModel {
|
||||
public async childrenCount(id: string): Promise<number> {
|
||||
const parent = await this.load(id);
|
||||
await this.checkCanReadPermissions(parent);
|
||||
const r = await this.db(this.tableName).select('id').where('parent_id', id).count('id', { as: 'total' });
|
||||
return r.length && r[0].total ? r[0].total : 0;
|
||||
const r: any = await this.db(this.tableName).where('parent_id', id).count('id', { as: 'total' }).first();
|
||||
return r.total;
|
||||
}
|
||||
|
||||
public async childrens(id: string, pagination: Pagination): Promise<PaginatedFiles> {
|
||||
|
49
packages/server/src/models/NotificationModel.test.ts
Normal file
49
packages/server/src/models/NotificationModel.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { createUserAndSession, beforeAllDb, afterAllDb, beforeEachDb, models, expectThrow } from '../utils/testUtils';
|
||||
import { Notification, NotificationLevel } from '../db';
|
||||
|
||||
describe('NotificationModel', function() {
|
||||
|
||||
beforeAll(async () => {
|
||||
await beforeAllDb('NotificationModel');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllDb();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await beforeEachDb();
|
||||
});
|
||||
|
||||
test('should require a user to create the notification', async function() {
|
||||
await expectThrow(async () => models().notification().add('test', NotificationLevel.Normal, 'test'));
|
||||
});
|
||||
|
||||
test('should create a notification', async function() {
|
||||
const { user } = await createUserAndSession(1, true);
|
||||
const model = models().notification({ userId: user.id });
|
||||
await model.add('test', NotificationLevel.Important, 'testing');
|
||||
const n: Notification = await model.loadByKey('test');
|
||||
expect(n.key).toBe('test');
|
||||
expect(n.message).toBe('testing');
|
||||
expect(n.level).toBe(NotificationLevel.Important);
|
||||
});
|
||||
|
||||
test('should create only one notification per key', async function() {
|
||||
const { user } = await createUserAndSession(1, true);
|
||||
const model = models().notification({ userId: user.id });
|
||||
await model.add('test', NotificationLevel.Important, 'testing');
|
||||
await model.add('test', NotificationLevel.Important, 'testing');
|
||||
expect((await model.all()).length).toBe(1);
|
||||
});
|
||||
|
||||
test('should mark a notification as read', async function() {
|
||||
const { user } = await createUserAndSession(1, true);
|
||||
const model = models().notification({ userId: user.id });
|
||||
await model.add('test', NotificationLevel.Important, 'testing');
|
||||
expect((await model.loadByKey('test')).read).toBe(0);
|
||||
await model.markAsRead('test');
|
||||
expect((await model.loadByKey('test')).read).toBe(1);
|
||||
});
|
||||
|
||||
});
|
51
packages/server/src/models/NotificationModel.ts
Normal file
51
packages/server/src/models/NotificationModel.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Notification, NotificationLevel, Uuid } from '../db';
|
||||
import BaseModel from './BaseModel';
|
||||
|
||||
export default class NotificationModel extends BaseModel {
|
||||
|
||||
protected get tableName(): string {
|
||||
return 'notifications';
|
||||
}
|
||||
|
||||
public async add(key: string, level: NotificationLevel, message: string): Promise<Notification> {
|
||||
const n: Notification = await this.loadByKey(key);
|
||||
if (n) return n;
|
||||
return this.save({ key, message, level, owner_id: this.userId });
|
||||
}
|
||||
|
||||
public async markAsRead(key: string): Promise<void> {
|
||||
await this.db(this.tableName)
|
||||
.update({ read: 1 })
|
||||
.where('key', '=', key)
|
||||
.andWhere('owner_id', '=', this.userId);
|
||||
}
|
||||
|
||||
public loadByKey(key: string): Promise<Notification> {
|
||||
return this.db(this.tableName)
|
||||
.select(this.defaultFields)
|
||||
.where('key', '=', key)
|
||||
.andWhere('owner_id', '=', this.userId)
|
||||
.first();
|
||||
}
|
||||
|
||||
public allUnreadByUserId(userId: Uuid): Promise<Notification[]> {
|
||||
return this.db(this.tableName)
|
||||
.select(this.defaultFields)
|
||||
.where('owner_id', '=', userId)
|
||||
.andWhere('read', '=', 0)
|
||||
.orderBy('updated_time', 'asc');
|
||||
}
|
||||
|
||||
public closeUrl(id: Uuid): string {
|
||||
return `${this.baseUrl}/notifications/${id}`;
|
||||
}
|
||||
|
||||
public load(id: Uuid): Promise<Notification> {
|
||||
return this.db(this.tableName)
|
||||
.select(this.defaultFields)
|
||||
.where({ id: id })
|
||||
.andWhere('owner_id', '=', this.userId)
|
||||
.first();
|
||||
}
|
||||
|
||||
}
|
@@ -14,6 +14,13 @@ export default class UserModel extends BaseModel {
|
||||
return this.db<User>(this.tableName).where(user).first();
|
||||
}
|
||||
|
||||
public async login(email: string, password: string): Promise<User> {
|
||||
const user = await this.loadByEmail(email);
|
||||
if (!user) return null;
|
||||
if (!auth.checkPassword(password, user.password)) return null;
|
||||
return user;
|
||||
}
|
||||
|
||||
public fromApiInput(object: User): User {
|
||||
const user: User = {};
|
||||
|
||||
@@ -64,6 +71,10 @@ export default class UserModel extends BaseModel {
|
||||
return !!s[0].length && !!s[1].length;
|
||||
}
|
||||
|
||||
public async profileUrl(): Promise<string> {
|
||||
return `${this.baseUrl}/profile`;
|
||||
}
|
||||
|
||||
private async checkIsOwnerOrAdmin(userId: string): Promise<void> {
|
||||
if (!this.userId) throw new ErrorForbidden('no user is active');
|
||||
|
||||
|
@@ -62,6 +62,7 @@ import UserModel from './UserModel';
|
||||
import PermissionModel from './PermissionModel';
|
||||
import SessionModel from './SessionModel';
|
||||
import ChangeModel from './ChangeModel';
|
||||
import NotificationModel from './NotificationModel';
|
||||
|
||||
export class Models {
|
||||
|
||||
@@ -96,6 +97,11 @@ export class Models {
|
||||
public change(options: ModelOptions = null) {
|
||||
return new ChangeModel(this.db_, newModelFactory, this.baseUrl_, options);
|
||||
}
|
||||
|
||||
public notification(options: ModelOptions = null) {
|
||||
return new NotificationModel(this.db_, newModelFactory, this.baseUrl_, options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default function newModelFactory(db: DbConnection, baseUrl: string): Models {
|
||||
|
@@ -26,7 +26,7 @@ export interface PaginatedResults {
|
||||
cursor?: string;
|
||||
}
|
||||
|
||||
export const pageMaxSize = 1000;
|
||||
export const pageMaxSize = 100;
|
||||
const defaultOrderField_ = 'updated_time';
|
||||
const defaultOrderDir_ = PaginationOrderDir.DESC;
|
||||
|
||||
|
20
packages/server/src/routes/index/notifications.ts
Normal file
20
packages/server/src/routes/index/notifications.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { SubPath, Route } from '../../utils/routeUtils';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { bodyFields, contextSessionId } from '../../utils/requestUtils';
|
||||
import { ErrorMethodNotAllowed } from '../../utils/errors';
|
||||
|
||||
const route: Route = {
|
||||
|
||||
exec: async function(path: SubPath, ctx: AppContext) {
|
||||
const sessionId = contextSessionId(ctx);
|
||||
|
||||
if (path.id && ctx.method === 'PATCH') {
|
||||
return ctx.controllers.indexNotifications().patchOne(sessionId, path.id, await bodyFields(ctx.req));
|
||||
}
|
||||
|
||||
throw new ErrorMethodNotAllowed();
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default route;
|
@@ -10,6 +10,7 @@ import indexProfileRoute from './index/profile';
|
||||
import indexUsersRoute from './index/users';
|
||||
import indexUserRoute from './index/user';
|
||||
import indexFilesRoute from './index/files';
|
||||
import indexNotificationsRoute from './index/notifications';
|
||||
import defaultRoute from './default';
|
||||
|
||||
const routes: Routes = {
|
||||
@@ -24,6 +25,7 @@ const routes: Routes = {
|
||||
'users': indexUsersRoute,
|
||||
'user': indexUserRoute,
|
||||
'files': indexFilesRoute,
|
||||
'notifications': indexNotificationsRoute,
|
||||
|
||||
'': defaultRoute,
|
||||
};
|
||||
|
@@ -46,7 +46,7 @@ class MustacheService {
|
||||
return output;
|
||||
}
|
||||
|
||||
public async renderView(view: View): Promise<string> {
|
||||
public async renderView(view: View, globalParams: any = null): Promise<string> {
|
||||
const partials = view.partials || [];
|
||||
const cssFiles = this.resolvesFilePaths('css', view.cssFiles || []);
|
||||
const jsFiles = this.resolvesFilePaths('js', view.jsFiles || []);
|
||||
@@ -59,16 +59,22 @@ class MustacheService {
|
||||
|
||||
const filePath = `${config().viewDir}/${view.path}.mustache`;
|
||||
|
||||
globalParams = {
|
||||
...this.defaultLayoutOptions,
|
||||
...globalParams,
|
||||
};
|
||||
|
||||
const contentHtml = Mustache.render(
|
||||
await this.loadTemplateContent(filePath),
|
||||
{
|
||||
...view.content,
|
||||
global: this.defaultLayoutOptions,
|
||||
global: globalParams,
|
||||
},
|
||||
partialContents
|
||||
);
|
||||
|
||||
const layoutView: any = Object.assign({}, this.defaultLayoutOptions, {
|
||||
const layoutView: any = Object.assign({}, {
|
||||
global: globalParams,
|
||||
pageName: view.name,
|
||||
contentHtml: contentHtml,
|
||||
cssFiles: cssFiles,
|
||||
@@ -79,23 +85,6 @@ class MustacheService {
|
||||
return Mustache.render(await this.loadTemplateContent(this.defaultLayoutPath), layoutView, partialContents);
|
||||
}
|
||||
|
||||
// public async render(path: string, view: any, options: RenderOptions = null): Promise<string> {
|
||||
// const partials = options && options.partials ? options.partials : {};
|
||||
// const cssFiles = this.resolvesFilePaths('css', options && options.cssFiles ? options.cssFiles : []);
|
||||
// const jsFiles = this.resolvesFilePaths('js', options && options.jsFiles ? options.jsFiles : []);
|
||||
|
||||
// const filePath = `${config().viewDir}/${path}.mustache`;
|
||||
// const contentHtml = Mustache.render(await this.loadTemplateContent(filePath), { ...view, global: this.defaultLayoutOptions }, partials);
|
||||
|
||||
// const layoutView: any = Object.assign({}, this.defaultLayoutOptions, {
|
||||
// contentHtml: contentHtml,
|
||||
// cssFiles: cssFiles,
|
||||
// jsFiles: jsFiles,
|
||||
// });
|
||||
|
||||
// return Mustache.render(await this.loadTemplateContent(this.defaultLayoutPath), layoutView);
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
const mustacheService = new MustacheService();
|
||||
|
@@ -28,6 +28,7 @@ const config = {
|
||||
'main.files': 'WithDates, WithUuid',
|
||||
'main.api_clients': 'WithDates, WithUuid',
|
||||
'main.changes': 'WithDates, WithUuid',
|
||||
'main.notifications': 'WithDates, WithUuid',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -66,6 +67,7 @@ function createTypeString(table: any) {
|
||||
if (name === 'item_type') type = 'ItemType';
|
||||
if (table.name === 'files' && name === 'content') type = 'Buffer';
|
||||
if (table.name === 'changes' && name === 'type') type = 'ChangeType';
|
||||
if (table.name === 'notifications' && name === 'level') type = 'NotificationLevel';
|
||||
if ((name === 'id' || name.endsWith('_id') || name === 'uuid') && type === 'string') type = 'Uuid';
|
||||
|
||||
colStrings.push(`\t${name}?: ${type};`);
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import { User } from '../db';
|
||||
import { View } from '../services/MustacheService';
|
||||
|
||||
// Populate a View object with some good defaults.
|
||||
export default function(name: string, owner: User = null): View {
|
||||
export default function(name: string): View {
|
||||
return {
|
||||
name: name,
|
||||
path: `index/${name}`,
|
||||
content: {
|
||||
owner,
|
||||
},
|
||||
partials: ['navbar'],
|
||||
content: {},
|
||||
partials: [
|
||||
'navbar',
|
||||
'notifications',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user