1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-10 22:11:50 +02:00

Merge branch 'dev' into sharing_bug_2

This commit is contained in:
Laurent Cozic
2025-07-11 17:28:09 +01:00
68 changed files with 2287 additions and 409 deletions

View File

@@ -937,7 +937,6 @@ packages/default-plugins/commands/editPatch.js
packages/default-plugins/utils/getCurrentCommitHash.js
packages/default-plugins/utils/getPathToPatchFileFor.js
packages/default-plugins/utils/readRepositoryJson.js
packages/default-plugins/utils/waitForCliInput.js
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5BuiltInOptions.js
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.js
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.js
@@ -1649,6 +1648,18 @@ packages/tools/checkIgnoredFiles.js
packages/tools/checkLibPaths.test.js
packages/tools/checkLibPaths.js
packages/tools/convertThemesToCss.js
packages/tools/fuzzer/ActionTracker.js
packages/tools/fuzzer/Client.js
packages/tools/fuzzer/ClientPool.js
packages/tools/fuzzer/Server.js
packages/tools/fuzzer/constants.js
packages/tools/fuzzer/sync-fuzzer.js
packages/tools/fuzzer/types.js
packages/tools/fuzzer/utils/SeededRandom.js
packages/tools/fuzzer/utils/getNumberProperty.js
packages/tools/fuzzer/utils/getProperty.js
packages/tools/fuzzer/utils/getStringProperty.js
packages/tools/fuzzer/utils/retryWithCount.js
packages/tools/generate-database-types.js
packages/tools/generate-images.js
packages/tools/git-changelog.test.js

View File

@@ -23,6 +23,7 @@ module.exports = {
'FileSystemCreateWritableOptions': 'readonly',
'FileSystemHandle': 'readonly',
'IDBTransactionMode': 'readonly',
'BigInt': 'readonly',
'globalThis': 'readonly',
// ServiceWorker

13
.gitignore vendored
View File

@@ -912,7 +912,6 @@ packages/default-plugins/commands/editPatch.js
packages/default-plugins/utils/getCurrentCommitHash.js
packages/default-plugins/utils/getPathToPatchFileFor.js
packages/default-plugins/utils/readRepositoryJson.js
packages/default-plugins/utils/waitForCliInput.js
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5BuiltInOptions.js
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.js
packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.js
@@ -1624,6 +1623,18 @@ packages/tools/checkIgnoredFiles.js
packages/tools/checkLibPaths.test.js
packages/tools/checkLibPaths.js
packages/tools/convertThemesToCss.js
packages/tools/fuzzer/ActionTracker.js
packages/tools/fuzzer/Client.js
packages/tools/fuzzer/ClientPool.js
packages/tools/fuzzer/Server.js
packages/tools/fuzzer/constants.js
packages/tools/fuzzer/sync-fuzzer.js
packages/tools/fuzzer/types.js
packages/tools/fuzzer/utils/SeededRandom.js
packages/tools/fuzzer/utils/getNumberProperty.js
packages/tools/fuzzer/utils/getProperty.js
packages/tools/fuzzer/utils/getStringProperty.js
packages/tools/fuzzer/utils/retryWithCount.js
packages/tools/generate-database-types.js
packages/tools/generate-images.js
packages/tools/git-changelog.test.js

View File

@@ -1300,4 +1300,9 @@ footer .bottom-links-row p {
:lang(zh-cn) #plans-section .faq {
display: none;
}
.cfa-button {
margin-top: 10px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

@@ -1,24 +1,28 @@
<div class="col-12 col-lg-4 account-type-{{priceMonthly.accountType}}">
<div class="col-12 col-lg-4 account-type-{{priceMonthly.accountType}} hosting-type-{{hostingType}}">
<div class="price-container {{#featured}}price-container-blue{{/featured}}">
<div class="price-row">
<div class="plan-type">
<img src="{{imageBaseUrl}}/{{iconName}}.png"/>&nbsp;{{title}}
<div class="price-row">
<div class="plan-type">
<img src="{{imageBaseUrl}}/{{iconName}}.png"/>&nbsp;{{title}}
</div>
{{#priceMonthly.formattedMonthlyAmount}}
<div class="plan-price plan-price-monthly">
{{priceMonthly.formattedMonthlyAmount}}<sub class="per-month">&nbsp;<span translate>/month</span>{{#footnote}} (*){{/footnote}}</sub>
</div>
<div class="plan-price plan-price-yearly">
{{priceYearly.formattedMonthlyAmount}}<sub class="per-month">&nbsp;<span translate>/month</span>{{#footnote}} (*){{/footnote}}</sub>
</div>
{{/priceMonthly.formattedMonthlyAmount}}
</div>
<div class="plan-price plan-price-monthly">
{{priceMonthly.formattedMonthlyAmount}}<sub class="per-month">&nbsp;<span translate>/month</span>{{#footnote}} (*){{/footnote}}</sub>
{{#priceYearly.formattedMonthlyAmount}}
<div class="plan-price-yearly-per-year">
<div>
({{priceYearly.formattedAmount}}<sub class="per-year">&nbsp;<span translate>/year</span></sub>)
</div>
</div>
<div class="plan-price plan-price-yearly">
{{priceYearly.formattedMonthlyAmount}}<sub class="per-month">&nbsp;<span translate>/month</span>{{#footnote}} (*){{/footnote}}</sub>
</div>
</div>
<div class="plan-price-yearly-per-year">
<div>
({{priceYearly.formattedAmount}}<sub class="per-year">&nbsp;<span translate>/year</span></sub>)
</div>
</div>
{{/priceYearly.formattedMonthlyAmount}}
{{#featureLabelsOn}}
<p><i class="fas fa-check feature feature-on"></i>{{.}}</p>
@@ -29,7 +33,11 @@
{{/featureLabelsOff}}
<p class="text-center subscribe-wrapper">
<a id="subscribeButton-{{name}}" href="{{cfaUrl}}" class="button-link btn-white subscribeButton">{{cfaLabel}}</a>
<a id="subscribeButton-{{name}}" href="{{cfaUrl}}" class="button-link btn-white subscribeButton cfa-button">{{cfaLabel}}</a>
{{#learnMoreUrl}}
<a id="learnMore-{{name}}" href="{{learnMoreUrl}}" class="button-link btn-white learnMoreButton cfa-button">Learn more</a>
{{/learnMoreUrl}}
</p>
{{#footnote}}<sub>(*) {{.}}</sub>{{/footnote}}

View File

@@ -1,23 +1,91 @@
<div id="plans-section" class="env-{{env}}">
<style>
.toggle-container {
display: flex;
border: 2px solid black;
border-radius: 100px;
overflow: hidden;
cursor: pointer;
margin-top: 20px;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.toggle-option {
flex: 1;
padding: 10px 20px;
text-align: center;
transition: background 0.3s, color 0.3s;
user-select: none;
white-space: nowrap;
}
.active {
background: black;
color: white;
}
.inactive {
background: white;
color: black;
}
@media (max-width: 480px) {
.toggle-container {
flex-direction: column;
width: 100%;
border-radius: 10px;
}
}
</style>
<div class="container">
<div class="row">
<div class="col-12 title-box">
<h1 translate class="text-center">
Joplin Cloud <span class="frame-bg frame-bg-yellow">plans</span>
Our synchronisation and sharing <span class="frame-bg frame-bg-yellow">solutions</span>
</h1>
<p translate class="text-center sub-title">
<a href="https://joplincloud.com">Joplin Cloud</a> allows you to synchronise your notes across devices. It also lets you publish notes, and collaborate on notebooks with your friends, family or colleagues.
Synchronise and share your notes with our range of plans.
</p>
</div>
</div>
<div class="toggle-container" id="toggle">
<div class="toggle-option active toggle-button-managed">Managed hosting</div>
<div class="toggle-option inactive toggle-button-self">Self-hosting</div>
</div>
<noscript>
<div class="alert alert-danger alert-env-dev" role="alert" style='text-align: center; margin-top: 10px;'>
To use this page please enable JavaScript!
</div>
</noscript>
<div style="display: flex; justify-content: center; margin-top: 1.2em">
<div class="row hosting-type-managed">
<div class="col-12 title-box">
<h1 translate class="text-center">
Joplin Cloud
</h1>
<p translate class="text-center sub-title">
<a href="https://joplincloud.com">Joplin Cloud</a> allows you to synchronise your notes across devices. It also lets you publish notes, and collaborate on notebooks with your friends, family or colleagues.
</p>
</div>
</div>
<div class="row hosting-type-self">
<div class="col-12 title-box">
<h1 translate class="text-center">
Joplin Server Business
</h1>
<p translate class="text-center sub-title">
Joplin Server Business is a synchronisation server that you can install on your own infrastructure, so that your data remains private and secure within your business.
</p>
</div>
</div>
<div style="display: flex; justify-content: center; margin-top: 1.2em" class="hosting-type-managed">
<div class="form-check form-check-inline">
<input id="pay-monthly-radio" class="form-check-input" type="radio" name="pay-radio" checked value="monthly">
<label translate style="font-weight: bold" class="form-check-label" for="pay-monthly-radio">
@@ -46,7 +114,11 @@
{{> plan}}
{{/plans.teams}}
<p translate class="joplin-cloud-login-info">Already have a Joplin Cloud account? <a href="https://joplincloud.com">Login now</a></p>
{{#plans.joplinServerBusiness}}
{{> plan}}
{{/plans.joplinServerBusiness}}
<p translate class="joplin-cloud-login-info hosting-type-managed">Already have a Joplin Cloud account? <a href="https://joplincloud.com">Login now</a></p>
</div>
<div class="row">
@@ -148,4 +220,30 @@
});
});
</script>
<script>
const setHostingType = (type) => {
const other = type === 'managed' ? 'self' : 'managed';
$('.toggle-button-' + type).addClass('active');
$('.toggle-button-' + type).removeClass('inactive');
$('.toggle-button-' + other).addClass('inactive');
$('.toggle-button-' + other).removeClass('active');
$('.hosting-type-' + type).show();
$('.hosting-type-' + other).hide();
}
$('.toggle-button-managed').click((event) => {
event.preventDefault();
setHostingType('managed');
});
$('.toggle-button-self').click((event) => {
event.preventDefault();
setHostingType('self');
});
setHostingType('managed');
</script>
</div>

View File

@@ -31,7 +31,7 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
# Sponsors
<!-- SPONSORS-ORG -->
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&amp;mtm_kwd=joplinapp&amp;mtm_source=joplinapp-webseite&amp;mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://casinoreviews.net"><img title="Casino Reviews" width="256" src="https://joplinapp.org/images/sponsors/CasinoReviews.png"/></a> <a href="https://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="topagency"/></a> <a href="https://realgambling.ca/"><img title="RealGambling.ca" width="256" src="https://joplinapp.org/images/sponsors/RealGambling.png" alt="RealGambling.ca"/></a> <a href="https://essaypro.com/"><img title="write an essay online with EssayPro" width="256" src="https://joplinapp.org/images/sponsors/EssayPro.png" alt="write an essay online with EssayPro"/></a> <a href="https://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a> <a href="https://www.reddit.com/r/tiktokRise/"><img title="Tiktok Rise" width="256" src="https://joplinapp.org/images/sponsors/TiktokRise.jpg" alt="Tiktok Rise"/></a> <a href="https://essaywriter.pro"><img title="write my essay services by EssayWriter" width="256" src="https://joplinapp.org/images/sponsors/EssayWriterPro.png" alt="write my essay services by EssayWriter"/></a> <a href="https://essayservice.com"><img title="quick and reliable service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/EssayService.png" alt="quick and reliable service to write my paper for me"/></a> <a href="https://writepaper.com/"><img title="best service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/WritePaper.png" alt="best service to write my paper for me"/></a> <a href="https://paperwriter.com/"><img title="high-quality paper writing service PaperWriter" width="256" src="https://joplinapp.org/images/sponsors/PaperWriter.png" alt="high-quality paper writing service PaperWriter"/></a>
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&amp;mtm_kwd=joplinapp&amp;mtm_source=joplinapp-webseite&amp;mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="topagency"/></a> <a href="https://realgambling.ca/"><img title="RealGambling.ca" width="256" src="https://joplinapp.org/images/sponsors/RealGambling.png" alt="RealGambling.ca"/></a> <a href="https://essaypro.com/"><img title="write an essay online with EssayPro" width="256" src="https://joplinapp.org/images/sponsors/EssayPro.png" alt="write an essay online with EssayPro"/></a> <a href="https://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a> <a href="https://essaywriter.pro"><img title="write my essay services by EssayWriter" width="256" src="https://joplinapp.org/images/sponsors/EssayWriterPro.png" alt="write my essay services by EssayWriter"/></a> <a href="https://essayservice.com"><img title="quick and reliable service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/EssayService.png" alt="quick and reliable service to write my paper for me"/></a> <a href="https://writepaper.com/"><img title="best service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/WritePaper.png" alt="best service to write my paper for me"/></a> <a href="https://paperwriter.com/"><img title="high-quality paper writing service PaperWriter" width="256" src="https://joplinapp.org/images/sponsors/PaperWriter.png" alt="high-quality paper writing service PaperWriter"/></a> <a href="https://homeworkguy.org/someone-to-take-my-online-class"><img title="someone to take my online class" width="256" src="https://joplinapp.org/images/sponsors/HomeworkGuy.png" alt="someone to take my online class"/></a> <a href="https://www.bestetf.net/"><img title="BestETF" width="256" src="https://joplinapp.org/images/sponsors/BestEtf.png" alt="BestETF"/></a> <a href="https://freespinny.io/free-spins-no-deposit/"><img title="Freespinny.io Free Spins Bonus site" width="256" src="https://joplinapp.org/images/sponsors/Freespinny.png" alt="Freespinny.io Free Spins Bonus site"/></a>
<!-- SPONSORS-ORG -->
* * *

View File

@@ -38,6 +38,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",
"packageJsonLint": "node ./packages/tools/packageJsonLint.js",
"syncFuzzer": "node ./packages/tools/fuzzer/sync-fuzzer.js",
"postinstall": "husky && gulp build",
"postPreReleasesToForum": "node ./packages/tools/postPreReleasesToForum",
"publishAll": "git pull && yarn buildParallel && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
@@ -83,7 +84,7 @@
"gulp": "4.0.2",
"husky": "9.1.7",
"lerna": "3.22.1",
"lint-staged": "15.4.3",
"lint-staged": "15.5.0",
"madge": "8.0.0",
"npm-package-json-lint": "8.0.0",
"typescript": "5.4.5"

View File

@@ -419,6 +419,11 @@ class Application extends BaseApplication {
this.initRedux();
// Since the settings need to be loaded before the store is created, it will never
// receive the SETTING_UPDATE_ALL even, which mean state.settings will not be
// initialised. So we manually call dispatchUpdateAll() to force an update.
Setting.dispatchUpdateAll();
if (!shim.sharpEnabled()) this.logger().warn('Sharp is disabled - certain image-related features will not be available');
initializeCommandService(this.store(), Setting.value('env') === Env.Dev);
@@ -461,11 +466,6 @@ class Application extends BaseApplication {
this.gui_.setLogger(this.logger());
await this.gui_.start();
// Since the settings need to be loaded before the store is created, it will never
// receive the SETTING_UPDATE_ALL even, which mean state.settings will not be
// initialised. So we manually call dispatchUpdateAll() to force an update.
Setting.dispatchUpdateAll();
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
await refreshFolders((action: any) => this.store().dispatch(action), '');

View File

@@ -26,6 +26,7 @@ class Command extends BaseCommand {
['-v, --verbose', 'More verbose output for the `target-status` command'],
['-o, --output <directory>', 'Output directory'],
['--retry-failed-items', 'Applies to `decrypt` command - retries decrypting items that previously could not be decrypted.'],
['-f, --force', 'Do not ask for input on failure'],
];
}
@@ -67,7 +68,7 @@ class Command extends BaseCommand {
this.stdout(line.join('\n'));
break;
} catch (error) {
if (error.code === 'masterKeyNotLoaded') {
if (error.code === 'masterKeyNotLoaded' && !args.options.force) {
const ok = await askForMasterKey(error);
if (!ok) return;
continue;

View File

@@ -17,6 +17,7 @@ import { pathExists, writeFile } from 'fs-extra';
import { checkIfLoginWasSuccessful, generateApplicationConfirmUrl } from '@joplin/lib/services/joplinCloudUtils';
import Logger from '@joplin/utils/Logger';
import { uuidgen } from '@joplin/lib/uuid';
import ShareService from '@joplin/lib/services/share/ShareService';
const logger = Logger.create('command-sync');
@@ -230,6 +231,10 @@ class Command extends BaseCommand {
return cleanUp();
}
// Refresh share invitations -- if running without a GUI, some of the
// maintenance tasks may otherwise be skipped.
await ShareService.instance().maintenance();
this.stdout(_('Starting synchronisation...'));
const contextKey = `sync.${this.syncTargetId_}.context`;

View File

@@ -0,0 +1 @@
<p><span style="/* Comment */ text-decoration: underline;">Test</span>. In the past, <span style="font-size: auto;/* Test! */">comments</span> in CSS have caused issues.</p>

View File

@@ -0,0 +1 @@
<ins>Test</ins>. In the past, comments in CSS have caused issues.

View File

@@ -343,6 +343,14 @@ export default class ElectronAppWrapper {
}, 1000);
}
const sendWindowFocused = (focusedWebContents: WebContents) => {
const joplinId = this.windowIdFromWebContents(focusedWebContents);
if (joplinId !== null) {
this.win_.webContents.send('window-focused', joplinId);
}
};
const addWindowEventHandlers = (webContents: WebContents) => {
// will-frame-navigate is fired by clicking on a link within the BrowserWindow.
webContents.on('will-frame-navigate', event => {
@@ -376,13 +384,10 @@ export default class ElectronAppWrapper {
addWindowEventHandlers(event.webContents);
});
webContents.on('focus', () => {
const joplinId = this.windowIdFromWebContents(webContents);
if (joplinId !== null) {
this.win_.webContents.send('window-focused', joplinId);
}
});
const onFocus = () => {
sendWindowFocused(webContents);
};
webContents.on('focus', onFocus);
};
addWindowEventHandlers(this.win_.webContents);
@@ -454,6 +459,10 @@ export default class ElectronAppWrapper {
this.win_.close();
}
});
if (window.isFocused()) {
sendWindowFocused(window.webContents);
}
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied

View File

@@ -1,9 +1,12 @@
import { filename, toForwardSlashes } from '@joplin/utils/path';
import * as esbuild from 'esbuild';
import { existsSync } from 'fs';
import { existsSync, readFileSync } from 'fs';
import { writeFile } from 'fs/promises';
import { dirname, join, relative } from 'path';
const baseDir = dirname(__dirname);
const baseNodeModules = join(baseDir, 'node_modules');
// Note: Roughly based on js-draw's use of esbuild:
// https://github.com/personalizedrefrigerator/js-draw/blob/6fe6d6821402a08a8d17f15a8f48d95e5d7b084f/packages/build-tool/src/BundledFile.ts#L64
const makeBuildContext = (entryPoint: string, renderer: boolean, computeFileSizeStats: boolean) => {
@@ -28,8 +31,6 @@ const makeBuildContext = (entryPoint: string, renderer: boolean, computeFileSize
name: 'joplin--relative-imports-for-externals',
setup: build => {
const externalRegex = /^(.*\.node|sqlite3|electron|@electron\/remote\/.*|electron\/.*|@mapbox\/node-pre-gyp|jsdom)$/;
const baseDir = dirname(__dirname);
const baseNodeModules = join(baseDir, 'node_modules');
build.onResolve({ filter: externalRegex }, args => {
// Electron packages don't need relative requires
if (args.path === 'electron' || args.path.startsWith('electron/')) {
@@ -66,8 +67,6 @@ const makeBuildContext = (entryPoint: string, renderer: boolean, computeFileSize
// Rewrite imports to prefer .js files to .ts. Otherwise, certain files are duplicated in the final bundle
name: 'joplin--prefer-js-imports',
setup: build => {
const baseDir = dirname(__dirname);
const baseNodeModules = join(baseDir, 'node_modules');
// Rewrite all relative imports
build.onResolve({ filter: /^\./ }, args => {
try {
@@ -90,6 +89,31 @@ const makeBuildContext = (entryPoint: string, renderer: boolean, computeFileSize
});
},
},
{
name: 'joplin--smaller-source-map-size',
setup: build => {
// Exclude dependencies from node_modules. This significantly reduces the size of the
// source map, improving startup performance.
//
// See https://github.com/evanw/esbuild/issues/1685#issuecomment-944916409
// and https://github.com/evanw/esbuild/issues/4130
const emptyMapData = Buffer.from(
JSON.stringify({ version: 3, sources: [null], mappings: 'AAAA' }),
'utf-8',
).toString('base64');
const emptyMapUrl = `data:application/json;base64,${emptyMapData}`;
build.onLoad({ filter: /node_modules.*js$/ }, args => {
return {
contents: [
readFileSync(args.path, 'utf8'),
`//# sourceMappingURL=${emptyMapUrl}`,
].join('\n'),
loader: 'default',
};
});
},
},
],
});
};

View File

@@ -60,6 +60,7 @@ const useCss = (editorTheme: Theme) => {
body, html {
padding: 0;
margin: 0;
overflow: hidden;
}
/* Hide the scrollbar. See scrollbar accessibility concerns

View File

@@ -533,7 +533,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
CURRENT_PROJECT_VERSION = 141;
CURRENT_PROJECT_VERSION = 142;
DEVELOPMENT_TEAM = A9BXAFS6CT;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Joplin/Info.plist;
@@ -568,7 +568,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
CURRENT_PROJECT_VERSION = 141;
CURRENT_PROJECT_VERSION = 142;
DEVELOPMENT_TEAM = A9BXAFS6CT;
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
@@ -767,7 +767,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 141;
CURRENT_PROJECT_VERSION = 142;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -810,7 +810,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 141;
CURRENT_PROJECT_VERSION = 142;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11;

View File

@@ -1,5 +1,5 @@
module.exports = {
hash:"e857ce4f63c45b5c1d25eb9a76c2127d", files: {
hash:"39ce682c4ff5dd85d571d0e99718648f", files: {
'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },
'highlight.js/atom-one-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' },
'katex/fonts/KaTeX_AMS-Regular.woff2': { data: require('./katex/fonts/KaTeX_AMS-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },

View File

@@ -1 +1 @@
module.exports = {"hash":"e857ce4f63c45b5c1d25eb9a76c2127d","files":["highlight.js/atom-one-dark-reasonable.css","highlight.js/atom-one-light.css","katex/fonts/KaTeX_AMS-Regular.woff2","katex/fonts/KaTeX_Caligraphic-Bold.woff2","katex/fonts/KaTeX_Caligraphic-Regular.woff2","katex/fonts/KaTeX_Fraktur-Bold.woff2","katex/fonts/KaTeX_Fraktur-Regular.woff2","katex/fonts/KaTeX_Main-Bold.woff2","katex/fonts/KaTeX_Main-BoldItalic.woff2","katex/fonts/KaTeX_Main-Italic.woff2","katex/fonts/KaTeX_Main-Regular.woff2","katex/fonts/KaTeX_Math-BoldItalic.woff2","katex/fonts/KaTeX_Math-Italic.woff2","katex/fonts/KaTeX_SansSerif-Bold.woff2","katex/fonts/KaTeX_SansSerif-Italic.woff2","katex/fonts/KaTeX_SansSerif-Regular.woff2","katex/fonts/KaTeX_Script-Regular.woff2","katex/fonts/KaTeX_Size1-Regular.woff2","katex/fonts/KaTeX_Size2-Regular.woff2","katex/fonts/KaTeX_Size3-Regular.woff2","katex/fonts/KaTeX_Size4-Regular.woff2","katex/fonts/KaTeX_Typewriter-Regular.woff2","katex/katex.css","mermaid/mermaid.min.js","mermaid/mermaid_render.js"]}
module.exports = {"hash":"39ce682c4ff5dd85d571d0e99718648f","files":["highlight.js/atom-one-dark-reasonable.css","highlight.js/atom-one-light.css","katex/fonts/KaTeX_AMS-Regular.woff2","katex/fonts/KaTeX_Caligraphic-Bold.woff2","katex/fonts/KaTeX_Caligraphic-Regular.woff2","katex/fonts/KaTeX_Fraktur-Bold.woff2","katex/fonts/KaTeX_Fraktur-Regular.woff2","katex/fonts/KaTeX_Main-Bold.woff2","katex/fonts/KaTeX_Main-BoldItalic.woff2","katex/fonts/KaTeX_Main-Italic.woff2","katex/fonts/KaTeX_Main-Regular.woff2","katex/fonts/KaTeX_Math-BoldItalic.woff2","katex/fonts/KaTeX_Math-Italic.woff2","katex/fonts/KaTeX_SansSerif-Bold.woff2","katex/fonts/KaTeX_SansSerif-Italic.woff2","katex/fonts/KaTeX_SansSerif-Regular.woff2","katex/fonts/KaTeX_Script-Regular.woff2","katex/fonts/KaTeX_Size1-Regular.woff2","katex/fonts/KaTeX_Size2-Regular.woff2","katex/fonts/KaTeX_Size3-Regular.woff2","katex/fonts/KaTeX_Size4-Regular.woff2","katex/fonts/KaTeX_Typewriter-Regular.woff2","katex/katex.css","mermaid/mermaid.min.js","mermaid/mermaid_render.js"]}

File diff suppressed because one or more lines are too long

View File

@@ -1353,7 +1353,6 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
isOpen={this.props.showSideMenu}
disableGestures={disableSideMenuGestures}
>
<StatusBar barStyle={statusBarStyle} />
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: '100%' }}>
<SafeAreaView style={{ flex: 0, backgroundColor: theme.backgroundColor2 }}/>
<SafeAreaView style={{ flex: 1 }}>
@@ -1362,11 +1361,6 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
</View>
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied */}
<DropdownAlert alert={(func: any) => (this.dropdownAlert_ = func)} />
{ !shouldShowMainContent && <BiometricPopup
dispatch={this.props.dispatch}
themeId={this.props.themeId}
sensorInfo={this.state.sensorInfo}
/> }
</SafeAreaView>
</View>
</SideMenu>
@@ -1416,12 +1410,21 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
},
}}>
<DialogManager themeId={this.props.themeId}>
<StatusBar barStyle={statusBarStyle} />
<MenuProvider
style={{ flex: 1 }}
closeButtonLabel={_('Dismiss')}
>
<FocusControl.MainAppContent style={{ flex: 1 }}>
{mainContent}
{shouldShowMainContent ? mainContent : (
<SafeAreaView>
<BiometricPopup
dispatch={this.props.dispatch}
themeId={this.props.themeId}
sensorInfo={this.state.sensorInfo}
/>
</SafeAreaView>
)}
</FocusControl.MainAppContent>
</MenuProvider>
</DialogManager>

View File

@@ -8,9 +8,9 @@ import { chdir, cwd } from 'process';
import { execCommand } from '@joplin/utils';
import { glob } from 'glob';
import readRepositoryJson from './utils/readRepositoryJson';
import waitForCliInput from './utils/waitForCliInput';
import getPathToPatchFileFor from './utils/getPathToPatchFileFor';
import getCurrentCommitHash from './utils/getCurrentCommitHash';
import { waitForCliInput } from '@joplin/utils/cli';
interface Options {
beforeInstall: (buildDir: string, pluginName: string)=> Promise<void>;

View File

@@ -1,7 +1,7 @@
import { execCommand } from '@joplin/utils';
import waitForCliInput from '../utils/waitForCliInput';
import { copy } from 'fs-extra';
import { join } from 'path';
import { waitForCliInput } from '@joplin/utils/cli';
import buildDefaultPlugins from '../buildDefaultPlugins';
import getPathToPatchFileFor from '../utils/getPathToPatchFileFor';

View File

@@ -98,7 +98,7 @@ export default class ClipperServer {
});
}
public async findAvailablePort() {
public async findAvailablePort(): Promise<number> {
const tcpPortUsed = require('tcp-port-used');
let state = null;

View File

@@ -1,13 +1,21 @@
const testPathIgnorePatterns = [
'<rootDir>/node_modules/',
'<rootDir>/rnInjectedJs/',
'<rootDir>/vendor/',
];
if (!process.env.IS_CONTINUOUS_INTEGRATION) {
// We don't require all developers to have Rust to run the project, so we skip this test if not running in CI
testPathIgnorePatterns.push('<rootDir>/services/interop/InteropService_Importer_OneNote.*');
}
module.exports = {
testMatch: [
'**/*.test.js',
],
testPathIgnorePatterns: [
'<rootDir>/node_modules/',
'<rootDir>/rnInjectedJs/',
'<rootDir>/vendor/',
],
testPathIgnorePatterns: testPathIgnorePatterns,
testEnvironment: 'node',

View File

@@ -16,6 +16,7 @@ export enum MarkdownTableJustify {
export interface MarkdownTableHeader {
name: string;
label: string;
labelUrl?: string;
filter?: (content: string)=> string;
disableEscape?: boolean;
disableHtmlEscape?: boolean;
@@ -159,7 +160,11 @@ const markdownUtils = {
const lineMd = [];
for (let i = 0; i < headers.length; i++) {
const h = headers[i];
headersMd.push(stringPadding(h.label, minCellWidth, ' ', stringPadding.RIGHT));
let label = h.label;
if (h.labelUrl) {
label = `[${h.label}](${h.labelUrl})`;
}
headersMd.push(stringPadding(label, minCellWidth, ' ', stringPadding.RIGHT));
const justify = h.justify ? h.justify : MarkdownTableJustify.Left;

View File

@@ -357,7 +357,7 @@ export default class Folder extends BaseItem {
if (options && options.includeConflictFolder) {
const conflictCount = await Note.conflictedCount();
if (conflictCount) output.push(this.conflictFolder());
if (conflictCount) output.unshift(this.conflictFolder());
}
return output;

View File

@@ -22,9 +22,7 @@ const expectWithInstructions = <T>(value: T) => {
return expect(value, instructionMessage);
};
// We don't require all developers to have Rust to run the project, so we skip this test if not running in CI
const skipIfNotCI = process.env.IS_CONTINUOUS_INTEGRATION ? it : it.skip;
// This file is ignored if not running in CI. Look at onenote-converter/README.md and jest.config.js for more information
describe('InteropService_Importer_OneNote', () => {
let tempDir: string;
async function importNote(path: string) {
@@ -52,7 +50,7 @@ describe('InteropService_Importer_OneNote', () => {
afterEach(async () => {
await remove(tempDir);
});
skipIfNotCI('should import a simple OneNote notebook', async () => {
it('should import a simple OneNote notebook', async () => {
const notes = await importNote(`${supportDir}/onenote/simple_notebook.zip`);
const folders = await Folder.all();
@@ -69,7 +67,7 @@ describe('InteropService_Importer_OneNote', () => {
expectWithInstructions(mainNote.body).toMatchSnapshot(mainNote.title);
});
skipIfNotCI('should preserve indentation of subpages in Section page', async () => {
it('should preserve indentation of subpages in Section page', async () => {
const notes = await importNote(`${supportDir}/onenote/subpages.zip`);
const sectionPage = notes.find(n => n.title === 'Section');
@@ -89,7 +87,7 @@ describe('InteropService_Importer_OneNote', () => {
expectWithInstructions(menuLines[7].trim()).toBe(`<li class="l2"><a href=":/${pageTwoB.id}" target="content" title="Page 2-b">${pageTwoB.title}</a>`);
});
skipIfNotCI('should created subsections', async () => {
it('should created subsections', async () => {
const notes = await importNote(`${supportDir}/onenote/subsections.zip`);
const folders = await Folder.all();
@@ -107,7 +105,7 @@ describe('InteropService_Importer_OneNote', () => {
expectWithInstructions(notesFromParentSection.length).toBe(2);
});
skipIfNotCI('should expect notes to be rendered the same', async () => {
it('should expect notes to be rendered the same', async () => {
let idx = 0;
const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
const notes = await importNote(`${supportDir}/onenote/complex_notes.zip`);
@@ -124,7 +122,7 @@ describe('InteropService_Importer_OneNote', () => {
BaseModel.setIdGenerator(originalIdGenerator);
});
skipIfNotCI('should render the proper tree for notebook with group sections', async () => {
it('should render the proper tree for notebook with group sections', async () => {
const notes = await importNote(`${supportDir}/onenote/group_sections.zip`);
const folders = await Folder.all();
@@ -152,7 +150,7 @@ describe('InteropService_Importer_OneNote', () => {
expectWithInstructions(notes.filter(n => n.parent_id === sectionD1.id).length).toBe(1);
});
skipIfNotCI.each([
it.each([
'svg_with_text_and_style.html',
'many_svgs.html',
])('should extract svgs', async (filename: string) => {
@@ -179,7 +177,7 @@ describe('InteropService_Importer_OneNote', () => {
expectWithInstructions(importer.extractSvgs(content, titleGenerator())).toMatchSnapshot();
});
skipIfNotCI('should ignore broken characters at the start of paragraph', async () => {
it('should ignore broken characters at the start of paragraph', async () => {
let idx = 0;
const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
const notes = await importNote(`${supportDir}/onenote/bug_broken_character.zip`);
@@ -189,7 +187,7 @@ describe('InteropService_Importer_OneNote', () => {
BaseModel.setIdGenerator(originalIdGenerator);
});
skipIfNotCI('should remove hyperlink from title', async () => {
it('should remove hyperlink from title', async () => {
let idx = 0;
const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
const notes = await importNote(`${supportDir}/onenote/remove_hyperlink_on_title.zip`);
@@ -200,7 +198,7 @@ describe('InteropService_Importer_OneNote', () => {
BaseModel.setIdGenerator(originalIdGenerator);
});
skipIfNotCI('should group link parts even if they have different css styles', async () => {
it('should group link parts even if they have different css styles', async () => {
const notes = await importNote(`${supportDir}/onenote/remove_hyperlink_on_title.zip`);
const noteToTest = notes.find(n => n.title === 'Tips from a Pro Using Trees for Dramatic Landscape Photography');
@@ -209,7 +207,7 @@ describe('InteropService_Importer_OneNote', () => {
expectWithInstructions(noteToTest.body.includes('<a href="onenote:https://d.docs.live.net/c8d3bbab7f1acf3a/Documents/Photography/风景.one#Tips%20from%20a%20Pro%20Using%20Trees%20for%20Dramatic%20Landscape%20Photography&section-id={262ADDFB-A4DC-4453-A239-0024D6769962}&page-id={88D803A5-4F43-48D4-9B16-4C024F5787DC}&end" style="">Tips from a Pro: Using Trees for Dramatic Landscape Photography</a>')).toBe(true);
});
skipIfNotCI('should render links properly by ignoring wrongly set indices when the first character is a hyperlink marker', async () => {
it('should render links properly by ignoring wrongly set indices when the first character is a hyperlink marker', async () => {
let idx = 0;
const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
const notes = await importNote(`${supportDir}/onenote/hyperlink_marker_as_first_character.zip`);
@@ -220,7 +218,7 @@ describe('InteropService_Importer_OneNote', () => {
BaseModel.setIdGenerator(originalIdGenerator);
});
skipIfNotCI('should be able to create notes from corrupted attachment', async () => {
it('should be able to create notes from corrupted attachment', async () => {
let idx = 0;
const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
const notes = await withWarningSilenced(/OneNoteConverter:/, async () => importNote(`${supportDir}/onenote/corrupted_attachment.zip`));
@@ -233,7 +231,7 @@ describe('InteropService_Importer_OneNote', () => {
BaseModel.setIdGenerator(originalIdGenerator);
});
skipIfNotCI('should render audio as links to resource', async () => {
it('should render audio as links to resource', async () => {
let idx = 0;
const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
const notes = await importNote(`${supportDir}/onenote/note_with_audio_embedded.zip`);
@@ -246,7 +244,7 @@ describe('InteropService_Importer_OneNote', () => {
BaseModel.setIdGenerator(originalIdGenerator);
});
skipIfNotCI('should use default value for EntityGuid and InkBias if not found', async () => {
it('should use default value for EntityGuid and InkBias if not found', async () => {
let idx = 0;
const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
const notes = await withWarningSilenced(/OneNoteConverter:/, async () => importNote(`${supportDir}/onenote/ink_bias_and_entity_guid.zip`));

View File

@@ -9,6 +9,7 @@ export enum PlanName {
Basic = 'basic',
Pro = 'pro',
Teams = 'teams',
JoplinServerBusiness = 'joplinServerBusiness',
}
interface PlanFeature {
@@ -17,6 +18,7 @@ interface PlanFeature {
basic: boolean;
pro: boolean;
teams: boolean;
joplinServerBusiness?: boolean;
basicInfo?: string;
proInfo?: string;
teamsInfo?: string;
@@ -25,11 +27,16 @@ interface PlanFeature {
teamsInfoShort?: string;
}
enum PlanHostingType {
Managed = 'managed',
Self = 'self',
}
export interface Plan {
name: string;
title: string;
priceMonthly: StripePublicConfigPrice;
priceYearly: StripePublicConfigPrice;
priceMonthly?: StripePublicConfigPrice;
priceYearly?: StripePublicConfigPrice;
featured: boolean;
iconName: string;
featuresOn: FeatureId[];
@@ -39,6 +46,8 @@ export interface Plan {
cfaLabel: string;
cfaUrl: string;
footnote: string;
learnMoreUrl?: string;
hostingType: PlanHostingType;
}
export enum PricePeriod {
@@ -155,26 +164,29 @@ const features = (): Record<FeatureId, PlanFeature> => {
basic: true,
pro: true,
teams: true,
joplinServerBusiness: true,
},
sync: {
title: _('Sync as many devices as you want'),
basic: true,
pro: true,
teams: true,
joplinServerBusiness: true,
},
clipper: {
title: _('Web Clipper'),
description: _('The [Web Clipper](%s) is a browser extension that allows you to save web pages and screenshots from your browser.', 'https://joplinapp.org/help/apps/clipper'),
basic: true,
pro: true,
teams: true,
},
// clipper: {
// title: _('Web Clipper'),
// description: _('The [Web Clipper](%s) is a browser extension that allows you to save web pages and screenshots from your browser.', 'https://joplinapp.org/help/apps/clipper'),
// basic: false,
// pro: false,
// teams: false,
// },
collaborate: {
title: _('Collaborate on a notebook with others'),
description: _('This allows another user to share a notebook with you, and you can then both collaborate on it. It does not however allow you to share a notebook with someone else, unless you have the feature "%s".', shareNotebookTitle),
basic: true,
pro: true,
teams: true,
joplinServerBusiness: true,
},
share: {
title: shareNotebookTitle,
@@ -182,6 +194,7 @@ const features = (): Record<FeatureId, PlanFeature> => {
basic: false,
pro: true,
teams: true,
joplinServerBusiness: true,
},
emailToNote: {
title: _('Email to Note'),
@@ -189,6 +202,7 @@ const features = (): Record<FeatureId, PlanFeature> => {
basic: false,
pro: true,
teams: true,
joplinServerBusiness: true,
},
customBanner: {
title: _('Customise the note publishing banner'),
@@ -196,6 +210,7 @@ const features = (): Record<FeatureId, PlanFeature> => {
basic: false,
pro: true,
teams: true,
joplinServerBusiness: true,
},
multiUsers: {
title: _('Manage multiple users'),
@@ -203,6 +218,7 @@ const features = (): Record<FeatureId, PlanFeature> => {
basic: false,
pro: false,
teams: true,
joplinServerBusiness: true,
},
consolidatedBilling: {
title: _('Consolidated billing'),
@@ -217,12 +233,28 @@ const features = (): Record<FeatureId, PlanFeature> => {
basic: false,
pro: false,
teams: true,
joplinServerBusiness: true,
},
prioritySupport: {
title: _('Priority support'),
basic: false,
pro: false,
teams: true,
joplinServerBusiness: true,
},
selfHosted: {
title: _('Self-hosted'),
basic: false,
pro: false,
teams: false,
joplinServerBusiness: true,
},
sourceCodeAvailable: {
title: _('Source code available'),
basic: false,
pro: false,
teams: false,
joplinServerBusiness: true,
},
};
};
@@ -303,6 +335,11 @@ export const createFeatureTableMd = () => {
name: 'teams',
label: 'Teams',
},
{
name: 'joplinServerBusiness',
label: 'Joplin Server Business',
labelUrl: 'https://joplinapp.org/help/apps/joplin_server_business',
},
];
const rows: MarkdownTableRow[] = [];
@@ -332,6 +369,7 @@ export const createFeatureTableMd = () => {
basic: getCellInfo(PlanName.Basic, feature),
pro: getCellInfo(PlanName.Pro, feature),
teams: getCellInfo(PlanName.Teams, feature),
joplinServerBusiness: getCellInfo(PlanName.JoplinServerBusiness, feature),
};
rows.push(row);
@@ -362,6 +400,7 @@ export function getPlans(stripeConfig: StripePublicConfig): Record<PlanName, Pla
cfaLabel: _('Try it now'),
cfaUrl: '',
footnote: '',
hostingType: PlanHostingType.Managed,
},
pro: {
@@ -384,6 +423,7 @@ export function getPlans(stripeConfig: StripePublicConfig): Record<PlanName, Pla
cfaLabel: _('Try it now'),
cfaUrl: '',
footnote: '',
hostingType: PlanHostingType.Managed,
},
teams: {
@@ -406,6 +446,23 @@ export function getPlans(stripeConfig: StripePublicConfig): Record<PlanName, Pla
cfaLabel: _('Try it now'),
cfaUrl: '',
footnote: _('Per user. Minimum of 2 users.'),
hostingType: PlanHostingType.Managed,
},
joplinServerBusiness: {
name: 'joplinServerBusiness',
title: _('Joplin Server Business'),
featured: false,
iconName: 'business-icon',
featuresOn: getFeatureIdsByPlan(PlanName.JoplinServerBusiness, true),
featuresOff: getFeatureIdsByPlan(PlanName.JoplinServerBusiness, false),
featureLabelsOn: getFeatureLabelsByPlan(PlanName.JoplinServerBusiness, true),
featureLabelsOff: getFeatureLabelsByPlan(PlanName.JoplinServerBusiness, false),
cfaLabel: _('Get a quote'),
cfaUrl: 'mailto:jsb-inquiry@joplin.cloud?subject=Joplin%20Server%20Business%20inquiry',
footnote: '',
learnMoreUrl: 'https://joplinapp.org/help/apps/joplin_server_business',
hostingType: PlanHostingType.Self,
},
};
}

File diff suppressed because one or more lines are too long

View File

@@ -38,7 +38,7 @@
"highlight.js": "11.11.1",
"html-entities": "1.4.0",
"json-stringify-safe": "5.0.1",
"katex": "0.16.21",
"katex": "0.16.22",
"markdown-it": "13.0.2",
"markdown-it-abbr": "1.0.4",
"markdown-it-anchor": "5.3.0",

View File

@@ -39,7 +39,7 @@
"html-entities": "1.4.0",
"jquery": "3.7.1",
"knex": "3.1.0",
"koa": "2.16.0",
"koa": "2.16.1",
"ldapts": "7.3.3",
"markdown-it": "13.0.2",
"mustache": "4.2.0",
@@ -47,7 +47,7 @@
"node-os-utils": "1.3.7",
"nodemailer": "6.10.0",
"nodemon": "3.1.9",
"pg": "8.13.3",
"pg": "8.14.1",
"pm2": "5.4.3",
"pretty-bytes": "5.6.0",
"prettycron": "0.10.0",

View File

@@ -914,8 +914,14 @@ export default class ItemModel extends BaseModel<Item> {
const share = await this.models().share().byItemId(item.id);
if (!share) throw new Error(`Cannot find share associated with item ${item.id}`);
const userShare = await this.models().shareUser().byShareAndUserId(share.id, userId);
if (!userShare) return;
await this.models().shareUser().delete(userShare.id);
if (userShare) {
// Leave the share
await this.models().shareUser().delete(userShare.id);
} else if (share.owner_id === userId) {
// Delete the share
await this.models().share().delete(share.id);
}
} else {
await this.delete(item.id);
}

View File

@@ -4,3 +4,4 @@ patreon_oauth_token.txt
*.po~
*.mo
*.mo~
fuzzer/profiles-tmp/

View File

@@ -183,4 +183,7 @@ topagency
esbuild
mapbox
outfile
fuzzer
Freespinny
BestEtf
Etf

View File

@@ -0,0 +1,387 @@
import { strict as assert } from 'assert';
import { ActionableClient, FolderData, FolderMetadata, FuzzContext, ItemId, NoteData, TreeItem, isFolder } from './types';
import type Client from './Client';
interface ClientData {
childIds: ItemId[];
// Shared folders belonging to the client
sharedFolderIds: ItemId[];
}
class ActionTracker {
private idToItem_: Map<ItemId, TreeItem> = new Map();
private tree_: Map<string, ClientData> = new Map();
public constructor(private readonly context_: FuzzContext) {}
private checkRep_() {
const checkItem = (itemId: ItemId) => {
assert.match(itemId, /^[a-zA-Z0-9]{32}$/, 'item IDs should be 32 character alphanumeric strings');
const item = this.idToItem_.get(itemId);
assert.ok(!!item, `should find item with ID ${itemId}`);
if (item.parentId) {
const parent = this.idToItem_.get(item.parentId);
assert.ok(parent, `should find parent (id: ${item.parentId})`);
assert.ok(isFolder(parent), 'parent should be a folder');
assert.ok(parent.childIds.includes(itemId), 'parent should include the current item in its children');
}
if (isFolder(item)) {
for (const childId of item.childIds) {
checkItem(childId);
}
assert.equal(
item.childIds.length,
[...new Set(item.childIds)].length,
'child IDs should be unique',
);
}
};
for (const clientData of this.tree_.values()) {
for (const childId of clientData.childIds) {
assert.ok(this.idToItem_.has(childId), `root item ${childId} should exist`);
const item = this.idToItem_.get(childId);
assert.ok(!!item);
assert.equal(item.parentId, '', `${childId} should not have a parent`);
checkItem(childId);
}
}
}
public track(client: { email: string }) {
const clientId = client.email;
this.tree_.set(clientId, {
childIds: [],
sharedFolderIds: [],
});
const getChildIds = (itemId: ItemId) => {
const item = this.idToItem_.get(itemId);
if (!item || !isFolder(item)) return [];
return item.childIds;
};
const updateChildren = (parentId: ItemId, updateFn: (oldChildren: ItemId[])=> ItemId[]) => {
const parent = this.idToItem_.get(parentId);
if (!parent) throw new Error(`Parent with ID ${parentId} not found.`);
if (!isFolder(parent)) throw new Error(`Item ${parentId} is not a folder`);
this.idToItem_.set(parentId, {
...parent,
childIds: updateFn(parent.childIds),
});
};
const addRootItem = (itemId: ItemId) => {
const clientData = this.tree_.get(clientId);
if (!clientData.childIds.includes(itemId)) {
this.tree_.set(clientId, {
...clientData,
childIds: [...clientData.childIds, itemId],
});
}
};
// Returns true iff the given item ID is now unused.
const removeRootItem = (itemId: ItemId) => {
const removeForClient = (clientId: string) => {
const clientData = this.tree_.get(clientId);
const childIds = clientData.childIds;
if (childIds.includes(itemId)) {
const newChildIds = childIds.filter(otherId => otherId !== itemId);
this.tree_.set(clientId, {
...clientData,
childIds: newChildIds,
});
return true;
}
return false;
};
const hasBeenCompletelyRemoved = () => {
for (const clientData of this.tree_.values()) {
if (clientData.childIds.includes(itemId)) {
return false;
}
}
return true;
};
const isOwnedByThis = this.tree_.get(clientId).sharedFolderIds.includes(itemId);
if (isOwnedByThis) { // Unshare
let removed = false;
for (const id of this.tree_.keys()) {
const result = removeForClient(id);
removed ||= result;
}
const clientData = this.tree_.get(clientId);
this.tree_.set(clientId, {
...clientData,
sharedFolderIds: clientData.sharedFolderIds.filter(id => id !== itemId),
});
// At this point, the item shouldn't be a child of any clients:
assert.ok(hasBeenCompletelyRemoved(), 'item should be removed from all clients');
assert.ok(removed, 'should be a toplevel item');
// The item is unshared and can be removed entirely
return true;
} else {
// Otherwise, even if part of a share, removing the
// notebook just leaves the share.
const removed = removeForClient(clientId);
assert.ok(removed, 'should be a toplevel item');
if (hasBeenCompletelyRemoved()) {
return true;
}
}
return false;
};
const addChild = (parentId: ItemId, childId: ItemId) => {
if (parentId) {
updateChildren(parentId, (oldChildren) => {
if (oldChildren.includes(childId)) return oldChildren;
return [...oldChildren, childId];
});
} else {
addRootItem(childId);
}
};
const removeChild = (parentId: ItemId, childId: ItemId) => {
if (!parentId) {
removeRootItem(childId);
} else {
updateChildren(parentId, (oldChildren) => {
return oldChildren.filter(otherId => otherId !== childId);
});
}
};
const removeItemRecursive = (id: ItemId) => {
const item = this.idToItem_.get(id);
if (!item) throw new Error(`Item with ID ${id} not found.`);
if (item.parentId) {
// The parent may already be removed
if (this.idToItem_.has(item.parentId)) {
removeChild(item.parentId, item.id);
}
this.idToItem_.delete(id);
} else {
const idIsUnused = removeRootItem(item.id);
if (idIsUnused) {
this.idToItem_.delete(id);
}
}
if (isFolder(item)) {
for (const childId of item.childIds) {
const child = this.idToItem_.get(childId);
assert.equal(child?.parentId, id, `child ${childId} should have accurate parent ID`);
removeItemRecursive(childId);
}
}
};
const mapItems = <T> (map: (item: TreeItem)=> T) => {
const workList: ItemId[] = [...this.tree_.get(clientId).childIds];
const result: T[] = [];
while (workList.length > 0) {
const id = workList.pop();
const item = this.idToItem_.get(id);
if (!item) throw new Error(`Not found: ${id}`);
result.push(map(item));
if (isFolder(item)) {
for (const childId of item.childIds) {
workList.push(childId);
}
}
}
return result;
};
const listFoldersDetailed = () => {
return mapItems((item): FolderData => {
return isFolder(item) ? item : null;
}).filter(item => !!item);
};
const tracker: ActionableClient = {
createNote: (data: NoteData) => {
assert.ok(!!data.parentId, `note ${data.id} should have a parentId`);
assert.ok(!this.idToItem_.has(data.id), `note ${data.id} should not yet exist`);
this.idToItem_.set(data.id, {
...data,
});
addChild(data.parentId, data.id);
this.checkRep_();
return Promise.resolve();
},
updateNote: (data: NoteData) => {
const oldItem = this.idToItem_.get(data.id);
assert.ok(oldItem, `note ${data.id} should exist`);
assert.ok(!!data.parentId, `note ${data.id} should have a parentId`);
removeChild(oldItem.parentId, data.id);
this.idToItem_.set(data.id, {
...data,
});
addChild(data.parentId, data.id);
this.checkRep_();
return Promise.resolve();
},
createFolder: (data: FolderMetadata) => {
this.idToItem_.set(data.id, {
...data,
parentId: data.parentId ?? '',
childIds: getChildIds(data.id),
isShareRoot: false,
});
addChild(data.parentId, data.id);
this.checkRep_();
return Promise.resolve();
},
deleteFolder: (id: ItemId) => {
this.checkRep_();
const item = this.idToItem_.get(id);
if (!item) throw new Error(`Not found ${id}`);
if (!isFolder(item)) throw new Error(`Not a folder ${id}`);
removeItemRecursive(id);
this.checkRep_();
return Promise.resolve();
},
shareFolder: (id: ItemId, shareWith: Client) => {
const shareWithChildIds = this.tree_.get(shareWith.email).childIds;
if (shareWithChildIds.includes(id)) {
throw new Error(`Folder ${id} already shared with ${shareWith.email}`);
}
assert.ok(this.idToItem_.has(id), 'should exist');
const sharerClient = this.tree_.get(clientId);
if (!sharerClient.sharedFolderIds.includes(id)) {
this.tree_.set(clientId, {
...sharerClient,
sharedFolderIds: [...sharerClient.sharedFolderIds, id],
});
}
this.tree_.set(shareWith.email, {
...this.tree_.get(shareWith.email),
childIds: [...shareWithChildIds, id],
});
this.idToItem_.set(id, {
...this.idToItem_.get(id),
isShareRoot: true,
});
this.checkRep_();
return Promise.resolve();
},
moveItem: (itemId, newParentId) => {
const item = this.idToItem_.get(itemId);
assert.ok(item, `item with ${itemId} should exist`);
if (newParentId) {
const parent = this.idToItem_.get(newParentId);
assert.ok(parent, `parent with ID ${newParentId} should exist`);
} else {
assert.equal(newParentId, '', 'parentId should be empty if a toplevel folder');
}
if (isFolder(item)) {
assert.equal(item.isShareRoot, false, 'cannot move toplevel shared folders without first unsharing');
}
removeChild(item.parentId, itemId);
addChild(newParentId, itemId);
this.idToItem_.set(itemId, {
...item,
parentId: newParentId,
});
this.checkRep_();
return Promise.resolve();
},
sync: () => Promise.resolve(),
listNotes: () => {
const notes = mapItems(item => {
return isFolder(item) ? null : item;
}).filter(item => !!item);
this.checkRep_();
return Promise.resolve(notes);
},
listFolders: () => {
this.checkRep_();
const folderData = listFoldersDetailed().map(item => ({
id: item.id,
title: item.title,
parentId: item.parentId,
}));
return Promise.resolve(folderData);
},
allFolderDescendants: (parentId) => {
this.checkRep_();
const descendants: ItemId[] = [];
const addDescendants = (id: ItemId) => {
const item = this.idToItem_.get(id);
assert.ok(isFolder(item), 'should be a folder');
for (const id of item.childIds) {
descendants.push(id);
const item = this.idToItem_.get(id);
if (isFolder(item)) {
addDescendants(item.id);
}
}
};
descendants.push(parentId);
addDescendants(parentId);
return Promise.resolve(descendants);
},
randomFolder: async (options) => {
let folders = listFoldersDetailed();
if (options.filter) {
folders = folders.filter(options.filter);
}
const folderIndex = this.context_.randInt(0, folders.length);
return folders.length ? folders[folderIndex] : null;
},
randomNote: async () => {
const notes = await tracker.listNotes();
const noteIndex = this.context_.randInt(0, notes.length);
return notes.length ? notes[noteIndex] : null;
},
};
return tracker;
}
}
export default ActionTracker;

View File

@@ -0,0 +1,420 @@
import uuid, { createSecureRandom } from '@joplin/lib/uuid';
import { ActionableClient, FolderMetadata, FuzzContext, HttpMethod, ItemId, Json, NoteData, RandomFolderOptions, UserData } from './types';
import { join } from 'path';
import { mkdir } from 'fs-extra';
import getStringProperty from './utils/getStringProperty';
import { strict as assert } from 'assert';
import ClipperServer from '@joplin/lib/ClipperServer';
import ActionTracker from './ActionTracker';
import Logger from '@joplin/utils/Logger';
import execa = require('execa');
import { cliDirectory } from './constants';
import { commandToString } from '@joplin/utils';
import { quotePath } from '@joplin/utils/path';
import getNumberProperty from './utils/getNumberProperty';
import retryWithCount from './utils/retryWithCount';
const logger = Logger.create('Client');
class Client implements ActionableClient {
public readonly email: string;
public static async create(actionTracker: ActionTracker, context: FuzzContext) {
const id = uuid.create();
const profileDirectory = join(context.baseDir, id);
await mkdir(profileDirectory);
const email = `${id}@localhost`;
const password = createSecureRandom();
const apiOutput = await context.execApi('POST', 'api/users', {
email,
});
const serverId = getStringProperty(apiOutput, 'id');
// The password needs to be set *after* creating the user.
const userRoute = `api/users/${encodeURIComponent(serverId)}`;
await context.execApi('PATCH', userRoute, {
email,
password,
email_confirmed: 1,
});
const closeAccount = async () => {
await context.execApi('DELETE', userRoute, {});
};
try {
const userData = {
email: getStringProperty(apiOutput, 'email'),
password,
};
assert.equal(email, userData.email);
const apiToken = createSecureRandom().replace(/[-]/g, '_');
const apiPort = await ClipperServer.instance().findAvailablePort();
const client = new Client(
actionTracker.track({ email }),
userData,
profileDirectory,
apiPort,
apiToken,
closeAccount,
);
// Joplin Server sync
await client.execCliCommand_('config', 'sync.target', '9');
await client.execCliCommand_('config', 'sync.9.path', context.serverUrl);
await client.execCliCommand_('config', 'sync.9.username', userData.email);
await client.execCliCommand_('config', 'sync.9.password', userData.password);
await client.execCliCommand_('config', 'api.token', apiToken);
await client.execCliCommand_('config', 'api.port', String(apiPort));
const e2eePassword = createSecureRandom().replace(/^-/, '_');
await client.execCliCommand_('e2ee', 'enable', '--password', e2eePassword);
logger.info('Created and configured client');
// Run asynchronously -- the API server command doesn't exit until the server
// is closed.
void (async () => {
try {
await client.execCliCommand_('server', 'start');
} catch (error) {
logger.info('API server exited');
logger.debug('API server exit status', error);
}
})();
await client.sync();
return client;
} catch (error) {
await closeAccount();
throw error;
}
}
private constructor(
private readonly tracker_: ActionableClient,
userData: UserData,
private readonly profileDirectory: string,
private readonly apiPort_: number,
private readonly apiToken_: string,
private readonly cleanUp_: ()=> Promise<void>,
) {
this.email = userData.email;
}
public async close() {
await this.execCliCommand_('server', 'stop');
await this.cleanUp_();
}
private get cliCommandArguments() {
return [
'start-no-build',
'--profile', this.profileDirectory,
'--env', 'dev',
];
}
public getHelpText() {
return [
`Client ${this.email}:`,
`\tCommand: cd ${quotePath(cliDirectory)} && ${commandToString('yarn', this.cliCommandArguments)}`,
].join('\n');
}
private async execCliCommand_(commandName: string, ...args: string[]) {
assert.match(commandName, /^[a-z]/, 'Command name must start with a lowercase letter.');
const commandResult = await execa('yarn', [
...this.cliCommandArguments,
commandName,
...args,
], {
cwd: cliDirectory,
// Connects /dev/null to stdin
stdin: 'ignore',
});
logger.debug('Ran command: ', commandResult.command, commandResult.exitCode);
logger.debug(' Output: ', commandResult.stdout);
return commandResult;
}
// eslint-disable-next-line no-dupe-class-members -- This is not a duplicate class member
private async execApiCommand_(method: 'GET', route: string): Promise<Json>;
// eslint-disable-next-line no-dupe-class-members -- This is not a duplicate class member
private async execApiCommand_(method: 'POST'|'PUT', route: string, data: Json): Promise<Json>;
// eslint-disable-next-line no-dupe-class-members -- This is not a duplicate class member
private async execApiCommand_(method: HttpMethod, route: string, data: Json|null = null): Promise<Json> {
route = route.replace(/^[/]/, '');
const url = new URL(`http://localhost:${this.apiPort_}/${route}`);
url.searchParams.append('token', this.apiToken_);
const response = await fetch(url, {
method,
body: data ? JSON.stringify(data) : undefined,
});
if (!response.ok) {
throw new Error(`Request to ${route} failed with error: ${await response.text()}`);
}
return await response.json();
}
private async execPagedApiCommand_<Result>(
method: 'GET',
route: string,
params: Record<string, string>,
deserializeItem: (data: Json)=> Result,
): Promise<Result[]> {
const searchParams = new URLSearchParams(params);
const results: Result[] = [];
let hasMore = true;
for (let page = 1; hasMore; page++) {
searchParams.set('page', String(page));
searchParams.set('limit', '10');
const response = await this.execApiCommand_(
method, `${route}?${searchParams}`,
);
if (
typeof response !== 'object'
|| !('has_more' in response)
|| !('items' in response)
|| !Array.isArray(response.items)
) {
throw new Error(`Invalid response: ${JSON.stringify(response)}`);
}
hasMore = !!response.has_more;
for (const item of response.items) {
results.push(deserializeItem(item));
}
}
return results;
}
private async decrypt_() {
// E2EE decryption can occasionally fail with "Master key is not loaded:".
// Allow e2ee decryption to be retried:
await retryWithCount(async () => {
const result = await this.execCliCommand_('e2ee', 'decrypt', '--force');
if (!result.stdout.includes('Completed decryption.')) {
throw new Error(`Decryption did not complete: ${result.stdout}`);
}
}, {
count: 3,
onFail: async (error)=>{
logger.warn('E2EE decryption failed:', error);
logger.info('Syncing before retry...');
await this.execCliCommand_('sync');
},
});
}
public async sync() {
logger.info('Sync', this.email);
await this.tracker_.sync();
const result = await this.execCliCommand_('sync');
if (result.stdout.match(/Last error:/i)) {
throw new Error(`Sync failed: ${result.stdout}`);
}
await this.decrypt_();
}
public async createFolder(folder: FolderMetadata) {
logger.info('Create folder', folder.id, 'in', `${folder.parentId ?? 'root'}/${this.email}`);
await this.tracker_.createFolder(folder);
await this.execApiCommand_('POST', '/folders', {
id: folder.id,
title: folder.title,
parent_id: folder.parentId ?? '',
});
}
private async assertNoteMatchesState_(expected: NoteData) {
assert.equal(
(await this.execCliCommand_('cat', expected.id)).stdout,
`${expected.title}\n\n${expected.body}`,
'note should exist',
);
}
public async createNote(note: NoteData) {
logger.info('Create note', note.id, 'in', `${note.parentId}/${this.email}`);
await this.tracker_.createNote(note);
await this.execApiCommand_('POST', '/notes', {
id: note.id,
title: note.title,
body: note.body,
parent_id: note.parentId ?? '',
});
await this.assertNoteMatchesState_(note);
}
public async updateNote(note: NoteData) {
logger.info('Update note', note.id, 'in', `${note.parentId}/${this.email}`);
await this.tracker_.updateNote(note);
await this.execApiCommand_('PUT', `/notes/${encodeURIComponent(note.id)}`, {
title: note.title,
body: note.body,
parent_id: note.parentId ?? '',
});
await this.assertNoteMatchesState_(note);
}
public async deleteFolder(id: string) {
logger.info('Delete folder', id, 'in', this.email);
await this.tracker_.deleteFolder(id);
await this.execCliCommand_('rmbook', '--permanent', '--force', id);
}
public async shareFolder(id: string, shareWith: Client) {
await this.tracker_.shareFolder(id, shareWith);
logger.info('Share', id, 'with', shareWith.email);
await this.execCliCommand_('share', 'add', id, shareWith.email);
await this.sync();
await shareWith.sync();
const shareWithIncoming = JSON.parse((await shareWith.execCliCommand_('share', 'list', '--json')).stdout);
const pendingInvitations = shareWithIncoming.invitations.filter((invitation: unknown) => {
if (typeof invitation !== 'object' || !('accepted' in invitation)) {
throw new Error('Invalid invitation format');
}
return !invitation.accepted;
});
assert.deepEqual(pendingInvitations, [
{
accepted: false,
waiting: true,
rejected: false,
folderId: id,
fromUser: {
email: this.email,
},
},
], 'there should be a single incoming share from the expected user');
await shareWith.execCliCommand_('share', 'accept', id);
}
public async moveItem(itemId: ItemId, newParentId: ItemId) {
logger.info('Move', itemId, 'to', newParentId);
await this.tracker_.moveItem(itemId, newParentId);
const movingToRoot = !newParentId;
await this.execCliCommand_('mv', itemId, movingToRoot ? 'root' : newParentId);
}
public async listNotes() {
const params = {
fields: 'id,parent_id,body,title,is_conflict,conflict_original_id',
include_deleted: '1',
include_conflicts: '1',
};
return await this.execPagedApiCommand_(
'GET',
'/notes',
params,
item => ({
id: getStringProperty(item, 'id'),
parentId: getNumberProperty(item, 'is_conflict') === 1 ? (
`[conflicts for ${getStringProperty(item, 'conflict_original_id')} in ${this.email}]`
) : getStringProperty(item, 'parent_id'),
title: getStringProperty(item, 'title'),
body: getStringProperty(item, 'body'),
}),
);
}
public async listFolders() {
const params = {
fields: 'id,parent_id,title',
include_deleted: '1',
};
return await this.execPagedApiCommand_(
'GET',
'/folders',
params,
item => ({
id: getStringProperty(item, 'id'),
parentId: getStringProperty(item, 'parent_id'),
title: getStringProperty(item, 'title'),
}),
);
}
public async randomFolder(options: RandomFolderOptions) {
return this.tracker_.randomFolder(options);
}
public async allFolderDescendants(parentId: ItemId) {
return this.tracker_.allFolderDescendants(parentId);
}
public async randomNote() {
return this.tracker_.randomNote();
}
public async checkState(_allClients: Client[]) {
logger.info('Check state', this.email);
type ItemSlice = { id: string };
const compare = (a: ItemSlice, b: ItemSlice) => {
if (a.id === b.id) return 0;
return a.id < b.id ? -1 : 1;
};
const assertNoAdjacentEqualIds = (sortedById: ItemSlice[], assertionLabel: string) => {
for (let i = 1; i < sortedById.length; i++) {
const current = sortedById[i];
const previous = sortedById[i - 1];
assert.notEqual(
current.id,
previous.id,
`[${assertionLabel}] item ${i} should have a different ID from item ${i - 1}`,
);
}
};
const checkNoteState = async () => {
const notes = [...await this.listNotes()];
const expectedNotes = [...await this.tracker_.listNotes()];
notes.sort(compare);
expectedNotes.sort(compare);
assertNoAdjacentEqualIds(notes, 'notes');
assertNoAdjacentEqualIds(expectedNotes, 'expectedNotes');
assert.deepEqual(notes, expectedNotes, 'should have the same notes as the expected state');
};
const checkFolderState = async () => {
const folders = [...await this.listFolders()];
const expectedFolders = [...await this.tracker_.listFolders()];
folders.sort(compare);
expectedFolders.sort(compare);
assertNoAdjacentEqualIds(folders, 'folders');
assertNoAdjacentEqualIds(expectedFolders, 'expectedFolders');
assert.deepEqual(folders, expectedFolders, 'should have the same folders as the expected state');
};
await checkNoteState();
await checkFolderState();
}
}
export default Client;

View File

@@ -0,0 +1,54 @@
import ActionTracker from './ActionTracker';
import Client from './Client';
import { CleanupTask, FuzzContext } from './types';
type AddCleanupTask = (task: CleanupTask)=> void;
type ClientFilter = (client: Client)=> boolean;
export default class ClientPool {
public static async create(
context: FuzzContext,
clientCount: number,
addCleanupTask: AddCleanupTask,
) {
if (clientCount <= 0) throw new Error('There must be at least 1 client');
const actionTracker = new ActionTracker(context);
const clientPool: Client[] = [];
for (let i = 0; i < clientCount; i++) {
const client = await Client.create(actionTracker, context);
addCleanupTask(() => client.close());
clientPool.push(client);
}
return new ClientPool(context, clientPool);
}
public constructor(
private readonly context_: FuzzContext,
public readonly clients: Client[],
) { }
public randomClient(filter: ClientFilter = ()=>true) {
const clients = this.clients.filter(filter);
return clients[
this.context_.randInt(0, clients.length)
];
}
public async checkState() {
for (const client of this.clients) {
await client.checkState(this.clients);
}
}
public async syncAll() {
for (const client of this.clients) {
await client.sync();
}
}
public helpText() {
return this.clients.map(client => client.getHelpText()).join('\n\n');
}
}

View File

@@ -0,0 +1,79 @@
import { join } from 'path';
import { HttpMethod, Json, UserData } from './types';
import { packagesDir } from './constants';
import JoplinServerApi from '@joplin/lib/JoplinServerApi';
import { Env } from '@joplin/lib/models/Setting';
import execa = require('execa');
import { msleep } from '@joplin/utils/time';
import Logger from '@joplin/utils/Logger';
const logger = Logger.create('Server');
const createApi = async (serverUrl: string, adminAuth: UserData) => {
const api = new JoplinServerApi({
baseUrl: () => serverUrl,
userContentBaseUrl: () => serverUrl,
password: () => adminAuth.password,
username: () => adminAuth.email,
session: ()=>null,
env: Env.Dev,
});
await api.loadSession();
return api;
};
export default class Server {
private api_: JoplinServerApi|null = null;
private server_: execa.ExecaChildProcess<string>;
public constructor(
private readonly serverUrl_: string,
private readonly adminAuth_: UserData,
) {
const serverDir = join(packagesDir, 'server');
const mainEntrypoint = join(serverDir, 'dist', 'app.js');
this.server_ = execa.node(mainEntrypoint, [
'--env', 'dev',
], {
env: { JOPLIN_IS_TESTING: '1' },
cwd: join(packagesDir, 'server'),
stdin: 'ignore', // No stdin
// For debugging:
// stderr: process.stderr,
// stdout: process.stdout,
});
}
public async checkConnection() {
let lastError;
for (let retry = 0; retry < 30; retry++) {
try {
const response = await fetch(`${this.serverUrl_}api/ping`);
if (response.ok) {
return true;
}
} catch (error) {
lastError = error;
}
await msleep(500);
}
if (lastError) {
throw lastError;
}
return false;
}
public async execApi(method: HttpMethod, route: string, action: Json) {
this.api_ ??= await createApi(this.serverUrl_, this.adminAuth_);
logger.debug('API EXEC', method, route, action);
const result = await this.api_.exec(method, route, {}, action);
return result;
}
public async close() {
this.server_.cancel();
logger.info('Closed the server.');
}
}

View File

@@ -0,0 +1,5 @@
import { dirname, join } from 'path';
export const packagesDir = dirname(dirname(__dirname));
export const cliDirectory = join(packagesDir, 'app-cli');

View File

@@ -0,0 +1,362 @@
import uuid from '@joplin/lib/uuid';
import { join } from 'path';
import { exists, mkdir, remove } from 'fs-extra';
import Setting, { Env } from '@joplin/lib/models/Setting';
import Logger, { TargetType } from '@joplin/utils/Logger';
import { waitForCliInput } from '@joplin/utils/cli';
import Server from './Server';
import { CleanupTask, FuzzContext } from './types';
import ClientPool from './ClientPool';
import retryWithCount from './utils/retryWithCount';
import Client from './Client';
import SeededRandom from './utils/SeededRandom';
import { env } from 'process';
import yargs = require('yargs');
import { strict as assert } from 'assert';
const { shimInit } = require('@joplin/lib/shim-init-node');
const globalLogger = new Logger();
globalLogger.addTarget(TargetType.Console);
Logger.initializeGlobalLogger(globalLogger);
const logger = Logger.create('fuzzer');
const createProfilesDirectory = async () => {
const path = join(__dirname, 'profiles-tmp');
if (await exists(path)) {
throw new Error([
'Another instance of the sync fuzzer may be running!',
'The parent directory for test profiles already exists. An instance of the fuzzer is either already running or was closed before it could clean up.',
`To ignore this issue, delete ${JSON.stringify(path)} and re-run the fuzzer.`,
].join('\n'));
}
await mkdir(path);
return {
path,
remove: async () => {
await remove(path);
},
};
};
const doRandomAction = async (context: FuzzContext, client: Client, clientPool: ClientPool) => {
const selectOrCreateParentFolder = async () => {
let parentId = (await client.randomFolder({}))?.id;
// Create a toplevel folder to serve as this
// folder's parent if none exist yet
if (!parentId) {
parentId = uuid.create();
await client.createFolder({
parentId: '',
id: parentId,
title: 'Parent folder',
});
}
return parentId;
};
const selectOrCreateNote = async () => {
let note = await client.randomNote();
if (!note) {
await client.createNote({
parentId: await selectOrCreateParentFolder(),
id: uuid.create(),
title: 'Test note',
body: 'Body',
});
note = await client.randomNote();
assert.ok(note, 'should have selected a random note');
}
return note;
};
const actions = {
newSubfolder: async () => {
const folderId = uuid.create();
const parentId = await selectOrCreateParentFolder();
await client.createFolder({
parentId: parentId,
id: folderId,
title: 'Subfolder',
});
return true;
},
newToplevelFolder: async () => {
const folderId = uuid.create();
await client.createFolder({
parentId: null,
id: folderId,
title: `Folder ${context.randInt(0, 1000)}`,
});
return true;
},
newNote: async () => {
const parentId = await selectOrCreateParentFolder();
await client.createNote({
parentId: parentId,
title: `Test (x${context.randInt(0, 1000)})`,
body: 'Testing...',
id: uuid.create(),
});
return true;
},
renameNote: async () => {
const note = await selectOrCreateNote();
await client.updateNote({
...note,
title: `Renamed (${context.randInt(0, 1000)})`,
});
return true;
},
updateNoteBody: async () => {
const note = await selectOrCreateNote();
await client.updateNote({
...note,
body: `${note.body}\n\nUpdated.\n`,
});
return true;
},
moveNote: async () => {
const note = await client.randomNote();
if (!note) return false;
const targetParent = await client.randomFolder({
filter: folder => folder.id !== note.parentId,
});
if (!targetParent) return false;
await client.moveItem(note.id, targetParent.id);
return true;
},
shareFolder: async () => {
const target = await client.randomFolder({
filter: candidate => (
!candidate.parentId && !candidate.isShareRoot
),
});
if (!target) return false;
const other = clientPool.randomClient(c => c !== client);
await client.shareFolder(target.id, other);
return true;
},
deleteFolder: async () => {
const target = await client.randomFolder({});
if (!target) return false;
await client.deleteFolder(target.id);
return true;
},
moveFolderToToplevel: async () => {
const target = await client.randomFolder({
// Don't choose items that are already toplevel
filter: item => !!item.parentId,
});
if (!target) return false;
await client.moveItem(target.id, '');
return true;
},
moveFolderTo: async () => {
const target = await client.randomFolder({
// Don't move shared folders (should not be allowed by the GUI in the main apps).
filter: item => !item.isShareRoot,
});
if (!target) return false;
const targetDescendants = new Set(await client.allFolderDescendants(target.id));
const newParent = await client.randomFolder({
filter: (item) => {
// Avoid making the folder a child of itself
return !targetDescendants.has(item.id);
},
});
if (!newParent) return false;
await client.moveItem(target.id, newParent.id);
return true;
},
};
const actionKeys = [...Object.keys(actions)] as (keyof typeof actions)[];
let result = false;
while (!result) { // Loop until an action was done
const randomAction = actionKeys[context.randInt(0, actionKeys.length)];
logger.info(`Action: ${randomAction} in ${client.email}`);
result = await actions[randomAction]();
if (!result) {
logger.info(` ${randomAction} was skipped (preconditions not met).`);
}
}
};
interface Options {
seed: number;
maximumSteps: number;
maximumStepsBetweenSyncs: number;
clientCount: number;
}
const main = async (options: Options) => {
shimInit();
Setting.setConstant('env', Env.Dev);
const cleanupTasks: CleanupTask[] = [];
const cleanUp = async () => {
logger.info('Cleaning up....');
while (cleanupTasks.length) {
const task = cleanupTasks.pop();
try {
await task();
} catch (error) {
logger.warn('Clean up task failed:', error);
}
}
};
// Run cleanup on Ctrl-C
process.on('SIGINT', async () => {
logger.info('Intercepted ctrl-c. Cleaning up...');
await cleanUp();
process.exit(1);
});
let clientHelpText;
try {
const joplinServerUrl = 'http://localhost:22300/';
const server = new Server(joplinServerUrl, {
email: 'admin@localhost',
password: env['FUZZER_SERVER_ADMIN_PASSWORD'] ?? 'admin',
});
cleanupTasks.push(() => server.close());
if (!await server.checkConnection()) {
throw new Error('Could not connect to the server.');
}
const profilesDirectory = await createProfilesDirectory();
cleanupTasks.push(profilesDirectory.remove);
logger.info('Starting with seed', options.seed);
const random = new SeededRandom(options.seed);
const fuzzContext: FuzzContext = {
serverUrl: joplinServerUrl,
baseDir: profilesDirectory.path,
execApi: server.execApi.bind(server),
randInt: (a, b) => random.nextInRange(a, b),
};
const clientPool = await ClientPool.create(
fuzzContext,
options.clientCount,
task => { cleanupTasks.push(task); },
);
clientHelpText = clientPool.helpText();
const maxSteps = options.maximumSteps;
for (let stepIndex = 1; maxSteps <= 0 || stepIndex <= maxSteps; stepIndex++) {
const client = clientPool.randomClient();
// Ensure that the client starts up-to-date with the other synced clients.
await client.sync();
logger.info('Step', stepIndex, '/', maxSteps > 0 ? maxSteps : 'Infinity');
const actionsBeforeFullSync = fuzzContext.randInt(1, options.maximumStepsBetweenSyncs + 1);
for (let subStepIndex = 1; subStepIndex <= actionsBeforeFullSync; subStepIndex++) {
if (actionsBeforeFullSync > 1) {
logger.info('Sub-step', subStepIndex, '/', actionsBeforeFullSync, '(in step', stepIndex, ')');
}
await doRandomAction(fuzzContext, client, clientPool);
}
await client.sync();
// .checkState can fail occasionally due to incomplete
// syncs (perhaps because the server is still processing
// share-related changes?). Allow this to be retried:
await retryWithCount(async () => {
await clientPool.checkState();
}, {
count: 3,
onFail: async () => {
logger.info('.checkState failed. Syncing all clients...');
await clientPool.syncAll();
},
});
}
} catch (error) {
logger.error('ERROR', error);
if (clientHelpText) {
logger.info('Client information:\n', clientHelpText);
}
logger.info('An error occurred. Pausing before continuing cleanup.');
await waitForCliInput();
process.exitCode = 1;
} finally {
await cleanUp();
logger.info('Cleanup complete');
process.exit();
}
};
void yargs
.usage('$0 <cmd>')
.command(
'start',
[
'Starts the synchronization fuzzer. The fuzzer starts Joplin Server, creates multiple CLI clients, and attempts to find sync bugs.\n\n',
'The fuzzer starts Joplin Server in development mode, using the existing development mode database and uses the admin@localhost user to',
'create and set up user accounts.\n',
'Use the FUZZER_SERVER_ADMIN_PASSWORD environment variable to specify the admin@localhost password for this dev version of Joplin Server.\n\n',
'If the fuzzer detects incorrect/unexpected client state, it pauses, allowing the profile directories and databases',
'of the clients to be inspected.',
].join(' '),
(yargs) => {
return yargs.options({
'seed': { type: 'number', default: 12345 },
'steps': {
type: 'number',
default: 0,
defaultDescription: 'The maximum number of steps to take before stopping the fuzzer. Set to zero for an unlimited number of steps.',
},
'steps-between-syncs': {
type: 'number',
default: 3,
defaultDescription: 'The maximum number of sub-steps taken before all clients are synchronised.',
},
'clients': {
type: 'number',
default: 3,
defaultDescription: 'Number of client apps to create.',
},
});
},
async (argv) => {
await main({
seed: argv.seed,
maximumSteps: argv.steps,
clientCount: argv.clients,
maximumStepsBetweenSyncs: argv['steps-between-syncs'],
});
},
)
.help()
.argv;

View File

@@ -0,0 +1,62 @@
import type Client from './Client';
export type Json = string|number|Json[]|{ [key: string]: Json };
export type HttpMethod = 'GET'|'POST'|'DELETE'|'PUT'|'PATCH';
export type ItemId = string;
export type NoteData = {
parentId: ItemId;
id: ItemId;
title: string;
body: string;
};
export type FolderMetadata = {
parentId: ItemId;
id: ItemId;
title: string;
};
export type FolderData = FolderMetadata & {
childIds: ItemId[];
isShareRoot: boolean;
};
export type TreeItem = NoteData|FolderData;
export const isFolder = (item: TreeItem): item is FolderData => {
return 'childIds' in item;
};
export interface FuzzContext {
serverUrl: string;
baseDir: string;
execApi: (method: HttpMethod, route: string, debugAction: Json)=> Promise<Json>;
randInt: (low: number, high: number)=> number;
}
export interface RandomFolderOptions {
filter?: (folder: FolderData)=> boolean;
}
export interface ActionableClient {
createFolder(data: FolderMetadata): Promise<void>;
shareFolder(id: ItemId, shareWith: Client): Promise<void>;
deleteFolder(id: ItemId): Promise<void>;
createNote(data: NoteData): Promise<void>;
updateNote(data: NoteData): Promise<void>;
moveItem(itemId: ItemId, newParentId: ItemId): Promise<void>;
sync(): Promise<void>;
listNotes(): Promise<NoteData[]>;
listFolders(): Promise<FolderMetadata[]>;
allFolderDescendants(parentId: ItemId): Promise<ItemId[]>;
randomFolder(options: RandomFolderOptions): Promise<FolderMetadata>;
randomNote(): Promise<NoteData>;
}
export interface UserData {
email: string;
password: string;
}
export type CleanupTask = ()=> Promise<void>;

View File

@@ -0,0 +1,52 @@
// SeededRandom provides a very simple random number generator
// that can be seeded (since NodeJS built-ins can't).
//
// See also:
// - https://arxiv.org/pdf/1704.00358
// - https://en.wikipedia.org/wiki/Middle-square_method
// Some large odd number, see https://en.wikipedia.org/wiki/Weyl_sequence
const step = BigInt('0x12345678ABCDE123'); // uint64
const maxSize = BigInt(1) << BigInt(64);
const extractMiddle = (value: bigint, halfSize: bigint) => {
// Remove the lower quarter
const quarterSize = halfSize / BigInt(2);
value >>= quarterSize;
// Remove the upper quarter
const halfMaximumValue = BigInt(1) << halfSize;
value %= halfMaximumValue;
return value;
};
export default class SeededRandom {
private value_: bigint;
private nextStep_: bigint = step;
private halfSize_ = BigInt(32);
public constructor(seed: number) {
this.value_ = BigInt(seed);
}
public next() {
this.value_ = this.value_ * this.value_ + this.nextStep_;
// Move to the next item in the sequence. Mod to prevent from getting
// too large. See https://en.wikipedia.org/wiki/Weyl_sequence.
this.nextStep_ = (step + this.nextStep_) % maxSize;
this.value_ = extractMiddle(this.value_, this.halfSize_);
return this.value_;
}
// The resultant range includes `a` but excludes `b`.
public nextInRange(a: number, b: number) {
if (b <= a + 1) return a;
const range = b - a;
return Number(this.next() % BigInt(range)) + a;
}
}

View File

@@ -0,0 +1,11 @@
import getProperty from './getProperty';
const getNumberProperty = (object: unknown, propertyName: string) => {
const value = getProperty(object, propertyName);
if (typeof value !== 'number') {
throw new Error(`Property value is not a string (is ${typeof value})`);
}
return value;
};
export default getNumberProperty;

View File

@@ -0,0 +1,15 @@
const getProperty = (object: unknown, propertyName: string) => {
if (typeof object !== 'object' || object === null) {
throw new Error(`Cannot access property ${JSON.stringify(propertyName)} on non-object`);
}
if (!(propertyName in object)) {
throw new Error(`No such property ${JSON.stringify(propertyName)} in object`);
}
return object[propertyName as keyof object];
};
export default getProperty;

View File

@@ -0,0 +1,11 @@
import getProperty from './getProperty';
const getStringProperty = (object: unknown, propertyName: string) => {
const value = getProperty(object, propertyName);
if (typeof value !== 'string') {
throw new Error(`Property value is not a string (is ${typeof value})`);
}
return value;
};
export default getStringProperty;

View File

@@ -0,0 +1,21 @@
interface Options {
count: number;
onFail: (error: Error)=> Promise<void>;
}
const retryWithCount = async (task: ()=> Promise<void>, { count, onFail }: Options) => {
let lastError: Error|null = null;
for (let retry = 0; retry < count; retry ++) {
try {
return await task();
} catch (error) {
await onFail(error);
lastError = error;
}
}
if (lastError) throw lastError;
};
export default retryWithCount;

View File

@@ -159,6 +159,8 @@
"v3.3.13": true,
"android-v3.3.9": true,
"android-v3.3.10": true,
"ios-v13.3.8": true
"ios-v13.3.8": true,
"android-v3.3.11": true,
"ios-v13.3.9": true
}
}

View File

@@ -95,11 +95,6 @@
"imageName": "Route4Me.png",
"githubUser": "route4me"
},
{
"url": "https://casinoreviews.net",
"title": "Casino Reviews",
"imageName": "CasinoReviews.png"
},
{
"url": "https://topagency.webflow.io",
"title": "WebDesignAgency",
@@ -124,13 +119,6 @@
"imageName": "Slotozilla.png",
"alt": "casino without making any upfront cost"
},
{
"url": "https://www.reddit.com/r/tiktokRise/",
"title": "Tiktok Rise",
"imageName": "TiktokRise.jpg",
"alt": "Tiktok Rise",
"githubUser": "knickman"
},
{
"url": "https://essaywriter.pro",
"title": "write my essay services by EssayWriter",
@@ -154,6 +142,26 @@
"title": "high-quality paper writing service PaperWriter",
"imageName": "PaperWriter.png",
"alt": "high-quality paper writing service PaperWriter"
},
{
"url": "https://homeworkguy.org/someone-to-take-my-online-class",
"title": "someone to take my online class",
"imageName": "HomeworkGuy.png",
"alt": "someone to take my online class",
"githubUser": "Nftsworld007"
},
{
"url": "https://www.bestetf.net/",
"title": "BestETF",
"imageName": "BestEtf.png",
"alt": "BestETF",
"githubUser": "traspire"
},
{
"url": "https://freespinny.io/free-spins-no-deposit/",
"title": "Freespinny.io Free Spins Bonus site",
"imageName": "Freespinny.png",
"alt": "Freespinny.io Free Spins Bonus site"
}
],
"orgsOld": [
@@ -195,6 +203,13 @@
"title": "Achieve academic success with Edubirdie — your trusted partner for expert writing assistance and resources!",
"imageName": "Edubirdie.png",
"alt": "EduBirdie"
},
{
"url": "https://www.reddit.com/r/tiktokRise/",
"title": "Tiktok Rise",
"imageName": "TiktokRise.jpg",
"alt": "Tiktok Rise",
"githubUser": "knickman"
}
]
}

View File

@@ -19,7 +19,7 @@
"file-type": "16.5.4",
"fs-extra": "11.2.0",
"knex": "3.1.0",
"koa": "2.16.0",
"koa": "2.16.1",
"koa-body": "6.0.1",
"pg-boss": "10.1.6",
"sqlite3": "5.1.6"

View File

@@ -15,7 +15,7 @@
},
"devDependencies": {
"@rollup/plugin-commonjs": "28.0.3",
"@rollup/plugin-node-resolve": "15.3.1",
"@rollup/plugin-node-resolve": "16.0.1",
"@rollup/plugin-replace": "6.0.2",
"browserify": "14.5.0",
"rollup": "4.2.0",

View File

@@ -124,6 +124,8 @@ export function getStyleProp(node, name) {
const o = css.parse('div {' + style + '}');
if (!o.stylesheet.rules.length) return null;
const prop = o.stylesheet.rules[0].declarations.find(d => d.property.toLowerCase() === name);
const prop = o.stylesheet.rules[0].declarations.find(d => {
return d.type === 'declaration' && d.property.toLowerCase() === name;
});
return prop ? prop.value : null;
}

View File

@@ -1,16 +1,17 @@
const readline = require('readline/promises');
/* eslint-disable no-console */
export const isTTY = () => process.stdin.isTTY;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
let readlineInterface: any = null;
const waitForCliInput = async () => {
export const waitForCliInput = async () => {
readlineInterface ??= readline.createInterface({
input: process.stdin,
output: process.stdout,
});
if (process.stdin.isTTY) {
if (isTTY()) {
const green = '\x1b[92m';
const reset = '\x1b[0m';
await readlineInterface.question(`${green}[Press enter to continue]${reset}`);
@@ -21,4 +22,3 @@ const waitForCliInput = async () => {
}
};
export default waitForCliInput;

View File

@@ -18,7 +18,8 @@
"./types": "./dist/types.js",
"./url": "./dist/url.js",
"./ipc": "./dist/ipc.js",
"./path": "./dist/path.js"
"./path": "./dist/path.js",
"./cli": "./dist/cli.js"
},
"publishConfig": {
"access": "public"

View File

@@ -1,5 +1,9 @@
# Joplin Android Changelog
## [android-v3.3.11](https://github.com/laurent22/joplin/releases/tag/android-v3.3.11) (Pre-release) - 2025-07-09T22:51:55Z
- Fixed: Biometrics: Fix notebook list can still be accessed when the app is locked (#12691 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.3.10](https://github.com/laurent22/joplin/releases/tag/android-v3.3.10) (Pre-release) - 2025-06-10T08:07:25Z
- New: Add additional checks when updating sidebar state (#12428 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))

View File

@@ -1,5 +1,10 @@
# Joplin iOS Changelog
## [ios-v13.3.9](https://github.com/laurent22/joplin/releases/tag/ios-v13.3.9) - 2025-07-09T23:17:23Z
- New: Add additional checks when updating sidebar state (#12428 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Biometrics: Fix notebook list can still be accessed when the app is locked (#12691 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [ios-v13.3.8](https://github.com/laurent22/joplin/releases/tag/ios-v13.3.8) - 2025-06-09T17:15:06Z
- Fixed: Fix error shown the first time a user attempts to record (#12328) (#12314 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))

View File

@@ -1,236 +1,237 @@
---
updated: 2025-06-01T02:15:29Z
updated: 2025-07-06T16:57:49Z
---
# Joplin statistics
| Name | Value |
| ----- | ----- |
| Total Windows downloads | 6,559,219 |
| Total macOs downloads | 1,988,312 |
| Total Linux downloads | 1,516,553 |
| Total Windows downloads | 6,736,767 |
| Total macOs downloads | 2,006,140 |
| Total Linux downloads | 1,545,047 |
| Windows % | 65% |
| macOS % | 20% |
| macOS % | 19% |
| Linux % | 15% |
(p) Indicates pre-releases
| Version | Date | Windows | macOS | Linux | Total |
| ----- | ----- | ----- | ----- | ----- | ----- |
| [v3.4.1](https://github.com/laurent22/joplin/releases/tag/v3.4.1) (p) | 2025-05-20T09:59:39Z | 1,523 | 325 | 391 | 2,239 |
| [v3.3.12](https://github.com/laurent22/joplin/releases/tag/v3.3.12) | 2025-05-04T18:12:23Z | 130,348 | 18,775 | 21,928 | 171,051 |
| [v3.3.10](https://github.com/laurent22/joplin/releases/tag/v3.3.10) | 2025-05-02T19:46:15Z | 23,052 | 5,064 | 1,794 | 29,910 |
| [v3.3.9](https://github.com/laurent22/joplin/releases/tag/v3.3.9) | 2025-05-01T21:02:12Z | 21,670 | 6,070 | 1,173 | 28,913 |
| [v3.3.7](https://github.com/laurent22/joplin/releases/tag/v3.3.7) (p) | 2025-04-29T13:47:19Z | 716 | 0 | 160 | 876 |
| [v3.3.6](https://github.com/laurent22/joplin/releases/tag/v3.3.6) (p) | 2025-04-24T12:27:20Z | 1,017 | 262 | 240 | 1,519 |
| [v3.3.5](https://github.com/laurent22/joplin/releases/tag/v3.3.5) (p) | 2025-04-17T13:40:31Z | 1,349 | 281 | 297 | 1,927 |
| [v3.3.4](https://github.com/laurent22/joplin/releases/tag/v3.3.4) (p) | 2025-04-07T20:23:35Z | 1,591 | 385 | 363 | 2,339 |
| [v3.3.3](https://github.com/laurent22/joplin/releases/tag/v3.3.3) (p) | 2025-03-16T11:52:33Z | 2,665 | 797 | 930 | 4,392 |
| [v3.2.13](https://github.com/laurent22/joplin/releases/tag/v3.2.13) | 2025-02-28T14:38:21Z | 220,987 | 32,528 | 44,285 | 297,800 |
| [v3.3.2](https://github.com/laurent22/joplin/releases/tag/v3.3.2) (p) | 2025-02-19T17:34:26Z | 2,332 | 558 | 639 | 3,529 |
| [v3.3.1](https://github.com/laurent22/joplin/releases/tag/v3.3.1) (p) | 2025-02-16T17:06:26Z | 828 | 166 | 162 | 1,156 |
| [v3.2.12](https://github.com/laurent22/joplin/releases/tag/v3.2.12) | 2025-01-23T23:52:04Z | 151,832 | 25,189 | 27,900 | 204,921 |
| [v3.2.11](https://github.com/laurent22/joplin/releases/tag/v3.2.11) | 2025-01-13T17:48:21Z | 65,814 | 14,811 | 6,855 | 87,480 |
| [v3.2.10](https://github.com/laurent22/joplin/releases/tag/v3.2.10) (p) | 2025-01-10T10:17:28Z | 1,279 | 161 | 184 | 1,624 |
| [v3.2.9](https://github.com/laurent22/joplin/releases/tag/v3.2.9) (p) | 2025-01-09T22:58:42Z | 354 | 84 | 50 | 488 |
| [v3.2.7](https://github.com/laurent22/joplin/releases/tag/v3.2.7) (p) | 2025-01-06T16:35:41Z | 862 | 150 | 809 | 1,821 |
| [v3.2.6](https://github.com/laurent22/joplin/releases/tag/v3.2.6) (p) | 2024-12-23T21:54:40Z | 1,723 | 329 | 474 | 2,526 |
| [v3.2.5](https://github.com/laurent22/joplin/releases/tag/v3.2.5) (p) | 2024-12-18T10:41:13Z | 986 | 193 | 217 | 1,396 |
| [v3.2.4](https://github.com/laurent22/joplin/releases/tag/v3.2.4) (p) | 2024-12-12T17:59:52Z | 1,002 | 146 | 232 | 1,380 |
| [v3.2.3](https://github.com/laurent22/joplin/releases/tag/v3.2.3) (p) | 2024-11-18T00:09:05Z | 2,688 | 548 | 883 | 4,119 |
| [v3.2.1](https://github.com/laurent22/joplin/releases/tag/v3.2.1) (p) | 2024-11-10T16:16:27Z | 1,232 | 227 | 345 | 1,804 |
| [v3.1.24](https://github.com/laurent22/joplin/releases/tag/v3.1.24) | 2024-11-09T15:08:29Z | 208,026 | 33,468 | 43,792 | 285,286 |
| [v3.1.23](https://github.com/laurent22/joplin/releases/tag/v3.1.23) | 2024-11-07T10:56:45Z | 27,269 | 6,728 | 1,520 | 35,517 |
| [v3.1.22](https://github.com/laurent22/joplin/releases/tag/v3.1.22) | 2024-11-05T08:59:32Z | 29,049 | 8,695 | 1,223 | 38,967 |
| [v3.1.20](https://github.com/laurent22/joplin/releases/tag/v3.1.20) | 2024-10-22T12:21:32Z | 94,807 | 19,206 | 13,917 | 127,930 |
| [v3.1.18](https://github.com/laurent22/joplin/releases/tag/v3.1.18) (p) | 2024-10-11T23:27:10Z | 1,504 | 279 | 587 | 2,370 |
| [v3.1.17](https://github.com/laurent22/joplin/releases/tag/v3.1.17) (p) | 2024-09-26T11:57:54Z | 1,698 | 348 | 531 | 2,577 |
| [v3.1.15](https://github.com/laurent22/joplin/releases/tag/v3.1.15) (p) | 2024-09-17T09:15:10Z | 1,177 | 223 | 492 | 1,892 |
| [v3.1.8](https://github.com/laurent22/joplin/releases/tag/v3.1.8) (p) | 2024-09-08T20:32:44Z | 1,217 | 252 | 332 | 1,801 |
| [v3.1.6](https://github.com/laurent22/joplin/releases/tag/v3.1.6) (p) | 2024-09-02T13:19:40Z | 960 | 226 | 425 | 1,611 |
| [v3.1.4](https://github.com/laurent22/joplin/releases/tag/v3.1.4) (p) | 2024-08-27T17:46:38Z | 941 | 174 | 247 | 1,362 |
| [v3.0.15](https://github.com/laurent22/joplin/releases/tag/v3.0.15) | 2024-08-21T09:19:58Z | 203,212 | 37,802 | 45,129 | 286,143 |
| [v3.1.3](https://github.com/laurent22/joplin/releases/tag/v3.1.3) (p) | 2024-08-17T13:08:21Z | 1,226 | 274 | 483 | 1,983 |
| [v3.1.2](https://github.com/laurent22/joplin/releases/tag/v3.1.2) (p) | 2024-08-16T09:00:59Z | 436 | 101 | 82 | 619 |
| [v3.1.1](https://github.com/laurent22/joplin/releases/tag/v3.1.1) (p) | 2024-08-10T11:36:02Z | 1,078 | 201 | 260 | 1,539 |
| [v2.14.23](https://github.com/laurent22/joplin/releases/tag/v2.14.23) | 2024-08-07T11:15:25Z | 10,759 | 2,636 | 619 | 14,014 |
| [v3.0.14](https://github.com/laurent22/joplin/releases/tag/v3.0.14) | 2024-07-28T13:55:50Z | 89,307 | 18,570 | 18,816 | 126,693 |
| [v3.0.12](https://github.com/laurent22/joplin/releases/tag/v3.0.12) | 2024-07-02T17:11:14Z | 44,191 | 12,830 | 7,334 | 64,355 |
| [v3.0.11](https://github.com/laurent22/joplin/releases/tag/v3.0.11) (p) | 2024-06-29T10:20:02Z | 839 | 162 | 264 | 1,265 |
| [v3.0.10](https://github.com/laurent22/joplin/releases/tag/v3.0.10) (p) | 2024-06-19T15:24:07Z | 1,616 | 289 | 559 | 2,464 |
| [v3.0.9](https://github.com/laurent22/joplin/releases/tag/v3.0.9) (p) | 2024-06-12T19:07:50Z | 1,264 | 258 | 377 | 1,899 |
| [v3.0.8](https://github.com/laurent22/joplin/releases/tag/v3.0.8) (p) | 2024-05-22T14:20:45Z | 2,658 | 0 | 911 | 3,569 |
| [v2.14.22](https://github.com/laurent22/joplin/releases/tag/v2.14.22) | 2024-05-22T19:19:02Z | 142,918 | 30,755 | 25,463 | 199,136 |
| [v3.0.6](https://github.com/laurent22/joplin/releases/tag/v3.0.6) (p) | 2024-04-27T13:16:04Z | 3,003 | 687 | 860 | 4,550 |
| [v3.0.3](https://github.com/laurent22/joplin/releases/tag/v3.0.3) (p) | 2024-04-18T15:41:38Z | 1,480 | 314 | 331 | 2,125 |
| [v3.0.2](https://github.com/laurent22/joplin/releases/tag/v3.0.2) (p) | 2024-03-21T18:18:49Z | 2,864 | 718 | 1,098 | 4,680 |
| [v2.14.20](https://github.com/laurent22/joplin/releases/tag/v2.14.20) | 2024-03-18T17:05:17Z | 192,850 | 39,529 | 38,229 | 270,608 |
| [v2.14.19](https://github.com/laurent22/joplin/releases/tag/v2.14.19) | 2024-03-08T10:45:16Z | 64,882 | 18,469 | 8,550 | 91,901 |
| [v2.14.17](https://github.com/laurent22/joplin/releases/tag/v2.14.17) | 2024-03-01T18:10:26Z | 63,543 | 18,636 | 7,617 | 89,796 |
| [v2.14.16](https://github.com/laurent22/joplin/releases/tag/v2.14.16) (p) | 2024-02-22T22:49:10Z | 1,347 | 283 | 377 | 2,007 |
| [v2.14.15](https://github.com/laurent22/joplin/releases/tag/v2.14.15) (p) | 2024-02-19T11:24:57Z | 851 | 179 | 189 | 1,219 |
| [v2.14.14](https://github.com/laurent22/joplin/releases/tag/v2.14.14) (p) | 2024-02-10T16:03:08Z | 1,270 | 242 | 378 | 1,890 |
| [v2.14.13](https://github.com/laurent22/joplin/releases/tag/v2.14.13) (p) | 2024-02-09T16:31:54Z | 409 | 80 | 79 | 568 |
| [v2.14.12](https://github.com/laurent22/joplin/releases/tag/v2.14.12) (p) | 2024-02-03T12:11:47Z | 978 | 217 | 244 | 1,439 |
| [v2.14.11](https://github.com/laurent22/joplin/releases/tag/v2.14.11) (p) | 2024-01-26T11:53:05Z | 1,264 | 258 | 459 | 1,981 |
| [v2.14.10](https://github.com/laurent22/joplin/releases/tag/v2.14.10) (p) | 2024-01-18T22:45:04Z | 2,085 | 267 | 370 | 2,722 |
| [v2.13.15](https://github.com/laurent22/joplin/releases/tag/v2.13.15) | 2024-01-15T13:01:19Z | 149,838 | 34,150 | 29,260 | 213,248 |
| [v2.13.14](https://github.com/laurent22/joplin/releases/tag/v2.13.14) | 2024-01-13T19:11:04Z | 19,929 | 7,826 | 2,361 | 30,116 |
| [v2.14.9](https://github.com/laurent22/joplin/releases/tag/v2.14.9) (p) | 2024-01-11T22:17:59Z | 1,055 | 0 | 241 | 1,296 |
| [v2.14.8](https://github.com/laurent22/joplin/releases/tag/v2.14.8) (p) | 2024-01-09T22:57:07Z | 753 | 235 | 173 | 1,161 |
| [v2.14.7](https://github.com/laurent22/joplin/releases/tag/v2.14.7) (p) | 2024-01-08T11:51:49Z | 621 | 127 | 173 | 921 |
| [v2.14.6](https://github.com/laurent22/joplin/releases/tag/v2.14.6) (p) | 2024-01-06T16:38:32Z | 727 | 149 | 149 | 1,025 |
| [v2.13.13](https://github.com/laurent22/joplin/releases/tag/v2.13.13) | 2024-01-06T13:33:11Z | 53,990 | 15,933 | 6,345 | 76,268 |
| [v2.13.12](https://github.com/laurent22/joplin/releases/tag/v2.13.12) | 2023-12-31T16:08:02Z | 45,268 | 14,297 | 5,083 | 64,648 |
| [v2.13.11](https://github.com/laurent22/joplin/releases/tag/v2.13.11) | 2023-12-24T12:58:53Z | 46,130 | 13,262 | 5,973 | 65,365 |
| [v2.13.10](https://github.com/laurent22/joplin/releases/tag/v2.13.10) | 2023-12-22T10:11:08Z | 19,865 | 8,696 | 1,470 | 30,031 |
| [v2.13.9](https://github.com/laurent22/joplin/releases/tag/v2.13.9) | 2023-12-09T17:18:58Z | 67,999 | 21,923 | 8,610 | 98,532 |
| [v2.13.8](https://github.com/laurent22/joplin/releases/tag/v2.13.8) | 2023-12-03T12:07:08Z | 50,709 | 17,902 | 5,214 | 73,825 |
| [v2.13.6](https://github.com/laurent22/joplin/releases/tag/v2.13.6) (p) | 2023-11-17T19:24:03Z | 2,157 | 446 | 573 | 3,176 |
| [v2.13.5](https://github.com/laurent22/joplin/releases/tag/v2.13.5) (p) | 2023-11-09T20:24:09Z | 1,467 | 339 | 440 | 2,246 |
| [v3.3.13](https://github.com/laurent22/joplin/releases/tag/v3.3.13) | 2025-06-09T20:13:30Z | 125,532 | 13,790 | 20,996 | 160,318 |
| [v3.4.1](https://github.com/laurent22/joplin/releases/tag/v3.4.1) (p) | 2025-05-20T09:59:39Z | 3,318 | 888 | 1,043 | 5,249 |
| [v3.3.12](https://github.com/laurent22/joplin/releases/tag/v3.3.12) | 2025-05-04T18:12:23Z | 156,635 | 21,490 | 28,133 | 206,258 |
| [v3.3.10](https://github.com/laurent22/joplin/releases/tag/v3.3.10) | 2025-05-02T19:46:15Z | 24,855 | 5,075 | 1,808 | 31,738 |
| [v3.3.9](https://github.com/laurent22/joplin/releases/tag/v3.3.9) | 2025-05-01T21:02:12Z | 23,518 | 6,090 | 1,174 | 30,782 |
| [v3.3.7](https://github.com/laurent22/joplin/releases/tag/v3.3.7) (p) | 2025-04-29T13:47:19Z | 726 | 0 | 162 | 888 |
| [v3.3.6](https://github.com/laurent22/joplin/releases/tag/v3.3.6) (p) | 2025-04-24T12:27:20Z | 1,030 | 264 | 242 | 1,536 |
| [v3.3.5](https://github.com/laurent22/joplin/releases/tag/v3.3.5) (p) | 2025-04-17T13:40:31Z | 1,360 | 283 | 298 | 1,941 |
| [v3.3.4](https://github.com/laurent22/joplin/releases/tag/v3.3.4) (p) | 2025-04-07T20:23:35Z | 1,598 | 390 | 364 | 2,352 |
| [v3.3.3](https://github.com/laurent22/joplin/releases/tag/v3.3.3) (p) | 2025-03-16T11:52:33Z | 2,671 | 801 | 963 | 4,435 |
| [v3.2.13](https://github.com/laurent22/joplin/releases/tag/v3.2.13) | 2025-02-28T14:38:21Z | 221,882 | 32,849 | 44,434 | 299,165 |
| [v3.3.2](https://github.com/laurent22/joplin/releases/tag/v3.3.2) (p) | 2025-02-19T17:34:26Z | 2,334 | 560 | 646 | 3,540 |
| [v3.3.1](https://github.com/laurent22/joplin/releases/tag/v3.3.1) (p) | 2025-02-16T17:06:26Z | 831 | 172 | 167 | 1,170 |
| [v3.2.12](https://github.com/laurent22/joplin/releases/tag/v3.2.12) | 2025-01-23T23:52:04Z | 152,204 | 25,222 | 27,919 | 205,345 |
| [v3.2.11](https://github.com/laurent22/joplin/releases/tag/v3.2.11) | 2025-01-13T17:48:21Z | 66,131 | 14,815 | 6,859 | 87,805 |
| [v3.2.10](https://github.com/laurent22/joplin/releases/tag/v3.2.10) (p) | 2025-01-10T10:17:28Z | 1,428 | 162 | 185 | 1,775 |
| [v3.2.9](https://github.com/laurent22/joplin/releases/tag/v3.2.9) (p) | 2025-01-09T22:58:42Z | 356 | 87 | 51 | 494 |
| [v3.2.7](https://github.com/laurent22/joplin/releases/tag/v3.2.7) (p) | 2025-01-06T16:35:41Z | 863 | 152 | 841 | 1,856 |
| [v3.2.6](https://github.com/laurent22/joplin/releases/tag/v3.2.6) (p) | 2024-12-23T21:54:40Z | 1,736 | 331 | 475 | 2,542 |
| [v3.2.5](https://github.com/laurent22/joplin/releases/tag/v3.2.5) (p) | 2024-12-18T10:41:13Z | 992 | 195 | 219 | 1,406 |
| [v3.2.4](https://github.com/laurent22/joplin/releases/tag/v3.2.4) (p) | 2024-12-12T17:59:52Z | 1,006 | 148 | 233 | 1,387 |
| [v3.2.3](https://github.com/laurent22/joplin/releases/tag/v3.2.3) (p) | 2024-11-18T00:09:05Z | 2,690 | 551 | 889 | 4,130 |
| [v3.2.1](https://github.com/laurent22/joplin/releases/tag/v3.2.1) (p) | 2024-11-10T16:16:27Z | 1,237 | 230 | 346 | 1,813 |
| [v3.1.24](https://github.com/laurent22/joplin/releases/tag/v3.1.24) | 2024-11-09T15:08:29Z | 208,423 | 33,473 | 43,800 | 285,696 |
| [v3.1.23](https://github.com/laurent22/joplin/releases/tag/v3.1.23) | 2024-11-07T10:56:45Z | 27,494 | 6,731 | 1,521 | 35,746 |
| [v3.1.22](https://github.com/laurent22/joplin/releases/tag/v3.1.22) | 2024-11-05T08:59:32Z | 29,300 | 8,697 | 1,223 | 39,220 |
| [v3.1.20](https://github.com/laurent22/joplin/releases/tag/v3.1.20) | 2024-10-22T12:21:32Z | 95,254 | 19,245 | 13,920 | 128,419 |
| [v3.1.18](https://github.com/laurent22/joplin/releases/tag/v3.1.18) (p) | 2024-10-11T23:27:10Z | 1,507 | 280 | 588 | 2,375 |
| [v3.1.17](https://github.com/laurent22/joplin/releases/tag/v3.1.17) (p) | 2024-09-26T11:57:54Z | 1,699 | 350 | 532 | 2,581 |
| [v3.1.15](https://github.com/laurent22/joplin/releases/tag/v3.1.15) (p) | 2024-09-17T09:15:10Z | 1,180 | 224 | 494 | 1,898 |
| [v3.1.8](https://github.com/laurent22/joplin/releases/tag/v3.1.8) (p) | 2024-09-08T20:32:44Z | 1,222 | 256 | 334 | 1,812 |
| [v3.1.6](https://github.com/laurent22/joplin/releases/tag/v3.1.6) (p) | 2024-09-02T13:19:40Z | 963 | 229 | 426 | 1,618 |
| [v3.1.4](https://github.com/laurent22/joplin/releases/tag/v3.1.4) (p) | 2024-08-27T17:46:38Z | 944 | 176 | 248 | 1,368 |
| [v3.0.15](https://github.com/laurent22/joplin/releases/tag/v3.0.15) | 2024-08-21T09:19:58Z | 203,585 | 37,807 | 45,139 | 286,531 |
| [v3.1.3](https://github.com/laurent22/joplin/releases/tag/v3.1.3) (p) | 2024-08-17T13:08:21Z | 1,235 | 283 | 486 | 2,004 |
| [v3.1.2](https://github.com/laurent22/joplin/releases/tag/v3.1.2) (p) | 2024-08-16T09:00:59Z | 446 | 110 | 87 | 643 |
| [v3.1.1](https://github.com/laurent22/joplin/releases/tag/v3.1.1) (p) | 2024-08-10T11:36:02Z | 1,082 | 203 | 264 | 1,549 |
| [v2.14.23](https://github.com/laurent22/joplin/releases/tag/v2.14.23) | 2024-08-07T11:15:25Z | 10,792 | 2,647 | 624 | 14,063 |
| [v3.0.14](https://github.com/laurent22/joplin/releases/tag/v3.0.14) | 2024-07-28T13:55:50Z | 89,640 | 18,571 | 18,826 | 127,037 |
| [v3.0.12](https://github.com/laurent22/joplin/releases/tag/v3.0.12) | 2024-07-02T17:11:14Z | 44,593 | 12,832 | 7,336 | 64,761 |
| [v3.0.11](https://github.com/laurent22/joplin/releases/tag/v3.0.11) (p) | 2024-06-29T10:20:02Z | 841 | 164 | 265 | 1,270 |
| [v3.0.10](https://github.com/laurent22/joplin/releases/tag/v3.0.10) (p) | 2024-06-19T15:24:07Z | 1,618 | 291 | 559 | 2,468 |
| [v3.0.9](https://github.com/laurent22/joplin/releases/tag/v3.0.9) (p) | 2024-06-12T19:07:50Z | 1,267 | 259 | 378 | 1,904 |
| [v3.0.8](https://github.com/laurent22/joplin/releases/tag/v3.0.8) (p) | 2024-05-22T14:20:45Z | 2,660 | 0 | 912 | 3,572 |
| [v2.14.22](https://github.com/laurent22/joplin/releases/tag/v2.14.22) | 2024-05-22T19:19:02Z | 143,224 | 30,785 | 25,477 | 199,486 |
| [v3.0.6](https://github.com/laurent22/joplin/releases/tag/v3.0.6) (p) | 2024-04-27T13:16:04Z | 3,004 | 687 | 861 | 4,552 |
| [v3.0.3](https://github.com/laurent22/joplin/releases/tag/v3.0.3) (p) | 2024-04-18T15:41:38Z | 1,483 | 320 | 331 | 2,134 |
| [v3.0.2](https://github.com/laurent22/joplin/releases/tag/v3.0.2) (p) | 2024-03-21T18:18:49Z | 2,870 | 722 | 1,101 | 4,693 |
| [v2.14.20](https://github.com/laurent22/joplin/releases/tag/v2.14.20) | 2024-03-18T17:05:17Z | 193,231 | 39,539 | 38,243 | 271,013 |
| [v2.14.19](https://github.com/laurent22/joplin/releases/tag/v2.14.19) | 2024-03-08T10:45:16Z | 65,150 | 18,470 | 8,552 | 92,172 |
| [v2.14.17](https://github.com/laurent22/joplin/releases/tag/v2.14.17) | 2024-03-01T18:10:26Z | 63,726 | 18,640 | 7,618 | 89,984 |
| [v2.14.16](https://github.com/laurent22/joplin/releases/tag/v2.14.16) (p) | 2024-02-22T22:49:10Z | 1,349 | 284 | 378 | 2,011 |
| [v2.14.15](https://github.com/laurent22/joplin/releases/tag/v2.14.15) (p) | 2024-02-19T11:24:57Z | 853 | 180 | 191 | 1,224 |
| [v2.14.14](https://github.com/laurent22/joplin/releases/tag/v2.14.14) (p) | 2024-02-10T16:03:08Z | 1,272 | 243 | 379 | 1,894 |
| [v2.14.13](https://github.com/laurent22/joplin/releases/tag/v2.14.13) (p) | 2024-02-09T16:31:54Z | 412 | 82 | 80 | 574 |
| [v2.14.12](https://github.com/laurent22/joplin/releases/tag/v2.14.12) (p) | 2024-02-03T12:11:47Z | 980 | 218 | 247 | 1,445 |
| [v2.14.11](https://github.com/laurent22/joplin/releases/tag/v2.14.11) (p) | 2024-01-26T11:53:05Z | 1,266 | 259 | 464 | 1,989 |
| [v2.14.10](https://github.com/laurent22/joplin/releases/tag/v2.14.10) (p) | 2024-01-18T22:45:04Z | 2,087 | 269 | 371 | 2,727 |
| [v2.13.15](https://github.com/laurent22/joplin/releases/tag/v2.13.15) | 2024-01-15T13:01:19Z | 150,122 | 34,151 | 29,262 | 213,535 |
| [v2.13.14](https://github.com/laurent22/joplin/releases/tag/v2.13.14) | 2024-01-13T19:11:04Z | 20,115 | 7,827 | 2,363 | 30,305 |
| [v2.14.9](https://github.com/laurent22/joplin/releases/tag/v2.14.9) (p) | 2024-01-11T22:17:59Z | 1,057 | 0 | 241 | 1,298 |
| [v2.14.8](https://github.com/laurent22/joplin/releases/tag/v2.14.8) (p) | 2024-01-09T22:57:07Z | 757 | 237 | 174 | 1,168 |
| [v2.14.7](https://github.com/laurent22/joplin/releases/tag/v2.14.7) (p) | 2024-01-08T11:51:49Z | 622 | 128 | 174 | 924 |
| [v2.14.6](https://github.com/laurent22/joplin/releases/tag/v2.14.6) (p) | 2024-01-06T16:38:32Z | 728 | 150 | 151 | 1,029 |
| [v2.13.13](https://github.com/laurent22/joplin/releases/tag/v2.13.13) | 2024-01-06T13:33:11Z | 54,202 | 15,934 | 6,346 | 76,482 |
| [v2.13.12](https://github.com/laurent22/joplin/releases/tag/v2.13.12) | 2023-12-31T16:08:02Z | 45,490 | 14,298 | 5,084 | 64,872 |
| [v2.13.11](https://github.com/laurent22/joplin/releases/tag/v2.13.11) | 2023-12-24T12:58:53Z | 46,271 | 13,263 | 5,974 | 65,508 |
| [v2.13.10](https://github.com/laurent22/joplin/releases/tag/v2.13.10) | 2023-12-22T10:11:08Z | 19,973 | 8,697 | 1,477 | 30,147 |
| [v2.13.9](https://github.com/laurent22/joplin/releases/tag/v2.13.9) | 2023-12-09T17:18:58Z | 68,224 | 21,924 | 8,611 | 98,759 |
| [v2.13.8](https://github.com/laurent22/joplin/releases/tag/v2.13.8) | 2023-12-03T12:07:08Z | 50,889 | 17,904 | 5,216 | 74,009 |
| [v2.13.6](https://github.com/laurent22/joplin/releases/tag/v2.13.6) (p) | 2023-11-17T19:24:03Z | 2,159 | 446 | 576 | 3,181 |
| [v2.13.5](https://github.com/laurent22/joplin/releases/tag/v2.13.5) (p) | 2023-11-09T20:24:09Z | 1,469 | 340 | 440 | 2,249 |
| [v2.13.4](https://github.com/laurent22/joplin/releases/tag/v2.13.4) (p) | 2023-10-31T00:01:00Z | 1,540 | 372 | 485 | 2,397 |
| [v2.13.3](https://github.com/laurent22/joplin/releases/tag/v2.13.3) (p) | 2023-10-24T09:25:33Z | 1,303 | 281 | 301 | 1,885 |
| [v2.12.19](https://github.com/laurent22/joplin/releases/tag/v2.12.19) | 2023-10-21T09:39:18Z | 166,838 | 43,656 | 27,867 | 238,361 |
| [v2.13.3](https://github.com/laurent22/joplin/releases/tag/v2.13.3) (p) | 2023-10-24T09:25:33Z | 1,303 | 281 | 302 | 1,886 |
| [v2.12.19](https://github.com/laurent22/joplin/releases/tag/v2.12.19) | 2023-10-21T09:39:18Z | 167,229 | 43,659 | 27,880 | 238,768 |
| [v2.13.2](https://github.com/laurent22/joplin/releases/tag/v2.13.2) (p) | 2023-10-06T17:00:07Z | 2,034 | 503 | 704 | 3,241 |
| [v2.12.18](https://github.com/laurent22/joplin/releases/tag/v2.12.18) | 2023-09-22T14:37:24Z | 109,047 | 36,516 | 18,706 | 164,269 |
| [v2.12.17](https://github.com/laurent22/joplin/releases/tag/v2.12.17) | 2023-09-14T21:54:52Z | 47,458 | 21,032 | 6,632 | 75,122 |
| [v2.12.18](https://github.com/laurent22/joplin/releases/tag/v2.12.18) | 2023-09-22T14:37:24Z | 109,356 | 36,520 | 18,709 | 164,585 |
| [v2.12.17](https://github.com/laurent22/joplin/releases/tag/v2.12.17) | 2023-09-14T21:54:52Z | 47,687 | 21,032 | 6,633 | 75,352 |
| [v2.13.1](https://github.com/laurent22/joplin/releases/tag/v2.13.1) (p) | 2023-09-13T09:31:50Z | 1,390 | 427 | 667 | 2,484 |
| [v2.12.16](https://github.com/laurent22/joplin/releases/tag/v2.12.16) | 2023-09-11T22:33:37Z | 28,383 | 14,668 | 2,452 | 45,503 |
| [v2.12.15](https://github.com/laurent22/joplin/releases/tag/v2.12.15) | 2023-08-27T11:35:39Z | 64,640 | 28,088 | 8,402 | 101,130 |
| [v2.12.12](https://github.com/laurent22/joplin/releases/tag/v2.12.12) (p) | 2023-08-19T22:44:56Z | 2,888 | 387 | 428 | 3,703 |
| [v2.12.10](https://github.com/laurent22/joplin/releases/tag/v2.12.10) (p) | 2023-07-30T18:25:58Z | 7,520 | 3,817 | 914 | 12,251 |
| [v2.12.9](https://github.com/laurent22/joplin/releases/tag/v2.12.9) (p) | 2023-07-25T16:06:08Z | 2,352 | 369 | 321 | 3,042 |
| [v2.12.7](https://github.com/laurent22/joplin/releases/tag/v2.12.7) (p) | 2023-07-13T12:55:31Z | 2,165 | 664 | 591 | 3,420 |
| [v2.12.5](https://github.com/laurent22/joplin/releases/tag/v2.12.5) (p) | 2023-07-12T15:03:46Z | 1,905 | 164 | 151 | 2,220 |
| [v2.12.4](https://github.com/laurent22/joplin/releases/tag/v2.12.4) (p) | 2023-07-07T22:36:53Z | 1,064 | 437 | 217 | 1,718 |
| [v2.12.3](https://github.com/laurent22/joplin/releases/tag/v2.12.3) (p) | 2023-07-07T10:16:55Z | 394 | 192 | 95 | 681 |
| [v2.11.11](https://github.com/laurent22/joplin/releases/tag/v2.11.11) | 2023-06-23T15:16:37Z | 189,480 | 67,230 | 38,865 | 295,575 |
| [v2.11.9](https://github.com/laurent22/joplin/releases/tag/v2.11.9) (p) | 2023-06-06T16:23:27Z | 2,303 | 574 | 746 | 3,623 |
| [v2.11.6](https://github.com/laurent22/joplin/releases/tag/v2.11.6) (p) | 2023-05-31T20:13:08Z | 1,162 | 435 | 345 | 1,942 |
| [v2.11.5](https://github.com/laurent22/joplin/releases/tag/v2.11.5) (p) | 2023-05-28T00:41:40Z | 1,029 | 307 | 282 | 1,618 |
| [v2.10.19](https://github.com/laurent22/joplin/releases/tag/v2.10.19) | 2023-05-17T12:25:41Z | 124,680 | 48,335 | 22,477 | 195,492 |
| [v2.11.4](https://github.com/laurent22/joplin/releases/tag/v2.11.4) (p) | 2023-05-16T10:02:21Z | 1,083 | 468 | 415 | 1,966 |
| [v2.11.3](https://github.com/laurent22/joplin/releases/tag/v2.11.3) (p) | 2023-05-16T09:09:57Z | 138 | 40 | 40 | 218 |
| [v2.10.18](https://github.com/laurent22/joplin/releases/tag/v2.10.18) | 2023-05-09T13:27:43Z | 56,526 | 24,257 | 6,791 | 87,574 |
| [v2.10.17](https://github.com/laurent22/joplin/releases/tag/v2.10.17) | 2023-05-08T17:27:28Z | 19,229 | 11,508 | 887 | 31,624 |
| [v2.10.16](https://github.com/laurent22/joplin/releases/tag/v2.10.16) | 2023-04-27T09:27:45Z | 9,488 | 4,257 | 780 | 14,525 |
| [v2.10.15](https://github.com/laurent22/joplin/releases/tag/v2.10.15) (p) | 2023-04-26T22:02:16Z | 374 | 141 | 59 | 574 |
| [v2.10.13](https://github.com/laurent22/joplin/releases/tag/v2.10.13) (p) | 2023-04-03T16:53:46Z | 4,659 | 829 | 1,078 | 6,566 |
| [v2.10.12](https://github.com/laurent22/joplin/releases/tag/v2.10.12) (p) | 2023-03-23T12:17:13Z | 3,625 | 519 | 604 | 4,748 |
| [v2.10.11](https://github.com/laurent22/joplin/releases/tag/v2.10.11) (p) | 2023-03-17T10:54:02Z | 2,995 | 383 | 393 | 3,771 |
| [v2.10.10](https://github.com/laurent22/joplin/releases/tag/v2.10.10) (p) | 2023-03-13T23:16:37Z | 2,501 | 284 | 255 | 3,040 |
| [v2.10.9](https://github.com/laurent22/joplin/releases/tag/v2.10.9) (p) | 2023-03-12T16:16:45Z | 2,035 | 214 | 297 | 2,546 |
| [v2.10.8](https://github.com/laurent22/joplin/releases/tag/v2.10.8) (p) | 2023-02-26T12:53:55Z | 4,517 | 573 | 872 | 5,962 |
| [v2.10.7](https://github.com/laurent22/joplin/releases/tag/v2.10.7) (p) | 2023-02-24T10:56:20Z | 2,187 | 191 | 280 | 2,658 |
| [v2.10.6](https://github.com/laurent22/joplin/releases/tag/v2.10.6) (p) | 2023-02-20T14:00:05Z | 2,978 | 342 | 290 | 3,610 |
| [v2.12.16](https://github.com/laurent22/joplin/releases/tag/v2.12.16) | 2023-09-11T22:33:37Z | 28,478 | 14,668 | 2,452 | 45,598 |
| [v2.12.15](https://github.com/laurent22/joplin/releases/tag/v2.12.15) | 2023-08-27T11:35:39Z | 64,835 | 28,100 | 8,417 | 101,352 |
| [v2.12.12](https://github.com/laurent22/joplin/releases/tag/v2.12.12) (p) | 2023-08-19T22:44:56Z | 3,005 | 387 | 429 | 3,821 |
| [v2.12.10](https://github.com/laurent22/joplin/releases/tag/v2.12.10) (p) | 2023-07-30T18:25:58Z | 7,631 | 3,817 | 914 | 12,362 |
| [v2.12.9](https://github.com/laurent22/joplin/releases/tag/v2.12.9) (p) | 2023-07-25T16:06:08Z | 2,457 | 369 | 322 | 3,148 |
| [v2.12.7](https://github.com/laurent22/joplin/releases/tag/v2.12.7) (p) | 2023-07-13T12:55:31Z | 2,172 | 664 | 593 | 3,429 |
| [v2.12.5](https://github.com/laurent22/joplin/releases/tag/v2.12.5) (p) | 2023-07-12T15:03:46Z | 1,931 | 165 | 156 | 2,252 |
| [v2.12.4](https://github.com/laurent22/joplin/releases/tag/v2.12.4) (p) | 2023-07-07T22:36:53Z | 1,071 | 448 | 220 | 1,739 |
| [v2.12.3](https://github.com/laurent22/joplin/releases/tag/v2.12.3) (p) | 2023-07-07T10:16:55Z | 401 | 202 | 99 | 702 |
| [v2.11.11](https://github.com/laurent22/joplin/releases/tag/v2.11.11) | 2023-06-23T15:16:37Z | 189,759 | 67,237 | 38,874 | 295,870 |
| [v2.11.9](https://github.com/laurent22/joplin/releases/tag/v2.11.9) (p) | 2023-06-06T16:23:27Z | 2,310 | 578 | 749 | 3,637 |
| [v2.11.6](https://github.com/laurent22/joplin/releases/tag/v2.11.6) (p) | 2023-05-31T20:13:08Z | 1,173 | 439 | 348 | 1,960 |
| [v2.11.5](https://github.com/laurent22/joplin/releases/tag/v2.11.5) (p) | 2023-05-28T00:41:40Z | 1,035 | 311 | 286 | 1,632 |
| [v2.10.19](https://github.com/laurent22/joplin/releases/tag/v2.10.19) | 2023-05-17T12:25:41Z | 124,926 | 48,338 | 22,482 | 195,746 |
| [v2.11.4](https://github.com/laurent22/joplin/releases/tag/v2.11.4) (p) | 2023-05-16T10:02:21Z | 1,089 | 472 | 419 | 1,980 |
| [v2.11.3](https://github.com/laurent22/joplin/releases/tag/v2.11.3) (p) | 2023-05-16T09:09:57Z | 146 | 44 | 43 | 233 |
| [v2.10.18](https://github.com/laurent22/joplin/releases/tag/v2.10.18) | 2023-05-09T13:27:43Z | 56,717 | 24,261 | 6,801 | 87,779 |
| [v2.10.17](https://github.com/laurent22/joplin/releases/tag/v2.10.17) | 2023-05-08T17:27:28Z | 19,372 | 11,512 | 892 | 31,776 |
| [v2.10.16](https://github.com/laurent22/joplin/releases/tag/v2.10.16) | 2023-04-27T09:27:45Z | 9,680 | 4,260 | 784 | 14,724 |
| [v2.10.15](https://github.com/laurent22/joplin/releases/tag/v2.10.15) (p) | 2023-04-26T22:02:16Z | 381 | 145 | 62 | 588 |
| [v2.10.13](https://github.com/laurent22/joplin/releases/tag/v2.10.13) (p) | 2023-04-03T16:53:46Z | 4,764 | 829 | 1,078 | 6,671 |
| [v2.10.12](https://github.com/laurent22/joplin/releases/tag/v2.10.12) (p) | 2023-03-23T12:17:13Z | 3,734 | 519 | 604 | 4,857 |
| [v2.10.11](https://github.com/laurent22/joplin/releases/tag/v2.10.11) (p) | 2023-03-17T10:54:02Z | 3,103 | 383 | 393 | 3,879 |
| [v2.10.10](https://github.com/laurent22/joplin/releases/tag/v2.10.10) (p) | 2023-03-13T23:16:37Z | 2,608 | 284 | 256 | 3,148 |
| [v2.10.9](https://github.com/laurent22/joplin/releases/tag/v2.10.9) (p) | 2023-03-12T16:16:45Z | 2,146 | 214 | 297 | 2,657 |
| [v2.10.8](https://github.com/laurent22/joplin/releases/tag/v2.10.8) (p) | 2023-02-26T12:53:55Z | 4,628 | 573 | 872 | 6,073 |
| [v2.10.7](https://github.com/laurent22/joplin/releases/tag/v2.10.7) (p) | 2023-02-24T10:56:20Z | 2,295 | 191 | 280 | 2,766 |
| [v2.10.6](https://github.com/laurent22/joplin/releases/tag/v2.10.6) (p) | 2023-02-20T14:00:05Z | 3,084 | 342 | 290 | 3,716 |
| [v2.10.5](https://github.com/laurent22/joplin/releases/tag/v2.10.5) | 2023-01-16T15:00:53Z | 367 | 103 | 309 | 779 |
| [v2.10.4](https://github.com/laurent22/joplin/releases/tag/v2.10.4) (p) | 2023-01-05T13:09:20Z | 8,201 | 1,303 | 1,812 | 11,316 |
| [v2.10.3](https://github.com/laurent22/joplin/releases/tag/v2.10.3) (p) | 2022-12-31T15:53:23Z | 2,763 | 314 | 417 | 3,494 |
| [v2.10.2](https://github.com/laurent22/joplin/releases/tag/v2.10.2) (p) | 2022-12-18T18:05:08Z | 4,191 | 591 | 639 | 5,421 |
| [v2.9.17](https://github.com/laurent22/joplin/releases/tag/v2.9.17) | 2022-11-15T10:28:37Z | 335,646 | 108,788 | 83,358 | 527,792 |
| [v2.9.12](https://github.com/laurent22/joplin/releases/tag/v2.9.12) (p) | 2022-11-01T17:06:05Z | 11,379 | 612 | 545 | 12,536 |
| [v2.9.11](https://github.com/laurent22/joplin/releases/tag/v2.9.11) (p) | 2022-10-23T16:09:58Z | 3,528 | 531 | 762 | 4,821 |
| [v2.9.4](https://github.com/laurent22/joplin/releases/tag/v2.9.4) (p) | 2022-08-18T16:52:26Z | 8,578 | 1,868 | 2,200 | 12,646 |
| [v2.10.4](https://github.com/laurent22/joplin/releases/tag/v2.10.4) (p) | 2023-01-05T13:09:20Z | 8,305 | 1,303 | 1,812 | 11,420 |
| [v2.10.3](https://github.com/laurent22/joplin/releases/tag/v2.10.3) (p) | 2022-12-31T15:53:23Z | 2,866 | 314 | 417 | 3,597 |
| [v2.10.2](https://github.com/laurent22/joplin/releases/tag/v2.10.2) (p) | 2022-12-18T18:05:08Z | 4,296 | 591 | 639 | 5,526 |
| [v2.9.17](https://github.com/laurent22/joplin/releases/tag/v2.9.17) | 2022-11-15T10:28:37Z | 335,842 | 108,794 | 83,364 | 528,000 |
| [v2.9.12](https://github.com/laurent22/joplin/releases/tag/v2.9.12) (p) | 2022-11-01T17:06:05Z | 11,482 | 612 | 546 | 12,640 |
| [v2.9.11](https://github.com/laurent22/joplin/releases/tag/v2.9.11) (p) | 2022-10-23T16:09:58Z | 3,634 | 531 | 762 | 4,927 |
| [v2.9.4](https://github.com/laurent22/joplin/releases/tag/v2.9.4) (p) | 2022-08-18T16:52:26Z | 8,687 | 1,868 | 2,201 | 12,756 |
| [v2.9.3](https://github.com/laurent22/joplin/releases/tag/v2.9.3) (p) | 2022-08-18T13:11:09Z | 363 | 92 | 275 | 730 |
| [v2.9.2](https://github.com/laurent22/joplin/releases/tag/v2.9.2) (p) | 2022-08-12T18:12:12Z | 1,533 | 447 | 0 | 1,980 |
| [v2.9.1](https://github.com/laurent22/joplin/releases/tag/v2.9.1) (p) | 2022-07-11T09:59:32Z | 8,011 | 1,343 | 1,412 | 10,766 |
| [v2.8.8](https://github.com/laurent22/joplin/releases/tag/v2.8.8) | 2022-05-17T14:48:06Z | 351,763 | 114,379 | 113,587 | 579,729 |
| [v2.8.7](https://github.com/laurent22/joplin/releases/tag/v2.8.7) (p) | 2022-05-06T11:34:27Z | 4,513 | 366 | 429 | 5,308 |
| [v2.8.6](https://github.com/laurent22/joplin/releases/tag/v2.8.6) (p) | 2022-05-03T10:08:25Z | 4,143 | 403 | 332 | 4,878 |
| [v2.8.5](https://github.com/laurent22/joplin/releases/tag/v2.8.5) (p) | 2022-04-27T13:51:50Z | 4,236 | 370 | 357 | 4,963 |
| [v2.8.4](https://github.com/laurent22/joplin/releases/tag/v2.8.4) (p) | 2022-04-19T18:00:09Z | 4,718 | 590 | 334 | 5,642 |
| [v2.8.2](https://github.com/laurent22/joplin/releases/tag/v2.8.2) (p) | 2022-04-14T11:35:45Z | 4,136 | 280 | 282 | 4,698 |
| [v2.7.15](https://github.com/laurent22/joplin/releases/tag/v2.7.15) | 2022-03-17T13:03:23Z | 156,446 | 56,783 | 51,285 | 264,514 |
| [v2.7.14](https://github.com/laurent22/joplin/releases/tag/v2.7.14) | 2022-02-27T11:30:53Z | 34,701 | 16,783 | 4,811 | 56,295 |
| [v2.7.13](https://github.com/laurent22/joplin/releases/tag/v2.7.13) | 2022-02-24T17:42:12Z | 55,125 | 25,726 | 11,724 | 92,575 |
| [v2.7.12](https://github.com/laurent22/joplin/releases/tag/v2.7.12) (p) | 2022-02-14T15:06:14Z | 4,907 | 465 | 504 | 5,876 |
| [v2.7.11](https://github.com/laurent22/joplin/releases/tag/v2.7.11) (p) | 2022-02-12T13:00:02Z | 4,052 | 195 | 174 | 4,421 |
| [v2.7.10](https://github.com/laurent22/joplin/releases/tag/v2.7.10) (p) | 2022-02-11T18:19:09Z | 3,563 | 126 | 95 | 3,784 |
| [v2.7.8](https://github.com/laurent22/joplin/releases/tag/v2.7.8) (p) | 2022-01-19T09:35:27Z | 5,786 | 771 | 830 | 7,387 |
| [v2.7.7](https://github.com/laurent22/joplin/releases/tag/v2.7.7) (p) | 2022-01-18T14:05:07Z | 3,881 | 157 | 144 | 4,182 |
| [v2.7.6](https://github.com/laurent22/joplin/releases/tag/v2.7.6) (p) | 2022-01-17T17:08:28Z | 3,809 | 184 | 121 | 4,114 |
| [v2.6.10](https://github.com/laurent22/joplin/releases/tag/v2.6.10) | 2021-12-19T11:31:16Z | 136,462 | 51,206 | 49,319 | 236,987 |
| [v2.6.9](https://github.com/laurent22/joplin/releases/tag/v2.6.9) | 2021-12-17T11:57:32Z | 19,441 | 9,500 | 3,194 | 32,135 |
| [v2.6.7](https://github.com/laurent22/joplin/releases/tag/v2.6.7) (p) | 2021-12-16T10:47:23Z | 4,025 | 180 | 112 | 4,317 |
| [v2.6.6](https://github.com/laurent22/joplin/releases/tag/v2.6.6) (p) | 2021-12-13T12:31:43Z | 4,020 | 257 | 174 | 4,451 |
| [v2.6.5](https://github.com/laurent22/joplin/releases/tag/v2.6.5) (p) | 2021-12-13T10:07:04Z | 3,355 | 51 | 36 | 3,442 |
| [v2.6.4](https://github.com/laurent22/joplin/releases/tag/v2.6.4) (p) | 2021-12-09T19:53:43Z | 4,091 | 291 | 207 | 4,589 |
| [v2.6.2](https://github.com/laurent22/joplin/releases/tag/v2.6.2) (p) | 2021-11-18T12:19:12Z | 5,892 | 793 | 702 | 7,387 |
| [v2.5.12](https://github.com/laurent22/joplin/releases/tag/v2.5.12) | 2021-11-08T11:07:11Z | 83,180 | 32,507 | 25,236 | 140,923 |
| [v2.5.10](https://github.com/laurent22/joplin/releases/tag/v2.5.10) | 2021-11-01T08:22:42Z | 47,339 | 19,050 | 10,098 | 76,487 |
| [v2.5.8](https://github.com/laurent22/joplin/releases/tag/v2.5.8) | 2021-10-31T11:38:03Z | 16,061 | 6,581 | 2,329 | 24,971 |
| [v2.5.7](https://github.com/laurent22/joplin/releases/tag/v2.5.7) (p) | 2021-10-29T14:47:33Z | 3,624 | 203 | 164 | 3,991 |
| [v2.5.6](https://github.com/laurent22/joplin/releases/tag/v2.5.6) (p) | 2021-10-28T22:03:09Z | 3,676 | 178 | 108 | 3,962 |
| [v2.5.4](https://github.com/laurent22/joplin/releases/tag/v2.5.4) (p) | 2021-10-19T10:10:54Z | 5,168 | 568 | 580 | 6,316 |
| [v2.4.12](https://github.com/laurent22/joplin/releases/tag/v2.4.12) | 2021-10-13T17:24:34Z | 47,788 | 19,984 | 9,789 | 77,561 |
| [v2.5.1](https://github.com/laurent22/joplin/releases/tag/v2.5.1) (p) | 2021-10-02T09:51:58Z | 6,229 | 907 | 947 | 8,083 |
| [v2.4.9](https://github.com/laurent22/joplin/releases/tag/v2.4.9) | 2021-09-29T19:08:58Z | 59,488 | 23,262 | 15,914 | 98,664 |
| [v2.4.8](https://github.com/laurent22/joplin/releases/tag/v2.4.8) (p) | 2021-09-22T19:01:46Z | 10,020 | 1,774 | 535 | 12,329 |
| [v2.4.7](https://github.com/laurent22/joplin/releases/tag/v2.4.7) (p) | 2021-09-19T12:53:22Z | 4,149 | 259 | 210 | 4,618 |
| [v2.4.6](https://github.com/laurent22/joplin/releases/tag/v2.4.6) (p) | 2021-09-09T18:57:17Z | 4,913 | 460 | 520 | 5,893 |
| [v2.4.5](https://github.com/laurent22/joplin/releases/tag/v2.4.5) (p) | 2021-09-06T18:03:28Z | 4,157 | 275 | 225 | 4,657 |
| [v2.4.4](https://github.com/laurent22/joplin/releases/tag/v2.4.4) (p) | 2021-08-30T16:02:51Z | 4,371 | 380 | 373 | 5,124 |
| [v2.4.3](https://github.com/laurent22/joplin/releases/tag/v2.4.3) (p) | 2021-08-28T15:27:32Z | 3,858 | 207 | 180 | 4,245 |
| [v2.4.2](https://github.com/laurent22/joplin/releases/tag/v2.4.2) (p) | 2021-08-27T17:13:21Z | 3,634 | 151 | 91 | 3,876 |
| [v2.4.1](https://github.com/laurent22/joplin/releases/tag/v2.4.1) (p) | 2021-08-21T11:52:30Z | 4,472 | 372 | 337 | 5,181 |
| [v2.3.5](https://github.com/laurent22/joplin/releases/tag/v2.3.5) | 2021-08-17T06:43:30Z | 85,517 | 31,439 | 33,144 | 150,100 |
| [v2.3.3](https://github.com/laurent22/joplin/releases/tag/v2.3.3) | 2021-08-14T09:19:40Z | 18,702 | 6,886 | 4,062 | 29,650 |
| [v2.2.7](https://github.com/laurent22/joplin/releases/tag/v2.2.7) | 2021-08-11T11:03:26Z | 18,568 | 7,524 | 2,607 | 28,699 |
| [v2.2.6](https://github.com/laurent22/joplin/releases/tag/v2.2.6) (p) | 2021-08-09T19:29:20Z | 10,896 | 4,619 | 958 | 16,473 |
| [v2.2.5](https://github.com/laurent22/joplin/releases/tag/v2.2.5) (p) | 2021-08-07T10:35:24Z | 4,206 | 278 | 208 | 4,692 |
| [v2.2.4](https://github.com/laurent22/joplin/releases/tag/v2.2.4) (p) | 2021-08-05T16:42:48Z | 3,862 | 208 | 133 | 4,203 |
| [v2.2.2](https://github.com/laurent22/joplin/releases/tag/v2.2.2) (p) | 2021-07-19T10:28:35Z | 5,785 | 737 | 648 | 7,170 |
| [v2.1.9](https://github.com/laurent22/joplin/releases/tag/v2.1.9) | 2021-07-19T10:28:43Z | 50,195 | 18,962 | 16,821 | 85,978 |
| [v2.2.1](https://github.com/laurent22/joplin/releases/tag/v2.2.1) (p) | 2021-07-09T17:38:25Z | 5,409 | 417 | 394 | 6,220 |
| [v2.1.8](https://github.com/laurent22/joplin/releases/tag/v2.1.8) | 2021-07-03T08:25:16Z | 33,677 | 12,206 | 12,736 | 58,619 |
| [v2.1.7](https://github.com/laurent22/joplin/releases/tag/v2.1.7) | 2021-06-26T19:48:55Z | 16,985 | 6,410 | 3,638 | 27,033 |
| [v2.1.5](https://github.com/laurent22/joplin/releases/tag/v2.1.5) (p) | 2021-06-23T15:08:52Z | 4,306 | 254 | 202 | 4,762 |
| [v2.1.3](https://github.com/laurent22/joplin/releases/tag/v2.1.3) (p) | 2021-06-19T16:32:51Z | 4,373 | 312 | 217 | 4,902 |
| [v2.0.11](https://github.com/laurent22/joplin/releases/tag/v2.0.11) | 2021-06-16T17:55:49Z | 26,740 | 9,281 | 9,905 | 45,926 |
| [v2.0.10](https://github.com/laurent22/joplin/releases/tag/v2.0.10) | 2021-06-16T07:58:29Z | 5,918 | 946 | 399 | 7,263 |
| [v2.0.9](https://github.com/laurent22/joplin/releases/tag/v2.0.9) (p) | 2021-06-12T09:30:30Z | 4,360 | 307 | 899 | 5,566 |
| [v2.0.8](https://github.com/laurent22/joplin/releases/tag/v2.0.8) (p) | 2021-06-10T16:15:08Z | 3,797 | 246 | 599 | 4,642 |
| [v2.9.1](https://github.com/laurent22/joplin/releases/tag/v2.9.1) (p) | 2022-07-11T09:59:32Z | 8,123 | 1,343 | 1,413 | 10,879 |
| [v2.8.8](https://github.com/laurent22/joplin/releases/tag/v2.8.8) | 2022-05-17T14:48:06Z | 351,940 | 114,392 | 113,590 | 579,922 |
| [v2.8.7](https://github.com/laurent22/joplin/releases/tag/v2.8.7) (p) | 2022-05-06T11:34:27Z | 4,623 | 366 | 430 | 5,419 |
| [v2.8.6](https://github.com/laurent22/joplin/releases/tag/v2.8.6) (p) | 2022-05-03T10:08:25Z | 4,254 | 403 | 333 | 4,990 |
| [v2.8.5](https://github.com/laurent22/joplin/releases/tag/v2.8.5) (p) | 2022-04-27T13:51:50Z | 4,345 | 370 | 357 | 5,072 |
| [v2.8.4](https://github.com/laurent22/joplin/releases/tag/v2.8.4) (p) | 2022-04-19T18:00:09Z | 4,823 | 590 | 334 | 5,747 |
| [v2.8.2](https://github.com/laurent22/joplin/releases/tag/v2.8.2) (p) | 2022-04-14T11:35:45Z | 4,247 | 280 | 282 | 4,809 |
| [v2.7.15](https://github.com/laurent22/joplin/releases/tag/v2.7.15) | 2022-03-17T13:03:23Z | 156,564 | 56,784 | 51,287 | 264,635 |
| [v2.7.14](https://github.com/laurent22/joplin/releases/tag/v2.7.14) | 2022-02-27T11:30:53Z | 34,807 | 16,783 | 4,813 | 56,403 |
| [v2.7.13](https://github.com/laurent22/joplin/releases/tag/v2.7.13) | 2022-02-24T17:42:12Z | 55,244 | 25,726 | 11,724 | 92,694 |
| [v2.7.12](https://github.com/laurent22/joplin/releases/tag/v2.7.12) (p) | 2022-02-14T15:06:14Z | 5,011 | 465 | 505 | 5,981 |
| [v2.7.11](https://github.com/laurent22/joplin/releases/tag/v2.7.11) (p) | 2022-02-12T13:00:02Z | 4,167 | 195 | 174 | 4,536 |
| [v2.7.10](https://github.com/laurent22/joplin/releases/tag/v2.7.10) (p) | 2022-02-11T18:19:09Z | 3,678 | 126 | 95 | 3,899 |
| [v2.7.8](https://github.com/laurent22/joplin/releases/tag/v2.7.8) (p) | 2022-01-19T09:35:27Z | 5,899 | 771 | 830 | 7,500 |
| [v2.7.7](https://github.com/laurent22/joplin/releases/tag/v2.7.7) (p) | 2022-01-18T14:05:07Z | 3,989 | 157 | 144 | 4,290 |
| [v2.7.6](https://github.com/laurent22/joplin/releases/tag/v2.7.6) (p) | 2022-01-17T17:08:28Z | 3,917 | 184 | 121 | 4,222 |
| [v2.6.10](https://github.com/laurent22/joplin/releases/tag/v2.6.10) | 2021-12-19T11:31:16Z | 136,611 | 51,208 | 49,320 | 237,139 |
| [v2.6.9](https://github.com/laurent22/joplin/releases/tag/v2.6.9) | 2021-12-17T11:57:32Z | 19,558 | 9,502 | 3,194 | 32,254 |
| [v2.6.7](https://github.com/laurent22/joplin/releases/tag/v2.6.7) (p) | 2021-12-16T10:47:23Z | 4,136 | 180 | 112 | 4,428 |
| [v2.6.6](https://github.com/laurent22/joplin/releases/tag/v2.6.6) (p) | 2021-12-13T12:31:43Z | 4,135 | 257 | 174 | 4,566 |
| [v2.6.5](https://github.com/laurent22/joplin/releases/tag/v2.6.5) (p) | 2021-12-13T10:07:04Z | 3,462 | 51 | 36 | 3,549 |
| [v2.6.4](https://github.com/laurent22/joplin/releases/tag/v2.6.4) (p) | 2021-12-09T19:53:43Z | 4,200 | 291 | 207 | 4,698 |
| [v2.6.2](https://github.com/laurent22/joplin/releases/tag/v2.6.2) (p) | 2021-11-18T12:19:12Z | 5,997 | 793 | 702 | 7,492 |
| [v2.5.12](https://github.com/laurent22/joplin/releases/tag/v2.5.12) | 2021-11-08T11:07:11Z | 83,335 | 32,507 | 25,236 | 141,078 |
| [v2.5.10](https://github.com/laurent22/joplin/releases/tag/v2.5.10) | 2021-11-01T08:22:42Z | 47,459 | 19,051 | 10,098 | 76,608 |
| [v2.5.8](https://github.com/laurent22/joplin/releases/tag/v2.5.8) | 2021-10-31T11:38:03Z | 16,182 | 6,581 | 2,332 | 25,095 |
| [v2.5.7](https://github.com/laurent22/joplin/releases/tag/v2.5.7) (p) | 2021-10-29T14:47:33Z | 3,731 | 203 | 164 | 4,098 |
| [v2.5.6](https://github.com/laurent22/joplin/releases/tag/v2.5.6) (p) | 2021-10-28T22:03:09Z | 3,786 | 178 | 108 | 4,072 |
| [v2.5.4](https://github.com/laurent22/joplin/releases/tag/v2.5.4) (p) | 2021-10-19T10:10:54Z | 5,278 | 568 | 580 | 6,426 |
| [v2.4.12](https://github.com/laurent22/joplin/releases/tag/v2.4.12) | 2021-10-13T17:24:34Z | 47,915 | 19,984 | 9,789 | 77,688 |
| [v2.5.1](https://github.com/laurent22/joplin/releases/tag/v2.5.1) (p) | 2021-10-02T09:51:58Z | 6,335 | 907 | 947 | 8,189 |
| [v2.4.9](https://github.com/laurent22/joplin/releases/tag/v2.4.9) | 2021-09-29T19:08:58Z | 59,609 | 23,262 | 15,914 | 98,785 |
| [v2.4.8](https://github.com/laurent22/joplin/releases/tag/v2.4.8) (p) | 2021-09-22T19:01:46Z | 10,121 | 1,774 | 535 | 12,430 |
| [v2.4.7](https://github.com/laurent22/joplin/releases/tag/v2.4.7) (p) | 2021-09-19T12:53:22Z | 4,259 | 259 | 210 | 4,728 |
| [v2.4.6](https://github.com/laurent22/joplin/releases/tag/v2.4.6) (p) | 2021-09-09T18:57:17Z | 5,023 | 460 | 520 | 6,003 |
| [v2.4.5](https://github.com/laurent22/joplin/releases/tag/v2.4.5) (p) | 2021-09-06T18:03:28Z | 4,264 | 275 | 225 | 4,764 |
| [v2.4.4](https://github.com/laurent22/joplin/releases/tag/v2.4.4) (p) | 2021-08-30T16:02:51Z | 4,473 | 380 | 373 | 5,226 |
| [v2.4.3](https://github.com/laurent22/joplin/releases/tag/v2.4.3) (p) | 2021-08-28T15:27:32Z | 3,963 | 207 | 181 | 4,351 |
| [v2.4.2](https://github.com/laurent22/joplin/releases/tag/v2.4.2) (p) | 2021-08-27T17:13:21Z | 3,742 | 151 | 91 | 3,984 |
| [v2.4.1](https://github.com/laurent22/joplin/releases/tag/v2.4.1) (p) | 2021-08-21T11:52:30Z | 4,579 | 372 | 337 | 5,288 |
| [v2.3.5](https://github.com/laurent22/joplin/releases/tag/v2.3.5) | 2021-08-17T06:43:30Z | 85,691 | 31,439 | 33,145 | 150,275 |
| [v2.3.3](https://github.com/laurent22/joplin/releases/tag/v2.3.3) | 2021-08-14T09:19:40Z | 18,869 | 6,886 | 4,062 | 29,817 |
| [v2.2.7](https://github.com/laurent22/joplin/releases/tag/v2.2.7) | 2021-08-11T11:03:26Z | 18,724 | 7,524 | 2,607 | 28,855 |
| [v2.2.6](https://github.com/laurent22/joplin/releases/tag/v2.2.6) (p) | 2021-08-09T19:29:20Z | 10,994 | 4,619 | 958 | 16,571 |
| [v2.2.5](https://github.com/laurent22/joplin/releases/tag/v2.2.5) (p) | 2021-08-07T10:35:24Z | 4,310 | 278 | 208 | 4,796 |
| [v2.2.4](https://github.com/laurent22/joplin/releases/tag/v2.2.4) (p) | 2021-08-05T16:42:48Z | 3,972 | 208 | 133 | 4,313 |
| [v2.2.2](https://github.com/laurent22/joplin/releases/tag/v2.2.2) (p) | 2021-07-19T10:28:35Z | 5,891 | 737 | 648 | 7,276 |
| [v2.1.9](https://github.com/laurent22/joplin/releases/tag/v2.1.9) | 2021-07-19T10:28:43Z | 50,342 | 18,962 | 16,822 | 86,126 |
| [v2.2.1](https://github.com/laurent22/joplin/releases/tag/v2.2.1) (p) | 2021-07-09T17:38:25Z | 5,511 | 417 | 394 | 6,322 |
| [v2.1.8](https://github.com/laurent22/joplin/releases/tag/v2.1.8) | 2021-07-03T08:25:16Z | 33,780 | 12,206 | 12,736 | 58,722 |
| [v2.1.7](https://github.com/laurent22/joplin/releases/tag/v2.1.7) | 2021-06-26T19:48:55Z | 17,095 | 6,410 | 3,638 | 27,143 |
| [v2.1.5](https://github.com/laurent22/joplin/releases/tag/v2.1.5) (p) | 2021-06-23T15:08:52Z | 4,409 | 254 | 202 | 4,865 |
| [v2.1.3](https://github.com/laurent22/joplin/releases/tag/v2.1.3) (p) | 2021-06-19T16:32:51Z | 4,475 | 312 | 217 | 5,004 |
| [v2.0.11](https://github.com/laurent22/joplin/releases/tag/v2.0.11) | 2021-06-16T17:55:49Z | 26,862 | 9,282 | 9,908 | 46,052 |
| [v2.0.10](https://github.com/laurent22/joplin/releases/tag/v2.0.10) | 2021-06-16T07:58:29Z | 6,025 | 946 | 399 | 7,370 |
| [v2.0.9](https://github.com/laurent22/joplin/releases/tag/v2.0.9) (p) | 2021-06-12T09:30:30Z | 4,484 | 307 | 900 | 5,691 |
| [v2.0.8](https://github.com/laurent22/joplin/releases/tag/v2.0.8) (p) | 2021-06-10T16:15:08Z | 3,904 | 246 | 599 | 4,749 |
| [v2.0.4](https://github.com/laurent22/joplin/releases/tag/v2.0.4) (p) | 2021-06-02T12:54:17Z | 1,642 | 404 | 393 | 2,439 |
| [v2.0.2](https://github.com/laurent22/joplin/releases/tag/v2.0.2) (p) | 2021-05-21T18:07:48Z | 5,892 | 503 | 1,681 | 8,076 |
| [v2.0.1](https://github.com/laurent22/joplin/releases/tag/v2.0.1) (p) | 2021-05-15T13:22:58Z | 948 | 288 | 1,039 | 2,275 |
| [v1.8.5](https://github.com/laurent22/joplin/releases/tag/v1.8.5) | 2021-05-10T11:58:14Z | 41,897 | 16,306 | 19,440 | 77,643 |
| [v1.8.4](https://github.com/laurent22/joplin/releases/tag/v1.8.4) (p) | 2021-05-09T18:05:05Z | 3,740 | 152 | 473 | 4,365 |
| [v1.8.3](https://github.com/laurent22/joplin/releases/tag/v1.8.3) (p) | 2021-05-04T10:38:16Z | 4,764 | 320 | 951 | 6,035 |
| [v1.8.2](https://github.com/laurent22/joplin/releases/tag/v1.8.2) (p) | 2021-04-25T10:50:51Z | 5,043 | 451 | 1,300 | 6,794 |
| [v1.8.1](https://github.com/laurent22/joplin/releases/tag/v1.8.1) (p) | 2021-03-29T10:46:41Z | 6,063 | 840 | 2,466 | 9,369 |
| [v1.7.11](https://github.com/laurent22/joplin/releases/tag/v1.7.11) | 2021-02-03T12:50:01Z | 121,096 | 43,007 | 64,457 | 228,560 |
| [v1.7.10](https://github.com/laurent22/joplin/releases/tag/v1.7.10) | 2021-01-30T13:25:29Z | 14,610 | 4,883 | 4,529 | 24,022 |
| [v2.0.2](https://github.com/laurent22/joplin/releases/tag/v2.0.2) (p) | 2021-05-21T18:07:48Z | 5,997 | 503 | 1,681 | 8,181 |
| [v2.0.1](https://github.com/laurent22/joplin/releases/tag/v2.0.1) (p) | 2021-05-15T13:22:58Z | 950 | 288 | 1,039 | 2,277 |
| [v1.8.5](https://github.com/laurent22/joplin/releases/tag/v1.8.5) | 2021-05-10T11:58:14Z | 42,030 | 16,310 | 19,440 | 77,780 |
| [v1.8.4](https://github.com/laurent22/joplin/releases/tag/v1.8.4) (p) | 2021-05-09T18:05:05Z | 3,846 | 152 | 473 | 4,471 |
| [v1.8.3](https://github.com/laurent22/joplin/releases/tag/v1.8.3) (p) | 2021-05-04T10:38:16Z | 4,870 | 320 | 951 | 6,141 |
| [v1.8.2](https://github.com/laurent22/joplin/releases/tag/v1.8.2) (p) | 2021-04-25T10:50:51Z | 5,152 | 451 | 1,300 | 6,903 |
| [v1.8.1](https://github.com/laurent22/joplin/releases/tag/v1.8.1) (p) | 2021-03-29T10:46:41Z | 6,168 | 840 | 2,466 | 9,474 |
| [v1.7.11](https://github.com/laurent22/joplin/releases/tag/v1.7.11) | 2021-02-03T12:50:01Z | 121,243 | 43,010 | 64,462 | 228,715 |
| [v1.7.10](https://github.com/laurent22/joplin/releases/tag/v1.7.10) | 2021-01-30T13:25:29Z | 14,623 | 4,883 | 4,529 | 24,035 |
| [v1.7.9](https://github.com/laurent22/joplin/releases/tag/v1.7.9) (p) | 2021-01-28T09:50:21Z | 531 | 148 | 514 | 1,193 |
| [v1.7.6](https://github.com/laurent22/joplin/releases/tag/v1.7.6) (p) | 2021-01-27T10:36:05Z | 346 | 108 | 304 | 758 |
| [v1.7.5](https://github.com/laurent22/joplin/releases/tag/v1.7.5) (p) | 2021-01-26T09:53:05Z | 439 | 219 | 468 | 1,126 |
| [v1.7.4](https://github.com/laurent22/joplin/releases/tag/v1.7.4) (p) | 2021-01-22T17:58:38Z | 729 | 219 | 641 | 1,589 |
| [v1.6.8](https://github.com/laurent22/joplin/releases/tag/v1.6.8) | 2021-01-20T18:11:34Z | 23,464 | 7,731 | 7,636 | 38,831 |
| [v1.6.8](https://github.com/laurent22/joplin/releases/tag/v1.6.8) | 2021-01-20T18:11:34Z | 23,625 | 7,731 | 7,636 | 38,992 |
| [v1.7.3](https://github.com/laurent22/joplin/releases/tag/v1.7.3) (p) | 2021-01-20T11:23:50Z | 383 | 95 | 459 | 937 |
| [v1.6.7](https://github.com/laurent22/joplin/releases/tag/v1.6.7) | 2021-01-11T23:20:33Z | 14,999 | 4,669 | 4,572 | 24,240 |
| [v1.6.6](https://github.com/laurent22/joplin/releases/tag/v1.6.6) | 2021-01-09T16:15:31Z | 12,865 | 3,441 | 4,819 | 21,125 |
| [v1.6.5](https://github.com/laurent22/joplin/releases/tag/v1.6.5) (p) | 2021-01-09T01:24:32Z | 4,007 | 91 | 324 | 4,422 |
| [v1.6.7](https://github.com/laurent22/joplin/releases/tag/v1.6.7) | 2021-01-11T23:20:33Z | 15,128 | 4,669 | 4,572 | 24,369 |
| [v1.6.6](https://github.com/laurent22/joplin/releases/tag/v1.6.6) | 2021-01-09T16:15:31Z | 12,865 | 3,441 | 4,820 | 21,126 |
| [v1.6.5](https://github.com/laurent22/joplin/releases/tag/v1.6.5) (p) | 2021-01-09T01:24:32Z | 4,111 | 91 | 324 | 4,526 |
| [v1.6.4](https://github.com/laurent22/joplin/releases/tag/v1.6.4) (p) | 2021-01-07T19:11:32Z | 422 | 93 | 220 | 735 |
| [v1.6.2](https://github.com/laurent22/joplin/releases/tag/v1.6.2) (p) | 2021-01-04T22:34:55Z | 704 | 242 | 606 | 1,552 |
| [v1.5.14](https://github.com/laurent22/joplin/releases/tag/v1.5.14) | 2020-12-30T01:48:46Z | 15,502 | 5,233 | 5,551 | 26,286 |
| [v1.6.2](https://github.com/laurent22/joplin/releases/tag/v1.6.2) (p) | 2021-01-04T22:34:55Z | 705 | 242 | 606 | 1,553 |
| [v1.5.14](https://github.com/laurent22/joplin/releases/tag/v1.5.14) | 2020-12-30T01:48:46Z | 15,630 | 5,233 | 5,551 | 26,414 |
| [v1.6.1](https://github.com/laurent22/joplin/releases/tag/v1.6.1) (p) | 2020-12-29T19:37:45Z | 202 | 53 | 184 | 439 |
| [v1.5.13](https://github.com/laurent22/joplin/releases/tag/v1.5.13) | 2020-12-29T18:29:15Z | 786 | 242 | 228 | 1,256 |
| [v1.5.12](https://github.com/laurent22/joplin/releases/tag/v1.5.12) | 2020-12-28T15:14:08Z | 2,567 | 1,796 | 943 | 5,306 |
| [v1.5.11](https://github.com/laurent22/joplin/releases/tag/v1.5.11) | 2020-12-27T19:54:07Z | 14,417 | 4,662 | 4,309 | 23,388 |
| [v1.5.11](https://github.com/laurent22/joplin/releases/tag/v1.5.11) | 2020-12-27T19:54:07Z | 14,417 | 4,662 | 4,311 | 23,390 |
| [v1.5.10](https://github.com/laurent22/joplin/releases/tag/v1.5.10) (p) | 2020-12-26T12:35:36Z | 324 | 124 | 286 | 734 |
| [v1.5.9](https://github.com/laurent22/joplin/releases/tag/v1.5.9) (p) | 2020-12-23T18:01:08Z | 358 | 386 | 427 | 1,171 |
| [v1.5.8](https://github.com/laurent22/joplin/releases/tag/v1.5.8) (p) | 2020-12-20T09:45:19Z | 594 | 179 | 660 | 1,433 |
| [v1.5.7](https://github.com/laurent22/joplin/releases/tag/v1.5.7) (p) | 2020-12-10T12:58:33Z | 920 | 269 | 1,010 | 2,199 |
| [v1.5.4](https://github.com/laurent22/joplin/releases/tag/v1.5.4) (p) | 2020-12-05T12:07:49Z | 732 | 182 | 652 | 1,566 |
| [v1.4.19](https://github.com/laurent22/joplin/releases/tag/v1.4.19) | 2020-12-01T11:11:16Z | 29,857 | 13,629 | 11,714 | 55,200 |
| [v1.4.18](https://github.com/laurent22/joplin/releases/tag/v1.4.18) | 2020-11-28T12:21:41Z | 11,757 | 3,910 | 3,162 | 18,829 |
| [v1.4.19](https://github.com/laurent22/joplin/releases/tag/v1.4.19) | 2020-12-01T11:11:16Z | 29,982 | 13,630 | 11,715 | 55,327 |
| [v1.4.18](https://github.com/laurent22/joplin/releases/tag/v1.4.18) | 2020-11-28T12:21:41Z | 11,757 | 3,910 | 3,163 | 18,830 |
| [v1.4.16](https://github.com/laurent22/joplin/releases/tag/v1.4.16) | 2020-11-27T19:40:16Z | 1,648 | 861 | 621 | 3,130 |
| [v1.4.15](https://github.com/laurent22/joplin/releases/tag/v1.4.15) | 2020-11-27T13:25:43Z | 1,037 | 513 | 296 | 1,846 |
| [v1.4.12](https://github.com/laurent22/joplin/releases/tag/v1.4.12) | 2020-11-23T18:58:07Z | 3,219 | 1,376 | 1,328 | 5,923 |
| [v1.4.11](https://github.com/laurent22/joplin/releases/tag/v1.4.11) (p) | 2020-11-19T23:06:51Z | 4,800 | 191 | 611 | 5,602 |
| [v1.4.10](https://github.com/laurent22/joplin/releases/tag/v1.4.10) (p) | 2020-11-14T09:53:15Z | 694 | 232 | 703 | 1,629 |
| [v1.4.11](https://github.com/laurent22/joplin/releases/tag/v1.4.11) (p) | 2020-11-19T23:06:51Z | 4,906 | 192 | 611 | 5,709 |
| [v1.4.10](https://github.com/laurent22/joplin/releases/tag/v1.4.10) (p) | 2020-11-14T09:53:15Z | 695 | 232 | 703 | 1,630 |
| [v1.4.9](https://github.com/laurent22/joplin/releases/tag/v1.4.9) (p) | 2020-11-11T14:23:17Z | 873 | 177 | 420 | 1,470 |
| [v1.4.7](https://github.com/laurent22/joplin/releases/tag/v1.4.7) (p) | 2020-11-07T18:23:29Z | 559 | 208 | 534 | 1,301 |
| [v1.3.18](https://github.com/laurent22/joplin/releases/tag/v1.3.18) | 2020-11-06T12:07:02Z | 35,449 | 11,388 | 10,548 | 57,385 |
| [v1.3.18](https://github.com/laurent22/joplin/releases/tag/v1.3.18) | 2020-11-06T12:07:02Z | 35,573 | 11,388 | 10,548 | 57,509 |
| [v1.3.17](https://github.com/laurent22/joplin/releases/tag/v1.3.17) (p) | 2020-11-06T11:35:15Z | 86 | 64 | 43 | 193 |
| [v1.4.6](https://github.com/laurent22/joplin/releases/tag/v1.4.6) (p) | 2020-11-05T22:44:12Z | 753 | 128 | 70 | 951 |
| [v1.3.15](https://github.com/laurent22/joplin/releases/tag/v1.3.15) | 2020-11-04T12:22:50Z | 2,795 | 1,344 | 871 | 5,010 |
@@ -243,137 +244,137 @@ updated: 2025-06-01T02:15:29Z
| [v1.3.3](https://github.com/laurent22/joplin/releases/tag/v1.3.3) (p) | 2020-10-17T10:56:57Z | 156 | 81 | 52 | 289 |
| [v1.3.2](https://github.com/laurent22/joplin/releases/tag/v1.3.2) (p) | 2020-10-11T20:39:49Z | 711 | 217 | 587 | 1,515 |
| [v1.3.1](https://github.com/laurent22/joplin/releases/tag/v1.3.1) (p) | 2020-10-11T15:10:18Z | 121 | 87 | 63 | 271 |
| [v1.2.6](https://github.com/laurent22/joplin/releases/tag/v1.2.6) | 2020-10-09T13:56:59Z | 49,405 | 17,792 | 14,091 | 81,288 |
| [v1.2.6](https://github.com/laurent22/joplin/releases/tag/v1.2.6) | 2020-10-09T13:56:59Z | 49,533 | 17,792 | 14,093 | 81,418 |
| [v1.2.4](https://github.com/laurent22/joplin/releases/tag/v1.2.4) (p) | 2020-09-30T07:34:29Z | 860 | 288 | 821 | 1,969 |
| [v1.2.3](https://github.com/laurent22/joplin/releases/tag/v1.2.3) (p) | 2020-09-29T15:13:02Z | 258 | 100 | 102 | 460 |
| [v1.2.2](https://github.com/laurent22/joplin/releases/tag/v1.2.2) (p) | 2020-09-22T20:31:55Z | 1,155 | 242 | 658 | 2,055 |
| [v1.1.4](https://github.com/laurent22/joplin/releases/tag/v1.1.4) | 2020-09-21T11:20:09Z | 28,233 | 13,555 | 7,785 | 49,573 |
| [v1.2.2](https://github.com/laurent22/joplin/releases/tag/v1.2.2) (p) | 2020-09-22T20:31:55Z | 1,155 | 243 | 658 | 2,056 |
| [v1.1.4](https://github.com/laurent22/joplin/releases/tag/v1.1.4) | 2020-09-21T11:20:09Z | 28,233 | 13,555 | 7,786 | 49,574 |
| [v1.1.3](https://github.com/laurent22/joplin/releases/tag/v1.1.3) (p) | 2020-09-17T10:30:37Z | 626 | 189 | 484 | 1,299 |
| [v1.1.2](https://github.com/laurent22/joplin/releases/tag/v1.1.2) (p) | 2020-09-15T12:58:38Z | 420 | 152 | 271 | 843 |
| [v1.1.1](https://github.com/laurent22/joplin/releases/tag/v1.1.1) (p) | 2020-09-11T23:32:47Z | 597 | 232 | 371 | 1,200 |
| [v1.0.245](https://github.com/laurent22/joplin/releases/tag/v1.0.245) | 2020-09-09T12:56:10Z | 22,959 | 10,065 | 5,676 | 38,700 |
| [v1.0.245](https://github.com/laurent22/joplin/releases/tag/v1.0.245) | 2020-09-09T12:56:10Z | 22,966 | 10,065 | 5,676 | 38,707 |
| [v1.0.242](https://github.com/laurent22/joplin/releases/tag/v1.0.242) | 2020-09-04T22:00:34Z | 12,908 | 6,463 | 3,046 | 22,417 |
| [v1.0.241](https://github.com/laurent22/joplin/releases/tag/v1.0.241) | 2020-09-04T18:06:00Z | 26,664 | 5,987 | 5,147 | 37,798 |
| [v1.0.241](https://github.com/laurent22/joplin/releases/tag/v1.0.241) | 2020-09-04T18:06:00Z | 26,671 | 5,987 | 5,149 | 37,807 |
| [v1.0.239](https://github.com/laurent22/joplin/releases/tag/v1.0.239) (p) | 2020-09-01T21:56:36Z | 992 | 268 | 425 | 1,685 |
| [v1.0.237](https://github.com/laurent22/joplin/releases/tag/v1.0.237) (p) | 2020-08-29T15:38:04Z | 637 | 963 | 360 | 1,960 |
| [v1.0.236](https://github.com/laurent22/joplin/releases/tag/v1.0.236) (p) | 2020-08-28T09:16:54Z | 360 | 150 | 126 | 636 |
| [v1.0.235](https://github.com/laurent22/joplin/releases/tag/v1.0.235) (p) | 2020-08-18T22:08:01Z | 2,065 | 528 | 944 | 3,537 |
| [v1.0.234](https://github.com/laurent22/joplin/releases/tag/v1.0.234) (p) | 2020-08-17T23:13:02Z | 698 | 164 | 123 | 985 |
| [v1.0.233](https://github.com/laurent22/joplin/releases/tag/v1.0.233) | 2020-08-01T14:51:15Z | 48,026 | 18,246 | 12,387 | 78,659 |
| [v1.0.233](https://github.com/laurent22/joplin/releases/tag/v1.0.233) | 2020-08-01T14:51:15Z | 48,154 | 18,246 | 12,388 | 78,788 |
| [v1.0.232](https://github.com/laurent22/joplin/releases/tag/v1.0.232) (p) | 2020-07-28T22:34:40Z | 703 | 262 | 201 | 1,166 |
| [v1.0.227](https://github.com/laurent22/joplin/releases/tag/v1.0.227) | 2020-07-07T20:44:54Z | 41,979 | 15,324 | 9,674 | 66,977 |
| [v1.0.227](https://github.com/laurent22/joplin/releases/tag/v1.0.227) | 2020-07-07T20:44:54Z | 41,979 | 15,324 | 9,677 | 66,980 |
| [v1.0.226](https://github.com/laurent22/joplin/releases/tag/v1.0.226) (p) | 2020-07-04T10:21:26Z | 4,978 | 2,293 | 710 | 7,981 |
| [v1.0.224](https://github.com/laurent22/joplin/releases/tag/v1.0.224) | 2020-06-20T22:26:08Z | 25,297 | 11,049 | 6,031 | 42,377 |
| [v1.0.223](https://github.com/laurent22/joplin/releases/tag/v1.0.223) (p) | 2020-06-20T11:51:27Z | 231 | 152 | 101 | 484 |
| [v1.0.221](https://github.com/laurent22/joplin/releases/tag/v1.0.221) (p) | 2020-06-20T01:44:20Z | 901 | 247 | 235 | 1,383 |
| [v1.0.220](https://github.com/laurent22/joplin/releases/tag/v1.0.220) | 2020-06-13T18:26:22Z | 32,825 | 9,970 | 6,446 | 49,241 |
| [v1.0.220](https://github.com/laurent22/joplin/releases/tag/v1.0.220) | 2020-06-13T18:26:22Z | 32,826 | 9,970 | 6,447 | 49,243 |
| [v1.0.218](https://github.com/laurent22/joplin/releases/tag/v1.0.218) | 2020-06-07T10:43:34Z | 14,642 | 7,018 | 3,154 | 24,814 |
| [v1.0.217](https://github.com/laurent22/joplin/releases/tag/v1.0.217) (p) | 2020-06-06T15:17:27Z | 280 | 138 | 85 | 503 |
| [v1.0.216](https://github.com/laurent22/joplin/releases/tag/v1.0.216) | 2020-05-24T14:21:01Z | 41,798 | 14,355 | 10,220 | 66,373 |
| [v1.0.216](https://github.com/laurent22/joplin/releases/tag/v1.0.216) | 2020-05-24T14:21:01Z | 41,930 | 14,357 | 10,221 | 66,508 |
| [v1.0.214](https://github.com/laurent22/joplin/releases/tag/v1.0.214) (p) | 2020-05-21T17:15:15Z | 7,107 | 3,513 | 789 | 11,409 |
| [v1.0.212](https://github.com/laurent22/joplin/releases/tag/v1.0.212) (p) | 2020-05-21T07:48:39Z | 255 | 112 | 72 | 439 |
| [v1.0.211](https://github.com/laurent22/joplin/releases/tag/v1.0.211) (p) | 2020-05-20T08:59:16Z | 354 | 174 | 113 | 641 |
| [v1.0.209](https://github.com/laurent22/joplin/releases/tag/v1.0.209) (p) | 2020-05-17T18:32:51Z | 1,449 | 894 | 173 | 2,516 |
| [v1.0.207](https://github.com/laurent22/joplin/releases/tag/v1.0.207) (p) | 2020-05-10T16:37:35Z | 1,252 | 305 | 1,044 | 2,601 |
| [v1.0.201](https://github.com/laurent22/joplin/releases/tag/v1.0.201) | 2020-04-15T22:55:13Z | 54,411 | 20,102 | 18,218 | 92,731 |
| [v1.0.201](https://github.com/laurent22/joplin/releases/tag/v1.0.201) | 2020-04-15T22:55:13Z | 54,414 | 20,102 | 18,220 | 92,736 |
| [v1.0.200](https://github.com/laurent22/joplin/releases/tag/v1.0.200) | 2020-04-12T12:17:46Z | 9,637 | 4,935 | 1,931 | 16,503 |
| [v1.0.199](https://github.com/laurent22/joplin/releases/tag/v1.0.199) | 2020-04-10T18:41:58Z | 19,883 | 5,932 | 3,827 | 29,642 |
| [v1.0.197](https://github.com/laurent22/joplin/releases/tag/v1.0.197) | 2020-03-30T17:21:22Z | 23,514 | 9,870 | 6,629 | 40,013 |
| [v1.0.195](https://github.com/laurent22/joplin/releases/tag/v1.0.195) | 2020-03-22T19:56:12Z | 19,178 | 7,997 | 4,535 | 31,710 |
| [v1.0.199](https://github.com/laurent22/joplin/releases/tag/v1.0.199) | 2020-04-10T18:41:58Z | 19,899 | 5,932 | 3,827 | 29,658 |
| [v1.0.197](https://github.com/laurent22/joplin/releases/tag/v1.0.197) | 2020-03-30T17:21:22Z | 23,517 | 9,870 | 6,641 | 40,028 |
| [v1.0.195](https://github.com/laurent22/joplin/releases/tag/v1.0.195) | 2020-03-22T19:56:12Z | 19,178 | 7,998 | 4,535 | 31,711 |
| [v1.0.194](https://github.com/laurent22/joplin/releases/tag/v1.0.194) (p) | 2020-03-14T00:00:32Z | 1,339 | 1,431 | 543 | 3,313 |
| [v1.0.193](https://github.com/laurent22/joplin/releases/tag/v1.0.193) | 2020-03-08T08:58:53Z | 28,845 | 10,968 | 7,445 | 47,258 |
| [v1.0.192](https://github.com/laurent22/joplin/releases/tag/v1.0.192) (p) | 2020-03-06T23:27:52Z | 563 | 164 | 113 | 840 |
| [v1.0.190](https://github.com/laurent22/joplin/releases/tag/v1.0.190) (p) | 2020-03-06T01:22:22Z | 441 | 133 | 110 | 684 |
| [v1.0.189](https://github.com/laurent22/joplin/releases/tag/v1.0.189) (p) | 2020-03-04T17:27:15Z | 432 | 136 | 122 | 690 |
| [v1.0.187](https://github.com/laurent22/joplin/releases/tag/v1.0.187) (p) | 2020-03-01T12:31:06Z | 983 | 277 | 298 | 1,558 |
| [v1.0.179](https://github.com/laurent22/joplin/releases/tag/v1.0.179) | 2020-01-24T22:42:41Z | 71,769 | 29,127 | 22,597 | 123,493 |
| [v1.0.178](https://github.com/laurent22/joplin/releases/tag/v1.0.178) | 2020-01-20T19:06:45Z | 17,678 | 6,006 | 2,620 | 26,304 |
| [v1.0.177](https://github.com/laurent22/joplin/releases/tag/v1.0.177) (p) | 2019-12-30T14:40:40Z | 2,013 | 478 | 750 | 3,241 |
| [v1.0.179](https://github.com/laurent22/joplin/releases/tag/v1.0.179) | 2020-01-24T22:42:41Z | 71,780 | 29,128 | 22,597 | 123,505 |
| [v1.0.178](https://github.com/laurent22/joplin/releases/tag/v1.0.178) | 2020-01-20T19:06:45Z | 17,678 | 6,006 | 2,621 | 26,305 |
| [v1.0.177](https://github.com/laurent22/joplin/releases/tag/v1.0.177) (p) | 2019-12-30T14:40:40Z | 2,013 | 478 | 752 | 3,243 |
| [v1.0.176](https://github.com/laurent22/joplin/releases/tag/v1.0.176) (p) | 2019-12-14T10:36:44Z | 3,176 | 2,579 | 496 | 6,251 |
| [v1.0.175](https://github.com/laurent22/joplin/releases/tag/v1.0.175) | 2019-12-08T11:48:47Z | 73,798 | 17,022 | 16,625 | 107,445 |
| [v1.0.174](https://github.com/laurent22/joplin/releases/tag/v1.0.174) | 2019-11-12T18:20:58Z | 30,755 | 11,804 | 8,254 | 50,813 |
| [v1.0.173](https://github.com/laurent22/joplin/releases/tag/v1.0.173) | 2019-11-11T08:33:35Z | 5,154 | 2,123 | 773 | 8,050 |
| [v1.0.170](https://github.com/laurent22/joplin/releases/tag/v1.0.170) | 2019-10-13T22:13:04Z | 27,879 | 8,825 | 7,716 | 44,420 |
| [v1.0.169](https://github.com/laurent22/joplin/releases/tag/v1.0.169) | 2019-09-27T18:35:13Z | 17,291 | 5,973 | 3,785 | 27,049 |
| [v1.0.168](https://github.com/laurent22/joplin/releases/tag/v1.0.168) | 2019-09-25T21:21:38Z | 5,394 | 2,316 | 745 | 8,455 |
| [v1.0.167](https://github.com/laurent22/joplin/releases/tag/v1.0.167) | 2019-09-10T08:48:37Z | 17,013 | 5,747 | 3,741 | 26,501 |
| [v1.0.166](https://github.com/laurent22/joplin/releases/tag/v1.0.166) | 2019-09-09T17:35:54Z | 2,006 | 601 | 261 | 2,868 |
| [v1.0.165](https://github.com/laurent22/joplin/releases/tag/v1.0.165) | 2019-08-14T21:46:29Z | 19,121 | 7,022 | 5,491 | 31,634 |
| [v1.0.161](https://github.com/laurent22/joplin/releases/tag/v1.0.161) | 2019-07-13T18:30:00Z | 19,452 | 6,391 | 4,161 | 30,004 |
| [v1.0.160](https://github.com/laurent22/joplin/releases/tag/v1.0.160) | 2019-06-15T00:21:40Z | 30,870 | 7,796 | 8,134 | 46,800 |
| [v1.0.159](https://github.com/laurent22/joplin/releases/tag/v1.0.159) | 2019-06-08T00:00:19Z | 5,272 | 2,220 | 1,309 | 8,801 |
| [v1.0.175](https://github.com/laurent22/joplin/releases/tag/v1.0.175) | 2019-12-08T11:48:47Z | 73,802 | 17,023 | 16,625 | 107,450 |
| [v1.0.174](https://github.com/laurent22/joplin/releases/tag/v1.0.174) | 2019-11-12T18:20:58Z | 30,765 | 11,806 | 8,255 | 50,826 |
| [v1.0.173](https://github.com/laurent22/joplin/releases/tag/v1.0.173) | 2019-11-11T08:33:35Z | 5,154 | 2,123 | 774 | 8,051 |
| [v1.0.170](https://github.com/laurent22/joplin/releases/tag/v1.0.170) | 2019-10-13T22:13:04Z | 27,888 | 8,827 | 7,716 | 44,431 |
| [v1.0.169](https://github.com/laurent22/joplin/releases/tag/v1.0.169) | 2019-09-27T18:35:13Z | 17,294 | 5,973 | 3,785 | 27,052 |
| [v1.0.168](https://github.com/laurent22/joplin/releases/tag/v1.0.168) | 2019-09-25T21:21:38Z | 5,394 | 2,316 | 747 | 8,457 |
| [v1.0.167](https://github.com/laurent22/joplin/releases/tag/v1.0.167) | 2019-09-10T08:48:37Z | 17,015 | 5,747 | 3,742 | 26,504 |
| [v1.0.166](https://github.com/laurent22/joplin/releases/tag/v1.0.166) | 2019-09-09T17:35:54Z | 2,006 | 603 | 261 | 2,870 |
| [v1.0.165](https://github.com/laurent22/joplin/releases/tag/v1.0.165) | 2019-08-14T21:46:29Z | 19,123 | 7,022 | 5,491 | 31,636 |
| [v1.0.161](https://github.com/laurent22/joplin/releases/tag/v1.0.161) | 2019-07-13T18:30:00Z | 19,453 | 6,391 | 4,161 | 30,005 |
| [v1.0.160](https://github.com/laurent22/joplin/releases/tag/v1.0.160) | 2019-06-15T00:21:40Z | 30,871 | 7,796 | 8,134 | 46,801 |
| [v1.0.159](https://github.com/laurent22/joplin/releases/tag/v1.0.159) | 2019-06-08T00:00:19Z | 5,273 | 2,220 | 1,315 | 8,808 |
| [v1.0.158](https://github.com/laurent22/joplin/releases/tag/v1.0.158) | 2019-05-27T19:01:18Z | 9,891 | 3,590 | 1,962 | 15,443 |
| [v1.0.157](https://github.com/laurent22/joplin/releases/tag/v1.0.157) | 2019-05-26T17:55:53Z | 2,248 | 881 | 314 | 3,443 |
| [v1.0.153](https://github.com/laurent22/joplin/releases/tag/v1.0.153) (p) | 2019-05-15T06:27:29Z | 916 | 143 | 131 | 1,190 |
| [v1.0.152](https://github.com/laurent22/joplin/releases/tag/v1.0.152) | 2019-05-13T09:08:07Z | 13,948 | 4,475 | 4,091 | 22,514 |
| [v1.0.151](https://github.com/laurent22/joplin/releases/tag/v1.0.151) | 2019-05-12T15:14:32Z | 2,002 | 575 | 984 | 3,561 |
| [v1.0.152](https://github.com/laurent22/joplin/releases/tag/v1.0.152) | 2019-05-13T09:08:07Z | 13,948 | 4,477 | 4,092 | 22,517 |
| [v1.0.151](https://github.com/laurent22/joplin/releases/tag/v1.0.151) | 2019-05-12T15:14:32Z | 2,003 | 575 | 985 | 3,563 |
| [v1.0.150](https://github.com/laurent22/joplin/releases/tag/v1.0.150) | 2019-05-12T11:27:48Z | 496 | 175 | 96 | 767 |
| [v1.0.148](https://github.com/laurent22/joplin/releases/tag/v1.0.148) (p) | 2019-05-08T19:12:24Z | 186 | 93 | 120 | 399 |
| [v1.0.145](https://github.com/laurent22/joplin/releases/tag/v1.0.145) | 2019-05-03T09:16:53Z | 7,083 | 2,901 | 1,465 | 11,449 |
| [v1.0.143](https://github.com/laurent22/joplin/releases/tag/v1.0.143) | 2019-04-22T10:51:38Z | 11,994 | 3,593 | 2,809 | 18,396 |
| [v1.0.142](https://github.com/laurent22/joplin/releases/tag/v1.0.142) | 2019-04-02T16:44:51Z | 14,863 | 4,607 | 4,757 | 24,227 |
| [v1.0.143](https://github.com/laurent22/joplin/releases/tag/v1.0.143) | 2019-04-22T10:51:38Z | 11,994 | 3,593 | 2,811 | 18,398 |
| [v1.0.142](https://github.com/laurent22/joplin/releases/tag/v1.0.142) | 2019-04-02T16:44:51Z | 14,863 | 4,607 | 4,758 | 24,228 |
| [v1.0.140](https://github.com/laurent22/joplin/releases/tag/v1.0.140) | 2019-03-10T20:59:58Z | 13,729 | 4,218 | 3,407 | 21,354 |
| [v1.0.139](https://github.com/laurent22/joplin/releases/tag/v1.0.139) (p) | 2019-03-09T10:06:48Z | 186 | 104 | 69 | 359 |
| [v1.0.138](https://github.com/laurent22/joplin/releases/tag/v1.0.138) (p) | 2019-03-03T17:23:00Z | 230 | 128 | 107 | 465 |
| [v1.0.137](https://github.com/laurent22/joplin/releases/tag/v1.0.137) (p) | 2019-03-03T01:12:51Z | 650 | 95 | 107 | 852 |
| [v1.0.135](https://github.com/laurent22/joplin/releases/tag/v1.0.135) | 2019-02-27T23:36:57Z | 12,704 | 3,998 | 4,108 | 20,810 |
| [v1.0.135](https://github.com/laurent22/joplin/releases/tag/v1.0.135) | 2019-02-27T23:36:57Z | 12,704 | 3,998 | 4,109 | 20,811 |
| [v1.0.134](https://github.com/laurent22/joplin/releases/tag/v1.0.134) | 2019-02-27T10:21:44Z | 1,512 | 610 | 244 | 2,366 |
| [v1.0.132](https://github.com/laurent22/joplin/releases/tag/v1.0.132) | 2019-02-26T23:02:05Z | 1,150 | 491 | 119 | 1,760 |
| [v1.0.127](https://github.com/laurent22/joplin/releases/tag/v1.0.127) | 2019-02-14T23:12:48Z | 10,028 | 3,214 | 2,960 | 16,202 |
| [v1.0.127](https://github.com/laurent22/joplin/releases/tag/v1.0.127) | 2019-02-14T23:12:48Z | 10,029 | 3,214 | 2,961 | 16,204 |
| [v1.0.126](https://github.com/laurent22/joplin/releases/tag/v1.0.126) (p) | 2019-02-09T19:46:16Z | 1,001 | 115 | 138 | 1,254 |
| [v1.0.125](https://github.com/laurent22/joplin/releases/tag/v1.0.125) | 2019-01-26T18:14:33Z | 10,416 | 3,597 | 1,726 | 15,739 |
| [v1.0.120](https://github.com/laurent22/joplin/releases/tag/v1.0.120) | 2019-01-10T21:42:53Z | 15,706 | 5,253 | 6,548 | 27,507 |
| [v1.0.125](https://github.com/laurent22/joplin/releases/tag/v1.0.125) | 2019-01-26T18:14:33Z | 10,421 | 3,597 | 1,726 | 15,744 |
| [v1.0.120](https://github.com/laurent22/joplin/releases/tag/v1.0.120) | 2019-01-10T21:42:53Z | 15,706 | 5,253 | 6,549 | 27,508 |
| [v1.0.119](https://github.com/laurent22/joplin/releases/tag/v1.0.119) | 2018-12-18T12:40:22Z | 9,011 | 3,302 | 2,041 | 14,354 |
| [v1.0.118](https://github.com/laurent22/joplin/releases/tag/v1.0.118) | 2019-01-11T08:34:13Z | 763 | 288 | 115 | 1,166 |
| [v1.0.117](https://github.com/laurent22/joplin/releases/tag/v1.0.117) | 2018-11-24T12:05:24Z | 16,348 | 4,940 | 6,409 | 27,697 |
| [v1.0.116](https://github.com/laurent22/joplin/releases/tag/v1.0.116) | 2018-11-20T19:09:24Z | 4,069 | 1,167 | 740 | 5,976 |
| [v1.0.117](https://github.com/laurent22/joplin/releases/tag/v1.0.117) | 2018-11-24T12:05:24Z | 16,348 | 4,940 | 6,412 | 27,700 |
| [v1.0.116](https://github.com/laurent22/joplin/releases/tag/v1.0.116) | 2018-11-20T19:09:24Z | 4,069 | 1,169 | 742 | 5,980 |
| [v1.0.115](https://github.com/laurent22/joplin/releases/tag/v1.0.115) | 2018-11-16T16:52:02Z | 3,723 | 1,341 | 827 | 5,891 |
| [v1.0.114](https://github.com/laurent22/joplin/releases/tag/v1.0.114) | 2018-10-24T20:14:10Z | 11,471 | 3,540 | 3,855 | 18,866 |
| [v1.0.111](https://github.com/laurent22/joplin/releases/tag/v1.0.111) | 2018-09-30T20:15:09Z | 12,544 | 3,555 | 3,707 | 19,806 |
| [v1.0.114](https://github.com/laurent22/joplin/releases/tag/v1.0.114) | 2018-10-24T20:14:10Z | 11,471 | 3,540 | 3,856 | 18,867 |
| [v1.0.111](https://github.com/laurent22/joplin/releases/tag/v1.0.111) | 2018-09-30T20:15:09Z | 12,558 | 3,563 | 3,708 | 19,829 |
| [v1.0.110](https://github.com/laurent22/joplin/releases/tag/v1.0.110) | 2018-09-29T12:29:21Z | 1,010 | 449 | 142 | 1,601 |
| [v1.0.109](https://github.com/laurent22/joplin/releases/tag/v1.0.109) | 2018-09-27T18:01:41Z | 2,146 | 743 | 356 | 3,245 |
| [v1.0.109](https://github.com/laurent22/joplin/releases/tag/v1.0.109) | 2018-09-27T18:01:41Z | 2,146 | 743 | 357 | 3,246 |
| [v1.0.108](https://github.com/laurent22/joplin/releases/tag/v1.0.108) (p) | 2018-09-29T18:49:29Z | 66 | 60 | 38 | 164 |
| [v1.0.107](https://github.com/laurent22/joplin/releases/tag/v1.0.107) | 2018-09-16T19:51:07Z | 7,209 | 2,175 | 1,737 | 11,121 |
| [v1.0.107](https://github.com/laurent22/joplin/releases/tag/v1.0.107) | 2018-09-16T19:51:07Z | 7,209 | 2,175 | 1,738 | 11,122 |
| [v1.0.106](https://github.com/laurent22/joplin/releases/tag/v1.0.106) | 2018-09-08T15:23:40Z | 4,601 | 1,495 | 341 | 6,437 |
| [v1.0.105](https://github.com/laurent22/joplin/releases/tag/v1.0.105) | 2018-09-05T11:29:36Z | 4,702 | 1,634 | 1,485 | 7,821 |
| [v1.0.104](https://github.com/laurent22/joplin/releases/tag/v1.0.104) | 2018-06-28T20:25:36Z | 15,208 | 4,743 | 7,411 | 27,362 |
| [v1.0.103](https://github.com/laurent22/joplin/releases/tag/v1.0.103) | 2018-06-21T19:38:13Z | 2,128 | 926 | 703 | 3,757 |
| [v1.0.101](https://github.com/laurent22/joplin/releases/tag/v1.0.101) | 2018-06-17T18:35:11Z | 1,375 | 648 | 434 | 2,457 |
| [v1.0.100](https://github.com/laurent22/joplin/releases/tag/v1.0.100) | 2018-06-14T17:41:43Z | 960 | 477 | 272 | 1,709 |
| [v1.0.104](https://github.com/laurent22/joplin/releases/tag/v1.0.104) | 2018-06-28T20:25:36Z | 15,208 | 4,743 | 7,414 | 27,365 |
| [v1.0.103](https://github.com/laurent22/joplin/releases/tag/v1.0.103) | 2018-06-21T19:38:13Z | 2,130 | 926 | 704 | 3,760 |
| [v1.0.101](https://github.com/laurent22/joplin/releases/tag/v1.0.101) | 2018-06-17T18:35:11Z | 1,375 | 648 | 436 | 2,459 |
| [v1.0.100](https://github.com/laurent22/joplin/releases/tag/v1.0.100) | 2018-06-14T17:41:43Z | 961 | 477 | 274 | 1,712 |
| [v1.0.99](https://github.com/laurent22/joplin/releases/tag/v1.0.99) | 2018-06-10T13:18:23Z | 1,329 | 642 | 409 | 2,380 |
| [v1.0.97](https://github.com/laurent22/joplin/releases/tag/v1.0.97) | 2018-06-09T19:23:34Z | 357 | 193 | 87 | 637 |
| [v1.0.96](https://github.com/laurent22/joplin/releases/tag/v1.0.96) | 2018-05-26T16:36:39Z | 2,800 | 1,267 | 1,749 | 5,816 |
| [v1.0.95](https://github.com/laurent22/joplin/releases/tag/v1.0.95) | 2018-05-25T13:04:30Z | 466 | 258 | 163 | 887 |
| [v1.0.94](https://github.com/laurent22/joplin/releases/tag/v1.0.94) | 2018-05-21T20:52:59Z | 1,214 | 629 | 444 | 2,287 |
| [v1.0.93](https://github.com/laurent22/joplin/releases/tag/v1.0.93) | 2018-05-14T11:36:01Z | 1,879 | 1,358 | 804 | 4,041 |
| [v1.0.95](https://github.com/laurent22/joplin/releases/tag/v1.0.95) | 2018-05-25T13:04:30Z | 468 | 258 | 163 | 889 |
| [v1.0.94](https://github.com/laurent22/joplin/releases/tag/v1.0.94) | 2018-05-21T20:52:59Z | 1,216 | 629 | 446 | 2,291 |
| [v1.0.93](https://github.com/laurent22/joplin/releases/tag/v1.0.93) | 2018-05-14T11:36:01Z | 1,880 | 1,359 | 804 | 4,043 |
| [v1.0.91](https://github.com/laurent22/joplin/releases/tag/v1.0.91) | 2018-05-10T14:48:04Z | 872 | 594 | 364 | 1,830 |
| [v1.0.89](https://github.com/laurent22/joplin/releases/tag/v1.0.89) | 2018-05-09T13:05:05Z | 546 | 286 | 157 | 989 |
| [v1.0.85](https://github.com/laurent22/joplin/releases/tag/v1.0.85) | 2018-05-01T21:08:24Z | 1,696 | 992 | 677 | 3,365 |
| [v1.0.83](https://github.com/laurent22/joplin/releases/tag/v1.0.83) | 2018-04-04T19:43:58Z | 5,630 | 2,577 | 2,703 | 10,910 |
| [v1.0.85](https://github.com/laurent22/joplin/releases/tag/v1.0.85) | 2018-05-01T21:08:24Z | 1,698 | 992 | 677 | 3,367 |
| [v1.0.83](https://github.com/laurent22/joplin/releases/tag/v1.0.83) | 2018-04-04T19:43:58Z | 5,632 | 2,577 | 2,703 | 10,912 |
| [v1.0.82](https://github.com/laurent22/joplin/releases/tag/v1.0.82) | 2018-03-31T19:16:31Z | 751 | 450 | 167 | 1,368 |
| [v1.0.81](https://github.com/laurent22/joplin/releases/tag/v1.0.81) | 2018-03-28T08:13:58Z | 1,039 | 637 | 826 | 2,502 |
| [v1.0.81](https://github.com/laurent22/joplin/releases/tag/v1.0.81) | 2018-03-28T08:13:58Z | 1,039 | 637 | 827 | 2,503 |
| [v1.0.79](https://github.com/laurent22/joplin/releases/tag/v1.0.79) | 2018-03-23T18:00:11Z | 971 | 580 | 428 | 1,979 |
| [v1.0.78](https://github.com/laurent22/joplin/releases/tag/v1.0.78) | 2018-03-17T15:27:18Z | 1,354 | 943 | 915 | 3,212 |
| [v1.0.77](https://github.com/laurent22/joplin/releases/tag/v1.0.77) | 2018-03-16T15:12:35Z | 202 | 144 | 88 | 434 |
| [v1.0.72](https://github.com/laurent22/joplin/releases/tag/v1.0.72) | 2018-03-14T09:44:35Z | 451 | 296 | 98 | 845 |
| [v1.0.70](https://github.com/laurent22/joplin/releases/tag/v1.0.70) | 2018-02-28T20:04:30Z | 2,004 | 1,093 | 1,296 | 4,393 |
| [v1.0.70](https://github.com/laurent22/joplin/releases/tag/v1.0.70) | 2018-02-28T20:04:30Z | 2,004 | 1,093 | 1,297 | 4,394 |
| [v1.0.67](https://github.com/laurent22/joplin/releases/tag/v1.0.67) | 2018-02-19T22:51:08Z | 1,947 | 645 | 0 | 2,592 |
| [v1.0.66](https://github.com/laurent22/joplin/releases/tag/v1.0.66) | 2018-02-18T23:09:09Z | 450 | 174 | 109 | 733 |
| [v1.0.65](https://github.com/laurent22/joplin/releases/tag/v1.0.65) | 2018-02-17T20:02:25Z | 349 | 171 | 156 | 676 |
| [v1.0.64](https://github.com/laurent22/joplin/releases/tag/v1.0.64) | 2018-02-16T00:58:20Z | 1,196 | 583 | 1,148 | 2,927 |
| [v1.0.63](https://github.com/laurent22/joplin/releases/tag/v1.0.63) | 2018-02-14T19:40:36Z | 413 | 200 | 115 | 728 |
| [v1.0.63](https://github.com/laurent22/joplin/releases/tag/v1.0.63) | 2018-02-14T19:40:36Z | 413 | 200 | 117 | 730 |
| [v1.0.62](https://github.com/laurent22/joplin/releases/tag/v1.0.62) | 2018-02-12T20:19:58Z | 714 | 345 | 400 | 1,459 |
| [v0.10.61](https://github.com/laurent22/joplin/releases/tag/v0.10.61) | 2018-02-08T18:27:39Z | 1,124 | 678 | 987 | 2,789 |
| [v0.10.60](https://github.com/laurent22/joplin/releases/tag/v0.10.60) | 2018-02-06T13:09:56Z | 772 | 565 | 577 | 1,914 |
| [v0.10.54](https://github.com/laurent22/joplin/releases/tag/v0.10.54) | 2018-01-31T20:21:30Z | 1,959 | 1,504 | 348 | 3,811 |
| [v0.10.52](https://github.com/laurent22/joplin/releases/tag/v0.10.52) | 2018-01-31T19:25:18Z | 164 | 677 | 41 | 882 |
| [v0.10.51](https://github.com/laurent22/joplin/releases/tag/v0.10.51) | 2018-01-28T18:47:02Z | 1,430 | 1,643 | 352 | 3,425 |
| [v0.10.54](https://github.com/laurent22/joplin/releases/tag/v0.10.54) | 2018-01-31T20:21:30Z | 1,961 | 1,504 | 348 | 3,813 |
| [v0.10.52](https://github.com/laurent22/joplin/releases/tag/v0.10.52) | 2018-01-31T19:25:18Z | 164 | 677 | 42 | 883 |
| [v0.10.51](https://github.com/laurent22/joplin/releases/tag/v0.10.51) | 2018-01-28T18:47:02Z | 1,431 | 1,643 | 352 | 3,426 |
| [v0.10.48](https://github.com/laurent22/joplin/releases/tag/v0.10.48) | 2018-01-23T11:19:51Z | 2,112 | 1,795 | 56 | 3,963 |
| [v0.10.47](https://github.com/laurent22/joplin/releases/tag/v0.10.47) | 2018-01-16T17:27:17Z | 1,334 | 1,317 | 91 | 2,742 |
| [v0.10.43](https://github.com/laurent22/joplin/releases/tag/v0.10.43) | 2018-01-08T10:12:10Z | 3,486 | 2,401 | 1,240 | 7,127 |
| [v0.10.43](https://github.com/laurent22/joplin/releases/tag/v0.10.43) | 2018-01-08T10:12:10Z | 3,486 | 2,401 | 1,241 | 7,128 |
| [v0.10.41](https://github.com/laurent22/joplin/releases/tag/v0.10.41) | 2018-01-05T20:38:12Z | 1,245 | 1,598 | 266 | 3,109 |
| [v0.10.40](https://github.com/laurent22/joplin/releases/tag/v0.10.40) | 2018-01-02T23:16:57Z | 1,638 | 1,832 | 362 | 3,832 |
| [v0.10.39](https://github.com/laurent22/joplin/releases/tag/v0.10.39) | 2017-12-11T21:19:44Z | 5,995 | 4,445 | 3,328 | 13,768 |
| [v0.10.39](https://github.com/laurent22/joplin/releases/tag/v0.10.39) | 2017-12-11T21:19:44Z | 5,995 | 4,445 | 3,329 | 13,769 |
| [v0.10.38](https://github.com/laurent22/joplin/releases/tag/v0.10.38) | 2017-12-08T10:12:06Z | 1,089 | 1,274 | 330 | 2,693 |
| [v0.10.37](https://github.com/laurent22/joplin/releases/tag/v0.10.37) | 2017-12-07T19:38:05Z | 291 | 892 | 109 | 1,292 |
| [v0.10.36](https://github.com/laurent22/joplin/releases/tag/v0.10.36) | 2017-12-05T09:34:40Z | 1,052 | 1,405 | 465 | 2,922 |
@@ -382,11 +383,11 @@ updated: 2025-06-01T02:15:29Z
| [v0.10.33](https://github.com/laurent22/joplin/releases/tag/v0.10.33) | 2017-12-02T13:20:39Z | 95 | 711 | 51 | 857 |
| [v0.10.31](https://github.com/laurent22/joplin/releases/tag/v0.10.31) | 2017-12-01T09:56:44Z | 918 | 1,499 | 437 | 2,854 |
| [v0.10.30](https://github.com/laurent22/joplin/releases/tag/v0.10.30) | 2017-11-30T20:28:16Z | 842 | 1,428 | 452 | 2,722 |
| [v0.10.28](https://github.com/laurent22/joplin/releases/tag/v0.10.28) | 2017-11-30T01:07:46Z | 1,492 | 1,761 | 906 | 4,159 |
| [v0.10.26](https://github.com/laurent22/joplin/releases/tag/v0.10.26) | 2017-11-29T16:02:17Z | 307 | 756 | 291 | 1,354 |
| [v0.10.25](https://github.com/laurent22/joplin/releases/tag/v0.10.25) | 2017-11-24T14:27:49Z | 259 | 752 | 6,780 | 7,791 |
| [v0.10.28](https://github.com/laurent22/joplin/releases/tag/v0.10.28) | 2017-11-30T01:07:46Z | 1,492 | 1,761 | 908 | 4,161 |
| [v0.10.26](https://github.com/laurent22/joplin/releases/tag/v0.10.26) | 2017-11-29T16:02:17Z | 307 | 757 | 291 | 1,355 |
| [v0.10.25](https://github.com/laurent22/joplin/releases/tag/v0.10.25) | 2017-11-24T14:27:49Z | 259 | 752 | 6,784 | 7,795 |
| [v0.10.23](https://github.com/laurent22/joplin/releases/tag/v0.10.23) | 2017-11-21T19:38:41Z | 247 | 714 | 67 | 1,028 |
| [v0.10.22](https://github.com/laurent22/joplin/releases/tag/v0.10.22) | 2017-11-20T21:45:57Z | 192 | 700 | 50 | 942 |
| [v0.10.22](https://github.com/laurent22/joplin/releases/tag/v0.10.22) | 2017-11-20T21:45:57Z | 192 | 701 | 50 | 943 |
| [v0.10.21](https://github.com/laurent22/joplin/releases/tag/v0.10.21) | 2017-11-18T00:53:15Z | 170 | 693 | 44 | 907 |
| [v0.10.20](https://github.com/laurent22/joplin/releases/tag/v0.10.20) | 2017-11-17T17:18:25Z | 161 | 706 | 56 | 923 |
| [v0.10.20](https://github.com/laurent22/joplin/releases/tag/v0.10.20) | 2017-11-17T17:18:25Z | 163 | 706 | 57 | 926 |
| [v0.10.19](https://github.com/laurent22/joplin/releases/tag/v0.10.19) | 2017-11-20T18:59:48Z | 183 | 707 | 55 | 945 |

View File

@@ -0,0 +1,95 @@
# Joplin Server Business
<div style="overflow: auto;">
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/joplin_server_business/main.png" width="200px" style="float: left; margin-right: 16px; margin-bottom: 16px;"/>
Joplin Server Business is a synchronisation server that you can install on your own infrastructure, so that your data remains private and secure within your business.
Your teams can collaborate on notebooks and share information. They can also publish notes to the internet or within your own intranet. All that secured by Joplin end-to-end encryption.
Interested? [Contact us for a quote](mailto:jsb-inquiry@joplin.cloud?subject=Joplin%20Server%20Business%20inquiry)
</div>
## Smart teamwork with Joplin Server
### Self-host to keep your data within your organisation
<div style="overflow: auto;">
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/joplin_server_business/self_host.jpg" width="200px" style="float: left; margin-right: 16px; margin-bottom: 16px;"/>
The data is hosted on your own server, giving you full control over it and ensuring it stays within your organisation.
</div>
### Share and collaborate on a notebook
<div style="overflow: auto;">
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/joplin_server_business/share.jpg" width="200px" style="float: left; margin-right: 16px; margin-bottom: 16px;"/>
Our service allows you to share notes and documents across unlimited devices. Create and modify teams to manage projects and planning.
</div>
### Publish notes to the internet
<div style="overflow: auto;">
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/joplin_server_business/publish.jpg" width="200px" style="float: left; margin-right: 16px; margin-bottom: 16px;"/>
You can publish a note so that it can be viewed in a browser by your colleagues and customers. The note can be available publicly on the internet or remain within your intranet.
</div>
### Manage multiples users and teams
<div style="overflow: auto;">
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/joplin_server_business/teams.jpg" width="200px" style="float: left; margin-right: 16px; margin-bottom: 16px;"/>
Using Joplin Server Business you can create and manage teams of users. Each team can collaborate on notebooks and notes and share information.
</div>
## By choosing Joplin Server Business your organisation benefits also from other features including:
### End-to-end encryption
Activate encryption to protect your data and secure communications across teams.
### Web clipper
Capture web pages and screenshots and save them as notes in Joplin.
### Open source code
Our desktop and mobile applications, as well as the end-to-end technology, are fully open source, ensuring transparency and increased security.
### Synchronization across devices
Securely synchronise your data across multiple devices - including iOS, Android, Windows, macOS and Linux.
### Customise it
Customise the app with plugins, custom themes and multiple text editors (Rich Text or Markdown). Or create your own company-specific workflow by developing scripts and plugins using the Extension API.
### Open source code
Our desktop and mobile applications, as well as the end-to-end technology, are fully open source, ensuring transparency and increased security.
### Multimedia notes (PDF, images, etc.)
Keep all your resources in one place. Save and share images, PDFs, videos, audio files and math expressions.
## Did you know that there are over 150 plugins available for Joplin products ?
[Go to the plugin website](https://joplinapp.org/plugins/)
## Ready to give it a try ?
To find out more about Joplin Server Business and how it can be integrated to your organisation, feel free to contact us. Our experts can prepare a demo for you. We can provide a quote to accommodate your company’s needs.
[Contact us for a quote!](mailto:jsb-inquiry@joplin.cloud?subject=Joplin%20Server%20Business%20inquiry)

View File

@@ -27,7 +27,7 @@ The install and update script supports the [following flags](https://github.com/
Operating System | Download | Alt. Download
---|---|---
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' style="max-height: 40px;" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeAndroid.png'/></a> | or download the [APK file](https://objects.joplinusercontent.com/v3.3.9/joplin-v3.3.9.apk?source=JoplinWebsite&type=New)
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' style="max-height: 40px;" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeAndroid.png'/></a> | or download the [APK file](https://objects.joplinusercontent.com/v3.3.11/joplin-v3.3.11.apk?source=JoplinWebsite&type=New)
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' style="max-height: 40px;" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeIOS.png'/></a> | -
## Terminal application

View File

@@ -9468,7 +9468,7 @@ __metadata:
jest: "npm:29.7.0"
jest-environment-jsdom: "npm:29.7.0"
json-stringify-safe: "npm:5.0.1"
katex: "npm:0.16.21"
katex: "npm:0.16.22"
markdown-it: "npm:13.0.2"
markdown-it-abbr: "npm:1.0.4"
markdown-it-anchor: "npm:5.3.0"
@@ -9529,7 +9529,7 @@ __metadata:
jquery: "npm:3.7.1"
jsdom: "npm:25.0.1"
knex: "npm:3.1.0"
koa: "npm:2.16.0"
koa: "npm:2.16.1"
ldapts: "npm:7.3.3"
markdown-it: "npm:13.0.2"
mustache: "npm:4.2.0"
@@ -9538,7 +9538,7 @@ __metadata:
node-os-utils: "npm:1.3.7"
nodemailer: "npm:6.10.0"
nodemon: "npm:3.1.9"
pg: "npm:8.13.3"
pg: "npm:8.14.1"
pm2: "npm:5.4.3"
pretty-bytes: "npm:5.6.0"
prettycron: "npm:0.10.0"
@@ -9625,7 +9625,7 @@ __metadata:
jest: "npm:29.7.0"
jest-expect-message: "npm:1.1.3"
knex: "npm:3.1.0"
koa: "npm:2.16.0"
koa: "npm:2.16.1"
koa-body: "npm:6.0.1"
pg-boss: "npm:10.1.6"
sqlite3: "npm:5.1.6"
@@ -9651,7 +9651,7 @@ __metadata:
dependencies:
"@adobe/css-tools": "npm:4.4.2"
"@rollup/plugin-commonjs": "npm:28.0.3"
"@rollup/plugin-node-resolve": "npm:15.3.1"
"@rollup/plugin-node-resolve": "npm:16.0.1"
"@rollup/plugin-replace": "npm:6.0.2"
browserify: "npm:14.5.0"
html-entities: "npm:1.4.0"
@@ -12428,9 +12428,9 @@ __metadata:
languageName: node
linkType: hard
"@rollup/plugin-node-resolve@npm:15.3.1":
version: 15.3.1
resolution: "@rollup/plugin-node-resolve@npm:15.3.1"
"@rollup/plugin-node-resolve@npm:16.0.1":
version: 16.0.1
resolution: "@rollup/plugin-node-resolve@npm:16.0.1"
dependencies:
"@rollup/pluginutils": "npm:^5.0.1"
"@types/resolve": "npm:1.20.2"
@@ -12442,7 +12442,7 @@ __metadata:
peerDependenciesMeta:
rollup:
optional: true
checksum: 10/874494c0daca8fb0d633a237dd9df0d30609b374326e57508710f2b6d7ddaa93d203d8daa0257960b2b6723f56dfec1177573126f31ff9604700303b6f5fdbe3
checksum: 10/88fee8c003a5730cca2c06edd200ec6a46c7ab28bed3a99aea6d3070f34f980f575fcbea906946579e41b0be6fd7a2fbc24cdf0ca24f172a555f130726915d8b
languageName: node
linkType: hard
@@ -33105,14 +33105,14 @@ __metadata:
languageName: node
linkType: hard
"katex@npm:0.16.21":
version: 0.16.21
resolution: "katex@npm:0.16.21"
"katex@npm:0.16.22":
version: 0.16.22
resolution: "katex@npm:0.16.22"
dependencies:
commander: "npm:^8.3.0"
bin:
katex: cli.js
checksum: 10/db1094c528972ffb881c64969e87cbca465d21f918f4dad8bfe583f68e1bd601438eda3d79e8d74bc7ccc14e7b76616a9053bb21945749a30a73bc68f20e721b
checksum: 10/fdb8667d9aa971154502b120ba340766754d202e3d3e322aca0a96de27032ad2dbb8a7295d798d310cd7ce4ddd21ed1f3318895541b61c9b4fdf611166589e02
languageName: node
linkType: hard
@@ -33339,9 +33339,9 @@ __metadata:
languageName: node
linkType: hard
"koa@npm:2.16.0":
version: 2.16.0
resolution: "koa@npm:2.16.0"
"koa@npm:2.16.1":
version: 2.16.1
resolution: "koa@npm:2.16.1"
dependencies:
accepts: "npm:^1.3.5"
cache-content-type: "npm:^1.0.0"
@@ -33366,7 +33366,7 @@ __metadata:
statuses: "npm:^1.5.0"
type-is: "npm:^1.6.16"
vary: "npm:^1.1.2"
checksum: 10/88284e5da49cd54a2db663c818f5370d00f32b6aefbe5ecfc75bdaf7937d3b08cfbb884d07564b8e2b856dfe74d930997a6bdca2e2090dc2bfae0fa8af56a214
checksum: 10/f33b95227e48bffd3a682996e6cf72c4ae2992671529c6c914b76d28172219c9cbd8201b16cc028dc25fafc8f1dc9391a6e7e045740a10ee7d89a5631031a974
languageName: node
linkType: hard
@@ -33828,9 +33828,9 @@ __metadata:
languageName: node
linkType: hard
"lint-staged@npm:15.4.3":
version: 15.4.3
resolution: "lint-staged@npm:15.4.3"
"lint-staged@npm:15.5.0":
version: 15.5.0
resolution: "lint-staged@npm:15.5.0"
dependencies:
chalk: "npm:^5.4.1"
commander: "npm:^13.1.0"
@@ -33844,7 +33844,7 @@ __metadata:
yaml: "npm:^2.7.0"
bin:
lint-staged: bin/lint-staged.js
checksum: 10/14a6a9cb9b5e8027b1347cb24e114839d618d343d5c724c26def7d45ca9b9a9b813b585531c68f5a3d13332407c2dba198987a73f0350df483d99a876ba69c60
checksum: 10/5873584649c5f840b990036c20abd4b58d6b1313dad5505627b4d0cc077f0ec8ac0d6cf4cf4d959e66e0ab085db384bb12dce9490ff29217bf4ed96d0442ed51
languageName: node
linkType: hard
@@ -39269,7 +39269,7 @@ __metadata:
languageName: node
linkType: hard
"pg-pool@npm:^3.10.0, pg-pool@npm:^3.7.1":
"pg-pool@npm:^3.10.0":
version: 3.10.0
resolution: "pg-pool@npm:3.10.0"
peerDependencies:
@@ -39278,13 +39278,29 @@ __metadata:
languageName: node
linkType: hard
"pg-protocol@npm:^1.10.0, pg-protocol@npm:^1.7.1":
"pg-pool@npm:^3.8.0":
version: 3.10.1
resolution: "pg-pool@npm:3.10.1"
peerDependencies:
pg: ">=8.0"
checksum: 10/b389a714be59ebe53ec412cbff513191cc0b7a203faa5d26416b6a038cafdfe30fbf1a5936b77bb76109c49bd7c4a116870a5a46a45796b1b34c96f016d7fbe2
languageName: node
linkType: hard
"pg-protocol@npm:^1.10.0":
version: 1.10.0
resolution: "pg-protocol@npm:1.10.0"
checksum: 10/975184d9f67dd2325afc8b5e79008c39bbdf6baf43db1158a90a9c624c86d0ca51cff68031759e196739d2e04b90a6a4749b42206ab7b9aca03a25243a7c2094
languageName: node
linkType: hard
"pg-protocol@npm:^1.8.0":
version: 1.10.3
resolution: "pg-protocol@npm:1.10.3"
checksum: 10/31da85319084c03f403efee7accce9786964df82a7feb60e6bd77b71f1e622c74a2a644a2bc434389d0ab92e5abdeedea69ebdb53b1897d9f01d2a1f51a8a2fe
languageName: node
linkType: hard
"pg-types@npm:2.2.0, pg-types@npm:^2.1.0":
version: 2.2.0
resolution: "pg-types@npm:2.2.0"
@@ -39298,14 +39314,14 @@ __metadata:
languageName: node
linkType: hard
"pg@npm:8.13.3":
version: 8.13.3
resolution: "pg@npm:8.13.3"
"pg@npm:8.14.1":
version: 8.14.1
resolution: "pg@npm:8.14.1"
dependencies:
pg-cloudflare: "npm:^1.1.1"
pg-connection-string: "npm:^2.7.0"
pg-pool: "npm:^3.7.1"
pg-protocol: "npm:^1.7.1"
pg-pool: "npm:^3.8.0"
pg-protocol: "npm:^1.8.0"
pg-types: "npm:^2.1.0"
pgpass: "npm:1.x"
peerDependencies:
@@ -39316,7 +39332,7 @@ __metadata:
peerDependenciesMeta:
pg-native:
optional: true
checksum: 10/be1be61fa46f7ccc3441794e390c41fc548f1bbee9744e3e7fae00b3d91e3974c4c51e25c1013075ec3a289d9290cd01ee926357e68ba20fcbb15308dbdef87c
checksum: 10/45f2d5719fd74a6a4784c5115c0ff482af92d1e5b101bf423160b6a983e37cc2fad4a7eea2a06f27e6f8bdb8abce23486d2d522c8c52c90f68a2bc897f0553c4
languageName: node
linkType: hard
@@ -43725,7 +43741,7 @@ __metadata:
http-server: "npm:14.1.1"
husky: "npm:9.1.7"
lerna: "npm:3.22.1"
lint-staged: "npm:15.4.3"
lint-staged: "npm:15.5.0"
madge: "npm:8.0.0"
node-gyp: "npm:9.4.1"
nodemon: "npm:3.1.9"