mirror of
https://github.com/standardnotes/app
synced 2026-09-15 06:45:59 -04:00
Compare commits
75
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4971e3c56e | ||
|
|
d54e6965c1 | ||
|
|
ca144be66a | ||
|
|
a90e4a50e8 | ||
|
|
65818ac1cd | ||
|
|
52962ed65d | ||
|
|
be2c4720fc | ||
|
|
47106fdf83 | ||
|
|
88e29fe1b6 | ||
|
|
b15d80eb29 | ||
|
|
86a0b61612 | ||
|
|
5f24e7285e | ||
|
|
1c530ab534 | ||
|
|
08b70968f2 | ||
|
|
0fa9f6d7b6 | ||
|
|
3c699eaa4a | ||
|
|
fc86cebc75 | ||
|
|
e0ec53f85a | ||
|
|
492c9bacbf | ||
|
|
88f35b39fb | ||
|
|
f28ed36136 | ||
|
|
f02465346f | ||
|
|
11dd39c126 | ||
|
|
d7a90c4d91 | ||
|
|
00075616e6 | ||
|
|
5ffffbff20 | ||
|
|
ecd8d56171 | ||
|
|
33226000f5 | ||
|
|
c841ac99d2 | ||
|
|
b0e708186c | ||
|
|
20e420820d | ||
|
|
c3d6a91730 | ||
|
|
0ed692ff9b | ||
|
|
c74fa272fb | ||
|
|
3f9d3ceffa | ||
|
|
f6d0aa0f0a | ||
|
|
eafa65e934 | ||
|
|
a83003ee69 | ||
|
|
554f59a3fe | ||
|
|
c0af596f2d | ||
|
|
6e10911d02 | ||
|
|
9502a7fa32 | ||
|
|
727d78cfe2 | ||
|
|
44bb4b7ca3 | ||
|
|
981d8a7497 | ||
|
|
04245dfeeb | ||
|
|
5db47e83e8 | ||
|
|
c08602b7b8 | ||
|
|
ec52e5684d | ||
|
|
f3f7f9d362 | ||
|
|
b9a07d8b5a | ||
|
|
31213bf268 | ||
|
|
70e57e32b3 | ||
|
|
720215eea7 | ||
|
|
6d5ebdeaa1 | ||
|
|
c99693876f | ||
|
|
f8f6a5212b | ||
|
|
58cd919267 | ||
|
|
507823dc4f | ||
|
|
6ea1def0cf | ||
|
|
fe2ce9f1e8 | ||
|
|
4985787c5d | ||
|
|
8614a638a4 | ||
|
|
15957c71cb | ||
|
|
59e814bcec | ||
|
|
4cf9ebf52a | ||
|
|
a3611c802f | ||
|
|
9be315730d | ||
|
|
789a78bda3 | ||
|
|
73e170dedd | ||
|
|
e4b1b64119 | ||
|
|
c2a1918843 | ||
|
|
d3621d70b1 | ||
|
|
bf286f4854 | ||
|
|
97835e64ac |
Binary file not shown.
BIN
Binary file not shown.
@@ -3,6 +3,32 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.9.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-03)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** add http status code for gone resource ([d54e696](https://github.com/standardnotes/app/commit/d54e6965c188e9f12371b32868e8de1c2f627f30))
|
||||
|
||||
## [1.8.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.8.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.8.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.8.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.8.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.8.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api",
|
||||
"version": "1.8.2",
|
||||
"version": "1.9.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -24,7 +24,12 @@ describe('UserApiService', () => {
|
||||
})
|
||||
|
||||
it('should register a user', async () => {
|
||||
const response = await createService().register('[email protected]', 'testpasswd', keyParams, false)
|
||||
const response = await createService().register({
|
||||
email: '[email protected]',
|
||||
serverPassword: 'testpasswd',
|
||||
keyParams,
|
||||
ephemeral: false,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
@@ -52,7 +57,7 @@ describe('UserApiService', () => {
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.register('[email protected]', 'testpasswd', keyParams, false)
|
||||
await service.register({ email: '[email protected]', serverPassword: 'testpasswd', keyParams, ephemeral: false })
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
@@ -67,7 +72,12 @@ describe('UserApiService', () => {
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().register('[email protected]', 'testpasswd', keyParams, false)
|
||||
await createService().register({
|
||||
email: '[email protected]',
|
||||
serverPassword: 'testpasswd',
|
||||
keyParams,
|
||||
ephemeral: false,
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
@@ -15,12 +15,12 @@ export class UserApiService implements UserApiServiceInterface {
|
||||
this.registering = false
|
||||
}
|
||||
|
||||
async register(
|
||||
email: string,
|
||||
serverPassword: string,
|
||||
keyParams: RootKeyParamsInterface,
|
||||
ephemeral: boolean,
|
||||
): Promise<UserRegistrationResponse> {
|
||||
async register(registerDTO: {
|
||||
email: string
|
||||
serverPassword: string
|
||||
keyParams: RootKeyParamsInterface
|
||||
ephemeral: boolean
|
||||
}): Promise<UserRegistrationResponse> {
|
||||
if (this.registering) {
|
||||
throw new ApiCallError(ErrorMessage.RegistrationInProgress)
|
||||
}
|
||||
@@ -29,10 +29,10 @@ export class UserApiService implements UserApiServiceInterface {
|
||||
try {
|
||||
const response = await this.userServer.register({
|
||||
[ApiEndpointParam.ApiVersion]: ApiVersion.v0,
|
||||
password: serverPassword,
|
||||
email,
|
||||
ephemeral,
|
||||
...keyParams.getPortableValue(),
|
||||
password: registerDTO.serverPassword,
|
||||
email: registerDTO.email,
|
||||
ephemeral: registerDTO.ephemeral,
|
||||
...registerDTO.keyParams.getPortableValue(),
|
||||
})
|
||||
|
||||
this.registering = false
|
||||
|
||||
@@ -2,10 +2,10 @@ import { RootKeyParamsInterface } from '@standardnotes/models'
|
||||
import { UserRegistrationResponse } from '../../Response/User/UserRegistrationResponse'
|
||||
|
||||
export interface UserApiServiceInterface {
|
||||
register(
|
||||
email: string,
|
||||
serverPassword: string,
|
||||
keyParams: RootKeyParamsInterface,
|
||||
ephemeral: boolean,
|
||||
): Promise<UserRegistrationResponse>
|
||||
register(registerDTO: {
|
||||
email: string
|
||||
serverPassword: string
|
||||
keyParams: RootKeyParamsInterface
|
||||
ephemeral: boolean
|
||||
}): Promise<UserRegistrationResponse>
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@ export enum HttpStatusCode {
|
||||
BadRequest = 400,
|
||||
Unauthorized = 401,
|
||||
Forbidden = 403,
|
||||
Gone = 410,
|
||||
ExpiredAccessToken = 498,
|
||||
}
|
||||
|
||||
@@ -8,4 +8,6 @@ export type UserRegistrationRequestParams = AnyKeyParamsContent & {
|
||||
email: string
|
||||
ephemeral: boolean
|
||||
[additionalParam: string]: unknown
|
||||
pkcPublicKey?: string
|
||||
pkcEncryptedPrivateKey?: string
|
||||
}
|
||||
|
||||
@@ -3,6 +3,134 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.23.184](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.183](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.182](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.181](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.180](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.179](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.178](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.177](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.176](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.175](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.174](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.173](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.172](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.171](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.170](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.169](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.168](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.167](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.166](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.165](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.164](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.163](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.162](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.161](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.160](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.159](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.158](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.157](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.156](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.155](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.154](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.153](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.152](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/desktop",
|
||||
"main": "./app/dist/index.js",
|
||||
"version": "3.23.152",
|
||||
"version": "3.23.184",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.15.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.15.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.15.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.15.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.15.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.15.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/encryption",
|
||||
"version": "1.15.4",
|
||||
"version": "1.15.9",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.23.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.23.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.23.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.23.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.23.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.23.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/filepicker",
|
||||
"version": "1.23.4",
|
||||
"version": "1.23.9",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.10.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.10.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.10.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.10.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.10.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.10.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files",
|
||||
"version": "1.10.4",
|
||||
"version": "1.10.9",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,156 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.39.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
# [3.39.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-02)
|
||||
|
||||
### Features
|
||||
|
||||
* use native preview for pdf in mobile webview ([#1728](https://github.com/standardnotes/app/issues/1728)) ([a90e4a5](https://github.com/standardnotes/app/commit/a90e4a50e820aa13b6af1f76ee9b2f935d12f211))
|
||||
|
||||
## [3.38.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
# [3.38.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
### Features
|
||||
|
||||
* sync android status bar bg with theme bg color ([#1727](https://github.com/standardnotes/app/issues/1727)) ([47106fd](https://github.com/standardnotes/app/commit/47106fdf83ec89e67444e84a94350870645993cf))
|
||||
|
||||
## [3.37.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.37.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* downloading backups in mobile webview ([#1703](https://github.com/standardnotes/app/issues/1703)) ([5f24e72](https://github.com/standardnotes/app/commit/5f24e7285e941b44e05cec5be32bee4589f4963c))
|
||||
|
||||
## [3.37.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.37.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.37.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.37.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.37.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.37.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.37.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.37.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.37.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.37.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.37.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.37.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
# [3.37.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
### Features
|
||||
|
||||
* handle android back button on android ([#1656](https://github.com/standardnotes/app/issues/1656)) ([981d8a7](https://github.com/standardnotes/app/commit/981d8a7497ea2600e1535ad9170cb5559550ca4d))
|
||||
|
||||
## [3.36.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.36.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.36.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.36.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.36.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* export/sharing notes on mobile webview ([#1644](https://github.com/standardnotes/app/issues/1644)) ([6d5ebde](https://github.com/standardnotes/app/commit/6d5ebdeaa1777f31878b6e7d4dfc33fe83abc9fc))
|
||||
|
||||
## [3.36.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.36.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.36.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.36.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **mobile:** hide drawer on new mobile experience ([#1642](https://github.com/standardnotes/app/issues/1642)) ([8614a63](https://github.com/standardnotes/app/commit/8614a638a4bfc8fdf97020bf1fc147c62998cf3b))
|
||||
|
||||
## [3.36.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.36.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.36.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **mobile:** accurate version number ([789a78b](https://github.com/standardnotes/app/commit/789a78bda33f12d214c12a63ae27d2ba0f39feab))
|
||||
|
||||
## [3.36.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.36.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.36.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.36.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/mobile",
|
||||
"version": "3.36.3",
|
||||
"version": "3.39.1",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
@@ -69,7 +69,7 @@
|
||||
"react-native-screens": "3.13.1",
|
||||
"react-native-search-bar": "standardnotes/react-native-search-bar#7d2139daf9b7663b570403f21f520deceba9bb09",
|
||||
"react-native-search-box": "standardnotes/react-native-search-box#8c46369912cba78dca718588aca9c16926953ff7",
|
||||
"react-native-share": "^7.3.7",
|
||||
"react-native-share": "^7.9.0",
|
||||
"react-native-sodium-jsi": "1.2.0",
|
||||
"react-native-static-server": "standardnotes/react-native-static-server#28ef0175dbee3db9aadfab57498497067556a836",
|
||||
"react-native-store-review": "^0.2.0",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { AbstractService, InternalEventBus, ReactNativeToWebEvent } from '@standardnotes/snjs'
|
||||
import { BackHandler, NativeEventSubscription } from 'react-native'
|
||||
|
||||
export class AndroidBackHandlerService extends AbstractService<ReactNativeToWebEvent> {
|
||||
private removeListener: NativeEventSubscription
|
||||
|
||||
constructor() {
|
||||
const internalEventBus = new InternalEventBus()
|
||||
super(internalEventBus)
|
||||
|
||||
this.removeListener = BackHandler.addEventListener('hardwareBackPress', () => {
|
||||
void this.notifyEvent(ReactNativeToWebEvent.AndroidBackButtonPressed)
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.removeListener.remove()
|
||||
}
|
||||
}
|
||||
@@ -121,7 +121,9 @@ export const AppStackComponent = (props: ModalStackNavigationProp<'AppStack'>) =
|
||||
[application],
|
||||
)
|
||||
|
||||
if (IsMobileWeb) {
|
||||
const shouldOpenWebApp = application?.getValue(AlwaysOpenWebAppOnLaunchKey, StorageValueModes.Nonwrapped) as boolean
|
||||
|
||||
if (IsMobileWeb || shouldOpenWebApp) {
|
||||
return (
|
||||
<AppStack.Navigator
|
||||
screenOptions={() => ({
|
||||
@@ -134,12 +136,6 @@ export const AppStackComponent = (props: ModalStackNavigationProp<'AppStack'>) =
|
||||
)
|
||||
}
|
||||
|
||||
if (!application) {
|
||||
return null
|
||||
}
|
||||
|
||||
const shouldOpenWebApp = application.getValue(AlwaysOpenWebAppOnLaunchKey, StorageValueModes.Nonwrapped) as boolean
|
||||
|
||||
return (
|
||||
<DrawerLayout
|
||||
ref={drawerRef}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import AsyncStorage from '@react-native-community/async-storage'
|
||||
import { AndroidBackHandlerService } from '@Root/AndroidBackHandlerService'
|
||||
import SNReactNative from '@standardnotes/react-native-utils'
|
||||
import {
|
||||
ApplicationIdentifier,
|
||||
@@ -7,14 +8,25 @@ import {
|
||||
LegacyRawKeychainValue,
|
||||
MobileDeviceInterface,
|
||||
NamespacedRootKeyInKeychain,
|
||||
Platform as SNPlatform,
|
||||
RawKeychainValue,
|
||||
removeFromArray,
|
||||
TransferPayload,
|
||||
} from '@standardnotes/snjs'
|
||||
import { Alert, Linking, Platform, StatusBar } from 'react-native'
|
||||
import { Alert, Linking, PermissionsAndroid, Platform, StatusBar } from 'react-native'
|
||||
import FileViewer from 'react-native-file-viewer'
|
||||
import FingerprintScanner from 'react-native-fingerprint-scanner'
|
||||
import FlagSecure from 'react-native-flag-secure-android'
|
||||
import {
|
||||
CachesDirectoryPath,
|
||||
DocumentDirectoryPath,
|
||||
DownloadDirectoryPath,
|
||||
exists,
|
||||
unlink,
|
||||
writeFile,
|
||||
} from 'react-native-fs'
|
||||
import { hide, show } from 'react-native-privacy-snapshot'
|
||||
import Share from 'react-native-share'
|
||||
import { AppStateObserverService } from './../AppStateObserverService'
|
||||
import Keychain from './Keychain'
|
||||
import { IsMobileWeb } from './Utils'
|
||||
@@ -64,14 +76,21 @@ const showLoadFailForItemIds = (failedItemIds: string[]) => {
|
||||
|
||||
export class MobileDevice implements MobileDeviceInterface {
|
||||
environment: Environment.Mobile = Environment.Mobile
|
||||
platform: SNPlatform.Ios | SNPlatform.Android = Platform.OS === 'ios' ? SNPlatform.Ios : SNPlatform.Android
|
||||
private eventObservers: MobileDeviceEventHandler[] = []
|
||||
public isDarkMode = false
|
||||
public statusBarBgColor: string | undefined
|
||||
|
||||
constructor(private stateObserverService?: AppStateObserverService) {}
|
||||
constructor(
|
||||
private stateObserverService?: AppStateObserverService,
|
||||
private androidBackHandlerService?: AndroidBackHandlerService,
|
||||
) {}
|
||||
|
||||
deinit() {
|
||||
this.stateObserverService?.deinit()
|
||||
;(this.stateObserverService as unknown) = undefined
|
||||
this.androidBackHandlerService?.deinit()
|
||||
;(this.androidBackHandlerService as unknown) = undefined
|
||||
}
|
||||
|
||||
consoleLog(...args: any[]): void {
|
||||
@@ -433,13 +452,17 @@ export class MobileDevice implements MobileDeviceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
handleThemeSchemeChange(isDark: boolean): void {
|
||||
handleThemeSchemeChange(isDark: boolean, bgColor: string): void {
|
||||
this.isDarkMode = isDark
|
||||
this.statusBarBgColor = bgColor
|
||||
|
||||
this.reloadStatusBarStyle()
|
||||
}
|
||||
|
||||
reloadStatusBarStyle(animated = true) {
|
||||
if (this.statusBarBgColor) {
|
||||
StatusBar.setBackgroundColor(this.statusBarBgColor, animated)
|
||||
}
|
||||
StatusBar.setBarStyle(this.isDarkMode ? 'light-content' : 'dark-content', animated)
|
||||
}
|
||||
|
||||
@@ -455,4 +478,122 @@ export class MobileDevice implements MobileDeviceInterface {
|
||||
isDeviceDestroyed() {
|
||||
return false
|
||||
}
|
||||
|
||||
async deleteFileAtPathIfExists(path: string) {
|
||||
if (await exists(path)) {
|
||||
await unlink(path)
|
||||
}
|
||||
}
|
||||
|
||||
async shareBase64AsFile(base64: string, filename: string) {
|
||||
let downloadedTempFilePath: string | undefined
|
||||
try {
|
||||
downloadedTempFilePath = await this.downloadBase64AsFile(base64, filename, true)
|
||||
if (!downloadedTempFilePath) {
|
||||
return
|
||||
}
|
||||
await Share.open({
|
||||
url: `file://${downloadedTempFilePath}`,
|
||||
failOnCancel: false,
|
||||
})
|
||||
} catch (error) {
|
||||
this.consoleLog(`${error}`)
|
||||
} finally {
|
||||
if (downloadedTempFilePath) {
|
||||
void this.deleteFileAtPathIfExists(downloadedTempFilePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getFileDestinationPath(filename: string, saveInTempLocation: boolean): string {
|
||||
let directory = DocumentDirectoryPath
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
directory = saveInTempLocation ? CachesDirectoryPath : DownloadDirectoryPath
|
||||
}
|
||||
|
||||
return `${directory}/${filename}`
|
||||
}
|
||||
|
||||
async hasStoragePermissionOnAndroid(): Promise<boolean> {
|
||||
if (Platform.OS !== 'android') {
|
||||
return true
|
||||
}
|
||||
const grantedStatus = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE)
|
||||
if (grantedStatus === PermissionsAndroid.RESULTS.GRANTED) {
|
||||
return true
|
||||
}
|
||||
Alert.alert(
|
||||
'Storage permissions are required in order to download files. Please accept the permissions prompt and try again.',
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
async downloadBase64AsFile(
|
||||
base64: string,
|
||||
filename: string,
|
||||
saveInTempLocation = false,
|
||||
): Promise<string | undefined> {
|
||||
const isGrantedStoragePermissionOnAndroid = await this.hasStoragePermissionOnAndroid()
|
||||
|
||||
if (!isGrantedStoragePermissionOnAndroid) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const path = this.getFileDestinationPath(filename, saveInTempLocation)
|
||||
await this.deleteFileAtPathIfExists(path)
|
||||
await writeFile(path, base64.replace(/data.*base64,/, ''), 'base64')
|
||||
return path
|
||||
} catch (error) {
|
||||
this.consoleLog(`${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async previewFile(base64: string, filename: string): Promise<boolean> {
|
||||
const tempLocation = await this.downloadBase64AsFile(base64, filename, true)
|
||||
|
||||
if (!tempLocation) {
|
||||
this.consoleLog('Error: Could not download file to preview')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
await FileViewer.open(tempLocation, {
|
||||
onDismiss: async () => {
|
||||
await this.deleteFileAtPathIfExists(tempLocation)
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
this.consoleLog(error)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
confirmAndExit() {
|
||||
Alert.alert(
|
||||
'Close app',
|
||||
'Do you want to close the app?',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onPress: async () => {},
|
||||
},
|
||||
{
|
||||
text: 'Close',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
SNReactNative.exitApp()
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
cancelable: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ import { IsDev } from '@Lib/Utils'
|
||||
import { ReactNativeToWebEvent } from '@standardnotes/snjs'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Keyboard, Platform } from 'react-native'
|
||||
import VersionInfo from 'react-native-version-info'
|
||||
import { WebView, WebViewMessageEvent } from 'react-native-webview'
|
||||
import pjson from '../package.json'
|
||||
import { AndroidBackHandlerService } from './AndroidBackHandlerService'
|
||||
import { AppStateObserverService } from './AppStateObserverService'
|
||||
|
||||
const LoggingEnabled = IsDev
|
||||
@@ -22,13 +25,23 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
||||
const webViewRef = useRef<WebView>(null)
|
||||
const sourceUri = (Platform.OS === 'android' ? 'file:///android_asset/' : '') + 'Web.bundle/src/index.html'
|
||||
const stateService = useMemo(() => new AppStateObserverService(), [])
|
||||
const device = useMemo(() => new MobileDevice(stateService), [stateService])
|
||||
const androidBackHandlerService = useMemo(() => new AndroidBackHandlerService(), [])
|
||||
const device = useMemo(
|
||||
() => new MobileDevice(stateService, androidBackHandlerService),
|
||||
[androidBackHandlerService, stateService],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const removeListener = stateService.addEventObserver((event: ReactNativeToWebEvent) => {
|
||||
const removeStateServiceListener = stateService.addEventObserver((event: ReactNativeToWebEvent) => {
|
||||
webViewRef.current?.postMessage(JSON.stringify({ reactNativeEvent: event, messageType: 'event' }))
|
||||
})
|
||||
|
||||
const removeBackHandlerServiceListener = androidBackHandlerService.addEventObserver(
|
||||
(event: ReactNativeToWebEvent) => {
|
||||
webViewRef.current?.postMessage(JSON.stringify({ reactNativeEvent: event, messageType: 'event' }))
|
||||
},
|
||||
)
|
||||
|
||||
const keyboardShowListener = Keyboard.addListener('keyboardWillShow', () => {
|
||||
device.reloadStatusBarStyle(false)
|
||||
})
|
||||
@@ -38,11 +51,12 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
||||
})
|
||||
|
||||
return () => {
|
||||
removeListener()
|
||||
removeStateServiceListener()
|
||||
removeBackHandlerServiceListener()
|
||||
keyboardShowListener.remove()
|
||||
keyboardHideListener.remove()
|
||||
}
|
||||
}, [webViewRef, stateService])
|
||||
}, [webViewRef, stateService, device, androidBackHandlerService])
|
||||
|
||||
useEffect(() => {
|
||||
const observer = device.addMobileWebEventReceiver((event) => {
|
||||
@@ -84,8 +98,9 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
||||
const WebProcessDeviceInterface = `
|
||||
class WebProcessDeviceInterface {
|
||||
constructor(messageSender) {
|
||||
this.appVersion = '1.2.3'
|
||||
this.appVersion = '${pjson.version} (${VersionInfo.buildVersion})'
|
||||
this.environment = 4
|
||||
this.platform = ${device.platform}
|
||||
this.databases = []
|
||||
this.messageSender = messageSender
|
||||
}
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.22.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
### Features
|
||||
|
||||
* add pref to disable note status updates ([#1702](https://github.com/standardnotes/app/issues/1702)) ([08b7096](https://github.com/standardnotes/app/commit/08b70968f255d5a72562610f08fe727ebcffa8bb))
|
||||
|
||||
# [1.21.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
### Features
|
||||
|
||||
* add line height & font size settings for plaintext editor ([#1700](https://github.com/standardnotes/app/issues/1700)) ([3c699ea](https://github.com/standardnotes/app/commit/3c699eaa4a92a47774d4762eeddd45e34925e53f))
|
||||
|
||||
# [1.20.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
### Features
|
||||
|
||||
* add custom note title format pref ([#1678](https://github.com/standardnotes/app/issues/1678)) ([11dd39c](https://github.com/standardnotes/app/commit/11dd39c126019c4295c03fb59b05ea5aa3adcd27))
|
||||
|
||||
# [1.19.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
### Features
|
||||
|
||||
* new note title format w/ prefs ([#1629](https://github.com/standardnotes/app/issues/1629)) ([d3621d7](https://github.com/standardnotes/app/commit/d3621d70b1a34537417b06830275467055b753cb))
|
||||
|
||||
## [1.18.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/models",
|
||||
"version": "1.18.4",
|
||||
"version": "1.22.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -9,6 +9,8 @@ export enum PrefKey {
|
||||
EditorMonospaceEnabled = 'monospaceFont',
|
||||
EditorSpellcheck = 'spellcheck',
|
||||
EditorResizersEnabled = 'marginResizersEnabled',
|
||||
EditorLineHeight = 'editorLineHeight',
|
||||
EditorFontSize = 'editorFontSize',
|
||||
SortNotesBy = 'sortBy',
|
||||
SortNotesReverse = 'sortReverse',
|
||||
NotesShowArchived = 'showArchived',
|
||||
@@ -32,6 +34,33 @@ export enum PrefKey {
|
||||
MobileDoNotShowAgainUnsupportedEditors = 'mobileDoNotShowAgainUnsupportedEditors',
|
||||
MobileSelectedTagUuid = 'mobileSelectedTagUuid',
|
||||
MobileNotesHideEditorIcon = 'mobileHideEditorIcon',
|
||||
NewNoteTitleFormat = 'newNoteTitleFormat',
|
||||
CustomNoteTitleFormat = 'customNoteTitleFormat',
|
||||
UpdateSavingStatusIndicator = 'updateSavingStatusIndicator',
|
||||
}
|
||||
|
||||
export enum NewNoteTitleFormat {
|
||||
CurrentDateAndTime = 'CurrentDateAndTime',
|
||||
CurrentNoteCount = 'CurrentNoteCount',
|
||||
CustomFormat = 'CustomFormat',
|
||||
Empty = 'Empty',
|
||||
}
|
||||
|
||||
export enum EditorLineHeight {
|
||||
None = 'None',
|
||||
Tight = 'Tight',
|
||||
Snug = 'Snug',
|
||||
Normal = 'Normal',
|
||||
Relaxed = 'Relaxed',
|
||||
Loose = 'Loose',
|
||||
}
|
||||
|
||||
export enum EditorFontSize {
|
||||
ExtraSmall = 'ExtraSmall',
|
||||
Small = 'Small',
|
||||
Normal = 'Normal',
|
||||
Medium = 'Medium',
|
||||
Large = 'Large',
|
||||
}
|
||||
|
||||
export type PrefValue = {
|
||||
@@ -65,4 +94,9 @@ export type PrefValue = {
|
||||
[PrefKey.MobileDoNotShowAgainUnsupportedEditors]: boolean
|
||||
[PrefKey.MobileSelectedTagUuid]: string | undefined
|
||||
[PrefKey.MobileNotesHideEditorIcon]: boolean
|
||||
[PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat
|
||||
[PrefKey.CustomNoteTitleFormat]: string
|
||||
[PrefKey.EditorLineHeight]: EditorLineHeight
|
||||
[PrefKey.EditorFontSize]: EditorFontSize
|
||||
[PrefKey.UpdateSavingStatusIndicator]: boolean
|
||||
}
|
||||
|
||||
@@ -3,6 +3,142 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.3.109](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.108](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.107](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.106](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.105](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.104](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.103](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.102](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.101](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.100](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.99](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.98](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.97](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.96](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.95](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.94](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.93](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.92](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.91](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.90](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.89](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.88](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.87](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.86](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.85](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.84](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.83](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.82](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.81](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.80](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.79](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.78](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.77](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.76](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.75](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/releases",
|
||||
"version": "1.3.75",
|
||||
"version": "1.3.109",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/releases.json",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -3,6 +3,54 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.24.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
# [1.24.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-02)
|
||||
|
||||
### Features
|
||||
|
||||
* use native preview for pdf in mobile webview ([#1728](https://github.com/standardnotes/app/issues/1728)) ([a90e4a5](https://github.com/standardnotes/app/commit/a90e4a50e820aa13b6af1f76ee9b2f935d12f211))
|
||||
|
||||
# [1.23.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
### Features
|
||||
|
||||
* sync android status bar bg with theme bg color ([#1727](https://github.com/standardnotes/app/issues/1727)) ([47106fd](https://github.com/standardnotes/app/commit/47106fdf83ec89e67444e84a94350870645993cf))
|
||||
|
||||
## [1.22.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.22.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.22.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.22.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
# [1.22.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
### Features
|
||||
|
||||
* handle android back button on android ([#1656](https://github.com/standardnotes/app/issues/1656)) ([981d8a7](https://github.com/standardnotes/app/commit/981d8a7497ea2600e1535ad9170cb5559550ca4d))
|
||||
|
||||
## [1.21.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* export/sharing notes on mobile webview ([#1644](https://github.com/standardnotes/app/issues/1644)) ([6d5ebde](https://github.com/standardnotes/app/commit/6d5ebdeaa1777f31878b6e7d4dfc33fe83abc9fc))
|
||||
|
||||
## [1.21.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.21.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/services",
|
||||
"version": "1.21.3",
|
||||
"version": "1.24.1",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -11,5 +11,7 @@ export interface WebApplicationInterface extends ApplicationInterface {
|
||||
handleMobileLosingFocusEvent(): Promise<void>
|
||||
handleMobileResumingFromBackgroundEvent(): Promise<void>
|
||||
isNativeMobileWeb(): boolean
|
||||
get mobileDevice(): MobileDeviceInterface
|
||||
mobileDevice(): MobileDeviceInterface
|
||||
handleAndroidBackButtonPressed(): void
|
||||
addAndroidBackHandlerEventListener(listener: () => boolean): (() => void) | undefined
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { DeviceInterface } from './DeviceInterface'
|
||||
import { Environment, RawKeychainValue } from '@standardnotes/models'
|
||||
import { Environment, Platform, RawKeychainValue } from '@standardnotes/models'
|
||||
|
||||
export interface MobileDeviceInterface extends DeviceInterface {
|
||||
environment: Environment.Mobile
|
||||
platform: Platform.Ios | Platform.Android
|
||||
|
||||
getRawKeychainValue(): Promise<RawKeychainValue | undefined>
|
||||
getDeviceBiometricsAvailability(): Promise<boolean>
|
||||
@@ -11,5 +12,9 @@ export interface MobileDeviceInterface extends DeviceInterface {
|
||||
hideMobileInterfaceFromScreenshots(): void
|
||||
stopHidingMobileInterfaceFromScreenshots(): void
|
||||
consoleLog(...args: any[]): void
|
||||
handleThemeSchemeChange(isDark: boolean): void
|
||||
handleThemeSchemeChange(isDark: boolean, bgColor: string): void
|
||||
shareBase64AsFile(base64: string, filename: string): Promise<void>
|
||||
downloadBase64AsFile(base64: string, filename: string, saveInTempLocation?: boolean): Promise<string | undefined>
|
||||
previewFile(base64: string, filename: string): Promise<boolean>
|
||||
confirmAndExit(): void
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.12.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
### Features
|
||||
|
||||
* **crypto:** pkc algos for key generation, encrypt, and decrypt ([#1663](https://github.com/standardnotes/app/issues/1663)) ([a83003e](https://github.com/standardnotes/app/commit/a83003ee69d144c8e80eaee20bc7be2533d92c63))
|
||||
|
||||
## [1.11.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-08-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/sncrypto-common
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/sncrypto-common",
|
||||
"version": "1.11.1",
|
||||
"version": "1.12.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -26,7 +26,8 @@
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
||||
"@typescript-eslint/parser": "^5.12.1",
|
||||
"eslint-plugin-prettier": "^4.2.1"
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"typescript": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"reflect-metadata": "^0.1.13"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { HexString } from './HexString'
|
||||
|
||||
export type PkcKeyPair = {
|
||||
keyType: 'curve25519' | 'ed25519' | 'x25519'
|
||||
privateKey: HexString
|
||||
publicKey: HexString
|
||||
}
|
||||
@@ -8,3 +8,4 @@ export * from './StreamDecryptorResult'
|
||||
export * from './StreamEncryptor'
|
||||
export * from './Unencrypted'
|
||||
export * from './Utf8String'
|
||||
export * from './PkcKeyPair'
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.13.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
### Features
|
||||
|
||||
* **crypto:** pkc algos for key generation, encrypt, and decrypt ([#1663](https://github.com/standardnotes/app/issues/1663)) ([a83003e](https://github.com/standardnotes/app/commit/a83003ee69d144c8e80eaee20bc7be2533d92c63))
|
||||
|
||||
## [1.12.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-08-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/sncrypto-web
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/sncrypto-web",
|
||||
"version": "1.12.3",
|
||||
"version": "1.13.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -39,11 +39,13 @@
|
||||
"babel-loader": "^8.2.3",
|
||||
"chai": "^4.3.6",
|
||||
"connect": "^3.7.0",
|
||||
"eslint": "*",
|
||||
"eslint-plugin-prettier": "*",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"serve-static": "^1.14.2",
|
||||
"ts-loader": "^9.2.6",
|
||||
"typedarray-to-buffer": "^4.0.0",
|
||||
"typescript": "*",
|
||||
"uuid": "^8.3.2",
|
||||
"webpack": "*",
|
||||
"webpack-cli": "*"
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
PureCryptoInterface,
|
||||
Utf8String,
|
||||
timingSafeEqual,
|
||||
PkcKeyPair,
|
||||
} from '@standardnotes/sncrypto-common'
|
||||
import * as Utils from './utils'
|
||||
import * as sodium from './libsodium'
|
||||
@@ -83,7 +84,7 @@ export class SNWebCrypto implements PureCryptoInterface {
|
||||
iterations: number,
|
||||
length: number,
|
||||
): Promise<HexString | null> {
|
||||
const keyData = await Utils.stringToArrayBuffer(password)
|
||||
const keyData = Utils.stringToArrayBuffer(password)
|
||||
const key = await this.webCryptoImportKey(keyData, WebCryptoAlgs.Pbkdf2, [WebCryptoActions.DeriveBits])
|
||||
if (!key) {
|
||||
console.error('Key is null, unable to continue')
|
||||
@@ -99,21 +100,21 @@ export class SNWebCrypto implements PureCryptoInterface {
|
||||
}
|
||||
|
||||
public async aes256CbcEncrypt(plaintext: Utf8String, iv: HexString, key: HexString): Promise<Base64String> {
|
||||
const keyData = await Utils.hexStringToArrayBuffer(key)
|
||||
const ivData = await Utils.hexStringToArrayBuffer(iv)
|
||||
const keyData = Utils.hexStringToArrayBuffer(key)
|
||||
const ivData = Utils.hexStringToArrayBuffer(iv)
|
||||
const alg = { name: WebCryptoAlgs.AesCbc, iv: ivData }
|
||||
const importedKeyData = await this.webCryptoImportKey(keyData, alg.name, [WebCryptoActions.Encrypt])
|
||||
const textData = await Utils.stringToArrayBuffer(plaintext)
|
||||
const textData = Utils.stringToArrayBuffer(plaintext)
|
||||
const result = await crypto.subtle.encrypt(alg, importedKeyData, textData)
|
||||
return Utils.arrayBufferToBase64(result)
|
||||
}
|
||||
|
||||
public async aes256CbcDecrypt(ciphertext: Base64String, iv: HexString, key: HexString): Promise<Utf8String | null> {
|
||||
const keyData = await Utils.hexStringToArrayBuffer(key)
|
||||
const ivData = await Utils.hexStringToArrayBuffer(iv)
|
||||
const keyData = Utils.hexStringToArrayBuffer(key)
|
||||
const ivData = Utils.hexStringToArrayBuffer(iv)
|
||||
const alg = { name: WebCryptoAlgs.AesCbc, iv: ivData }
|
||||
const importedKeyData = await this.webCryptoImportKey(keyData, alg.name, [WebCryptoActions.Decrypt])
|
||||
const textData = await Utils.base64ToArrayBuffer(ciphertext)
|
||||
const textData = Utils.base64ToArrayBuffer(ciphertext)
|
||||
|
||||
try {
|
||||
const result = await crypto.subtle.decrypt(alg, importedKeyData, textData)
|
||||
@@ -150,11 +151,11 @@ export class SNWebCrypto implements PureCryptoInterface {
|
||||
}
|
||||
|
||||
public async hmac1(message: Utf8String, key: HexString): Promise<HexString | null> {
|
||||
const keyHexData = await Utils.hexStringToArrayBuffer(key)
|
||||
const keyHexData = Utils.hexStringToArrayBuffer(key)
|
||||
const keyData = await this.webCryptoImportKey(keyHexData, WebCryptoAlgs.Hmac, [WebCryptoActions.Sign], {
|
||||
name: WebCryptoAlgs.Sha1,
|
||||
})
|
||||
const messageData = await Utils.stringToArrayBuffer(message)
|
||||
const messageData = Utils.stringToArrayBuffer(message)
|
||||
const funcParams = { name: WebCryptoAlgs.Hmac }
|
||||
|
||||
try {
|
||||
@@ -169,15 +170,14 @@ export class SNWebCrypto implements PureCryptoInterface {
|
||||
}
|
||||
|
||||
public async unsafeSha1(text: string): Promise<string> {
|
||||
const textData = await Utils.stringToArrayBuffer(text)
|
||||
const textData = Utils.stringToArrayBuffer(text)
|
||||
const digest = await crypto.subtle.digest(WebCryptoAlgs.Sha1, textData)
|
||||
return Utils.arrayBufferToHexString(digest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a raw string key to a WebCrypto CryptoKey object.
|
||||
* @param rawKey
|
||||
* A plain utf8 string or an array buffer
|
||||
* @param keyData
|
||||
* @param alg
|
||||
* The name of the algorithm this key will be used for (i.e 'AES-CBC' or 'HMAC')
|
||||
* @param actions
|
||||
@@ -220,7 +220,7 @@ export class SNWebCrypto implements PureCryptoInterface {
|
||||
): Promise<HexString> {
|
||||
const params = {
|
||||
name: WebCryptoAlgs.Pbkdf2,
|
||||
salt: await Utils.stringToArrayBuffer(salt),
|
||||
salt: Utils.stringToArrayBuffer(salt),
|
||||
iterations: iterations,
|
||||
hash: { name: WebCryptoAlgs.Sha512 },
|
||||
}
|
||||
@@ -298,13 +298,13 @@ export class SNWebCrypto implements PureCryptoInterface {
|
||||
public xchacha20StreamEncryptorPush(
|
||||
encryptor: StreamEncryptor,
|
||||
plainBuffer: Uint8Array,
|
||||
assocData: Utf8String,
|
||||
assocData?: Utf8String,
|
||||
tag: SodiumConstant = SodiumConstant.CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_PUSH,
|
||||
): Uint8Array {
|
||||
const encryptedBuffer = sodium.crypto_secretstream_xchacha20poly1305_push(
|
||||
encryptor.state as sodium.StateAddress,
|
||||
plainBuffer,
|
||||
assocData.length > 0 ? Utils.stringToArrayBuffer(assocData) : null,
|
||||
assocData && assocData.length > 0 ? Utils.stringToArrayBuffer(assocData) : null,
|
||||
tag,
|
||||
)
|
||||
return encryptedBuffer
|
||||
@@ -325,7 +325,7 @@ export class SNWebCrypto implements PureCryptoInterface {
|
||||
public xchacha20StreamDecryptorPush(
|
||||
decryptor: StreamDecryptor,
|
||||
encryptedBuffer: Uint8Array,
|
||||
assocData: Utf8String,
|
||||
assocData?: Utf8String,
|
||||
): StreamDecryptorResult | false {
|
||||
if (encryptedBuffer.length < SodiumConstant.CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES) {
|
||||
throw new Error('Invalid ciphertext size')
|
||||
@@ -334,7 +334,7 @@ export class SNWebCrypto implements PureCryptoInterface {
|
||||
const result = sodium.crypto_secretstream_xchacha20poly1305_pull(
|
||||
decryptor.state as sodium.StateAddress,
|
||||
encryptedBuffer,
|
||||
assocData.length > 0 ? Utils.stringToArrayBuffer(assocData) : null,
|
||||
assocData && assocData.length > 0 ? Utils.stringToArrayBuffer(assocData) : null,
|
||||
)
|
||||
|
||||
if ((result as unknown) === false) {
|
||||
@@ -344,6 +344,51 @@ export class SNWebCrypto implements PureCryptoInterface {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* https://doc.libsodium.org/public-key_cryptography/authenticated_encryption
|
||||
*/
|
||||
public sodiumCryptoBoxEasyEncrypt(
|
||||
message: Utf8String,
|
||||
nonce: HexString,
|
||||
senderSecretKey: HexString,
|
||||
recipientPublicKey: HexString,
|
||||
): Base64String {
|
||||
const result = sodium.crypto_box_easy(
|
||||
message,
|
||||
Utils.hexStringToArrayBuffer(nonce),
|
||||
Utils.hexStringToArrayBuffer(recipientPublicKey),
|
||||
Utils.hexStringToArrayBuffer(senderSecretKey),
|
||||
)
|
||||
|
||||
return Utils.arrayBufferToBase64(result)
|
||||
}
|
||||
|
||||
public sodiumCryptoBoxEasyDecrypt(
|
||||
ciphertext: Base64String,
|
||||
nonce: HexString,
|
||||
senderPublicKey: HexString,
|
||||
recipientSecretKey: HexString,
|
||||
): Base64String {
|
||||
const result = sodium.crypto_box_open_easy(
|
||||
Utils.base64ToArrayBuffer(ciphertext),
|
||||
Utils.hexStringToArrayBuffer(nonce),
|
||||
Utils.hexStringToArrayBuffer(senderPublicKey),
|
||||
Utils.hexStringToArrayBuffer(recipientSecretKey),
|
||||
'text',
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public sodiumCryptoBoxGenerateKeypair(): PkcKeyPair {
|
||||
const result = sodium.crypto_box_keypair()
|
||||
|
||||
const publicKey = Utils.arrayBufferToHexString(result.publicKey)
|
||||
const privateKey = Utils.arrayBufferToHexString(result.privateKey)
|
||||
|
||||
return { publicKey, privateKey, keyType: result.keyType }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random secret for TOTP authentication
|
||||
*
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
export { SNWebCrypto } from './crypto'
|
||||
export {
|
||||
arrayBufferToBase64,
|
||||
arrayBufferToHexString,
|
||||
arrayBufferToString,
|
||||
base64Decode,
|
||||
base64Encode,
|
||||
base64ToArrayBuffer,
|
||||
base64ToHex,
|
||||
hexStringToArrayBuffer,
|
||||
hexToBase64,
|
||||
isWebCryptoAvailable,
|
||||
stringToArrayBuffer,
|
||||
base32Decode,
|
||||
base32Encode,
|
||||
} from './utils'
|
||||
export * from './crypto'
|
||||
export * from './utils'
|
||||
export { SodiumConstant } from '@standardnotes/sncrypto-common'
|
||||
|
||||
@@ -3,12 +3,15 @@ export {
|
||||
base64_variants,
|
||||
crypto_aead_xchacha20poly1305_ietf_decrypt,
|
||||
crypto_aead_xchacha20poly1305_ietf_encrypt,
|
||||
crypto_secretstream_xchacha20poly1305_push,
|
||||
crypto_secretstream_xchacha20poly1305_pull,
|
||||
crypto_secretstream_xchacha20poly1305_init_push,
|
||||
crypto_secretstream_xchacha20poly1305_init_pull,
|
||||
crypto_box_easy,
|
||||
crypto_box_keypair,
|
||||
crypto_box_open_easy,
|
||||
crypto_pwhash_ALG_DEFAULT,
|
||||
crypto_pwhash,
|
||||
crypto_secretstream_xchacha20poly1305_init_pull,
|
||||
crypto_secretstream_xchacha20poly1305_init_push,
|
||||
crypto_secretstream_xchacha20poly1305_pull,
|
||||
crypto_secretstream_xchacha20poly1305_push,
|
||||
from_base64,
|
||||
from_hex,
|
||||
from_string,
|
||||
|
||||
@@ -50,31 +50,18 @@ describe('crypto operations', async function () {
|
||||
|
||||
it('compares strings with timing safe comparison', async function () {
|
||||
const crypto = new SNWebCrypto()
|
||||
expect(crypto.timingSafeEqual('hello world 🌍', 'hello world 🌍')).to.equal(
|
||||
true,
|
||||
)
|
||||
expect(crypto.timingSafeEqual('helo world 🌍', 'hello world 🌍')).to.equal(
|
||||
false,
|
||||
)
|
||||
expect(crypto.timingSafeEqual('hello world 🌍', 'hello world 🌍')).to.equal(true)
|
||||
expect(crypto.timingSafeEqual('helo world 🌍', 'hello world 🌍')).to.equal(false)
|
||||
expect(crypto.timingSafeEqual('', 'a')).to.equal(false)
|
||||
expect(crypto.timingSafeEqual('', '')).to.equal(true)
|
||||
expect(
|
||||
crypto.timingSafeEqual(
|
||||
'2e1ee7920bb188a88f94bb912153befd83cc55cd',
|
||||
'2e1ee7920bb188a88f94bb912153befd83cc55cd',
|
||||
),
|
||||
crypto.timingSafeEqual('2e1ee7920bb188a88f94bb912153befd83cc55cd', '2e1ee7920bb188a88f94bb912153befd83cc55cd'),
|
||||
).to.equal(true)
|
||||
expect(
|
||||
crypto.timingSafeEqual(
|
||||
'1e1ee7920bb188a88f94bb912153befd83cc55cd',
|
||||
'2e1ee7920bb188a88f94bb912153befd83cc55cd',
|
||||
),
|
||||
crypto.timingSafeEqual('1e1ee7920bb188a88f94bb912153befd83cc55cd', '2e1ee7920bb188a88f94bb912153befd83cc55cd'),
|
||||
).to.equal(false)
|
||||
expect(
|
||||
crypto.timingSafeEqual(
|
||||
'2e1ee7920bb188a88f94bb912153befd83cc55cc',
|
||||
'2e1ee7920bb188a88f94bb912153befd83cc55cd',
|
||||
),
|
||||
crypto.timingSafeEqual('2e1ee7920bb188a88f94bb912153befd83cc55cc', '2e1ee7920bb188a88f94bb912153befd83cc55cd'),
|
||||
).to.equal(false)
|
||||
})
|
||||
|
||||
@@ -85,22 +72,16 @@ describe('crypto operations', async function () {
|
||||
|
||||
it('pbkdf2 1', async function () {
|
||||
const password = 'very_secure🔒'
|
||||
const salt =
|
||||
'c3feb78823adce65c4ab024dab9c5cdcda5a04cdbd98f65eac0311dfa432d67b'
|
||||
const expected =
|
||||
'bbb3d3af19dd1cbb901c958003faa55f193aad6a57fff30e51a62591bdc054d8'
|
||||
const salt = 'c3feb78823adce65c4ab024dab9c5cdcda5a04cdbd98f65eac0311dfa432d67b'
|
||||
const expected = 'bbb3d3af19dd1cbb901c958003faa55f193aad6a57fff30e51a62591bdc054d8'
|
||||
const result = await webCrypto.pbkdf2(password, salt, 100000, 256)
|
||||
expect(result).to.equal(expected)
|
||||
})
|
||||
|
||||
it('pbkdf2 2', async function () {
|
||||
const password = 'correct horse battery staple ✅'
|
||||
const salt = Buffer.from(
|
||||
'808182838485868788898a8b8c8d8e8f',
|
||||
'hex',
|
||||
).toString('utf8')
|
||||
const expected =
|
||||
'795d83b18e55d860d3799f85a20f66ee17eb9dcf041df1d7a13fac30af7103d9'
|
||||
const salt = Buffer.from('808182838485868788898a8b8c8d8e8f', 'hex').toString('utf8')
|
||||
const expected = '795d83b18e55d860d3799f85a20f66ee17eb9dcf041df1d7a13fac30af7103d9'
|
||||
const result = await webCrypto.pbkdf2(password, salt, 100000, 256)
|
||||
expect(result).to.equal(expected)
|
||||
})
|
||||
@@ -116,19 +97,16 @@ describe('crypto operations', async function () {
|
||||
|
||||
it('hmac 256', async function () {
|
||||
const text = 'hello world 🌍'
|
||||
const key =
|
||||
'e802dc953f3f1f7b5db62409b74ac848559d4711c4e0047ecc5e312ad8ab8397'
|
||||
const key = 'e802dc953f3f1f7b5db62409b74ac848559d4711c4e0047ecc5e312ad8ab8397'
|
||||
const hash = await webCrypto.hmac256(text, key)
|
||||
const expected =
|
||||
'b63f94ee33a067ffac3ee97c7987dd3171dcdc747a322bb3f3ab890201c8e6f9'
|
||||
const expected = 'b63f94ee33a067ffac3ee97c7987dd3171dcdc747a322bb3f3ab890201c8e6f9'
|
||||
expect(hash).to.equal(expected)
|
||||
})
|
||||
|
||||
it('sha256', async function () {
|
||||
const text = 'hello world 🌍'
|
||||
const hash = await webCrypto.sha256(text)
|
||||
const expected =
|
||||
'1e71fe32476da1ff115b44dfd74aed5c90d68a1d80a2033065e30cff4335211a'
|
||||
const expected = '1e71fe32476da1ff115b44dfd74aed5c90d68a1d80a2033065e30cff4335211a'
|
||||
expect(hash).to.equal(expected)
|
||||
})
|
||||
|
||||
@@ -154,13 +132,7 @@ describe('crypto operations', async function () {
|
||||
const bytes = 67108864
|
||||
const length = 16
|
||||
const iterations = 2
|
||||
const result = await webCrypto.argon2(
|
||||
password,
|
||||
salt,
|
||||
iterations,
|
||||
bytes,
|
||||
length,
|
||||
)
|
||||
const result = await webCrypto.argon2(password, salt, iterations, bytes, length)
|
||||
const expectedResult = '18dfbc268f251701652c8e38b5273f73'
|
||||
expect(result).to.equal(expectedResult)
|
||||
})
|
||||
@@ -172,15 +144,8 @@ describe('crypto operations', async function () {
|
||||
const bytes = 67108864
|
||||
const length = 32
|
||||
const iterations = 5
|
||||
const result = await webCrypto.argon2(
|
||||
password,
|
||||
truncatedSalt,
|
||||
iterations,
|
||||
bytes,
|
||||
length,
|
||||
)
|
||||
const expected =
|
||||
'bb6ec440708c271ce34decd7f997e2444d309b1105992779ccdb47f78a5fda6f'
|
||||
const result = await webCrypto.argon2(password, truncatedSalt, iterations, bytes, length)
|
||||
const expected = 'bb6ec440708c271ce34decd7f997e2444d309b1105992779ccdb47f78a5fda6f'
|
||||
expect(result).to.equal(expected)
|
||||
})
|
||||
|
||||
@@ -189,69 +154,38 @@ describe('crypto operations', async function () {
|
||||
const nonce = await webCrypto.generateRandomKey(192)
|
||||
const plaintext = 'hello world 🌍'
|
||||
const aad = JSON.stringify({ uuid: '123🎤' })
|
||||
const ciphertext = await webCrypto.xchacha20Encrypt(
|
||||
plaintext,
|
||||
nonce,
|
||||
key,
|
||||
aad,
|
||||
)
|
||||
const decrypted = await webCrypto.xchacha20Decrypt(
|
||||
ciphertext,
|
||||
nonce,
|
||||
key,
|
||||
aad,
|
||||
)
|
||||
const ciphertext = await webCrypto.xchacha20Encrypt(plaintext, nonce, key, aad)
|
||||
const decrypted = await webCrypto.xchacha20Decrypt(ciphertext, nonce, key, aad)
|
||||
expect(decrypted).to.equal(plaintext)
|
||||
})
|
||||
|
||||
it('xchacha20 streaming encrypt/decrypt', async function () {
|
||||
const key = await webCrypto.generateRandomKey(256)
|
||||
const bigFile = await fetch(
|
||||
'http://localhost:9003/test/resources/big_file.md',
|
||||
)
|
||||
const bigFile = await fetch('http://localhost:9003/test/resources/big_file.md')
|
||||
const bigText = await bigFile.text()
|
||||
const plaintext = bigText
|
||||
const plainBuffer = stringToArrayBuffer(plaintext)
|
||||
const encryptor = webCrypto.xchacha20StreamEncryptInitEncryptor(key)
|
||||
const header = base64StringToArrayBuffer(encryptor.header)
|
||||
const encryptor = webCrypto.xchacha20StreamInitEncryptor(key)
|
||||
|
||||
let encryptedBuffer = Buffer.concat([header])
|
||||
const headerBase64 = encryptor.header
|
||||
const headerBuffer = base64ToArrayBuffer(encryptor.header)
|
||||
|
||||
let encryptedBuffer = Buffer.concat([headerBuffer])
|
||||
const pushChunkSize = plainBuffer.length / 200
|
||||
const pullChunkSize =
|
||||
pushChunkSize +
|
||||
SodiumConstant.CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES
|
||||
const pullChunkSize = pushChunkSize + SodiumConstant.CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES
|
||||
|
||||
for (let i = 0; i < plainBuffer.length; i += pushChunkSize) {
|
||||
const readUntil =
|
||||
i + pushChunkSize > plainBuffer.length
|
||||
? plainBuffer.length
|
||||
: i + pushChunkSize
|
||||
const chunk = webCrypto.xchacha20StreamEncryptorPush(
|
||||
encryptor,
|
||||
plainBuffer.slice(i, readUntil),
|
||||
)
|
||||
const readUntil = i + pushChunkSize > plainBuffer.length ? plainBuffer.length : i + pushChunkSize
|
||||
const chunk = webCrypto.xchacha20StreamEncryptorPush(encryptor, plainBuffer.slice(i, readUntil))
|
||||
encryptedBuffer = Buffer.concat([encryptedBuffer, chunk])
|
||||
}
|
||||
|
||||
const decryptor = webCrypto.xchacha20StreamEncryptInitDecryptor(
|
||||
header,
|
||||
key,
|
||||
)
|
||||
const decryptor = webCrypto.xchacha20StreamInitDecryptor(headerBase64, key)
|
||||
|
||||
let decryptedBuffer = Buffer.alloc(0)
|
||||
for (
|
||||
let i = header.length;
|
||||
i < encryptedBuffer.length;
|
||||
i += pullChunkSize
|
||||
) {
|
||||
const readUntil =
|
||||
i + pullChunkSize > encryptedBuffer.length
|
||||
? encryptedBuffer.length
|
||||
: i + pullChunkSize
|
||||
const chunk = webCrypto.xchacha20StreamDecryptorPush(
|
||||
decryptor,
|
||||
encryptedBuffer.slice(i, readUntil),
|
||||
)
|
||||
for (let i = headerBuffer.length; i < encryptedBuffer.length; i += pullChunkSize) {
|
||||
const readUntil = i + pullChunkSize > encryptedBuffer.length ? encryptedBuffer.length : i + pullChunkSize
|
||||
const chunk = webCrypto.xchacha20StreamDecryptorPush(decryptor, encryptedBuffer.slice(i, readUntil))
|
||||
decryptedBuffer = Buffer.concat([decryptedBuffer, chunk.message])
|
||||
}
|
||||
|
||||
@@ -263,18 +197,8 @@ describe('crypto operations', async function () {
|
||||
const key = await webCrypto.generateRandomKey(256)
|
||||
const nonce = await webCrypto.generateRandomKey(192)
|
||||
const plaintext = 'hello world 🌍'
|
||||
const ciphertext = await webCrypto.xchacha20Encrypt(
|
||||
plaintext,
|
||||
nonce,
|
||||
key,
|
||||
JSON.stringify({ uuid: 'foo🎲' }),
|
||||
)
|
||||
const result = await webCrypto.xchacha20Decrypt(
|
||||
ciphertext,
|
||||
nonce,
|
||||
key,
|
||||
JSON.stringify({ uuid: 'bar🎲' }),
|
||||
)
|
||||
const ciphertext = await webCrypto.xchacha20Encrypt(plaintext, nonce, key, JSON.stringify({ uuid: 'foo🎲' }))
|
||||
const result = await webCrypto.xchacha20Decrypt(ciphertext, nonce, key, JSON.stringify({ uuid: 'bar🎲' }))
|
||||
expect(result).to.not.be.ok
|
||||
})
|
||||
|
||||
@@ -284,23 +208,12 @@ describe('crypto operations', async function () {
|
||||
"Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.🌞"
|
||||
const assocData = await hexStringToArrayBuffer('50515253c0c1c2c3c4c5c6c7')
|
||||
const nonce = '404142434445464748494a4b4c4d4e4f5051525354555657'
|
||||
const key =
|
||||
'808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f'
|
||||
const ciphertext = await webCrypto.xchacha20Encrypt(
|
||||
plaintext,
|
||||
nonce,
|
||||
key,
|
||||
assocData,
|
||||
)
|
||||
const key = '808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f'
|
||||
const ciphertext = await webCrypto.xchacha20Encrypt(plaintext, nonce, key, assocData)
|
||||
const expected =
|
||||
'vW0XnT6D1DuVdleUk8DpOVcqFwAlK/rMvtKQLCE5bLtzHH8bC0qmRAvzqC9O2n45rmTGcIxUwhbLlrcuEhO0Ui+Mm6QNtdlFsRtpuYLBu54/P6wrw2lIj3ayODVl0//5IflmTJdjfal2iBL2FcaLE7UuOqw6kdl7HV6PKzn0pIOeHH3rkwQ='
|
||||
expect(ciphertext).to.equal(expected)
|
||||
const decrypted = await webCrypto.xchacha20Decrypt(
|
||||
ciphertext,
|
||||
nonce,
|
||||
key,
|
||||
assocData,
|
||||
)
|
||||
const decrypted = await webCrypto.xchacha20Decrypt(ciphertext, nonce, key, assocData)
|
||||
expect(decrypted).to.equal(plaintext)
|
||||
})
|
||||
|
||||
@@ -315,16 +228,10 @@ describe('crypto operations', async function () {
|
||||
).toString('utf8')
|
||||
const assocData = Buffer.from('50515253c0c1c2c3c4c5c6c7', 'hex')
|
||||
const nonce = '404142434445464748494a4b4c4d4e4f5051525354555657'
|
||||
const key =
|
||||
'808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f'
|
||||
const key = '808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f'
|
||||
|
||||
/** Encrypt */
|
||||
const ciphertextBase64 = await webCrypto.xchacha20Encrypt(
|
||||
plaintext,
|
||||
nonce,
|
||||
key,
|
||||
assocData,
|
||||
)
|
||||
const ciphertextBase64 = await webCrypto.xchacha20Encrypt(plaintext, nonce, key, assocData)
|
||||
const ciphertextHex = await base64ToHex(ciphertextBase64)
|
||||
const expected =
|
||||
'bd6d179d3e83d43b9576579493c0e939572a1700252bfaccbed2902c21396cbb' +
|
||||
@@ -335,12 +242,7 @@ describe('crypto operations', async function () {
|
||||
expect(ciphertextHex).to.equal(expected)
|
||||
|
||||
/** Decrypt */
|
||||
const decrypted = await webCrypto.xchacha20Decrypt(
|
||||
ciphertextBase64,
|
||||
nonce,
|
||||
key,
|
||||
assocData,
|
||||
)
|
||||
const decrypted = await webCrypto.xchacha20Decrypt(ciphertextBase64, nonce, key, assocData)
|
||||
expect(decrypted).to.equal(plaintext)
|
||||
})
|
||||
|
||||
@@ -351,17 +253,44 @@ describe('crypto operations', async function () {
|
||||
const bytes = 67108864
|
||||
const length = 16
|
||||
const iterations = 2
|
||||
const result = await webCrypto.argon2(
|
||||
password,
|
||||
salt,
|
||||
iterations,
|
||||
bytes,
|
||||
length,
|
||||
)
|
||||
const result = await webCrypto.argon2(password, salt, iterations, bytes, length)
|
||||
const expectedResult = '720f95400220748a811bca9b8cff5d6e'
|
||||
expect(result).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
it('pkc crypto_box_easy keypair generation', async function () {
|
||||
const keypair = await webCrypto.sodiumCryptoBoxGenerateKeypair()
|
||||
expect(keypair.keyType).to.equal('x25519')
|
||||
expect(keypair.publicKey.length).to.equal(64)
|
||||
expect(keypair.privateKey.length).to.equal(64)
|
||||
})
|
||||
|
||||
it('pkc crypto_box_easy encrypt/decrypt', async function () {
|
||||
const senderKeypair = await webCrypto.sodiumCryptoBoxGenerateKeypair()
|
||||
const recipientKeypair = await webCrypto.sodiumCryptoBoxGenerateKeypair()
|
||||
|
||||
const nonce = await webCrypto.generateRandomKey(192)
|
||||
const plaintext = 'hello world 🌍'
|
||||
|
||||
const ciphertext = await webCrypto.sodiumCryptoBoxEasyEncrypt(
|
||||
plaintext,
|
||||
nonce,
|
||||
senderKeypair.privateKey,
|
||||
recipientKeypair.publicKey,
|
||||
)
|
||||
|
||||
expect(ciphertext.length).to.equal(44)
|
||||
|
||||
const decrypted = await webCrypto.sodiumCryptoBoxEasyDecrypt(
|
||||
ciphertext,
|
||||
nonce,
|
||||
senderKeypair.publicKey,
|
||||
recipientKeypair.privateKey,
|
||||
)
|
||||
|
||||
expect(decrypted).to.equal(plaintext)
|
||||
})
|
||||
|
||||
it('generates random OTP secret 160 bits long', async function () {
|
||||
const secret = await webCrypto.generateOtpSecret()
|
||||
expect(secret).to.have.length(32)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" />
|
||||
<script src="https://unpkg.com/chai/chai.js"></script>
|
||||
<script src="https://unpkg.com/mocha/mocha.js"></script>
|
||||
<script src="../../../node_modules/regenerator-runtime/runtime.js"></script>
|
||||
<script src="https://unpkg.com/regenerator-runtime@0.13.9/runtime.js"></script>
|
||||
<script src="../dist/sncrypto-web.js"></script>
|
||||
<script>
|
||||
Object.assign(window, SNCrypto);
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('utils', async function () {
|
||||
|
||||
it('base64URLEncode', async function () {
|
||||
const str = 'hello world'
|
||||
const b64 = await base64Encode(str)
|
||||
const b64 = await base64URLEncode(str)
|
||||
expect(b64).to.equal('aGVsbG8gd29ybGQ')
|
||||
})
|
||||
|
||||
|
||||
@@ -3,6 +3,67 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.134.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.134.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.134.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.134.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.134.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.134.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **web:** change component keys to use id tied to controller lifecycle instead of item id ([#1681](https://github.com/standardnotes/app/issues/1681)) ([492c9ba](https://github.com/standardnotes/app/commit/492c9bacbf7a5a0dc811b6535484dc6bd40193f3))
|
||||
|
||||
## [2.134.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
# [2.134.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
### Features
|
||||
|
||||
* **snjs:** add e2e test for subsequent subscriptions settings persistance ([5ffffbf](https://github.com/standardnotes/app/commit/5ffffbff202ab037b22a72c64fe34673fe49ecaf))
|
||||
|
||||
## [2.133.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **snjs:** increase wait time in retrieving settings test for processing replaceable settings ([3322600](https://github.com/standardnotes/app/commit/33226000f58f5b7f97882479e62de3b24a6b217d))
|
||||
* **snjs:** instructions on running e2e locally ([c841ac9](https://github.com/standardnotes/app/commit/c841ac99d2e51ecca05f4a349efa1a0acb09a86d))
|
||||
|
||||
## [2.133.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
# [2.133.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
### Features
|
||||
|
||||
* handle android back button on android ([#1656](https://github.com/standardnotes/app/issues/1656)) ([981d8a7](https://github.com/standardnotes/app/commit/981d8a7497ea2600e1535ad9170cb5559550ca4d))
|
||||
|
||||
## [2.132.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.132.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.132.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
@@ -44,8 +44,9 @@ To run a stable server environment for E2E tests that is up to date with product
|
||||
|
||||
```
|
||||
yarn install --immutable
|
||||
yarn test:stable-server
|
||||
yarn start:local-server
|
||||
```
|
||||
Wait for the `All services are up!` message.
|
||||
|
||||
Once the server infrastructure is ready, and you've built all packages, you can run the test suite in the browser via:
|
||||
|
||||
@@ -53,6 +54,11 @@ Once the server infrastructure is ready, and you've built all packages, you can
|
||||
yarn start:server:e2e
|
||||
```
|
||||
|
||||
Once you are finished you can close the running local server on E2E repo by typing:
|
||||
```
|
||||
yarn stop:local-server
|
||||
```
|
||||
|
||||
### Unit Tests
|
||||
|
||||
From the root of the repository, run:
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ItemViewControllerInterface } from './ItemViewControllerInterface'
|
||||
export class FileViewController implements ItemViewControllerInterface {
|
||||
public dealloced = false
|
||||
private removeStreamObserver?: () => void
|
||||
public runtimeId = `${Math.random()}`
|
||||
|
||||
constructor(private application: SNApplication, public item: FileItem) {}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export class NoteViewController implements ItemViewControllerInterface {
|
||||
private saveTimeout?: ReturnType<typeof setTimeout>
|
||||
private defaultTitle: string | undefined
|
||||
private defaultTag: UuidString | undefined
|
||||
public runtimeId = `${Math.random()}`
|
||||
|
||||
constructor(
|
||||
private application: SNApplication,
|
||||
|
||||
@@ -3,4 +3,5 @@ export enum ReactNativeToWebEvent {
|
||||
ResumingFromBackground = 'ResumingFromBackground',
|
||||
GainingFocus = 'GainingFocus',
|
||||
LosingFocus = 'LosingFocus',
|
||||
AndroidBackButtonPressed = 'AndroidBackButtonPressed',
|
||||
}
|
||||
|
||||
@@ -286,7 +286,7 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
const serverPassword = rootKey.serverPassword as string
|
||||
const keyParams = rootKey.keyParams
|
||||
|
||||
const registerResponse = await this.userApiService.register(email, serverPassword, keyParams, ephemeral)
|
||||
const registerResponse = await this.userApiService.register({ email, serverPassword, keyParams, ephemeral })
|
||||
|
||||
if ('error' in registerResponse.data) {
|
||||
throw new ApiCallError((registerResponse.data as HttpErrorResponseBody).error.message)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as Factory from './lib/factory.js'
|
||||
import * as Utils from './lib/Utils.js'
|
||||
import * as Files from './lib/Files.js'
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
|
||||
@@ -52,38 +54,6 @@ describe('files', function () {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
const uploadFile = async (fileService, buffer, name, ext, chunkSize) => {
|
||||
const operation = await fileService.beginNewFileUpload(buffer.byteLength)
|
||||
|
||||
let chunkId = 1
|
||||
for (let i = 0; i < buffer.length; i += chunkSize) {
|
||||
const readUntil = i + chunkSize > buffer.length ? buffer.length : i + chunkSize
|
||||
const chunk = buffer.slice(i, readUntil)
|
||||
const isFinalChunk = readUntil === buffer.length
|
||||
|
||||
const error = await fileService.pushBytesForUpload(operation, chunk, chunkId++, isFinalChunk)
|
||||
if (error) {
|
||||
throw new Error('Could not upload file chunk')
|
||||
}
|
||||
}
|
||||
|
||||
const file = await fileService.finishUpload(operation, name, ext)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
const downloadFile = async (fileService, itemManager, remoteIdentifier) => {
|
||||
const file = itemManager.getItems(ContentType.File).find((file) => file.remoteIdentifier === remoteIdentifier)
|
||||
|
||||
let receivedBytes = new Uint8Array()
|
||||
|
||||
await fileService.downloadFile(file, (decryptedBytes) => {
|
||||
receivedBytes = new Uint8Array([...receivedBytes, ...decryptedBytes])
|
||||
})
|
||||
|
||||
return receivedBytes
|
||||
}
|
||||
|
||||
it('should create valet token from server', async function () {
|
||||
await setup({ fakeCrypto: true, subscription: true })
|
||||
const remoteIdentifier = Utils.generateUuid()
|
||||
@@ -141,9 +111,9 @@ describe('files', function () {
|
||||
const response = await fetch('/packages/snjs/mocha/assets/small_file.md')
|
||||
const buffer = new Uint8Array(await response.arrayBuffer())
|
||||
|
||||
const file = await uploadFile(fileService, buffer, 'my-file', 'md', 1000)
|
||||
const file = await Files.uploadFile(fileService, buffer, 'my-file', 'md', 1000)
|
||||
|
||||
const downloadedBytes = await downloadFile(fileService, itemManager, file.remoteIdentifier)
|
||||
const downloadedBytes = await Files.downloadFile(fileService, itemManager, file.remoteIdentifier)
|
||||
|
||||
expect(downloadedBytes).to.eql(buffer)
|
||||
})
|
||||
@@ -154,9 +124,9 @@ describe('files', function () {
|
||||
const response = await fetch('/packages/snjs/mocha/assets/two_mb_file.md')
|
||||
const buffer = new Uint8Array(await response.arrayBuffer())
|
||||
|
||||
const file = await uploadFile(fileService, buffer, 'my-file', 'md', 100000)
|
||||
const file = await Files.uploadFile(fileService, buffer, 'my-file', 'md', 100000)
|
||||
|
||||
const downloadedBytes = await downloadFile(fileService, itemManager, file.remoteIdentifier)
|
||||
const downloadedBytes = await Files.downloadFile(fileService, itemManager, file.remoteIdentifier)
|
||||
|
||||
expect(downloadedBytes).to.eql(buffer)
|
||||
})
|
||||
@@ -167,7 +137,7 @@ describe('files', function () {
|
||||
const response = await fetch('/packages/snjs/mocha/assets/small_file.md')
|
||||
const buffer = new Uint8Array(await response.arrayBuffer())
|
||||
|
||||
const file = await uploadFile(fileService, buffer, 'my-file', 'md', 1000)
|
||||
const file = await Files.uploadFile(fileService, buffer, 'my-file', 'md', 1000)
|
||||
|
||||
const error = await fileService.deleteFile(file)
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
export async function uploadFile(fileService, buffer, name, ext, chunkSize) {
|
||||
const operation = await fileService.beginNewFileUpload(buffer.byteLength)
|
||||
|
||||
let chunkId = 1
|
||||
for (let i = 0; i < buffer.length; i += chunkSize) {
|
||||
const readUntil = i + chunkSize > buffer.length ? buffer.length : i + chunkSize
|
||||
const chunk = buffer.slice(i, readUntil)
|
||||
const isFinalChunk = readUntil === buffer.length
|
||||
|
||||
const error = await fileService.pushBytesForUpload(operation, chunk, chunkId++, isFinalChunk)
|
||||
if (error) {
|
||||
throw new Error('Could not upload file chunk')
|
||||
}
|
||||
}
|
||||
|
||||
const file = await fileService.finishUpload(operation, name, ext)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
export async function downloadFile(fileService, itemManager, remoteIdentifier) {
|
||||
const file = itemManager.getItems(ContentType.File).find((file) => file.remoteIdentifier === remoteIdentifier)
|
||||
|
||||
let receivedBytes = new Uint8Array()
|
||||
|
||||
await fileService.downloadFile(file, (decryptedBytes) => {
|
||||
receivedBytes = new Uint8Array([...receivedBytes, ...decryptedBytes])
|
||||
})
|
||||
|
||||
return receivedBytes
|
||||
}
|
||||
@@ -144,7 +144,11 @@ export async function registerOldUser({ application, email, password, version })
|
||||
const operator = application.protocolService.operatorManager.operatorForVersion(version)
|
||||
const accountKey = await operator.createRootKey(email, password, KeyParamsOrigination.Registration)
|
||||
|
||||
const response = await application.userApiService.register(email, accountKey.serverPassword, accountKey.keyParams)
|
||||
const response = await application.userApiService.register({
|
||||
email: email,
|
||||
serverPassword: accountKey.serverPassword,
|
||||
keyParams: accountKey.keyParams,
|
||||
})
|
||||
/** Mark all existing items as dirty. */
|
||||
await application.itemManager.changeItems(application.itemManager.items, (m) => {
|
||||
m.dirty = true
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import * as Factory from './lib/factory.js'
|
||||
import * as Files from './lib/Files.js'
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
|
||||
describe('settings service', function () {
|
||||
this.timeout(Factory.TwentySecondTimeout)
|
||||
|
||||
const validSetting = SettingName.GoogleDriveBackupFrequency
|
||||
const fakePayload = 'Im so meta even this acronym'
|
||||
const updatedFakePayload = 'is meta'
|
||||
|
||||
let application
|
||||
let context
|
||||
let user
|
||||
|
||||
beforeEach(async function () {
|
||||
context = await Factory.createAppContextWithFakeCrypto()
|
||||
@@ -17,13 +22,31 @@ describe('settings service', function () {
|
||||
|
||||
application = context.application
|
||||
|
||||
await Factory.registerUserToApplication({
|
||||
const registerResponse = await Factory.registerUserToApplication({
|
||||
application: context.application,
|
||||
email: context.email,
|
||||
password: context.password,
|
||||
})
|
||||
user = registerResponse.user
|
||||
})
|
||||
|
||||
const reInitializeApplicationWithRealCrypto = async () => {
|
||||
await Factory.safeDeinit(application)
|
||||
|
||||
context = await Factory.createAppContextWithRealCrypto()
|
||||
|
||||
await context.launch()
|
||||
|
||||
application = context.application
|
||||
|
||||
const registerResponse = await Factory.registerUserToApplication({
|
||||
application: context.application,
|
||||
email: context.email,
|
||||
password: context.password,
|
||||
})
|
||||
user = registerResponse.user
|
||||
}
|
||||
|
||||
afterEach(async function () {
|
||||
await Factory.safeDeinit(application)
|
||||
})
|
||||
@@ -103,9 +126,68 @@ describe('settings service', function () {
|
||||
offline: false,
|
||||
})
|
||||
|
||||
await Factory.sleep(0.5)
|
||||
await Factory.sleep(1)
|
||||
|
||||
const setting = await application.settings.getSubscriptionSetting('FILE_UPLOAD_BYTES_LIMIT')
|
||||
expect(setting).to.be.a('string')
|
||||
})
|
||||
|
||||
it('persist irreplaceable subscription settings between subsequent subscriptions', async () => {
|
||||
await reInitializeApplicationWithRealCrypto()
|
||||
|
||||
await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
|
||||
userEmail: context.email,
|
||||
subscriptionId: 1,
|
||||
subscriptionName: 'PRO_PLAN',
|
||||
subscriptionExpiresAt: (new Date().getTime() + 3_600_000) * 1_000,
|
||||
timestamp: Date.now(),
|
||||
offline: false,
|
||||
})
|
||||
await Factory.sleep(1)
|
||||
|
||||
const response = await fetch('/packages/snjs/mocha/assets/small_file.md')
|
||||
const buffer = new Uint8Array(await response.arrayBuffer())
|
||||
|
||||
await Files.uploadFile(application.fileService, buffer, 'my-file', 'md', 1000)
|
||||
|
||||
await Factory.publishMockedEvent('FILE_UPLOADED', {
|
||||
userUuid: user.uuid,
|
||||
fileByteSize: 123,
|
||||
filePath: 'foobar',
|
||||
fileName: 'barbuzz',
|
||||
})
|
||||
await Factory.sleep(1)
|
||||
|
||||
const limitSettingBefore = await application.settings.getSubscriptionSetting('FILE_UPLOAD_BYTES_LIMIT')
|
||||
expect(limitSettingBefore).to.equal('107374182400')
|
||||
|
||||
const usedSettingBefore = await application.settings.getSubscriptionSetting('FILE_UPLOAD_BYTES_USED')
|
||||
expect(usedSettingBefore).to.equal('123')
|
||||
|
||||
|
||||
await Factory.publishMockedEvent('SUBSCRIPTION_EXPIRED', {
|
||||
userEmail: context.email,
|
||||
subscriptionId: 1,
|
||||
subscriptionName: 'PRO_PLAN',
|
||||
timestamp: Date.now(),
|
||||
offline: false,
|
||||
})
|
||||
await Factory.sleep(1)
|
||||
|
||||
await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', {
|
||||
userEmail: context.email,
|
||||
subscriptionId: 2,
|
||||
subscriptionName: 'PRO_PLAN',
|
||||
subscriptionExpiresAt: (new Date().getTime() + 3_600_000) * 1_000,
|
||||
timestamp: Date.now(),
|
||||
offline: false,
|
||||
})
|
||||
await Factory.sleep(1)
|
||||
|
||||
const limitSettingAfter = await application.settings.getSubscriptionSetting('FILE_UPLOAD_BYTES_LIMIT')
|
||||
expect(limitSettingAfter).to.equal(limitSettingBefore)
|
||||
|
||||
const usedSettingAfter = await application.settings.getSubscriptionSetting('FILE_UPLOAD_BYTES_USED')
|
||||
expect(usedSettingAfter).to.equal(usedSettingBefore)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/snjs",
|
||||
"version": "2.132.4",
|
||||
"version": "2.134.7",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,64 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.5.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
# [1.5.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-02)
|
||||
|
||||
### Features
|
||||
|
||||
* use native preview for pdf in mobile webview ([#1728](https://github.com/standardnotes/app/issues/1728)) ([a90e4a5](https://github.com/standardnotes/app/commit/a90e4a50e820aa13b6af1f76ee9b2f935d12f211))
|
||||
|
||||
# [1.4.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
### Features
|
||||
|
||||
* sync android status bar bg with theme bg color ([#1727](https://github.com/standardnotes/app/issues/1727)) ([47106fd](https://github.com/standardnotes/app/commit/47106fdf83ec89e67444e84a94350870645993cf))
|
||||
|
||||
## [1.3.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* downloading backups in mobile webview ([#1703](https://github.com/standardnotes/app/issues/1703)) ([5f24e72](https://github.com/standardnotes/app/commit/5f24e7285e941b44e05cec5be32bee4589f4963c))
|
||||
|
||||
## [1.3.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.3.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.3.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.3.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.3.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.3.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **web:** incorrect native access ([b9a07d8](https://github.com/standardnotes/app/commit/b9a07d8b5abb9ae6e7fb00b0edc1fa87a2b209e0))
|
||||
|
||||
## [1.3.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* export/sharing notes on mobile webview ([#1644](https://github.com/standardnotes/app/issues/1644)) ([6d5ebde](https://github.com/standardnotes/app/commit/6d5ebdeaa1777f31878b6e7d4dfc33fe83abc9fc))
|
||||
|
||||
## [1.3.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.3.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/ui-services",
|
||||
"version": "1.3.1",
|
||||
"version": "1.5.1",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { ApplicationInterface } from '@standardnotes/services'
|
||||
|
||||
function sanitizeFileName(name: string): string {
|
||||
export function sanitizeFileName(name: string): string {
|
||||
return name.trim().replace(/[.\\/:"?*|<>]/g, '_')
|
||||
}
|
||||
|
||||
@@ -52,13 +52,16 @@ export class ArchiveManager {
|
||||
})
|
||||
|
||||
if (encrypted) {
|
||||
this.downloadData(blobData, `Standard Notes Encrypted Backup and Import File - ${this.formattedDate()}.txt`)
|
||||
this.downloadData(
|
||||
blobData,
|
||||
`Standard Notes Encrypted Backup and Import File - ${this.formattedDateForExports()}.txt`,
|
||||
)
|
||||
} else {
|
||||
this.downloadZippedDecryptedItems(data).catch(console.error)
|
||||
}
|
||||
}
|
||||
|
||||
private formattedDate() {
|
||||
formattedDateForExports() {
|
||||
const string = `${new Date()}`
|
||||
// Match up to the first parenthesis, i.e do not include '(Central Standard Time)'
|
||||
const matches = string.match(/^(.*?) \(/)
|
||||
@@ -68,7 +71,7 @@ export class ArchiveManager {
|
||||
return string
|
||||
}
|
||||
|
||||
private async downloadZippedDecryptedItems(data: BackupFile) {
|
||||
async getZippedDecryptedItemsBlob(data: BackupFile) {
|
||||
const zip = await import('@zip.js/zip.js')
|
||||
const zipWriter = new zip.ZipWriter(new zip.BlobWriter('application/zip'))
|
||||
const items = data.items
|
||||
@@ -80,8 +83,7 @@ export class ArchiveManager {
|
||||
const fileName = zippableFileName('Standard Notes Backup and Import File')
|
||||
await zipWriter.add(fileName, new zip.BlobReader(blob))
|
||||
|
||||
let index = 0
|
||||
const nextFile = async () => {
|
||||
for (let index = 0; index < items.length; index++) {
|
||||
const item = items[index]
|
||||
let name, contents
|
||||
|
||||
@@ -102,17 +104,14 @@ export class ArchiveManager {
|
||||
const fileName =
|
||||
`Items/${sanitizeFileName(item.content_type)}/` + zippableFileName(name, `-${item.uuid.split('-')[0]}`)
|
||||
await zipWriter.add(fileName, new zip.BlobReader(blob))
|
||||
|
||||
index++
|
||||
if (index < items.length) {
|
||||
await nextFile()
|
||||
} else {
|
||||
const finalBlob = await zipWriter.close()
|
||||
this.downloadData(finalBlob, `Standard Notes Backup - ${this.formattedDate()}.zip`)
|
||||
}
|
||||
}
|
||||
|
||||
await nextFile()
|
||||
return await zipWriter.close()
|
||||
}
|
||||
|
||||
private async downloadZippedDecryptedItems(data: BackupFile) {
|
||||
const zippedDecryptedItemsBlob = await this.getZippedDecryptedItemsBlob(data)
|
||||
this.downloadData(zippedDecryptedItemsBlob, `Standard Notes Backup - ${this.formattedDateForExports()}.zip`)
|
||||
}
|
||||
|
||||
async zipData(data: ZippableData): Promise<Blob> {
|
||||
@@ -143,7 +142,7 @@ export class ArchiveManager {
|
||||
|
||||
async downloadDataAsZip(data: ZippableData) {
|
||||
const zipFileAsBlob = await this.zipData(data)
|
||||
this.downloadData(zipFileAsBlob, `Standard Notes Export - ${this.formattedDate()}.zip`)
|
||||
this.downloadData(zipFileAsBlob, `Standard Notes Export - ${this.formattedDateForExports()}.zip`)
|
||||
}
|
||||
|
||||
private hrefForData(data: Blob) {
|
||||
|
||||
@@ -288,12 +288,23 @@ export class ThemeManager extends AbstractService {
|
||||
link.rel = 'stylesheet'
|
||||
link.media = 'screen,print'
|
||||
link.id = theme.uuid
|
||||
link.onload = this.syncThemeColorMetadata
|
||||
document.getElementsByTagName('head')[0].appendChild(link)
|
||||
link.onload = () => {
|
||||
this.syncThemeColorMetadata()
|
||||
|
||||
if (this.application.isNativeMobileWeb()) {
|
||||
this.application.mobileDevice.handleThemeSchemeChange(theme.package_info.isDark ?? false)
|
||||
if (this.application.isNativeMobileWeb()) {
|
||||
setTimeout(() => {
|
||||
this.application
|
||||
.mobileDevice()
|
||||
.handleThemeSchemeChange(theme.package_info.isDark ?? false, this.getBackgroundColor())
|
||||
})
|
||||
}
|
||||
}
|
||||
document.getElementsByTagName('head')[0].appendChild(link)
|
||||
}
|
||||
|
||||
private getBackgroundColor() {
|
||||
const bgColor = getComputedStyle(document.documentElement).getPropertyValue('--sn-stylekit-background-color').trim()
|
||||
return bgColor.length ? bgColor : '#ffffff'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -306,8 +317,7 @@ export class ThemeManager extends AbstractService {
|
||||
return
|
||||
}
|
||||
|
||||
const bgColor = getComputedStyle(document.documentElement).getPropertyValue('--sn-stylekit-background-color')
|
||||
themeColorMetaElement.setAttribute('content', bgColor ? bgColor.trim() : '#ffffff')
|
||||
themeColorMetaElement.setAttribute('content', this.getBackgroundColor())
|
||||
}
|
||||
|
||||
private deactivateTheme(uuid: string) {
|
||||
@@ -323,8 +333,8 @@ export class ThemeManager extends AbstractService {
|
||||
|
||||
removeFromArray(this.activeThemes, uuid)
|
||||
|
||||
if (this.activeThemes.length === 0) {
|
||||
this.application.mobileDevice.handleThemeSchemeChange(false)
|
||||
if (this.activeThemes.length === 0 && this.application.isNativeMobileWeb()) {
|
||||
this.application.mobileDevice().handleThemeSchemeChange(false, '#ffffff')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,195 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.68.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
# [3.68.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-02)
|
||||
|
||||
### Features
|
||||
|
||||
* use native preview for pdf in mobile webview ([#1728](https://github.com/standardnotes/app/issues/1728)) ([a90e4a5](https://github.com/standardnotes/app/commit/a90e4a50e820aa13b6af1f76ee9b2f935d12f211))
|
||||
|
||||
## [3.67.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* text wrapping in notes list panel title ([52962ed](https://github.com/standardnotes/app/commit/52962ed65d9e8f64916e6bc0e8c032642a67fcc8))
|
||||
|
||||
## [3.67.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.67.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* files download in mobile webview ([#1726](https://github.com/standardnotes/app/issues/1726)) ([b15d80e](https://github.com/standardnotes/app/commit/b15d80eb2949eedef59722bc6c7e9cb6b7847db2))
|
||||
|
||||
## [3.67.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* downloading backups in mobile webview ([#1703](https://github.com/standardnotes/app/issues/1703)) ([5f24e72](https://github.com/standardnotes/app/commit/5f24e7285e941b44e05cec5be32bee4589f4963c))
|
||||
|
||||
# [3.67.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-30)
|
||||
|
||||
### Features
|
||||
|
||||
* add pref to disable note status updates ([#1702](https://github.com/standardnotes/app/issues/1702)) ([08b7096](https://github.com/standardnotes/app/commit/08b70968f255d5a72562610f08fe727ebcffa8bb))
|
||||
|
||||
# [3.66.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
### Features
|
||||
|
||||
* add line height & font size settings for plaintext editor ([#1700](https://github.com/standardnotes/app/issues/1700)) ([3c699ea](https://github.com/standardnotes/app/commit/3c699eaa4a92a47774d4762eeddd45e34925e53f))
|
||||
|
||||
# [3.65.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* disable spellcheck on custom note title format ([f28ed36](https://github.com/standardnotes/app/commit/f28ed3613642e498805fbc5adf3b7e79a9dfdb3d))
|
||||
* navigation panel resizer not showing ([#1682](https://github.com/standardnotes/app/issues/1682)) ([88f35b3](https://github.com/standardnotes/app/commit/88f35b39fbd254b80ad9d8b9e4c47b1bc896a576))
|
||||
* **web:** change component keys to use id tied to controller lifecycle instead of item id ([#1681](https://github.com/standardnotes/app/issues/1681)) ([492c9ba](https://github.com/standardnotes/app/commit/492c9bacbf7a5a0dc811b6535484dc6bd40193f3))
|
||||
|
||||
### Features
|
||||
|
||||
* new note status indicator ([#1679](https://github.com/standardnotes/app/issues/1679)) ([e0ec53f](https://github.com/standardnotes/app/commit/e0ec53f85a0103069ad004e1eec04d2ce33273bd))
|
||||
|
||||
# [3.64.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
### Features
|
||||
|
||||
* add custom note title format pref ([#1678](https://github.com/standardnotes/app/issues/1678)) ([11dd39c](https://github.com/standardnotes/app/commit/11dd39c126019c4295c03fb59b05ea5aa3adcd27))
|
||||
|
||||
## [3.63.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.63.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.63.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ipad web view ui improvements ([#1664](https://github.com/standardnotes/app/issues/1664)) ([20e4208](https://github.com/standardnotes/app/commit/20e420820d45e2556f60604de41a1bee96c98fe1))
|
||||
|
||||
# [3.63.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-29)
|
||||
|
||||
### Features
|
||||
|
||||
* add panel settings section in quick settings ([#1669](https://github.com/standardnotes/app/issues/1669)) ([0ed692f](https://github.com/standardnotes/app/commit/0ed692ff9b056d00dde73040482e6b1bdc404d04))
|
||||
|
||||
## [3.62.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve biometrics input on mobile webview ([#1658](https://github.com/standardnotes/app/issues/1658)) ([3f9d3ce](https://github.com/standardnotes/app/commit/3f9d3ceffac206645ff9e5b66e68bf2e7b9cd0e2))
|
||||
|
||||
## [3.62.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* grey flash on textarea focus on safari/webkit ([#1661](https://github.com/standardnotes/app/issues/1661)) ([eafa65e](https://github.com/standardnotes/app/commit/eafa65e93487e60a4b9ee37f8cc9932bbbd8917c))
|
||||
|
||||
## [3.62.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* sticky header behavior on iOS ([#1662](https://github.com/standardnotes/app/issues/1662)) ([c0af596](https://github.com/standardnotes/app/commit/c0af596f2d69654b50dffc36250137998dc629fb))
|
||||
|
||||
## [3.62.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* challenge modal ([9502a7f](https://github.com/standardnotes/app/commit/9502a7fa32f2cca26b6964788a44acc9778bf721))
|
||||
* webapp blanking because of mobileDevice check ([727d78c](https://github.com/standardnotes/app/commit/727d78cfe23c268dcd97566f355584a23c8b9d45))
|
||||
|
||||
# [3.62.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-28)
|
||||
|
||||
### Features
|
||||
|
||||
* handle android back button on android ([#1656](https://github.com/standardnotes/app/issues/1656)) ([981d8a7](https://github.com/standardnotes/app/commit/981d8a7497ea2600e1535ad9170cb5559550ca4d))
|
||||
|
||||
# [3.61.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
### Features
|
||||
|
||||
* delete account ([#1653](https://github.com/standardnotes/app/issues/1653)) ([5db47e8](https://github.com/standardnotes/app/commit/5db47e83e82993bb9d210cb264d05f59ea1db5f3))
|
||||
|
||||
# [3.60.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
### Features
|
||||
|
||||
* upgrade cta ([#1655](https://github.com/standardnotes/app/issues/1655)) ([ec52e56](https://github.com/standardnotes/app/commit/ec52e5684da2c7caaa24929275727cdbc23c28e1))
|
||||
|
||||
## [3.59.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.59.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't hide note tags container on desktop ([70e57e3](https://github.com/standardnotes/app/commit/70e57e32b3bd6e3943d6fc1af2ae0ad9618e1812))
|
||||
|
||||
## [3.59.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* export/sharing notes on mobile webview ([#1644](https://github.com/standardnotes/app/issues/1644)) ([6d5ebde](https://github.com/standardnotes/app/commit/6d5ebdeaa1777f31878b6e7d4dfc33fe83abc9fc))
|
||||
|
||||
# [3.59.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-26)
|
||||
|
||||
### Features
|
||||
|
||||
* sticky header on mobile if plaintext note is scrolled ([#1645](https://github.com/standardnotes/app/issues/1645)) ([f8f6a52](https://github.com/standardnotes/app/commit/f8f6a5212b608c7a43957449d42b1d462134773c))
|
||||
|
||||
## [3.58.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **web:** check for proper feature in the subscription sharing section ([507823d](https://github.com/standardnotes/app/commit/507823dc4f1a2c1db47635ac26a686d4d8f2f4e8))
|
||||
|
||||
# [3.58.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-25)
|
||||
|
||||
### Features
|
||||
|
||||
* note tags button for mobile ([#1641](https://github.com/standardnotes/app/issues/1641)) ([fe2ce9f](https://github.com/standardnotes/app/commit/fe2ce9f1e8b0c027c30bcd892606e914f71d4634))
|
||||
|
||||
## [3.57.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve new note title format ([#1640](https://github.com/standardnotes/app/issues/1640)) ([59e814b](https://github.com/standardnotes/app/commit/59e814bcec8c3633b1b2baeae3e435fa2a19777d))
|
||||
|
||||
## [3.57.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* move prefs button next to account menu button in footer ([#1639](https://github.com/standardnotes/app/issues/1639)) ([a3611c8](https://github.com/standardnotes/app/commit/a3611c802f08817d2ba73adf6b9d48465738b1b6))
|
||||
|
||||
## [3.57.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* responsiveness when resizing window on web/desktop ([#1637](https://github.com/standardnotes/app/issues/1637)) ([e4b1b64](https://github.com/standardnotes/app/commit/e4b1b641194cede65b8f0413bb1c805a84585dfe))
|
||||
|
||||
# [3.57.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
### Features
|
||||
|
||||
* new note title format w/ prefs ([#1629](https://github.com/standardnotes/app/issues/1629)) ([d3621d7](https://github.com/standardnotes/app/commit/d3621d70b1a34537417b06830275467055b753cb))
|
||||
|
||||
# [3.56.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-24)
|
||||
|
||||
### Features
|
||||
|
||||
* flatten quick settings menu and add separate prefs button ([#1631](https://github.com/standardnotes/app/issues/1631)) ([97835e6](https://github.com/standardnotes/app/commit/97835e64ac0ebea829a923af082880b269a2965e))
|
||||
|
||||
## [3.55.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-09-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/web",
|
||||
"version": "3.55.2",
|
||||
"version": "3.68.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/app.js",
|
||||
"author": "Standard Notes.",
|
||||
@@ -85,6 +85,7 @@
|
||||
"@standardnotes/toast": "workspace:*",
|
||||
"@standardnotes/ui-services": "workspace:^",
|
||||
"@zip.js/zip.js": "^2.6.26",
|
||||
"dayjs": "^1.11.5",
|
||||
"mobx": "^6.6.2",
|
||||
"mobx-react-lite": "^3.4.0",
|
||||
"qrcode.react": "^3.1.0",
|
||||
|
||||
@@ -41,6 +41,13 @@ const getKey = () => {
|
||||
return keyCount++
|
||||
}
|
||||
|
||||
const setViewportHeight = () => {
|
||||
document.documentElement.style.setProperty(
|
||||
'--viewport-height',
|
||||
`${visualViewport ? visualViewport.height : window.innerHeight}px`,
|
||||
)
|
||||
}
|
||||
|
||||
const startApplication: StartApplication = async function startApplication(
|
||||
defaultSyncServerHost: string,
|
||||
device: WebOrDesktopDevice,
|
||||
@@ -53,6 +60,7 @@ const startApplication: StartApplication = async function startApplication(
|
||||
let root: Root
|
||||
|
||||
const onDestroy = () => {
|
||||
window.removeEventListener('orientationchange', setViewportHeight)
|
||||
const rootElement = document.getElementById(ElementIds.RootId) as HTMLElement
|
||||
root.unmount()
|
||||
rootElement.remove()
|
||||
@@ -66,10 +74,8 @@ const startApplication: StartApplication = async function startApplication(
|
||||
root = createRoot(appendedRootNode)
|
||||
|
||||
disableIosTextFieldZoom()
|
||||
document.documentElement.style.setProperty(
|
||||
'--viewport-height',
|
||||
`${visualViewport ? visualViewport.height : window.innerHeight}px`,
|
||||
)
|
||||
setViewportHeight()
|
||||
window.addEventListener('orientationchange', setViewportHeight)
|
||||
|
||||
root.render(
|
||||
<ApplicationGroupView
|
||||
|
||||
@@ -26,6 +26,8 @@ import { isDesktopApplication } from '@/Utils'
|
||||
import { DesktopManager } from './Device/DesktopManager'
|
||||
import { ArchiveManager, AutolockService, IOService, WebAlertService, ThemeManager } from '@standardnotes/ui-services'
|
||||
import { MobileWebReceiver } from './MobileWebReceiver'
|
||||
import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
|
||||
type WebServices = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
@@ -45,6 +47,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
public iconsController: IconsController
|
||||
private onVisibilityChange: () => void
|
||||
private mobileWebReceiver?: MobileWebReceiver
|
||||
private androidBackHandler?: AndroidBackHandler
|
||||
|
||||
constructor(
|
||||
deviceInterface: WebOrDesktopDevice,
|
||||
@@ -76,10 +79,11 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
|
||||
if (this.isNativeMobileWeb()) {
|
||||
this.mobileWebReceiver = new MobileWebReceiver(this)
|
||||
this.androidBackHandler = new AndroidBackHandler()
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log = (...args) => {
|
||||
this.mobileDevice.consoleLog(...args)
|
||||
this.mobileDevice().consoleLog(...args)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +179,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
return undefined
|
||||
}
|
||||
|
||||
get mobileDevice(): MobileDeviceInterface {
|
||||
mobileDevice(): MobileDeviceInterface {
|
||||
if (!this.isNativeMobileWeb()) {
|
||||
throw Error('Attempting to access device as mobile device on non mobile platform')
|
||||
}
|
||||
@@ -207,7 +211,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
}
|
||||
|
||||
isGlobalSpellcheckEnabled(): boolean {
|
||||
return this.getPreference(PrefKey.EditorSpellcheck, true)
|
||||
return this.getPreference(PrefKey.EditorSpellcheck, PrefDefaults[PrefKey.EditorSpellcheck])
|
||||
}
|
||||
|
||||
public getItemTags(item: DecryptedItemInterface) {
|
||||
@@ -234,7 +238,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
|
||||
async handleMobileLosingFocusEvent(): Promise<void> {
|
||||
if (this.getMobileScreenshotPrivacyEnabled()) {
|
||||
this.mobileDevice.stopHidingMobileInterfaceFromScreenshots()
|
||||
this.mobileDevice().stopHidingMobileInterfaceFromScreenshots()
|
||||
}
|
||||
|
||||
await this.lockApplicationAfterMobileEventIfApplicable()
|
||||
@@ -242,7 +246,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
|
||||
async handleMobileResumingFromBackgroundEvent(): Promise<void> {
|
||||
if (this.getMobileScreenshotPrivacyEnabled()) {
|
||||
this.mobileDevice.hideMobileInterfaceFromScreenshots()
|
||||
this.mobileDevice().hideMobileInterfaceFromScreenshots()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,4 +268,17 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
await this.lock()
|
||||
}
|
||||
}
|
||||
|
||||
handleAndroidBackButtonPressed(): void {
|
||||
if (typeof this.androidBackHandler !== 'undefined') {
|
||||
this.androidBackHandler.notifyEvent()
|
||||
}
|
||||
}
|
||||
|
||||
addAndroidBackHandlerEventListener(listener: () => boolean) {
|
||||
if (typeof this.androidBackHandler !== 'undefined') {
|
||||
return this.androidBackHandler.addEventListener(listener)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const createApplication = (
|
||||
device: WebOrDesktopDevice,
|
||||
webSocketUrl: string,
|
||||
) => {
|
||||
const platform = getPlatform()
|
||||
const platform = getPlatform(device)
|
||||
|
||||
const application = new WebApplication(
|
||||
deviceInterface,
|
||||
|
||||
@@ -53,6 +53,9 @@ export class MobileWebReceiver {
|
||||
case ReactNativeToWebEvent.ResumingFromBackground:
|
||||
void this.application.handleMobileResumingFromBackgroundEvent()
|
||||
break
|
||||
case ReactNativeToWebEvent.AndroidBackButtonPressed:
|
||||
void this.application.handleAndroidBackButtonPressed()
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
|
||||
@@ -12,7 +12,7 @@ import PreferencesViewWrapper from '@/Components/Preferences/PreferencesViewWrap
|
||||
import ChallengeModal from '@/Components/ChallengeModal/ChallengeModal'
|
||||
import NotesContextMenu from '@/Components/NotesContextMenu/NotesContextMenu'
|
||||
import PurchaseFlowWrapper from '@/Components/PurchaseFlow/PurchaseFlowWrapper'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import RevisionHistoryModal from '@/Components/RevisionHistoryModal/RevisionHistoryModal'
|
||||
import PremiumModalProvider from '@/Hooks/usePremiumModal'
|
||||
import ConfirmSignoutContainer from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal'
|
||||
@@ -25,6 +25,8 @@ import { PanelResizedData } from '@/Types/PanelResizedData'
|
||||
import TagContextMenuWrapper from '@/Components/Tags/TagContextMenuWrapper'
|
||||
import FileDragNDropProvider from '../FileDragNDropProvider/FileDragNDropProvider'
|
||||
import ResponsivePaneProvider from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import AndroidBackHandlerProvider from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||
import ConfirmDeleteAccountContainer from '@/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -33,13 +35,14 @@ type Props = {
|
||||
|
||||
const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicationGroup }) => {
|
||||
const platformString = getPlatformString()
|
||||
const [appClass, setAppClass] = useState('')
|
||||
const [launched, setLaunched] = useState(false)
|
||||
const [needsUnlock, setNeedsUnlock] = useState(true)
|
||||
const [challenges, setChallenges] = useState<Challenge[]>([])
|
||||
|
||||
const viewControllerManager = application.getViewControllerManager()
|
||||
|
||||
const appColumnContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const desktopService = application.getDesktopService()
|
||||
|
||||
@@ -123,15 +126,27 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
useEffect(() => {
|
||||
const removeObserver = application.addWebEventObserver(async (eventName, data) => {
|
||||
if (eventName === WebAppEvent.PanelResized) {
|
||||
if (!appColumnContainerRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const { panel, collapsed } = data as PanelResizedData
|
||||
let appClass = ''
|
||||
if (panel === PANEL_NAME_NOTES && collapsed) {
|
||||
appClass += 'collapsed-notes'
|
||||
|
||||
if (panel === PANEL_NAME_NOTES) {
|
||||
if (collapsed) {
|
||||
appColumnContainerRef.current.classList.add('collapsed-notes')
|
||||
} else {
|
||||
appColumnContainerRef.current.classList.remove('collapsed-notes')
|
||||
}
|
||||
}
|
||||
if (panel === PANEL_NAME_NAVIGATION && collapsed) {
|
||||
appClass += ' collapsed-navigation'
|
||||
|
||||
if (panel === PANEL_NAME_NAVIGATION) {
|
||||
if (collapsed) {
|
||||
appColumnContainerRef.current.classList.add('collapsed-navigation')
|
||||
} else {
|
||||
appColumnContainerRef.current.classList.remove('collapsed-navigation')
|
||||
}
|
||||
}
|
||||
setAppClass(appClass)
|
||||
} else if (eventName === WebAppEvent.WindowDidFocus) {
|
||||
if (!(await application.isLocked())) {
|
||||
application.sync.sync().catch(console.error)
|
||||
@@ -150,7 +165,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
|
||||
const renderChallenges = useCallback(() => {
|
||||
return (
|
||||
<>
|
||||
<AndroidBackHandlerProvider application={application}>
|
||||
{challenges.map((challenge) => {
|
||||
return (
|
||||
<div className="sk-modal" key={`${challenge.id}${application.ephemeralIdentifier}`}>
|
||||
@@ -165,7 +180,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
</AndroidBackHandlerProvider>
|
||||
)
|
||||
}, [viewControllerManager, challenges, mainApplicationGroup, removeChallenge, application])
|
||||
|
||||
@@ -174,76 +189,79 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
}
|
||||
|
||||
return (
|
||||
<PremiumModalProvider application={application} viewControllerManager={viewControllerManager}>
|
||||
<AndroidBackHandlerProvider application={application}>
|
||||
<ResponsivePaneProvider>
|
||||
<div className={platformString + ' main-ui-view sn-component'}>
|
||||
<div id="app" className={appClass + ' app app-column-container'}>
|
||||
<FileDragNDropProvider
|
||||
application={application}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
filesController={viewControllerManager.filesController}
|
||||
>
|
||||
<Navigation application={application} />
|
||||
<ContentListView
|
||||
<PremiumModalProvider application={application} viewControllerManager={viewControllerManager}>
|
||||
<div className={platformString + ' main-ui-view sn-component'}>
|
||||
<div id="app" className="app app-column-container" ref={appColumnContainerRef}>
|
||||
<FileDragNDropProvider
|
||||
application={application}
|
||||
accountMenuController={viewControllerManager.accountMenuController}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
filesController={viewControllerManager.filesController}
|
||||
itemListController={viewControllerManager.itemListController}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
noAccountWarningController={viewControllerManager.noAccountWarningController}
|
||||
noteTagsController={viewControllerManager.noteTagsController}
|
||||
>
|
||||
<Navigation application={application} />
|
||||
<ContentListView
|
||||
application={application}
|
||||
accountMenuController={viewControllerManager.accountMenuController}
|
||||
filesController={viewControllerManager.filesController}
|
||||
itemListController={viewControllerManager.itemListController}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
noAccountWarningController={viewControllerManager.noAccountWarningController}
|
||||
noteTagsController={viewControllerManager.noteTagsController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
searchOptionsController={viewControllerManager.searchOptionsController}
|
||||
/>
|
||||
<NoteGroupView application={application} />
|
||||
</FileDragNDropProvider>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<Footer application={application} applicationGroup={mainApplicationGroup} />
|
||||
<SessionsModal application={application} viewControllerManager={viewControllerManager} />
|
||||
<PreferencesViewWrapper viewControllerManager={viewControllerManager} application={application} />
|
||||
<RevisionHistoryModal
|
||||
application={application}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
searchOptionsController={viewControllerManager.searchOptionsController}
|
||||
subscriptionController={viewControllerManager.subscriptionController}
|
||||
/>
|
||||
<NoteGroupView application={application} />
|
||||
</FileDragNDropProvider>
|
||||
</>
|
||||
|
||||
{renderChallenges()}
|
||||
|
||||
<>
|
||||
<NotesContextMenu
|
||||
application={application}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
noteTagsController={viewControllerManager.noteTagsController}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
/>
|
||||
<TagContextMenuWrapper
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
/>
|
||||
<FileContextMenuWrapper
|
||||
filesController={viewControllerManager.filesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
/>
|
||||
<PurchaseFlowWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||
<ConfirmSignoutContainer
|
||||
applicationGroup={mainApplicationGroup}
|
||||
viewControllerManager={viewControllerManager}
|
||||
application={application}
|
||||
/>
|
||||
<ToastContainer />
|
||||
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||
<PermissionsModalWrapper application={application} />
|
||||
<ConfirmDeleteAccountContainer application={application} viewControllerManager={viewControllerManager} />
|
||||
</>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<Footer application={application} applicationGroup={mainApplicationGroup} />
|
||||
<SessionsModal application={application} viewControllerManager={viewControllerManager} />
|
||||
<PreferencesViewWrapper viewControllerManager={viewControllerManager} application={application} />
|
||||
<RevisionHistoryModal
|
||||
application={application}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
subscriptionController={viewControllerManager.subscriptionController}
|
||||
/>
|
||||
</>
|
||||
|
||||
{renderChallenges()}
|
||||
|
||||
<>
|
||||
<NotesContextMenu
|
||||
application={application}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
noteTagsController={viewControllerManager.noteTagsController}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
/>
|
||||
<TagContextMenuWrapper
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
/>
|
||||
<FileContextMenuWrapper
|
||||
filesController={viewControllerManager.filesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
/>
|
||||
<PurchaseFlowWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||
<ConfirmSignoutContainer
|
||||
applicationGroup={mainApplicationGroup}
|
||||
viewControllerManager={viewControllerManager}
|
||||
application={application}
|
||||
/>
|
||||
<ToastContainer />
|
||||
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||
<PermissionsModalWrapper application={application} />
|
||||
</>
|
||||
</div>
|
||||
</PremiumModalProvider>
|
||||
</ResponsivePaneProvider>
|
||||
</PremiumModalProvider>
|
||||
</AndroidBackHandlerProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ChallengePrompt } from '@standardnotes/services'
|
||||
import { RefObject, useState } from 'react'
|
||||
import Button from '../Button/Button'
|
||||
import Icon from '../Icon/Icon'
|
||||
import { InputValue } from './InputValue'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
onValueChange: (value: InputValue['value'], prompt: ChallengePrompt) => void
|
||||
prompt: ChallengePrompt
|
||||
buttonRef: RefObject<HTMLButtonElement>
|
||||
}
|
||||
|
||||
const BiometricsPrompt = ({ application, onValueChange, prompt, buttonRef }: Props) => {
|
||||
const [authenticated, setAuthenticated] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="min-w-76">
|
||||
<Button
|
||||
primary
|
||||
fullWidth
|
||||
colorStyle={authenticated ? 'success' : 'info'}
|
||||
onClick={async () => {
|
||||
const authenticated = await application.mobileDevice().authenticateWithBiometrics()
|
||||
setAuthenticated(authenticated)
|
||||
onValueChange(authenticated, prompt)
|
||||
}}
|
||||
ref={buttonRef}
|
||||
>
|
||||
{authenticated ? (
|
||||
<span className="flex items-center justify-center gap-3">
|
||||
<Icon type="check-circle" />
|
||||
Biometrics successful
|
||||
</span>
|
||||
) : (
|
||||
'Tap to use biometrics'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BiometricsPrompt
|
||||
@@ -184,6 +184,20 @@ const ChallengeModal: FunctionComponent<Props> = ({
|
||||
}
|
||||
}, [hasBiometricPromptValue, hasOnlyBiometricPrompt, submit])
|
||||
|
||||
useEffect(() => {
|
||||
const removeListener = application.addAndroidBackHandlerEventListener(() => {
|
||||
if (challenge.cancelable) {
|
||||
cancelChallenge()
|
||||
}
|
||||
return true
|
||||
})
|
||||
return () => {
|
||||
if (removeListener) {
|
||||
removeListener()
|
||||
}
|
||||
}
|
||||
}, [application, cancelChallenge, challenge.cancelable])
|
||||
|
||||
if (!challenge.prompts) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import {
|
||||
ChallengePrompt,
|
||||
ChallengeValidation,
|
||||
MobileDeviceInterface,
|
||||
ProtectionSessionDurations,
|
||||
} from '@standardnotes/snjs'
|
||||
import { ChallengePrompt, ChallengeValidation, ProtectionSessionDurations } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useEffect, useRef } from 'react'
|
||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||
import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput'
|
||||
import { ChallengeModalValues } from './ChallengeModalValues'
|
||||
import Button from '../Button/Button'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { InputValue } from './InputValue'
|
||||
import BiometricsPrompt from './BiometricsPrompt'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -87,21 +82,12 @@ const ChallengeModalPrompt: FunctionComponent<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
) : prompt.validation === ChallengeValidation.Biometric ? (
|
||||
<div className="min-w-76">
|
||||
<Button
|
||||
primary
|
||||
fullWidth
|
||||
onClick={async () => {
|
||||
const authenticated = await (
|
||||
application.deviceInterface as MobileDeviceInterface
|
||||
).authenticateWithBiometrics()
|
||||
onValueChange(authenticated, prompt)
|
||||
}}
|
||||
ref={biometricsButtonRef}
|
||||
>
|
||||
Tap to use biometrics
|
||||
</Button>
|
||||
</div>
|
||||
<BiometricsPrompt
|
||||
application={application}
|
||||
onValueChange={onValueChange}
|
||||
prompt={prompt}
|
||||
buttonRef={biometricsButtonRef}
|
||||
/>
|
||||
) : prompt.secureTextEntry ? (
|
||||
<DecoratedPasswordInput
|
||||
ref={inputRef}
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { ViewControllerManager } from '@Controllers/ViewControllerManager'
|
||||
import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@reach/alert-dialog'
|
||||
import { useRef } from 'react'
|
||||
import { STRING_DELETE_ACCOUNT_CONFIRMATION } from '@/Constants/Strings'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
|
||||
type Props = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
const ConfirmDeleteAccountModal = ({ application, viewControllerManager }: Props) => {
|
||||
function closeDialog() {
|
||||
viewControllerManager.accountMenuController.setDeletingAccount(false)
|
||||
}
|
||||
|
||||
const cancelRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
return (
|
||||
<AlertDialog onDismiss={closeDialog} leastDestructiveRef={cancelRef} className="max-w-[600px] p-0">
|
||||
<div className="sk-modal-content">
|
||||
<div className="sn-component">
|
||||
<div className="sk-panel">
|
||||
<div className="sk-panel-content">
|
||||
<div className="sk-panel-section">
|
||||
<AlertDialogLabel className="sk-h3 sk-panel-section-title">Delete account?</AlertDialogLabel>
|
||||
<AlertDialogDescription className="sk-panel-row">
|
||||
<div>
|
||||
<p className="text-foreground">{STRING_DELETE_ACCOUNT_CONFIRMATION}</p>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
|
||||
<div className="my-1 mt-4 flex gap-2">
|
||||
<Button primary small colorStyle="neutral" rounded={false} ref={cancelRef} onClick={closeDialog}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
primary
|
||||
small
|
||||
colorStyle="danger"
|
||||
rounded={false}
|
||||
onClick={() => {
|
||||
application.user.deleteAccount().catch(console.error)
|
||||
closeDialog()
|
||||
}}
|
||||
>
|
||||
Delete my account for good
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
ConfirmDeleteAccountModal.displayName = 'ConfirmDeleteAccountModal'
|
||||
|
||||
const ConfirmDeleteAccountContainer = (props: Props) => {
|
||||
if (!props.viewControllerManager.accountMenuController.deletingAccount) {
|
||||
return null
|
||||
}
|
||||
return <ConfirmDeleteAccountModal {...props} />
|
||||
}
|
||||
|
||||
export default observer(ConfirmDeleteAccountContainer)
|
||||
@@ -69,7 +69,7 @@ const ContentList: FunctionComponent<Props> = ({
|
||||
<div
|
||||
className={classNames(
|
||||
'infinite-scroll overflow-y-auto overflow-x-hidden focus:shadow-none focus:outline-none',
|
||||
'md:max-h-full md:overflow-y-hidden md:hover:overflow-y-auto',
|
||||
'md:max-h-full md:overflow-y-hidden md:hover:overflow-y-auto pointer-coarse:md:overflow-y-auto',
|
||||
'md:hover:[overflow-y:_overlay]',
|
||||
)}
|
||||
id={ElementIds.ContentList}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { StreamingFileReader } from '@standardnotes/filepicker'
|
||||
import SearchBar from '../SearchBar/SearchBar'
|
||||
import { SearchOptionsController } from '@/Controllers/SearchOptionsController'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
|
||||
type Props = {
|
||||
accountMenuController: AccountMenuController
|
||||
@@ -50,7 +51,7 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
selectionController,
|
||||
searchOptionsController,
|
||||
}) => {
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
const { isNotesListVisibleOnTablets, toggleAppPane } = useResponsiveAppPane()
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const itemsViewPanelRef = useRef<HTMLDivElement>(null)
|
||||
@@ -181,12 +182,19 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
[isFilesSmartView],
|
||||
)
|
||||
|
||||
const matchesMediumBreakpoint = useMediaQuery(MediaQueryBreakpoints.md)
|
||||
const matchesXLBreakpoint = useMediaQuery(MediaQueryBreakpoints.xl)
|
||||
const isTabletScreenSize = matchesMediumBreakpoint && !matchesXLBreakpoint
|
||||
|
||||
return (
|
||||
<div
|
||||
id="items-column"
|
||||
className={classNames(
|
||||
'sn-component section app-column flex h-screen flex-col pt-safe-top md:h-full',
|
||||
'xl:w-87.5 xsm-only:!w-full sm-only:!w-full pointer-coarse:md-only:!w-52 pointer-coarse:lg-only:!w-52',
|
||||
'xl:w-87.5 xsm-only:!w-full sm-only:!w-full',
|
||||
isTabletScreenSize && !isNotesListVisibleOnTablets
|
||||
? 'pointer-coarse:md-only:!w-0 pointer-coarse:lg-only:!w-0'
|
||||
: 'pointer-coarse:md-only:!w-60 pointer-coarse:lg-only:!w-60',
|
||||
)}
|
||||
aria-label={'Notes & Files'}
|
||||
ref={itemsViewPanelRef}
|
||||
|
||||
+2
-2
@@ -36,9 +36,9 @@ const ContentListHeader = ({
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="section-title-bar-header items-start gap-1">
|
||||
<div className="section-title-bar-header items-start gap-1 overflow-hidden">
|
||||
<NavigationMenuButton />
|
||||
<div className="flex flex-grow flex-col">
|
||||
<div className="flex min-w-0 flex-grow flex-col break-words">
|
||||
<div className="text-lg font-semibold text-text">{panelTitle}</div>
|
||||
{optionsSubtitle && <div className="text-xs text-passive-0">{optionsSubtitle}</div>}
|
||||
</div>
|
||||
|
||||
+29
-10
@@ -7,6 +7,7 @@ import MenuItem from '@/Components/Menu/MenuItem'
|
||||
import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator'
|
||||
import { MenuItemType } from '@/Components/Menu/MenuItemType'
|
||||
import { DisplayOptionsMenuProps } from './DisplayOptionsMenuProps'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
|
||||
const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
||||
closeDisplayOptionsMenu,
|
||||
@@ -14,17 +15,35 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
||||
isOpen,
|
||||
isFilesSmartView,
|
||||
}) => {
|
||||
const [sortBy, setSortBy] = useState(() => application.getPreference(PrefKey.SortNotesBy, CollectionSort.CreatedAt))
|
||||
const [sortReverse, setSortReverse] = useState(() => application.getPreference(PrefKey.SortNotesReverse, false))
|
||||
const [hidePreview, setHidePreview] = useState(() => application.getPreference(PrefKey.NotesHideNotePreview, false))
|
||||
const [hideDate, setHideDate] = useState(() => application.getPreference(PrefKey.NotesHideDate, false))
|
||||
const [hideTags, setHideTags] = useState(() => application.getPreference(PrefKey.NotesHideTags, true))
|
||||
const [hidePinned, setHidePinned] = useState(() => application.getPreference(PrefKey.NotesHidePinned, false))
|
||||
const [showArchived, setShowArchived] = useState(() => application.getPreference(PrefKey.NotesShowArchived, false))
|
||||
const [showTrashed, setShowTrashed] = useState(() => application.getPreference(PrefKey.NotesShowTrashed, false))
|
||||
const [hideProtected, setHideProtected] = useState(() => application.getPreference(PrefKey.NotesHideProtected, false))
|
||||
const [sortBy, setSortBy] = useState(() =>
|
||||
application.getPreference(PrefKey.SortNotesBy, PrefDefaults[PrefKey.SortNotesBy]),
|
||||
)
|
||||
const [sortReverse, setSortReverse] = useState(() =>
|
||||
application.getPreference(PrefKey.SortNotesReverse, PrefDefaults[PrefKey.SortNotesReverse]),
|
||||
)
|
||||
const [hidePreview, setHidePreview] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesHideNotePreview, PrefDefaults[PrefKey.NotesHideNotePreview]),
|
||||
)
|
||||
const [hideDate, setHideDate] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesHideDate, PrefDefaults[PrefKey.NotesHideDate]),
|
||||
)
|
||||
const [hideTags, setHideTags] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesHideTags, PrefDefaults[PrefKey.NotesHideTags]),
|
||||
)
|
||||
const [hidePinned, setHidePinned] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesHidePinned, PrefDefaults[PrefKey.NotesHidePinned]),
|
||||
)
|
||||
const [showArchived, setShowArchived] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesShowArchived, PrefDefaults[PrefKey.NotesShowArchived]),
|
||||
)
|
||||
const [showTrashed, setShowTrashed] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesShowTrashed, PrefDefaults[PrefKey.NotesShowTrashed]),
|
||||
)
|
||||
const [hideProtected, setHideProtected] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesHideProtected, PrefDefaults[PrefKey.NotesHideProtected]),
|
||||
)
|
||||
const [hideEditorIcon, setHideEditorIcon] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesHideEditorIcon, false),
|
||||
application.getPreference(PrefKey.NotesHideEditorIcon, PrefDefaults[PrefKey.NotesHideEditorIcon]),
|
||||
)
|
||||
|
||||
const toggleSortReverse = useCallback(() => {
|
||||
|
||||
@@ -66,7 +66,7 @@ const FilePreview = ({ file, application }: Props) => {
|
||||
<span className="mt-3">Loading file...</span>
|
||||
</div>
|
||||
) : downloadedBytes ? (
|
||||
<PreviewComponent file={file} bytes={downloadedBytes} />
|
||||
<PreviewComponent application={application} file={file} bytes={downloadedBytes} />
|
||||
) : (
|
||||
<FilePreviewError
|
||||
file={file}
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { getBase64FromBlob } from '@/Utils'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useEffect, useMemo, useRef } from 'react'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import Button from '../Button/Button'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { createObjectURLWithRef } from './CreateObjectURLWithRef'
|
||||
import ImagePreview from './ImagePreview'
|
||||
import { PreviewableTextFileTypes } from './isFilePreviewable'
|
||||
import { PreviewableTextFileTypes, RequiresNativeFilePreview } from './isFilePreviewable'
|
||||
import TextPreview from './TextPreview'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
file: FileItem
|
||||
bytes: Uint8Array
|
||||
}
|
||||
|
||||
const PreviewComponent: FunctionComponent<Props> = ({ file, bytes }) => {
|
||||
const PreviewComponent: FunctionComponent<Props> = ({ application, file, bytes }) => {
|
||||
const { selectedPane } = useResponsiveAppPane()
|
||||
|
||||
const objectUrlRef = useRef<string>()
|
||||
|
||||
const objectUrl = useMemo(() => {
|
||||
@@ -28,6 +36,42 @@ const PreviewComponent: FunctionComponent<Props> = ({ file, bytes }) => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const isNativeMobileWeb = application.isNativeMobileWeb()
|
||||
const requiresNativePreview = RequiresNativeFilePreview.includes(file.mimeType)
|
||||
|
||||
const openNativeFilePreview = useCallback(async () => {
|
||||
if (!isNativeMobileWeb) {
|
||||
throw new Error('Native file preview cannot be used on non-native platform')
|
||||
}
|
||||
|
||||
const fileBase64 = await getBase64FromBlob(
|
||||
new Blob([bytes], {
|
||||
type: file.mimeType,
|
||||
}),
|
||||
)
|
||||
|
||||
application.mobileDevice().previewFile(fileBase64, file.name)
|
||||
}, [application, bytes, file.mimeType, file.name, isNativeMobileWeb])
|
||||
|
||||
useEffect(() => {
|
||||
const shouldOpenNativePreviewOnLoad =
|
||||
isNativeMobileWeb && selectedPane === AppPaneId.Editor && requiresNativePreview
|
||||
if (shouldOpenNativePreviewOnLoad) {
|
||||
void openNativeFilePreview()
|
||||
}
|
||||
}, [isNativeMobileWeb, openNativeFilePreview, requiresNativePreview, selectedPane])
|
||||
|
||||
if (isNativeMobileWeb && requiresNativePreview) {
|
||||
return (
|
||||
<div className="flex flex-grow flex-col items-center justify-center">
|
||||
<div className="text-base font-bold">Previewing file...</div>
|
||||
<Button className="mt-3" primary onClick={openNativeFilePreview}>
|
||||
Re-open file preview
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (file.mimeType.startsWith('image/')) {
|
||||
return <ImagePreview objectUrl={objectUrl} />
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export const PreviewableTextFileTypes = ['text/plain', 'text/csv', 'application/json']
|
||||
|
||||
export const RequiresNativeFilePreview = ['application/pdf']
|
||||
|
||||
export const isFileTypePreviewable = (fileType: string) => {
|
||||
const isImage = fileType.startsWith('image/')
|
||||
const isVideo = fileType.startsWith('video/')
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useRef } from 'react'
|
||||
import AccountMenu, { AccountMenuProps } from '../AccountMenu/AccountMenu'
|
||||
import Icon from '../Icon/Icon'
|
||||
import Popover from '../Popover/Popover'
|
||||
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
||||
|
||||
type Props = AccountMenuProps & {
|
||||
isOpen: boolean
|
||||
@@ -25,18 +26,20 @@ const AccountMenuButton = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={toggleMenu}
|
||||
className={classNames(
|
||||
isOpen ? 'bg-border' : '',
|
||||
'flex h-full w-8 cursor-pointer items-center justify-center rounded-full',
|
||||
)}
|
||||
>
|
||||
<div className={hasError ? 'text-danger' : user ? 'text-info' : 'text-neutral'}>
|
||||
<Icon type="account-circle" className="h-5 w-5 hover:text-info" />
|
||||
</div>
|
||||
</button>
|
||||
<StyledTooltip label="Open account menu">
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={toggleMenu}
|
||||
className={classNames(
|
||||
isOpen ? 'bg-border' : '',
|
||||
'flex h-full w-8 cursor-pointer items-center justify-center rounded-full',
|
||||
)}
|
||||
>
|
||||
<div className={hasError ? 'text-danger' : user ? 'text-info' : 'text-neutral'}>
|
||||
<Icon type="account-circle" className="h-5 w-5 hover:text-info" />
|
||||
</div>
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<Popover anchorElement={buttonRef.current} open={isOpen} togglePopover={toggleMenu} side="top" className="py-2">
|
||||
<AccountMenu
|
||||
onClickOutside={onClickOutside}
|
||||
|
||||
@@ -18,6 +18,8 @@ import { AccountMenuPane } from '../AccountMenu/AccountMenuPane'
|
||||
import { EditorEventSource } from '@/Types/EditorEventSource'
|
||||
import QuickSettingsButton from './QuickSettingsButton'
|
||||
import AccountMenuButton from './AccountMenuButton'
|
||||
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
||||
import UpgradeNow from './UpgradeNow'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -336,6 +338,11 @@ class Footer extends PureComponent<Props, State> {
|
||||
this.viewControllerManager.quickSettingsMenuController.closeQuickSettingsMenu()
|
||||
}
|
||||
|
||||
openPreferences = () => {
|
||||
this.clickOutsideQuickSettingsMenu()
|
||||
this.viewControllerManager.preferencesController.openPreferences()
|
||||
}
|
||||
|
||||
override render() {
|
||||
return (
|
||||
<div className="sn-component">
|
||||
@@ -356,15 +363,30 @@ class Footer extends PureComponent<Props, State> {
|
||||
viewControllerManager={this.viewControllerManager}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-footer-bar-item select-none">
|
||||
<StyledTooltip label="Open preferences">
|
||||
<button
|
||||
onClick={this.openPreferences}
|
||||
className="flex h-full w-8 cursor-pointer items-center justify-center"
|
||||
>
|
||||
<div className="h-5">
|
||||
<Icon type="tune" className="rounded hover:text-info" />
|
||||
</div>
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
</div>
|
||||
<div className="relative z-footer-bar-item select-none">
|
||||
<QuickSettingsButton
|
||||
isOpen={this.state.showQuickSettingsMenu}
|
||||
toggleMenu={this.quickSettingsClickHandler}
|
||||
application={this.application}
|
||||
preferencesController={this.viewControllerManager.preferencesController}
|
||||
quickSettingsMenuController={this.viewControllerManager.quickSettingsMenuController}
|
||||
/>
|
||||
</div>
|
||||
<UpgradeNow
|
||||
application={this.application}
|
||||
featuresController={this.viewControllerManager.featuresController}
|
||||
/>
|
||||
{this.state.showBetaWarning && (
|
||||
<Fragment>
|
||||
<div className="relative z-footer-bar-item ml-3 flex select-none items-center border-l border-solid border-border pl-3">
|
||||
@@ -417,14 +439,16 @@ class Footer extends PureComponent<Props, State> {
|
||||
</div>
|
||||
)}
|
||||
{this.state.hasPasscode && (
|
||||
<div
|
||||
id="lock-item"
|
||||
onClick={this.lockClickHandler}
|
||||
title="Locks application and wipes unencrypted data from memory."
|
||||
className="relative z-footer-bar-item ml-3 flex cursor-pointer select-none items-center border-l border-solid border-border pl-2 hover:text-info"
|
||||
>
|
||||
<Icon type="lock-filled" size="custom" className="h-4.5 w-4.5" />
|
||||
</div>
|
||||
<StyledTooltip label="Lock application">
|
||||
<div
|
||||
id="lock-item"
|
||||
onClick={this.lockClickHandler}
|
||||
title="Locks application and wipes unencrypted data from memory."
|
||||
className="relative z-footer-bar-item ml-3 flex cursor-pointer select-none items-center border-l border-solid border-border pl-2 hover:text-info"
|
||||
>
|
||||
<Icon type="lock-filled" size="custom" className="h-4.5 w-4.5" />
|
||||
</div>
|
||||
</StyledTooltip>
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,40 +1,35 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { PreferencesController } from '@/Controllers/PreferencesController'
|
||||
import { QuickSettingsController } from '@/Controllers/QuickSettingsController'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { useRef } from 'react'
|
||||
import Icon from '../Icon/Icon'
|
||||
import Popover from '../Popover/Popover'
|
||||
import QuickSettingsMenu from '../QuickSettingsMenu/QuickSettingsMenu'
|
||||
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
toggleMenu: () => void
|
||||
application: WebApplication
|
||||
preferencesController: PreferencesController
|
||||
quickSettingsMenuController: QuickSettingsController
|
||||
}
|
||||
|
||||
const QuickSettingsButton = ({
|
||||
application,
|
||||
isOpen,
|
||||
toggleMenu,
|
||||
preferencesController,
|
||||
quickSettingsMenuController,
|
||||
}: Props) => {
|
||||
const QuickSettingsButton = ({ application, isOpen, toggleMenu, quickSettingsMenuController }: Props) => {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={toggleMenu}
|
||||
className="flex h-full w-8 cursor-pointer items-center justify-center"
|
||||
ref={buttonRef}
|
||||
>
|
||||
<div className="h-5">
|
||||
<Icon type="tune" className={classNames(isOpen && 'text-info', 'rounded hover:text-info')} />
|
||||
</div>
|
||||
</button>
|
||||
<StyledTooltip label="Open quick settings menu">
|
||||
<button
|
||||
onClick={toggleMenu}
|
||||
className="flex h-full w-8 cursor-pointer items-center justify-center"
|
||||
ref={buttonRef}
|
||||
>
|
||||
<div className="h-5">
|
||||
<Icon type="themes" className={classNames(isOpen && 'text-info', 'rounded hover:text-info')} />
|
||||
</div>
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<Popover
|
||||
togglePopover={toggleMenu}
|
||||
anchorElement={buttonRef.current}
|
||||
@@ -43,11 +38,7 @@ const QuickSettingsButton = ({
|
||||
align="start"
|
||||
className="py-2"
|
||||
>
|
||||
<QuickSettingsMenu
|
||||
preferencesController={preferencesController}
|
||||
quickSettingsMenuController={quickSettingsMenuController}
|
||||
application={application}
|
||||
/>
|
||||
<QuickSettingsMenu quickSettingsMenuController={quickSettingsMenuController} application={application} />
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { FeaturesController } from '@/Controllers/FeaturesController'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { loadPurchaseFlowUrl } from '../PurchaseFlow/PurchaseFlowFunctions'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
featuresController: FeaturesController
|
||||
}
|
||||
|
||||
const UpgradeNow = ({ application, featuresController }: Props) => {
|
||||
const shouldShowCTA = !featuresController.hasFolders
|
||||
const hasAccount = application.hasAccount()
|
||||
|
||||
const openPlansPage = () => {
|
||||
if (!window.plansUrl) {
|
||||
return
|
||||
}
|
||||
|
||||
if (application.isNativeMobileWeb()) {
|
||||
application.mobileDevice().openUrl(window.plansUrl)
|
||||
} else {
|
||||
window.location.assign(window.plansUrl)
|
||||
}
|
||||
}
|
||||
|
||||
return shouldShowCTA ? (
|
||||
<div className="flex h-full items-center px-2">
|
||||
<button
|
||||
className="rounded bg-info py-0.5 px-1.5 text-xs font-bold uppercase text-info-contrast hover:brightness-125"
|
||||
onClick={() => {
|
||||
if (hasAccount) {
|
||||
void loadPurchaseFlowUrl(application)
|
||||
return
|
||||
}
|
||||
|
||||
openPlansPage()
|
||||
}}
|
||||
>
|
||||
Upgrade now
|
||||
</button>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default observer(UpgradeNow)
|
||||
@@ -86,6 +86,7 @@ export const ICONS = {
|
||||
security: icons.SecurityIcon,
|
||||
server: icons.ServerIcon,
|
||||
settings: icons.SettingsIcon,
|
||||
share: icons.ShareIcon,
|
||||
signIn: icons.SignInIcon,
|
||||
signOut: icons.SignOutIcon,
|
||||
spreadsheets: icons.SpreadsheetsIcon,
|
||||
@@ -99,6 +100,7 @@ export const ICONS = {
|
||||
unarchive: icons.UnarchiveIcon,
|
||||
unpin: icons.UnpinIcon,
|
||||
user: icons.UserIcon,
|
||||
view: icons.ViewIcon,
|
||||
warning: icons.WarningIcon,
|
||||
window: icons.WindowIcon,
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { WebApplication } from '@/Application/Application'
|
||||
import { PANEL_NAME_NAVIGATION } from '@/Constants/Constants'
|
||||
import { ApplicationEvent, PrefKey } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import PanelResizer, { PanelSide, ResizeFinishCallback, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
||||
import ResponsivePaneContent from '@/Components/ResponsivePane/ResponsivePaneContent'
|
||||
import { AppPaneId } from '@/Components/ResponsivePane/AppPaneMetadata'
|
||||
@@ -12,6 +12,7 @@ import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import Icon from '../Icon/Icon'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { isIOS } from '@/Utils'
|
||||
import UpgradeNow from '../Footer/UpgradeNow'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -19,9 +20,9 @@ type Props = {
|
||||
|
||||
const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
const viewControllerManager = useMemo(() => application.getViewControllerManager(), [application])
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [panelElement, setPanelElement] = useState<HTMLDivElement>()
|
||||
const [panelWidth, setPanelWidth] = useState<number>(0)
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
const { selectedPane, toggleAppPane } = useResponsiveAppPane()
|
||||
|
||||
const [hasPasscode, setHasPasscode] = useState(() => application.hasPasscode())
|
||||
useEffect(() => {
|
||||
@@ -62,15 +63,23 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
<div
|
||||
id="navigation"
|
||||
className={classNames(
|
||||
'sn-component section app-column h-screen max-h-screen w-[220px] overflow-hidden pt-safe-top md:h-full md:min-h-0 md:py-0 xsm-only:!w-full sm-only:!w-full',
|
||||
'sn-component section app-column h-screen max-h-screen overflow-hidden pt-safe-top md:h-full md:max-h-full md:min-h-0 md:pb-0',
|
||||
'w-[220px] xl:w-[220px] xsm-only:!w-full sm-only:!w-full',
|
||||
selectedPane === AppPaneId.Navigation
|
||||
? 'pointer-coarse:md-only:!w-48 pointer-coarse:lg-only:!w-48'
|
||||
: 'pointer-coarse:md-only:!w-0 pointer-coarse:lg-only:!w-0',
|
||||
isIOS() ? 'pb-safe-bottom' : 'pb-2.5',
|
||||
)}
|
||||
ref={ref}
|
||||
ref={(element) => {
|
||||
if (element) {
|
||||
setPanelElement(element)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ResponsivePaneContent paneId={AppPaneId.Navigation} contentElementId="navigation-content">
|
||||
<div
|
||||
className={classNames(
|
||||
'flex-grow overflow-y-auto overflow-x-hidden md:overflow-y-hidden md:hover:overflow-y-auto',
|
||||
'flex-grow overflow-y-auto overflow-x-hidden md:overflow-y-hidden md:hover:overflow-y-auto pointer-coarse:md:overflow-y-auto',
|
||||
'md:hover:[overflow-y:_overlay]',
|
||||
)}
|
||||
>
|
||||
@@ -86,7 +95,7 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
</div>
|
||||
<div className="flex items-center border-t border-border px-3.5 pt-2.5 md:hidden">
|
||||
<button
|
||||
className="flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border bg-default text-neutral hover:bg-contrast focus:bg-contrast"
|
||||
className="mr-auto flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border bg-default text-neutral hover:bg-contrast focus:bg-contrast"
|
||||
onClick={() => {
|
||||
toggleAppPane(AppPaneId.Items)
|
||||
}}
|
||||
@@ -95,8 +104,9 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
>
|
||||
<Icon type="chevron-left" />
|
||||
</button>
|
||||
<UpgradeNow application={application} featuresController={viewControllerManager.featuresController} />
|
||||
<button
|
||||
className="ml-auto flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border bg-default text-neutral hover:bg-contrast focus:bg-contrast"
|
||||
className="ml-2.5 flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border bg-default text-neutral hover:bg-contrast focus:bg-contrast"
|
||||
onClick={() => {
|
||||
viewControllerManager.accountMenuController.toggleShow()
|
||||
}}
|
||||
@@ -138,11 +148,11 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
</button>
|
||||
</div>
|
||||
</ResponsivePaneContent>
|
||||
{ref.current && (
|
||||
{panelElement && (
|
||||
<PanelResizer
|
||||
collapsable={true}
|
||||
defaultWidth={150}
|
||||
panel={ref.current}
|
||||
panel={panelElement}
|
||||
hoverable={true}
|
||||
side={PanelSide.Right}
|
||||
type={PanelResizeType.WidthOnly}
|
||||
|
||||
@@ -3,13 +3,17 @@ import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
|
||||
export const NavigationMenuButton = () => {
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
const { selectedPane, toggleAppPane } = useResponsiveAppPane()
|
||||
|
||||
return (
|
||||
<button
|
||||
className="bg-text-padding mr-3 inline-flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border align-middle text-neutral hover:bg-contrast focus:bg-contrast md:hidden"
|
||||
className="bg-text-padding mr-3 inline-flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border align-middle text-neutral hover:bg-contrast focus:bg-contrast md:hidden pointer-coarse:md-only:inline-flex pointer-coarse:lg-only:inline-flex"
|
||||
onClick={() => {
|
||||
toggleAppPane(AppPaneId.Navigation)
|
||||
if (selectedPane === AppPaneId.Items || selectedPane === AppPaneId.Editor) {
|
||||
toggleAppPane(AppPaneId.Navigation)
|
||||
} else {
|
||||
toggleAppPane(AppPaneId.Items)
|
||||
}
|
||||
}}
|
||||
title="Navigation menu"
|
||||
aria-label="Navigation menu"
|
||||
|
||||
@@ -1,20 +1,36 @@
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import Icon from '../Icon/Icon'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { useMediaQuery, MediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
|
||||
const MobileItemsListButton = () => {
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
const { toggleAppPane, isNotesListVisibleOnTablets, toggleNotesListOnTablets } = useResponsiveAppPane()
|
||||
const matchesMediumBreakpoint = useMediaQuery(MediaQueryBreakpoints.md)
|
||||
const matchesXLBreakpoint = useMediaQuery(MediaQueryBreakpoints.xl)
|
||||
const isTabletScreenSize = matchesMediumBreakpoint && !matchesXLBreakpoint
|
||||
|
||||
const iconType: IconType = isTabletScreenSize && !isNotesListVisibleOnTablets ? 'chevron-right' : 'chevron-left'
|
||||
const label = isTabletScreenSize
|
||||
? isNotesListVisibleOnTablets
|
||||
? 'Hide items list'
|
||||
: 'Show items list'
|
||||
: 'Go to items list'
|
||||
|
||||
return (
|
||||
<button
|
||||
className="bg-text-padding mr-3 flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast md:hidden"
|
||||
className="bg-text-padding mr-3 flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast md:hidden pointer-coarse:md-only:flex pointer-coarse:lg-only:flex"
|
||||
onClick={() => {
|
||||
toggleAppPane(AppPaneId.Items)
|
||||
if (isTabletScreenSize) {
|
||||
toggleNotesListOnTablets()
|
||||
} else {
|
||||
toggleAppPane(AppPaneId.Items)
|
||||
}
|
||||
}}
|
||||
title="Go to items list"
|
||||
aria-label="Go to items list"
|
||||
title={label}
|
||||
aria-label={label}
|
||||
>
|
||||
<Icon type="chevron-left" />
|
||||
<Icon type={iconType} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -122,10 +122,10 @@ class NoteGroupView extends PureComponent<Props, State> {
|
||||
<>
|
||||
{this.state.controllers.map((controller) => {
|
||||
return controller instanceof NoteViewController ? (
|
||||
<NoteView key={controller.item.uuid} application={this.application} controller={controller} />
|
||||
<NoteView key={controller.runtimeId} application={this.application} controller={controller} />
|
||||
) : (
|
||||
<FileView
|
||||
key={controller.item.uuid}
|
||||
key={controller.runtimeId}
|
||||
application={this.application}
|
||||
viewControllerManager={this.viewControllerManager}
|
||||
file={controller.item}
|
||||
|
||||
@@ -8,21 +8,23 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
||||
import { SNTag } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { NoteTagsController } from '@/Controllers/NoteTagsController'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
|
||||
type Props = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
noteTagsController: NoteTagsController
|
||||
navigationController: NavigationController
|
||||
tag: SNTag
|
||||
}
|
||||
|
||||
const NoteTag = ({ viewControllerManager, tag }: Props) => {
|
||||
const NoteTag = ({ noteTagsController, navigationController, tag }: Props) => {
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
|
||||
const noteTags = viewControllerManager.noteTagsController
|
||||
const noteTags = noteTagsController
|
||||
|
||||
const { autocompleteInputFocused, focusedTagUuid, tags } = noteTags
|
||||
|
||||
@@ -37,9 +39,9 @@ const NoteTag = ({ viewControllerManager, tag }: Props) => {
|
||||
const longTitle = noteTags.getLongTitle(tag)
|
||||
|
||||
const deleteTag = useCallback(() => {
|
||||
viewControllerManager.noteTagsController.focusPreviousTag(tag)
|
||||
viewControllerManager.noteTagsController.removeTagFromActiveNote(tag).catch(console.error)
|
||||
}, [viewControllerManager, tag])
|
||||
noteTagsController.focusPreviousTag(tag)
|
||||
noteTagsController.removeTagFromActiveNote(tag).catch(console.error)
|
||||
}, [noteTagsController, tag])
|
||||
|
||||
const onDeleteTagClick: MouseEventHandler = useCallback(
|
||||
(event) => {
|
||||
@@ -53,30 +55,30 @@ const NoteTag = ({ viewControllerManager, tag }: Props) => {
|
||||
async (event) => {
|
||||
if (tagClicked && event.target !== deleteTagRef.current) {
|
||||
setTagClicked(false)
|
||||
await viewControllerManager.navigationController.setSelectedTag(tag)
|
||||
await navigationController.setSelectedTag(tag)
|
||||
toggleAppPane(AppPaneId.Items)
|
||||
} else {
|
||||
setTagClicked(true)
|
||||
tagRef.current?.focus()
|
||||
}
|
||||
},
|
||||
[viewControllerManager, tagClicked, tag, toggleAppPane],
|
||||
[tagClicked, navigationController, tag, toggleAppPane],
|
||||
)
|
||||
|
||||
const onFocus = useCallback(() => {
|
||||
viewControllerManager.noteTagsController.setFocusedTagUuid(tag.uuid)
|
||||
noteTagsController.setFocusedTagUuid(tag.uuid)
|
||||
setShowDeleteButton(true)
|
||||
}, [viewControllerManager, tag])
|
||||
}, [noteTagsController, tag])
|
||||
|
||||
const onBlur: FocusEventHandler = useCallback(
|
||||
(event) => {
|
||||
const relatedTarget = event.relatedTarget as Node
|
||||
if (relatedTarget !== deleteTagRef.current) {
|
||||
viewControllerManager.noteTagsController.setFocusedTagUuid(undefined)
|
||||
noteTagsController.setFocusedTagUuid(undefined)
|
||||
setShowDeleteButton(false)
|
||||
}
|
||||
},
|
||||
[viewControllerManager],
|
||||
[noteTagsController],
|
||||
)
|
||||
|
||||
const getTabIndex = useCallback(() => {
|
||||
@@ -91,33 +93,33 @@ const NoteTag = ({ viewControllerManager, tag }: Props) => {
|
||||
|
||||
const onKeyDown: KeyboardEventHandler = useCallback(
|
||||
(event) => {
|
||||
const tagIndex = viewControllerManager.noteTagsController.getTagIndex(tag, tags)
|
||||
const tagIndex = noteTagsController.getTagIndex(tag, tags)
|
||||
switch (event.key) {
|
||||
case 'Backspace':
|
||||
deleteTag()
|
||||
break
|
||||
case 'ArrowLeft':
|
||||
viewControllerManager.noteTagsController.focusPreviousTag(tag)
|
||||
noteTagsController.focusPreviousTag(tag)
|
||||
break
|
||||
case 'ArrowRight':
|
||||
if (tagIndex === tags.length - 1) {
|
||||
viewControllerManager.noteTagsController.setAutocompleteInputFocused(true)
|
||||
noteTagsController.setAutocompleteInputFocused(true)
|
||||
} else {
|
||||
viewControllerManager.noteTagsController.focusNextTag(tag)
|
||||
noteTagsController.focusNextTag(tag)
|
||||
}
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
},
|
||||
[viewControllerManager, deleteTag, tag, tags],
|
||||
[noteTagsController, deleteTag, tag, tags],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (focusedTagUuid === tag.uuid) {
|
||||
tagRef.current?.focus()
|
||||
}
|
||||
}, [viewControllerManager, focusedTagUuid, tag])
|
||||
}, [noteTagsController, focusedTagUuid, tag])
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import AutocompleteTagInput from '@/Components/TagAutocomplete/AutocompleteTagInput'
|
||||
import NoteTag from './NoteTag'
|
||||
import { useEffect } from 'react'
|
||||
import { NoteTagsController } from '@/Controllers/NoteTagsController'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
|
||||
type Props = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
noteTagsController: NoteTagsController
|
||||
navigationController: NavigationController
|
||||
}
|
||||
|
||||
const NoteTagsContainer = ({ viewControllerManager }: Props) => {
|
||||
const { tags, tagsContainerMaxWidth } = viewControllerManager.noteTagsController
|
||||
const NoteTagsContainer = ({ noteTagsController, navigationController }: Props) => {
|
||||
const { tags } = noteTagsController
|
||||
|
||||
useEffect(() => {
|
||||
viewControllerManager.noteTagsController.reloadTagsContainerMaxWidth()
|
||||
}, [viewControllerManager])
|
||||
noteTagsController.reloadTagsContainerMaxWidth()
|
||||
}, [noteTagsController])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex min-w-80 flex-wrap bg-transparent md:-mr-2"
|
||||
style={{
|
||||
maxWidth: tagsContainerMaxWidth,
|
||||
}}
|
||||
>
|
||||
<div className="hidden min-w-80 max-w-full flex-wrap bg-transparent md:-mr-2 md:flex">
|
||||
{tags.map((tag) => (
|
||||
<NoteTag key={tag.uuid} viewControllerManager={viewControllerManager} tag={tag} />
|
||||
<NoteTag
|
||||
key={tag.uuid}
|
||||
noteTagsController={noteTagsController}
|
||||
navigationController={navigationController}
|
||||
tag={tag}
|
||||
/>
|
||||
))}
|
||||
<AutocompleteTagInput viewControllerManager={viewControllerManager} />
|
||||
<AutocompleteTagInput noteTagsController={noteTagsController} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { NoteTagsController } from '@/Controllers/NoteTagsController'
|
||||
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
import { splitQueryInString } from '@/Utils'
|
||||
import { SNTag } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { ChangeEventHandler, FormEventHandler, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Icon from '../Icon/Icon'
|
||||
import Popover from '../Popover/Popover'
|
||||
|
||||
const ListItem = ({
|
||||
tag,
|
||||
isSearching,
|
||||
noteTagsController,
|
||||
autocompleteSearchQuery,
|
||||
}: {
|
||||
tag: SNTag
|
||||
isSearching: boolean
|
||||
noteTagsController: NoteTagsController
|
||||
autocompleteSearchQuery: string
|
||||
}) => {
|
||||
const handleSearchResultClick = useCallback(async () => {
|
||||
await noteTagsController.addTagToActiveNote(tag)
|
||||
noteTagsController.clearAutocompleteSearch()
|
||||
noteTagsController.setAutocompleteInputFocused(true)
|
||||
}, [noteTagsController, tag])
|
||||
|
||||
const handleNoteTagRemove = useCallback(() => {
|
||||
noteTagsController.removeTagFromActiveNote(tag).catch(console.error)
|
||||
}, [noteTagsController, tag])
|
||||
|
||||
const longTitle = noteTagsController.getLongTitle(tag)
|
||||
|
||||
return isSearching ? (
|
||||
<button
|
||||
onClick={handleSearchResultClick}
|
||||
className="max-w-80 flex w-full items-center border-0 bg-transparent px-3 py-2 text-left text-menu-item text-text hover:bg-info-backdrop focus:bg-info-backdrop"
|
||||
>
|
||||
{splitQueryInString(longTitle, autocompleteSearchQuery).map((substring, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className={
|
||||
substring.toLowerCase() === autocompleteSearchQuery.toLowerCase()
|
||||
? 'whitespace-pre-wrap font-bold'
|
||||
: 'whitespace-pre-wrap'
|
||||
}
|
||||
>
|
||||
{substring}
|
||||
</span>
|
||||
))}
|
||||
</button>
|
||||
) : (
|
||||
<div className="max-w-80 flex w-full items-center justify-between border-0 bg-transparent px-3 py-2 text-left text-menu-item text-text">
|
||||
<span className="overflow-hidden overflow-ellipsis whitespace-nowrap">{longTitle}</span>
|
||||
<button
|
||||
onClick={handleNoteTagRemove}
|
||||
className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-danger hover:bg-info-backdrop focus:bg-info-backdrop"
|
||||
>
|
||||
<Icon type="trash" size="small" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const NoteTagsPanel = ({
|
||||
noteTagsController,
|
||||
onClickPreprocessing,
|
||||
}: {
|
||||
noteTagsController: NoteTagsController
|
||||
onClickPreprocessing?: () => Promise<void>
|
||||
}) => {
|
||||
const isDesktopScreen = useMediaQuery(MediaQueryBreakpoints.md)
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const { tags, autocompleteTagResults, autocompleteSearchQuery, autocompleteTagHintVisible } = noteTagsController
|
||||
const isSearching = autocompleteSearchQuery.length > 0
|
||||
const visibleTagsList = isSearching ? autocompleteTagResults : tags
|
||||
|
||||
const toggleMenu = useCallback(async () => {
|
||||
const willMenuOpen = !isOpen
|
||||
if (willMenuOpen && onClickPreprocessing) {
|
||||
await onClickPreprocessing()
|
||||
}
|
||||
setIsOpen(willMenuOpen)
|
||||
}, [onClickPreprocessing, isOpen])
|
||||
|
||||
const onSearchQueryChange: ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
const query = event.target.value
|
||||
|
||||
if (query === '') {
|
||||
noteTagsController.clearAutocompleteSearch()
|
||||
} else {
|
||||
noteTagsController.setAutocompleteSearchQuery(query)
|
||||
noteTagsController.searchActiveNoteAutocompleteTags()
|
||||
}
|
||||
}
|
||||
|
||||
const onFormSubmit: FormEventHandler = async (event) => {
|
||||
event.preventDefault()
|
||||
if (autocompleteSearchQuery !== '') {
|
||||
await noteTagsController.createAndAddNewTag()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isDesktopScreen) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}, [isDesktopScreen])
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast md:hidden"
|
||||
title="Note options menu"
|
||||
aria-label="Note options menu"
|
||||
onClick={toggleMenu}
|
||||
ref={buttonRef}
|
||||
>
|
||||
<Icon type="hashtag" />
|
||||
</button>
|
||||
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="pb-2">
|
||||
<form onSubmit={onFormSubmit} className="sticky top-0 border-b border-border bg-default px-2.5 py-2.5">
|
||||
<input
|
||||
type="text"
|
||||
className="w-full rounded border border-solid border-border bg-default py-1.5 px-3 text-sm text-text"
|
||||
placeholder="Create or search tag..."
|
||||
value={autocompleteSearchQuery}
|
||||
onChange={onSearchQueryChange}
|
||||
ref={(node) => {
|
||||
if (isOpen && node) {
|
||||
node.focus()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
<div className="pt-2.5">
|
||||
{visibleTagsList.map((tag) => (
|
||||
<ListItem
|
||||
key={tag.uuid}
|
||||
tag={tag}
|
||||
isSearching={isSearching}
|
||||
noteTagsController={noteTagsController}
|
||||
autocompleteSearchQuery={autocompleteSearchQuery}
|
||||
/>
|
||||
))}
|
||||
{autocompleteTagHintVisible && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
await noteTagsController.createAndAddNewTag()
|
||||
}}
|
||||
className="max-w-80 flex w-full items-center border-0 bg-transparent px-3 py-2 text-left text-menu-item text-text hover:bg-info-backdrop focus:bg-info-backdrop"
|
||||
>
|
||||
<span>Create new tag:</span>
|
||||
<span className="ml-2 flex items-center rounded bg-contrast py-1 pl-1 pr-2 text-xs text-text">
|
||||
<Icon type="hashtag" className="mr-1 text-neutral" size="small" />
|
||||
<span className="max-w-40 overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
{autocompleteSearchQuery}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(NoteTagsPanel)
|
||||
@@ -3,7 +3,10 @@ import { ComponentPropsWithoutRef, ForwardedRef, forwardRef } from 'react'
|
||||
|
||||
// Based on: https://css-tricks.com/auto-growing-inputs-textareas/#aa-other-ideas
|
||||
const AutoresizingNoteViewTextarea = forwardRef(
|
||||
({ value, ...textareaProps }: ComponentPropsWithoutRef<'textarea'>, ref: ForwardedRef<HTMLTextAreaElement>) => {
|
||||
(
|
||||
{ value, className, ...textareaProps }: ComponentPropsWithoutRef<'textarea'>,
|
||||
ref: ForwardedRef<HTMLTextAreaElement>,
|
||||
) => {
|
||||
return (
|
||||
<div className="relative inline-grid min-h-[75vh] w-full grid-rows-1 items-stretch md:block md:flex-grow">
|
||||
<pre
|
||||
@@ -11,6 +14,7 @@ const AutoresizingNoteViewTextarea = forwardRef(
|
||||
className={classNames(
|
||||
'editable font-editor break-word whitespace-pre-wrap',
|
||||
'invisible [grid-area:1_/_1] md:hidden',
|
||||
className,
|
||||
)}
|
||||
aria-hidden
|
||||
>
|
||||
@@ -18,7 +22,7 @@ const AutoresizingNoteViewTextarea = forwardRef(
|
||||
</pre>
|
||||
<textarea
|
||||
value={value}
|
||||
className="editable font-editor [grid-area:1_/_1] md:h-full md:min-h-0"
|
||||
className={classNames('editable font-editor [grid-area:1_/_1] md:h-full md:min-h-0', className)}
|
||||
{...textareaProps}
|
||||
ref={ref}
|
||||
></textarea>
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import { ReactNode, useState } from 'react'
|
||||
import { IconType, PrefKey } from '@standardnotes/snjs'
|
||||
import Icon from '../Icon/Icon'
|
||||
|
||||
export type NoteStatus = {
|
||||
type: 'saving' | 'saved' | 'error'
|
||||
message: string
|
||||
desc?: string
|
||||
}
|
||||
|
||||
const IndicatorWithTooltip = ({
|
||||
className,
|
||||
onClick,
|
||||
onBlur,
|
||||
icon,
|
||||
isTooltipVisible,
|
||||
children,
|
||||
animateIcon = false,
|
||||
}: {
|
||||
className: string
|
||||
onClick: () => void
|
||||
onBlur: () => void
|
||||
icon: IconType
|
||||
isTooltipVisible: boolean
|
||||
children: ReactNode
|
||||
animateIcon?: boolean
|
||||
}) => (
|
||||
<div className="relative">
|
||||
<button
|
||||
className={classNames('peer flex h-5 w-5 items-center justify-center rounded-full', className)}
|
||||
onClick={onClick}
|
||||
onBlur={onBlur}
|
||||
aria-describedby={ElementIds.NoteStatusTooltip}
|
||||
>
|
||||
<Icon className={animateIcon ? 'animate-spin' : ''} type={icon} size="small" />
|
||||
<span className="sr-only">Note sync status</span>
|
||||
</button>
|
||||
<div
|
||||
id={ElementIds.NoteStatusTooltip}
|
||||
className={classNames(
|
||||
isTooltipVisible ? '' : 'hidden',
|
||||
'absolute top-full right-0 min-w-[90vw] translate-x-2 translate-y-1 select-none rounded border border-border bg-default py-1.5 px-3 text-left peer-hover:block peer-focus:block md:min-w-max',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
type Props = {
|
||||
status: NoteStatus | undefined
|
||||
syncTakingTooLong: boolean
|
||||
updateSavingIndicator?: boolean
|
||||
}
|
||||
|
||||
const NoteStatusIndicator = ({
|
||||
status,
|
||||
syncTakingTooLong,
|
||||
updateSavingIndicator = PrefDefaults[PrefKey.UpdateSavingStatusIndicator],
|
||||
}: Props) => {
|
||||
const [isTooltipVisible, setIsTooltipVisible] = useState(false)
|
||||
|
||||
const onClick = () => setIsTooltipVisible((show) => !show)
|
||||
const onBlur = () => setIsTooltipVisible(false)
|
||||
|
||||
if (updateSavingIndicator && !status) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (status && status.type === 'error') {
|
||||
return (
|
||||
<IndicatorWithTooltip
|
||||
className="bg-danger text-danger-contrast"
|
||||
onClick={onClick}
|
||||
onBlur={onBlur}
|
||||
icon="warning"
|
||||
isTooltipVisible={isTooltipVisible}
|
||||
>
|
||||
<div className="text-sm font-bold text-danger">{status.message}</div>
|
||||
{status.desc && <div className="mt-0.5">{status.desc}</div>}
|
||||
</IndicatorWithTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
if (syncTakingTooLong) {
|
||||
return (
|
||||
<IndicatorWithTooltip
|
||||
className="bg-warning text-warning-contrast"
|
||||
onClick={onClick}
|
||||
onBlur={onBlur}
|
||||
icon={status && status.type === 'saving' ? 'sync' : 'warning'}
|
||||
isTooltipVisible={isTooltipVisible}
|
||||
>
|
||||
{status ? (
|
||||
<>
|
||||
<div className="text-sm font-bold text-warning">{status.message}</div>
|
||||
{status.desc && <div className="mt-0.5">{status.desc}</div>}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-sm font-bold text-warning">Sync taking too long</div>
|
||||
)}
|
||||
</IndicatorWithTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
if (updateSavingIndicator && status) {
|
||||
return (
|
||||
<IndicatorWithTooltip
|
||||
className={classNames(
|
||||
status.type === 'saving' && 'bg-contrast',
|
||||
status.type === 'saved' && 'bg-success text-success-contrast',
|
||||
)}
|
||||
onClick={onClick}
|
||||
onBlur={onBlur}
|
||||
icon={status.type === 'saving' ? 'sync' : 'check'}
|
||||
animateIcon={status.type === 'saving'}
|
||||
isTooltipVisible={isTooltipVisible}
|
||||
>
|
||||
<div className="text-sm font-bold">{status.message}</div>
|
||||
{status.desc && <div className="mt-0.5">{status.desc}</div>}
|
||||
</IndicatorWithTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<IndicatorWithTooltip
|
||||
className="bg-contrast text-passive-1"
|
||||
onClick={onClick}
|
||||
onBlur={onBlur}
|
||||
icon="info"
|
||||
isTooltipVisible={isTooltipVisible}
|
||||
>
|
||||
<div className="text-sm font-bold">Note status updates are disabled</div>
|
||||
<div className="mt-0.5">They can be re-enabled in the Preferences under General > Tools</div>
|
||||
</IndicatorWithTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default NoteStatusIndicator
|
||||
@@ -13,8 +13,11 @@ import {
|
||||
NoteViewController,
|
||||
PayloadEmitSource,
|
||||
WebAppEvent,
|
||||
Platform,
|
||||
EditorLineHeight,
|
||||
EditorFontSize,
|
||||
} from '@standardnotes/snjs'
|
||||
import { debounce, isDesktopApplication } from '@/Utils'
|
||||
import { debounce, isDesktopApplication, isIOS } from '@/Utils'
|
||||
import { EditorEventSource } from '../../Types/EditorEventSource'
|
||||
import { confirmDialog, KeyboardModifier, KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { STRING_DELETE_PLACEHOLDER_ATTEMPT, STRING_DELETE_LOCKED_ATTEMPT, StringDeleteNote } from '@/Constants/Strings'
|
||||
@@ -22,7 +25,6 @@ import { PureComponent } from '@/Components/Abstract/PureComponent'
|
||||
import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedItemOverlay'
|
||||
import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton'
|
||||
import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel'
|
||||
import NoteTagsContainer from '@/Components/NoteTags/NoteTagsContainer'
|
||||
import ComponentView from '@/Components/ComponentView/ComponentView'
|
||||
import PanelResizer, { PanelSide, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
@@ -39,15 +41,15 @@ import IndicatorCircle from '../IndicatorCircle/IndicatorCircle'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import AutoresizingNoteViewTextarea from './AutoresizingTextarea'
|
||||
import MobileItemsListButton from '../NoteGroupView/MobileItemsListButton'
|
||||
import NoteTagsPanel from '../NoteTags/NoteTagsPanel'
|
||||
import NoteTagsContainer from '../NoteTags/NoteTagsContainer'
|
||||
import NoteStatusIndicator, { NoteStatus } from './NoteStatusIndicator'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
|
||||
const MINIMUM_STATUS_DURATION = 400
|
||||
const TEXTAREA_DEBOUNCE = 100
|
||||
const NOTE_EDITING_DISABLED_TEXT = 'Note editing disabled.'
|
||||
|
||||
type NoteStatus = {
|
||||
message?: string
|
||||
desc?: string
|
||||
}
|
||||
const MinimumStatusDuration = 400
|
||||
const TextareaDebounce = 100
|
||||
const NoteEditingDisabledText = 'Note editing disabled.'
|
||||
const StickyHeaderScrollThresholdInPx = 20
|
||||
|
||||
function sortAlphabetically(array: SNComponent[]): SNComponent[] {
|
||||
return array.sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1))
|
||||
@@ -63,7 +65,6 @@ type State = {
|
||||
isDesktop?: boolean
|
||||
lockText: string
|
||||
marginResizersEnabled?: boolean
|
||||
monospaceFont?: boolean
|
||||
noteLocked: boolean
|
||||
noteStatus?: NoteStatus
|
||||
saveError?: boolean
|
||||
@@ -80,6 +81,21 @@ type State = {
|
||||
leftResizerOffset: number
|
||||
rightResizerWidth: number
|
||||
rightResizerOffset: number
|
||||
|
||||
shouldStickyHeader: boolean
|
||||
|
||||
monospaceFont?: boolean
|
||||
lineHeight?: EditorLineHeight
|
||||
fontSize?: EditorFontSize
|
||||
updateSavingIndicator?: boolean
|
||||
}
|
||||
|
||||
const PlaintextFontSizeMapping: Record<EditorFontSize, string> = {
|
||||
ExtraSmall: 'text-xs',
|
||||
Small: 'text-sm',
|
||||
Normal: 'text-editor',
|
||||
Medium: 'text-lg',
|
||||
Large: 'text-xl',
|
||||
}
|
||||
|
||||
class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
@@ -111,7 +127,9 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
|
||||
this.debounceReloadEditorComponent = debounce(this.debounceReloadEditorComponent.bind(this), 25)
|
||||
|
||||
this.textAreaChangeDebounceSave = debounce(this.textAreaChangeDebounceSave, TEXTAREA_DEBOUNCE)
|
||||
this.textAreaChangeDebounceSave = debounce(this.textAreaChangeDebounceSave, TextareaDebounce)
|
||||
|
||||
this.handleWindowScroll = debounce(this.handleWindowScroll, 10)
|
||||
|
||||
this.state = {
|
||||
availableStackComponents: [],
|
||||
@@ -119,7 +137,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
editorText: '',
|
||||
editorTitle: '',
|
||||
isDesktop: isDesktopApplication(),
|
||||
lockText: NOTE_EDITING_DISABLED_TEXT,
|
||||
lockText: NoteEditingDisabledText,
|
||||
noteStatus: undefined,
|
||||
noteLocked: this.controller.item.locked,
|
||||
showLockedIcon: true,
|
||||
@@ -132,12 +150,20 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
leftResizerOffset: 0,
|
||||
rightResizerWidth: 0,
|
||||
rightResizerOffset: 0,
|
||||
shouldStickyHeader: false,
|
||||
}
|
||||
|
||||
this.editorContentRef = createRef<HTMLDivElement>()
|
||||
|
||||
window.addEventListener('scroll', this.handleWindowScroll)
|
||||
}
|
||||
|
||||
override deinit() {
|
||||
super.deinit()
|
||||
;(this.controller as unknown) = undefined
|
||||
|
||||
window.removeEventListener('scroll', this.handleWindowScroll)
|
||||
|
||||
this.removeComponentStreamObserver?.()
|
||||
;(this.removeComponentStreamObserver as unknown) = undefined
|
||||
|
||||
@@ -152,7 +178,6 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
|
||||
this.clearNoteProtectionInactivityTimer()
|
||||
;(this.ensureNoteIsInsertedBeforeUIAction as unknown) = undefined
|
||||
;(this.controller as unknown) = undefined
|
||||
|
||||
this.removeTabObserver?.()
|
||||
this.removeTabObserver = undefined
|
||||
@@ -160,7 +185,6 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
|
||||
this.statusTimeout = undefined
|
||||
;(this.onPanelResizeFinish as unknown) = undefined
|
||||
super.deinit()
|
||||
;(this.dismissProtectedWarning as unknown) = undefined
|
||||
;(this.editorComponentViewerRequestsReload as unknown) = undefined
|
||||
;(this.onTextAreaChange as unknown) = undefined
|
||||
@@ -300,6 +324,10 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
}
|
||||
|
||||
override async onAppEvent(eventName: ApplicationEvent) {
|
||||
if (this.controller?.dealloced) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (eventName) {
|
||||
case ApplicationEvent.PreferencesChanged:
|
||||
this.reloadPreferences().catch(console.error)
|
||||
@@ -328,6 +356,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
break
|
||||
case ApplicationEvent.LocalDatabaseWriteError:
|
||||
this.showErrorStatus({
|
||||
type: 'error',
|
||||
message: 'Offline Saving Issue',
|
||||
desc: 'Changes not saved',
|
||||
})
|
||||
@@ -485,7 +514,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
}
|
||||
|
||||
showSavingStatus() {
|
||||
this.setStatus({ message: 'Saving…' }, false)
|
||||
this.setStatus({ type: 'saving', message: 'Saving…' }, false)
|
||||
}
|
||||
|
||||
showAllChangesSavedStatus() {
|
||||
@@ -494,6 +523,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
syncTakingTooLong: false,
|
||||
})
|
||||
this.setStatus({
|
||||
type: 'saved',
|
||||
message: 'All changes saved' + (this.application.noAccount() ? ' offline' : ''),
|
||||
})
|
||||
}
|
||||
@@ -501,6 +531,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
showErrorStatus(error?: NoteStatus) {
|
||||
if (!error) {
|
||||
error = {
|
||||
type: 'error',
|
||||
message: 'Sync Unreachable',
|
||||
desc: 'Changes saved offline',
|
||||
}
|
||||
@@ -521,7 +552,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
this.setState({
|
||||
noteStatus: status,
|
||||
})
|
||||
}, MINIMUM_STATUS_DURATION)
|
||||
}, MinimumStatusDuration)
|
||||
} else {
|
||||
this.setState({
|
||||
noteStatus: status,
|
||||
@@ -674,28 +705,46 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
}
|
||||
|
||||
async reloadPreferences() {
|
||||
const monospaceFont = this.application.getPreference(PrefKey.EditorMonospaceEnabled, true)
|
||||
const monospaceFont = this.application.getPreference(
|
||||
PrefKey.EditorMonospaceEnabled,
|
||||
PrefDefaults[PrefKey.EditorMonospaceEnabled],
|
||||
)
|
||||
|
||||
const marginResizersEnabled = this.application.getPreference(PrefKey.EditorResizersEnabled, true)
|
||||
const marginResizersEnabled = this.application.getPreference(
|
||||
PrefKey.EditorResizersEnabled,
|
||||
PrefDefaults[PrefKey.EditorResizersEnabled],
|
||||
)
|
||||
|
||||
const lineHeight = this.application.getPreference(PrefKey.EditorLineHeight, PrefDefaults[PrefKey.EditorLineHeight])
|
||||
|
||||
const fontSize = this.application.getPreference(PrefKey.EditorFontSize, PrefDefaults[PrefKey.EditorFontSize])
|
||||
|
||||
const updateSavingIndicator = this.application.getPreference(
|
||||
PrefKey.UpdateSavingStatusIndicator,
|
||||
PrefDefaults[PrefKey.UpdateSavingStatusIndicator],
|
||||
)
|
||||
|
||||
await this.reloadSpellcheck()
|
||||
|
||||
this.setState({
|
||||
monospaceFont,
|
||||
marginResizersEnabled,
|
||||
lineHeight,
|
||||
fontSize,
|
||||
updateSavingIndicator,
|
||||
})
|
||||
|
||||
reloadFont(monospaceFont)
|
||||
|
||||
if (marginResizersEnabled) {
|
||||
const width = this.application.getPreference(PrefKey.EditorWidth, null)
|
||||
const width = this.application.getPreference(PrefKey.EditorWidth, PrefDefaults[PrefKey.EditorWidth])
|
||||
if (width != null) {
|
||||
this.setState({
|
||||
leftResizerWidth: width,
|
||||
rightResizerWidth: width,
|
||||
})
|
||||
}
|
||||
const left = this.application.getPreference(PrefKey.EditorLeft, null)
|
||||
const left = this.application.getPreference(PrefKey.EditorLeft, PrefDefaults[PrefKey.EditorLeft])
|
||||
if (left != null) {
|
||||
this.setState({
|
||||
leftResizerOffset: left,
|
||||
@@ -871,6 +920,12 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
}
|
||||
}
|
||||
|
||||
handleWindowScroll = () => {
|
||||
this.setState({
|
||||
shouldStickyHeader: window.scrollY > StickyHeaderScrollThresholdInPx,
|
||||
})
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this.state.showProtectedWarning) {
|
||||
return (
|
||||
@@ -889,7 +944,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
<EditingDisabledBanner
|
||||
onMouseLeave={() => {
|
||||
this.setState({
|
||||
lockText: NOTE_EDITING_DISABLED_TEXT,
|
||||
lockText: NoteEditingDisabledText,
|
||||
showLockedIcon: true,
|
||||
})
|
||||
}}
|
||||
@@ -906,8 +961,19 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
)}
|
||||
|
||||
{this.note && (
|
||||
<div id="editor-title-bar" className="content-title-bar section-title-bar z-editor-title-bar w-full">
|
||||
<div className="mb-2 flex flex-wrap items-start justify-between gap-2 md:mb-0 md:flex-nowrap md:gap-0 xl:items-center">
|
||||
<div
|
||||
id="editor-title-bar"
|
||||
className={classNames(
|
||||
'content-title-bar section-title-bar z-editor-title-bar w-full bg-default',
|
||||
this.state.shouldStickyHeader && 'fixed top-0',
|
||||
this.state.shouldStickyHeader
|
||||
? isIOS() || this.application.platform === Platform.Ios
|
||||
? 'pt-safe-top'
|
||||
: 'pt-4'
|
||||
: '',
|
||||
)}
|
||||
>
|
||||
<div className="mb-2 flex flex-wrap items-start justify-between gap-2 md:mb-0 md:flex-nowrap md:gap-4 xl:items-center">
|
||||
<div className={classNames(this.state.noteLocked && 'locked', 'flex flex-grow items-center')}>
|
||||
<MobileItemsListButton />
|
||||
<div className="title flex-grow overflow-auto">
|
||||
@@ -925,25 +991,18 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<NoteStatusIndicator
|
||||
status={this.state.noteStatus}
|
||||
syncTakingTooLong={this.state.syncTakingTooLong}
|
||||
updateSavingIndicator={this.state.updateSavingIndicator}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row-reverse items-center gap-3 md:flex-col-reverse md:items-end xl:flex-row xl:flex-nowrap xl:items-center">
|
||||
{this.state.noteStatus?.message?.length && (
|
||||
<div id="save-status-container" className={'xl:mr-5 xl:max-w-[16ch]'}>
|
||||
<div id="save-status">
|
||||
<div
|
||||
className={
|
||||
(this.state.syncTakingTooLong ? 'font-bold text-warning ' : '') +
|
||||
(this.state.saveError ? 'font-bold text-danger ' : '') +
|
||||
'message text-xs'
|
||||
}
|
||||
>
|
||||
{this.state.noteStatus?.message}
|
||||
</div>
|
||||
{this.state.noteStatus?.desc && <div className="desc text-xs">{this.state.noteStatus.desc}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!this.state.shouldStickyHeader && (
|
||||
<div className="flex items-center gap-3">
|
||||
<NoteTagsPanel
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
noteTagsController={this.viewControllerManager.noteTagsController}
|
||||
/>
|
||||
<AttachedFilesButton
|
||||
application={this.application}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
@@ -972,9 +1031,14 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<NoteTagsContainer viewControllerManager={this.viewControllerManager} />
|
||||
{!this.state.shouldStickyHeader && (
|
||||
<NoteTagsContainer
|
||||
noteTagsController={this.viewControllerManager.noteTagsController}
|
||||
navigationController={this.viewControllerManager.navigationController}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1012,14 +1076,18 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
{this.state.editorStateDidLoad && !this.state.editorComponentViewer && !this.state.textareaUnloading && (
|
||||
<AutoresizingNoteViewTextarea
|
||||
autoComplete="off"
|
||||
className={classNames(
|
||||
this.state.lineHeight && `leading-${this.state.lineHeight.toLowerCase()}`,
|
||||
this.state.fontSize && PlaintextFontSizeMapping[this.state.fontSize],
|
||||
)}
|
||||
dir="auto"
|
||||
id={ElementIds.NoteTextEditor}
|
||||
onChange={this.onTextAreaChange}
|
||||
value={this.state.editorText}
|
||||
readOnly={this.state.noteLocked}
|
||||
onFocus={this.onContentFocus}
|
||||
spellCheck={this.state.spellcheck}
|
||||
readOnly={this.state.noteLocked}
|
||||
ref={(ref) => ref && this.onSystemEditorLoad(ref)}
|
||||
spellCheck={this.state.spellcheck}
|
||||
value={this.state.editorText}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ const NotesContextMenu = ({
|
||||
side="right"
|
||||
togglePopover={closeMenu}
|
||||
>
|
||||
<div ref={contextMenuRef}>
|
||||
<div className="select-none" ref={contextMenuRef}>
|
||||
<NotesOptions
|
||||
application={application}
|
||||
navigationController={navigationController}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Icon from '@/Components/Icon/Icon'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useState, useEffect, useMemo, useCallback, FunctionComponent } from 'react'
|
||||
import { SNApplication, SNComponent, SNNote } from '@standardnotes/snjs'
|
||||
import { Platform, SNApplication, SNComponent, SNNote } from '@standardnotes/snjs'
|
||||
import { KeyboardModifier } from '@standardnotes/ui-services'
|
||||
import ChangeEditorOption from './ChangeEditorOption'
|
||||
import { BYTES_IN_ONE_MEGABYTE } from '@/Constants/Constants'
|
||||
@@ -15,6 +15,9 @@ import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||
import { formatDateForContextMenu } from '@/Utils/DateUtils'
|
||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||
import { getNoteBlob, getNoteFileName } from '@/Utils/NoteExportUtils'
|
||||
import { shareSelectedNotes } from '@/NativeMobileWeb/ShareSelectedNotes'
|
||||
import { downloadSelectedNotesOnAndroid } from '@/NativeMobileWeb/DownloadSelectedNotesOnAndroid'
|
||||
|
||||
type DeletePermanentlyButtonProps = {
|
||||
onClick: () => void
|
||||
@@ -95,7 +98,7 @@ const NoteAttributes: FunctionComponent<{
|
||||
const format = editor?.package_info?.file_type || 'txt'
|
||||
|
||||
return (
|
||||
<div className="px-3 py-1.5 text-xs font-medium text-neutral">
|
||||
<div className="select-text px-3 py-1.5 text-xs font-medium text-neutral">
|
||||
{typeof words === 'number' && (format === 'txt' || format === 'md') ? (
|
||||
<>
|
||||
<div className="mb-1">
|
||||
@@ -220,18 +223,11 @@ const NotesOptions = ({
|
||||
}
|
||||
}, [application])
|
||||
|
||||
const getNoteFileName = useCallback(
|
||||
(note: SNNote): string => {
|
||||
const editor = application.componentManager.editorForNote(note)
|
||||
const format = editor?.package_info?.file_type || 'txt'
|
||||
return `${note.title}.${format}`
|
||||
},
|
||||
[application.componentManager],
|
||||
)
|
||||
|
||||
const downloadSelectedItems = useCallback(async () => {
|
||||
if (notes.length === 1) {
|
||||
application.getArchiveService().downloadData(new Blob([notes[0].text]), getNoteFileName(notes[0]))
|
||||
const note = notes[0]
|
||||
const blob = getNoteBlob(application, note)
|
||||
application.getArchiveService().downloadData(blob, getNoteFileName(application, note))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -243,8 +239,8 @@ const NotesOptions = ({
|
||||
await application.getArchiveService().downloadDataAsZip(
|
||||
notes.map((note) => {
|
||||
return {
|
||||
name: getNoteFileName(note),
|
||||
content: new Blob([note.text]),
|
||||
name: getNoteFileName(application, note),
|
||||
content: getNoteBlob(application, note),
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -254,7 +250,7 @@ const NotesOptions = ({
|
||||
message: `Exported ${notes.length} notes`,
|
||||
})
|
||||
}
|
||||
}, [application, getNoteFileName, notes])
|
||||
}, [application, notes])
|
||||
|
||||
const closeMenuAndToggleNotesList = useCallback(() => {
|
||||
toggleAppPane(AppPaneId.Items)
|
||||
@@ -358,11 +354,22 @@ const NotesOptions = ({
|
||||
)}
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={downloadSelectedItems}
|
||||
onClick={() => {
|
||||
application.isNativeMobileWeb() ? shareSelectedNotes(application, notes) : downloadSelectedItems()
|
||||
}}
|
||||
>
|
||||
<Icon type="download" className={iconClass} />
|
||||
Export
|
||||
<Icon type={application.platform === Platform.Android ? 'share' : 'download'} className={iconClass} />
|
||||
{application.platform === Platform.Android ? 'Share' : 'Export'}
|
||||
</button>
|
||||
{application.platform === Platform.Android && (
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={() => downloadSelectedNotesOnAndroid(application, notes)}
|
||||
>
|
||||
<Icon type="download" className={iconClass} />
|
||||
Export
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={duplicateSelectedItems}
|
||||
|
||||
@@ -48,7 +48,7 @@ const NotesOptionsPanel = ({
|
||||
>
|
||||
<Icon type="more" />
|
||||
</button>
|
||||
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="py-2">
|
||||
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="select-none py-2">
|
||||
<NotesOptions
|
||||
application={application}
|
||||
navigationController={navigationController}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
|
||||
import { CSSProperties } from 'react'
|
||||
import { PopoverAlignment, PopoverSide } from './Types'
|
||||
import { OppositeSide, checkCollisions, getNonCollidingSide, getNonCollidingAlignment } from './Utils/Collisions'
|
||||
@@ -29,7 +30,7 @@ export const getPositionedPopoverStyles = ({
|
||||
return [null, side, align]
|
||||
}
|
||||
|
||||
const matchesMediumBreakpoint = matchMedia('(min-width: 768px)').matches
|
||||
const matchesMediumBreakpoint = matchMedia(MediaQueryBreakpoints.md).matches
|
||||
|
||||
if (!matchesMediumBreakpoint) {
|
||||
return [null, side, align]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||
import { UuidGenerator } from '@standardnotes/snjs'
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import PositionedPopoverContent from './PositionedPopoverContent'
|
||||
@@ -41,6 +42,8 @@ const Popover = ({
|
||||
}: Props) => {
|
||||
const popoverId = useRef(UuidGenerator.GenerateUuid())
|
||||
|
||||
const addAndroidBackHandler = useAndroidBackHandler()
|
||||
|
||||
useRegisterPopoverToParent(popoverId.current)
|
||||
|
||||
const [childPopovers, setChildPopovers] = useState<Set<string>>(new Set())
|
||||
@@ -64,6 +67,23 @@ const Popover = ({
|
||||
[registerChildPopover, unregisterChildPopover],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let removeListener: (() => void) | undefined
|
||||
|
||||
if (open) {
|
||||
removeListener = addAndroidBackHandler(() => {
|
||||
togglePopover()
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (removeListener) {
|
||||
removeListener()
|
||||
}
|
||||
}
|
||||
}, [addAndroidBackHandler, open, togglePopover])
|
||||
|
||||
return open ? (
|
||||
<PopoverContext.Provider value={contextValue}>
|
||||
<PositionedPopoverContent
|
||||
|
||||
@@ -9,8 +9,8 @@ import { getPositionedPopoverStyles } from './GetPositionedPopoverStyles'
|
||||
import { PopoverContentProps } from './Types'
|
||||
import { getPopoverMaxHeight, getAppRect } from './Utils/Rect'
|
||||
import { usePopoverCloseOnClickOutside } from './Utils/usePopoverCloseOnClickOutside'
|
||||
import { fitNodeToMobileScreen } from '@/Utils'
|
||||
import { useDisableBodyScrollOnMobile } from '@/Hooks/useDisableBodyScrollOnMobile'
|
||||
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
|
||||
const PositionedPopoverContent = ({
|
||||
align = 'end',
|
||||
@@ -35,6 +35,7 @@ const PositionedPopoverContent = ({
|
||||
})
|
||||
const anchorRect = anchorPoint ? anchorPointRect : anchorElementRect
|
||||
const documentRect = useDocumentRect()
|
||||
const isDesktopScreen = useMediaQuery(MediaQueryBreakpoints.md)
|
||||
|
||||
const [styles, positionedSide, positionedAlignment] = getPositionedPopoverStyles({
|
||||
align,
|
||||
@@ -57,30 +58,29 @@ const PositionedPopoverContent = ({
|
||||
<Portal>
|
||||
<div
|
||||
className={classNames(
|
||||
'safe-area-padding absolute top-0 left-0 flex h-full w-full min-w-80 cursor-auto flex-col overflow-y-auto rounded bg-default shadow-main md:h-auto md:max-w-xs',
|
||||
'absolute top-0 left-0 flex h-full w-full min-w-80 cursor-auto flex-col overflow-y-auto rounded bg-default shadow-main md:h-auto md:max-w-xs',
|
||||
overrideZIndex ? overrideZIndex : 'z-dropdown-menu',
|
||||
!isDesktopScreen ? 'pt-safe-top pb-safe-bottom' : '',
|
||||
)}
|
||||
style={{
|
||||
...styles,
|
||||
maxHeight: getPopoverMaxHeight(getAppRect(documentRect), anchorRect, positionedSide, positionedAlignment),
|
||||
top: !isDesktopScreen ? `${document.documentElement.scrollTop}px` : '',
|
||||
}}
|
||||
ref={(node) => {
|
||||
setPopoverElement(node)
|
||||
fitNodeToMobileScreen(node)
|
||||
}}
|
||||
data-popover={id}
|
||||
>
|
||||
<div className={className}>
|
||||
<div className="md:hidden">
|
||||
<div className="flex items-center justify-end px-3">
|
||||
<button className="rounded-full border border-border p-1" onClick={togglePopover}>
|
||||
<Icon type="close" className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<HorizontalSeparator classes="my-2" />
|
||||
<div className="md:hidden">
|
||||
<div className="flex items-center justify-end px-3 pt-2">
|
||||
<button className="rounded-full border border-border p-1" onClick={togglePopover}>
|
||||
<Icon type="close" className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
{children}
|
||||
<HorizontalSeparator classes="my-2" />
|
||||
</div>
|
||||
<div className={className}>{children}</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
|
||||
import { PopoverSide, PopoverAlignment } from '../Types'
|
||||
|
||||
export const getPopoverMaxHeight = (
|
||||
@@ -6,7 +7,7 @@ export const getPopoverMaxHeight = (
|
||||
side: PopoverSide,
|
||||
alignment: PopoverAlignment,
|
||||
): number | 'none' => {
|
||||
const matchesMediumBreakpoint = matchMedia('(min-width: 768px)').matches
|
||||
const matchesMediumBreakpoint = matchMedia(MediaQueryBreakpoints.md).matches
|
||||
|
||||
if (!matchesMediumBreakpoint) {
|
||||
return 'none'
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
import { MediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
type Options = {
|
||||
@@ -15,7 +16,7 @@ export const usePopoverCloseOnClickOutside = ({
|
||||
}: Options) => {
|
||||
useEffect(() => {
|
||||
const closeIfClickedOutside = (event: MouseEvent) => {
|
||||
const matchesMediumBreakpoint = matchMedia('(min-width: 768px)').matches
|
||||
const matchesMediumBreakpoint = matchMedia(MediaQueryBreakpoints.md).matches
|
||||
|
||||
if (!matchesMediumBreakpoint) {
|
||||
return
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user