1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-02-04 07:53:44 +02:00

Compare commits

..

32 Commits

Author SHA1 Message Date
Laurent Cozic
7d0cd1ab3e update 2026-02-03 00:45:08 +00:00
renovate[bot]
c278b45c78 Update dependency nodejs to v24.8.0 (#14229)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-01 23:54:22 +00:00
renovate[bot]
0dafd21db0 Update dependency electron-updater to v6.6.8 (#14239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-01 23:53:58 +00:00
Sebastian
490d35919c All: Translation: Update de_DE.po (#14242) 2026-02-01 06:24:59 -05:00
Nick
4c1ca5480d All: Translation: Update sv.po (#14241) 2026-02-01 06:20:55 -05:00
Joplin Bot
d414c6354a Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-02-01 02:36:23 +00:00
rnbastos
7651d8e3c4 All: Translation: Update pt_BR.po (#14238) 2026-01-31 17:23:25 -05:00
renovate[bot]
d5c72c13cb Update dependency @types/serviceworker to v0.0.164 (#14237)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-31 09:49:32 +00:00
renovate[bot]
4377634e7b Update dependency esbuild to v0.25.12 (#14236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-31 04:59:05 +00:00
renovate[bot]
69ec5c7f86 Update dependency @types/serviceworker to v0.0.163 (#14234)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 17:50:22 +00:00
renovate[bot]
f02b0f48d8 Update dependency react-refresh to v0.18.0 (#14233)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 14:33:32 +01:00
renovate[bot]
4d77c1385f Update dependency sass to v1.93.3 (#14228)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 16:51:56 +00:00
renovate[bot]
c83f9ddeac Update dependency dayjs to v1.11.19 (#14227)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 14:49:50 +00:00
renovate[bot]
1b9c11df7b Update dependency @types/serviceworker to v0.0.162 (#14225)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 09:28:36 +00:00
Nick
333a8723e8 All: Translation: Update sv.po (#14220) 2026-01-28 18:14:28 -05:00
Laurent Cozic
e030c8271d Chore: Try to fix app-desktop tests on local 2026-01-28 12:55:08 +00:00
renovate[bot]
560bc31445 Update dependency gettext-extractor to v4.0.1 (#14217)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-28 01:45:26 +00:00
renovate[bot]
c71aeb74b2 Update dependency gettext-extractor to v4 (#14213)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 23:36:41 +00:00
renovate[bot]
ffaf2acb66 Update dependency @rollup/plugin-replace to v6.0.3 (#14212)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 18:16:02 +00:00
renovate[bot]
f442f1fb23 Update dependency @types/serviceworker to v0.0.161 (#14206)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 11:12:24 +00:00
renovate[bot]
81a1451820 Update dependency react-native-safe-area-context to v5.6.2 (#14202)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 02:16:37 +00:00
renovate[bot]
b3a3d71461 Update dependency @react-native-community/datetimepicker to v8.4.7 (#14191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-26 22:47:00 +00:00
bwat47
1db38c3232 Desktop, Mobile: Fixes #13933: Markdown editor: Scroll jumps in notes with many inline images (#13955) 2026-01-26 15:21:08 +00:00
Fardin96
42e645eb70 Mobile: Fixes #13243: Align tag search-input-clear behavior across input methods (#14042) 2026-01-26 15:12:06 +00:00
mrjo118
3860f44d06 Mobile: Fixes #14153: Prevent the back button sometimes disappearing when switching between editors (#14164) 2026-01-26 15:09:37 +00:00
Henry Heino
4df0f8668d Desktop,Mobile: Resolves #14158: Markdown Editor: Make code block highlighting closer to the viewer (#14168) 2026-01-26 15:06:37 +00:00
Henry Heino
306d0fddd8 Desktop: OneNote import: Import invalid attachments as empty attachments (#14177) 2026-01-26 15:06:24 +00:00
Henry Heino
56d12b28f2 All: Unlinked resource deletion: Fix resources attached only via reference links are auto-deleted (#14178) 2026-01-26 15:06:15 +00:00
Henry Heino
6c5ea4872a Desktop,Mobile: Markdown editor: Fix error logged in "hide markdown" mode for certain markup (#14179) 2026-01-26 15:06:06 +00:00
Henry Heino
9856e8ae93 Chore: Sync fuzzer: Test adding, removing resources from notes (#14185) 2026-01-26 15:05:50 +00:00
Henry Heino
5712da4c0f Desktop,Mobile: Fixes #14009: Markdown editor: Upgrade most CodeMirror dependencies (#14186) 2026-01-26 15:04:35 +00:00
Henry Heino
4f7ee56444 Desktop: Fixes #13793: Make conflicts caused by resource duplication less likely (#14188) 2026-01-26 15:04:26 +00:00
44 changed files with 1549 additions and 1191 deletions

View File

@@ -1851,17 +1851,22 @@ packages/tools/fuzzer/ClientPool.js
packages/tools/fuzzer/Server.js
packages/tools/fuzzer/constants.js
packages/tools/fuzzer/model/FolderRecord.js
packages/tools/fuzzer/model/ResourceRecord.js
packages/tools/fuzzer/sync-fuzzer.js
packages/tools/fuzzer/types.js
packages/tools/fuzzer/utils/ProgressBar.js
packages/tools/fuzzer/utils/SeededRandom.js
packages/tools/fuzzer/utils/diffSortedStringArrays.test.js
packages/tools/fuzzer/utils/diffSortedStringArrays.js
packages/tools/fuzzer/utils/extractResourceIds.js
packages/tools/fuzzer/utils/getNumberProperty.js
packages/tools/fuzzer/utils/getProperty.js
packages/tools/fuzzer/utils/getStringProperty.js
packages/tools/fuzzer/utils/hangingIndent.js
packages/tools/fuzzer/utils/logDiffDebug.js
packages/tools/fuzzer/utils/openDebugSession.js
packages/tools/fuzzer/utils/randomId.test.js
packages/tools/fuzzer/utils/randomId.js
packages/tools/fuzzer/utils/randomString.js
packages/tools/fuzzer/utils/retryWithCount.js
packages/tools/generate-database-types.js

5
.gitignore vendored
View File

@@ -1825,17 +1825,22 @@ packages/tools/fuzzer/ClientPool.js
packages/tools/fuzzer/Server.js
packages/tools/fuzzer/constants.js
packages/tools/fuzzer/model/FolderRecord.js
packages/tools/fuzzer/model/ResourceRecord.js
packages/tools/fuzzer/sync-fuzzer.js
packages/tools/fuzzer/types.js
packages/tools/fuzzer/utils/ProgressBar.js
packages/tools/fuzzer/utils/SeededRandom.js
packages/tools/fuzzer/utils/diffSortedStringArrays.test.js
packages/tools/fuzzer/utils/diffSortedStringArrays.js
packages/tools/fuzzer/utils/extractResourceIds.js
packages/tools/fuzzer/utils/getNumberProperty.js
packages/tools/fuzzer/utils/getProperty.js
packages/tools/fuzzer/utils/getStringProperty.js
packages/tools/fuzzer/utils/hangingIndent.js
packages/tools/fuzzer/utils/logDiffDebug.js
packages/tools/fuzzer/utils/openDebugSession.js
packages/tools/fuzzer/utils/randomId.test.js
packages/tools/fuzzer/utils/randomId.js
packages/tools/fuzzer/utils/randomString.js
packages/tools/fuzzer/utils/retryWithCount.js
packages/tools/generate-database-types.js

View File

@@ -9,7 +9,7 @@
"vips.dev": {
"platforms": ["aarch64-darwin"],
},
"nodejs": "24.5.0",
"nodejs": "24.8.0",
"pkg-config": "latest",
"python": "3.13.3",
"bat": "latest",

View File

@@ -156,7 +156,10 @@ const useContextMenu = (props: ContextMenuProps) => {
// Prepend the event listener so that it gets called before
// the listener that shows the default menu.
const targetWindow = bridge().activeWindow();
// Use mainWindow() instead of activeWindow() because activeWindow() can return
// the DevTools window when it's focused, causing the listener to be registered
// on the wrong webContents.
const targetWindow = bridge().mainWindow();
targetWindow.webContents.prependListener('context-menu', onContextMenu);
return () => {

View File

@@ -34,7 +34,10 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
if (!editor) return () => {};
const contextMenuItems = menuItems(dispatch);
const targetWindow = bridge().activeWindow();
// Use mainWindow() instead of activeWindow() because activeWindow() can return
// the DevTools window when it's focused, causing the listener to be registered
// on the wrong webContents.
const targetWindow = bridge().mainWindow();
const makeMainMenuItems = (element: Element) => {
let itemType: ContextMenuItemType = ContextMenuItemType.None;

View File

@@ -131,7 +131,9 @@ module.exports = {
testEnvironment: 'jsdom',
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
testEnvironmentOptions: {
customExportConditions: ['node', 'require'],
},
// Adds a location field to test results
// testLocationInResults: false,

View File

@@ -169,7 +169,7 @@
"debounce": "1.2.1",
"electron": "39.2.3",
"electron-builder": "24.13.3",
"electron-updater": "6.6.2",
"electron-updater": "6.6.8",
"electron-window-state": "5.0.3",
"esbuild": "^0.25.3",
"formatcoords": "1.1.3",

View File

@@ -438,9 +438,8 @@ const useInputEventHandlers = ({
const onSubmit = useCallback(() => {
if (selectedResult) {
onItemSelected(selectedResult, selectedIndex);
setSearch('');
}
}, [onItemSelected, selectedResult, selectedIndex, setSearch]);
}, [onItemSelected, selectedResult, selectedIndex]);
// For now, onKeyPress only works on web.
// See https://github.com/react-native-community/discussions-and-proposals/issues/249

View File

@@ -694,10 +694,17 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
</Menu>
);
// Updating the state of this component can result in the left most element becoming hidden, so add a dummy as the first element to prevent this
// See https://github.com/laurent22/joplin/issues/14153
const zeroWidthSpacer = (
<View style={{ width: 0 }} pointerEvents="none"/>
);
return (
<View style={this.styles().outerContainer}>
<View style={this.styles().aboveHeader}/>
<View style={this.styles().innerContainer}>
{zeroWidthSpacer}
{sideMenuComp}
{backButtonComp}
{renderUndoButton()}

View File

@@ -30,7 +30,7 @@
"@joplin/utils": "~3.6",
"@js-draw/material-icons": "1.33.0",
"@react-native-clipboard/clipboard": "1.16.3",
"@react-native-community/datetimepicker": "8.4.5",
"@react-native-community/datetimepicker": "8.4.7",
"@react-native-community/geolocation": "3.4.0",
"@react-native-community/netinfo": "11.4.1",
"@react-native-community/push-notification-ios": "1.11.0",
@@ -73,7 +73,7 @@
"react-native-quick-actions": "0.3.13",
"react-native-quick-crypto": "0.7.17",
"react-native-rsa-native": "2.0.5",
"react-native-safe-area-context": "5.6.1",
"react-native-safe-area-context": "5.6.2",
"react-native-securerandom": "1.0.1",
"react-native-share": "12.2.0",
"react-native-sqlite-storage": "6.0.1",
@@ -114,13 +114,13 @@
"@types/node": "18.19.130",
"@types/react": "19.0.14",
"@types/react-redux": "7.1.33",
"@types/serviceworker": "0.0.160",
"@types/serviceworker": "0.0.164",
"@types/tar-stream": "3.1.4",
"babel-jest": "29.7.0",
"babel-loader": "9.1.3",
"babel-plugin-module-resolver": "4.1.0",
"babel-plugin-react-native-web": "0.21.2",
"esbuild": "0.25.11",
"esbuild": "0.25.12",
"fast-deep-equal": "3.1.3",
"fs-extra": "11.3.2",
"gulp": "4.0.2",
@@ -132,7 +132,7 @@
"punycode": "2.3.1",
"react-dom": "19.0.0",
"react-native-web": "0.21.2",
"react-refresh": "0.17.0",
"react-refresh": "0.18.0",
"react-test-renderer": "19.0.0",
"sharp": "0.34.4",
"sqlite3": "5.1.6",

View File

@@ -11,6 +11,7 @@ import Alarm from '@joplin/lib/models/Alarm';
import time from '@joplin/lib/time';
import Logger from '@joplin/utils/Logger';
import NoteScreen from './components/screens/Note/Note';
import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen';
import Setting, { } from '@joplin/lib/models/Setting';
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
import { NotesParent, serializeNotesParent } from '@joplin/lib/reducer';
@@ -35,6 +36,15 @@ import reduxSharedMiddleware from '@joplin/lib/components/shared/reduxSharedMidd
const { AppNav } = require('./components/app-nav.js');
import Folder from '@joplin/lib/models/Folder';
import NotesScreen from './components/screens/Notes/Notes';
import TagsScreen from './components/screens/tags';
import ConfigScreen from './components/screens/ConfigScreen/ConfigScreen';
const { FolderScreen } = require('./components/screens/folder.js');
import LogScreen from './components/screens/LogScreen';
import StatusScreen from './components/screens/status';
import SearchScreen from './components/screens/SearchScreen';
const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js');
import EncryptionConfigScreen from './components/screens/encryption-config';
import DropboxLoginScreen from './components/screens/dropbox-login.js';
import { MenuProvider } from 'react-native-popup-menu';
import SideMenu, { SideMenuPosition } from './components/SideMenu';
import SideMenuContent from './components/side-menu-content';
@@ -53,47 +63,11 @@ const SyncTargetAmazonS3 = require('@joplin/lib/SyncTargetAmazonS3.js');
import SyncTargetJoplinServerSAML from '@joplin/lib/SyncTargetJoplinServerSAML';
import BiometricPopup from './components/biometrics/BiometricPopup';
import { isCallbackUrl, parseCallbackUrl, CallbackUrlCommand } from '@joplin/lib/callbackUrlUtils';
import JoplinCloudLoginScreen from './components/screens/JoplinCloudLoginScreen';
import SyncTargetNone from '@joplin/lib/SyncTargetNone';
// Lazy-loaded screens for faster startup
const TagsScreen = React.lazy(() => import('./components/screens/tags'));
const ConfigScreen = React.lazy(() => import('./components/screens/ConfigScreen/ConfigScreen'));
const FolderScreen = React.lazy(async () => {
// @ts-expect-error JS file without type declarations
const m: { FolderScreen: React.ComponentType } = await import('./components/screens/folder.js');
return { default: m.FolderScreen };
});
const LogScreen = React.lazy(() => import('./components/screens/LogScreen'));
const StatusScreen = React.lazy(() => import('./components/screens/status'));
const SearchScreen = React.lazy(() => import('./components/screens/SearchScreen'));
const OneDriveLoginScreen = React.lazy(async () => {
// @ts-expect-error JS file without type declarations
const m: { OneDriveLoginScreen: React.ComponentType } = await import('./components/screens/onedrive-login.js');
return { default: m.OneDriveLoginScreen };
});
const EncryptionConfigScreen = React.lazy(() => import('./components/screens/encryption-config'));
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- JS file without type declarations
const DropboxLoginScreen = React.lazy(async (): Promise<{ default: any }> => {
return await import('./components/screens/dropbox-login.js');
});
const JoplinCloudLoginScreen = React.lazy(() => import('./components/screens/JoplinCloudLoginScreen'));
const UpgradeSyncTargetScreen = React.lazy(() => import('./components/screens/UpgradeSyncTargetScreen'));
const ShareManager = React.lazy(() => import('./components/screens/ShareManager'));
const ProfileSwitcher = React.lazy(() => import('./components/ProfileSwitcher/ProfileSwitcher'));
const ProfileEditor = React.lazy(() => import('./components/ProfileSwitcher/ProfileEditor'));
const NoteRevisionViewer = React.lazy(() => import('./components/screens/NoteRevisionViewer'));
const DocumentScanner = React.lazy(() => import('./components/screens/DocumentScanner/DocumentScanner'));
const SyncWizard = React.lazy(() => import('./components/SyncWizard/SyncWizard'));
// SsoLoginScreen needs special handling due to its factory pattern
const SsoLoginScreen = React.lazy(async () => {
const [{ default: SsoLoginScreenFactory }, { default: SamlShared }] = await Promise.all([
import('./components/screens/SsoLoginScreen'),
import('@joplin/lib/components/shared/SamlShared'),
]);
return { default: SsoLoginScreenFactory(new SamlShared()) };
});
SyncTargetRegistry.addClass(SyncTargetNone);
SyncTargetRegistry.addClass(SyncTargetOneDrive);
@@ -111,21 +85,29 @@ import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import setupNotifications from './utils/setupNotifications';
import { loadMasterKeysFromSettings } from '@joplin/lib/services/e2ee/utils';
import { Theme, ThemeAppearance } from '@joplin/lib/themes/type';
import ProfileSwitcher from './components/ProfileSwitcher/ProfileSwitcher';
import ProfileEditor from './components/ProfileSwitcher/ProfileEditor';
import sensorInfo, { SensorInfo } from './components/biometrics/sensorInfo';
import { setDispatch } from './services/profiles';
import { ReactNode } from 'react';
import autodetectTheme, { onSystemColorSchemeChange } from './utils/autodetectTheme';
import PluginRunnerWebView from './components/plugins/PluginRunnerWebView';
import { refreshFolders, scheduleRefreshFolders } from '@joplin/lib/folders-screen-utils';
import ShareManager from './components/screens/ShareManager';
import { setDateFormat, setTimeFormat, setTimeLocale } from '@joplin/utils/time';
import DialogManager from './components/DialogManager';
import { AppState } from './utils/types';
import { getDisplayParentId } from '@joplin/lib/services/trash';
import PluginNotification from './components/plugins/PluginNotification';
import FocusControl from './components/accessibility/FocusControl/FocusControl';
import SsoLoginScreen from './components/screens/SsoLoginScreen';
import SamlShared from '@joplin/lib/components/shared/SamlShared';
import NoteRevisionViewer from './components/screens/NoteRevisionViewer';
import DocumentScanner from './components/screens/DocumentScanner/DocumentScanner';
import buildStartupTasks from './utils/buildStartupTasks';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import appReducer from './utils/appReducer';
import SyncWizard from './components/SyncWizard/SyncWizard';
const logger = Logger.create('root');
const perfLogger = PerformanceLogger.create();
@@ -729,7 +711,7 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
OneDriveLogin: { screen: OneDriveLoginScreen },
DropboxLogin: { screen: DropboxLoginScreen },
JoplinCloudLogin: { screen: JoplinCloudLoginScreen },
JoplinServerSamlLogin: { screen: SsoLoginScreen },
JoplinServerSamlLogin: { screen: SsoLoginScreen(new SamlShared()) },
EncryptionConfig: { screen: EncryptionConfigScreen },
UpgradeSyncTarget: { screen: UpgradeSyncTargetScreen },
ShareManager: { screen: ShareManager },
@@ -777,15 +759,11 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: '100%' }}>
<SafeAreaView style={{ flex: 1 }} titleBarUnderlayColor={theme.backgroundColor2}>
<View style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
<React.Suspense fallback={<View/>}>
{ shouldShowMainContent && <AppNav screens={appNavInit} dispatch={this.props.dispatch} /> }
</React.Suspense>
{ shouldShowMainContent && <AppNav screens={appNavInit} dispatch={this.props.dispatch} /> }
</View>
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied */}
<DropdownAlert alert={(func: any) => (this.dropdownAlert_ = func)} />
<React.Suspense fallback={null}>
<SyncWizard/>
</React.Suspense>
<SyncWizard/>
</SafeAreaView>
</View>
</SideMenu>

View File

@@ -6,6 +6,33 @@ import makeBlockReplaceExtension from './utils/makeBlockReplaceExtension';
const imageClassName = 'cm-md-image';
class ImageHeightCache {
private readonly cache = new Map<string, number>();
private readonly maxEntries = 500;
public get(key: string): number | undefined {
const value = this.cache.get(key);
if (value !== undefined) {
// Refresh recency
this.cache.delete(key);
this.cache.set(key, value);
}
return value;
}
public set(key: string, height: number): void {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.maxEntries) {
const firstKey = this.cache.keys().next().value;
if (firstKey) this.cache.delete(firstKey);
}
this.cache.set(key, height);
}
}
const imageHeightCache = new ImageHeightCache();
class ImageWidget extends WidgetType {
private resolvedSrc_: string;
@@ -41,9 +68,16 @@ class ImageWidget extends WidgetType {
const updateImageUrl = () => {
if (this.resolvedSrc_) {
// Use a background-image style property rather than img[src=]. This
// simplifies setting the image to the correct size/position.
image.src = this.resolvedSrc_;
// When the image loads, measure and cache the height
image.onload = () => {
// Measure container height (what CodeMirror uses for scroll calculations).
if (dom.isConnected) {
imageHeightCache.set(this.cacheKey, dom.offsetHeight);
}
dom.style.minHeight = '';
};
}
};
@@ -56,10 +90,16 @@ class ImageWidget extends WidgetType {
updateImageUrl();
}
// Apply cached height as min-height to prevent collapse during load.
const cached = imageHeightCache.get(this.cacheKey);
if (cached) {
dom.style.minHeight = `${cached}px`;
}
return true;
}
public toDOM() {
public toDOM(_view: EditorView) {
const container = document.createElement('div');
container.classList.add(imageClassName);
@@ -72,8 +112,12 @@ class ImageWidget extends WidgetType {
return container;
}
private get cacheKey() {
return `${this.src_}_${this.width_ ?? ''}_${this.reloadCounter_}`;
}
public get estimatedHeight() {
return -1;
return imageHeightCache.get(this.cacheKey) ?? -1;
}
}

View File

@@ -14,13 +14,18 @@ const createEditor = async (initialMarkdown: string, expectedTags: string[] = ['
describe('replaceInlineHtml', () => {
test.each([
{ markdown: '<sup>Test</sup>', expectedTagsQuery: 'sup' },
{ markdown: '<strike>Test</strike>', expectedTagsQuery: 'strike' },
{ markdown: 'Test: <span style="color: red;">Test</span>', expectedTagsQuery: 'span[style]' },
{ markdown: 'Test: <span style="color: rgb(123, 0, 0);">Test</span>', expectedTagsQuery: 'span[style]' },
])('should render inline HTML (case %#)', async ({ markdown, expectedTagsQuery }) => {
{ markdown: '<sup>Test</sup>', expectedDomTags: 'sup' },
{ markdown: '<strike>Test</strike>', expectedDomTags: 'strike' },
{ markdown: 'Test: <span style="color: red;">Test</span>', expectedDomTags: 'span[style]' },
{ markdown: 'Test: <span style="color: rgb(123, 0, 0);">Test</span>', expectedDomTags: 'span[style]' },
{
markdown: '<sup>Test *test*...</sup>',
expectedDomTags: 'sup',
initialSyntaxTags: ['HTMLTag', 'Emphasis'],
},
])('should render inline HTML (case %#)', async ({ markdown, expectedDomTags: expectedTagsQuery, initialSyntaxTags }) => {
// Add additional newlines: Ensure that the cursor isn't initially on the same line as the content to be rendered:
const editor = await createEditor(`\n\n${markdown}\n\n`);
const editor = await createEditor(`\n\n${markdown}\n\n`, initialSyntaxTags);
expect(editor.contentDOM.querySelector(expectedTagsQuery)).toBeTruthy();
});

View File

@@ -31,9 +31,9 @@ const createHtmlReplacementExtension = (tagName: string, onRenderContent: OnRend
// Find the matching closing tag
for (; !!cursor && nestedTagCounter > 0; cursor = cursor.nextSibling) {
const info = htmlNodeInfo(cursor, state);
if (isMatchingOpeningTag(info)) {
if (info && isMatchingOpeningTag(info)) {
nestedTagCounter ++;
} else if (isMatchingClosingTag(info)) {
} else if (info && isMatchingClosingTag(info)) {
nestedTagCounter --;
}

View File

@@ -139,25 +139,7 @@ const createTheme = (theme: EditorTheme): Extension[] => {
},
'& .cm-codeBlock': {
'&.cm-regionFirstLine, &.cm-regionLastLine': {
borderRadius: '3px',
},
'&:not(.cm-regionFirstLine)': {
borderTop: 'none',
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
},
'&:not(.cm-regionLastLine)': {
borderBottom: 'none',
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
},
borderWidth: '1px',
borderStyle: 'solid',
borderColor: theme.colorFaded,
backgroundColor: 'rgba(155, 155, 155, 0.1)',
backgroundColor: 'rgba(155, 155, 155, 0.07)',
...monospaceStyle,
},
@@ -269,8 +251,8 @@ const createTheme = (theme: EditorTheme): Extension[] => {
},
{
tag: tags.comment,
opacity: 0.9,
fontStyle: 'italic',
color: isDarkTheme ? '#b18eb1' : '#6d7086',
},
{
tag: tags.link,
@@ -281,26 +263,23 @@ const createTheme = (theme: EditorTheme): Extension[] => {
fontStyle: 'italic',
},
// Content of code blocks
// Content of code blocks. This should roughly match the colors used by the default
// highlight.js theme in the note viewer, while also preserving at least 4.5:1 contrast.
{
tag: tags.keyword,
color: isDarkTheme ? '#ff7' : '#740',
},
{
tag: tags.operator,
color: isDarkTheme ? '#f7f' : '#805',
color: isDarkTheme ? '#F92672' : '#a626a4',
},
{
tag: tags.literal,
color: isDarkTheme ? '#aaf' : '#037',
},
{
tag: tags.operator,
color: isDarkTheme ? '#fa9' : '#490',
tag: tags.number,
color: isDarkTheme ? '#d19a66' : '#986801',
},
{
tag: tags.typeName,
color: isDarkTheme ? '#7ff' : '#a00',
color: isDarkTheme ? '#d19a66' : '#986801',
},
{
tag: tags.inserted,
@@ -312,13 +291,21 @@ const createTheme = (theme: EditorTheme): Extension[] => {
},
{
tag: tags.propertyName,
color: isDarkTheme ? '#d96' : '#940',
color: isDarkTheme ? '#61aeee' : '#406be5',
},
{
tag: tags.string,
color: isDarkTheme ? '#98c379' : '#50a14f',
},
{
// CSS class names (and class names in other languages)
tag: tags.className,
color: isDarkTheme ? '#d8a' : '#904',
},
{
tag: tags.macroName,
color: isDarkTheme ? '#e6c07b' : '#986801',
},
]);
return [

View File

@@ -28,21 +28,21 @@
"typescript": "5.8.3"
},
"dependencies": {
"@codemirror/autocomplete": "6.18.3",
"@codemirror/commands": "6.7.1",
"@codemirror/lang-html": "6.4.9",
"@codemirror/lang-markdown": "6.3.1",
"@codemirror/autocomplete": "6.20.0",
"@codemirror/commands": "6.10.1",
"@codemirror/lang-html": "6.4.11",
"@codemirror/lang-markdown": "6.5.0",
"@codemirror/language": "6.10.4",
"@codemirror/language-data": "6.3.1",
"@codemirror/legacy-modes": "6.4.2",
"@codemirror/lint": "6.8.3",
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/lint": "6.9.2",
"@codemirror/search": "6.5.8",
"@codemirror/state": "6.4.1",
"@codemirror/state": "6.5.4",
"@codemirror/view": "6.35.0",
"@joplin/fork-uslug": "^2.0.0",
"@lezer/common": "1.2.3",
"@lezer/highlight": "1.2.1",
"@lezer/markdown": "1.3.2",
"@lezer/common": "1.5.0",
"@lezer/highlight": "1.2.3",
"@lezer/markdown": "1.6.3",
"@replit/codemirror-vim": "6.2.1",
"dompurify": "3.2.7",
"orderedmap": "2.1.1",

View File

@@ -520,6 +520,50 @@ describe('models/Folder.sharing', () => {
expect(note4.user_updated_time).toBe(userUpdatedTimes[note4.id]);
});
it('should prefer duplicating resources in unshared folders to shared folders', async () => {
const resourceService = new ResourceService();
const folder1 = await createFolderTree('', [
{
title: 'folder 1', // Share 1
children: [
{
title: 'note 1',
},
],
},
{
title: 'folder 2', // Not shared
children: [
{
title: 'note 2',
},
],
},
]);
let note1: NoteEntity = await Note.loadByTitle('note 1');
let note2: NoteEntity = await Note.loadByTitle('note 2');
await Folder.save({ id: folder1.id, share_id: 'share1' });
note1 = await shim.attachFileToNote(note1, testImagePath);
note2 = await Note.save({ id: note2.id, body: note1.body });
await msleep(1);
await resourceService.indexNoteResources(); // Populate note_resources
await Folder.updateAllShareIds(resourceService, []);
// After
expect(await Resource.all()).toHaveLength(2);
// note1 should have the same body
expect(await Note.load(note1.id)).toMatchObject({ body: note1.body, share_id: 'share1' });
// note2's body should be updated
expect(await Note.load(note2.id)).not.toMatchObject({ body: note2.body, share_id: '' });
});
it('should clear share_ids for items that are no longer part of an existing share', async () => {
await createFolderTree('', [
{

View File

@@ -639,12 +639,21 @@ export default class Folder extends BaseItem {
// one note. If it is not, we create duplicate resources so that
// each note has its own separate resource.
// Order unshared items first: This makes conflicts less likely, since shared
// items are more likely to be duplicated by multiple users.
const orderingSql = 'ORDER BY is_shared ASC';
const noteResourceAssociations = await this.db().selectAll(`
SELECT resource_id, note_id, notes.share_id
SELECT
resource_id,
note_id,
notes.share_id,
(notes.share_id != '') AS is_shared
FROM note_resources
LEFT JOIN notes ON notes.id = note_resources.note_id
WHERE resource_id IN (${this.escapeIdsForSql(resourceIds)})
AND is_associated = 1
${orderingSql}
`) as NoteResourceRow[];
const resourceIdToNotes: Record<string, NoteResourceRow[]> = {};

View File

@@ -48,7 +48,20 @@ describe('services/ResourceService', () => {
expect(!(await NoteResource.all()).length).toBe(true);
}));
it('should not delete resource if still associated with at least one note', (async () => {
it.each([
{
linkStyle: 'image 1',
markupTag: (id: string) => `![image](:/${id})`,
},
{
linkStyle: 'image 2',
markupTag: (id: string) => `![image][image]\n\n[image]: :/${id}`,
},
{
linkStyle: 'html link',
markupTag: (id: string) => `<a href=":/${id}">test</a>`,
},
])('should not delete resource if still associated with at least one note (link style: $linkStyle)', (async ({ markupTag }) => {
const service = new ResourceService();
const folder1 = await Folder.save({ title: 'folder1' });
@@ -63,7 +76,7 @@ describe('services/ResourceService', () => {
await service.indexNoteResources();
await Note.save({ id: note2.id, body: Resource.markupTag(resource1) });
await Note.save({ id: note2.id, body: markupTag(resource1.id) });
await service.indexNoteResources();

View File

@@ -79,6 +79,7 @@ describe('urlUtils', () => {
['Bla [](:/11111111111111111111111111111111 "Some title") bla [](:/22222222222222222222222222222222 "something else") bla', ['11111111111111111111111111111111', '22222222222222222222222222222222']],
['Bla <img src=":/fcca2938a96a22570e8eae2565bc6b0b"/> bla [](:/22222222222222222222222222222222) bla', ['fcca2938a96a22570e8eae2565bc6b0b', '22222222222222222222222222222222']],
['Bla <img src=":/fcca2938a96a22570e8eae2565bc6b0b"/> bla <a href=":/33333333333333333333333333333333"/>Some note link</a> blu [](:/22222222222222222222222222222222) bla', ['fcca2938a96a22570e8eae2565bc6b0b', '33333333333333333333333333333333', '22222222222222222222222222222222']],
['Link to [a test note] and [another] note.\n\n[a test note]: :/fcca2938a96a22570e8eae2565bc6b0b\n[another]: :/f04a2938a26822570e8eae2505bc6b0c', ['fcca2938a96a22570e8eae2565bc6b0b', 'f04a2938a26822570e8eae2505bc6b0c']],
['nothing here', []],
['', []],
];

View File

@@ -94,13 +94,20 @@ export const fileUrlToResourceUrl = (fileUrl: string, resourceDir: string) => {
};
export const extractResourceUrls = (text: string) => {
const markdownLinksRE = /\]\((.*?)\)/g;
const markdownLinkRegexes = [
// Standard [link](...)-style links
/\]\((.*?)\)/g,
// Reference links
/\]:(.*?)(?:[\n]|$)/g,
];
const output = [];
let result = null;
while ((result = markdownLinksRE.exec(text)) !== null) {
const resourceUrlInfo = parseResourceUrl(result[1]);
if (resourceUrlInfo) output.push(resourceUrlInfo);
for (const regex of markdownLinkRegexes) {
while ((result = regex.exec(text)) !== null) {
const resourceUrlInfo = parseResourceUrl(result[1].trim());
if (resourceUrlInfo) output.push(resourceUrlInfo);
}
}
const htmlRegexes = [

View File

@@ -767,14 +767,9 @@ impl AttachmentInfo {
.into())
} else if self.data_ref.starts_with("<invfdo>") {
// "invalid"
log_warn!("Attempted to load an invalid {} file", self.extension);
Err(parser_error!(
ResolutionFailed,
"Unable to load invalid file reference: {} (ext: {})",
self.data_ref,
self.extension
)
.into())
log_warn!("Attempted to load an invalid {} file. Importing an empty file.", self.extension);
// Return empty data
Ok(FileBlob::default())
} else {
Err(parser_error!(
ResolutionFailed,

View File

@@ -34,7 +34,7 @@
"bcryptjs": "2.4.3",
"bulma": "1.0.4",
"compare-versions": "6.1.1",
"dayjs": "1.11.18",
"dayjs": "1.11.19",
"formidable": "2.1.2",
"fs-extra": "11.3.2",
"html-entities": "1.4.0",

View File

@@ -1,7 +1,6 @@
import uuid from '@joplin/lib/uuid';
import Client from './Client';
import ClientPool from './ClientPool';
import { assertIsFolder, assertIsNote, FuzzContext, ItemId, RandomFolderOptions } from './types';
import { assertIsFolder, assertIsNote, FuzzContext, ItemId, RandomFolderOptions, ResourceData } from './types';
import { strict as assert } from 'assert';
import Logger from '@joplin/utils/Logger';
import retryWithCount from './utils/retryWithCount';
@@ -31,10 +30,12 @@ export default class ActionRunner {
await this.clientPool_.checkState();
}, {
count: 4,
delayOnFailure: count => count * Second * 2,
onFail: async () => {
logger.info('.checkState failed. Syncing all clients...');
await this.clientPool_.syncAll();
delayOnFailure: count => count * Second * 3,
onFail: async ({ willRetry }) => {
if (willRetry) {
logger.info('.checkState failed. Syncing all clients...');
await this.clientPool_.syncAll();
}
},
});
}
@@ -148,7 +149,7 @@ const getActions = (context: FuzzContext, clientPool: ClientPool, client: Client
// Create a toplevel folder to serve as this
// folder's parent if none exist yet
if (!parentId) {
parentId = uuid.create();
parentId = context.randomId();
await client.createFolder({
parentId: '',
id: parentId,
@@ -171,7 +172,7 @@ const getActions = (context: FuzzContext, clientPool: ClientPool, client: Client
await client.createNote({
...defaultNoteProperties,
parentId: await selectOrCreateWriteableFolder(),
id: uuid.create(),
id: context.randomId(),
title: 'Test note',
body: 'Body',
});
@@ -184,12 +185,16 @@ const getActions = (context: FuzzContext, clientPool: ClientPool, client: Client
};
const noteById = (id: ItemId) => {
assert.ok(client.itemExists(id), `Could not find note with ID ${id} in client ${client.email}'s expected state.`);
const note = client.itemById(id);
assertIsNote(note);
return note;
};
const folderById = (id: ItemId) => {
assert.ok(client.itemExists(id), `Could not find folder with ID ${id} in client ${client.email}'s expected state.`);
const folder = client.itemById(id);
assertIsFolder(folder);
return folder;
@@ -244,14 +249,29 @@ const getActions = (context: FuzzContext, clientPool: ClientPool, client: Client
addAction('updateNoteBody', async ({ id }) => {
const note = noteById(id);
await client.updateNote({
...note,
body: `${note.body}\n\nUpdated.\n`,
body: `${note.body}\n\nUpdated!`,
});
return true;
}, { id: selectOrCreateWriteableNote });
addAction('attachResourceTo', async ({ noteId, resourceId }) => {
const resourceData: ResourceData = {
id: resourceId,
mimeType: 'text/plain',
title: 'Test!',
};
await client.attachResource(noteById(noteId), resourceData);
return true;
}, {
noteId: selectOrCreateWriteableNote,
resourceId: () => context.randomId(),
});
addAction('moveNote', async ({ noteId, targetFolderId }) => {
const note = noteById(noteId);
const newParent = await folderByIdOrRandom(targetFolderId, {
@@ -267,6 +287,19 @@ const getActions = (context: FuzzContext, clientPool: ClientPool, client: Client
targetFolderId: undefinedId,
});
addAction('duplicateNote', async ({ id, newNoteId }) => {
const note = noteById(id);
await client.createNote({
...note,
id: newNoteId,
});
return true;
}, {
id: selectOrCreateWriteableNote,
newNoteId: () => context.randomId(),
});
addAction('deleteNote', async ({ id }) => {
const validatedNote = noteById(id); // Ensure, e.g., that the note exists
@@ -420,8 +453,12 @@ const getActions = (context: FuzzContext, clientPool: ClientPool, client: Client
}, {
delayOnFailure: (count) => Second * count,
count: 3,
onFail: async (error) => {
logger.warn('other.sync/other.checkState failed with', error, 'retrying...');
onFail: async ({ error, willRetry }) => {
logger.warn(
'other.sync/other.checkState failed with',
error,
willRetry ? 'retrying...' : '',
);
},
});

View File

@@ -1,6 +1,8 @@
import { strict as assert } from 'assert';
import { ActionableClient, FolderData, FuzzContext, ItemId, NoteData, ShareOptions, TreeItem, assertIsFolder, isFolder } from './types';
import { ActionableClient, FolderData, FuzzContext, ItemId, NoteData, ShareOptions, TreeItem, assertIsFolder, isFolder, isNote, isResource } from './types';
import FolderRecord from './model/FolderRecord';
import { extractResourceUrls } from '@joplin/lib/urlUtils';
import ResourceRecord from './model/ResourceRecord';
interface ClientData {
childIds: ItemId[];
@@ -68,44 +70,71 @@ class ActionTracker {
}
private checkRep_() {
const checkParentId = (item: TreeItem) => {
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(item.id), 'parent should include the current item in its children');
}
};
const checkFolder = (folder: FolderRecord) => {
for (const childId of folder.childIds) {
checkItem(childId);
}
// Shared folders
assert.ok(folder.ownedByEmail, 'all folders should have a "shareOwner" property (even if not shared)');
if (folder.isRootSharedItem) {
assert.equal(folder.parentId, '', 'only toplevel folders should be shared');
}
for (const sharedWith of folder.shareRecipients) {
assert.ok(this.tree_.has(sharedWith), 'all sharee users should exist');
}
// isSharedWith is only valid for toplevel folders
if (folder.parentId === '') {
assert.ok(!folder.isSharedWith(folder.ownedByEmail), 'the share owner should not be in an item\'s sharedWith list');
}
// Uniqueness
assert.equal(
folder.childIds.length,
[...new Set(folder.childIds)].length,
'child IDs should be unique',
);
};
const checkNote = (note: NoteData) => {
assert.ok(!isFolder(note));
assert.ok(!isResource(note));
};
const checkResource = (resource: ResourceRecord) => {
assert.ok(!isFolder(resource));
assert.ok(!isNote(resource));
assert.ok(isResource(resource));
// References list should be up-to-date
for (const noteId of resource.referencedBy) {
const note = this.idToItem_.get(noteId);
assert.ok(note, `all references should exist (testing ID ${noteId})`);
assert.ok(isNote(note), 'all references should be notes');
assert.ok(note.body.includes(resource.id), 'all references should include the resource ID');
}
};
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');
}
checkParentId(item);
if (isFolder(item)) {
for (const childId of item.childIds) {
checkItem(childId);
}
// Shared folders
assert.ok(item.ownedByEmail, 'all folders should have a "shareOwner" property (even if not shared)');
if (item.isRootSharedItem) {
assert.equal(item.parentId, '', 'only toplevel folders should be shared');
}
for (const sharedWith of item.shareRecipients) {
assert.ok(this.tree_.has(sharedWith), 'all sharee users should exist');
}
// isSharedWith is only valid for toplevel folders
if (item.parentId === '') {
assert.ok(!item.isSharedWith(item.ownedByEmail), 'the share owner should not be in an item\'s sharedWith list');
}
// Uniqueness
assert.equal(
item.childIds.length,
[...new Set(item.childIds)].length,
'child IDs should be unique',
);
checkFolder(item);
} else if (isNote(item)) {
checkNote(item);
} else {
checkResource(item);
}
};
@@ -263,6 +292,8 @@ class ActionTracker {
removeItemRecursive(childId);
}
} else if (isNote(item)) {
updateResourceReferences(item, { ...item, body: '' });
}
};
const mapItems = <T> (map: (item: TreeItem)=> T, startFolder?: FolderRecord) => {
@@ -282,6 +313,16 @@ class ActionTracker {
workList.push(childId);
}
}
if (isNote(item)) {
// Map linked resources
const linkedIds = extractResourceUrls(item.body);
for (const id of linkedIds) {
const item = this.idToItem_.get(id.itemId);
if (!item || !isResource(item)) continue;
result.push(map(item));
}
}
}
return result;
@@ -342,6 +383,50 @@ class ActionTracker {
this.checkRep_();
};
const updateResourceReferences = (noteBefore: NoteData|null, noteAfter: NoteData|null) => {
assert.ok(!!noteBefore || !!noteAfter, 'at least one of (noteBefore, noteAfter) must be specified');
if (noteBefore && noteAfter) {
assert.equal(noteBefore.id, noteAfter.id, 'changing note IDs is not supported');
}
const bodyBefore = noteBefore?.body ?? '';
const bodyAfter = noteAfter?.body ?? '';
if (bodyBefore === bodyAfter) return;
const id = noteBefore?.id ?? noteAfter?.id;
const referencesBefore = extractResourceUrls(bodyBefore).map(r => r.itemId);
const referencesAfter = extractResourceUrls(bodyAfter).map(r => r.itemId);
const newReferences = new Set(referencesAfter);
for (const reference of referencesBefore) {
newReferences.delete(reference);
}
const removedReferences = new Set(referencesBefore);
for (const reference of referencesAfter) {
removedReferences.delete(reference);
}
for (const reference of newReferences) {
const item = this.idToItem_.get(reference);
if (item && isResource(item)) {
updateItem(item.id, item.withReference(id), `referenced by ${id}`);
}
}
for (const reference of removedReferences) {
const item = this.idToItem_.get(reference);
if (item && isResource(item)) {
updateItem(
item.id,
item.withoutReference(id),
`dereferenced by ${id}`,
);
}
}
};
const tracker: ActionableClient = {
createNote: (data: NoteData) => {
assertWriteable(data.parentId);
@@ -350,8 +435,9 @@ class ActionTracker {
assert.ok(!this.idToItem_.has(data.id), `note ${data.id} should not yet exist`);
updateItem(data.id, {
...data,
}, 'created');
}, `created in ${data.parentId}`);
addChild(data.parentId, data.id);
updateResourceReferences(null, data);
this.checkRep_();
return Promise.resolve();
@@ -364,7 +450,7 @@ class ActionTracker {
assert.ok(!!data.parentId, `note ${data.id} should have a parentId`);
// Additional debugging information about what changed:
const changedFieldsInfo = Object.entries(data)
const changedFields = Object.entries(data)
.filter(([key, newValue]) => {
const itemKey = key as keyof NoteData;
// isShared is a virtual property
@@ -378,12 +464,39 @@ class ActionTracker {
removeChild(oldItem.parentId, data.id);
updateItem(data.id, {
...data,
}, `updated (changed fields: ${JSON.stringify(changedFieldsInfo)})`);
}, `updated (changed fields: ${JSON.stringify(changedFields)})`);
addChild(data.parentId, data.id);
updateResourceReferences(oldItem, data);
this.checkRep_();
return Promise.resolve();
},
attachResource: async (note, resource) => {
const resourceMarkup = `[resource](:/${resource.id})`;
const withAttached = { ...note, body: `${note.body}${resourceMarkup}` };
if (!tracker.itemExists(resource.id)) {
await tracker.createResource(resource);
}
await tracker.updateNote(withAttached);
return withAttached;
},
createResource: async (resource) => {
if (tracker.itemExists(resource.id)) {
// Don't double-create the item.
return Promise.resolve();
}
updateItem(
resource.id, new ResourceRecord({
...resource,
referencedBy: [],
}),
'created',
);
this.checkRep_();
return Promise.resolve();
},
createFolder: (data: FolderData) => {
const parentId = data.parentId ?? '';
assertWriteable(parentId);
@@ -395,7 +508,7 @@ class ActionTracker {
sharedWith: [],
ownedByEmail: clientId,
isShared: false,
}), 'created');
}), `created ${data.parentId ? `in ${data.parentId}` : '(toplevel)'}`);
addChild(data.parentId, data.id);
this.checkRep_();
@@ -419,7 +532,7 @@ class ActionTracker {
const item = this.idToItem_.get(id);
if (!item) throw new Error(`Not found ${id}`);
assert.ok(!isFolder(item), 'should be a note');
assert.ok(isNote(item), 'should be a note');
assertWriteable(item);
removeItemRecursive(id);
@@ -480,6 +593,7 @@ class ActionTracker {
},
moveItem: (itemId, newParentId) => {
const item = this.idToItem_.get(itemId);
assert.ok(isFolder(item) || isNote(item), `item with ${itemId} should be a folder or a note`);
const validateParameters = () => {
assert.ok(item, `item with ${itemId} should exist`);
@@ -514,10 +628,9 @@ class ActionTracker {
publishNote: (id) => {
const oldItem = this.idToItem_.get(id);
assert.ok(oldItem, 'should exist');
assert.ok(!isFolder(oldItem), 'folders cannot be published');
assert.ok(isNote(oldItem), 'only notes can be published');
assert.ok(!oldItem.published, 'should not be published');
updateItem(id, {
...oldItem,
published: true,
@@ -529,7 +642,7 @@ class ActionTracker {
unpublishNote: (id) => {
const oldItem = this.idToItem_.get(id);
assert.ok(oldItem, 'should exist');
assert.ok(!isFolder(oldItem), 'folders cannot be unpublished');
assert.ok(isNote(oldItem), 'only notes can be unpublished');
assert.ok(oldItem.published, 'should be published');
updateItem(id, {
@@ -541,9 +654,15 @@ class ActionTracker {
return Promise.resolve();
},
sync: () => Promise.resolve(),
listResources: () => {
const items = mapItems(item => {
return !isResource(item) ? null : item;
}).filter(item => !!item && item.referenceCount > 0);
return Promise.resolve(items);
},
listNotes: () => {
const notes = mapItems(item => {
return isFolder(item) ? null : item;
return !isNote(item) ? null : item;
}).filter(item => !!item).map(item => ({
...item,
isShared: isShared(item),
@@ -597,8 +716,11 @@ class ActionTracker {
return folders.length ? this.context_.randomFrom(folders) : null;
},
randomNote: async () => {
const notes = await tracker.listNotes();
randomNote: async (options) => {
let notes = await tracker.listNotes();
if (!options.includeReadOnly) {
notes = notes.filter(note => !isReadOnly(note.id));
}
const noteIndex = this.context_.randInt(0, notes.length);
return notes.length ? notes[noteIndex] : null;
},
@@ -609,6 +731,18 @@ class ActionTracker {
if (!item) throw new Error(`No item found with ID ${id}`);
return item;
},
itemExists: (id: ItemId) => {
const item = this.idToItem_.get(id);
if (!item) return false;
if (isResource(item)) return true;
const root = this.getToplevelParent_(id);
if (isFolder(root)) {
return root.ownedByEmail === client.email || root.isSharedWith(client.email);
}
return this.tree_.get(clientId).childIds.includes(id);
},
};
return tracker;
}

View File

@@ -1,5 +1,5 @@
import uuid, { createSecureRandom } from '@joplin/lib/uuid';
import { ActionableClient, FolderData, FuzzContext, HttpMethod, ItemId, Json, NoteData, RandomFolderOptions, RandomNoteOptions, ShareOptions } from './types';
import { ActionableClient, assertIsNote, FolderData, FuzzContext, HttpMethod, ItemId, Json, NoteData, RandomFolderOptions, RandomNoteOptions, ResourceData, ShareOptions } from './types';
import { join } from 'path';
import { mkdir, remove } from 'fs-extra';
import getStringProperty from './utils/getStringProperty';
@@ -14,7 +14,6 @@ import getNumberProperty from './utils/getNumberProperty';
import retryWithCount from './utils/retryWithCount';
import resolvePathWithinDir from '@joplin/lib/utils/resolvePathWithinDir';
import { formatMsToDateTimeLocal, msleep, Second } from '@joplin/utils/time';
import shim from '@joplin/lib/shim';
import { spawn } from 'child_process';
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
import { createInterface } from 'readline/promises';
@@ -23,6 +22,9 @@ import ProgressBar from './utils/ProgressBar';
import logDiffDebug from './utils/logDiffDebug';
import { NoteEntity } from '@joplin/lib/services/database/types';
import diffSortedStringArrays from './utils/diffSortedStringArrays';
import extractResourceIds from './utils/extractResourceIds';
import { substrWithEllipsis } from '@joplin/lib/string-utils';
import hangingIndent from './utils/hangingIndent';
const logger = Logger.create('Client');
@@ -105,6 +107,12 @@ interface CreateRandomItemOptions extends CreateOrUpdateOptions {
quiet?: boolean;
}
class ApiResponseError extends Error {
public constructor(public readonly code: number, message: string) {
super(message);
}
}
class Client implements ActionableClient {
public readonly email: string;
@@ -122,7 +130,7 @@ class Client implements ActionableClient {
}
private static async fromAccount(account: AccountData, actionTracker: ActionTracker, context: FuzzContext) {
const id = uuid.create();
const id = context.randomId();
const profileDirectory = join(context.baseDir, id);
await mkdir(profileDirectory);
@@ -242,6 +250,14 @@ class Client implements ActionableClient {
private closed_ = false;
public async close() {
if (this.closed_) {
// This can happen if:
// - Multiple cleanup callbacks are registered for the client.
// - The client was manually closed, but also has a cleanup callback registered.
logger.info('Client', this.clientLabel_, 'already closed. Skipping.');
return;
}
assert.ok(!this.closed_, 'should not be closed');
await this.account_.onClientDisconnected();
@@ -388,22 +404,24 @@ class Client implements ActionableClient {
// eslint-disable-next-line no-dupe-class-members -- This is not a duplicate class member
private async execApiCommand_(method: 'GET', route: string): Promise<string>;
// 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<string>;
private async execApiCommand_(method: 'POST'|'PUT', route: string, data: Json|FormData): Promise<string>;
// 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<string> {
private async execApiCommand_(method: HttpMethod, route: string, data: Json|FormData|null = null): Promise<string> {
route = route.replace(/^[/]/, '');
const url = new URL(`http://localhost:${this.apiData_.port}/${route}`);
url.searchParams.append('token', this.apiData_.token);
this.transcript_.push(`\n[[${method} ${url}; body: ${JSON.stringify(data)}]]\n`);
const response = await shim.fetch(url.toString(), {
const response = await fetch(url.toString(), {
method,
...(data ? { body: JSON.stringify(data) } : undefined),
...(data ? {
body: data instanceof FormData ? data : JSON.stringify(data),
} : undefined),
});
if (!response.ok) {
throw new Error(`Request to ${route} failed with error: ${await response.text()}`);
throw new ApiResponseError(response.status, `Request to ${route} failed with error: ${await response.text()}`);
}
return await response.text();
@@ -468,11 +486,114 @@ class Client implements ActionableClient {
// Certain sync failures self-resolve after a background task is allowed to
// run. Delay:
delayOnFailure: retry => retry * Second * 2,
onFail: async (error) => {
onFail: async ({ error, willRetry }) => {
logger.debug('Sync error: ', error);
logger.info('Sync failed. Retrying...');
if (willRetry) {
logger.info('Sync failed. Retrying...');
}
},
});
await this.handleResourceIdChanges_();
}
// Joplin occasionally changes the ID of a resource. Handle this here.
// Assumes that the client is up-to-date with the server.
private async handleResourceIdChanges_() {
type UntrackedAttachment = {
id: ItemId;
linkedNotes: Set<ItemId>;
};
const collectUntrackedAttachments = async () => {
// Maps from untracked item IDs to the notes that contain that item.
const untrackedItemsById = new Map<ItemId, UntrackedAttachment>();
const noteActualStates = new Map<ItemId, NoteData>();
for (const note of await this.listNotes()) {
// Skip notes that are not yet in the expected state. It's possible
// that these notes still need to be synced by another client. If so,
// attachments in these notes will be processed later:
if (!this.tracker_.itemExists(note.id)) continue;
for (const itemId of extractResourceIds(note.body)) {
if (this.tracker_.itemExists(itemId)) continue;
const noteIds = untrackedItemsById.get(itemId);
if (noteIds) {
noteIds.linkedNotes.add(note.id);
} else {
untrackedItemsById.set(itemId, {
id: itemId,
linkedNotes: new Set([note.id]),
});
}
noteActualStates.set(note.id, note);
}
}
return { untrackedItemsById, noteActualStates };
};
const fetchResourceData = async (resourceId: ItemId) => {
try {
const resourceJson = JSON.parse(
await this.execApiCommand_('GET', `/resources/${resourceId}?fields=id,title,mime`),
);
const resourceData: ResourceData = {
id: getStringProperty(resourceJson, 'id'),
mimeType: getStringProperty(resourceJson, 'mime'),
title: getStringProperty(resourceJson, 'title'),
};
return resourceData;
} catch (error) {
if (error instanceof ApiResponseError && error.code === 404) {
return null;
} else {
throw error;
}
}
};
const removeResourceIds = (text: string) => {
for (const id of extractResourceIds(text)) {
text = text.split(id).join('');
}
return text;
};
const textsMatchIgnoringResources = (actual: string, expected: string) => {
return removeResourceIds(expected) === removeResourceIds(actual);
};
const { untrackedItemsById, noteActualStates } = await collectUntrackedAttachments();
for (const { id: resourceId, linkedNotes } of untrackedItemsById.values()) {
const resourceData = await fetchResourceData(resourceId);
if (!resourceData) {
logger.warn('Resource not found:', resourceId);
continue;
}
await this.createResource(resourceData);
for (const id of linkedNotes) {
const expected = this.tracker_.itemById(id);
assertIsNote(expected);
const actual = noteActualStates.get(id);
assertIsNote(actual);
if (textsMatchIgnoringResources(actual.body, expected.body)) {
const firstMatchIndex = actual.body.indexOf(resourceId);
// This relies on the fact that **all** resource IDs are length-32 strings:
const originalId = expected.body.substring(firstMatchIndex, firstMatchIndex + 32);
logger.info('Resource rewrite: Updating note', id, ': Replacing', originalId, 'with', resourceId);
await this.tracker_.updateNote({
...expected,
body: expected.body.split(originalId).join(resourceId),
});
}
}
}
}
public async createOrUpdateMany(actionCount: number) {
@@ -530,7 +651,7 @@ class Client implements ActionableClient {
const titleLength = this.context_.randInt(1, 128);
const folder = {
parentId: parentId,
id: id ?? uuid.create(),
id: id ?? this.context_.randomId(),
title: this.context_.randomString(titleLength).replace(/\n/g, ' '),
};
@@ -609,7 +730,7 @@ class Client implements ActionableClient {
parentId,
title: this.context_.randomString(titleLength),
body: this.context_.randomString(bodyLength),
id: id ?? uuid.create(),
id: id ?? this.context_.randomId(),
}, { quiet });
}
@@ -652,6 +773,52 @@ class Client implements ActionableClient {
await this.execCliCommand_('rmnote', '--permanent', '--force', id);
}
public async attachResource(note: NoteData, resource: ResourceData): Promise<NoteData> {
logger.info('Attach resource', resource.id, 'to note', note.id);
const updatedNote = await this.tracker_.attachResource(note, resource);
await this.execApiCommand_('PUT', `/notes/${encodeURIComponent(note.id)}`, {
title: updatedNote.title,
body: updatedNote.body,
parent_id: updatedNote.parentId ?? '',
});
// Create the resource on the client *after* attaching it to the note so that the
// resource is always referenced by at least one note:
await this.createResource(resource);
await this.assertNoteMatchesState_(updatedNote);
return updatedNote;
}
public async createResource(resource: ResourceData): Promise<void> {
await this.tracker_.createResource(resource);
const checkExists = async () => {
try {
await this.execApiCommand_('GET', `/resources/${resource.id}`);
return true;
} catch (error) {
if (error instanceof ApiResponseError && error.code === 404) {
return false;
}
throw error;
}
};
if (!await checkExists()) {
const resourceForm = new FormData();
resourceForm.append('data', new Blob(['test'], { type: resource.mimeType }));
resourceForm.append('props', JSON.stringify({
title: resource.title,
id: resource.id,
mime: resource.mimeType,
}));
await this.execApiCommand_('POST', '/resources', resourceForm);
}
}
public async deleteFolder(id: string) {
logger.info('Delete folder', id, 'in', this.label);
await this.tracker_.deleteFolder(id);
@@ -698,8 +865,8 @@ class Client implements ActionableClient {
}, {
count: 2,
delayOnFailure: count => count * Second,
onFail: (error)=>{
logger.warn('Share failed:', error);
onFail: ({ error, willRetry })=>{
logger.warn('Share failed:', error, willRetry ? 'Retrying...' : '');
},
});
@@ -755,6 +922,24 @@ class Client implements ActionableClient {
await this.execCliCommand_('mv', itemId, movingToRoot ? 'root' : newParentId);
}
public async listResources() {
const params = {
fields: 'id,title,mime',
include_deleted: '1',
include_conflicts: '1',
};
return await this.execPagedApiCommand_(
'GET',
'/resources',
params,
(item): ResourceData => ({
id: getStringProperty(item, 'id'),
title: getStringProperty(item, 'title'),
mimeType: getStringProperty(item, 'mime'),
}),
);
}
public async listNotes() {
const params = {
fields: 'id,parent_id,body,title,is_conflict,conflict_original_id,share_id,is_shared',
@@ -812,10 +997,14 @@ class Client implements ActionableClient {
return this.tracker_.itemById(itemId);
}
public itemExists(itemId: ItemId) {
return this.tracker_.itemExists(itemId);
}
public async checkState() {
logger.info('Check state', this.label);
type ItemSlice = { id: string };
type ItemSlice = { id: string; title: string };
const compare = (a: ItemSlice, b: ItemSlice) => {
if (a.id === b.id) return 0;
return a.id < b.id ? -1 : 1;
@@ -833,32 +1022,46 @@ class Client implements ActionableClient {
}
};
const assertSameIds = (actualSorted: ItemSlice[], expectedSorted: ItemSlice[], testLabel: string) => {
const idLogs = (ids: ItemId[], items: ItemSlice[]) => {
const itemTitle = (id: ItemId) => {
const itemTitle = items.find(item => item.id === id)?.title;
return itemTitle ? JSON.stringify(substrWithEllipsis(itemTitle, 0, 28)) : 'Unknown';
};
const output = [];
for (const id of ids) {
const log = this.globalActionTracker_.getActionLog(id);
output.push(`id: ${id} (${itemTitle(id)})`);
if (log.length > 0) {
output.push(
log
.map(item => `\t${item.source}: ${item.action}`)
.join('\n'),
);
} else {
output.push('\tNo history found');
}
}
return output.join('\n');
};
const assertSameIds = async (actualSorted: ItemSlice[], expectedSorted: ItemSlice[], assertionLabel: string) => {
const actualIds = actualSorted.map(i => i.id);
const expectedIds = expectedSorted.map(i => i.id);
const { missing, unexpected } = diffSortedStringArrays(actualIds, expectedIds);
if (missing.length || unexpected.length) {
const idLogs = (ids: string[]) => {
const output = [];
for (const id of ids) {
const log = this.globalActionTracker_.getActionLog(id);
output.push(`\nid:${id}`);
output.push(log.map(item => `\t${item.source}: ${item.action}`).join('\n'));
}
return output.join('\n');
};
throw new Error([
`IDs were different (${testLabel}):`,
missing.length && `- Expected ${JSON.stringify(missing)} to be present, but were missing.`,
unexpected.length && `- Present but should not have been: ${JSON.stringify(unexpected)}`,
'\n',
const message = [
`${assertionLabel}: IDs were different:`,
missing.length && `Expected ${JSON.stringify(missing)} to be present, but were missing.`,
unexpected.length && `Present but should not have been: ${JSON.stringify(unexpected)}`,
'Logs:',
idLogs(missing),
idLogs(unexpected),
].filter(line => !!line).join('\n'));
idLogs(missing, expectedSorted),
idLogs(unexpected, actualSorted),
].filter(line => !!line).join('\n');
throw new Error(message);
}
};
@@ -871,7 +1074,7 @@ class Client implements ActionableClient {
assertNoAdjacentEqualIds(notes, 'notes');
assertNoAdjacentEqualIds(expectedNotes, 'expectedNotes');
assertSameIds(notes, expectedNotes, 'should have the same note IDs');
await assertSameIds(notes, expectedNotes, 'Note IDs should match');
assert.deepEqual(notes, expectedNotes, 'should have the same notes as the expected state');
};
@@ -884,12 +1087,49 @@ class Client implements ActionableClient {
assertNoAdjacentEqualIds(folders, 'folders');
assertNoAdjacentEqualIds(expectedFolders, 'expectedFolders');
assertSameIds(folders, expectedFolders, 'should have the same folder IDs');
await assertSameIds(folders, expectedFolders, 'Folder IDs should match');
assert.deepEqual(folders, expectedFolders, 'should have the same folders as the expected state');
};
await checkNoteState();
await checkFolderState();
const checkResourceState = async () => {
const actualResources = [...await this.listResources()];
const actualResourceIds = new Set(actualResources.map(r => r.id));
const expectedResources = [...await this.tracker_.listResources()];
const missingResources = [];
for (const resource of expectedResources) {
if (!actualResourceIds.has(resource.id)) {
missingResources.push(resource.id);
}
}
if (missingResources.length > 0) {
const log = idLogs(missingResources, expectedResources);
throw new Error(`Missing resource(s): All expected resources should exist on the client. Resource(s) with ID(s) ${JSON.stringify(missingResources)} were not found (total resource count: ${actualResourceIds.size}).\nResource action history:\n${log}`);
}
};
const errors: Error[] = [];
const runCheck = async (check: ()=> Promise<void>) => {
try {
await check();
} catch (error) {
errors.push(error);
}
};
await runCheck(checkResourceState);
await runCheck(checkNoteState);
await runCheck(checkFolderState);
if (errors.length) {
const errorList = errors
.map((error, index) => `Error ${index + 1} of ${errors.length}: ${error}`)
.map(message => hangingIndent(message))
.join('\n');
throw new Error(`Incorrect state in client: ${this.clientLabel_}:\n${errorList}`);
}
}
}

View File

@@ -19,8 +19,8 @@ const validateId = (id: string) => {
};
export default class FolderRecord implements FolderData {
public readonly parentId: string;
public readonly id: string;
public readonly parentId: ItemId;
public readonly id: ItemId;
public readonly title: string;
public readonly ownedByEmail: string;
public readonly childIds: ItemId[];

View File

@@ -0,0 +1,49 @@
import { ItemId, ResourceData } from '../types';
interface InitializationOptions extends ResourceData {
referencedBy: ItemId[];
}
export default class ResourceRecord implements ResourceData {
public readonly parentId: undefined;
public readonly id: ItemId;
public readonly title: string;
public readonly mimeType: string;
public readonly referencedBy: readonly ItemId[] = [];
public constructor(options: InitializationOptions) {
this.id = options.id;
this.title = options.title;
this.mimeType = options.mimeType;
this.referencedBy = [...options.referencedBy];
}
public get referenceCount() {
return this.referencedBy.length;
}
public withReference(noteId: ItemId) {
if (this.referencedBy.includes(noteId)) {
return this;
}
return new ResourceRecord({
id: this.id,
title: this.title,
mimeType: this.mimeType,
referencedBy: [...this.referencedBy, noteId],
});
}
public withoutReference(noteId: ItemId) {
if (this.referencedBy.includes(noteId)) {
return this;
}
return new ResourceRecord({
id: this.id,
title: this.title,
mimeType: this.mimeType,
referencedBy: this.referencedBy.filter(ref => ref !== noteId),
});
}
}

View File

@@ -13,6 +13,7 @@ import { packagesDir } from './constants';
import ActionRunner, { ActionSpec } from './ActionRunner';
import randomString from './utils/randomString';
import { readFile } from 'fs/promises';
import randomId from './utils/randomId';
const { shimInit } = require('@joplin/lib/shim-init-node');
const globalLogger = new Logger();
@@ -55,9 +56,10 @@ interface Options {
const createContext = (options: Options, server: Server, profilesDirectory: string) => {
const random = new SeededRandom(options.seed);
// Use a separate random number generator for strings. This prevents
// Use a separate random number generator for strings and IDs. This prevents
// the random strings setting from affecting the other output.
const stringRandom = new SeededRandom(random.next());
const idRandom = new SeededRandom(random.next());
if (options.isJoplinCloud) {
logger.info('Sync target: Joplin Cloud');
@@ -71,6 +73,7 @@ const createContext = (options: Options, server: Server, profilesDirectory: stri
return (_targetLength: number) => `Placeholder (x${stringCount++})`;
}
})();
const randomIdGenerator = randomId((min, max) => idRandom.nextInRange(min, max));
const fuzzContext: FuzzContext = {
serverUrl: server.url,
@@ -82,6 +85,7 @@ const createContext = (options: Options, server: Server, profilesDirectory: stri
randInt: (a, b) => random.nextInRange(a, b),
randomFrom: (data) => data[random.nextInRange(0, data.length)],
randomString: randomStringGenerator,
randomId: randomIdGenerator,
keepAccounts: options.keepAccountsOnClose,
};
return fuzzContext;

View File

@@ -1,5 +1,6 @@
import type Client from './Client';
import type FolderRecord from './model/FolderRecord';
import ResourceRecord from './model/ResourceRecord';
export type Json = string|number|Json[]|{ [key: string]: Json };
@@ -25,12 +26,26 @@ export interface DetailedFolderData extends FolderData {
isShared: boolean;
}
export type TreeItem = NoteData|FolderRecord;
export interface ResourceData {
id: ItemId;
title: string;
mimeType: string;
}
export type TreeItem = NoteData|FolderRecord|ResourceRecord;
export const isFolder = (item: TreeItem): item is FolderRecord => {
return 'childIds' in item;
};
export const isResource = (item: TreeItem): item is ResourceRecord => {
return 'mimeType' in item;
};
export const isNote = (item: TreeItem): item is NoteData => {
return !isFolder(item) && !isResource(item);
};
// Typescript type assertions require type definitions on the left for arrow functions.
// See https://github.com/microsoft/TypeScript/issues/53450.
export const assertIsFolder: (item: TreeItem)=> asserts item is FolderRecord = item => {
@@ -57,6 +72,7 @@ export interface FuzzContext {
execApi: (method: HttpMethod, route: string, debugAction: Json)=> Promise<Json>;
randInt: (low: number, high: number)=> number;
randomString: (targetLength: number)=> string;
randomId: ()=> string;
randomFrom: <T> (data: T[])=> T;
}
@@ -82,6 +98,8 @@ export interface ActionableClient {
deleteNote(id: ItemId): Promise<void>;
createNote(data: NoteData): Promise<void>;
updateNote(data: NoteData): Promise<void>;
attachResource(note: NoteData, resource: ResourceData): Promise<NoteData>;
createResource(resource: ResourceData): Promise<void>;
moveItem(itemId: ItemId, newParentId: ItemId): Promise<void>;
publishNote(id: ItemId): Promise<void>;
unpublishNote(id: ItemId): Promise<void>;
@@ -89,10 +107,12 @@ export interface ActionableClient {
listNotes(): Promise<NoteData[]>;
listFolders(): Promise<DetailedFolderData[]>;
listResources(): Promise<ResourceData[]>;
allFolderDescendants(parentId: ItemId): Promise<ItemId[]>;
randomFolder(options: RandomFolderOptions): Promise<FolderRecord>;
randomNote(options: RandomNoteOptions): Promise<NoteData>;
itemById(id: ItemId): TreeItem;
itemExists(id: ItemId): boolean;
}
export interface UserData {

View File

@@ -0,0 +1,7 @@
import { extractResourceUrls } from '@joplin/lib/urlUtils';
const extractResourceIds = (text: string) => {
return extractResourceUrls(text).map(item => item.itemId);
};
export default extractResourceIds;

View File

@@ -5,7 +5,9 @@ const getProperty = (object: unknown, propertyName: string) => {
}
if (!(propertyName in object)) {
throw new Error(`No such property ${JSON.stringify(propertyName)} in object`);
throw new Error(
`No such property ${JSON.stringify(propertyName)} in object. Available keys: (${JSON.stringify(Object.keys(object))})`,
);
}
return object[propertyName as keyof object];

View File

@@ -0,0 +1,7 @@
// Hanging indent: Indents all lines after the first
const hangingIndent = (text: string, indentation = ' ') => {
return text.replace(/\n/g, `\n${indentation}`);
};
export default hangingIndent;

View File

@@ -0,0 +1,12 @@
import randomId from './randomId';
describe('randomId', () => {
test('should generate a 32-character alphanumeric ID', () => {
expect(
randomId((_low, high) => high - 1)(),
).toBe('ffffffffffffffffffffffffffffffff');
expect(
randomId((low, _high) => low)(),
).toBe('00000000000000000000000000000000');
});
});

View File

@@ -0,0 +1,16 @@
type OnNextRandom = (lowInclusive: number, highExclusive: number)=> number;
const randomId = (nextRandomInteger: OnNextRandom)=> () => {
const bytes = [];
for (let i = 0; i < 16; i++) {
bytes.push(nextRandomInteger(0, 256));
}
return Buffer.from(bytes)
.toString('hex')
.toLowerCase()
.padStart(32, '0');
};
export default randomId;

View File

@@ -3,10 +3,15 @@ import { msleep } from '@joplin/utils/time';
const logger = Logger.create('retryWithCount');
interface FailureEvent {
error: Error;
willRetry: boolean;
}
interface Options {
count: number;
delayOnFailure?: (retryCount: number)=> number;
onFail: (error: Error)=> void|Promise<void>;
onFail: (event: FailureEvent)=> void|Promise<void>;
}
const retryWithCount = async (task: ()=> Promise<void>, { count, delayOnFailure, onFail }: Options) => {
@@ -15,10 +20,11 @@ const retryWithCount = async (task: ()=> Promise<void>, { count, delayOnFailure,
try {
return await task();
} catch (error) {
await onFail(error);
lastError = error;
const willRetry = retry + 1 < count;
await onFail({ error, willRetry });
const delay = willRetry && delayOnFailure ? delayOnFailure(retry + 1) : 0;
if (delay) {
logger.info(`Retrying after ${delay}ms...`);

View File

@@ -7,7 +7,9 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: Christoph Eder\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: Sebastian Aust <code@ryanthara.de>\n"
"Language-Team: \n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
@@ -331,7 +333,7 @@ msgstr "A5"
#: packages/lib/models/settings/builtInMetadata.ts:1121
msgid "ABC musical notation: Options"
msgstr ""
msgstr "ABC-Notenschrift: Optionen"
#: packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx:62
#: packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx:240
@@ -821,7 +823,7 @@ msgstr "Beta"
#: packages/app-mobile/components/NoteEditor/commandDeclarations.ts:123
msgid "Block code"
msgstr ""
msgstr "Blockcode"
#: packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts:55
#: packages/app-mobile/components/NoteEditor/commandDeclarations.ts:80
@@ -939,9 +941,8 @@ msgid "Cannot change encrypted item"
msgstr "Kann verschlüsseltes Element nicht ändern"
#: packages/lib/commands/convertNoteToMarkdown.ts:42
#, fuzzy
msgid "Cannot convert read-only item: \"%s\""
msgstr "Konnte neue Notiz nicht erstellen: %s"
msgstr "Schreibgeschütztes Element kann nicht konvertiert werden: %s"
#: packages/lib/models/Note.ts:622
msgid "Cannot copy note to \"%s\" notebook"
@@ -1103,7 +1104,7 @@ msgstr "Überprüfen… Bitte warten."
#: packages/app-desktop/gui/NoteContentPropertiesDialog.tsx:114
msgid "Chinese/Japanese/Korean characters"
msgstr ""
msgstr "Chinesische/Japanische/Koreanische Schriftzeichen"
#: packages/app-mobile/components/screens/Note/commands/attachFile.ts:98
msgid "Choose an option"
@@ -1270,7 +1271,7 @@ msgstr "Befehl"
#: packages/app-cli/app/command-keymap.ts:30
msgid "COMMAND"
msgstr ""
msgstr "COMMAND"
#: packages/app-desktop/plugins/GotoAnything.tsx:783
msgid "Command palette"
@@ -1334,9 +1335,8 @@ msgid "Configuration"
msgstr "Konfiguration"
#: packages/app-cli/app/command-keymap.ts:24
#, fuzzy
msgid "Configured keyboard shortcuts:"
msgstr "Tastaturkürzel"
msgstr "Konfigurierte Tastaturkürzel:"
#: packages/lib/models/settings/builtInMetadata.ts:1296
msgid "Configures the size of scrollbars used in the app."
@@ -1412,9 +1412,8 @@ msgid "Convert it"
msgstr "Umwandeln"
#: packages/lib/commands/convertNoteToMarkdown.ts:18
#, fuzzy
msgid "Convert to Markdown"
msgstr "Notiz in Markdown umwandeln"
msgstr "In Markdown umwandeln"
#: packages/app-mobile/components/screens/Note/Note.tsx:1350
msgid "Convert to note"
@@ -1521,9 +1520,8 @@ msgid "Could not connect to plugin repository."
msgstr "Konnte keine Verbindung zum Plugin-Repository herstellen."
#: packages/lib/commands/convertNoteToMarkdown.ts:70
#, fuzzy
msgid "Could not convert notes to Markdown: %s"
msgstr "Konnte Notiz nicht in Markdown umwandeln: %s"
msgstr "Notizen konnten nicht in Markdown konvertiert werden: %s"
#: packages/app-desktop/InteropServiceHelper.ts:235
msgid "Could not export notes: %s"
@@ -1837,13 +1835,14 @@ msgid "Delete profile \"%s\""
msgstr "Lösche Profil „%s“"
#: packages/app-desktop/gui/ProfileEditor.tsx:147
#, fuzzy
msgid ""
"Delete profile \"%s\"?\n"
"\n"
"All data, including notes, notebooks and tags will be permanently deleted."
msgstr ""
"Alle Daten, inklusive Notizen, Notizbücher und Schlagwörter werden dauerhaft "
"Profil „%s“ löschen?\n"
"\n"
"Alle Daten, einschließlich Notizen, Notizbücher und Tags, werden dauerhaft "
"gelöscht."
#: packages/editor/ProseMirror/plugins/tablePlugin.ts:36
@@ -1861,6 +1860,10 @@ msgid ""
"All notes associated with this tag will remain, but the tag will be removed "
"from all notes."
msgstr ""
"Schlagworte „%s“ löschen?\n"
"\n"
"Alle mit diesem Tag verbundenen Notizen bleiben erhalten, aber das Tag wird "
"aus allen Notizen entfernt."
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.ts:38
#: packages/app-mobile/components/side-menu-content.tsx:414
@@ -2045,7 +2048,7 @@ msgstr "Zeigt alle Informationen über die Notiz an."
#: packages/app-cli/app/command-keymap.ts:14
msgid "Displays the configured keyboard shortcuts."
msgstr ""
msgstr "Zeigt die konfigurierten Tastaturkürzel an."
#: packages/app-cli/app/command-cat.ts:14
msgid "Displays the given note."
@@ -2269,9 +2272,8 @@ msgid "Edit profile configuration..."
msgstr "Profilkonfiguration bearbeiten..."
#: packages/app-mobile/components/screens/tags.tsx:64
#, fuzzy
msgid "Edit tag"
msgstr "Notiz bearbeiten."
msgstr "Schlagwort bearbeiten"
#: packages/app-desktop/gui/MainScreen.tsx:129
#: packages/app-desktop/gui/NoteContentPropertiesDialog.tsx:151
@@ -2382,9 +2384,8 @@ msgid "Enable abbreviation syntax"
msgstr "Abkürzungssyntax aktivieren"
#: packages/lib/models/settings/builtInMetadata.ts:1093
#, fuzzy
msgid "Enable ABC musical notation support"
msgstr "Fountain-Syntaxunterstützung aktivieren"
msgstr "Unterstützung für ABC-Notenschrift aktivieren"
#: packages/lib/models/settings/builtInMetadata.ts:1095
msgid "Enable audio player"
@@ -3167,7 +3168,6 @@ msgid "Import or export your data"
msgstr "Importiere oder Exportiere deine Daten"
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/importFrom.ts:20
#, fuzzy
msgid "Import..."
msgstr "Importieren..."
@@ -3569,7 +3569,7 @@ msgstr "Unterstützter Schlüsselbund: %s"
#: packages/app-cli/app/command-keymap.ts:30
msgid "KEYS"
msgstr ""
msgstr "KEYS"
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:74
msgid "Keys that need upgrading"
@@ -3800,6 +3800,8 @@ msgid ""
"Manage your profiles. You can rename or delete profiles. The active profile "
"cannot be deleted."
msgstr ""
"Verwalte deine Profile. Du kannst Profile umbenennen oder löschen. Das "
"aktive Profil kann nicht gelöscht werden."
#. `generate-ppk`
#: packages/app-cli/app/command-e2ee.ts:19
@@ -3962,17 +3964,16 @@ msgstr[0] "%d Notiz in das Notizbuch „%s“ verschieben?"
msgstr[1] "%d Notizen in das Notizbuch „%s“ verschieben?"
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.ts:34
#, fuzzy
msgid ""
"Move %d notebooks to the trash?\n"
"\n"
"All notes and sub-notebooks within these notebooks will also be moved to the "
"trash."
msgstr ""
"Notizbuch „%s“ in den Papierkorb bewegen?\n"
"%d Notizbücher in den Papierkorb verschieben?\n"
"\n"
"Alle Notizen und Unter-Notizbücher in diesem Notizbuch werden ebenfalls in "
"den Papierkorb bewegt."
"Alle Notizen und Unternotizbücher in diesen Notizbüchern werden ebenfalls in "
"den Papierkorb verschoben."
#: packages/app-desktop/gui/ResizableLayout/MoveButtons.tsx:73
msgid "Move down"
@@ -4215,7 +4216,7 @@ msgstr "Keine Updates verfügbar"
#: packages/lib/components/shared/SamlShared.ts:12
msgid "No URL for SAML authentication set."
msgstr ""
msgstr "Keine URL für SAML-Authentifizierung festgelegt."
#: packages/app-cli/app/command-share.ts:188
#: packages/app-cli/app/command-share.ts:208
@@ -4565,7 +4566,7 @@ msgstr "Sync-Assistent öffnen..."
#: packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx:635
msgid "Open-source licences"
msgstr ""
msgstr "Open-Source-Lizenzen"
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts:87
msgid "Open..."
@@ -4604,6 +4605,9 @@ msgid ""
"Options that should be used whenever rendering ABC code. It must be a JSON5 "
"object. The full list of options is available at: %s"
msgstr ""
"Optionen, die beim Rendern von ABC-Code verwendet werden sollten. Es muss "
"sich um ein JSON5-Objekt handeln. Die vollständige Liste der Optionen "
"findest du unter: %s"
#: packages/app-mobile/components/NoteEditor/commandDeclarations.ts:103
msgid "Ordered list"
@@ -4979,9 +4983,8 @@ msgid "Profile name"
msgstr "Profilname"
#: packages/app-desktop/gui/ProfileEditor.tsx:120
#, fuzzy
msgid "Profile name cannot be empty"
msgstr "Bestätigungs-Passwort darf nicht leer sein"
msgstr "Profilname darf nicht leer sein"
#: packages/app-desktop/gui/ProfileEditor.tsx:116
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/addProfile.ts:18
@@ -5176,11 +5179,10 @@ msgid "Remove"
msgstr "Entfernen"
#: packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx:134
#, fuzzy
msgid "Remove %d tags from all notes? This cannot be undone."
msgstr ""
"Modell löschen und neu herunterladen?\n"
"Dies kann nicht rückgängig gemacht werden."
"%d Schlagworte aus allen Notizen entfernen? Dieser Vorgang kann nicht "
"rückgängig gemacht werden."
#: packages/app-mobile/components/TagEditor.tsx:136
msgid "Remove %s"
@@ -5598,7 +5600,7 @@ msgstr "Eltern-Notizbuch auswählen"
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/importFrom.ts:42
msgid "Select the type of file to be imported:"
msgstr ""
msgstr "Wähle den Typ der zu importierenden Datei aus:"
#: packages/app-mobile/components/ComboBox.tsx:378
msgid "Selected: %s"
@@ -5927,7 +5929,7 @@ msgstr "Quelle: "
#: packages/app-cli/app/command-keymap.ts:35
msgid "SPACE"
msgstr ""
msgstr "SPACE"
#: packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx:456
msgid "Spacer"
@@ -6241,18 +6243,16 @@ msgid "Tab moves focus"
msgstr "Tab verschiebt Fokus"
#: packages/app-mobile/components/NoteEditor/commandDeclarations.ts:118
#, fuzzy
msgid "Table"
msgstr "Aktivieren"
msgstr "Tabelle"
#: packages/lib/models/settings/builtInMetadata.ts:1440
msgid "Tabloid"
msgstr "Tabloid"
#: packages/app-mobile/components/screens/tags.tsx:206
#, fuzzy
msgid "Tag: %s"
msgstr "Nutzung: %s"
msgstr "Schlagwort: %s"
#: packages/app-cli/app/command-import.ts:58
#: packages/app-desktop/gui/ImportScreen.tsx:94
@@ -6485,7 +6485,6 @@ msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr "Die Notiz „%s“ wurde erfolgreich im Notizbuch „%s“ wiederhergestellt."
#: packages/lib/commands/convertNoteToMarkdown.ts:64
#, fuzzy
msgid ""
"The note has been converted to Markdown and the original note has been moved "
"to the trash"
@@ -6493,11 +6492,11 @@ msgid_plural ""
"The notes have been converted to Markdown and the original notes have been "
"moved to the trash"
msgstr[0] ""
"Die Notiz wurde in Markdown umgewandelt und die Originalnotiz in den "
"Papierkorb verschoben"
"Die Notiz wurde in Markdown konvertiert und die ursprüngliche Notiz wurde in "
"den Papierkorb verschoben"
msgstr[1] ""
"Die Notiz wurde in Markdown umgewandelt und die Originalnotiz in den "
"Papierkorb verschoben"
"Die Notizen wurden in Markdown konvertiert und die ursprünglichen Notizen "
"wurden in den Papierkorb verschoben"
#: packages/app-desktop/gui/TrashNotification/TrashNotification.tsx:45
msgid "The note was successfully moved to the trash."
@@ -7093,7 +7092,7 @@ msgstr "Jetzt testen"
#: packages/app-cli/app/command-keymap.ts:30
msgid "TYPE"
msgstr ""
msgstr "TYPE"
#: packages/app-cli/app/command-help.ts:72
msgid ""

File diff suppressed because it is too large Load Diff

View File

@@ -960,7 +960,7 @@ msgstr "Det går inte att hitta \"%s\"."
#: packages/app-cli/app/command-mkbook.ts:28
msgid "Cannot find: \"%s\""
msgstr "Det går inte att hitta: \"%s\"."
msgstr "Det går inte att hitta: \"%s\""
#: packages/app-cli/app/command-sync.ts:203
msgid "Cannot initialise synchroniser."
@@ -2357,7 +2357,7 @@ msgstr "Aktivera förkortningssyntax"
#: packages/lib/models/settings/builtInMetadata.ts:1093
msgid "Enable ABC musical notation support"
msgstr "Aktivera stöd för ABC musiknotation."
msgstr "Aktivera stöd för ABC musiknotation"
#: packages/lib/models/settings/builtInMetadata.ts:1095
msgid "Enable audio player"
@@ -2741,7 +2741,7 @@ msgstr ""
#: packages/app-desktop/gui/SsoLoginScreen/SsoLoginScreen.tsx:29
#: packages/app-mobile/components/screens/SsoLoginScreen.tsx:54
msgid "Failed to connect to your account. Please try again."
msgstr "Det gick inte att ansluta till ditt konto. Vänligen försök igen."
msgstr "Det gick inte att ansluta till ditt konto. Försök igen."
#: packages/app-cli/app/main.js:107
msgid "Fatal error:"
@@ -4750,7 +4750,7 @@ msgstr ""
#: packages/server/src/utils/saml.ts:58
msgid "Please wait while we load your organisation sign-in page..."
msgstr "Vänligen vänta medan vi laddar din organisations inloggningssida..."
msgstr "Vänta medan vi laddar din organisations inloggningssida..."
#: packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.tsx:118
#: packages/app-desktop/gui/ResourceScreen.tsx:315
@@ -6801,7 +6801,7 @@ msgid ""
"To allow Joplin to synchronise with Joplin Cloud, please login using this "
"URL:"
msgstr ""
"För att tillåta Joplin att synkronisera med Joplin Cloud, vänligen logga in "
"För att tillåta Joplin att synkronisera med Joplin Cloud, logga in "
"med denna URL:"
#: packages/app-desktop/gui/SsoLoginScreen/SsoLoginScreen.tsx:36

View File

@@ -25,7 +25,7 @@
"@joplin/renderer": "~3.6",
"@joplin/utils": "~3.6",
"compare-versions": "6.1.1",
"dayjs": "1.11.18",
"dayjs": "1.11.19",
"execa": "4.1.0",
"fs-extra": "11.3.2",
"gettext-parser": "7.0.1",
@@ -55,13 +55,13 @@
"@types/node": "18.19.130",
"@types/node-fetch": "2.6.13",
"@types/yargs": "17.0.34",
"gettext-extractor": "3.8.0",
"gettext-extractor": "4.0.1",
"gulp": "4.0.2",
"html-entities": "1.4.0",
"jest": "29.7.0",
"js-yaml": "4.1.1",
"rss": "1.2.2",
"sass": "1.93.2",
"sass": "1.93.3",
"sqlite3": "5.1.6",
"style-to-js": "1.1.18",
"ts-node": "10.9.2",

View File

@@ -16,7 +16,7 @@
"devDependencies": {
"@rollup/plugin-commonjs": "28.0.9",
"@rollup/plugin-node-resolve": "16.0.3",
"@rollup/plugin-replace": "6.0.2",
"@rollup/plugin-replace": "6.0.3",
"browserify": "14.5.0",
"rollup": "4.2.0",
"standard": "17.1.2",

View File

@@ -37,7 +37,7 @@
"dependencies": {
"@joplin/fork-htmlparser2": "^4.1.60",
"async-mutex": "0.5.0",
"dayjs": "1.11.18",
"dayjs": "1.11.19",
"execa": "5.1.1",
"fs-extra": "11.3.2",
"glob": "11.0.3",

View File

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

597
yarn.lock

File diff suppressed because it is too large Load Diff