mirror of
https://github.com/standardnotes/app
synced 2026-09-13 09:46:02 -04:00
Compare commits
50
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4f9a18135 | ||
|
|
65c16ed246 | ||
|
|
852ddc677d | ||
|
|
17458b61d1 | ||
|
|
03da45537d | ||
|
|
7332f10ff4 | ||
|
|
b6eda707bd | ||
|
|
a37e095907 | ||
|
|
ef81e9d97e | ||
|
|
c6a31f209d | ||
|
|
292153333f | ||
|
|
6cef772212 | ||
|
|
f4ab9b0d97 | ||
|
|
73eba38c14 | ||
|
|
4b19076f9b | ||
|
|
da9c412703 | ||
|
|
9784ff0173 | ||
|
|
d8df19b0bd | ||
|
|
da16a779ce | ||
|
|
473fb6be53 | ||
|
|
d5cac8e32d | ||
|
|
45ca99c627 | ||
|
|
8a6e123fbc | ||
|
|
914c444349 | ||
|
|
bf8ad837ef | ||
|
|
e3ae421f7c | ||
|
|
a162294d04 | ||
|
|
f0613e7af5 | ||
|
|
7967ddead7 | ||
|
|
f868989bb5 | ||
|
|
eb5e5e11e0 | ||
|
|
0d1831ea49 | ||
|
|
a22000242a | ||
|
|
4068d5b480 | ||
|
|
fd0d08c153 | ||
|
|
adef74f23f | ||
|
|
b90c725f96 | ||
|
|
74b459cae0 | ||
|
|
c1e9a7fe40 | ||
|
|
edf8de4468 | ||
|
|
bb81c8acfc | ||
|
|
414a2dda0b | ||
|
|
45c661c136 | ||
|
|
c5ca4a9857 | ||
|
|
56813909bb | ||
|
|
7a4172ad11 | ||
|
|
e97e788b5f | ||
|
|
20a9ef0dad | ||
|
|
670f27b9cd | ||
|
|
f45d854006 |
@@ -49,11 +49,12 @@ Questions? Find answers on our [Help page](https://standardnotes.com/help).
|
||||
|
||||
Our web app is compiled into a folder of static HTML, JS, and CSS files. You can serve these files behind a web server to get started:
|
||||
|
||||
1. Clone the repo
|
||||
2. `cd packages/web`
|
||||
3. `yarn build`
|
||||
4. `cd dist`
|
||||
5. You can then use Python to serve this folder over http: `python -m http.server 8080`
|
||||
1. `git clone https://github.com/standardnotes/app.git`
|
||||
2. `cd app`
|
||||
3. `yarn install`
|
||||
4. `yarn build:web`
|
||||
5. `cd packages/web`
|
||||
6. You can then use Python to serve this folder over http: `python -m http.server 8080`
|
||||
|
||||
You can now access the app at `http://localhost:8080`.
|
||||
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.26.87](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.26.86](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.26.85](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.26.84](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.26.83](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api",
|
||||
"version": "1.26.83",
|
||||
"version": "1.26.87",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -72,6 +72,7 @@ export class AuthApiService implements AuthApiServiceInterface {
|
||||
password: string
|
||||
codeVerifier: string
|
||||
recoveryCodes: string
|
||||
hvmToken?: string
|
||||
}): Promise<HttpResponse<SignInWithRecoveryCodesResponseBody>> {
|
||||
if (this.operationsInProgress.get(AuthApiOperations.SignInWithRecoveryCodes)) {
|
||||
throw new ApiCallError(ErrorMessage.GenericInProgress)
|
||||
@@ -86,6 +87,7 @@ export class AuthApiService implements AuthApiServiceInterface {
|
||||
password: dto.password,
|
||||
recovery_codes: dto.recoveryCodes,
|
||||
username: dto.username,
|
||||
hvm_token: dto.hvmToken,
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
@@ -17,5 +17,6 @@ export interface AuthApiServiceInterface {
|
||||
password: string
|
||||
codeVerifier: string
|
||||
recoveryCodes: string
|
||||
hvmToken?: string
|
||||
}): Promise<HttpResponse<SignInWithRecoveryCodesResponseBody>>
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ export class UserApiService implements UserApiServiceInterface {
|
||||
async register(registerDTO: {
|
||||
email: string
|
||||
serverPassword: string
|
||||
hvmToken?: string
|
||||
keyParams: RootKeyParamsInterface
|
||||
ephemeral: boolean
|
||||
}): Promise<HttpResponse<UserRegistrationResponseBody>> {
|
||||
@@ -76,6 +77,7 @@ export class UserApiService implements UserApiServiceInterface {
|
||||
[ApiEndpointParam.ApiVersion]: this.apiVersion,
|
||||
password: registerDTO.serverPassword,
|
||||
email: registerDTO.email,
|
||||
hvm_token: registerDTO.hvmToken,
|
||||
ephemeral: registerDTO.ephemeral,
|
||||
...registerDTO.keyParams.getPortableValue(),
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface UserApiServiceInterface {
|
||||
register(registerDTO: {
|
||||
email: string
|
||||
serverPassword: string
|
||||
hvmToken?: string
|
||||
keyParams: RootKeyParamsInterface
|
||||
ephemeral: boolean
|
||||
}): Promise<HttpResponse<UserRegistrationResponseBody>>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LoggerInterface, joinPaths, sleep } from '@standardnotes/utils'
|
||||
import { Environment } from '@standardnotes/models'
|
||||
import { LegacySession, Session, SessionToken } from '@standardnotes/domain-core'
|
||||
import { LegacySession, Result, Session, SessionToken } from '@standardnotes/domain-core'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
HttpRequestParams,
|
||||
@@ -23,10 +23,11 @@ import { HttpRequestOptions } from './HttpRequestOptions'
|
||||
export class HttpService implements HttpServiceInterface {
|
||||
private session?: Session | LegacySession
|
||||
private __latencySimulatorMs?: number
|
||||
private __simulateNextSessionRefreshResponseDrop = false
|
||||
private declare host: string
|
||||
loggingEnabled = false
|
||||
|
||||
private inProgressRefreshSessionPromise?: Promise<boolean>
|
||||
private inProgressRefreshSessionPromise?: Promise<Result<HttpResponse<SessionRefreshResponseBody>>>
|
||||
private updateMetaCallback!: (meta: HttpResponseMeta) => void
|
||||
private refreshSessionCallback!: (session: Session) => void
|
||||
|
||||
@@ -173,12 +174,19 @@ export class HttpService implements HttpServiceInterface {
|
||||
if (this.inProgressRefreshSessionPromise) {
|
||||
await this.inProgressRefreshSessionPromise
|
||||
} else {
|
||||
this.inProgressRefreshSessionPromise = this.refreshSession()
|
||||
const isSessionRefreshed = await this.inProgressRefreshSessionPromise
|
||||
this.inProgressRefreshSessionPromise = undefined
|
||||
const hasSessionTokenRenewedInBetweenOurRequest = httpRequest.authentication !== this.getSessionAccessToken()
|
||||
if (!hasSessionTokenRenewedInBetweenOurRequest) {
|
||||
this.inProgressRefreshSessionPromise = this.refreshSession()
|
||||
const isSessionRefreshedResultOrError = await this.inProgressRefreshSessionPromise
|
||||
let isSessionRefreshed = false
|
||||
if (!isSessionRefreshedResultOrError.isFailed()) {
|
||||
isSessionRefreshed = !isErrorResponse(isSessionRefreshedResultOrError.getValue())
|
||||
}
|
||||
this.inProgressRefreshSessionPromise = undefined
|
||||
|
||||
if (!isSessionRefreshed) {
|
||||
return response
|
||||
if (!isSessionRefreshed) {
|
||||
return response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,13 +198,13 @@ export class HttpService implements HttpServiceInterface {
|
||||
return response
|
||||
}
|
||||
|
||||
async refreshSession(): Promise<boolean> {
|
||||
async refreshSession(): Promise<Result<HttpResponse<SessionRefreshResponseBody>>> {
|
||||
if (!this.session) {
|
||||
return false
|
||||
return Result.fail('No session to refresh')
|
||||
}
|
||||
|
||||
if (this.session instanceof LegacySession) {
|
||||
return false
|
||||
return Result.fail('Cannot refresh legacy session')
|
||||
}
|
||||
|
||||
const response = await this.post<SessionRefreshResponseBody>(Paths.v1.refreshSession, {
|
||||
@@ -204,8 +212,13 @@ export class HttpService implements HttpServiceInterface {
|
||||
refresh_token: this.session.refreshToken.value,
|
||||
})
|
||||
|
||||
if (this.__simulateNextSessionRefreshResponseDrop) {
|
||||
this.__simulateNextSessionRefreshResponseDrop = false
|
||||
return Result.fail('Simulating a dropped response')
|
||||
}
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return false
|
||||
return Result.ok(response)
|
||||
}
|
||||
|
||||
if (response.meta) {
|
||||
@@ -217,7 +230,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
response.data.session.access_expiration,
|
||||
)
|
||||
if (accessTokenOrError.isFailed()) {
|
||||
return false
|
||||
return Result.fail(accessTokenOrError.getError())
|
||||
}
|
||||
|
||||
const accessToken = accessTokenOrError.getValue()
|
||||
@@ -227,21 +240,21 @@ export class HttpService implements HttpServiceInterface {
|
||||
response.data.session.refresh_expiration,
|
||||
)
|
||||
if (refreshTokenOrError.isFailed()) {
|
||||
return false
|
||||
return Result.fail(refreshTokenOrError.getError())
|
||||
}
|
||||
|
||||
const refreshToken = refreshTokenOrError.getValue()
|
||||
|
||||
const sessionOrError = Session.create(accessToken, refreshToken, response.data.session.readonly_access)
|
||||
if (sessionOrError.isFailed()) {
|
||||
return false
|
||||
return Result.fail(sessionOrError.getError())
|
||||
}
|
||||
|
||||
this.setSession(sessionOrError.getValue())
|
||||
|
||||
this.refreshSessionCallback(this.session)
|
||||
|
||||
return true
|
||||
return Result.ok(response)
|
||||
}
|
||||
|
||||
private params(inParams: Record<string | number | symbol, unknown>): HttpRequestParams {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { LegacySession, Session } from '@standardnotes/domain-core'
|
||||
import { LegacySession, Result, Session } from '@standardnotes/domain-core'
|
||||
import { HttpRequest, HttpRequestParams, HttpResponse, HttpResponseMeta } from '@standardnotes/responses'
|
||||
|
||||
import { HttpRequestOptions } from './HttpRequestOptions'
|
||||
import { SessionRefreshResponseBody } from '../Response'
|
||||
|
||||
export interface HttpServiceInterface {
|
||||
setHost(host: string): void
|
||||
@@ -16,7 +17,7 @@ export interface HttpServiceInterface {
|
||||
runHttp<T>(httpRequest: HttpRequest): Promise<HttpResponse<T>>
|
||||
|
||||
setSession(session: Session | LegacySession): void
|
||||
refreshSession(): Promise<boolean>
|
||||
refreshSession(): Promise<Result<HttpResponse<SessionRefreshResponseBody>>>
|
||||
setCallbacks(
|
||||
updateMetaCallback: (meta: HttpResponseMeta) => void,
|
||||
refreshSessionCallback: (session: Session) => void,
|
||||
|
||||
@@ -4,4 +4,5 @@ export interface SignInWithRecoveryCodesRequestParams {
|
||||
password: string
|
||||
code_verifier: string
|
||||
recovery_codes: string
|
||||
hvm_token?: string
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@ export type UserRegistrationRequestParams = AnyKeyParamsContent & {
|
||||
[additionalParam: string]: unknown
|
||||
password: string
|
||||
email: string
|
||||
hvm_token?: string
|
||||
ephemeral: boolean
|
||||
}
|
||||
|
||||
@@ -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.450](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.449](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.448](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.447](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.446](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.445](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.444](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.443](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.442](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.441](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.440](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.439](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.438](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.437](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.436](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.435](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.434](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-12)
|
||||
|
||||
**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.434",
|
||||
"version": "1.1.450",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build-mv2": "yarn clean && webpack --config ./webpack.config.prod.js",
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [3.110.52](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.51](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.50](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.49](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.48](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.47](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.46](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.45](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.44](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.43](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.42](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.41](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.40](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.39](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.38](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.37](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.110.36](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/desktop",
|
||||
"main": "./app/dist/index.js",
|
||||
"version": "3.110.36",
|
||||
"version": "3.110.52",
|
||||
"license": "AGPL-3.0",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.21.105](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.21.104](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.21.103](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.21.102](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-02-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/encryption",
|
||||
"version": "1.21.102",
|
||||
"version": "1.21.105",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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.61.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/features
|
||||
|
||||
## [1.61.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/features
|
||||
|
||||
## [1.61.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-11-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/features",
|
||||
"version": "1.61.1",
|
||||
"version": "1.61.3",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -28,6 +28,7 @@ export class NativeFeatureIdentifier extends ValueObject<NativeFeatureIdentifier
|
||||
MidnightTheme: 'org.standardnotes.theme-midnight',
|
||||
SolarizedDarkTheme: 'org.standardnotes.theme-solarized-dark',
|
||||
TitaniumTheme: 'org.standardnotes.theme-titanium',
|
||||
ProtonTheme: 'com.standardnotes.theme-proton',
|
||||
|
||||
PlainEditor: 'com.standardnotes.plain-text',
|
||||
SuperEditor: 'com.standardnotes.super-editor',
|
||||
|
||||
@@ -97,5 +97,18 @@ export function themes(): ThemeFeatureDescription[] {
|
||||
no_mobile: true,
|
||||
})
|
||||
|
||||
return [midnight, futura, solarizedDark, autobiography, dark, titanium, dynamic]
|
||||
const proton: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||
availableInRoles: [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
name: 'Carbon',
|
||||
identifier: NativeFeatureIdentifier.TYPES.ProtonTheme,
|
||||
permission_name: PermissionName.ProtonTheme,
|
||||
dock_icon: {
|
||||
type: 'circle',
|
||||
background_color: '#16141c',
|
||||
foreground_color: '#ffffff',
|
||||
border_color: '#4a4658',
|
||||
},
|
||||
})
|
||||
|
||||
return [midnight, futura, solarizedDark, autobiography, dark, proton, titanium, dynamic]
|
||||
}
|
||||
|
||||
@@ -40,4 +40,5 @@ export enum PermissionName {
|
||||
Clipper = 'app:clipper',
|
||||
Vaults = 'app:vaults',
|
||||
SharedVaults = 'server:shared-vaults',
|
||||
ProtonTheme = 'theme:proton',
|
||||
}
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.28.120](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.28.119](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.28.118](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.28.117](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-02-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/filepicker",
|
||||
"version": "1.28.117",
|
||||
"version": "1.28.120",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.20.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.20.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.20.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.20.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-02-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files",
|
||||
"version": "1.20.2",
|
||||
"version": "1.20.5",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,72 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.58.109](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.108](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.107](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.106](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.105](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed super note "Table of contents" not working on mobile ([73eba38](https://github.com/standardnotes/app/commit/73eba38c1444e85839d771fb297e3035eabd99c8))
|
||||
|
||||
## [3.58.104](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.103](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.102](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.101](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.100](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.99](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.98](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.97](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.96](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.95](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.94](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.58.93](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
@@ -158,7 +158,7 @@ platform :android do
|
||||
|
||||
desc 'Deploy production app'
|
||||
lane :prod do
|
||||
version = 3_002_000 + ENV['BUILD_NUMBER'].to_i
|
||||
version = 3_004_000 + ENV['BUILD_NUMBER'].to_i
|
||||
deploy_android 'prod', version
|
||||
end
|
||||
end
|
||||
|
||||
@@ -736,7 +736,7 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
boost: a7c83b31436843459a1961bfd74b96033dc77234
|
||||
boost: 57d2868c099736d80fcd648bf211b4431e51a558
|
||||
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
|
||||
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
|
||||
FBLazyVector: 71803c074f6325f10b5ec891c443b6bbabef0ca7
|
||||
@@ -756,7 +756,7 @@ SPEC CHECKSUMS:
|
||||
MMKV: 9c6c3fa4ddd849f28c7b9a5c9d23aab84f14ee35
|
||||
MMKVCore: 9bb7440b170181ac5b81f542ac258103542e693d
|
||||
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
|
||||
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
|
||||
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
|
||||
RCTRequired: df81ab637d35fac9e6eb94611cfd20f0feb05455
|
||||
RCTTypeSafety: 4636e4a36c7c2df332bda6d59b19b41c443d4287
|
||||
React: e0cc5197a804031a6c53fb38483c3485fcb9d6f3
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/mobile",
|
||||
"version": "3.58.93",
|
||||
"version": "3.58.109",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.55.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.55.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.55.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
# [1.55.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-02-17)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/models",
|
||||
"version": "1.55.0",
|
||||
"version": "1.55.3",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -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.4.745](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.744](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.743](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.742](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.741](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.740](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.739](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.738](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.737](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.736](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.735](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.734](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.733](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.732](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.731](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.730](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.729](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/releases",
|
||||
"version": "1.4.729",
|
||||
"version": "1.4.745",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "dist/releases.json",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.13.56](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/responses
|
||||
|
||||
## [1.13.55](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/responses
|
||||
|
||||
## [1.13.54](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/responses
|
||||
|
||||
## [1.13.53](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-11-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/responses
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/responses",
|
||||
"version": "1.13.53",
|
||||
"version": "1.13.56",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export type MetaEndpointResponse = {
|
||||
captchaUIUrl: string | null
|
||||
}
|
||||
@@ -26,6 +26,30 @@ export function isErrorResponse<T>(response: HttpResponse<T>): response is HttpE
|
||||
return (response.data as HttpErrorResponseBody)?.error != undefined || response.status >= 400
|
||||
}
|
||||
|
||||
export function getCaptchaHeader<T>(response: HttpResponse<T>) {
|
||||
const captchaHeader = response.headers?.get('x-captcha-required')
|
||||
if (captchaHeader) {
|
||||
return captchaHeader
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function getErrorMessageFromErrorResponseBody(data: HttpErrorResponseBody, defaultMessage?: string): string {
|
||||
let errorMessage = defaultMessage || 'Unknown error'
|
||||
if (
|
||||
data &&
|
||||
typeof data === 'object' &&
|
||||
'error' in data &&
|
||||
data.error &&
|
||||
typeof data.error === 'object' &&
|
||||
'message' in data.error
|
||||
) {
|
||||
errorMessage = data.error.message as string
|
||||
}
|
||||
|
||||
return errorMessage
|
||||
}
|
||||
|
||||
export function getErrorFromErrorResponse(response: HttpErrorResponse): HttpError {
|
||||
const embeddedError = response.data.error
|
||||
if (embeddedError) {
|
||||
|
||||
@@ -13,6 +13,7 @@ export * from './Auth/SignInData'
|
||||
export * from './Auth/SignInResponse'
|
||||
export * from './Auth/SignOutResponse'
|
||||
export * from './Auth/User'
|
||||
export * from './Auth/MetaEndpointResponse'
|
||||
|
||||
/** Temps are awaiting final publish state on server repo */
|
||||
export * from './Temp/SharedVaultMoveType'
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.70.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.70.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.70.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.70.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed issue with system color scheme not being applied correctly on start ([7967dde](https://github.com/standardnotes/app/commit/7967ddead790d89478554cd6a96429fd06958590))
|
||||
|
||||
## [1.70.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.70.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/services",
|
||||
"version": "1.70.3",
|
||||
"version": "1.70.8",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -13,8 +13,10 @@ export interface AuthClientInterface {
|
||||
password: string
|
||||
codeVerifier: string
|
||||
recoveryCodes: string
|
||||
hvmToken?: string
|
||||
}): Promise<
|
||||
| {
|
||||
success: true
|
||||
keyParams: AnyKeyParamsContent
|
||||
session: SessionBody
|
||||
user: {
|
||||
@@ -23,6 +25,9 @@ export interface AuthClientInterface {
|
||||
protocolVersion: string
|
||||
}
|
||||
}
|
||||
| false
|
||||
| {
|
||||
success: false
|
||||
captchaURL: string
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AuthApiServiceInterface } from '@standardnotes/api'
|
||||
import { AnyKeyParamsContent } from '@standardnotes/common'
|
||||
import { isErrorResponse, SessionBody } from '@standardnotes/responses'
|
||||
import { isErrorResponse, getCaptchaHeader } from '@standardnotes/responses'
|
||||
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
@@ -45,37 +45,39 @@ export class AuthManager extends AbstractService implements AuthClientInterface
|
||||
}
|
||||
}
|
||||
|
||||
async signInWithRecoveryCodes(dto: {
|
||||
username: string
|
||||
password: string
|
||||
codeVerifier: string
|
||||
recoveryCodes: string
|
||||
}): Promise<
|
||||
| {
|
||||
keyParams: AnyKeyParamsContent
|
||||
session: SessionBody
|
||||
user: {
|
||||
uuid: string
|
||||
email: string
|
||||
protocolVersion: string
|
||||
}
|
||||
}
|
||||
| false
|
||||
> {
|
||||
async signInWithRecoveryCodes(
|
||||
dto: Parameters<AuthClientInterface['signInWithRecoveryCodes']>[0],
|
||||
): ReturnType<AuthClientInterface['signInWithRecoveryCodes']> {
|
||||
try {
|
||||
const result = await this.authApiService.signInWithRecoveryCodes(dto)
|
||||
|
||||
const captchaURL = getCaptchaHeader(result)
|
||||
|
||||
if (captchaURL) {
|
||||
return {
|
||||
success: false,
|
||||
captchaURL,
|
||||
}
|
||||
}
|
||||
|
||||
if (isErrorResponse(result)) {
|
||||
return false
|
||||
return {
|
||||
success: false,
|
||||
captchaURL: '',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
keyParams: result.data.key_params as AnyKeyParamsContent,
|
||||
session: result.data.session,
|
||||
user: result.data.user,
|
||||
}
|
||||
} catch (error) {
|
||||
return false
|
||||
return {
|
||||
success: false,
|
||||
captchaURL: '',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@ export interface ComponentManagerInterface {
|
||||
getDefaultEditorIdentifier(currentTag?: SNTag): string
|
||||
|
||||
isThemeActive(theme: UIFeature<ThemeFeatureDescription>): boolean
|
||||
toggleTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void>
|
||||
toggleTheme(theme: UIFeature<ThemeFeatureDescription>, skipEntitlementCheck?: boolean): Promise<void>
|
||||
toggleOtherNonLayerableThemes(uiFeature: UIFeature<ThemeFeatureDescription>): void
|
||||
getActiveThemes(): UIFeature<ThemeFeatureDescription>[]
|
||||
getActiveThemesIdentifiers(): { features: NativeFeatureIdentifier[]; uuids: Uuid[] }
|
||||
|
||||
|
||||
@@ -30,13 +30,14 @@ export interface SessionsClientInterface {
|
||||
revokeAllOtherSessions(): Promise<void>
|
||||
|
||||
isCurrentSessionReadOnly(): boolean | undefined
|
||||
register(email: string, password: string, ephemeral: boolean): Promise<UserRegistrationResponseBody>
|
||||
register(email: string, password: string, hvmToken: string, ephemeral: boolean): Promise<UserRegistrationResponseBody>
|
||||
signIn(
|
||||
email: string,
|
||||
password: string,
|
||||
strict: boolean,
|
||||
ephemeral: boolean,
|
||||
minAllowedVersion?: ProtocolVersion,
|
||||
hvmToken?: string,
|
||||
): Promise<SessionManagerResponse>
|
||||
bypassChecksAndSignInWithRootKey(
|
||||
email: string,
|
||||
|
||||
@@ -142,6 +142,7 @@ export class UserService
|
||||
public async register(
|
||||
email: string,
|
||||
password: string,
|
||||
hvmToken: string,
|
||||
ephemeral = false,
|
||||
mergeLocal = true,
|
||||
): Promise<UserRegistrationResponseBody> {
|
||||
@@ -157,7 +158,7 @@ export class UserService
|
||||
|
||||
try {
|
||||
this.lockSyncing()
|
||||
const response = await this.sessions.register(email, password, ephemeral)
|
||||
const response = await this.sessions.register(email, password, hvmToken, ephemeral)
|
||||
|
||||
await this.notifyEventSync(AccountEvent.SignedInOrRegistered, {
|
||||
payload: {
|
||||
@@ -190,6 +191,7 @@ export class UserService
|
||||
ephemeral = false,
|
||||
mergeLocal = true,
|
||||
awaitSync = false,
|
||||
hvmToken?: string,
|
||||
): Promise<HttpResponse<SignInResponse>> {
|
||||
if (this.encryption.hasAccount()) {
|
||||
throw Error('Tried to sign in when an account already exists.')
|
||||
@@ -205,7 +207,7 @@ export class UserService
|
||||
/** Prevent a timed sync from occuring while signing in. */
|
||||
this.lockSyncing()
|
||||
|
||||
const { response } = await this.sessions.signIn(email, password, strict, ephemeral)
|
||||
const { response } = await this.sessions.signIn(email, password, strict, ephemeral, undefined, hvmToken)
|
||||
|
||||
if (!isErrorResponse(response)) {
|
||||
const notifyingFunction = awaitSync ? this.notifyEventSync.bind(this) : this.notifyEvent.bind(this)
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface UserServiceInterface extends AbstractService<AccountEvent, Acco
|
||||
register(
|
||||
email: string,
|
||||
password: string,
|
||||
hvmToken: string,
|
||||
ephemeral: boolean,
|
||||
mergeLocal: boolean,
|
||||
): Promise<UserRegistrationResponseBody>
|
||||
@@ -31,6 +32,7 @@ export interface UserServiceInterface extends AbstractService<AccountEvent, Acco
|
||||
ephemeral: boolean,
|
||||
mergeLocal: boolean,
|
||||
awaitSync: boolean,
|
||||
hvmToken?: string,
|
||||
): Promise<HttpResponse<SignInResponse>>
|
||||
deleteAccount(): Promise<{
|
||||
error: boolean
|
||||
|
||||
@@ -203,7 +203,6 @@ export * from './User/SignedInOrRegisteredEventPayload'
|
||||
export * from './User/SignedOutEventPayload'
|
||||
export * from './User/UserService'
|
||||
export * from './User/UserServiceInterface'
|
||||
export * from './User/UserServiceInterface'
|
||||
export * from './UserEvent/NotificationService'
|
||||
export * from './UserEvent/NotificationServiceEvent'
|
||||
export * from './Vault/UseCase/AuthorizeVaultDeletion'
|
||||
|
||||
@@ -3,6 +3,32 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.208.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.208.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.208.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.208.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.208.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed issue with system color scheme not being applied correctly on start ([7967dde](https://github.com/standardnotes/app/commit/7967ddead790d89478554cd6a96429fd06958590))
|
||||
|
||||
## [2.208.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.208.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
@@ -98,6 +98,7 @@ import {
|
||||
SignInResponse,
|
||||
ClientDisplayableError,
|
||||
SessionListEntry,
|
||||
MetaEndpointResponse,
|
||||
} from '@standardnotes/responses'
|
||||
import {
|
||||
SyncService,
|
||||
@@ -117,7 +118,7 @@ import {
|
||||
LoggerInterface,
|
||||
canBlockDeinit,
|
||||
} from '@standardnotes/utils'
|
||||
import { UuidString, ApplicationEventPayload } from '../Types'
|
||||
import { UuidString } from '../Types'
|
||||
import { applicationEventForSyncEvent } from '@Lib/Application/Event'
|
||||
import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files'
|
||||
import { ComputePrivateUsername } from '@standardnotes/encryption'
|
||||
@@ -275,12 +276,12 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
}),
|
||||
)
|
||||
|
||||
const syncEventCallback = async (eventName: SyncEvent) => {
|
||||
const syncEventCallback = async (eventName: SyncEvent, data?: unknown) => {
|
||||
const appEvent = applicationEventForSyncEvent(eventName)
|
||||
if (appEvent) {
|
||||
await encryptionService.onSyncEvent(eventName)
|
||||
|
||||
await this.notifyEvent(appEvent)
|
||||
await this.notifyEvent(appEvent, data)
|
||||
|
||||
if (appEvent === ApplicationEvent.CompletedFullSync) {
|
||||
if (!this.handledFullSyncStage) {
|
||||
@@ -535,7 +536,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
return this.addEventObserver(filteredCallback, event)
|
||||
}
|
||||
|
||||
private async notifyEvent(event: ApplicationEvent, data?: ApplicationEventPayload) {
|
||||
private async notifyEvent(event: ApplicationEvent, data?: unknown) {
|
||||
if (event === ApplicationEvent.Started) {
|
||||
this.onStart()
|
||||
} else if (event === ApplicationEvent.Launched) {
|
||||
@@ -768,10 +769,11 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
public async register(
|
||||
email: string,
|
||||
password: string,
|
||||
hvmToken: string,
|
||||
ephemeral = false,
|
||||
mergeLocal = true,
|
||||
): Promise<UserRegistrationResponseBody> {
|
||||
return this.user.register(email, password, ephemeral, mergeLocal)
|
||||
return this.user.register(email, password, hvmToken, ephemeral, mergeLocal)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -785,8 +787,13 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
ephemeral = false,
|
||||
mergeLocal = true,
|
||||
awaitSync = false,
|
||||
hvmToken?: string,
|
||||
): Promise<HttpResponse<SignInResponse>> {
|
||||
return this.user.signIn(email, password, strict, ephemeral, mergeLocal, awaitSync)
|
||||
return this.user.signIn(email, password, strict, ephemeral, mergeLocal, awaitSync, hvmToken)
|
||||
}
|
||||
|
||||
public async getCaptchaUrl(): Promise<HttpResponse<MetaEndpointResponse>> {
|
||||
return this.legacyApi.getCaptchaUrl()
|
||||
}
|
||||
|
||||
public async changeEmail(
|
||||
|
||||
+3
-1
@@ -166,7 +166,9 @@ describe('SignInWithRecoveryCodes', () => {
|
||||
})
|
||||
|
||||
it('should fail if the sign in with recovery code fails', async () => {
|
||||
authManager.signInWithRecoveryCodes = jest.fn().mockReturnValue(false)
|
||||
authManager.signInWithRecoveryCodes = jest.fn().mockReturnValue({
|
||||
success: false,
|
||||
})
|
||||
|
||||
const useCase = createUseCase()
|
||||
const result = await useCase.execute({
|
||||
|
||||
+10
-1
@@ -68,9 +68,18 @@ export class SignInWithRecoveryCodes implements UseCaseInterface<void> {
|
||||
recoveryCodes: dto.recoveryCodes,
|
||||
username: dto.username,
|
||||
password: rootKey.serverPassword as string,
|
||||
hvmToken: dto.hvmToken,
|
||||
})
|
||||
|
||||
if (signInResult === false) {
|
||||
if (signInResult.success === false) {
|
||||
if (signInResult.captchaURL) {
|
||||
return Result.fail(
|
||||
JSON.stringify({
|
||||
captchaURL: signInResult.captchaURL,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return Result.fail('Could not sign in with recovery code')
|
||||
}
|
||||
|
||||
|
||||
@@ -2,4 +2,5 @@ export interface SignInWithRecoveryCodesDTO {
|
||||
recoveryCodes: string
|
||||
username: string
|
||||
password: string
|
||||
hvmToken?: string
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
isErrorResponse,
|
||||
MoveFileResponse,
|
||||
ValetTokenOperation,
|
||||
MetaEndpointResponse,
|
||||
} from '@standardnotes/responses'
|
||||
import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
|
||||
import { HttpServiceInterface } from '@standardnotes/api'
|
||||
@@ -290,6 +291,7 @@ export class LegacyApiService
|
||||
email: string
|
||||
serverPassword: string
|
||||
ephemeral: boolean
|
||||
hvmToken?: string
|
||||
}): Promise<HttpResponse<SignInResponse>> {
|
||||
if (this.authenticating) {
|
||||
return this.createErrorResponse(API_MESSAGE_LOGIN_IN_PROGRESS, HttpStatusCode.BadRequest)
|
||||
@@ -301,6 +303,7 @@ export class LegacyApiService
|
||||
password: dto.serverPassword,
|
||||
ephemeral: dto.ephemeral,
|
||||
code_verifier: this.inMemoryStore.getValue(StorageKey.CodeVerifier) as string,
|
||||
hvm_token: dto.hvmToken,
|
||||
})
|
||||
|
||||
const response = await this.request<SignInResponse>({
|
||||
@@ -408,7 +411,12 @@ export class LegacyApiService
|
||||
}
|
||||
}
|
||||
|
||||
async refreshSession(): Promise<HttpResponse<SessionRenewalResponse>> {
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
* This function should be replaced with @standardnotes/api's `HttpService::refreshSession` function.
|
||||
*/
|
||||
async deprecatedRefreshSessionOnlyUsedInE2eTests(): Promise<HttpResponse<SessionRenewalResponse>> {
|
||||
const preprocessingError = this.preprocessingError()
|
||||
if (preprocessingError) {
|
||||
return preprocessingError
|
||||
@@ -953,4 +961,9 @@ export class LegacyApiService
|
||||
|
||||
return this.session.accessToken
|
||||
}
|
||||
|
||||
public getCaptchaUrl() {
|
||||
const response = this.httpService.get<MetaEndpointResponse>(Paths.v1.meta)
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ export const Paths = {
|
||||
...SettingsPaths,
|
||||
...SubscriptionPaths,
|
||||
...UserPaths,
|
||||
meta: '/v1/meta',
|
||||
},
|
||||
v2: {
|
||||
...UserPathsV2,
|
||||
|
||||
@@ -390,7 +390,20 @@ export class ComponentManager
|
||||
return this.viewers.find((viewer) => viewer.sessionKey === key)
|
||||
}
|
||||
|
||||
public async toggleTheme(uiFeature: UIFeature<ThemeFeatureDescription>): Promise<void> {
|
||||
public toggleOtherNonLayerableThemes(uiFeature: UIFeature<ThemeFeatureDescription>): void {
|
||||
const activeThemes = this.getActiveThemes()
|
||||
for (const candidate of activeThemes) {
|
||||
if (candidate.featureIdentifier === uiFeature.featureIdentifier) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!candidate.layerable) {
|
||||
this.removeActiveTheme(candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async toggleTheme(uiFeature: UIFeature<ThemeFeatureDescription>, skipEntitlementCheck = false): Promise<void> {
|
||||
this.logger.info('Toggling theme', uiFeature.uniqueIdentifier)
|
||||
|
||||
if (this.isThemeActive(uiFeature)) {
|
||||
@@ -399,7 +412,7 @@ export class ComponentManager
|
||||
}
|
||||
|
||||
const featureStatus = this.features.getFeatureStatus(uiFeature.uniqueIdentifier)
|
||||
if (featureStatus !== FeatureStatus.Entitled) {
|
||||
if (!skipEntitlementCheck && featureStatus !== FeatureStatus.Entitled) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -410,16 +423,7 @@ export class ComponentManager
|
||||
if (!uiFeature.layerable) {
|
||||
await sleep(10)
|
||||
|
||||
const activeThemes = this.getActiveThemes()
|
||||
for (const candidate of activeThemes) {
|
||||
if (candidate.featureIdentifier === uiFeature.featureIdentifier) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!candidate.layerable) {
|
||||
this.removeActiveTheme(candidate)
|
||||
}
|
||||
}
|
||||
this.toggleOtherNonLayerableThemes(uiFeature)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,7 +458,7 @@ export class ComponentManager
|
||||
const features: NativeFeatureIdentifier[] = []
|
||||
const uuids: Uuid[] = []
|
||||
|
||||
const strings = this.preferences.getLocalValue(LocalPrefKey.ActiveThemes, [])
|
||||
const strings = new Set(this.preferences.getLocalValue(LocalPrefKey.ActiveThemes, []))
|
||||
for (const string of strings) {
|
||||
const nativeIdentifier = NativeFeatureIdentifier.create(string)
|
||||
if (!nativeIdentifier.isFailed()) {
|
||||
|
||||
@@ -114,8 +114,10 @@ export class GetFeatureStatusUseCase {
|
||||
}
|
||||
|
||||
private isFreeFeature(featureId: NativeFeatureIdentifier) {
|
||||
return [NativeFeatureIdentifier.TYPES.DarkTheme, NativeFeatureIdentifier.TYPES.PlainEditor].includes(
|
||||
featureId.value,
|
||||
)
|
||||
return [
|
||||
NativeFeatureIdentifier.TYPES.DarkTheme,
|
||||
NativeFeatureIdentifier.TYPES.PlainEditor,
|
||||
NativeFeatureIdentifier.TYPES.ProtonTheme,
|
||||
].includes(featureId.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +404,12 @@ export class SessionManager
|
||||
return undefined
|
||||
}
|
||||
|
||||
async register(email: string, password: string, ephemeral: boolean): Promise<UserRegistrationResponseBody> {
|
||||
async register(
|
||||
email: string,
|
||||
password: string,
|
||||
hvmToken: string,
|
||||
ephemeral: boolean,
|
||||
): Promise<UserRegistrationResponseBody> {
|
||||
if (password.length < MINIMUM_PASSWORD_LENGTH) {
|
||||
throw new ApiCallError(
|
||||
ErrorMessage.InsufficientPasswordMessage.replace('%LENGTH%', MINIMUM_PASSWORD_LENGTH.toString()),
|
||||
@@ -429,6 +434,7 @@ export class SessionManager
|
||||
const registerResponse = await this.userApiService.register({
|
||||
email,
|
||||
serverPassword,
|
||||
hvmToken,
|
||||
keyParams,
|
||||
ephemeral,
|
||||
})
|
||||
@@ -503,8 +509,9 @@ export class SessionManager
|
||||
strict = false,
|
||||
ephemeral = false,
|
||||
minAllowedVersion?: Common.ProtocolVersion,
|
||||
hvmToken?: string,
|
||||
): Promise<SessionManagerResponse> {
|
||||
const result = await this.performSignIn(email, password, strict, ephemeral, minAllowedVersion)
|
||||
const result = await this.performSignIn(email, password, strict, ephemeral, minAllowedVersion, hvmToken)
|
||||
if (
|
||||
isErrorResponse(result.response) &&
|
||||
getErrorFromErrorResponse(result.response).tag !== ErrorTag.ClientValidationError &&
|
||||
@@ -515,7 +522,7 @@ export class SessionManager
|
||||
/**
|
||||
* Try signing in with trimmed + lowercase version of email
|
||||
*/
|
||||
return this.performSignIn(cleanedEmail, password, strict, ephemeral, minAllowedVersion)
|
||||
return this.performSignIn(cleanedEmail, password, strict, ephemeral, minAllowedVersion, hvmToken)
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
@@ -530,6 +537,7 @@ export class SessionManager
|
||||
strict = false,
|
||||
ephemeral = false,
|
||||
minAllowedVersion?: Common.ProtocolVersion,
|
||||
hvmToken?: string,
|
||||
): Promise<SessionManagerResponse> {
|
||||
const paramsResult = await this.retrieveKeyParams({
|
||||
email,
|
||||
@@ -593,7 +601,7 @@ export class SessionManager
|
||||
}
|
||||
}
|
||||
const rootKey = await this.encryptionService.computeRootKey(password, keyParams)
|
||||
const signInResponse = await this.bypassChecksAndSignInWithRootKey(email, rootKey, ephemeral)
|
||||
const signInResponse = await this.bypassChecksAndSignInWithRootKey(email, rootKey, ephemeral, hvmToken)
|
||||
|
||||
return {
|
||||
response: signInResponse,
|
||||
@@ -604,6 +612,7 @@ export class SessionManager
|
||||
email: string,
|
||||
rootKey: SNRootKey,
|
||||
ephemeral = false,
|
||||
hvmToken?: string,
|
||||
): Promise<HttpResponse<SignInResponse>> {
|
||||
const { wrappingKey, canceled } = await this.challengeService.getWrappingKeyIfApplicable()
|
||||
|
||||
@@ -619,6 +628,7 @@ export class SessionManager
|
||||
email,
|
||||
serverPassword: rootKey.serverPassword as string,
|
||||
ephemeral,
|
||||
hvmToken,
|
||||
})
|
||||
|
||||
if (!signInResponse.data || isErrorResponse(signInResponse)) {
|
||||
@@ -874,7 +884,14 @@ export class SessionManager
|
||||
const willRefreshTokenExpireSoon = refreshTokenExpiration.getTime() - Date.now() < ThirtyMinutes
|
||||
|
||||
if (willAccessTokenExpireSoon || willRefreshTokenExpireSoon) {
|
||||
return this.httpService.refreshSession()
|
||||
const refreshSessionResultOrError = await this.httpService.refreshSession()
|
||||
if (refreshSessionResultOrError.isFailed()) {
|
||||
return false
|
||||
}
|
||||
|
||||
const refreshSessionResult = refreshSessionResultOrError.getValue()
|
||||
|
||||
return isErrorResponse(refreshSessionResult)
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
@@ -2,7 +2,6 @@ import { SettingName } from '@standardnotes/domain-core'
|
||||
import { SettingData } from '@standardnotes/responses'
|
||||
import {
|
||||
MuteSignInEmailsOption,
|
||||
MuteFailedBackupsEmailsOption,
|
||||
EmailBackupFrequency,
|
||||
ListedAuthorSecretsData,
|
||||
LogSessionUserAgentOption,
|
||||
@@ -13,7 +12,6 @@ type SettingType =
|
||||
| EmailBackupFrequency
|
||||
| ListedAuthorSecretsData
|
||||
| LogSessionUserAgentOption
|
||||
| MuteFailedBackupsEmailsOption
|
||||
| MuteSignInEmailsOption
|
||||
| MuteMarketingEmailsOption
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('server session', function () {
|
||||
password: password,
|
||||
})
|
||||
|
||||
const response = await application.legacyApi.refreshSession()
|
||||
const response = await application.legacyApi.deprecatedRefreshSessionOnlyUsedInE2eTests()
|
||||
|
||||
expect(response.status).to.equal(200)
|
||||
expect(response.data.session.access_token).to.be.a('string')
|
||||
@@ -178,7 +178,7 @@ describe('server session', function () {
|
||||
expect(sessionFromStorage.refreshExpiration).to.equal(sessionFromApiService.refreshToken.expiresAt)
|
||||
expect(sessionFromStorage.readonlyAccess).to.equal(sessionFromApiService.isReadOnly())
|
||||
|
||||
await application.legacyApi.refreshSession()
|
||||
await application.legacyApi.deprecatedRefreshSessionOnlyUsedInE2eTests()
|
||||
|
||||
const updatedSessionFromStorage = await getSessionFromStorage(application)
|
||||
const updatedSessionFromApiService = application.legacyApi.getSession()
|
||||
@@ -407,7 +407,7 @@ describe('server session', function () {
|
||||
|
||||
await sleepUntilSessionExpires(application, false)
|
||||
|
||||
const refreshSessionResponse = await application.legacyApi.refreshSession()
|
||||
const refreshSessionResponse = await application.legacyApi.deprecatedRefreshSessionOnlyUsedInE2eTests()
|
||||
|
||||
expect(refreshSessionResponse.status).to.equal(400)
|
||||
/**
|
||||
@@ -452,7 +452,7 @@ describe('server session', function () {
|
||||
})
|
||||
application.sessions.initializeFromDisk()
|
||||
|
||||
const refreshSessionResponse = await application.legacyApi.refreshSession()
|
||||
const refreshSessionResponse = await application.legacyApi.deprecatedRefreshSessionOnlyUsedInE2eTests()
|
||||
|
||||
expect(refreshSessionResponse.status).to.equal(400)
|
||||
expect(refreshSessionResponse.data.error.tag).to.equal('invalid-refresh-token')
|
||||
@@ -470,7 +470,7 @@ describe('server session', function () {
|
||||
password: password,
|
||||
})
|
||||
|
||||
const refreshPromise = application.legacyApi.refreshSession()
|
||||
const refreshPromise = application.legacyApi.deprecatedRefreshSessionOnlyUsedInE2eTests()
|
||||
const syncResponse = await application.legacyApi.sync([])
|
||||
|
||||
expect(syncResponse.data.error).to.be.ok
|
||||
@@ -481,6 +481,57 @@ describe('server session', function () {
|
||||
await refreshPromise
|
||||
})
|
||||
|
||||
it('should tell the client to refresh the token if one is used during the cooldown period after a refresh', async function () {
|
||||
await Factory.registerUserToApplication({
|
||||
application: application,
|
||||
email: email,
|
||||
password: password,
|
||||
})
|
||||
|
||||
const mimickApplyingSessionFromTheServerUnsuccessfully = () => {}
|
||||
const originalSetSessionFn = application.http.setSession
|
||||
const originalRefreshSessionCallbackFn = application.http.refreshSessionCallback
|
||||
application.http.setSession = mimickApplyingSessionFromTheServerUnsuccessfully
|
||||
application.http.refreshSessionCallback = mimickApplyingSessionFromTheServerUnsuccessfully
|
||||
|
||||
const refreshResultOrError = await application.http.refreshSession()
|
||||
expect(refreshResultOrError.isFailed()).to.equal(false)
|
||||
|
||||
const refreshResult = refreshResultOrError.getValue()
|
||||
expect(isErrorResponse(refreshResult)).to.equal(false)
|
||||
|
||||
const secondRefreshResultOrErrorWithNotAppliedSession = await application.http.refreshSession()
|
||||
expect(secondRefreshResultOrErrorWithNotAppliedSession.isFailed()).to.equal(false)
|
||||
|
||||
const secondRefreshResultWithNotAppliedSession = secondRefreshResultOrErrorWithNotAppliedSession.getValue()
|
||||
expect(isErrorResponse(secondRefreshResultWithNotAppliedSession)).to.equal(false)
|
||||
|
||||
application.http.setSession = originalSetSessionFn
|
||||
application.http.refreshSessionCallback = originalRefreshSessionCallbackFn
|
||||
})
|
||||
|
||||
it('if session renewal response is dropped, next sync with server should return a 498 and successfully renew the session', async function () {
|
||||
await Factory.registerUserToApplication({
|
||||
application: application,
|
||||
email: email,
|
||||
password: password,
|
||||
})
|
||||
|
||||
await sleepUntilSessionExpires(application)
|
||||
|
||||
const refreshSpy = sinon.spy(application.http, 'refreshSession')
|
||||
|
||||
/**
|
||||
* With this sync, we expect refreshSession to be called twice, once where the response is dropped,
|
||||
* and the other time where the request succeeds
|
||||
*/
|
||||
application.http.__simulateNextSessionRefreshResponseDrop = true
|
||||
await application.sync.sync(syncOptions)
|
||||
await application.sync.sync(syncOptions)
|
||||
|
||||
expect(refreshSpy.callCount).to.equal(2)
|
||||
})
|
||||
|
||||
it('notes should be synced as expected after refreshing a session', async function () {
|
||||
await Factory.registerUserToApplication({
|
||||
application: application,
|
||||
|
||||
@@ -123,12 +123,12 @@ describe('settings service', function () {
|
||||
true,
|
||||
)
|
||||
await application.settings.updateSetting(
|
||||
SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue(),
|
||||
MuteFailedBackupsEmailsOption.Muted,
|
||||
SettingName.create(SettingName.NAMES.LogSessionUserAgent).getValue(),
|
||||
LogSessionUserAgentOption.Enabled,
|
||||
)
|
||||
const settings = await application.settings.listSettings()
|
||||
expect(settings.getSettingValue(SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue())).to.eql(
|
||||
MuteFailedBackupsEmailsOption.Muted,
|
||||
expect(settings.getSettingValue(SettingName.create(SettingName.NAMES.LogSessionUserAgent).getValue())).to.eql(
|
||||
LogSessionUserAgentOption.Enabled,
|
||||
)
|
||||
expect(settings.getSettingValue(SettingName.create(SettingName.NAMES.MfaSecret).getValue())).to.not.be.ok
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/snjs",
|
||||
"version": "2.208.12",
|
||||
"version": "2.208.18",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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.8.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/styles
|
||||
|
||||
## [1.8.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-10-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/styles
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/styles",
|
||||
"version": "1.8.1",
|
||||
"version": "1.8.2",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -45,3 +45,10 @@
|
||||
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.windows-web,
|
||||
.windows-desktop {
|
||||
.md\:hover\:overflow-y-auto {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.37.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.37.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.37.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.37.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed issue with system color scheme on mobile ([45ca99c](https://github.com/standardnotes/app/commit/45ca99c627e51b0b6b0b55e211788fa4cb0f1c70))
|
||||
|
||||
## [1.37.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed issue with system color scheme not being applied correctly on start ([7967dde](https://github.com/standardnotes/app/commit/7967ddead790d89478554cd6a96429fd06958590))
|
||||
|
||||
## [1.37.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.37.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.37.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/ui-services",
|
||||
"version": "1.37.3",
|
||||
"version": "1.37.10",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -27,7 +27,9 @@ const DefaultThemeIdentifier = 'Default'
|
||||
|
||||
export class ThemeManager extends AbstractUIService {
|
||||
private themesActiveInTheUI: ActiveThemeList
|
||||
private lastUseDeviceThemeSettings = false
|
||||
private lastUseDeviceThemeSettings: boolean | undefined
|
||||
private lastAutoLightTheme: string | undefined
|
||||
private lastAutoDarkTheme: string | undefined
|
||||
|
||||
constructor(
|
||||
application: WebApplicationInterface,
|
||||
@@ -89,7 +91,7 @@ export class ThemeManager extends AbstractUIService {
|
||||
break
|
||||
}
|
||||
case ApplicationEvent.FeaturesAvailabilityChanged: {
|
||||
this.handleFeaturesAvailabilityChanged()
|
||||
this.handleFeaturesAvailabilityChanged().catch(console.error)
|
||||
break
|
||||
}
|
||||
case ApplicationEvent.Launched: {
|
||||
@@ -166,15 +168,25 @@ export class ThemeManager extends AbstractUIService {
|
||||
|
||||
this.toggleTranslucentUIColors()
|
||||
|
||||
const useDeviceThemeSettings = this.preferences.getLocalValue(LocalPrefKey.UseSystemColorScheme, false)
|
||||
const useSystemColorScheme = this.preferences.getLocalValue(LocalPrefKey.UseSystemColorScheme, false)
|
||||
const autoLightTheme = this.preferences.getLocalValue(LocalPrefKey.AutoLightThemeIdentifier, DefaultThemeIdentifier)
|
||||
const autoDarkTheme = this.preferences.getLocalValue(
|
||||
LocalPrefKey.AutoDarkThemeIdentifier,
|
||||
NativeFeatureIdentifier.TYPES.DarkTheme,
|
||||
)
|
||||
|
||||
const hasPreferenceChanged = useDeviceThemeSettings !== this.lastUseDeviceThemeSettings
|
||||
const hasPreferenceChanged =
|
||||
useSystemColorScheme !== this.lastUseDeviceThemeSettings ||
|
||||
autoLightTheme !== this.lastAutoLightTheme ||
|
||||
autoDarkTheme !== this.lastAutoDarkTheme
|
||||
|
||||
if (hasPreferenceChanged) {
|
||||
this.lastUseDeviceThemeSettings = useDeviceThemeSettings
|
||||
this.lastUseDeviceThemeSettings = useSystemColorScheme
|
||||
this.lastAutoLightTheme = autoLightTheme
|
||||
this.lastAutoDarkTheme = autoDarkTheme
|
||||
}
|
||||
|
||||
if (hasPreferenceChanged && useDeviceThemeSettings) {
|
||||
if (hasPreferenceChanged && useSystemColorScheme) {
|
||||
let prefersDarkColorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
|
||||
if (this.application.isNativeMobileWeb()) {
|
||||
@@ -185,7 +197,7 @@ export class ThemeManager extends AbstractUIService {
|
||||
}
|
||||
}
|
||||
|
||||
private handleFeaturesAvailabilityChanged(): void {
|
||||
private async handleFeaturesAvailabilityChanged() {
|
||||
let hasChange = false
|
||||
|
||||
for (const theme of this.themesActiveInTheUI.asThemes()) {
|
||||
@@ -205,6 +217,15 @@ export class ThemeManager extends AbstractUIService {
|
||||
}
|
||||
}
|
||||
|
||||
const shouldSetThemeAsPerColorScheme = this.preferences.getLocalValue(LocalPrefKey.UseSystemColorScheme, false)
|
||||
if (shouldSetThemeAsPerColorScheme) {
|
||||
let prefersDarkColorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
if (this.application.isNativeMobileWeb()) {
|
||||
prefersDarkColorScheme = (await this.application.mobileDevice.getColorScheme()) === 'dark'
|
||||
}
|
||||
hasChange = this.setThemeAsPerColorScheme(prefersDarkColorScheme)
|
||||
}
|
||||
|
||||
if (hasChange) {
|
||||
void this.cacheThemeState()
|
||||
}
|
||||
@@ -218,7 +239,9 @@ export class ThemeManager extends AbstractUIService {
|
||||
}
|
||||
}
|
||||
|
||||
private setThemeAsPerColorScheme(prefersDarkColorScheme: boolean) {
|
||||
private setThemeAsPerColorScheme(prefersDarkColorScheme: boolean): boolean {
|
||||
let didChangeTheme = false
|
||||
|
||||
const preference = prefersDarkColorScheme
|
||||
? LocalPrefKey.AutoDarkThemeIdentifier
|
||||
: LocalPrefKey.AutoLightThemeIdentifier
|
||||
@@ -239,6 +262,7 @@ export class ThemeManager extends AbstractUIService {
|
||||
const toggleActiveTheme = () => {
|
||||
if (activeTheme) {
|
||||
void this.components.toggleTheme(activeTheme)
|
||||
didChangeTheme = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,10 +270,17 @@ export class ThemeManager extends AbstractUIService {
|
||||
toggleActiveTheme()
|
||||
} else {
|
||||
const theme = themes.find((theme) => theme.featureIdentifier === themeIdentifier)
|
||||
if (theme && !this.components.isThemeActive(theme)) {
|
||||
this.components.toggleTheme(theme).catch(console.error)
|
||||
if (theme) {
|
||||
if (!this.components.isThemeActive(theme)) {
|
||||
this.components.toggleTheme(theme, true).catch(console.error)
|
||||
} else {
|
||||
this.components.toggleOtherNonLayerableThemes(theme)
|
||||
}
|
||||
didChangeTheme = true
|
||||
}
|
||||
}
|
||||
|
||||
return didChangeTheme
|
||||
}
|
||||
|
||||
private async activateCachedThemes() {
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [3.192.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.192.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.192.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.192.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.192.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed super note "Table of contents" not working on mobile ([73eba38](https://github.com/standardnotes/app/commit/73eba38c1444e85839d771fb297e3035eabd99c8))
|
||||
|
||||
## [3.192.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.192.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.192.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.192.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.192.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.192.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.192.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.192.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "feat: Markdown text is now parsed correctly when pasted" ([bb81c8a](https://github.com/standardnotes/app/commit/bb81c8acfc470df76a70ab74fa8d487216e2d190))
|
||||
|
||||
# [3.192.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.191.24](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.191.23](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.191.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
@@ -11,7 +79,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use API v0 for Android [skip e2e] ([6d872cd](https://github.com/standardnotes/app/commit/6d872cd45a504c824d6fa2d3325e1ba2401f3046))
|
||||
* Fixed issue with re-authentication prompts ([6d872cd](https://github.com/standardnotes/app/commit/6d872cd45a504c824d6fa2d3325e1ba2401f3046))
|
||||
|
||||
## [3.191.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)
|
||||
|
||||
@@ -29,7 +97,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use API v0 on iOS ([#2858](https://github.com/standardnotes/app/issues/2858)) [skip e2e] ([3b2f761](https://github.com/standardnotes/app/commit/3b2f76166126a5b11f35b64aacefa37618622c3d))
|
||||
* Fixed issue with re-authentication prompts ([#2858](https://github.com/standardnotes/app/issues/2858)) [skip e2e] ([3b2f761](https://github.com/standardnotes/app/commit/3b2f76166126a5b11f35b64aacefa37618622c3d))
|
||||
|
||||
## [3.191.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-07)
|
||||
|
||||
|
||||
@@ -1,5 +1,187 @@
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"version": "3.192.15",
|
||||
"title": "[3.192.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.192.14",
|
||||
"title": "[3.192.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-10)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.192.13",
|
||||
"title": "[3.192.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-08)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.192.10",
|
||||
"title": "[3.192.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-04)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.192.9",
|
||||
"title": "[3.192.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-04-01)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Fixed super note \"Table of contents\" not working on mobile ([73eba38](https://github.com/standardnotes/app/commit/73eba38c1444e85839d771fb297e3035eabd99c8))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Fixed super note \"Table of contents\" not working on mobile (73eba38)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Fixed super note \"Table of contents\" not working on mobile (73eba38)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.192.8",
|
||||
"title": "[3.192.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-28)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.192.7",
|
||||
"title": "[3.192.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-27)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.192.6",
|
||||
"title": "[3.192.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.192.5",
|
||||
"title": "[3.192.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-26)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.192.4",
|
||||
"title": "[3.192.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.192.3",
|
||||
"title": "[3.192.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.192.2",
|
||||
"title": "[3.192.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-22)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.192.1",
|
||||
"title": "[3.192.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)",
|
||||
"date": null,
|
||||
"body": "### Reverts\n\n* Revert \"feat: Markdown text is now parsed correctly when pasted\" ([bb81c8a](https://github.com/standardnotes/app/commit/bb81c8acfc470df76a70ab74fa8d487216e2d190))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Revert \"feat: Markdown text is now parsed correctly when pasted\" (bb81c8a)"
|
||||
],
|
||||
"Reverts": [
|
||||
"Revert \"feat: Markdown text is now parsed correctly when pasted\" (bb81c8a)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.192.0",
|
||||
"title": "[3.192.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.191.24",
|
||||
"title": "[3.191.24](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-14)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.191.23",
|
||||
"title": "[3.191.23](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-13)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.191.22",
|
||||
"title": "[3.191.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-12)",
|
||||
@@ -15,13 +197,13 @@
|
||||
"version": "3.191.21",
|
||||
"title": "[3.191.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-11)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* use API v0 for Android [skip e2e] ([6d872cd](https://github.com/standardnotes/app/commit/6d872cd45a504c824d6fa2d3325e1ba2401f3046))",
|
||||
"body": "### Bug Fixes\n\n* Fixed issue with re-authentication prompts ([6d872cd](https://github.com/standardnotes/app/commit/6d872cd45a504c824d6fa2d3325e1ba2401f3046))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"use API v0 for Android skip e2e] ([6d872cd)"
|
||||
"Fixed issue with re-authentication prompts (6d872cd)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"use API v0 for Android skip e2e] ([6d872cd)"
|
||||
"Fixed issue with re-authentication prompts (6d872cd)"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -62,13 +244,13 @@
|
||||
"version": "3.191.17",
|
||||
"title": "[3.191.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2024-03-07)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* use API v0 on iOS ([#2858](https://github.com/standardnotes/app/issues/2858)) [skip e2e] ([3b2f761](https://github.com/standardnotes/app/commit/3b2f76166126a5b11f35b64aacefa37618622c3d))",
|
||||
"body": "### Bug Fixes\n\n* Fixed issue with re-authentication prompts ([#2858](https://github.com/standardnotes/app/issues/2858)) [skip e2e] ([3b2f761](https://github.com/standardnotes/app/commit/3b2f76166126a5b11f35b64aacefa37618622c3d))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"use API v0 on iOS (#2858) skip e2e] ([3b2f761)"
|
||||
"Fixed issue with re-authentication prompts (#2858) skip e2e] ([3b2f761)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"use API v0 on iOS (#2858) skip e2e] ([3b2f761)"
|
||||
"Fixed issue with re-authentication prompts (#2858) skip e2e] ([3b2f761)"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@standardnotes/web",
|
||||
"version": "3.191.22",
|
||||
"version": "3.192.15",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "dist/app.js",
|
||||
"author": "Standard Notes.",
|
||||
"author": "Standard Notes",
|
||||
"private": true,
|
||||
"files": [
|
||||
"dist"
|
||||
@@ -110,6 +110,7 @@
|
||||
"dependencies": {
|
||||
"@ariakit/react": "^0.3.9",
|
||||
"@lexical/headless": "0.13.1",
|
||||
"@lexical/link": "0.13.1",
|
||||
"@lexical/list": "0.13.1",
|
||||
"@lexical/rich-text": "0.13.1",
|
||||
"@lexical/utils": "0.13.1",
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
:root {
|
||||
--background-norm: #16141c;
|
||||
--background-weak: #292733;
|
||||
--background-strong: #3f3b4c;
|
||||
--text-norm: #ffffff;
|
||||
--text-weak: #a7a4b5;
|
||||
--border-norm: #4a4658;
|
||||
--border-weak: #343140;
|
||||
--primary: #8a6eff;
|
||||
--interaction-norm: #6d4aff;
|
||||
|
||||
--sn-stylekit-background-color: var(--background-norm);
|
||||
--sn-stylekit-foreground-color: var(--text-norm);
|
||||
--sn-stylekit-border-color: var(--border-weak);
|
||||
--sn-stylekit-contrast-background-color: var(--background-weak);
|
||||
--sn-stylekit-contrast-foreground-color: var(--text-norm);
|
||||
--sn-stylekit-contrast-border-color: var(--border-norm);
|
||||
--sn-stylekit-secondary-background-color: var(--background-weak);
|
||||
--sn-stylekit-secondary-foreground-color: var(--text-norm);
|
||||
--sn-stylekit-info-color: var(--interaction-norm);
|
||||
--sn-stylekit-info-backdrop-color: #6e4aff0f;
|
||||
--sn-stylekit-passive-color-0: var(--text-weak);
|
||||
--sn-stylekit-passive-color-3: var(--background-strong);
|
||||
--sn-stylekit-passive-color-4: #3f3b4c;
|
||||
--sn-stylekit-passive-color-5: var(--background-weak);
|
||||
|
||||
--navigation-item-selected-background-color: var(--background-strong);
|
||||
|
||||
--sn-stylekit-paragraph-text-color: var(--text-weak);
|
||||
--sn-stylekit-scrollbar-thumb-color: var(--primary);
|
||||
}
|
||||
|
||||
a,
|
||||
.Lexical__link,
|
||||
.Lexical__textCode {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.Collapsible__container,
|
||||
.Lexical__tableCellHeader {
|
||||
border-color: var(--border-norm);
|
||||
}
|
||||
|
||||
.Lexical__quote {
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
#blocks-editor hr.selected {
|
||||
outline-color: var(--primary);
|
||||
}
|
||||
@@ -16,6 +16,8 @@ import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import IconButton from '@/Components/Button/IconButton'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { useCaptcha } from '@/Hooks/useCaptcha'
|
||||
import { isErrorResponse } from '@standardnotes/snjs'
|
||||
|
||||
type Props = {
|
||||
setMenuPane: (pane: AccountMenuPane) => void
|
||||
@@ -33,6 +35,39 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
|
||||
const [shouldMergeLocal, setShouldMergeLocal] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const [hvmToken, setHVMToken] = useState('')
|
||||
const [captchaURL, setCaptchaURL] = useState('')
|
||||
|
||||
const register = useCallback(() => {
|
||||
setIsRegistering(true)
|
||||
application
|
||||
.register(email, password, hvmToken, isEphemeral, shouldMergeLocal)
|
||||
.then(() => {
|
||||
application.accountMenuController.closeAccountMenu()
|
||||
application.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
setError(err.message)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsRegistering(false)
|
||||
})
|
||||
}, [application, email, hvmToken, isEphemeral, password, shouldMergeLocal])
|
||||
|
||||
const captchaIframe = useCaptcha(captchaURL, (token) => {
|
||||
setHVMToken(token)
|
||||
setCaptchaURL('')
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!hvmToken) {
|
||||
return
|
||||
}
|
||||
|
||||
register()
|
||||
}, [hvmToken, register])
|
||||
|
||||
const passwordInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -51,6 +86,28 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
|
||||
setShouldMergeLocal(!shouldMergeLocal)
|
||||
}, [shouldMergeLocal])
|
||||
|
||||
const checkIfCaptchaRequiredAndRegister = useCallback(() => {
|
||||
application
|
||||
.getCaptchaUrl()
|
||||
.then((response) => {
|
||||
if (isErrorResponse(response)) {
|
||||
throw new Error()
|
||||
}
|
||||
const { captchaUIUrl } = response.data
|
||||
if (captchaUIUrl) {
|
||||
setCaptchaURL(captchaUIUrl)
|
||||
} else {
|
||||
setCaptchaURL('')
|
||||
register()
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
setCaptchaURL('')
|
||||
register()
|
||||
})
|
||||
}, [application, register])
|
||||
|
||||
const handleConfirmFormSubmit: FormEventHandler = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault()
|
||||
@@ -60,28 +117,16 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
|
||||
return
|
||||
}
|
||||
|
||||
if (password === confirmPassword) {
|
||||
setIsRegistering(true)
|
||||
application
|
||||
.register(email, password, isEphemeral, shouldMergeLocal)
|
||||
.then(() => {
|
||||
application.accountMenuController.closeAccountMenu()
|
||||
application.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
setError(err.message)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsRegistering(false)
|
||||
})
|
||||
} else {
|
||||
if (password !== confirmPassword) {
|
||||
setError(STRING_NON_MATCHING_PASSWORDS)
|
||||
setConfirmPassword('')
|
||||
passwordInputRef.current?.focus()
|
||||
return
|
||||
}
|
||||
|
||||
checkIfCaptchaRequiredAndRegister()
|
||||
},
|
||||
[application, confirmPassword, email, isEphemeral, password, shouldMergeLocal],
|
||||
[checkIfCaptchaRequiredAndRegister, confirmPassword, password],
|
||||
)
|
||||
|
||||
const handleKeyDown: KeyboardEventHandler = useCallback(
|
||||
@@ -100,35 +145,26 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
|
||||
setMenuPane(AccountMenuPane.Register)
|
||||
}, [setMenuPane])
|
||||
|
||||
return (
|
||||
const confirmPasswordForm = (
|
||||
<>
|
||||
<div className="mb-3 mt-1 flex items-center px-3">
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
title="Go back"
|
||||
className="mr-2 flex p-0 text-neutral"
|
||||
onClick={handleGoBack}
|
||||
focusable={true}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
<div className="text-base font-bold">Confirm password</div>
|
||||
</div>
|
||||
<div className="mb-3 px-3 text-sm">
|
||||
Because your notes are encrypted using your password,{' '}
|
||||
<span className="text-danger">Standard Notes does not have a password reset option</span>. If you forget your
|
||||
password, you will permanently lose access to your data.
|
||||
</div>
|
||||
<form onSubmit={handleConfirmFormSubmit} className="mb-1 px-3">
|
||||
<DecoratedPasswordInput
|
||||
className={{ container: 'mb-2' }}
|
||||
disabled={isRegistering}
|
||||
left={[<Icon type="password" className="text-neutral" />]}
|
||||
onChange={handlePasswordChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Confirm password"
|
||||
ref={passwordInputRef}
|
||||
value={confirmPassword}
|
||||
/>
|
||||
{!isRegistering && (
|
||||
<DecoratedPasswordInput
|
||||
className={{ container: 'mb-2' }}
|
||||
disabled={isRegistering}
|
||||
left={[<Icon type="password" className="text-neutral" />]}
|
||||
onChange={handlePasswordChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Confirm password"
|
||||
ref={passwordInputRef}
|
||||
value={confirmPassword}
|
||||
/>
|
||||
)}
|
||||
{error ? <div className="my-2 text-danger">{error}</div> : null}
|
||||
<Button
|
||||
primary
|
||||
@@ -157,6 +193,23 @@ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, passwor
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-3 mt-1 flex items-center px-3">
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
title="Go back"
|
||||
className="mr-2 flex p-0 text-neutral"
|
||||
onClick={handleGoBack}
|
||||
focusable={true}
|
||||
disabled={isRegistering}
|
||||
/>
|
||||
<div className="text-base font-bold">{captchaURL ? 'Human verification' : 'Confirm password'}</div>
|
||||
</div>
|
||||
{captchaURL ? <div className="p-[10px]">{captchaIframe}</div> : confirmPasswordForm}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(ConfirmPassword)
|
||||
|
||||
@@ -10,8 +10,9 @@ import Icon from '@/Components/Icon/Icon'
|
||||
import IconButton from '@/Components/Button/IconButton'
|
||||
import AdvancedOptions from './AdvancedOptions'
|
||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||
import { getErrorFromErrorResponse, isErrorResponse } from '@standardnotes/snjs'
|
||||
import { getErrorFromErrorResponse, isErrorResponse, getCaptchaHeader } from '@standardnotes/snjs'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { useCaptcha } from '@/Hooks/useCaptcha'
|
||||
|
||||
type Props = {
|
||||
setMenuPane: (pane: AccountMenuPane) => void
|
||||
@@ -34,6 +35,15 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
||||
|
||||
const [isRecoverySignIn, setIsRecoverySignIn] = useState(false)
|
||||
|
||||
const [captchaURL, setCaptchaURL] = useState('')
|
||||
const [showCaptcha, setShowCaptcha] = useState(false)
|
||||
const [hvmToken, setHVMToken] = useState('')
|
||||
const captchaIframe = useCaptcha(captchaURL, (token) => {
|
||||
setHVMToken(token)
|
||||
setShowCaptcha(false)
|
||||
setCaptchaURL('')
|
||||
})
|
||||
|
||||
const emailInputRef = useRef<HTMLInputElement>(null)
|
||||
const passwordInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
@@ -95,8 +105,12 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
||||
passwordInputRef?.current?.blur()
|
||||
|
||||
application
|
||||
.signIn(email, password, isStrictSignin, isEphemeral, shouldMergeLocal)
|
||||
.signIn(email, password, isStrictSignin, isEphemeral, shouldMergeLocal, false, hvmToken)
|
||||
.then((response) => {
|
||||
const captchaURL = getCaptchaHeader(response)
|
||||
if (captchaURL) {
|
||||
setCaptchaURL(captchaURL)
|
||||
}
|
||||
if (isErrorResponse(response)) {
|
||||
throw new Error(getErrorFromErrorResponse(response).message)
|
||||
}
|
||||
@@ -106,12 +120,13 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
||||
console.error(err)
|
||||
setError(err.message ?? err.toString())
|
||||
setPassword('')
|
||||
setHVMToken('')
|
||||
passwordInputRef?.current?.blur()
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSigningIn(false)
|
||||
})
|
||||
}, [application, email, isEphemeral, isStrictSignin, password, shouldMergeLocal])
|
||||
}, [application, email, hvmToken, isEphemeral, isStrictSignin, password, shouldMergeLocal])
|
||||
|
||||
const recoverySignIn = useCallback(() => {
|
||||
setIsSigningIn(true)
|
||||
@@ -123,10 +138,21 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
||||
recoveryCodes,
|
||||
username: email,
|
||||
password: password,
|
||||
hvmToken,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.isFailed()) {
|
||||
throw new Error(result.getError())
|
||||
const error = result.getError()
|
||||
try {
|
||||
const parsed = JSON.parse(error)
|
||||
if (parsed.captchaURL) {
|
||||
setCaptchaURL(parsed.captchaURL)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
setCaptchaURL('')
|
||||
}
|
||||
throw new Error(error)
|
||||
}
|
||||
application.accountMenuController.closeAccountMenu()
|
||||
})
|
||||
@@ -134,12 +160,13 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
||||
console.error(err)
|
||||
setError(err.message ?? err.toString())
|
||||
setPassword('')
|
||||
setHVMToken('')
|
||||
passwordInputRef?.current?.blur()
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSigningIn(false)
|
||||
})
|
||||
}, [application, email, password, recoveryCodes])
|
||||
}, [application.accountMenuController, application.signInWithRecoveryCodes, email, hvmToken, password, recoveryCodes])
|
||||
|
||||
const onPrivateUsernameChange = useCallback(
|
||||
(newisPrivateUsername: boolean, privateUsernameIdentifier?: string) => {
|
||||
@@ -151,28 +178,37 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
||||
[setEmail],
|
||||
)
|
||||
|
||||
const performSignIn = useCallback(() => {
|
||||
if (!email || email.length === 0) {
|
||||
emailInputRef?.current?.focus()
|
||||
return
|
||||
}
|
||||
|
||||
if (!password || password.length === 0) {
|
||||
passwordInputRef?.current?.focus()
|
||||
return
|
||||
}
|
||||
|
||||
if (isRecoverySignIn) {
|
||||
recoverySignIn()
|
||||
return
|
||||
}
|
||||
|
||||
signIn()
|
||||
}, [email, isRecoverySignIn, password, recoverySignIn, signIn])
|
||||
|
||||
const handleSignInFormSubmit = useCallback(
|
||||
(e: React.SyntheticEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!email || email.length === 0) {
|
||||
emailInputRef?.current?.focus()
|
||||
if (captchaURL) {
|
||||
setShowCaptcha(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (!password || password.length === 0) {
|
||||
passwordInputRef?.current?.focus()
|
||||
return
|
||||
}
|
||||
|
||||
if (isRecoverySignIn) {
|
||||
recoverySignIn()
|
||||
return
|
||||
}
|
||||
|
||||
signIn()
|
||||
performSignIn()
|
||||
},
|
||||
[email, password, isRecoverySignIn, signIn, recoverySignIn],
|
||||
[captchaURL, performSignIn],
|
||||
)
|
||||
|
||||
const handleKeyDown: KeyboardEventHandler = useCallback(
|
||||
@@ -184,19 +220,16 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
||||
[handleSignInFormSubmit],
|
||||
)
|
||||
|
||||
return (
|
||||
useEffect(() => {
|
||||
if (!hvmToken) {
|
||||
return
|
||||
}
|
||||
|
||||
performSignIn()
|
||||
}, [hvmToken, performSignIn])
|
||||
|
||||
const signInForm = (
|
||||
<>
|
||||
<div className="mb-3 mt-1 flex items-center px-3">
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
title="Go back"
|
||||
className="mr-2 flex p-0 text-neutral"
|
||||
onClick={() => setMenuPane(AccountMenuPane.GeneralMenu)}
|
||||
focusable={true}
|
||||
disabled={isSigningIn}
|
||||
/>
|
||||
<div className="text-base font-bold">Sign in</div>
|
||||
</div>
|
||||
<div className="mb-1 px-3">
|
||||
<DecoratedInput
|
||||
className={{ container: `mb-2 ${error ? 'border-danger' : null}` }}
|
||||
@@ -257,6 +290,23 @@ const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-3 mt-1 flex items-center px-3">
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
title="Go back"
|
||||
className="mr-2 flex p-0 text-neutral"
|
||||
onClick={() => setMenuPane(AccountMenuPane.GeneralMenu)}
|
||||
focusable={true}
|
||||
disabled={isSigningIn}
|
||||
/>
|
||||
<div className="text-base font-bold">{showCaptcha ? 'Human verification' : 'Sign in'}</div>
|
||||
</div>
|
||||
{showCaptcha ? <div className="p-[10px]">{captchaIframe}</div> : signInForm}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(SignInPane)
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { WebApplicationGroup } from '@/Application/WebApplicationGroup'
|
||||
import { getPlatformString } from '@/Utils'
|
||||
import { ApplicationEvent, Challenge, removeFromArray, WebAppEvent } from '@standardnotes/snjs'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
Challenge,
|
||||
getErrorMessageFromErrorResponseBody,
|
||||
HttpErrorResponseBody,
|
||||
removeFromArray,
|
||||
WebAppEvent,
|
||||
} from '@standardnotes/snjs'
|
||||
import { alertDialog, isIOS, RouteType } from '@standardnotes/ui-services'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import Footer from '@/Components/Footer/Footer'
|
||||
@@ -117,7 +124,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
onAppLaunch()
|
||||
}
|
||||
|
||||
const removeAppObserver = application.addEventObserver(async (eventName) => {
|
||||
const removeAppObserver = application.addEventObserver(async (eventName, data?: unknown) => {
|
||||
if (eventName === ApplicationEvent.Started) {
|
||||
onAppStart()
|
||||
} else if (eventName === ApplicationEvent.Launched) {
|
||||
@@ -147,6 +154,14 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
type: ToastType.Error,
|
||||
message: 'Too many requests. Please try again later.',
|
||||
})
|
||||
} else if (eventName === ApplicationEvent.FailedSync) {
|
||||
addToast({
|
||||
type: ToastType.Error,
|
||||
message: getErrorMessageFromErrorResponseBody(
|
||||
data as HttpErrorResponseBody,
|
||||
'Sync error. Please try again later.',
|
||||
),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -23,12 +23,15 @@ const QuickSettingsButton = ({ application, isMobileNavigation = false }: Props)
|
||||
const toggleMenu = () => setIsOpen(!isOpen)
|
||||
|
||||
useEffect(() => {
|
||||
const darkThemeFeature = new UIFeature(GetDarkThemeFeature())
|
||||
|
||||
return commandService.addCommandHandler({
|
||||
command: TOGGLE_DARK_MODE_COMMAND,
|
||||
category: 'General',
|
||||
description: 'Toggle dark mode',
|
||||
onKeyDown: () => {
|
||||
void application.componentManager.toggleTheme(new UIFeature(GetDarkThemeFeature()))
|
||||
void application.componentManager.toggleTheme(darkThemeFeature)
|
||||
return true
|
||||
},
|
||||
})
|
||||
}, [application, commandService])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { convertStringifiedBooleanToBoolean, isDesktopApplication } from '@/Utils'
|
||||
import { isDesktopApplication } from '@/Utils'
|
||||
import { STRING_FAILED_TO_UPDATE_USER_SETTING } from '@/Constants/Strings'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
@@ -6,9 +6,7 @@ import { observer } from 'mobx-react-lite'
|
||||
import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import Dropdown from '@/Components/Dropdown/Dropdown'
|
||||
import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||
import { EmailBackupFrequency, MuteFailedBackupsEmailsOption, SettingName } from '@standardnotes/snjs'
|
||||
import { EmailBackupFrequency, SettingName } from '@standardnotes/snjs'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
import Spinner from '@/Components/Spinner/Spinner'
|
||||
@@ -21,7 +19,6 @@ const EmailBackups = ({ application }: Props) => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [emailFrequency, setEmailFrequency] = useState<EmailBackupFrequency>(EmailBackupFrequency.Disabled)
|
||||
const [emailFrequencyOptions, setEmailFrequencyOptions] = useState<DropdownItem[]>([])
|
||||
const [isFailedBackupEmailMuted, setIsFailedBackupEmailMuted] = useState(true)
|
||||
const hasAccount = application.hasAccount()
|
||||
|
||||
const loadEmailFrequencySetting = useCallback(async () => {
|
||||
@@ -38,14 +35,6 @@ const EmailBackups = ({ application }: Props) => {
|
||||
EmailBackupFrequency.Disabled,
|
||||
),
|
||||
)
|
||||
setIsFailedBackupEmailMuted(
|
||||
convertStringifiedBooleanToBoolean(
|
||||
userSettings.getSettingValue<MuteFailedBackupsEmailsOption>(
|
||||
SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue(),
|
||||
MuteFailedBackupsEmailsOption.NotMuted,
|
||||
),
|
||||
),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
@@ -90,19 +79,6 @@ const EmailBackups = ({ application }: Props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const toggleMuteFailedBackupEmails = async () => {
|
||||
const previousValue = isFailedBackupEmailMuted
|
||||
setIsFailedBackupEmailMuted(!isFailedBackupEmailMuted)
|
||||
|
||||
const updateResult = await updateSetting(
|
||||
SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue(),
|
||||
`${!isFailedBackupEmailMuted}`,
|
||||
)
|
||||
if (!updateResult) {
|
||||
setIsFailedBackupEmailMuted(previousValue)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEmailFrequencyChange = (item: string) => {
|
||||
updateEmailFrequency(item as EmailBackupFrequency).catch(console.error)
|
||||
}
|
||||
@@ -132,18 +108,6 @@ const EmailBackups = ({ application }: Props) => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<HorizontalSeparator classes="my-4" />
|
||||
<Subtitle>Email preferences</Subtitle>
|
||||
<div className="flex justify-between gap-2 md:items-center">
|
||||
<div className="flex flex-col">
|
||||
<Text>Receive a notification email if an email backup fails.</Text>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<Spinner className="h-5 w-5 flex-shrink-0" />
|
||||
) : (
|
||||
<Switch onChange={toggleMuteFailedBackupEmails} checked={!isFailedBackupEmailMuted} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { NativeFeatureIdentifier, FeatureStatus } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useEffect, useState } from 'react'
|
||||
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { FunctionComponent } from 'react'
|
||||
import TwoFactorAuthWrapper from './TwoFactorAuth/TwoFactorAuthWrapper'
|
||||
import { MfaProps } from './TwoFactorAuth/MfaProps'
|
||||
import Encryption from './Encryption'
|
||||
import PasscodeLock from './PasscodeLock'
|
||||
import Privacy from './Privacy'
|
||||
@@ -12,14 +10,33 @@ import ErroredItems from './ErroredItems'
|
||||
import PreferencesPane from '@/Components/Preferences/PreferencesComponents/PreferencesPane'
|
||||
import BiometricsLock from '@/Components/Preferences/Panes/Security/BiometricsLock'
|
||||
import MultitaskingPrivacy from '@/Components/Preferences/Panes/Security/MultitaskingPrivacy'
|
||||
import U2FWrapper from './U2F/U2FWrapper'
|
||||
import { TwoFactorAuth, is2FAEnabled as checkIf2FAIsEnabled } from './TwoFactorAuth/TwoFactorAuth'
|
||||
import U2FView from './U2F/U2FView/U2FView'
|
||||
import TwoFactorAuthView from './TwoFactorAuth/TwoFactorAuthView/TwoFactorAuthView'
|
||||
|
||||
interface SecurityProps extends MfaProps {
|
||||
interface SecurityProps {
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
const Security: FunctionComponent<SecurityProps> = (props) => {
|
||||
const isNativeMobileWeb = props.application.isNativeMobileWeb()
|
||||
const [is2FAEnabled, setIs2FAEnabled] = useState(false)
|
||||
const [canDisable2FA, setCanDisable2FA] = useState(true)
|
||||
|
||||
const [auth] = useState(
|
||||
() =>
|
||||
new TwoFactorAuth(props.application.sessions, props.application.mfa, (status) =>
|
||||
setIs2FAEnabled(checkIf2FAIsEnabled(status)),
|
||||
),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
auth.fetchStatus()
|
||||
}, [auth])
|
||||
|
||||
const onU2FDevicesLoaded = (devices: Array<{ id: string; name: string }>) => {
|
||||
setCanDisable2FA(devices.length === 0)
|
||||
}
|
||||
|
||||
const isU2FFeatureAvailable =
|
||||
props.application.features.getFeatureStatus(
|
||||
@@ -31,8 +48,14 @@ const Security: FunctionComponent<SecurityProps> = (props) => {
|
||||
<Encryption />
|
||||
{props.application.items.invalidNonVaultedItems.length > 0 && <ErroredItems />}
|
||||
<Protections application={props.application} />
|
||||
<TwoFactorAuthWrapper application={props.application} />
|
||||
{isU2FFeatureAvailable && <U2FWrapper application={props.application} />}
|
||||
<TwoFactorAuthView auth={auth} application={props.application} canDisable2FA={canDisable2FA} />
|
||||
{isU2FFeatureAvailable && (
|
||||
<U2FView
|
||||
application={props.application}
|
||||
is2FAEnabled={is2FAEnabled}
|
||||
loadAuthenticatorsCallback={onU2FDevicesLoaded}
|
||||
/>
|
||||
)}
|
||||
{isNativeMobileWeb && <MultitaskingPrivacy application={props.application} />}
|
||||
<PasscodeLock application={props.application} />
|
||||
{isNativeMobileWeb && <BiometricsLock application={props.application} />}
|
||||
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
|
||||
export interface MfaProps {
|
||||
application: WebApplication
|
||||
}
|
||||
+4
@@ -19,6 +19,7 @@ export class TwoFactorAuth {
|
||||
constructor(
|
||||
private readonly sessions: SessionsClientInterface,
|
||||
private readonly mfa: MfaServiceInterface,
|
||||
private callback?: (status: TwoFactorStatus) => void,
|
||||
) {
|
||||
this._errorMessage = null
|
||||
|
||||
@@ -90,6 +91,9 @@ export class TwoFactorAuth {
|
||||
action((active) => {
|
||||
this._status = active ? 'two-factor-enabled' : 'two-factor-disabled'
|
||||
this.setError(null)
|
||||
if (this.callback) {
|
||||
this.callback(this._status)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.catch(
|
||||
|
||||
+3
-2
@@ -16,9 +16,10 @@ import ModalOverlay from '@/Components/Modal/ModalOverlay'
|
||||
type Props = {
|
||||
auth: TwoFactorAuth
|
||||
application: WebApplication
|
||||
canDisable2FA: boolean
|
||||
}
|
||||
|
||||
const TwoFactorAuthView: FunctionComponent<Props> = ({ auth, application }) => {
|
||||
const TwoFactorAuthView: FunctionComponent<Props> = ({ auth, application, canDisable2FA }) => {
|
||||
const shouldShowActivationModal = auth.status !== 'fetching' && is2FAActivation(auth.status)
|
||||
|
||||
const activationModalTitle = shouldShowActivationModal
|
||||
@@ -96,7 +97,7 @@ const TwoFactorAuthView: FunctionComponent<Props> = ({ auth, application }) => {
|
||||
<TwoFactorTitle auth={auth} />
|
||||
<TwoFactorDescription auth={auth} />
|
||||
</div>
|
||||
<TwoFactorSwitch auth={auth} />
|
||||
<TwoFactorSwitch auth={auth} canDisable2FA={canDisable2FA} />
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
|
||||
|
||||
+5
-2
@@ -6,9 +6,10 @@ import Spinner from '@/Components/Spinner/Spinner'
|
||||
|
||||
type Props = {
|
||||
auth: TwoFactorAuth
|
||||
canDisable2FA: boolean
|
||||
}
|
||||
|
||||
const TwoFactorSwitch: FunctionComponent<Props> = ({ auth }) => {
|
||||
const TwoFactorSwitch: FunctionComponent<Props> = ({ auth, canDisable2FA }) => {
|
||||
if (!auth.isLoggedIn()) {
|
||||
return null
|
||||
}
|
||||
@@ -17,7 +18,9 @@ const TwoFactorSwitch: FunctionComponent<Props> = ({ auth }) => {
|
||||
return <Spinner className="h-4 w-4" />
|
||||
}
|
||||
|
||||
return <Switch checked={!is2FADisabled(auth.status)} onChange={auth.toggle2FA} />
|
||||
const shouldSwitchBeDisabled = auth.status === 'two-factor-enabled' && !canDisable2FA
|
||||
|
||||
return <Switch checked={!is2FADisabled(auth.status)} onChange={auth.toggle2FA} disabled={shouldSwitchBeDisabled} />
|
||||
}
|
||||
|
||||
export default observer(TwoFactorSwitch)
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
import { FunctionComponent, useState } from 'react'
|
||||
import { MfaProps } from './MfaProps'
|
||||
import { TwoFactorAuth } from './TwoFactorAuth'
|
||||
import TwoFactorAuthView from './TwoFactorAuthView/TwoFactorAuthView'
|
||||
|
||||
const TwoFactorAuthWrapper: FunctionComponent<MfaProps> = (props) => {
|
||||
const [auth] = useState(() => new TwoFactorAuth(props.application.sessions, props.application.mfa))
|
||||
auth.fetchStatus()
|
||||
return <TwoFactorAuthView auth={auth} application={props.application} />
|
||||
}
|
||||
|
||||
export default TwoFactorAuthWrapper
|
||||
@@ -1,5 +0,0 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
|
||||
export interface U2FProps {
|
||||
application: WebApplication
|
||||
}
|
||||
+8
-1
@@ -4,7 +4,11 @@ import { observer } from 'mobx-react-lite'
|
||||
import { Text } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
|
||||
const U2FDescription: FunctionComponent = () => {
|
||||
type Props = {
|
||||
is2FAEnabled: boolean
|
||||
}
|
||||
|
||||
const U2FDescription: FunctionComponent<Props> = ({ is2FAEnabled }) => {
|
||||
const application = useApplication()
|
||||
|
||||
if (application.sessions.getUser() === undefined) {
|
||||
@@ -17,6 +21,9 @@ const U2FDescription: FunctionComponent = () => {
|
||||
{!application.isFullU2FClient && (
|
||||
<Text className="italic">Please visit the web app in order to add a hardware security key.</Text>
|
||||
)}
|
||||
{!is2FAEnabled && (
|
||||
<Text className="italic">You must enable two-factor authentication before adding a hardware security key.</Text>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+9
-5
@@ -15,9 +15,11 @@ import RecoveryCodeBanner from '@/Components/RecoveryCodeBanner/RecoveryCodeBann
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
is2FAEnabled: boolean
|
||||
loadAuthenticatorsCallback: (devices: Array<{ id: string; name: string }>) => void
|
||||
}
|
||||
|
||||
const U2FView: FunctionComponent<Props> = ({ application }) => {
|
||||
const U2FView: FunctionComponent<Props> = ({ application, is2FAEnabled, loadAuthenticatorsCallback }) => {
|
||||
const [showDeviceAddingModal, setShowDeviceAddingModal] = useState(false)
|
||||
const [devices, setDevices] = useState<Array<{ id: string; name: string }>>([])
|
||||
const [error, setError] = useState('')
|
||||
@@ -34,8 +36,10 @@ const U2FView: FunctionComponent<Props> = ({ application }) => {
|
||||
return
|
||||
}
|
||||
|
||||
setDevices(authenticatorListOrError.getValue())
|
||||
}, [setError, setDevices, application])
|
||||
const authenticatorList = authenticatorListOrError.getValue()
|
||||
setDevices(authenticatorList)
|
||||
loadAuthenticatorsCallback(authenticatorList)
|
||||
}, [setError, setDevices, application, loadAuthenticatorsCallback])
|
||||
|
||||
useEffect(() => {
|
||||
loadAuthenticatorDevices().catch(console.error)
|
||||
@@ -47,7 +51,7 @@ const U2FView: FunctionComponent<Props> = ({ application }) => {
|
||||
<PreferencesSegment>
|
||||
<div className="flex flex-col">
|
||||
<U2FTitle />
|
||||
<U2FDescription />
|
||||
<U2FDescription is2FAEnabled={is2FAEnabled} />
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
<PreferencesSegment classes="mt-2">
|
||||
@@ -60,7 +64,7 @@ const U2FView: FunctionComponent<Props> = ({ application }) => {
|
||||
/>
|
||||
<Button
|
||||
className="mt-1"
|
||||
disabled={!application.isFullU2FClient}
|
||||
disabled={!application.isFullU2FClient || !is2FAEnabled}
|
||||
label="Add Device"
|
||||
primary
|
||||
onClick={handleAddDeviceClick}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { FunctionComponent } from 'react'
|
||||
|
||||
import { U2FProps } from './U2FProps'
|
||||
import U2FView from './U2FView/U2FView'
|
||||
|
||||
const U2FWrapper: FunctionComponent<U2FProps> = (props) => {
|
||||
return <U2FView application={props.application} />
|
||||
}
|
||||
|
||||
export default U2FWrapper
|
||||
@@ -1,7 +1,6 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { MfaProps } from './Panes/Security/TwoFactorAuth/MfaProps'
|
||||
|
||||
export interface PreferencesProps extends MfaProps {
|
||||
export interface PreferencesProps {
|
||||
application: WebApplication
|
||||
closePreferences: () => void
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ import Button from '@/Components/Button/Button'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { PurchaseFlowPane } from '@/Controllers/PurchaseFlow/PurchaseFlowPane'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { ChangeEventHandler, FunctionComponent, useEffect, useRef, useState } from 'react'
|
||||
import { ChangeEventHandler, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import FloatingLabelInput from '@/Components/Input/FloatingLabelInput'
|
||||
import { isEmailValid } from '@/Utils'
|
||||
import { BlueDotIcon, CircleIcon, DiamondIcon, CreateAccountIllustration } from '@standardnotes/icons'
|
||||
import { useCaptcha } from '@/Hooks/useCaptcha'
|
||||
import { AccountMenuPane } from '../../AccountMenu/AccountMenuPane'
|
||||
import { isErrorResponse } from '@standardnotes/snjs'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -20,6 +23,61 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
|
||||
const [isEmailInvalid, setIsEmailInvalid] = useState(false)
|
||||
const [isPasswordNotMatching, setIsPasswordNotMatching] = useState(false)
|
||||
|
||||
const [hvmToken, setHVMToken] = useState('')
|
||||
const [captchaURL, setCaptchaURL] = useState('')
|
||||
|
||||
const register = useCallback(() => {
|
||||
setIsCreatingAccount(true)
|
||||
application
|
||||
.register(email, password, hvmToken)
|
||||
.then(() => {
|
||||
application.accountMenuController.closeAccountMenu()
|
||||
application.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
application.alerts.alert(err as string).catch(console.error)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsCreatingAccount(false)
|
||||
})
|
||||
}, [application, email, hvmToken, password])
|
||||
|
||||
const captchaIframe = useCaptcha(captchaURL, (token) => {
|
||||
setHVMToken(token)
|
||||
setCaptchaURL('')
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!hvmToken) {
|
||||
return
|
||||
}
|
||||
|
||||
register()
|
||||
}, [hvmToken, register])
|
||||
|
||||
const checkIfCaptchaRequiredAndRegister = useCallback(() => {
|
||||
application
|
||||
.getCaptchaUrl()
|
||||
.then((response) => {
|
||||
if (isErrorResponse(response)) {
|
||||
throw new Error()
|
||||
}
|
||||
const { captchaUIUrl } = response.data
|
||||
if (captchaUIUrl) {
|
||||
setCaptchaURL(captchaUIUrl)
|
||||
} else {
|
||||
setCaptchaURL('')
|
||||
register()
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
setCaptchaURL('')
|
||||
register()
|
||||
})
|
||||
}, [application, register])
|
||||
|
||||
const emailInputRef = useRef<HTMLInputElement>(null)
|
||||
const passwordInputRef = useRef<HTMLInputElement>(null)
|
||||
const confirmPasswordInputRef = useRef<HTMLInputElement>(null)
|
||||
@@ -81,21 +139,52 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
|
||||
return
|
||||
}
|
||||
|
||||
setIsCreatingAccount(true)
|
||||
|
||||
try {
|
||||
await application.register(email, password)
|
||||
|
||||
application.purchaseFlowController.closePurchaseFlow()
|
||||
void application.purchaseFlowController.openPurchaseFlow()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
application.alerts.alert(err as string).catch(console.error)
|
||||
} finally {
|
||||
setIsCreatingAccount(false)
|
||||
}
|
||||
checkIfCaptchaRequiredAndRegister()
|
||||
}
|
||||
|
||||
const CreateAccountForm = (
|
||||
<form onSubmit={handleCreateAccount}>
|
||||
<div className="flex flex-col">
|
||||
<FloatingLabelInput
|
||||
className={`min-w-auto md:min-w-90 ${isEmailInvalid ? 'mb-2' : 'mb-4'}`}
|
||||
id="purchase-sign-in-email"
|
||||
type="email"
|
||||
label="Email"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
ref={emailInputRef}
|
||||
disabled={isCreatingAccount}
|
||||
isInvalid={isEmailInvalid}
|
||||
/>
|
||||
{isEmailInvalid ? <div className="mb-4 text-danger">Please provide a valid email.</div> : null}
|
||||
<FloatingLabelInput
|
||||
className="min-w-auto mb-4 md:min-w-90"
|
||||
id="purchase-create-account-password"
|
||||
type="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
ref={passwordInputRef}
|
||||
disabled={isCreatingAccount}
|
||||
/>
|
||||
<FloatingLabelInput
|
||||
className={`min-w-auto md:min-w-90 ${isPasswordNotMatching ? 'mb-2' : 'mb-4'}`}
|
||||
id="create-account-confirm"
|
||||
type="password"
|
||||
label="Repeat password"
|
||||
value={confirmPassword}
|
||||
onChange={handleConfirmPasswordChange}
|
||||
ref={confirmPasswordInputRef}
|
||||
disabled={isCreatingAccount}
|
||||
isInvalid={isPasswordNotMatching}
|
||||
/>
|
||||
{isPasswordNotMatching ? (
|
||||
<div className="mb-4 text-danger">Passwords don't match. Please try again.</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<CircleIcon className="absolute -left-28 top-[40%] h-8 w-8" />
|
||||
@@ -109,46 +198,7 @@ const CreateAccount: FunctionComponent<Props> = ({ application }) => {
|
||||
<div className="mr-0 lg:mr-12">
|
||||
<h1 className="mb-2 mt-0 text-2xl font-bold">Create your free account</h1>
|
||||
<div className="mb-4 text-sm font-medium">to continue to Standard Notes.</div>
|
||||
<form onSubmit={handleCreateAccount}>
|
||||
<div className="flex flex-col">
|
||||
<FloatingLabelInput
|
||||
className={`min-w-auto md:min-w-90 ${isEmailInvalid ? 'mb-2' : 'mb-4'}`}
|
||||
id="purchase-sign-in-email"
|
||||
type="email"
|
||||
label="Email"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
ref={emailInputRef}
|
||||
disabled={isCreatingAccount}
|
||||
isInvalid={isEmailInvalid}
|
||||
/>
|
||||
{isEmailInvalid ? <div className="mb-4 text-danger">Please provide a valid email.</div> : null}
|
||||
<FloatingLabelInput
|
||||
className="min-w-auto mb-4 md:min-w-90"
|
||||
id="purchase-create-account-password"
|
||||
type="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
ref={passwordInputRef}
|
||||
disabled={isCreatingAccount}
|
||||
/>
|
||||
<FloatingLabelInput
|
||||
className={`min-w-auto md:min-w-90 ${isPasswordNotMatching ? 'mb-2' : 'mb-4'}`}
|
||||
id="create-account-confirm"
|
||||
type="password"
|
||||
label="Repeat password"
|
||||
value={confirmPassword}
|
||||
onChange={handleConfirmPasswordChange}
|
||||
ref={confirmPasswordInputRef}
|
||||
disabled={isCreatingAccount}
|
||||
isInvalid={isPasswordNotMatching}
|
||||
/>
|
||||
{isPasswordNotMatching ? (
|
||||
<div className="mb-4 text-danger">Passwords don't match. Please try again.</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
{captchaURL ? captchaIframe : CreateAccountForm}
|
||||
<div className="flex flex-col-reverse items-start justify-between md:flex-row md:items-center">
|
||||
<div className="flex flex-col">
|
||||
<button
|
||||
|
||||
@@ -6,7 +6,8 @@ import { ChangeEventHandler, FunctionComponent, useEffect, useRef, useState } fr
|
||||
import FloatingLabelInput from '@/Components/Input/FloatingLabelInput'
|
||||
import { isEmailValid } from '@/Utils'
|
||||
import { BlueDotIcon, CircleIcon, DiamondIcon } from '@standardnotes/icons'
|
||||
import { isErrorResponse } from '@standardnotes/snjs'
|
||||
import { isErrorResponse, getCaptchaHeader } from '@standardnotes/snjs'
|
||||
import { useCaptcha } from '@/Hooks/useCaptcha'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -21,6 +22,15 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
|
||||
const [isPasswordInvalid, setIsPasswordInvalid] = useState(false)
|
||||
const [otherErrorMessage, setOtherErrorMessage] = useState('')
|
||||
|
||||
const [captchaURL, setCaptchaURL] = useState('')
|
||||
const [showCaptcha, setShowCaptcha] = useState(false)
|
||||
const [hvmToken, setHVMToken] = useState('')
|
||||
const captchaIframe = useCaptcha(captchaURL, (token) => {
|
||||
setHVMToken(token)
|
||||
setShowCaptcha(false)
|
||||
setCaptchaURL('')
|
||||
})
|
||||
|
||||
const emailInputRef = useRef<HTMLInputElement>(null)
|
||||
const passwordInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
@@ -65,10 +75,22 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (captchaURL) {
|
||||
setShowCaptcha(true)
|
||||
return
|
||||
}
|
||||
|
||||
setIsSigningIn(true)
|
||||
|
||||
try {
|
||||
const response = await application.signIn(email, password)
|
||||
const response = await application.signIn(email, password, undefined, undefined, undefined, undefined, hvmToken)
|
||||
const captchaURL = getCaptchaHeader(response)
|
||||
if (captchaURL) {
|
||||
setCaptchaURL(captchaURL)
|
||||
return
|
||||
} else {
|
||||
setCaptchaURL('')
|
||||
}
|
||||
if (isErrorResponse(response)) {
|
||||
throw new Error(response.data.error?.message)
|
||||
} else {
|
||||
@@ -78,7 +100,6 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
if ((err as Error).toString().includes('Invalid email or password')) {
|
||||
setIsSigningIn(false)
|
||||
setIsEmailInvalid(true)
|
||||
setIsPasswordInvalid(true)
|
||||
setOtherErrorMessage('Invalid email or password.')
|
||||
@@ -86,9 +107,51 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
|
||||
} else {
|
||||
application.alerts.alert(err as string).catch(console.error)
|
||||
}
|
||||
} finally {
|
||||
setIsSigningIn(false)
|
||||
}
|
||||
}
|
||||
|
||||
const signInForm = (
|
||||
<form onSubmit={handleSignIn}>
|
||||
<div className="flex flex-col">
|
||||
<FloatingLabelInput
|
||||
className={`min-w-auto sm:min-w-90 ${isEmailInvalid && !otherErrorMessage ? 'mb-2' : 'mb-4'}`}
|
||||
id="purchase-sign-in-email"
|
||||
type="email"
|
||||
label="Email"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
ref={emailInputRef}
|
||||
disabled={isSigningIn}
|
||||
isInvalid={isEmailInvalid}
|
||||
/>
|
||||
{isEmailInvalid && !otherErrorMessage ? (
|
||||
<div className="mb-4 text-danger">Please provide a valid email.</div>
|
||||
) : null}
|
||||
<FloatingLabelInput
|
||||
className={`min-w-auto sm:min-w-90 ${otherErrorMessage ? 'mb-2' : 'mb-4'}`}
|
||||
id="purchase-sign-in-password"
|
||||
type="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
ref={passwordInputRef}
|
||||
disabled={isSigningIn}
|
||||
isInvalid={isPasswordInvalid}
|
||||
/>
|
||||
{otherErrorMessage ? <div className="mb-4 text-danger">{otherErrorMessage}</div> : null}
|
||||
</div>
|
||||
<Button
|
||||
className={`${isSigningIn ? 'min-w-30' : 'min-w-24'} mb-5 py-2.5`}
|
||||
primary
|
||||
label={isSigningIn ? 'Signing in...' : 'Sign in'}
|
||||
onClick={handleSignIn}
|
||||
disabled={isSigningIn}
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<CircleIcon className="absolute -left-56 top-[35%] h-8 w-8" />
|
||||
@@ -102,43 +165,7 @@ const SignIn: FunctionComponent<Props> = ({ application }) => {
|
||||
<div>
|
||||
<h1 className="mb-2 mt-0 text-2xl font-bold">Sign in</h1>
|
||||
<div className="mb-4 text-sm font-medium">to continue to Standard Notes.</div>
|
||||
<form onSubmit={handleSignIn}>
|
||||
<div className="flex flex-col">
|
||||
<FloatingLabelInput
|
||||
className={`min-w-auto sm:min-w-90 ${isEmailInvalid && !otherErrorMessage ? 'mb-2' : 'mb-4'}`}
|
||||
id="purchase-sign-in-email"
|
||||
type="email"
|
||||
label="Email"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
ref={emailInputRef}
|
||||
disabled={isSigningIn}
|
||||
isInvalid={isEmailInvalid}
|
||||
/>
|
||||
{isEmailInvalid && !otherErrorMessage ? (
|
||||
<div className="mb-4 text-danger">Please provide a valid email.</div>
|
||||
) : null}
|
||||
<FloatingLabelInput
|
||||
className={`min-w-auto sm:min-w-90 ${otherErrorMessage ? 'mb-2' : 'mb-4'}`}
|
||||
id="purchase-sign-in-password"
|
||||
type="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
ref={passwordInputRef}
|
||||
disabled={isSigningIn}
|
||||
isInvalid={isPasswordInvalid}
|
||||
/>
|
||||
{otherErrorMessage ? <div className="mb-4 text-danger">{otherErrorMessage}</div> : null}
|
||||
</div>
|
||||
<Button
|
||||
className={`${isSigningIn ? 'min-w-30' : 'min-w-24'} mb-5 py-2.5`}
|
||||
primary
|
||||
label={isSigningIn ? 'Signing in...' : 'Sign in'}
|
||||
onClick={handleSignIn}
|
||||
disabled={isSigningIn}
|
||||
/>
|
||||
</form>
|
||||
{showCaptcha ? captchaIframe : signInForm}
|
||||
<div className="text-sm font-medium text-passive-1">
|
||||
Don’t have an account yet?{' '}
|
||||
<a
|
||||
|
||||
+3
-5
@@ -21,7 +21,6 @@ import { $isCollapsibleContentNode } from '../../../Plugins/CollapsiblePlugin/Co
|
||||
import { $isCollapsibleTitleNode } from '../../../Plugins/CollapsiblePlugin/CollapsibleTitleNode'
|
||||
import { PDFDataNode, PDFWorker } from './PDFWorker'
|
||||
import { wrap } from 'comlink'
|
||||
import { getBase64FromBlob } from '@/Utils'
|
||||
import { PrefKey, PrefValue } from '@standardnotes/snjs'
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -421,7 +420,7 @@ const getPDFDataNodesFromLexicalNodes = (nodes: LexicalNode[]): PDFDataNode[] =>
|
||||
const PDFWorkerComlink = wrap<PDFWorker>(new Worker(new URL('./PDFWorker.tsx', import.meta.url)))
|
||||
|
||||
/**
|
||||
* @returns The PDF as a base64 string
|
||||
* @returns The PDF as an object url
|
||||
*/
|
||||
export function $generatePDFFromNodes(editor: LexicalEditor, pageSize: PrefValue[PrefKey.SuperNoteExportPDFPageSize]) {
|
||||
return new Promise<string>((resolve) => {
|
||||
@@ -432,9 +431,8 @@ export function $generatePDFFromNodes(editor: LexicalEditor, pageSize: PrefValue
|
||||
const pdfDataNodes = getPDFDataNodesFromLexicalNodes(nodes)
|
||||
|
||||
void PDFWorkerComlink.renderPDF(pdfDataNodes, pageSize).then((blob) => {
|
||||
void getBase64FromBlob(blob).then((base64) => {
|
||||
resolve(base64)
|
||||
})
|
||||
const url = URL.createObjectURL(blob)
|
||||
resolve(url)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+20
@@ -16,6 +16,7 @@ import {
|
||||
COMMAND_PRIORITY_HIGH,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
DRAGOVER_COMMAND,
|
||||
DRAGEND_COMMAND,
|
||||
DROP_COMMAND,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
@@ -32,6 +33,7 @@ const DRAGGABLE_BLOCK_MENU_LEFT_SPACE = -2
|
||||
const TARGET_LINE_HALF_HEIGHT = 2
|
||||
const DRAGGABLE_BLOCK_MENU_CLASSNAME = 'draggable-block-menu'
|
||||
const DRAG_DATA_FORMAT = 'application/x-lexical-drag-block'
|
||||
let draggedNodeKey = ''
|
||||
const TEXT_BOX_HORIZONTAL_PADDING = 24
|
||||
|
||||
const Downward = 1
|
||||
@@ -283,6 +285,9 @@ function useDraggableBlockMenu(editor: LexicalEditor, anchorElem: HTMLElement, i
|
||||
if (!isHTMLElement(target)) {
|
||||
return false
|
||||
}
|
||||
if (!draggedNodeKey) {
|
||||
return false
|
||||
}
|
||||
const targetBlockElem = getBlockElement(anchorElem, editor, new Point(event.pageX, pageY))
|
||||
const targetLineElem = targetLineRef.current
|
||||
if (targetBlockElem === null || targetLineElem === null) {
|
||||
@@ -331,6 +336,12 @@ function useDraggableBlockMenu(editor: LexicalEditor, anchorElem: HTMLElement, i
|
||||
return true
|
||||
}
|
||||
|
||||
function onDragEnd(): boolean {
|
||||
hideTargetLine(targetLineRef.current)
|
||||
draggedNodeKey = ''
|
||||
return true
|
||||
}
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
DRAGOVER_COMMAND,
|
||||
@@ -339,6 +350,13 @@ function useDraggableBlockMenu(editor: LexicalEditor, anchorElem: HTMLElement, i
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
editor.registerCommand(
|
||||
DRAGEND_COMMAND,
|
||||
() => {
|
||||
return onDragEnd()
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
editor.registerCommand(
|
||||
DROP_COMMAND,
|
||||
(event) => {
|
||||
@@ -363,10 +381,12 @@ function useDraggableBlockMenu(editor: LexicalEditor, anchorElem: HTMLElement, i
|
||||
}
|
||||
})
|
||||
dataTransfer.setData(DRAG_DATA_FORMAT, nodeKey)
|
||||
draggedNodeKey = nodeKey
|
||||
}
|
||||
|
||||
function onDragEnd(): void {
|
||||
hideTargetLine(targetLineRef.current)
|
||||
draggedNodeKey = ''
|
||||
}
|
||||
|
||||
function onTouchStart(): void {
|
||||
|
||||
-285
@@ -1,285 +0,0 @@
|
||||
import { getPositionedPopoverStyles } from '@/Components/Popover/GetPositionedPopoverStyles'
|
||||
import {
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
LexicalEditor,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
} from 'lexical'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { getDOMRangeRect } from '../../Lexical/Utils/getDOMRangeRect'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||
import { TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { sanitizeUrl } from '../../Lexical/Utils/sanitizeUrl'
|
||||
import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode'
|
||||
import { $isLinkTextNode } from './ToolbarLinkTextEditor'
|
||||
import { useElementResize } from '@/Hooks/useElementRect'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { getAdjustedStylesForNonPortalPopover } from '@/Components/Popover/Utils/getAdjustedStylesForNonPortal'
|
||||
|
||||
const FloatingLinkEditor = ({
|
||||
linkUrl,
|
||||
linkText,
|
||||
isEditMode,
|
||||
setEditMode,
|
||||
editor,
|
||||
isAutoLink,
|
||||
isLinkText,
|
||||
isMobile,
|
||||
}: {
|
||||
linkUrl: string
|
||||
linkText: string
|
||||
isEditMode: boolean
|
||||
setEditMode: (isEditMode: boolean) => void
|
||||
editor: LexicalEditor
|
||||
isLinkText: boolean
|
||||
isAutoLink: boolean
|
||||
isMobile: boolean
|
||||
}) => {
|
||||
const [editedLinkUrl, setEditedLinkUrl] = useState(() => linkUrl)
|
||||
useEffect(() => {
|
||||
setEditedLinkUrl(linkUrl)
|
||||
}, [linkUrl])
|
||||
const [editedLinkText, setEditedLinkText] = useState(() => linkText)
|
||||
useEffect(() => {
|
||||
setEditedLinkText(linkText)
|
||||
}, [linkText])
|
||||
|
||||
const linkEditorRef = useRef<HTMLDivElement>(null)
|
||||
const rangeRect = useRef<DOMRect>()
|
||||
|
||||
const updateLinkEditorPosition = useCallback(() => {
|
||||
if (isMobile) {
|
||||
return
|
||||
}
|
||||
|
||||
const nativeSelection = window.getSelection()
|
||||
const rootElement = editor.getRootElement()
|
||||
|
||||
if (nativeSelection !== null && rootElement !== null) {
|
||||
if (rootElement.contains(nativeSelection.anchorNode)) {
|
||||
rangeRect.current = getDOMRangeRect(nativeSelection, rootElement)
|
||||
}
|
||||
}
|
||||
|
||||
const linkEditorElement = linkEditorRef.current
|
||||
|
||||
if (!linkEditorElement) {
|
||||
setTimeout(updateLinkEditorPosition)
|
||||
return
|
||||
}
|
||||
|
||||
if (!rootElement) {
|
||||
return
|
||||
}
|
||||
|
||||
const linkEditorRect = linkEditorElement.getBoundingClientRect()
|
||||
const rootElementRect = rootElement.getBoundingClientRect()
|
||||
|
||||
const calculatedStyles = getPositionedPopoverStyles({
|
||||
align: 'center',
|
||||
side: 'top',
|
||||
anchorRect: rangeRect.current,
|
||||
popoverRect: linkEditorRect,
|
||||
documentRect: rootElementRect,
|
||||
offset: 12,
|
||||
maxHeightFunction: () => 'none',
|
||||
})
|
||||
if (calculatedStyles) {
|
||||
const adjustedStyles = getAdjustedStylesForNonPortalPopover(linkEditorElement, calculatedStyles)
|
||||
Object.entries(adjustedStyles).forEach(([key, value]) => {
|
||||
linkEditorElement.style.setProperty(key, value)
|
||||
})
|
||||
linkEditorElement.style.opacity = '1'
|
||||
}
|
||||
}, [editor, isMobile])
|
||||
|
||||
useElementResize(linkEditorRef.current, updateLinkEditorPosition)
|
||||
|
||||
useEffect(() => {
|
||||
updateLinkEditorPosition()
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(() => {
|
||||
updateLinkEditorPosition()
|
||||
}),
|
||||
editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
(_payload) => {
|
||||
updateLinkEditorPosition()
|
||||
return false
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
)
|
||||
}, [editor, updateLinkEditorPosition])
|
||||
|
||||
const focusInput = useCallback((input: HTMLInputElement | null) => {
|
||||
if (input) {
|
||||
input.focus()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleSubmission = () => {
|
||||
if (editedLinkUrl !== '') {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(editedLinkUrl))
|
||||
}
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if (!$isRangeSelection(selection)) {
|
||||
return
|
||||
}
|
||||
const node = getSelectedNode(selection)
|
||||
if (!$isLinkTextNode(node, selection)) {
|
||||
return
|
||||
}
|
||||
node.setTextContent(editedLinkText)
|
||||
})
|
||||
setEditMode(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(updateLinkEditorPosition)
|
||||
}, [isEditMode, updateLinkEditorPosition])
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
id="super-link-editor"
|
||||
className="absolute bottom-12 left-1/2 z-modal w-[calc(100%_-_1rem)] -translate-x-1/2 rounded-lg border border-border bg-contrast px-2 py-1 shadow-sm shadow-contrast translucent-ui:border-[--popover-border-color] translucent-ui:bg-[--popover-background-color] translucent-ui:[backdrop-filter:var(--popover-backdrop-filter)] md:bottom-[unset] md:left-0 md:top-0 md:w-auto md:translate-x-0 md:opacity-0"
|
||||
ref={linkEditorRef}
|
||||
>
|
||||
{isEditMode ? (
|
||||
<div
|
||||
className="flex flex-col gap-2 py-1"
|
||||
onBlur={(event) => {
|
||||
if (!linkEditorRef.current?.contains(event.relatedTarget as Node)) {
|
||||
setEditMode(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isLinkText && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Icon type="plain-text" className="flex-shrink-0" />
|
||||
<input
|
||||
value={editedLinkText}
|
||||
onChange={(event) => {
|
||||
setEditedLinkText(event.target.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
event.preventDefault()
|
||||
handleSubmission()
|
||||
} else if (event.key === KeyboardKey.Escape) {
|
||||
event.preventDefault()
|
||||
setEditMode(false)
|
||||
}
|
||||
}}
|
||||
className="flex-grow rounded-sm bg-contrast p-1 text-text sm:min-w-[20ch]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Icon type="link" className="flex-shrink-0" />
|
||||
<input
|
||||
ref={focusInput}
|
||||
value={editedLinkUrl}
|
||||
onChange={(event) => {
|
||||
setEditedLinkUrl(event.target.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
event.preventDefault()
|
||||
handleSubmission()
|
||||
} else if (event.key === KeyboardKey.Escape) {
|
||||
event.preventDefault()
|
||||
setEditMode(false)
|
||||
}
|
||||
}}
|
||||
className="flex-grow rounded-sm bg-contrast p-1 text-text sm:min-w-[40ch]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-1.5">
|
||||
<StyledTooltip showOnMobile showOnHover label="Cancel editing">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setEditMode(false)
|
||||
editor.focus()
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip showOnMobile showOnHover label="Save link">
|
||||
<Button primary onClick={handleSubmission} onMouseDown={(event) => event.preventDefault()}>
|
||||
Apply
|
||||
</Button>
|
||||
</StyledTooltip>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<a
|
||||
className={classNames(
|
||||
'mr-1 flex flex-grow items-center gap-2 overflow-hidden whitespace-nowrap underline',
|
||||
isAutoLink && 'py-2.5',
|
||||
)}
|
||||
href={linkUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon type="open-in" className="ml-1 flex-shrink-0" />
|
||||
<div className="max-w-[35ch] overflow-hidden text-ellipsis">{linkUrl}</div>
|
||||
</a>
|
||||
<StyledTooltip showOnMobile showOnHover label="Copy link">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(linkUrl).catch(console.error)
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="copy" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
{!isAutoLink && (
|
||||
<>
|
||||
<StyledTooltip showOnMobile showOnHover label="Edit link">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
setEditedLinkUrl(linkUrl)
|
||||
setEditMode(true)
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="pencil-filled" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip showOnMobile showOnHover label="Remove link">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="trash-filled" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>,
|
||||
document.getElementById(ElementIds.SuperEditor) ?? document.body,
|
||||
)
|
||||
}
|
||||
|
||||
export default FloatingLinkEditor
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
import { getPositionedPopoverStyles } from '@/Components/Popover/GetPositionedPopoverStyles'
|
||||
import {
|
||||
$isTextNode,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
LexicalEditor,
|
||||
RangeSelection,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
TextNode,
|
||||
} from 'lexical'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { getDOMRangeRect } from '../../Lexical/Utils/getDOMRangeRect'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||
import { $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { sanitizeUrl } from '../../Lexical/Utils/sanitizeUrl'
|
||||
import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode'
|
||||
import { useElementResize } from '@/Hooks/useElementRect'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { getAdjustedStylesForNonPortalPopover } from '@/Components/Popover/Utils/getAdjustedStylesForNonPortal'
|
||||
|
||||
export const $isLinkTextNode = (
|
||||
node: ReturnType<typeof getSelectedNode>,
|
||||
selection: RangeSelection,
|
||||
): node is TextNode => {
|
||||
const parent = node.getParent()
|
||||
return (
|
||||
$isLinkNode(parent) &&
|
||||
parent.getChildrenSize() === 1 &&
|
||||
$isTextNode(node) &&
|
||||
parent.getFirstChild() === node &&
|
||||
selection.anchor.getNode() === selection.focus.getNode()
|
||||
)
|
||||
}
|
||||
|
||||
const LinkEditor = ({
|
||||
editor,
|
||||
setIsEditingLink,
|
||||
isMobile,
|
||||
linkNode,
|
||||
linkTextNode,
|
||||
}: {
|
||||
editor: LexicalEditor
|
||||
setIsEditingLink: (isEditMode: boolean) => void
|
||||
isMobile: boolean
|
||||
linkNode: LinkNode | null
|
||||
linkTextNode: TextNode | null
|
||||
}) => {
|
||||
const [url, setURL] = useState('')
|
||||
const [text, setText] = useState('')
|
||||
useEffect(() => {
|
||||
editor.getEditorState().read(() => {
|
||||
if (linkNode) {
|
||||
setURL(linkNode.getURL())
|
||||
}
|
||||
if (linkTextNode) {
|
||||
setText(linkTextNode.getTextContent())
|
||||
}
|
||||
})
|
||||
}, [editor, linkNode, linkTextNode])
|
||||
|
||||
const linkInputRef = useRef<HTMLInputElement>(null)
|
||||
const linkEditorRef = useRef<HTMLDivElement>(null)
|
||||
const rangeRect = useRef<DOMRect>()
|
||||
const positionUpdateRAF = useRef<number>()
|
||||
|
||||
const updateLinkEditorPosition = useCallback(() => {
|
||||
if (positionUpdateRAF.current) {
|
||||
cancelAnimationFrame(positionUpdateRAF.current)
|
||||
}
|
||||
|
||||
positionUpdateRAF.current = requestAnimationFrame(() => {
|
||||
if (isMobile) {
|
||||
linkInputRef.current?.focus()
|
||||
return
|
||||
}
|
||||
|
||||
const nativeSelection = window.getSelection()
|
||||
const rootElement = editor.getRootElement()
|
||||
|
||||
if (nativeSelection !== null && rootElement !== null) {
|
||||
if (rootElement.contains(nativeSelection.anchorNode)) {
|
||||
rangeRect.current = getDOMRangeRect(nativeSelection, rootElement)
|
||||
}
|
||||
}
|
||||
|
||||
const linkEditorElement = linkEditorRef.current
|
||||
|
||||
if (!linkEditorElement) {
|
||||
setTimeout(updateLinkEditorPosition)
|
||||
return
|
||||
}
|
||||
|
||||
if (!rootElement) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!rangeRect.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const linkEditorRect = linkEditorElement.getBoundingClientRect()
|
||||
const rootElementRect = rootElement.getBoundingClientRect()
|
||||
|
||||
const calculatedStyles = getPositionedPopoverStyles({
|
||||
align: 'center',
|
||||
side: 'top',
|
||||
anchorRect: rangeRect.current,
|
||||
popoverRect: linkEditorRect,
|
||||
documentRect: rootElementRect,
|
||||
offset: 12,
|
||||
maxHeightFunction: () => 'none',
|
||||
})
|
||||
if (calculatedStyles) {
|
||||
const adjustedStyles = getAdjustedStylesForNonPortalPopover(linkEditorElement, calculatedStyles)
|
||||
Object.entries(adjustedStyles).forEach(([key, value]) => {
|
||||
linkEditorElement.style.setProperty(key, value)
|
||||
})
|
||||
linkEditorElement.style.display = 'block'
|
||||
linkInputRef.current?.focus()
|
||||
}
|
||||
})
|
||||
}, [editor, isMobile])
|
||||
|
||||
useElementResize(linkEditorRef.current, updateLinkEditorPosition)
|
||||
|
||||
useEffect(() => {
|
||||
updateLinkEditorPosition()
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(() => {
|
||||
updateLinkEditorPosition()
|
||||
}),
|
||||
editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
(_payload) => {
|
||||
updateLinkEditorPosition()
|
||||
return false
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
)
|
||||
}, [editor, updateLinkEditorPosition])
|
||||
|
||||
const handleSubmission = () => {
|
||||
if (url !== '') {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(url))
|
||||
}
|
||||
if (linkTextNode !== null && text !== '') {
|
||||
editor.update(
|
||||
() => {
|
||||
linkTextNode.setTextContent(text)
|
||||
},
|
||||
{
|
||||
discrete: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
setIsEditingLink(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const linkEditor = linkEditorRef.current
|
||||
if (!linkEditor) {
|
||||
return
|
||||
}
|
||||
|
||||
const handleFocusOut = (event: FocusEvent) => {
|
||||
if (!linkEditor.contains(event.relatedTarget as Node)) {
|
||||
setIsEditingLink(false)
|
||||
}
|
||||
}
|
||||
|
||||
linkEditor.addEventListener('focusout', handleFocusOut)
|
||||
|
||||
return () => {
|
||||
linkEditor.removeEventListener('focusout', handleFocusOut)
|
||||
}
|
||||
}, [setIsEditingLink])
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute z-dropdown-menu rounded-lg border border-border bg-contrast px-2 py-1 shadow-sm shadow-contrast',
|
||||
isMobile
|
||||
? 'bottom-12 left-1/2 w-[calc(100%_-_1rem)] -translate-x-1/2'
|
||||
: 'left-0 top-0 hidden w-auto translate-x-0 translucent-ui:border-[--popover-border-color] translucent-ui:bg-[--popover-background-color] translucent-ui:[backdrop-filter:var(--popover-backdrop-filter)]',
|
||||
)}
|
||||
ref={linkEditorRef}
|
||||
>
|
||||
<div className="flex flex-col gap-2 py-1">
|
||||
{linkTextNode && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Icon type="plain-text" className="flex-shrink-0" />
|
||||
<input
|
||||
value={text}
|
||||
onChange={(event) => {
|
||||
setText(event.target.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
event.preventDefault()
|
||||
handleSubmission()
|
||||
} else if (event.key === KeyboardKey.Escape) {
|
||||
event.preventDefault()
|
||||
setIsEditingLink(false)
|
||||
}
|
||||
}}
|
||||
className="flex-grow rounded-sm border border-border bg-contrast p-1 text-text sm:min-w-[20ch] translucent-ui:md:border-0"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Icon type="link" className="flex-shrink-0" />
|
||||
<input
|
||||
ref={linkInputRef}
|
||||
value={url}
|
||||
onChange={(event) => {
|
||||
setURL(event.target.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
event.preventDefault()
|
||||
handleSubmission()
|
||||
} else if (event.key === KeyboardKey.Escape) {
|
||||
event.preventDefault()
|
||||
setIsEditingLink(false)
|
||||
}
|
||||
}}
|
||||
className="flex-grow rounded-sm border border-border bg-contrast p-1 text-text sm:min-w-[40ch] translucent-ui:md:border-0"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-1.5">
|
||||
<StyledTooltip showOnMobile showOnHover label="Cancel editing">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsEditingLink(false)
|
||||
editor.focus()
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip showOnMobile showOnHover label="Save link">
|
||||
<Button primary onClick={handleSubmission} onMouseDown={(event) => event.preventDefault()}>
|
||||
Apply
|
||||
</Button>
|
||||
</StyledTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.getElementById(ElementIds.SuperEditor) ?? document.body,
|
||||
)
|
||||
}
|
||||
|
||||
export default LinkEditor
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { getPositionedPopoverStyles } from '@/Components/Popover/GetPositionedPopoverStyles'
|
||||
import { getAdjustedStylesForNonPortalPopover } from '@/Components/Popover/Utils/getAdjustedStylesForNonPortal'
|
||||
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||
import { useElementResize } from '@/Hooks/useElementRect'
|
||||
import { $isAutoLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { COMMAND_PRIORITY_LOW, LexicalEditor, SELECTION_CHANGE_COMMAND } from 'lexical'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { getDOMRangeRect } from '../../Lexical/Utils/getDOMRangeRect'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
type Props = {
|
||||
linkNode: LinkNode
|
||||
editor: LexicalEditor
|
||||
isMobile: boolean
|
||||
setIsEditingLink: (isEditingLink: boolean) => void
|
||||
}
|
||||
|
||||
const LinkViewer = ({ isMobile, editor, linkNode, setIsEditingLink }: Props) => {
|
||||
const linkViewerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [linkUrl, isAutoLink] = useMemo(() => {
|
||||
let linkUrl = ''
|
||||
let isAutoLink = false
|
||||
editor.getEditorState().read(() => {
|
||||
linkUrl = linkNode.getURL()
|
||||
isAutoLink = $isAutoLinkNode(linkNode)
|
||||
})
|
||||
return [linkUrl, isAutoLink]
|
||||
}, [editor, linkNode])
|
||||
|
||||
const rangeRect = useRef<DOMRect>()
|
||||
const updateLinkEditorPosition = useCallback(() => {
|
||||
if (isMobile) {
|
||||
return
|
||||
}
|
||||
|
||||
const nativeSelection = window.getSelection()
|
||||
const rootElement = editor.getRootElement()
|
||||
|
||||
if (nativeSelection !== null && rootElement !== null) {
|
||||
if (rootElement.contains(nativeSelection.anchorNode)) {
|
||||
rangeRect.current = getDOMRangeRect(nativeSelection, rootElement)
|
||||
}
|
||||
}
|
||||
|
||||
const linkEditorElement = linkViewerRef.current
|
||||
|
||||
if (!linkEditorElement) {
|
||||
setTimeout(updateLinkEditorPosition)
|
||||
return
|
||||
}
|
||||
|
||||
if (!rootElement) {
|
||||
return
|
||||
}
|
||||
|
||||
const linkEditorRect = linkEditorElement.getBoundingClientRect()
|
||||
const rootElementRect = rootElement.getBoundingClientRect()
|
||||
|
||||
const calculatedStyles = getPositionedPopoverStyles({
|
||||
align: 'center',
|
||||
side: 'top',
|
||||
anchorRect: rangeRect.current,
|
||||
popoverRect: linkEditorRect,
|
||||
documentRect: rootElementRect,
|
||||
offset: 12,
|
||||
maxHeightFunction: () => 'none',
|
||||
})
|
||||
if (calculatedStyles) {
|
||||
const adjustedStyles = getAdjustedStylesForNonPortalPopover(linkEditorElement, calculatedStyles)
|
||||
Object.entries(adjustedStyles).forEach(([key, value]) => {
|
||||
linkEditorElement.style.setProperty(key, value)
|
||||
})
|
||||
linkEditorElement.style.opacity = '1'
|
||||
}
|
||||
}, [editor, isMobile])
|
||||
|
||||
useElementResize(linkViewerRef.current, updateLinkEditorPosition)
|
||||
|
||||
useEffect(() => {
|
||||
updateLinkEditorPosition()
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(() => {
|
||||
updateLinkEditorPosition()
|
||||
}),
|
||||
editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
(_payload) => {
|
||||
updateLinkEditorPosition()
|
||||
return false
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
)
|
||||
}, [editor, updateLinkEditorPosition])
|
||||
|
||||
if (!linkUrl) {
|
||||
return null
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute z-dropdown-menu rounded-lg border border-border bg-contrast px-2 py-1 shadow-sm shadow-contrast',
|
||||
isMobile
|
||||
? 'bottom-12 left-1/2 w-[calc(100%_-_1rem)] -translate-x-1/2'
|
||||
: 'left-0 top-0 w-auto translate-x-0 opacity-0 translucent-ui:border-[--popover-border-color] translucent-ui:bg-[--popover-background-color] translucent-ui:[backdrop-filter:var(--popover-backdrop-filter)]',
|
||||
)}
|
||||
ref={linkViewerRef}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<a
|
||||
className={classNames(
|
||||
'mr-1 flex flex-grow items-center gap-2 overflow-hidden whitespace-nowrap underline',
|
||||
isAutoLink && 'py-2.5',
|
||||
)}
|
||||
href={linkUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon type="open-in" className="ml-1 flex-shrink-0" />
|
||||
<div className="max-w-[35ch] overflow-hidden text-ellipsis">{linkUrl}</div>
|
||||
</a>
|
||||
<StyledTooltip showOnMobile showOnHover label="Copy link">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(linkUrl).catch(console.error)
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="copy" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
{!isAutoLink && (
|
||||
<>
|
||||
<StyledTooltip showOnMobile showOnHover label="Edit link">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
setIsEditingLink(true)
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="pencil-filled" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip showOnMobile showOnHover label="Remove link">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="trash-filled" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
document.getElementById(ElementIds.SuperEditor) ?? document.body,
|
||||
)
|
||||
}
|
||||
|
||||
export default LinkViewer
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { sanitizeUrl } from '../../Lexical/Utils/sanitizeUrl'
|
||||
import { TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||
import { useCallback, useState, useRef, useEffect } from 'react'
|
||||
import { LexicalEditor } from 'lexical'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||
|
||||
type Props = {
|
||||
linkUrl: string
|
||||
isEditMode: boolean
|
||||
setEditMode: (isEditMode: boolean) => void
|
||||
editor: LexicalEditor
|
||||
isAutoLink: boolean
|
||||
}
|
||||
|
||||
const LinkEditor = ({ linkUrl, isEditMode, setEditMode, editor, isAutoLink }: Props) => {
|
||||
const [editedLinkUrl, setEditedLinkUrl] = useState('')
|
||||
const editModeContainer = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleLinkSubmission = () => {
|
||||
if (editedLinkUrl !== '') {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(editedLinkUrl))
|
||||
}
|
||||
setEditMode(false)
|
||||
}
|
||||
|
||||
const focusInput = useCallback((input: HTMLInputElement | null) => {
|
||||
if (input) {
|
||||
input.focus()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setEditedLinkUrl(linkUrl)
|
||||
}, [linkUrl])
|
||||
|
||||
return isEditMode ? (
|
||||
<div className="flex items-center gap-2" ref={editModeContainer}>
|
||||
<input
|
||||
id="link-input"
|
||||
ref={focusInput}
|
||||
value={editedLinkUrl}
|
||||
onChange={(event) => {
|
||||
setEditedLinkUrl(event.target.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
event.preventDefault()
|
||||
handleLinkSubmission()
|
||||
} else if (event.key === KeyboardKey.Escape) {
|
||||
event.preventDefault()
|
||||
setEditMode(false)
|
||||
}
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
if (!editModeContainer.current?.contains(event.relatedTarget as Node)) {
|
||||
setEditMode(false)
|
||||
}
|
||||
}}
|
||||
className="flex-grow rounded-sm bg-contrast p-1 text-text sm:min-w-[40ch]"
|
||||
/>
|
||||
<StyledTooltip showOnMobile showOnHover label="Cancel editing">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
setEditMode(false)
|
||||
editor.focus()
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="close" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip showOnMobile showOnHover label="Save link">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={handleLinkSubmission}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="check" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<a
|
||||
className={classNames(
|
||||
'mr-1 flex flex-grow items-center gap-2 overflow-hidden whitespace-nowrap underline',
|
||||
isAutoLink && 'py-2.5',
|
||||
)}
|
||||
href={linkUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon type="open-in" className="ml-1 flex-shrink-0" />
|
||||
<div className="max-w-[35ch] overflow-hidden text-ellipsis">{linkUrl}</div>
|
||||
</a>
|
||||
{!isAutoLink && (
|
||||
<>
|
||||
<StyledTooltip showOnMobile showOnHover label="Edit link">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
setEditedLinkUrl(linkUrl)
|
||||
setEditMode(true)
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="pencil-filled" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip showOnMobile showOnHover label="Remove link">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="trash-filled" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LinkEditor
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { $getSelection, $isRangeSelection, $isTextNode, LexicalEditor, RangeSelection, TextNode } from 'lexical'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { VisuallyHidden } from '@ariakit/react'
|
||||
import { getSelectedNode } from '../../Lexical/Utils/getSelectedNode'
|
||||
import { $isLinkNode } from '@lexical/link'
|
||||
import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||
|
||||
type Props = {
|
||||
linkText: string
|
||||
editor: LexicalEditor
|
||||
isEditMode: boolean
|
||||
setEditMode: (isEditMode: boolean) => void
|
||||
}
|
||||
|
||||
export const $isLinkTextNode = (
|
||||
node: ReturnType<typeof getSelectedNode>,
|
||||
selection: RangeSelection,
|
||||
): node is TextNode => {
|
||||
const parent = node.getParent()
|
||||
return $isLinkNode(parent) && $isTextNode(node) && selection.anchor.getNode() === selection.focus.getNode()
|
||||
}
|
||||
|
||||
const LinkTextEditor = ({ linkText, editor, isEditMode, setEditMode }: Props) => {
|
||||
const [editedLinkText, setEditedLinkText] = useState(() => linkText)
|
||||
const editModeContainer = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setEditedLinkText(linkText)
|
||||
}, [linkText])
|
||||
|
||||
const focusInput = useCallback((input: HTMLInputElement | null) => {
|
||||
if (input) {
|
||||
input.focus()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleLinkTextSubmission = () => {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if (!$isRangeSelection(selection)) {
|
||||
return
|
||||
}
|
||||
const node = getSelectedNode(selection)
|
||||
if (!$isLinkTextNode(node, selection)) {
|
||||
return
|
||||
}
|
||||
node.setTextContent(editedLinkText)
|
||||
})
|
||||
setEditMode(false)
|
||||
}
|
||||
|
||||
return isEditMode ? (
|
||||
<div className="flex items-center gap-2" ref={editModeContainer}>
|
||||
<input
|
||||
id="link-input"
|
||||
ref={focusInput}
|
||||
value={editedLinkText}
|
||||
onChange={(event) => {
|
||||
setEditedLinkText(event.target.value)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
event.preventDefault()
|
||||
handleLinkTextSubmission()
|
||||
} else if (event.key === KeyboardKey.Escape) {
|
||||
event.preventDefault()
|
||||
setEditMode(false)
|
||||
}
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
if (!editModeContainer.current?.contains(event.relatedTarget as Node)) {
|
||||
setEditMode(false)
|
||||
}
|
||||
}}
|
||||
className="flex-grow rounded-sm bg-contrast p-1 text-text sm:min-w-[20ch]"
|
||||
/>
|
||||
<StyledTooltip showOnMobile showOnHover label="Cancel editing">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
setEditMode(false)
|
||||
editor.focus()
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="close" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip showOnMobile showOnHover label="Save link text">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={handleLinkTextSubmission}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="check" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon type="plain-text" className="ml-1 mr-1 flex-shrink-0" />
|
||||
<div className="flex-grow overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<VisuallyHidden>Link text:</VisuallyHidden>
|
||||
{linkText}
|
||||
</div>
|
||||
<StyledTooltip showOnMobile showOnHover label="Edit link text">
|
||||
<button
|
||||
className="flex select-none items-center justify-center rounded p-2 enabled:hover:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]"
|
||||
onClick={() => {
|
||||
setEditedLinkText(linkText)
|
||||
setEditMode(true)
|
||||
}}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<Icon type="pencil-filled" size="medium" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LinkTextEditor
|
||||
+56
-47
@@ -22,6 +22,8 @@ import {
|
||||
COMMAND_PRIORITY_LOW,
|
||||
$createParagraphNode,
|
||||
$isTextNode,
|
||||
$getNodeByKey,
|
||||
TextNode,
|
||||
} from 'lexical'
|
||||
import {
|
||||
mergeRegister,
|
||||
@@ -29,7 +31,7 @@ import {
|
||||
$getNearestNodeOfType,
|
||||
$getNearestBlockElementAncestorOrThrow,
|
||||
} from '@lexical/utils'
|
||||
import { $isLinkNode, $isAutoLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||
import { $isLinkNode, TOGGLE_LINK_COMMAND, LinkNode } from '@lexical/link'
|
||||
import { $isListNode, ListNode } from '@lexical/list'
|
||||
import { $isHeadingNode, $isQuoteNode } from '@lexical/rich-text'
|
||||
import {
|
||||
@@ -60,13 +62,12 @@ import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip'
|
||||
import { Toolbar, ToolbarItem, useToolbarStore } from '@ariakit/react'
|
||||
import { PasswordBlock } from '../Blocks/Password'
|
||||
import { URL_REGEX } from '@/Constants/Constants'
|
||||
import { $isLinkTextNode } from './ToolbarLinkTextEditor'
|
||||
import Popover from '@/Components/Popover/Popover'
|
||||
import LexicalTableOfContents from '@lexical/react/LexicalTableOfContents'
|
||||
import Menu from '@/Components/Menu/Menu'
|
||||
import MenuItem, { MenuItemProps } from '@/Components/Menu/MenuItem'
|
||||
import { debounce, remToPx } from '@/Utils'
|
||||
import FloatingLinkEditor from './FloatingLinkEditor'
|
||||
import LinkEditor, { $isLinkTextNode } from './LinkEditor'
|
||||
import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator'
|
||||
import { useStateRef } from '@/Hooks/useStateRef'
|
||||
import { getDOMRangeRect } from '../../Lexical/Utils/getDOMRangeRect'
|
||||
@@ -74,6 +75,7 @@ import { getPositionedPopoverStyles } from '@/Components/Popover/GetPositionedPo
|
||||
import usePreference from '@/Hooks/usePreference'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { $isDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||
import LinkViewer from './LinkViewer'
|
||||
|
||||
const TOGGLE_LINK_AND_EDIT_COMMAND = createCommand<string | null>('TOGGLE_LINK_AND_EDIT_COMMAND')
|
||||
|
||||
@@ -140,9 +142,11 @@ const ToolbarButton = forwardRef(
|
||||
'flex select-none items-center justify-center rounded p-0.5 focus:shadow-none focus:outline-none enabled:hover:bg-default enabled:focus-visible:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]',
|
||||
className,
|
||||
)}
|
||||
onClick={() => {
|
||||
onSelect()
|
||||
}}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault()
|
||||
onSelect()
|
||||
}}
|
||||
onContextMenu={(event) => {
|
||||
editor.focus()
|
||||
@@ -184,7 +188,8 @@ const ToolbarMenuItem = ({ name, iconName, active, onClick, ...props }: ToolbarM
|
||||
return (
|
||||
<MenuItem
|
||||
className={classNames('overflow-hidden md:py-2', active ? '!bg-info !text-info-contrast' : 'hover:bg-contrast')}
|
||||
onMouseDown={onClick}
|
||||
onClick={onClick}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
{...props}
|
||||
>
|
||||
<Icon type={iconName} className="-mt-px mr-2.5 flex-shrink-0" />
|
||||
@@ -216,12 +221,9 @@ const ToolbarPlugin = () => {
|
||||
const [isCode, setIsCode] = useState(false)
|
||||
const [isHighlight, setIsHighlight] = useState(false)
|
||||
|
||||
const [isLink, setIsLink] = useState(false)
|
||||
const [isAutoLink, setIsAutoLink] = useState(false)
|
||||
const [isLinkText, setIsLinkText] = useState(false)
|
||||
const [isLinkEditMode, setIsLinkEditMode] = useState(false)
|
||||
const [linkText, setLinkText] = useState<string>('')
|
||||
const [linkUrl, setLinkUrl] = useState<string>('')
|
||||
const [linkNode, setLinkNode] = useState<LinkNode | null>(null)
|
||||
const [linkTextNode, setLinkTextNode] = useState<TextNode | null>(null)
|
||||
const [isEditingLink, setIsEditingLink] = useState(false)
|
||||
|
||||
const [isTOCOpen, setIsTOCOpen] = useState(false)
|
||||
const tocAnchorRef = useRef<HTMLButtonElement>(null)
|
||||
@@ -338,23 +340,18 @@ const ToolbarPlugin = () => {
|
||||
// Update links
|
||||
const node = getSelectedNode(selection)
|
||||
const parent = node.getParent()
|
||||
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
||||
setIsLink(true)
|
||||
setIsEditingLink(false)
|
||||
if ($isLinkNode(node)) {
|
||||
setLinkNode(node)
|
||||
} else if ($isLinkNode(parent)) {
|
||||
setLinkNode(parent)
|
||||
} else {
|
||||
setIsLink(false)
|
||||
}
|
||||
setLinkUrl($isLinkNode(parent) ? parent.getURL() : $isLinkNode(node) ? node.getURL() : '')
|
||||
if ($isAutoLinkNode(parent) || $isAutoLinkNode(node)) {
|
||||
setIsAutoLink(true)
|
||||
} else {
|
||||
setIsAutoLink(false)
|
||||
setLinkNode(null)
|
||||
}
|
||||
if ($isLinkTextNode(node, selection)) {
|
||||
setIsLinkText(true)
|
||||
setLinkText(node.getTextContent())
|
||||
setLinkTextNode(node)
|
||||
} else {
|
||||
setIsLinkText(false)
|
||||
setLinkText('')
|
||||
setLinkTextNode(null)
|
||||
}
|
||||
|
||||
if (elementDOM !== null) {
|
||||
@@ -501,13 +498,11 @@ const ToolbarPlugin = () => {
|
||||
TOGGLE_LINK_AND_EDIT_COMMAND,
|
||||
(payload) => {
|
||||
if (payload === null) {
|
||||
setIsEditingLink(false)
|
||||
return activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
||||
} else if (typeof payload === 'string') {
|
||||
const dispatched = activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, payload)
|
||||
setIsLink(true)
|
||||
setLinkUrl(payload)
|
||||
setIsLinkEditMode(true)
|
||||
return dispatched
|
||||
} else {
|
||||
setIsEditingLink(true)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
@@ -538,11 +533,9 @@ const ToolbarPlugin = () => {
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
activeEditor.dispatchCommand(TOGGLE_LINK_AND_EDIT_COMMAND, '')
|
||||
setIsLinkEditMode(true)
|
||||
})
|
||||
} else {
|
||||
activeEditor.dispatchCommand(TOGGLE_LINK_AND_EDIT_COMMAND, '')
|
||||
setIsLinkEditMode(true)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -551,7 +544,7 @@ const ToolbarPlugin = () => {
|
||||
},
|
||||
COMMAND_PRIORITY_NORMAL,
|
||||
)
|
||||
}, [activeEditor, isLink])
|
||||
}, [activeEditor])
|
||||
|
||||
const dismissButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
@@ -576,7 +569,7 @@ const ToolbarPlugin = () => {
|
||||
const elementToBeFocused = event.relatedTarget as Node
|
||||
const containerContainsElementToFocus = container?.contains(elementToBeFocused)
|
||||
const linkEditorContainsElementToFocus = document
|
||||
.getElementById('super-link-editor')
|
||||
.getElementById(ElementIds.SuperEditor)
|
||||
?.contains(elementToBeFocused)
|
||||
const willFocusDismissButton = dismissButtonRef.current === elementToBeFocused
|
||||
if ((containerContainsElementToFocus || linkEditorContainsElementToFocus) && !willFocusDismissButton) {
|
||||
@@ -660,16 +653,22 @@ const ToolbarPlugin = () => {
|
||||
id="super-mobile-toolbar"
|
||||
ref={containerRef}
|
||||
>
|
||||
{isLink && (
|
||||
<FloatingLinkEditor
|
||||
linkUrl={linkUrl}
|
||||
linkText={linkText}
|
||||
isEditMode={isLinkEditMode}
|
||||
setEditMode={setIsLinkEditMode}
|
||||
editor={editor}
|
||||
isAutoLink={isAutoLink}
|
||||
isLinkText={isLinkText}
|
||||
{linkNode && !isEditingLink && (
|
||||
<LinkViewer
|
||||
key={linkNode.__key}
|
||||
linkNode={linkNode}
|
||||
isMobile={isMobile}
|
||||
setIsEditingLink={setIsEditingLink}
|
||||
editor={activeEditor}
|
||||
/>
|
||||
)}
|
||||
{isEditingLink && (
|
||||
<LinkEditor
|
||||
editor={activeEditor}
|
||||
setIsEditingLink={setIsEditingLink}
|
||||
isMobile={isMobile}
|
||||
linkNode={linkNode}
|
||||
linkTextNode={linkTextNode}
|
||||
/>
|
||||
)}
|
||||
<div className="flex w-full flex-shrink-0 border-t border-border md:border-0">
|
||||
@@ -738,7 +737,7 @@ const ToolbarPlugin = () => {
|
||||
<ToolbarButton
|
||||
name="Link"
|
||||
iconName="link"
|
||||
active={isLink}
|
||||
active={!!linkNode}
|
||||
onSelect={() => {
|
||||
editor.dispatchCommand(TOGGLE_LINK_AND_EDIT_COMMAND, '')
|
||||
}}
|
||||
@@ -816,6 +815,7 @@ const ToolbarPlugin = () => {
|
||||
className="py-1"
|
||||
disableMobileFullscreenTakeover
|
||||
disableFlip
|
||||
disableApplyingMobileWidth
|
||||
portal={false}
|
||||
documentElement={popoverDocumentElement}
|
||||
>
|
||||
@@ -838,15 +838,24 @@ const ToolbarPlugin = () => {
|
||||
key={key}
|
||||
className="overflow-hidden md:py-2"
|
||||
onClick={() => {
|
||||
editor.getEditorState().read(() => {
|
||||
setIsTOCOpen(false)
|
||||
editor.update(() => {
|
||||
const node = $getNodeByKey(key)
|
||||
if (!node) {
|
||||
return
|
||||
}
|
||||
node.selectEnd()
|
||||
editor.focus()
|
||||
const domElement = editor.getElementByKey(key)
|
||||
if (!domElement) {
|
||||
return
|
||||
}
|
||||
domElement.scrollIntoView({ block: 'start' })
|
||||
setIsTOCOpen(false)
|
||||
setTimeout(() => {
|
||||
domElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}, 1)
|
||||
})
|
||||
}}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
style={{
|
||||
paddingLeft: `${(level - 1) * remToPx(1) + remToPx(0.75)}px`,
|
||||
}}
|
||||
|
||||
+5
-1
@@ -278,7 +278,11 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface {
|
||||
this.exportEditor.getEditorState().read(() => {
|
||||
const fileNodes = $nodesOfType(FileNode)
|
||||
fileNodes.forEach((fileNode) => {
|
||||
ids.push(fileNode.getId())
|
||||
const nodeId = fileNode.getId()
|
||||
if (ids.includes(nodeId)) {
|
||||
return
|
||||
}
|
||||
ids.push(nodeId)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -72,13 +72,7 @@ const Navigation = forwardRef<HTMLDivElement, Props>(({ application, className,
|
||||
)}
|
||||
ref={mergeRefs([ref, setElement])}
|
||||
>
|
||||
<div
|
||||
id="navigation-content"
|
||||
className={classNames(
|
||||
'flex-grow overflow-y-auto overflow-x-hidden md:overflow-y-hidden md:hover:overflow-y-auto',
|
||||
'md:hover:[overflow-y:_overlay] pointer-coarse:md:overflow-y-auto',
|
||||
)}
|
||||
>
|
||||
<div id="navigation-content" className="flex-grow overflow-y-auto overflow-x-hidden">
|
||||
<TagSearchBar navigationController={application.navigationController} />
|
||||
<SmartViewsSection
|
||||
application={application}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user