mirror of
https://github.com/standardnotes/app
synced 2026-09-13 00:45:26 -04:00
Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f705304681 | ||
|
|
f2e4763aa5 | ||
|
|
35efdbdab3 | ||
|
|
384d004b64 | ||
|
|
6b408a670d | ||
|
|
0055696c35 | ||
|
|
20787f0cc5 | ||
|
|
fa995656df | ||
|
|
f7b07c1e06 | ||
|
|
66aadbd60a | ||
|
|
efba7c682d | ||
|
|
cb92c10625 | ||
|
|
e3ecf364d3 | ||
|
|
b43a97f08e | ||
|
|
54ddd37052 | ||
|
|
51025de17f | ||
|
|
9b02378283 | ||
|
|
be64dcba83 | ||
|
|
6977b7c0f0 | ||
|
|
789ed47774 | ||
|
|
2338449425 | ||
|
|
d6840ba41c | ||
|
|
50c42eda50 | ||
|
|
ea3b65af04 | ||
|
|
c964740d2b | ||
|
|
f5b69691c0 | ||
|
|
54af28aa04 | ||
|
|
cf4d2196de | ||
|
|
abf14d1e50 | ||
|
|
86c00056bc | ||
|
|
f77a6a4480 | ||
|
|
98038da99f | ||
|
|
b9d7f36884 | ||
|
|
19831e834e | ||
|
|
7e50b864ef | ||
|
|
687668c8e2 | ||
|
|
d135e09bda | ||
|
|
f284111a0c | ||
|
|
be109a0f6b | ||
|
|
c08d9034e1 | ||
|
|
010864881d | ||
|
|
2b41c8337b | ||
|
|
45bcc0e0b1 | ||
|
|
d928477d82 | ||
|
|
b86aebb7ab | ||
|
|
3cce44f5ab | ||
|
|
ff80775cd6 | ||
|
|
c75f655ffe | ||
|
|
87cf17d02f | ||
|
|
0d17961570 | ||
|
|
63d84725d2 | ||
|
|
9583d0db44 | ||
|
|
fa4182ba9f | ||
|
|
259017bd39 | ||
|
|
f1f9070dcc | ||
|
|
6e6b5a017b | ||
|
|
e72648d8a0 | ||
|
|
fef887ec0d | ||
|
|
62713cc49d | ||
|
|
8016cb39e9 | ||
|
|
9259cc8f32 | ||
|
|
43d31d8bb4 | ||
|
|
092dc7957f | ||
|
|
77f8bf6368 | ||
|
|
1935c98cf0 | ||
|
|
c52030a525 |
@@ -88,11 +88,18 @@ jobs:
|
||||
- name: Compile for AppImage
|
||||
run: yarn run webpack --config desktop.webpack.prod.js
|
||||
- name: AppImage
|
||||
run: yarn run electron-builder --linux --x64 -c.linux.target=AppImage --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
run: |
|
||||
yarn run electron-builder --linux --x64 -c.linux.target=AppImage --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
|
||||
- name: AppImageX64
|
||||
run: yarn run electron-builder --linux --x64 -c.linux.target=AppImage --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
run: |
|
||||
yarn run electron-builder --linux --x64 -c.linux.target=AppImage --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
- name: AppImageArm64
|
||||
run: yarn run electron-builder --linux --arm64 -c.linux.target=AppImage --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
run: |
|
||||
yarn workspace @standardnotes/desktop rebuild:home-server --arch arm64
|
||||
yarn run electron-builder --linux --arm64 -c.linux.target=AppImage --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
env:
|
||||
npm_config_target_arch: 'arm64'
|
||||
|
||||
- name: Upload
|
||||
uses: actions/[email protected]
|
||||
@@ -138,6 +145,7 @@ jobs:
|
||||
yarn run electron-builder --linux --x64 -c.linux.target=dir --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
- name: DirArm64
|
||||
run: |
|
||||
yarn workspace @standardnotes/desktop rebuild:home-server --arch arm64
|
||||
yarn run webpack --config desktop.webpack.prod.js
|
||||
yarn run electron-builder --linux --arm64 -c.linux.target=dir --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
env:
|
||||
@@ -181,6 +189,7 @@ jobs:
|
||||
npm_config_target_arch: 'arm64'
|
||||
USE_SYSTEM_FPM: 'true'
|
||||
run: |
|
||||
yarn workspace @standardnotes/desktop rebuild:home-server --arch arm64
|
||||
yarn run webpack --config desktop.webpack.prod.js --env deb
|
||||
yarn run electron-builder --linux --arm64 -c.linux.target=deb --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
|
||||
@@ -317,7 +326,7 @@ jobs:
|
||||
|
||||
- name: Certificates Sync
|
||||
run: |
|
||||
smctl windows certsync
|
||||
smctl windows certsync
|
||||
shell: cmd
|
||||
|
||||
- name: Build & Sign With Electron Builder
|
||||
@@ -373,8 +382,8 @@ jobs:
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
token: ${{ secrets.CI_PAT_TOKEN }}
|
||||
tag_name: "@standardnotes/desktop@${{ env.APP_VERSION }}"
|
||||
name: "Desktop ${{ env.APP_VERSION }}"
|
||||
tag_name: '@standardnotes/desktop@${{ env.APP_VERSION }}'
|
||||
name: 'Desktop ${{ env.APP_VERSION }}'
|
||||
body: ${{ steps.release-notes.outputs.result }}
|
||||
prerelease: true
|
||||
draft: false
|
||||
@@ -395,5 +404,3 @@ jobs:
|
||||
token: ${{ secrets.CI_PAT_TOKEN }}
|
||||
repository: ${{ secrets.RELEASES_EVENT_RECEIVING_REPO }}
|
||||
event-type: releases-updated-event
|
||||
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/mobile
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "@standardnotes/app-monorepo",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"author": "Standard Notes.",
|
||||
"author": "Standard Notes",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/standardnotes/app"
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.26.94](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.26.93](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api",
|
||||
"version": "1.26.93",
|
||||
"version": "1.26.94",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -22,7 +22,9 @@ export class AuthApiService implements AuthApiServiceInterface {
|
||||
this.operationsInProgress = new Map()
|
||||
}
|
||||
|
||||
async generateRecoveryCodes(): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>> {
|
||||
async generateRecoveryCodes(dto: {
|
||||
serverPassword: string
|
||||
}): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>> {
|
||||
if (this.operationsInProgress.get(AuthApiOperations.GenerateRecoveryCodes)) {
|
||||
throw new ApiCallError(ErrorMessage.GenericInProgress)
|
||||
}
|
||||
@@ -30,7 +32,9 @@ export class AuthApiService implements AuthApiServiceInterface {
|
||||
this.operationsInProgress.set(AuthApiOperations.GenerateRecoveryCodes, true)
|
||||
|
||||
try {
|
||||
const response = await this.authServer.generateRecoveryCodes()
|
||||
const response = await this.authServer.generateRecoveryCodes({
|
||||
headers: [{ key: 'x-server-password', value: dto.serverPassword }],
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '../../Response'
|
||||
|
||||
export interface AuthApiServiceInterface {
|
||||
generateRecoveryCodes(): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>>
|
||||
generateRecoveryCodes(dto: { serverPassword: string }): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>>
|
||||
recoveryKeyParams(dto: {
|
||||
username: string
|
||||
codeChallenge: string
|
||||
|
||||
@@ -27,13 +27,19 @@ export class UserApiService implements UserApiServiceInterface {
|
||||
this.operationsInProgress = new Map()
|
||||
}
|
||||
|
||||
async deleteAccount(userUuid: string): Promise<HttpResponse<UserDeletionResponseBody>> {
|
||||
async deleteAccount(dto: {
|
||||
userUuid: string
|
||||
serverPassword: string
|
||||
}): Promise<HttpResponse<UserDeletionResponseBody>> {
|
||||
this.lockOperation(UserApiOperations.DeletingAccount)
|
||||
|
||||
try {
|
||||
const response = await this.userServer.deleteAccount({
|
||||
userUuid: userUuid,
|
||||
})
|
||||
const response = await this.userServer.deleteAccount(
|
||||
{
|
||||
userUuid: dto.userUuid,
|
||||
},
|
||||
{ headers: [{ key: 'x-server-password', value: dto.serverPassword }] },
|
||||
)
|
||||
|
||||
this.unlockOperation(UserApiOperations.DeletingAccount)
|
||||
|
||||
|
||||
@@ -22,5 +22,8 @@ export interface UserApiServiceInterface {
|
||||
requestType: UserRequestType
|
||||
}): Promise<HttpResponse<UserRequestResponseBody>>
|
||||
|
||||
deleteAccount(userUuid: string): Promise<HttpResponse<UserDeletionResponseBody>>
|
||||
deleteAccount(dto: {
|
||||
userUuid: string
|
||||
serverPassword: string | undefined
|
||||
}): Promise<HttpResponse<UserDeletionResponseBody>>
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
params,
|
||||
verb: HttpVerb.Get,
|
||||
authentication: options?.authentication ?? this.getSessionAccessToken(),
|
||||
customHeaders: options?.headers,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -123,6 +124,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
params,
|
||||
verb: HttpVerb.Put,
|
||||
authentication: options?.authentication ?? this.getSessionAccessToken(),
|
||||
customHeaders: options?.headers,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -141,6 +143,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
params,
|
||||
verb: HttpVerb.Delete,
|
||||
authentication: options?.authentication ?? this.getSessionAccessToken(),
|
||||
customHeaders: options?.headers,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface GenerateRecoveryCodesRequestParams {
|
||||
serverPassword: string
|
||||
}
|
||||
@@ -2,6 +2,7 @@ export * from './Authenticator/DeleteAuthenticatorRequestParams'
|
||||
export * from './Authenticator/GenerateAuthenticatorAuthenticationOptionsRequestParams'
|
||||
export * from './Authenticator/ListAuthenticatorsRequestParams'
|
||||
export * from './Authenticator/VerifyAuthenticatorRegistrationResponseRequestParams'
|
||||
export * from './Recovery/GenerateRecoveryCodesRequestParams'
|
||||
export * from './Recovery/RecoveryKeyParamsRequestParams'
|
||||
export * from './Recovery/SignInWithRecoveryCodesRequestParams'
|
||||
export * from './Revision/DeleteRevisionRequestParams'
|
||||
|
||||
@@ -8,12 +8,13 @@ import {
|
||||
} from '../../Response'
|
||||
import { AuthServerInterface } from './AuthServerInterface'
|
||||
import { Paths } from './Paths'
|
||||
import { HttpRequestOptions } from '../../Http/HttpRequestOptions'
|
||||
|
||||
export class AuthServer implements AuthServerInterface {
|
||||
constructor(private httpService: HttpServiceInterface) {}
|
||||
|
||||
async generateRecoveryCodes(): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>> {
|
||||
return this.httpService.post(Paths.v1.generateRecoveryCodes)
|
||||
async generateRecoveryCodes(options?: HttpRequestOptions): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>> {
|
||||
return this.httpService.post(Paths.v1.generateRecoveryCodes, undefined, options)
|
||||
}
|
||||
|
||||
async recoveryKeyParams(
|
||||
|
||||
@@ -5,9 +5,10 @@ import {
|
||||
RecoveryKeyParamsResponseBody,
|
||||
SignInWithRecoveryCodesResponseBody,
|
||||
} from '../../Response'
|
||||
import { HttpRequestOptions } from '../../Http/HttpRequestOptions'
|
||||
|
||||
export interface AuthServerInterface {
|
||||
generateRecoveryCodes(): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>>
|
||||
generateRecoveryCodes(options?: HttpRequestOptions): Promise<HttpResponse<GenerateRecoveryCodesResponseBody>>
|
||||
recoveryKeyParams(params: RecoveryKeyParamsRequestParams): Promise<HttpResponse<RecoveryKeyParamsResponseBody>>
|
||||
signInWithRecoveryCodes(
|
||||
params: SignInWithRecoveryCodesRequestParams,
|
||||
|
||||
@@ -8,12 +8,16 @@ import { UserRegistrationResponseBody } from '../../Response/User/UserRegistrati
|
||||
import { Paths } from './Paths'
|
||||
import { UserServerInterface } from './UserServerInterface'
|
||||
import { UserUpdateRequestParams } from '../../Request/User/UserUpdateRequestParams'
|
||||
import { HttpRequestOptions } from '../../Http/HttpRequestOptions'
|
||||
|
||||
export class UserServer implements UserServerInterface {
|
||||
constructor(private httpService: HttpServiceInterface) {}
|
||||
|
||||
async deleteAccount(params: UserDeletionRequestParams): Promise<HttpResponse<UserDeletionResponseBody>> {
|
||||
return this.httpService.delete(Paths.v1.deleteAccount(params.userUuid), params)
|
||||
async deleteAccount(
|
||||
params: UserDeletionRequestParams,
|
||||
options?: HttpRequestOptions,
|
||||
): Promise<HttpResponse<UserDeletionResponseBody>> {
|
||||
return this.httpService.delete(Paths.v1.deleteAccount(params.userUuid), params, options)
|
||||
}
|
||||
|
||||
async register(params: UserRegistrationRequestParams): Promise<HttpResponse<UserRegistrationResponseBody>> {
|
||||
|
||||
@@ -5,9 +5,13 @@ import { UserDeletionResponseBody } from '../../Response/User/UserDeletionRespon
|
||||
import { UserRegistrationResponseBody } from '../../Response/User/UserRegistrationResponseBody'
|
||||
import { UserUpdateResponse } from '../../Response/User/UserUpdateResponse'
|
||||
import { UserUpdateRequestParams } from '../../Request/User/UserUpdateRequestParams'
|
||||
import { HttpRequestOptions } from '../../Http/HttpRequestOptions'
|
||||
|
||||
export interface UserServerInterface {
|
||||
register(params: UserRegistrationRequestParams): Promise<HttpResponse<UserRegistrationResponseBody>>
|
||||
deleteAccount(params: UserDeletionRequestParams): Promise<HttpResponse<UserDeletionResponseBody>>
|
||||
deleteAccount(
|
||||
params: UserDeletionRequestParams,
|
||||
options?: HttpRequestOptions,
|
||||
): Promise<HttpResponse<UserDeletionResponseBody>>
|
||||
update(params: UserUpdateRequestParams): Promise<HttpResponse<UserUpdateResponse>>
|
||||
}
|
||||
|
||||
@@ -3,6 +3,86 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.1.552](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.551](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.550](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.549](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.548](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.547](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.546](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.545](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.544](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.543](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.542](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.541](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.540](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.539](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.538](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.537](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.536](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.535](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.534](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.533](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.532](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/clipper",
|
||||
"description": "Web clipper browser extension for Standard Notes",
|
||||
"version": "1.1.532",
|
||||
"version": "1.1.552",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build-mv2": "yarn clean && webpack --config ./webpack.config.prod.js",
|
||||
|
||||
@@ -3,6 +3,89 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.110.157](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.156](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.155](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.154](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.153](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.152](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.151](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.150](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.149](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.148](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.147](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.146](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixes issue where plaintext backups would use invalid special characters for filenames ([#2924](https://github.com/standardnotes/app/issues/2924)) ([45bcc0e](https://github.com/standardnotes/app/commit/45bcc0e0b1541558ecf410e9a92bc2db192cfe93))
|
||||
* Fixes portions of window not being draggable on desktop ([#2925](https://github.com/standardnotes/app/issues/2925)) ([2b41c83](https://github.com/standardnotes/app/commit/2b41c8337b1408209dc4903cacbc701b73c590b0))
|
||||
|
||||
## [3.110.145](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.144](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.143](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.142](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.141](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.140](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.139](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.138](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.137](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
@@ -17,6 +17,7 @@ import { FileReadOperation } from './FileReadOperation'
|
||||
import { Paths } from '../Types/Paths'
|
||||
import { MessageToWebApp } from '../../Shared/IpcMessages'
|
||||
import { FilesManagerInterface } from '../File/FilesManagerInterface'
|
||||
import { sanitizeFileName } from '@standardnotes/utils'
|
||||
|
||||
const TextBackupFileExtension = '.txt'
|
||||
|
||||
@@ -316,7 +317,8 @@ export class FilesBackupManager implements FileBackupsDevice {
|
||||
|
||||
const relativePath = forTag ?? ''
|
||||
const filenameWithSlashesEscaped = filename.replace(/\//g, '\u2215')
|
||||
const fileAbsolutePath = path.join(absolutePath, relativePath, filenameWithSlashesEscaped)
|
||||
const sanitizedFilename = sanitizeFileName(filenameWithSlashesEscaped)
|
||||
const fileAbsolutePath = path.join(absolutePath, relativePath, sanitizedFilename)
|
||||
await this.filesManager.writeFile(fileAbsolutePath, data)
|
||||
|
||||
const existingRecord = findMappingRecord(forTag)
|
||||
|
||||
@@ -70,12 +70,19 @@
|
||||
|
||||
/* https://github.com/electron/electron/issues/37789 */
|
||||
.mac-desktop #navigation,
|
||||
.mac-desktop #items-column,
|
||||
.mac-desktop #editor-column,
|
||||
.mac-desktop #items-title-bar,
|
||||
.mac-desktop #editor-title-bar,
|
||||
.mac-desktop [data-preferences-header] {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.mac-desktop #items-column > *:not(#items-title-bar),
|
||||
.mac-desktop #editor-column > *:not(#editor-title-bar) {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.mac-desktop.focus-mode #editor-title-bar {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/desktop",
|
||||
"main": "./app/dist/index.js",
|
||||
"version": "3.110.137",
|
||||
"version": "3.110.157",
|
||||
"license": "AGPL-3.0",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
@@ -55,7 +55,7 @@
|
||||
"@babel/core": "*",
|
||||
"@babel/preset-env": "*",
|
||||
"@electron/notarize": "^3.0.0",
|
||||
"@electron/rebuild": "3.6.0",
|
||||
"@electron/rebuild": "3.6.1",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/lodash": "^4.14.189",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
|
||||
@@ -3,6 +3,94 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.58.215](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.214](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.213](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.212](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixes spellcheck menu unreadable text on Android dark mode ([#2938](https://github.com/standardnotes/app/issues/2938)) ([cb92c10](https://github.com/standardnotes/app/commit/cb92c10625cb4ed89102a5e92459eab970627c30))
|
||||
|
||||
## [3.58.211](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.210](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.209](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.208](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.207](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.206](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.205](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.204](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.203](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.202](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.201](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.200](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.199](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.198](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.197](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.196](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.195](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove extra bottom padding on Android ([#2915](https://github.com/standardnotes/app/issues/2915)) ([c52030a](https://github.com/standardnotes/app/commit/c52030a525eaf3c8488c19beb84229911bcda8fc))
|
||||
|
||||
## [3.58.194](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Base application theme with DayNight support for proper dark mode colors. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:windowBackground">@color/background</item>
|
||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||
|
||||
@@ -38,8 +38,10 @@ newArchEnabled=true
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
|
||||
# Set AsyncStorage limit
|
||||
AsyncStorage_db_size_in_MB=50
|
||||
# Use Next storage implementation for AsyncStorage
|
||||
AsyncStorage_useNextStorage=true
|
||||
AsyncStorage_kotlinVersion=2.0.21
|
||||
AsyncStorage_next_kspVersion=2.0.21-1.0.28
|
||||
|
||||
# The URL of the server
|
||||
host=https://app.standardnotes.com
|
||||
|
||||
@@ -68,7 +68,7 @@ platform :ios do
|
||||
run_precheck_before_submit: false,
|
||||
submission_information: { add_id_info_uses_idfa: false },
|
||||
release_notes: {
|
||||
'default': 'Fixes and improvements.'
|
||||
'en-US': 'Fixes and improvements.'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@standardnotes/mobile",
|
||||
"version": "3.58.194",
|
||||
"version": "3.58.215",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"android-dev": "react-native run-android --mode devDebug --appIdSuffix dev",
|
||||
"android-prod-debug": "react-native run-android --variant prodDebug",
|
||||
"android-prod-release": "yarn run android:bundle && react-native run-android --variant prodRelease",
|
||||
"android-prod-debug": "react-native run-android --mode prodDebug",
|
||||
"android-prod-release": "yarn run android:bundle && react-native run-android --mode prodRelease",
|
||||
"android:bundle": "react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/build/intermediates/res/merged/release/",
|
||||
"build": "yarn bundle:web && yarn install:pods && yarn android:bundle",
|
||||
"clear-cache": "watchman watch-del-all && rm -rf $TMPDIR/react-native-packager-cache-* && rm -rf $TMPDIR/metro-bundler-cache-*",
|
||||
|
||||
@@ -3,6 +3,90 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.855](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.854](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.853](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.852](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.851](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.850](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.849](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.848](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.847](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.846](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.845](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.844](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.843](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.842](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.841](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.840](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.839](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.838](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.837](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.836](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.835](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.834](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/releases",
|
||||
"version": "1.4.834",
|
||||
"version": "1.4.855",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "dist/releases.json",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -3,6 +3,20 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.72.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-25)
|
||||
|
||||
### Features
|
||||
|
||||
* Added command palette for quick actions and switching between items ([#2933](https://github.com/standardnotes/app/issues/2933)) [skip e2e] ([efba7c6](https://github.com/standardnotes/app/commit/efba7c682d81d518d02ea52aea8c022f190679eb))
|
||||
|
||||
## [1.71.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.71.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.71.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/services",
|
||||
"version": "1.71.9",
|
||||
"version": "1.72.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AnyKeyParamsContent } from '@standardnotes/common'
|
||||
import { SessionBody } from '@standardnotes/responses'
|
||||
|
||||
export interface AuthClientInterface {
|
||||
generateRecoveryCodes(): Promise<string | false>
|
||||
generateRecoveryCodes(dto: { serverPassword: string }): Promise<string | false>
|
||||
recoveryKeyParams(dto: {
|
||||
username: string
|
||||
codeChallenge: string
|
||||
|
||||
@@ -14,9 +14,9 @@ export class AuthManager extends AbstractService implements AuthClientInterface
|
||||
super(internalEventBus)
|
||||
}
|
||||
|
||||
async generateRecoveryCodes(): Promise<string | false> {
|
||||
async generateRecoveryCodes(dto: { serverPassword: string }): Promise<string | false> {
|
||||
try {
|
||||
const result = await this.authApiService.generateRecoveryCodes()
|
||||
const result = await this.authApiService.generateRecoveryCodes(dto)
|
||||
|
||||
if (isErrorResponse(result)) {
|
||||
return false
|
||||
|
||||
@@ -128,6 +128,8 @@ export interface ItemManagerInterface extends AbstractService {
|
||||
getDisplayableFiles(): FileItem[]
|
||||
setVaultDisplayOptions(options: VaultDisplayOptions): void
|
||||
numberOfNotesWithConflicts(): number
|
||||
/** Returns all notes, files, tags and views */
|
||||
getInteractableItems(): DecryptedItemInterface[]
|
||||
getDisplayableNotes(): SNNote[]
|
||||
getDisplayableNotesAndFiles(): (SNNote | FileItem)[]
|
||||
setPrimaryItemDisplayOptions(options: NotesAndFilesDisplayControllerOptions): void
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export interface MfaServiceInterface {
|
||||
isMfaActivated(): Promise<boolean>
|
||||
generateMfaSecret(): Promise<string>
|
||||
getOtpToken(secret: string): Promise<string>
|
||||
enableMfa(secret: string, otpToken: string): Promise<void>
|
||||
disableMfa(): Promise<void>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ApplicationServiceInterface } from './../Service/ApplicationServiceInterface'
|
||||
import { DecryptedItem, DecryptedItemInterface, FileItem, SNNote } from '@standardnotes/models'
|
||||
import { ChallengeInterface, ChallengeReason } from '../Challenge'
|
||||
import { ChallengeInterface, ChallengeReason, ChallengeResponseInterface } from '../Challenge'
|
||||
import { MobileUnlockTiming } from './MobileUnlockTiming'
|
||||
import { TimingDisplayOption } from './TimingDisplayOption'
|
||||
import { ProtectionEvent } from './ProtectionEvent'
|
||||
@@ -28,6 +28,10 @@ export interface ProtectionsClientInterface extends ApplicationServiceInterface<
|
||||
reason: ChallengeReason,
|
||||
dto: { fallBackToAccountPassword: boolean; requireAccountPassword: boolean; forcePrompt: boolean },
|
||||
): Promise<boolean>
|
||||
authorizeActionWithChallengeResponse(
|
||||
reason: ChallengeReason,
|
||||
dto: { fallBackToAccountPassword: boolean; requireAccountPassword: boolean; forcePrompt: boolean },
|
||||
): Promise<{ success: boolean; challengeResponse?: ChallengeResponseInterface }>
|
||||
authorizeAddingPasscode(): Promise<boolean>
|
||||
authorizeRemovingPasscode(): Promise<boolean>
|
||||
authorizeChangingPasscode(): Promise<boolean>
|
||||
@@ -36,7 +40,8 @@ export interface ProtectionsClientInterface extends ApplicationServiceInterface<
|
||||
authorizeAutolockIntervalChange(): Promise<boolean>
|
||||
authorizeSearchingProtectedNotesText(): Promise<boolean>
|
||||
authorizeBackupCreation(): Promise<boolean>
|
||||
authorizeMfaDisable(): Promise<boolean>
|
||||
authorizeMfaDisable(): Promise<{ success: boolean; challengeResponse?: ChallengeResponseInterface }>
|
||||
authorizeAccountDeletion(): Promise<{ success: boolean; challengeResponse?: ChallengeResponseInterface }>
|
||||
|
||||
protectItems<I extends DecryptedItemInterface>(items: I[]): Promise<I[]>
|
||||
unprotectItems<I extends DecryptedItemInterface>(items: I[], reason: ChallengeReason): Promise<I[] | undefined>
|
||||
|
||||
@@ -237,13 +237,9 @@ export class UserService
|
||||
error: boolean
|
||||
message?: string
|
||||
}> {
|
||||
if (
|
||||
!(await this.protections.authorizeAction(ChallengeReason.DeleteAccount, {
|
||||
fallBackToAccountPassword: true,
|
||||
requireAccountPassword: true,
|
||||
forcePrompt: false,
|
||||
}))
|
||||
) {
|
||||
const { success, challengeResponse } = await this.protections.authorizeAccountDeletion()
|
||||
|
||||
if (!success) {
|
||||
return {
|
||||
error: true,
|
||||
message: Messages.INVALID_PASSWORD,
|
||||
@@ -251,7 +247,13 @@ export class UserService
|
||||
}
|
||||
|
||||
const uuid = this.sessions.getSureUser().uuid
|
||||
const response = await this.userApi.deleteAccount(uuid)
|
||||
const password = challengeResponse?.getValueForType(ChallengeValidation.AccountPassword).value as string
|
||||
const currentRootKey = await this.encryption.computeRootKey(
|
||||
password,
|
||||
this.encryption.getRootKeyParams() as SNRootKeyParams,
|
||||
)
|
||||
const serverPassword = currentRootKey.serverPassword
|
||||
const response = await this.userApi.deleteAccount({ userUuid: uuid, serverPassword: serverPassword })
|
||||
if (isErrorResponse(response)) {
|
||||
return {
|
||||
error: true,
|
||||
|
||||
@@ -3,6 +3,20 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.210.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-25)
|
||||
|
||||
### Features
|
||||
|
||||
* Added command palette for quick actions and switching between items ([#2933](https://github.com/standardnotes/app/issues/2933)) [skip e2e] ([efba7c6](https://github.com/standardnotes/app/commit/efba7c682d81d518d02ea52aea8c022f190679eb))
|
||||
|
||||
## [2.209.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.209.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.209.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
@@ -1026,6 +1026,7 @@ export class Dependencies {
|
||||
return new GetRecoveryCodes(
|
||||
this.get<AuthManager>(TYPES.AuthManager),
|
||||
this.get<SettingsService>(TYPES.SettingsService),
|
||||
this.get<EncryptionService>(TYPES.EncryptionService),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1228,9 +1229,9 @@ export class Dependencies {
|
||||
this.factory.set(TYPES.MfaService, () => {
|
||||
return new MfaService(
|
||||
this.get<SettingsService>(TYPES.SettingsService),
|
||||
this.get<PureCryptoInterface>(TYPES.Crypto),
|
||||
this.get<FeaturesService>(TYPES.FeaturesService),
|
||||
this.get<ProtectionsClientInterface>(TYPES.ProtectionService),
|
||||
this.get<EncryptionService>(TYPES.EncryptionService),
|
||||
this.get<InternalEventBus>(TYPES.InternalEventBus),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AuthClientInterface } from '@standardnotes/services'
|
||||
import { AuthClientInterface, EncryptionService } from '@standardnotes/services'
|
||||
import { SettingsClientInterface } from '@Lib/Services/Settings/SettingsClientInterface'
|
||||
|
||||
import { GetRecoveryCodes } from './GetRecoveryCodes'
|
||||
@@ -6,8 +6,9 @@ import { GetRecoveryCodes } from './GetRecoveryCodes'
|
||||
describe('GetRecoveryCodes', () => {
|
||||
let authClient: AuthClientInterface
|
||||
let settingsClient: SettingsClientInterface
|
||||
let encryption: EncryptionService
|
||||
|
||||
const createUseCase = () => new GetRecoveryCodes(authClient, settingsClient)
|
||||
const createUseCase = () => new GetRecoveryCodes(authClient, settingsClient, encryption)
|
||||
|
||||
beforeEach(() => {
|
||||
authClient = {} as jest.Mocked<AuthClientInterface>
|
||||
@@ -15,12 +16,16 @@ describe('GetRecoveryCodes', () => {
|
||||
|
||||
settingsClient = {} as jest.Mocked<SettingsClientInterface>
|
||||
settingsClient.getSetting = jest.fn().mockResolvedValue('existing-recovery-codes')
|
||||
|
||||
encryption = {} as jest.Mocked<EncryptionService>
|
||||
encryption.computeRootKey = jest.fn().mockResolvedValue({ serverPassword: 'test-server-password' })
|
||||
encryption.getRootKeyParams = jest.fn().mockReturnValue({ algorithm: 'test-algorithm' })
|
||||
})
|
||||
|
||||
it('should return existing recovery code if they exist', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute()
|
||||
const result = await useCase.execute({ password: 'test-password' })
|
||||
|
||||
expect(result.getValue()).toBe('existing-recovery-codes')
|
||||
})
|
||||
@@ -30,7 +35,7 @@ describe('GetRecoveryCodes', () => {
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute()
|
||||
const result = await useCase.execute({ password: 'test-password' })
|
||||
|
||||
expect(result.getValue()).toBe('recovery-codes')
|
||||
})
|
||||
@@ -41,7 +46,7 @@ describe('GetRecoveryCodes', () => {
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute()
|
||||
const result = await useCase.execute({ password: 'test-password' })
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
import { AuthClientInterface } from '@standardnotes/services'
|
||||
import { AuthClientInterface, EncryptionService } from '@standardnotes/services'
|
||||
import { Result, SettingName, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
|
||||
import { SettingsClientInterface } from '@Lib/Services/Settings/SettingsClientInterface'
|
||||
import { GetRecoveryCodesDTO } from './GetRecoveryCodesDTO'
|
||||
import { SNRootKeyParams } from '@standardnotes/encryption'
|
||||
|
||||
export class GetRecoveryCodes implements UseCaseInterface<string> {
|
||||
constructor(
|
||||
private authClient: AuthClientInterface,
|
||||
private settingsClient: SettingsClientInterface,
|
||||
private encryption: EncryptionService,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Result<string>> {
|
||||
async execute(dto: GetRecoveryCodesDTO): Promise<Result<string>> {
|
||||
if (!dto.password) {
|
||||
return Result.fail('Password is required to get recovery code.')
|
||||
}
|
||||
const currentRootKey = await this.encryption.computeRootKey(
|
||||
dto.password,
|
||||
this.encryption.getRootKeyParams() as SNRootKeyParams,
|
||||
)
|
||||
const serverPassword = currentRootKey.serverPassword
|
||||
|
||||
if (!serverPassword) {
|
||||
return Result.fail('Could not compute server password')
|
||||
}
|
||||
|
||||
const existingRecoveryCodes = await this.settingsClient.getSetting(
|
||||
SettingName.create(SettingName.NAMES.RecoveryCodes).getValue(),
|
||||
serverPassword,
|
||||
)
|
||||
if (existingRecoveryCodes !== undefined) {
|
||||
return Result.ok(existingRecoveryCodes)
|
||||
}
|
||||
|
||||
const generatedRecoveryCodes = await this.authClient.generateRecoveryCodes()
|
||||
const generatedRecoveryCodes = await this.authClient.generateRecoveryCodes({ serverPassword })
|
||||
if (generatedRecoveryCodes === false) {
|
||||
return Result.fail('Could not generate recovery code')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface GetRecoveryCodesDTO {
|
||||
password?: string
|
||||
}
|
||||
@@ -77,7 +77,7 @@ import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { Paths } from './Paths'
|
||||
import { DiskStorageService } from '../Storage/DiskStorageService'
|
||||
import { UuidString } from '../../Types/UuidString'
|
||||
import { SettingsServerInterface } from '../Settings/SettingsServerInterface'
|
||||
import { SettingsServerInterface, MfaSecretResponse } from '../Settings/SettingsServerInterface'
|
||||
import { Strings } from '@Lib/Strings'
|
||||
import { AnyFeatureDescription } from '@standardnotes/features'
|
||||
|
||||
@@ -563,11 +563,13 @@ export class LegacyApiService
|
||||
settingName: string,
|
||||
settingValue: string | null,
|
||||
sensitive: boolean,
|
||||
totpToken?: string,
|
||||
): Promise<HttpResponse<UpdateSettingResponse>> {
|
||||
const params = {
|
||||
name: settingName,
|
||||
value: settingValue,
|
||||
sensitive: sensitive,
|
||||
...(totpToken && { totpToken }),
|
||||
}
|
||||
return this.tokenRefreshableRequest<UpdateSettingResponse>({
|
||||
verb: HttpVerb.Put,
|
||||
@@ -578,12 +580,18 @@ export class LegacyApiService
|
||||
})
|
||||
}
|
||||
|
||||
async getSetting(userUuid: UuidString, settingName: string): Promise<HttpResponse<GetSettingResponse>> {
|
||||
async getSetting(
|
||||
userUuid: UuidString,
|
||||
settingName: string,
|
||||
serverPassword?: string,
|
||||
): Promise<HttpResponse<GetSettingResponse>> {
|
||||
const customHeaders = serverPassword ? [{ key: 'x-server-password', value: serverPassword }] : undefined
|
||||
return await this.tokenRefreshableRequest<GetSettingResponse>({
|
||||
verb: HttpVerb.Get,
|
||||
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName.toLowerCase())),
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
||||
customHeaders,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -616,12 +624,27 @@ export class LegacyApiService
|
||||
})
|
||||
}
|
||||
|
||||
async deleteSetting(userUuid: UuidString, settingName: string): Promise<HttpResponse<DeleteSettingResponse>> {
|
||||
async deleteSetting(
|
||||
userUuid: UuidString,
|
||||
settingName: string,
|
||||
serverPassword?: string,
|
||||
): Promise<HttpResponse<DeleteSettingResponse>> {
|
||||
const customHeaders = serverPassword ? [{ key: 'x-server-password', value: serverPassword }] : undefined
|
||||
return this.tokenRefreshableRequest<DeleteSettingResponse>({
|
||||
verb: HttpVerb.Delete,
|
||||
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName)),
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_UPDATE_SETTINGS,
|
||||
customHeaders,
|
||||
})
|
||||
}
|
||||
|
||||
async getMfaSecret(userUuid: UuidString): Promise<HttpResponse<MfaSecretResponse>> {
|
||||
return this.tokenRefreshableRequest<MfaSecretResponse>({
|
||||
verb: HttpVerb.Get,
|
||||
url: joinPaths(this.host, Paths.v1.mfaSecret(userUuid)),
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: 'Failed to get MFA secret.',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ const ItemsPaths = {
|
||||
const SettingsPaths = {
|
||||
settings: (userUuid: string) => `/v1/users/${userUuid}/settings`,
|
||||
setting: (userUuid: string, settingName: string) => `/v1/users/${userUuid}/settings/${settingName}`,
|
||||
mfaSecret: (userUuid: string) => `/v1/users/${userUuid}/mfa-secret`,
|
||||
subscriptionSetting: (userUuid: string, settingName: string) =>
|
||||
`/v1/users/${userUuid}/subscription-settings/${settingName}`,
|
||||
subscriptionSettings: (userUuid: string) => `/v1/users/${userUuid}/subscription-settings`,
|
||||
|
||||
@@ -220,6 +220,17 @@ export class ItemManager extends Services.AbstractService implements Services.It
|
||||
this.itemCounter.setVaultDisplayOptions(options)
|
||||
}
|
||||
|
||||
public getInteractableItems(): Models.DecryptedItemInterface[] {
|
||||
return (this.systemSmartViews as Models.DecryptedItemInterface[]).concat(
|
||||
this.collection.all([
|
||||
ContentType.TYPES.Note,
|
||||
ContentType.TYPES.File,
|
||||
ContentType.TYPES.Tag,
|
||||
ContentType.TYPES.SmartView,
|
||||
]) as Models.DecryptedItemInterface[],
|
||||
)
|
||||
}
|
||||
|
||||
public getDisplayableNotes(): Models.SNNote[] {
|
||||
assert(this.navigationDisplayController.contentTypes.length === 2)
|
||||
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
import { SettingsService } from '../Settings'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { FeaturesService } from '../Features/FeaturesService'
|
||||
import {
|
||||
AbstractService,
|
||||
InternalEventBusInterface,
|
||||
MfaServiceInterface,
|
||||
ProtectionsClientInterface,
|
||||
SignInStrings,
|
||||
EncryptionService,
|
||||
ChallengeValidation,
|
||||
} from '@standardnotes/services'
|
||||
import { SettingName } from '@standardnotes/domain-core'
|
||||
import { SNRootKeyParams } from '@standardnotes/encryption'
|
||||
|
||||
export class MfaService extends AbstractService implements MfaServiceInterface {
|
||||
constructor(
|
||||
private settingsService: SettingsService,
|
||||
private crypto: PureCryptoInterface,
|
||||
private featuresService: FeaturesService,
|
||||
private protections: ProtectionsClientInterface,
|
||||
private encryption: EncryptionService,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
}
|
||||
|
||||
private async saveMfaSetting(secret: string): Promise<void> {
|
||||
return await this.settingsService.updateSetting(
|
||||
SettingName.create(SettingName.NAMES.MfaSecret).getValue(),
|
||||
secret,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
async isMfaActivated(): Promise<boolean> {
|
||||
const mfaSetting = await this.settingsService.getDoesSensitiveSettingExist(
|
||||
SettingName.create(SettingName.NAMES.MfaSecret).getValue(),
|
||||
@@ -37,34 +30,35 @@ export class MfaService extends AbstractService implements MfaServiceInterface {
|
||||
}
|
||||
|
||||
async generateMfaSecret(): Promise<string> {
|
||||
return this.crypto.generateOtpSecret()
|
||||
}
|
||||
|
||||
async getOtpToken(secret: string): Promise<string> {
|
||||
return this.crypto.totpToken(secret, Date.now(), 6, 30)
|
||||
return this.settingsService.generateMfaSecret()
|
||||
}
|
||||
|
||||
async enableMfa(secret: string, otpToken: string): Promise<void> {
|
||||
const otpTokenValid = otpToken != undefined && otpToken === (await this.getOtpToken(secret))
|
||||
|
||||
if (!otpTokenValid) {
|
||||
throw new Error(SignInStrings.IncorrectMfa)
|
||||
}
|
||||
|
||||
return this.saveMfaSetting(secret)
|
||||
return this.settingsService.updateMfaSetting(secret, otpToken)
|
||||
}
|
||||
|
||||
async disableMfa(): Promise<void> {
|
||||
if (!(await this.protections.authorizeMfaDisable())) {
|
||||
const { success, challengeResponse } = await this.protections.authorizeMfaDisable()
|
||||
|
||||
if (!success) {
|
||||
return
|
||||
}
|
||||
|
||||
return await this.settingsService.deleteSetting(SettingName.create(SettingName.NAMES.MfaSecret).getValue())
|
||||
const password = challengeResponse?.getValueForType(ChallengeValidation.AccountPassword).value as string
|
||||
const currentRootKey = await this.encryption.computeRootKey(
|
||||
password,
|
||||
this.encryption.getRootKeyParams() as SNRootKeyParams,
|
||||
)
|
||||
const serverPassword = currentRootKey.serverPassword
|
||||
|
||||
return await this.settingsService.deleteSetting(
|
||||
SettingName.create(SettingName.NAMES.MfaSecret).getValue(),
|
||||
serverPassword,
|
||||
)
|
||||
}
|
||||
|
||||
override deinit(): void {
|
||||
;(this.settingsService as unknown) = undefined
|
||||
;(this.crypto as unknown) = undefined
|
||||
;(this.featuresService as unknown) = undefined
|
||||
super.deinit()
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { isValidProtectionSessionLength } from './isValidProtectionSessionLength'
|
||||
import { UnprotectedAccessSecondsDuration } from './UnprotectedAccessSecondsDuration'
|
||||
import { ChallengeResponse } from '../Challenge'
|
||||
|
||||
/**
|
||||
* Enforces certain actions to require extra authentication,
|
||||
@@ -246,11 +247,11 @@ export class ProtectionService
|
||||
})
|
||||
}
|
||||
|
||||
async authorizeMfaDisable(): Promise<boolean> {
|
||||
return this.authorizeAction(ChallengeReason.DisableMfa, {
|
||||
async authorizeMfaDisable(): Promise<{ success: boolean; challengeResponse?: ChallengeResponse }> {
|
||||
return this.authorizeActionWithChallengeResponse(ChallengeReason.DisableMfa, {
|
||||
fallBackToAccountPassword: true,
|
||||
requireAccountPassword: true,
|
||||
forcePrompt: false,
|
||||
forcePrompt: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -278,6 +279,14 @@ export class ProtectionService
|
||||
})
|
||||
}
|
||||
|
||||
async authorizeAccountDeletion(): Promise<{ success: boolean; challengeResponse?: ChallengeResponse }> {
|
||||
return this.authorizeActionWithChallengeResponse(ChallengeReason.DeleteAccount, {
|
||||
fallBackToAccountPassword: true,
|
||||
requireAccountPassword: true,
|
||||
forcePrompt: true,
|
||||
})
|
||||
}
|
||||
|
||||
async authorizeAction(
|
||||
reason: ChallengeReason,
|
||||
dto: { fallBackToAccountPassword: boolean; requireAccountPassword: boolean; forcePrompt: boolean },
|
||||
@@ -285,6 +294,13 @@ export class ProtectionService
|
||||
return this.validateOrRenewSession(reason, dto)
|
||||
}
|
||||
|
||||
async authorizeActionWithChallengeResponse(
|
||||
reason: ChallengeReason,
|
||||
dto: { fallBackToAccountPassword: boolean; requireAccountPassword: boolean; forcePrompt: boolean },
|
||||
): Promise<{ success: boolean; challengeResponse?: ChallengeResponse }> {
|
||||
return this.validateOrRenewSessionWithChallengeResponse(reason, dto)
|
||||
}
|
||||
|
||||
getMobilePasscodeTimingOptions(): TimingDisplayOption[] {
|
||||
return [
|
||||
{
|
||||
@@ -353,8 +369,20 @@ export class ProtectionService
|
||||
reason: ChallengeReason,
|
||||
{ fallBackToAccountPassword = true, requireAccountPassword = false, forcePrompt = false } = {},
|
||||
): Promise<boolean> {
|
||||
const response = await this.validateOrRenewSessionWithChallengeResponse(reason, {
|
||||
fallBackToAccountPassword,
|
||||
requireAccountPassword,
|
||||
forcePrompt,
|
||||
})
|
||||
return response.success
|
||||
}
|
||||
|
||||
private async validateOrRenewSessionWithChallengeResponse(
|
||||
reason: ChallengeReason,
|
||||
{ fallBackToAccountPassword = true, requireAccountPassword = false, forcePrompt = false } = {},
|
||||
): Promise<{ success: boolean; challengeResponse?: ChallengeResponse }> {
|
||||
if (this.getSessionExpiryDate() > new Date() && !forcePrompt) {
|
||||
return true
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
const prompts: ChallengePrompt[] = []
|
||||
@@ -378,9 +406,10 @@ export class ProtectionService
|
||||
if (fallBackToAccountPassword && this.encryption.hasAccount()) {
|
||||
prompts.push(new ChallengePrompt(ChallengeValidation.AccountPassword))
|
||||
} else {
|
||||
return true
|
||||
return { success: true }
|
||||
}
|
||||
}
|
||||
|
||||
const lastSessionLength = this.getLastSessionLength()
|
||||
const chosenSessionLength = isValidProtectionSessionLength(lastSessionLength)
|
||||
? lastSessionLength
|
||||
@@ -407,9 +436,9 @@ export class ProtectionService
|
||||
} else {
|
||||
this.setSessionLength(length as UnprotectedAccessSecondsDuration)
|
||||
}
|
||||
return true
|
||||
return { success: true, challengeResponse: response }
|
||||
} else {
|
||||
return false
|
||||
return { success: false }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ export class SettingsService extends AbstractService implements SettingsClientIn
|
||||
return this.provider.listSettings()
|
||||
}
|
||||
|
||||
async getSetting(name: SettingName) {
|
||||
return this.provider.getSetting(name)
|
||||
async getSetting(name: SettingName, serverPassword?: string) {
|
||||
return this.provider.getSetting(name, serverPassword)
|
||||
}
|
||||
|
||||
async getSubscriptionSetting(name: SettingName) {
|
||||
@@ -42,16 +42,29 @@ export class SettingsService extends AbstractService implements SettingsClientIn
|
||||
return this.provider.updateSubscriptionSetting(name, payload, sensitive)
|
||||
}
|
||||
|
||||
async updateSetting(name: SettingName, payload: string, sensitive = false) {
|
||||
return this.provider.updateSetting(name, payload, sensitive)
|
||||
async updateSetting(name: SettingName, payload: string, sensitive = false, totpToken?: string) {
|
||||
return this.provider.updateSetting(name, payload, sensitive, totpToken)
|
||||
}
|
||||
|
||||
async getDoesSensitiveSettingExist(name: SettingName) {
|
||||
return this.provider.getDoesSensitiveSettingExist(name)
|
||||
}
|
||||
|
||||
async deleteSetting(name: SettingName) {
|
||||
return this.provider.deleteSetting(name)
|
||||
async deleteSetting(name: SettingName, serverPassword?: string) {
|
||||
return this.provider.deleteSetting(name, serverPassword)
|
||||
}
|
||||
|
||||
async generateMfaSecret(): Promise<string> {
|
||||
return this.provider.getMfaSecret()
|
||||
}
|
||||
|
||||
async updateMfaSetting(secret: string, totpToken: string): Promise<void> {
|
||||
return this.provider.updateSetting(
|
||||
SettingName.create(SettingName.NAMES.MfaSecret).getValue(),
|
||||
secret,
|
||||
true,
|
||||
totpToken,
|
||||
)
|
||||
}
|
||||
|
||||
getEmailBackupFrequencyOptionLabel(frequency: EmailBackupFrequency): string {
|
||||
|
||||
@@ -5,13 +5,17 @@ import { SettingName } from '@standardnotes/domain-core'
|
||||
export interface SettingsClientInterface {
|
||||
listSettings(): Promise<SettingsList>
|
||||
|
||||
getSetting(name: SettingName): Promise<string | undefined>
|
||||
getSetting(name: SettingName, serverPassword?: string): Promise<string | undefined>
|
||||
|
||||
getDoesSensitiveSettingExist(name: SettingName): Promise<boolean>
|
||||
|
||||
updateSetting(name: SettingName, payload: string, sensitive?: boolean): Promise<void>
|
||||
updateSetting(name: SettingName, payload: string, sensitive?: boolean, totpToken?: string): Promise<void>
|
||||
|
||||
deleteSetting(name: SettingName): Promise<void>
|
||||
deleteSetting(name: SettingName, serverPassword?: string): Promise<void>
|
||||
|
||||
generateMfaSecret(): Promise<string>
|
||||
|
||||
updateMfaSetting(secret: string, totpToken: string): Promise<void>
|
||||
|
||||
getEmailBackupFrequencyOptionLabel(frequency: EmailBackupFrequency): string
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@ export class SettingsGateway {
|
||||
return settings
|
||||
}
|
||||
|
||||
async getSetting(name: SettingName): Promise<string | undefined> {
|
||||
const response = await this.settingsApi.getSetting(this.userUuid, name.value)
|
||||
async getSetting(name: SettingName, serverPassword?: string): Promise<string | undefined> {
|
||||
const response = await this.settingsApi.getSetting(this.userUuid, name.value, serverPassword)
|
||||
|
||||
if (response.status === HttpStatusCode.BadRequest) {
|
||||
return undefined
|
||||
@@ -102,20 +102,28 @@ export class SettingsGateway {
|
||||
return response.data?.success ?? false
|
||||
}
|
||||
|
||||
async updateSetting(name: SettingName, payload: string, sensitive: boolean): Promise<void> {
|
||||
const response = await this.settingsApi.updateSetting(this.userUuid, name.value, payload, sensitive)
|
||||
async updateSetting(name: SettingName, payload: string, sensitive: boolean, totpToken?: string): Promise<void> {
|
||||
const response = await this.settingsApi.updateSetting(this.userUuid, name.value, payload, sensitive, totpToken)
|
||||
if (isErrorResponse(response)) {
|
||||
throw new Error(getErrorFromErrorResponse(response).message)
|
||||
}
|
||||
}
|
||||
|
||||
async deleteSetting(name: SettingName): Promise<void> {
|
||||
const response = await this.settingsApi.deleteSetting(this.userUuid, name.value)
|
||||
async deleteSetting(name: SettingName, serverPassword?: string): Promise<void> {
|
||||
const response = await this.settingsApi.deleteSetting(this.userUuid, name.value, serverPassword)
|
||||
if (isErrorResponse(response)) {
|
||||
throw new Error(getErrorFromErrorResponse(response).message)
|
||||
}
|
||||
}
|
||||
|
||||
async getMfaSecret(): Promise<string> {
|
||||
const response = await this.settingsApi.getMfaSecret(this.userUuid)
|
||||
if (isErrorResponse(response)) {
|
||||
throw new Error(getErrorFromErrorResponse(response).message)
|
||||
}
|
||||
return response.data.secret
|
||||
}
|
||||
|
||||
deinit() {
|
||||
;(this.settingsApi as unknown) = undefined
|
||||
;(this.userProvider as unknown) = undefined
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
} from '@standardnotes/responses'
|
||||
import { UuidString } from '@Lib/Types/UuidString'
|
||||
|
||||
export interface MfaSecretResponse {
|
||||
secret: string
|
||||
}
|
||||
|
||||
export interface SettingsServerInterface {
|
||||
listSettings(userUuid: UuidString): Promise<HttpResponse<ListSettingsResponse>>
|
||||
|
||||
@@ -15,9 +19,14 @@ export interface SettingsServerInterface {
|
||||
settingName: string,
|
||||
settingValue: string,
|
||||
sensitive: boolean,
|
||||
totpToken?: string,
|
||||
): Promise<HttpResponse<UpdateSettingResponse>>
|
||||
|
||||
getSetting(userUuid: UuidString, settingName: string): Promise<HttpResponse<GetSettingResponse>>
|
||||
getSetting(
|
||||
userUuid: UuidString,
|
||||
settingName: string,
|
||||
serverPassword?: string,
|
||||
): Promise<HttpResponse<GetSettingResponse>>
|
||||
|
||||
getSubscriptionSetting(userUuid: UuidString, settingName: string): Promise<HttpResponse<GetSettingResponse>>
|
||||
|
||||
@@ -28,5 +37,11 @@ export interface SettingsServerInterface {
|
||||
sensitive: boolean,
|
||||
): Promise<HttpResponse<UpdateSettingResponse>>
|
||||
|
||||
deleteSetting(userUuid: UuidString, settingName: string): Promise<HttpResponse<DeleteSettingResponse>>
|
||||
getMfaSecret(userUuid: UuidString): Promise<HttpResponse<MfaSecretResponse>>
|
||||
|
||||
deleteSetting(
|
||||
userUuid: UuidString,
|
||||
settingName: string,
|
||||
serverPassword?: string,
|
||||
): Promise<HttpResponse<DeleteSettingResponse>>
|
||||
}
|
||||
|
||||
@@ -549,6 +549,24 @@ describe('basic auth', function () {
|
||||
expect(sendChallengeSpy.callCount).to.equal(1)
|
||||
}).timeout(Factory.TenSecondTimeout)
|
||||
|
||||
it('should send server password when deleting account', async function () {
|
||||
Factory.handlePasswordChallenges(context.application, context.password)
|
||||
|
||||
const userApiService = context.application.dependencies.get(TYPES.UserApiService)
|
||||
const deleteAccountSpy = sinon.spy(userApiService, 'deleteAccount')
|
||||
|
||||
await context.application.user.deleteAccount()
|
||||
|
||||
expect(deleteAccountSpy.callCount).to.equal(1)
|
||||
const deleteAccountCall = deleteAccountSpy.getCall(0)
|
||||
const callArgs = deleteAccountCall.args[0]
|
||||
|
||||
expect(callArgs).to.have.property('serverPassword')
|
||||
expect(callArgs.serverPassword).to.not.be.undefined
|
||||
expect(typeof callArgs.serverPassword).to.equal('string')
|
||||
expect(callArgs.serverPassword.length).to.be.above(0)
|
||||
}).timeout(Factory.TenSecondTimeout)
|
||||
|
||||
it('deleting account should sign out current user', async function () {
|
||||
Factory.handlePasswordChallenges(context.application, context.password)
|
||||
|
||||
@@ -567,12 +585,40 @@ describe('basic auth', function () {
|
||||
|
||||
const response = await context.application.dependencies
|
||||
.get(TYPES.UserApiService)
|
||||
.deleteAccount(registerResponse.user.uuid)
|
||||
.deleteAccount({
|
||||
userUuid: registerResponse.user.uuid,
|
||||
serverPassword: 'dummy-password'
|
||||
})
|
||||
|
||||
expect(response.status).to.equal(401)
|
||||
expect(response.data.error.message).to.equal('Operation not allowed.')
|
||||
|
||||
await secondContext.deinit()
|
||||
})
|
||||
|
||||
it('should not allow deleting account if server password is not sent', async function () {
|
||||
Factory.handlePasswordChallenges(context.application, context.password)
|
||||
|
||||
const response = await context.application.dependencies
|
||||
.get(TYPES.UserApiService)
|
||||
.deleteAccount({
|
||||
userUuid: context.application.user.uuid,
|
||||
})
|
||||
|
||||
expect(response.status).to.equal(400)
|
||||
}).timeout(Factory.TenSecondTimeout)
|
||||
|
||||
it('should not allow deleting account if server password is incorrect', async function () {
|
||||
Factory.handlePasswordChallenges(context.application, context.password)
|
||||
|
||||
const response = await context.application.dependencies
|
||||
.get(TYPES.UserApiService)
|
||||
.deleteAccount({
|
||||
userUuid: context.application.user.uuid,
|
||||
serverPassword: 'wrong-password'
|
||||
})
|
||||
|
||||
expect(response.status).to.equal(400)
|
||||
}).timeout(Factory.TenSecondTimeout)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -65,6 +65,7 @@ describe('mfa service', () => {
|
||||
const token = await application.mfa.getOtpToken(secret)
|
||||
|
||||
sinon.spy(application.challenges, 'sendChallenge')
|
||||
|
||||
await application.mfa.enableMfa(secret, token)
|
||||
await application.mfa.disableMfa()
|
||||
|
||||
@@ -73,4 +74,64 @@ describe('mfa service', () => {
|
||||
expect(challenge.prompts).to.have.lengthOf(2)
|
||||
expect(challenge.prompts[0].validation).to.equal(ChallengeValidation.AccountPassword)
|
||||
}).timeout(Factory.TenSecondTimeout)
|
||||
|
||||
it('sends server password when disabling mfa', async () => {
|
||||
await registerApp(application)
|
||||
|
||||
Factory.handlePasswordChallenges(application, accountPassword)
|
||||
const secret = await application.mfa.generateMfaSecret()
|
||||
const token = await application.mfa.getOtpToken(secret)
|
||||
|
||||
await application.mfa.enableMfa(secret, token)
|
||||
|
||||
sinon.spy(application.settings.settingsApi, 'deleteSetting')
|
||||
|
||||
await application.mfa.disableMfa()
|
||||
|
||||
const deleteSettingCall = application.settings.settingsApi.deleteSetting.getCall(0)
|
||||
const [serverPassword] = deleteSettingCall.args
|
||||
expect(typeof serverPassword).to.equal('string')
|
||||
expect(serverPassword.length).to.be.above(0)
|
||||
}).timeout(Factory.TenSecondTimeout)
|
||||
|
||||
it('should not allow disabling mfa if server password is not sent', async function () {
|
||||
await registerApp(application)
|
||||
|
||||
Factory.handlePasswordChallenges(application, accountPassword)
|
||||
|
||||
const secret = await application.mfa.generateMfaSecret()
|
||||
const token = await application.mfa.getOtpToken(secret)
|
||||
|
||||
await application.mfa.enableMfa(secret, token)
|
||||
|
||||
const response = await application.dependencies
|
||||
.get(TYPES.SettingsApiService)
|
||||
.deleteSetting({
|
||||
userUuid: application.user.uuid,
|
||||
settingName: 'MFA_SECRET',
|
||||
})
|
||||
|
||||
expect(response.status).to.equal(400)
|
||||
}).timeout(Factory.TenSecondTimeout)
|
||||
|
||||
it('should not allow disabling mfa if server password is incorrect', async function () {
|
||||
await registerApp(application)
|
||||
|
||||
Factory.handlePasswordChallenges(application, accountPassword)
|
||||
|
||||
const secret = await application.mfa.generateMfaSecret()
|
||||
const token = await application.mfa.getOtpToken(secret)
|
||||
|
||||
await application.mfa.enableMfa(secret, token)
|
||||
|
||||
const response = await application.dependencies
|
||||
.get(TYPES.SettingsApiService)
|
||||
.deleteSetting({
|
||||
userUuid: application.user.uuid,
|
||||
settingName: 'MFA_SECRET',
|
||||
serverPassword: 'wrong-password'
|
||||
})
|
||||
|
||||
expect(response.status).to.equal(400)
|
||||
}).timeout(Factory.TenSecondTimeout)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/snjs",
|
||||
"version": "2.209.12",
|
||||
"version": "2.210.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.39.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-25)
|
||||
|
||||
### Features
|
||||
|
||||
* Added command palette for quick actions and switching between items ([#2933](https://github.com/standardnotes/app/issues/2933)) [skip e2e] ([efba7c6](https://github.com/standardnotes/app/commit/efba7c682d81d518d02ea52aea8c022f190679eb))
|
||||
|
||||
## [1.38.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixes Google Keep comverter failing if date format in heading is invalid ([#2935](https://github.com/standardnotes/app/issues/2935)) ([6977b7c](https://github.com/standardnotes/app/commit/6977b7c0f0a62e385e12905d463ad5d6453d7247))
|
||||
|
||||
## [1.38.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.38.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.38.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/ui-services",
|
||||
"version": "1.38.11",
|
||||
"version": "1.39.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -78,7 +78,8 @@ export class GoogleKeepConverter implements Converter {
|
||||
rootElement.innerHTML = data
|
||||
|
||||
const headingElement = rootElement.getElementsByClassName('heading')[0]
|
||||
const date = new Date(headingElement?.textContent || '')
|
||||
const parsedDate = new Date(headingElement?.textContent || '')
|
||||
const date = parsedDate instanceof Date && !isNaN(parsedDate.getTime()) ? parsedDate : new Date()
|
||||
headingElement?.remove()
|
||||
|
||||
const contentElement = rootElement.getElementsByClassName('content')[0]
|
||||
|
||||
@@ -41,3 +41,4 @@ export const OPEN_PREFERENCES_COMMAND = createKeyboardCommand('OPEN_PREFERENCES_
|
||||
export const CHANGE_EDITOR_WIDTH_COMMAND = createKeyboardCommand('CHANGE_EDITOR_WIDTH_COMMAND')
|
||||
|
||||
export const TOGGLE_KEYBOARD_SHORTCUTS_MODAL = createKeyboardCommand('TOGGLE_KEYBOARD_SHORTCUTS_MODAL')
|
||||
export const TOGGLE_COMMAND_PALETTE = createKeyboardCommand('TOGGLE_COMMAND_PALETTE')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Environment, Platform } from '@standardnotes/snjs'
|
||||
import { Environment, Platform, UuidGenerator } from '@standardnotes/snjs'
|
||||
import { eventMatchesKeyAndModifiers } from './eventMatchesKeyAndModifiers'
|
||||
import { KeyboardCommand } from './KeyboardCommands'
|
||||
import { KeyboardKeyEvent } from './KeyboardKeyEvent'
|
||||
@@ -28,6 +28,20 @@ export class KeyboardService {
|
||||
}
|
||||
}
|
||||
|
||||
private isDisabled = false
|
||||
/**
|
||||
* When called, the service will stop triggering command handlers
|
||||
* on keydown/keyup events. Useful when you need to handle events
|
||||
* yourself while keeping the rest of behaviours inert.
|
||||
* Make sure to call {@link enableEventHandling} once done.
|
||||
*/
|
||||
public disableEventHandling() {
|
||||
this.isDisabled = true
|
||||
}
|
||||
public enableEventHandling() {
|
||||
this.isDisabled = false
|
||||
}
|
||||
|
||||
get isMac() {
|
||||
return this.platform === Platform.MacDesktop || this.platform === Platform.MacWeb
|
||||
}
|
||||
@@ -116,6 +130,9 @@ export class KeyboardService {
|
||||
}
|
||||
|
||||
private handleKeyboardEvent(event: KeyboardEvent, keyEvent: KeyboardKeyEvent): void {
|
||||
if (this.isDisabled) {
|
||||
return
|
||||
}
|
||||
for (const command of this.commandMap.keys()) {
|
||||
const shortcut = this.commandMap.get(command)
|
||||
if (!shortcut) {
|
||||
@@ -243,6 +260,7 @@ export class KeyboardService {
|
||||
...shortcut,
|
||||
category: handler.category,
|
||||
description: handler.description,
|
||||
id: UuidGenerator.GenerateUuid(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,11 +268,12 @@ export class KeyboardService {
|
||||
* Register help item for a keyboard shortcut that is handled outside of the KeyboardService,
|
||||
* for example by a library like Lexical.
|
||||
*/
|
||||
registerExternalKeyboardShortcutHelpItem(item: KeyboardShortcutHelpItem): () => void {
|
||||
this.keyboardShortcutHelpItems.add(item)
|
||||
registerExternalKeyboardShortcutHelpItem(item: Omit<KeyboardShortcutHelpItem, 'id'>): () => void {
|
||||
const itemWithId = { ...item, id: UuidGenerator.GenerateUuid() }
|
||||
this.keyboardShortcutHelpItems.add(itemWithId)
|
||||
|
||||
return () => {
|
||||
this.keyboardShortcutHelpItems.delete(item)
|
||||
this.keyboardShortcutHelpItems.delete(itemWithId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +281,7 @@ export class KeyboardService {
|
||||
* Register help item for a keyboard shortcut that is handled outside of the KeyboardService,
|
||||
* for example by a library like Lexical.
|
||||
*/
|
||||
registerExternalKeyboardShortcutHelpItems(items: KeyboardShortcutHelpItem[]): () => void {
|
||||
registerExternalKeyboardShortcutHelpItems(items: Omit<KeyboardShortcutHelpItem, 'id'>[]): () => void {
|
||||
const disposers = items.map((item) => this.registerExternalKeyboardShortcutHelpItem(item))
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -21,8 +21,9 @@ export type PlatformedKeyboardShortcut = KeyboardShortcut & {
|
||||
|
||||
export type KeyboardShortcutCategory = 'General' | 'Notes list' | 'Current note' | 'Super notes' | 'Formatting'
|
||||
|
||||
export type KeyboardShortcutHelpItem = Omit<PlatformedKeyboardShortcut, 'command'> & {
|
||||
export interface KeyboardShortcutHelpItem extends Omit<PlatformedKeyboardShortcut, 'command'> {
|
||||
command?: KeyboardCommand
|
||||
category: KeyboardShortcutCategory
|
||||
description: string
|
||||
id: string
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
CHANGE_EDITOR_WIDTH_COMMAND,
|
||||
SUPER_TOGGLE_TOOLBAR,
|
||||
TOGGLE_KEYBOARD_SHORTCUTS_MODAL,
|
||||
TOGGLE_COMMAND_PALETTE,
|
||||
} from './KeyboardCommands'
|
||||
import { KeyboardKey } from './KeyboardKey'
|
||||
import { KeyboardModifier, getPrimaryModifier } from './KeyboardModifier'
|
||||
@@ -108,7 +109,7 @@ export function getKeyboardShortcuts(platform: Platform, _environment: Environme
|
||||
},
|
||||
{
|
||||
command: CHANGE_EDITOR_COMMAND,
|
||||
key: '/',
|
||||
key: '?',
|
||||
modifiers: [primaryModifier, KeyboardModifier.Shift],
|
||||
preventDefault: true,
|
||||
},
|
||||
@@ -200,5 +201,10 @@ export function getKeyboardShortcuts(platform: Platform, _environment: Environme
|
||||
key: '/',
|
||||
modifiers: [primaryModifier],
|
||||
},
|
||||
{
|
||||
command: TOGGLE_COMMAND_PALETTE,
|
||||
code: 'Semicolon',
|
||||
modifiers: [primaryModifier, KeyboardModifier.Shift],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export function keyboardCharacterForKeyOrCode(keyOrCode: string) {
|
||||
export function keyboardCharacterForKeyOrCode(keyOrCode: string, shiftKey = false) {
|
||||
if (keyOrCode.startsWith('Digit')) {
|
||||
return keyOrCode.replace('Digit', '')
|
||||
}
|
||||
@@ -14,6 +14,8 @@ export function keyboardCharacterForKeyOrCode(keyOrCode: string) {
|
||||
return '←'
|
||||
case 'ArrowRight':
|
||||
return '→'
|
||||
case 'Semicolon':
|
||||
return shiftKey ? ':' : ';'
|
||||
default:
|
||||
return keyOrCode
|
||||
}
|
||||
|
||||
@@ -3,6 +3,107 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.200.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.200.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixes rendering of non-Latin alphabet characters in PDF export ([#2937](https://github.com/standardnotes/app/issues/2937)) ([6b408a6](https://github.com/standardnotes/app/commit/6b408a670d05f32ebb101fb129ebcb728f11dc2d))
|
||||
* Fixes Use recovery code option being shown on account creation ([#2939](https://github.com/standardnotes/app/issues/2939)) ([0055696](https://github.com/standardnotes/app/commit/0055696c35f6016eee88ce14cfabe0e13a4e6aa6))
|
||||
|
||||
## [3.200.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
# [3.200.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-25)
|
||||
|
||||
### Features
|
||||
|
||||
* Added command palette for quick actions and switching between items ([#2933](https://github.com/standardnotes/app/issues/2933)) [skip e2e] ([efba7c6](https://github.com/standardnotes/app/commit/efba7c682d81d518d02ea52aea8c022f190679eb))
|
||||
|
||||
# [3.199.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-23)
|
||||
|
||||
### Features
|
||||
|
||||
* Improves Add Tag dropdown UI ([#2936](https://github.com/standardnotes/app/issues/2936)) ([54ddd37](https://github.com/standardnotes/app/commit/54ddd37052c202c808d0e3bef8bd0df7fc72b148))
|
||||
|
||||
## [3.198.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.198.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.198.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.198.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.198.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.198.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixes Share note option not working on Android for note titles with special characters ([#2928](https://github.com/standardnotes/app/issues/2928)) ([d135e09](https://github.com/standardnotes/app/commit/d135e09bda39814585a3d76b0544ed8b5834b4d0))
|
||||
|
||||
## [3.198.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixes Back and Navigation buttons not working on mobile view when panels explicitly collapsed ([#2926](https://github.com/standardnotes/app/issues/2926)) ([0108648](https://github.com/standardnotes/app/commit/010864881d0c056e88dae2394fb398c0f1e822a5))
|
||||
|
||||
## [3.198.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Prevents quick type suggestions on iOS for password input ([ff80775](https://github.com/standardnotes/app/commit/ff80775cd6f09863429f053e590a054c6e45722c))
|
||||
|
||||
## [3.198.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed font size not updating on plain editor when changing it from Preferences ([#2920](https://github.com/standardnotes/app/issues/2920)) ([0d17961](https://github.com/standardnotes/app/commit/0d17961570cdadbbb909709075bdef3b8d77434b))
|
||||
* Fixes extra bottom padding in account modal ([63d8472](https://github.com/standardnotes/app/commit/63d84725d2084d014de6aa73e74ba77a7e343484))
|
||||
* Fixes quick type suggestions showing up in iOS for password field ([9583d0d](https://github.com/standardnotes/app/commit/9583d0db44ba315e125fbc36b50a8da527ae3a89))
|
||||
|
||||
## [3.198.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.198.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.198.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.198.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.198.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed issues with embedded linked items in Super editor ([#2918](https://github.com/standardnotes/app/issues/2918)) ([43d31d8](https://github.com/standardnotes/app/commit/43d31d8bb44644b0d78d8a7f543da790b30e1eea))
|
||||
|
||||
## [3.198.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove extra bottom padding on Android ([#2915](https://github.com/standardnotes/app/issues/2915)) ([c52030a](https://github.com/standardnotes/app/commit/c52030a525eaf3c8488c19beb84229911bcda8fc))
|
||||
|
||||
## [3.198.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,5 +1,258 @@
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"version": "3.200.3",
|
||||
"title": "[3.200.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-29)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.200.2",
|
||||
"title": "[3.200.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-29)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Fixes rendering of non-Latin alphabet characters in PDF export ([#2937](https://github.com/standardnotes/app/issues/2937)) ([6b408a6](https://github.com/standardnotes/app/commit/6b408a670d05f32ebb101fb129ebcb728f11dc2d))\n* Fixes Use recovery code option being shown on account creation ([#2939](https://github.com/standardnotes/app/issues/2939)) ([0055696](https://github.com/standardnotes/app/commit/0055696c35f6016eee88ce14cfabe0e13a4e6aa6))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Fixes rendering of non-Latin alphabet characters in PDF export (#2937) (6b408a6)",
|
||||
"Fixes Use recovery code option being shown on account creation (#2939) (0055696)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Fixes rendering of non-Latin alphabet characters in PDF export (#2937) (6b408a6)",
|
||||
"Fixes Use recovery code option being shown on account creation (#2939) (0055696)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.200.1",
|
||||
"title": "[3.200.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-26)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.200.0",
|
||||
"title": "[3.200.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-25)",
|
||||
"date": null,
|
||||
"body": "### Features\n\n* Added command palette for quick actions and switching between items ([#2933](https://github.com/standardnotes/app/issues/2933)) [skip e2e] ([efba7c6](https://github.com/standardnotes/app/commit/efba7c682d81d518d02ea52aea8c022f190679eb))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Added command palette for quick actions and switching between items (#2933) skip e2e] ([efba7c6)"
|
||||
],
|
||||
"Features": [
|
||||
"Added command palette for quick actions and switching between items (#2933) skip e2e] ([efba7c6)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.199.0",
|
||||
"title": "[3.199.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-23)",
|
||||
"date": null,
|
||||
"body": "### Features\n\n* Improves Add Tag dropdown UI ([#2936](https://github.com/standardnotes/app/issues/2936)) ([54ddd37](https://github.com/standardnotes/app/commit/54ddd37052c202c808d0e3bef8bd0df7fc72b148))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Improves Add Tag dropdown UI (#2936) (54ddd37)"
|
||||
],
|
||||
"Features": [
|
||||
"Improves Add Tag dropdown UI (#2936) (54ddd37)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.20",
|
||||
"title": "[3.198.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-15)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.19",
|
||||
"title": "[3.198.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-12)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.18",
|
||||
"title": "[3.198.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-04)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.17",
|
||||
"title": "[3.198.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-26)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.16",
|
||||
"title": "[3.198.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-25)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.15",
|
||||
"title": "[3.198.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-11)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Fixes Share note option not working on Android for note titles with special characters ([#2928](https://github.com/standardnotes/app/issues/2928)) ([d135e09](https://github.com/standardnotes/app/commit/d135e09bda39814585a3d76b0544ed8b5834b4d0))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Fixes Share note option not working on Android for note titles with special characters (#2928) (d135e09)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Fixes Share note option not working on Android for note titles with special characters (#2928) (d135e09)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.14",
|
||||
"title": "[3.198.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-05)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Fixes Back and Navigation buttons not working on mobile view when panels explicitly collapsed ([#2926](https://github.com/standardnotes/app/issues/2926)) ([0108648](https://github.com/standardnotes/app/commit/010864881d0c056e88dae2394fb398c0f1e822a5))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Fixes Back and Navigation buttons not working on mobile view when panels explicitly collapsed (#2926) (0108648)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Fixes Back and Navigation buttons not working on mobile view when panels explicitly collapsed (#2926) (0108648)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.13",
|
||||
"title": "[3.198.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-04)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Prevents quick type suggestions on iOS for password input ([ff80775](https://github.com/standardnotes/app/commit/ff80775cd6f09863429f053e590a054c6e45722c))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Prevents quick type suggestions on iOS for password input (ff80775)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Prevents quick type suggestions on iOS for password input (ff80775)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.12",
|
||||
"title": "[3.198.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-29)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Fixed font size not updating on plain editor when changing it from Preferences ([#2920](https://github.com/standardnotes/app/issues/2920)) ([0d17961](https://github.com/standardnotes/app/commit/0d17961570cdadbbb909709075bdef3b8d77434b))\n* Fixes extra bottom padding in account modal ([63d8472](https://github.com/standardnotes/app/commit/63d84725d2084d014de6aa73e74ba77a7e343484))\n* Fixes quick type suggestions showing up in iOS for password field ([9583d0d](https://github.com/standardnotes/app/commit/9583d0db44ba315e125fbc36b50a8da527ae3a89))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Fixed font size not updating on plain editor when changing it from Preferences (#2920) (0d17961)",
|
||||
"Fixes extra bottom padding in account modal (63d8472)",
|
||||
"Fixes quick type suggestions showing up in iOS for password field (9583d0d)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Fixed font size not updating on plain editor when changing it from Preferences (#2920) (0d17961)",
|
||||
"Fixes extra bottom padding in account modal (63d8472)",
|
||||
"Fixes quick type suggestions showing up in iOS for password field (9583d0d)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.11",
|
||||
"title": "[3.198.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-23)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.10",
|
||||
"title": "[3.198.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.9",
|
||||
"title": "[3.198.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.8",
|
||||
"title": "[3.198.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.7",
|
||||
"title": "[3.198.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Fixed issues with embedded linked items in Super editor ([#2918](https://github.com/standardnotes/app/issues/2918)) ([43d31d8](https://github.com/standardnotes/app/commit/43d31d8bb44644b0d78d8a7f543da790b30e1eea))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Fixed issues with embedded linked items in Super editor (#2918) (43d31d8)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Fixed issues with embedded linked items in Super editor (#2918) (43d31d8)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.6",
|
||||
"title": "[3.198.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-19)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Remove extra bottom padding on Android ([#2915](https://github.com/standardnotes/app/issues/2915)) ([c52030a](https://github.com/standardnotes/app/commit/c52030a525eaf3c8488c19beb84229911bcda8fc))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Remove extra bottom padding on Android (#2915) (c52030a)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Remove extra bottom padding on Android (#2915) (c52030a)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.5",
|
||||
"title": "[3.198.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-15)",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@standardnotes/web",
|
||||
"version": "3.198.5",
|
||||
"version": "3.200.3",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "dist/app.js",
|
||||
"author": "Standard Notes.",
|
||||
"author": "Standard Notes",
|
||||
"private": true,
|
||||
"files": [
|
||||
"dist"
|
||||
@@ -107,7 +107,7 @@
|
||||
"app/**/*.{js,ts,jsx,tsx,css,md}": "prettier --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ariakit/react": "^0.3.9",
|
||||
"@ariakit/react": "^0.4.18",
|
||||
"@lexical/clipboard": "0.32.1",
|
||||
"@lexical/headless": "0.32.1",
|
||||
"@lexical/link": "0.32.1",
|
||||
|
||||
@@ -9,6 +9,7 @@ export const Web_TYPES = {
|
||||
Importer: Symbol.for('Importer'),
|
||||
ItemGroupController: Symbol.for('ItemGroupController'),
|
||||
KeyboardService: Symbol.for('KeyboardService'),
|
||||
CommandService: Symbol.for('CommandService'),
|
||||
MobileWebReceiver: Symbol.for('MobileWebReceiver'),
|
||||
MomentsService: Symbol.for('MomentsService'),
|
||||
PersistenceService: Symbol.for('PersistenceService'),
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
ThemeManager,
|
||||
ToastService,
|
||||
VaultDisplayService,
|
||||
WebApplicationInterface,
|
||||
} from '@standardnotes/ui-services'
|
||||
import { DependencyContainer } from '@standardnotes/utils'
|
||||
import { Web_TYPES } from './Types'
|
||||
@@ -50,9 +49,11 @@ import { LoadPurchaseFlowUrl } from '../UseCase/LoadPurchaseFlowUrl'
|
||||
import { GetPurchaseFlowUrl } from '../UseCase/GetPurchaseFlowUrl'
|
||||
import { OpenSubscriptionDashboard } from '../UseCase/OpenSubscriptionDashboard'
|
||||
import { HeadlessSuperConverter } from '@/Components/SuperEditor/Tools/HeadlessSuperConverter'
|
||||
import { WebApplication } from '../WebApplication'
|
||||
import { CommandService } from '../../Components/CommandPalette/CommandService'
|
||||
|
||||
export class WebDependencies extends DependencyContainer {
|
||||
constructor(private application: WebApplicationInterface) {
|
||||
constructor(private application: WebApplication) {
|
||||
super()
|
||||
|
||||
this.bind(Web_TYPES.SuperConverter, () => {
|
||||
@@ -124,6 +125,9 @@ export class WebDependencies extends DependencyContainer {
|
||||
this.bind(Web_TYPES.KeyboardService, () => {
|
||||
return new KeyboardService(application.platform, application.environment)
|
||||
})
|
||||
this.bind(Web_TYPES.CommandService, () => {
|
||||
return new CommandService(this.get<KeyboardService>(Web_TYPES.KeyboardService), application.generateUuid)
|
||||
})
|
||||
|
||||
this.bind(Web_TYPES.ArchiveManager, () => {
|
||||
return new ArchiveManager(application)
|
||||
@@ -199,6 +203,7 @@ export class WebDependencies extends DependencyContainer {
|
||||
return new PaneController(
|
||||
application.preferences,
|
||||
this.get<KeyboardService>(Web_TYPES.KeyboardService),
|
||||
application.commands,
|
||||
this.get<IsTabletOrMobileScreen>(Web_TYPES.IsTabletOrMobileScreen),
|
||||
this.get<PanesForLayout>(Web_TYPES.PanesForLayout),
|
||||
application.events,
|
||||
@@ -233,7 +238,7 @@ export class WebDependencies extends DependencyContainer {
|
||||
return new NavigationController(
|
||||
this.get<FeaturesController>(Web_TYPES.FeaturesController),
|
||||
this.get<VaultDisplayService>(Web_TYPES.VaultDisplayService),
|
||||
this.get<KeyboardService>(Web_TYPES.KeyboardService),
|
||||
this.get<CommandService>(Web_TYPES.CommandService),
|
||||
this.get<PaneController>(Web_TYPES.PaneController),
|
||||
application.sync,
|
||||
application.mutator,
|
||||
@@ -241,25 +246,16 @@ export class WebDependencies extends DependencyContainer {
|
||||
application.preferences,
|
||||
application.alerts,
|
||||
application.changeAndSaveItem,
|
||||
application.recents,
|
||||
application.events,
|
||||
)
|
||||
})
|
||||
|
||||
this.bind(Web_TYPES.NotesController, () => {
|
||||
return new NotesController(
|
||||
this.get<ItemListController>(Web_TYPES.ItemListController),
|
||||
this.get<NavigationController>(Web_TYPES.NavigationController),
|
||||
this.get<ItemGroupController>(Web_TYPES.ItemGroupController),
|
||||
this.get<KeyboardService>(Web_TYPES.KeyboardService),
|
||||
application.preferences,
|
||||
application.items,
|
||||
application.mutator,
|
||||
application.sync,
|
||||
application.protections,
|
||||
application.alerts,
|
||||
application,
|
||||
this.get<IsGlobalSpellcheckEnabled>(Web_TYPES.IsGlobalSpellcheckEnabled),
|
||||
this.get<GetItemTags>(Web_TYPES.GetItemTags),
|
||||
application.events,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -304,6 +300,7 @@ export class WebDependencies extends DependencyContainer {
|
||||
application.options,
|
||||
this.get<IsNativeMobileWeb>(Web_TYPES.IsNativeMobileWeb),
|
||||
application.changeAndSaveItem,
|
||||
application.recents,
|
||||
application.events,
|
||||
)
|
||||
})
|
||||
@@ -374,6 +371,7 @@ export class WebDependencies extends DependencyContainer {
|
||||
application.platform,
|
||||
application.mobileDevice,
|
||||
this.get<IsNativeMobileWeb>(Web_TYPES.IsNativeMobileWeb),
|
||||
application.recents,
|
||||
application.events,
|
||||
)
|
||||
})
|
||||
@@ -381,7 +379,7 @@ export class WebDependencies extends DependencyContainer {
|
||||
this.bind(Web_TYPES.HistoryModalController, () => {
|
||||
return new HistoryModalController(
|
||||
this.get<NotesController>(Web_TYPES.NotesController),
|
||||
this.get<KeyboardService>(Web_TYPES.KeyboardService),
|
||||
this.get<CommandService>(Web_TYPES.CommandService),
|
||||
application.events,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
const MaxCommands = 5
|
||||
const MaxItems = 10
|
||||
|
||||
export class RecentActionsState {
|
||||
#commandUuids: string[] = []
|
||||
#itemUuids: string[] = []
|
||||
|
||||
/**
|
||||
* Recently used commands, most recent at the start
|
||||
*/
|
||||
get commandUuids() {
|
||||
return this.#commandUuids
|
||||
}
|
||||
/**
|
||||
* Recently opened items, most recent at the start
|
||||
*/
|
||||
get itemUuids() {
|
||||
return this.#itemUuids
|
||||
}
|
||||
|
||||
add(id: string, action_type: 'item' | 'command' = 'item') {
|
||||
const action_array = action_type === 'item' ? this.#itemUuids : this.#commandUuids
|
||||
const existing = action_array.findIndex((uuid) => uuid === id)
|
||||
if (existing !== -1) {
|
||||
action_array.splice(existing, 1)
|
||||
}
|
||||
const max = action_type === 'item' ? MaxItems : MaxCommands
|
||||
if (action_array.length == max) {
|
||||
action_array.pop()
|
||||
}
|
||||
action_array.unshift(id)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,19 @@ import { Environment, namespacedKey, Platform, RawStorageKey, SNLog } from '@sta
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { WebOrDesktopDevice } from './Device/WebOrDesktopDevice'
|
||||
|
||||
jest.mock('@standardnotes/sncrypto-web', () => {
|
||||
return {
|
||||
SNWebCrypto: class {
|
||||
initialize() {
|
||||
return Promise.resolve()
|
||||
}
|
||||
generateUUID() {
|
||||
return 'mock-uuid'
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('web application', () => {
|
||||
let application: WebApplication
|
||||
|
||||
|
||||
@@ -80,6 +80,8 @@ import { SearchOptionsController } from '@/Controllers/SearchOptionsController'
|
||||
import { PersistenceService } from '@/Controllers/Abstract/PersistenceService'
|
||||
import { removeFromArray } from '@standardnotes/utils'
|
||||
import { FileItemActionType } from '@/Components/AttachedFilesPopover/PopoverFileItemAction'
|
||||
import { RecentActionsState } from './Recents'
|
||||
import { CommandService } from '../Components/CommandPalette/CommandService'
|
||||
|
||||
export type WebEventObserver = (event: WebAppEvent, data?: unknown) => void
|
||||
|
||||
@@ -95,6 +97,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
public isSessionsModalVisible = false
|
||||
|
||||
public devMode?: DevMode
|
||||
public recents = new RecentActionsState()
|
||||
|
||||
constructor(
|
||||
deviceInterface: WebOrDesktopDevice,
|
||||
@@ -597,6 +600,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
return this.deps.get<KeyboardService>(Web_TYPES.KeyboardService)
|
||||
}
|
||||
|
||||
get commands(): CommandService {
|
||||
return this.deps.get<CommandService>(Web_TYPES.CommandService)
|
||||
}
|
||||
|
||||
get featuresController(): FeaturesController {
|
||||
return this.deps.get<FeaturesController>(Web_TYPES.FeaturesController)
|
||||
}
|
||||
|
||||
@@ -170,15 +170,17 @@ const AdvancedOptions: FunctionComponent<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<Checkbox
|
||||
name="recovery-codes"
|
||||
label="Use recovery code"
|
||||
checked={isRecoveryCodes}
|
||||
disabled={disabled}
|
||||
onChange={handleIsRecoveryCodesChange}
|
||||
/>
|
||||
</div>
|
||||
{onRecoveryCodesChange && (
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<Checkbox
|
||||
name="recovery-codes"
|
||||
label="Use recovery code"
|
||||
checked={isRecoveryCodes}
|
||||
disabled={disabled}
|
||||
onChange={handleIsRecoveryCodesChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isRecoveryCodes && (
|
||||
<>
|
||||
|
||||
@@ -13,7 +13,7 @@ import Spinner from '@/Components/Spinner/Spinner'
|
||||
import { MenuItemIconSize } from '@/Constants/TailwindClassNames'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import MenuSection from '../Menu/MenuSection'
|
||||
import { TOGGLE_KEYBOARD_SHORTCUTS_MODAL, isMobilePlatform } from '@standardnotes/ui-services'
|
||||
import { TOGGLE_COMMAND_PALETTE, TOGGLE_KEYBOARD_SHORTCUTS_MODAL, isMobilePlatform } from '@standardnotes/ui-services'
|
||||
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
|
||||
|
||||
type Props = {
|
||||
@@ -95,6 +95,9 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
|
||||
const keyboardShortcutsHelpShortcut = useMemo(() => {
|
||||
return application.keyboardService.keyboardShortcutForCommand(TOGGLE_KEYBOARD_SHORTCUTS_MODAL)
|
||||
}, [application.keyboardService])
|
||||
const commandPaletteShortcut = useMemo(() => {
|
||||
return application.keyboardService.keyboardShortcutForCommand(TOGGLE_COMMAND_PALETTE)
|
||||
}, [application.keyboardService])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -194,17 +197,30 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
|
||||
<span className="text-neutral">v{application.version}</span>
|
||||
</MenuItem>
|
||||
{!isMobilePlatform(application.platform) && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
application.keyboardService.triggerCommand(TOGGLE_KEYBOARD_SHORTCUTS_MODAL)
|
||||
}}
|
||||
>
|
||||
<Icon type="keyboard" className={iconClassName} />
|
||||
Keyboard shortcuts
|
||||
{keyboardShortcutsHelpShortcut && (
|
||||
<KeyboardShortcutIndicator shortcut={keyboardShortcutsHelpShortcut} className="ml-auto" />
|
||||
)}
|
||||
</MenuItem>
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
application.keyboardService.triggerCommand(TOGGLE_KEYBOARD_SHORTCUTS_MODAL)
|
||||
}}
|
||||
>
|
||||
<Icon type="keyboard" className={iconClassName} />
|
||||
Keyboard shortcuts
|
||||
{keyboardShortcutsHelpShortcut && (
|
||||
<KeyboardShortcutIndicator shortcut={keyboardShortcutsHelpShortcut} className="ml-auto" />
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
application.keyboardService.triggerCommand(TOGGLE_COMMAND_PALETTE)
|
||||
}}
|
||||
>
|
||||
<Icon type="info" className={iconClassName} />
|
||||
Command palette
|
||||
{commandPaletteShortcut && (
|
||||
<KeyboardShortcutIndicator shortcut={commandPaletteShortcut} className="ml-auto" />
|
||||
)}
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</MenuSection>
|
||||
{user ? (
|
||||
|
||||
@@ -23,7 +23,7 @@ import ResponsivePaneProvider from '../Panes/ResponsivePaneProvider'
|
||||
import AndroidBackHandlerProvider from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||
import ConfirmDeleteAccountContainer from '@/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal'
|
||||
import ApplicationProvider from '../ApplicationProvider'
|
||||
import CommandProvider from '../CommandProvider'
|
||||
import KeyboardServiceProvider from '../KeyboardServiceProvider'
|
||||
import PanesSystemComponent from '../Panes/PanesSystemComponent'
|
||||
import DotOrgNotice from './DotOrgNotice'
|
||||
import LinkingControllerProvider from '@/Controllers/LinkingControllerProvider'
|
||||
@@ -32,6 +32,8 @@ import IosKeyboardClose from '../IosKeyboardClose/IosKeyboardClose'
|
||||
import EditorWidthSelectionModalWrapper from '../EditorWidthSelectionModal/EditorWidthSelectionModal'
|
||||
import { ProtectionEvent } from '@standardnotes/services'
|
||||
import KeyboardShortcutsModal from '../KeyboardShortcutsHelpModal/KeyboardShortcutsHelpModal'
|
||||
import CommandPalette from '../CommandPalette/CommandPalette'
|
||||
import SuperExportModal from '../NotesOptions/SuperExportModal'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -212,7 +214,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
if (route.type === RouteType.AppViewRoute && route.appViewRouteParam === 'extension') {
|
||||
return (
|
||||
<ApplicationProvider application={application}>
|
||||
<CommandProvider service={application.keyboardService}>
|
||||
<KeyboardServiceProvider service={application.keyboardService}>
|
||||
<AndroidBackHandlerProvider application={application}>
|
||||
<ResponsivePaneProvider paneController={application.paneController}>
|
||||
<PremiumModalProvider application={application}>
|
||||
@@ -227,14 +229,14 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
</PremiumModalProvider>
|
||||
</ResponsivePaneProvider>
|
||||
</AndroidBackHandlerProvider>
|
||||
</CommandProvider>
|
||||
</KeyboardServiceProvider>
|
||||
</ApplicationProvider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ApplicationProvider application={application}>
|
||||
<CommandProvider service={application.keyboardService}>
|
||||
<KeyboardServiceProvider service={application.keyboardService}>
|
||||
<AndroidBackHandlerProvider application={application}>
|
||||
<ResponsivePaneProvider paneController={application.paneController}>
|
||||
<PremiumModalProvider application={application}>
|
||||
@@ -269,6 +271,8 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
<ConfirmDeleteAccountContainer application={application} />
|
||||
<ImportModal importModalController={application.importModalController} />
|
||||
<KeyboardShortcutsModal keyboardService={application.keyboardService} />
|
||||
<SuperExportModal />
|
||||
<CommandPalette />
|
||||
</>
|
||||
{application.routeService.isDotOrg && <DotOrgNotice />}
|
||||
{isIOS() && <IosKeyboardClose />}
|
||||
@@ -277,7 +281,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
</PremiumModalProvider>
|
||||
</ResponsivePaneProvider>
|
||||
</AndroidBackHandlerProvider>
|
||||
</CommandProvider>
|
||||
</KeyboardServiceProvider>
|
||||
</ApplicationProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -55,14 +55,15 @@ const ChangeEditorButton: FunctionComponent<Props> = ({ noteViewController, onCl
|
||||
}, [isOpen, onClickPreprocessing, onClick])
|
||||
|
||||
useEffect(() => {
|
||||
return application.keyboardService.addCommandHandler({
|
||||
command: CHANGE_EDITOR_COMMAND,
|
||||
category: 'Current note',
|
||||
description: 'Change note type',
|
||||
onKeyDown: () => {
|
||||
return application.commands.addWithShortcut(
|
||||
CHANGE_EDITOR_COMMAND,
|
||||
'Current note',
|
||||
'Change note type',
|
||||
() => {
|
||||
void toggleMenu()
|
||||
},
|
||||
})
|
||||
'notes',
|
||||
)
|
||||
}, [application, toggleMenu])
|
||||
|
||||
const shortcut = useMemo(
|
||||
|
||||
@@ -0,0 +1,425 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { startTransition, useCallback, useEffect, useState } from 'react'
|
||||
import { useKeyboardService } from '../KeyboardServiceProvider'
|
||||
import { PlatformedKeyboardShortcut, TOGGLE_COMMAND_PALETTE } from '@standardnotes/ui-services'
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxGroup,
|
||||
ComboboxGroupLabel,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
ComboboxProvider,
|
||||
Dialog,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabProvider,
|
||||
useDialogStore,
|
||||
useTabContext,
|
||||
} from '@ariakit/react'
|
||||
import {
|
||||
classNames,
|
||||
DecryptedItemInterface,
|
||||
FileItem,
|
||||
SmartView,
|
||||
SNNote,
|
||||
SNTag,
|
||||
UuidGenerator,
|
||||
} from '@standardnotes/snjs'
|
||||
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import Icon from '../Icon/Icon'
|
||||
import { getIconForItem } from '../../Utils/Items/Icons/getIconForItem'
|
||||
import { FileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
|
||||
import type { CommandService } from './CommandService'
|
||||
import { requestCloseAllOpenModalsAndPopovers } from '../../Utils/CloseOpenModalsAndPopovers'
|
||||
|
||||
type CommandPaletteItem = {
|
||||
id: string
|
||||
description: string
|
||||
icon: JSX.Element
|
||||
shortcut?: PlatformedKeyboardShortcut
|
||||
resultRange?: [number, number]
|
||||
} & ({ section: 'notes' | 'files' | 'tags'; itemUuid: string } | { section: 'commands' })
|
||||
|
||||
function ListItemDescription({ item }: { item: CommandPaletteItem }) {
|
||||
const range = item.resultRange
|
||||
if (!range) {
|
||||
return item.description
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{item.description.slice(0, range[0])}
|
||||
<span className="rounded-sm bg-[color-mix(in_srgb,var(--sn-stylekit-accessory-tint-color-1),rgba(255,255,255,.1))] p-px">
|
||||
{item.description.slice(range[0], range[1])}
|
||||
</span>
|
||||
{item.description.slice(range[1])}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Tabs = ['all', 'commands', 'notes', 'files', 'tags'] as const
|
||||
type TabId = (typeof Tabs)[number]
|
||||
|
||||
function CommandPaletteListItem({
|
||||
id,
|
||||
item,
|
||||
index,
|
||||
handleClick,
|
||||
selectedTab,
|
||||
}: {
|
||||
id: string
|
||||
item: CommandPaletteItem
|
||||
index: number
|
||||
handleClick: (item: CommandPaletteItem) => void
|
||||
selectedTab: TabId
|
||||
}) {
|
||||
if (selectedTab !== 'all' && selectedTab !== item.section) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<ComboboxItem
|
||||
id={id}
|
||||
value={item.id}
|
||||
hideOnClick={true}
|
||||
focusOnHover={true}
|
||||
blurOnHoverEnd={false}
|
||||
className={classNames(
|
||||
'flex scroll-m-2 items-center gap-2 whitespace-nowrap rounded-md px-2 py-2.5 text-[0.95rem] data-[active-item]:bg-info data-[active-item]:text-info-contrast [&>svg]:flex-shrink-0',
|
||||
index === 0 && 'scroll-m-8',
|
||||
)}
|
||||
onClick={() => handleClick(item)}
|
||||
>
|
||||
{item.icon}
|
||||
<div className="mr-auto overflow-hidden text-ellipsis whitespace-nowrap leading-none">
|
||||
<ListItemDescription item={item} />
|
||||
</div>
|
||||
{item.shortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={item.shortcut} small={false} />}
|
||||
</ComboboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxInput() {
|
||||
const tab = useTabContext()
|
||||
return (
|
||||
<Combobox
|
||||
autoSelect="always"
|
||||
className="h-10 w-full appearance-none bg-transparent px-1 text-base focus:shadow-none focus:outline-none"
|
||||
placeholder="Search notes, files, commands, etc..."
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== 'Tab') {
|
||||
return
|
||||
}
|
||||
const activeId = tab?.getState().selectedId
|
||||
const options = { activeId }
|
||||
const nextId = event.shiftKey ? tab?.previous(options) : tab?.next(options)
|
||||
if (nextId) {
|
||||
event.preventDefault()
|
||||
tab?.select(nextId)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Future TODO, nice to have: A way to expose items like the current note's options
|
||||
// directly in the command palette rather than only having a way to open the menu
|
||||
function CommandPalette() {
|
||||
const application = useApplication()
|
||||
const keyboardService = useKeyboardService()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const [recents, setRecents] = useState<CommandPaletteItem[]>([])
|
||||
const [items, setItems] = useState<CommandPaletteItem[]>([])
|
||||
// Storing counts as separate state to avoid iterating items multiple times
|
||||
const [itemCountsPerTab, setItemCounts] = useState({
|
||||
commands: 0,
|
||||
notes: 0,
|
||||
files: 0,
|
||||
tags: 0,
|
||||
})
|
||||
const [selectedTab, setSelectedTab] = useState<TabId>('all')
|
||||
const dialog = useDialogStore({
|
||||
open: isOpen,
|
||||
setOpen: setIsOpen,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
keyboardService.disableEventHandling()
|
||||
requestCloseAllOpenModalsAndPopovers()
|
||||
} else {
|
||||
keyboardService.enableEventHandling()
|
||||
}
|
||||
}, [keyboardService, isOpen])
|
||||
|
||||
useEffect(() => {
|
||||
return keyboardService.addCommandHandler({
|
||||
command: TOGGLE_COMMAND_PALETTE,
|
||||
category: 'General',
|
||||
description: 'Toggle command palette',
|
||||
onKeyDown: (e) => {
|
||||
e.preventDefault()
|
||||
setIsOpen((open) => !open)
|
||||
setQuery('')
|
||||
},
|
||||
})
|
||||
}, [keyboardService])
|
||||
|
||||
const handleItemClick = useCallback(
|
||||
(item: CommandPaletteItem) => {
|
||||
if (item.section === 'commands') {
|
||||
application.commands.triggerCommand(item.id)
|
||||
application.recents.add(item.id, 'command')
|
||||
} else {
|
||||
const decryptedItem = application.items.findItem<DecryptedItemInterface>(item.itemUuid)
|
||||
if (!decryptedItem) {
|
||||
return
|
||||
}
|
||||
if (decryptedItem instanceof SNNote) {
|
||||
void application.itemListController.selectItemUsingInstance(decryptedItem, true)
|
||||
} else if (decryptedItem instanceof FileItem) {
|
||||
void application.filesController.handleFileAction({
|
||||
type: FileItemActionType.PreviewFile,
|
||||
payload: { file: decryptedItem },
|
||||
})
|
||||
} else if (decryptedItem instanceof SNTag || decryptedItem instanceof SmartView) {
|
||||
void application.navigationController.setSelectedTag(decryptedItem, 'all', {
|
||||
userTriggered: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
application.commands,
|
||||
application.filesController,
|
||||
application.itemListController,
|
||||
application.items,
|
||||
application.navigationController,
|
||||
application.recents,
|
||||
],
|
||||
)
|
||||
|
||||
const createItemForInteractableItem = useCallback(
|
||||
(item: DecryptedItemInterface): CommandPaletteItem => {
|
||||
const icon = getIconForItem(item, application)
|
||||
let section: 'notes' | 'files' | 'tags'
|
||||
if (item instanceof SNNote) {
|
||||
section = 'notes'
|
||||
} else if (item instanceof FileItem) {
|
||||
section = 'files'
|
||||
} else if (item instanceof SNTag || item instanceof SmartView) {
|
||||
section = 'tags'
|
||||
} else {
|
||||
throw new Error('Item is not a note, file or tag')
|
||||
}
|
||||
return {
|
||||
section,
|
||||
id: UuidGenerator.GenerateUuid(),
|
||||
itemUuid: item.uuid,
|
||||
description: item.title || '<no title>',
|
||||
icon: <Icon type={icon[0]} className={item instanceof SNNote ? icon[1] : ''} />,
|
||||
}
|
||||
},
|
||||
[application],
|
||||
)
|
||||
|
||||
const createItemForCommand = useCallback(
|
||||
(command: ReturnType<CommandService['getCommandDescriptions']>[0]): CommandPaletteItem => {
|
||||
const shortcut = command.shortcut_id
|
||||
? application.keyboardService.keyboardShortcutForCommand(command.shortcut_id)
|
||||
: undefined
|
||||
return {
|
||||
id: command.id,
|
||||
description: command.description,
|
||||
section: 'commands',
|
||||
icon: <Icon type={command.icon} />,
|
||||
shortcut,
|
||||
}
|
||||
},
|
||||
[application.keyboardService],
|
||||
)
|
||||
|
||||
useEffect(
|
||||
function updateCommandPaletteItems() {
|
||||
if (!isOpen) {
|
||||
setSelectedTab('all')
|
||||
setItems([])
|
||||
return
|
||||
}
|
||||
|
||||
const recents: CommandPaletteItem[] = []
|
||||
const items: CommandPaletteItem[] = []
|
||||
const itemCounts: typeof itemCountsPerTab = {
|
||||
commands: 0,
|
||||
notes: 0,
|
||||
files: 0,
|
||||
tags: 0,
|
||||
}
|
||||
|
||||
const searchQuery = query.toLowerCase()
|
||||
const hasQuery = searchQuery.length > 0
|
||||
|
||||
if (hasQuery) {
|
||||
const commands = application.commands.getCommandDescriptions()
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
const command = commands[i]
|
||||
if (!command) {
|
||||
continue
|
||||
}
|
||||
if (items.length >= 50) {
|
||||
break
|
||||
}
|
||||
const index = command.description.toLowerCase().indexOf(searchQuery)
|
||||
if (index === -1) {
|
||||
continue
|
||||
}
|
||||
const item = createItemForCommand(command)
|
||||
item.resultRange = [index, index + searchQuery.length]
|
||||
items.push(item)
|
||||
itemCounts[item.section]++
|
||||
}
|
||||
|
||||
const interactableItems = application.items.getInteractableItems()
|
||||
for (let i = 0; i < interactableItems.length; i++) {
|
||||
if (items.length >= 50) {
|
||||
break
|
||||
}
|
||||
const decryptedItem = interactableItems[i]
|
||||
if (!decryptedItem || !decryptedItem.title) {
|
||||
continue
|
||||
}
|
||||
const index = decryptedItem.title.toLowerCase().indexOf(searchQuery)
|
||||
if (index === -1) {
|
||||
continue
|
||||
}
|
||||
const item = createItemForInteractableItem(decryptedItem)
|
||||
item.resultRange = [index, index + searchQuery.length]
|
||||
items.push(item)
|
||||
itemCounts[item.section]++
|
||||
}
|
||||
} else {
|
||||
const recentCommands = application.recents.commandUuids
|
||||
for (let i = 0; i < recentCommands.length; i++) {
|
||||
const command = application.commands.getCommandDescription(recentCommands[i])
|
||||
if (!command) {
|
||||
continue
|
||||
}
|
||||
const item = createItemForCommand(command)
|
||||
recents.push(item)
|
||||
itemCounts[item.section]++
|
||||
}
|
||||
const recentItems = application.recents.itemUuids
|
||||
for (let i = 0; i < recentItems.length; i++) {
|
||||
const decryptedItem = application.items.findItem(recentItems[i])
|
||||
if (!decryptedItem) {
|
||||
continue
|
||||
}
|
||||
const item = createItemForInteractableItem(decryptedItem)
|
||||
recents.push(item)
|
||||
itemCounts[item.section]++
|
||||
}
|
||||
|
||||
const commands = application.commands.getCommandDescriptions()
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
const command = commands[i]
|
||||
if (!command) {
|
||||
continue
|
||||
}
|
||||
const item = createItemForCommand(command)
|
||||
items.push(item)
|
||||
itemCounts[item.section]++
|
||||
}
|
||||
items.sort((a, b) => (a.description.toLowerCase() < b.description.toLowerCase() ? -1 : 1))
|
||||
}
|
||||
|
||||
setItems(items)
|
||||
setRecents(recents)
|
||||
setItemCounts(itemCounts)
|
||||
},
|
||||
[application, createItemForCommand, createItemForInteractableItem, isOpen, query],
|
||||
)
|
||||
|
||||
const hasNoItemsAtAll = items.length === 0
|
||||
const hasNoItemsInSelectedTab = selectedTab !== 'all' && itemCountsPerTab[selectedTab] === 0
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
store={dialog}
|
||||
className="fixed inset-3 bottom-[10vh] top-[10vh] z-modal m-auto mt-0 flex h-fit max-h-[70vh] w-[min(45rem,90vw)] flex-col gap-3 overflow-auto rounded-xl border border-[--popover-border-color] bg-[--popover-background-color] px-3 py-3 shadow-main [backdrop-filter:var(--popover-backdrop-filter)]"
|
||||
backdrop={<div className="bg-passive-5 opacity-50 transition-opacity duration-75 data-[enter]:opacity-85" />}
|
||||
>
|
||||
<ComboboxProvider
|
||||
disclosure={dialog}
|
||||
includesBaseElement={false}
|
||||
resetValueOnHide={true}
|
||||
setValue={(value) => {
|
||||
startTransition(() => setQuery(value))
|
||||
}}
|
||||
>
|
||||
<TabProvider selectedId={selectedTab} setSelectedId={(id) => setSelectedTab((id as TabId) || 'all')}>
|
||||
<div className="flex rounded-lg border border-[--popover-border-color] bg-[--popover-background-color] px-2">
|
||||
<ComboboxInput />
|
||||
</div>
|
||||
<TabList className="flex items-center gap-1">
|
||||
{Tabs.map((id) => (
|
||||
<Tab
|
||||
key={id}
|
||||
id={id}
|
||||
className="rounded-full px-3 py-1 capitalize disabled:opacity-65 aria-selected:bg-info aria-selected:text-info-contrast data-[active-item]:ring-1 data-[active-item]:ring-info data-[active-item]:ring-offset-1 data-[active-item]:ring-offset-transparent"
|
||||
disabled={hasNoItemsAtAll || (id !== 'all' && itemCountsPerTab[id] === 0)}
|
||||
accessibleWhenDisabled={false}
|
||||
>
|
||||
{id}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
<TabPanel className="flex flex-col gap-1.5 overflow-y-auto" tabId={selectedTab}>
|
||||
{query.length > 0 && (hasNoItemsAtAll || hasNoItemsInSelectedTab) && (
|
||||
<div className="mx-auto px-2 text-sm font-semibold opacity-75">No items found</div>
|
||||
)}
|
||||
<ComboboxList className="focus:shadow-none focus:outline-none">
|
||||
{recents.length > 0 && (
|
||||
<ComboboxGroup>
|
||||
<ComboboxGroupLabel className="px-2 font-semibold opacity-75">Recent</ComboboxGroupLabel>
|
||||
{recents.map((item, index) => (
|
||||
<CommandPaletteListItem
|
||||
key={item.id}
|
||||
id={
|
||||
/* ariakit doesn't like multiple items with the same id in the same combobox list */
|
||||
item.id + 'recent'
|
||||
}
|
||||
index={index}
|
||||
item={item}
|
||||
handleClick={handleItemClick}
|
||||
selectedTab={selectedTab}
|
||||
/>
|
||||
))}
|
||||
</ComboboxGroup>
|
||||
)}
|
||||
{!hasNoItemsAtAll && (
|
||||
<ComboboxGroup>
|
||||
{recents.length > 0 && (
|
||||
<ComboboxGroupLabel className="mt-2 px-2 font-semibold opacity-75">All commands</ComboboxGroupLabel>
|
||||
)}
|
||||
{items.map((item, index) => (
|
||||
<CommandPaletteListItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
index={index}
|
||||
item={item}
|
||||
handleClick={handleItemClick}
|
||||
selectedTab={selectedTab}
|
||||
/>
|
||||
))}
|
||||
</ComboboxGroup>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</TabPanel>
|
||||
</TabProvider>
|
||||
</ComboboxProvider>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(CommandPalette)
|
||||
@@ -0,0 +1,66 @@
|
||||
import { GenerateUuid, IconType } from '@standardnotes/snjs'
|
||||
import { KeyboardCommand, KeyboardService, KeyboardShortcutCategory } from '@standardnotes/ui-services'
|
||||
import mergeRegister from '../../Hooks/mergeRegister'
|
||||
|
||||
type CommandInfo = {
|
||||
description: string
|
||||
icon: IconType
|
||||
shortcut_id?: KeyboardCommand
|
||||
}
|
||||
|
||||
type CommandDescription = { id: string } & CommandInfo
|
||||
|
||||
export class CommandService {
|
||||
#commandInfo = new Map<string, CommandInfo>()
|
||||
#commandHandlers = new Map<string, () => void>()
|
||||
|
||||
constructor(
|
||||
private keyboardService: KeyboardService,
|
||||
private generateUuid: GenerateUuid,
|
||||
) {}
|
||||
|
||||
public add(id: string, description: string, handler: () => void, icon?: IconType, shortcut_id?: KeyboardCommand) {
|
||||
this.#commandInfo.set(id, { description, icon: icon ?? 'info', shortcut_id })
|
||||
this.#commandHandlers.set(id, handler)
|
||||
return () => {
|
||||
this.#commandInfo.delete(id)
|
||||
this.#commandHandlers.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
public addWithShortcut(
|
||||
id: KeyboardCommand,
|
||||
category: KeyboardShortcutCategory,
|
||||
description: string,
|
||||
handler: (event?: KeyboardEvent, data?: unknown) => void,
|
||||
icon?: IconType,
|
||||
) {
|
||||
return mergeRegister(
|
||||
this.add(id.description ?? this.generateUuid.execute().getValue(), description, handler, icon, id),
|
||||
this.keyboardService.addCommandHandler({ command: id, category, description, onKeyDown: handler }),
|
||||
)
|
||||
}
|
||||
|
||||
public triggerCommand(id: string) {
|
||||
const handler = this.#commandHandlers.get(id)
|
||||
if (handler) {
|
||||
handler()
|
||||
}
|
||||
}
|
||||
|
||||
public getCommandDescriptions() {
|
||||
const descriptions: CommandDescription[] = []
|
||||
for (const [id, { description, icon, shortcut_id }] of this.#commandInfo) {
|
||||
descriptions.push({ id, description, icon, shortcut_id })
|
||||
}
|
||||
return descriptions
|
||||
}
|
||||
|
||||
public getCommandDescription(id: string): CommandDescription | undefined {
|
||||
const command = this.#commandInfo.get(id)
|
||||
if (!command) {
|
||||
return
|
||||
}
|
||||
return { id, ...command }
|
||||
}
|
||||
}
|
||||
@@ -175,15 +175,6 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
|
||||
* probably better to be consistent.
|
||||
*/
|
||||
return application.keyboardService.addCommandHandlers([
|
||||
{
|
||||
command: CREATE_NEW_NOTE_KEYBOARD_COMMAND,
|
||||
category: 'General',
|
||||
description: 'Create new note',
|
||||
onKeyDown: (event) => {
|
||||
event.preventDefault()
|
||||
void addNewItem()
|
||||
},
|
||||
},
|
||||
{
|
||||
command: NEXT_LIST_ITEM_KEYBOARD_COMMAND,
|
||||
category: 'Notes list',
|
||||
@@ -262,11 +253,28 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
|
||||
)
|
||||
|
||||
const addButtonLabel = useMemo(() => {
|
||||
return isFilesSmartView
|
||||
? 'Upload file'
|
||||
: `Create a new note in the selected tag (${shortcutForCreate && keyboardStringForShortcut(shortcutForCreate)})`
|
||||
let shortcut = keyboardStringForShortcut(shortcutForCreate)
|
||||
if (shortcut) {
|
||||
shortcut = '(' + shortcut + ')'
|
||||
}
|
||||
return isFilesSmartView ? `Upload file ${shortcut}` : `Create a new note in the selected tag ${shortcut}`
|
||||
}, [isFilesSmartView, shortcutForCreate])
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
application.commands.addWithShortcut(
|
||||
CREATE_NEW_NOTE_KEYBOARD_COMMAND,
|
||||
'General',
|
||||
isFilesSmartView ? 'Upload file' : 'Create new note',
|
||||
(event) => {
|
||||
event?.preventDefault()
|
||||
void addNewItem()
|
||||
},
|
||||
isFilesSmartView ? 'upload' : 'add',
|
||||
),
|
||||
[addNewItem, application.commands, isFilesSmartView],
|
||||
)
|
||||
|
||||
const dailyMode = selectedAsTag?.isDailyEntry
|
||||
|
||||
const handleDailyListSelection = useCallback(
|
||||
|
||||
@@ -103,6 +103,17 @@ const ContentListHeader = ({
|
||||
setShowDisplayOptionsMenu((show) => !show)
|
||||
}, [])
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
application.commands.add(
|
||||
'open-display-opts-menu',
|
||||
'Open display options menu',
|
||||
toggleDisplayOptionsMenu,
|
||||
'sort-descending',
|
||||
),
|
||||
[application.commands, toggleDisplayOptionsMenu],
|
||||
)
|
||||
|
||||
const OptionsMenu = useMemo(() => {
|
||||
return (
|
||||
<div className="flex">
|
||||
|
||||
+7
-6
@@ -178,11 +178,11 @@ const EditorWidthSelectionModalWrapper = () => {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
return application.keyboardService.addCommandHandler({
|
||||
command: CHANGE_EDITOR_WIDTH_COMMAND,
|
||||
category: 'Current note',
|
||||
description: 'Change editor width',
|
||||
onKeyDown: (_, data) => {
|
||||
return application.commands.addWithShortcut(
|
||||
CHANGE_EDITOR_WIDTH_COMMAND,
|
||||
'Current note',
|
||||
'Change editor width',
|
||||
(_, data) => {
|
||||
if (typeof data === 'boolean' && data) {
|
||||
setIsGlobal(data)
|
||||
} else {
|
||||
@@ -190,7 +190,8 @@ const EditorWidthSelectionModalWrapper = () => {
|
||||
}
|
||||
toggle()
|
||||
},
|
||||
})
|
||||
'line-width',
|
||||
)
|
||||
}, [application, toggle])
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { useRef } from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import AccountMenu, { AccountMenuProps } from '../AccountMenu/AccountMenu'
|
||||
import Icon from '../Icon/Icon'
|
||||
import Popover from '../Popover/Popover'
|
||||
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
|
||||
type Props = AccountMenuProps & {
|
||||
controller: AccountMenuController
|
||||
@@ -15,9 +16,15 @@ type Props = AccountMenuProps & {
|
||||
}
|
||||
|
||||
const AccountMenuButton = ({ hasError, controller, mainApplicationGroup, onClickOutside, toggleMenu, user }: Props) => {
|
||||
const application = useApplication()
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const { show: isOpen } = controller
|
||||
|
||||
useEffect(
|
||||
() => application.commands.add('open-acc-menu', 'Open account menu', toggleMenu, 'account-circle'),
|
||||
[application.commands, toggleMenu],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTooltip label="Open account menu">
|
||||
|
||||
@@ -2,7 +2,7 @@ import { compareSemVersions, StatusServiceEvent } from '@standardnotes/snjs'
|
||||
import { keyboardStringForShortcut, OPEN_PREFERENCES_COMMAND } from '@standardnotes/ui-services'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { useCommandService } from '../CommandProvider'
|
||||
import { useKeyboardService } from '../KeyboardServiceProvider'
|
||||
import Icon from '../Icon/Icon'
|
||||
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
@@ -16,10 +16,10 @@ type Props = {
|
||||
const PreferencesButton = ({ openPreferences }: Props) => {
|
||||
const application = useApplication()
|
||||
|
||||
const commandService = useCommandService()
|
||||
const keyboardService = useKeyboardService()
|
||||
const shortcut = useMemo(
|
||||
() => keyboardStringForShortcut(commandService.keyboardShortcutForCommand(OPEN_PREFERENCES_COMMAND)),
|
||||
[commandService],
|
||||
() => keyboardStringForShortcut(keyboardService.keyboardShortcutForCommand(OPEN_PREFERENCES_COMMAND)),
|
||||
[keyboardService],
|
||||
)
|
||||
|
||||
const [changelogLastReadVersion, setChangelogLastReadVersion] = useState(() =>
|
||||
|
||||
@@ -2,13 +2,14 @@ import { WebApplication } from '@/Application/WebApplication'
|
||||
import { UIFeature, GetDarkThemeFeature } from '@standardnotes/snjs'
|
||||
import { TOGGLE_DARK_MODE_COMMAND } from '@standardnotes/ui-services'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useCommandService } from '../CommandProvider'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useKeyboardService } from '../KeyboardServiceProvider'
|
||||
import Icon from '../Icon/Icon'
|
||||
import Popover from '../Popover/Popover'
|
||||
import QuickSettingsMenu from '../QuickSettingsMenu/QuickSettingsMenu'
|
||||
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
||||
import RoundIconButton from '../Button/RoundIconButton'
|
||||
import mergeRegister from '../../Hooks/mergeRegister'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -17,30 +18,37 @@ type Props = {
|
||||
|
||||
const QuickSettingsButton = ({ application, isMobileNavigation = false }: Props) => {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const commandService = useCommandService()
|
||||
const keyboardService = useKeyboardService()
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const toggleMenu = () => setIsOpen(!isOpen)
|
||||
const toggleMenu = useCallback(() => setIsOpen((isOpen) => !isOpen), [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobileNavigation) {
|
||||
return
|
||||
}
|
||||
|
||||
const darkThemeFeature = new UIFeature(GetDarkThemeFeature())
|
||||
|
||||
return commandService.addCommandHandler({
|
||||
command: TOGGLE_DARK_MODE_COMMAND,
|
||||
category: 'General',
|
||||
description: 'Toggle dark mode',
|
||||
onKeyDown: () => {
|
||||
return mergeRegister(
|
||||
application.commands.addWithShortcut(TOGGLE_DARK_MODE_COMMAND, 'General', 'Toggle dark mode', () => {
|
||||
void application.componentManager.toggleTheme(darkThemeFeature)
|
||||
return true
|
||||
},
|
||||
})
|
||||
}, [application, commandService])
|
||||
}),
|
||||
application.commands.add('open-quick-settings-menu', 'Open quick settings menu', toggleMenu, 'themes'),
|
||||
)
|
||||
}, [application, isMobileNavigation, keyboardService, toggleMenu])
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTooltip label="Open quick settings menu">
|
||||
{isMobileNavigation ? (
|
||||
<RoundIconButton className="ml-2.5 bg-default" onClick={toggleMenu} label="Go to vaults menu" icon="themes" />
|
||||
<RoundIconButton
|
||||
className="ml-2.5 bg-default"
|
||||
onClick={toggleMenu}
|
||||
label="Go to quick settings menu"
|
||||
icon="themes"
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
onClick={toggleMenu}
|
||||
|
||||
@@ -75,6 +75,8 @@ const DecoratedInput = forwardRef(
|
||||
|
||||
<input
|
||||
autoComplete={autocomplete ? 'on' : 'off'}
|
||||
autoCorrect={autocomplete ? 'on' : 'off'}
|
||||
autoCapitalize={autocomplete ? 'on' : 'off'}
|
||||
autoFocus={autofocus}
|
||||
className={`${computedClassNames.input} ${disabled ? computedClassNames.disabled : ''} ${className?.input}`}
|
||||
data-lpignore={type !== 'password' ? true : false}
|
||||
|
||||
@@ -30,6 +30,8 @@ const DecoratedPasswordInput = forwardRef((props: DecoratedInputProps, ref: Ref<
|
||||
{...props}
|
||||
ref={ref}
|
||||
type={isToggled ? 'text' : 'password'}
|
||||
autocomplete={false}
|
||||
spellcheck={false}
|
||||
right={[...rightSideDecorations, <Toggle isToggled={isToggled} setIsToggled={setIsToggled} />]}
|
||||
/>
|
||||
)
|
||||
|
||||
+8
-8
@@ -3,13 +3,13 @@ import { ReactNode, createContext, useContext, memo } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { KeyboardService } from '@standardnotes/ui-services'
|
||||
|
||||
const CommandServiceContext = createContext<KeyboardService | undefined>(undefined)
|
||||
const KeyboardServiceContext = createContext<KeyboardService | undefined>(undefined)
|
||||
|
||||
export const useCommandService = () => {
|
||||
const value = useContext(CommandServiceContext)
|
||||
export const useKeyboardService = () => {
|
||||
const value = useContext(KeyboardServiceContext)
|
||||
|
||||
if (!value) {
|
||||
throw new Error('Component must be a child of <CommandServiceProvider />')
|
||||
throw new Error('Component must be a child of <KeyboardServiceProvider />')
|
||||
}
|
||||
|
||||
return value
|
||||
@@ -25,12 +25,12 @@ type ProviderProps = {
|
||||
|
||||
const MemoizedChildren = memo(({ children }: ChildrenProps) => <>{children}</>)
|
||||
|
||||
const CommandServiceProvider = ({ service, children }: ProviderProps) => {
|
||||
const KeyboardServiceProvider = ({ service, children }: ProviderProps) => {
|
||||
return (
|
||||
<CommandServiceContext.Provider value={service}>
|
||||
<KeyboardServiceContext.Provider value={service}>
|
||||
<MemoizedChildren children={children} />
|
||||
</CommandServiceContext.Provider>
|
||||
</KeyboardServiceContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(CommandServiceProvider)
|
||||
export default observer(KeyboardServiceProvider)
|
||||
+2
-1
@@ -4,6 +4,7 @@ import {
|
||||
keyboardCharacterForModifier,
|
||||
isMobilePlatform,
|
||||
keyboardCharacterForKeyOrCode,
|
||||
KeyboardModifier,
|
||||
} from '@standardnotes/ui-services'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
@@ -20,7 +21,7 @@ export const KeyboardShortcutIndicator = ({ shortcut, small = true, dimmed = tru
|
||||
const primaryKey = shortcut.key
|
||||
? keyboardCharacterForKeyOrCode(shortcut.key)
|
||||
: shortcut.code
|
||||
? keyboardCharacterForKeyOrCode(shortcut.code)
|
||||
? keyboardCharacterForKeyOrCode(shortcut.code, modifiers.includes(KeyboardModifier.Shift))
|
||||
: undefined
|
||||
|
||||
const results: string[] = []
|
||||
|
||||
@@ -56,8 +56,10 @@ const LinkedItemBubble = ({
|
||||
setShowUnlinkButton(true)
|
||||
}
|
||||
|
||||
const onBlur = () => {
|
||||
setShowUnlinkButton(false)
|
||||
const onBlur: React.FocusEventHandler<HTMLButtonElement | HTMLAnchorElement> = (event) => {
|
||||
if (!event.currentTarget.contains(event.relatedTarget)) {
|
||||
setShowUnlinkButton(false)
|
||||
}
|
||||
setWasClicked(false)
|
||||
}
|
||||
|
||||
@@ -131,7 +133,7 @@ const LinkedItemBubble = ({
|
||||
<button
|
||||
ref={(el) => (unlinkButtonRef.current = el as HTMLElement)}
|
||||
role="button"
|
||||
className="-mr-1 ml-2 inline-flex cursor-pointer border-0 bg-transparent p-0 h-[1.15em] align-middle"
|
||||
className="-mr-1 ml-2 inline-flex h-[1.15em] cursor-pointer border-0 bg-transparent p-0 align-middle"
|
||||
onClick={onUnlinkClick}
|
||||
>
|
||||
<Icon type="close" className="text-neutral hover:text-info" size="small" />
|
||||
|
||||
+25
-17
@@ -2,7 +2,7 @@ import { observer } from 'mobx-react-lite'
|
||||
import ItemLinkAutocompleteInput from './ItemLinkAutocompleteInput'
|
||||
import { LinkingController } from '@/Controllers/LinkingController'
|
||||
import LinkedItemBubble from './LinkedItemBubble'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
@@ -10,12 +10,13 @@ import { ContentType, DecryptedItemInterface } from '@standardnotes/snjs'
|
||||
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
|
||||
import { ItemLink } from '@/Utils/Items/Search/ItemLink'
|
||||
import { FOCUS_TAGS_INPUT_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services'
|
||||
import { useCommandService } from '../CommandProvider'
|
||||
import { useItemLinks } from '@/Hooks/useItemLinks'
|
||||
import RoundIconButton from '../Button/RoundIconButton'
|
||||
import VaultNameBadge from '../Vaults/VaultNameBadge'
|
||||
import LastEditedByBadge from '../Vaults/LastEditedByBadge'
|
||||
import { useItemVaultInfo } from '@/Hooks/useItemVaultInfo'
|
||||
import mergeRegister from '../../Hooks/mergeRegister'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
|
||||
type Props = {
|
||||
linkingController: LinkingController
|
||||
@@ -39,7 +40,8 @@ const LinkedItemBubblesContainer = ({
|
||||
}: Props) => {
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
|
||||
const commandService = useCommandService()
|
||||
const application = useApplication()
|
||||
const keyboardService = application.keyboardService
|
||||
|
||||
const { unlinkItems, activateItem } = linkingController
|
||||
const unlinkItem = useCallback(
|
||||
@@ -57,23 +59,28 @@ const LinkedItemBubblesContainer = ({
|
||||
[filesLinkedToItem, notesLinkedToItem, tagsLinkedToItem],
|
||||
)
|
||||
|
||||
const linkInputRef = useRef<HTMLInputElement>(null)
|
||||
useEffect(() => {
|
||||
return commandService.addCommandHandler({
|
||||
command: FOCUS_TAGS_INPUT_COMMAND,
|
||||
category: 'Current note',
|
||||
description: 'Link tags, notes, files',
|
||||
onKeyDown: () => {
|
||||
const input = document.getElementById(ElementIds.ItemLinkAutocompleteInput)
|
||||
if (input) {
|
||||
input.focus()
|
||||
}
|
||||
},
|
||||
})
|
||||
}, [commandService])
|
||||
const focusInput = () => {
|
||||
const input = linkInputRef.current
|
||||
if (input) {
|
||||
setTimeout(() => input.focus())
|
||||
}
|
||||
}
|
||||
return mergeRegister(
|
||||
keyboardService.addCommandHandler({
|
||||
command: FOCUS_TAGS_INPUT_COMMAND,
|
||||
category: 'Current note',
|
||||
description: 'Link tags, notes, files',
|
||||
onKeyDown: focusInput,
|
||||
}),
|
||||
application.commands.add('link-items-current', 'Link items to current note', focusInput, 'link'),
|
||||
)
|
||||
}, [application.commands, keyboardService])
|
||||
|
||||
const shortcut = useMemo(
|
||||
() => keyboardStringForShortcut(commandService.keyboardShortcutForCommand(FOCUS_TAGS_INPUT_COMMAND)),
|
||||
[commandService],
|
||||
() => keyboardStringForShortcut(keyboardService.keyboardShortcutForCommand(FOCUS_TAGS_INPUT_COMMAND)),
|
||||
[keyboardService],
|
||||
)
|
||||
|
||||
const [focusedId, setFocusedId] = useState<string>()
|
||||
@@ -209,6 +216,7 @@ const LinkedItemBubblesContainer = ({
|
||||
{isCollapsed && nonVisibleItems > 0 && <span className="flex-shrink-0">and {nonVisibleItems} more...</span>}
|
||||
{!readonly && (
|
||||
<ItemLinkAutocompleteInput
|
||||
ref={linkInputRef}
|
||||
focusedId={focusedId}
|
||||
linkingController={linkingController}
|
||||
focusPreviousItem={focusPreviousItem}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { LinkingController } from '@/Controllers/LinkingController'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useRef, useCallback } from 'react'
|
||||
import { useRef, useCallback, useEffect } from 'react'
|
||||
import RoundIconButton from '../Button/RoundIconButton'
|
||||
import Popover from '../Popover/Popover'
|
||||
import LinkedItemsPanel from './LinkedItemsPanel'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
|
||||
type Props = {
|
||||
linkingController: LinkingController
|
||||
@@ -12,6 +13,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const LinkedItemsButton = ({ linkingController, onClick, onClickPreprocessing }: Props) => {
|
||||
const application = useApplication()
|
||||
const { activeItem, isLinkingPanelOpen, setIsLinkingPanelOpen } = linkingController
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
@@ -26,6 +28,8 @@ const LinkedItemsButton = ({ linkingController, onClick, onClickPreprocessing }:
|
||||
}
|
||||
}, [isLinkingPanelOpen, onClick, onClickPreprocessing, setIsLinkingPanelOpen])
|
||||
|
||||
useEffect(() => application.commands.add('open-linked-items-panel', 'Open linked items panel', toggleMenu, 'link'))
|
||||
|
||||
if (!activeItem) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ const ModalOverlay = forwardRef(
|
||||
close()
|
||||
}
|
||||
},
|
||||
animated: !isMobileScreen,
|
||||
})
|
||||
|
||||
const portalId = useId()
|
||||
|
||||
@@ -93,11 +93,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
|
||||
onEditorComponentLoad?: () => void
|
||||
|
||||
private removeTrashKeyObserver?: () => void
|
||||
private removeNoteStreamObserver?: () => void
|
||||
private removeComponentManagerObserver?: () => void
|
||||
private removeInnerNoteObserver?: () => void
|
||||
private removeVaultUsersEventHandler?: () => void
|
||||
#observers: (() => void)[] = []
|
||||
|
||||
private protectionTimeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
private noteViewElementRef: RefObject<HTMLDivElement>
|
||||
@@ -147,20 +143,12 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
super.deinit()
|
||||
;(this.controller as unknown) = undefined
|
||||
|
||||
this.removeNoteStreamObserver?.()
|
||||
;(this.removeNoteStreamObserver as unknown) = undefined
|
||||
|
||||
this.removeInnerNoteObserver?.()
|
||||
;(this.removeInnerNoteObserver as unknown) = undefined
|
||||
|
||||
this.removeComponentManagerObserver?.()
|
||||
;(this.removeComponentManagerObserver as unknown) = undefined
|
||||
|
||||
this.removeTrashKeyObserver?.()
|
||||
this.removeTrashKeyObserver = undefined
|
||||
|
||||
this.removeVaultUsersEventHandler?.()
|
||||
this.removeVaultUsersEventHandler = undefined
|
||||
for (let i = 0; i < this.#observers.length; i++) {
|
||||
const cleanup = this.#observers[i]
|
||||
cleanup()
|
||||
}
|
||||
this.#observers.length = 0
|
||||
;(this.#observers as unknown) = undefined
|
||||
|
||||
this.clearNoteProtectionInactivityTimer()
|
||||
;(this.ensureNoteIsInsertedBeforeUIAction as unknown) = undefined
|
||||
@@ -213,23 +201,27 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
override componentDidMount(): void {
|
||||
super.componentDidMount()
|
||||
|
||||
this.removeVaultUsersEventHandler = this.application.vaultUsers.addEventObserver((event, data) => {
|
||||
if (event === VaultUserServiceEvent.InvalidatedUserCacheForVault) {
|
||||
const vault = this.application.vaults.getItemVault(this.note)
|
||||
if ((data as string) !== vault?.sharing?.sharedVaultUuid) {
|
||||
return
|
||||
this.#observers.push(
|
||||
this.application.vaultUsers.addEventObserver((event, data) => {
|
||||
if (event === VaultUserServiceEvent.InvalidatedUserCacheForVault) {
|
||||
const vault = this.application.vaults.getItemVault(this.note)
|
||||
if ((data as string) !== vault?.sharing?.sharedVaultUuid) {
|
||||
return
|
||||
}
|
||||
this.setState({
|
||||
readonly: vault ? this.application.vaultUsers.isCurrentUserReadonlyVaultMember(vault) : undefined,
|
||||
})
|
||||
}
|
||||
this.setState({
|
||||
readonly: vault ? this.application.vaultUsers.isCurrentUserReadonlyVaultMember(vault) : undefined,
|
||||
})
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
this.registerKeyboardShortcuts()
|
||||
|
||||
this.removeInnerNoteObserver = this.controller.addNoteInnerValueChangeObserver((note, source) => {
|
||||
this.onNoteInnerChange(note, source)
|
||||
})
|
||||
this.#observers.push(
|
||||
this.controller.addNoteInnerValueChangeObserver((note, source) => {
|
||||
this.onNoteInnerChange(note, source)
|
||||
}),
|
||||
)
|
||||
|
||||
this.autorun(() => {
|
||||
const syncStatus = this.controller.syncStatus
|
||||
@@ -463,15 +455,17 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
}
|
||||
|
||||
streamItems() {
|
||||
this.removeNoteStreamObserver = this.application.items.streamItems<SNNote>(ContentType.TYPES.Note, async () => {
|
||||
if (!this.note) {
|
||||
return
|
||||
}
|
||||
this.#observers.push(
|
||||
this.application.items.streamItems<SNNote>(ContentType.TYPES.Note, async () => {
|
||||
if (!this.note) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
conflictedNotes: this.application.items.conflictsOf(this.note.uuid) as SNNote[],
|
||||
})
|
||||
})
|
||||
this.setState({
|
||||
conflictedNotes: this.application.items.conflictsOf(this.note.uuid) as SNNote[],
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private createComponentViewer(component: UIFeature<IframeComponentFeatureDescription>) {
|
||||
@@ -766,14 +760,18 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
}
|
||||
|
||||
registerKeyboardShortcuts() {
|
||||
this.removeTrashKeyObserver = this.application.keyboardService.addCommandHandler({
|
||||
command: DELETE_NOTE_KEYBOARD_COMMAND,
|
||||
notTags: ['INPUT', 'TEXTAREA'],
|
||||
notElementIds: [SuperEditorContentId],
|
||||
onKeyDown: () => {
|
||||
this.deleteNote(false).catch(console.error)
|
||||
},
|
||||
})
|
||||
const moveNoteToTrash = () => {
|
||||
this.deleteNote(false).catch(console.error)
|
||||
}
|
||||
|
||||
this.#observers.push(
|
||||
this.application.keyboardService.addCommandHandler({
|
||||
command: DELETE_NOTE_KEYBOARD_COMMAND,
|
||||
notTags: ['INPUT', 'TEXTAREA'],
|
||||
notElementIds: [SuperEditorContentId],
|
||||
onKeyDown: moveNoteToTrash,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
ensureNoteIsInsertedBeforeUIAction = async () => {
|
||||
|
||||
@@ -190,8 +190,11 @@ export const PlainEditor = forwardRef<PlainEditorInterface, Props>(
|
||||
useEffect(() => {
|
||||
reloadPreferences()
|
||||
|
||||
return application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => {
|
||||
reloadPreferences()
|
||||
return application.addEventObserver(async (event) => {
|
||||
const events = [ApplicationEvent.PreferencesChanged, ApplicationEvent.LocalPreferencesChanged]
|
||||
if (events.includes(event)) {
|
||||
reloadPreferences()
|
||||
}
|
||||
})
|
||||
}, [reloadPreferences, application])
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user