You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-23 23:33:01 +02:00
Compare commits
37 Commits
issue-8510
...
issue-8706
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb8b7ac966 | ||
|
|
59f8b43c21 | ||
|
|
5c63eb0913 | ||
|
|
5855748e06 | ||
|
|
4e2d36648e | ||
|
|
6aec75806e | ||
|
|
2e9f93ad9a | ||
|
|
26a967e53c | ||
|
|
2fda252a5e | ||
|
|
831b1ae035 | ||
|
|
f807a0179d | ||
|
|
b3801b333d | ||
|
|
808e175f7f | ||
|
|
03f1d86531 | ||
|
|
b92cb7deb7 | ||
|
|
0edc66da49 | ||
|
|
e96ad7ccfa | ||
|
|
817ef7bbed | ||
|
|
5bd0c9b3a0 | ||
|
|
46d9cd34a8 | ||
|
|
c3e08237fd | ||
|
|
b406f05241 | ||
|
|
2cbee6d8af | ||
|
|
c859ad48c1 | ||
|
|
1141b1c2a1 | ||
|
|
39c118be90 | ||
|
|
f9ac4e112b | ||
|
|
87e51aa8e6 | ||
|
|
41fdc0d44d | ||
|
|
a754a8d772 | ||
|
|
41d0363fd0 | ||
|
|
2a4c7a334e | ||
|
|
df1b0a96f4 | ||
|
|
0030681cb4 | ||
|
|
e7014492c5 | ||
|
|
4804c1c0c3 | ||
|
|
270d96ad07 |
@@ -260,6 +260,7 @@ packages/app-desktop/gui/NoteEditor/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useDropHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useMarkupToHtml.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
||||
@@ -268,13 +269,36 @@ packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.js
|
||||
packages/app-desktop/gui/NoteList/NoteList.js
|
||||
packages/app-desktop/gui/NoteList/NoteList2.js
|
||||
packages/app-desktop/gui/NoteList/NoteListSource.js
|
||||
packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js
|
||||
packages/app-desktop/gui/NoteList/commands/index.js
|
||||
packages/app-desktop/gui/NoteList/types.js
|
||||
packages/app-desktop/gui/NoteList/utils/canManuallySortNotes.js
|
||||
packages/app-desktop/gui/NoteList/utils/defaultLeftToRightListRenderer.js
|
||||
packages/app-desktop/gui/NoteList/utils/defaultListRenderer.js
|
||||
packages/app-desktop/gui/NoteList/utils/getNoteTitleHtml.js
|
||||
packages/app-desktop/gui/NoteList/utils/prepareViewProps.js
|
||||
packages/app-desktop/gui/NoteList/utils/types.js
|
||||
packages/app-desktop/gui/NoteList/utils/useDragAndDrop.js
|
||||
packages/app-desktop/gui/NoteList/utils/useFocusNote.js
|
||||
packages/app-desktop/gui/NoteList/utils/useItemCss.js
|
||||
packages/app-desktop/gui/NoteList/utils/useMoveNote.js
|
||||
packages/app-desktop/gui/NoteList/utils/useOnKeyDown.js
|
||||
packages/app-desktop/gui/NoteList/utils/useOnNoteClick.js
|
||||
packages/app-desktop/gui/NoteList/utils/useRenderedNotes.js
|
||||
packages/app-desktop/gui/NoteList/utils/useScroll.js
|
||||
packages/app-desktop/gui/NoteList/utils/useVisibleRange.test.js
|
||||
packages/app-desktop/gui/NoteList/utils/useVisibleRange.js
|
||||
packages/app-desktop/gui/NoteListControls/NoteListControls.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/focusSearch.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/index.js
|
||||
packages/app-desktop/gui/NoteListItem.js
|
||||
packages/app-desktop/gui/NoteListItem/NoteListItem.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/types.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useItemElement.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useItemEventHandlers.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useRootElement.js
|
||||
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
|
||||
packages/app-desktop/gui/NotePropertiesDialog.js
|
||||
packages/app-desktop/gui/NoteRevisionViewer.js
|
||||
@@ -410,6 +434,7 @@ packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
||||
packages/app-mobile/components/NoteEditor/SelectionFormatting.js
|
||||
@@ -898,6 +923,7 @@ packages/tools/generate-images.js
|
||||
packages/tools/git-changelog.test.js
|
||||
packages/tools/git-changelog.js
|
||||
packages/tools/licenseChecker.js
|
||||
packages/tools/packageJsonLint.js
|
||||
packages/tools/release-android.js
|
||||
packages/tools/release-cli.js
|
||||
packages/tools/release-electron.js
|
||||
@@ -908,6 +934,7 @@ packages/tools/setupNewRelease.js
|
||||
packages/tools/spellcheck.js
|
||||
packages/tools/tagServerLatest.js
|
||||
packages/tools/tool-utils.js
|
||||
packages/tools/update-readme-contributors.js
|
||||
packages/tools/update-readme-download.test.js
|
||||
packages/tools/update-readme-download.js
|
||||
packages/tools/update-readme-sponsors.js
|
||||
|
||||
@@ -119,7 +119,7 @@ module.exports = {
|
||||
'objects': 'always-multiline',
|
||||
'imports': 'always-multiline',
|
||||
'exports': 'always-multiline',
|
||||
'functions': 'never',
|
||||
'functions': 'always-multiline',
|
||||
}],
|
||||
'comma-spacing': ['error', { 'before': false, 'after': true }],
|
||||
'no-trailing-spaces': 'error',
|
||||
@@ -209,7 +209,7 @@ module.exports = {
|
||||
'enums': 'always-multiline',
|
||||
'generics': 'always-multiline',
|
||||
'tuples': 'always-multiline',
|
||||
'functions': 'never',
|
||||
'functions': 'always-multiline',
|
||||
}],
|
||||
'@typescript-eslint/object-curly-spacing': ['error', 'always'],
|
||||
'@typescript-eslint/semi': ['error', 'always'],
|
||||
|
||||
29
.gitignore
vendored
29
.gitignore
vendored
@@ -246,6 +246,7 @@ packages/app-desktop/gui/NoteEditor/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useDropHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useMarkupToHtml.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
||||
@@ -254,13 +255,36 @@ packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.js
|
||||
packages/app-desktop/gui/NoteList/NoteList.js
|
||||
packages/app-desktop/gui/NoteList/NoteList2.js
|
||||
packages/app-desktop/gui/NoteList/NoteListSource.js
|
||||
packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js
|
||||
packages/app-desktop/gui/NoteList/commands/index.js
|
||||
packages/app-desktop/gui/NoteList/types.js
|
||||
packages/app-desktop/gui/NoteList/utils/canManuallySortNotes.js
|
||||
packages/app-desktop/gui/NoteList/utils/defaultLeftToRightListRenderer.js
|
||||
packages/app-desktop/gui/NoteList/utils/defaultListRenderer.js
|
||||
packages/app-desktop/gui/NoteList/utils/getNoteTitleHtml.js
|
||||
packages/app-desktop/gui/NoteList/utils/prepareViewProps.js
|
||||
packages/app-desktop/gui/NoteList/utils/types.js
|
||||
packages/app-desktop/gui/NoteList/utils/useDragAndDrop.js
|
||||
packages/app-desktop/gui/NoteList/utils/useFocusNote.js
|
||||
packages/app-desktop/gui/NoteList/utils/useItemCss.js
|
||||
packages/app-desktop/gui/NoteList/utils/useMoveNote.js
|
||||
packages/app-desktop/gui/NoteList/utils/useOnKeyDown.js
|
||||
packages/app-desktop/gui/NoteList/utils/useOnNoteClick.js
|
||||
packages/app-desktop/gui/NoteList/utils/useRenderedNotes.js
|
||||
packages/app-desktop/gui/NoteList/utils/useScroll.js
|
||||
packages/app-desktop/gui/NoteList/utils/useVisibleRange.test.js
|
||||
packages/app-desktop/gui/NoteList/utils/useVisibleRange.js
|
||||
packages/app-desktop/gui/NoteListControls/NoteListControls.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/focusSearch.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/index.js
|
||||
packages/app-desktop/gui/NoteListItem.js
|
||||
packages/app-desktop/gui/NoteListItem/NoteListItem.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/types.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useItemElement.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useItemEventHandlers.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useRootElement.js
|
||||
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
|
||||
packages/app-desktop/gui/NotePropertiesDialog.js
|
||||
packages/app-desktop/gui/NoteRevisionViewer.js
|
||||
@@ -396,6 +420,7 @@ packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
||||
packages/app-mobile/components/NoteEditor/SelectionFormatting.js
|
||||
@@ -884,6 +909,7 @@ packages/tools/generate-images.js
|
||||
packages/tools/git-changelog.test.js
|
||||
packages/tools/git-changelog.js
|
||||
packages/tools/licenseChecker.js
|
||||
packages/tools/packageJsonLint.js
|
||||
packages/tools/release-android.js
|
||||
packages/tools/release-cli.js
|
||||
packages/tools/release-electron.js
|
||||
@@ -894,6 +920,7 @@ packages/tools/setupNewRelease.js
|
||||
packages/tools/spellcheck.js
|
||||
packages/tools/tagServerLatest.js
|
||||
packages/tools/tool-utils.js
|
||||
packages/tools/update-readme-contributors.js
|
||||
packages/tools/update-readme-download.test.js
|
||||
packages/tools/update-readme-download.js
|
||||
packages/tools/update-readme-sponsors.js
|
||||
|
||||
BIN
Assets/Aide.png
Normal file
BIN
Assets/Aide.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -112,7 +112,7 @@
|
||||
}).then(async function(result) {
|
||||
if (!result.ok) {
|
||||
console.error('Could not create Stripe checkout session', await result.text());
|
||||
alert('The checkout session could not be created. Please contact support@joplincloud.com for support.');
|
||||
alert('The checkout session could not be created. Please contact us on the forum for support.');
|
||||
} else {
|
||||
return result.json();
|
||||
}
|
||||
|
||||
163
README.md
163
README.md
@@ -587,78 +587,93 @@ Thank you to everyone who've contributed to Joplin's source code!
|
||||
<!-- CONTRIBUTORS-TABLE-AUTO-GENERATED -->
|
||||
| | | | | |
|
||||
| :---: | :---: | :---: | :---: | :---: |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/1285584?v=4"/></br>[laurent22](https://github.com/laurent22) | <img width="50" src="https://avatars.githubusercontent.com/u/223439?v=4"/></br>[tessus](https://github.com/tessus) | <img width="50" src="https://avatars.githubusercontent.com/u/2179547?v=4"/></br>[CalebJohn](https://github.com/CalebJohn) | <img width="50" src="https://avatars.githubusercontent.com/u/1732810?v=4"/></br>[mic704b](https://github.com/mic704b) | <img width="50" src="https://avatars.githubusercontent.com/u/995612?v=4"/></br>[roman-r-m](https://github.com/roman-r-m) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/29672555?v=4"/></br>[genneko](https://github.com/genneko) | <img width="50" src="https://avatars.githubusercontent.com/u/63491353?v=4"/></br>[j-krl](https://github.com/j-krl) | <img width="50" src="https://avatars.githubusercontent.com/u/4553672?v=4"/></br>[tanrax](https://github.com/tanrax) | <img width="50" src="https://avatars.githubusercontent.com/u/30305957?v=4"/></br>[naviji](https://github.com/naviji) | <img width="50" src="https://avatars.githubusercontent.com/u/3542031?v=4"/></br>[PackElend](https://github.com/PackElend) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/8701534?v=4"/></br>[rtmkrlv](https://github.com/rtmkrlv) | <img width="50" src="https://avatars.githubusercontent.com/u/10997189?v=4"/></br>[fmrtn](https://github.com/fmrtn) | <img width="50" src="https://avatars.githubusercontent.com/u/4374338?v=4"/></br>[potatogim](https://github.com/potatogim) | <img width="50" src="https://avatars.githubusercontent.com/u/6979755?v=4"/></br>[devonzuegel](https://github.com/devonzuegel) | <img width="50" src="https://avatars.githubusercontent.com/u/26695184?v=4"/></br>[anjulalk](https://github.com/anjulalk) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/16101778?v=4"/></br>[gabcoh](https://github.com/gabcoh) | <img width="50" src="https://avatars.githubusercontent.com/u/10927304?v=4"/></br>[matsest](https://github.com/matsest) | <img width="50" src="https://avatars.githubusercontent.com/u/6319051?v=4"/></br>[abonte](https://github.com/abonte) | <img width="50" src="https://avatars.githubusercontent.com/u/1685517?v=4"/></br>[Abijeet](https://github.com/Abijeet) | <img width="50" src="https://avatars.githubusercontent.com/u/27751740?v=4"/></br>[ishantgupta777](https://github.com/ishantgupta777) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/24863925?v=4"/></br>[JackGruber](https://github.com/JackGruber) | <img width="50" src="https://avatars.githubusercontent.com/u/2063957?v=4"/></br>[Ardakilic](https://github.com/Ardakilic) | <img width="50" src="https://avatars.githubusercontent.com/u/44024553?v=4"/></br>[rabeehrz](https://github.com/rabeehrz) | <img width="50" src="https://avatars.githubusercontent.com/u/35633575?v=4"/></br>[coderrsid](https://github.com/coderrsid) | <img width="50" src="https://avatars.githubusercontent.com/u/208212?v=4"/></br>[foxmask](https://github.com/foxmask) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/6557454?v=4"/></br>[innocuo](https://github.com/innocuo) | <img width="50" src="https://avatars.githubusercontent.com/u/54268438?v=4"/></br>[Rahulm2310](https://github.com/Rahulm2310) | <img width="50" src="https://avatars.githubusercontent.com/u/1904967?v=4"/></br>[readingsnail](https://github.com/readingsnail) | <img width="50" src="https://avatars.githubusercontent.com/u/7415668?v=4"/></br>[mablin7](https://github.com/mablin7) | <img width="50" src="https://avatars.githubusercontent.com/u/3985557?v=4"/></br>[XarisA](https://github.com/XarisA) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/49979415?v=4"/></br>[jonath92](https://github.com/jonath92) | <img width="50" src="https://avatars.githubusercontent.com/u/4237724?v=4"/></br>[alexdevero](https://github.com/alexdevero) | <img width="50" src="https://avatars.githubusercontent.com/u/35904727?v=4"/></br>[Runo-saduwa](https://github.com/Runo-saduwa) | <img width="50" src="https://avatars.githubusercontent.com/u/5365582?v=4"/></br>[marcosvega91](https://github.com/marcosvega91) | <img width="50" src="https://avatars.githubusercontent.com/u/37639389?v=4"/></br>[petrz12](https://github.com/petrz12) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/51550769?v=4"/></br>[rnbastos](https://github.com/rnbastos) | <img width="50" src="https://avatars.githubusercontent.com/u/32396?v=4"/></br>[ProgramFan](https://github.com/ProgramFan) | <img width="50" src="https://avatars.githubusercontent.com/u/4245227?v=4"/></br>[zblesk](https://github.com/zblesk) | <img width="50" src="https://avatars.githubusercontent.com/u/5730052?v=4"/></br>[vsimkus](https://github.com/vsimkus) | <img width="50" src="https://avatars.githubusercontent.com/u/3194829?v=4"/></br>[moltenform](https://github.com/moltenform) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/36989112?v=4"/></br>[nishantwrp](https://github.com/nishantwrp) | <img width="50" src="https://avatars.githubusercontent.com/u/5199995?v=4"/></br>[zuphilip](https://github.com/zuphilip) | <img width="50" src="https://avatars.githubusercontent.com/u/54576074?v=4"/></br>[Rishabh-malhotraa](https://github.com/Rishabh-malhotraa) | <img width="50" src="https://avatars.githubusercontent.com/u/559346?v=4"/></br>[metbril](https://github.com/metbril) | <img width="50" src="https://avatars.githubusercontent.com/u/47623588?v=4"/></br>[WhiredPlanck](https://github.com/WhiredPlanck) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/43657314?v=4"/></br>[milotype](https://github.com/milotype) | <img width="50" src="https://avatars.githubusercontent.com/u/32196447?v=4"/></br>[yaozeye](https://github.com/yaozeye) | <img width="50" src="https://avatars.githubusercontent.com/u/12264626?v=4"/></br>[ylc395](https://github.com/ylc395) | <img width="50" src="https://avatars.githubusercontent.com/u/17768566?v=4"/></br>[RenatoXSR](https://github.com/RenatoXSR) | <img width="50" src="https://avatars.githubusercontent.com/u/54888685?v=4"/></br>[RedDocMD](https://github.com/RedDocMD) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/31567272?v=4"/></br>[q1011](https://github.com/q1011) | <img width="50" src="https://avatars.githubusercontent.com/u/12906090?v=4"/></br>[amitsin6h](https://github.com/amitsin6h) | <img width="50" src="https://avatars.githubusercontent.com/u/628474?v=4"/></br>[Atalanttore](https://github.com/Atalanttore) | <img width="50" src="https://avatars.githubusercontent.com/u/42747216?v=4"/></br>[Mannivu](https://github.com/Mannivu) | <img width="50" src="https://avatars.githubusercontent.com/u/23281486?v=4"/></br>[martonpaulo](https://github.com/martonpaulo) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/390889?v=4"/></br>[mmahmoudian](https://github.com/mmahmoudian) | <img width="50" src="https://avatars.githubusercontent.com/u/4497566?v=4"/></br>[rccavalcanti](https://github.com/rccavalcanti) | <img width="50" src="https://avatars.githubusercontent.com/u/1540054?v=4"/></br>[ShaneKilkelly](https://github.com/ShaneKilkelly) | <img width="50" src="https://avatars.githubusercontent.com/u/7091080?v=4"/></br>[sinkuu](https://github.com/sinkuu) | <img width="50" src="https://avatars.githubusercontent.com/u/6734573?v=4"/></br>[stweil](https://github.com/stweil) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/692072?v=4"/></br>[conyx](https://github.com/conyx) | <img width="50" src="https://avatars.githubusercontent.com/u/49116134?v=4"/></br>[anihm136](https://github.com/anihm136) | <img width="50" src="https://avatars.githubusercontent.com/u/937861?v=4"/></br>[archont00](https://github.com/archont00) | <img width="50" src="https://avatars.githubusercontent.com/u/32770029?v=4"/></br>[bradmcl](https://github.com/bradmcl) | <img width="50" src="https://avatars.githubusercontent.com/u/22592201?v=4"/></br>[tfinnberg](https://github.com/tfinnberg) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/8716226?v=4"/></br>[amandamcg](https://github.com/amandamcg) | <img width="50" src="https://avatars.githubusercontent.com/u/3870964?v=4"/></br>[marcushill](https://github.com/marcushill) | <img width="50" src="https://avatars.githubusercontent.com/u/102242?v=4"/></br>[nathanleiby](https://github.com/nathanleiby) | <img width="50" src="https://avatars.githubusercontent.com/u/226708?v=4"/></br>[RaphaelKimmig](https://github.com/RaphaelKimmig) | <img width="50" src="https://avatars.githubusercontent.com/u/20461071?v=4"/></br>[Vaso3](https://github.com/Vaso3) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/36303913?v=4"/></br>[sensor-freak](https://github.com/sensor-freak) | <img width="50" src="https://avatars.githubusercontent.com/u/63918341?v=4"/></br>[lkiThakur](https://github.com/lkiThakur) | <img width="50" src="https://avatars.githubusercontent.com/u/28987176?v=4"/></br>[infinity052](https://github.com/infinity052) | <img width="50" src="https://avatars.githubusercontent.com/u/21161146?v=4"/></br>[BartBucknill](https://github.com/BartBucknill) | <img width="50" src="https://avatars.githubusercontent.com/u/2494769?v=4"/></br>[mrwulf](https://github.com/mrwulf) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/560571?v=4"/></br>[chrisb86](https://github.com/chrisb86) | <img width="50" src="https://avatars.githubusercontent.com/u/1686759?v=4"/></br>[chrmoritz](https://github.com/chrmoritz) | <img width="50" src="https://avatars.githubusercontent.com/u/58074586?v=4"/></br>[Daeraxa](https://github.com/Daeraxa) | <img width="50" src="https://avatars.githubusercontent.com/u/71190696?v=4"/></br>[Elaborendum](https://github.com/Elaborendum) | <img width="50" src="https://avatars.githubusercontent.com/u/5001259?v=4"/></br>[ethan42411](https://github.com/ethan42411) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/2733783?v=4"/></br>[JOJ0](https://github.com/JOJ0) | <img width="50" src="https://avatars.githubusercontent.com/u/17108695?v=4"/></br>[jalajcodes](https://github.com/jalajcodes) | <img width="50" src="https://avatars.githubusercontent.com/u/238088?v=4"/></br>[jblunck](https://github.com/jblunck) | <img width="50" src="https://avatars.githubusercontent.com/u/3140223?v=4"/></br>[jdrobertso](https://github.com/jdrobertso) | <img width="50" src="https://avatars.githubusercontent.com/u/37297218?v=4"/></br>[Jesssullivan](https://github.com/Jesssullivan) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/339645?v=4"/></br>[jmontane](https://github.com/jmontane) | <img width="50" src="https://avatars.githubusercontent.com/u/69011?v=4"/></br>[johanhammar](https://github.com/johanhammar) | <img width="50" src="https://avatars.githubusercontent.com/u/4168339?v=4"/></br>[solariz](https://github.com/solariz) | <img width="50" src="https://avatars.githubusercontent.com/u/25288?v=4"/></br>[maicki](https://github.com/maicki) | <img width="50" src="https://avatars.githubusercontent.com/u/2136373?v=4"/></br>[mjjzf](https://github.com/mjjzf) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/27608187?v=4"/></br>[rt-oliveira](https://github.com/rt-oliveira) | <img width="50" src="https://avatars.githubusercontent.com/u/2486806?v=4"/></br>[sebastienjust](https://github.com/sebastienjust) | <img width="50" src="https://avatars.githubusercontent.com/u/28362310?v=4"/></br>[sealch](https://github.com/sealch) | <img width="50" src="https://avatars.githubusercontent.com/u/34258070?v=4"/></br>[StarFang208](https://github.com/StarFang208) | <img width="50" src="https://avatars.githubusercontent.com/u/59690052?v=4"/></br>[Subhra264](https://github.com/Subhra264) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/1782292?v=4"/></br>[SubodhDahal](https://github.com/SubodhDahal) | <img width="50" src="https://avatars.githubusercontent.com/u/5912371?v=4"/></br>[TobiasDev](https://github.com/TobiasDev) | <img width="50" src="https://avatars.githubusercontent.com/u/13502069?v=4"/></br>[Whaell](https://github.com/Whaell) | <img width="50" src="https://avatars.githubusercontent.com/u/29891001?v=4"/></br>[jyuvaraj03](https://github.com/jyuvaraj03) | <img width="50" src="https://avatars.githubusercontent.com/u/15380913?v=4"/></br>[kowalskidev](https://github.com/kowalskidev) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/337455?v=4"/></br>[alexchee](https://github.com/alexchee) | <img width="50" src="https://avatars.githubusercontent.com/u/5077221?v=4"/></br>[axq](https://github.com/axq) | <img width="50" src="https://avatars.githubusercontent.com/u/8808502?v=4"/></br>[barbowza](https://github.com/barbowza) | <img width="50" src="https://avatars.githubusercontent.com/u/42007357?v=4"/></br>[eresytter](https://github.com/eresytter) | <img width="50" src="https://avatars.githubusercontent.com/u/4316805?v=4"/></br>[lightray22](https://github.com/lightray22) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/11711053?v=4"/></br>[lscolombo](https://github.com/lscolombo) | <img width="50" src="https://avatars.githubusercontent.com/u/36228623?v=4"/></br>[mrkaato](https://github.com/mrkaato) | <img width="50" src="https://avatars.githubusercontent.com/u/17399340?v=4"/></br>[pf-siedler](https://github.com/pf-siedler) | <img width="50" src="https://avatars.githubusercontent.com/u/17232523?v=4"/></br>[ruuti](https://github.com/ruuti) | <img width="50" src="https://avatars.githubusercontent.com/u/23638148?v=4"/></br>[s1nceri7y](https://github.com/s1nceri7y) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/10117386?v=4"/></br>[kornava](https://github.com/kornava) | <img width="50" src="https://avatars.githubusercontent.com/u/7471938?v=4"/></br>[ShuiHuo](https://github.com/ShuiHuo) | <img width="50" src="https://avatars.githubusercontent.com/u/11596277?v=4"/></br>[ikunya](https://github.com/ikunya) | <img width="50" src="https://avatars.githubusercontent.com/u/8184424?v=4"/></br>[Ahmad45123](https://github.com/Ahmad45123) | <img width="50" src="https://avatars.githubusercontent.com/u/59133880?v=4"/></br>[bedwardly-down](https://github.com/bedwardly-down) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/50335724?v=4"/></br>[dcaveiro](https://github.com/dcaveiro) | <img width="50" src="https://avatars.githubusercontent.com/u/47456195?v=4"/></br>[hexclover](https://github.com/hexclover) | <img width="50" src="https://avatars.githubusercontent.com/u/45535789?v=4"/></br>[2jaeyeol](https://github.com/2jaeyeol) | <img width="50" src="https://avatars.githubusercontent.com/u/25622825?v=4"/></br>[thackeraaron](https://github.com/thackeraaron) | <img width="50" src="https://avatars.githubusercontent.com/u/15862474?v=4"/></br>[aaronxn](https://github.com/aaronxn) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/40672207?v=4"/></br>[xUser5000](https://github.com/xUser5000) | <img width="50" src="https://avatars.githubusercontent.com/u/56785486?v=4"/></br>[iamabhi222](https://github.com/iamabhi222) | <img width="50" src="https://avatars.githubusercontent.com/u/63443657?v=4"/></br>[Aksh-Konda](https://github.com/Aksh-Konda) | <img width="50" src="https://avatars.githubusercontent.com/u/3660978?v=4"/></br>[alanfortlink](https://github.com/alanfortlink) | <img width="50" src="https://avatars.githubusercontent.com/u/53372753?v=4"/></br>[AverageUser2](https://github.com/AverageUser2) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/4056990?v=4"/></br>[afischer211](https://github.com/afischer211) | <img width="50" src="https://avatars.githubusercontent.com/u/26230870?v=4"/></br>[a13xk](https://github.com/a13xk) | <img width="50" src="https://avatars.githubusercontent.com/u/14836659?v=4"/></br>[apankratov](https://github.com/apankratov) | <img width="50" src="https://avatars.githubusercontent.com/u/7045739?v=4"/></br>[teterkin](https://github.com/teterkin) | <img width="50" src="https://avatars.githubusercontent.com/u/215668?v=4"/></br>[avanderberg](https://github.com/avanderberg) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/41290751?v=4"/></br>[serenitatis](https://github.com/serenitatis) | <img width="50" src="https://avatars.githubusercontent.com/u/4408379?v=4"/></br>[lex111](https://github.com/lex111) | <img width="50" src="https://avatars.githubusercontent.com/u/60134194?v=4"/></br>[Alkindi42](https://github.com/Alkindi42) | <img width="50" src="https://avatars.githubusercontent.com/u/7129815?v=4"/></br>[Jumanjii](https://github.com/Jumanjii) | <img width="50" src="https://avatars.githubusercontent.com/u/19962243?v=4"/></br>[AlphaJack](https://github.com/AlphaJack) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/65647302?v=4"/></br>[Lord-Aman](https://github.com/Lord-Aman) | <img width="50" src="https://avatars.githubusercontent.com/u/14096959?v=4"/></br>[richtwin567](https://github.com/richtwin567) | <img width="50" src="https://avatars.githubusercontent.com/u/487182?v=4"/></br>[ajilderda](https://github.com/ajilderda) | <img width="50" src="https://avatars.githubusercontent.com/u/922429?v=4"/></br>[adrynov](https://github.com/adrynov) | <img width="50" src="https://avatars.githubusercontent.com/u/94937?v=4"/></br>[andrewperry](https://github.com/andrewperry) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/5417051?v=4"/></br>[tekdel](https://github.com/tekdel) | <img width="50" src="https://avatars.githubusercontent.com/u/54475686?v=4"/></br>[anshuman9999](https://github.com/anshuman9999) | <img width="50" src="https://avatars.githubusercontent.com/u/25694659?v=4"/></br>[rasklaad](https://github.com/rasklaad) | <img width="50" src="https://avatars.githubusercontent.com/u/17809291?v=4"/></br>[Technik-J](https://github.com/Technik-J) | <img width="50" src="https://avatars.githubusercontent.com/u/498326?v=4"/></br>[Shaxine](https://github.com/Shaxine) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/9095073?v=4"/></br>[antonio-ramadas](https://github.com/antonio-ramadas) | <img width="50" src="https://avatars.githubusercontent.com/u/28067395?v=4"/></br>[heyapoorva](https://github.com/heyapoorva) | <img width="50" src="https://avatars.githubusercontent.com/u/201215?v=4"/></br>[assimd](https://github.com/assimd) | <img width="50" src="https://avatars.githubusercontent.com/u/26827848?v=4"/></br>[Atrate](https://github.com/Atrate) | <img width="50" src="https://avatars.githubusercontent.com/u/60288895?v=4"/></br>[Beowulf2](https://github.com/Beowulf2) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/7034200?v=4"/></br>[bimlas](https://github.com/bimlas) | <img width="50" src="https://avatars.githubusercontent.com/u/47641641?v=4"/></br>[brenobaptista](https://github.com/brenobaptista) | <img width="50" src="https://avatars.githubusercontent.com/u/60824?v=4"/></br>[brttbndr](https://github.com/brttbndr) | <img width="50" src="https://avatars.githubusercontent.com/u/16287077?v=4"/></br>[carlbordum](https://github.com/carlbordum) | <img width="50" src="https://avatars.githubusercontent.com/u/20382?v=4"/></br>[carlosedp](https://github.com/carlosedp) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/105843?v=4"/></br>[chaifeng](https://github.com/chaifeng) | <img width="50" src="https://avatars.githubusercontent.com/u/549349?v=4"/></br>[charles-e](https://github.com/charles-e) | <img width="50" src="https://avatars.githubusercontent.com/u/19870089?v=4"/></br>[cyy5358](https://github.com/cyy5358) | <img width="50" src="https://avatars.githubusercontent.com/u/32337926?v=4"/></br>[Chillu1](https://github.com/Chillu1) | <img width="50" src="https://avatars.githubusercontent.com/u/2348463?v=4"/></br>[Techwolf12](https://github.com/Techwolf12) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/2282880?v=4"/></br>[cloudtrends](https://github.com/cloudtrends) | <img width="50" src="https://avatars.githubusercontent.com/u/17257053?v=4"/></br>[idcristi](https://github.com/idcristi) | <img width="50" src="https://avatars.githubusercontent.com/u/15956322?v=4"/></br>[damienmascre](https://github.com/damienmascre) | <img width="50" src="https://avatars.githubusercontent.com/u/1044056?v=4"/></br>[daniellandau](https://github.com/daniellandau) | <img width="50" src="https://avatars.githubusercontent.com/u/12847693?v=4"/></br>[danil-tolkachev](https://github.com/danil-tolkachev) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/7279100?v=4"/></br>[darshani28](https://github.com/darshani28) | <img width="50" src="https://avatars.githubusercontent.com/u/26189247?v=4"/></br>[daukadolt](https://github.com/daukadolt) | <img width="50" src="https://avatars.githubusercontent.com/u/28535750?v=4"/></br>[NeverMendel](https://github.com/NeverMendel) | <img width="50" src="https://avatars.githubusercontent.com/u/26790323?v=4"/></br>[dervist](https://github.com/dervist) | <img width="50" src="https://avatars.githubusercontent.com/u/11378282?v=4"/></br>[diego-betto](https://github.com/diego-betto) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/215270?v=4"/></br>[erdody](https://github.com/erdody) | <img width="50" src="https://avatars.githubusercontent.com/u/10371667?v=4"/></br>[domgoodwin](https://github.com/domgoodwin) | <img width="50" src="https://avatars.githubusercontent.com/u/72066?v=4"/></br>[b4mboo](https://github.com/b4mboo) | <img width="50" src="https://avatars.githubusercontent.com/u/5131923?v=4"/></br>[donbowman](https://github.com/donbowman) | <img width="50" src="https://avatars.githubusercontent.com/u/579727?v=4"/></br>[sirnacnud](https://github.com/sirnacnud) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/47756?v=4"/></br>[dflock](https://github.com/dflock) | <img width="50" src="https://avatars.githubusercontent.com/u/7990534?v=4"/></br>[drobilica](https://github.com/drobilica) | <img width="50" src="https://avatars.githubusercontent.com/u/21699905?v=4"/></br>[educbraga](https://github.com/educbraga) | <img width="50" src="https://avatars.githubusercontent.com/u/67867099?v=4"/></br>[eduardokimmel](https://github.com/eduardokimmel) | <img width="50" src="https://avatars.githubusercontent.com/u/30393516?v=4"/></br>[VodeniZeko](https://github.com/VodeniZeko) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/17415256?v=4"/></br>[ei-ke](https://github.com/ei-ke) | <img width="50" src="https://avatars.githubusercontent.com/u/1962738?v=4"/></br>[einverne](https://github.com/einverne) | <img width="50" src="https://avatars.githubusercontent.com/u/16492558?v=4"/></br>[eodeluga](https://github.com/eodeluga) | <img width="50" src="https://avatars.githubusercontent.com/u/16875937?v=4"/></br>[fathyar](https://github.com/fathyar) | <img width="50" src="https://avatars.githubusercontent.com/u/3057302?v=4"/></br>[fer22f](https://github.com/fer22f) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/43272148?v=4"/></br>[fpindado](https://github.com/fpindado) | <img width="50" src="https://avatars.githubusercontent.com/u/1714374?v=4"/></br>[FleischKarussel](https://github.com/FleischKarussel) | <img width="50" src="https://avatars.githubusercontent.com/u/18525376?v=4"/></br>[talkdirty](https://github.com/talkdirty) | <img width="50" src="https://avatars.githubusercontent.com/u/19814827?v=4"/></br>[gmaubach](https://github.com/gmaubach) | <img width="50" src="https://avatars.githubusercontent.com/u/6190183?v=4"/></br>[gmag11](https://github.com/gmag11) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/6209647?v=4"/></br>[Jackymancs4](https://github.com/Jackymancs4) | <img width="50" src="https://avatars.githubusercontent.com/u/297578?v=4"/></br>[Glandos](https://github.com/Glandos) | <img width="50" src="https://avatars.githubusercontent.com/u/24235344?v=4"/></br>[vibraniumdev](https://github.com/vibraniumdev) | <img width="50" src="https://avatars.githubusercontent.com/u/2257024?v=4"/></br>[gusbemacbe](https://github.com/gusbemacbe) | <img width="50" src="https://avatars.githubusercontent.com/u/64917442?v=4"/></br>[HOLLYwyh](https://github.com/HOLLYwyh) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/18524580?v=4"/></br>[Fvbor](https://github.com/Fvbor) | <img width="50" src="https://avatars.githubusercontent.com/u/22606250?v=4"/></br>[bennetthanna](https://github.com/bennetthanna) | <img width="50" src="https://avatars.githubusercontent.com/u/67231570?v=4"/></br>[harshitkathuria](https://github.com/harshitkathuria) | <img width="50" src="https://avatars.githubusercontent.com/u/1716229?v=4"/></br>[Vistaus](https://github.com/Vistaus) | <img width="50" src="https://avatars.githubusercontent.com/u/6509881?v=4"/></br>[ianjs](https://github.com/ianjs) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/19862172?v=4"/></br>[iahmedbacha](https://github.com/iahmedbacha) | <img width="50" src="https://avatars.githubusercontent.com/u/1533624?v=4"/></br>[IrvinDominin](https://github.com/IrvinDominin) | <img width="50" src="https://avatars.githubusercontent.com/u/33200024?v=4"/></br>[ishammahajan](https://github.com/ishammahajan) | <img width="50" src="https://avatars.githubusercontent.com/u/6916297?v=4"/></br>[ffadilaputra](https://github.com/ffadilaputra) | <img width="50" src="https://avatars.githubusercontent.com/u/19985741?v=4"/></br>[JRaiden16](https://github.com/JRaiden16) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/11466782?v=4"/></br>[jacobherrington](https://github.com/jacobherrington) | <img width="50" src="https://avatars.githubusercontent.com/u/9365179?v=4"/></br>[jamesadjinwa](https://github.com/jamesadjinwa) | <img width="50" src="https://avatars.githubusercontent.com/u/20801821?v=4"/></br>[jrwrigh](https://github.com/jrwrigh) | <img width="50" src="https://avatars.githubusercontent.com/u/4995433?v=4"/></br>[jaredcrowe](https://github.com/jaredcrowe) | <img width="50" src="https://avatars.githubusercontent.com/u/4087105?v=4"/></br>[volatilevar](https://github.com/volatilevar) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/47724360?v=4"/></br>[innkuika](https://github.com/innkuika) | <img width="50" src="https://avatars.githubusercontent.com/u/163555?v=4"/></br>[JoelRSimpson](https://github.com/JoelRSimpson) | <img width="50" src="https://avatars.githubusercontent.com/u/6965062?v=4"/></br>[joeltaylor](https://github.com/joeltaylor) | <img width="50" src="https://avatars.githubusercontent.com/u/242107?v=4"/></br>[exic](https://github.com/exic) | <img width="50" src="https://avatars.githubusercontent.com/u/13716151?v=4"/></br>[JonathanPlasse](https://github.com/JonathanPlasse) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/1248504?v=4"/></br>[joesfer](https://github.com/joesfer) | <img width="50" src="https://avatars.githubusercontent.com/u/6048003?v=4"/></br>[joybinchen](https://github.com/joybinchen) | <img width="50" src="https://avatars.githubusercontent.com/u/37601331?v=4"/></br>[kaustubhsh](https://github.com/kaustubhsh) | <img width="50" src="https://avatars.githubusercontent.com/u/1560189?v=4"/></br>[y-usuzumi](https://github.com/y-usuzumi) | <img width="50" src="https://avatars.githubusercontent.com/u/1660460?v=4"/></br>[xuhcc](https://github.com/xuhcc) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/16933735?v=4"/></br>[kirtanprht](https://github.com/kirtanprht) | <img width="50" src="https://avatars.githubusercontent.com/u/37491732?v=4"/></br>[k0ur0x](https://github.com/k0ur0x) | <img width="50" src="https://avatars.githubusercontent.com/u/7824233?v=4"/></br>[kklas](https://github.com/kklas) | <img width="50" src="https://avatars.githubusercontent.com/u/8622992?v=4"/></br>[xmlangel](https://github.com/xmlangel) | <img width="50" src="https://avatars.githubusercontent.com/u/1055100?v=4"/></br>[troilus](https://github.com/troilus) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/2599210?v=4"/></br>[lboullo0](https://github.com/lboullo0) | <img width="50" src="https://avatars.githubusercontent.com/u/1562062?v=4"/></br>[dbinary](https://github.com/dbinary) | <img width="50" src="https://avatars.githubusercontent.com/u/15436007?v=4"/></br>[marc-bouvier](https://github.com/marc-bouvier) | <img width="50" src="https://avatars.githubusercontent.com/u/5699725?v=4"/></br>[mvonmaltitz](https://github.com/mvonmaltitz) | <img width="50" src="https://avatars.githubusercontent.com/u/11036464?v=4"/></br>[mlkood](https://github.com/mlkood) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/2480960?v=4"/></br>[plextoriano](https://github.com/plextoriano) | <img width="50" src="https://avatars.githubusercontent.com/u/5788516?v=4"/></br>[Marmo](https://github.com/Marmo) | <img width="50" src="https://avatars.githubusercontent.com/u/29300939?v=4"/></br>[mcejp](https://github.com/mcejp) | <img width="50" src="https://avatars.githubusercontent.com/u/640949?v=4"/></br>[freaktechnik](https://github.com/freaktechnik) | <img width="50" src="https://avatars.githubusercontent.com/u/79802125?v=4"/></br>[martinkorelic](https://github.com/martinkorelic) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/287105?v=4"/></br>[Petemir](https://github.com/Petemir) | <img width="50" src="https://avatars.githubusercontent.com/u/5218859?v=4"/></br>[matsair](https://github.com/matsair) | <img width="50" src="https://avatars.githubusercontent.com/u/12831489?v=4"/></br>[mgroth0](https://github.com/mgroth0) | <img width="50" src="https://avatars.githubusercontent.com/u/21796?v=4"/></br>[silentmatt](https://github.com/silentmatt) | <img width="50" src="https://avatars.githubusercontent.com/u/76700192?v=4"/></br>[maxs-test](https://github.com/maxs-test) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/59669349?v=4"/></br>[MichBoi](https://github.com/MichBoi) | <img width="50" src="https://avatars.githubusercontent.com/u/51273874?v=4"/></br>[MichipX](https://github.com/MichipX) | <img width="50" src="https://avatars.githubusercontent.com/u/53177864?v=4"/></br>[MrTraduttore](https://github.com/MrTraduttore) | <img width="50" src="https://avatars.githubusercontent.com/u/48156230?v=4"/></br>[sanjarcode](https://github.com/sanjarcode) | <img width="50" src="https://avatars.githubusercontent.com/u/43955099?v=4"/></br>[Mustafa-ALD](https://github.com/Mustafa-ALD) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/9076687?v=4"/></br>[NJannasch](https://github.com/NJannasch) | <img width="50" src="https://avatars.githubusercontent.com/u/8016073?v=4"/></br>[zomglings](https://github.com/zomglings) | <img width="50" src="https://avatars.githubusercontent.com/u/10386884?v=4"/></br>[Frichetten](https://github.com/Frichetten) | <img width="50" src="https://avatars.githubusercontent.com/u/5541611?v=4"/></br>[nicolas-suzuki](https://github.com/nicolas-suzuki) | <img width="50" src="https://avatars.githubusercontent.com/u/12369770?v=4"/></br>[Ouvill](https://github.com/Ouvill) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/43815417?v=4"/></br>[shorty2380](https://github.com/shorty2380) | <img width="50" src="https://avatars.githubusercontent.com/u/15014287?v=4"/></br>[dist3r](https://github.com/dist3r) | <img width="50" src="https://avatars.githubusercontent.com/u/19418601?v=4"/></br>[rakleed](https://github.com/rakleed) | <img width="50" src="https://avatars.githubusercontent.com/u/7881932?v=4"/></br>[idle-code](https://github.com/idle-code) | <img width="50" src="https://avatars.githubusercontent.com/u/168931?v=4"/></br>[bobchao](https://github.com/bobchao) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/6306608?v=4"/></br>[Diadlo](https://github.com/Diadlo) | <img width="50" src="https://avatars.githubusercontent.com/u/42793024?v=4"/></br>[pranavmodx](https://github.com/pranavmodx) | <img width="50" src="https://avatars.githubusercontent.com/u/50834839?v=4"/></br>[R3dError](https://github.com/R3dError) | <img width="50" src="https://avatars.githubusercontent.com/u/42652941?v=4"/></br>[rajprakash00](https://github.com/rajprakash00) | <img width="50" src="https://avatars.githubusercontent.com/u/32304956?v=4"/></br>[rahil1304](https://github.com/rahil1304) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/8257474?v=4"/></br>[rasulkireev](https://github.com/rasulkireev) | <img width="50" src="https://avatars.githubusercontent.com/u/17312341?v=4"/></br>[reinhart1010](https://github.com/reinhart1010) | <img width="50" src="https://avatars.githubusercontent.com/u/60484714?v=4"/></br>[Retew](https://github.com/Retew) | <img width="50" src="https://avatars.githubusercontent.com/u/10456131?v=4"/></br>[ambrt](https://github.com/ambrt) | <img width="50" src="https://avatars.githubusercontent.com/u/15892014?v=4"/></br>[Derkades](https://github.com/Derkades) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/49439044?v=4"/></br>[fourstepper](https://github.com/fourstepper) | <img width="50" src="https://avatars.githubusercontent.com/u/54365?v=4"/></br>[rodgco](https://github.com/rodgco) | <img width="50" src="https://avatars.githubusercontent.com/u/96014?v=4"/></br>[Ronnie76er](https://github.com/Ronnie76er) | <img width="50" src="https://avatars.githubusercontent.com/u/79168?v=4"/></br>[roryokane](https://github.com/roryokane) | <img width="50" src="https://avatars.githubusercontent.com/u/744655?v=4"/></br>[ruzaq](https://github.com/ruzaq) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/20490839?v=4"/></br>[szokesandor](https://github.com/szokesandor) | <img width="50" src="https://avatars.githubusercontent.com/u/19328605?v=4"/></br>[SamuelBlickle](https://github.com/SamuelBlickle) | <img width="50" src="https://avatars.githubusercontent.com/u/80849457?v=4"/></br>[livingc0l0ur](https://github.com/livingc0l0ur) | <img width="50" src="https://avatars.githubusercontent.com/u/1776?v=4"/></br>[bronson](https://github.com/bronson) | <img width="50" src="https://avatars.githubusercontent.com/u/24606935?v=4"/></br>[semperor](https://github.com/semperor) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/607938?v=4"/></br>[shawnaxsom](https://github.com/shawnaxsom) | <img width="50" src="https://avatars.githubusercontent.com/u/9937486?v=4"/></br>[SFoskitt](https://github.com/SFoskitt) | <img width="50" src="https://avatars.githubusercontent.com/u/505011?v=4"/></br>[kcrt](https://github.com/kcrt) | <img width="50" src="https://avatars.githubusercontent.com/u/538584?v=4"/></br>[xissy](https://github.com/xissy) | <img width="50" src="https://avatars.githubusercontent.com/u/164962?v=4"/></br>[tams](https://github.com/tams) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/466122?v=4"/></br>[Tekki](https://github.com/Tekki) | <img width="50" src="https://avatars.githubusercontent.com/u/2112477?v=4"/></br>[ThatcherC](https://github.com/ThatcherC) | <img width="50" src="https://avatars.githubusercontent.com/u/21969426?v=4"/></br>[TheoDutch](https://github.com/TheoDutch) | <img width="50" src="https://avatars.githubusercontent.com/u/8731922?v=4"/></br>[tbroadley](https://github.com/tbroadley) | <img width="50" src="https://avatars.githubusercontent.com/u/114300?v=4"/></br>[Kriechi](https://github.com/Kriechi) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/3457339?v=4"/></br>[tkilaker](https://github.com/tkilaker) | <img width="50" src="https://avatars.githubusercontent.com/u/802148?v=4"/></br>[Tim-Erwin](https://github.com/Tim-Erwin) | <img width="50" src="https://avatars.githubusercontent.com/u/4201229?v=4"/></br>[tcyrus](https://github.com/tcyrus) | <img width="50" src="https://avatars.githubusercontent.com/u/834914?v=4"/></br>[tobias-grasse](https://github.com/tobias-grasse) | <img width="50" src="https://avatars.githubusercontent.com/u/6691273?v=4"/></br>[strobeltobias](https://github.com/strobeltobias) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/1677578?v=4"/></br>[kostegit](https://github.com/kostegit) | <img width="50" src="https://avatars.githubusercontent.com/u/70296?v=4"/></br>[tbergeron](https://github.com/tbergeron) | <img width="50" src="https://avatars.githubusercontent.com/u/10265443?v=4"/></br>[Ullas-Aithal](https://github.com/Ullas-Aithal) | <img width="50" src="https://avatars.githubusercontent.com/u/6104498?v=4"/></br>[MyTheValentinus](https://github.com/MyTheValentinus) | <img width="50" src="https://avatars.githubusercontent.com/u/2830093?v=4"/></br>[vassudanagunta](https://github.com/vassudanagunta) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/54314949?v=4"/></br>[vijayjoshi16](https://github.com/vijayjoshi16) | <img width="50" src="https://avatars.githubusercontent.com/u/59287619?v=4"/></br>[max-keviv](https://github.com/max-keviv) | <img width="50" src="https://avatars.githubusercontent.com/u/598576?v=4"/></br>[vandreykiv](https://github.com/vandreykiv) | <img width="50" src="https://avatars.githubusercontent.com/u/26511487?v=4"/></br>[WisdomCode](https://github.com/WisdomCode) | <img width="50" src="https://avatars.githubusercontent.com/u/1921957?v=4"/></br>[xsak](https://github.com/xsak) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/11031696?v=4"/></br>[ymitsos](https://github.com/ymitsos) | <img width="50" src="https://avatars.githubusercontent.com/u/63324960?v=4"/></br>[abolishallprivateproperty](https://github.com/abolishallprivateproperty) | <img width="50" src="https://avatars.githubusercontent.com/u/11336076?v=4"/></br>[aerotog](https://github.com/aerotog) | <img width="50" src="https://avatars.githubusercontent.com/u/39854348?v=4"/></br>[albertopasqualetto](https://github.com/albertopasqualetto) | <img width="50" src="https://avatars.githubusercontent.com/u/44570278?v=4"/></br>[asrient](https://github.com/asrient) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/621360?v=4"/></br>[bestlibre](https://github.com/bestlibre) | <img width="50" src="https://avatars.githubusercontent.com/u/35600612?v=4"/></br>[boring10](https://github.com/boring10) | <img width="50" src="https://avatars.githubusercontent.com/u/13894820?v=4"/></br>[cadolphs](https://github.com/cadolphs) | <img width="50" src="https://avatars.githubusercontent.com/u/12461043?v=4"/></br>[colorchestra](https://github.com/colorchestra) | <img width="50" src="https://avatars.githubusercontent.com/u/30935096?v=4"/></br>[cybertramp](https://github.com/cybertramp) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/15824892?v=4"/></br>[dartero](https://github.com/dartero) | <img width="50" src="https://avatars.githubusercontent.com/u/9694906?v=4"/></br>[delta-emil](https://github.com/delta-emil) | <img width="50" src="https://avatars.githubusercontent.com/u/926263?v=4"/></br>[doc75](https://github.com/doc75) | <img width="50" src="https://avatars.githubusercontent.com/u/5589253?v=4"/></br>[dsp77](https://github.com/dsp77) | <img width="50" src="https://avatars.githubusercontent.com/u/2903013?v=4"/></br>[ebayer](https://github.com/ebayer) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/9206310?v=4"/></br>[elsiehupp](https://github.com/elsiehupp) | <img width="50" src="https://avatars.githubusercontent.com/u/701050?v=4"/></br>[espinosa](https://github.com/espinosa) | <img width="50" src="https://avatars.githubusercontent.com/u/18619090?v=4"/></br>[exponentactivity](https://github.com/exponentactivity) | <img width="50" src="https://avatars.githubusercontent.com/u/16708935?v=4"/></br>[exprez135](https://github.com/exprez135) | <img width="50" src="https://avatars.githubusercontent.com/u/9768112?v=4"/></br>[fab4x](https://github.com/fab4x) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/47755037?v=4"/></br>[fabianski7](https://github.com/fabianski7) | <img width="50" src="https://avatars.githubusercontent.com/u/14201321?v=4"/></br>[rasperepodvipodvert](https://github.com/rasperepodvipodvert) | <img width="50" src="https://avatars.githubusercontent.com/u/748808?v=4"/></br>[gasolin](https://github.com/gasolin) | <img width="50" src="https://avatars.githubusercontent.com/u/47191051?v=4"/></br>[githubaccount073](https://github.com/githubaccount073) | <img width="50" src="https://avatars.githubusercontent.com/u/43672033?v=4"/></br>[hms5232](https://github.com/hms5232) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/11388094?v=4"/></br>[hydrandt](https://github.com/hydrandt) | <img width="50" src="https://avatars.githubusercontent.com/u/61012185?v=4"/></br>[iamtalwinder](https://github.com/iamtalwinder) | <img width="50" src="https://avatars.githubusercontent.com/u/557540?v=4"/></br>[jabdoa2](https://github.com/jabdoa2) | <img width="50" src="https://avatars.githubusercontent.com/u/29166402?v=4"/></br>[jduar](https://github.com/jduar) | <img width="50" src="https://avatars.githubusercontent.com/u/2678545?v=4"/></br>[jibedoubleve](https://github.com/jibedoubleve) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/53862536?v=4"/></br>[johanvanheusden](https://github.com/johanvanheusden) | <img width="50" src="https://avatars.githubusercontent.com/u/38327267?v=4"/></br>[jtagcat](https://github.com/jtagcat) | <img width="50" src="https://avatars.githubusercontent.com/u/61631665?v=4"/></br>[konhi](https://github.com/konhi) | <img width="50" src="https://avatars.githubusercontent.com/u/54991735?v=4"/></br>[krzysiekwie](https://github.com/krzysiekwie) | <img width="50" src="https://avatars.githubusercontent.com/u/12849008?v=4"/></br>[lighthousebulb](https://github.com/lighthousebulb) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/4140247?v=4"/></br>[luzpaz](https://github.com/luzpaz) | <img width="50" src="https://avatars.githubusercontent.com/u/29355048?v=4"/></br>[majsterkovic](https://github.com/majsterkovic) | <img width="50" src="https://avatars.githubusercontent.com/u/77744862?v=4"/></br>[mak2002](https://github.com/mak2002) | <img width="50" src="https://avatars.githubusercontent.com/u/30428258?v=4"/></br>[nmiquan](https://github.com/nmiquan) | <img width="50" src="https://avatars.githubusercontent.com/u/31123054?v=4"/></br>[nullpointer666](https://github.com/nullpointer666) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/2979926?v=4"/></br>[oscaretu](https://github.com/oscaretu) | <img width="50" src="https://avatars.githubusercontent.com/u/36965591?v=4"/></br>[oskarsh](https://github.com/oskarsh) | <img width="50" src="https://avatars.githubusercontent.com/u/52031346?v=4"/></br>[osso73](https://github.com/osso73) | <img width="50" src="https://avatars.githubusercontent.com/u/29743024?v=4"/></br>[over-soul](https://github.com/over-soul) | <img width="50" src="https://avatars.githubusercontent.com/u/42961947?v=4"/></br>[pensierocrea](https://github.com/pensierocrea) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/45542782?v=4"/></br>[pomeloy](https://github.com/pomeloy) | <img width="50" src="https://avatars.githubusercontent.com/u/10206967?v=4"/></br>[rhtenhove](https://github.com/rhtenhove) | <img width="50" src="https://avatars.githubusercontent.com/u/16728217?v=4"/></br>[rikanotank1](https://github.com/rikanotank1) | <img width="50" src="https://avatars.githubusercontent.com/u/24560368?v=4"/></br>[rxliuli](https://github.com/rxliuli) | <img width="50" src="https://avatars.githubusercontent.com/u/14062932?v=4"/></br>[simonsan](https://github.com/simonsan) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/5004545?v=4"/></br>[stellarpower](https://github.com/stellarpower) | <img width="50" src="https://avatars.githubusercontent.com/u/20983267?v=4"/></br>[suixinio](https://github.com/suixinio) | <img width="50" src="https://avatars.githubusercontent.com/u/12995773?v=4"/></br>[sumomo-99](https://github.com/sumomo-99) | <img width="50" src="https://avatars.githubusercontent.com/u/367170?v=4"/></br>[xtatsux](https://github.com/xtatsux) | <img width="50" src="https://avatars.githubusercontent.com/u/6908872?v=4"/></br>[taw00](https://github.com/taw00) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/10956653?v=4"/></br>[tcassaert](https://github.com/tcassaert) | <img width="50" src="https://avatars.githubusercontent.com/u/46327531?v=4"/></br>[victante](https://github.com/victante) | <img width="50" src="https://avatars.githubusercontent.com/u/7252567?v=4"/></br>[Voltinus](https://github.com/Voltinus) | <img width="50" src="https://avatars.githubusercontent.com/u/2216902?v=4"/></br>[xcffl](https://github.com/xcffl) | <img width="50" src="https://avatars.githubusercontent.com/u/46404814?v=4"/></br>[yourcontact](https://github.com/yourcontact) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/37692927?v=4"/></br>[zaoyifan](https://github.com/zaoyifan) | <img width="50" src="https://avatars.githubusercontent.com/u/10813608?v=4"/></br>[zawnk](https://github.com/zawnk) | <img width="50" src="https://avatars.githubusercontent.com/u/55245068?v=4"/></br>[zen-quo](https://github.com/zen-quo) | <img width="50" src="https://avatars.githubusercontent.com/u/23507174?v=4"/></br>[zozolina123](https://github.com/zozolina123) | <img width="50" src="https://avatars.githubusercontent.com/u/25315?v=4"/></br>[xcession](https://github.com/xcession) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/34542665?v=4"/></br>[paventyang](https://github.com/paventyang) | <img width="50" src="https://avatars.githubusercontent.com/u/608014?v=4"/></br>[jackytsu](https://github.com/jackytsu) | <img width="50" src="https://avatars.githubusercontent.com/u/1308646?v=4"/></br>[zhangmx](https://github.com/zhangmx) | | |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/1285584?v=4"/></br>[laurent22](https://github.com/laurent22) | <img width="50" src="https://avatars.githubusercontent.com/u/223439?v=4"/></br>[tessus](https://github.com/tessus) | <img width="50" src="https://avatars.githubusercontent.com/u/2179547?v=4"/></br>[CalebJohn](https://github.com/CalebJohn) | <img width="50" src="https://avatars.githubusercontent.com/u/46334387?v=4"/></br>[personalizedrefrigerator](https://github.com/personalizedrefrigerator) | <img width="50" src="https://avatars.githubusercontent.com/u/995612?v=4"/></br>[roman-r-m](https://github.com/roman-r-m) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/1732810?v=4"/></br>[miciasto](https://github.com/miciasto) | <img width="50" src="https://avatars.githubusercontent.com/u/16041683?v=4"/></br>[ken1kob](https://github.com/ken1kob) | <img width="50" src="https://avatars.githubusercontent.com/u/29672555?v=4"/></br>[genneko](https://github.com/genneko) | <img width="50" src="https://avatars.githubusercontent.com/u/58074586?v=4"/></br>[Daeraxa](https://github.com/Daeraxa) | <img width="50" src="https://avatars.githubusercontent.com/u/4553672?v=4"/></br>[tanrax](https://github.com/tanrax) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/63491353?v=4"/></br>[j-krl](https://github.com/j-krl) | <img width="50" src="https://avatars.githubusercontent.com/u/62299611?v=4"/></br>[wh201906](https://github.com/wh201906) | <img width="50" src="https://avatars.githubusercontent.com/u/24863925?v=4"/></br>[JackGruber](https://github.com/JackGruber) | <img width="50" src="https://avatars.githubusercontent.com/u/30305957?v=4"/></br>[naviji](https://github.com/naviji) | <img width="50" src="https://avatars.githubusercontent.com/u/3542031?v=4"/></br>[PackElend](https://github.com/PackElend) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/32807437?v=4"/></br>[julien-me](https://github.com/julien-me) | <img width="50" src="https://avatars.githubusercontent.com/u/5051088?v=4"/></br>[pedr](https://github.com/pedr) | <img width="50" src="https://avatars.githubusercontent.com/u/4374338?v=4"/></br>[potatogim](https://github.com/potatogim) | <img width="50" src="https://avatars.githubusercontent.com/u/84130654?v=4"/></br>[JonatanWick](https://github.com/JonatanWick) | <img width="50" src="https://avatars.githubusercontent.com/u/2063957?v=4"/></br>[Ardakilic](https://github.com/Ardakilic) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/43657314?v=4"/></br>[milotype](https://github.com/milotype) | <img width="50" src="https://avatars.githubusercontent.com/u/44570278?v=4"/></br>[asrient](https://github.com/asrient) | <img width="50" src="https://avatars.githubusercontent.com/u/8701534?v=4"/></br>[rtmkrlv](https://github.com/rtmkrlv) | <img width="50" src="https://avatars.githubusercontent.com/u/10997189?v=4"/></br>[fmrtn](https://github.com/fmrtn) | <img width="50" src="https://avatars.githubusercontent.com/u/68117355?v=4"/></br>[Mr-Kanister](https://github.com/Mr-Kanister) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/4299398?v=4"/></br>[palerdot](https://github.com/palerdot) | <img width="50" src="https://avatars.githubusercontent.com/u/10927304?v=4"/></br>[matsest](https://github.com/matsest) | <img width="50" src="https://avatars.githubusercontent.com/u/6979755?v=4"/></br>[devonzuegel](https://github.com/devonzuegel) | <img width="50" src="https://avatars.githubusercontent.com/u/26695184?v=4"/></br>[anjulalk](https://github.com/anjulalk) | <img width="50" src="https://avatars.githubusercontent.com/u/16101778?v=4"/></br>[gabcoh](https://github.com/gabcoh) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/19213902?v=4"/></br>[hubertfilho](https://github.com/hubertfilho) | <img width="50" src="https://avatars.githubusercontent.com/u/6319051?v=4"/></br>[abonte](https://github.com/abonte) | <img width="50" src="https://avatars.githubusercontent.com/u/1685517?v=4"/></br>[Abijeet](https://github.com/Abijeet) | <img width="50" src="https://avatars.githubusercontent.com/u/27751740?v=4"/></br>[ishantgupta777](https://github.com/ishantgupta777) | <img width="50" src="https://avatars.githubusercontent.com/u/63025323?v=4"/></br>[ScriptInfra](https://github.com/ScriptInfra) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/6196533?v=4"/></br>[jd1378](https://github.com/jd1378) | <img width="50" src="https://avatars.githubusercontent.com/u/44024553?v=4"/></br>[rabeehrz](https://github.com/rabeehrz) | <img width="50" src="https://avatars.githubusercontent.com/u/35633575?v=4"/></br>[coderrsid](https://github.com/coderrsid) | <img width="50" src="https://avatars.githubusercontent.com/u/7415668?v=4"/></br>[mablin7](https://github.com/mablin7) | <img width="50" src="https://avatars.githubusercontent.com/u/608014?v=4"/></br>[jackytsu](https://github.com/jackytsu) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/77744862?v=4"/></br>[mak2002](https://github.com/mak2002) | <img width="50" src="https://avatars.githubusercontent.com/u/3985557?v=4"/></br>[XarisA](https://github.com/XarisA) | <img width="50" src="https://avatars.githubusercontent.com/u/208212?v=4"/></br>[foxmask](https://github.com/foxmask) | <img width="50" src="https://avatars.githubusercontent.com/u/6557454?v=4"/></br>[innocuo](https://github.com/innocuo) | <img width="50" src="https://avatars.githubusercontent.com/u/54268438?v=4"/></br>[Rahulm2310](https://github.com/Rahulm2310) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/8184424?v=4"/></br>[Ahmad45123](https://github.com/Ahmad45123) | <img width="50" src="https://avatars.githubusercontent.com/u/49979415?v=4"/></br>[jonath92](https://github.com/jonath92) | <img width="50" src="https://avatars.githubusercontent.com/u/1904967?v=4"/></br>[readingsnail](https://github.com/readingsnail) | <img width="50" src="https://avatars.githubusercontent.com/u/134083?v=4"/></br>[xavivars](https://github.com/xavivars) | <img width="50" src="https://avatars.githubusercontent.com/u/51550769?v=4"/></br>[rnbastos](https://github.com/rnbastos) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/4237724?v=4"/></br>[alexdevero](https://github.com/alexdevero) | <img width="50" src="https://avatars.githubusercontent.com/u/71190696?v=4"/></br>[Elaborendum](https://github.com/Elaborendum) | <img width="50" src="https://avatars.githubusercontent.com/u/42747216?v=4"/></br>[Mannivu](https://github.com/Mannivu) | <img width="50" src="https://avatars.githubusercontent.com/u/36989112?v=4"/></br>[nishantwrp](https://github.com/nishantwrp) | <img width="50" src="https://avatars.githubusercontent.com/u/35904727?v=4"/></br>[Runo-saduwa](https://github.com/Runo-saduwa) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/3250983?v=4"/></br>[shinglyu](https://github.com/shinglyu) | <img width="50" src="https://avatars.githubusercontent.com/u/30352484?v=4"/></br>[Tolu-Mals](https://github.com/Tolu-Mals) | <img width="50" src="https://avatars.githubusercontent.com/u/5365582?v=4"/></br>[marcosvega91](https://github.com/marcosvega91) | <img width="50" src="https://avatars.githubusercontent.com/u/99097412?v=4"/></br>[mrkaato0](https://github.com/mrkaato0) | <img width="50" src="https://avatars.githubusercontent.com/u/37639389?v=4"/></br>[petrz12](https://github.com/petrz12) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/4245227?v=4"/></br>[zblesk](https://github.com/zblesk) | <img width="50" src="https://avatars.githubusercontent.com/u/5730052?v=4"/></br>[vsimkus](https://github.com/vsimkus) | <img width="50" src="https://avatars.githubusercontent.com/u/20461071?v=4"/></br>[Vaso3](https://github.com/Vaso3) | <img width="50" src="https://avatars.githubusercontent.com/u/3194829?v=4"/></br>[moltenform](https://github.com/moltenform) | <img width="50" src="https://avatars.githubusercontent.com/u/33229141?v=4"/></br>[marph91](https://github.com/marph91) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/5199995?v=4"/></br>[zuphilip](https://github.com/zuphilip) | <img width="50" src="https://avatars.githubusercontent.com/u/10289737?v=4"/></br>[Retr0ve](https://github.com/Retr0ve) | <img width="50" src="https://avatars.githubusercontent.com/u/54576074?v=4"/></br>[Rishabh-malhotraa](https://github.com/Rishabh-malhotraa) | <img width="50" src="https://avatars.githubusercontent.com/u/559346?v=4"/></br>[metbril](https://github.com/metbril) | <img width="50" src="https://avatars.githubusercontent.com/u/36622934?v=4"/></br>[SFulpius](https://github.com/SFulpius) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/531704?v=4"/></br>[TaoK](https://github.com/TaoK) | <img width="50" src="https://avatars.githubusercontent.com/u/47623588?v=4"/></br>[WhiredPlanck](https://github.com/WhiredPlanck) | <img width="50" src="https://avatars.githubusercontent.com/u/32396?v=4"/></br>[ProgramFan](https://github.com/ProgramFan) | <img width="50" src="https://avatars.githubusercontent.com/u/32196447?v=4"/></br>[yaozeye](https://github.com/yaozeye) | <img width="50" src="https://avatars.githubusercontent.com/u/12264626?v=4"/></br>[ylc395](https://github.com/ylc395) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/8716226?v=4"/></br>[amandamcg](https://github.com/amandamcg) | <img width="50" src="https://avatars.githubusercontent.com/u/359140?v=4"/></br>[leematos](https://github.com/leematos) | <img width="50" src="https://avatars.githubusercontent.com/u/17768566?v=4"/></br>[RenatoXSR](https://github.com/RenatoXSR) | <img width="50" src="https://avatars.githubusercontent.com/u/54888685?v=4"/></br>[RedDocMD](https://github.com/RedDocMD) | <img width="50" src="https://avatars.githubusercontent.com/u/31567272?v=4"/></br>[t1011](https://github.com/t1011) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/44198148?v=4"/></br>[whalehub](https://github.com/whalehub) | <img width="50" src="https://avatars.githubusercontent.com/u/12906090?v=4"/></br>[amitsin6h](https://github.com/amitsin6h) | <img width="50" src="https://avatars.githubusercontent.com/u/628474?v=4"/></br>[Atalanttore](https://github.com/Atalanttore) | <img width="50" src="https://avatars.githubusercontent.com/u/5058349?v=4"/></br>[hieuthi](https://github.com/hieuthi) | <img width="50" src="https://avatars.githubusercontent.com/u/23281486?v=4"/></br>[martonpaulo](https://github.com/martonpaulo) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/390889?v=4"/></br>[mmahmoudian](https://github.com/mmahmoudian) | <img width="50" src="https://avatars.githubusercontent.com/u/168931?v=4"/></br>[bobchao](https://github.com/bobchao) | <img width="50" src="https://avatars.githubusercontent.com/u/4497566?v=4"/></br>[rc2dev](https://github.com/rc2dev) | <img width="50" src="https://avatars.githubusercontent.com/u/43534227?v=4"/></br>[Rishabhraghwendra18](https://github.com/Rishabhraghwendra18) | <img width="50" src="https://avatars.githubusercontent.com/u/7091080?v=4"/></br>[sinkuu](https://github.com/sinkuu) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/6734573?v=4"/></br>[stweil](https://github.com/stweil) | <img width="50" src="https://avatars.githubusercontent.com/u/59690052?v=4"/></br>[Subhra264](https://github.com/Subhra264) | <img width="50" src="https://avatars.githubusercontent.com/u/692072?v=4"/></br>[conyx](https://github.com/conyx) | <img width="50" src="https://avatars.githubusercontent.com/u/49116134?v=4"/></br>[anihm136](https://github.com/anihm136) | <img width="50" src="https://avatars.githubusercontent.com/u/937861?v=4"/></br>[archont00](https://github.com/archont00) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/32770029?v=4"/></br>[bradmcl](https://github.com/bradmcl) | <img width="50" src="https://avatars.githubusercontent.com/u/1340627?v=4"/></br>[jcgurango](https://github.com/jcgurango) | <img width="50" src="https://avatars.githubusercontent.com/u/36228623?v=4"/></br>[mrkaato](https://github.com/mrkaato) | <img width="50" src="https://avatars.githubusercontent.com/u/22592201?v=4"/></br>[tfinnberg](https://github.com/tfinnberg) | <img width="50" src="https://avatars.githubusercontent.com/u/63918341?v=4"/></br>[adarsh-sgh](https://github.com/adarsh-sgh) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/3870964?v=4"/></br>[marcushill](https://github.com/marcushill) | <img width="50" src="https://avatars.githubusercontent.com/u/102242?v=4"/></br>[nathanleiby](https://github.com/nathanleiby) | <img width="50" src="https://avatars.githubusercontent.com/u/13251?v=4"/></br>[piotrb](https://github.com/piotrb) | <img width="50" src="https://avatars.githubusercontent.com/u/226708?v=4"/></br>[RaphaelKimmig](https://github.com/RaphaelKimmig) | <img width="50" src="https://avatars.githubusercontent.com/u/10060747?v=4"/></br>[Wartijn](https://github.com/Wartijn) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/40672207?v=4"/></br>[xUser5000](https://github.com/xUser5000) | <img width="50" src="https://avatars.githubusercontent.com/u/41290751?v=4"/></br>[serenitatis](https://github.com/serenitatis) | <img width="50" src="https://avatars.githubusercontent.com/u/81777961?v=4"/></br>[k33pn3xtlvl](https://github.com/k33pn3xtlvl) | <img width="50" src="https://avatars.githubusercontent.com/u/17809291?v=4"/></br>[antontkv](https://github.com/antontkv) | <img width="50" src="https://avatars.githubusercontent.com/u/28987176?v=4"/></br>[infinity052](https://github.com/infinity052) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/55127997?v=4"/></br>[entrymaster](https://github.com/entrymaster) | <img width="50" src="https://avatars.githubusercontent.com/u/21161146?v=4"/></br>[BartBucknill](https://github.com/BartBucknill) | <img width="50" src="https://avatars.githubusercontent.com/u/94234459?v=4"/></br>[betty-alagwu](https://github.com/betty-alagwu) | <img width="50" src="https://avatars.githubusercontent.com/u/2494769?v=4"/></br>[mrwulf](https://github.com/mrwulf) | <img width="50" src="https://avatars.githubusercontent.com/u/60824?v=4"/></br>[brttbndr](https://github.com/brttbndr) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/606038?v=4"/></br>[cas--](https://github.com/cas--) | <img width="50" src="https://avatars.githubusercontent.com/u/560571?v=4"/></br>[chrisb86](https://github.com/chrisb86) | <img width="50" src="https://avatars.githubusercontent.com/u/1686759?v=4"/></br>[chrmoritz](https://github.com/chrmoritz) | <img width="50" src="https://avatars.githubusercontent.com/u/11857950?v=4"/></br>[djunho](https://github.com/djunho) | <img width="50" src="https://avatars.githubusercontent.com/u/1044056?v=4"/></br>[daniellandau](https://github.com/daniellandau) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/40627944?v=4"/></br>[krote5k](https://github.com/krote5k) | <img width="50" src="https://avatars.githubusercontent.com/u/5001259?v=4"/></br>[ethan42411](https://github.com/ethan42411) | <img width="50" src="https://avatars.githubusercontent.com/u/2733783?v=4"/></br>[JOJ0](https://github.com/JOJ0) | <img width="50" src="https://avatars.githubusercontent.com/u/17108695?v=4"/></br>[jalajcodes](https://github.com/jalajcodes) | <img width="50" src="https://avatars.githubusercontent.com/u/238088?v=4"/></br>[jblunck](https://github.com/jblunck) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/3140223?v=4"/></br>[jdrobertso](https://github.com/jdrobertso) | <img width="50" src="https://avatars.githubusercontent.com/u/37297218?v=4"/></br>[Jesssullivan](https://github.com/Jesssullivan) | <img width="50" src="https://avatars.githubusercontent.com/u/339645?v=4"/></br>[jmontane](https://github.com/jmontane) | <img width="50" src="https://avatars.githubusercontent.com/u/69011?v=4"/></br>[johanhammar](https://github.com/johanhammar) | <img width="50" src="https://avatars.githubusercontent.com/u/71817691?v=4"/></br>[krishna8421](https://github.com/krishna8421) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/118282653?v=4"/></br>[Linkosred](https://github.com/Linkosred) | <img width="50" src="https://avatars.githubusercontent.com/u/4168339?v=4"/></br>[solariz](https://github.com/solariz) | <img width="50" src="https://avatars.githubusercontent.com/u/25288?v=4"/></br>[maicki](https://github.com/maicki) | <img width="50" src="https://avatars.githubusercontent.com/u/2136373?v=4"/></br>[mjjzf](https://github.com/mjjzf) | <img width="50" src="https://avatars.githubusercontent.com/u/7561827?v=4"/></br>[popovoleksandr](https://github.com/popovoleksandr) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/2501211?v=4"/></br>[Philipp91](https://github.com/Philipp91) | <img width="50" src="https://avatars.githubusercontent.com/u/27608187?v=4"/></br>[rt-oliveira](https://github.com/rt-oliveira) | <img width="50" src="https://avatars.githubusercontent.com/u/2486806?v=4"/></br>[sebastienjust](https://github.com/sebastienjust) | <img width="50" src="https://avatars.githubusercontent.com/u/28362310?v=4"/></br>[sealch](https://github.com/sealch) | <img width="50" src="https://avatars.githubusercontent.com/u/34258070?v=4"/></br>[StarFang208](https://github.com/StarFang208) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/1782292?v=4"/></br>[SubodhDahal](https://github.com/SubodhDahal) | <img width="50" src="https://avatars.githubusercontent.com/u/5912371?v=4"/></br>[TobiasDev](https://github.com/TobiasDev) | <img width="50" src="https://avatars.githubusercontent.com/u/53571657?v=4"/></br>[tmclo](https://github.com/tmclo) | <img width="50" src="https://avatars.githubusercontent.com/u/13502069?v=4"/></br>[Whaell](https://github.com/Whaell) | <img width="50" src="https://avatars.githubusercontent.com/u/29891001?v=4"/></br>[jyuvaraj03](https://github.com/jyuvaraj03) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/15380913?v=4"/></br>[kowalskidev](https://github.com/kowalskidev) | <img width="50" src="https://avatars.githubusercontent.com/u/337455?v=4"/></br>[alexchee](https://github.com/alexchee) | <img width="50" src="https://avatars.githubusercontent.com/u/5077221?v=4"/></br>[axq](https://github.com/axq) | <img width="50" src="https://avatars.githubusercontent.com/u/15636584?v=4"/></br>[balmag](https://github.com/balmag) | <img width="50" src="https://avatars.githubusercontent.com/u/8808502?v=4"/></br>[barbowza](https://github.com/barbowza) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/42007357?v=4"/></br>[eresytter](https://github.com/eresytter) | <img width="50" src="https://avatars.githubusercontent.com/u/4346449?v=4"/></br>[kik0220](https://github.com/kik0220) | <img width="50" src="https://avatars.githubusercontent.com/u/4316805?v=4"/></br>[stingray-11](https://github.com/stingray-11) | <img width="50" src="https://avatars.githubusercontent.com/u/11711053?v=4"/></br>[lscolombo](https://github.com/lscolombo) | <img width="50" src="https://avatars.githubusercontent.com/u/29355048?v=4"/></br>[majsterkovic](https://github.com/majsterkovic) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/17399340?v=4"/></br>[pf-siedler](https://github.com/pf-siedler) | <img width="50" src="https://avatars.githubusercontent.com/u/17232523?v=4"/></br>[ruuti](https://github.com/ruuti) | <img width="50" src="https://avatars.githubusercontent.com/u/23638148?v=4"/></br>[s1nceri7y](https://github.com/s1nceri7y) | <img width="50" src="https://avatars.githubusercontent.com/u/10117386?v=4"/></br>[kornava](https://github.com/kornava) | <img width="50" src="https://avatars.githubusercontent.com/u/36303913?v=4"/></br>[sensor-freak](https://github.com/sensor-freak) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/34542665?v=4"/></br>[paventyang](https://github.com/paventyang) | <img width="50" src="https://avatars.githubusercontent.com/u/7471938?v=4"/></br>[ShuiHuo](https://github.com/ShuiHuo) | <img width="50" src="https://avatars.githubusercontent.com/u/11596277?v=4"/></br>[ikunya](https://github.com/ikunya) | <img width="50" src="https://avatars.githubusercontent.com/u/59133880?v=4"/></br>[bedwardly-down](https://github.com/bedwardly-down) | <img width="50" src="https://avatars.githubusercontent.com/u/250887?v=4"/></br>[fstanis](https://github.com/fstanis) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/116026761?v=4"/></br>[sammyhori](https://github.com/sammyhori) | <img width="50" src="https://avatars.githubusercontent.com/u/47456195?v=4"/></br>[hexclover](https://github.com/hexclover) | <img width="50" src="https://avatars.githubusercontent.com/u/45535789?v=4"/></br>[2jaeyeol](https://github.com/2jaeyeol) | <img width="50" src="https://avatars.githubusercontent.com/u/25622825?v=4"/></br>[thackeraaron](https://github.com/thackeraaron) | <img width="50" src="https://avatars.githubusercontent.com/u/32984653?v=4"/></br>[AIbnuHIbban](https://github.com/AIbnuHIbban) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/8325984?v=4"/></br>[asalthobaity](https://github.com/asalthobaity) | <img width="50" src="https://avatars.githubusercontent.com/u/63901956?v=4"/></br>[abhi-bhatra](https://github.com/abhi-bhatra) | <img width="50" src="https://avatars.githubusercontent.com/u/56785486?v=4"/></br>[iamabhi222](https://github.com/iamabhi222) | <img width="50" src="https://avatars.githubusercontent.com/u/69760168?v=4"/></br>[waditos](https://github.com/waditos) | <img width="50" src="https://avatars.githubusercontent.com/u/61756360?v=4"/></br>[sandstone991](https://github.com/sandstone991) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/63443657?v=4"/></br>[Aksh-Konda](https://github.com/Aksh-Konda) | <img width="50" src="https://avatars.githubusercontent.com/u/3660978?v=4"/></br>[alanfortlink](https://github.com/alanfortlink) | <img width="50" src="https://avatars.githubusercontent.com/u/32174181?v=4"/></br>[alecmaly](https://github.com/alecmaly) | <img width="50" src="https://avatars.githubusercontent.com/u/53372753?v=4"/></br>[AverageUser2](https://github.com/AverageUser2) | <img width="50" src="https://avatars.githubusercontent.com/u/51818821?v=4"/></br>[adw2019](https://github.com/adw2019) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/4056990?v=4"/></br>[afischer211](https://github.com/afischer211) | <img width="50" src="https://avatars.githubusercontent.com/u/61735677?v=4"/></br>[bablecopherye](https://github.com/bablecopherye) | <img width="50" src="https://avatars.githubusercontent.com/u/26230870?v=4"/></br>[a13xk](https://github.com/a13xk) | <img width="50" src="https://avatars.githubusercontent.com/u/14836659?v=4"/></br>[apankratov](https://github.com/apankratov) | <img width="50" src="https://avatars.githubusercontent.com/u/7045739?v=4"/></br>[teterkin](https://github.com/teterkin) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/215668?v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars.githubusercontent.com/u/4408379?v=4"/></br>[lex111](https://github.com/lex111) | <img width="50" src="https://avatars.githubusercontent.com/u/60134194?v=4"/></br>[Alkindi42](https://github.com/Alkindi42) | <img width="50" src="https://avatars.githubusercontent.com/u/7129815?v=4"/></br>[Jumanjii](https://github.com/Jumanjii) | <img width="50" src="https://avatars.githubusercontent.com/u/19962243?v=4"/></br>[AlphaJack](https://github.com/AlphaJack) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/65647302?v=4"/></br>[Lord-Aman](https://github.com/Lord-Aman) | <img width="50" src="https://avatars.githubusercontent.com/u/12948692?v=4"/></br>[aminvakil](https://github.com/aminvakil) | <img width="50" src="https://avatars.githubusercontent.com/u/14096959?v=4"/></br>[richtwin567](https://github.com/richtwin567) | <img width="50" src="https://avatars.githubusercontent.com/u/487182?v=4"/></br>[andrejilderda](https://github.com/andrejilderda) | <img width="50" src="https://avatars.githubusercontent.com/u/18169566?v=4"/></br>[deining](https://github.com/deining) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/922429?v=4"/></br>[adrynov](https://github.com/adrynov) | <img width="50" src="https://avatars.githubusercontent.com/u/94937?v=4"/></br>[andrewperry](https://github.com/andrewperry) | <img width="50" src="https://avatars.githubusercontent.com/u/5417051?v=4"/></br>[tekdel](https://github.com/tekdel) | <img width="50" src="https://avatars.githubusercontent.com/u/4471821?v=4"/></br>[fobo66](https://github.com/fobo66) | <img width="50" src="https://avatars.githubusercontent.com/u/7015947?v=4"/></br>[andzs](https://github.com/andzs) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/54475686?v=4"/></br>[pandeymangg](https://github.com/pandeymangg) | <img width="50" src="https://avatars.githubusercontent.com/u/25694659?v=4"/></br>[rasklaad](https://github.com/rasklaad) | <img width="50" src="https://avatars.githubusercontent.com/u/498326?v=4"/></br>[Shaxine](https://github.com/Shaxine) | <img width="50" src="https://avatars.githubusercontent.com/u/9095073?v=4"/></br>[antonio-ramadas](https://github.com/antonio-ramadas) | <img width="50" src="https://avatars.githubusercontent.com/u/28067395?v=4"/></br>[aprvsh](https://github.com/aprvsh) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/75756768?v=4"/></br>[aynp](https://github.com/aynp) | <img width="50" src="https://avatars.githubusercontent.com/u/201215?v=4"/></br>[assimd](https://github.com/assimd) | <img width="50" src="https://avatars.githubusercontent.com/u/26827848?v=4"/></br>[Atrate](https://github.com/Atrate) | <img width="50" src="https://avatars.githubusercontent.com/u/794314?v=4"/></br>[austindoupnik](https://github.com/austindoupnik) | <img width="50" src="https://avatars.githubusercontent.com/u/110668146?v=4"/></br>[BeeverTeeth](https://github.com/BeeverTeeth) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/16528381?v=4"/></br>[be-we](https://github.com/be-we) | <img width="50" src="https://avatars.githubusercontent.com/u/1899506?v=4"/></br>[ei8fdb](https://github.com/ei8fdb) | <img width="50" src="https://avatars.githubusercontent.com/u/7034200?v=4"/></br>[bimlas](https://github.com/bimlas) | <img width="50" src="https://avatars.githubusercontent.com/u/58605547?v=4"/></br>[bishoy-magdy](https://github.com/bishoy-magdy) | <img width="50" src="https://avatars.githubusercontent.com/u/1614?v=4"/></br>[brad](https://github.com/brad) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/47641641?v=4"/></br>[brenobaptista](https://github.com/brenobaptista) | <img width="50" src="https://avatars.githubusercontent.com/u/1001769?v=4"/></br>[CandleCandle](https://github.com/CandleCandle) | <img width="50" src="https://avatars.githubusercontent.com/u/16287077?v=4"/></br>[carlbordum](https://github.com/carlbordum) | <img width="50" src="https://avatars.githubusercontent.com/u/36130773?v=4"/></br>[carlosngo](https://github.com/carlosngo) | <img width="50" src="https://avatars.githubusercontent.com/u/20382?v=4"/></br>[carlosedp](https://github.com/carlosedp) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/105843?v=4"/></br>[chaifeng](https://github.com/chaifeng) | <img width="50" src="https://avatars.githubusercontent.com/u/549349?v=4"/></br>[charles-e](https://github.com/charles-e) | <img width="50" src="https://avatars.githubusercontent.com/u/19870089?v=4"/></br>[cyy53589](https://github.com/cyy53589) | <img width="50" src="https://avatars.githubusercontent.com/u/32337926?v=4"/></br>[Chillu1](https://github.com/Chillu1) | <img width="50" src="https://avatars.githubusercontent.com/u/2348463?v=4"/></br>[Techwolf12](https://github.com/Techwolf12) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/22433652?v=4"/></br>[christopher-o-toole](https://github.com/christopher-o-toole) | <img width="50" src="https://avatars.githubusercontent.com/u/2282880?v=4"/></br>[cloudtrends](https://github.com/cloudtrends) | <img width="50" src="https://avatars.githubusercontent.com/u/17257053?v=4"/></br>[idcristi](https://github.com/idcristi) | <img width="50" src="https://avatars.githubusercontent.com/u/15956322?v=4"/></br>[damienmascre](https://github.com/damienmascre) | <img width="50" src="https://avatars.githubusercontent.com/u/1102886?v=4"/></br>[da2x](https://github.com/da2x) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/26678?v=4"/></br>[danielb2](https://github.com/danielb2) | <img width="50" src="https://avatars.githubusercontent.com/u/12847693?v=4"/></br>[danil-tolkachev](https://github.com/danil-tolkachev) | <img width="50" src="https://avatars.githubusercontent.com/u/7279100?v=4"/></br>[darshani28](https://github.com/darshani28) | <img width="50" src="https://avatars.githubusercontent.com/u/26189247?v=4"/></br>[daukadolt](https://github.com/daukadolt) | <img width="50" src="https://avatars.githubusercontent.com/u/3041566?v=4"/></br>[DavidBeale](https://github.com/DavidBeale) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/28535750?v=4"/></br>[NeverMendel](https://github.com/NeverMendel) | <img width="50" src="https://avatars.githubusercontent.com/u/81250703?v=4"/></br>[Mr-DG-Wick](https://github.com/Mr-DG-Wick) | <img width="50" src="https://avatars.githubusercontent.com/u/2138893?v=4"/></br>[DG0lden](https://github.com/DG0lden) | <img width="50" src="https://avatars.githubusercontent.com/u/54697735?v=4"/></br>[deunlee](https://github.com/deunlee) | <img width="50" src="https://avatars.githubusercontent.com/u/11378282?v=4"/></br>[diego-betto](https://github.com/diego-betto) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/215270?v=4"/></br>[erdody](https://github.com/erdody) | <img width="50" src="https://avatars.githubusercontent.com/u/36959928?v=4"/></br>[diragb](https://github.com/diragb) | <img width="50" src="https://avatars.githubusercontent.com/u/10371667?v=4"/></br>[domgoodwin](https://github.com/domgoodwin) | <img width="50" src="https://avatars.githubusercontent.com/u/72066?v=4"/></br>[b4mboo](https://github.com/b4mboo) | <img width="50" src="https://avatars.githubusercontent.com/u/5131923?v=4"/></br>[donbowman](https://github.com/donbowman) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/60024671?v=4"/></br>[DeeJayLSP](https://github.com/DeeJayLSP) | <img width="50" src="https://avatars.githubusercontent.com/u/579727?v=4"/></br>[sirnacnud](https://github.com/sirnacnud) | <img width="50" src="https://avatars.githubusercontent.com/u/47756?v=4"/></br>[dflock](https://github.com/dflock) | <img width="50" src="https://avatars.githubusercontent.com/u/7990534?v=4"/></br>[drobilica](https://github.com/drobilica) | <img width="50" src="https://avatars.githubusercontent.com/u/21699905?v=4"/></br>[educbraga](https://github.com/educbraga) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/92958867?v=4"/></br>[eduebernal](https://github.com/eduebernal) | <img width="50" src="https://avatars.githubusercontent.com/u/67867099?v=4"/></br>[eduardokimmel](https://github.com/eduardokimmel) | <img width="50" src="https://avatars.githubusercontent.com/u/17415256?v=4"/></br>[ei-ke](https://github.com/ei-ke) | <img width="50" src="https://avatars.githubusercontent.com/u/1962738?v=4"/></br>[einverne](https://github.com/einverne) | <img width="50" src="https://avatars.githubusercontent.com/u/15069703?v=4"/></br>[etho201](https://github.com/etho201) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/16492558?v=4"/></br>[eodeluga](https://github.com/eodeluga) | <img width="50" src="https://avatars.githubusercontent.com/u/16875937?v=4"/></br>[fathyar](https://github.com/fathyar) | <img width="50" src="https://avatars.githubusercontent.com/u/73366988?v=4"/></br>[Fejby](https://github.com/Fejby) | <img width="50" src="https://avatars.githubusercontent.com/u/126302554?v=4"/></br>[fkinoshita](https://github.com/fkinoshita) | <img width="50" src="https://avatars.githubusercontent.com/u/3057302?v=4"/></br>[fer22f](https://github.com/fer22f) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/43272148?v=4"/></br>[fpindado](https://github.com/fpindado) | <img width="50" src="https://avatars.githubusercontent.com/u/1714374?v=4"/></br>[FleischKarussel](https://github.com/FleischKarussel) | <img width="50" src="https://avatars.githubusercontent.com/u/23738961?v=4"/></br>[easyteacher](https://github.com/easyteacher) | <img width="50" src="https://avatars.githubusercontent.com/u/110087?v=4"/></br>[halkeye](https://github.com/halkeye) | <img width="50" src="https://avatars.githubusercontent.com/u/19814827?v=4"/></br>[gmaubach](https://github.com/gmaubach) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/6190183?v=4"/></br>[gmag11](https://github.com/gmag11) | <img width="50" src="https://avatars.githubusercontent.com/u/6209647?v=4"/></br>[Jackymancs4](https://github.com/Jackymancs4) | <img width="50" src="https://avatars.githubusercontent.com/u/1501599?v=4"/></br>[gitstart](https://github.com/gitstart) | <img width="50" src="https://avatars.githubusercontent.com/u/297578?v=4"/></br>[Glandos](https://github.com/Glandos) | <img width="50" src="https://avatars.githubusercontent.com/u/24235344?v=4"/></br>[ggteixeira](https://github.com/ggteixeira) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/2257024?v=4"/></br>[gusbemacbe](https://github.com/gusbemacbe) | <img width="50" src="https://avatars.githubusercontent.com/u/64917442?v=4"/></br>[HOLLYwyh](https://github.com/HOLLYwyh) | <img width="50" src="https://avatars.githubusercontent.com/u/18524580?v=4"/></br>[Fvbor](https://github.com/Fvbor) | <img width="50" src="https://avatars.githubusercontent.com/u/16725441?v=4"/></br>[hamishmb](https://github.com/hamishmb) | <img width="50" src="https://avatars.githubusercontent.com/u/22606250?v=4"/></br>[bennetthanna](https://github.com/bennetthanna) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/78360007?v=4"/></br>[graueneko](https://github.com/graueneko) | <img width="50" src="https://avatars.githubusercontent.com/u/67231570?v=4"/></br>[harshitkathuria](https://github.com/harshitkathuria) | <img width="50" src="https://avatars.githubusercontent.com/u/1716229?v=4"/></br>[Vistaus](https://github.com/Vistaus) | <img width="50" src="https://avatars.githubusercontent.com/u/47787284?v=4"/></br>[gtlsgamr](https://github.com/gtlsgamr) | <img width="50" src="https://avatars.githubusercontent.com/u/32321396?v=4"/></br>[horaceyoung](https://github.com/horaceyoung) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/6509881?v=4"/></br>[ianjs](https://github.com/ianjs) | <img width="50" src="https://avatars.githubusercontent.com/u/19862172?v=4"/></br>[iahmedbacha](https://github.com/iahmedbacha) | <img width="50" src="https://avatars.githubusercontent.com/u/76095?v=4"/></br>[caseycs](https://github.com/caseycs) | <img width="50" src="https://avatars.githubusercontent.com/u/1533624?v=4"/></br>[IrvinDominin](https://github.com/IrvinDominin) | <img width="50" src="https://avatars.githubusercontent.com/u/33200024?v=4"/></br>[ishammahajan](https://github.com/ishammahajan) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/6916297?v=4"/></br>[ffadilaputra](https://github.com/ffadilaputra) | <img width="50" src="https://avatars.githubusercontent.com/u/64505041?v=4"/></br>[Iwantgreencard](https://github.com/Iwantgreencard) | <img width="50" src="https://avatars.githubusercontent.com/u/16137232?v=4"/></br>[j0hn-mc-clane](https://github.com/j0hn-mc-clane) | <img width="50" src="https://avatars.githubusercontent.com/u/19985741?v=4"/></br>[JRaiden16](https://github.com/JRaiden16) | <img width="50" src="https://avatars.githubusercontent.com/u/11466782?v=4"/></br>[jacobherrington](https://github.com/jacobherrington) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/9365179?v=4"/></br>[jamesadjinwa](https://github.com/jamesadjinwa) | <img width="50" src="https://avatars.githubusercontent.com/u/20801821?v=4"/></br>[jrwrigh](https://github.com/jrwrigh) | <img width="50" src="https://avatars.githubusercontent.com/u/7652978?v=4"/></br>[analogist](https://github.com/analogist) | <img width="50" src="https://avatars.githubusercontent.com/u/4995433?v=4"/></br>[jaredcrowe](https://github.com/jaredcrowe) | <img width="50" src="https://avatars.githubusercontent.com/u/936006?v=4"/></br>[jasonwilliams](https://github.com/jasonwilliams) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/4087105?v=4"/></br>[volatilevar](https://github.com/volatilevar) | <img width="50" src="https://avatars.githubusercontent.com/u/47724360?v=4"/></br>[innkuika](https://github.com/innkuika) | <img width="50" src="https://avatars.githubusercontent.com/u/163555?v=4"/></br>[JoelRSimpson](https://github.com/JoelRSimpson) | <img width="50" src="https://avatars.githubusercontent.com/u/6965062?v=4"/></br>[joeltaylor](https://github.com/joeltaylor) | <img width="50" src="https://avatars.githubusercontent.com/u/1133852?v=4"/></br>[thejohnfreeman](https://github.com/thejohnfreeman) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/242107?v=4"/></br>[exic](https://github.com/exic) | <img width="50" src="https://avatars.githubusercontent.com/u/13716151?v=4"/></br>[JonathanPlasse](https://github.com/JonathanPlasse) | <img width="50" src="https://avatars.githubusercontent.com/u/2520458?v=4"/></br>[joschaschmiedt](https://github.com/joschaschmiedt) | <img width="50" src="https://avatars.githubusercontent.com/u/1248504?v=4"/></br>[joesfer](https://github.com/joesfer) | <img width="50" src="https://avatars.githubusercontent.com/u/1586795?v=4"/></br>[joserebelo](https://github.com/joserebelo) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/6048003?v=4"/></br>[joybinchen](https://github.com/joybinchen) | <img width="50" src="https://avatars.githubusercontent.com/u/31921499?v=4"/></br>[Juvecu](https://github.com/Juvecu) | <img width="50" src="https://avatars.githubusercontent.com/u/20700283?v=4"/></br>[KaneGreen](https://github.com/KaneGreen) | <img width="50" src="https://avatars.githubusercontent.com/u/37601331?v=4"/></br>[kaustubhsh](https://github.com/kaustubhsh) | <img width="50" src="https://avatars.githubusercontent.com/u/1560189?v=4"/></br>[y-usuzumi](https://github.com/y-usuzumi) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/56685204?v=4"/></br>[kevinshu1995](https://github.com/kevinshu1995) | <img width="50" src="https://avatars.githubusercontent.com/u/98966350?v=4"/></br>[Kevin-vdberg](https://github.com/Kevin-vdberg) | <img width="50" src="https://avatars.githubusercontent.com/u/11942650?v=4"/></br>[kkoyung](https://github.com/kkoyung) | <img width="50" src="https://avatars.githubusercontent.com/u/1660460?v=4"/></br>[xuhcc](https://github.com/xuhcc) | <img width="50" src="https://avatars.githubusercontent.com/u/16933735?v=4"/></br>[kirtanprht](https://github.com/kirtanprht) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/37491732?v=4"/></br>[k0ur0x](https://github.com/k0ur0x) | <img width="50" src="https://avatars.githubusercontent.com/u/7824233?v=4"/></br>[kklas](https://github.com/kklas) | <img width="50" src="https://avatars.githubusercontent.com/u/8622992?v=4"/></br>[xmlangel](https://github.com/xmlangel) | <img width="50" src="https://avatars.githubusercontent.com/u/465678?v=4"/></br>[Letty](https://github.com/Letty) | <img width="50" src="https://avatars.githubusercontent.com/u/1055100?v=4"/></br>[troilus](https://github.com/troilus) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/47911535?v=4"/></br>[LightAPIs](https://github.com/LightAPIs) | <img width="50" src="https://avatars.githubusercontent.com/u/35413451?v=4"/></br>[Longhao-Chen](https://github.com/Longhao-Chen) | <img width="50" src="https://avatars.githubusercontent.com/u/50335724?v=4"/></br>[diogocaveiro](https://github.com/diogocaveiro) | <img width="50" src="https://avatars.githubusercontent.com/u/2599210?v=4"/></br>[lboullo0](https://github.com/lboullo0) | <img width="50" src="https://avatars.githubusercontent.com/u/1562062?v=4"/></br>[luisperezmarin](https://github.com/luisperezmarin) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/18382454?v=4"/></br>[MHolkamp](https://github.com/MHolkamp) | <img width="50" src="https://avatars.githubusercontent.com/u/15436007?v=4"/></br>[marc-bouvier](https://github.com/marc-bouvier) | <img width="50" src="https://avatars.githubusercontent.com/u/5699725?v=4"/></br>[mvonmaltitz](https://github.com/mvonmaltitz) | <img width="50" src="https://avatars.githubusercontent.com/u/11036464?v=4"/></br>[mlkood](https://github.com/mlkood) | <img width="50" src="https://avatars.githubusercontent.com/u/2480960?v=4"/></br>[plextoriano](https://github.com/plextoriano) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/5788516?v=4"/></br>[Marmo](https://github.com/Marmo) | <img width="50" src="https://avatars.githubusercontent.com/u/29300939?v=4"/></br>[mcejp](https://github.com/mcejp) | <img width="50" src="https://avatars.githubusercontent.com/u/640949?v=4"/></br>[freaktechnik](https://github.com/freaktechnik) | <img width="50" src="https://avatars.githubusercontent.com/u/79802125?v=4"/></br>[martinkorelic](https://github.com/martinkorelic) | <img width="50" src="https://avatars.githubusercontent.com/u/287105?v=4"/></br>[Petemir](https://github.com/Petemir) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/5218859?v=4"/></br>[matsair](https://github.com/matsair) | <img width="50" src="https://avatars.githubusercontent.com/u/7098804?v=4"/></br>[MattDemers](https://github.com/MattDemers) | <img width="50" src="https://avatars.githubusercontent.com/u/12831489?v=4"/></br>[mgroth0](https://github.com/mgroth0) | <img width="50" src="https://avatars.githubusercontent.com/u/21796?v=4"/></br>[silentmatt](https://github.com/silentmatt) | <img width="50" src="https://avatars.githubusercontent.com/u/76700192?v=4"/></br>[maxs-test](https://github.com/maxs-test) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/59669349?v=4"/></br>[MichBoi](https://github.com/MichBoi) | <img width="50" src="https://avatars.githubusercontent.com/u/3941344?v=4"/></br>[MikkCZ](https://github.com/MikkCZ) | <img width="50" src="https://avatars.githubusercontent.com/u/51273874?v=4"/></br>[MichipX](https://github.com/MichipX) | <img width="50" src="https://avatars.githubusercontent.com/u/59350?v=4"/></br>[Elleo](https://github.com/Elleo) | <img width="50" src="https://avatars.githubusercontent.com/u/14942380?v=4"/></br>[phucbm](https://github.com/phucbm) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/72992390?v=4"/></br>[miucci](https://github.com/miucci) | <img width="50" src="https://avatars.githubusercontent.com/u/40818895?v=4"/></br>[MovingEarth](https://github.com/MovingEarth) | <img width="50" src="https://avatars.githubusercontent.com/u/53177864?v=4"/></br>[MrTraduttore](https://github.com/MrTraduttore) | <img width="50" src="https://avatars.githubusercontent.com/u/48156230?v=4"/></br>[sanjarcode](https://github.com/sanjarcode) | <img width="50" src="https://avatars.githubusercontent.com/u/43955099?v=4"/></br>[Mustafa-ALD](https://github.com/Mustafa-ALD) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/1592048?v=4"/></br>[LeMyst](https://github.com/LeMyst) | <img width="50" src="https://avatars.githubusercontent.com/u/66901039?v=4"/></br>[matmolni](https://github.com/matmolni) | <img width="50" src="https://avatars.githubusercontent.com/u/9076687?v=4"/></br>[NJannasch](https://github.com/NJannasch) | <img width="50" src="https://avatars.githubusercontent.com/u/77619?v=4"/></br>[kna](https://github.com/kna) | <img width="50" src="https://avatars.githubusercontent.com/u/8016073?v=4"/></br>[zomglings](https://github.com/zomglings) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/63354547?v=4"/></br>[nicholas-10](https://github.com/nicholas-10) | <img width="50" src="https://avatars.githubusercontent.com/u/11778560?v=4"/></br>[nickhobbs94](https://github.com/nickhobbs94) | <img width="50" src="https://avatars.githubusercontent.com/u/10386884?v=4"/></br>[Frichetten](https://github.com/Frichetten) | <img width="50" src="https://avatars.githubusercontent.com/u/5541611?v=4"/></br>[nicolas-suzuki](https://github.com/nicolas-suzuki) | <img width="50" src="https://avatars.githubusercontent.com/u/15157120?v=4"/></br>[Nicryc](https://github.com/Nicryc) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/40800932?v=4"/></br>[nik-gautam](https://github.com/nik-gautam) | <img width="50" src="https://avatars.githubusercontent.com/u/48302704?v=4"/></br>[noah-nash](https://github.com/noah-nash) | <img width="50" src="https://avatars.githubusercontent.com/u/90026187?v=4"/></br>[OmGole](https://github.com/OmGole) | <img width="50" src="https://avatars.githubusercontent.com/u/12369770?v=4"/></br>[Ouvill](https://github.com/Ouvill) | <img width="50" src="https://avatars.githubusercontent.com/u/43815417?v=4"/></br>[shorty2380](https://github.com/shorty2380) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/15014287?v=4"/></br>[dist3r](https://github.com/dist3r) | <img width="50" src="https://avatars.githubusercontent.com/u/19418601?v=4"/></br>[rakleed](https://github.com/rakleed) | <img width="50" src="https://avatars.githubusercontent.com/u/7881932?v=4"/></br>[idle-code](https://github.com/idle-code) | <img width="50" src="https://avatars.githubusercontent.com/u/13076552?v=4"/></br>[Oaklight](https://github.com/Oaklight) | <img width="50" src="https://avatars.githubusercontent.com/u/53913724?v=4"/></br>[Perkolator](https://github.com/Perkolator) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/24394304?v=4"/></br>[petzi53](https://github.com/petzi53) | <img width="50" src="https://avatars.githubusercontent.com/u/29787?v=4"/></br>[phitsc](https://github.com/phitsc) | <img width="50" src="https://avatars.githubusercontent.com/u/56399446?v=4"/></br>[KowalskiPiotr98](https://github.com/KowalskiPiotr98) | <img width="50" src="https://avatars.githubusercontent.com/u/64375061?v=4"/></br>[Polaris66](https://github.com/Polaris66) | <img width="50" src="https://avatars.githubusercontent.com/u/6306608?v=4"/></br>[Diadlo](https://github.com/Diadlo) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/42793024?v=4"/></br>[pranavmodx](https://github.com/pranavmodx) | <img width="50" src="https://avatars.githubusercontent.com/u/50834839?v=4"/></br>[R3dError](https://github.com/R3dError) | <img width="50" src="https://avatars.githubusercontent.com/u/42652941?v=4"/></br>[rajprakash00](https://github.com/rajprakash00) | <img width="50" src="https://avatars.githubusercontent.com/u/32304956?v=4"/></br>[rahil1304](https://github.com/rahil1304) | <img width="50" src="https://avatars.githubusercontent.com/u/8257474?v=4"/></br>[rasulkireev](https://github.com/rasulkireev) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/17312341?v=4"/></br>[reinhart1010](https://github.com/reinhart1010) | <img width="50" src="https://avatars.githubusercontent.com/u/60484714?v=4"/></br>[Retew](https://github.com/Retew) | <img width="50" src="https://avatars.githubusercontent.com/u/10456131?v=4"/></br>[ambrt](https://github.com/ambrt) | <img width="50" src="https://avatars.githubusercontent.com/u/791713?v=4"/></br>[rio-codes](https://github.com/rio-codes) | <img width="50" src="https://avatars.githubusercontent.com/u/568673?v=4"/></br>[robmoffat](https://github.com/robmoffat) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/15892014?v=4"/></br>[Derkades](https://github.com/Derkades) | <img width="50" src="https://avatars.githubusercontent.com/u/49439044?v=4"/></br>[fourstepper](https://github.com/fourstepper) | <img width="50" src="https://avatars.githubusercontent.com/u/54365?v=4"/></br>[rodgco](https://github.com/rodgco) | <img width="50" src="https://avatars.githubusercontent.com/u/96014?v=4"/></br>[Ronnie76er](https://github.com/Ronnie76er) | <img width="50" src="https://avatars.githubusercontent.com/u/79168?v=4"/></br>[roryokane](https://github.com/roryokane) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/744655?v=4"/></br>[ruzaq](https://github.com/ruzaq) | <img width="50" src="https://avatars.githubusercontent.com/u/20490839?v=4"/></br>[szokesandor](https://github.com/szokesandor) | <img width="50" src="https://avatars.githubusercontent.com/u/10775512?v=4"/></br>[forsh4w](https://github.com/forsh4w) | <img width="50" src="https://avatars.githubusercontent.com/u/19328605?v=4"/></br>[SamuelBlickle](https://github.com/SamuelBlickle) | <img width="50" src="https://avatars.githubusercontent.com/u/80849457?v=4"/></br>[livingc0l0ur](https://github.com/livingc0l0ur) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/1776?v=4"/></br>[bronson](https://github.com/bronson) | <img width="50" src="https://avatars.githubusercontent.com/u/426959?v=4"/></br>[sebthom](https://github.com/sebthom) | <img width="50" src="https://avatars.githubusercontent.com/u/24606935?v=4"/></br>[semperor](https://github.com/semperor) | <img width="50" src="https://avatars.githubusercontent.com/u/18042424?v=4"/></br>[SeptemberHX](https://github.com/SeptemberHX) | <img width="50" src="https://avatars.githubusercontent.com/u/607938?v=4"/></br>[shawnaxsom](https://github.com/shawnaxsom) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/2786333?v=4"/></br>[hurutoriya](https://github.com/hurutoriya) | <img width="50" src="https://avatars.githubusercontent.com/u/60000624?v=4"/></br>[siddharthmagadum16](https://github.com/siddharthmagadum16) | <img width="50" src="https://avatars.githubusercontent.com/u/30827929?v=4"/></br>[5idereal](https://github.com/5idereal) | <img width="50" src="https://avatars.githubusercontent.com/u/43588516?v=4"/></br>[stephan-dev](https://github.com/stephan-dev) | <img width="50" src="https://avatars.githubusercontent.com/u/9937486?v=4"/></br>[SFoskitt](https://github.com/SFoskitt) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/24674127?v=4"/></br>[stephanoskomnenos](https://github.com/stephanoskomnenos) | <img width="50" src="https://avatars.githubusercontent.com/u/94064167?v=4"/></br>[WebSnke](https://github.com/WebSnke) | <img width="50" src="https://avatars.githubusercontent.com/u/505011?v=4"/></br>[kcrt](https://github.com/kcrt) | <img width="50" src="https://avatars.githubusercontent.com/u/538584?v=4"/></br>[xissy](https://github.com/xissy) | <img width="50" src="https://avatars.githubusercontent.com/u/164962?v=4"/></br>[tams](https://github.com/tams) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/466122?v=4"/></br>[Tekki](https://github.com/Tekki) | <img width="50" src="https://avatars.githubusercontent.com/u/13370356?v=4"/></br>[Teko-uy](https://github.com/Teko-uy) | <img width="50" src="https://avatars.githubusercontent.com/u/2112477?v=4"/></br>[ThatcherC](https://github.com/ThatcherC) | <img width="50" src="https://avatars.githubusercontent.com/u/21969426?v=4"/></br>[TheoDutch](https://github.com/TheoDutch) | <img width="50" src="https://avatars.githubusercontent.com/u/19636565?v=4"/></br>[Theta-Dev](https://github.com/Theta-Dev) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/12467511?v=4"/></br>[ThibaultJanBeyer](https://github.com/ThibaultJanBeyer) | <img width="50" src="https://avatars.githubusercontent.com/u/8731922?v=4"/></br>[tbroadley](https://github.com/tbroadley) | <img width="50" src="https://avatars.githubusercontent.com/u/114300?v=4"/></br>[Kriechi](https://github.com/Kriechi) | <img width="50" src="https://avatars.githubusercontent.com/u/3457339?v=4"/></br>[tkilaker](https://github.com/tkilaker) | <img width="50" src="https://avatars.githubusercontent.com/u/802148?v=4"/></br>[Archelyst](https://github.com/Archelyst) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/4201229?v=4"/></br>[tcyrus](https://github.com/tcyrus) | <img width="50" src="https://avatars.githubusercontent.com/u/834914?v=4"/></br>[tobias-grasse](https://github.com/tobias-grasse) | <img width="50" src="https://avatars.githubusercontent.com/u/6691273?v=4"/></br>[strobeltobias](https://github.com/strobeltobias) | <img width="50" src="https://avatars.githubusercontent.com/u/1677578?v=4"/></br>[kostegit](https://github.com/kostegit) | <img width="50" src="https://avatars.githubusercontent.com/u/9092682?v=4"/></br>[TomBursch](https://github.com/TomBursch) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/70296?v=4"/></br>[tbergeron](https://github.com/tbergeron) | <img width="50" src="https://avatars.githubusercontent.com/u/1117052?v=4"/></br>[tbjers](https://github.com/tbjers) | <img width="50" src="https://avatars.githubusercontent.com/u/238296?v=4"/></br>[trentlarson](https://github.com/trentlarson) | <img width="50" src="https://avatars.githubusercontent.com/u/10265443?v=4"/></br>[Ullas-Aithal](https://github.com/Ullas-Aithal) | <img width="50" src="https://avatars.githubusercontent.com/u/6104498?v=4"/></br>[vdeville](https://github.com/vdeville) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/2830093?v=4"/></br>[vassudanagunta](https://github.com/vassudanagunta) | <img width="50" src="https://avatars.githubusercontent.com/u/54314949?v=4"/></br>[vijayjoshi16](https://github.com/vijayjoshi16) | <img width="50" src="https://avatars.githubusercontent.com/u/13400593?v=4"/></br>[vjocw](https://github.com/vjocw) | <img width="50" src="https://avatars.githubusercontent.com/u/59287619?v=4"/></br>[max-keviv](https://github.com/max-keviv) | <img width="50" src="https://avatars.githubusercontent.com/u/598576?v=4"/></br>[vandreykiv](https://github.com/vandreykiv) |
|
||||
| <img width="50" src="https://avatars.githubusercontent.com/u/4094814?v=4"/></br>[warddr](https://github.com/warddr) | <img width="50" src="https://avatars.githubusercontent.com/u/22241609?v=4"/></br>[westfalenyeti](https://github.com/westfalenyeti) | <img width="50" src="https://avatars.githubusercontent.com/u/26511487?v=4"/></br>[WisdomCode](https://github.com/WisdomCode) | <img width="50" src="https://avatars.githubusercontent.com/u/48159366?v=4"/></br>[X3NOOO](https://github.com/X3NOOO) | <img width="50" src="https://avatars.githubusercontent.com/u/1921957?v=4"/></br>[xsak](https://github.com/xsak) |
|
||||
| | | | | |
|
||||
<!-- CONTRIBUTORS-TABLE-AUTO-GENERATED -->
|
||||
|
||||
@@ -11,8 +11,9 @@ module.exports = {
|
||||
//
|
||||
// '**/*.ts?(x)': () => 'npm run tsc',
|
||||
'*.{js,jsx,ts,tsx}': [
|
||||
'yarn run linter-precommit',
|
||||
'yarn run checkIgnoredFiles',
|
||||
'yarn run checkLibPaths',
|
||||
'node packages/tools/checkIgnoredFiles.js',
|
||||
'yarn run packageJsonLint',
|
||||
'yarn run linter-precommit',
|
||||
],
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"buildWebsiteTranslations": "node packages/tools/website/buildTranslations.js",
|
||||
"buildWebsite": "node ./packages/tools/website/build.js && yarn run buildPluginDoc && yarn run buildSettingJsonSchema",
|
||||
"checkLibPaths": "node ./packages/tools/checkLibPaths.js",
|
||||
"checkIgnoredFiles": "node ./packages/tools/checkIgnoredFiles.js",
|
||||
"circularDependencyCheck": "madge --warning --circular --extensions js ./",
|
||||
"clean": "npm run clean --workspaces --if-present && node packages/tools/clean && yarn cache clean",
|
||||
"dependencyTree": "madge",
|
||||
@@ -33,7 +34,7 @@
|
||||
"linter-precommit": "eslint --resolve-plugins-relative-to . --fix --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"linter": "eslint --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"linter-interactive": "eslint-interactive --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"packageJsonLint": "npmPkgJsonLint --configFile .npmpackagejsonlintrc.json --quiet .",
|
||||
"packageJsonLint": "node ./packages/tools/packageJsonLint.js",
|
||||
"postinstall": "gulp build",
|
||||
"publishAll": "git pull && yarn run buildParallel && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
|
||||
"releaseAndroid": "PATH=\"/usr/local/opt/openjdk@11/bin:$PATH\" node packages/tools/release-android.js",
|
||||
@@ -60,7 +61,7 @@
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged && yarn run packageJsonLint"
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -50,7 +50,7 @@ class LinkSelector {
|
||||
link: matches[n][0],
|
||||
noteX: matches[n].index,
|
||||
noteY: i,
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -482,7 +482,7 @@ class AppGui {
|
||||
if (this.linkSelector_.link) {
|
||||
this.term_.moveTo(
|
||||
this.linkSelector_.noteX + cursorOffsetX,
|
||||
this.linkSelector_.noteY + cursorOffsetY
|
||||
this.linkSelector_.noteY + cursorOffsetY,
|
||||
);
|
||||
shim.setTimeout(() => this.term_.term().inverse(this.linkSelector_.link), 50);
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ class Command extends BaseCommand {
|
||||
reg.db(),
|
||||
sync.lockHandler(),
|
||||
appTypeToLockType(Setting.value('appType')),
|
||||
Setting.value('clientId')
|
||||
Setting.value('clientId'),
|
||||
);
|
||||
|
||||
migrationHandler.setLogger(cliUtils.stdoutLogger(this.stdout.bind(this)));
|
||||
|
||||
@@ -39,7 +39,7 @@ async function createClients() {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
execCommand(client, 'config sync.target 2').then(() => {
|
||||
return execCommand(client, `config sync.2.path ${syncDir}`);
|
||||
})
|
||||
}),
|
||||
);
|
||||
output.push(client);
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ describe('MdToHtml', () => {
|
||||
const result = await mdToHtml.render(input, null, { bodyOnly: true, mapsToLine: true });
|
||||
expect(result.html.trim()).toBe('<h1 id="head" class="maps-to-line" source-line="0" source-line-end="1">Head</h1>\n' +
|
||||
'<p class="maps-to-line" source-line="1" source-line-end="2">Fruits</p>\n' +
|
||||
'<ul>\n<li class="maps-to-line" source-line="2" source-line-end="3">Apple</li>\n</ul>'
|
||||
'<ul>\n<li class="maps-to-line" source-line="2" source-line-end="3">Apple</li>\n</ul>',
|
||||
);
|
||||
}
|
||||
}));
|
||||
|
||||
18
packages/app-cli/tests/enex_to_md/invalid_html.enex
Normal file
18
packages/app-cli/tests/enex_to_md/invalid_html.enex
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE en-export SYSTEM "http://xml.evernote.com/pub/evernote-export4.dtd">
|
||||
<en-export export-date="20230811T195236Z" application="Evernote" version="10.58.8">
|
||||
<note>
|
||||
<title>DSCN0716.JPG</title>
|
||||
<created>20130228T193612Z</created>
|
||||
<updated>20230616T123049Z</updated>
|
||||
<tag>Eye-Fi</tag>
|
||||
<tag>2013-02-27</tag>
|
||||
<tag>records</tag>
|
||||
<note-attributes>
|
||||
</note-attributes>
|
||||
<content>
|
||||
<![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><en-media type="image/jpeg" hash="33c6ed66e09569ed167dec1e1fd66b87"/><br/><![CDATA[DSCN0716.JPG]]></en-note> ]]>
|
||||
</content>
|
||||
</note>
|
||||
</en-export>
|
||||
@@ -24,7 +24,7 @@ function newPluginService(appVersion = '1.4') {
|
||||
{
|
||||
dispatch: () => {},
|
||||
getState: () => {},
|
||||
}
|
||||
},
|
||||
);
|
||||
return service;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ function newPluginService(appVersion = '1.4') {
|
||||
{
|
||||
dispatch: () => {},
|
||||
getState: () => {},
|
||||
}
|
||||
},
|
||||
);
|
||||
return service;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export function newPluginService(appVersion = '1.4', options: PluginServiceOptio
|
||||
{
|
||||
dispatch: () => {},
|
||||
getState: options.getState ? options.getState : () => {},
|
||||
}
|
||||
},
|
||||
);
|
||||
return service;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ delete require.cache[require.resolve('./paths')];
|
||||
const NODE_ENV = process.env.NODE_ENV;
|
||||
if (!NODE_ENV) {
|
||||
throw new Error(
|
||||
'The NODE_ENV environment variable is required but was not specified.'
|
||||
'The NODE_ENV environment variable is required but was not specified.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ dotenvFiles.forEach(dotenvFile => {
|
||||
require('dotenv-expand')(
|
||||
require('dotenv').config({
|
||||
path: dotenvFile,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -78,7 +78,7 @@ function getClientEnvironment(publicUrl) {
|
||||
// This should only be used as an escape hatch. Normally you would put
|
||||
// images into the `src` and `import` them in code to get their paths.
|
||||
PUBLIC_URL: publicUrl,
|
||||
}
|
||||
},
|
||||
);
|
||||
// Stringify all values so we can feed into Webpack DefinePlugin
|
||||
const stringified = {
|
||||
|
||||
@@ -55,8 +55,8 @@ function getAdditionalModulePaths(options = {}) {
|
||||
throw new Error(
|
||||
chalk.red.bold(
|
||||
'Your project\'s `baseUrl` can only be set to `src` or `node_modules`.' +
|
||||
' Create React App does not support other values at this time.'
|
||||
)
|
||||
' Create React App does not support other values at this time.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ function getModules() {
|
||||
|
||||
if (hasTsConfig && hasJsConfig) {
|
||||
throw new Error(
|
||||
'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.'
|
||||
'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ const moduleFileExtensions = [
|
||||
// Resolve file paths in the same order as webpack
|
||||
const resolveModule = (resolveFn, filePath) => {
|
||||
const extension = moduleFileExtensions.find(extension =>
|
||||
fs.existsSync(resolveFn(`${filePath}.${extension}`))
|
||||
fs.existsSync(resolveFn(`${filePath}.${extension}`)),
|
||||
);
|
||||
|
||||
if (extension) {
|
||||
|
||||
@@ -7,14 +7,14 @@ exports.resolveModuleName = (
|
||||
moduleName,
|
||||
containingFile,
|
||||
compilerOptions,
|
||||
resolutionHost
|
||||
resolutionHost,
|
||||
) => {
|
||||
return resolveModuleName(
|
||||
moduleName,
|
||||
containingFile,
|
||||
compilerOptions,
|
||||
resolutionHost,
|
||||
typescript.resolveModuleName
|
||||
typescript.resolveModuleName,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,13 +23,13 @@ exports.resolveTypeReferenceDirective = (
|
||||
moduleName,
|
||||
containingFile,
|
||||
compilerOptions,
|
||||
resolutionHost
|
||||
resolutionHost,
|
||||
) => {
|
||||
return resolveModuleName(
|
||||
moduleName,
|
||||
containingFile,
|
||||
compilerOptions,
|
||||
resolutionHost,
|
||||
typescript.resolveTypeReferenceDirective
|
||||
typescript.resolveTypeReferenceDirective,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -46,15 +46,15 @@ if (process.env.HOST) {
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Attempting to bind to HOST environment variable: ${chalk.yellow(
|
||||
chalk.bold(process.env.HOST)
|
||||
)}`
|
||||
)
|
||||
chalk.bold(process.env.HOST),
|
||||
)}`,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
'If this was unintentional, check that you haven\'t mistakenly set it in your shell.'
|
||||
'If this was unintentional, check that you haven\'t mistakenly set it in your shell.',
|
||||
);
|
||||
console.log(
|
||||
`Learn more here: ${chalk.yellow('https://bit.ly/CRA-advanced-config')}`
|
||||
`Learn more here: ${chalk.yellow('https://bit.ly/CRA-advanced-config')}`,
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
@@ -102,7 +102,7 @@ checkBrowsers(paths.appPath, isInteractive)
|
||||
// Serve webpack assets generated by the compiler over a web server.
|
||||
const serverConfig = createDevServerConfig(
|
||||
proxyConfig,
|
||||
urls.lanUrlForConfig
|
||||
urls.lanUrlForConfig,
|
||||
);
|
||||
const devServer = new WebpackDevServer(compiler, serverConfig);
|
||||
// Launch WebpackDevServer.
|
||||
@@ -120,8 +120,8 @@ checkBrowsers(paths.appPath, isInteractive)
|
||||
if (process.env.NODE_PATH) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.'
|
||||
)
|
||||
'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.',
|
||||
),
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ const pluginClasses = [
|
||||
|
||||
const appDefaultState = createAppDefaultState(
|
||||
bridge().windowContentSize(),
|
||||
resourceEditWatcherDefaultState
|
||||
resourceEditWatcherDefaultState,
|
||||
);
|
||||
|
||||
class Application extends BaseApplication {
|
||||
|
||||
@@ -23,8 +23,8 @@ function onCheckEnded() {
|
||||
isCheckingForUpdate_ = false;
|
||||
}
|
||||
|
||||
async function fetchLatestRelease() {
|
||||
const response = await shim.fetch('https://api.github.com/repos/laurent22/joplin/releases');
|
||||
async function fetchLatestReleases() {
|
||||
const response = await shim.fetch('https://objects.joplinusercontent.com/r/releases');
|
||||
|
||||
if (!response.ok) {
|
||||
const responseText = await response.text();
|
||||
@@ -76,7 +76,7 @@ export default async function checkForUpdates(inBackground: boolean, parentWindo
|
||||
logger.info(`Checking with options ${JSON.stringify(options)}`);
|
||||
|
||||
try {
|
||||
const releases = await fetchLatestRelease();
|
||||
const releases = await fetchLatestReleases();
|
||||
const release = extractVersionInfo(releases, process.platform, process.arch, shim.isPortable(), options);
|
||||
|
||||
logger.info(`Current version: ${packageInfo.version}`);
|
||||
|
||||
@@ -71,36 +71,36 @@ class ClipperConfigScreenComponent extends React.Component {
|
||||
webClipperStatusComps.push(
|
||||
<p key="text_1" style={theme.textStyle}>
|
||||
<b>{_('The web clipper service is enabled and set to auto-start.')}</b>
|
||||
</p>
|
||||
</p>,
|
||||
);
|
||||
if (this.props.clipperServer.startState === 'started') {
|
||||
webClipperStatusComps.push(
|
||||
<p key="text_2" style={theme.textStyle}>
|
||||
{_('Status: Started on port %d', this.props.clipperServer.port)}
|
||||
</p>
|
||||
</p>,
|
||||
);
|
||||
} else {
|
||||
webClipperStatusComps.push(
|
||||
<p key="text_3" style={theme.textStyle}>
|
||||
{_('Status: %s', this.props.clipperServer.startState)}
|
||||
</p>
|
||||
</p>,
|
||||
);
|
||||
}
|
||||
webClipperStatusComps.push(
|
||||
<button key="disable_button" style={buttonStyle} onClick={this.disableClipperServer_click}>
|
||||
{_('Disable Web Clipper Service')}
|
||||
</button>
|
||||
</button>,
|
||||
);
|
||||
} else {
|
||||
webClipperStatusComps.push(
|
||||
<p key="text_4" style={theme.textStyle}>
|
||||
{_('The web clipper service is not enabled.')}
|
||||
</p>
|
||||
</p>,
|
||||
);
|
||||
webClipperStatusComps.push(
|
||||
<button key="enable_button" style={buttonStyle} onClick={this.enableClipperServer_click}>
|
||||
{_('Enable Web Clipper Service')}
|
||||
</button>
|
||||
</button>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
settingComps.push(
|
||||
<p key='missing-password-warning' style={warningStyle}>
|
||||
{_('Warning: Missing password.')}{' '}{showMacInfoLink ? macInfoLink : null}
|
||||
</p>
|
||||
</p>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
onClick={this.checkSyncConfig_}
|
||||
/>
|
||||
{statusComp}
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -393,7 +393,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
items.push(
|
||||
<option value={e.key.toString()} key={e.key}>
|
||||
{settingOptions[e.key]}
|
||||
</option>
|
||||
</option>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ const callHook = (isUpdate: boolean, pluginEnabled = true, pluginInstalledViaGUI
|
||||
},
|
||||
repoApi,
|
||||
onPluginSettingsChange,
|
||||
isUpdate
|
||||
isUpdate,
|
||||
);
|
||||
|
||||
describe('useOnInstallHandler', () => {
|
||||
@@ -37,7 +37,7 @@ describe('useOnInstallHandler', () => {
|
||||
beforeAll(() => {
|
||||
(PluginService.instance as jest.Mock).mockReturnValue(pluginServiceInstance);
|
||||
(defaultPluginSetting as jest.Mock).mockImplementation(
|
||||
jest.requireActual('@joplin/lib/services/plugins/PluginService').defaultPluginSetting
|
||||
jest.requireActual('@joplin/lib/services/plugins/PluginService').defaultPluginSetting,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function DialogButtonRow(props: Props) {
|
||||
buttonComps.push(
|
||||
<button key={b.name} style={buttonStyle} onClick={() => onCustomButtonClick({ buttonName: b.name })} onKeyDown={onKeyDown}>
|
||||
{b.label}
|
||||
</button>
|
||||
</button>,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export default function DialogButtonRow(props: Props) {
|
||||
buttonComps.push(
|
||||
<button disabled={props.okButtonDisabled} key="ok" style={buttonStyle} onClick={onOkButtonClick} ref={props.okButtonRef} onKeyDown={onKeyDown}>
|
||||
{props.okButtonLabel ? props.okButtonLabel : _('OK')}
|
||||
</button>
|
||||
</button>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function DialogButtonRow(props: Props) {
|
||||
buttonComps.push(
|
||||
<button disabled={props.cancelButtonDisabled} key="cancel" style={{ ...buttonStyle }} onClick={onCancelButtonClick}>
|
||||
{props.cancelButtonLabel ? props.cancelButtonLabel : _('Cancel')}
|
||||
</button>
|
||||
</button>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
<tr key={mk.id}>
|
||||
<td style={theme.textStyle}>{mk.id}</td>
|
||||
<td><button onClick={() => onUpgradeMasterKey(mk)} style={theme.buttonStyle}>Upgrade</button></td>
|
||||
</tr>
|
||||
</tr>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
rows.push(
|
||||
<tr key={id}>
|
||||
<td style={theme.textStyle}>{id}</td>
|
||||
</tr>
|
||||
</tr>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -89,14 +89,14 @@ export default class ErrorBoundary extends React.Component<Props, State> {
|
||||
<section key="message">
|
||||
<h2>Message</h2>
|
||||
<p>{this.state.error.message}</p>
|
||||
</section>
|
||||
</section>,
|
||||
);
|
||||
|
||||
output.push(
|
||||
<section key="versionInfo">
|
||||
<h2>Version info</h2>
|
||||
<pre>{versionInfo(packageInfo, this.state.plugins).message}</pre>
|
||||
</section>
|
||||
</section>,
|
||||
);
|
||||
|
||||
if (this.state.pluginInfos.length) {
|
||||
@@ -104,7 +104,7 @@ export default class ErrorBoundary extends React.Component<Props, State> {
|
||||
<section key="pluginSettings">
|
||||
<h2>Plugins</h2>
|
||||
<pre>{JSON.stringify(this.state.pluginInfos, null, 4)}</pre>
|
||||
</section>
|
||||
</section>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export default class ErrorBoundary extends React.Component<Props, State> {
|
||||
<section key="stacktrace">
|
||||
<h2>Stack trace</h2>
|
||||
<pre>{this.state.error.stack}</pre>
|
||||
</section>
|
||||
</section>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ export default class ErrorBoundary extends React.Component<Props, State> {
|
||||
<section key="componentStack">
|
||||
<h2>Component stack</h2>
|
||||
<pre>{this.state.errorInfo.componentStack}</pre>
|
||||
</section>
|
||||
</section>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ const styleSelector = createSelector(
|
||||
};
|
||||
|
||||
return output;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function platformAssets(type: string) {
|
||||
|
||||
@@ -42,7 +42,7 @@ class ImportScreenComponent extends React.Component<Props, State> {
|
||||
},
|
||||
() => {
|
||||
void this.doImport();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ export const KeymapConfigScreen = ({ themeId }: KeymapConfigScreenProps) => {
|
||||
<div>
|
||||
{accelerator.split('+').map(part => <kbd style={styles.kbd} key={part}>{part}</kbd>).reduce(
|
||||
(accumulator, part) => (accumulator.length ? [...accumulator, ' + ', part] : [part]),
|
||||
[]
|
||||
[],
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ const useCommandStatus = (): [CommandStatus, (commandName: string)=> void, (comm
|
||||
keymapService.getCommandNames().reduce((accumulator: CommandStatus, command: string) => {
|
||||
accumulator[command] = false;
|
||||
return accumulator;
|
||||
}, {})
|
||||
}, {}),
|
||||
);
|
||||
|
||||
const disableStatus = (commandName: string) => setStatus(prevStatus => ({ ...prevStatus, [commandName]: false }));
|
||||
|
||||
@@ -608,37 +608,37 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
msg = this.renderNotificationMessage(
|
||||
_('Safe mode is currently active. Note rendering and all plugins are temporarily disabled.'),
|
||||
_('Disable safe mode and restart'),
|
||||
onDisableSafeModeAndRestart
|
||||
onDisableSafeModeAndRestart,
|
||||
);
|
||||
} else if (this.props.hasMissingSyncCredentials) {
|
||||
msg = this.renderNotificationMessage(
|
||||
_('The synchronisation password is missing.'),
|
||||
_('Set the password'),
|
||||
onViewSyncSettingsScreen
|
||||
onViewSyncSettingsScreen,
|
||||
);
|
||||
} else if (this.props.shouldUpgradeSyncTarget) {
|
||||
msg = this.renderNotificationMessage(
|
||||
_('The sync target needs to be upgraded before Joplin can sync. The operation may take a few minutes to complete and the app needs to be restarted. To proceed please click on the link.'),
|
||||
_('Restart and upgrade'),
|
||||
onRestartAndUpgrade
|
||||
onRestartAndUpgrade,
|
||||
);
|
||||
} else if (this.props.hasDisabledEncryptionItems) {
|
||||
msg = this.renderNotificationMessage(
|
||||
_('Some items cannot be decrypted.'),
|
||||
_('View them now'),
|
||||
onViewStatusScreen
|
||||
onViewStatusScreen,
|
||||
);
|
||||
} else if (this.props.showNeedUpgradingMasterKeyMessage) {
|
||||
msg = this.renderNotificationMessage(
|
||||
_('One of your master keys use an obsolete encryption method.'),
|
||||
_('View them now'),
|
||||
onViewEncryptionConfigScreen
|
||||
onViewEncryptionConfigScreen,
|
||||
);
|
||||
} else if (this.props.showShouldReencryptMessage) {
|
||||
msg = this.renderNotificationMessage(
|
||||
_('The default encryption method has been changed, you should re-encrypt your data.'),
|
||||
_('More info'),
|
||||
onViewEncryptionConfigScreen
|
||||
onViewEncryptionConfigScreen,
|
||||
);
|
||||
} else if (this.showShareInvitationNotification(this.props)) {
|
||||
const invitation = this.props.shareInvitations.find(inv => inv.status === 0);
|
||||
@@ -649,25 +649,25 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
_('Accept'),
|
||||
() => onInvitationRespond(invitation.id, invitation.share.folder_id, invitation.master_key, true),
|
||||
_('Reject'),
|
||||
() => onInvitationRespond(invitation.id, invitation.share.folder_id, invitation.master_key, false)
|
||||
() => onInvitationRespond(invitation.id, invitation.share.folder_id, invitation.master_key, false),
|
||||
);
|
||||
} else if (this.props.hasDisabledSyncItems) {
|
||||
msg = this.renderNotificationMessage(
|
||||
_('Some items cannot be synchronised.'),
|
||||
_('View them now'),
|
||||
onViewStatusScreen
|
||||
onViewStatusScreen,
|
||||
);
|
||||
} else if (this.props.showMissingMasterKeyMessage) {
|
||||
msg = this.renderNotificationMessage(
|
||||
_('One or more master keys need a password.'),
|
||||
_('Set the password'),
|
||||
onViewEncryptionConfigScreen
|
||||
onViewEncryptionConfigScreen,
|
||||
);
|
||||
} else if (this.props.showInstallTemplatesPlugin) {
|
||||
msg = this.renderNotificationMessage(
|
||||
'The template feature has been moved to a plugin called "Templates".',
|
||||
'Install plugin',
|
||||
onViewPluginScreen
|
||||
onViewPluginScreen,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -301,7 +301,7 @@ function useMenu(props: Props) {
|
||||
return menuUtils.commandsToMenuItems(
|
||||
commandNames.concat(pluginCommandNames),
|
||||
(commandName: string) => onMenuItemClickRef.current(commandName),
|
||||
props.locale
|
||||
props.locale,
|
||||
);
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [commandNames, pluginCommandNames, props.locale]);
|
||||
@@ -347,7 +347,7 @@ function useMenu(props: Props) {
|
||||
if (type === 'notes') {
|
||||
sortItems.push(
|
||||
{ ...menuItemDic.toggleNotesSortOrderReverse, type: 'checkbox' },
|
||||
{ ...menuItemDic.toggleNotesSortOrderField, visible: false }
|
||||
{ ...menuItemDic.toggleNotesSortOrderField, visible: false },
|
||||
);
|
||||
} else {
|
||||
sortItems.push({
|
||||
@@ -391,7 +391,7 @@ function useMenu(props: Props) {
|
||||
{
|
||||
plugins: pluginsRef.current,
|
||||
customCss: props.customCss,
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -414,7 +414,7 @@ function useMenu(props: Props) {
|
||||
});
|
||||
|
||||
exportItems.push(
|
||||
menuItemDic.exportPdf
|
||||
menuItemDic.exportPdf,
|
||||
);
|
||||
|
||||
// We need a dummy entry, otherwise the ternary operator to show a
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import * as React from 'react';
|
||||
import NoteListUtils from './utils/NoteListUtils';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
const { buildStyle } = require('@joplin/lib/theme');
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
@@ -9,8 +10,7 @@ interface MultiNoteActionsProps {
|
||||
themeId: number;
|
||||
selectedNoteIds: string[];
|
||||
notes: any[];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
dispatch: Function;
|
||||
dispatch: Dispatch;
|
||||
watchedNoteFiles: string[];
|
||||
plugins: PluginStates;
|
||||
inConflictFolder: boolean;
|
||||
@@ -68,7 +68,7 @@ export default function MultiNoteActions(props: MultiNoteActionsProps) {
|
||||
itemComps.push(
|
||||
<button key={item.label} style={styles.button} onClick={() => multiNotesButton_click(item)}>
|
||||
{item.label}
|
||||
</button>
|
||||
</button>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHand
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { EditorCommand, NoteBodyEditorProps } from '../../utils/types';
|
||||
import { commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceHandling';
|
||||
import { commandAttachFileToBody, getResourcesFromPasteEvent } from '../../utils/resourceHandling';
|
||||
import { ScrollOptions, ScrollOptionTypes } from '../../utils/types';
|
||||
import { CommandValue } from '../../utils/types';
|
||||
import { usePrevious, cursorPositionToTextOffset } from './utils';
|
||||
@@ -268,7 +268,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
}, [props.content, props.visiblePanes, addListItem, wrapSelectionWithStrings, setEditorPercentScroll, setViewerPercentScroll, resetScroll]);
|
||||
|
||||
const onEditorPaste = useCallback(async (event: any = null) => {
|
||||
const resourceMds = await handlePasteEvent(event);
|
||||
const resourceMds = await getResourcesFromPasteEvent(event);
|
||||
if (!resourceMds.length) return;
|
||||
if (editorRef.current) {
|
||||
editorRef.current.replaceSelection(resourceMds.join('\n'));
|
||||
@@ -838,7 +838,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
click: async () => {
|
||||
editorCutText();
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
menu.append(
|
||||
@@ -848,7 +848,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
click: async () => {
|
||||
editorCopyText();
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
menu.append(
|
||||
@@ -858,7 +858,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
click: async () => {
|
||||
editorPaste();
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps, ResourceInfos } from '../../utils/types';
|
||||
import { resourcesStatus, commandAttachFileToBody, handlePasteEvent, processPastedHtml, attachedResources } from '../../utils/resourceHandling';
|
||||
import { resourcesStatus, commandAttachFileToBody, getResourcesFromPasteEvent, processPastedHtml, attachedResources } from '../../utils/resourceHandling';
|
||||
import useScroll from './utils/useScroll';
|
||||
import styles_ from './styles';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
@@ -750,13 +750,13 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
].concat(
|
||||
pluginAssets
|
||||
.filter((a: any) => a.mime === 'text/css')
|
||||
.map((a: any) => a.path)
|
||||
.map((a: any) => a.path),
|
||||
);
|
||||
|
||||
const allJsFiles = [].concat(
|
||||
pluginAssets
|
||||
.filter((a: any) => a.mime === 'application/javascript')
|
||||
.map((a: any) => a.path)
|
||||
.map((a: any) => a.path),
|
||||
);
|
||||
|
||||
|
||||
@@ -1064,38 +1064,43 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
// to be processed in various ways.
|
||||
event.preventDefault();
|
||||
|
||||
const resourceMds = await handlePasteEvent(event);
|
||||
if (resourceMds.length) {
|
||||
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, resourceMds.join('\n'), markupRenderOptions({ bodyOnly: true }));
|
||||
editor.insertContent(result.html);
|
||||
} else {
|
||||
const pastedText = event.clipboardData.getData('text/plain');
|
||||
const pastedText = event.clipboardData.getData('text/plain');
|
||||
|
||||
// event.clipboardData.getData('text/html') wraps the
|
||||
// content with <html><body></body></html>, which seems to
|
||||
// be not supported in editor.insertContent().
|
||||
//
|
||||
// when pasting text with Ctrl+Shift+V, the format should be
|
||||
// ignored. In this case,
|
||||
// event.clopboardData.getData('text/html') returns an empty
|
||||
// string, but the clipboard.readHTML() still returns the
|
||||
// formatted text.
|
||||
const pastedHtml = event.clipboardData.getData('text/html') ? clipboard.readHTML() : '';
|
||||
|
||||
// We should only process the images if there is no plain text or
|
||||
// HTML text in the clipboard. This is because certain applications,
|
||||
// such as Word, are going to add multiple versions of the copied
|
||||
// data to the clipboard - one with the text formatted as HTML, and
|
||||
// one with the text as an image. In that case, we need to ignore
|
||||
// the image and only process the HTML.
|
||||
|
||||
if (!pastedText && !pastedHtml) {
|
||||
const resourceMds = await getResourcesFromPasteEvent(event);
|
||||
if (resourceMds.length) {
|
||||
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, resourceMds.join('\n'), markupRenderOptions({ bodyOnly: true }));
|
||||
editor.insertContent(result.html);
|
||||
}
|
||||
} else {
|
||||
if (BaseItem.isMarkdownTag(pastedText)) { // Paste a link to a note
|
||||
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, pastedText, markupRenderOptions({ bodyOnly: true }));
|
||||
editor.insertContent(result.html);
|
||||
} else { // Paste regular text
|
||||
// event.clipboardData.getData('text/html') wraps the content with <html><body></body></html>,
|
||||
// which seems to be not supported in editor.insertContent().
|
||||
//
|
||||
// when pasting text with Ctrl+Shift+V, the format should be ignored.
|
||||
// In this case, event.clopboardData.getData('text/html') returns an empty string, but the clipboard.readHTML() still returns the formatted text.
|
||||
const pastedHtml = event.clipboardData.getData('text/html') ? clipboard.readHTML() : '';
|
||||
if (pastedHtml) { // Handles HTML
|
||||
const modifiedHtml = await processPastedHtml(pastedHtml);
|
||||
editor.insertContent(modifiedHtml);
|
||||
} else { // Handles plain text
|
||||
pasteAsPlainText(pastedText);
|
||||
}
|
||||
|
||||
// This code before was necessary to get undo working after
|
||||
// pasting but it seems it's no longer necessary, so
|
||||
// removing it for now. We also couldn't do it immediately
|
||||
// it seems, or else nothing is added to the stack, so do it
|
||||
// on the next frame.
|
||||
//
|
||||
// window.requestAnimationFrame(() =>
|
||||
// editor.undoManager.add()); onChangeHandler();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
|
||||
const { formNote, setFormNote, isNewNote, resourceInfos } = useFormNote({
|
||||
syncStarted: props.syncStarted,
|
||||
decryptionStarted: props.decryptionStarted,
|
||||
noteId: effectiveNoteId,
|
||||
isProvisional: props.isProvisional,
|
||||
titleInputRef: titleInputRef,
|
||||
@@ -633,6 +634,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
isProvisional: state.provisionalNoteIds.includes(noteId),
|
||||
editorNoteStatuses: state.editorNoteStatuses,
|
||||
syncStarted: state.syncStarted,
|
||||
decryptionStarted: state.decryptionWorker?.state !== 'idle',
|
||||
themeId: state.settings.theme,
|
||||
richTextBannerDismissed: state.settings.richTextBannerDismissed,
|
||||
watchedNoteFiles: state.watchedNoteFiles,
|
||||
|
||||
@@ -6,7 +6,7 @@ import Resource from '@joplin/lib/models/Resource';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
|
||||
import htmlUtils from '@joplin/lib/htmlUtils';
|
||||
import rendererHtmlUtils from '@joplin/renderer/htmlUtils';
|
||||
import rendererHtmlUtils, { extractHtmlBody } from '@joplin/renderer/htmlUtils';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
const { fileUriToPath } = require('@joplin/lib/urlUtils');
|
||||
const joplinRendererUtils = require('@joplin/renderer').utils;
|
||||
@@ -107,7 +107,7 @@ export function resourcesStatus(resourceInfos: any) {
|
||||
return joplinRendererUtils.resourceStatusName(lowestIndex);
|
||||
}
|
||||
|
||||
export async function handlePasteEvent(event: any) {
|
||||
export async function getResourcesFromPasteEvent(event: any) {
|
||||
const output = [];
|
||||
const formats = clipboard.availableFormats();
|
||||
for (let i = 0; i < formats.length; i++) {
|
||||
@@ -176,9 +176,9 @@ export async function processPastedHtml(html: string) {
|
||||
}
|
||||
}
|
||||
|
||||
return rendererHtmlUtils.sanitizeHtml(
|
||||
return extractHtmlBody(rendererHtmlUtils.sanitizeHtml(
|
||||
htmlUtils.replaceImageUrls(html, (src: string) => {
|
||||
return mappedResources[src];
|
||||
})
|
||||
);
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
|
||||
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/MarkupToHtml';
|
||||
import { MarkupToHtmlOptions } from './useMarkupToHtml';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
export interface AllAssetsOptions {
|
||||
contentMaxWidthTarget?: string;
|
||||
@@ -15,11 +15,9 @@ export interface ToolbarButtonInfos {
|
||||
}
|
||||
|
||||
export interface NoteEditorProps {
|
||||
// style: any;
|
||||
noteId: string;
|
||||
themeId: number;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
dispatch: Function;
|
||||
dispatch: Dispatch;
|
||||
selectedNoteIds: string[];
|
||||
selectedFolderId: string;
|
||||
notes: any[];
|
||||
@@ -27,6 +25,7 @@ export interface NoteEditorProps {
|
||||
isProvisional: boolean;
|
||||
editorNoteStatuses: any;
|
||||
syncStarted: boolean;
|
||||
decryptionStarted: boolean;
|
||||
bodyEditor: string;
|
||||
notesParentType: string;
|
||||
selectedNoteTags: any[];
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import useFormNote, { HookDependencies } from './useFormNote';
|
||||
|
||||
|
||||
describe('useFormNote', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
});
|
||||
|
||||
it('should update note when decryption completes', async () => {
|
||||
const testNote = await Note.save({ title: 'Test Note!' });
|
||||
|
||||
const makeFormNoteProps = (syncStarted: boolean, decryptionStarted: boolean): HookDependencies => {
|
||||
return {
|
||||
syncStarted,
|
||||
decryptionStarted,
|
||||
noteId: testNote.id,
|
||||
isProvisional: false,
|
||||
titleInputRef: null,
|
||||
editorRef: null,
|
||||
onBeforeLoad: ()=>{},
|
||||
onAfterLoad: ()=>{},
|
||||
};
|
||||
};
|
||||
|
||||
const formNote = renderHook(props => useFormNote(props), {
|
||||
initialProps: makeFormNoteProps(true, false),
|
||||
});
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
encryption_applied: 0,
|
||||
title: testNote.title,
|
||||
});
|
||||
});
|
||||
|
||||
await Note.save({
|
||||
id: testNote.id,
|
||||
encryption_cipher_text: 'cipher_text',
|
||||
encryption_applied: 1,
|
||||
});
|
||||
|
||||
// Sync starting should cause a re-render
|
||||
formNote.rerender(makeFormNoteProps(false, false));
|
||||
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
encryption_applied: 1,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
formNote.rerender(makeFormNoteProps(false, true));
|
||||
|
||||
await Note.save({
|
||||
id: testNote.id,
|
||||
encryption_applied: 0,
|
||||
title: 'Test Note!',
|
||||
});
|
||||
|
||||
// Ending decryption should also cause a re-render
|
||||
formNote.rerender(makeFormNoteProps(false, false));
|
||||
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
encryption_applied: 0,
|
||||
title: 'Test Note!',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -18,8 +18,9 @@ export interface OnLoadEvent {
|
||||
formNote: FormNote;
|
||||
}
|
||||
|
||||
interface HookDependencies {
|
||||
export interface HookDependencies {
|
||||
syncStarted: boolean;
|
||||
decryptionStarted: boolean;
|
||||
noteId: string;
|
||||
isProvisional: boolean;
|
||||
titleInputRef: any;
|
||||
@@ -61,15 +62,21 @@ function resourceInfosChanged(a: ResourceInfos, b: ResourceInfos): boolean {
|
||||
}
|
||||
|
||||
export default function useFormNote(dependencies: HookDependencies) {
|
||||
const { syncStarted, noteId, isProvisional, titleInputRef, editorRef, onBeforeLoad, onAfterLoad } = dependencies;
|
||||
const {
|
||||
syncStarted, decryptionStarted, noteId, isProvisional, titleInputRef, editorRef, onBeforeLoad, onAfterLoad,
|
||||
} = dependencies;
|
||||
|
||||
const [formNote, setFormNote] = useState<FormNote>(defaultFormNote());
|
||||
const [formNoteRefeshScheduled, setFormNoteRefreshScheduled] = useState<boolean>(false);
|
||||
const [isNewNote, setIsNewNote] = useState(false);
|
||||
const prevSyncStarted = usePrevious(syncStarted);
|
||||
const prevDecryptionStarted = usePrevious(decryptionStarted);
|
||||
const previousNoteId = usePrevious(formNote.id);
|
||||
const [resourceInfos, setResourceInfos] = useState<ResourceInfos>({});
|
||||
|
||||
// Increasing the value of this counter cancels any ongoing note refreshes and starts
|
||||
// a new refresh.
|
||||
const [formNoteRefeshScheduled, setFormNoteRefreshScheduled] = useState<number>(0);
|
||||
|
||||
async function initNoteState(n: any) {
|
||||
let originalCss = '';
|
||||
|
||||
@@ -107,7 +114,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!formNoteRefeshScheduled) return () => {};
|
||||
if (formNoteRefeshScheduled <= 0) return () => {};
|
||||
|
||||
reg.logger().info('Sync has finished and note has never been changed - reloading it');
|
||||
|
||||
@@ -126,7 +133,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
}
|
||||
|
||||
await initNoteState(n);
|
||||
setFormNoteRefreshScheduled(false);
|
||||
setFormNoteRefreshScheduled(0);
|
||||
};
|
||||
|
||||
void loadNote();
|
||||
@@ -136,21 +143,32 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
};
|
||||
}, [formNoteRefeshScheduled, noteId]);
|
||||
|
||||
const refreshFormNote = useCallback(() => {
|
||||
// Increase the counter to cancel any ongoing refresh attempts
|
||||
// and start a new one.
|
||||
setFormNoteRefreshScheduled(formNoteRefeshScheduled + 1);
|
||||
}, [formNoteRefeshScheduled]);
|
||||
|
||||
useEffect(() => {
|
||||
// Check that synchronisation has just finished - and
|
||||
// if the note has never been changed, we reload it.
|
||||
// If the note has already been changed, it's a conflict
|
||||
// that's already been handled by the synchronizer.
|
||||
const decryptionJustEnded = prevDecryptionStarted && !decryptionStarted;
|
||||
const syncJustEnded = prevSyncStarted && !syncStarted;
|
||||
|
||||
if (!prevSyncStarted) return;
|
||||
if (syncStarted) return;
|
||||
if (!decryptionJustEnded && !syncJustEnded) return;
|
||||
if (formNote.hasChanged) return;
|
||||
|
||||
// Refresh the form note.
|
||||
// This is kept separate from the above logic so that when prevSyncStarted is changed
|
||||
// from true to false, it doesn't cancel the note from loading.
|
||||
setFormNoteRefreshScheduled(true);
|
||||
}, [prevSyncStarted, syncStarted, formNote.hasChanged]);
|
||||
refreshFormNote();
|
||||
}, [
|
||||
prevSyncStarted, syncStarted,
|
||||
prevDecryptionStarted, decryptionStarted,
|
||||
formNote.hasChanged, refreshFormNote,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!noteId) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import ItemList from '../ItemList';
|
||||
const { connect } = require('react-redux');
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import { Props } from './types';
|
||||
import { Props } from './utils/types';
|
||||
import usePrevious from '../hooks/usePrevious';
|
||||
import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
|
||||
304
packages/app-desktop/gui/NoteList/NoteList2.tsx
Normal file
304
packages/app-desktop/gui/NoteList/NoteList2.tsx
Normal file
@@ -0,0 +1,304 @@
|
||||
import * as React from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { useMemo, useRef, useEffect } from 'react';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
|
||||
import { ItemFlow, Props } from './utils/types';
|
||||
import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
import ItemChange from '@joplin/lib/models/ItemChange';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import NoteListItem from '../NoteListItem/NoteListItem';
|
||||
import useRenderedNotes from './utils/useRenderedNotes';
|
||||
import useItemCss from './utils/useItemCss';
|
||||
import useOnContextMenu from '../NoteListItem/utils/useOnContextMenu';
|
||||
import useVisibleRange from './utils/useVisibleRange';
|
||||
import useScroll from './utils/useScroll';
|
||||
import useFocusNote from './utils/useFocusNote';
|
||||
import useOnNoteClick from './utils/useOnNoteClick';
|
||||
import useMoveNote from './utils/useMoveNote';
|
||||
import useOnKeyDown from './utils/useOnKeyDown';
|
||||
import * as focusElementNoteList from './commands/focusElementNoteList';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import useDragAndDrop from './utils/useDragAndDrop';
|
||||
import usePrevious from '../hooks/usePrevious';
|
||||
// import defaultLeftToRightItemRenderer from './utils/defaultLeftToRightListRenderer';
|
||||
import defaultListRenderer from './utils/defaultListRenderer';
|
||||
const { connect } = require('react-redux');
|
||||
|
||||
const commands = {
|
||||
focusElementNoteList,
|
||||
};
|
||||
|
||||
const NoteList = (props: Props) => {
|
||||
const listRef = useRef(null);
|
||||
const itemRefs = useRef<Record<string, HTMLDivElement>>({});
|
||||
// const listRenderer = defaultLeftToRightItemRenderer;
|
||||
const listRenderer = defaultListRenderer;
|
||||
|
||||
const itemSize: Size = useMemo(() => {
|
||||
return {
|
||||
width: listRenderer.itemSize.width ? listRenderer.itemSize.width : props.size.width,
|
||||
height: listRenderer.itemSize.height,
|
||||
};
|
||||
}, [listRenderer.itemSize, props.size.width]);
|
||||
|
||||
const itemsPerLine = useMemo(() => {
|
||||
if (listRenderer.flow === ItemFlow.TopToBottom) {
|
||||
return 1;
|
||||
} else {
|
||||
return Math.max(1, Math.floor(props.size.width / itemSize.width));
|
||||
}
|
||||
}, [listRenderer.flow, props.size.width, itemSize.width]);
|
||||
|
||||
const { scrollTop, onScroll, makeItemIndexVisible } = useScroll(
|
||||
itemsPerLine,
|
||||
props.notes.length,
|
||||
itemSize,
|
||||
props.size,
|
||||
listRef,
|
||||
);
|
||||
|
||||
const [startNoteIndex, endNoteIndex, startLineIndex, endLineIndex, totalLineCount, visibleItemCount] = useVisibleRange(
|
||||
itemsPerLine,
|
||||
scrollTop,
|
||||
props.size,
|
||||
itemSize,
|
||||
props.notes.length,
|
||||
);
|
||||
|
||||
const focusNote = useFocusNote(itemRefs);
|
||||
|
||||
const moveNote = useMoveNote(
|
||||
props.notesParentType,
|
||||
props.noteSortOrder,
|
||||
props.selectedNoteIds,
|
||||
props.selectedFolderId,
|
||||
props.uncompletedTodosOnTop,
|
||||
props.showCompletedTodos,
|
||||
props.notes,
|
||||
);
|
||||
|
||||
const renderedNotes = useRenderedNotes(
|
||||
startNoteIndex,
|
||||
endNoteIndex,
|
||||
props.notes,
|
||||
props.selectedNoteIds,
|
||||
listRenderer,
|
||||
props.highlightedWords,
|
||||
props.watchedNoteFiles,
|
||||
);
|
||||
|
||||
const noteItemStyle = useMemo(() => {
|
||||
return {
|
||||
width: 'auto',
|
||||
height: itemSize.height,
|
||||
};
|
||||
}, [itemSize.height]);
|
||||
|
||||
const noteListStyle = useMemo(() => {
|
||||
return {
|
||||
width: props.size.width,
|
||||
height: props.size.height,
|
||||
};
|
||||
}, [props.size]);
|
||||
|
||||
const onNoteClick = useOnNoteClick(props.dispatch, focusNote);
|
||||
|
||||
const onKeyDown = useOnKeyDown(
|
||||
props.selectedNoteIds,
|
||||
moveNote,
|
||||
makeItemIndexVisible,
|
||||
focusNote,
|
||||
props.notes,
|
||||
props.dispatch,
|
||||
visibleItemCount,
|
||||
props.notes.length,
|
||||
listRenderer.flow,
|
||||
itemsPerLine,
|
||||
);
|
||||
|
||||
useItemCss(listRenderer.itemCss);
|
||||
|
||||
useEffect(() => {
|
||||
CommandService.instance().registerRuntime(commands.focusElementNoteList.declaration.name, commands.focusElementNoteList.runtime(focusNote));
|
||||
return () => {
|
||||
CommandService.instance().unregisterRuntime(commands.focusElementNoteList.declaration.name);
|
||||
};
|
||||
}, [focusNote]);
|
||||
|
||||
const onItemContextMenu = useOnContextMenu(
|
||||
props.selectedNoteIds,
|
||||
props.selectedFolderId,
|
||||
props.notes,
|
||||
props.dispatch,
|
||||
props.watchedNoteFiles,
|
||||
props.plugins,
|
||||
props.customCss,
|
||||
);
|
||||
|
||||
const { onDragStart, onDragOver, onDrop, dragOverTargetNoteIndex } = useDragAndDrop(props.parentFolderIsReadOnly,
|
||||
props.selectedNoteIds,
|
||||
props.selectedFolderId,
|
||||
listRef,
|
||||
scrollTop,
|
||||
itemSize,
|
||||
props.notesParentType,
|
||||
props.noteSortOrder,
|
||||
props.uncompletedTodosOnTop,
|
||||
props.showCompletedTodos,
|
||||
listRenderer.flow,
|
||||
itemsPerLine,
|
||||
);
|
||||
|
||||
const previousSelectedNoteIds = usePrevious(props.selectedNoteIds, []);
|
||||
const previousNoteCount = usePrevious(props.notes.length, 0);
|
||||
const previousVisible = usePrevious(props.visible, false);
|
||||
|
||||
useEffect(() => {
|
||||
if (previousSelectedNoteIds !== props.selectedNoteIds && props.selectedNoteIds.length === 1) {
|
||||
const id = props.selectedNoteIds[0];
|
||||
const doRefocus = props.notes.length < previousNoteCount && !props.focusedField;
|
||||
|
||||
for (let i = 0; i < props.notes.length; i++) {
|
||||
if (props.notes[i].id === id) {
|
||||
makeItemIndexVisible(i);
|
||||
if (doRefocus) {
|
||||
const ref = itemRefs.current[id];
|
||||
if (ref) ref.focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [makeItemIndexVisible, previousSelectedNoteIds, previousNoteCount, previousVisible, props.selectedNoteIds, props.notes, props.focusedField, props.visible]);
|
||||
|
||||
const highlightedWords = useMemo(() => {
|
||||
if (props.notesParentType === 'Search') {
|
||||
const query = BaseModel.byId(props.searches, props.selectedSearchId);
|
||||
if (query) return props.highlightedWords;
|
||||
}
|
||||
return [];
|
||||
}, [props.notesParentType, props.searches, props.selectedSearchId, props.highlightedWords]);
|
||||
|
||||
const renderEmptyList = () => {
|
||||
if (props.notes.length) return null;
|
||||
return <div className="emptylist">{props.folders.length ? _('No notes in here. Create one by clicking on "New note".') : _('There is currently no notebook. Create one by clicking on "New notebook".')}</div>;
|
||||
};
|
||||
|
||||
const renderFiller = (key: string, style: React.CSSProperties) => {
|
||||
if (!props.notes.length) return null;
|
||||
if (style.height as number <= 0) return null;
|
||||
return <div key={key} style={style}></div>;
|
||||
};
|
||||
|
||||
const renderNotes = () => {
|
||||
if (!props.notes.length) return null;
|
||||
|
||||
const output: JSX.Element[] = [];
|
||||
|
||||
for (let i = startNoteIndex; i <= endNoteIndex; i++) {
|
||||
const note = props.notes[i];
|
||||
const renderedNote = renderedNotes[note.id];
|
||||
|
||||
output.push(
|
||||
<NoteListItem
|
||||
key={note.id}
|
||||
ref={el => itemRefs.current[note.id] = el}
|
||||
index={i}
|
||||
dragIndex={dragOverTargetNoteIndex}
|
||||
noteCount={props.notes.length}
|
||||
itemSize={itemSize}
|
||||
noteHtml={renderedNote ? renderedNote.html : ''}
|
||||
noteId={note.id}
|
||||
onChange={listRenderer.onChange}
|
||||
onClick={onNoteClick}
|
||||
onContextMenu={onItemContextMenu}
|
||||
onDragStart={onDragStart}
|
||||
onDragOver={onDragOver}
|
||||
style={noteItemStyle}
|
||||
highlightedWords={highlightedWords}
|
||||
isProvisional={props.provisionalNoteIds.includes(note.id)}
|
||||
flow={listRenderer.flow}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
const topFillerHeight = startLineIndex * itemSize.height;
|
||||
const bottomFillerHeight = (totalLineCount - endLineIndex - 1) * itemSize.height;
|
||||
|
||||
const fillerBaseStyle = useMemo(() => {
|
||||
// return { width: 'auto', border: '1px solid red', backgroundColor: 'green' };
|
||||
return { width: 'auto' };
|
||||
}, []);
|
||||
|
||||
const topFillerStyle = useMemo(() => {
|
||||
return { ...fillerBaseStyle, height: topFillerHeight };
|
||||
}, [fillerBaseStyle, topFillerHeight]);
|
||||
|
||||
const bottomFillerStyle = useMemo(() => {
|
||||
return { ...fillerBaseStyle, height: bottomFillerHeight };
|
||||
}, [fillerBaseStyle, bottomFillerHeight]);
|
||||
|
||||
const notesStyle = useMemo(() => {
|
||||
const output: React.CSSProperties = {};
|
||||
|
||||
if (listRenderer.flow === ItemFlow.LeftToRight) {
|
||||
output.flexFlow = 'row wrap';
|
||||
} else {
|
||||
output.flexDirection = 'column';
|
||||
}
|
||||
|
||||
return output;
|
||||
}, [listRenderer.flow]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="note-list"
|
||||
style={noteListStyle}
|
||||
ref={listRef}
|
||||
onScroll={onScroll}
|
||||
onKeyDown={onKeyDown}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
{renderEmptyList()}
|
||||
{renderFiller('top', topFillerStyle)}
|
||||
<div className="notes" style={notesStyle}>
|
||||
{renderNotes()}
|
||||
</div>
|
||||
{renderFiller('bottom', bottomFillerStyle)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const selectedFolder: FolderEntity = state.notesParentType === 'Folder' ? BaseModel.byId(state.folders, state.selectedFolderId) : null;
|
||||
const userId = state.settings['sync.userId'];
|
||||
|
||||
return {
|
||||
notes: state.notes,
|
||||
folders: state.folders,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
themeId: state.settings.theme,
|
||||
notesParentType: state.notesParentType,
|
||||
searches: state.searches,
|
||||
selectedSearchId: state.selectedSearchId,
|
||||
watchedNoteFiles: state.watchedNoteFiles,
|
||||
provisionalNoteIds: state.provisionalNoteIds,
|
||||
isInsertingNotes: state.isInsertingNotes,
|
||||
noteSortOrder: state.settings['notes.sortOrder.field'],
|
||||
uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
|
||||
showCompletedTodos: state.settings.showCompletedTodos,
|
||||
highlightedWords: state.highlightedWords,
|
||||
plugins: state.pluginService.plugins,
|
||||
customCss: state.customCss,
|
||||
focusedField: state.focusedField,
|
||||
parentFolderIsReadOnly: state.notesParentType === 'Folder' && selectedFolder ? itemIsReadOnlySync(ModelType.Folder, ItemChange.SOURCE_UNSPECIFIED, selectedFolder as ItemSlice, userId, state.shareService) : false,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(NoteList);
|
||||
113
packages/app-desktop/gui/NoteList/NoteListSource.tsx
Normal file
113
packages/app-desktop/gui/NoteList/NoteListSource.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo, useState, useRef, useCallback } from 'react';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
|
||||
import NoteListItem from '../NoteListItem';
|
||||
import styled from 'styled-components';
|
||||
import ItemList from '../ItemList';
|
||||
const { connect } = require('react-redux');
|
||||
import { Props } from './utils/types';
|
||||
import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
import ItemChange from '@joplin/lib/models/ItemChange';
|
||||
|
||||
const StyledRoot = styled.div``;
|
||||
|
||||
const NoteListComponent = (props: Props) => {
|
||||
const [width] = useState(0);
|
||||
|
||||
const itemHeight = 34;
|
||||
|
||||
const noteListRef = useRef(null);
|
||||
const itemListRef = useRef(null);
|
||||
|
||||
const style = useMemo(() => {
|
||||
return {};
|
||||
}, []);
|
||||
|
||||
const renderItem = useCallback((item: any, index: number) => {
|
||||
return <NoteListItem
|
||||
key={item.id}
|
||||
style={style}
|
||||
item={item}
|
||||
index={index}
|
||||
themeId={props.themeId}
|
||||
width={width}
|
||||
height={itemHeight}
|
||||
dragItemIndex={0}
|
||||
highlightedWords={[]}
|
||||
isProvisional={props.provisionalNoteIds.includes(item.id)}
|
||||
isSelected={props.selectedNoteIds.indexOf(item.id) >= 0}
|
||||
isWatched={props.watchedNoteFiles.indexOf(item.id) < 0}
|
||||
itemCount={props.notes.length}
|
||||
onCheckboxClick={() => {}}
|
||||
onDragStart={()=>{}}
|
||||
onNoteDragOver={()=>{}}
|
||||
onTitleClick={() => {}}
|
||||
onContextMenu={() => {}}
|
||||
draggable={!props.parentFolderIsReadOnly}
|
||||
/>;
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [style, props.themeId, width, itemHeight, props.provisionalNoteIds, props.selectedNoteIds, props.watchedNoteFiles,
|
||||
props.notes,
|
||||
props.notesParentType,
|
||||
props.searches,
|
||||
props.selectedSearchId,
|
||||
props.highlightedWords,
|
||||
props.parentFolderIsReadOnly,
|
||||
]);
|
||||
const renderItemList = () => {
|
||||
if (!props.notes.length) return null;
|
||||
|
||||
return (
|
||||
<ItemList
|
||||
ref={itemListRef}
|
||||
disabled={props.isInsertingNotes}
|
||||
itemHeight={32}
|
||||
className={'note-list'}
|
||||
items={props.notes}
|
||||
style={props.size}
|
||||
itemRenderer={renderItem}
|
||||
onKeyDown={() => {}}
|
||||
onNoteDrop={()=>{}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
if (!props.size) throw new Error('props.size is required');
|
||||
|
||||
return (
|
||||
<StyledRoot ref={noteListRef}>
|
||||
{renderItemList()}
|
||||
</StyledRoot>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const selectedFolder: FolderEntity = state.notesParentType === 'Folder' ? BaseModel.byId(state.folders, state.selectedFolderId) : null;
|
||||
const userId = state.settings['sync.userId'];
|
||||
|
||||
return {
|
||||
notes: state.notes,
|
||||
folders: state.folders,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
themeId: state.settings.theme,
|
||||
notesParentType: state.notesParentType,
|
||||
searches: state.searches,
|
||||
selectedSearchId: state.selectedSearchId,
|
||||
watchedNoteFiles: state.watchedNoteFiles,
|
||||
provisionalNoteIds: state.provisionalNoteIds,
|
||||
isInsertingNotes: state.isInsertingNotes,
|
||||
noteSortOrder: state.settings['notes.sortOrder.field'],
|
||||
uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
|
||||
showCompletedTodos: state.settings.showCompletedTodos,
|
||||
highlightedWords: state.highlightedWords,
|
||||
plugins: state.pluginService.plugins,
|
||||
customCss: state.customCss,
|
||||
focusedField: state.focusedField,
|
||||
parentFolderIsReadOnly: state.notesParentType === 'Folder' && selectedFolder ? itemIsReadOnlySync(ModelType.Folder, ItemChange.SOURCE_UNSPECIFIED, selectedFolder as ItemSlice, userId, state.shareService) : false,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(NoteListComponent);
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import { itemAnchorRef } from '../NoteList';
|
||||
import { FocusNote } from '../utils/useFocusNote';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'focusElementNoteList',
|
||||
@@ -9,15 +9,11 @@ export const declaration: CommandDeclaration = {
|
||||
parentLabel: () => _('Focus'),
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
export const runtime = (focusNote: FocusNote): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext, noteId: string = null) => {
|
||||
noteId = noteId || stateUtils.selectedNoteId(context.state);
|
||||
|
||||
if (noteId) {
|
||||
const ref = itemAnchorRef(noteId);
|
||||
if (ref) ref.focus();
|
||||
}
|
||||
focusNote(noteId);
|
||||
},
|
||||
enabledCondition: 'noteListHasNotes',
|
||||
};
|
||||
|
||||
43
packages/app-desktop/gui/NoteList/style.scss
Normal file
43
packages/app-desktop/gui/NoteList/style.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
.note-list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--joplin-background-color3);
|
||||
border-right: 1px solid var(--joplin-divider-color);
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
> .notes {
|
||||
display: flex;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
> .emptylist {
|
||||
padding: 10px;
|
||||
font-size: var(--joplin-font-size);
|
||||
color: var(--joplin-color);
|
||||
background-color: var(--joplin-background-color);
|
||||
font-family: var(--joplin-font-family);
|
||||
}
|
||||
}
|
||||
|
||||
.note-list-item {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.note-list-item-wrapper {
|
||||
border-color: var(--joplin-color);
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
||||
> .dragcursor {
|
||||
background-color: var(--joplin-color);
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.note-list-item-wrapper.-provisional {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { FolderEntity, NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
|
||||
export interface Props {
|
||||
themeId: any;
|
||||
selectedNoteIds: string[];
|
||||
notes: NoteEntity[];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
dispatch: Function;
|
||||
watchedNoteFiles: any[];
|
||||
plugins: PluginStates;
|
||||
selectedFolderId: string;
|
||||
customCss: string;
|
||||
notesParentType: string;
|
||||
noteSortOrder: string;
|
||||
uncompletedTodosOnTop: boolean;
|
||||
showCompletedTodos: boolean;
|
||||
resizableLayoutEventEmitter: any;
|
||||
isInsertingNotes: boolean;
|
||||
folders: FolderEntity[];
|
||||
size: any;
|
||||
searches: any[];
|
||||
selectedSearchId: string;
|
||||
highlightedWords: string[];
|
||||
provisionalNoteIds: string[];
|
||||
visible: boolean;
|
||||
focusedField: string;
|
||||
parentFolderIsReadOnly: boolean;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import bridge from '../../../services/bridge';
|
||||
|
||||
const canManuallySortNotes = (notesParentType: string, noteSortOrder: string) => {
|
||||
if (notesParentType !== 'Folder') return false;
|
||||
|
||||
if (noteSortOrder !== 'order') {
|
||||
const doIt = bridge().showConfirmMessageBox(_('To manually sort the notes, the sort order must be changed to "%s" in the menu "%s" > "%s"', _('Custom order'), _('View'), _('Sort notes by')), {
|
||||
buttons: [_('Do it now'), _('Cancel')],
|
||||
});
|
||||
if (!doIt) return false;
|
||||
|
||||
Setting.setValue('notes.sortOrder.field', 'order');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export default canManuallySortNotes;
|
||||
@@ -0,0 +1,165 @@
|
||||
import { MarkupLanguage, MarkupToHtml } from '@joplin/renderer';
|
||||
import { ItemFlow, ListRenderer } from './types';
|
||||
|
||||
interface Props {
|
||||
note: {
|
||||
id: string;
|
||||
title: string;
|
||||
is_todo: number;
|
||||
todo_completed: number;
|
||||
body: string;
|
||||
};
|
||||
item: {
|
||||
size: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
selected: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const defaultLeftToRightItemRenderer: ListRenderer = {
|
||||
flow: ItemFlow.LeftToRight,
|
||||
|
||||
itemSize: {
|
||||
width: 150,
|
||||
height: 150,
|
||||
},
|
||||
|
||||
dependencies: [
|
||||
'item.selected',
|
||||
'item.size.width',
|
||||
'item.size.height',
|
||||
'note.body',
|
||||
'note.id',
|
||||
'note.is_shared',
|
||||
'note.is_todo',
|
||||
'note.isWatched',
|
||||
'note.titleHtml',
|
||||
'note.todo_completed',
|
||||
],
|
||||
|
||||
itemCss: // css
|
||||
`
|
||||
&:before {
|
||||
content: '';
|
||||
border-bottom: 1px solid var(--joplin-divider-color);
|
||||
width: 90%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 5%;
|
||||
}
|
||||
|
||||
> .content.-selected {
|
||||
background-color: var(--joplin-selected-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--joplin-background-color-hover3);
|
||||
}
|
||||
|
||||
> .content {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
align-items: flex-start;
|
||||
overflow-y: hidden;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
|
||||
> .checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> input {
|
||||
margin: 0px 10px 1px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
> .title {
|
||||
font-family: var(--joplin-font-family);
|
||||
font-size: var(--joplin-font-size);
|
||||
color: var(--joplin-color);
|
||||
cursor: default;
|
||||
flex: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
|
||||
> .checkbox {
|
||||
margin: 0 6px 0 0;
|
||||
}
|
||||
|
||||
> .watchedicon {
|
||||
display: none;
|
||||
padding-right: 4px;
|
||||
color: var(--joplin-color);
|
||||
}
|
||||
|
||||
> .titlecontent {
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
> .preview {
|
||||
overflow-y: hidden;
|
||||
font-family: var(--joplin-font-family);
|
||||
font-size: var(--joplin-font-size);
|
||||
color: var(--joplin-color);
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
> .content.-shared {
|
||||
> .title {
|
||||
color: var(--joplin-color-warn3);
|
||||
}
|
||||
}
|
||||
|
||||
> .content.-completed {
|
||||
> .title {
|
||||
opacity: 0.5;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
> .content.-watched {
|
||||
> .title {
|
||||
> .watchedicon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
itemTemplate: // html
|
||||
`
|
||||
<div class="content {{#item.selected}}-selected{{/item.selected}} {{#note.is_shared}}-shared{{/note.is_shared}} {{#note.todo_completed}}-completed{{/note.todo_completed}} {{#note.isWatched}}-watched{{/note.isWatched}}">
|
||||
<div style="width: {{titleWidth}}px;" class="title" data-id="{{note.id}}">
|
||||
{{#note.is_todo}}
|
||||
<input class="checkbox" data-id="todo-checkbox" type="checkbox" {{#note.todo_completed}}checked="checked"{{/note.todo_completed}}>
|
||||
{{/note.is_todo}}
|
||||
<i class="watchedicon fa fa-share-square"></i>
|
||||
<div class="titlecontent">{{{note.titleHtml}}}</div>
|
||||
</div>
|
||||
<div class="preview">{{notePreview}}</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
onRenderNote: async (props: Props) => {
|
||||
const markupToHtml_ = new MarkupToHtml();
|
||||
|
||||
return {
|
||||
...props,
|
||||
notePreview: markupToHtml_.stripMarkup(MarkupLanguage.Markdown, props.note.body).substring(0, 200),
|
||||
titleWidth: props.item.size.width - 32,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default defaultLeftToRightItemRenderer;
|
||||
134
packages/app-desktop/gui/NoteList/utils/defaultListRenderer.ts
Normal file
134
packages/app-desktop/gui/NoteList/utils/defaultListRenderer.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { ItemFlow, ListRenderer } from './types';
|
||||
|
||||
interface Props {
|
||||
note: {
|
||||
id: string;
|
||||
title: string;
|
||||
is_todo: number;
|
||||
todo_completed: number;
|
||||
};
|
||||
item: {
|
||||
size: {
|
||||
height: number;
|
||||
};
|
||||
selected: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const defaultItemRenderer: ListRenderer = {
|
||||
flow: ItemFlow.TopToBottom,
|
||||
|
||||
itemSize: {
|
||||
width: 0,
|
||||
height: 34,
|
||||
},
|
||||
|
||||
dependencies: [
|
||||
'item.selected',
|
||||
'item.size.height',
|
||||
'note.id',
|
||||
'note.is_shared',
|
||||
'note.is_todo',
|
||||
'note.isWatched',
|
||||
'note.titleHtml',
|
||||
'note.todo_completed',
|
||||
],
|
||||
|
||||
itemCss: // css
|
||||
`
|
||||
&:before {
|
||||
content: '';
|
||||
border-bottom: 1px solid var(--joplin-divider-color);
|
||||
width: 90%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 5%;
|
||||
}
|
||||
|
||||
> .content.-selected {
|
||||
background-color: var(--joplin-selected-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--joplin-background-color-hover3);
|
||||
}
|
||||
|
||||
> .content {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-left: 16px;
|
||||
|
||||
> .checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> input {
|
||||
margin: 0px 10px 1px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
> .title {
|
||||
font-family: var(--joplin-font-family);
|
||||
font-size: var(--joplin-font-size);
|
||||
text-decoration: none;
|
||||
color: var(--joplin-color);
|
||||
cursor: default;
|
||||
white-space: nowrap;
|
||||
flex: 1 1 0%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
> .watchedicon {
|
||||
display: none;
|
||||
padding-right: 4px;
|
||||
color: var(--joplin-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .content.-shared {
|
||||
> .title {
|
||||
color: var(--joplin-color-warn3);
|
||||
}
|
||||
}
|
||||
|
||||
> .content.-completed {
|
||||
> .title {
|
||||
opacity: 0.5;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
> .content.-watched {
|
||||
> .title {
|
||||
> .watchedicon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
itemTemplate: // html
|
||||
`
|
||||
<div class="content {{#item.selected}}-selected{{/item.selected}} {{#note.is_shared}}-shared{{/note.is_shared}} {{#note.todo_completed}}-completed{{/note.todo_completed}} {{#note.isWatched}}-watched{{/note.isWatched}}">
|
||||
{{#note.is_todo}}
|
||||
<div class="checkbox">
|
||||
<input data-id="todo-checkbox" type="checkbox" {{#note.todo_completed}}checked="checked"{{/note.todo_completed}}>
|
||||
</div>
|
||||
{{/note.is_todo}}
|
||||
<div class="title" data-id="{{note.id}}">
|
||||
<i class="watchedicon fa fa-share-square"></i>
|
||||
<span>{{{note.titleHtml}}}</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
onRenderNote: async (props: Props) => {
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
export default defaultItemRenderer;
|
||||
45
packages/app-desktop/gui/NoteList/utils/getNoteTitleHtml.ts
Normal file
45
packages/app-desktop/gui/NoteList/utils/getNoteTitleHtml.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { htmlentities } from '@joplin/utils/html';
|
||||
const Mark = require('mark.js/dist/mark.min.js');
|
||||
const markJsUtils = require('@joplin/lib/markJsUtils');
|
||||
const { replaceRegexDiacritics, pregQuote } = require('@joplin/lib/string-utils');
|
||||
|
||||
const getNoteTitleHtml = (highlightedWords: string[], displayTitle: string) => {
|
||||
if (highlightedWords.length) {
|
||||
const titleElement = document.createElement('span');
|
||||
titleElement.textContent = displayTitle;
|
||||
const mark = new Mark(titleElement, {
|
||||
exclude: ['img'],
|
||||
acrossElements: true,
|
||||
});
|
||||
|
||||
mark.unmark();
|
||||
|
||||
try {
|
||||
for (const wordToBeHighlighted of highlightedWords) {
|
||||
markJsUtils.markKeyword(mark, wordToBeHighlighted, {
|
||||
pregQuote: pregQuote,
|
||||
replaceRegexDiacritics: replaceRegexDiacritics,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name !== 'SyntaxError') {
|
||||
throw error;
|
||||
}
|
||||
// An error of 'Regular expression too large' might occour in the markJs library
|
||||
// when the input is really big, this catch is here to avoid the application crashing
|
||||
// https://github.com/laurent22/joplin/issues/7634
|
||||
// console.error('Error while trying to highlight words from search: ', error);
|
||||
}
|
||||
|
||||
// Note: in this case it is safe to use dangerouslySetInnerHTML because titleElement
|
||||
// is a span tag that we created and that contains data that's been inserted as plain text
|
||||
// with `textContent` so it cannot contain any XSS attacks. We use this feature because
|
||||
// mark.js can only deal with DOM elements.
|
||||
// https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
|
||||
return titleElement.outerHTML;
|
||||
} else {
|
||||
return htmlentities(displayTitle);
|
||||
}
|
||||
};
|
||||
|
||||
export default getNoteTitleHtml;
|
||||
51
packages/app-desktop/gui/NoteList/utils/prepareViewProps.ts
Normal file
51
packages/app-desktop/gui/NoteList/utils/prepareViewProps.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ListRendererDepependency } from './types';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
|
||||
const prepareViewProps = async (dependencies: ListRendererDepependency[], note: NoteEntity, itemSize: Size, selected: boolean, noteTitleHtml: string, noteIsWatched: boolean) => {
|
||||
const output: any = {};
|
||||
|
||||
for (const dep of dependencies) {
|
||||
|
||||
if (dep.startsWith('note.')) {
|
||||
const splitted = dep.split('.');
|
||||
if (splitted.length !== 2) throw new Error(`Invalid dependency name: ${dep}`);
|
||||
const propName = splitted.pop();
|
||||
if (!output.note) output.note = {};
|
||||
if (dep === 'note.titleHtml') {
|
||||
output.note.titleHtml = noteTitleHtml;
|
||||
} else if (dep === 'note.isWatched') {
|
||||
output.note.isWatched = noteIsWatched;
|
||||
} else {
|
||||
// The notes in the state only contain the properties defined in
|
||||
// Note.previewFields(). It means that if a view request a
|
||||
// property not present there, we need to load the full note.
|
||||
// One such missing property is the note body, which we don't
|
||||
// load by default.
|
||||
if (!(propName in note)) note = await Note.load(note.id);
|
||||
if (!(propName in note)) throw new Error(`Invalid dependency name: ${dep}`);
|
||||
output.note[propName] = (note as any)[propName];
|
||||
}
|
||||
}
|
||||
|
||||
if (dep.startsWith('item.size.')) {
|
||||
const splitted = dep.split('.');
|
||||
if (splitted.length !== 3) throw new Error(`Invalid dependency name: ${dep}`);
|
||||
const propName = splitted.pop();
|
||||
if (!output.item) output.item = {};
|
||||
if (!output.item.size) output.item.size = {};
|
||||
if (!(propName in itemSize)) throw new Error(`Invalid dependency name: ${dep}`);
|
||||
output.item.size[propName] = (itemSize as any)[propName];
|
||||
}
|
||||
|
||||
if (dep === 'item.selected') {
|
||||
if (!output.item) output.item = {};
|
||||
output.item.selected = selected;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
export default prepareViewProps;
|
||||
64
packages/app-desktop/gui/NoteList/utils/types.ts
Normal file
64
packages/app-desktop/gui/NoteList/utils/types.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { FolderEntity, ItemRendererDatabaseDependency, NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
export interface Props {
|
||||
themeId: any;
|
||||
selectedNoteIds: string[];
|
||||
notes: NoteEntity[];
|
||||
dispatch: Dispatch;
|
||||
watchedNoteFiles: string[];
|
||||
plugins: PluginStates;
|
||||
selectedFolderId: string;
|
||||
customCss: string;
|
||||
notesParentType: string;
|
||||
noteSortOrder: string;
|
||||
uncompletedTodosOnTop: boolean;
|
||||
showCompletedTodos: boolean;
|
||||
resizableLayoutEventEmitter: any;
|
||||
isInsertingNotes: boolean;
|
||||
folders: FolderEntity[];
|
||||
size: Size;
|
||||
searches: any[];
|
||||
selectedSearchId: string;
|
||||
highlightedWords: string[];
|
||||
provisionalNoteIds: string[];
|
||||
visible: boolean;
|
||||
focusedField: string;
|
||||
parentFolderIsReadOnly: boolean;
|
||||
}
|
||||
|
||||
export enum ItemFlow {
|
||||
TopToBottom = 'topToBottom',
|
||||
LeftToRight = 'leftToRight',
|
||||
}
|
||||
|
||||
export type RenderNoteView = Record<string, any>;
|
||||
|
||||
export interface OnChangeEvent {
|
||||
elementId: string;
|
||||
value: any;
|
||||
noteId: string;
|
||||
}
|
||||
|
||||
export type OnRenderNoteHandler = (props: any)=> Promise<RenderNoteView>;
|
||||
export type OnChangeHandler = (event: OnChangeEvent)=> Promise<void>;
|
||||
|
||||
export type ListRendererDepependency =
|
||||
ItemRendererDatabaseDependency |
|
||||
'item.size.width' |
|
||||
'item.size.height' |
|
||||
'item.selected' |
|
||||
'note.titleHtml' |
|
||||
'note.isWatched';
|
||||
|
||||
export interface ListRenderer {
|
||||
flow: ItemFlow;
|
||||
itemSize: Size;
|
||||
itemCss?: string;
|
||||
dependencies: ListRendererDepependency[];
|
||||
itemTemplate: string;
|
||||
onRenderNote: OnRenderNoteHandler;
|
||||
onChange?: OnChangeHandler;
|
||||
}
|
||||
102
packages/app-desktop/gui/NoteList/utils/useDragAndDrop.ts
Normal file
102
packages/app-desktop/gui/NoteList/utils/useDragAndDrop.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, DragEventHandler, MutableRefObject, useState, useEffect } from 'react';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import canManuallySortNotes from './canManuallySortNotes';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import { ItemFlow } from './types';
|
||||
|
||||
const useDragAndDrop = (
|
||||
parentFolderIsReadOnly: boolean,
|
||||
selectedNoteIds: string[],
|
||||
selectedFolderId: string,
|
||||
listRef: MutableRefObject<HTMLDivElement>,
|
||||
scrollTop: number,
|
||||
itemSize: Size,
|
||||
notesParentType: string,
|
||||
noteSortOrder: string,
|
||||
uncompletedTodosOnTop: boolean,
|
||||
showCompletedTodos: boolean,
|
||||
flow: ItemFlow,
|
||||
itemsPerLine: number,
|
||||
) => {
|
||||
const [dragOverTargetNoteIndex, setDragOverTargetNoteIndex] = useState(null);
|
||||
|
||||
const onGlobalDrop = useCallback(() => {
|
||||
setDragOverTargetNoteIndex(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('dragend', onGlobalDrop);
|
||||
return () => {
|
||||
document.removeEventListener('dragend', onGlobalDrop);
|
||||
};
|
||||
}, [onGlobalDrop]);
|
||||
|
||||
const onDragStart: DragEventHandler = useCallback(event => {
|
||||
if (parentFolderIsReadOnly) return false;
|
||||
|
||||
let noteIds = [];
|
||||
|
||||
// Here there is two cases:
|
||||
// - If multiple notes are selected, we drag the group
|
||||
// - If only one note is selected, we drag the note that was clicked on
|
||||
// (which might be different from the currently selected note)
|
||||
if (selectedNoteIds.length >= 2) {
|
||||
noteIds = selectedNoteIds;
|
||||
} else {
|
||||
const clickedNoteId = event.currentTarget.getAttribute('data-id');
|
||||
if (clickedNoteId) noteIds.push(clickedNoteId);
|
||||
}
|
||||
|
||||
if (!noteIds.length) return false;
|
||||
|
||||
event.dataTransfer.setDragImage(new Image(), 1, 1);
|
||||
event.dataTransfer.clearData();
|
||||
event.dataTransfer.setData('text/x-jop-note-ids', JSON.stringify(noteIds));
|
||||
return true;
|
||||
}, [parentFolderIsReadOnly, selectedNoteIds]);
|
||||
|
||||
|
||||
const dragTargetNoteIndex = useCallback((event: React.DragEvent) => {
|
||||
const rect = listRef.current.getBoundingClientRect();
|
||||
const lineIndexFloat = (event.clientY - rect.top + scrollTop) / itemSize.height;
|
||||
if (flow === ItemFlow.TopToBottom) {
|
||||
return Math.abs(Math.round(lineIndexFloat));
|
||||
} else {
|
||||
const lineIndex = Math.floor(lineIndexFloat);
|
||||
const rowIndexFloat = (event.clientX - rect.left) / itemSize.width;
|
||||
const rowIndex = Math.round(rowIndexFloat);
|
||||
return lineIndex * itemsPerLine + rowIndex;
|
||||
}
|
||||
}, [listRef, itemSize, scrollTop, flow, itemsPerLine]);
|
||||
|
||||
const onDragOver: DragEventHandler = useCallback(event => {
|
||||
if (notesParentType !== 'Folder') return;
|
||||
|
||||
const dt = event.dataTransfer;
|
||||
|
||||
if (dt.types.indexOf('text/x-jop-note-ids') >= 0) {
|
||||
event.preventDefault();
|
||||
const newIndex = dragTargetNoteIndex(event);
|
||||
if (dragOverTargetNoteIndex === newIndex) return;
|
||||
setDragOverTargetNoteIndex(newIndex);
|
||||
}
|
||||
}, [notesParentType, dragTargetNoteIndex, dragOverTargetNoteIndex]);
|
||||
|
||||
const onDrop: DragEventHandler = useCallback(async (event: any) => {
|
||||
// TODO: check that parent type is folder
|
||||
if (!canManuallySortNotes(notesParentType, noteSortOrder)) return;
|
||||
|
||||
const dt = event.dataTransfer;
|
||||
setDragOverTargetNoteIndex(null);
|
||||
|
||||
const targetNoteIndex = dragTargetNoteIndex(event);
|
||||
const noteIds: string[] = JSON.parse(dt.getData('text/x-jop-note-ids'));
|
||||
|
||||
await Note.insertNotesAt(selectedFolderId, noteIds, targetNoteIndex, uncompletedTodosOnTop, showCompletedTodos);
|
||||
}, [notesParentType, dragTargetNoteIndex, noteSortOrder, selectedFolderId, uncompletedTodosOnTop, showCompletedTodos]);
|
||||
|
||||
return { onDragStart, onDragOver, onDrop, dragOverTargetNoteIndex };
|
||||
};
|
||||
|
||||
export default useDragAndDrop;
|
||||
33
packages/app-desktop/gui/NoteList/utils/useFocusNote.ts
Normal file
33
packages/app-desktop/gui/NoteList/utils/useFocusNote.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { useRef, useCallback, MutableRefObject } from 'react';
|
||||
|
||||
export type FocusNote = (noteId: string)=> void;
|
||||
|
||||
const useFocusNote = (itemRefs: MutableRefObject<Record<string, HTMLDivElement>>) => {
|
||||
const focusItemIID = useRef(null);
|
||||
|
||||
const focusNote: FocusNote = useCallback((noteId: string) => {
|
||||
// - We need to focus the item manually otherwise focus might be lost when the
|
||||
// list is scrolled and items within it are being rebuilt.
|
||||
// - We need to use an interval because when leaving the arrow pressed, the rendering
|
||||
// of items might lag behind and so the ref is not yet available at this point.
|
||||
|
||||
if (!itemRefs.current[noteId]) {
|
||||
if (focusItemIID.current) shim.clearInterval(focusItemIID.current);
|
||||
focusItemIID.current = shim.setInterval(() => {
|
||||
if (itemRefs.current[noteId]) {
|
||||
itemRefs.current[noteId].focus();
|
||||
shim.clearInterval(focusItemIID.current);
|
||||
focusItemIID.current = null;
|
||||
}
|
||||
}, 10);
|
||||
} else {
|
||||
if (focusItemIID.current) shim.clearInterval(focusItemIID.current);
|
||||
itemRefs.current[noteId].focus();
|
||||
}
|
||||
}, [itemRefs]);
|
||||
|
||||
return focusNote;
|
||||
};
|
||||
|
||||
export default useFocusNote;
|
||||
19
packages/app-desktop/gui/NoteList/utils/useItemCss.ts
Normal file
19
packages/app-desktop/gui/NoteList/utils/useItemCss.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const useItemCss = (itemCss: string) => {
|
||||
useEffect(() => {
|
||||
const element = document.createElement('style');
|
||||
element.setAttribute('type', 'text/css');
|
||||
element.appendChild(document.createTextNode(`
|
||||
.note-list-item {
|
||||
${itemCss};
|
||||
}
|
||||
`));
|
||||
document.head.appendChild(element);
|
||||
return () => {
|
||||
element.remove();
|
||||
};
|
||||
}, [itemCss]);
|
||||
};
|
||||
|
||||
export default useItemCss;
|
||||
25
packages/app-desktop/gui/NoteList/utils/useMoveNote.ts
Normal file
25
packages/app-desktop/gui/NoteList/utils/useMoveNote.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { useCallback } from 'react';
|
||||
import canManuallySortNotes from './canManuallySortNotes';
|
||||
|
||||
const useMoveNote = (notesParentType: string, noteSortOrder: string, selectedNoteIds: string[], selectedFolderId: string, uncompletedTodosOnTop: boolean, showCompletedTodos: boolean, notes: NoteEntity[]) => {
|
||||
const moveNote = useCallback((direction: number, inc: number) => {
|
||||
if (!canManuallySortNotes(notesParentType, noteSortOrder)) return;
|
||||
|
||||
const noteId = selectedNoteIds[0];
|
||||
let targetNoteIndex = BaseModel.modelIndexById(notes, noteId);
|
||||
if ((direction === 1)) {
|
||||
targetNoteIndex += inc + 1;
|
||||
}
|
||||
if ((direction === -1)) {
|
||||
targetNoteIndex -= inc;
|
||||
}
|
||||
void Note.insertNotesAt(selectedFolderId, selectedNoteIds, targetNoteIndex, uncompletedTodosOnTop, showCompletedTodos);
|
||||
}, [selectedFolderId, noteSortOrder, notes, notesParentType, selectedNoteIds, uncompletedTodosOnTop, showCompletedTodos]);
|
||||
|
||||
return moveNote;
|
||||
};
|
||||
|
||||
export default useMoveNote;
|
||||
150
packages/app-desktop/gui/NoteList/utils/useOnKeyDown.ts
Normal file
150
packages/app-desktop/gui/NoteList/utils/useOnKeyDown.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import * as React from 'react';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { useCallback } from 'react';
|
||||
import { Dispatch } from 'redux';
|
||||
import { FocusNote } from './useFocusNote';
|
||||
import { ItemFlow } from './types';
|
||||
import { KeyboardEventKey } from '@joplin/lib/dom';
|
||||
|
||||
const useOnKeyDown = (
|
||||
selectedNoteIds: string[],
|
||||
moveNote: (direction: number, inc: number)=> void,
|
||||
makeItemIndexVisible: (itemIndex: number)=> void,
|
||||
focusNote: FocusNote,
|
||||
notes: NoteEntity[],
|
||||
dispatch: Dispatch,
|
||||
visibleItemCount: number,
|
||||
noteCount: number,
|
||||
flow: ItemFlow,
|
||||
itemsPerLine: number,
|
||||
) => {
|
||||
const scrollNoteIndex = useCallback((visibleItemCount: number, key: KeyboardEventKey, ctrlKey: boolean, metaKey: boolean, noteIndex: number) => {
|
||||
if (flow === ItemFlow.TopToBottom) {
|
||||
if (key === 'PageUp') {
|
||||
noteIndex -= (visibleItemCount - 1);
|
||||
} else if (key === 'PageDown') {
|
||||
noteIndex += (visibleItemCount - 1);
|
||||
} else if ((key === 'End' && ctrlKey) || (key === 'ArrowDown' && metaKey)) {
|
||||
noteIndex = noteCount - 1;
|
||||
} else if ((key === 'Home' && ctrlKey) || (key === 'ArrowUp' && metaKey)) {
|
||||
noteIndex = 0;
|
||||
} else if (key === 'ArrowUp' && !metaKey) {
|
||||
noteIndex -= 1;
|
||||
} else if (key === 'ArrowDown' && !metaKey) {
|
||||
noteIndex += 1;
|
||||
}
|
||||
if (noteIndex < 0) noteIndex = 0;
|
||||
if (noteIndex > noteCount - 1) noteIndex = noteCount - 1;
|
||||
}
|
||||
|
||||
if (flow === ItemFlow.LeftToRight) {
|
||||
if (key === 'PageUp') {
|
||||
noteIndex -= (visibleItemCount - itemsPerLine);
|
||||
} else if (key === 'PageDown') {
|
||||
noteIndex += (visibleItemCount - itemsPerLine);
|
||||
} else if ((key === 'End' && ctrlKey) || (key === 'ArrowDown' && metaKey)) {
|
||||
noteIndex = noteCount - 1;
|
||||
} else if ((key === 'Home' && ctrlKey) || (key === 'ArrowUp' && metaKey)) {
|
||||
noteIndex = 0;
|
||||
} else if (key === 'ArrowUp' && !metaKey) {
|
||||
noteIndex -= itemsPerLine;
|
||||
} else if (key === 'ArrowDown' && !metaKey) {
|
||||
noteIndex += itemsPerLine;
|
||||
} else if (key === 'ArrowLeft' && !metaKey) {
|
||||
noteIndex -= 1;
|
||||
} else if (key === 'ArrowRight' && !metaKey) {
|
||||
noteIndex += 1;
|
||||
}
|
||||
if (noteIndex < 0) noteIndex = 0;
|
||||
if (noteIndex > noteCount - 1) noteIndex = noteCount - 1;
|
||||
}
|
||||
|
||||
return noteIndex;
|
||||
}, [noteCount, flow, itemsPerLine]);
|
||||
|
||||
const onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = useCallback(async (event) => {
|
||||
const noteIds = selectedNoteIds;
|
||||
const key = event.key as KeyboardEventKey;
|
||||
|
||||
if (['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'].includes(key) && event.altKey) {
|
||||
if (flow === ItemFlow.TopToBottom) {
|
||||
await moveNote(key === 'ArrowDown' ? 1 : -1, 1);
|
||||
} else {
|
||||
if (key === 'ArrowRight') {
|
||||
await moveNote(1, 1);
|
||||
} else if (key === 'ArrowLeft') {
|
||||
await moveNote(-1, 1);
|
||||
} else if (key === 'ArrowUp') {
|
||||
await moveNote(-1, itemsPerLine);
|
||||
} else if (key === 'ArrowDown') {
|
||||
await moveNote(1, itemsPerLine);
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
} else if (noteIds.length > 0 && (key === 'ArrowDown' || key === 'ArrowUp' || key === 'ArrowLeft' || key === 'ArrowRight' || key === 'PageDown' || key === 'PageUp' || key === 'End' || key === 'Home')) {
|
||||
const noteId = noteIds[0];
|
||||
let noteIndex = BaseModel.modelIndexById(notes, noteId);
|
||||
|
||||
noteIndex = scrollNoteIndex(visibleItemCount, key, event.ctrlKey, event.metaKey, noteIndex);
|
||||
|
||||
const newSelectedNote = notes[noteIndex];
|
||||
|
||||
dispatch({
|
||||
type: 'NOTE_SELECT',
|
||||
id: newSelectedNote.id,
|
||||
});
|
||||
|
||||
makeItemIndexVisible(noteIndex);
|
||||
|
||||
focusNote(newSelectedNote.id);
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (noteIds.length && (key === 'Delete' || (key === 'Backspace' && event.metaKey))) {
|
||||
event.preventDefault();
|
||||
void CommandService.instance().execute('deleteNote', noteIds);
|
||||
}
|
||||
|
||||
if (noteIds.length && key === ' ') {
|
||||
event.preventDefault();
|
||||
|
||||
const selectedNotes = BaseModel.modelsByIds(notes, noteIds);
|
||||
const todos = selectedNotes.filter((n: any) => !!n.is_todo);
|
||||
if (!todos.length) return;
|
||||
|
||||
for (let i = 0; i < todos.length; i++) {
|
||||
const toggledTodo = Note.toggleTodoCompleted(todos[i]);
|
||||
await Note.save(toggledTodo);
|
||||
}
|
||||
|
||||
focusNote(todos[0].id);
|
||||
}
|
||||
|
||||
if (key === 'Tab') {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.shiftKey) {
|
||||
void CommandService.instance().execute('focusElement', 'sideBar');
|
||||
} else {
|
||||
void CommandService.instance().execute('focusElement', 'noteTitle');
|
||||
}
|
||||
}
|
||||
|
||||
if (key.toUpperCase() === 'A' && (event.ctrlKey || event.metaKey)) {
|
||||
event.preventDefault();
|
||||
|
||||
dispatch({
|
||||
type: 'NOTE_SELECT_ALL',
|
||||
});
|
||||
}
|
||||
}, [moveNote, focusNote, visibleItemCount, scrollNoteIndex, makeItemIndexVisible, notes, selectedNoteIds, dispatch, flow, itemsPerLine]);
|
||||
|
||||
|
||||
return onKeyDown;
|
||||
};
|
||||
|
||||
export default useOnKeyDown;
|
||||
41
packages/app-desktop/gui/NoteList/utils/useOnNoteClick.ts
Normal file
41
packages/app-desktop/gui/NoteList/utils/useOnNoteClick.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { Dispatch } from 'redux';
|
||||
import { FocusNote } from './useFocusNote';
|
||||
|
||||
const useOnNoteClick = (dispatch: Dispatch, focusNote: FocusNote) => {
|
||||
const onNoteClick = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
|
||||
const noteId = event.currentTarget.getAttribute('data-id');
|
||||
|
||||
const targetTagName = event.target ? (event.target as any).tagName : '';
|
||||
|
||||
// If we are for example on a checkbox, don't process the click since it
|
||||
// should be handled by the checkbox onChange handler.
|
||||
if (['INPUT'].includes(targetTagName)) return;
|
||||
|
||||
focusNote(noteId);
|
||||
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
event.preventDefault();
|
||||
dispatch({
|
||||
type: 'NOTE_SELECT_TOGGLE',
|
||||
id: noteId,
|
||||
});
|
||||
} else if (event.shiftKey) {
|
||||
event.preventDefault();
|
||||
dispatch({
|
||||
type: 'NOTE_SELECT_EXTEND',
|
||||
id: noteId,
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'NOTE_SELECT',
|
||||
id: noteId,
|
||||
});
|
||||
}
|
||||
}, [dispatch, focusNote]);
|
||||
|
||||
return onNoteClick;
|
||||
};
|
||||
|
||||
export default useOnNoteClick;
|
||||
82
packages/app-desktop/gui/NoteList/utils/useRenderedNotes.ts
Normal file
82
packages/app-desktop/gui/NoteList/utils/useRenderedNotes.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useState } from 'react';
|
||||
import { ListRenderer } from './types';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import * as Mustache from 'mustache';
|
||||
import { createHash } from 'crypto';
|
||||
import getNoteTitleHtml from './getNoteTitleHtml';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import prepareViewProps from './prepareViewProps';
|
||||
|
||||
interface RenderedNote {
|
||||
id: string;
|
||||
hash: string;
|
||||
html: string;
|
||||
}
|
||||
|
||||
const hashContent = (content: any) => {
|
||||
return createHash('sha1').update(JSON.stringify(content)).digest('hex');
|
||||
};
|
||||
|
||||
const useRenderedNotes = (startNoteIndex: number, endNoteIndex: number, notes: NoteEntity[], selectedNoteIds: string[], listRenderer: ListRenderer, highlightedWords: string[], watchedNoteFiles: string[]) => {
|
||||
const [renderedNotes, setRenderedNotes] = useState<Record<string, RenderedNote>>({});
|
||||
|
||||
useAsyncEffect(async (event) => {
|
||||
if (event.cancelled) return;
|
||||
|
||||
const renderNote = async (note: NoteEntity): Promise<void> => {
|
||||
const isSelected = selectedNoteIds.includes(note.id);
|
||||
const isWatched = watchedNoteFiles.includes(note.id);
|
||||
|
||||
// Note: with this hash we're assuming that the list renderer
|
||||
// properties never changes. It means that later if we support
|
||||
// dynamic list renderers, we should include these into the hash.
|
||||
const viewHash = hashContent([
|
||||
note.updated_time,
|
||||
isSelected,
|
||||
isWatched,
|
||||
highlightedWords,
|
||||
]);
|
||||
|
||||
if (renderedNotes[note.id] && renderedNotes[note.id].hash === viewHash) return null;
|
||||
|
||||
const titleHtml = getNoteTitleHtml(highlightedWords, Note.displayTitle(note));
|
||||
const viewProps = await prepareViewProps(
|
||||
listRenderer.dependencies,
|
||||
note,
|
||||
listRenderer.itemSize,
|
||||
isSelected,
|
||||
titleHtml,
|
||||
isWatched,
|
||||
);
|
||||
const view = await listRenderer.onRenderNote(viewProps);
|
||||
|
||||
if (event.cancelled) return null;
|
||||
|
||||
setRenderedNotes(prev => {
|
||||
if (prev[note.id] && prev[note.id].hash === viewHash) return prev;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[note.id]: {
|
||||
id: note.id,
|
||||
hash: viewHash,
|
||||
html: Mustache.render(listRenderer.itemTemplate, view),
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (let i = startNoteIndex; i <= endNoteIndex; i++) {
|
||||
promises.push(renderNote(notes[i]));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}, [startNoteIndex, endNoteIndex, notes, selectedNoteIds, listRenderer, renderedNotes, watchedNoteFiles]);
|
||||
|
||||
return renderedNotes;
|
||||
};
|
||||
|
||||
export default useRenderedNotes;
|
||||
99
packages/app-desktop/gui/NoteList/utils/useScroll.ts
Normal file
99
packages/app-desktop/gui/NoteList/utils/useScroll.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import * as React from 'react';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import { useCallback, useState, useRef, useEffect, useMemo } from 'react';
|
||||
|
||||
const useScroll = (itemsPerLine: number, noteCount: number, itemSize: Size, listSize: Size, listRef: React.MutableRefObject<HTMLDivElement>) => {
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
const lastScrollSetTime = useRef(0);
|
||||
|
||||
const maxScrollTop = useMemo(() => {
|
||||
return Math.max(0, itemSize.height * noteCount - listSize.height);
|
||||
}, [itemSize.height, noteCount, listSize.height]);
|
||||
|
||||
// This ugly hack is necessary because setting scrollTop at a high
|
||||
// frequency, while scrolling with the keyboard, is unreliable - the
|
||||
// property will appear to be set (reading it back gives the correct value),
|
||||
// but the scrollbar will not be at the expected position. That can be
|
||||
// verified by moving the scrollbar a little and reading the event value -
|
||||
// it will be different from what was set, and what was read.
|
||||
//
|
||||
// As a result, since we can't rely on setting or reading that value (to
|
||||
// check if it's correct), we forcefully set it multiple times over the next
|
||||
// few milliseconds, hoping that maybe one of these attempts will stick.
|
||||
//
|
||||
// This is most likely a race condition in either Chromimum or Electron
|
||||
// although I couldn't find an upstream issue.
|
||||
//
|
||||
// Setting the value only once after a short time, for example 10ms, helps
|
||||
// but still fails now and then. Setting it after 500ms would probably work
|
||||
// reliably but it's too slow so it makes sense to do it in an interval.
|
||||
|
||||
const setScrollTopLikeYouMeanItTimer = useRef(null);
|
||||
const setScrollTopLikeYouMeanItStartTime = useRef(0);
|
||||
const setScrollTopLikeYouMeanIt = useCallback((newScrollTop: number) => {
|
||||
if (setScrollTopLikeYouMeanItTimer.current) shim.clearInterval(setScrollTopLikeYouMeanItTimer.current);
|
||||
setScrollTopLikeYouMeanItStartTime.current = Date.now();
|
||||
|
||||
setScrollTopLikeYouMeanItTimer.current = shim.setInterval(() => {
|
||||
if (!listRef.current) {
|
||||
shim.clearInterval(setScrollTopLikeYouMeanItTimer.current);
|
||||
setScrollTopLikeYouMeanItTimer.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
listRef.current.scrollTop = newScrollTop;
|
||||
lastScrollSetTime.current = Date.now();
|
||||
|
||||
if (Date.now() - setScrollTopLikeYouMeanItStartTime.current > 500) {
|
||||
shim.clearInterval(setScrollTopLikeYouMeanItTimer.current);
|
||||
setScrollTopLikeYouMeanItTimer.current = null;
|
||||
}
|
||||
}, 10);
|
||||
}, [listRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (setScrollTopLikeYouMeanItTimer.current) shim.clearInterval(setScrollTopLikeYouMeanItTimer.current);
|
||||
setScrollTopLikeYouMeanItTimer.current = null;
|
||||
}, []);
|
||||
|
||||
const makeItemIndexVisible = useCallback((itemIndex: number) => {
|
||||
const lineTopFloat = scrollTop / itemSize.height;
|
||||
const topFloat = lineTopFloat * itemsPerLine; // scrollTop / itemSize.height;
|
||||
const lineBottomFloat = (scrollTop + listSize.height - itemSize.height) / itemSize.height;
|
||||
const bottomFloat = lineBottomFloat * itemsPerLine; // (scrollTop + listSize.height - itemSize.height) / itemSize.height;
|
||||
const top = Math.min(noteCount - 1, Math.floor(topFloat) + 1);
|
||||
const bottom = Math.max(0, Math.floor(bottomFloat));
|
||||
|
||||
if (itemIndex >= top && itemIndex <= bottom) return;
|
||||
|
||||
const lineIndex = Math.floor(itemIndex / itemsPerLine);
|
||||
|
||||
let newScrollTop = 0;
|
||||
if (itemIndex < top) {
|
||||
newScrollTop = itemSize.height * lineIndex;
|
||||
} else {
|
||||
newScrollTop = itemSize.height * (lineIndex + 1) - listSize.height;
|
||||
}
|
||||
|
||||
if (newScrollTop < 0) newScrollTop = 0;
|
||||
if (newScrollTop > maxScrollTop) newScrollTop = maxScrollTop;
|
||||
|
||||
setScrollTop(newScrollTop);
|
||||
setScrollTopLikeYouMeanIt(newScrollTop);
|
||||
}, [itemsPerLine, noteCount, itemSize.height, scrollTop, listSize.height, maxScrollTop, setScrollTopLikeYouMeanIt]);
|
||||
|
||||
const onScroll = useCallback((event: any) => {
|
||||
// Ignore the scroll event if it has just been set programmatically.
|
||||
if (Date.now() - lastScrollSetTime.current < 100) return;
|
||||
setScrollTop(event.target.scrollTop);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
scrollTop,
|
||||
onScroll,
|
||||
makeItemIndexVisible,
|
||||
};
|
||||
};
|
||||
|
||||
export default useScroll;
|
||||
@@ -0,0 +1,61 @@
|
||||
import useVisibleRange from './useVisibleRange';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
|
||||
describe('useVisibleRange', () => {
|
||||
|
||||
test('should calculate indexes', () => {
|
||||
|
||||
// IN: scrollTop, listSize, itemSize, noteCount, flow
|
||||
//
|
||||
// OUT: [itemsPerLine, startNoteIndex, endNoteIndex, startLineIndex, endLineIndex, totalLineCount, visibleItemCount]
|
||||
|
||||
const testCases: [number, number, Size, Size, number, ReturnType<typeof useVisibleRange>][] = [
|
||||
[
|
||||
1,
|
||||
150,
|
||||
{ width: 100, height: 400 },
|
||||
{ width: 100, height: 100 },
|
||||
8,
|
||||
[1, 5, 1, 5, 8, 5],
|
||||
],
|
||||
[
|
||||
2,
|
||||
100,
|
||||
{ width: 220, height: 380 },
|
||||
{ width: 100, height: 100 },
|
||||
12,
|
||||
[2, 9, 1, 4, 6, 8],
|
||||
],
|
||||
[
|
||||
2,
|
||||
50,
|
||||
{ width: 220, height: 300 },
|
||||
{ width: 100, height: 100 },
|
||||
9,
|
||||
[0, 7, 0, 3, 5, 8],
|
||||
],
|
||||
[
|
||||
4,
|
||||
0,
|
||||
{ width: 410, height: 450 },
|
||||
{ width: 100, height: 100 },
|
||||
30,
|
||||
[0, 19, 0, 4, 8, 20],
|
||||
],
|
||||
];
|
||||
|
||||
for (const [scrollTop, listSize, itemSize, noteCount, flow, expected] of testCases) {
|
||||
const { result } = renderHook(() => useVisibleRange(
|
||||
scrollTop,
|
||||
listSize,
|
||||
itemSize,
|
||||
noteCount,
|
||||
flow,
|
||||
));
|
||||
|
||||
expect(result.current).toEqual(expected);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
57
packages/app-desktop/gui/NoteList/utils/useVisibleRange.ts
Normal file
57
packages/app-desktop/gui/NoteList/utils/useVisibleRange.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const useVisibleRange = (itemsPerLine: number, scrollTop: number, listSize: Size, itemSize: Size, noteCount: number) => {
|
||||
const startLineIndexFloat = useMemo(() => {
|
||||
return scrollTop / itemSize.height;
|
||||
}, [scrollTop, itemSize.height]);
|
||||
|
||||
const endLineIndexFloat = useMemo(() => {
|
||||
return startLineIndexFloat + (listSize.height / itemSize.height);
|
||||
}, [startLineIndexFloat, listSize.height, itemSize.height]);
|
||||
|
||||
const startLineIndex = useMemo(() => {
|
||||
return Math.floor(startLineIndexFloat);
|
||||
}, [startLineIndexFloat]);
|
||||
|
||||
const endLineIndex = useMemo(() => {
|
||||
return Math.floor(endLineIndexFloat);
|
||||
}, [endLineIndexFloat]);
|
||||
|
||||
const visibleLineCount = useMemo(() => {
|
||||
return endLineIndex - startLineIndex + 1;
|
||||
}, [endLineIndex, startLineIndex]);
|
||||
|
||||
const visibleItemCount = useMemo(() => {
|
||||
return visibleLineCount * itemsPerLine;
|
||||
}, [visibleLineCount, itemsPerLine]);
|
||||
|
||||
const startNoteIndex = useMemo(() => {
|
||||
return itemsPerLine * startLineIndex;
|
||||
}, [itemsPerLine, startLineIndex]);
|
||||
|
||||
const endNoteIndex = useMemo(() => {
|
||||
let output = (endLineIndex + 1) * itemsPerLine - 1;
|
||||
if (output >= noteCount) output = noteCount - 1;
|
||||
return output;
|
||||
}, [endLineIndex, itemsPerLine, noteCount]);
|
||||
|
||||
const totalLineCount = useMemo(() => {
|
||||
return Math.ceil(noteCount / itemsPerLine);
|
||||
}, [noteCount, itemsPerLine]);
|
||||
|
||||
// console.info('itemsPerLine', itemsPerLine);
|
||||
// console.info('startLineIndexFloat', startLineIndexFloat);
|
||||
// console.info('endLineIndexFloat', endLineIndexFloat);
|
||||
// console.info('visibleLineCount', visibleLineCount);
|
||||
// console.info('startNoteIndex', startNoteIndex);
|
||||
// console.info('endNoteIndex', endNoteIndex);
|
||||
// console.info('startLineIndex', startLineIndex);
|
||||
// console.info('endLineIndex', endLineIndex);
|
||||
// console.info('totalLineCount', totalLineCount);
|
||||
// console.info('visibleItemCount', visibleItemCount);
|
||||
|
||||
return [startNoteIndex, endNoteIndex, startLineIndex, endLineIndex, totalLineCount, visibleItemCount];
|
||||
};
|
||||
|
||||
export default useVisibleRange;
|
||||
143
packages/app-desktop/gui/NoteListItem/NoteListItem.tsx
Normal file
143
packages/app-desktop/gui/NoteListItem/NoteListItem.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, forwardRef, LegacyRef, ChangeEvent, CSSProperties, MouseEventHandler, DragEventHandler, useMemo, memo } from 'react';
|
||||
import { ItemFlow, OnChangeEvent, OnChangeHandler } from '../NoteList/utils/types';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import useRootElement from './utils/useRootElement';
|
||||
import useItemElement from './utils/useItemElement';
|
||||
import useItemEventHandlers from './utils/useItemEventHandlers';
|
||||
import { OnCheckboxChange } from './utils/types';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
|
||||
interface NoteItemProps {
|
||||
dragIndex: number;
|
||||
flow: ItemFlow;
|
||||
highlightedWords: string[];
|
||||
index: number;
|
||||
isProvisional: boolean;
|
||||
itemSize: Size;
|
||||
noteCount: number;
|
||||
noteHtml: string;
|
||||
noteId: string;
|
||||
onChange: OnChangeHandler;
|
||||
onClick: MouseEventHandler<HTMLDivElement>;
|
||||
onContextMenu: MouseEventHandler;
|
||||
onDragOver: DragEventHandler;
|
||||
onDragStart: DragEventHandler;
|
||||
style: CSSProperties;
|
||||
}
|
||||
|
||||
const NoteListItem = (props: NoteItemProps, ref: LegacyRef<HTMLDivElement>) => {
|
||||
const elementId = `list-note-${props.noteId}`;
|
||||
|
||||
const onCheckboxChange: OnCheckboxChange = useCallback(async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const changeEvent: OnChangeEvent = {
|
||||
noteId: props.noteId,
|
||||
elementId: event.currentTarget.getAttribute('data-id'),
|
||||
value: event.currentTarget.checked,
|
||||
};
|
||||
|
||||
if (changeEvent.elementId === 'todo-checkbox') {
|
||||
await Note.save({
|
||||
id: changeEvent.noteId,
|
||||
todo_completed: changeEvent.value ? Date.now() : 0,
|
||||
}, { userSideValidation: true });
|
||||
} else {
|
||||
if (props.onChange) await props.onChange(changeEvent);
|
||||
}
|
||||
}, [props.onChange, props.noteId]);
|
||||
|
||||
const rootElement = useRootElement(elementId);
|
||||
|
||||
const itemElement = useItemElement(
|
||||
rootElement,
|
||||
props.noteId,
|
||||
props.noteHtml,
|
||||
props.style,
|
||||
props.itemSize,
|
||||
props.onClick,
|
||||
props.flow,
|
||||
);
|
||||
|
||||
useItemEventHandlers(rootElement, itemElement, onCheckboxChange);
|
||||
|
||||
const className = useMemo(() => {
|
||||
return [
|
||||
'note-list-item-wrapper',
|
||||
|
||||
// This is not used by the app, but kept here because it may be used
|
||||
// by users for custom CSS.
|
||||
(props.index + 1) % 2 === 0 ? 'even' : 'odd',
|
||||
|
||||
props.isProvisional && '-provisional',
|
||||
].filter(e => !!e).join(' ');
|
||||
}, [props.index, props.isProvisional]);
|
||||
|
||||
const isActiveDragItem = props.dragIndex === props.index;
|
||||
const isLastActiveDragItem = props.index === props.noteCount - 1 && props.dragIndex >= props.noteCount;
|
||||
|
||||
const dragCursorStyle = useMemo(() => {
|
||||
if (props.flow === ItemFlow.TopToBottom) {
|
||||
let dragItemPosition = '';
|
||||
if (isActiveDragItem) {
|
||||
dragItemPosition = 'top';
|
||||
} else if (isLastActiveDragItem) {
|
||||
dragItemPosition = 'bottom';
|
||||
}
|
||||
|
||||
const output: React.CSSProperties = {
|
||||
width: props.itemSize.width,
|
||||
display: dragItemPosition ? 'block' : 'none',
|
||||
left: 0,
|
||||
};
|
||||
|
||||
if (dragItemPosition === 'top') {
|
||||
output.top = 0;
|
||||
} else {
|
||||
output.bottom = 0;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
if (props.flow === ItemFlow.LeftToRight) {
|
||||
let dragItemPosition = '';
|
||||
if (isActiveDragItem) {
|
||||
dragItemPosition = 'left';
|
||||
} else if (isLastActiveDragItem) {
|
||||
dragItemPosition = 'right';
|
||||
}
|
||||
|
||||
const output: React.CSSProperties = {
|
||||
height: props.itemSize.height,
|
||||
display: dragItemPosition ? 'block' : 'none',
|
||||
top: 0,
|
||||
};
|
||||
|
||||
if (dragItemPosition === 'left') {
|
||||
output.left = 0;
|
||||
} else {
|
||||
output.right = 0;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
throw new Error('Unreachable');
|
||||
}, [isActiveDragItem, isLastActiveDragItem, props.flow, props.itemSize]);
|
||||
|
||||
return <div
|
||||
id={elementId}
|
||||
ref={ref}
|
||||
draggable={true}
|
||||
tabIndex={0}
|
||||
className={className}
|
||||
data-id={props.noteId}
|
||||
onContextMenu={props.onContextMenu}
|
||||
onDragStart={props.onDragStart}
|
||||
onDragOver={props.onDragOver}
|
||||
>
|
||||
<div className="dragcursor" style={dragCursorStyle}></div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default memo(forwardRef(NoteListItem));
|
||||
3
packages/app-desktop/gui/NoteListItem/utils/types.ts
Normal file
3
packages/app-desktop/gui/NoteListItem/utils/types.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export type OnCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>)=> void;
|
||||
@@ -0,0 +1,35 @@
|
||||
import * as React from 'react';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ItemFlow } from '../../NoteList/utils/types';
|
||||
|
||||
const useItemElement = (rootElement: HTMLDivElement, noteId: string, noteHtml: string, style: any, itemSize: Size, onClick: React.MouseEventHandler<HTMLDivElement>, flow: ItemFlow) => {
|
||||
const [itemElement, setItemElement] = useState<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rootElement) return () => {};
|
||||
|
||||
const element = document.createElement('div');
|
||||
element.setAttribute('data-id', noteId);
|
||||
element.className = 'note-list-item';
|
||||
for (const [n, v] of Object.entries(style)) {
|
||||
(element.style as any)[n] = v;
|
||||
}
|
||||
if (flow === ItemFlow.LeftToRight) element.style.width = `${itemSize.width}px`;
|
||||
element.style.height = `${itemSize.height}px`;
|
||||
element.innerHTML = noteHtml;
|
||||
element.addEventListener('click', onClick as any);
|
||||
|
||||
rootElement.appendChild(element);
|
||||
|
||||
setItemElement(element);
|
||||
|
||||
return () => {
|
||||
element.remove();
|
||||
};
|
||||
}, [rootElement, itemSize, noteHtml, noteId, style, onClick, flow]);
|
||||
|
||||
return itemElement;
|
||||
};
|
||||
|
||||
export default useItemElement;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { OnCheckboxChange } from './types';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const useItemEventHandlers = (rootElement: HTMLDivElement, itemElement: HTMLDivElement, onCheckboxChange: OnCheckboxChange) => {
|
||||
useEffect(() => {
|
||||
if (!itemElement) return () => {};
|
||||
|
||||
const inputs = itemElement.getElementsByTagName('input');
|
||||
|
||||
const mods: HTMLInputElement[] = [];
|
||||
|
||||
for (const input of inputs) {
|
||||
if (input.type === 'checkbox') {
|
||||
input.addEventListener('change', onCheckboxChange as any);
|
||||
mods.push(input);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const input of mods) {
|
||||
input.removeEventListener('change', onCheckboxChange as any);
|
||||
}
|
||||
};
|
||||
}, [itemElement, rootElement, onCheckboxChange]);
|
||||
};
|
||||
|
||||
export default useItemEventHandlers;
|
||||
@@ -0,0 +1,44 @@
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { useCallback } from 'react';
|
||||
import { Dispatch } from 'redux';
|
||||
import bridge from '../../../services/bridge';
|
||||
import NoteListUtils from '../../utils/NoteListUtils';
|
||||
|
||||
const useOnContextMenu = (
|
||||
selectedNoteIds: string[],
|
||||
selectedFolderId: string,
|
||||
notes: NoteEntity[],
|
||||
dispatch: Dispatch,
|
||||
watchedNoteFiles: string[],
|
||||
plugins: PluginStates,
|
||||
customCss: string,
|
||||
) => {
|
||||
return useCallback((event: any) => {
|
||||
const currentNoteId = event.currentTarget.getAttribute('data-id');
|
||||
if (!currentNoteId) return;
|
||||
|
||||
let noteIds = [];
|
||||
if (selectedNoteIds.indexOf(currentNoteId) < 0) {
|
||||
noteIds = [currentNoteId];
|
||||
} else {
|
||||
noteIds = selectedNoteIds;
|
||||
}
|
||||
|
||||
if (!noteIds.length) return;
|
||||
|
||||
const menu = NoteListUtils.makeContextMenu(noteIds, {
|
||||
notes: notes,
|
||||
dispatch: dispatch,
|
||||
watchedNoteFiles: watchedNoteFiles,
|
||||
plugins: plugins,
|
||||
inConflictFolder: selectedFolderId === Folder.conflictFolderId(),
|
||||
customCss: customCss,
|
||||
});
|
||||
|
||||
menu.popup({ window: bridge().window() });
|
||||
}, [selectedNoteIds, notes, dispatch, watchedNoteFiles, plugins, selectedFolderId, customCss]);
|
||||
};
|
||||
|
||||
export default useOnContextMenu;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useState } from 'react';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { waitForElement } from '@joplin/lib/dom';
|
||||
|
||||
const useRootElement = (elementId: string) => {
|
||||
const [rootElement, setRootElement] = useState<HTMLDivElement>(null);
|
||||
|
||||
useAsyncEffect(async (event) => {
|
||||
const element = await waitForElement(document, elementId);
|
||||
if (event.cancelled) return;
|
||||
setRootElement(element);
|
||||
}, [document, elementId]);
|
||||
|
||||
return rootElement;
|
||||
};
|
||||
|
||||
export default useRootElement;
|
||||
@@ -1,7 +1,8 @@
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import * as React from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import NoteList from '../NoteList/NoteList';
|
||||
// import NoteList from '../NoteList/NoteList';
|
||||
import NoteList2 from '../NoteList/NoteList2';
|
||||
import NoteListControls from '../NoteListControls/NoteListControls';
|
||||
import { Size } from '../ResizableLayout/utils/types';
|
||||
import styled from 'styled-components';
|
||||
@@ -39,10 +40,12 @@ export default function NoteListWrapper(props: Props) {
|
||||
};
|
||||
}, [props.size, controlHeight]);
|
||||
|
||||
// <NoteList resizableLayoutEventEmitter={props.resizableLayoutEventEmitter} size={noteListSize} visible={props.visible}/>
|
||||
|
||||
return (
|
||||
<StyledRoot>
|
||||
<NoteListControls height={controlHeight} width={noteListSize.width} onContentHeightChange={onContentHeightChange}/>
|
||||
<NoteList resizableLayoutEventEmitter={props.resizableLayoutEventEmitter} size={noteListSize} visible={props.visible}/>
|
||||
<NoteList2 resizableLayoutEventEmitter={props.resizableLayoutEventEmitter} size={noteListSize} visible={props.visible}/>
|
||||
</StyledRoot>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
},
|
||||
() => {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
|
||||
},
|
||||
() => {
|
||||
void this.reloadNote();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
|
||||
},
|
||||
() => {
|
||||
void this.reloadNote();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -198,7 +198,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
|
||||
revisionListItems.push(
|
||||
<option key={rev.id} value={rev.id}>
|
||||
{`${time.formatMsToLocal(rev.item_updated_time)} (${stats})`}
|
||||
</option>
|
||||
</option>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -275,28 +275,28 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
buttonComps.push(
|
||||
<button key="create" disabled={!this.state.answer} style={styles.button} onClick={() => onClose(true, 'create')}>
|
||||
{_('Create')}
|
||||
</button>
|
||||
</button>,
|
||||
);
|
||||
}
|
||||
if (buttonTypes.indexOf('ok') >= 0) {
|
||||
buttonComps.push(
|
||||
<button key="ok" disabled={!this.state.answer} style={styles.button} onClick={() => onClose(true, 'ok')}>
|
||||
{_('OK')}
|
||||
</button>
|
||||
</button>,
|
||||
);
|
||||
}
|
||||
if (buttonTypes.indexOf('cancel') >= 0) {
|
||||
buttonComps.push(
|
||||
<button key="cancel" style={styles.button} onClick={() => onClose(false, 'cancel')}>
|
||||
{_('Cancel')}
|
||||
</button>
|
||||
</button>,
|
||||
);
|
||||
}
|
||||
if (buttonTypes.indexOf('clear') >= 0) {
|
||||
buttonComps.push(
|
||||
<button key="clear" style={styles.button} onClick={() => onClose(false, 'clear')}>
|
||||
{_('Clear')}
|
||||
</button>
|
||||
</button>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ const ResourceTableComp = (props: ResourceTable) => {
|
||||
<td style={cellStyle} className="dataCell">
|
||||
<button style={theme.buttonStyle} onClick={() => props.onResourceDelete(resource)}>{_('Delete')}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tr>,
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -267,5 +267,5 @@ root.render(
|
||||
<ErrorBoundary>
|
||||
<Root />
|
||||
</ErrorBoundary>
|
||||
</Provider>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
@@ -280,7 +280,7 @@ const SidebarComponent = (props: Props) => {
|
||||
const menu = new Menu();
|
||||
|
||||
menu.append(
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('newFolder'))
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('newFolder')),
|
||||
);
|
||||
|
||||
menu.popup({ window: bridge().window() });
|
||||
@@ -314,13 +314,13 @@ const SidebarComponent = (props: Props) => {
|
||||
|
||||
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
|
||||
menu.append(
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('newFolder', itemId))
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('newFolder', itemId)),
|
||||
);
|
||||
}
|
||||
|
||||
if (itemType === BaseModel.TYPE_FOLDER) {
|
||||
menu.append(
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('deleteFolder', itemId))
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('deleteFolder', itemId)),
|
||||
);
|
||||
} else {
|
||||
menu.append(
|
||||
@@ -342,7 +342,7 @@ const SidebarComponent = (props: Props) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -364,7 +364,7 @@ const SidebarComponent = (props: Props) => {
|
||||
click: async () => {
|
||||
await InteropServiceHelper.export(props.dispatch, module, { sourceFolderIds: [itemId], plugins: pluginsRef.current });
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -386,7 +386,7 @@ const SidebarComponent = (props: Props) => {
|
||||
new MenuItem({
|
||||
label: _('Export'),
|
||||
submenu: exportMenu,
|
||||
})
|
||||
}),
|
||||
);
|
||||
if (Setting.value('notes.perFolderSortOrderEnabled')) {
|
||||
menu.append(new MenuItem({
|
||||
@@ -404,13 +404,13 @@ const SidebarComponent = (props: Props) => {
|
||||
click: () => {
|
||||
clipboard.writeText(getFolderCallbackUrl(itemId));
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (itemType === BaseModel.TYPE_TAG) {
|
||||
menu.append(new MenuItem(
|
||||
menuUtils.commandToStatefulMenuItem('renameTag', itemId)
|
||||
menuUtils.commandToStatefulMenuItem('renameTag', itemId),
|
||||
));
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
@@ -418,7 +418,7 @@ const SidebarComponent = (props: Props) => {
|
||||
click: () => {
|
||||
clipboard.writeText(getTagCallbackUrl(itemId));
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -431,7 +431,7 @@ const SidebarComponent = (props: Props) => {
|
||||
itemType === ModelType.Folder && location === MenuItemLocation.FolderContextMenu
|
||||
) {
|
||||
menu.append(
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem(view.commandName, itemId))
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem(view.commandName, itemId)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -705,7 +705,7 @@ const SidebarComponent = (props: Props) => {
|
||||
onDrop: onFolderDrop_,
|
||||
['data-folder-id']: '',
|
||||
toggleblock: 1,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const foldersStyle = useMemo(() => {
|
||||
@@ -725,14 +725,14 @@ const SidebarComponent = (props: Props) => {
|
||||
style={foldersStyle}
|
||||
>
|
||||
{folderItems}
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
items.push(
|
||||
renderHeader('tagHeader', _('Tags'), 'icon-tags', null, null, {
|
||||
toggleblock: 1,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
if (props.tags.length) {
|
||||
@@ -743,7 +743,7 @@ const SidebarComponent = (props: Props) => {
|
||||
items.push(
|
||||
<div className="tags" key="tag_items" style={{ display: props.tagHeaderIsExpanded ? 'block' : 'none' }}>
|
||||
{tagItems}
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -765,7 +765,7 @@ const SidebarComponent = (props: Props) => {
|
||||
syncReportText.push(
|
||||
<StyledSyncReportText key={i}>
|
||||
{lines[i]}
|
||||
</StyledSyncReportText>
|
||||
</StyledSyncReportText>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -133,14 +133,14 @@ function StatusScreen(props: Props) {
|
||||
<li style={theme.textStyle} key={`item_${n}`}>
|
||||
<span>{text}</span>
|
||||
{retryLink}
|
||||
</li>
|
||||
</li>,
|
||||
);
|
||||
} else {
|
||||
itemsHtml.push(
|
||||
<div style={theme.textStyle} key={`item_${n}`}>
|
||||
<span>{text}</span>
|
||||
{retryLink}
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import themeToCss from '@joplin/lib/services/style/themeToCss';
|
||||
import { addExtraStyles, themeById } from '@joplin/lib/theme';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
|
||||
interface Props {
|
||||
themeId: any;
|
||||
@@ -21,7 +21,7 @@ export default function(props: Props): any {
|
||||
const [styleSheetContent, setStyleSheetContent] = useState('');
|
||||
|
||||
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||
const theme = addExtraStyles(themeById(props.themeId));
|
||||
const theme = themeStyle(props.themeId);
|
||||
const themeCss = themeToCss(theme);
|
||||
if (event.cancelled) return;
|
||||
setStyleSheetContent(themeCss);
|
||||
|
||||
@@ -112,7 +112,7 @@ markJsUtils.markKeyword = (mark, keyword, stringUtils, extraOptions = null) => {
|
||||
return true;
|
||||
},
|
||||
...extraOptions,
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ const style = createSelector(
|
||||
output.buttonLabelSelected = { ...output.buttonLabel, color: theme.color };
|
||||
|
||||
return output;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
module.exports = style;
|
||||
|
||||
@@ -7,19 +7,19 @@ import InteropServiceHelper from '../../InteropServiceHelper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
|
||||
import { getNoteCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
|
||||
import bridge from '../../services/bridge';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
const { clipboard } = require('electron');
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
|
||||
interface ContextMenuProps {
|
||||
notes: any[];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
dispatch: Function;
|
||||
dispatch: Dispatch;
|
||||
watchedNoteFiles: string[];
|
||||
plugins: PluginStates;
|
||||
inConflictFolder: boolean;
|
||||
@@ -45,27 +45,27 @@ export default class NoteListUtils {
|
||||
|
||||
if (!hasEncrypted) {
|
||||
menu.append(
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('setTags', noteIds))
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('setTags', noteIds) as any),
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('moveToFolder', noteIds))
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('moveToFolder', noteIds) as any),
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('duplicateNote', noteIds))
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('duplicateNote', noteIds) as any),
|
||||
);
|
||||
|
||||
if (singleNoteId) {
|
||||
const cmd = props.watchedNoteFiles.includes(singleNoteId) ? 'stopExternalEditing' : 'startExternalEditing';
|
||||
menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem(cmd, singleNoteId)));
|
||||
menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem(cmd, singleNoteId) as any));
|
||||
}
|
||||
|
||||
if (noteIds.length <= 1) {
|
||||
menu.append(
|
||||
new MenuItem(
|
||||
menuUtils.commandToStatefulMenuItem('toggleNoteType', noteIds)
|
||||
)
|
||||
menuUtils.commandToStatefulMenuItem('toggleNoteType', noteIds) as any,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
const switchNoteType = async (noteIds: string[], type: string) => {
|
||||
@@ -84,7 +84,7 @@ export default class NoteListUtils {
|
||||
click: async () => {
|
||||
await switchNoteType(noteIds, 'note');
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
menu.append(
|
||||
@@ -93,7 +93,7 @@ export default class NoteListUtils {
|
||||
click: async () => {
|
||||
await switchNoteType(noteIds, 'todo');
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ export default class NoteListUtils {
|
||||
}
|
||||
clipboard.writeText(links.join(' '));
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
if (noteIds.length === 1) {
|
||||
@@ -118,15 +118,15 @@ export default class NoteListUtils {
|
||||
click: () => {
|
||||
clipboard.writeText(getNoteCallbackUrl(noteIds[0]));
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if ([9, 10].includes(Setting.value('sync.target'))) {
|
||||
menu.append(
|
||||
new MenuItem(
|
||||
menuUtils.commandToStatefulMenuItem('showShareNoteDialog', noteIds.slice())
|
||||
)
|
||||
menuUtils.commandToStatefulMenuItem('showShareNoteDialog', noteIds.slice()) as any,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -150,14 +150,14 @@ export default class NoteListUtils {
|
||||
customCss: props.customCss,
|
||||
});
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
exportMenu.append(
|
||||
new MenuItem(
|
||||
menuUtils.commandToStatefulMenuItem('exportPdf', noteIds)
|
||||
)
|
||||
menuUtils.commandToStatefulMenuItem('exportPdf', noteIds) as any,
|
||||
),
|
||||
);
|
||||
|
||||
const exportMenuItem = new MenuItem({ label: _('Export'), submenu: exportMenu });
|
||||
@@ -167,8 +167,8 @@ export default class NoteListUtils {
|
||||
|
||||
menu.append(
|
||||
new MenuItem(
|
||||
menuUtils.commandToStatefulMenuItem('deleteNote', noteIds)
|
||||
)
|
||||
menuUtils.commandToStatefulMenuItem('deleteNote', noteIds) as any,
|
||||
),
|
||||
);
|
||||
|
||||
const pluginViewInfos = pluginUtils.viewInfosByType(props.plugins, 'menuItem');
|
||||
@@ -179,7 +179,7 @@ export default class NoteListUtils {
|
||||
|
||||
if (cmdService.isEnabled(info.view.commandName)) {
|
||||
menu.append(
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem(info.view.commandName, noteIds))
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem(info.view.commandName, noteIds) as any),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ const tasks = {
|
||||
fn: async () => {
|
||||
await compileSass(
|
||||
`${__dirname}/style.scss`,
|
||||
`${__dirname}/style.min.css`
|
||||
`${__dirname}/style.min.css`,
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
/* eslint-disable jest/require-top-level-describe */
|
||||
|
||||
const { default: Logger, TargetType } = require('@joplin/utils/Logger');
|
||||
const initLib = require('@joplin/lib/initLib').default;
|
||||
|
||||
// TODO: Some libraries required by test-utils.js seem to fail to import with the
|
||||
// jsdom environment.
|
||||
//
|
||||
// Thus, require('@joplin/lib/testing/test-utils.js') fails and some setup must be
|
||||
// copied.
|
||||
|
||||
const logger = new Logger();
|
||||
logger.addTarget(TargetType.Console);
|
||||
logger.setLevel(Logger.LEVEL_WARN);
|
||||
Logger.initializeGlobalLogger(logger);
|
||||
initLib(logger);
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node');
|
||||
const sqlite3 = require('sqlite3');
|
||||
const SyncTargetNone = require('@joplin/lib/SyncTargetNone').default;
|
||||
|
||||
// Mock the S3 sync target -- the @aws-s3 libraries depend on an old version
|
||||
// of uuid that doesn't work with jest without additional configuration.
|
||||
jest.doMock('@joplin/lib/SyncTargetAmazonS3', () => {
|
||||
return SyncTargetNone;
|
||||
});
|
||||
|
||||
// @electron/remote requires electron to be running. Mock it.
|
||||
jest.mock('@electron/remote', () => {
|
||||
@@ -25,3 +20,18 @@ jest.mock('@electron/remote', () => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Import after mocking problematic libraries
|
||||
const { afterEachCleanUp, afterAllCleanUp } = require('@joplin/lib/testing/test-utils.js');
|
||||
|
||||
|
||||
shimInit({ nodeSqlite: sqlite3 });
|
||||
|
||||
afterEach(async () => {
|
||||
await afterEachCleanUp();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllCleanUp();
|
||||
});
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"@joplin/lib": "~2.12",
|
||||
"@joplin/renderer": "~2.12",
|
||||
"@joplin/utils": "~2.12",
|
||||
"@types/mustache": "4.2.2",
|
||||
"async-mutex": "0.4.0",
|
||||
"codemirror": "5.65.9",
|
||||
"color": "3.2.1",
|
||||
@@ -154,6 +155,7 @@
|
||||
"mark.js": "8.11.1",
|
||||
"md5": "2.3.0",
|
||||
"moment": "2.29.4",
|
||||
"mustache": "4.2.0",
|
||||
"node-fetch": "2.6.7",
|
||||
"node-notifier": "10.0.1",
|
||||
"node-rsa": "1.1.1",
|
||||
@@ -162,7 +164,7 @@
|
||||
"react": "18.2.0",
|
||||
"react-datetime": "3.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-redux": "8.1.1",
|
||||
"react-redux": "8.1.2",
|
||||
"react-select": "5.7.4",
|
||||
"react-toggle-button": "2.2.0",
|
||||
"react-tooltip": "4.5.1",
|
||||
|
||||
@@ -108,7 +108,7 @@ function UserWebview(props: Props, ref: any) {
|
||||
frameWindow(),
|
||||
isReady,
|
||||
postMessage,
|
||||
props.html
|
||||
props.html,
|
||||
);
|
||||
|
||||
const contentSize = useContentSize(
|
||||
@@ -117,14 +117,14 @@ function UserWebview(props: Props, ref: any) {
|
||||
minWidth,
|
||||
minHeight,
|
||||
props.fitToContent,
|
||||
isReady
|
||||
isReady,
|
||||
);
|
||||
|
||||
useSubmitHandler(
|
||||
frameWindow(),
|
||||
props.onSubmit,
|
||||
props.onDismiss,
|
||||
htmlHash
|
||||
htmlHash,
|
||||
);
|
||||
|
||||
useWebviewToPluginMessages(
|
||||
@@ -132,14 +132,14 @@ function UserWebview(props: Props, ref: any) {
|
||||
isReady,
|
||||
props.pluginId,
|
||||
props.viewId,
|
||||
postMessage
|
||||
postMessage,
|
||||
);
|
||||
|
||||
useScriptLoader(
|
||||
postMessage,
|
||||
isReady,
|
||||
props.scripts,
|
||||
cssFilePath
|
||||
cssFilePath,
|
||||
);
|
||||
|
||||
return <StyledFrame
|
||||
|
||||
@@ -5,4 +5,5 @@
|
||||
@use 'gui/JoplinCloudConfigScreen.scss' as joplin-cloud-config-screen;
|
||||
@use 'gui/Dropdown/style.scss' as dropdown-control;
|
||||
@use 'gui/ShareFolderDialog/style.scss' as share-folder-dialog;
|
||||
@use 'gui/NoteList/style.scss' as note-list;
|
||||
@use 'main.scss' as main;
|
||||
@@ -110,11 +110,11 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097717
|
||||
versionName "2.12.0"
|
||||
// ndk {
|
||||
// abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
// }
|
||||
versionCode 2097719
|
||||
versionName "2.12.2"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
||||
// https://github.com/react-native-community/react-native-camera/issues/2138
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
|
||||
@@ -32,7 +32,7 @@ const FolderPicker: FunctionComponent<FolderPickerProps> = ({
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
const addFolderChildren = (
|
||||
folders: FolderEntityWithChildren[], pickerItems: DropdownListItem[], indent: number
|
||||
folders: FolderEntityWithChildren[], pickerItems: DropdownListItem[], indent: number,
|
||||
) => {
|
||||
folders.sort((a, b) => {
|
||||
const aTitle = a && a.title ? a.title : '';
|
||||
|
||||
@@ -43,12 +43,12 @@ export default function NoteBodyViewer(props: Props) {
|
||||
props.highlightedKeywords,
|
||||
props.noteResources,
|
||||
props.paddingBottom,
|
||||
props.noteHash
|
||||
props.noteHash,
|
||||
);
|
||||
|
||||
const onResourceLongPress = useOnResourceLongPress(
|
||||
props.onJoplinLinkClick,
|
||||
dialogBoxRef
|
||||
dialogBoxRef,
|
||||
);
|
||||
|
||||
const onMessage = useOnMessage(
|
||||
@@ -56,7 +56,7 @@ export default function NoteBodyViewer(props: Props) {
|
||||
props.noteBody,
|
||||
props.onMarkForDownload,
|
||||
props.onJoplinLinkClick,
|
||||
onResourceLongPress
|
||||
onResourceLongPress,
|
||||
);
|
||||
|
||||
const onLoadEnd = useCallback(() => {
|
||||
|
||||
@@ -125,7 +125,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
noteMarkupLanguage,
|
||||
bodyToRender,
|
||||
rendererTheme,
|
||||
mdOptions
|
||||
mdOptions,
|
||||
);
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
@@ -51,7 +51,7 @@ interface CodeMirrorResult extends CodeMirrorControl {
|
||||
}
|
||||
|
||||
export function initCodeMirror(
|
||||
parentElement: any, initialText: string, settings: EditorSettings
|
||||
parentElement: any, initialText: string, settings: EditorSettings,
|
||||
): CodeMirrorResult {
|
||||
logMessage('Initializing CodeMirror...');
|
||||
const theme = settings.themeData;
|
||||
|
||||
@@ -82,7 +82,7 @@ const computeDecorations = (view: EditorView) => {
|
||||
for (const { from, to } of view.visibleRanges) {
|
||||
ensureSyntaxTree(
|
||||
view.state,
|
||||
to
|
||||
to,
|
||||
)?.iterate({
|
||||
from, to,
|
||||
enter: node => {
|
||||
|
||||
@@ -11,35 +11,35 @@ describe('markdownCommands.bulletedVsChecklist', () => {
|
||||
|
||||
it('should remove a checklist following a bulleted list without modifying the bulleted list', async () => {
|
||||
const editor = await createEditor(
|
||||
initialDocText, EditorSelection.cursor(bulletedListPart.length + 5), expectedTags
|
||||
initialDocText, EditorSelection.cursor(bulletedListPart.length + 5), expectedTags,
|
||||
);
|
||||
|
||||
toggleList(ListType.CheckList)(editor);
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
`${bulletedListPart}\n\nThis is a checklist\nwith multiple items.\n☑`
|
||||
`${bulletedListPart}\n\nThis is a checklist\nwith multiple items.\n☑`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove an unordered list following a checklist without modifying the checklist', async () => {
|
||||
const editor = await createEditor(
|
||||
initialDocText, EditorSelection.cursor(bulletedListPart.length - 5), expectedTags
|
||||
initialDocText, EditorSelection.cursor(bulletedListPart.length - 5), expectedTags,
|
||||
);
|
||||
|
||||
toggleList(ListType.UnorderedList)(editor);
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
`Test\nThis is a test.\n3\n4\n5\n\n${checklistPart}`
|
||||
`Test\nThis is a test.\n3\n4\n5\n\n${checklistPart}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should replace a selection of unordered and task lists with a correctly-numbered list', async () => {
|
||||
const editor = await createEditor(
|
||||
initialDocText, EditorSelection.range(0, initialDocText.length), expectedTags
|
||||
initialDocText, EditorSelection.range(0, initialDocText.length), expectedTags,
|
||||
);
|
||||
|
||||
toggleList(ListType.OrderedList)(editor);
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
'1. Test\n2. This is a test.\n3. 3\n4. 4\n5. 5'
|
||||
+ '\n\n6. This is a checklist\n7. with multiple items.\n8. ☑'
|
||||
+ '\n\n6. This is a checklist\n7. with multiple items.\n8. ☑',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('markdownCommands', () => {
|
||||
it('should bold/italicize everything selected', async () => {
|
||||
const initialDocText = 'Testing...';
|
||||
const editor = await createEditor(
|
||||
initialDocText, EditorSelection.range(0, initialDocText.length), []
|
||||
initialDocText, EditorSelection.range(0, initialDocText.length), [],
|
||||
);
|
||||
|
||||
toggleBolded(editor);
|
||||
@@ -36,7 +36,7 @@ describe('markdownCommands', () => {
|
||||
it('for a cursor, bolding, then italicizing, should produce a bold-italic region', async () => {
|
||||
const initialDocText = '';
|
||||
const editor = await createEditor(
|
||||
initialDocText, EditorSelection.cursor(0), []
|
||||
initialDocText, EditorSelection.cursor(0), [],
|
||||
);
|
||||
|
||||
toggleBolded(editor);
|
||||
@@ -110,14 +110,14 @@ describe('markdownCommands', () => {
|
||||
const editor = await createEditor(
|
||||
initialDocText,
|
||||
EditorSelection.cursor('Testing...\n\n> This'.length),
|
||||
['Blockquote']
|
||||
['Blockquote'],
|
||||
);
|
||||
|
||||
toggleHeaderLevel(1)(editor);
|
||||
|
||||
const mainSel = editor.state.selection.main;
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
'Testing...\n\n> # This is a test.\n> ...a test'
|
||||
'Testing...\n\n> # This is a test.\n> ...a test',
|
||||
);
|
||||
expect(mainSel.empty).toBe(true);
|
||||
expect(mainSel.from).toBe('Testing...\n\n> # This is a test.'.length);
|
||||
@@ -125,7 +125,7 @@ describe('markdownCommands', () => {
|
||||
toggleHeaderLevel(3)(editor);
|
||||
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
'Testing...\n\n> ### This is a test.\n> ...a test'
|
||||
'Testing...\n\n> ### This is a test.\n> ...a test',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -135,9 +135,9 @@ describe('markdownCommands', () => {
|
||||
initialDocText,
|
||||
EditorSelection.range(
|
||||
'Testing...\n\n> This'.length,
|
||||
'Testing...\n\n> This is a test.\n> y = mx + b'.length
|
||||
'Testing...\n\n> This is a test.\n> y = mx + b'.length,
|
||||
),
|
||||
['Blockquote']
|
||||
['Blockquote'],
|
||||
);
|
||||
|
||||
toggleMath(editor);
|
||||
@@ -145,7 +145,7 @@ describe('markdownCommands', () => {
|
||||
// Toggling math should surround the content in '$$'s
|
||||
const mainSel = editor.state.selection.main;
|
||||
expect(editor.state.doc.toString()).toEqual(
|
||||
'Testing...\n\n> $$\n> This is a test.\n> y = mx + b\n> $$\n> ...a test'
|
||||
'Testing...\n\n> $$\n> This is a test.\n> y = mx + b\n> $$\n> ...a test',
|
||||
);
|
||||
expect(mainSel.from).toBe('Testing...\n\n'.length);
|
||||
expect(mainSel.to).toBe('Testing...\n\n> $$\n> This is a test.\n> y = mx + b\n> $$'.length);
|
||||
@@ -157,7 +157,7 @@ describe('markdownCommands', () => {
|
||||
const editor = await createEditor(
|
||||
initialDocText,
|
||||
EditorSelection.cursor('Testing...\n\n> $$\n> This is'.length),
|
||||
['Blockquote', blockMathTagName]
|
||||
['Blockquote', blockMathTagName],
|
||||
);
|
||||
|
||||
// Toggling math should remove the '$$'s
|
||||
@@ -174,12 +174,12 @@ describe('markdownCommands', () => {
|
||||
|
||||
updateLink('bar', 'https://example.com/')(editor);
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
'[bar](https://example.com/)'
|
||||
'[bar](https://example.com/)',
|
||||
);
|
||||
|
||||
updateLink('', 'https://example.com/')(editor);
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
'https://example.com/'
|
||||
'https://example.com/',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -225,7 +225,7 @@ describe('markdownCommands', () => {
|
||||
toggleMath(editor);
|
||||
editor.dispatch(editor.state.replaceSelection('f(x) = ...'));
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
'> Testing...> \n> \n> $$\n> f(x) = ...\n> $$'
|
||||
'> Testing...> \n> \n> $$\n> f(x) = ...\n> $$',
|
||||
);
|
||||
|
||||
// If we toggle math again, everything from the start of the line with the first
|
||||
|
||||
@@ -12,12 +12,12 @@ describe('markdownCommands.toggleList', () => {
|
||||
const editor = await createEditor(
|
||||
initialDocText,
|
||||
EditorSelection.cursor(5),
|
||||
['BulletList', 'InlineCode']
|
||||
['BulletList', 'InlineCode'],
|
||||
);
|
||||
|
||||
toggleList(ListType.UnorderedList)(editor);
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
'testing\nthis is a `test`\n'
|
||||
'testing\nthis is a `test`\n',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -26,12 +26,12 @@ describe('markdownCommands.toggleList', () => {
|
||||
const editor = await createEditor(
|
||||
initialDocText,
|
||||
EditorSelection.cursor('Testing...\nThis is a'.length),
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
toggleList(ListType.OrderedList)(editor);
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
'Testing...\n1. This is a test\nof list toggling...'
|
||||
'Testing...\n1. This is a test\nof list toggling...',
|
||||
);
|
||||
|
||||
editor.setState(EditorState.create({
|
||||
@@ -41,7 +41,7 @@ describe('markdownCommands.toggleList', () => {
|
||||
|
||||
toggleList(ListType.OrderedList)(editor);
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
'1. Testing...\n2. This is a test\n3. of list toggling...'
|
||||
'1. Testing...\n2. This is a test\n3. of list toggling...',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -51,12 +51,12 @@ describe('markdownCommands.toggleList', () => {
|
||||
const editor = await createEditor(
|
||||
unorderedListText,
|
||||
EditorSelection.cursor(unorderedListText.length),
|
||||
['BulletList']
|
||||
['BulletList'],
|
||||
);
|
||||
|
||||
toggleList(ListType.OrderedList)(editor);
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
'1. 1\n2. 2\n3. 3\n4. 4\n5. 5\n6. 6\n7. 7'
|
||||
'1. 1\n2. 2\n3. 3\n4. 4\n5. 5\n6. 6\n7. 7',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -154,12 +154,12 @@ describe('markdownCommands.toggleList', () => {
|
||||
const editor = await createEditor(
|
||||
initialDocText,
|
||||
EditorSelection.cursor(0),
|
||||
['OrderedList', 'BulletList']
|
||||
['OrderedList', 'BulletList'],
|
||||
);
|
||||
|
||||
toggleList(ListType.CheckList)(editor);
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
'- [ ] Foo\n- [ ] Bar\n- [ ] Baz\n\t- Test\n\t- of\n\t- sublists\n- [ ] Foo'
|
||||
'- [ ] Foo\n- [ ] Bar\n- [ ] Baz\n\t- Test\n\t- of\n\t- sublists\n- [ ] Foo',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -169,7 +169,7 @@ describe('markdownCommands.toggleList', () => {
|
||||
const editor = await createEditor(
|
||||
initialDocText,
|
||||
EditorSelection.cursor(initialDocText.length),
|
||||
['OrderedList']
|
||||
['OrderedList'],
|
||||
);
|
||||
|
||||
increaseIndent(editor);
|
||||
@@ -177,12 +177,12 @@ describe('markdownCommands.toggleList', () => {
|
||||
|
||||
toggleList(ListType.CheckList)(editor);
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
'1. This\n2. is\n\t- [ ] '
|
||||
'1. This\n2. is\n\t- [ ] ',
|
||||
);
|
||||
|
||||
editor.dispatch(editor.state.replaceSelection('a test.'));
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
'1. This\n2. is\n\t- [ ] a test.'
|
||||
'1. This\n2. is\n\t- [ ] a test.',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -191,12 +191,12 @@ describe('markdownCommands.toggleList', () => {
|
||||
const initialDocText = `${preSubListText}> \t* a\n> \t* test\n> * of list toggling`;
|
||||
const editor = await createEditor(
|
||||
initialDocText, EditorSelection.cursor(preSubListText.length + 3),
|
||||
['BlockQuote', 'BulletList']
|
||||
['BlockQuote', 'BulletList'],
|
||||
);
|
||||
|
||||
toggleList(ListType.OrderedList)(editor);
|
||||
expect(editor.state.doc.toString()).toBe(
|
||||
'> # List test\n> * This\n> * is\n> \t1. a\n> \t2. test\n> * of list toggling'
|
||||
'> # List test\n> * This\n> * is\n> \t1. a\n> \t2. test\n> * of list toggling',
|
||||
);
|
||||
expect(editor.state.selection.main.from).toBe(preSubListText.length);
|
||||
});
|
||||
|
||||
@@ -250,7 +250,7 @@ export const toggleList = (listType: ListType): Command => {
|
||||
|
||||
sel = EditorSelection.range(
|
||||
doc.line(newFromLineNo).from,
|
||||
doc.line(newToLineNo).to
|
||||
doc.line(newToLineNo).to,
|
||||
);
|
||||
computeSelectionProps();
|
||||
}
|
||||
@@ -299,7 +299,7 @@ export const toggleList = (listType: ListType): Command => {
|
||||
const containerMatch = lineContent.match(containerRegex);
|
||||
if (!containerMatch) {
|
||||
throw new Error(
|
||||
'Assertion failed: container regex does not match line content.'
|
||||
'Assertion failed: container regex does not match line content.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -336,7 +336,7 @@ export const toggleList = (listType: ListType): Command => {
|
||||
} else {
|
||||
sel = EditorSelection.range(
|
||||
sel.from,
|
||||
sel.to + charsAdded
|
||||
sel.to + charsAdded,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -374,10 +374,10 @@ export const toggleHeaderLevel = (level: number): Command => {
|
||||
`${level - 1 >= 1 ? `(?:^[#]{1,${level - 1}}\\s)|` : ''
|
||||
|
||||
// Check all number of #s higher than [level]
|
||||
}(?:^[#]{${level + 1},}\\s)`
|
||||
}(?:^[#]{${level + 1},}\\s)`,
|
||||
),
|
||||
'',
|
||||
matchEmpty
|
||||
matchEmpty,
|
||||
);
|
||||
view.dispatch(changes);
|
||||
|
||||
@@ -387,7 +387,7 @@ export const toggleHeaderLevel = (level: number): Command => {
|
||||
// We want exactly [level] '#' characters.
|
||||
new RegExp(`^[#]{${level}} `),
|
||||
`${headerStr} `,
|
||||
matchEmpty
|
||||
matchEmpty,
|
||||
);
|
||||
view.dispatch(changes);
|
||||
|
||||
@@ -408,7 +408,7 @@ export const increaseIndent: Command = (view: EditorView): boolean => {
|
||||
matchNothing,
|
||||
// ...and thus always add indentUnit.
|
||||
indentUnit,
|
||||
matchEmpty
|
||||
matchEmpty,
|
||||
);
|
||||
view.dispatch(changes);
|
||||
|
||||
@@ -429,7 +429,7 @@ export const decreaseIndent: Command = (view: EditorView): boolean => {
|
||||
new RegExp(`^(?:[\\t]|[ ]{1,${getIndentUnit(view.state)}})`),
|
||||
// Don't add new text
|
||||
'',
|
||||
matchEmpty
|
||||
matchEmpty,
|
||||
);
|
||||
|
||||
view.dispatch(changes);
|
||||
|
||||
@@ -143,7 +143,7 @@ const BlockMathConfig: MarkdownConfig = {
|
||||
let stop;
|
||||
|
||||
let endMatch = mathBlockEndRegex.exec(
|
||||
line.text.substring(mathStartMatch[0].length)
|
||||
line.text.substring(mathStartMatch[0].length),
|
||||
);
|
||||
|
||||
// If the math region ends immediately (on the same line),
|
||||
@@ -183,7 +183,7 @@ const BlockMathConfig: MarkdownConfig = {
|
||||
Math.min(lineEnd, stop + delimLen),
|
||||
|
||||
// The child of the container element should be the content element
|
||||
[contentElem]
|
||||
[contentElem],
|
||||
);
|
||||
cx.addElement(containerElement);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user