mirror of
https://github.com/standardnotes/app
synced 2026-09-13 00:45:26 -04:00
Compare commits
54
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3ecf364d3 | ||
|
|
b43a97f08e | ||
|
|
54ddd37052 | ||
|
|
51025de17f | ||
|
|
9b02378283 | ||
|
|
be64dcba83 | ||
|
|
6977b7c0f0 | ||
|
|
789ed47774 | ||
|
|
2338449425 | ||
|
|
d6840ba41c | ||
|
|
50c42eda50 | ||
|
|
ea3b65af04 | ||
|
|
c964740d2b | ||
|
|
f5b69691c0 | ||
|
|
54af28aa04 | ||
|
|
cf4d2196de | ||
|
|
abf14d1e50 | ||
|
|
86c00056bc | ||
|
|
f77a6a4480 | ||
|
|
98038da99f | ||
|
|
b9d7f36884 | ||
|
|
19831e834e | ||
|
|
7e50b864ef | ||
|
|
687668c8e2 | ||
|
|
d135e09bda | ||
|
|
f284111a0c | ||
|
|
be109a0f6b | ||
|
|
c08d9034e1 | ||
|
|
010864881d | ||
|
|
2b41c8337b | ||
|
|
45bcc0e0b1 | ||
|
|
d928477d82 | ||
|
|
b86aebb7ab | ||
|
|
3cce44f5ab | ||
|
|
ff80775cd6 | ||
|
|
c75f655ffe | ||
|
|
87cf17d02f | ||
|
|
0d17961570 | ||
|
|
63d84725d2 | ||
|
|
9583d0db44 | ||
|
|
fa4182ba9f | ||
|
|
259017bd39 | ||
|
|
f1f9070dcc | ||
|
|
6e6b5a017b | ||
|
|
e72648d8a0 | ||
|
|
fef887ec0d | ||
|
|
62713cc49d | ||
|
|
8016cb39e9 | ||
|
|
9259cc8f32 | ||
|
|
43d31d8bb4 | ||
|
|
092dc7957f | ||
|
|
77f8bf6368 | ||
|
|
1935c98cf0 | ||
|
|
c52030a525 |
@@ -88,11 +88,18 @@ jobs:
|
||||
- name: Compile for AppImage
|
||||
run: yarn run webpack --config desktop.webpack.prod.js
|
||||
- name: AppImage
|
||||
run: yarn run electron-builder --linux --x64 -c.linux.target=AppImage --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
run: |
|
||||
yarn run electron-builder --linux --x64 -c.linux.target=AppImage --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
|
||||
- name: AppImageX64
|
||||
run: yarn run electron-builder --linux --x64 -c.linux.target=AppImage --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
run: |
|
||||
yarn run electron-builder --linux --x64 -c.linux.target=AppImage --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
- name: AppImageArm64
|
||||
run: yarn run electron-builder --linux --arm64 -c.linux.target=AppImage --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
run: |
|
||||
yarn workspace @standardnotes/desktop rebuild:home-server --arch arm64
|
||||
yarn run electron-builder --linux --arm64 -c.linux.target=AppImage --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
env:
|
||||
npm_config_target_arch: 'arm64'
|
||||
|
||||
- name: Upload
|
||||
uses: actions/[email protected]
|
||||
@@ -138,6 +145,7 @@ jobs:
|
||||
yarn run electron-builder --linux --x64 -c.linux.target=dir --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
- name: DirArm64
|
||||
run: |
|
||||
yarn workspace @standardnotes/desktop rebuild:home-server --arch arm64
|
||||
yarn run webpack --config desktop.webpack.prod.js
|
||||
yarn run electron-builder --linux --arm64 -c.linux.target=dir --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
env:
|
||||
@@ -181,6 +189,7 @@ jobs:
|
||||
npm_config_target_arch: 'arm64'
|
||||
USE_SYSTEM_FPM: 'true'
|
||||
run: |
|
||||
yarn workspace @standardnotes/desktop rebuild:home-server --arch arm64
|
||||
yarn run webpack --config desktop.webpack.prod.js --env deb
|
||||
yarn run electron-builder --linux --arm64 -c.linux.target=deb --publish=never --c.extraMetadata.version=${{ env.APP_VERSION }}
|
||||
|
||||
@@ -317,7 +326,7 @@ jobs:
|
||||
|
||||
- name: Certificates Sync
|
||||
run: |
|
||||
smctl windows certsync
|
||||
smctl windows certsync
|
||||
shell: cmd
|
||||
|
||||
- name: Build & Sign With Electron Builder
|
||||
@@ -373,8 +382,8 @@ jobs:
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
token: ${{ secrets.CI_PAT_TOKEN }}
|
||||
tag_name: "@standardnotes/desktop@${{ env.APP_VERSION }}"
|
||||
name: "Desktop ${{ env.APP_VERSION }}"
|
||||
tag_name: '@standardnotes/desktop@${{ env.APP_VERSION }}'
|
||||
name: 'Desktop ${{ env.APP_VERSION }}'
|
||||
body: ${{ steps.release-notes.outputs.result }}
|
||||
prerelease: true
|
||||
draft: false
|
||||
@@ -395,5 +404,3 @@ jobs:
|
||||
token: ${{ secrets.CI_PAT_TOKEN }}
|
||||
repository: ${{ secrets.RELEASES_EVENT_RECEIVING_REPO }}
|
||||
event-type: releases-updated-event
|
||||
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/mobile
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+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,70 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.1.548](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.547](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.546](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.545](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.544](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.543](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.542](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.541](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.540](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.539](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.538](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.537](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.536](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.535](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.534](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.533](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.532](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/clipper",
|
||||
"description": "Web clipper browser extension for Standard Notes",
|
||||
"version": "1.1.532",
|
||||
"version": "1.1.548",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build-mv2": "yarn clean && webpack --config ./webpack.config.prod.js",
|
||||
|
||||
@@ -3,6 +3,73 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.110.153](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.152](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.151](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.150](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.149](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.148](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.147](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.146](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixes issue where plaintext backups would use invalid special characters for filenames ([#2924](https://github.com/standardnotes/app/issues/2924)) ([45bcc0e](https://github.com/standardnotes/app/commit/45bcc0e0b1541558ecf410e9a92bc2db192cfe93))
|
||||
* Fixes portions of window not being draggable on desktop ([#2925](https://github.com/standardnotes/app/issues/2925)) ([2b41c83](https://github.com/standardnotes/app/commit/2b41c8337b1408209dc4903cacbc701b73c590b0))
|
||||
|
||||
## [3.110.145](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.144](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.143](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.142](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.141](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.140](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.139](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.138](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.137](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
@@ -17,6 +17,7 @@ import { FileReadOperation } from './FileReadOperation'
|
||||
import { Paths } from '../Types/Paths'
|
||||
import { MessageToWebApp } from '../../Shared/IpcMessages'
|
||||
import { FilesManagerInterface } from '../File/FilesManagerInterface'
|
||||
import { sanitizeFileName } from '@standardnotes/utils'
|
||||
|
||||
const TextBackupFileExtension = '.txt'
|
||||
|
||||
@@ -316,7 +317,8 @@ export class FilesBackupManager implements FileBackupsDevice {
|
||||
|
||||
const relativePath = forTag ?? ''
|
||||
const filenameWithSlashesEscaped = filename.replace(/\//g, '\u2215')
|
||||
const fileAbsolutePath = path.join(absolutePath, relativePath, filenameWithSlashesEscaped)
|
||||
const sanitizedFilename = sanitizeFileName(filenameWithSlashesEscaped)
|
||||
const fileAbsolutePath = path.join(absolutePath, relativePath, sanitizedFilename)
|
||||
await this.filesManager.writeFile(fileAbsolutePath, data)
|
||||
|
||||
const existingRecord = findMappingRecord(forTag)
|
||||
|
||||
@@ -70,12 +70,19 @@
|
||||
|
||||
/* https://github.com/electron/electron/issues/37789 */
|
||||
.mac-desktop #navigation,
|
||||
.mac-desktop #items-column,
|
||||
.mac-desktop #editor-column,
|
||||
.mac-desktop #items-title-bar,
|
||||
.mac-desktop #editor-title-bar,
|
||||
.mac-desktop [data-preferences-header] {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.mac-desktop #items-column > *:not(#items-title-bar),
|
||||
.mac-desktop #editor-column > *:not(#editor-title-bar) {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.mac-desktop.focus-mode #editor-title-bar {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/desktop",
|
||||
"main": "./app/dist/index.js",
|
||||
"version": "3.110.137",
|
||||
"version": "3.110.153",
|
||||
"license": "AGPL-3.0",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
@@ -55,7 +55,7 @@
|
||||
"@babel/core": "*",
|
||||
"@babel/preset-env": "*",
|
||||
"@electron/notarize": "^3.0.0",
|
||||
"@electron/rebuild": "3.6.0",
|
||||
"@electron/rebuild": "3.6.1",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/lodash": "^4.14.189",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
|
||||
@@ -3,6 +3,76 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.58.211](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.210](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.209](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.208](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.207](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.206](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.205](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.204](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.203](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.202](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.201](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.200](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.199](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.198](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.197](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.196](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.195](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove extra bottom padding on Android ([#2915](https://github.com/standardnotes/app/issues/2915)) ([c52030a](https://github.com/standardnotes/app/commit/c52030a525eaf3c8488c19beb84229911bcda8fc))
|
||||
|
||||
## [3.58.194](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
@@ -38,8 +38,10 @@ newArchEnabled=true
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
|
||||
# Set AsyncStorage limit
|
||||
AsyncStorage_db_size_in_MB=50
|
||||
# Use Next storage implementation for AsyncStorage
|
||||
AsyncStorage_useNextStorage=true
|
||||
AsyncStorage_kotlinVersion=2.0.21
|
||||
AsyncStorage_next_kspVersion=2.0.21-1.0.28
|
||||
|
||||
# The URL of the server
|
||||
host=https://app.standardnotes.com
|
||||
|
||||
@@ -68,7 +68,7 @@ platform :ios do
|
||||
run_precheck_before_submit: false,
|
||||
submission_information: { add_id_info_uses_idfa: false },
|
||||
release_notes: {
|
||||
'default': 'Fixes and improvements.'
|
||||
'en-US': 'Fixes and improvements.'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@standardnotes/mobile",
|
||||
"version": "3.58.194",
|
||||
"version": "3.58.211",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"android-dev": "react-native run-android --mode devDebug --appIdSuffix dev",
|
||||
"android-prod-debug": "react-native run-android --variant prodDebug",
|
||||
"android-prod-release": "yarn run android:bundle && react-native run-android --variant prodRelease",
|
||||
"android-prod-debug": "react-native run-android --mode prodDebug",
|
||||
"android-prod-release": "yarn run android:bundle && react-native run-android --mode prodRelease",
|
||||
"android:bundle": "react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/build/intermediates/res/merged/release/",
|
||||
"build": "yarn bundle:web && yarn install:pods && yarn android:bundle",
|
||||
"clear-cache": "watchman watch-del-all && rm -rf $TMPDIR/react-native-packager-cache-* && rm -rf $TMPDIR/metro-bundler-cache-*",
|
||||
|
||||
@@ -3,6 +3,74 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.851](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.850](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.849](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.848](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.847](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.846](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.845](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.844](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.843](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.842](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.841](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.840](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.839](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.838](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.837](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.836](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.835](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.834](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/releases",
|
||||
"version": "1.4.834",
|
||||
"version": "1.4.851",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "dist/releases.json",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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.71.11",
|
||||
"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
|
||||
|
||||
@@ -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,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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`,
|
||||
|
||||
@@ -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.209.14",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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.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.38.14",
|
||||
"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]
|
||||
|
||||
@@ -3,6 +3,86 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.199.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-23)
|
||||
|
||||
### Features
|
||||
|
||||
* Improves Add Tag dropdown UI ([#2936](https://github.com/standardnotes/app/issues/2936)) ([54ddd37](https://github.com/standardnotes/app/commit/54ddd37052c202c808d0e3bef8bd0df7fc72b148))
|
||||
|
||||
## [3.198.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.198.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.198.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.198.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.198.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.198.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixes Share note option not working on Android for note titles with special characters ([#2928](https://github.com/standardnotes/app/issues/2928)) ([d135e09](https://github.com/standardnotes/app/commit/d135e09bda39814585a3d76b0544ed8b5834b4d0))
|
||||
|
||||
## [3.198.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixes Back and Navigation buttons not working on mobile view when panels explicitly collapsed ([#2926](https://github.com/standardnotes/app/issues/2926)) ([0108648](https://github.com/standardnotes/app/commit/010864881d0c056e88dae2394fb398c0f1e822a5))
|
||||
|
||||
## [3.198.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Prevents quick type suggestions on iOS for password input ([ff80775](https://github.com/standardnotes/app/commit/ff80775cd6f09863429f053e590a054c6e45722c))
|
||||
|
||||
## [3.198.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed font size not updating on plain editor when changing it from Preferences ([#2920](https://github.com/standardnotes/app/issues/2920)) ([0d17961](https://github.com/standardnotes/app/commit/0d17961570cdadbbb909709075bdef3b8d77434b))
|
||||
* Fixes extra bottom padding in account modal ([63d8472](https://github.com/standardnotes/app/commit/63d84725d2084d014de6aa73e74ba77a7e343484))
|
||||
* Fixes quick type suggestions showing up in iOS for password field ([9583d0d](https://github.com/standardnotes/app/commit/9583d0db44ba315e125fbc36b50a8da527ae3a89))
|
||||
|
||||
## [3.198.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.198.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.198.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.198.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.198.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed issues with embedded linked items in Super editor ([#2918](https://github.com/standardnotes/app/issues/2918)) ([43d31d8](https://github.com/standardnotes/app/commit/43d31d8bb44644b0d78d8a7f543da790b30e1eea))
|
||||
|
||||
## [3.198.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove extra bottom padding on Android ([#2915](https://github.com/standardnotes/app/issues/2915)) ([c52030a](https://github.com/standardnotes/app/commit/c52030a525eaf3c8488c19beb84229911bcda8fc))
|
||||
|
||||
## [3.198.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,5 +1,206 @@
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"version": "3.199.0",
|
||||
"title": "[3.199.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-23)",
|
||||
"date": null,
|
||||
"body": "### Features\n\n* Improves Add Tag dropdown UI ([#2936](https://github.com/standardnotes/app/issues/2936)) ([54ddd37](https://github.com/standardnotes/app/commit/54ddd37052c202c808d0e3bef8bd0df7fc72b148))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Improves Add Tag dropdown UI (#2936) (54ddd37)"
|
||||
],
|
||||
"Features": [
|
||||
"Improves Add Tag dropdown UI (#2936) (54ddd37)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.20",
|
||||
"title": "[3.198.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-15)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.19",
|
||||
"title": "[3.198.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-12)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.18",
|
||||
"title": "[3.198.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-09-04)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.17",
|
||||
"title": "[3.198.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-26)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.16",
|
||||
"title": "[3.198.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-25)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.15",
|
||||
"title": "[3.198.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-11)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Fixes Share note option not working on Android for note titles with special characters ([#2928](https://github.com/standardnotes/app/issues/2928)) ([d135e09](https://github.com/standardnotes/app/commit/d135e09bda39814585a3d76b0544ed8b5834b4d0))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Fixes Share note option not working on Android for note titles with special characters (#2928) (d135e09)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Fixes Share note option not working on Android for note titles with special characters (#2928) (d135e09)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.14",
|
||||
"title": "[3.198.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-05)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Fixes Back and Navigation buttons not working on mobile view when panels explicitly collapsed ([#2926](https://github.com/standardnotes/app/issues/2926)) ([0108648](https://github.com/standardnotes/app/commit/010864881d0c056e88dae2394fb398c0f1e822a5))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Fixes Back and Navigation buttons not working on mobile view when panels explicitly collapsed (#2926) (0108648)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Fixes Back and Navigation buttons not working on mobile view when panels explicitly collapsed (#2926) (0108648)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.13",
|
||||
"title": "[3.198.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-08-04)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Prevents quick type suggestions on iOS for password input ([ff80775](https://github.com/standardnotes/app/commit/ff80775cd6f09863429f053e590a054c6e45722c))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Prevents quick type suggestions on iOS for password input (ff80775)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Prevents quick type suggestions on iOS for password input (ff80775)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.12",
|
||||
"title": "[3.198.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-29)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Fixed font size not updating on plain editor when changing it from Preferences ([#2920](https://github.com/standardnotes/app/issues/2920)) ([0d17961](https://github.com/standardnotes/app/commit/0d17961570cdadbbb909709075bdef3b8d77434b))\n* Fixes extra bottom padding in account modal ([63d8472](https://github.com/standardnotes/app/commit/63d84725d2084d014de6aa73e74ba77a7e343484))\n* Fixes quick type suggestions showing up in iOS for password field ([9583d0d](https://github.com/standardnotes/app/commit/9583d0db44ba315e125fbc36b50a8da527ae3a89))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Fixed font size not updating on plain editor when changing it from Preferences (#2920) (0d17961)",
|
||||
"Fixes extra bottom padding in account modal (63d8472)",
|
||||
"Fixes quick type suggestions showing up in iOS for password field (9583d0d)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Fixed font size not updating on plain editor when changing it from Preferences (#2920) (0d17961)",
|
||||
"Fixes extra bottom padding in account modal (63d8472)",
|
||||
"Fixes quick type suggestions showing up in iOS for password field (9583d0d)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.11",
|
||||
"title": "[3.198.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-23)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.10",
|
||||
"title": "[3.198.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.9",
|
||||
"title": "[3.198.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.8",
|
||||
"title": "[3.198.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.7",
|
||||
"title": "[3.198.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-22)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Fixed issues with embedded linked items in Super editor ([#2918](https://github.com/standardnotes/app/issues/2918)) ([43d31d8](https://github.com/standardnotes/app/commit/43d31d8bb44644b0d78d8a7f543da790b30e1eea))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Fixed issues with embedded linked items in Super editor (#2918) (43d31d8)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Fixed issues with embedded linked items in Super editor (#2918) (43d31d8)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.6",
|
||||
"title": "[3.198.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-19)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Remove extra bottom padding on Android ([#2915](https://github.com/standardnotes/app/issues/2915)) ([c52030a](https://github.com/standardnotes/app/commit/c52030a525eaf3c8488c19beb84229911bcda8fc))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Remove extra bottom padding on Android (#2915) (c52030a)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Remove extra bottom padding on Android (#2915) (c52030a)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.198.5",
|
||||
"title": "[3.198.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2025-07-15)",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@standardnotes/web",
|
||||
"version": "3.198.5",
|
||||
"version": "3.199.0",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "dist/app.js",
|
||||
"author": "Standard Notes.",
|
||||
"author": "Standard Notes",
|
||||
"private": true,
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -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} />]}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -56,8 +56,10 @@ const LinkedItemBubble = ({
|
||||
setShowUnlinkButton(true)
|
||||
}
|
||||
|
||||
const onBlur = () => {
|
||||
setShowUnlinkButton(false)
|
||||
const onBlur: React.FocusEventHandler<HTMLButtonElement | HTMLAnchorElement> = (event) => {
|
||||
if (!event.currentTarget.contains(event.relatedTarget)) {
|
||||
setShowUnlinkButton(false)
|
||||
}
|
||||
setWasClicked(false)
|
||||
}
|
||||
|
||||
@@ -131,7 +133,7 @@ const LinkedItemBubble = ({
|
||||
<button
|
||||
ref={(el) => (unlinkButtonRef.current = el as HTMLElement)}
|
||||
role="button"
|
||||
className="-mr-1 ml-2 inline-flex cursor-pointer border-0 bg-transparent p-0 h-[1.15em] align-middle"
|
||||
className="-mr-1 ml-2 inline-flex h-[1.15em] cursor-pointer border-0 bg-transparent p-0 align-middle"
|
||||
onClick={onUnlinkClick}
|
||||
>
|
||||
<Icon type="close" className="text-neutral hover:text-info" size="small" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useMediaQuery, MutuallyExclusiveMediaQueryBreakpoints } from '@/Hooks/u
|
||||
import { SupportsPassiveListeners } from '@/Constants/Constants'
|
||||
import { useLifecycleAnimation } from '@/Hooks/useLifecycleAnimation'
|
||||
import { getScrollParent } from '@/Utils'
|
||||
import { useAvailableSafeAreaPadding } from '@/Hooks/useSafeAreaPadding'
|
||||
|
||||
const DisableScroll = () => {
|
||||
useDisableBodyScrollOnMobile()
|
||||
@@ -63,6 +64,8 @@ const MobilePopoverContent = ({
|
||||
},
|
||||
})
|
||||
|
||||
const { hasBottomInset } = useAvailableSafeAreaPadding()
|
||||
|
||||
useEffect(() => {
|
||||
if (!element) {
|
||||
return
|
||||
@@ -181,7 +184,8 @@ const MobilePopoverContent = ({
|
||||
<div
|
||||
ref={mergeRefs([setPopoverElement, addCloseMethod])}
|
||||
className={classNames(
|
||||
'z-1 absolute bottom-0 flex max-h-[calc(100%_-_max(var(--safe-area-inset-top),2rem))] min-h-[40%] w-full flex-col rounded-t-xl bg-passive-5 pb-safe-bottom',
|
||||
'z-1 absolute bottom-0 flex max-h-[calc(100%_-_max(var(--safe-area-inset-top),2rem))] min-h-[40%] w-full flex-col rounded-t-xl bg-passive-5',
|
||||
hasBottomInset && 'pb-safe-bottom',
|
||||
forceFullHeightOnMobile && 'h-full',
|
||||
)}
|
||||
style={{
|
||||
|
||||
@@ -10,13 +10,13 @@ const RecoveryCodeBanner = ({ application }: { application: WebApplication }) =>
|
||||
const [errorMessage, setErrorMessage] = useState<string>()
|
||||
|
||||
const onClickShow = async () => {
|
||||
const authorized = await application.challenges.promptForAccountPassword()
|
||||
const password = await application.challenges.promptForAccountPassword()
|
||||
|
||||
if (!authorized) {
|
||||
if (!password) {
|
||||
return
|
||||
}
|
||||
|
||||
const recoveryCodeOrError = await application.getRecoveryCodes.execute()
|
||||
const recoveryCodeOrError = await application.getRecoveryCodes.execute({ password })
|
||||
if (recoveryCodeOrError.isFailed()) {
|
||||
setErrorMessage(recoveryCodeOrError.getError())
|
||||
return
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { useState, useCallback, KeyboardEventHandler, useRef } from 'react'
|
||||
import { useCallback, KeyboardEventHandler, useRef } from 'react'
|
||||
import SearchOptions from '@/Components/SearchOptions/SearchOptions'
|
||||
import { SearchOptionsController } from '@/Controllers/SearchOptionsController'
|
||||
import Icon from '../Icon/Icon'
|
||||
@@ -8,6 +8,7 @@ import DecoratedInput from '../Input/DecoratedInput'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import ClearInputButton from '../ClearInputButton/ClearInputButton'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
|
||||
type Props = {
|
||||
itemListController: ItemListController
|
||||
@@ -16,12 +17,11 @@ type Props = {
|
||||
}
|
||||
|
||||
const SearchBar = ({ itemListController, searchOptionsController, hideOptions = false }: Props) => {
|
||||
const searchBarRef = useRef<HTMLDivElement>(null)
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const { noteFilterText, setNoteFilterText, clearFilterText, onFilterEnter } = itemListController
|
||||
|
||||
const [focusedSearch, setFocusedSearch] = useState(false)
|
||||
|
||||
const onNoteFilterTextChange = useCallback(
|
||||
(text: string) => {
|
||||
setNoteFilterText(text)
|
||||
@@ -38,16 +38,13 @@ const SearchBar = ({ itemListController, searchOptionsController, hideOptions =
|
||||
[onFilterEnter],
|
||||
)
|
||||
|
||||
const onSearchFocus = useCallback(() => setFocusedSearch(true), [])
|
||||
const onSearchBlur = useCallback(() => setFocusedSearch(false), [])
|
||||
|
||||
const onClearSearch = useCallback(() => {
|
||||
clearFilterText()
|
||||
searchInputRef.current?.focus()
|
||||
}, [clearFilterText])
|
||||
|
||||
return (
|
||||
<div className="pb-0.5 pt-3" role="search">
|
||||
<div className="group pb-0.5 pt-3" role="search" ref={searchBarRef}>
|
||||
<DecoratedInput
|
||||
autocomplete={false}
|
||||
id={ElementIds.SearchBar}
|
||||
@@ -58,20 +55,21 @@ const SearchBar = ({ itemListController, searchOptionsController, hideOptions =
|
||||
placeholder={'Search...'}
|
||||
value={noteFilterText}
|
||||
ref={searchInputRef}
|
||||
onBlur={onSearchBlur}
|
||||
onChange={onNoteFilterTextChange}
|
||||
onFocus={onSearchFocus}
|
||||
onKeyUp={onNoteFilterKeyUp}
|
||||
left={[<Icon type="search" className="mr-1 h-4.5 w-4.5 flex-shrink-0 text-passive-1" />]}
|
||||
right={[noteFilterText && <ClearInputButton onClick={onClearSearch} />]}
|
||||
roundedFull
|
||||
/>
|
||||
|
||||
{(focusedSearch || noteFilterText) && !hideOptions && (
|
||||
<div className="animate-fade-from-top">
|
||||
<SearchOptions searchOptions={searchOptionsController} />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
'animate-fade-from-top',
|
||||
hideOptions ? 'hidden' : !noteFilterText && 'hidden group-focus-within:flex',
|
||||
)}
|
||||
>
|
||||
<SearchOptions searchOptions={searchOptionsController} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,9 +12,6 @@ import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin'
|
||||
import { ListPlugin } from '@lexical/react/LexicalListPlugin'
|
||||
import { EditorState, LexicalEditor } from 'lexical'
|
||||
import HorizontalRulePlugin from './Plugins/HorizontalRulePlugin'
|
||||
import TwitterPlugin from './Plugins/TwitterPlugin'
|
||||
import YouTubePlugin from './Plugins/YouTubePlugin'
|
||||
import AutoEmbedPlugin from './Plugins/AutoEmbedPlugin'
|
||||
import CollapsiblePlugin from './Plugins/CollapsiblePlugin'
|
||||
import DraggableBlockPlugin from './Plugins/DraggableBlockPlugin'
|
||||
import CodeHighlightPlugin from './Plugins/CodeHighlightPlugin'
|
||||
@@ -125,9 +122,6 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
||||
<CodeHighlightPlugin />
|
||||
<LinkPlugin />
|
||||
<HashtagPlugin />
|
||||
<AutoEmbedPlugin />
|
||||
<TwitterPlugin />
|
||||
<YouTubePlugin />
|
||||
<CollapsiblePlugin />
|
||||
<TabIndentationPlugin />
|
||||
<RemoveBrokenTablesPlugin />
|
||||
|
||||
@@ -20,22 +20,6 @@ import type {
|
||||
|
||||
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
|
||||
import { DecoratorBlockNode, SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
const WIDGET_SCRIPT_URL = '/dist/twitter-widgets.js'
|
||||
|
||||
type TweetComponentProps = Readonly<{
|
||||
className: Readonly<{
|
||||
base: string
|
||||
focus: string
|
||||
}>
|
||||
format: ElementFormatType | null
|
||||
loadingComponent?: JSX.Element | string
|
||||
nodeKey: NodeKey
|
||||
onError?: (error: string) => void
|
||||
onLoad?: () => void
|
||||
tweetID: string
|
||||
}>
|
||||
|
||||
function convertTweetElement(domNode: HTMLDivElement): DOMConversionOutput | null {
|
||||
const id = domNode.getAttribute('data-lexical-tweet-id')
|
||||
@@ -46,71 +30,6 @@ function convertTweetElement(domNode: HTMLDivElement): DOMConversionOutput | nul
|
||||
return null
|
||||
}
|
||||
|
||||
let isTwitterScriptLoading = true
|
||||
|
||||
function TweetComponent({
|
||||
className,
|
||||
format,
|
||||
loadingComponent,
|
||||
nodeKey,
|
||||
onError,
|
||||
onLoad,
|
||||
tweetID,
|
||||
}: TweetComponentProps) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const previousTweetIDRef = useRef<string>('')
|
||||
const [isTweetLoading, setIsTweetLoading] = useState(false)
|
||||
|
||||
const createTweet = useCallback(async () => {
|
||||
try {
|
||||
// @ts-expect-error Twitter is attached to the window.
|
||||
await window.twttr.widgets.createTweet(tweetID, containerRef.current)
|
||||
|
||||
setIsTweetLoading(false)
|
||||
isTwitterScriptLoading = false
|
||||
|
||||
if (onLoad) {
|
||||
onLoad()
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(String(error))
|
||||
}
|
||||
}
|
||||
}, [onError, onLoad, tweetID])
|
||||
|
||||
useEffect(() => {
|
||||
if (tweetID !== previousTweetIDRef.current) {
|
||||
setIsTweetLoading(true)
|
||||
|
||||
if (isTwitterScriptLoading) {
|
||||
const script = document.createElement('script')
|
||||
script.src = WIDGET_SCRIPT_URL
|
||||
script.async = true
|
||||
document.body?.appendChild(script)
|
||||
script.onload = createTweet
|
||||
if (onError) {
|
||||
script.onerror = onError as OnErrorEventHandler
|
||||
}
|
||||
} else {
|
||||
createTweet().catch(console.error)
|
||||
}
|
||||
|
||||
if (previousTweetIDRef) {
|
||||
previousTweetIDRef.current = tweetID
|
||||
}
|
||||
}
|
||||
}, [createTweet, onError, tweetID])
|
||||
|
||||
return (
|
||||
<BlockWithAlignableContents className={className} format={format} nodeKey={nodeKey}>
|
||||
{isTweetLoading ? loadingComponent : null}
|
||||
<div style={{ display: 'inline-block', width: '550px' }} ref={containerRef} />
|
||||
</BlockWithAlignableContents>
|
||||
)
|
||||
}
|
||||
|
||||
export type SerializedTweetNode = Spread<
|
||||
{
|
||||
id: string
|
||||
@@ -172,7 +91,7 @@ export class TweetNode extends DecoratorBlockNode {
|
||||
}
|
||||
|
||||
override getTextContent(_includeInert?: boolean | undefined, _includeDirectionless?: false | undefined): string {
|
||||
return `https://twitter.com/i/web/status/${this.__id}`
|
||||
return `https://x.com/i/web/status/${this.__id}`
|
||||
}
|
||||
|
||||
override decorate(_: LexicalEditor, config: EditorConfig): JSX.Element {
|
||||
@@ -181,14 +100,13 @@ export class TweetNode extends DecoratorBlockNode {
|
||||
base: embedBlockTheme.base || '',
|
||||
focus: embedBlockTheme.focus || '',
|
||||
}
|
||||
const link = this.getTextContent()
|
||||
return (
|
||||
<TweetComponent
|
||||
className={className}
|
||||
format={this.__format}
|
||||
loadingComponent="Loading..."
|
||||
nodeKey={this.getKey()}
|
||||
tweetID={this.__id}
|
||||
/>
|
||||
<BlockWithAlignableContents className={className} format={this.__format} nodeKey={this.getKey()}>
|
||||
<a href={link} target="_blank" rel="noreferrer noopener">
|
||||
{link}
|
||||
</a>
|
||||
</BlockWithAlignableContents>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
:root {
|
||||
--lexical-ordered-list-left-margin: 16px;
|
||||
|
||||
--checkbox-size: 18px;
|
||||
@media screen and (max-width: 450px) {
|
||||
--checkbox-size: 26px;
|
||||
}
|
||||
}
|
||||
.monospace-font {
|
||||
--lexical-ordered-list-left-margin: 42px;
|
||||
@@ -52,11 +57,19 @@
|
||||
.Lexical__listItemChecked,
|
||||
.Lexical__listItemUnchecked {
|
||||
position: relative;
|
||||
padding-left: calc(var(--font-size) + 0.5rem);
|
||||
padding-right: calc(var(--font-size) + 0.5rem);
|
||||
padding-left: calc(var(--checkbox-size) + 0.5rem);
|
||||
padding-right: calc(var(--checkbox-size) + 0.5rem);
|
||||
list-style-type: none;
|
||||
outline: none;
|
||||
vertical-align: middle;
|
||||
--line-height: calc(var(--checkbox-size) + 0.375rem);
|
||||
line-height: var(--line-height);
|
||||
|
||||
@media screen and (max-width: 450px) {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
@@ -70,12 +83,12 @@
|
||||
}
|
||||
.Lexical__listItemUnchecked:before,
|
||||
.Lexical__listItemChecked:before {
|
||||
/* checkbox */
|
||||
content: '';
|
||||
--size: 16px;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
width: var(--checkbox-size);
|
||||
height: var(--checkbox-size);
|
||||
left: 0;
|
||||
top: calc(var(--line-height, 1) * var(--font-size) / 2);
|
||||
top: calc(var(--line-height) / 2);
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
background-size: cover;
|
||||
@@ -97,19 +110,19 @@
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.Lexical__listItemChecked:after {
|
||||
/* checkmark */
|
||||
content: '';
|
||||
cursor: pointer;
|
||||
border-color: var(--sn-stylekit-info-contrast-color);
|
||||
border-style: solid;
|
||||
background-image: url('data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2020%2020%22%20fill%3D%22white%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M17.5001%205.83345L7.50008%2015.8334L2.91675%2011.2501L4.09175%2010.0751L7.50008%2013.4751L16.3251%204.65845L17.5001%205.83345Z%22%2F%3E%3C%2Fsvg%3E%0A');
|
||||
background-repeat: no-repeat;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
--top: calc(var(--line-height) / 2);
|
||||
top: var(--top);
|
||||
transform: translateY(-50%);
|
||||
display: block;
|
||||
width: 5px;
|
||||
height: 11px;
|
||||
--top: calc(var(--line-height, 1) * var(--font-size) / 2);
|
||||
top: calc(var(--top) - 1px);
|
||||
left: 5px;
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
border-width: 0 2px 2px 0;
|
||||
width: var(--checkbox-size);
|
||||
height: var(--checkbox-size);
|
||||
}
|
||||
.Lexical__nestedListItem {
|
||||
list-style-type: none;
|
||||
|
||||
-306
@@ -1,306 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import type { LexicalEditor } from 'lexical'
|
||||
|
||||
import {
|
||||
AutoEmbedOption,
|
||||
EmbedConfig,
|
||||
EmbedMatchResult,
|
||||
LexicalAutoEmbedPlugin,
|
||||
URL_MATCHER,
|
||||
} from '@lexical/react/LexicalAutoEmbedPlugin'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import * as ReactDOM from 'react-dom'
|
||||
|
||||
import useModal from '../../Lexical/Hooks/useModal'
|
||||
import { INSERT_TWEET_COMMAND } from '../TwitterPlugin'
|
||||
import { INSERT_YOUTUBE_COMMAND } from '../YouTubePlugin'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { isMobileScreen } from '../../../../Utils'
|
||||
|
||||
interface PlaygroundEmbedConfig extends EmbedConfig {
|
||||
// Human readable name of the embeded content e.g. Tweet or Google Map.
|
||||
contentName: string
|
||||
|
||||
// Icon for display.
|
||||
icon?: JSX.Element
|
||||
iconName: string
|
||||
|
||||
// An example of a matching url https://twitter.com/jack/status/20
|
||||
exampleUrl: string
|
||||
|
||||
// For extra searching.
|
||||
keywords: Array<string>
|
||||
|
||||
// Embed a Figma Project.
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const YoutubeEmbedConfig: PlaygroundEmbedConfig = {
|
||||
contentName: 'Youtube Video',
|
||||
|
||||
exampleUrl: 'https://www.youtube.com/watch?v=jNQXAC9IVRw',
|
||||
|
||||
// Icon for display.
|
||||
icon: <i className="icon youtube" />,
|
||||
iconName: 'youtube',
|
||||
|
||||
insertNode: (editor: LexicalEditor, result: EmbedMatchResult) => {
|
||||
editor.dispatchCommand(INSERT_YOUTUBE_COMMAND, result.id)
|
||||
},
|
||||
|
||||
keywords: ['youtube', 'video'],
|
||||
|
||||
// Determine if a given URL is a match and return url data.
|
||||
parseUrl: (url: string) => {
|
||||
const match = /^.*(youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/.exec(url)
|
||||
|
||||
const id = match ? (match?.[2].length === 11 ? match[2] : null) : null
|
||||
|
||||
if (id != null) {
|
||||
return {
|
||||
id,
|
||||
url,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
type: 'youtube-video',
|
||||
}
|
||||
|
||||
export const TwitterEmbedConfig: PlaygroundEmbedConfig = {
|
||||
// e.g. Tweet or Google Map.
|
||||
contentName: 'Tweet',
|
||||
|
||||
exampleUrl: 'https://twitter.com/jack/status/20',
|
||||
|
||||
// Icon for display.
|
||||
icon: <i className="icon tweet" />,
|
||||
iconName: 'tweet',
|
||||
|
||||
// Create the Lexical embed node from the url data.
|
||||
insertNode: (editor: LexicalEditor, result: EmbedMatchResult) => {
|
||||
editor.dispatchCommand(INSERT_TWEET_COMMAND, result.id)
|
||||
},
|
||||
|
||||
// For extra searching.
|
||||
keywords: ['tweet', 'twitter'],
|
||||
|
||||
// Determine if a given URL is a match and return url data.
|
||||
parseUrl: (text: string) => {
|
||||
const match = /^https:\/\/twitter\.com\/(#!\/)?(\w+)\/status(es)*\/(\d+)$/.exec(text)
|
||||
|
||||
if (match != null) {
|
||||
return {
|
||||
id: match[4],
|
||||
url: match[0],
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
type: 'tweet',
|
||||
}
|
||||
|
||||
export const EmbedConfigs = [TwitterEmbedConfig, YoutubeEmbedConfig]
|
||||
|
||||
function AutoEmbedMenuItem({
|
||||
index,
|
||||
isSelected,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
option,
|
||||
}: {
|
||||
index: number
|
||||
isSelected: boolean
|
||||
onClick: () => void
|
||||
onMouseEnter: () => void
|
||||
option: AutoEmbedOption
|
||||
}) {
|
||||
return (
|
||||
<li
|
||||
key={option.key}
|
||||
tabIndex={-1}
|
||||
className={classNames('cursor-pointer rounded px-2 py-1', isSelected && 'bg-info-backdrop')}
|
||||
ref={option.setRefElement}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
id={'typeahead-item-' + index}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className="text">{option.title}</span>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function AutoEmbedMenu({
|
||||
options,
|
||||
selectedItemIndex,
|
||||
onOptionClick,
|
||||
onOptionMouseEnter,
|
||||
}: {
|
||||
selectedItemIndex: number | null
|
||||
onOptionClick: (option: AutoEmbedOption, index: number) => void
|
||||
onOptionMouseEnter: (index: number) => void
|
||||
options: Array<AutoEmbedOption>
|
||||
}) {
|
||||
return (
|
||||
<div className="typeahead-popover min-w-max rounded border border-border bg-default p-1">
|
||||
<ul className="list-none">
|
||||
{options.map((option: AutoEmbedOption, i: number) => (
|
||||
<AutoEmbedMenuItem
|
||||
index={i}
|
||||
isSelected={selectedItemIndex === i}
|
||||
onClick={() => onOptionClick(option, i)}
|
||||
onMouseEnter={() => onOptionMouseEnter(i)}
|
||||
key={option.key}
|
||||
option={option}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const debounce = (callback: (text: string) => void, delay: number) => {
|
||||
let timeoutId: number
|
||||
|
||||
return (text: string) => {
|
||||
window.clearTimeout(timeoutId)
|
||||
|
||||
timeoutId = window.setTimeout(() => {
|
||||
callback(text)
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
|
||||
export function AutoEmbedDialog({
|
||||
embedConfig,
|
||||
onClose,
|
||||
}: {
|
||||
embedConfig: PlaygroundEmbedConfig
|
||||
onClose: () => void
|
||||
}): JSX.Element {
|
||||
const [text, setText] = useState('')
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [embedResult, setEmbedResult] = useState<EmbedMatchResult | null>(null)
|
||||
|
||||
const validateText = useMemo(
|
||||
() =>
|
||||
debounce((inputText: string) => {
|
||||
const urlMatch = URL_MATCHER.exec(inputText)
|
||||
if (embedConfig != null && inputText != null && urlMatch != null) {
|
||||
void Promise.resolve(embedConfig.parseUrl(inputText)).then((parseResult) => {
|
||||
setEmbedResult(parseResult)
|
||||
})
|
||||
} else if (embedResult != null) {
|
||||
setEmbedResult(null)
|
||||
}
|
||||
}, 200),
|
||||
[embedConfig, embedResult],
|
||||
)
|
||||
|
||||
const onClick = () => {
|
||||
if (embedResult != null) {
|
||||
embedConfig.insertNode(editor, embedResult)
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const focusOnMount = useCallback((element: HTMLInputElement | null) => {
|
||||
if (element) {
|
||||
setTimeout(() => element.focus())
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<label className="flex flex-col gap-1.5">
|
||||
URL:
|
||||
<DecoratedInput
|
||||
value={text}
|
||||
onChange={(text) => {
|
||||
setText(text)
|
||||
validateText(text)
|
||||
}}
|
||||
ref={focusOnMount}
|
||||
/>
|
||||
</label>
|
||||
<div className="mt-2.5 flex justify-end">
|
||||
<Button disabled={!embedResult} onClick={onClick} small={isMobileScreen()}>
|
||||
Embed
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AutoEmbedPlugin(): JSX.Element {
|
||||
const [modal, showModal] = useModal()
|
||||
|
||||
const openEmbedModal = (embedConfig: PlaygroundEmbedConfig) => {
|
||||
showModal(`Embed ${embedConfig.contentName}`, (onClose) => (
|
||||
<AutoEmbedDialog embedConfig={embedConfig} onClose={onClose} />
|
||||
))
|
||||
}
|
||||
|
||||
const getMenuOptions = (activeEmbedConfig: PlaygroundEmbedConfig, embedFn: () => void, dismissFn: () => void) => {
|
||||
return [
|
||||
new AutoEmbedOption('Dismiss', {
|
||||
onSelect: dismissFn,
|
||||
}),
|
||||
new AutoEmbedOption(`Embed ${activeEmbedConfig.contentName}`, {
|
||||
onSelect: embedFn,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{modal}
|
||||
<LexicalAutoEmbedPlugin<PlaygroundEmbedConfig>
|
||||
embedConfigs={EmbedConfigs}
|
||||
onOpenEmbedModalForConfig={openEmbedModal}
|
||||
getMenuOptions={getMenuOptions}
|
||||
menuRenderFn={(anchorElementRef, { selectedIndex, options, selectOptionAndCleanUp, setHighlightedIndex }) => {
|
||||
return anchorElementRef.current
|
||||
? ReactDOM.createPortal(
|
||||
<div
|
||||
className="typeahead-popover auto-embed-menu"
|
||||
style={{
|
||||
marginLeft: anchorElementRef.current.style.width,
|
||||
}}
|
||||
>
|
||||
<AutoEmbedMenu
|
||||
options={options}
|
||||
selectedItemIndex={selectedIndex}
|
||||
onOptionClick={(option: AutoEmbedOption, index: number) => {
|
||||
setHighlightedIndex(index)
|
||||
selectOptionAndCleanUp(option)
|
||||
}}
|
||||
onOptionMouseEnter={(index: number) => {
|
||||
setHighlightedIndex(index)
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
anchorElementRef.current,
|
||||
)
|
||||
: null
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
-2
@@ -28,7 +28,6 @@ import { GetCodeBlockOption } from '../Blocks/Code'
|
||||
import { GetQuoteBlockOption } from '../Blocks/Quote'
|
||||
import { GetDividerBlockOption } from '../Blocks/Divider'
|
||||
import { GetCollapsibleBlockOption } from '../Blocks/Collapsible'
|
||||
import { GetEmbedsBlockOptions } from '../Blocks/Embeds'
|
||||
import { GetUploadFileOption } from '../Blocks/File'
|
||||
|
||||
export default function BlockPickerMenuPlugin({ popoverZIndex }: { popoverZIndex?: string }): JSX.Element {
|
||||
@@ -72,7 +71,6 @@ export default function BlockPickerMenuPlugin({ popoverZIndex }: { popoverZIndex
|
||||
GetJustifyAlignBlockOption(editor),
|
||||
GetPasswordBlockOption(editor),
|
||||
GetCollapsibleBlockOption(editor),
|
||||
...GetEmbedsBlockOptions(editor),
|
||||
]
|
||||
|
||||
const dynamicOptions = [
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { INSERT_EMBED_COMMAND } from '@lexical/react/LexicalAutoEmbedPlugin'
|
||||
import { EmbedConfigs } from '../AutoEmbedPlugin'
|
||||
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
|
||||
import { BlockPickerOption } from '../BlockPickerPlugin/BlockPickerOption'
|
||||
|
||||
export function GetEmbedsBlocks(editor: LexicalEditor) {
|
||||
return EmbedConfigs.map((embedConfig) => ({
|
||||
name: `Embed ${embedConfig.contentName}`,
|
||||
iconName: embedConfig.iconName as LexicalIconName,
|
||||
keywords: [...embedConfig.keywords, 'embed'],
|
||||
onSelect: () => editor.dispatchCommand(INSERT_EMBED_COMMAND, embedConfig.type),
|
||||
}))
|
||||
}
|
||||
|
||||
export function GetEmbedsBlockOptions(editor: LexicalEditor) {
|
||||
return GetEmbedsBlocks(editor).map(
|
||||
(block) =>
|
||||
new BlockPickerOption(block.name, {
|
||||
iconName: block.iconName as LexicalIconName,
|
||||
keywords: block.keywords,
|
||||
onSelect: block.onSelect,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
$isRangeSelection,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
KEY_ENTER_COMMAND,
|
||||
SKIP_DOM_SELECTION_TAG,
|
||||
} from 'lexical'
|
||||
import { useEffect } from 'react'
|
||||
import { useApplication } from '../../ApplicationProvider'
|
||||
@@ -53,8 +54,6 @@ export function CheckListPlugin(): null {
|
||||
return
|
||||
}
|
||||
|
||||
editor.getRootElement()?.focus()
|
||||
|
||||
const rect = target.getBoundingClientRect()
|
||||
|
||||
const listItemElementStyles = getComputedStyle(target)
|
||||
@@ -81,25 +80,40 @@ export function CheckListPlugin(): null {
|
||||
|
||||
function handleClick(event: Event) {
|
||||
handleCheckItemEvent(event as PointerEvent, () => {
|
||||
const isTouchEvent = (event as PointerEvent).pointerType === 'touch'
|
||||
if (!editor.isEditable()) {
|
||||
return
|
||||
}
|
||||
|
||||
editor.update(() => {
|
||||
const domNode = event.target as HTMLElement
|
||||
editor.update(
|
||||
() => {
|
||||
const domNode = event.target
|
||||
if (!(domNode instanceof HTMLElement)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!event.target) {
|
||||
return
|
||||
}
|
||||
const node = $getNearestNodeFromDOMNode(domNode)
|
||||
|
||||
const node = $getNearestNodeFromDOMNode(domNode)
|
||||
if (!$isListItemNode(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!$isListItemNode(node)) {
|
||||
return
|
||||
}
|
||||
const isFocusWithinEditor = editor.getRootElement()?.contains(document.activeElement)
|
||||
if (!isTouchEvent && !isFocusWithinEditor) {
|
||||
// on desktop, we want to focus & select the list item so that if you then press the up or down arrow keys,
|
||||
// the caret moves in the editor instead of triggering the note navigation shortcuts.
|
||||
// however on mobile, focusing the editor brings up the keyboard even if you just want to quickly toggle
|
||||
// an item. the keyboard also causes a layout shift which might end up leading to an incorrect toggle.
|
||||
node.selectStart()
|
||||
}
|
||||
|
||||
node.toggleChecked()
|
||||
})
|
||||
node.toggleChecked()
|
||||
},
|
||||
{
|
||||
// without this lexical will reconcile the new selection to the dom and focus the editor causing the keyboard to show up
|
||||
tag: isTouchEvent ? SKIP_DOM_SELECTION_TAG : undefined,
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+5
-9
@@ -30,15 +30,11 @@ export function BubbleComponent({ itemUuid, node }: BubbleComponentProps) {
|
||||
[toggleAppPane, linkingController],
|
||||
)
|
||||
|
||||
const unlinkPressed = useCallback(
|
||||
async (itemToUnlink: LinkableItem) => {
|
||||
linkingController.unlinkItemFromSelectedItem(itemToUnlink).catch(console.error)
|
||||
editor.update(() => {
|
||||
node.remove()
|
||||
})
|
||||
},
|
||||
[linkingController, node, editor],
|
||||
)
|
||||
const unlinkPressed = useCallback(async () => {
|
||||
editor.update(() => {
|
||||
node.remove()
|
||||
})
|
||||
}, [linkingController, node, editor])
|
||||
|
||||
if (!item) {
|
||||
return <div>Unable to find item {itemUuid}</div>
|
||||
|
||||
+1
-7
@@ -32,13 +32,7 @@ export function ItemSelectionItemComponent({ index, isSelected, onClick, onMouse
|
||||
onClick={onClick}
|
||||
>
|
||||
{option.item && <LinkedItemMeta item={option.item} searchQuery={searchQuery} />}
|
||||
{!option.item && (
|
||||
<LinkedItemSearchResultsAddTagOption
|
||||
searchQuery={searchQuery}
|
||||
onClickCallback={onClick}
|
||||
isFocused={isSelected}
|
||||
/>
|
||||
)}
|
||||
{!option.item && <LinkedItemSearchResultsAddTagOption searchQuery={searchQuery} isFocused={isSelected} />}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { $insertNodeToNearestRoot } from '@lexical/utils'
|
||||
import { COMMAND_PRIORITY_EDITOR, createCommand, LexicalCommand } from 'lexical'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { $createTweetNode, TweetNode } from '../../Lexical/Nodes/TweetNode'
|
||||
|
||||
export const INSERT_TWEET_COMMAND: LexicalCommand<string> = createCommand('INSERT_TWEET_COMMAND')
|
||||
|
||||
export default function TwitterPlugin(): JSX.Element | null {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([TweetNode])) {
|
||||
throw new Error('TwitterPlugin: TweetNode not registered on editor')
|
||||
}
|
||||
|
||||
return editor.registerCommand<string>(
|
||||
INSERT_TWEET_COMMAND,
|
||||
(payload) => {
|
||||
const tweetNode = $createTweetNode(payload)
|
||||
$insertNodeToNearestRoot(tweetNode)
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
)
|
||||
}, [editor])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { $insertNodeToNearestRoot } from '@lexical/utils'
|
||||
import { COMMAND_PRIORITY_EDITOR, createCommand, LexicalCommand } from 'lexical'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { $createYouTubeNode, YouTubeNode } from '../../Lexical/Nodes/YouTubeNode'
|
||||
|
||||
export const INSERT_YOUTUBE_COMMAND: LexicalCommand<string> = createCommand('INSERT_YOUTUBE_COMMAND')
|
||||
|
||||
export default function YouTubePlugin(): JSX.Element | null {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([YouTubeNode])) {
|
||||
throw new Error('YouTubePlugin: YouTubeNode not registered on editor')
|
||||
}
|
||||
|
||||
return editor.registerCommand<string>(
|
||||
INSERT_YOUTUBE_COMMAND,
|
||||
(payload) => {
|
||||
const youTubeNode = $createYouTubeNode(payload)
|
||||
$insertNodeToNearestRoot(youTubeNode)
|
||||
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
)
|
||||
}, [editor])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import TagsList from '@/Components/Tags/TagsList'
|
||||
import { ApplicationEvent } from '@/__mocks__/@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import { FunctionComponent } from 'react'
|
||||
import TagsSectionAddButton from './TagsSectionAddButton'
|
||||
import TagsSectionTitle from './TagsSectionTitle'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
@@ -9,47 +8,6 @@ import { useApplication } from '../ApplicationProvider'
|
||||
const TagsSection: FunctionComponent = () => {
|
||||
const application = useApplication()
|
||||
|
||||
const [hasMigration, setHasMigration] = useState<boolean>(false)
|
||||
|
||||
const checkIfMigrationNeeded = useCallback(() => {
|
||||
setHasMigration(application.items.hasTagsNeedingFoldersMigration())
|
||||
}, [application])
|
||||
|
||||
useEffect(() => {
|
||||
const removeObserver = application.addEventObserver(async (event) => {
|
||||
const events = [ApplicationEvent.CompletedInitialSync, ApplicationEvent.SignedIn]
|
||||
if (events.includes(event)) {
|
||||
checkIfMigrationNeeded()
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
removeObserver()
|
||||
}
|
||||
}, [application, checkIfMigrationNeeded])
|
||||
|
||||
const runMigration = useCallback(async () => {
|
||||
if (
|
||||
await application.alerts.confirm(
|
||||
'<i>Introducing native, built-in nested tags without requiring the legacy Folders extension.</i><br/></br> ' +
|
||||
" To get started, we'll need to migrate any tags containing a dot character to the new system.<br/></br> " +
|
||||
' This migration will convert any tags with dots appearing in their name into a natural' +
|
||||
' hierarchy that is compatible with the new nested tags feature.' +
|
||||
' Running this migration will remove any "." characters appearing in tag names.',
|
||||
'New: Folders to Nested Tags',
|
||||
'Run Migration',
|
||||
)
|
||||
) {
|
||||
application.mutator
|
||||
.migrateTagsToFolders()
|
||||
.then(() => {
|
||||
void application.sync.sync()
|
||||
checkIfMigrationNeeded()
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
}, [application, checkIfMigrationNeeded])
|
||||
|
||||
return (
|
||||
<>
|
||||
{application.navigationController.starredTags.length > 0 && (
|
||||
@@ -68,11 +26,7 @@ const TagsSection: FunctionComponent = () => {
|
||||
<section>
|
||||
<div className={'section-title-bar'}>
|
||||
<div className="section-title-bar-header">
|
||||
<TagsSectionTitle
|
||||
features={application.featuresController}
|
||||
hasMigration={hasMigration}
|
||||
onClickMigration={runMigration}
|
||||
/>
|
||||
<TagsSectionTitle features={application.featuresController} />
|
||||
{!application.navigationController.isSearching && (
|
||||
<TagsSectionAddButton tags={application.navigationController} features={application.featuresController} />
|
||||
)}
|
||||
|
||||
@@ -7,11 +7,9 @@ import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
||||
|
||||
type Props = {
|
||||
features: FeaturesController
|
||||
hasMigration: boolean
|
||||
onClickMigration: () => void
|
||||
}
|
||||
|
||||
const TagsSectionTitle: FunctionComponent<Props> = ({ features, hasMigration, onClickMigration }) => {
|
||||
const TagsSectionTitle: FunctionComponent<Props> = ({ features }) => {
|
||||
const entitledToFolders = features.hasFolders
|
||||
const modal = usePremiumModal()
|
||||
|
||||
@@ -24,11 +22,6 @@ const TagsSectionTitle: FunctionComponent<Props> = ({ features, hasMigration, on
|
||||
<>
|
||||
<div className="title text-base md:text-sm">
|
||||
<span className="font-bold">Folders</span>
|
||||
{hasMigration && (
|
||||
<label className="ml-1 cursor-pointer font-bold text-info" onClick={onClickMigration}>
|
||||
Migration Available
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -213,11 +213,15 @@ export class PaneController extends AbstractViewController implements InternalEv
|
||||
|
||||
const panes = this._panesForLayout.execute(layout).getValue()
|
||||
|
||||
if (panes.includes(AppPaneId.Items) && this.listPaneExplicitelyCollapsed) {
|
||||
if (panes.includes(AppPaneId.Items) && this.listPaneExplicitelyCollapsed && layout !== PaneLayout.ItemSelection) {
|
||||
removeFromArray(panes, AppPaneId.Items)
|
||||
}
|
||||
|
||||
if (panes.includes(AppPaneId.Navigation) && this.navigationPaneExplicitelyCollapsed) {
|
||||
if (
|
||||
panes.includes(AppPaneId.Navigation) &&
|
||||
this.navigationPaneExplicitelyCollapsed &&
|
||||
layout !== PaneLayout.TagSelection
|
||||
) {
|
||||
removeFromArray(panes, AppPaneId.Navigation)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ export const useAvailableSafeAreaPadding = () => {
|
||||
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false)
|
||||
useEffect(() => {
|
||||
return application.addNativeMobileEventListener((event) => {
|
||||
if (event === ReactNativeToWebEvent.KeyboardWillShow) {
|
||||
if (event === ReactNativeToWebEvent.KeyboardWillShow || event === ReactNativeToWebEvent.KeyboardDidShow) {
|
||||
setIsKeyboardVisible(true)
|
||||
} else if (event === ReactNativeToWebEvent.KeyboardWillHide) {
|
||||
} else if (event === ReactNativeToWebEvent.KeyboardWillHide || event === ReactNativeToWebEvent.KeyboardDidHide) {
|
||||
setIsKeyboardVisible(false)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ import snColorsCSS from '!css-loader?{"sourceMap":false}!sass-loader!@standardno
|
||||
// @ts-expect-error Using inline loaders to load CSS as string
|
||||
import exportOverridesCSS from '!css-loader?{"sourceMap":false}!sass-loader!../Components/SuperEditor/Lexical/Theme/export-overrides.scss'
|
||||
import { getBase64FromBlob } from './Utils'
|
||||
import { parseFileName, parseAndCreateZippableFileName } from '@standardnotes/utils'
|
||||
import { parseFileName, parseAndCreateZippableFileName, sanitizeFileName } from '@standardnotes/utils'
|
||||
|
||||
export const getNoteFormat = (application: WebApplicationInterface, note: SNNote) => {
|
||||
if (note.noteType === NoteType.Super) {
|
||||
@@ -28,7 +28,8 @@ export const getNoteFormat = (application: WebApplicationInterface, note: SNNote
|
||||
|
||||
export const getNoteFileName = (application: WebApplicationInterface, note: SNNote): string => {
|
||||
const format = getNoteFormat(application, note)
|
||||
return `${note.title}.${format}`
|
||||
const filename = sanitizeFileName(note.title)
|
||||
return `${filename}.${format}`
|
||||
}
|
||||
|
||||
const headlessSuperConverter = new HeadlessSuperConverter()
|
||||
|
||||
-8
File diff suppressed because one or more lines are too long
@@ -3763,9 +3763,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@electron/rebuild@npm:3.6.0":
|
||||
version: 3.6.0
|
||||
resolution: "@electron/rebuild@npm:3.6.0"
|
||||
"@electron/rebuild@npm:3.6.1":
|
||||
version: 3.6.1
|
||||
resolution: "@electron/rebuild@npm:3.6.1"
|
||||
dependencies:
|
||||
"@malept/cross-spawn-promise": ^2.0.0
|
||||
chalk: ^4.0.0
|
||||
@@ -3783,7 +3783,7 @@ __metadata:
|
||||
yargs: ^17.0.1
|
||||
bin:
|
||||
electron-rebuild: lib/cli.js
|
||||
checksum: 0dbf320b71ba5bc31ff7e0f46b157a16d56a7ee2b2d503ad60b46fe4673f9ba63c5b270a00e44856564e6a9fd5da216034a26eb3321679c40b681156ab6289a2
|
||||
checksum: 90766c7341997a54319bc215014de829d3f3ab1976fd80c4fa9846f5672bdf2b6c7b0f87fe5edee290caf6c7e9a60774607b46b97dea20ac65c5567af7f694ae
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7850,7 +7850,7 @@ __metadata:
|
||||
"@babel/core": "*"
|
||||
"@babel/preset-env": "*"
|
||||
"@electron/notarize": ^3.0.0
|
||||
"@electron/rebuild": 3.6.0
|
||||
"@electron/rebuild": 3.6.1
|
||||
"@electron/remote": ^2.1.2
|
||||
"@standardnotes/domain-core": ^1.40.0
|
||||
"@standardnotes/electron-clear-data": 1.1.1
|
||||
|
||||
Reference in New Issue
Block a user