Merge branch 'release-2.2' into dev

pull/5294/head
Laurent Cozic 2021-08-11 16:42:22 +01:00
commit 3a22674c03
41 changed files with 637 additions and 6152 deletions

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,7 @@
], ],
"owner": "Laurent Cozic" "owner": "Laurent Cozic"
}, },
"version": "2.2.1", "version": "2.2.2",
"bin": { "bin": {
"joplin": "./main.js" "joplin": "./main.js"
}, },
@ -42,6 +42,7 @@
"dependencies": { "dependencies": {
"@joplin/lib": "~2.2", "@joplin/lib": "~2.2",
"@joplin/renderer": "~2.2", "@joplin/renderer": "~2.2",
"aws-sdk": "^2.588.0",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"compare-version": "^0.1.2", "compare-version": "^0.1.2",
"fs-extra": "^5.0.0", "fs-extra": "^5.0.0",

View File

@ -41,22 +41,6 @@ joplin.plugins.register({
const result3 = await dialogs.open(handle3); const result3 = await dialogs.open(handle3);
console.info('Got result: ' + JSON.stringify(result3)); console.info('Got result: ' + JSON.stringify(result3));
const handle4 = await dialogs.create('myDialog4');
await dialogs.setHtml(handle4, `
<h1>This dialog tests dynamic sizing</h1>
<h3>Resize the window and the dialog should resize accordingly</h3>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum
</p>
`);
await (dialogs as any).setFitToContent(handle4, false);
await dialogs.open(handle4);
}, },
}); });

View File

@ -782,7 +782,6 @@ class MainScreenComponent extends React.Component<Props, State> {
scripts={view.scripts} scripts={view.scripts}
pluginId={plugin.id} pluginId={plugin.id}
buttons={view.buttons} buttons={view.buttons}
fitToContent={view.fitToContent}
/>); />);
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@joplin/app-desktop", "name": "@joplin/app-desktop",
"version": "2.2.6", "version": "2.2.7",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@joplin/app-desktop", "name": "@joplin/app-desktop",
"version": "2.2.6", "version": "2.2.7",
"description": "Joplin for Desktop", "description": "Joplin for Desktop",
"main": "main.js", "main": "main.js",
"private": true, "private": true,

View File

@ -31,8 +31,8 @@ export interface Props {
const StyledFrame = styled.iframe` const StyledFrame = styled.iframe`
padding: 0; padding: 0;
margin: 0; margin: 0;
width: ${(props: any) => props.fitToContent ? `${props.width}px` : '90vw'}; width: ${(props: any) => props.fitToContent ? `${props.width}px` : '100%'};
height: ${(props: any) => props.fitToContent ? `${props.height}px` : '80vh'}; height: ${(props: any) => props.fitToContent ? `${props.height}px` : '100%'};
border: none; border: none;
border-bottom: ${(props: Props) => props.borderBottom ? `1px solid ${props.theme.dividerColor}` : 'none'}; border-bottom: ${(props: Props) => props.borderBottom ? `1px solid ${props.theme.dividerColor}` : 'none'};
`; `;

View File

@ -9,7 +9,6 @@ const styled = require('styled-components').default;
interface Props extends UserWebviewProps { interface Props extends UserWebviewProps {
buttons: ButtonSpec[]; buttons: ButtonSpec[];
fitToContent: boolean;
} }
const StyledRoot = styled.div` const StyledRoot = styled.div`
@ -114,7 +113,7 @@ export default function UserWebviewDialog(props: Props) {
viewId={props.viewId} viewId={props.viewId}
themeId={props.themeId} themeId={props.themeId}
borderBottom={false} borderBottom={false}
fitToContent={props.fitToContent} fitToContent={true}
onSubmit={onSubmit} onSubmit={onSubmit}
onDismiss={onDismiss} onDismiss={onDismiss}
onReady={onReady} onReady={onReady}

View File

@ -141,10 +141,10 @@ android {
applicationId "net.cozic.joplin" applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097642 versionCode 2097644
versionName "2.2.3" versionName "2.2.5"
ndk { ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64" abiFilters "armeabi-v7a", "x86"
} }
// https://github.com/react-native-community/react-native-camera/issues/2138 // https://github.com/react-native-community/react-native-camera/issues/2138
@ -158,7 +158,7 @@ android {
reset() reset()
enable enableSeparateBuildPerCPUArchitecture enable enableSeparateBuildPerCPUArchitecture
universalApk false // If true, also generate a universal APK universalApk false // If true, also generate a universal APK
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" include "armeabi-v7a", "x86"
} }
} }
signingConfigs { signingConfigs {

View File

@ -6,10 +6,6 @@
// So there's basically still a one way flux: React => SQLite => Redux => React // So there's basically still a one way flux: React => SQLite => Redux => React
// For aws-sdk-js-v3
import 'react-native-get-random-values';
import 'react-native-url-polyfill/auto';
import { LogBox, AppRegistry } from 'react-native'; import { LogBox, AppRegistry } from 'react-native';
const Root = require('./root').default; const Root = require('./root').default;

View File

@ -221,8 +221,6 @@ PODS:
- React-Core - React-Core
- react-native-geolocation (2.0.2): - react-native-geolocation (2.0.2):
- React - React
- react-native-get-random-values (1.7.0):
- React-Core
- react-native-image-picker (2.3.4): - react-native-image-picker (2.3.4):
- React-Core - React-Core
- react-native-image-resizer (1.3.0): - react-native-image-resizer (1.3.0):
@ -347,7 +345,6 @@ DEPENDENCIES:
- react-native-camera (from `../node_modules/react-native-camera`) - react-native-camera (from `../node_modules/react-native-camera`)
- react-native-document-picker (from `../node_modules/react-native-document-picker`) - react-native-document-picker (from `../node_modules/react-native-document-picker`)
- "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)" - "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)"
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
- react-native-image-picker (from `../node_modules/react-native-image-picker`) - react-native-image-picker (from `../node_modules/react-native-image-picker`)
- react-native-image-resizer (from `../node_modules/react-native-image-resizer`) - react-native-image-resizer (from `../node_modules/react-native-image-resizer`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
@ -426,8 +423,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-document-picker" :path: "../node_modules/react-native-document-picker"
react-native-geolocation: react-native-geolocation:
:path: "../node_modules/@react-native-community/geolocation" :path: "../node_modules/@react-native-community/geolocation"
react-native-get-random-values:
:path: "../node_modules/react-native-get-random-values"
react-native-image-picker: react-native-image-picker:
:path: "../node_modules/react-native-image-picker" :path: "../node_modules/react-native-image-picker"
react-native-image-resizer: react-native-image-resizer:
@ -512,7 +507,6 @@ SPEC CHECKSUMS:
react-native-camera: 5c1fbfecf63b802b8ca4a71c60d30a71550fb348 react-native-camera: 5c1fbfecf63b802b8ca4a71c60d30a71550fb348
react-native-document-picker: b3e78a8f7fef98b5cb069f20fc35797d55e68e28 react-native-document-picker: b3e78a8f7fef98b5cb069f20fc35797d55e68e28
react-native-geolocation: cbd9d6bd06bac411eed2671810f454d4908484a8 react-native-geolocation: cbd9d6bd06bac411eed2671810f454d4908484a8
react-native-get-random-values: 237bffb1c7e05fb142092681531810a29ba53015
react-native-image-picker: 32d1ad2c0024ca36161ae0d5c2117e2d6c441f11 react-native-image-picker: 32d1ad2c0024ca36161ae0d5c2117e2d6c441f11
react-native-image-resizer: b53bf95ad880100e20262687e41f76fdbc9df255 react-native-image-resizer: b53bf95ad880100e20262687e41f76fdbc9df255
react-native-netinfo: 34f4d7a42f49157f3b45c14217d256bce7dc9682 react-native-netinfo: 34f4d7a42f49157f3b45c14217d256bce7dc9682

View File

@ -3636,11 +3636,6 @@
"time-stamp": "^1.0.0" "time-stamp": "^1.0.0"
} }
}, },
"fast-base64-decode": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz",
"integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q=="
},
"fb-watchman": { "fb-watchman": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz",
@ -7073,11 +7068,6 @@
"escape-goat": "^2.0.0" "escape-goat": "^2.0.0"
} }
}, },
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
},
"range-parser": { "range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -7352,14 +7342,6 @@
"utf8": "^3.0.0" "utf8": "^3.0.0"
} }
}, },
"react-native-get-random-values": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/react-native-get-random-values/-/react-native-get-random-values-1.7.0.tgz",
"integrity": "sha512-zDhmpWUekGRFb9I+MQkxllHcqXN9HBSsgPwBQfrZ1KZYpzDspWLZ6/yLMMZrtq4pVqNR7C7N96L3SuLpXv1nhQ==",
"requires": {
"fast-base64-decode": "^1.0.0"
}
},
"react-native-image-picker": { "react-native-image-picker": {
"version": "2.3.4", "version": "2.3.4",
"resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-2.3.4.tgz", "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-2.3.4.tgz",
@ -7423,14 +7405,6 @@
"resolved": "https://registry.npmjs.org/react-native-sqlite-storage/-/react-native-sqlite-storage-5.0.0.tgz", "resolved": "https://registry.npmjs.org/react-native-sqlite-storage/-/react-native-sqlite-storage-5.0.0.tgz",
"integrity": "sha512-c1Joq3/tO1nmIcP8SkRZNolPSbfvY8uZg5lXse0TmjIPC0qHVbk96IMvWGyly1TmYCIpxpuDRc0/xCffDbYIvg==" "integrity": "sha512-c1Joq3/tO1nmIcP8SkRZNolPSbfvY8uZg5lXse0TmjIPC0qHVbk96IMvWGyly1TmYCIpxpuDRc0/xCffDbYIvg=="
}, },
"react-native-url-polyfill": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-1.3.0.tgz",
"integrity": "sha512-w9JfSkvpqqlix9UjDvJjm1EjSt652zVQ6iwCIj1cVVkwXf4jQhQgTNXY6EVTwuAmUjg6BC6k9RHCBynoLFo3IQ==",
"requires": {
"whatwg-url-without-unicode": "8.0.0-3"
}
},
"react-native-vector-icons": { "react-native-vector-icons": {
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-7.1.0.tgz", "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-7.1.0.tgz",
@ -9092,22 +9066,6 @@
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI="
}, },
"url": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
"integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
"requires": {
"punycode": "1.3.2",
"querystring": "0.2.0"
},
"dependencies": {
"punycode": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
}
}
},
"url-parse-lax": { "url-parse-lax": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
@ -9274,26 +9232,11 @@
"defaults": "^1.0.3" "defaults": "^1.0.3"
} }
}, },
"webidl-conversions": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
"integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA=="
},
"whatwg-fetch": { "whatwg-fetch": {
"version": "3.4.1", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz",
"integrity": "sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ==" "integrity": "sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ=="
}, },
"whatwg-url-without-unicode": {
"version": "8.0.0-3",
"resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz",
"integrity": "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==",
"requires": {
"buffer": "^5.4.3",
"punycode": "^2.1.1",
"webidl-conversions": "^5.0.0"
}
},
"which": { "which": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",

View File

@ -39,7 +39,6 @@
"react-native-dropdownalert": "^3.1.2", "react-native-dropdownalert": "^3.1.2",
"react-native-file-viewer": "^2.1.4", "react-native-file-viewer": "^2.1.4",
"react-native-fs": "^2.16.6", "react-native-fs": "^2.16.6",
"react-native-get-random-values": "^1.7.0",
"react-native-image-picker": "^2.3.4", "react-native-image-picker": "^2.3.4",
"react-native-image-resizer": "^1.3.0", "react-native-image-resizer": "^1.3.0",
"react-native-modal-datetime-picker": "^9.0.0", "react-native-modal-datetime-picker": "^9.0.0",
@ -50,7 +49,6 @@
"react-native-share": "^5.1.5", "react-native-share": "^5.1.5",
"react-native-side-menu": "^1.1.3", "react-native-side-menu": "^1.1.3",
"react-native-sqlite-storage": "^5.0.0", "react-native-sqlite-storage": "^5.0.0",
"react-native-url-polyfill": "^1.3.0",
"react-native-vector-icons": "^7.1.0", "react-native-vector-icons": "^7.1.0",
"react-native-version-info": "^1.1.0", "react-native-version-info": "^1.1.0",
"react-native-webview": "^10.9.2", "react-native-webview": "^10.9.2",
@ -61,7 +59,6 @@
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
"string-natural-compare": "^2.0.2", "string-natural-compare": "^2.0.2",
"timers": "^0.1.1", "timers": "^0.1.1",
"url": "^0.11.0",
"valid-url": "^1.0.9" "valid-url": "^1.0.9"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,6 +1,8 @@
import FsDriverBase from '@joplin/lib/fs-driver-base'; import FsDriverBase from '@joplin/lib/fs-driver-base';
const RNFetchBlob = require('rn-fetch-blob').default; const RNFetchBlob = require('rn-fetch-blob').default;
const RNFS = require('react-native-fs'); const RNFS = require('react-native-fs');
const { Writable } = require('stream-browserify');
const { Buffer } = require('buffer');
export default class FsDriverRN extends FsDriverBase { export default class FsDriverRN extends FsDriverBase {
public appendFileSync() { public appendFileSync() {
@ -24,6 +26,27 @@ export default class FsDriverRN extends FsDriverBase {
return await this.unlink(path); return await this.unlink(path);
} }
public writeBinaryFile(path: string, content: any) {
const buffer = Buffer.from(content);
return RNFetchBlob.fs.writeStream(path, 'base64').then((stream: any) => {
const fileStream = new Writable({
write(chunk: any, _encoding: any, callback: Function) {
this.stream.write(chunk.toString('base64'));
callback();
},
final(callback: Function) {
this.stream.close();
callback();
},
});
// using options.construct is not implemented in readable-stream so lets
// pass the stream from RNFetchBlob to the Writable instance here
fileStream.stream = stream;
fileStream.write(buffer);
fileStream.end();
});
}
// Returns a format compatible with Node.js format // Returns a format compatible with Node.js format
private rnfsStatToStd_(stat: any, path: string) { private rnfsStatToStd_(stat: any, path: string) {
return { return {

View File

@ -1,6 +1,6 @@
{ {
"name": "@joplin/fork-htmlparser2", "name": "@joplin/fork-htmlparser2",
"version": "4.1.31", "version": "4.1.32",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,7 +1,7 @@
{ {
"name": "@joplin/fork-htmlparser2", "name": "@joplin/fork-htmlparser2",
"description": "Fast & forgiving HTML/XML/RSS parser", "description": "Fast & forgiving HTML/XML/RSS parser",
"version": "4.1.31", "version": "4.1.32",
"author": "Felix Boehm <me@feedic.com>", "author": "Felix Boehm <me@feedic.com>",
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@ -1,6 +1,6 @@
{ {
"name": "@joplin/fork-sax", "name": "@joplin/fork-sax",
"version": "1.2.35", "version": "1.2.36",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -2,7 +2,7 @@
"name": "@joplin/fork-sax", "name": "@joplin/fork-sax",
"description": "An evented streaming XML parser in JavaScript", "description": "An evented streaming XML parser in JavaScript",
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)", "author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
"version": "1.2.35", "version": "1.2.36",
"main": "lib/sax.js", "main": "lib/sax.js",
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@ -4,7 +4,7 @@ const Setting = require('./models/Setting').default;
const { FileApi } = require('./file-api.js'); const { FileApi } = require('./file-api.js');
const Synchronizer = require('./Synchronizer').default; const Synchronizer = require('./Synchronizer').default;
const { FileApiDriverAmazonS3 } = require('./file-api-driver-amazon-s3.js'); const { FileApiDriverAmazonS3 } = require('./file-api-driver-amazon-s3.js');
const { S3Client, HeadBucketCommand } = require('@aws-sdk/client-s3'); const S3 = require('aws-sdk/clients/s3');
class SyncTargetAmazonS3 extends BaseSyncTarget { class SyncTargetAmazonS3 extends BaseSyncTarget {
static id() { static id() {
@ -38,14 +38,10 @@ class SyncTargetAmazonS3 extends BaseSyncTarget {
s3AuthParameters() { s3AuthParameters() {
return { return {
// We need to set a region. See https://github.com/aws/aws-sdk-js-v3/issues/1845#issuecomment-754832210 accessKeyId: Setting.value('sync.8.username'),
region: 'us-east-1', secretAccessKey: Setting.value('sync.8.password'),
credentials: { s3UseArnRegion: true, // override the request region with the region inferred from requested resource's ARN
accessKeyId: Setting.value('sync.8.username'), s3ForcePathStyle: true,
secretAccessKey: Setting.value('sync.8.password'),
},
UseArnRegion: true, // override the request region with the region inferred from requested resource's ARN
forcePathStyle: true,
endpoint: Setting.value('sync.8.url'), endpoint: Setting.value('sync.8.url'),
}; };
} }
@ -53,31 +49,20 @@ class SyncTargetAmazonS3 extends BaseSyncTarget {
api() { api() {
if (this.api_) return this.api_; if (this.api_) return this.api_;
this.api_ = new S3Client(this.s3AuthParameters()); this.api_ = new S3(this.s3AuthParameters());
// There is a bug with auto skew correction in aws-sdk-js-v3
// and this attempts to remove the skew correction for all calls.
// There are some additional spots in the app where we reset this
// to zero as well as it appears the skew logic gets triggered
// which makes "RequestTimeTooSkewed" errors...
// See https://github.com/aws/aws-sdk-js-v3/issues/2208
this.api_.config.systemClockOffset = 0;
return this.api_; return this.api_;
} }
static async newFileApi_(syncTargetId, options) { static async newFileApi_(syncTargetId, options) {
const apiOptions = { const apiOptions = {
region: 'us-east-1', accessKeyId: options.username(),
credentials: { secretAccessKey: options.password(),
accessKeyId: options.username(), s3UseArnRegion: true,
secretAccessKey: options.password(), s3ForcePathStyle: true,
},
UseArnRegion: true, // override the request region with the region inferred from requested resource's ARN
forcePathStyle: true,
endpoint: options.url(), endpoint: options.url(),
}; };
const api = new S3Client(apiOptions); const api = new S3(apiOptions);
const driver = new FileApiDriverAmazonS3(api, SyncTargetAmazonS3.s3BucketName()); const driver = new FileApiDriverAmazonS3(api, SyncTargetAmazonS3.s3BucketName());
const fileApi = new FileApi('', driver); const fileApi = new FileApi('', driver);
fileApi.setSyncTargetId(syncTargetId); fileApi.setSyncTargetId(syncTargetId);
@ -95,14 +80,12 @@ class SyncTargetAmazonS3 extends BaseSyncTarget {
try { try {
const headBucketReq = new Promise((resolve, reject) => { const headBucketReq = new Promise((resolve, reject) => {
fileApi.driver().api().send( fileApi.driver().api().headBucket({
Bucket: options.path(),
new HeadBucketCommand({ },(err, response) => {
Bucket: options.path(), if (err) reject(err);
}),(err, response) => { else resolve(response);
if (err) reject(err); });
else resolve(response);
});
}); });
const result = await headBucketReq; const result = await headBucketReq;
if (!result) throw new Error(`AWS S3 bucket not found: ${SyncTargetAmazonS3.s3BucketName()}`); if (!result) throw new Error(`AWS S3 bucket not found: ${SyncTargetAmazonS3.s3BucketName()}`);

View File

@ -85,6 +85,7 @@ shared.saveSettings = function(comp) {
for (const key in comp.state.settings) { for (const key in comp.state.settings) {
if (!comp.state.settings.hasOwnProperty(key)) continue; if (!comp.state.settings.hasOwnProperty(key)) continue;
if (comp.state.changedSettingKeys.indexOf(key) < 0) continue; if (comp.state.changedSettingKeys.indexOf(key) < 0) continue;
console.info('Saving', key, comp.state.settings[key]);
Setting.setValue(key, comp.state.settings[key]); Setting.setValue(key, comp.state.settings[key]);
} }

View File

@ -3,8 +3,6 @@ const { basename } = require('./path-utils');
const shim = require('./shim').default; const shim = require('./shim').default;
const JoplinError = require('./JoplinError').default; const JoplinError = require('./JoplinError').default;
const { Buffer } = require('buffer'); const { Buffer } = require('buffer');
const { GetObjectCommand, ListObjectsV2Command, HeadObjectCommand, PutObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, CopyObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const S3_MAX_DELETES = 1000; const S3_MAX_DELETES = 1000;
@ -28,33 +26,31 @@ class FileApiDriverAmazonS3 {
} }
hasErrorCode_(error, errorCode) { hasErrorCode_(error, errorCode) {
if (!error || typeof error.name !== 'string') return false; if (!error || typeof error.code !== 'string') return false;
return error.name.indexOf(errorCode) >= 0; return error.code.indexOf(errorCode) >= 0;
} }
// Because of the way AWS-SDK-v3 works for getting data from a bucket we will
// use a pre-signed URL to avoid https://github.com/aws/aws-sdk-js-v3/issues/1877
async s3GenerateGetURL(key) {
const signedUrl = await getSignedUrl(this.api(), new GetObjectCommand({
Bucket: this.s3_bucket_,
Key: key,
}), {
expiresIn: 3600,
});
return signedUrl;
}
// We've now moved to aws-sdk-v3 and this note is outdated, but explains the promise structure.
// Need to make a custom promise, built-in promise is broken: https://github.com/aws/aws-sdk-js/issues/1436 // Need to make a custom promise, built-in promise is broken: https://github.com/aws/aws-sdk-js/issues/1436
// TODO: Re-factor to https://github.com/aws/aws-sdk-js-v3/tree/main/clients/client-s3#asyncawait async s3GetObject(key) {
return new Promise((resolve, reject) => {
this.api().getObject({
Bucket: this.s3_bucket_,
Key: key,
}, (err, response) => {
if (err) reject(err);
else resolve(response);
});
});
}
async s3ListObjects(key, cursor) { async s3ListObjects(key, cursor) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.api().send(new ListObjectsV2Command({ this.api().listObjectsV2({
Bucket: this.s3_bucket_, Bucket: this.s3_bucket_,
Prefix: key, Prefix: key,
Delimiter: '/', Delimiter: '/',
ContinuationToken: cursor, ContinuationToken: cursor,
}), (err, response) => { }, (err, response) => {
if (err) reject(err); if (err) reject(err);
else resolve(response); else resolve(response);
}); });
@ -63,10 +59,10 @@ class FileApiDriverAmazonS3 {
async s3HeadObject(key) { async s3HeadObject(key) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.api().send(new HeadObjectCommand({ this.api().headObject({
Bucket: this.s3_bucket_, Bucket: this.s3_bucket_,
Key: key, Key: key,
}), (err, response) => { }, (err, response) => {
if (err) reject(err); if (err) reject(err);
else resolve(response); else resolve(response);
}); });
@ -75,11 +71,11 @@ class FileApiDriverAmazonS3 {
async s3PutObject(key, body) { async s3PutObject(key, body) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.api().send(new PutObjectCommand({ this.api().putObject({
Bucket: this.s3_bucket_, Bucket: this.s3_bucket_,
Key: key, Key: key,
Body: body, Body: body,
}), (err, response) => { }, (err, response) => {
if (err) reject(err); if (err) reject(err);
else resolve(response); else resolve(response);
}); });
@ -91,12 +87,12 @@ class FileApiDriverAmazonS3 {
const body = await shim.fsDriver().readFile(path, 'base64'); const body = await shim.fsDriver().readFile(path, 'base64');
const fileStat = await shim.fsDriver().stat(path); const fileStat = await shim.fsDriver().stat(path);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.api().send(new PutObjectCommand({ this.api().putObject({
Bucket: this.s3_bucket_, Bucket: this.s3_bucket_,
Key: key, Key: key,
Body: Buffer.from(body, 'base64'), Body: Buffer.from(body, 'base64'),
ContentLength: `${fileStat.size}`, ContentLength: `${fileStat.size}`,
}), (err, response) => { }, (err, response) => {
if (err) reject(err); if (err) reject(err);
else resolve(response); else resolve(response);
}); });
@ -105,10 +101,10 @@ class FileApiDriverAmazonS3 {
async s3DeleteObject(key) { async s3DeleteObject(key) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.api().send(new DeleteObjectCommand({ this.api().deleteObject({
Bucket: this.s3_bucket_, Bucket: this.s3_bucket_,
Key: key, Key: key,
}), },
(err, response) => { (err, response) => {
if (err) { if (err) {
console.log(err.code); console.log(err.code);
@ -122,10 +118,10 @@ class FileApiDriverAmazonS3 {
// Assumes key is formatted, like `{Key: 's3 path'}` // Assumes key is formatted, like `{Key: 's3 path'}`
async s3DeleteObjects(keys) { async s3DeleteObjects(keys) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.api().send(new DeleteObjectsCommand({ this.api().deleteObjects({
Bucket: this.s3_bucket_, Bucket: this.s3_bucket_,
Delete: { Objects: keys }, Delete: { Objects: keys },
}), },
(err, response) => { (err, response) => {
if (err) { if (err) {
console.log(err.code); console.log(err.code);
@ -192,20 +188,8 @@ class FileApiDriverAmazonS3 {
prefixPath = `${prefixPath}/`; prefixPath = `${prefixPath}/`;
} }
// There is a bug/quirk of aws-sdk-js-v3 which causes the
// S3Client systemClockOffset to be wildly inaccurate. This
// effectively removes the offset and sets it to system time.
// See https://github.com/aws/aws-sdk-js-v3/issues/2208 for more.
// If the user's time actaully off, then this should correctly
// result in a RequestTimeTooSkewed error from s3ListObjects.
this.api().config.systemClockOffset = 0;
let response = await this.s3ListObjects(prefixPath); let response = await this.s3ListObjects(prefixPath);
// In aws-sdk-js-v3 if there are no contents it no longer returns
// an empty array. This creates an Empty array to pass onward.
if (response.Contents === undefined) response.Contents = [];
let output = this.metadataToStats_(response.Contents, prefixPath); let output = this.metadataToStats_(response.Contents, prefixPath);
while (response.IsTruncated) { while (response.IsTruncated) {
@ -228,17 +212,31 @@ class FileApiDriverAmazonS3 {
try { try {
let output = null; let output = null;
let response = null; const response = await this.s3GetObject(remotePath);
output = response.Body;
const s3Url = await this.s3GenerateGetURL(remotePath);
if (options.target === 'file') { if (options.target === 'file') {
output = await shim.fetchBlob(s3Url, options); const filePath = options.path;
if (!filePath) throw new Error('get: target options.path is missing');
// TODO: check if this ever hits on RN
await shim.fsDriver().writeBinaryFile(filePath, output);
return {
ok: true,
path: filePath,
text: () => {
return response.statusMessage;
},
json: () => {
return { message: `${response.statusCode}: ${response.statusMessage}` };
},
status: response.statusCode,
headers: response.headers,
};
} }
if (responseFormat === 'text') { if (responseFormat === 'text') {
response = await shim.fetch(s3Url, options); output = output.toString();
output = await response.text();
} }
return output; return output;
@ -312,11 +310,11 @@ class FileApiDriverAmazonS3 {
async move(oldPath, newPath) { async move(oldPath, newPath) {
const req = new Promise((resolve, reject) => { const req = new Promise((resolve, reject) => {
this.api().send(new CopyObjectCommand({ this.api().copyObject({
Bucket: this.s3_bucket_, Bucket: this.s3_bucket_,
CopySource: this.makePath_(oldPath), CopySource: this.makePath_(oldPath),
Key: newPath, Key: newPath,
}),(err, response) => { },(err, response) => {
if (err) reject(err); if (err) reject(err);
else resolve(response); else resolve(response);
}); });
@ -342,10 +340,10 @@ class FileApiDriverAmazonS3 {
async clearRoot() { async clearRoot() {
const listRecursive = async (cursor) => { const listRecursive = async (cursor) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
return this.api().send(new ListObjectsV2Command({ return this.api().listObjectsV2({
Bucket: this.s3_bucket_, Bucket: this.s3_bucket_,
ContinuationToken: cursor, ContinuationToken: cursor,
}), (err, response) => { }, (err, response) => {
if (err) reject(err); if (err) reject(err);
else resolve(response); else resolve(response);
}); });
@ -353,9 +351,6 @@ class FileApiDriverAmazonS3 {
}; };
let response = await listRecursive(); let response = await listRecursive();
// In aws-sdk-js-v3 if there are no contents it no longer returns
// an empty array. This creates an Empty array to pass onward.
if (response.Contents === undefined) response.Contents = [];
let keys = response.Contents.map((content) => content.Key); let keys = response.Contents.map((content) => content.Key);
while (response.IsTruncated) { while (response.IsTruncated) {

View File

@ -1,6 +1,7 @@
class FsDriverDummy { class FsDriverDummy {
constructor() {} constructor() {}
appendFileSync() {} appendFileSync() {}
writeBinaryFile() {}
readFile() {} readFile() {}
} }

View File

@ -26,6 +26,16 @@ export default class FsDriverNode extends FsDriverBase {
} }
} }
public async writeBinaryFile(path: string, content: any) {
try {
// let buffer = new Buffer(content);
const buffer = Buffer.from(content);
return await fs.writeFile(path, buffer);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
public async writeFile(path: string, string: string, encoding: string = 'base64') { public async writeFile(path: string, string: string, encoding: string = 'base64') {
try { try {
if (encoding === 'buffer') { if (encoding === 'buffer') {

View File

@ -245,6 +245,10 @@ export default class Resource extends BaseItem {
return this.fsDriver().readFile(this.fullPath(resource), 'Buffer'); return this.fsDriver().readFile(this.fullPath(resource), 'Buffer');
} }
static setContent(resource: ResourceEntity, content: any) {
return this.fsDriver().writeBinaryFile(this.fullPath(resource), content);
}
static isResourceUrl(url: string) { static isResourceUrl(url: string) {
return url && url.length === 34 && url[0] === ':' && url[1] === '/'; return url && url.length === 34 && url[0] === ':' && url[1] === '/';
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "@joplin/lib", "name": "@joplin/lib",
"version": "2.2.3", "version": "2.2.4",
"description": "Joplin Core library", "description": "Joplin Core library",
"author": "Laurent Cozic", "author": "Laurent Cozic",
"homepage": "", "homepage": "",
@ -25,14 +25,13 @@
"typescript": "^4.0.5" "typescript": "^4.0.5"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.22.0", "@joplin/fork-htmlparser2": "^4.1.32",
"@aws-sdk/s3-request-presigner": "^3.23.0", "@joplin/fork-sax": "^1.2.36",
"@joplin/fork-htmlparser2": "^4.1.31", "@joplin/renderer": "^2.2.4",
"@joplin/fork-sax": "^1.2.35", "@joplin/turndown": "^4.0.54",
"@joplin/renderer": "^2.2.3", "@joplin/turndown-plugin-gfm": "^1.0.36",
"@joplin/turndown": "^4.0.53",
"@joplin/turndown-plugin-gfm": "^1.0.35",
"async-mutex": "^0.1.3", "async-mutex": "^0.1.3",
"aws-sdk": "^2.588.0",
"base-64": "^0.1.0", "base-64": "^0.1.0",
"base64-stream": "^1.0.0", "base64-stream": "^1.0.0",
"builtin-modules": "^3.1.0", "builtin-modules": "^3.1.0",

View File

@ -58,7 +58,6 @@ export default class WebviewController extends ViewController {
scripts: [], scripts: [],
opened: false, opened: false,
buttons: null, buttons: null,
fitToContent: true,
}, },
}); });
} }
@ -174,11 +173,4 @@ export default class WebviewController extends ViewController {
this.setStoreProp('buttons', buttons); this.setStoreProp('buttons', buttons);
} }
public get fitToContent(): boolean {
return this.storeView.fitToContent;
}
public set fitToContent(fitToContent: boolean) {
this.setStoreProp('fitToContent', fitToContent);
}
} }

View File

@ -98,13 +98,4 @@ export default class JoplinViewsDialogs {
return this.controller(handle).open(); return this.controller(handle).open();
} }
/**
* Toggle on whether to fit the dialog size to the content or not.
* When set to false, the dialog stretches to fill the application
* window.
* @default true
*/
public async setFitToContent(handle: ViewHandle, status: boolean) {
return this.controller(handle).fitToContent = status;
}
} }

View File

@ -55,7 +55,7 @@ import SyncTargetJoplinCloud from '../SyncTargetJoplinCloud';
import KeychainService from '../services/keychain/KeychainService'; import KeychainService from '../services/keychain/KeychainService';
import { loadKeychainServiceAndSettings } from '../services/SettingUtils'; import { loadKeychainServiceAndSettings } from '../services/SettingUtils';
const md5 = require('md5'); const md5 = require('md5');
const { S3Client } = require('@aws-sdk/client-s3'); const S3 = require('aws-sdk/clients/s3');
const { Dirnames } = require('../services/synchronizer/utils/types'); const { Dirnames } = require('../services/synchronizer/utils/types');
// Each suite has its own separate data and temp directory so that multiple // Each suite has its own separate data and temp directory so that multiple
@ -569,16 +569,10 @@ async function initFileApi() {
const appDir = await api.appDirectory(); const appDir = await api.appDirectory();
fileApi = new FileApi(appDir, new FileApiDriverOneDrive(api)); fileApi = new FileApi(appDir, new FileApiDriverOneDrive(api));
} else if (syncTargetId_ == SyncTargetRegistry.nameToId('amazon_s3')) { } else if (syncTargetId_ == SyncTargetRegistry.nameToId('amazon_s3')) {
// We make sure for S3 tests run in band because tests
// share the same directory which will cause locking errors.
mustRunInBand();
const amazonS3CredsPath = `${oldTestDir}/support/amazon-s3-auth.json`; const amazonS3CredsPath = `${oldTestDir}/support/amazon-s3-auth.json`;
const amazonS3Creds = require(amazonS3CredsPath); const amazonS3Creds = require(amazonS3CredsPath);
if (!amazonS3Creds || !amazonS3Creds.accessKeyId) throw new Error(`AWS auth JSON missing in ${amazonS3CredsPath} format should be: { "accessKeyId": "", "secretAccessKey": "", "bucket": "mybucket"}`); if (!amazonS3Creds || !amazonS3Creds.accessKeyId) throw new Error(`AWS auth JSON missing in ${amazonS3CredsPath} format should be: { "accessKeyId": "", "secretAccessKey": "", "bucket": "mybucket"}`);
const api = new S3Client({ region: 'us-east-1', accessKeyId: amazonS3Creds.accessKeyId, secretAccessKey: amazonS3Creds.secretAccessKey, s3UseArnRegion: true }); const api = new S3({ accessKeyId: amazonS3Creds.accessKeyId, secretAccessKey: amazonS3Creds.secretAccessKey, s3UseArnRegion: true });
fileApi = new FileApi('', new FileApiDriverAmazonS3(api, amazonS3Creds.bucket)); fileApi = new FileApi('', new FileApiDriverAmazonS3(api, amazonS3Creds.bucket));
} else if (syncTargetId_ == SyncTargetRegistry.nameToId('joplinServer')) { } else if (syncTargetId_ == SyncTargetRegistry.nameToId('joplinServer')) {
mustRunInBand(); mustRunInBand();

View File

@ -1,6 +1,6 @@
{ {
"name": "@joplin/plugin-repo-cli", "name": "@joplin/plugin-repo-cli",
"version": "2.2.3", "version": "2.2.4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@joplin/plugin-repo-cli", "name": "@joplin/plugin-repo-cli",
"version": "2.2.3", "version": "2.2.4",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"bin": { "bin": {
@ -18,8 +18,8 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@joplin/lib": "^2.2.3", "@joplin/lib": "^2.2.4",
"@joplin/tools": "^2.2.3", "@joplin/tools": "^2.2.4",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"gh-release-assets": "^2.0.0", "gh-release-assets": "^2.0.0",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",

View File

@ -1,6 +1,6 @@
{ {
"name": "@joplin/renderer", "name": "@joplin/renderer",
"version": "2.2.3", "version": "2.2.4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@joplin/renderer", "name": "@joplin/renderer",
"version": "2.2.3", "version": "2.2.4",
"description": "The Joplin note renderer, used the mobile and desktop application", "description": "The Joplin note renderer, used the mobile and desktop application",
"repository": "https://github.com/laurent22/joplin/tree/dev/packages/renderer", "repository": "https://github.com/laurent22/joplin/tree/dev/packages/renderer",
"main": "index.js", "main": "index.js",
@ -24,7 +24,7 @@
"typescript": "^4.0.5" "typescript": "^4.0.5"
}, },
"dependencies": { "dependencies": {
"@joplin/fork-htmlparser2": "^4.1.31", "@joplin/fork-htmlparser2": "^4.1.32",
"font-awesome-filetypes": "^2.1.0", "font-awesome-filetypes": "^2.1.0",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"highlight.js": "^10.2.1", "highlight.js": "^10.2.1",

View File

@ -1,6 +1,6 @@
{ {
"name": "@joplin/tools", "name": "@joplin/tools",
"version": "2.2.3", "version": "2.2.4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@joplin/tools", "name": "@joplin/tools",
"version": "2.2.3", "version": "2.2.4",
"description": "Various tools for Joplin", "description": "Various tools for Joplin",
"main": "index.js", "main": "index.js",
"author": "Laurent Cozic", "author": "Laurent Cozic",
@ -18,7 +18,7 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@joplin/lib": "^2.2.3", "@joplin/lib": "^2.2.4",
"execa": "^4.1.0", "execa": "^4.1.0",
"fs-extra": "^4.0.3", "fs-extra": "^4.0.3",
"gettext-parser": "^1.3.0", "gettext-parser": "^1.3.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "@joplin/turndown-plugin-gfm", "name": "@joplin/turndown-plugin-gfm",
"version": "1.0.35", "version": "1.0.36",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -4,7 +4,7 @@
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
"version": "1.0.35", "version": "1.0.36",
"author": "Dom Christie", "author": "Dom Christie",
"main": "lib/turndown-plugin-gfm.cjs.js", "main": "lib/turndown-plugin-gfm.cjs.js",
"devDependencies": { "devDependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@joplin/turndown", "name": "@joplin/turndown",
"version": "4.0.53", "version": "4.0.54",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,7 +1,7 @@
{ {
"name": "@joplin/turndown", "name": "@joplin/turndown",
"description": "A library that converts HTML to Markdown", "description": "A library that converts HTML to Markdown",
"version": "4.0.53", "version": "4.0.54",
"author": "Dom Christie", "author": "Dom Christie",
"main": "lib/turndown.cjs.js", "main": "lib/turndown.cjs.js",
"publishConfig": { "publishConfig": {

View File

@ -1,5 +1,11 @@
# Joplin Android app changelog # Joplin Android app changelog
## [android-v2.2.5](https://github.com/laurent22/joplin/releases/tag/android-v2.2.5) (Pre-release) - 2021-08-11T10:54:38Z
- Revert "Plugins: Add ability to make dialogs fit the application window (#5219)" as it breaks several plugin webviews.
- Revert "Resolves #4810, Resolves #4610: Fix AWS S3 sync error and upgrade framework to v3 (#5212)" due to incompatibility with some AWS providers.
- Improved: Upgraded React Native to v0.64 (afb7e1a)
## [android-v2.2.3](https://github.com/laurent22/joplin/releases/tag/android-v2.2.3) (Pre-release) - 2021-08-09T18:48:29Z ## [android-v2.2.3](https://github.com/laurent22/joplin/releases/tag/android-v2.2.3) (Pre-release) - 2021-08-09T18:48:29Z
- Improved: Ensure that timestamps are not changed when sharing or unsharing a note (cafaa9c) - Improved: Ensure that timestamps are not changed when sharing or unsharing a note (cafaa9c)

View File

@ -1,5 +1,9 @@
# Joplin terminal app changelog # Joplin terminal app changelog
## [cli-v2.2.2](https://github.com/laurent22/joplin/releases/tag/cli-v2.2.2) - 2021-08-11T15:34:56Z
- Fixed: Fixed version command so that it does not require the keychain (15766d1)
## [cli-v2.2.1](https://github.com/laurent22/joplin/releases/tag/cli-v2.2.1) - 2021-08-10T10:21:09Z ## [cli-v2.2.1](https://github.com/laurent22/joplin/releases/tag/cli-v2.2.1) - 2021-08-10T10:21:09Z
- Improved: Ensure that timestamps are not changed when sharing or unsharing a note (cafaa9c) - Improved: Ensure that timestamps are not changed when sharing or unsharing a note (cafaa9c)