Compare commits

...
Author SHA1 Message Date
StandardNotes CI 4971e3c56e chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-10-03 08:24:54 +00:00
Karol Sójko d54e6965c1 feat(api): add http status code for gone resource 2022-10-03 09:54:18 +02:00
StandardNotes CI ca144be66a chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-10-02 18:16:57 +00:00
Aman Harwara a90e4a50e8 feat: use native preview for pdf in mobile webview (#1728) 2022-10-02 23:10:44 +05:30
StandardNotes CI 65818ac1cd chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
2022-10-02 16:48:10 +00:00
Aman Harwara 52962ed65d fix: text wrapping in notes list panel title 2022-10-02 21:40:49 +05:30
StandardNotes CI be2c4720fc chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
2022-09-30 19:58:37 +00:00
Aman Harwara 47106fdf83 feat: sync android status bar bg with theme bg color (#1727) 2022-10-01 00:58:16 +05:30
StandardNotes CI 88e29fe1b6 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
2022-09-30 17:32:50 +00:00
Aman Harwara b15d80eb29 fix: files download in mobile webview (#1726) 2022-09-30 22:31:49 +05:30
StandardNotes CI 86a0b61612 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-09-30 16:33:58 +00:00
Aman Harwara 5f24e7285e fix: downloading backups in mobile webview (#1703) 2022-09-30 21:33:30 +05:30
StandardNotes CI 1c530ab534 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].9
 - @standardnotes/[email protected].9
 - @standardnotes/[email protected].9
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-09-30 15:36:21 +00:00
Aman Harwara 08b70968f2 feat: add pref to disable note status updates (#1702) 2022-09-30 20:34:56 +05:30
StandardNotes CI 0fa9f6d7b6 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-09-29 19:51:41 +00:00
Aman Harwara 3c699eaa4a feat: add line height & font size settings for plaintext editor (#1700) 2022-09-30 00:50:33 +05:30
StandardNotes CI fc86cebc75 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].0
2022-09-29 16:43:04 +00:00
Aman Harwara e0ec53f85a feat: new note status indicator (#1679) 2022-09-29 21:36:08 +05:30
Mo 492c9bacbf fix(web): change component keys to use id tied to controller lifecycle instead of item id (#1681)
* fix(web): change component keys to use id tied to controller lifecycle instead of item id

* chore: lint
2022-09-29 10:58:10 -05:00
Aman Harwara 88f35b39fb fix: navigation panel resizer not showing (#1682) 2022-09-29 21:09:03 +05:30
Aman Harwara f28ed36136 fix: disable spellcheck on custom note title format
fix: open title format help page in new tab/window
2022-09-29 20:43:44 +05:30
StandardNotes CI f02465346f chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected].9
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-09-29 15:10:20 +00:00
Aman Harwara 11dd39c126 feat: add custom note title format pref (#1678) 2022-09-29 20:10:05 +05:30
Mo d7a90c4d91 refactor: add pkc fields for registration endpoint (#1680) 2022-09-29 09:13:05 -05:00
StandardNotes CI 00075616e6 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].3
2022-09-29 13:41:22 +00:00
Karol Sójko 5ffffbff20 feat(snjs): add e2e test for subsequent subscriptions settings persistance 2022-09-29 15:11:44 +02:00
StandardNotes CI ecd8d56171 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].2
2022-09-29 10:01:20 +00:00
Karol Sójko 33226000f5 fix(snjs): increase wait time in retrieving settings test for processing replaceable settings 2022-09-29 11:30:36 +02:00
Karol Sójko c841ac99d2 fix(snjs): instructions on running e2e locally 2022-09-29 11:07:12 +02:00
StandardNotes CI b0e708186c chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-09-29 08:54:37 +00:00
Aman Harwara 20e420820d fix: ipad web view ui improvements (#1664) 2022-09-29 13:47:00 +05:30
StandardNotes CI c3d6a91730 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-09-29 07:45:24 +00:00
Aman Harwara 0ed692ff9b feat: add panel settings section in quick settings (#1669) 2022-09-29 12:46:06 +05:30
StandardNotes CI c74fa272fb chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
2022-09-28 16:34:58 +00:00
Aman Harwara 3f9d3ceffa fix: improve biometrics input on mobile webview (#1658) 2022-09-28 21:26:20 +05:30
StandardNotes CI f6d0aa0f0a chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
2022-09-28 15:16:53 +00:00
Aman Harwara eafa65e934 fix: grey flash on textarea focus on safari/webkit (#1661) 2022-09-28 20:11:03 +05:30
Mo a83003ee69 feat(crypto): pkc algos for key generation, encrypt, and decrypt (#1663) 2022-09-28 09:15:34 -05:00
StandardNotes CI 554f59a3fe chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
2022-09-28 13:42:14 +00:00
Aman Harwara c0af596f2d fix: sticky header behavior on iOS (#1662) 2022-09-28 18:36:42 +05:30
StandardNotes CI 6e10911d02 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-09-28 09:41:43 +00:00
Aman Harwara 9502a7fa32 fix: challenge modal 2022-09-28 14:30:31 +05:30
Aman Harwara 727d78cfe2 fix: webapp blanking because of mobileDevice check 2022-09-28 14:24:05 +05:30
StandardNotes CI 44bb4b7ca3 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-09-28 07:19:59 +00:00
Aman Harwara 981d8a7497 feat: handle android back button on android (#1656) 2022-09-28 12:12:55 +05:30
StandardNotes CI 04245dfeeb chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-09-27 14:51:15 +00:00
Vardan HakobyanandMo 5db47e83e8 feat: delete account (#1653)
* feat: delete account

* fix: copy adjustments

Co-authored-by: Mo
2022-09-27 18:13:54 +04:00
StandardNotes CI c08602b7b8 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-09-27 13:28:41 +00:00
Aman Harwara ec52e5684d feat: upgrade cta (#1655) 2022-09-27 18:29:50 +05:30
StandardNotes CI f3f7f9d362 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
2022-09-27 12:52:54 +00:00
Mo b9a07d8b5a fix(web): incorrect native access 2022-09-27 07:23:21 -05:00
StandardNotes CI 31213bf268 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
2022-09-27 11:20:27 +00:00
Aman Harwara 70e57e32b3 fix: don't hide note tags container on desktop 2022-09-27 16:19:06 +05:30
StandardNotes CI 720215eea7 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-09-27 07:23:11 +00:00
Aman Harwara 6d5ebdeaa1 fix: export/sharing notes on mobile webview (#1644) 2022-09-27 12:24:44 +05:30
StandardNotes CI c99693876f chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-09-26 16:21:50 +00:00
Aman Harwara f8f6a5212b feat: sticky header on mobile if plaintext note is scrolled (#1645) 2022-09-26 21:15:11 +05:30
StandardNotes CI 58cd919267 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-09-26 12:42:43 +00:00
Karol Sójko 507823dc4f fix(web): check for proper feature in the subscription sharing section 2022-09-26 14:12:01 +02:00
StandardNotes CI 6ea1def0cf chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-09-25 22:06:15 +00:00
Aman Harwara fe2ce9f1e8 feat: note tags button for mobile (#1641) 2022-09-26 02:58:55 +05:30
StandardNotes CI 4985787c5d chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
2022-09-25 16:08:44 +00:00
Aman Harwara 8614a638a4 fix(mobile): hide drawer on new mobile experience (#1642) 2022-09-25 21:04:04 +05:30
StandardNotes CI 15957c71cb chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].9
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
2022-09-24 21:45:51 +00:00
Aman Harwara 59e814bcec fix: improve new note title format (#1640) 2022-09-24 16:18:06 -05:00
StandardNotes CI 4cf9ebf52a chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
2022-09-24 19:32:22 +00:00
Aman Harwara a3611c802f fix: move prefs button next to account menu button in footer (#1639) 2022-09-25 00:33:30 +05:30
StandardNotes CI 9be315730d chore(release): publish
- @standardnotes/[email protected].7
 - @standardnotes/[email protected]
2022-09-24 17:28:41 +00:00
Mo 789a78bda3 fix(mobile): accurate version number 2022-09-24 11:58:29 -05:00
StandardNotes CI 73e170dedd chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-09-24 16:32:37 +00:00
Aman Harwara e4b1b64119 fix: responsiveness when resizing window on web/desktop (#1637) 2022-09-24 21:24:10 +05:30
StandardNotes CI c2a1918843 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-09-24 11:03:09 +00:00
Aman Harwara d3621d70b1 feat: new note title format w/ prefs (#1629) 2022-09-24 16:04:31 +05:30
StandardNotes CI bf286f4854 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-09-24 09:32:43 +00:00
Aman Harwara 97835e64ac feat: flatten quick settings menu and add separate prefs button (#1631) 2022-09-24 14:26:55 +05:30
146 changed files with 3712 additions and 948 deletions
Binary file not shown.
+26
View File
@@ -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 -1
View File
@@ -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
}
+128
View File
@@ -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 -1
View File
@@ -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,
+20
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/encryption",
"version": "1.15.4",
"version": "1.15.9",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+20
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/filepicker",
"version": "1.23.4",
"version": "1.23.9",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+20
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files",
"version": "1.10.4",
"version": "1.10.9",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+150
View File
@@ -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
+2 -2
View File
@@ -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()
}
}
+3 -7
View File
@@ -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}
+144 -3
View File
@@ -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,
},
)
}
}
+20 -5
View File
@@ -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
}
+24
View File
@@ -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 -1
View File
@@ -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
}
+136
View File
@@ -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 -1
View File
@@ -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",
+48
View File
@@ -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 -1
View File
@@ -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
}
+6
View File
@@ -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
+3 -2
View File
@@ -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'
+6
View File
@@ -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
+3 -1
View File
@@ -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": "*"
+62 -17
View File
@@ -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
*
+3 -16
View File
@@ -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'
+7 -4
View File
@@ -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,
+72 -143
View File
@@ -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)
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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')
})
+61
View File
@@ -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
+7 -1
View File
@@ -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)
+7 -37
View File
@@ -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)
+31
View 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
}
+5 -1
View File
@@ -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
+84 -2
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/snjs",
"version": "2.132.4",
"version": "2.134.7",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+58
View File
@@ -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 -1
View File
@@ -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) {
+18 -8
View File
@@ -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')
}
}
+189
View File
@@ -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
+2 -1
View File
@@ -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",
+10 -4
View File
@@ -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}
@@ -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}
@@ -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>
@@ -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 &gt; 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'
@@ -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