mirror of
https://github.com/standardnotes/app
synced 2026-09-13 00:45:26 -04:00
Compare commits
50
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -78,22 +78,25 @@ jobs:
|
||||
|
||||
- run: yarn install --immutable
|
||||
|
||||
- name: Rebuild Electron Native Modules
|
||||
run: yarn workspace @standardnotes/desktop rebuild:home-server
|
||||
|
||||
- run: yarn build:desktop
|
||||
|
||||
- run: echo APP_VERSION=$(node -p "require('./../web/package.json').version") >> $GITHUB_ENV
|
||||
|
||||
- name: Compile for AppImage
|
||||
run: yarn run webpack --config desktop.webpack.prod.js
|
||||
- name: AppImage
|
||||
run: |
|
||||
yarn workspace @standardnotes/desktop rebuild:home-server
|
||||
yarn run webpack --config desktop.webpack.prod.js
|
||||
yarn run electron-builder --linux --x64 -c.linux.target=AppImage --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
|
||||
- name: AppImageX64
|
||||
run: |
|
||||
yarn workspace @standardnotes/desktop rebuild:home-server
|
||||
yarn run webpack --config desktop.webpack.prod.js
|
||||
yarn run electron-builder --linux --x64 -c.linux.target=AppImage --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
- name: AppImageArm64
|
||||
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=AppImage --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
env:
|
||||
npm_config_target_arch: 'arm64'
|
||||
@@ -138,7 +141,6 @@ jobs:
|
||||
|
||||
- name: Dir
|
||||
run: |
|
||||
yarn workspace @standardnotes/desktop rebuild:home-server
|
||||
yarn run webpack --config desktop.webpack.prod.js
|
||||
yarn run electron-builder --linux --x64 -c.linux.target=dir --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
- name: DirArm64
|
||||
@@ -171,11 +173,15 @@ jobs:
|
||||
|
||||
- run: yarn install --immutable
|
||||
|
||||
- name: Rebuild Electron Native Modules
|
||||
run: yarn workspace @standardnotes/desktop rebuild:home-server
|
||||
|
||||
- run: yarn build:desktop
|
||||
|
||||
- run: echo APP_VERSION=$(node -p "require('./../web/package.json').version") >> $GITHUB_ENV
|
||||
|
||||
- name: Deb
|
||||
run: |
|
||||
yarn workspace @standardnotes/desktop rebuild:home-server
|
||||
yarn run webpack --config desktop.webpack.prod.js --env deb
|
||||
yarn run electron-builder --linux --x64 -c.linux.target=deb --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
- name: DebArm64
|
||||
|
||||
@@ -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.
+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,62 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/clipper",
|
||||
"description": "Web clipper browser extension for Standard Notes",
|
||||
"version": "1.1.536",
|
||||
"version": "1.1.550",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build-mv2": "yarn clean && webpack --config ./webpack.config.prod.js",
|
||||
|
||||
@@ -3,6 +3,65 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
@@ -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.141",
|
||||
"version": "3.110.155",
|
||||
"license": "AGPL-3.0",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
|
||||
@@ -3,6 +3,68 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/mobile",
|
||||
"version": "3.58.198",
|
||||
"version": "3.58.213",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
|
||||
@@ -3,6 +3,66 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/releases",
|
||||
"version": "1.4.838",
|
||||
"version": "1.4.853",
|
||||
"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,76 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1,5 +1,181 @@
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"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)",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@standardnotes/web",
|
||||
"version": "3.198.9",
|
||||
"version": "3.200.1",
|
||||
"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)
|
||||
}
|
||||
|
||||
@@ -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[] = []
|
||||
|
||||
+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])
|
||||
|
||||
|
||||
@@ -78,30 +78,38 @@ const AddTagOption: FunctionComponent<Props> = ({
|
||||
overrideZIndex="z-modal"
|
||||
>
|
||||
<Menu a11yLabel="Tag selection menu" className="!px-0">
|
||||
{navigationController.tags.map((tag) => (
|
||||
<MenuItem
|
||||
key={tag.uuid}
|
||||
onClick={() => {
|
||||
isTagLinkedToSelectedItems(tag) ? unlinkTagFromSelectedItems(tag) : linkTagToSelectedItems(tag)
|
||||
}}
|
||||
>
|
||||
{tag.iconString && (
|
||||
<Icon
|
||||
type={tag.iconString as IconType}
|
||||
size={'custom'}
|
||||
className={'ml-0.5 mr-1.5 h-7 w-7 text-2xl text-neutral lg:h-6 lg:w-6 lg:text-lg'}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={classNames(
|
||||
'overflow-hidden overflow-ellipsis whitespace-nowrap',
|
||||
isTagLinkedToSelectedItems(tag) ? 'font-bold' : '',
|
||||
)}
|
||||
{navigationController.tags.map((tag) => {
|
||||
const tagTitlePrefix = getTitleForLinkedTag(tag, application)?.titlePrefix
|
||||
return (
|
||||
<MenuItem
|
||||
key={tag.uuid}
|
||||
onClick={() => {
|
||||
isTagLinkedToSelectedItems(tag) ? unlinkTagFromSelectedItems(tag) : linkTagToSelectedItems(tag)
|
||||
}}
|
||||
>
|
||||
{getTitleForLinkedTag(tag, application)?.longTitle}
|
||||
</span>
|
||||
</MenuItem>
|
||||
))}
|
||||
{tag.iconString && (
|
||||
<Icon
|
||||
type={tag.iconString as IconType}
|
||||
size={'custom'}
|
||||
className={'ml-0.5 mr-1.5 h-7 w-7 text-2xl text-neutral lg:h-6 lg:w-6 lg:text-lg'}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
{!!tagTitlePrefix && (
|
||||
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-neutral">{tagTitlePrefix}</p>
|
||||
)}
|
||||
<p
|
||||
className={classNames(
|
||||
'overflow-hidden overflow-ellipsis whitespace-nowrap',
|
||||
isTagLinkedToSelectedItems(tag) ? 'font-bold' : '',
|
||||
)}
|
||||
>
|
||||
{tag.title}
|
||||
</p>
|
||||
</div>
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</Menu>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react'
|
||||
import { NoteType, Platform, SNNote, pluralize } from '@standardnotes/snjs'
|
||||
import { NoteType, Platform } from '@standardnotes/snjs'
|
||||
import {
|
||||
CHANGE_EDITOR_WIDTH_COMMAND,
|
||||
OPEN_NOTE_HISTORY_COMMAND,
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
import ChangeEditorOption from './ChangeEditorOption'
|
||||
import ListedActionsOption from './Listed/ListedActionsOption'
|
||||
import AddTagOption from './AddTagOption'
|
||||
import { addToast, dismissToast, ToastType } from '@standardnotes/toast'
|
||||
import { NotesOptionsProps } from './NotesOptionsProps'
|
||||
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
||||
import { AppPaneId } from '../Panes/AppPaneMetadata'
|
||||
@@ -27,13 +26,10 @@ import { iconClass } from './ClassNames'
|
||||
import SuperNoteOptions from './SuperNoteOptions'
|
||||
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
||||
import MenuItem from '../Menu/MenuItem'
|
||||
import ModalOverlay from '../Modal/ModalOverlay'
|
||||
import SuperExportModal from './SuperExportModal'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { MutuallyExclusiveMediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
|
||||
import AddToVaultMenuOption from '../Vaults/AddToVaultMenuOption'
|
||||
import MenuSection from '../Menu/MenuSection'
|
||||
import { downloadOrShareBlobBasedOnPlatform } from '@/Utils/DownloadOrShareBasedOnPlatform'
|
||||
import { shareBlobOnMobile } from '@/NativeMobileWeb/ShareBlobOnMobile'
|
||||
|
||||
const iconSize = MenuItemIconSize
|
||||
@@ -43,26 +39,13 @@ const iconClassSuccess = `text-success mr-2 ${iconSize}`
|
||||
|
||||
const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
const application = useApplication()
|
||||
const notesController = application.notesController
|
||||
|
||||
const [altKeyDown, setAltKeyDown] = useState(false)
|
||||
const { toggleAppPane } = useResponsiveAppPane()
|
||||
|
||||
const toggleOn = (condition: (note: SNNote) => boolean) => {
|
||||
const notesMatchingAttribute = notes.filter(condition)
|
||||
const notesNotMatchingAttribute = notes.filter((note) => !condition(note))
|
||||
return notesMatchingAttribute.length > notesNotMatchingAttribute.length
|
||||
}
|
||||
|
||||
const hidePreviews = toggleOn((note) => note.hidePreview)
|
||||
const locked = toggleOn((note) => note.locked)
|
||||
const protect = toggleOn((note) => note.protected)
|
||||
const archived = notes.some((note) => note.archived)
|
||||
const unarchived = notes.some((note) => !note.archived)
|
||||
const trashed = notes.some((note) => note.trashed)
|
||||
const notTrashed = notes.some((note) => !note.trashed)
|
||||
const pinned = notes.some((note) => note.pinned)
|
||||
const unpinned = notes.some((note) => !note.pinned)
|
||||
const starred = notes.some((note) => note.starred)
|
||||
const { trashed, notTrashed, pinned, unpinned, starred, archived, unarchived, locked, protect, hidePreviews } =
|
||||
notesController.getNotesInfo(notes)
|
||||
|
||||
const editorForNote = useMemo(
|
||||
() => (notes[0] ? application.componentManager.editorForNote(notes[0]) : undefined),
|
||||
@@ -85,55 +68,6 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
}
|
||||
}, [application])
|
||||
|
||||
const [showExportSuperModal, setShowExportSuperModal] = useState(false)
|
||||
const closeSuperExportModal = useCallback(() => {
|
||||
setShowExportSuperModal(false)
|
||||
}, [])
|
||||
|
||||
const downloadSelectedItems = useCallback(async () => {
|
||||
if (notes.length === 0) {
|
||||
return
|
||||
}
|
||||
const toast = addToast({
|
||||
type: ToastType.Progress,
|
||||
message: `Exporting ${notes.length} ${pluralize(notes.length, 'note', 'notes')}...`,
|
||||
})
|
||||
try {
|
||||
const result = await createNoteExport(application, notes)
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
const { blob, fileName } = result
|
||||
void downloadOrShareBlobBasedOnPlatform({
|
||||
archiveService: application.archiveService,
|
||||
platform: application.platform,
|
||||
mobileDevice: application.mobileDevice,
|
||||
blob: blob,
|
||||
filename: fileName,
|
||||
isNativeMobileWeb: application.isNativeMobileWeb(),
|
||||
})
|
||||
dismissToast(toast)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
addToast({
|
||||
type: ToastType.Error,
|
||||
message: 'Could not export notes',
|
||||
})
|
||||
dismissToast(toast)
|
||||
}
|
||||
}, [application, notes])
|
||||
|
||||
const exportSelectedItems = useCallback(() => {
|
||||
const hasSuperNote = notes.some((note) => note.noteType === NoteType.Super)
|
||||
|
||||
if (hasSuperNote) {
|
||||
setShowExportSuperModal(true)
|
||||
return
|
||||
}
|
||||
|
||||
downloadSelectedItems().catch(console.error)
|
||||
}, [downloadSelectedItems, notes])
|
||||
|
||||
const shareSelectedItems = useCallback(() => {
|
||||
createNoteExport(application, notes)
|
||||
.then((result) => {
|
||||
@@ -158,37 +92,14 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
closeMenu()
|
||||
}, [closeMenu, toggleAppPane])
|
||||
|
||||
const duplicateSelectedItems = useCallback(async () => {
|
||||
await Promise.all(
|
||||
notes.map((note) =>
|
||||
application.mutator
|
||||
.duplicateItem(note)
|
||||
.then((duplicated) =>
|
||||
addToast({
|
||||
type: ToastType.Regular,
|
||||
message: `Duplicated note "${duplicated.title}"`,
|
||||
actions: [
|
||||
{
|
||||
label: 'Open',
|
||||
handler: (toastId) => {
|
||||
application.itemListController.selectUuids([duplicated.uuid], true).catch(console.error)
|
||||
dismissToast(toastId)
|
||||
},
|
||||
},
|
||||
],
|
||||
autoClose: true,
|
||||
}),
|
||||
)
|
||||
.catch(console.error),
|
||||
),
|
||||
)
|
||||
void application.sync.sync()
|
||||
const duplicateSelectedNotes = useCallback(async () => {
|
||||
await notesController.duplicateSelectedNotes()
|
||||
closeMenuAndToggleNotesList()
|
||||
}, [application.mutator, application.itemListController, application.sync, closeMenuAndToggleNotesList, notes])
|
||||
}, [closeMenuAndToggleNotesList, notesController])
|
||||
|
||||
const openRevisionHistoryModal = useCallback(() => {
|
||||
application.historyModalController.openModal(application.notesController.firstSelectedNote)
|
||||
}, [application.historyModalController, application.notesController.firstSelectedNote])
|
||||
application.historyModalController.openModal(notesController.firstSelectedNote)
|
||||
}, [application.historyModalController, notesController.firstSelectedNote])
|
||||
|
||||
const historyShortcut = useMemo(
|
||||
() => application.keyboardService.keyboardShortcutForCommand(OPEN_NOTE_HISTORY_COMMAND),
|
||||
@@ -259,7 +170,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
<MenuSwitchButtonItem
|
||||
checked={locked}
|
||||
onChange={(locked) => {
|
||||
application.notesController.setLockSelectedNotes(locked)
|
||||
notesController.setLockSelectedNotes(locked)
|
||||
}}
|
||||
disabled={areSomeNotesInReadonlySharedVault}
|
||||
>
|
||||
@@ -269,7 +180,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
<MenuSwitchButtonItem
|
||||
checked={!hidePreviews}
|
||||
onChange={(hidePreviews) => {
|
||||
application.notesController.setHideSelectedNotePreviews(!hidePreviews)
|
||||
notesController.setHideSelectedNotePreviews(!hidePreviews)
|
||||
}}
|
||||
disabled={areSomeNotesInReadonlySharedVault}
|
||||
>
|
||||
@@ -279,7 +190,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
<MenuSwitchButtonItem
|
||||
checked={protect}
|
||||
onChange={(protect) => {
|
||||
application.notesController.setProtectSelectedNotes(protect).catch(console.error)
|
||||
notesController.setProtectSelectedNotes(protect).catch(console.error)
|
||||
}}
|
||||
disabled={areSomeNotesInReadonlySharedVault}
|
||||
>
|
||||
@@ -318,7 +229,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
application.notesController.setStarSelectedNotes(!starred)
|
||||
notesController.setStarSelectedNotes(!starred)
|
||||
}}
|
||||
disabled={areSomeNotesInReadonlySharedVault}
|
||||
>
|
||||
@@ -330,7 +241,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
{unpinned && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
application.notesController.setPinSelectedNotes(true)
|
||||
notesController.setPinSelectedNotes(true)
|
||||
}}
|
||||
disabled={areSomeNotesInReadonlySharedVault}
|
||||
>
|
||||
@@ -342,7 +253,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
{pinned && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
application.notesController.setPinSelectedNotes(false)
|
||||
notesController.setPinSelectedNotes(false)
|
||||
}}
|
||||
disabled={areSomeNotesInReadonlySharedVault}
|
||||
>
|
||||
@@ -351,7 +262,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
{pinShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={pinShortcut} />}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={exportSelectedItems}>
|
||||
<MenuItem onClick={notesController.exportSelectedNotes}>
|
||||
<Icon type="download" className={iconClass} />
|
||||
Export
|
||||
</MenuItem>
|
||||
@@ -361,14 +272,14 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
Share
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={duplicateSelectedItems} disabled={areSomeNotesInReadonlySharedVault}>
|
||||
<MenuItem onClick={duplicateSelectedNotes} disabled={areSomeNotesInReadonlySharedVault}>
|
||||
<Icon type="copy" className={iconClass} />
|
||||
Duplicate
|
||||
</MenuItem>
|
||||
{unarchived && (
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
await application.notesController.setArchiveSelectedNotes(true).catch(console.error)
|
||||
await notesController.setArchiveSelectedNotes(true).catch(console.error)
|
||||
closeMenuAndToggleNotesList()
|
||||
}}
|
||||
disabled={areSomeNotesInReadonlySharedVault}
|
||||
@@ -380,7 +291,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
{archived && (
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
await application.notesController.setArchiveSelectedNotes(false).catch(console.error)
|
||||
await notesController.setArchiveSelectedNotes(false).catch(console.error)
|
||||
closeMenuAndToggleNotesList()
|
||||
}}
|
||||
disabled={areSomeNotesInReadonlySharedVault}
|
||||
@@ -394,7 +305,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
<MenuItem
|
||||
disabled={areSomeNotesInReadonlySharedVault}
|
||||
onClick={async () => {
|
||||
await application.notesController.deleteNotesPermanently()
|
||||
await notesController.deleteNotesPermanently()
|
||||
closeMenuAndToggleNotesList()
|
||||
}}
|
||||
>
|
||||
@@ -404,7 +315,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
) : (
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
await application.notesController.setTrashSelectedNotes(true)
|
||||
await notesController.setTrashSelectedNotes(true)
|
||||
closeMenuAndToggleNotesList()
|
||||
}}
|
||||
disabled={areSomeNotesInReadonlySharedVault}
|
||||
@@ -417,7 +328,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
await application.notesController.setTrashSelectedNotes(false)
|
||||
await notesController.setTrashSelectedNotes(false)
|
||||
closeMenuAndToggleNotesList()
|
||||
}}
|
||||
disabled={areSomeNotesInReadonlySharedVault}
|
||||
@@ -428,7 +339,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
<MenuItem
|
||||
disabled={areSomeNotesInReadonlySharedVault}
|
||||
onClick={async () => {
|
||||
await application.notesController.deleteNotesPermanently()
|
||||
await notesController.deleteNotesPermanently()
|
||||
closeMenuAndToggleNotesList()
|
||||
}}
|
||||
>
|
||||
@@ -437,7 +348,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
await application.notesController.emptyTrash()
|
||||
await notesController.emptyTrash()
|
||||
closeMenuAndToggleNotesList()
|
||||
}}
|
||||
disabled={areSomeNotesInReadonlySharedVault}
|
||||
@@ -446,7 +357,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
<Icon type="trash-sweep" className="mr-2 text-danger" />
|
||||
<div className="flex-row">
|
||||
<div className="text-danger">Empty Trash</div>
|
||||
<div className="text-xs">{application.notesController.trashedNotesCount} notes in Trash</div>
|
||||
<div className="text-xs">{notesController.trashedNotesCount} notes in Trash</div>
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
@@ -468,7 +379,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
<MenuSection>
|
||||
<SpellcheckOptions
|
||||
editorForNote={editorForNote}
|
||||
notesController={application.notesController}
|
||||
notesController={notesController}
|
||||
note={notes[0]}
|
||||
disabled={areSomeNotesInReadonlySharedVault}
|
||||
/>
|
||||
@@ -480,10 +391,6 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
<NoteSizeWarning note={notes[0]} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<ModalOverlay isOpen={showExportSuperModal} close={closeSuperExportModal} className="md:max-w-[25vw]">
|
||||
<SuperExportModal notes={notes} exportNotes={downloadSelectedItems} close={closeSuperExportModal} />
|
||||
</ModalOverlay>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PrefKey, PrefValue, SNNote } from '@standardnotes/snjs'
|
||||
import { PrefKey, PrefValue } from '@standardnotes/snjs'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import Modal from '../Modal/Modal'
|
||||
import usePreference from '@/Hooks/usePreference'
|
||||
@@ -6,15 +6,13 @@ import { useEffect } from 'react'
|
||||
import Switch from '../Switch/Switch'
|
||||
import { noteHasEmbeddedFiles } from '@/Utils/NoteExportUtils'
|
||||
import Dropdown from '../Dropdown/Dropdown'
|
||||
import ModalOverlay from '../Modal/ModalOverlay'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
type Props = {
|
||||
notes: SNNote[]
|
||||
exportNotes: () => void
|
||||
close: () => void
|
||||
}
|
||||
|
||||
const SuperExportModal = ({ notes, exportNotes, close }: Props) => {
|
||||
const ModalContent = observer(() => {
|
||||
const application = useApplication()
|
||||
const notesController = application.notesController
|
||||
const notes = notesController.selectedNotes
|
||||
|
||||
const superNoteExportFormat = usePreference(PrefKey.SuperNoteExportFormat)
|
||||
const superNoteExportEmbedBehavior = usePreference(PrefKey.SuperNoteExportEmbedBehavior)
|
||||
@@ -53,8 +51,8 @@ const SuperExportModal = ({ notes, exportNotes, close }: Props) => {
|
||||
label: 'Export',
|
||||
type: 'primary',
|
||||
onClick: () => {
|
||||
close()
|
||||
exportNotes()
|
||||
void notesController.downloadSelectedNotes()
|
||||
notesController.closeSuperExportModal()
|
||||
},
|
||||
mobileSlot: 'right',
|
||||
},
|
||||
@@ -157,6 +155,21 @@ const SuperExportModal = ({ notes, exportNotes, close }: Props) => {
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
})
|
||||
|
||||
const SuperExportModal = () => {
|
||||
const application = useApplication()
|
||||
const notesController = application.notesController
|
||||
|
||||
return (
|
||||
<ModalOverlay
|
||||
isOpen={notesController.shouldShowSuperExportModal}
|
||||
close={notesController.closeSuperExportModal}
|
||||
className="md:max-w-[25vw]"
|
||||
>
|
||||
<ModalContent />
|
||||
</ModalOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
export default SuperExportModal
|
||||
export default observer(SuperExportModal)
|
||||
|
||||
@@ -5,30 +5,30 @@ import { iconClass } from './ClassNames'
|
||||
import MenuSection from '../Menu/MenuSection'
|
||||
import { SUPER_SHOW_MARKDOWN_PREVIEW, SUPER_TOGGLE_SEARCH } from '@standardnotes/ui-services'
|
||||
import { useMemo, useCallback } from 'react'
|
||||
import { useCommandService } from '../CommandProvider'
|
||||
import { useKeyboardService } from '../KeyboardServiceProvider'
|
||||
|
||||
type Props = {
|
||||
closeMenu: () => void
|
||||
}
|
||||
|
||||
const SuperNoteOptions = ({ closeMenu }: Props) => {
|
||||
const commandService = useCommandService()
|
||||
const keyboardService = useKeyboardService()
|
||||
|
||||
const markdownShortcut = useMemo(
|
||||
() => commandService.keyboardShortcutForCommand(SUPER_SHOW_MARKDOWN_PREVIEW),
|
||||
[commandService],
|
||||
() => keyboardService.keyboardShortcutForCommand(SUPER_SHOW_MARKDOWN_PREVIEW),
|
||||
[keyboardService],
|
||||
)
|
||||
|
||||
const findShortcut = useMemo(() => commandService.keyboardShortcutForCommand(SUPER_TOGGLE_SEARCH), [commandService])
|
||||
const findShortcut = useMemo(() => keyboardService.keyboardShortcutForCommand(SUPER_TOGGLE_SEARCH), [keyboardService])
|
||||
|
||||
const enableSuperMarkdownPreview = useCallback(() => {
|
||||
commandService.triggerCommand(SUPER_SHOW_MARKDOWN_PREVIEW)
|
||||
}, [commandService])
|
||||
keyboardService.triggerCommand(SUPER_SHOW_MARKDOWN_PREVIEW)
|
||||
}, [keyboardService])
|
||||
|
||||
const findInNote = useCallback(() => {
|
||||
commandService.triggerCommand(SUPER_TOGGLE_SEARCH)
|
||||
keyboardService.triggerCommand(SUPER_TOGGLE_SEARCH)
|
||||
closeMenu()
|
||||
}, [closeMenu, commandService])
|
||||
}, [closeMenu, keyboardService])
|
||||
|
||||
return (
|
||||
<MenuSection>
|
||||
|
||||
@@ -4,7 +4,7 @@ import Icon from '@/Components/Icon/Icon'
|
||||
import { NotesController } from '@/Controllers/NotesController/NotesController'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { keyboardStringForShortcut, PIN_NOTE_COMMAND } from '@standardnotes/ui-services'
|
||||
import { useCommandService } from '../CommandProvider'
|
||||
import { useKeyboardService } from '../KeyboardServiceProvider'
|
||||
import { VisuallyHidden } from '@ariakit/react'
|
||||
|
||||
type Props = {
|
||||
@@ -24,11 +24,11 @@ const PinNoteButton: FunctionComponent<Props> = ({ className = '', notesControll
|
||||
notesController.togglePinSelectedNotes()
|
||||
}, [onClickPreprocessing, notesController])
|
||||
|
||||
const commandService = useCommandService()
|
||||
const keyboardService = useKeyboardService()
|
||||
|
||||
const shortcut = useMemo(
|
||||
() => keyboardStringForShortcut(commandService.keyboardShortcutForCommand(PIN_NOTE_COMMAND)),
|
||||
[commandService],
|
||||
() => keyboardStringForShortcut(keyboardService.keyboardShortcutForCommand(PIN_NOTE_COMMAND)),
|
||||
[keyboardService],
|
||||
)
|
||||
|
||||
const label = pinned ? `Unpin note (${shortcut})` : `Pin note (${shortcut})`
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user