mirror of
https://github.com/standardnotes/app
synced 2026-09-14 12:45:58 -04:00
Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9078f374b | ||
|
|
bb4f1ff099 | ||
|
|
abdaec89b7 | ||
|
|
f559442a67 | ||
|
|
51aed0a518 | ||
|
|
35bb671e31 | ||
|
|
d76b190aba | ||
|
|
d6fd85e451 | ||
|
|
c90087462f | ||
|
|
58838a44cc | ||
|
|
0223800fdf | ||
|
|
318b1fdb0f | ||
|
|
fb517a91b0 | ||
|
|
c7b158eb64 | ||
|
|
d4b63e4ea6 | ||
|
|
cd8596b14e | ||
|
|
6c11766051 | ||
|
|
7f41564328 | ||
|
|
4e576d7526 | ||
|
|
317c6555d2 | ||
|
|
220f62665e | ||
|
|
1dc9086257 | ||
|
|
d149b500ee | ||
|
|
ad70bea368 | ||
|
|
3bcd969f83 | ||
|
|
d4af612fea | ||
|
|
b068b814f2 | ||
|
|
51de012236 | ||
|
|
2694c2f1a9 | ||
|
|
d3609853c6 | ||
|
|
a7ac715499 | ||
|
|
5ac93a6bd8 | ||
|
|
be4cc4e605 |
@@ -64,7 +64,7 @@ jobs:
|
||||
|
||||
- name: Run E2E test suite
|
||||
if: "${{ contains(github.event.head_commit.message, 'skip e2e') == false }}"
|
||||
uses: convictional/trigger-workflow-and-wait@v1.6.1
|
||||
uses: convictional/trigger-workflow-and-wait@master
|
||||
with:
|
||||
owner: standardnotes
|
||||
repo: e2e
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
docker tag standardnotes/snjs:${{ github.sha }} standardnotes/snjs:test
|
||||
docker push standardnotes/snjs:test
|
||||
- name: Run E2E test suite
|
||||
uses: convictional/trigger-workflow-and-wait@v1.6.1
|
||||
uses: convictional/trigger-workflow-and-wait@master
|
||||
with:
|
||||
owner: standardnotes
|
||||
repo: e2e
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.20.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **snjs:** refreshing sessions ([#2106](https://github.com/standardnotes/app/issues/2106)) ([bb4f1ff](https://github.com/standardnotes/app/commit/bb4f1ff0990eb5a6f64e76a699fb95540cffdbb4))
|
||||
|
||||
## [1.20.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.20.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
@@ -6,12 +6,4 @@ module.exports = {
|
||||
transform: {
|
||||
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }],
|
||||
},
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 22,
|
||||
functions: 69,
|
||||
lines: 67,
|
||||
statements: 67
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api",
|
||||
"version": "1.20.18",
|
||||
"version": "1.20.20",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -23,7 +23,7 @@
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"lint:fix": "eslint src --ext .ts --fix",
|
||||
"test": "jest spec --coverage"
|
||||
"test": "jest spec --coverage --passWithNoTests"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.2.3",
|
||||
@@ -37,6 +37,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.45.0",
|
||||
"@standardnotes/domain-core": "^1.11.0",
|
||||
"@standardnotes/encryption": "workspace:*",
|
||||
"@standardnotes/models": "workspace:*",
|
||||
"@standardnotes/responses": "workspace:*",
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
import { type Invitation } from '@standardnotes/models'
|
||||
|
||||
import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/SubscriptionInviteAcceptResponse'
|
||||
import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse'
|
||||
import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse'
|
||||
import { SubscriptionInviteResponse } from '../../Response/Subscription/SubscriptionInviteResponse'
|
||||
import { SubscriptionServerInterface } from '../../Server/Subscription/SubscriptionServerInterface'
|
||||
|
||||
import { SubscriptionApiOperations } from './SubscriptionApiOperations'
|
||||
import { SubscriptionApiService } from './SubscriptionApiService'
|
||||
|
||||
describe('SubscriptionApiService', () => {
|
||||
let subscriptionServer: SubscriptionServerInterface
|
||||
|
||||
const createService = () => new SubscriptionApiService(subscriptionServer)
|
||||
|
||||
beforeEach(() => {
|
||||
subscriptionServer = {} as jest.Mocked<SubscriptionServerInterface>
|
||||
subscriptionServer.invite = jest.fn().mockReturnValue({
|
||||
data: { success: true, sharedSubscriptionInvitationUuid: '1-2-3' },
|
||||
} as jest.Mocked<SubscriptionInviteResponse>)
|
||||
subscriptionServer.cancelInvite = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<SubscriptionInviteCancelResponse>)
|
||||
subscriptionServer.listInvites = jest.fn().mockReturnValue({
|
||||
data: { invitations: [{} as jest.Mocked<Invitation>] },
|
||||
} as jest.Mocked<SubscriptionInviteListResponse>)
|
||||
subscriptionServer.acceptInvite = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<SubscriptionInviteAcceptResponse>)
|
||||
})
|
||||
|
||||
it('should invite a user', async () => {
|
||||
const response = await createService().invite('[email protected]')
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
sharedSubscriptionInvitationUuid: '1-2-3',
|
||||
},
|
||||
})
|
||||
expect(subscriptionServer.invite).toHaveBeenCalledWith({
|
||||
api: '20200115',
|
||||
identifier: '[email protected]',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not invite a user if it is already inviting', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[SubscriptionApiOperations.Inviting, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.invite('[email protected]')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not invite a user if the server fails', async () => {
|
||||
subscriptionServer.invite = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().invite('[email protected]')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should cancel an invite', async () => {
|
||||
const response = await createService().cancelInvite('1-2-3')
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
expect(subscriptionServer.cancelInvite).toHaveBeenCalledWith({
|
||||
api: '20200115',
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not cancel an invite if it is already canceling', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[SubscriptionApiOperations.CancelingInvite, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.cancelInvite('1-2-3')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not cancel an invite if the server fails', async () => {
|
||||
subscriptionServer.cancelInvite = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().cancelInvite('1-2-3')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should list invites', async () => {
|
||||
const response = await createService().listInvites()
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
invitations: [{} as jest.Mocked<Invitation>],
|
||||
},
|
||||
})
|
||||
expect(subscriptionServer.listInvites).toHaveBeenCalledWith({
|
||||
api: '20200115',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not list invitations if it is already listing', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[SubscriptionApiOperations.ListingInvites, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.listInvites()
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not list invites if the server fails', async () => {
|
||||
subscriptionServer.listInvites = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().listInvites()
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should accept an invite', async () => {
|
||||
const response = await createService().acceptInvite('1-2-3')
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
expect(subscriptionServer.acceptInvite).toHaveBeenCalledWith({
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not accept an invite if it is already accepting', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[SubscriptionApiOperations.AcceptingInvite, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.acceptInvite('1-2-3')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not accept an invite if the server fails', async () => {
|
||||
subscriptionServer.acceptInvite = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().acceptInvite('1-2-3')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,197 +0,0 @@
|
||||
import { ProtocolVersion, UserRequestType } from '@standardnotes/common'
|
||||
import { type RootKeyParamsInterface } from '@standardnotes/models'
|
||||
import { UserDeletionResponse } from '../../Response/User/UserDeletionResponse'
|
||||
|
||||
import { UserRegistrationResponse } from '../../Response/User/UserRegistrationResponse'
|
||||
import { UserRequestResponse } from '../../Response/UserRequest/UserRequestResponse'
|
||||
import { UserServerInterface } from '../../Server'
|
||||
import { UserRequestServerInterface } from '../../Server/UserRequest/UserRequestServerInterface'
|
||||
|
||||
import { UserApiOperations } from './UserApiOperations'
|
||||
import { UserApiService } from './UserApiService'
|
||||
|
||||
describe('UserApiService', () => {
|
||||
let userServer: UserServerInterface
|
||||
let userRequestServer: UserRequestServerInterface
|
||||
let keyParams: RootKeyParamsInterface
|
||||
|
||||
const createService = () => new UserApiService(userServer, userRequestServer)
|
||||
|
||||
beforeEach(() => {
|
||||
userServer = {} as jest.Mocked<UserServerInterface>
|
||||
userServer.register = jest.fn().mockReturnValue({
|
||||
data: { user: { email: '[email protected]', uuid: '1-2-3' } },
|
||||
} as jest.Mocked<UserRegistrationResponse>)
|
||||
userServer.deleteAccount = jest.fn().mockReturnValue({
|
||||
data: { message: 'Success' },
|
||||
} as jest.Mocked<UserDeletionResponse>)
|
||||
|
||||
userRequestServer = {} as jest.Mocked<UserRequestServerInterface>
|
||||
userRequestServer.submitUserRequest = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<UserRequestResponse>)
|
||||
|
||||
keyParams = {} as jest.Mocked<RootKeyParamsInterface>
|
||||
keyParams.getPortableValue = jest.fn().mockReturnValue({
|
||||
identifier: '[email protected]',
|
||||
version: ProtocolVersion.V004,
|
||||
})
|
||||
})
|
||||
|
||||
it('should register a user', async () => {
|
||||
const response = await createService().register({
|
||||
email: '[email protected]',
|
||||
serverPassword: 'testpasswd',
|
||||
keyParams,
|
||||
ephemeral: false,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
user: {
|
||||
email: '[email protected]',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(userServer.register).toHaveBeenCalledWith({
|
||||
api: '20200115',
|
||||
email: '[email protected]',
|
||||
ephemeral: false,
|
||||
identifier: '[email protected]',
|
||||
password: 'testpasswd',
|
||||
version: '004',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not register a user if it is already registering', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[UserApiOperations.Registering, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.register({ email: '[email protected]', serverPassword: 'testpasswd', keyParams, ephemeral: false })
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not register a user if the server fails', async () => {
|
||||
userServer.register = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().register({
|
||||
email: '[email protected]',
|
||||
serverPassword: 'testpasswd',
|
||||
keyParams,
|
||||
ephemeral: false,
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should submit a user request', async () => {
|
||||
const response = await createService().submitUserRequest({
|
||||
userUuid: '1-2-3',
|
||||
requestType: UserRequestType.ExitDiscount,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
expect(userRequestServer.submitUserRequest).toHaveBeenCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
requestType: UserRequestType.ExitDiscount,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not submit a user request if it is already submitting', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[UserApiOperations.SubmittingRequest, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.submitUserRequest({ userUuid: '1-2-3', requestType: UserRequestType.ExitDiscount })
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not submit a user request if the server fails', async () => {
|
||||
userRequestServer.submitUserRequest = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().submitUserRequest({
|
||||
userUuid: '1-2-3',
|
||||
requestType: UserRequestType.ExitDiscount,
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should delete a user', async () => {
|
||||
const response = await createService().deleteAccount('1-2-3')
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
message: 'Success',
|
||||
},
|
||||
})
|
||||
expect(userServer.deleteAccount).toHaveBeenCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not delete a user if it is already deleting', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[UserApiOperations.DeletingAccount, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.deleteAccount('1-2-3')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not delete a user if the server fails', async () => {
|
||||
userServer.deleteAccount = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().deleteAccount('1-2-3')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,61 +0,0 @@
|
||||
import { WebSocketConnectionTokenResponse } from '../../Response'
|
||||
|
||||
import { WebSocketServerInterface } from '../../Server/WebSocket/WebSocketServerInterface'
|
||||
import { WebSocketApiOperations } from './WebSocketApiOperations'
|
||||
|
||||
import { WebSocketApiService } from './WebSocketApiService'
|
||||
|
||||
describe('WebSocketApiService', () => {
|
||||
let webSocketServer: WebSocketServerInterface
|
||||
|
||||
const createService = () => new WebSocketApiService(webSocketServer)
|
||||
|
||||
beforeEach(() => {
|
||||
webSocketServer = {} as jest.Mocked<WebSocketServerInterface>
|
||||
webSocketServer.createConnectionToken = jest.fn().mockReturnValue({
|
||||
data: { token: 'foobar' },
|
||||
} as jest.Mocked<WebSocketConnectionTokenResponse>)
|
||||
})
|
||||
|
||||
it('should create a websocket connection token', async () => {
|
||||
const response = await createService().createConnectionToken()
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
token: 'foobar',
|
||||
},
|
||||
})
|
||||
expect(webSocketServer.createConnectionToken).toHaveBeenCalledWith({})
|
||||
})
|
||||
|
||||
it('should not create a token if it is already creating', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WebSocketApiOperations.CreatingConnectionToken, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.createConnectionToken()
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not create a token if the server fails', async () => {
|
||||
webSocketServer.createConnectionToken = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().createConnectionToken()
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,368 +0,0 @@
|
||||
import { WorkspaceAccessLevel, WorkspaceType } from '@standardnotes/common'
|
||||
|
||||
import { HttpStatusCode } from '../../Http'
|
||||
import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse'
|
||||
import { WorkspaceInvitationAcceptingResponse } from '../../Response/Workspace/WorkspaceInvitationAcceptingResponse'
|
||||
import { WorkspaceInvitationResponse } from '../../Response/Workspace/WorkspaceInvitationResponse'
|
||||
import { WorkspaceListResponse } from '../../Response/Workspace/WorkspaceListResponse'
|
||||
import { WorkspaceUserListResponse } from '../../Response/Workspace/WorkspaceUserListResponse'
|
||||
import { WorkspaceServerInterface } from '../../Server/Workspace/WorkspaceServerInterface'
|
||||
import { WorkspaceKeyshareInitiatingResponse } from '../../Response/Workspace/WorkspaceKeyshareInitiatingResponse'
|
||||
|
||||
import { WorkspaceApiOperations } from './WorkspaceApiOperations'
|
||||
import { WorkspaceApiService } from './WorkspaceApiService'
|
||||
|
||||
describe('WorkspaceApiService', () => {
|
||||
let workspaceServer: WorkspaceServerInterface
|
||||
|
||||
const createService = () => new WorkspaceApiService(workspaceServer)
|
||||
|
||||
beforeEach(() => {
|
||||
workspaceServer = {} as jest.Mocked<WorkspaceServerInterface>
|
||||
workspaceServer.createWorkspace = jest.fn().mockReturnValue({
|
||||
data: { uuid: '1-2-3' },
|
||||
} as jest.Mocked<WorkspaceCreationResponse>)
|
||||
workspaceServer.inviteToWorkspace = jest.fn().mockReturnValue({
|
||||
data: { uuid: 'i-1-2-3' },
|
||||
} as jest.Mocked<WorkspaceInvitationResponse>)
|
||||
workspaceServer.acceptInvite = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<WorkspaceInvitationAcceptingResponse>)
|
||||
workspaceServer.listWorkspaces = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCode.Success,
|
||||
data: { ownedWorkspaces: [], joinedWorkspaces: [] },
|
||||
} as jest.Mocked<WorkspaceListResponse>)
|
||||
workspaceServer.listWorkspaceUsers = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCode.Success,
|
||||
data: { users: [] },
|
||||
} as jest.Mocked<WorkspaceUserListResponse>)
|
||||
workspaceServer.initiateKeyshare = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCode.Success,
|
||||
data: { success: true },
|
||||
} as jest.Mocked<WorkspaceKeyshareInitiatingResponse>)
|
||||
})
|
||||
|
||||
it('should create a workspace', async () => {
|
||||
const response = await createService().createWorkspace({
|
||||
workspaceType: WorkspaceType.Private,
|
||||
encryptedPrivateKey: 'foo',
|
||||
encryptedWorkspaceKey: 'bar',
|
||||
publicKey: 'buzz',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
})
|
||||
expect(workspaceServer.createWorkspace).toHaveBeenCalledWith({
|
||||
encryptedPrivateKey: 'foo',
|
||||
encryptedWorkspaceKey: 'bar',
|
||||
publicKey: 'buzz',
|
||||
workspaceType: 'private',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not create a workspace if it is already creating', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WorkspaceApiOperations.Creating, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.createWorkspace({
|
||||
workspaceType: WorkspaceType.Private,
|
||||
encryptedPrivateKey: 'foo',
|
||||
encryptedWorkspaceKey: 'bar',
|
||||
publicKey: 'buzz',
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not create a workspace if the server fails', async () => {
|
||||
workspaceServer.createWorkspace = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().createWorkspace({
|
||||
workspaceType: WorkspaceType.Private,
|
||||
encryptedPrivateKey: 'foo',
|
||||
encryptedWorkspaceKey: 'bar',
|
||||
publicKey: 'buzz',
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should invite to a workspace', async () => {
|
||||
const response = await createService().inviteToWorkspace({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
inviteeEmail: '[email protected]',
|
||||
accessLevel: WorkspaceAccessLevel.WriteAndRead,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
uuid: 'i-1-2-3',
|
||||
},
|
||||
})
|
||||
expect(workspaceServer.inviteToWorkspace).toHaveBeenCalledWith({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
inviteeEmail: '[email protected]',
|
||||
accessLevel: 'write-and-read',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not invite to a workspace if it is already inviting', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WorkspaceApiOperations.Inviting, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.inviteToWorkspace({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
inviteeEmail: '[email protected]',
|
||||
accessLevel: WorkspaceAccessLevel.WriteAndRead,
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not invite to a workspace if the server fails', async () => {
|
||||
workspaceServer.inviteToWorkspace = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().inviteToWorkspace({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
inviteeEmail: '[email protected]',
|
||||
accessLevel: WorkspaceAccessLevel.WriteAndRead,
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should accept invite to a workspace', async () => {
|
||||
const response = await createService().acceptInvite({
|
||||
userUuid: 'u-1-2-3',
|
||||
inviteUuid: 'i-1-2-3',
|
||||
publicKey: 'foo',
|
||||
encryptedPrivateKey: 'bar',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
expect(workspaceServer.acceptInvite).toHaveBeenCalledWith({
|
||||
userUuid: 'u-1-2-3',
|
||||
inviteUuid: 'i-1-2-3',
|
||||
publicKey: 'foo',
|
||||
encryptedPrivateKey: 'bar',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not accept invite to a workspace if it is already accepting', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WorkspaceApiOperations.Accepting, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.acceptInvite({
|
||||
userUuid: 'u-1-2-3',
|
||||
inviteUuid: 'i-1-2-3',
|
||||
publicKey: 'foo',
|
||||
encryptedPrivateKey: 'bar',
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not accept invite to a workspace if the server fails', async () => {
|
||||
workspaceServer.acceptInvite = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().acceptInvite({
|
||||
userUuid: 'u-1-2-3',
|
||||
inviteUuid: 'i-1-2-3',
|
||||
publicKey: 'foo',
|
||||
encryptedPrivateKey: 'bar',
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should list workspaces', async () => {
|
||||
const response = await createService().listWorkspaces()
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: {
|
||||
ownedWorkspaces: [],
|
||||
joinedWorkspaces: [],
|
||||
},
|
||||
})
|
||||
expect(workspaceServer.listWorkspaces).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not list workspaces if it is already listing them', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WorkspaceApiOperations.ListingWorkspaces, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.listWorkspaces()
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not list workspaces if the server fails', async () => {
|
||||
workspaceServer.listWorkspaces = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().listWorkspaces()
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should list workspace users', async () => {
|
||||
const response = await createService().listWorkspaceUsers({ workspaceUuid: 'w-1-2-3' })
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: {
|
||||
users: [],
|
||||
},
|
||||
})
|
||||
expect(workspaceServer.listWorkspaceUsers).toHaveBeenCalledWith({ workspaceUuid: 'w-1-2-3' })
|
||||
})
|
||||
|
||||
it('should not list workspace users if it is already listing them', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WorkspaceApiOperations.ListingWorkspaceUsers, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.listWorkspaceUsers({ workspaceUuid: 'w-1-2-3' })
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not list workspace users if the server fails', async () => {
|
||||
workspaceServer.listWorkspaceUsers = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().listWorkspaceUsers({ workspaceUuid: 'w-1-2-3' })
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should initiate keyshare in workspace for user', async () => {
|
||||
const response = await createService().initiateKeyshare({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
userUuid: 'u-1-2-3',
|
||||
encryptedWorkspaceKey: 'foobar',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
expect(workspaceServer.initiateKeyshare).toHaveBeenCalledWith({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
userUuid: 'u-1-2-3',
|
||||
encryptedWorkspaceKey: 'foobar',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not initiate keyshare in workspace if it is already initiating', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WorkspaceApiOperations.InitiatingKeyshare, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.initiateKeyshare({ workspaceUuid: 'w-1-2-3', userUuid: 'u-1-2-3', encryptedWorkspaceKey: 'foobar' })
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not initiate keyshare in workspace if the server fails', async () => {
|
||||
workspaceServer.initiateKeyshare = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().initiateKeyshare({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
userUuid: 'u-1-2-3',
|
||||
encryptedWorkspaceKey: 'foobar',
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Environment } from '@standardnotes/models'
|
||||
|
||||
import { HttpResponseMeta } from './HttpResponseMeta'
|
||||
import { HttpService } from './HttpService'
|
||||
|
||||
describe('HttpService', () => {
|
||||
const environment = Environment.Web
|
||||
const appVersion = '1.2.3'
|
||||
const snjsVersion = '2.3.4'
|
||||
const host = 'http://bar'
|
||||
let updateMetaCallback: (meta: HttpResponseMeta) => void
|
||||
|
||||
const createService = () => {
|
||||
const service = new HttpService(environment, appVersion, snjsVersion, updateMetaCallback)
|
||||
service.setHost(host)
|
||||
return service
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
updateMetaCallback = jest.fn()
|
||||
})
|
||||
|
||||
it('should set host', () => {
|
||||
const service = createService()
|
||||
|
||||
expect(service['host']).toEqual('http://bar')
|
||||
|
||||
service.setHost('http://foo')
|
||||
|
||||
expect(service['host']).toEqual('http://foo')
|
||||
})
|
||||
|
||||
it('should set and use the authorization token', () => {
|
||||
const service = createService()
|
||||
|
||||
expect(service['authorizationToken']).toBeUndefined()
|
||||
|
||||
service.setAuthorizationToken('foo-bar')
|
||||
|
||||
expect(service['authorizationToken']).toEqual('foo-bar')
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
import { isString, joinPaths, sleep } from '@standardnotes/utils'
|
||||
import { Environment } from '@standardnotes/models'
|
||||
import { Session, SessionToken } from '@standardnotes/domain-core'
|
||||
|
||||
import { HttpRequestParams } from './HttpRequestParams'
|
||||
import { HttpVerb } from './HttpVerb'
|
||||
import { HttpRequest } from './HttpRequest'
|
||||
@@ -10,21 +12,26 @@ import { XMLHttpRequestState } from './XMLHttpRequestState'
|
||||
import { ErrorMessage } from '../Error/ErrorMessage'
|
||||
import { HttpResponseMeta } from './HttpResponseMeta'
|
||||
import { HttpErrorResponseBody } from './HttpErrorResponseBody'
|
||||
import { Paths } from '../Server/Auth/Paths'
|
||||
import { SessionRefreshResponse } from '../Response/Auth/SessionRefreshResponse'
|
||||
|
||||
export class HttpService implements HttpServiceInterface {
|
||||
private authorizationToken?: string
|
||||
private session: Session | null
|
||||
private __latencySimulatorMs?: number
|
||||
private host!: string
|
||||
private declare host: string
|
||||
|
||||
constructor(
|
||||
private environment: Environment,
|
||||
private appVersion: string,
|
||||
private snjsVersion: string,
|
||||
private updateMetaCallback: (meta: HttpResponseMeta) => void,
|
||||
) {}
|
||||
private refreshSessionCallback: (session: Session) => void,
|
||||
) {
|
||||
this.session = null
|
||||
}
|
||||
|
||||
setAuthorizationToken(authorizationToken: string): void {
|
||||
this.authorizationToken = authorizationToken
|
||||
setSession(session: Session): void {
|
||||
this.session = session
|
||||
}
|
||||
|
||||
setHost(host: string): void {
|
||||
@@ -36,7 +43,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
url: joinPaths(this.host, path),
|
||||
params,
|
||||
verb: HttpVerb.Get,
|
||||
authentication: authentication ?? this.authorizationToken,
|
||||
authentication: authentication ?? this.session?.accessToken.value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -45,7 +52,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
url: joinPaths(this.host, path),
|
||||
params,
|
||||
verb: HttpVerb.Post,
|
||||
authentication: authentication ?? this.authorizationToken,
|
||||
authentication: authentication ?? this.session?.accessToken.value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,7 +61,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
url: joinPaths(this.host, path),
|
||||
params,
|
||||
verb: HttpVerb.Put,
|
||||
authentication: authentication ?? this.authorizationToken,
|
||||
authentication: authentication ?? this.session?.accessToken.value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -63,7 +70,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
url: joinPaths(this.host, path),
|
||||
params,
|
||||
verb: HttpVerb.Patch,
|
||||
authentication: authentication ?? this.authorizationToken,
|
||||
authentication: authentication ?? this.session?.accessToken.value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,7 +79,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
url: joinPaths(this.host, path),
|
||||
params,
|
||||
verb: HttpVerb.Delete,
|
||||
authentication: authentication ?? this.authorizationToken,
|
||||
authentication: authentication ?? this.session?.accessToken.value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -89,9 +96,68 @@ export class HttpService implements HttpServiceInterface {
|
||||
this.updateMetaCallback(response.meta)
|
||||
}
|
||||
|
||||
if (response.status === HttpStatusCode.ExpiredAccessToken) {
|
||||
const isSessionRefreshed = await this.refreshSession()
|
||||
if (!isSessionRefreshed) {
|
||||
return response
|
||||
}
|
||||
|
||||
httpRequest.authentication = this.session?.accessToken.value
|
||||
|
||||
return this.runHttp(httpRequest)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
private async refreshSession(): Promise<boolean> {
|
||||
if (this.session === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const response = (await this.post(joinPaths(this.host, Paths.v1.refreshSession), {
|
||||
access_token: this.session.accessToken.value,
|
||||
refresh_token: this.session.refreshToken.value,
|
||||
})) as SessionRefreshResponse
|
||||
|
||||
if (response.data.error) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (response.meta) {
|
||||
this.updateMetaCallback(response.meta)
|
||||
}
|
||||
|
||||
const accessTokenOrError = SessionToken.create(
|
||||
response.data.session.access_token,
|
||||
response.data.session.access_expiration,
|
||||
)
|
||||
if (accessTokenOrError.isFailed()) {
|
||||
return false
|
||||
}
|
||||
const accessToken = accessTokenOrError.getValue()
|
||||
|
||||
const refreshTokenOrError = SessionToken.create(
|
||||
response.data.session.refresh_token,
|
||||
response.data.session.refresh_expiration,
|
||||
)
|
||||
if (refreshTokenOrError.isFailed()) {
|
||||
return false
|
||||
}
|
||||
const refreshToken = refreshTokenOrError.getValue()
|
||||
|
||||
const sessionOrError = Session.create(accessToken, refreshToken, response.data.session.readonly_access)
|
||||
if (sessionOrError.isFailed()) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.setSession(sessionOrError.getValue())
|
||||
|
||||
this.refreshSessionCallback(this.session)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private createRequestBody(httpRequest: HttpRequest): string | Uint8Array | undefined {
|
||||
if (
|
||||
httpRequest.params !== undefined &&
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Session } from '@standardnotes/domain-core'
|
||||
|
||||
import { HttpRequestParams } from './HttpRequestParams'
|
||||
import { HttpResponse } from './HttpResponse'
|
||||
|
||||
export interface HttpServiceInterface {
|
||||
setHost(host: string): void
|
||||
setAuthorizationToken(authorizationToken: string): void
|
||||
setSession(session: Session): void
|
||||
get(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse>
|
||||
post(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse>
|
||||
put(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Either } from '@standardnotes/common'
|
||||
|
||||
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
|
||||
import { HttpResponse } from '../../Http/HttpResponse'
|
||||
import { SessionRefreshResponseBody } from './SessionRefreshResponseBody'
|
||||
|
||||
export interface SessionRefreshResponse extends HttpResponse {
|
||||
data: Either<SessionRefreshResponseBody, HttpErrorResponseBody>
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export type SessionRefreshResponseBody = {
|
||||
session: {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
access_expiration: number
|
||||
refresh_expiration: number
|
||||
readonly_access: boolean
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
const SessionPaths = {
|
||||
refreshSession: '/v1/sessions/refresh',
|
||||
}
|
||||
|
||||
export const Paths = {
|
||||
v1: {
|
||||
...SessionPaths,
|
||||
},
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import { type Invitation } from '@standardnotes/models'
|
||||
|
||||
import { ApiVersion } from '../../Api'
|
||||
import { HttpServiceInterface } from '../../Http'
|
||||
import { SubscriptionInviteResponse } from '../../Response'
|
||||
import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/SubscriptionInviteAcceptResponse'
|
||||
import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse'
|
||||
import { SubscriptionInviteDeclineResponse } from '../../Response/Subscription/SubscriptionInviteDeclineResponse'
|
||||
import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse'
|
||||
|
||||
import { SubscriptionServer } from './SubscriptionServer'
|
||||
|
||||
describe('SubscriptionServer', () => {
|
||||
let httpService: HttpServiceInterface
|
||||
|
||||
const createServer = () => new SubscriptionServer(httpService)
|
||||
|
||||
beforeEach(() => {
|
||||
httpService = {} as jest.Mocked<HttpServiceInterface>
|
||||
})
|
||||
|
||||
it('should invite a user to a shared subscription', async () => {
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { success: true, sharedSubscriptionInvitationUuid: '1-2-3' },
|
||||
} as jest.Mocked<SubscriptionInviteResponse>)
|
||||
|
||||
const response = await createServer().invite({
|
||||
api: ApiVersion.v0,
|
||||
identifier: '[email protected]',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
sharedSubscriptionInvitationUuid: '1-2-3',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should accept an invite to a shared subscription', async () => {
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<SubscriptionInviteAcceptResponse>)
|
||||
|
||||
const response = await createServer().acceptInvite({
|
||||
api: ApiVersion.v0,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should decline an invite to a shared subscription', async () => {
|
||||
httpService.get = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<SubscriptionInviteDeclineResponse>)
|
||||
|
||||
const response = await createServer().declineInvite({
|
||||
api: ApiVersion.v0,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should cancel an invite to a shared subscription', async () => {
|
||||
httpService.delete = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<SubscriptionInviteCancelResponse>)
|
||||
|
||||
const response = await createServer().cancelInvite({
|
||||
api: ApiVersion.v0,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should list invitations to a shared subscription', async () => {
|
||||
httpService.get = jest.fn().mockReturnValue({
|
||||
data: { invitations: [{} as jest.Mocked<Invitation>] },
|
||||
} as jest.Mocked<SubscriptionInviteListResponse>)
|
||||
|
||||
const response = await createServer().listInvites({
|
||||
api: ApiVersion.v0,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
invitations: [{} as jest.Mocked<Invitation>],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,54 +0,0 @@
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { ApiVersion } from '../../Api'
|
||||
import { HttpServiceInterface } from '../../Http'
|
||||
import { UserDeletionResponse, UserRegistrationResponse } from '../../Response'
|
||||
import { UserServer } from './UserServer'
|
||||
|
||||
describe('UserServer', () => {
|
||||
let httpService: HttpServiceInterface
|
||||
|
||||
const createServer = () => new UserServer(httpService)
|
||||
|
||||
beforeEach(() => {
|
||||
httpService = {} as jest.Mocked<HttpServiceInterface>
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { user: { email: '[email protected]', uuid: '1-2-3' } },
|
||||
} as jest.Mocked<UserRegistrationResponse>)
|
||||
httpService.delete = jest.fn().mockReturnValue({
|
||||
data: { message: 'Success' },
|
||||
} as jest.Mocked<UserDeletionResponse>)
|
||||
})
|
||||
|
||||
it('should register a user', async () => {
|
||||
const response = await createServer().register({
|
||||
password: 'test',
|
||||
api: ApiVersion.v0,
|
||||
email: '[email protected]',
|
||||
ephemeral: false,
|
||||
version: ProtocolVersion.V004,
|
||||
pw_nonce: 'test',
|
||||
identifier: '[email protected]',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
user: {
|
||||
email: '[email protected]',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should delete a user', async () => {
|
||||
const response = await createServer().deleteAccount({
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
message: 'Success',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
import { UserRequestType } from '@standardnotes/common'
|
||||
|
||||
import { HttpServiceInterface } from '../../Http'
|
||||
import { UserRequestResponse } from '../../Response/UserRequest/UserRequestResponse'
|
||||
|
||||
import { UserRequestServer } from './UserRequestServer'
|
||||
|
||||
describe('UserRequestServer', () => {
|
||||
let httpService: HttpServiceInterface
|
||||
|
||||
const createServer = () => new UserRequestServer(httpService)
|
||||
|
||||
beforeEach(() => {
|
||||
httpService = {} as jest.Mocked<HttpServiceInterface>
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<UserRequestResponse>)
|
||||
})
|
||||
|
||||
it('should submit a user request', async () => {
|
||||
const response = await createServer().submitUserRequest({
|
||||
userUuid: '1-2-3',
|
||||
requestType: UserRequestType.ExitDiscount,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,27 +0,0 @@
|
||||
import { HttpServiceInterface } from '../../Http'
|
||||
import { WebSocketConnectionTokenResponse } from '../../Response'
|
||||
|
||||
import { WebSocketServer } from './WebSocketServer'
|
||||
|
||||
describe('WebSocketServer', () => {
|
||||
let httpService: HttpServiceInterface
|
||||
|
||||
const createServer = () => new WebSocketServer(httpService)
|
||||
|
||||
beforeEach(() => {
|
||||
httpService = {} as jest.Mocked<HttpServiceInterface>
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { token: 'foobar' },
|
||||
} as jest.Mocked<WebSocketConnectionTokenResponse>)
|
||||
})
|
||||
|
||||
it('should create a websocket connection token', async () => {
|
||||
const response = await createServer().createConnectionToken({})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
token: 'foobar',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,124 +0,0 @@
|
||||
import { WorkspaceAccessLevel, WorkspaceType } from '@standardnotes/common'
|
||||
|
||||
import { HttpServiceInterface, HttpStatusCode } from '../../Http'
|
||||
import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse'
|
||||
import { WorkspaceInvitationAcceptingResponse } from '../../Response/Workspace/WorkspaceInvitationAcceptingResponse'
|
||||
import { WorkspaceInvitationResponse } from '../../Response/Workspace/WorkspaceInvitationResponse'
|
||||
import { WorkspaceKeyshareInitiatingResponse } from '../../Response/Workspace/WorkspaceKeyshareInitiatingResponse'
|
||||
import { WorkspaceListResponse } from '../../Response/Workspace/WorkspaceListResponse'
|
||||
import { WorkspaceUserListResponse } from '../../Response/Workspace/WorkspaceUserListResponse'
|
||||
|
||||
import { WorkspaceServer } from './WorkspaceServer'
|
||||
|
||||
describe('WorkspaceServer', () => {
|
||||
let httpService: HttpServiceInterface
|
||||
|
||||
const createServer = () => new WorkspaceServer(httpService)
|
||||
|
||||
beforeEach(() => {
|
||||
httpService = {} as jest.Mocked<HttpServiceInterface>
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { uuid: '1-2-3' },
|
||||
} as jest.Mocked<WorkspaceCreationResponse>)
|
||||
})
|
||||
|
||||
it('should create a workspace', async () => {
|
||||
const response = await createServer().createWorkspace({
|
||||
workspaceType: WorkspaceType.Private,
|
||||
encryptedPrivateKey: 'foo',
|
||||
encryptedWorkspaceKey: 'bar',
|
||||
publicKey: 'buzz',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should inivte to a workspace', async () => {
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { uuid: 'i-1-2-3' },
|
||||
} as jest.Mocked<WorkspaceInvitationResponse>)
|
||||
|
||||
const response = await createServer().inviteToWorkspace({
|
||||
inviteeEmail: '[email protected]',
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
accessLevel: WorkspaceAccessLevel.WriteAndRead,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
uuid: 'i-1-2-3',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should accept invitation to a workspace', async () => {
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<WorkspaceInvitationAcceptingResponse>)
|
||||
|
||||
const response = await createServer().acceptInvite({
|
||||
encryptedPrivateKey: 'foo',
|
||||
inviteUuid: 'i-1-2-3',
|
||||
publicKey: 'bar',
|
||||
userUuid: 'u-1-2-3',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should list workspaces', async () => {
|
||||
httpService.get = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCode.Success,
|
||||
data: { ownedWorkspaces: [], joinedWorkspaces: [] },
|
||||
} as jest.Mocked<WorkspaceListResponse>)
|
||||
|
||||
const response = await createServer().listWorkspaces({})
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: { ownedWorkspaces: [], joinedWorkspaces: [] },
|
||||
})
|
||||
})
|
||||
|
||||
it('should list workspace users', async () => {
|
||||
httpService.get = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCode.Success,
|
||||
data: { users: [] },
|
||||
} as jest.Mocked<WorkspaceUserListResponse>)
|
||||
|
||||
const response = await createServer().listWorkspaceUsers({ workspaceUuid: 'w-1-2-3' })
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: { users: [] },
|
||||
})
|
||||
})
|
||||
|
||||
it('should initiate keyshare for user in a workspace', async () => {
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCode.Success,
|
||||
data: { success: true },
|
||||
} as jest.Mocked<WorkspaceKeyshareInitiatingResponse>)
|
||||
|
||||
const response = await createServer().initiateKeyshare({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
userUuid: 'u-1-2-3',
|
||||
encryptedWorkspaceKey: 'foobar',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.10.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
### Features
|
||||
|
||||
* add ability to convert selection in Super to bulleted or numbered list ([be4cc4e](https://github.com/standardnotes/app/commit/be4cc4e605100e0a86de214e548e9ed10e2568aa))
|
||||
|
||||
## [1.9.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/blocks-editor",
|
||||
"version": "1.9.7",
|
||||
"version": "1.10.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
|
||||
+120
-7
@@ -11,7 +11,11 @@ import './index.css';
|
||||
import {$isCodeHighlightNode} from '@lexical/code';
|
||||
import {$isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link';
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import {mergeRegister} from '@lexical/utils';
|
||||
import {
|
||||
mergeRegister,
|
||||
$findMatchingParent,
|
||||
$getNearestNodeOfType,
|
||||
} from '@lexical/utils';
|
||||
import {
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
@@ -20,7 +24,17 @@ import {
|
||||
FORMAT_TEXT_COMMAND,
|
||||
LexicalEditor,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
$isRootOrShadowRoot,
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
} from 'lexical';
|
||||
import {$isHeadingNode} from '@lexical/rich-text';
|
||||
import {
|
||||
INSERT_UNORDERED_LIST_COMMAND,
|
||||
REMOVE_LIST_COMMAND,
|
||||
$isListNode,
|
||||
ListNode,
|
||||
INSERT_ORDERED_LIST_COMMAND,
|
||||
} from '@lexical/list';
|
||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {createPortal} from 'react-dom';
|
||||
|
||||
@@ -36,9 +50,26 @@ import {
|
||||
LinkIcon,
|
||||
SuperscriptIcon,
|
||||
SubscriptIcon,
|
||||
ListBulleted,
|
||||
ListNumbered,
|
||||
} from '@standardnotes/icons';
|
||||
import {IconComponent} from '../../Theme/IconComponent';
|
||||
|
||||
const blockTypeToBlockName = {
|
||||
bullet: 'Bulleted List',
|
||||
check: 'Check List',
|
||||
code: 'Code Block',
|
||||
h1: 'Heading 1',
|
||||
h2: 'Heading 2',
|
||||
h3: 'Heading 3',
|
||||
h4: 'Heading 4',
|
||||
h5: 'Heading 5',
|
||||
h6: 'Heading 6',
|
||||
number: 'Numbered List',
|
||||
paragraph: 'Normal',
|
||||
quote: 'Quote',
|
||||
};
|
||||
|
||||
const IconSize = 15;
|
||||
|
||||
function TextFormatFloatingToolbar({
|
||||
@@ -52,6 +83,8 @@ function TextFormatFloatingToolbar({
|
||||
isStrikethrough,
|
||||
isSubscript,
|
||||
isSuperscript,
|
||||
isBulletedList,
|
||||
isNumberedList,
|
||||
}: {
|
||||
editor: LexicalEditor;
|
||||
anchorElem: HTMLElement;
|
||||
@@ -63,6 +96,8 @@ function TextFormatFloatingToolbar({
|
||||
isSubscript: boolean;
|
||||
isSuperscript: boolean;
|
||||
isUnderline: boolean;
|
||||
isBulletedList: boolean;
|
||||
isNumberedList: boolean;
|
||||
}): JSX.Element {
|
||||
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -74,6 +109,22 @@ function TextFormatFloatingToolbar({
|
||||
}
|
||||
}, [editor, isLink]);
|
||||
|
||||
const formatBulletList = useCallback(() => {
|
||||
if (!isBulletedList) {
|
||||
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
||||
} else {
|
||||
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
||||
}
|
||||
}, [isBulletedList]);
|
||||
|
||||
const formatNumberedList = useCallback(() => {
|
||||
if (!isNumberedList) {
|
||||
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
||||
} else {
|
||||
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
||||
}
|
||||
}, [isNumberedList]);
|
||||
|
||||
const updateTextFormatFloatingToolbar = useCallback(() => {
|
||||
const selection = $getSelection();
|
||||
|
||||
@@ -226,6 +277,22 @@ function TextFormatFloatingToolbar({
|
||||
<LinkIcon />
|
||||
</IconComponent>
|
||||
</button>
|
||||
<button
|
||||
onClick={formatBulletList}
|
||||
className={'popup-item spaced ' + (isBulletedList ? 'active' : '')}
|
||||
aria-label="Insert bulleted list">
|
||||
<IconComponent size={IconSize}>
|
||||
<ListBulleted />
|
||||
</IconComponent>
|
||||
</button>
|
||||
<button
|
||||
onClick={formatNumberedList}
|
||||
className={'popup-item spaced ' + (isNumberedList ? 'active' : '')}
|
||||
aria-label="Insert numbered list">
|
||||
<IconComponent size={IconSize}>
|
||||
<ListNumbered />
|
||||
</IconComponent>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -236,6 +303,7 @@ function useFloatingTextFormatToolbar(
|
||||
editor: LexicalEditor,
|
||||
anchorElem: HTMLElement,
|
||||
): JSX.Element | null {
|
||||
const [activeEditor, setActiveEditor] = useState(editor);
|
||||
const [isText, setIsText] = useState(false);
|
||||
const [isLink, setIsLink] = useState(false);
|
||||
const [isBold, setIsBold] = useState(false);
|
||||
@@ -245,6 +313,8 @@ function useFloatingTextFormatToolbar(
|
||||
const [isSubscript, setIsSubscript] = useState(false);
|
||||
const [isSuperscript, setIsSuperscript] = useState(false);
|
||||
const [isCode, setIsCode] = useState(false);
|
||||
const [blockType, setBlockType] =
|
||||
useState<keyof typeof blockTypeToBlockName>('paragraph');
|
||||
|
||||
const updatePopup = useCallback(() => {
|
||||
editor.getEditorState().read(() => {
|
||||
@@ -270,6 +340,42 @@ function useFloatingTextFormatToolbar(
|
||||
return;
|
||||
}
|
||||
|
||||
const anchorNode = selection.anchor.getNode();
|
||||
let element =
|
||||
anchorNode.getKey() === 'root'
|
||||
? anchorNode
|
||||
: $findMatchingParent(anchorNode, (e) => {
|
||||
const parent = e.getParent();
|
||||
return parent !== null && $isRootOrShadowRoot(parent);
|
||||
});
|
||||
|
||||
if (element === null) {
|
||||
element = anchorNode.getTopLevelElementOrThrow();
|
||||
}
|
||||
|
||||
const elementKey = element.getKey();
|
||||
const elementDOM = activeEditor.getElementByKey(elementKey);
|
||||
|
||||
if (elementDOM !== null) {
|
||||
if ($isListNode(element)) {
|
||||
const parentList = $getNearestNodeOfType<ListNode>(
|
||||
anchorNode,
|
||||
ListNode,
|
||||
);
|
||||
const type = parentList
|
||||
? parentList.getListType()
|
||||
: element.getListType();
|
||||
setBlockType(type);
|
||||
} else {
|
||||
const type = $isHeadingNode(element)
|
||||
? element.getTag()
|
||||
: element.getType();
|
||||
if (type in blockTypeToBlockName) {
|
||||
setBlockType(type as keyof typeof blockTypeToBlockName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const node = getSelectedNode(selection);
|
||||
|
||||
// Update text format
|
||||
@@ -298,14 +404,19 @@ function useFloatingTextFormatToolbar(
|
||||
setIsText(false);
|
||||
}
|
||||
});
|
||||
}, [editor]);
|
||||
}, [editor, activeEditor]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('selectionchange', updatePopup);
|
||||
return () => {
|
||||
document.removeEventListener('selectionchange', updatePopup);
|
||||
};
|
||||
}, [updatePopup]);
|
||||
return editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
(_payload, newEditor) => {
|
||||
setActiveEditor(newEditor);
|
||||
updatePopup();
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
);
|
||||
}, [editor, updatePopup]);
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
@@ -336,6 +447,8 @@ function useFloatingTextFormatToolbar(
|
||||
isSuperscript={isSuperscript}
|
||||
isUnderline={isUnderline}
|
||||
isCode={isCode}
|
||||
isBulletedList={blockType === 'bullet'}
|
||||
isNumberedList={blockType === 'number'}
|
||||
/>,
|
||||
anchorElem,
|
||||
);
|
||||
|
||||
@@ -3,6 +3,56 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.104.27](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.26](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.25](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.24](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **desktop:** Fixed issue where the "Record video" modal would error after stopping recording ([d6fd85e](https://github.com/standardnotes/app/commit/d6fd85e451962e539bada9e207aa0ec65a92f511))
|
||||
|
||||
## [3.104.23](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
default-src 'self' blob:;
|
||||
script-src 'self' 'unsafe-eval';
|
||||
worker-src blob:;
|
||||
connect-src * data:;
|
||||
connect-src * data: blob:;
|
||||
style-src 'unsafe-inline' 'self' http://localhost:* http://127.0.0.1:45653;
|
||||
frame-src * blob:;
|
||||
"
|
||||
|
||||
@@ -4,12 +4,6 @@
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/desktop",
|
||||
"main": "./app/dist/index.js",
|
||||
"version": "3.104.15",
|
||||
"version": "3.104.27",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
@@ -107,6 +107,9 @@
|
||||
"extendInfo": {
|
||||
"NSCameraUsageDescription": "Standard Notes requires access to your camera to enable the Moments feature."
|
||||
},
|
||||
"asarUnpack": [
|
||||
"node_modules/keytar"
|
||||
],
|
||||
"target": [
|
||||
"dmg",
|
||||
"zip"
|
||||
|
||||
@@ -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.19.27](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.19.26](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/encryption",
|
||||
"version": "1.19.26",
|
||||
"version": "1.19.27",
|
||||
"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.27.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.27.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/filepicker",
|
||||
"version": "1.27.3",
|
||||
"version": "1.27.4",
|
||||
"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.14.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.14.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files",
|
||||
"version": "1.14.3",
|
||||
"version": "1.14.4",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.7.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
### Features
|
||||
|
||||
* add ability to convert selection in Super to bulleted or numbered list ([be4cc4e](https://github.com/standardnotes/app/commit/be4cc4e605100e0a86de214e548e9ed10e2568aa))
|
||||
|
||||
## [1.6.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/icons",
|
||||
"version": "1.6.1",
|
||||
"version": "1.7.0",
|
||||
"private": true,
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.83325 4.16667H17.4999V5.83333H5.83325V4.16667ZM5.83325 10.8333V9.16667H17.4999V10.8333H5.83325ZM3.33325 3.75C3.66477 3.75 3.98272 3.8817 4.21714 4.11612C4.45156 4.35054 4.58325 4.66848 4.58325 5C4.58325 5.33152 4.45156 5.64946 4.21714 5.88388C3.98272 6.1183 3.66477 6.25 3.33325 6.25C3.00173 6.25 2.68379 6.1183 2.44937 5.88388C2.21495 5.64946 2.08325 5.33152 2.08325 5C2.08325 4.66848 2.21495 4.35054 2.44937 4.11612C2.68379 3.8817 3.00173 3.75 3.33325 3.75ZM3.33325 8.75C3.66477 8.75 3.98272 8.8817 4.21714 9.11612C4.45156 9.35054 4.58325 9.66848 4.58325 10C4.58325 10.3315 4.45156 10.6495 4.21714 10.8839C3.98272 11.1183 3.66477 11.25 3.33325 11.25C3.00173 11.25 2.68379 11.1183 2.44937 10.8839C2.21495 10.6495 2.08325 10.3315 2.08325 10C2.08325 9.66848 2.21495 9.35054 2.44937 9.11612C2.68379 8.8817 3.00173 8.75 3.33325 8.75ZM5.83325 15.8333V14.1667H17.4999V15.8333H5.83325ZM3.33325 13.75C3.66477 13.75 3.98272 13.8817 4.21714 14.1161C4.45156 14.3505 4.58325 14.6685 4.58325 15C4.58325 15.3315 4.45156 15.6495 4.21714 15.8839C3.98272 16.1183 3.66477 16.25 3.33325 16.25C3.00173 16.25 2.68379 16.1183 2.44937 15.8839C2.21495 15.6495 2.08325 15.3315 2.08325 15C2.08325 14.6685 2.21495 14.3505 2.44937 14.1161C2.68379 13.8817 3.00173 13.75 3.33325 13.75Z"/>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.83325 3.16667H17.4999V4.83333H5.83325V3.16667ZM5.83325 10.8333V9.16667H17.4999V10.8333H5.83325ZM3.33325 2.75C3.66477 2.75 3.98272 2.8817 4.21714 3.11612C4.45156 3.35054 4.58325 3.66848 4.58325 4C4.58325 4.33152 4.45156 4.64946 4.21714 4.88388C3.98272 5.1183 3.66477 5.25 3.33325 5.25C3.00173 5.25 2.68379 5.1183 2.44937 4.88388C2.21495 4.64946 2.08325 4.33152 2.08325 4C2.08325 3.66848 2.21495 3.35054 2.44937 3.11612C2.68379 2.8817 3.00173 2.75 3.33325 2.75ZM3.33325 8.75C3.66477 8.75 3.98272 8.8817 4.21714 9.11612C4.45156 9.35054 4.58325 9.66848 4.58325 10C4.58325 10.3315 4.45156 10.6495 4.21714 10.8839C3.98272 11.1183 3.66477 11.25 3.33325 11.25C3.00173 11.25 2.68379 11.1183 2.44937 10.8839C2.21495 10.6495 2.08325 10.3315 2.08325 10C2.08325 9.66848 2.21495 9.35054 2.44937 9.11612C2.68379 8.8817 3.00173 8.75 3.33325 8.75ZM5.83325 16.8333V15.1667H17.4999V16.8333H5.83325ZM3.33325 14.75C3.66477 14.75 3.98272 14.8817 4.21714 15.1161C4.45156 15.3505 4.58325 15.6685 4.58325 16C4.58325 16.3315 4.45156 16.6495 4.21714 16.8839C3.98272 17.1183 3.66477 17.25 3.33325 17.25C3.00173 17.25 2.68379 17.1183 2.44937 16.8839C2.21495 16.6495 2.08325 16.3315 2.08325 16C2.08325 15.6685 2.21495 15.3505 2.44937 15.1161C2.68379 14.8817 3.00173 14.75 3.33325 14.75Z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.83325 3.16675H17.4999V4.83341H5.83325V3.16675ZM5.83325 10.8334V9.16675H17.4999V10.8334H5.83325ZM5.83325 16.8334V15.1667H17.4999V16.8334H5.83325Z" />
|
||||
<path d="M4.02415 2.36364V6H3.03693V3.27273H3.01562L2.22017 3.74858V2.91051L3.11506 2.36364H4.02415Z" />
|
||||
<path d="M2.24148 12V11.2898L3.59801 10.1676C3.68797 10.093 3.76491 10.0232 3.82884 9.9581C3.89394 9.89181 3.94366 9.82375 3.97798 9.75391C4.01349 9.68407 4.03125 9.60653 4.03125 9.52131C4.03125 9.42779 4.01113 9.34789 3.97088 9.28161C3.93182 9.21532 3.87737 9.16442 3.80753 9.12891C3.73769 9.09221 3.6572 9.07386 3.56605 9.07386C3.47491 9.07386 3.39441 9.09221 3.32457 9.12891C3.25592 9.1656 3.20265 9.21946 3.16477 9.29048C3.12689 9.36151 3.10795 9.44792 3.10795 9.54972H2.17045C2.17045 9.29403 2.22786 9.07386 2.34268 8.8892C2.4575 8.70455 2.61967 8.5625 2.82919 8.46307C3.03871 8.36364 3.28433 8.31392 3.56605 8.31392C3.85724 8.31392 4.10938 8.36068 4.32244 8.45419C4.5367 8.54652 4.70182 8.67673 4.81783 8.84482C4.93501 9.0129 4.99361 9.20999 4.99361 9.43608C4.99361 9.57576 4.96461 9.71484 4.90661 9.85334C4.8486 9.99065 4.74444 10.1428 4.59411 10.3097C4.44377 10.4766 4.23011 10.6754 3.95312 10.9062L3.61222 11.1903V11.2116H5.03267V12H2.24148Z" />
|
||||
<path d="M3.66193 18.0497C3.37547 18.0497 3.12098 18.0006 2.89844 17.9023C2.67708 17.8029 2.50308 17.6662 2.37642 17.4922C2.24976 17.3182 2.18584 17.1181 2.18466 16.892H3.17898C3.18016 16.9619 3.20147 17.0246 3.2429 17.0803C3.28551 17.1347 3.34351 17.1773 3.4169 17.2081C3.49029 17.2389 3.57434 17.2543 3.66903 17.2543C3.76018 17.2543 3.84067 17.2383 3.91051 17.2063C3.98035 17.1732 4.0348 17.1276 4.07386 17.0696C4.11293 17.0116 4.13187 16.9453 4.13068 16.8707C4.13187 16.7973 4.10938 16.7322 4.06321 16.6754C4.01823 16.6186 3.9549 16.5742 3.87322 16.5423C3.79155 16.5103 3.69744 16.4943 3.59091 16.4943H3.22159V15.8125H3.59091C3.68916 15.8125 3.77557 15.7965 3.85014 15.7646C3.9259 15.7326 3.98449 15.6882 4.02592 15.6314C4.06854 15.5746 4.08925 15.5095 4.08807 15.4361C4.08925 15.3651 4.07209 15.3023 4.03658 15.2479C4.00107 15.1934 3.95135 15.1508 3.88743 15.12C3.82469 15.0893 3.75189 15.0739 3.66903 15.0739C3.57907 15.0739 3.49858 15.0898 3.42756 15.1218C3.35772 15.1538 3.30268 15.1982 3.26243 15.255C3.22218 15.3118 3.20147 15.3769 3.20028 15.4503H2.25568C2.25687 15.2277 2.31783 15.0312 2.43857 14.8608C2.56049 14.6903 2.72739 14.5566 2.93928 14.4595C3.15116 14.3625 3.39441 14.3139 3.66903 14.3139C3.93774 14.3139 4.17507 14.3595 4.38104 14.4506C4.58819 14.5418 4.74976 14.6673 4.86577 14.8271C4.98295 14.9857 5.04096 15.1674 5.03977 15.3722C5.04214 15.5758 4.9723 15.7427 4.83026 15.8729C4.68939 16.0031 4.51065 16.08 4.29403 16.1037V16.1321C4.58759 16.1641 4.80836 16.254 4.95632 16.402C5.10429 16.5488 5.17708 16.7334 5.17472 16.956C5.1759 17.169 5.11198 17.3578 4.98295 17.5224C4.85511 17.6869 4.67696 17.8159 4.44851 17.9094C4.22124 18.003 3.95904 18.0497 3.66193 18.0497Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -1,3 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.9999 8.6665C11.5579 8.6665 11.134 8.49091 10.8214 8.17835C10.5088 7.86579 10.3333 7.44187 10.3333 6.99984C10.3333 6.55781 10.5088 6.13389 10.8214 5.82133C11.134 5.50877 11.5579 5.33317 11.9999 5.33317C12.4419 5.33317 12.8659 5.50877 13.1784 5.82133C13.491 6.13389 13.6666 6.55781 13.6666 6.99984C13.6666 7.44187 13.491 7.86579 13.1784 8.17835C12.8659 8.49091 12.4419 8.6665 11.9999 8.6665ZM11.9999 13.6665C11.5579 13.6665 11.134 13.4909 10.8214 13.1783C10.5088 12.8658 10.3333 12.4419 10.3333 11.9998C10.3333 11.5578 10.5088 11.1339 10.8214 10.8213C11.134 10.5088 11.5579 10.3332 11.9999 10.3332C12.4419 10.3332 12.8659 10.5088 13.1784 10.8213C13.491 11.1339 13.6666 11.5578 13.6666 11.9998C13.6666 12.4419 13.491 12.8658 13.1784 13.1783C12.8659 13.4909 12.4419 13.6665 11.9999 13.6665ZM11.9999 18.6665C11.5579 18.6665 11.134 18.4909 10.8214 18.1783C10.5088 17.8658 10.3333 17.4419 10.3333 16.9998C10.3333 16.5578 10.5088 16.1339 10.8214 15.8213C11.134 15.5088 11.5579 15.3332 11.9999 15.3332C12.4419 15.3332 12.8659 15.5088 13.1784 15.8213C13.491 16.1339 13.6666 16.5578 13.6666 16.9998C13.6666 17.4419 13.491 17.8658 13.1784 18.1783C12.8659 18.4909 12.4419 18.6665 11.9999 18.6665Z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -112,6 +112,7 @@ import LineWidthIcon from './ic-line-width.svg'
|
||||
import LinkIcon from './ic-link.svg'
|
||||
import LinkOffIcon from './ic-link-off.svg'
|
||||
import ListBulleted from './ic-list-bulleted.svg'
|
||||
import ListNumbered from './ic-list-numbered.svg'
|
||||
import ListedFilledIcon from './ic-listed-filled.svg'
|
||||
import ListedIcon from './ic-listed.svg'
|
||||
import LockFilledIcon from './ic-lock-filled.svg'
|
||||
@@ -318,6 +319,7 @@ export {
|
||||
ListBulleted,
|
||||
ListedFilledIcon,
|
||||
ListedIcon,
|
||||
ListNumbered,
|
||||
LockFilledIcon,
|
||||
LockIcon,
|
||||
MarkdownIcon,
|
||||
|
||||
@@ -3,6 +3,46 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.49.24](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.49.23](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.49.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.49.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.49.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.49.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.49.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.49.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.49.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.49.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.49.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/mobile",
|
||||
"version": "3.49.14",
|
||||
"version": "3.49.24",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
|
||||
@@ -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.39.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.39.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/models",
|
||||
"version": "1.39.2",
|
||||
"version": "1.39.3",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,54 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.50](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.49](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.48](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.47](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.46](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.45](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.44](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.43](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.42](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.41](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.40](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.39](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.38](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/releases",
|
||||
"version": "1.4.38",
|
||||
"version": "1.4.50",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/releases.json",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.50.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.50.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.50.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/services",
|
||||
"version": "1.50.4",
|
||||
"version": "1.50.6",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [2.156.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **snjs:** refreshing sessions ([#2106](https://github.com/standardnotes/app/issues/2106)) ([bb4f1ff](https://github.com/standardnotes/app/commit/bb4f1ff0990eb5a6f64e76a699fb95540cffdbb4))
|
||||
|
||||
## [2.156.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **snjs:** remove @newrelic/native-metrics by upgrading @standardnotes/settings ([7f41564](https://github.com/standardnotes/app/commit/7f415643283f418886a641deb7539f1e256e6a3d))
|
||||
|
||||
## [2.156.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
# [2.156.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -82,6 +82,9 @@ import { SNLog } from '../Log'
|
||||
import { ChallengeResponse, ListedClientInterface } from '../Services'
|
||||
import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions'
|
||||
import { ApplicationOptionsDefaults } from './Options/Defaults'
|
||||
import { LegacySession, MapperInterface, Session } from '@standardnotes/domain-core'
|
||||
import { SessionStorageMapper } from '@Lib/Services/Mapping/SessionStorageMapper'
|
||||
import { LegacySessionStorageMapper } from '@Lib/Services/Mapping/LegacySessionStorageMapper'
|
||||
|
||||
/** How often to automatically sync, in milliseconds */
|
||||
const DEFAULT_AUTO_SYNC_INTERVAL = 30_000
|
||||
@@ -154,6 +157,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
private integrityService!: ExternalServices.IntegrityService
|
||||
private statusService!: ExternalServices.StatusService
|
||||
private filesBackupService?: FilesBackupService
|
||||
private declare sessionStorageMapper: MapperInterface<Session, Record<string, unknown>>
|
||||
private declare legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>
|
||||
|
||||
private internalEventBus!: ExternalServices.InternalEventBusInterface
|
||||
|
||||
@@ -1078,6 +1083,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
}
|
||||
|
||||
private constructServices() {
|
||||
this.createMappers()
|
||||
this.createPayloadManager()
|
||||
this.createItemManager()
|
||||
this.createDiskStorageManager()
|
||||
@@ -1169,6 +1175,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
;(this.mutatorService as unknown) = undefined
|
||||
;(this.filesBackupService as unknown) = undefined
|
||||
;(this.statusService as unknown) = undefined
|
||||
;(this.sessionStorageMapper as unknown) = undefined
|
||||
;(this.legacySessionStorageMapper as unknown) = undefined
|
||||
|
||||
this.services = []
|
||||
}
|
||||
@@ -1289,6 +1297,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
environment: this.environment,
|
||||
identifier: this.identifier,
|
||||
internalEventBus: this.internalEventBus,
|
||||
legacySessionStorageMapper: this.legacySessionStorageMapper,
|
||||
})
|
||||
this.services.push(this.migrationService)
|
||||
}
|
||||
@@ -1335,6 +1344,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
this.options.defaultHost,
|
||||
this.inMemoryStore,
|
||||
this.options.crypto,
|
||||
this.sessionStorageMapper,
|
||||
this.legacySessionStorageMapper,
|
||||
this.internalEventBus,
|
||||
)
|
||||
this.services.push(this.apiService)
|
||||
@@ -1419,9 +1430,15 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
this.options.appVersion,
|
||||
SnjsVersion,
|
||||
this.apiService.processMetaObject.bind(this.apiService),
|
||||
this.apiService.setSession.bind(this.apiService),
|
||||
)
|
||||
}
|
||||
|
||||
private createMappers() {
|
||||
this.sessionStorageMapper = new SessionStorageMapper()
|
||||
this.legacySessionStorageMapper = new LegacySessionStorageMapper()
|
||||
}
|
||||
|
||||
private createPayloadManager() {
|
||||
this.payloadManager = new InternalServices.PayloadManager(this.internalEventBus)
|
||||
this.services.push(this.payloadManager)
|
||||
@@ -1497,6 +1514,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
this.challengeService,
|
||||
this.webSocketsService,
|
||||
this.httpService,
|
||||
this.sessionStorageMapper,
|
||||
this.legacySessionStorageMapper,
|
||||
this.internalEventBus,
|
||||
)
|
||||
this.serviceObservers.push(
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SNSessionManager } from '../Services/Session/SessionManager'
|
||||
import { ApplicationIdentifier } from '@standardnotes/common'
|
||||
import { ItemManager } from '@Lib/Services/Items/ItemManager'
|
||||
import { ChallengeService, SNSingletonManager, SNFeaturesService, DiskStorageService } from '@Lib/Services'
|
||||
import { LegacySession, MapperInterface } from '@standardnotes/domain-core'
|
||||
|
||||
export type MigrationServices = {
|
||||
protocolService: EncryptionService
|
||||
@@ -17,5 +18,6 @@ export type MigrationServices = {
|
||||
featuresService: SNFeaturesService
|
||||
environment: Environment
|
||||
identifier: ApplicationIdentifier
|
||||
legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>
|
||||
internalEventBus: InternalEventBusInterface
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AnyKeyParamsContent, ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||
import { JwtSession } from '../../Services/Session/Sessions/JwtSession'
|
||||
import { Migration } from '@Lib/Migrations/Migration'
|
||||
import { MigrationServices } from '../MigrationServices'
|
||||
import { PreviousSnjsVersion2_0_0 } from '../../Version'
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
PayloadTimestampDefaults,
|
||||
} from '@standardnotes/models'
|
||||
import { isMobileDevice } from '@standardnotes/services'
|
||||
import { LegacySession } from '@standardnotes/domain-core'
|
||||
|
||||
interface LegacyStorageContent extends Models.ItemContent {
|
||||
storage: unknown
|
||||
@@ -673,8 +673,13 @@ export class Migration2_0_0 extends Migration {
|
||||
}
|
||||
}
|
||||
|
||||
const session = new JwtSession(currentToken)
|
||||
this.services.storageService.setValue(Services.StorageKey.Session, session)
|
||||
const sessionOrError = LegacySession.create(currentToken)
|
||||
if (!sessionOrError.isFailed()) {
|
||||
this.services.storageService.setValue(
|
||||
Services.StorageKey.Session,
|
||||
this.services.legacySessionStorageMapper.toProjection(sessionOrError.getValue()),
|
||||
)
|
||||
}
|
||||
|
||||
/** Server has to be migrated separately on mobile */
|
||||
if (isEnvironmentMobile(this.services.environment)) {
|
||||
|
||||
@@ -34,23 +34,22 @@ import {
|
||||
API_MESSAGE_TOKEN_REFRESH_IN_PROGRESS,
|
||||
} from '@standardnotes/services'
|
||||
import { FilesApiInterface } from '@standardnotes/files'
|
||||
|
||||
import { ServerSyncPushContextualPayload, SNFeatureRepo, FileContent } from '@standardnotes/models'
|
||||
import * as Responses from '@standardnotes/responses'
|
||||
import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
|
||||
import { HttpResponseMeta } from '@standardnotes/api'
|
||||
import { SNRootKeyParams } from '@standardnotes/encryption'
|
||||
import { ApiEndpointParam, ClientDisplayableError, CreateValetTokenPayload } from '@standardnotes/responses'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
|
||||
import { HttpParams, HttpRequest, HttpVerb, SNHttpService } from './HttpService'
|
||||
import { isUrlFirstParty, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts'
|
||||
import { Paths } from './Paths'
|
||||
import { Session } from '../Session/Sessions/Session'
|
||||
import { TokenSession } from '../Session/Sessions/TokenSession'
|
||||
import { DiskStorageService } from '../Storage/DiskStorageService'
|
||||
import { HttpResponseMeta } from '@standardnotes/api'
|
||||
import { UuidString } from '../../Types/UuidString'
|
||||
import merge from 'lodash/merge'
|
||||
import { SettingsServerInterface } from '../Settings/SettingsServerInterface'
|
||||
import { Strings } from '@Lib/Strings'
|
||||
import { SNRootKeyParams } from '@standardnotes/encryption'
|
||||
import { ApiEndpointParam, ClientDisplayableError, CreateValetTokenPayload } from '@standardnotes/responses'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
|
||||
/** Legacy api version field to be specified in params when calling v0 APIs. */
|
||||
const V0_API_VERSION = '20200115'
|
||||
@@ -66,7 +65,7 @@ export class SNApiService
|
||||
ItemsServerInterface,
|
||||
SettingsServerInterface
|
||||
{
|
||||
private session?: Session
|
||||
private session: Session | LegacySession | null
|
||||
public user?: Responses.User
|
||||
private registering = false
|
||||
private authenticating = false
|
||||
@@ -81,16 +80,20 @@ export class SNApiService
|
||||
private host: string,
|
||||
private inMemoryStore: KeyValueStoreInterface<string>,
|
||||
private crypto: PureCryptoInterface,
|
||||
private sessionStorageMapper: MapperInterface<Session, Record<string, unknown>>,
|
||||
private legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
|
||||
this.session = null
|
||||
}
|
||||
|
||||
override deinit(): void {
|
||||
;(this.httpService as unknown) = undefined
|
||||
;(this.storageService as unknown) = undefined
|
||||
this.invalidSessionObserver = undefined
|
||||
this.session = undefined
|
||||
this.session = null
|
||||
super.deinit()
|
||||
}
|
||||
|
||||
@@ -145,14 +148,21 @@ export class SNApiService
|
||||
return this.filesHost
|
||||
}
|
||||
|
||||
public setSession(session: Session, persist = true): void {
|
||||
public setSession(session: Session | LegacySession, persist = true): void {
|
||||
this.session = session
|
||||
if (persist) {
|
||||
this.storageService.setValue(StorageKey.Session, session)
|
||||
let sessionProjection: Record<string, unknown>
|
||||
if (session instanceof Session) {
|
||||
sessionProjection = this.sessionStorageMapper.toProjection(session)
|
||||
} else {
|
||||
sessionProjection = this.legacySessionStorageMapper.toProjection(session)
|
||||
}
|
||||
|
||||
this.storageService.setValue(StorageKey.Session, sessionProjection)
|
||||
}
|
||||
}
|
||||
|
||||
public getSession(): Session | undefined {
|
||||
public getSession(): Session | LegacySession | null {
|
||||
return this.session
|
||||
}
|
||||
|
||||
@@ -252,7 +262,7 @@ export class SNApiService
|
||||
fallbackErrorMessage: API_MESSAGE_GENERIC_INVALID_LOGIN,
|
||||
params,
|
||||
/** A session is optional here, if valid, endpoint bypasses 2FA and returns additional params */
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -289,7 +299,7 @@ export class SNApiService
|
||||
|
||||
signOut(): Promise<Responses.SignOutResponse> {
|
||||
const url = joinPaths(this.host, Paths.v1.signOut)
|
||||
return this.httpService.postAbsolute(url, undefined, this.session?.authorizationValue).catch((errorResponse) => {
|
||||
return this.httpService.postAbsolute(url, undefined, this.getSessionAccessToken()).catch((errorResponse) => {
|
||||
return errorResponse
|
||||
}) as Promise<Responses.SignOutResponse>
|
||||
}
|
||||
@@ -317,7 +327,7 @@ export class SNApiService
|
||||
...parameters.newKeyParams.getPortableValue(),
|
||||
})
|
||||
const response = await this.httpService
|
||||
.putAbsolute(url, params, this.session?.authorizationValue)
|
||||
.putAbsolute(url, params, this.getSessionAccessToken())
|
||||
.catch(async (errorResponse) => {
|
||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||
return this.refreshSessionThenRetryRequest({
|
||||
@@ -353,7 +363,7 @@ export class SNApiService
|
||||
[ApiEndpointParam.SyncDlLimit]: limit,
|
||||
})
|
||||
const response = await this.httpService
|
||||
.postAbsolute(url, params, this.session?.authorizationValue)
|
||||
.postAbsolute(url, params, this.getSessionAccessToken())
|
||||
.catch<Responses.HttpResponse>(async (errorResponse) => {
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||
@@ -378,7 +388,7 @@ export class SNApiService
|
||||
return this.httpService
|
||||
.runHttp({
|
||||
...httpRequest,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
.catch((errorResponse) => {
|
||||
return errorResponse
|
||||
@@ -393,16 +403,54 @@ export class SNApiService
|
||||
}
|
||||
this.refreshingSession = true
|
||||
const url = joinPaths(this.host, Paths.v1.refreshSession)
|
||||
const session = this.session as TokenSession
|
||||
const session = this.session as Session
|
||||
const params = this.params({
|
||||
access_token: session.accessToken,
|
||||
refresh_token: session.refreshToken,
|
||||
access_token: session.accessToken.value,
|
||||
refresh_token: session.refreshToken.value,
|
||||
})
|
||||
const result = await this.httpService
|
||||
.postAbsolute(url, params)
|
||||
.then(async (response) => {
|
||||
const session = TokenSession.FromApiResponse(response as Responses.SessionRenewalResponse)
|
||||
await this.setSession(session)
|
||||
const sessionRenewalResponse = response as Responses.SessionRenewalResponse
|
||||
if (
|
||||
sessionRenewalResponse.error ||
|
||||
sessionRenewalResponse.data?.error ||
|
||||
!sessionRenewalResponse.data.session
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const accessTokenOrError = SessionToken.create(
|
||||
sessionRenewalResponse.data.session.access_token,
|
||||
sessionRenewalResponse.data.session.access_expiration,
|
||||
)
|
||||
if (accessTokenOrError.isFailed()) {
|
||||
return null
|
||||
}
|
||||
const accessToken = accessTokenOrError.getValue()
|
||||
|
||||
const refreshTokenOrError = SessionToken.create(
|
||||
sessionRenewalResponse.data.session.refresh_token,
|
||||
sessionRenewalResponse.data.session.refresh_expiration,
|
||||
)
|
||||
if (refreshTokenOrError.isFailed()) {
|
||||
return null
|
||||
}
|
||||
const refreshToken = refreshTokenOrError.getValue()
|
||||
|
||||
const sessionOrError = Session.create(
|
||||
accessToken,
|
||||
refreshToken,
|
||||
sessionRenewalResponse.data.session.readonly_access,
|
||||
)
|
||||
if (sessionOrError.isFailed()) {
|
||||
return null
|
||||
}
|
||||
const session = sessionOrError.getValue()
|
||||
|
||||
this.session = session
|
||||
|
||||
this.setSession(session)
|
||||
this.processResponse(response)
|
||||
return response
|
||||
})
|
||||
@@ -411,6 +459,11 @@ export class SNApiService
|
||||
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_TOKEN_REFRESH_FAIL)
|
||||
})
|
||||
this.refreshingSession = false
|
||||
|
||||
if (result === null) {
|
||||
return this.createErrorResponse(API_MESSAGE_INVALID_SESSION)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -421,7 +474,7 @@ export class SNApiService
|
||||
}
|
||||
const url = joinPaths(this.host, Paths.v1.sessions)
|
||||
const response = await this.httpService
|
||||
.getAbsolute(url, {}, this.session?.authorizationValue)
|
||||
.getAbsolute(url, {}, this.getSessionAccessToken())
|
||||
.catch(async (errorResponse) => {
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||
@@ -444,7 +497,7 @@ export class SNApiService
|
||||
}
|
||||
const url = joinPaths(this.host, <string>Paths.v1.session(sessionId))
|
||||
const response: Responses.RevisionListResponse | Responses.HttpResponse = await this.httpService
|
||||
.deleteAbsolute(url, { uuid: sessionId }, this.session?.authorizationValue)
|
||||
.deleteAbsolute(url, { uuid: sessionId }, this.getSessionAccessToken())
|
||||
.catch((error: Responses.HttpResponse) => {
|
||||
const errorResponse = error as Responses.HttpResponse
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
@@ -467,7 +520,7 @@ export class SNApiService
|
||||
}
|
||||
const url = joinPaths(this.host, Paths.v1.itemRevisions(itemId))
|
||||
const response: Responses.RevisionListResponse | Responses.HttpResponse = await this.httpService
|
||||
.getAbsolute(url, undefined, this.session?.authorizationValue)
|
||||
.getAbsolute(url, undefined, this.getSessionAccessToken())
|
||||
.catch((errorResponse: Responses.HttpResponse) => {
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||
@@ -492,7 +545,7 @@ export class SNApiService
|
||||
}
|
||||
const url = joinPaths(this.host, Paths.v1.itemRevision(itemId, entry.uuid))
|
||||
const response: Responses.SingleRevisionResponse | Responses.HttpResponse = await this.httpService
|
||||
.getAbsolute(url, undefined, this.session?.authorizationValue)
|
||||
.getAbsolute(url, undefined, this.getSessionAccessToken())
|
||||
.catch((errorResponse: Responses.HttpResponse) => {
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||
@@ -510,7 +563,7 @@ export class SNApiService
|
||||
async getUserFeatures(userUuid: UuidString): Promise<Responses.HttpResponse | Responses.UserFeaturesResponse> {
|
||||
const url = joinPaths(this.host, Paths.v1.userFeatures(userUuid))
|
||||
const response = await this.httpService
|
||||
.getAbsolute(url, undefined, this.session?.authorizationValue)
|
||||
.getAbsolute(url, undefined, this.getSessionAccessToken())
|
||||
.catch((errorResponse: Responses.HttpResponse) => {
|
||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||
@@ -550,7 +603,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Get,
|
||||
url: joinPaths(this.host, Paths.v1.settings(userUuid)),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -568,7 +621,7 @@ export class SNApiService
|
||||
return this.tokenRefreshableRequest<Responses.UpdateSettingResponse>({
|
||||
verb: HttpVerb.Put,
|
||||
url: joinPaths(this.host, Paths.v1.settings(userUuid)),
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_UPDATE_SETTINGS,
|
||||
params,
|
||||
})
|
||||
@@ -578,7 +631,7 @@ export class SNApiService
|
||||
return await this.tokenRefreshableRequest<Responses.GetSettingResponse>({
|
||||
verb: HttpVerb.Get,
|
||||
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName.toLowerCase() as SettingName)),
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
||||
})
|
||||
}
|
||||
@@ -593,7 +646,7 @@ export class SNApiService
|
||||
this.host,
|
||||
Paths.v1.subscriptionSetting(userUuid, settingName.toLowerCase() as SubscriptionSettingName),
|
||||
),
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
||||
})
|
||||
}
|
||||
@@ -602,7 +655,7 @@ export class SNApiService
|
||||
return this.tokenRefreshableRequest<Responses.DeleteSettingResponse>({
|
||||
verb: HttpVerb.Delete,
|
||||
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName)),
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_UPDATE_SETTINGS,
|
||||
})
|
||||
}
|
||||
@@ -616,7 +669,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Delete,
|
||||
url,
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_DELETE_REVISION,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
return response
|
||||
}
|
||||
@@ -635,7 +688,7 @@ export class SNApiService
|
||||
const response = await this.tokenRefreshableRequest({
|
||||
verb: HttpVerb.Get,
|
||||
url,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_SUBSCRIPTION_INFO,
|
||||
})
|
||||
return response
|
||||
@@ -658,7 +711,7 @@ export class SNApiService
|
||||
const response: Responses.HttpResponse | Responses.PostSubscriptionTokensResponse = await this.request({
|
||||
verb: HttpVerb.Post,
|
||||
url,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_ACCESS_PURCHASE,
|
||||
})
|
||||
return (response as Responses.PostSubscriptionTokensResponse).data?.token
|
||||
@@ -706,7 +759,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Post,
|
||||
url: joinPaths(this.host, Paths.v1.listedRegistration(this.user.uuid)),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_LISTED_REGISTRATION,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -725,7 +778,7 @@ export class SNApiService
|
||||
const response = await this.tokenRefreshableRequest<Responses.CreateValetTokenResponse>({
|
||||
verb: HttpVerb.Post,
|
||||
url: url,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
fallbackErrorMessage: API_MESSAGE_FAILED_CREATE_FILE_TOKEN,
|
||||
params,
|
||||
})
|
||||
@@ -860,7 +913,7 @@ export class SNApiService
|
||||
integrityPayloads,
|
||||
},
|
||||
fallbackErrorMessage: API_MESSAGE_GENERIC_INTEGRITY_CHECK_FAIL,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -869,7 +922,7 @@ export class SNApiService
|
||||
verb: HttpVerb.Get,
|
||||
url: joinPaths(this.host, Paths.v1.getSingleItem(itemUuid)),
|
||||
fallbackErrorMessage: API_MESSAGE_GENERIC_SINGLE_ITEM_SYNC_FAIL,
|
||||
authentication: this.session?.authorizationValue,
|
||||
authentication: this.getSessionAccessToken(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -890,6 +943,18 @@ export class SNApiService
|
||||
}
|
||||
}
|
||||
|
||||
private getSessionAccessToken(): string | undefined {
|
||||
if (!this.session) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (this.session instanceof Session) {
|
||||
return this.session.accessToken.value
|
||||
}
|
||||
|
||||
return this.session.accessToken
|
||||
}
|
||||
|
||||
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||
return Promise.resolve({
|
||||
api: {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from './ApiService'
|
||||
export * from './HttpService'
|
||||
export * from './Paths'
|
||||
export * from '../Session/Sessions/Session'
|
||||
export * from '../Session/SessionManager'
|
||||
export * from './WebsocketsService'
|
||||
|
||||
@@ -290,7 +290,7 @@ export class ChallengeService extends AbstractService implements ChallengeServic
|
||||
if (operation.isFinished()) {
|
||||
this.deleteChallengeOperation(operation)
|
||||
|
||||
const observers = this.challengeObservers[challenge.id]
|
||||
const observers = this.challengeObservers[challenge.id] || []
|
||||
observers.forEach(clearChallengeObserver)
|
||||
observers.length = 0
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { LegacySession, MapperInterface } from '@standardnotes/domain-core'
|
||||
|
||||
export class LegacySessionStorageMapper implements MapperInterface<LegacySession, Record<string, unknown>> {
|
||||
toDomain(projection: Record<string, unknown>): LegacySession {
|
||||
const { jwt } = projection
|
||||
|
||||
const legacySessionOrError = LegacySession.create(jwt as string)
|
||||
if (legacySessionOrError.isFailed()) {
|
||||
throw new Error(legacySessionOrError.getError())
|
||||
}
|
||||
|
||||
return legacySessionOrError.getValue()
|
||||
}
|
||||
|
||||
toProjection(domain: LegacySession): Record<string, unknown> {
|
||||
return {
|
||||
jwt: domain.accessToken,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
|
||||
|
||||
export class SessionStorageMapper implements MapperInterface<Session, Record<string, unknown>> {
|
||||
toDomain(projection: Record<string, unknown>): Session {
|
||||
const accessTokenOrError = SessionToken.create(
|
||||
projection.accessToken as string,
|
||||
projection.accessExpiration as number,
|
||||
)
|
||||
if (accessTokenOrError.isFailed()) {
|
||||
throw new Error(accessTokenOrError.getError())
|
||||
}
|
||||
const accessToken = accessTokenOrError.getValue()
|
||||
|
||||
const refreshTokenOrError = SessionToken.create(
|
||||
projection.refreshToken as string,
|
||||
projection.refreshExpiration as number,
|
||||
)
|
||||
if (refreshTokenOrError.isFailed()) {
|
||||
throw new Error(refreshTokenOrError.getError())
|
||||
}
|
||||
const refreshToken = refreshTokenOrError.getValue()
|
||||
|
||||
const session = Session.create(accessToken, refreshToken, projection.readonlyAccess as boolean)
|
||||
if (session.isFailed()) {
|
||||
throw new Error(session.getError())
|
||||
}
|
||||
|
||||
return session.getValue()
|
||||
}
|
||||
|
||||
toProjection(domain: Session): Record<string, unknown> {
|
||||
return {
|
||||
accessToken: domain.accessToken.value,
|
||||
refreshToken: domain.refreshToken.value,
|
||||
accessExpiration: domain.accessToken.expiresAt,
|
||||
refreshExpiration: domain.refreshToken.expiresAt,
|
||||
readonlyAccess: domain.isReadOnly(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,21 +27,19 @@ import { Base64String } from '@standardnotes/sncrypto-common'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { CopyPayloadWithContentOverride } from '@standardnotes/models'
|
||||
import { isNullOrUndefined } from '@standardnotes/utils'
|
||||
import { JwtSession } from './Sessions/JwtSession'
|
||||
import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
|
||||
import { KeyParamsFromApiResponse, SNRootKeyParams, SNRootKey, CreateNewRootKey } from '@standardnotes/encryption'
|
||||
import * as Responses from '@standardnotes/responses'
|
||||
import { Subscription } from '@standardnotes/security'
|
||||
import * as Common from '@standardnotes/common'
|
||||
|
||||
import { RemoteSession, RawStorageValue } from './Sessions/Types'
|
||||
import { Session } from './Sessions/Session'
|
||||
import { SessionFromRawStorageValue } from './Sessions/Generator'
|
||||
import { ShareToken } from './ShareToken'
|
||||
import { SNApiService } from '../Api/ApiService'
|
||||
import { DiskStorageService } from '../Storage/DiskStorageService'
|
||||
import { SNWebSocketsService } from '../Api/WebsocketsService'
|
||||
import { Strings } from '@Lib/Strings'
|
||||
import { Subscription } from '@standardnotes/security'
|
||||
import { TokenSession } from './Sessions/TokenSession'
|
||||
import { UuidString } from '@Lib/Types/UuidString'
|
||||
import * as Common from '@standardnotes/common'
|
||||
import * as Responses from '@standardnotes/responses'
|
||||
import { ChallengeService } from '../Challenge'
|
||||
import {
|
||||
ApiCallError,
|
||||
@@ -82,6 +80,8 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
private challengeService: ChallengeService,
|
||||
private webSocketsService: SNWebSocketsService,
|
||||
private httpService: HttpServiceInterface,
|
||||
private sessionStorageMapper: MapperInterface<Session, Record<string, unknown>>,
|
||||
private legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
@@ -122,13 +122,23 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
|
||||
const rawSession = this.diskStorageService.getValue<RawStorageValue>(StorageKey.Session)
|
||||
if (rawSession) {
|
||||
const session = SessionFromRawStorageValue(rawSession)
|
||||
this.setSession(session, false)
|
||||
try {
|
||||
const session =
|
||||
'jwt' in rawSession
|
||||
? this.legacySessionStorageMapper.toDomain(rawSession)
|
||||
: this.sessionStorageMapper.toDomain(rawSession)
|
||||
|
||||
this.setSession(session, false)
|
||||
} catch (error) {
|
||||
console.error(`Could not deserialize session from storage: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setSession(session: Session, persist = true): void {
|
||||
this.httpService.setAuthorizationToken(session.authorizationValue)
|
||||
private setSession(session: Session | LegacySession, persist = true): void {
|
||||
if (session instanceof Session) {
|
||||
this.httpService.setSession(session)
|
||||
}
|
||||
|
||||
this.apiService.setSession(session, persist)
|
||||
|
||||
@@ -158,7 +168,7 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
public async signOut() {
|
||||
this.setUser(undefined)
|
||||
const session = this.apiService.getSession()
|
||||
if (session && session.canExpire()) {
|
||||
if (session && session instanceof Session) {
|
||||
await this.apiService.signOut()
|
||||
this.webSocketsService.closeWebSocketConnection()
|
||||
}
|
||||
@@ -560,17 +570,17 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
if (!session) {
|
||||
return new ClientDisplayableError('Cannot generate share token without active session')
|
||||
}
|
||||
if (!(session instanceof TokenSession)) {
|
||||
if (!(session instanceof Session)) {
|
||||
return new ClientDisplayableError('Cannot generate share token with non-token session')
|
||||
}
|
||||
|
||||
const keyParams = (await this.protocolService.getRootKeyParams()) as SNRootKeyParams
|
||||
|
||||
const payload: ShareToken = {
|
||||
accessToken: session.accessToken,
|
||||
refreshToken: session.refreshToken,
|
||||
accessExpiration: session.accessExpiration,
|
||||
refreshExpiration: session.refreshExpiration,
|
||||
accessToken: session.accessToken.value,
|
||||
refreshToken: session.refreshToken.value,
|
||||
accessExpiration: session.accessToken.expiresAt,
|
||||
refreshExpiration: session.refreshToken.expiresAt,
|
||||
readonlyAccess: true,
|
||||
masterKey: this.protocolService.getRootKey()?.masterKey as string,
|
||||
keyParams: keyParams.content,
|
||||
@@ -597,7 +607,7 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
|
||||
const user = sharePayload.user
|
||||
|
||||
const session = new TokenSession(
|
||||
const session = this.createSession(
|
||||
sharePayload.accessToken,
|
||||
sharePayload.accessExpiration,
|
||||
sharePayload.refreshToken,
|
||||
@@ -605,13 +615,15 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
sharePayload.readonlyAccess,
|
||||
)
|
||||
|
||||
await this.populateSession(rootKey, user, session, sharePayload.host)
|
||||
if (session !== null) {
|
||||
await this.populateSession(rootKey, user, session, sharePayload.host)
|
||||
}
|
||||
}
|
||||
|
||||
private async populateSession(
|
||||
rootKey: SNRootKey,
|
||||
user: Responses.User,
|
||||
session: Session,
|
||||
session: Session | LegacySession,
|
||||
host: string,
|
||||
wrappingKey?: SNRootKey,
|
||||
) {
|
||||
@@ -629,14 +641,17 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
}
|
||||
|
||||
private async handleAuthResponse(body: UserRegistrationResponseBody, rootKey: SNRootKey, wrappingKey?: SNRootKey) {
|
||||
const session = new TokenSession(
|
||||
const session = this.createSession(
|
||||
body.session.access_token,
|
||||
body.session.access_expiration,
|
||||
body.session.refresh_token,
|
||||
body.session.refresh_expiration,
|
||||
body.session.readonly_access,
|
||||
)
|
||||
await this.populateSession(rootKey, body.user, session, this.apiService.getHost(), wrappingKey)
|
||||
|
||||
if (session !== null) {
|
||||
await this.populateSession(rootKey, body.user, session, this.apiService.getHost(), wrappingKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -652,14 +667,51 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
|
||||
const isLegacyJwtResponse = data.token != undefined
|
||||
if (isLegacyJwtResponse) {
|
||||
const session = new JwtSession(data.token as string)
|
||||
await this.populateSession(rootKey, user, session, this.apiService.getHost(), wrappingKey)
|
||||
const sessionOrError = LegacySession.create(data.token as string)
|
||||
if (!sessionOrError.isFailed()) {
|
||||
await this.populateSession(rootKey, user, sessionOrError.getValue(), this.apiService.getHost(), wrappingKey)
|
||||
}
|
||||
} else if (data.session) {
|
||||
const session = TokenSession.FromApiResponse(response)
|
||||
await this.populateSession(rootKey, user, session, this.apiService.getHost(), wrappingKey)
|
||||
const session = this.createSession(
|
||||
data.session.access_token,
|
||||
data.session.access_expiration,
|
||||
data.session.refresh_token,
|
||||
data.session.refresh_expiration,
|
||||
data.session.readonly_access,
|
||||
)
|
||||
if (session !== null) {
|
||||
await this.populateSession(rootKey, user, session, this.apiService.getHost(), wrappingKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createSession(
|
||||
accessTokenValue: string,
|
||||
accessExpiration: number,
|
||||
refreshTokenValue: string,
|
||||
refreshExpiration: number,
|
||||
readonlyAccess: boolean,
|
||||
): Session | null {
|
||||
const accessTokenOrError = SessionToken.create(accessTokenValue, accessExpiration)
|
||||
if (accessTokenOrError.isFailed()) {
|
||||
return null
|
||||
}
|
||||
const accessToken = accessTokenOrError.getValue()
|
||||
|
||||
const refreshTokenOrError = SessionToken.create(refreshTokenValue, refreshExpiration)
|
||||
if (refreshTokenOrError.isFailed()) {
|
||||
return null
|
||||
}
|
||||
const refreshToken = refreshTokenOrError.getValue()
|
||||
|
||||
const sessionOrError = Session.create(accessToken, refreshToken, readonlyAccess)
|
||||
if (sessionOrError.isFailed()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return sessionOrError.getValue()
|
||||
}
|
||||
|
||||
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||
return Promise.resolve({
|
||||
session: {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { JwtSession } from './JwtSession'
|
||||
import { TokenSession } from './TokenSession'
|
||||
import { RawSessionPayload, RawStorageValue } from './Types'
|
||||
|
||||
export function SessionFromRawStorageValue(raw: RawStorageValue): JwtSession | TokenSession {
|
||||
if ('jwt' in raw) {
|
||||
return new JwtSession(raw.jwt as string)
|
||||
} else {
|
||||
const rawSession = raw as RawSessionPayload
|
||||
return new TokenSession(
|
||||
rawSession.accessToken,
|
||||
rawSession.accessExpiration,
|
||||
rawSession.refreshToken,
|
||||
rawSession.refreshExpiration,
|
||||
rawSession.readonlyAccess,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Session } from './Session'
|
||||
|
||||
/** Legacy, for protocol versions <= 003 */
|
||||
|
||||
export class JwtSession extends Session {
|
||||
public jwt: string
|
||||
|
||||
constructor(jwt: string) {
|
||||
super()
|
||||
this.jwt = jwt
|
||||
}
|
||||
|
||||
public get authorizationValue(): string {
|
||||
return this.jwt
|
||||
}
|
||||
|
||||
public canExpire(): false {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export abstract class Session {
|
||||
public abstract canExpire(): boolean
|
||||
|
||||
/** Return the token that should be included in the header of authorized network requests */
|
||||
public abstract get authorizationValue(): string
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { SessionBody, SessionRenewalResponse } from '@standardnotes/responses'
|
||||
import { Session } from './Session'
|
||||
|
||||
/** For protocol versions >= 004 */
|
||||
export class TokenSession extends Session {
|
||||
static FromApiResponse(response: SessionRenewalResponse) {
|
||||
const body = response.data.session as SessionBody
|
||||
const accessToken: string = body.access_token
|
||||
const refreshToken: string = body.refresh_token
|
||||
const accessExpiration: number = body.access_expiration
|
||||
const refreshExpiration: number = body.refresh_expiration
|
||||
const readonlyAccess: boolean = body.readonly_access
|
||||
|
||||
return new TokenSession(accessToken, accessExpiration, refreshToken, refreshExpiration, readonlyAccess)
|
||||
}
|
||||
|
||||
constructor(
|
||||
public accessToken: string,
|
||||
public accessExpiration: number,
|
||||
public refreshToken: string,
|
||||
public refreshExpiration: number,
|
||||
private readonlyAccess: boolean,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
isReadOnly() {
|
||||
return this.readonlyAccess
|
||||
}
|
||||
|
||||
private getExpireAt() {
|
||||
return this.accessExpiration || 0
|
||||
}
|
||||
|
||||
public get authorizationValue() {
|
||||
return this.accessToken
|
||||
}
|
||||
|
||||
public canExpire() {
|
||||
return true
|
||||
}
|
||||
|
||||
public isExpired() {
|
||||
return this.getExpireAt() < Date.now()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1 @@
|
||||
export * from './Generator'
|
||||
export * from './JwtSession'
|
||||
export * from './Session'
|
||||
export * from './TokenSession'
|
||||
export * from './Types'
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('server session', function () {
|
||||
|
||||
async function sleepUntilSessionExpires(application, basedOnAccessToken = true) {
|
||||
const currentSession = application.apiService.session
|
||||
const timestamp = basedOnAccessToken ? currentSession.accessExpiration : currentSession.refreshExpiration
|
||||
const timestamp = basedOnAccessToken ? currentSession.accessToken.expiresAt : currentSession.refreshToken.expiresAt
|
||||
const timeRemaining = (timestamp - Date.now()) / 1000 // in ms
|
||||
/*
|
||||
If the token has not expired yet, we will return the remaining time.
|
||||
@@ -98,12 +98,12 @@ describe('server session', function () {
|
||||
// After the above sync request is completed, we obtain the session information.
|
||||
const sessionAfterSync = this.application.apiService.getSession()
|
||||
|
||||
expect(sessionBeforeSync).to.not.equal(sessionAfterSync)
|
||||
expect(sessionBeforeSync.accessToken).to.not.equal(sessionAfterSync.accessToken)
|
||||
expect(sessionBeforeSync.refreshToken).to.not.equal(sessionAfterSync.refreshToken)
|
||||
expect(sessionBeforeSync.accessExpiration).to.be.lessThan(sessionAfterSync.accessExpiration)
|
||||
expect(sessionBeforeSync.equals(sessionAfterSync)).to.not.equal(true)
|
||||
expect(sessionBeforeSync.accessToken.value).to.not.equal(sessionAfterSync.accessToken.value)
|
||||
expect(sessionBeforeSync.refreshToken.value).to.not.equal(sessionAfterSync.refreshToken.value)
|
||||
expect(sessionBeforeSync.accessToken.expiresAt).to.be.lessThan(sessionAfterSync.accessToken.expiresAt)
|
||||
// New token should expire in the future.
|
||||
expect(sessionAfterSync.accessExpiration).to.be.greaterThan(Date.now())
|
||||
expect(sessionAfterSync.accessToken.expiresAt).to.be.greaterThan(Date.now())
|
||||
})
|
||||
|
||||
it('should succeed when a sync request is perfomed after signing into an ephemeral session', async function () {
|
||||
@@ -142,14 +142,22 @@ describe('server session', function () {
|
||||
const sessionFromStorage = await getSessionFromStorage(this.application)
|
||||
const sessionFromApiService = this.application.apiService.getSession()
|
||||
|
||||
expect(sessionFromStorage).to.equal(sessionFromApiService)
|
||||
expect(sessionFromStorage.accessToken).to.equal(sessionFromApiService.accessToken.value)
|
||||
expect(sessionFromStorage.refreshToken).to.equal(sessionFromApiService.refreshToken.value)
|
||||
expect(sessionFromStorage.accessExpiration).to.equal(sessionFromApiService.accessToken.expiresAt)
|
||||
expect(sessionFromStorage.refreshExpiration).to.equal(sessionFromApiService.refreshToken.expiresAt)
|
||||
expect(sessionFromStorage.readonlyAccess).to.equal(sessionFromApiService.isReadOnly())
|
||||
|
||||
await this.application.apiService.refreshSession()
|
||||
|
||||
const updatedSessionFromStorage = await getSessionFromStorage(this.application)
|
||||
const updatedSessionFromApiService = this.application.apiService.getSession()
|
||||
|
||||
expect(updatedSessionFromStorage).to.equal(updatedSessionFromApiService)
|
||||
expect(updatedSessionFromStorage.accessToken).to.equal(updatedSessionFromApiService.accessToken.value)
|
||||
expect(updatedSessionFromStorage.refreshToken).to.equal(updatedSessionFromApiService.refreshToken.value)
|
||||
expect(updatedSessionFromStorage.accessExpiration).to.equal(updatedSessionFromApiService.accessToken.expiresAt)
|
||||
expect(updatedSessionFromStorage.refreshExpiration).to.equal(updatedSessionFromApiService.refreshToken.expiresAt)
|
||||
expect(updatedSessionFromStorage.readonlyAccess).to.equal(updatedSessionFromApiService.isReadOnly())
|
||||
})
|
||||
|
||||
it('should be performed successfully and terminate session with a valid access token', async function () {
|
||||
@@ -221,8 +229,16 @@ describe('server session', function () {
|
||||
let { application, password } = await Factory.createAndInitSimpleAppContext({
|
||||
registerUser: true,
|
||||
})
|
||||
const fakeSession = application.apiService.getSession()
|
||||
fakeSession.accessToken = 'this-is-a-fake-token-1234'
|
||||
|
||||
application.diskStorageService.setValue(StorageKey.Session, {
|
||||
accessToken: 'this-is-a-fake-token-1234',
|
||||
refreshToken: 'this-is-a-fake-token-1234',
|
||||
accessExpiration: 999999999999999,
|
||||
refreshExpiration: 99999999999999,
|
||||
readonlyAccess: false,
|
||||
})
|
||||
application.sessions.initializeFromDisk()
|
||||
|
||||
Factory.ignoreChallenges(application)
|
||||
|
||||
const newEmail = UuidGenerator.GenerateUuid()
|
||||
@@ -311,8 +327,15 @@ describe('server session', function () {
|
||||
password: this.password,
|
||||
})
|
||||
|
||||
const fakeSession = this.application.apiService.getSession()
|
||||
fakeSession.accessToken = 'this-is-a-fake-token-1234'
|
||||
this.application.diskStorageService.setValue(StorageKey.Session, {
|
||||
accessToken: 'this-is-a-fake-token-1234',
|
||||
refreshToken: 'this-is-a-fake-token-1234',
|
||||
accessExpiration: 999999999999999,
|
||||
refreshExpiration: 99999999999999,
|
||||
readonlyAccess: false,
|
||||
})
|
||||
this.application.sessions.initializeFromDisk()
|
||||
|
||||
Factory.ignoreChallenges(this.application)
|
||||
const changePasswordResponse = await this.application.changePassword(this.password, this.newPassword)
|
||||
expect(changePasswordResponse.error.message).to.equal('Invalid login credentials.')
|
||||
@@ -354,7 +377,7 @@ describe('server session', function () {
|
||||
expect(currentSession).to.be.ok
|
||||
expect(currentSession.accessToken).to.be.ok
|
||||
expect(currentSession.refreshToken).to.be.ok
|
||||
expect(currentSession.accessExpiration).to.be.greaterThan(Date.now())
|
||||
expect(currentSession.accessToken.expiresAt).to.be.greaterThan(Date.now())
|
||||
})
|
||||
|
||||
it('should fail when renewing a session with an expired refresh token', async function () {
|
||||
@@ -392,10 +415,16 @@ describe('server session', function () {
|
||||
password: this.password,
|
||||
})
|
||||
|
||||
const fakeSession = this.application.apiService.getSession()
|
||||
fakeSession.refreshToken = 'this-is-a-fake-token-1234'
|
||||
const originalSession = this.application.apiService.getSession()
|
||||
|
||||
await this.application.apiService.setSession(fakeSession, true)
|
||||
this.application.diskStorageService.setValue(StorageKey.Session, {
|
||||
accessToken: originalSession.accessToken.value,
|
||||
refreshToken: 'this-is-a-fake-token-1234',
|
||||
accessExpiration: originalSession.accessToken.expiresAt,
|
||||
refreshExpiration: originalSession.refreshToken.expiresAt,
|
||||
readonlyAccess: false,
|
||||
})
|
||||
this.application.sessions.initializeFromDisk()
|
||||
|
||||
const refreshSessionResponse = await this.application.apiService.refreshSession()
|
||||
|
||||
@@ -530,8 +559,14 @@ describe('server session', function () {
|
||||
const oldRootKey = await appA.protocolService.getRootKey()
|
||||
|
||||
/** Set the session as nonsense */
|
||||
appA.apiService.session.accessToken = 'foo'
|
||||
appA.apiService.session.refreshToken = 'bar'
|
||||
appA.diskStorageService.setValue(StorageKey.Session, {
|
||||
accessToken: 'foo',
|
||||
refreshToken: 'bar',
|
||||
accessExpiration: 999999999999999,
|
||||
refreshExpiration: 999999999999999,
|
||||
readonlyAccess: false,
|
||||
})
|
||||
appA.sessions.initializeFromDisk()
|
||||
|
||||
/** Perform an authenticated network request */
|
||||
await appA.sync.sync()
|
||||
@@ -540,8 +575,8 @@ describe('server session', function () {
|
||||
await Factory.sleep(5.0)
|
||||
|
||||
expect(didPromptForSignIn).to.equal(true)
|
||||
expect(appA.apiService.session.accessToken).to.not.equal('foo')
|
||||
expect(appA.apiService.session.refreshToken).to.not.equal('bar')
|
||||
expect(appA.apiService.session.accessToken.value).to.not.equal('foo')
|
||||
expect(appA.apiService.session.refreshToken.value).to.not.equal('bar')
|
||||
|
||||
/** Expect that the session recovery replaces the global root key */
|
||||
const newRootKey = await appA.protocolService.getRootKey()
|
||||
@@ -646,9 +681,14 @@ describe('server session', function () {
|
||||
password: this.password,
|
||||
})
|
||||
|
||||
const invalidSession = this.application.apiService.getSession()
|
||||
invalidSession.accessToken = undefined
|
||||
invalidSession.refreshToken = undefined
|
||||
this.application.diskStorageService.setValue(StorageKey.Session, {
|
||||
accessToken: undefined,
|
||||
refreshToken: undefined,
|
||||
accessExpiration: 999999999999999,
|
||||
refreshExpiration: 999999999999999,
|
||||
readonlyAccess: false,
|
||||
})
|
||||
this.application.sessions.initializeFromDisk()
|
||||
|
||||
const storageKey = this.application.diskStorageService.getPersistenceKey()
|
||||
expect(localStorage.getItem(storageKey)).to.be.ok
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/snjs",
|
||||
"version": "2.156.0",
|
||||
"version": "2.156.3",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -44,7 +44,7 @@
|
||||
"@standardnotes/responses": "workspace:*",
|
||||
"@standardnotes/security": "^1.7.0",
|
||||
"@standardnotes/services": "workspace:*",
|
||||
"@standardnotes/settings": "^1.18.3",
|
||||
"@standardnotes/settings": "^1.18.4",
|
||||
"@standardnotes/sncrypto-common": "workspace:*",
|
||||
"@standardnotes/sncrypto-web": "workspace:*",
|
||||
"@standardnotes/utils": "workspace:*",
|
||||
@@ -83,5 +83,8 @@
|
||||
"webpack": "*",
|
||||
"webpack-cli": "*",
|
||||
"webpack-merge": "^5.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/domain-core": "^1.11.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.3.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/toast
|
||||
|
||||
## [1.3.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/toast
|
||||
|
||||
## [1.3.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/toast
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/toast",
|
||||
"version": "1.3.12",
|
||||
"version": "1.3.14",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -75,8 +75,10 @@ export const ToastTimer: FunctionComponent<Props> = ({ toast, index }) => {
|
||||
toastElement.addEventListener('mouseenter', handleMouseEnter)
|
||||
toastElement.addEventListener('mouseleave', handleMouseLeave)
|
||||
}
|
||||
window.addEventListener('focus', handlePageFocus)
|
||||
window.addEventListener('blur', handlePageBlur)
|
||||
if (toast.pauseOnWindowBlur) {
|
||||
window.addEventListener('focus', handlePageFocus)
|
||||
window.addEventListener('blur', handlePageBlur)
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearTimer()
|
||||
@@ -84,8 +86,10 @@ export const ToastTimer: FunctionComponent<Props> = ({ toast, index }) => {
|
||||
toastElement.removeEventListener('mouseenter', handleMouseEnter)
|
||||
toastElement.removeEventListener('mouseleave', handleMouseLeave)
|
||||
}
|
||||
window.removeEventListener('focus', handlePageFocus)
|
||||
window.removeEventListener('blur', handlePageBlur)
|
||||
if (toast.pauseOnWindowBlur) {
|
||||
window.removeEventListener('focus', handlePageFocus)
|
||||
window.removeEventListener('blur', handlePageBlur)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
clearTimer,
|
||||
|
||||
@@ -61,10 +61,11 @@ export const addToast = action(toastStore, 'addToast', (store: WritableAtom<Toas
|
||||
If you want to update an existing toast, use the \`updateToast()\` function instead.`)
|
||||
}
|
||||
|
||||
const toast = {
|
||||
const toast: Toast = {
|
||||
...options,
|
||||
id,
|
||||
dismissed: false,
|
||||
pauseOnWindowBlur: options.pauseOnWindowBlur ?? true,
|
||||
}
|
||||
|
||||
store.set([...existingToasts, toast])
|
||||
|
||||
@@ -12,6 +12,7 @@ type CommonToastProperties = {
|
||||
progress?: number
|
||||
autoClose?: boolean
|
||||
duration?: number
|
||||
pauseOnWindowBlur?: boolean
|
||||
}
|
||||
|
||||
export type Toast = CommonToastProperties & {
|
||||
|
||||
@@ -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.20.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
# [1.20.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-16)
|
||||
|
||||
### Features
|
||||
|
||||
* Added "Whats New" indicator to Preferences button ([#2107](https://github.com/standardnotes/app/issues/2107)) ([f559442](https://github.com/standardnotes/app/commit/f559442a6726444f338a43e51d7a872f19bce701))
|
||||
|
||||
## [1.19.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.19.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.19.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.19.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/ui-services",
|
||||
"version": "1.19.3",
|
||||
"version": "1.20.1",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Environment } from '@standardnotes/models'
|
||||
import { StorageServiceInterface, StorageKey } from '@standardnotes/services'
|
||||
import { Changelog, ChangelogVersion } from './Changelog'
|
||||
import { ChangelogServiceInterface } from './ChangelogServiceInterface'
|
||||
import { ChangelogLastReadVersionListener, ChangelogServiceInterface } from './ChangelogServiceInterface'
|
||||
import { LegacyWebToDesktopVersionMapping } from './LegacyDesktopMapping'
|
||||
import { LegacyWebToMobileVersionMapping } from './LegacyMobileMapping'
|
||||
|
||||
@@ -10,9 +10,18 @@ const DesktopDownloadsUrlBase = 'https://github.com/standardnotes/app/releases/t
|
||||
|
||||
export class ChangelogService implements ChangelogServiceInterface {
|
||||
private changeLog?: Changelog
|
||||
private lastReadChangeListeners: ChangelogLastReadVersionListener[] = []
|
||||
|
||||
constructor(private environment: Environment, private diskService: StorageServiceInterface) {}
|
||||
|
||||
public addLastReadChangeListener(listener: ChangelogLastReadVersionListener) {
|
||||
this.lastReadChangeListeners.push(listener)
|
||||
|
||||
return () => {
|
||||
this.lastReadChangeListeners = this.lastReadChangeListeners.filter((l) => l !== listener)
|
||||
}
|
||||
}
|
||||
|
||||
private async performDownloadChangelog(): Promise<Changelog> {
|
||||
const response = await fetch(WebChangelogUrl)
|
||||
const changelog = await response.text()
|
||||
@@ -47,7 +56,11 @@ export class ChangelogService implements ChangelogServiceInterface {
|
||||
return
|
||||
}
|
||||
|
||||
this.diskService.setValue(StorageKey.LastReadChangelogVersion, this.changeLog.versions[0].version)
|
||||
const version = this.changeLog.versions[0].version
|
||||
this.diskService.setValue(StorageKey.LastReadChangelogVersion, version)
|
||||
if (version) {
|
||||
this.lastReadChangeListeners.forEach((listener) => listener(version))
|
||||
}
|
||||
}
|
||||
|
||||
public getLastReadVersion(): string | undefined {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Changelog, ChangelogVersion } from './Changelog'
|
||||
|
||||
export type ChangelogLastReadVersionListener = (version: string) => void
|
||||
|
||||
export interface ChangelogServiceInterface {
|
||||
addLastReadChangeListener(listener: ChangelogLastReadVersionListener): () => void
|
||||
getChangelog(): Promise<Changelog>
|
||||
getVersions(): Promise<ChangelogVersion[]>
|
||||
getDesktopDownloadsUrl(version: string): string
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.15.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
### Features
|
||||
|
||||
* Add current Writing Streak to Daily Notebooks ([#2093](https://github.com/standardnotes/app/issues/2093)) ([d149b50](https://github.com/standardnotes/app/commit/d149b500ee3f13f63a1f831e6e0ba29a53e3e416))
|
||||
|
||||
## [1.14.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/utils",
|
||||
"version": "1.14.1",
|
||||
"version": "1.15.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -687,3 +687,7 @@ export function useBoolean(value: boolean | undefined, defaultValue: boolean): b
|
||||
export function spaceSeparatedStrings(...strings: string[]): string {
|
||||
return strings.join(' ')
|
||||
}
|
||||
|
||||
export function pluralize(count: number, singular: string, plural: string): string {
|
||||
return count === 1 ? singular : plural
|
||||
}
|
||||
|
||||
@@ -3,12 +3,70 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.127.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
# [3.127.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-16)
|
||||
|
||||
### Features
|
||||
|
||||
* Added "Whats New" indicator to Preferences button ([#2107](https://github.com/standardnotes/app/issues/2107)) ([f559442](https://github.com/standardnotes/app/commit/f559442a6726444f338a43e51d7a872f19bce701))
|
||||
|
||||
## [3.126.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.126.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
# [3.126.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-13)
|
||||
|
||||
### Features
|
||||
|
||||
* Ability to record videos and capture photos directly in app by selecting + in Files view ([#2095](https://github.com/standardnotes/app/issues/2095)) ([d4b63e4](https://github.com/standardnotes/app/commit/d4b63e4ea6e82ba17ebb02da3ca1cfc9ced92b57))
|
||||
|
||||
# [3.125.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
### Features
|
||||
|
||||
* Added "Open" button to file upload success notification ([#2091](https://github.com/standardnotes/app/issues/2091)) ([220f626](https://github.com/standardnotes/app/commit/220f62665e6daa18020b410b893207eccdfd0b67))
|
||||
|
||||
# [3.124.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
### Features
|
||||
|
||||
* Add current Writing Streak to Daily Notebooks ([#2093](https://github.com/standardnotes/app/issues/2093)) ([d149b50](https://github.com/standardnotes/app/commit/d149b500ee3f13f63a1f831e6e0ba29a53e3e416))
|
||||
|
||||
## [3.123.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed issue where passcode lock setting was not visible on mobile ([d4af612](https://github.com/standardnotes/app/commit/d4af612fea6960a5233404682aeb1819754b9b8b))
|
||||
|
||||
# [3.123.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* alignment of Loading text in What's New screen ([d360985](https://github.com/standardnotes/app/commit/d3609853c6c6121a5633d72a685723498099edba))
|
||||
|
||||
### Features
|
||||
|
||||
* Moments will now create a link with the current open note, so you can look back on what you were working on during a moment ([a7ac715](https://github.com/standardnotes/app/commit/a7ac715499fdf3f15a736f68af4bf5a78454742f))
|
||||
|
||||
# [3.122.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
### Features
|
||||
|
||||
* add ability to convert selection in Super to bulleted or numbered list ([be4cc4e](https://github.com/standardnotes/app/commit/be4cc4e605100e0a86de214e548e9ed10e2568aa))
|
||||
|
||||
## [3.121.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed issue about note UUID not being selectable in menu ([5a0c005](https://github.com/standardnotes/app/commit/5a0c005e8c0373e2f47fa613fc5bfec0e84cdb37))
|
||||
* Fixed workspace menu item rename issue ([45cdab6](https://github.com/standardnotes/app/commit/45cdab6f6c0416e0a01a66e4b4983ff0a52c1afc))
|
||||
* Fixed issue where note UUID was not selectable in context menu ([5a0c005](https://github.com/standardnotes/app/commit/5a0c005e8c0373e2f47fa613fc5bfec0e84cdb37))
|
||||
* Fixed issue where workspaces could not be renamed ([45cdab6](https://github.com/standardnotes/app/commit/45cdab6f6c0416e0a01a66e4b4983ff0a52c1afc))
|
||||
|
||||
# [3.121.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
|
||||
@@ -1,18 +1,153 @@
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"version": "3.127.1",
|
||||
"title": "[3.127.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.127.0",
|
||||
"title": "[3.127.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-16)",
|
||||
"date": null,
|
||||
"body": "### Features\n\n* Added \"Whats New\" indicator to Preferences button ([#2107](https://github.com/standardnotes/app/issues/2107)) ([f559442](https://github.com/standardnotes/app/commit/f559442a6726444f338a43e51d7a872f19bce701))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Added \"Whats New\" indicator to Preferences button (#2107) (f559442)"
|
||||
],
|
||||
"Features": [
|
||||
"Added \"Whats New\" indicator to Preferences button (#2107) (f559442)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.126.4",
|
||||
"title": "[3.126.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-15)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.126.2",
|
||||
"title": "[3.126.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-14)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.126.0",
|
||||
"title": "[3.126.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-13)",
|
||||
"date": null,
|
||||
"body": "### Features\n\n* Ability to record videos and capture photos directly in app by selecting + in Files view ([#2095](https://github.com/standardnotes/app/issues/2095)) ([d4b63e4](https://github.com/standardnotes/app/commit/d4b63e4ea6e82ba17ebb02da3ca1cfc9ced92b57))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Ability to record videos and capture photos directly in app by selecting + in Files view (#2095) (d4b63e4)"
|
||||
],
|
||||
"Features": [
|
||||
"Ability to record videos and capture photos directly in app by selecting + in Files view (#2095) (d4b63e4)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.125.0",
|
||||
"title": "[3.125.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)",
|
||||
"date": null,
|
||||
"body": "### Features\n\n* Added \"Open\" button to file upload success notification ([#2091](https://github.com/standardnotes/app/issues/2091)) ([220f626](https://github.com/standardnotes/app/commit/220f62665e6daa18020b410b893207eccdfd0b67))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Added \"Open\" button to file upload success notification (#2091) (220f626)"
|
||||
],
|
||||
"Features": [
|
||||
"Added \"Open\" button to file upload success notification (#2091) (220f626)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.124.0",
|
||||
"title": "[3.124.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)",
|
||||
"date": null,
|
||||
"body": "### Features\n\n* Add current Writing Streak to Daily Notebooks ([#2093](https://github.com/standardnotes/app/issues/2093)) ([d149b50](https://github.com/standardnotes/app/commit/d149b500ee3f13f63a1f831e6e0ba29a53e3e416))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Add current Writing Streak to Daily Notebooks (#2093) (d149b50)"
|
||||
],
|
||||
"Features": [
|
||||
"Add current Writing Streak to Daily Notebooks (#2093) (d149b50)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.123.1",
|
||||
"title": "[3.123.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Fixed issue where passcode lock setting was not visible on mobile ([d4af612](https://github.com/standardnotes/app/commit/d4af612fea6960a5233404682aeb1819754b9b8b))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Fixed issue where passcode lock setting was not visible on mobile (d4af612)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Fixed issue where passcode lock setting was not visible on mobile (d4af612)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.123.0",
|
||||
"title": "[3.123.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* alignment of Loading text in What's New screen ([d360985](https://github.com/standardnotes/app/commit/d3609853c6c6121a5633d72a685723498099edba))\n\n### Features\n\n* Moments will now create a link with the current open note, so you can look back on what you were working on during a moment ([a7ac715](https://github.com/standardnotes/app/commit/a7ac715499fdf3f15a736f68af4bf5a78454742f))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"alignment of Loading text in What's New screen (d360985)",
|
||||
"Moments will now create a link with the current open note, so you can look back on what you were working on during a moment (a7ac715)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"alignment of Loading text in What's New screen (d360985)"
|
||||
],
|
||||
"Features": [
|
||||
"Moments will now create a link with the current open note, so you can look back on what you were working on during a moment (a7ac715)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.122.0",
|
||||
"title": "[3.122.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)",
|
||||
"date": null,
|
||||
"body": "### Features\n\n* add ability to convert selection in Super to bulleted or numbered list ([be4cc4e](https://github.com/standardnotes/app/commit/be4cc4e605100e0a86de214e548e9ed10e2568aa))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"add ability to convert selection in Super to bulleted or numbered list (be4cc4e)"
|
||||
],
|
||||
"Features": [
|
||||
"add ability to convert selection in Super to bulleted or numbered list (be4cc4e)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.121.1",
|
||||
"title": "[3.121.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Fixed issue about note UUID not being selectable in menu ([5a0c005](https://github.com/standardnotes/app/commit/5a0c005e8c0373e2f47fa613fc5bfec0e84cdb37))\n* Fixed workspace menu item rename issue ([45cdab6](https://github.com/standardnotes/app/commit/45cdab6f6c0416e0a01a66e4b4983ff0a52c1afc))",
|
||||
"body": "### Bug Fixes\n\n* Fixed issue where note UUID was not selectable in context menu ([5a0c005](https://github.com/standardnotes/app/commit/5a0c005e8c0373e2f47fa613fc5bfec0e84cdb37))\n* Fixed issue where workspaces could not be renamed ([45cdab6](https://github.com/standardnotes/app/commit/45cdab6f6c0416e0a01a66e4b4983ff0a52c1afc))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Fixed issue about note UUID not being selectable in menu (5a0c005)",
|
||||
"Fixed workspace menu item rename issue (45cdab6)"
|
||||
"Fixed issue where note UUID was not selectable in context menu (5a0c005)",
|
||||
"Fixed issue where workspaces could not be renamed (45cdab6)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Fixed issue about note UUID not being selectable in menu (5a0c005)",
|
||||
"Fixed workspace menu item rename issue (45cdab6)"
|
||||
"Fixed issue where note UUID was not selectable in context menu (5a0c005)",
|
||||
"Fixed issue where workspaces could not be renamed (45cdab6)"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/web",
|
||||
"version": "3.121.1",
|
||||
"version": "3.127.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/app.js",
|
||||
"author": "Standard Notes",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FunctionComponent } from 'react'
|
||||
|
||||
const DotOrgNotice: FunctionComponent = () => {
|
||||
return (
|
||||
<div className="align-center z-modal flex h-30 w-full w-full items-center bg-danger text-center text-info-contrast">
|
||||
<div className="z-modal flex h-30 w-full w-full items-center bg-danger text-center text-info-contrast">
|
||||
<div className="w-full text-center text-xl font-bold">
|
||||
app.standardnotes.org is no longer maintained. Please switch to{' '}
|
||||
<a className="underline" href="https://app.standardnotes.com">
|
||||
|
||||
+1
-1
@@ -40,6 +40,6 @@ export type PopoverFileItemAction =
|
||||
type: PopoverFileItemActionType.PreviewFile
|
||||
payload: {
|
||||
file: FileItem
|
||||
otherFiles: FileItem[]
|
||||
otherFiles?: FileItem[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { PhotoRecorder } from '@/Controllers/Moments/PhotoRecorder'
|
||||
import { formatDateAndTimeForNote } from '@/Utils/DateUtils'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Button from '../Button/Button'
|
||||
import Dropdown from '../Dropdown/Dropdown'
|
||||
import Icon from '../Icon/Icon'
|
||||
import DecoratedInput from '../Input/DecoratedInput'
|
||||
import ModalDialog from '../Shared/ModalDialog'
|
||||
import ModalDialogButtons from '../Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '../Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '../Shared/ModalDialogLabel'
|
||||
|
||||
type Props = {
|
||||
filesController: FilesController
|
||||
close: () => void
|
||||
}
|
||||
|
||||
const PhotoCaptureModal = ({ filesController, close }: Props) => {
|
||||
const [fileName, setFileName] = useState(formatDateAndTimeForNote(new Date()))
|
||||
const [recorder, setRecorder] = useState<PhotoRecorder | undefined>(() => new PhotoRecorder())
|
||||
const [isRecorderReady, setIsRecorderReady] = useState(false)
|
||||
const [capturedPhoto, setCapturedPhoto] = useState<File>()
|
||||
|
||||
const fileNameInputRef = useRef<HTMLInputElement>(null)
|
||||
const previewRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!recorder) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsRecorderReady(false)
|
||||
|
||||
const init = async () => {
|
||||
await recorder.initialize()
|
||||
|
||||
if (previewRef.current) {
|
||||
recorder.video.style.position = ''
|
||||
recorder.video.style.display = ''
|
||||
recorder.video.style.height = '100%'
|
||||
previewRef.current.append(recorder.video)
|
||||
}
|
||||
|
||||
setIsRecorderReady(true)
|
||||
}
|
||||
|
||||
void init()
|
||||
|
||||
return () => {
|
||||
if (recorder.video) {
|
||||
recorder.finish()
|
||||
}
|
||||
}
|
||||
}, [recorder])
|
||||
|
||||
const takePhoto = useCallback(async () => {
|
||||
if (!recorder) {
|
||||
return
|
||||
}
|
||||
|
||||
const file = await recorder.takePhoto(fileName)
|
||||
setCapturedPhoto(file)
|
||||
setRecorder(undefined)
|
||||
}, [fileName, recorder])
|
||||
|
||||
const devicesAsDropdownItems = useMemo(() => {
|
||||
return recorder?.devices
|
||||
? recorder.devices.map((device) => ({
|
||||
label: device.label || `Camera (${device.deviceId.slice(0, 10)})`,
|
||||
value: device.deviceId,
|
||||
}))
|
||||
: []
|
||||
}, [recorder?.devices])
|
||||
|
||||
const savePhoto = useCallback(() => {
|
||||
if (!fileName) {
|
||||
fileNameInputRef.current?.focus()
|
||||
return
|
||||
}
|
||||
if (!capturedPhoto) {
|
||||
return
|
||||
}
|
||||
void filesController.uploadNewFile(capturedPhoto)
|
||||
close()
|
||||
}, [capturedPhoto, close, fileName, filesController])
|
||||
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={close}>Take a photo</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<div className="mb-4 flex flex-col">
|
||||
<label className="text-sm font-medium text-neutral">
|
||||
File name:
|
||||
<DecoratedInput
|
||||
className={{
|
||||
container: 'mt-1',
|
||||
}}
|
||||
value={fileName}
|
||||
onChange={(fileName) => setFileName(fileName)}
|
||||
ref={fileNameInputRef}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<div className="text-sm font-medium text-neutral">Preview:</div>
|
||||
{!isRecorderReady && (
|
||||
<div className="mt-1 w-full">
|
||||
<div className="flex h-64 w-full items-center justify-center gap-2 rounded-md bg-contrast text-base">
|
||||
<Icon type="camera" className="text-neutral-300" />
|
||||
Initializing...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={classNames('mt-1 w-full', capturedPhoto && 'hidden')} ref={previewRef}></div>
|
||||
{capturedPhoto && (
|
||||
<div className="mt-1 w-full">
|
||||
<img src={URL.createObjectURL(capturedPhoto)} alt="Captured photo" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{recorder && devicesAsDropdownItems.length > 1 && !capturedPhoto && (
|
||||
<div className="mt-4">
|
||||
<label className="text-sm font-medium text-neutral">
|
||||
Device:
|
||||
<Dropdown
|
||||
id={'photo-capture-device-dropdown'}
|
||||
label={'Photo Capture Device'}
|
||||
items={devicesAsDropdownItems}
|
||||
value={recorder.selectedDevice.deviceId}
|
||||
onChange={(value: string) => {
|
||||
void recorder.setDevice(value)
|
||||
}}
|
||||
className={{
|
||||
wrapper: 'mt-1',
|
||||
popover: 'z-modal',
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
{!capturedPhoto && (
|
||||
<Button
|
||||
primary
|
||||
colorStyle="danger"
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
void takePhoto()
|
||||
}}
|
||||
>
|
||||
<Icon type="camera" />
|
||||
Take photo
|
||||
</Button>
|
||||
)}
|
||||
{capturedPhoto && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
setCapturedPhoto(undefined)
|
||||
setRecorder(new PhotoRecorder())
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
<Button primary className="flex items-center gap-2" onClick={savePhoto}>
|
||||
<Icon type="upload" />
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(PhotoCaptureModal)
|
||||
@@ -0,0 +1,166 @@
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { VideoRecorder } from '@/Controllers/Moments/VideoRecorder'
|
||||
import { formatDateAndTimeForNote } from '@/Utils/DateUtils'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Button from '../Button/Button'
|
||||
import Icon from '../Icon/Icon'
|
||||
import DecoratedInput from '../Input/DecoratedInput'
|
||||
import ModalDialog from '../Shared/ModalDialog'
|
||||
import ModalDialogButtons from '../Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '../Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '../Shared/ModalDialogLabel'
|
||||
|
||||
type Props = {
|
||||
filesController: FilesController
|
||||
close: () => void
|
||||
}
|
||||
|
||||
const VideoCaptureModal = ({ filesController, close }: Props) => {
|
||||
const [fileName, setFileName] = useState(formatDateAndTimeForNote(new Date()))
|
||||
const [recorder, setRecorder] = useState(() => new VideoRecorder(fileName))
|
||||
const [isRecorderReady, setIsRecorderReady] = useState(false)
|
||||
const [isRecording, setIsRecording] = useState(false)
|
||||
const [capturedVideo, setCapturedVideo] = useState<File>()
|
||||
|
||||
const fileNameInputRef = useRef<HTMLInputElement>(null)
|
||||
const previewRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
await recorder.initialize()
|
||||
|
||||
if (previewRef.current) {
|
||||
recorder.video.style.position = ''
|
||||
recorder.video.style.display = ''
|
||||
recorder.video.style.height = '100%'
|
||||
previewRef.current.append(recorder.video)
|
||||
}
|
||||
|
||||
setIsRecorderReady(true)
|
||||
}
|
||||
|
||||
void init()
|
||||
|
||||
return () => {
|
||||
if (recorder.video) {
|
||||
void recorder.stop()
|
||||
}
|
||||
}
|
||||
}, [recorder])
|
||||
|
||||
const startRecording = useCallback(async () => {
|
||||
await recorder.startRecording()
|
||||
setIsRecording(true)
|
||||
}, [recorder])
|
||||
|
||||
const saveVideo = useCallback(() => {
|
||||
if (!fileName) {
|
||||
fileNameInputRef.current?.focus()
|
||||
return
|
||||
}
|
||||
if (!capturedVideo) {
|
||||
return
|
||||
}
|
||||
const namedFile = new File([capturedVideo], fileName, {
|
||||
type: capturedVideo.type,
|
||||
})
|
||||
void filesController.uploadNewFile(namedFile)
|
||||
close()
|
||||
}, [capturedVideo, close, fileName, filesController])
|
||||
|
||||
const capturedVideoObjectURL = useMemo(() => {
|
||||
if (!capturedVideo) {
|
||||
return
|
||||
}
|
||||
return URL.createObjectURL(capturedVideo)
|
||||
}, [capturedVideo])
|
||||
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={close}>Record a video</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<div className="mb-4 flex flex-col">
|
||||
<label className="text-sm font-medium text-neutral">
|
||||
File name:
|
||||
<DecoratedInput
|
||||
className={{
|
||||
container: 'mt-1',
|
||||
}}
|
||||
value={fileName}
|
||||
onChange={(fileName) => setFileName(fileName)}
|
||||
ref={fileNameInputRef}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<div className="text-sm font-medium text-neutral">Preview:</div>
|
||||
{!isRecorderReady && (
|
||||
<div className="mt-1 w-full">
|
||||
<div className="flex h-64 w-full items-center justify-center gap-2 rounded-md bg-contrast text-base">
|
||||
<Icon type="camera" className="text-neutral-300" />
|
||||
Initializing...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={classNames('mt-1 w-full', capturedVideo && 'hidden')} ref={previewRef}></div>
|
||||
{capturedVideo && (
|
||||
<div className="mt-1 w-full">
|
||||
<video src={capturedVideoObjectURL} controls />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
{!capturedVideo && !isRecording && (
|
||||
<Button
|
||||
primary
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
void startRecording()
|
||||
}}
|
||||
>
|
||||
<Icon type="camera" />
|
||||
Start recording
|
||||
</Button>
|
||||
)}
|
||||
{!capturedVideo && isRecording && (
|
||||
<Button
|
||||
primary
|
||||
colorStyle="danger"
|
||||
className="flex items-center gap-2"
|
||||
onClick={async () => {
|
||||
const capturedVideo = await recorder.stop()
|
||||
setIsRecording(false)
|
||||
setCapturedVideo(capturedVideo)
|
||||
}}
|
||||
>
|
||||
<Icon type="camera" />
|
||||
Stop recording
|
||||
</Button>
|
||||
)}
|
||||
{capturedVideo && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
setCapturedVideo(undefined)
|
||||
setRecorder(new VideoRecorder(fileName))
|
||||
setIsRecorderReady(false)
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
<Button primary className="flex items-center gap-2" onClick={saveVideo}>
|
||||
<Icon type="upload" />
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(VideoCaptureModal)
|
||||
+3
-1
@@ -18,6 +18,7 @@ type Props = {
|
||||
selectedDay?: Date
|
||||
selectedDayType?: 'item' | 'template'
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export type InfiniteCalendarInterface = {
|
||||
@@ -27,7 +28,7 @@ export type InfiniteCalendarInterface = {
|
||||
const PageSize = 2
|
||||
|
||||
const InfiniteCalendar = forwardRef<InfiniteCalendarInterface, Props>(
|
||||
({ activities, onDateSelect, selectedDay, className }: Props, ref) => {
|
||||
({ activities, onDateSelect, selectedDay, className, children }: Props, ref) => {
|
||||
const [expanded, setExpanded] = useState(isMobileScreen() ? false : true)
|
||||
const [restoreScrollAfterExpand, setRestoreScrollAfterExpand] = useState(false)
|
||||
const scrollerRef = useRef<InfiniteScrollerInterface | null>(null)
|
||||
@@ -216,6 +217,7 @@ const InfiniteCalendar = forwardRef<InfiniteCalendarInterface, Props>(
|
||||
})}
|
||||
</InfinteScroller>
|
||||
)}
|
||||
{expanded && children}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -302,6 +302,7 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
|
||||
isFilesSmartView={isFilesSmartView}
|
||||
optionsSubtitle={optionsSubtitle}
|
||||
selectedTag={selectedTag}
|
||||
filesController={filesController}
|
||||
/>
|
||||
)}
|
||||
<SearchBar itemListController={itemListController} searchOptionsController={searchOptionsController} />
|
||||
|
||||
@@ -7,9 +7,9 @@ import { AppPaneId } from '../../Panes/AppPaneMetadata'
|
||||
import { createDailyItemsWithToday, createItemsByDateMapping, insertBlanks } from './CreateDailySections'
|
||||
import { DailyItemsDay } from './DailyItemsDaySection'
|
||||
import { DailyItemCell } from './DailyItemCell'
|
||||
import { SNTag } from '@standardnotes/snjs'
|
||||
import { SNTag, pluralize } from '@standardnotes/snjs'
|
||||
import { CalendarActivity } from '../Calendar/CalendarActivity'
|
||||
import { dateToDailyDayIdentifier } from './Utils'
|
||||
import { dateToDailyDayIdentifier, getDailyWritingStreak } from './Utils'
|
||||
import InfiniteCalendar, { InfiniteCalendarInterface } from '../Calendar/InfiniteCalendar'
|
||||
import { InfiniteScrollerInterface, InfinteScroller } from '../InfiniteScroller/InfiniteScroller'
|
||||
import { LoggingDomain, log } from '@/Logging'
|
||||
@@ -50,6 +50,11 @@ const DailyContentList: FunctionComponent<Props> = ({
|
||||
return createItemsByDateMapping(items)
|
||||
}, [items])
|
||||
|
||||
const currentStreak = useMemo(
|
||||
() => getDailyWritingStreak(todayItem, itemsByDateMapping),
|
||||
[todayItem, itemsByDateMapping],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setTodayItem(dailyItems.find((item) => item.isToday) as DailyItemsDay)
|
||||
}, [dailyItems])
|
||||
@@ -182,7 +187,16 @@ const DailyContentList: FunctionComponent<Props> = ({
|
||||
selectedDayType={!selectedDay ? undefined : hasItemsOnSelectedDay ? 'item' : 'template'}
|
||||
ref={calendarRef}
|
||||
className={'flex-column flex'}
|
||||
/>
|
||||
>
|
||||
{currentStreak > 0 && (
|
||||
<div className="flex w-full items-center justify-center border-t border-solid border-border bg-secondary-background p-2">
|
||||
<span className="opacity-50">Current Streak</span>
|
||||
<span className="ml-1.5 font-bold">
|
||||
{currentStreak} {pluralize(currentStreak, 'Day', 'Days')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</InfiniteCalendar>
|
||||
|
||||
<InfinteScroller
|
||||
paginateFront={paginateTop}
|
||||
|
||||
@@ -1,3 +1,37 @@
|
||||
import { addDaysToDate } from '@standardnotes/utils'
|
||||
import { ListableContentItem } from '../Types/ListableContentItem'
|
||||
import { DailyItemsDay } from './DailyItemsDaySection'
|
||||
|
||||
export function dateToDailyDayIdentifier(date: Date): string {
|
||||
return date.toLocaleDateString()
|
||||
}
|
||||
|
||||
export function getDailyWritingStreak(
|
||||
todayItem: DailyItemsDay | undefined,
|
||||
itemsByDateMapping: Record<string, ListableContentItem[]>,
|
||||
) {
|
||||
if (!todayItem) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const startDay = todayItem.date
|
||||
let checkingDayOffsetFromToday = -1
|
||||
let keepLooping = true
|
||||
let streak = 0
|
||||
|
||||
while (keepLooping) {
|
||||
const checkingDay = addDaysToDate(startDay, checkingDayOffsetFromToday)
|
||||
const items = itemsByDateMapping[dateToDailyDayIdentifier(checkingDay)]
|
||||
if (!items || items?.length === 0) {
|
||||
keepLooping = false
|
||||
break
|
||||
}
|
||||
|
||||
streak++
|
||||
checkingDayOffsetFromToday--
|
||||
}
|
||||
|
||||
const hasEntryForToday = itemsByDateMapping[dateToDailyDayIdentifier(todayItem.date)]?.length > 0
|
||||
|
||||
return streak + (hasEntryForToday ? 1 : 0)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import PhotoCaptureModal from '@/Components/CameraCaptureModal/PhotoCaptureModal'
|
||||
import VideoCaptureModal from '@/Components/CameraCaptureModal/VideoCaptureModal'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import Menu from '@/Components/Menu/Menu'
|
||||
import MenuItem from '@/Components/Menu/MenuItem'
|
||||
import Popover from '@/Components/Popover/Popover'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { PhotoRecorder } from '@/Controllers/Moments/PhotoRecorder'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
type Props = {
|
||||
isDailyEntry: boolean
|
||||
isInFilesSmartView: boolean
|
||||
addButtonLabel: string
|
||||
addNewItem: () => void
|
||||
filesController: FilesController
|
||||
}
|
||||
|
||||
const AddItemMenuButton = ({
|
||||
filesController,
|
||||
isDailyEntry,
|
||||
addButtonLabel,
|
||||
isInFilesSmartView,
|
||||
addNewItem,
|
||||
}: Props) => {
|
||||
const addItemButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
const [deviceHasCamera, setDeviceHasCamera] = useState(false)
|
||||
const [captureType, setCaptureType] = useState<'photo' | 'video'>()
|
||||
|
||||
useEffect(() => {
|
||||
const setCameraSupport = async () => {
|
||||
setDeviceHasCamera(await PhotoRecorder.isSupported())
|
||||
}
|
||||
|
||||
void setCameraSupport()
|
||||
}, [])
|
||||
|
||||
const canShowMenu = isInFilesSmartView && deviceHasCamera
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className={classNames(
|
||||
'hidden md:flex',
|
||||
'h-8 w-8 hover:brightness-125',
|
||||
'z-editor-title-bar ml-3 cursor-pointer items-center',
|
||||
`justify-center rounded-full border border-solid border-transparent ${
|
||||
isDailyEntry ? 'bg-danger text-danger-contrast' : 'bg-info text-info-contrast'
|
||||
}`,
|
||||
)}
|
||||
title={addButtonLabel}
|
||||
aria-label={addButtonLabel}
|
||||
onClick={() => {
|
||||
if (canShowMenu) {
|
||||
setIsMenuOpen((isOpen) => !isOpen)
|
||||
} else {
|
||||
addNewItem()
|
||||
}
|
||||
}}
|
||||
ref={addItemButtonRef}
|
||||
>
|
||||
<Icon type="add" size="custom" className="h-5 w-5" />
|
||||
</button>
|
||||
<Popover
|
||||
open={canShowMenu && isMenuOpen}
|
||||
anchorElement={addItemButtonRef.current}
|
||||
togglePopover={() => {
|
||||
setIsMenuOpen((isOpen) => !isOpen)
|
||||
}}
|
||||
side="bottom"
|
||||
align="center"
|
||||
className="py-2"
|
||||
>
|
||||
<Menu a11yLabel={'test'} isOpen={isMenuOpen}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
addNewItem()
|
||||
setIsMenuOpen(false)
|
||||
}}
|
||||
>
|
||||
<Icon type="add" className="mr-2" />
|
||||
{addButtonLabel}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
setCaptureType('photo')
|
||||
setIsMenuOpen(false)
|
||||
}}
|
||||
>
|
||||
<Icon type="camera" className="mr-2" />
|
||||
Take photo
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
setCaptureType('video')
|
||||
setIsMenuOpen(false)
|
||||
}}
|
||||
>
|
||||
<Icon type="camera" className="mr-2" />
|
||||
Record video
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Popover>
|
||||
{captureType === 'photo' && (
|
||||
<PhotoCaptureModal
|
||||
filesController={filesController}
|
||||
close={() => {
|
||||
setCaptureType(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{captureType === 'video' && (
|
||||
<VideoCaptureModal
|
||||
filesController={filesController}
|
||||
close={() => {
|
||||
setCaptureType(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddItemMenuButton
|
||||
+12
-16
@@ -9,6 +9,8 @@ import { isTag, VectorIconNameOrEmoji } from '@standardnotes/snjs'
|
||||
import RoundIconButton from '@/Components/Button/RoundIconButton'
|
||||
import { AnyTag } from '@/Controllers/Navigation/AnyTagType'
|
||||
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
import AddItemMenuButton from './AddItemMenuButton'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -19,6 +21,7 @@ type Props = {
|
||||
isFilesSmartView: boolean
|
||||
optionsSubtitle?: string
|
||||
selectedTag: AnyTag
|
||||
filesController: FilesController
|
||||
}
|
||||
|
||||
const ContentListHeader = ({
|
||||
@@ -30,6 +33,7 @@ const ContentListHeader = ({
|
||||
isFilesSmartView,
|
||||
optionsSubtitle,
|
||||
selectedTag,
|
||||
filesController,
|
||||
}: Props) => {
|
||||
const displayOptionsContainerRef = useRef<HTMLDivElement>(null)
|
||||
const displayOptionsButtonRef = useRef<HTMLButtonElement>(null)
|
||||
@@ -84,23 +88,15 @@ const ContentListHeader = ({
|
||||
|
||||
const AddButton = useMemo(() => {
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
'hidden md:flex',
|
||||
'h-8 w-8 hover:brightness-125',
|
||||
'z-editor-title-bar ml-3 cursor-pointer items-center',
|
||||
`justify-center rounded-full border border-solid border-transparent ${
|
||||
isDailyEntry ? 'bg-danger text-danger-contrast' : 'bg-info text-info-contrast'
|
||||
}`,
|
||||
)}
|
||||
title={addButtonLabel}
|
||||
aria-label={addButtonLabel}
|
||||
onClick={addNewItem}
|
||||
>
|
||||
<Icon type="add" size="custom" className="h-5 w-5" />
|
||||
</button>
|
||||
<AddItemMenuButton
|
||||
isInFilesSmartView={isFilesSmartView}
|
||||
isDailyEntry={isDailyEntry}
|
||||
addButtonLabel={addButtonLabel}
|
||||
addNewItem={addNewItem}
|
||||
filesController={filesController}
|
||||
/>
|
||||
)
|
||||
}, [addButtonLabel, addNewItem, isDailyEntry])
|
||||
}, [addButtonLabel, addNewItem, filesController, isDailyEntry, isFilesSmartView])
|
||||
|
||||
const FolderName = useMemo(() => {
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,7 @@ import Icon from '@/Components/Icon/Icon'
|
||||
import { DropdownItem } from './DropdownItem'
|
||||
import StyledListboxButton from './StyledListboxButton'
|
||||
import StyledListboxOption from './StyledListboxOption'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
|
||||
type DropdownProps = {
|
||||
id: string
|
||||
@@ -14,7 +15,11 @@ type DropdownProps = {
|
||||
value: string
|
||||
onChange: (value: string, item: DropdownItem) => void
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
className?: {
|
||||
wrapper?: string
|
||||
button?: string
|
||||
popover?: string
|
||||
}
|
||||
fullWidth?: boolean
|
||||
portal?: boolean
|
||||
}
|
||||
@@ -63,12 +68,16 @@ const Dropdown: FunctionComponent<DropdownProps> = ({
|
||||
onChange(value, selectedItem)
|
||||
}
|
||||
|
||||
const wrapperClassName = className?.wrapper ?? ''
|
||||
const buttonClassName = className?.button ?? ''
|
||||
const popoverClassName = className?.popover ?? ''
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={wrapperClassName}>
|
||||
<VisuallyHidden id={labelId}>{label}</VisuallyHidden>
|
||||
<ListboxInput value={value} onChange={handleChange} aria-labelledby={labelId} disabled={disabled}>
|
||||
<StyledListboxButton
|
||||
className={`w-full ${!fullWidth ? 'md:w-fit' : ''}`}
|
||||
className={classNames('w-full', !fullWidth && 'md:w-fit', buttonClassName)}
|
||||
children={({ value, label, isExpanded }) => {
|
||||
const current = items.find((item) => item.value === value)
|
||||
const icon = current ? current?.icon : null
|
||||
@@ -82,7 +91,7 @@ const Dropdown: FunctionComponent<DropdownProps> = ({
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ListboxPopover portal={portal} className="sn-dropdown sn-dropdown-popover">
|
||||
<ListboxPopover portal={portal} className={classNames('sn-dropdown sn-dropdown-popover', popoverClassName)}>
|
||||
<div className="sn-component">
|
||||
<ListboxList>
|
||||
{items.map((item) => (
|
||||
|
||||
@@ -339,8 +339,11 @@ class Footer extends AbstractComponent<Props, State> {
|
||||
this.viewControllerManager.quickSettingsMenuController.closeQuickSettingsMenu()
|
||||
}
|
||||
|
||||
openPreferences = () => {
|
||||
openPreferences = (openWhatsNew: boolean) => {
|
||||
this.clickOutsideQuickSettingsMenu()
|
||||
if (openWhatsNew) {
|
||||
this.viewControllerManager.preferencesController.setCurrentPane('whats-new')
|
||||
}
|
||||
this.viewControllerManager.preferencesController.openPreferences()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,47 @@
|
||||
import { compareSemVersions } from '@standardnotes/snjs'
|
||||
import { keyboardStringForShortcut, OPEN_PREFERENCES_COMMAND } from '@standardnotes/ui-services'
|
||||
import { useMemo } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { useCommandService } from '../CommandProvider'
|
||||
import Icon from '../Icon/Icon'
|
||||
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
||||
|
||||
type Props = {
|
||||
openPreferences: () => void
|
||||
openPreferences: (openWhatsNew: boolean) => void
|
||||
}
|
||||
|
||||
const PreferencesButton = ({ openPreferences }: Props) => {
|
||||
const commandService = useCommandService()
|
||||
const application = useApplication()
|
||||
|
||||
const commandService = useCommandService()
|
||||
const shortcut = useMemo(
|
||||
() => keyboardStringForShortcut(commandService.keyboardShortcutForCommand(OPEN_PREFERENCES_COMMAND)),
|
||||
[commandService],
|
||||
)
|
||||
|
||||
const [changelogLastReadVersion, setChangelogLastReadVersion] = useState(() =>
|
||||
application.changelogService.getLastReadVersion(),
|
||||
)
|
||||
const isChangelogUnread = useMemo(
|
||||
() => (changelogLastReadVersion ? compareSemVersions(application.version, changelogLastReadVersion) > 0 : false),
|
||||
[application.version, changelogLastReadVersion],
|
||||
)
|
||||
useEffect(
|
||||
() => application.changelogService.addLastReadChangeListener(setChangelogLastReadVersion),
|
||||
[application.changelogService],
|
||||
)
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
openPreferences(isChangelogUnread)
|
||||
}, [isChangelogUnread, openPreferences])
|
||||
|
||||
return (
|
||||
<StyledTooltip label={`Open preferences (${shortcut})`}>
|
||||
<button onClick={openPreferences} className="flex h-full w-8 cursor-pointer items-center justify-center">
|
||||
<button onClick={onClick} className="relative flex h-full w-8 cursor-pointer items-center justify-center">
|
||||
<div className="h-5">
|
||||
<Icon type="tune" className="rounded hover:text-info" />
|
||||
</div>
|
||||
{isChangelogUnread && <div className="absolute top-0.5 right-0.5 h-2 w-2 rounded-full bg-info" />}
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
)
|
||||
|
||||
@@ -62,6 +62,7 @@ export const IconNameToSvgMapping = {
|
||||
archive: icons.ArchiveIcon,
|
||||
asterisk: icons.AsteriskIcon,
|
||||
authenticator: icons.AuthenticatorIcon,
|
||||
camera: icons.CameraIcon,
|
||||
check: icons.CheckIcon,
|
||||
close: icons.CloseIcon,
|
||||
code: icons.CodeIcon,
|
||||
@@ -106,6 +107,7 @@ export const IconNameToSvgMapping = {
|
||||
tune: icons.TuneIcon,
|
||||
unarchive: icons.UnarchiveIcon,
|
||||
unpin: icons.UnpinIcon,
|
||||
upload: icons.UploadIcon,
|
||||
user: icons.UserIcon,
|
||||
view: icons.ViewIcon,
|
||||
warning: icons.WarningIcon,
|
||||
|
||||
@@ -168,7 +168,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
|
||||
>
|
||||
<BlocksEditor
|
||||
onChange={handleChange}
|
||||
ignoreFirstChange={false}
|
||||
ignoreFirstChange={controller.isTemplateNote}
|
||||
className={classNames(
|
||||
'relative h-full resize-none px-4 py-4 focus:shadow-none focus:outline-none',
|
||||
lineHeight && `leading-${lineHeight.toLowerCase()}`,
|
||||
|
||||
+1
-14
@@ -8,7 +8,6 @@ import { WebApplication } from '@/Application/Application'
|
||||
import { useBeforeUnload } from '@/Hooks/useBeforeUnload'
|
||||
import ChangeEmailForm from './ChangeEmailForm'
|
||||
import ChangeEmailSuccess from './ChangeEmailSuccess'
|
||||
import { isEmailValid } from '@/Utils'
|
||||
|
||||
enum SubmitButtonTitles {
|
||||
Default = 'Continue',
|
||||
@@ -57,18 +56,6 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
|
||||
return success
|
||||
}
|
||||
|
||||
const validateNewEmail = async () => {
|
||||
if (!isEmailValid(newEmail)) {
|
||||
applicationAlertService
|
||||
.alert('The email you entered has an invalid format. Please review your input and try again.')
|
||||
.catch(console.error)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const resetProgressState = () => {
|
||||
setSubmitButtonTitle(SubmitButtonTitles.Default)
|
||||
setIsContinuing(false)
|
||||
@@ -110,7 +97,7 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
|
||||
setIsContinuing(true)
|
||||
setSubmitButtonTitle(SubmitButtonTitles.GeneratingKeys)
|
||||
|
||||
const valid = (await validateCurrentPassword()) && (await validateNewEmail())
|
||||
const valid = await validateCurrentPassword()
|
||||
|
||||
if (!valid) {
|
||||
resetProgressState()
|
||||
|
||||
@@ -186,10 +186,6 @@ const PasscodeLock = ({ application, viewControllerManager }: Props) => {
|
||||
|
||||
const autolockService = application.getAutolockService()
|
||||
|
||||
if (!autolockService) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreferencesGroup>
|
||||
@@ -246,7 +242,7 @@ const PasscodeLock = ({ application, viewControllerManager }: Props) => {
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
|
||||
{hasPasscode && !isNativeMobileWeb && (
|
||||
{hasPasscode && autolockService && (
|
||||
<>
|
||||
<div className="min-h-3" />
|
||||
<PreferencesGroup>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user