Compare commits

...
Author SHA1 Message Date
StandardNotes CI 63c10c703f chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
2022-11-03 14:04:27 +00:00
Aman Harwara 4036113206 fix: files list not updating when new file is uploaded 2022-11-03 19:02:30 +05:30
Aman Harwara de4adca059 fix: "No modifications allowed" error on file drag in Firefox 2022-11-03 19:00:34 +05:30
Aman Harwara 9ae0ce0bfd fix(desktop): editor column top padding on mac 2022-11-03 18:34:59 +05:30
StandardNotes CI 455cdc01fc chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-11-03 12:20:16 +00:00
Mo c4dbc5cbd2 fix: purchase flow close on complete 2022-11-03 06:46:07 -05:00
Mo 305ffdf984 fix: correctly refresh cta banner 2022-11-03 06:35:06 -05:00
Aman Harwara dc4530b2f3 fix(mobile): disable pinch zoom on mobile app UI 2022-11-03 16:49:32 +05:30
Aman Harwara d6cafa1e73 fix(desktop): selected column top padding on small windows 2022-11-03 16:28:28 +05:30
Aman Harwara ac96e197e0 fix(desktop): only add extra popover padding for small windows 2022-11-03 15:59:48 +05:30
StandardNotes CI 636963b409 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
2022-11-03 09:11:06 +00:00
Karol Sójko 7ead0f655b feat: add sending user requests from UI (#1927)
* feat: add sending user requests from UI

* fix(web): view controller manager user client references
2022-11-03 09:39:38 +01:00
StandardNotes CI 6b50372db2 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
2022-11-02 19:15:23 +00:00
Aman Harwara a6ef658dae feat: prioritize loading latest selected items (#1930) 2022-11-03 00:11:17 +05:30
StandardNotes CI 8c49ca5572 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-11-02 17:36:31 +00:00
Mo 35b21b21ce fix: hide subscription marketing on iOS (#1929) 2022-11-02 12:07:26 -05:00
64 changed files with 994 additions and 224 deletions
+25
View File
@@ -3,6 +3,31 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.23.271](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
### Bug Fixes
* **desktop:** editor column top padding on mac ([9ae0ce0](https://github.com/standardnotes/app/commit/9ae0ce0bfd78bedf3e8318aa56b8542dd81d5870))
## [3.23.270](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
### Bug Fixes
* **desktop:** only add extra popover padding for small windows ([ac96e19](https://github.com/standardnotes/app/commit/ac96e197e00a7e7af1b771e46a178b0fa21d0f94))
* **desktop:** selected column top padding on small windows ([d6cafa1](https://github.com/standardnotes/app/commit/d6cafa1e739dd46e4cf465d8511aad2024ebe294))
## [3.23.269](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.268](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.267](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
**Note:** Version bump only for package @standardnotes/desktop
## [3.23.266](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
**Note:** Version bump only for package @standardnotes/desktop
@@ -122,7 +122,7 @@ async function configureWindow(remoteBridge: CrossProcessBridge) {
the app content height so its not overflowing */
sheet.insertRule('body { padding-top: var(--sn-desktop-titlebar-height); }', sheet.cssRules.length)
sheet.insertRule(
'[data-popover] { padding-top: calc(var(--sn-desktop-titlebar-height) + 0.5rem); }',
'@media screen and (max-width: 768px) { [data-popover] { padding-top: calc(var(--sn-desktop-titlebar-height) + 0.5rem); } }',
sheet.cssRules.length,
)
sheet.insertRule(
@@ -9,6 +9,12 @@
transition: 0.15s padding ease;
}
@media screen and (max-width: 768px) {
.mac-desktop .app-column.selected {
padding-top: 18px;
}
}
@media screen and (min-width: 768px) {
.mac-desktop #app.collapsed-notes.collapsed-navigation #editor-column {
padding-top: 18px;
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/desktop",
"main": "./app/dist/index.js",
"version": "3.23.266",
"version": "3.23.271",
"license": "AGPL-3.0-or-later",
"author": "Standard Notes.",
"private": true,
+22
View File
@@ -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.
## [3.45.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
**Note:** Version bump only for package @standardnotes/mobile
## [3.45.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
### Bug Fixes
* **mobile:** disable pinch zoom on mobile app UI ([dc4530b](https://github.com/standardnotes/app/commit/dc4530b2f33057ec3d44a94b28dc0407656d4098))
## [3.45.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
**Note:** Version bump only for package @standardnotes/mobile
## [3.45.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
**Note:** Version bump only for package @standardnotes/mobile
## [3.45.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
**Note:** Version bump only for package @standardnotes/mobile
## [3.45.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
**Note:** Version bump only for package @standardnotes/mobile
@@ -4,7 +4,7 @@
<head>
<meta charset="utf-8" />
<meta content="IE=edge" http-equiv="X-UA-Compatible" />
<meta content="viewport-fit=cover, width=device-width, initial-scale=1" name="viewport" />
<meta content="viewport-fit=cover, width=device-width, initial-scale=1, maximum-scale=1.0" name="viewport" />
<meta content="#ffffff" name="theme-color" />
<link rel="stylesheet" href="web-src/app.css" />
<script>
@@ -23,4 +23,4 @@
<body>
</body>
</html>
</html>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/mobile",
"version": "3.45.15",
"version": "3.45.20",
"author": "Standard Notes.",
"private": true,
"license": "AGPL-3.0-or-later",
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.3.202](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.201](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.200](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.199](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.198](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
**Note:** Version bump only for package @standardnotes/releases
## [1.3.197](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
**Note:** Version bump only for package @standardnotes/releases
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/releases",
"version": "1.3.197",
"version": "1.3.202",
"license": "AGPL-3.0-or-later",
"main": "dist/releases.json",
"types": "dist/index.d.ts",
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.42.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
### Features
* add sending user requests from UI ([#1927](https://github.com/standardnotes/app/issues/1927)) ([7ead0f6](https://github.com/standardnotes/app/commit/7ead0f655bb2572e5805dd26c297d520949d661a))
# [1.41.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
### Features
* prioritize loading latest selected items ([#1930](https://github.com/standardnotes/app/issues/1930)) ([a6ef658](https://github.com/standardnotes/app/commit/a6ef658daef9a01f5ddacc967fe30d22f580799a))
## [1.40.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
**Note:** Version bump only for package @standardnotes/services
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/services",
"version": "1.40.1",
"version": "1.42.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -38,6 +38,7 @@ export enum StorageKey {
ExperimentalFeatures = 'experimental_features',
DeinitMode = 'deinit_mode',
CodeVerifier = 'code_verifier',
LaunchPriorityUuids = 'launch_priority_uuids',
}
export enum NonwrappedStorageKey {
@@ -1,3 +1,4 @@
import { UserRequestType } from '@standardnotes/common'
import { DeinitSource } from '../Application/DeinitSource'
export interface UserClientInterface {
@@ -6,4 +7,5 @@ export interface UserClientInterface {
message?: string
}>
signOut(force?: boolean, source?: DeinitSource): Promise<void>
submitUserRequest(requestType: UserRequestType): Promise<boolean>
}
@@ -0,0 +1,92 @@
import { UserApiServiceInterface } from '@standardnotes/api'
import { UserRequestType } from '@standardnotes/common'
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { User } from '@standardnotes/responses'
import {
AlertService,
ChallengeServiceInterface,
InternalEventBusInterface,
ItemManagerInterface,
ProtectionsClientInterface,
} from '..'
import { SessionsClientInterface } from '../Session/SessionsClientInterface'
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { UserService } from './UserService'
describe('UserService', () => {
let sessionManager: SessionsClientInterface
let syncService: SyncServiceInterface
let storageService: StorageServiceInterface
let itemManager: ItemManagerInterface
let protocolService: EncryptionProviderInterface
let alertService: AlertService
let challengeService: ChallengeServiceInterface
let protectionService: ProtectionsClientInterface
let userApiService: UserApiServiceInterface
let internalEventBus: InternalEventBusInterface
const createService = () =>
new UserService(
sessionManager,
syncService,
storageService,
itemManager,
protocolService,
alertService,
challengeService,
protectionService,
userApiService,
internalEventBus,
)
beforeEach(() => {
sessionManager = {} as jest.Mocked<SessionsClientInterface>
sessionManager.getSureUser = jest.fn().mockReturnValue({ uuid: '1-2-3' } as jest.Mocked<User>)
syncService = {} as jest.Mocked<SyncServiceInterface>
storageService = {} as jest.Mocked<StorageServiceInterface>
itemManager = {} as jest.Mocked<ItemManagerInterface>
protocolService = {} as jest.Mocked<EncryptionProviderInterface>
alertService = {} as jest.Mocked<AlertService>
challengeService = {} as jest.Mocked<ChallengeServiceInterface>
protectionService = {} as jest.Mocked<ProtectionsClientInterface>
userApiService = {} as jest.Mocked<UserApiServiceInterface>
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
})
it('should submit a user request to the server', async () => {
userApiService.submitUserRequest = jest.fn().mockReturnValue({ data: { success: true } })
expect(await createService().submitUserRequest(UserRequestType.ExitDiscount)).toBeTruthy()
})
it('should indicate error if submit a user request to the server fails', async () => {
userApiService.submitUserRequest = jest.fn().mockReturnValue({ data: { success: false } })
expect(await createService().submitUserRequest(UserRequestType.ExitDiscount)).toBeFalsy()
})
it('should indicate error if submit a user request to the server fails with an error on server side', async () => {
userApiService.submitUserRequest = jest.fn().mockReturnValue({ data: { error: { message: 'fail' } } })
expect(await createService().submitUserRequest(UserRequestType.ExitDiscount)).toBeFalsy()
})
it('should indicate error if submitting a user request throws an exception', async () => {
userApiService.submitUserRequest = jest.fn().mockImplementation(() => {
throw new Error('Oops')
})
expect(await createService().submitUserRequest(UserRequestType.ExitDiscount)).toBeFalsy()
})
})
@@ -1,6 +1,6 @@
import { EncryptionProviderInterface, SNRootKey, SNRootKeyParams } from '@standardnotes/encryption'
import { HttpResponse, SignInResponse, User } from '@standardnotes/responses'
import { KeyParamsOrigination } from '@standardnotes/common'
import { KeyParamsOrigination, UserRequestType } from '@standardnotes/common'
import { UuidGenerator } from '@standardnotes/utils'
import { UserApiServiceInterface, UserRegistrationResponseBody } from '@standardnotes/api'
@@ -233,6 +233,24 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
}
}
async submitUserRequest(requestType: UserRequestType): Promise<boolean> {
const userUuid = this.sessionManager.getSureUser().uuid
try {
const result = await this.userApiService.submitUserRequest({
userUuid,
requestType,
})
if (result.data.error) {
return false
}
return result.data.success
} catch (error) {
return false
}
}
/**
* A sign in request that occurs while the user was previously signed in, to correct
* for missing keys or storage values. Unlike regular sign in, this doesn't worry about
+10
View File
@@ -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.
## [2.147.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
**Note:** Version bump only for package @standardnotes/snjs
# [2.147.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
### Features
* prioritize loading latest selected items ([#1930](https://github.com/standardnotes/app/issues/1930)) ([a6ef658](https://github.com/standardnotes/app/commit/a6ef658daef9a01f5ddacc967fe30d22f580799a))
## [2.146.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
**Note:** Version bump only for package @standardnotes/snjs
@@ -35,7 +35,7 @@ export class SNPreferencesService
})
this.removeSyncObserver = syncService.addEventObserver((event) => {
if (event === SyncEvent.SyncCompletedWithAllItemsUploaded) {
if (event === SyncEvent.SyncCompletedWithAllItemsUploaded || event === SyncEvent.LocalDataIncrementalLoad) {
void this.reload()
}
})
@@ -2,6 +2,8 @@ import { SyncOpStatus } from './SyncOpStatus'
import { SyncOptions } from '@standardnotes/services'
export interface SyncClientInterface {
setLaunchPriorityUuids(launchPriorityUuids: string[]): void
sync(options?: Partial<SyncOptions>): Promise<unknown>
isOutOfSync(): boolean
+55 -36
View File
@@ -18,7 +18,7 @@ import { SNHistoryManager } from '../History/HistoryManager'
import { SNLog } from '@Lib/Log'
import { SNSessionManager } from '../Session/SessionManager'
import { DiskStorageService } from '../Storage/DiskStorageService'
import { SortPayloadsByRecentAndContentPriority } from '@Lib/Services/Sync/Utils'
import { GetSortedPayloadsByPriority } from '@Lib/Services/Sync/Utils'
import { SyncClientInterface } from './SyncClientInterface'
import { SyncPromise } from './Types'
import { SyncOpStatus } from '@Lib/Services/Sync/SyncOpStatus'
@@ -56,6 +56,7 @@ import {
PayloadEmitSource,
getIncrementedDirtyIndex,
getCurrentDirtyIndex,
ItemContent,
} from '@standardnotes/models'
import {
AbstractService,
@@ -158,6 +159,14 @@ export class SNSyncService
}
}
private get launchPriorityUuids() {
return this.storageService.getValue<string[]>(StorageKey.LaunchPriorityUuids) ?? []
}
public setLaunchPriorityUuids(launchPriorityUuids: string[]) {
this.storageService.setValue(StorageKey.LaunchPriorityUuids, launchPriorityUuids)
}
public override deinit(): void {
this.dealloced = true
;(this.sessionManager as unknown) = undefined
@@ -272,15 +281,15 @@ export class SNSyncService
})
.filter(isNotUndefined)
const payloads = SortPayloadsByRecentAndContentPriority(unsortedPayloads, this.localLoadPriorty)
const { itemsKeyPayloads, contentTypePriorityPayloads, remainingPayloads } = GetSortedPayloadsByPriority(
unsortedPayloads,
this.localLoadPriorty,
this.launchPriorityUuids,
)
const itemsKeysPayloads = payloads.filter((payload) => {
return payload.content_type === ContentType.ItemsKey
})
await this.processItemsKeysFirstDuringDatabaseLoad(itemsKeyPayloads)
subtractFromArray(payloads, itemsKeysPayloads)
await this.processItemsKeysFirstDuringDatabaseLoad(itemsKeysPayloads)
await this.processPayloadBatch(contentTypePriorityPayloads)
/**
* Map in batches to give interface a chance to update. Note that total decryption
@@ -288,45 +297,55 @@ export class SNSyncService
* batches will result in the same time spent. It's the emitting/painting/rendering
* that requires batch size optimization.
*/
const payloadCount = payloads.length
const payloadCount = remainingPayloads.length
const batchSize = this.options.loadBatchSize
const numBatches = Math.ceil(payloadCount / batchSize)
for (let batchIndex = 0; batchIndex < numBatches; batchIndex++) {
const currentPosition = batchIndex * batchSize
const batch = payloads.slice(currentPosition, currentPosition + batchSize)
const encrypted: EncryptedPayloadInterface[] = []
const nonencrypted: (DecryptedPayloadInterface | DeletedPayloadInterface)[] = []
for (const payload of batch) {
if (isEncryptedPayload(payload)) {
encrypted.push(payload)
} else {
nonencrypted.push(payload)
}
}
const split: KeyedDecryptionSplit = {
usesItemsKeyWithKeyLookup: {
items: encrypted,
},
}
const results = await this.protocolService.decryptSplit(split)
await this.payloadManager.emitPayloads([...nonencrypted, ...results], PayloadEmitSource.LocalDatabaseLoaded)
void this.notifyEvent(SyncEvent.LocalDataIncrementalLoad)
this.opStatus.setDatabaseLoadStatus(currentPosition, payloadCount, false)
await sleep(1, false)
const batch = remainingPayloads.slice(currentPosition, currentPosition + batchSize)
await this.processPayloadBatch(batch, currentPosition, payloadCount)
}
this.databaseLoaded = true
this.opStatus.setDatabaseLoadStatus(0, 0, true)
}
private async processPayloadBatch(
batch: FullyFormedPayloadInterface<ItemContent>[],
currentPosition?: number,
payloadCount?: number,
) {
const encrypted: EncryptedPayloadInterface[] = []
const nonencrypted: (DecryptedPayloadInterface | DeletedPayloadInterface)[] = []
for (const payload of batch) {
if (isEncryptedPayload(payload)) {
encrypted.push(payload)
} else {
nonencrypted.push(payload)
}
}
const split: KeyedDecryptionSplit = {
usesItemsKeyWithKeyLookup: {
items: encrypted,
},
}
const results = await this.protocolService.decryptSplit(split)
await this.payloadManager.emitPayloads([...nonencrypted, ...results], PayloadEmitSource.LocalDatabaseLoaded)
void this.notifyEvent(SyncEvent.LocalDataIncrementalLoad)
if (currentPosition != undefined && payloadCount != undefined) {
this.opStatus.setDatabaseLoadStatus(currentPosition, payloadCount, false)
}
await sleep(1, false)
}
private setLastSyncToken(token: string) {
this.syncToken = token
return this.storageService.setValue(StorageKey.LastSyncToken, token)
@@ -0,0 +1,146 @@
import { ContentType } from '@standardnotes/common'
import { FullyFormedPayloadInterface } from '@standardnotes/models'
import { GetSortedPayloadsByPriority } from './Utils'
describe('GetSortedPayloadsByPriority', () => {
let payloads: FullyFormedPayloadInterface[] = []
const contentTypePriority = [ContentType.ItemsKey, ContentType.UserPrefs, ContentType.Component, ContentType.Theme]
let launchPriorityUuids: string[] = []
it('should sort payloads based on content type priority', () => {
payloads = [
{
content_type: ContentType.Theme,
} as FullyFormedPayloadInterface,
{
content_type: ContentType.UserPrefs,
} as FullyFormedPayloadInterface,
{
content_type: ContentType.Component,
} as FullyFormedPayloadInterface,
{
content_type: ContentType.ItemsKey,
} as FullyFormedPayloadInterface,
{
content_type: ContentType.Note,
} as FullyFormedPayloadInterface,
]
const { itemsKeyPayloads, contentTypePriorityPayloads, remainingPayloads } = GetSortedPayloadsByPriority(
payloads,
contentTypePriority,
launchPriorityUuids,
)
expect(itemsKeyPayloads.length).toBe(1)
expect(itemsKeyPayloads[0].content_type).toBe(ContentType.ItemsKey)
expect(contentTypePriorityPayloads.length).toBe(3)
expect(contentTypePriorityPayloads[0].content_type).toBe(ContentType.UserPrefs)
expect(contentTypePriorityPayloads[1].content_type).toBe(ContentType.Component)
expect(contentTypePriorityPayloads[2].content_type).toBe(ContentType.Theme)
expect(remainingPayloads.length).toBe(1)
expect(remainingPayloads[0].content_type).toBe(ContentType.Note)
})
it('should sort payloads based on launch priority uuids', () => {
const unprioritizedNoteUuid = 'unprioritized-note'
const unprioritizedTagUuid = 'unprioritized-tag'
const prioritizedNoteUuid = 'prioritized-note'
const prioritizedTagUuid = 'prioritized-tag'
payloads = [
{
content_type: ContentType.Theme,
} as FullyFormedPayloadInterface,
{
content_type: ContentType.UserPrefs,
} as FullyFormedPayloadInterface,
{
content_type: ContentType.Component,
} as FullyFormedPayloadInterface,
{
content_type: ContentType.ItemsKey,
} as FullyFormedPayloadInterface,
{
content_type: ContentType.Note,
uuid: unprioritizedNoteUuid,
} as FullyFormedPayloadInterface,
{
content_type: ContentType.Tag,
uuid: unprioritizedTagUuid,
} as FullyFormedPayloadInterface,
{
content_type: ContentType.Note,
uuid: prioritizedNoteUuid,
} as FullyFormedPayloadInterface,
{
content_type: ContentType.Tag,
uuid: prioritizedTagUuid,
} as FullyFormedPayloadInterface,
]
launchPriorityUuids = [prioritizedNoteUuid, prioritizedTagUuid]
const { itemsKeyPayloads, contentTypePriorityPayloads, remainingPayloads } = GetSortedPayloadsByPriority(
payloads,
contentTypePriority,
launchPriorityUuids,
)
expect(itemsKeyPayloads.length).toBe(1)
expect(itemsKeyPayloads[0].content_type).toBe(ContentType.ItemsKey)
expect(contentTypePriorityPayloads.length).toBe(3)
expect(contentTypePriorityPayloads[0].content_type).toBe(ContentType.UserPrefs)
expect(contentTypePriorityPayloads[1].content_type).toBe(ContentType.Component)
expect(contentTypePriorityPayloads[2].content_type).toBe(ContentType.Theme)
expect(remainingPayloads.length).toBe(4)
expect(remainingPayloads[0].uuid).toBe(prioritizedNoteUuid)
expect(remainingPayloads[1].uuid).toBe(prioritizedTagUuid)
expect(remainingPayloads[2].uuid).toBe(unprioritizedNoteUuid)
expect(remainingPayloads[3].uuid).toBe(unprioritizedTagUuid)
})
it('should sort payloads based on server updated date if same content type', () => {
const unprioritizedNoteUuid = 'unprioritized-note'
const unprioritizedTagUuid = 'unprioritized-tag'
const prioritizedNoteUuid = 'prioritized-note'
const prioritizedTagUuid = 'prioritized-tag'
payloads = [
{
content_type: ContentType.Note,
uuid: unprioritizedNoteUuid,
serverUpdatedAt: new Date(1),
} as FullyFormedPayloadInterface,
{
content_type: ContentType.Tag,
uuid: unprioritizedTagUuid,
serverUpdatedAt: new Date(2),
} as FullyFormedPayloadInterface,
{
content_type: ContentType.Note,
uuid: prioritizedNoteUuid,
} as FullyFormedPayloadInterface,
{
content_type: ContentType.Tag,
uuid: prioritizedTagUuid,
} as FullyFormedPayloadInterface,
]
launchPriorityUuids = [prioritizedNoteUuid, prioritizedTagUuid]
const { remainingPayloads } = GetSortedPayloadsByPriority(payloads, contentTypePriority, launchPriorityUuids)
expect(remainingPayloads.length).toBe(4)
expect(remainingPayloads[0].uuid).toBe(prioritizedNoteUuid)
expect(remainingPayloads[1].uuid).toBe(prioritizedTagUuid)
expect(remainingPayloads[2].uuid).toBe(unprioritizedTagUuid)
expect(remainingPayloads[3].uuid).toBe(unprioritizedNoteUuid)
})
})
+84 -13
View File
@@ -1,3 +1,4 @@
import { UuidString } from '@Lib/Types'
import { ContentType } from '@standardnotes/common'
import { FullyFormedPayloadInterface } from '@standardnotes/models'
@@ -6,9 +7,9 @@ import { FullyFormedPayloadInterface } from '@standardnotes/models'
* whereby the earlier a content_type appears in the priorityList,
* the earlier it will appear in the resulting sorted array.
*/
export function SortPayloadsByRecentAndContentPriority(
function SortPayloadsByRecentAndContentPriority(
payloads: FullyFormedPayloadInterface[],
priorityList: ContentType[],
contentTypePriorityList: ContentType[],
): FullyFormedPayloadInterface[] {
return payloads.sort((a, b) => {
const dateResult = new Date(b.serverUpdatedAt).getTime() - new Date(a.serverUpdatedAt).getTime()
@@ -16,18 +17,15 @@ export function SortPayloadsByRecentAndContentPriority(
let aPriority = 0
let bPriority = 0
if (priorityList) {
aPriority = priorityList.indexOf(a.content_type)
bPriority = priorityList.indexOf(b.content_type)
aPriority = contentTypePriorityList.indexOf(a.content_type)
bPriority = contentTypePriorityList.indexOf(b.content_type)
if (aPriority === -1) {
/** Not found in list, not prioritized. Set it to max value */
aPriority = priorityList.length
}
if (bPriority === -1) {
/** Not found in list, not prioritized. Set it to max value */
bPriority = priorityList.length
}
if (aPriority === -1) {
aPriority = contentTypePriorityList.length
}
if (bPriority === -1) {
bPriority = contentTypePriorityList.length
}
if (aPriority === bPriority) {
@@ -41,3 +39,76 @@ export function SortPayloadsByRecentAndContentPriority(
}
})
}
/**
* Sorts payloads according by most recently modified first, according to the priority,
* whereby the earlier a uuid appears in the priorityList,
* the earlier it will appear in the resulting sorted array.
*/
function SortPayloadsByRecentAndUuidPriority(
payloads: FullyFormedPayloadInterface[],
uuidPriorityList: UuidString[],
): FullyFormedPayloadInterface[] {
return payloads.sort((a, b) => {
const dateResult = new Date(b.serverUpdatedAt).getTime() - new Date(a.serverUpdatedAt).getTime()
let aPriority = 0
let bPriority = 0
aPriority = uuidPriorityList.indexOf(a.uuid)
bPriority = uuidPriorityList.indexOf(b.uuid)
if (aPriority === -1) {
aPriority = uuidPriorityList.length
}
if (bPriority === -1) {
bPriority = uuidPriorityList.length
}
if (aPriority === bPriority) {
return dateResult
}
if (aPriority < bPriority) {
return -1
} else {
return 1
}
})
}
export function GetSortedPayloadsByPriority(
payloads: FullyFormedPayloadInterface[],
contentTypePriorityList: ContentType[],
uuidPriorityList: UuidString[],
): {
itemsKeyPayloads: FullyFormedPayloadInterface[]
contentTypePriorityPayloads: FullyFormedPayloadInterface[]
remainingPayloads: FullyFormedPayloadInterface[]
} {
const itemsKeyPayloads: FullyFormedPayloadInterface[] = []
const contentTypePriorityPayloads: FullyFormedPayloadInterface[] = []
const remainingPayloads: FullyFormedPayloadInterface[] = []
for (let index = 0; index < payloads.length; index++) {
const payload = payloads[index]
if (payload.content_type === ContentType.ItemsKey) {
itemsKeyPayloads.push(payload)
} else if (contentTypePriorityList.includes(payload.content_type)) {
contentTypePriorityPayloads.push(payload)
} else {
remainingPayloads.push(payload)
}
}
return {
itemsKeyPayloads,
contentTypePriorityPayloads: SortPayloadsByRecentAndContentPriority(
contentTypePriorityPayloads,
contentTypePriorityList,
),
remainingPayloads: SortPayloadsByRecentAndUuidPriority(remainingPayloads, uuidPriorityList),
}
}
@@ -672,10 +672,10 @@ describe('online syncing', function () {
const payload = Factory.createStorageItemPayload(contentTypes[Math.floor(i / 2)])
originalPayloads.push(payload)
}
const sorted = SortPayloadsByRecentAndContentPriority(originalPayloads, ['C', 'A', 'B'])
expect(sorted[0].content_type).to.equal('C')
expect(sorted[2].content_type).to.equal('A')
expect(sorted[4].content_type).to.equal('B')
const { contentTypePriorityPayloads } = GetSortedPayloadsByPriority(originalPayloads, ['C', 'A', 'B'])
expect(contentTypePriorityPayloads[0].content_type).to.equal('C')
expect(contentTypePriorityPayloads[2].content_type).to.equal('A')
expect(contentTypePriorityPayloads[4].content_type).to.equal('B')
})
it('should sign in and retrieve large number of items', async function () {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/snjs",
"version": "2.146.1",
"version": "2.147.1",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.13.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
### Features
* add sending user requests from UI ([#1927](https://github.com/standardnotes/app/issues/1927)) ([7ead0f6](https://github.com/standardnotes/app/commit/7ead0f655bb2572e5805dd26c297d520949d661a))
# [1.12.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
### Features
* prioritize loading latest selected items ([#1930](https://github.com/standardnotes/app/issues/1930)) ([a6ef658](https://github.com/standardnotes/app/commit/a6ef658daef9a01f5ddacc967fe30d22f580799a))
## [1.11.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
**Note:** Version bump only for package @standardnotes/ui-services
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/ui-services",
"version": "1.11.1",
"version": "1.13.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -0,0 +1,5 @@
import { UserRequestType } from '@standardnotes/common'
export type UserRequestParams = {
requestType: UserRequestType
}
@@ -3,4 +3,5 @@ export enum RootQueryParam {
Settings = 'settings',
DemoToken = 'demo-token',
AcceptSubscriptionInvite = 'accept-subscription-invite',
UserRequest = 'user-request',
}
@@ -1,3 +1,5 @@
import { UserRequestType } from '@standardnotes/common'
import { RouteParser } from './RouteParser'
import { RouteType } from './RouteType'
@@ -56,4 +58,12 @@ describe('route parser', () => {
expect(parser.type).toEqual(RouteType.AcceptSubscriptionInvite)
expect(parser.subscriptionInviteParams.inviteUuid).toEqual('1-2-3')
})
it('routes to user request', () => {
const url = 'https://app.standardnotes.com/?user-request=exit-discount'
const parser = new RouteParser(url)
expect(parser.type).toEqual(RouteType.UserRequest)
expect(parser.userRequestParams.requestType).toEqual(UserRequestType.ExitDiscount)
})
})
+11 -1
View File
@@ -1,10 +1,11 @@
import { Uuid } from '@standardnotes/common'
import { UserRequestType, Uuid } from '@standardnotes/common'
import { PreferenceId } from './../Preferences/PreferenceId'
import { DemoParams } from './Params/DemoParams'
import { OnboardingParams } from './Params/OnboardingParams'
import { PurchaseParams } from './Params/PurchaseParams'
import { SettingsParams } from './Params/SettingsParams'
import { SubscriptionInviteParams } from './Params/SubscriptionInviteParams'
import { UserRequestParams } from './Params/UserRequestParams'
import { RootQueryParam } from './RootQueryParam'
import { RootRoutes } from './RootRoutes'
@@ -28,6 +29,14 @@ export class RouteParser implements RouteParserInterface {
return this.parsedType
}
get userRequestParams(): UserRequestParams {
this.checkForProperRouteType(RouteType.UserRequest)
return {
requestType: this.searchParams.get(RootQueryParam.UserRequest) as UserRequestType,
}
}
get subscriptionInviteParams(): SubscriptionInviteParams {
this.checkForProperRouteType(RouteType.AcceptSubscriptionInvite)
@@ -89,6 +98,7 @@ export class RouteParser implements RouteParserInterface {
[RootQueryParam.Settings, RouteType.Settings],
[RootQueryParam.DemoToken, RouteType.Demo],
[RootQueryParam.AcceptSubscriptionInvite, RouteType.AcceptSubscriptionInvite],
[RootQueryParam.UserRequest, RouteType.UserRequest],
])
for (const rootQueryParam of rootQueryParametersMap.keys()) {
@@ -3,6 +3,7 @@ import { OnboardingParams } from './Params/OnboardingParams'
import { PurchaseParams } from './Params/PurchaseParams'
import { SettingsParams } from './Params/SettingsParams'
import { SubscriptionInviteParams } from './Params/SubscriptionInviteParams'
import { UserRequestParams } from './Params/UserRequestParams'
import { RouteType } from './RouteType'
export interface RouteParserInterface {
@@ -11,5 +12,6 @@ export interface RouteParserInterface {
get purchaseParams(): PurchaseParams
get onboardingParams(): OnboardingParams
get subscriptionInviteParams(): SubscriptionInviteParams
get userRequestParams(): UserRequestParams
get type(): RouteType
}
@@ -3,6 +3,7 @@ export enum RouteType {
Settings = 'settings',
Purchase = 'purchase',
AcceptSubscriptionInvite = 'accept-subscription-invite',
UserRequest = 'user-request',
Demo = 'demo',
None = 'none',
}
@@ -0,0 +1,17 @@
export enum PersistenceKey {
SelectedItemsController = 'selected-items-controller',
NavigationController = 'navigation-controller',
}
export type SelectionControllerPersistableValue = {
selectedUuids: string[]
}
export type NavigationControllerPersistableValue = {
selectedTagUuid: string
}
export type PersistedStateValue = {
[PersistenceKey.SelectedItemsController]: SelectionControllerPersistableValue
[PersistenceKey.NavigationController]: NavigationControllerPersistableValue
}
@@ -1,8 +1,11 @@
import { PersistedStateValue } from '../StatePersistence/StatePersistence'
export enum StorageKey {
AnonymousUserId = 'AnonymousUserId',
ShowBetaWarning = 'ShowBetaWarning',
ShowNoAccountWarning = 'ShowNoAccountWarning',
FilesNavigationEnabled = 'FilesNavigationEnabled',
MasterStatePersistenceKey = 'master-persistence-key',
}
export type StorageValue = {
@@ -10,6 +13,7 @@ export type StorageValue = {
[StorageKey.ShowBetaWarning]: boolean
[StorageKey.ShowNoAccountWarning]: boolean
[StorageKey.FilesNavigationEnabled]: boolean
[StorageKey.MasterStatePersistenceKey]: PersistedStateValue
}
export const storage = {
@@ -1,10 +1,14 @@
import { addToast, ToastType } from '@standardnotes/toast'
import { addToast, dismissToast, ToastType } from '@standardnotes/toast'
import { ToastServiceInterface } from './ToastServiceInterface'
export class ToastService implements ToastServiceInterface {
showToast(type: ToastType, message: string): void {
addToast({
hideToast(toastId: string): void {
dismissToast(toastId)
}
showToast(type: ToastType, message: string): string {
return addToast({
type: type,
message,
})
@@ -1,5 +1,6 @@
import { ToastType } from '@standardnotes/toast'
export interface ToastServiceInterface {
showToast(type: ToastType, message: string): void
showToast(type: ToastType, message: string): string
hideToast(toastId: string): void
}
+2
View File
@@ -8,6 +8,7 @@ export * from './Route/Params/OnboardingParams'
export * from './Route/Params/PurchaseParams'
export * from './Route/Params/SettingsParams'
export * from './Route/Params/SubscriptionInviteParams'
export * from './Route/Params/UserRequestParams'
export * from './Route/RootQueryParam'
export * from './Route/RouteParser'
export * from './Route/RouteParserInterface'
@@ -20,3 +21,4 @@ export * from './Storage/LocalStorage'
export * from './Theme/ThemeManager'
export * from './Toast/ToastService'
export * from './Toast/ToastServiceInterface'
export * from './StatePersistence/StatePersistence'
+32
View File
@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.93.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
### Bug Fixes
* "No modifications allowed" error on file drag in Firefox ([de4adca](https://github.com/standardnotes/app/commit/de4adca059b169e75267c8a29012e9988a607bc2))
* files list not updating when new file is uploaded ([4036113](https://github.com/standardnotes/app/commit/40361132063e70e9851baf01eb772a0b3ff0aa1c))
## [3.93.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
### Bug Fixes
* correctly refresh cta banner ([305ffdf](https://github.com/standardnotes/app/commit/305ffdf9847cfe67e2d022f84c1e2443b4bc2c76))
* purchase flow close on complete ([c4dbc5c](https://github.com/standardnotes/app/commit/c4dbc5cbd2147b695403e6f57254a97d42337965))
# [3.93.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-03)
### Features
* add sending user requests from UI ([#1927](https://github.com/standardnotes/app/issues/1927)) ([7ead0f6](https://github.com/standardnotes/app/commit/7ead0f655bb2572e5805dd26c297d520949d661a))
# [3.92.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
### Features
* prioritize loading latest selected items ([#1930](https://github.com/standardnotes/app/issues/1930)) ([a6ef658](https://github.com/standardnotes/app/commit/a6ef658daef9a01f5ddacc967fe30d22f580799a))
## [3.91.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
### Bug Fixes
* hide subscription marketing on iOS ([#1929](https://github.com/standardnotes/app/issues/1929)) ([35b21b2](https://github.com/standardnotes/app/commit/35b21b21ce2438540d799b5a2f04fe5616bf2c3e))
# [3.91.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-02)
### Features
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/web",
"version": "3.91.0",
"version": "3.93.2",
"license": "AGPL-3.0-or-later",
"main": "dist/app.js",
"author": "Standard Notes.",
@@ -203,6 +203,14 @@ export class WebApplication extends SNApplication implements WebApplicationInter
return undefined
}
isNativeIOS() {
return this.isNativeMobileWeb() && this.platform === Platform.Ios
}
get hideSubscriptionMarketing() {
return this.isNativeIOS()
}
mobileDevice(): MobileDeviceInterface {
if (!this.isNativeMobileWeb()) {
throw Error('Attempting to access device as mobile device on non mobile platform')
@@ -110,6 +110,7 @@ const ContentListView: FunctionComponent<Props> = ({
renderedItems,
items,
searchBarElement,
isCurrentNoteTemplate,
} = itemListController
const { selectedUuids, selectNextItem, selectPreviousItem } = selectionController
@@ -245,13 +246,13 @@ const ContentListView: FunctionComponent<Props> = ({
)
useEffect(() => {
const hasEditorPane = selectedUuids.size > 0
const hasEditorPane = selectedUuids.size > 0 || renderedItems.length === 0 || isCurrentNoteTemplate
if (!hasEditorPane) {
itemsViewPanelRef.current?.style.removeProperty('width')
}
}, [selectedUuids, itemsViewPanelRef])
}, [selectedUuids, itemsViewPanelRef, isCurrentNoteTemplate, renderedItems])
const hasEditorPane = selectedUuids.size > 0 || renderedItems.length === 0
const hasEditorPane = selectedUuids.size > 0 || renderedItems.length === 0 || isCurrentNoteTemplate
return (
<div
@@ -118,7 +118,7 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
void changePreferences({ sortBy: sort })
}
},
[preferences, changePreferences, toggleSortReverse],
[preferences.sortBy, toggleSortReverse, changePreferences],
)
const toggleSortByDateModified = useCallback(() => {
@@ -212,14 +212,17 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
{!DailyEntryModeEnabled &&
'Create powerful workflows and organizational layouts with per-tag display preferences.'}
</p>
<Button
primary
small
className="col-start-1 col-end-3 mt-3 justify-self-start uppercase"
onClick={() => application.openPurchaseFlow()}
>
Upgrade Features
</Button>
{!application.hideSubscriptionMarketing && (
<Button
primary
small
className="col-start-1 col-end-3 mt-3 justify-self-start uppercase"
onClick={() => application.openPurchaseFlow()}
>
Upgrade Features
</Button>
)}
</div>
)
@@ -100,6 +100,20 @@ const FileDragNDropProvider = ({ application, children, featuresController, file
[application],
)
const handleDragStart = useCallback(
(event: DragEvent) => {
if (isHandlingFileDrag(event, application)) {
event.preventDefault()
event.stopPropagation()
if (event.dataTransfer) {
event.dataTransfer.clearData()
}
}
},
[application],
)
const handleDragIn = useCallback(
(event: DragEvent) => {
if (!isHandlingFileDrag(event, application)) {
@@ -200,7 +214,6 @@ const FileDragNDropProvider = ({ application, children, featuresController, file
}
})
event.dataTransfer.clearData()
dragCounter.current = 0
}
},
@@ -208,18 +221,20 @@ const FileDragNDropProvider = ({ application, children, featuresController, file
)
useEffect(() => {
window.addEventListener('dragstart', handleDragStart)
window.addEventListener('dragenter', handleDragIn)
window.addEventListener('dragleave', handleDragOut)
window.addEventListener('dragover', handleDrag)
window.addEventListener('drop', handleDrop)
return () => {
window.removeEventListener('dragstart', handleDragStart)
window.removeEventListener('dragenter', handleDragIn)
window.removeEventListener('dragleave', handleDragOut)
window.removeEventListener('dragover', handleDrag)
window.removeEventListener('drop', handleDrop)
}
}, [handleDragIn, handleDrop, handleDrag, handleDragOut])
}, [handleDragIn, handleDrop, handleDrag, handleDragOut, handleDragStart])
const contextValue = useMemo(() => {
return {
@@ -386,6 +386,7 @@ class Footer extends AbstractComponent<Props, State> {
<UpgradeNow
application={this.application}
featuresController={this.viewControllerManager.featuresController}
subscriptionContoller={this.viewControllerManager.subscriptionController}
/>
{this.state.showBetaWarning && (
<Fragment>
@@ -1,16 +1,22 @@
import { WebApplication } from '@/Application/Application'
import { FeaturesController } from '@/Controllers/FeaturesController'
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
import { observer } from 'mobx-react-lite'
import { loadPurchaseFlowUrl } from '../PurchaseFlow/PurchaseFlowFunctions'
type Props = {
application: WebApplication
featuresController: FeaturesController
subscriptionContoller: SubscriptionController
}
const UpgradeNow = ({ application, featuresController }: Props) => {
const UpgradeNow = ({ application, featuresController, subscriptionContoller }: Props) => {
const shouldShowCTA = !featuresController.hasFolders
const hasAccount = application.hasAccount()
const hasAccount = subscriptionContoller.hasAccount
if (hasAccount && subscriptionContoller.hideSubscriptionMarketing) {
return null
}
return shouldShowCTA ? (
<div className="flex h-full items-center px-2">
@@ -286,11 +286,13 @@ const NotesOptions = ({
const switchClassNames = classNames(textClassNames, defaultClassNames, 'justify-between')
const firstItemClass = 'pt-4'
return (
<>
{notes.length === 1 && (
<>
<button className={defaultClassNames} onClick={openRevisionHistoryModal}>
<button className={classNames(defaultClassNames, firstItemClass)} onClick={openRevisionHistoryModal}>
<Icon type="history" className={iconClass} />
Note history
</button>
@@ -31,12 +31,14 @@ const NoSubscription: FunctionComponent<Props> = ({ application }) => {
<Text>You don't have a Standard Notes subscription yet.</Text>
{isLoadingPurchaseFlow && <Text>Redirecting you to the subscription page...</Text>}
{purchaseFlowError && <Text className="text-danger">{purchaseFlowError}</Text>}
<div className="flex">
<LinkButton className="mt-3 mr-3 min-w-20" label="Learn More" link={window.plansUrl as string} />
{application.hasAccount() && (
<Button className="mt-3 min-w-20" primary label="Subscribe" onClick={onPurchaseClick} />
)}
</div>
{!application.hideSubscriptionMarketing && (
<div className="flex">
<LinkButton className="mt-3 mr-3 min-w-20" label="Learn More" link={window.plansUrl as string} />
{application.hasAccount() && (
<Button className="mt-3 min-w-20" primary label="Subscribe" onClick={onPurchaseClick} />
)}
</div>
)}
</>
)
}
@@ -34,12 +34,15 @@ const NoProSubscription: FunctionComponent<Props> = ({ application }) => {
</Text>
{isLoadingPurchaseFlow && <Text>Redirecting you to the subscription page...</Text>}
{purchaseFlowError && <Text className="text-danger">{purchaseFlowError}</Text>}
<div className="flex">
<LinkButton className="mt-3 mr-3 min-w-20" label="Learn More" link={window.plansUrl as string} />
{application.hasAccount() && (
<Button className="mt-3 min-w-20" primary label="Upgrade" onClick={onPurchaseClick} />
)}
</div>
{!application.hideSubscriptionMarketing && (
<div className="flex">
<LinkButton className="mt-3 mr-3 min-w-20" label="Learn More" link={window.plansUrl as string} />
{application.hasAccount() && (
<Button className="mt-3 min-w-20" primary label="Upgrade" onClick={onPurchaseClick} />
)}
</div>
)}
</>
)
}
@@ -17,6 +17,12 @@ const Persistence = ({ application }: Props) => {
const toggleStatePersistence = (shouldPersist: boolean) => {
application.setValue(ShouldPersistNoteStateKey, shouldPersist)
setShouldPersistNoteState(shouldPersist)
if (shouldPersist) {
application.getViewControllerManager().persistValues()
} else {
application.getViewControllerManager().clearPersistedValues()
}
}
return (
@@ -61,15 +61,17 @@ const PremiumFeaturesModal: FunctionComponent<Props> = ({
To take advantage of <span className="font-semibold">{featureName}</span> and other advanced features,
upgrade your current plan.
</AlertDialogDescription>
<div className="p-4">
<button
onClick={handleClick}
className="no-border w-full cursor-pointer rounded bg-info py-2 font-bold text-info-contrast hover:brightness-125 focus:brightness-125"
ref={plansButtonRef}
>
Upgrade
</button>
</div>
{!application.hideSubscriptionMarketing && (
<div className="p-4">
<button
onClick={handleClick}
className="no-border w-full cursor-pointer rounded bg-info py-2 font-bold text-info-contrast hover:brightness-125 focus:brightness-125"
ref={plansButtonRef}
>
Upgrade
</button>
</div>
)}
</div>
</div>
</AlertDialog>
@@ -91,10 +91,15 @@ const CreateAccount: FunctionComponent<Props> = ({ viewControllerManager, applic
try {
await application.register(email, password)
loadPurchaseFlowUrl(application).catch((err) => {
console.error(err)
application.alertService.alert(err).catch(console.error)
})
viewControllerManager.purchaseFlowController.closePurchaseFlow()
if (!application.hideSubscriptionMarketing) {
loadPurchaseFlowUrl(application).catch((err) => {
console.error(err)
application.alertService.alert(err).catch(console.error)
})
}
} catch (err) {
console.error(err)
application.alertService.alert(err as string).catch(console.error)
@@ -74,10 +74,14 @@ const SignIn: FunctionComponent<Props> = ({ viewControllerManager, application }
if (response.error || response.data?.error) {
throw new Error(response.error?.message || response.data?.error?.message)
} else {
loadPurchaseFlowUrl(application).catch((err) => {
console.error(err)
application.alertService.alert(err).catch(console.error)
})
viewControllerManager.purchaseFlowController.closePurchaseFlow()
if (!application.hideSubscriptionMarketing) {
loadPurchaseFlowUrl(application).catch((err) => {
console.error(err)
application.alertService.alert(err).catch(console.error)
})
}
}
} catch (err) {
console.error(err)
@@ -2,6 +2,7 @@ import { WebApplication } from '@/Application/Application'
import { FunctionComponent, MouseEventHandler, useCallback } from 'react'
import Switch from '@/Components/Switch/Switch'
import { isMobileScreen } from '@/Utils'
import { classNames } from '@/Utils/ConcatenateClassNames'
type Props = {
application: WebApplication
@@ -29,7 +30,11 @@ const FocusModeSwitch: FunctionComponent<Props> = ({ application, onToggle, onCl
return (
<button
className="group flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none disabled:bg-default disabled:text-passive-2"
className={classNames(
'group flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left',
'text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none disabled:bg-default disabled:text-passive-2',
'text-mobile-menu-item md:text-tablet-menu-item lg:text-menu-item',
)}
onClick={toggle}
>
<div className="flex items-center">Focused Writing</div>
@@ -53,7 +53,7 @@ const PanelSettingsSection = ({ application }: Props) => {
}, [application])
return (
<div className="hidden text-sm md:block pointer-coarse:md-only:hidden pointer-coarse:lg-only:hidden">
<div className="hidden md:block pointer-coarse:md-only:hidden pointer-coarse:lg-only:hidden">
<MenuItem
type={MenuItemType.SwitchButton}
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
@@ -21,6 +21,7 @@ import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { QuickSettingsController } from '@/Controllers/QuickSettingsController'
import PanelSettingsSection from './PanelSettingsSection'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { classNames } from '@/Utils/ConcatenateClassNames'
const focusModeAnimationDuration = 1255
@@ -173,7 +174,11 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, quickSet
<div className="my-1 px-3 text-sm font-semibold uppercase text-text">Tools</div>
{toggleableComponents.map((component) => (
<button
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
className={classNames(
'flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left',
'text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none',
'text-mobile-menu-item md:text-tablet-menu-item lg:text-menu-item',
)}
onClick={() => {
toggleComponent(component)
}}
@@ -191,7 +196,11 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, quickSet
)}
<div className="my-1 px-3 text-sm font-semibold uppercase text-text">Appearance</div>
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
className={classNames(
'flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left',
'text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none',
'text-mobile-menu-item md:text-tablet-menu-item lg:text-menu-item',
)}
onClick={toggleDefaultTheme}
ref={defaultThemeButtonRef}
>
@@ -8,6 +8,7 @@ import { ThemeItem } from './ThemeItem'
import RadioIndicator from '../Radio/RadioIndicator'
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
import { isMobileScreen } from '@/Utils'
import { classNames } from '@/Utils/ConcatenateClassNames'
type Props = {
item: ThemeItem
@@ -54,9 +55,11 @@ const ThemesMenuButton: FunctionComponent<Props> = ({ application, item }) => {
return (
<button
className={
'group flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none disabled:bg-default disabled:text-passive-2 md:text-sm'
}
className={classNames(
'group flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5',
'text-left text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none disabled:bg-default disabled:text-passive-2',
'text-mobile-menu-item md:text-tablet-menu-item lg:text-menu-item',
)}
onClick={toggleTheme}
>
{item.component?.isLayerable() ? (
@@ -69,7 +69,11 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
label="Go to items list"
icon="chevron-left"
/>
<UpgradeNow application={application} featuresController={viewControllerManager.featuresController} />
<UpgradeNow
application={application}
subscriptionContoller={viewControllerManager.subscriptionController}
featuresController={viewControllerManager.featuresController}
/>
<RoundIconButton
className="ml-2.5 bg-default"
onClick={() => {
@@ -1,20 +1,12 @@
import { WebApplication } from '@/Application/Application'
import { ShouldPersistNoteStateKey } from '@/Components/Preferences/Panes/General/Persistence'
import { ApplicationEvent, InternalEventBus } from '@standardnotes/snjs'
import { ApplicationEvent, ContentType, InternalEventBus } from '@standardnotes/snjs'
import { PersistedStateValue, StorageKey } from '@standardnotes/ui-services'
import { CrossControllerEvent } from '../CrossControllerEvent'
const MasterPersistenceKey = 'master-persistence-key'
export enum PersistenceKey {
SelectedItemsController = 'selected-items-controller',
NavigationController = 'navigation-controller',
ItemListController = 'item-list-controller',
}
export type MasterPersistedValue = Record<PersistenceKey, unknown>
export class PersistenceService {
private unsubAppEventObserver: () => void
private didHydrateOnce = false
constructor(private application: WebApplication, private eventBus: InternalEventBus) {
this.unsubAppEventObserver = this.application.addEventObserver(async (eventName) => {
@@ -27,31 +19,54 @@ export class PersistenceService {
}
async onAppEvent(eventName: ApplicationEvent) {
if (eventName === ApplicationEvent.LocalDataLoaded) {
let shouldHydrateState = this.application.getValue(ShouldPersistNoteStateKey)
if (eventName === ApplicationEvent.LocalDataLoaded && !this.didHydrateOnce) {
this.hydratePersistedValues()
this.didHydrateOnce = true
} else if (eventName === ApplicationEvent.LocalDataIncrementalLoad) {
const canHydrate = this.application.items.getItems([ContentType.Note, ContentType.Tag]).length > 0
if (typeof shouldHydrateState === 'undefined') {
this.application.setValue(ShouldPersistNoteStateKey, true)
shouldHydrateState = true
if (!canHydrate) {
return
}
this.eventBus.publish({
type: CrossControllerEvent.HydrateFromPersistedValues,
payload: shouldHydrateState ? this.getPersistedValues() : undefined,
})
this.hydratePersistedValues()
this.didHydrateOnce = true
}
}
persistValues(values: MasterPersistedValue): void {
get persistenceEnabled() {
return this.application.getValue(ShouldPersistNoteStateKey) ?? true
}
hydratePersistedValues = () => {
this.eventBus.publish({
type: CrossControllerEvent.HydrateFromPersistedValues,
payload: this.persistenceEnabled ? this.getPersistedValues() : undefined,
})
}
persistValues(values: PersistedStateValue): void {
if (!this.application.isDatabaseLoaded()) {
return
}
this.application.setValue(MasterPersistenceKey, values)
if (!this.persistenceEnabled) {
return
}
this.application.setValue(StorageKey.MasterStatePersistenceKey, values)
}
getPersistedValues(): MasterPersistedValue {
return this.application.getValue(MasterPersistenceKey) as MasterPersistedValue
clearPersistedValues(): void {
if (!this.application.isDatabaseLoaded()) {
return
}
this.application.removeValue(StorageKey.MasterStatePersistenceKey)
}
getPersistedValues(): PersistedStateValue {
return this.application.getValue(StorageKey.MasterStatePersistenceKey) as PersistedStateValue
}
deinit() {
@@ -37,7 +37,6 @@ import { PrefDefaults } from '@/Constants/PrefDefaults'
import dayjs from 'dayjs'
import { LinkingController } from '../LinkingController'
import { AbstractViewController } from '../Abstract/AbstractViewController'
import { Persistable } from '../Abstract/Persistable'
import { log, LoggingDomain } from '@/Logging'
const MinNoteCellHeight = 51.0
@@ -55,14 +54,7 @@ enum ItemsReloadSource {
FilterTextChange,
}
export type ItemListControllerPersistableValue = {
displayOptions: DisplayOptions
}
export class ItemListController
extends AbstractViewController
implements Persistable<ItemListControllerPersistableValue>, InternalEventHandlerInterface
{
export class ItemListController extends AbstractViewController implements InternalEventHandlerInterface {
completedFullSync = false
noteFilterText = ''
notes: SNNote[] = []
@@ -121,19 +113,11 @@ export class ItemListController
this.resetPagination()
this.disposers.push(
application.streamItems<SNNote>(ContentType.Note, () => {
application.streamItems<SNNote>([ContentType.Note, ContentType.File], () => {
void this.reloadItems(ItemsReloadSource.ItemStream)
}),
)
this.disposers.push(
reaction(
() => [this.navigationController.selected],
() => {
void this.reloadDisplayPreferences()
},
),
)
this.disposers.push(
application.streamItems<SNTag>([ContentType.Tag], async ({ changed, inserted }) => {
const tags = [...changed, ...inserted]
@@ -142,10 +126,9 @@ export class ItemListController
if (!didReloadItems) {
/** A tag could have changed its relationships, so we need to reload the filter */
this.reloadNotesDisplayOptions()
void this.reloadItems(ItemsReloadSource.ItemStream)
}
void this.reloadItems(ItemsReloadSource.ItemStream)
if (this.navigationController.selected && findInArray(tags, 'uuid', this.navigationController.selected.uuid)) {
/** Tag title could have changed */
this.reloadPanelTitle()
@@ -233,8 +216,6 @@ export class ItemListController
optionsSubtitle: computed,
activeControllerItem: computed,
hydrateFromPersistedValue: action,
})
window.onresize = () => {
@@ -242,21 +223,6 @@ export class ItemListController
}
}
getPersistableValue = (): ItemListControllerPersistableValue => {
return {
displayOptions: this.displayOptions,
}
}
hydrateFromPersistedValue = (state: ItemListControllerPersistableValue | undefined) => {
if (!state) {
return
}
if (state.displayOptions) {
this.displayOptions = state.displayOptions
}
}
async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === CrossControllerEvent.TagChanged) {
const payload = event.payload as { userTriggered: boolean }
@@ -386,7 +352,12 @@ export class ItemListController
* In some cases we want to keep the selected item open even if it doesn't appear in results,
* for example if you are inside tag Foo and remove tag Foo from the note, we want to keep the note open.
*/
private shouldCloseActiveItem = (activeItem: SNNote | FileItem | undefined) => {
private shouldCloseActiveItem = (activeItem: SNNote | FileItem | undefined, source?: ItemsReloadSource) => {
if (source === ItemsReloadSource.UserTriggeredTagChange) {
log(LoggingDomain.Selection, 'shouldCloseActiveItem true due to ItemsReloadSource.UserTriggeredTagChange')
return true
}
const activeItemExistsInUpdatedResults = this.items.find((item) => item.uuid === activeItem?.uuid)
const closeBecauseActiveItemIsFileAndDoesntExistInUpdatedResults =
@@ -417,6 +388,7 @@ export class ItemListController
return true
}
log(LoggingDomain.Selection, 'shouldCloseActiveItem false')
return false
}
@@ -462,7 +434,7 @@ export class ItemListController
const activeItem = activeController?.item
if (activeController && activeItem && this.shouldCloseActiveItem(activeItem)) {
if (activeController && activeItem && this.shouldCloseActiveItem(activeItem, itemsReloadSource)) {
this.closeItemController(activeController)
this.selectionController.deselectItem(activeItem)
@@ -529,6 +501,7 @@ export class ItemListController
}
newDisplayOptions.sortBy = sortBy
const currentSortDirection = this.displayOptions.sortDirection
newDisplayOptions.sortDirection =
useBoolean(
selectedTag?.preferences?.sortReverse,
@@ -607,18 +580,14 @@ export class ItemListController
await this.reloadItems(ItemsReloadSource.DisplayOptionsChange)
if (
newDisplayOptions.sortBy !== currentSortBy &&
this.shouldSelectFirstItem(ItemsReloadSource.DisplayOptionsChange)
) {
const didSortByChange = currentSortBy !== this.displayOptions.sortBy
const didSortDirectionChange = currentSortDirection !== this.displayOptions.sortDirection
const didSortPrefChange = didSortByChange || didSortDirectionChange
if (didSortPrefChange && this.shouldSelectFirstItem(ItemsReloadSource.DisplayOptionsChange)) {
await this.selectFirstItem()
}
this.eventBus.publish({
type: CrossControllerEvent.RequestValuePersistence,
payload: undefined,
})
return { didReloadItems: true }
}
@@ -819,9 +788,12 @@ export class ItemListController
this.resetPagination()
this.reloadNotesDisplayOptions()
const { didReloadItems } = await this.reloadDisplayPreferences()
await this.reloadItems(userTriggered ? ItemsReloadSource.UserTriggeredTagChange : ItemsReloadSource.TagChange)
if (!didReloadItems) {
this.reloadNotesDisplayOptions()
void this.reloadItems(userTriggered ? ItemsReloadSource.UserTriggeredTagChange : ItemsReloadSource.TagChange)
}
}
onFilterEnter = () => {
@@ -835,6 +807,16 @@ export class ItemListController
this.application.getDesktopService()?.searchText(this.noteFilterText)
}
get isCurrentNoteTemplate(): boolean {
const controller = this.getActiveItemController()
if (!controller) {
return false
}
return controller instanceof NoteViewController && controller.isTemplateNote
}
public async insertCurrentIfTemplate(): Promise<void> {
const controller = this.getActiveItemController()
@@ -1,4 +1,4 @@
import { confirmDialog } from '@standardnotes/ui-services'
import { confirmDialog, NavigationControllerPersistableValue } from '@standardnotes/ui-services'
import { STRING_DELETE_TAG } from '@/Constants/Strings'
import { MAX_MENU_SIZE_MULTIPLIER, MENU_MARGIN_FROM_APP_BORDER, SMART_TAGS_FEATURE_NAME } from '@/Constants/Constants'
import {
@@ -28,10 +28,6 @@ import { AbstractViewController } from '../Abstract/AbstractViewController'
import { Persistable } from '../Abstract/Persistable'
import { TagListSectionType } from '@/Components/Tags/TagListSection'
export type NavigationControllerPersistableValue = {
selectedTagUuid: AnyTag['uuid']
}
export class NavigationController
extends AbstractViewController
implements Persistable<NavigationControllerPersistableValue>
@@ -11,6 +11,7 @@ import {
isFile,
Uuids,
} from '@standardnotes/snjs'
import { SelectionControllerPersistableValue } from '@standardnotes/ui-services'
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
import { WebApplication } from '../Application/Application'
import { AbstractViewController } from './Abstract/AbstractViewController'
@@ -18,10 +19,6 @@ import { Persistable } from './Abstract/Persistable'
import { CrossControllerEvent } from './CrossControllerEvent'
import { ItemListController } from './ItemList/ItemListController'
export type SelectionControllerPersistableValue = {
selectedUuids: UuidString[]
}
export class SelectedItemsController
extends AbstractViewController
implements Persistable<SelectionControllerPersistableValue>
@@ -9,7 +9,7 @@ import {
SubscriptionClientInterface,
Uuid,
} from '@standardnotes/snjs'
import { action, computed, makeObservable, observable } from 'mobx'
import { action, computed, makeObservable, observable, runInAction } from 'mobx'
import { WebApplication } from '../../Application/Application'
import { AbstractViewController } from '../Abstract/AbstractViewController'
import { AvailableSubscriptions } from './AvailableSubscriptionsType'
@@ -21,6 +21,8 @@ export class SubscriptionController extends AbstractViewController {
userSubscription: Subscription | undefined = undefined
availableSubscriptions: AvailableSubscriptions | undefined = undefined
subscriptionInvitations: Invitation[] | undefined = undefined
hideSubscriptionMarketing: boolean
hasAccount: boolean
override deinit() {
super.deinit()
@@ -37,11 +39,15 @@ export class SubscriptionController extends AbstractViewController {
private subscriptionManager: SubscriptionClientInterface,
) {
super(application, eventBus)
this.hideSubscriptionMarketing = application.hideSubscriptionMarketing
this.hasAccount = application.hasAccount()
makeObservable(this, {
userSubscription: observable,
availableSubscriptions: observable,
subscriptionInvitations: observable,
hideSubscriptionMarketing: observable,
hasAccount: observable,
userSubscriptionName: computed,
userSubscriptionExpirationDate: computed,
@@ -61,6 +67,9 @@ export class SubscriptionController extends AbstractViewController {
this.getSubscriptionInfo().catch(console.error)
this.reloadSubscriptionInvitations().catch(console.error)
}
runInAction(() => {
this.hasAccount = application.hasAccount()
})
}, ApplicationEvent.Launched),
)
@@ -68,6 +77,9 @@ export class SubscriptionController extends AbstractViewController {
application.addEventObserver(async () => {
this.getSubscriptionInfo().catch(console.error)
this.reloadSubscriptionInvitations().catch(console.error)
runInAction(() => {
this.hasAccount = application.hasAccount()
})
}, ApplicationEvent.SignedIn),
)
@@ -1,5 +1,12 @@
import { PaneController } from './PaneController'
import { storage, StorageKey, ToastService, ToastServiceInterface } from '@standardnotes/ui-services'
import {
PersistedStateValue,
PersistenceKey,
storage,
StorageKey,
ToastService,
ToastServiceInterface,
} from '@standardnotes/ui-services'
import { WebApplication } from '@/Application/Application'
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
import { destroyAllObjectProperties } from '@/Utils'
@@ -18,7 +25,7 @@ import { ActionsMenuController } from './ActionsMenuController'
import { FeaturesController } from './FeaturesController'
import { FilesController } from './FilesController'
import { NotesController } from './NotesController'
import { ItemListController, ItemListControllerPersistableValue } from './ItemList/ItemListController'
import { ItemListController } from './ItemList/ItemListController'
import { NoAccountWarningController } from './NoAccountWarningController'
import { PreferencesController } from './PreferencesController'
import { PurchaseFlowController } from './PurchaseFlow/PurchaseFlowController'
@@ -26,12 +33,12 @@ import { QuickSettingsController } from './QuickSettingsController'
import { SearchOptionsController } from './SearchOptionsController'
import { SubscriptionController } from './Subscription/SubscriptionController'
import { SyncStatusController } from './SyncStatusController'
import { NavigationController, NavigationControllerPersistableValue } from './Navigation/NavigationController'
import { NavigationController } from './Navigation/NavigationController'
import { FilePreviewModalController } from './FilePreviewModalController'
import { SelectedItemsController, SelectionControllerPersistableValue } from './SelectedItemsController'
import { SelectedItemsController } from './SelectedItemsController'
import { HistoryModalController } from './NoteHistory/HistoryModalController'
import { LinkingController } from './LinkingController'
import { MasterPersistedValue, PersistenceKey, PersistenceService } from './Abstract/PersistenceService'
import { PersistenceService } from './Abstract/PersistenceService'
import { CrossControllerEvent } from './CrossControllerEvent'
import { EventObserverInterface } from '@/Event/EventObserverInterface'
import { ApplicationEventObserver } from '@/Event/ApplicationEventObserver'
@@ -160,6 +167,7 @@ export class ViewControllerManager implements InternalEventHandlerInterface {
application.sessions,
application.subscriptions,
this.toastService,
application.user,
)
this.addAppEventObserver()
@@ -263,29 +271,40 @@ export class ViewControllerManager implements InternalEventHandlerInterface {
}
persistValues = (): void => {
const values: MasterPersistedValue = {
const values: PersistedStateValue = {
[PersistenceKey.SelectedItemsController]: this.selectionController.getPersistableValue(),
[PersistenceKey.NavigationController]: this.navigationController.getPersistableValue(),
[PersistenceKey.ItemListController]: this.itemListController.getPersistableValue(),
}
this.persistenceService.persistValues(values)
const selectedItemsState = values['selected-items-controller']
const navigationSelectionState = values['navigation-controller']
const launchPriorityUuids: string[] = []
if (selectedItemsState.selectedUuids.length) {
launchPriorityUuids.push(...selectedItemsState.selectedUuids)
}
if (navigationSelectionState.selectedTagUuid) {
launchPriorityUuids.push(navigationSelectionState.selectedTagUuid)
}
this.application.sync.setLaunchPriorityUuids(launchPriorityUuids)
}
hydrateFromPersistedValues = (values: MasterPersistedValue | undefined): void => {
const itemListState = values?.[PersistenceKey.ItemListController] as ItemListControllerPersistableValue
this.itemListController.hydrateFromPersistedValue(itemListState)
clearPersistedValues = (): void => {
this.persistenceService.clearPersistedValues()
}
const selectedItemsState = values?.[PersistenceKey.SelectedItemsController] as SelectionControllerPersistableValue
this.selectionController.hydrateFromPersistedValue(selectedItemsState)
const navigationState = values?.[PersistenceKey.NavigationController] as NavigationControllerPersistableValue
hydrateFromPersistedValues = (values: PersistedStateValue | undefined): void => {
const navigationState = values?.[PersistenceKey.NavigationController]
this.navigationController.hydrateFromPersistedValue(navigationState)
const selectedItemsState = values?.[PersistenceKey.SelectedItemsController]
this.selectionController.hydrateFromPersistedValue(selectedItemsState)
}
async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === CrossControllerEvent.HydrateFromPersistedValues) {
this.hydrateFromPersistedValues(event.payload as MasterPersistedValue | undefined)
this.hydrateFromPersistedValues(event.payload as PersistedStateValue | undefined)
} else if (event.type === CrossControllerEvent.RequestValuePersistence) {
this.persistValues()
}
@@ -17,6 +17,8 @@ import {
SyncClientInterface,
SyncOpStatus,
User,
UserClientInterface,
UserRequestType,
} from '@standardnotes/snjs'
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
@@ -39,6 +41,7 @@ describe('ApplicationEventObserver', () => {
let sessionManager: SessionsClientInterface
let subscriptionManager: SubscriptionClientInterface
let toastService: ToastServiceInterface
let userService: UserClientInterface
const createObserver = () =>
new ApplicationEventObserver(
@@ -52,6 +55,7 @@ describe('ApplicationEventObserver', () => {
sessionManager,
subscriptionManager,
toastService,
userService,
)
beforeEach(() => {
@@ -87,7 +91,11 @@ describe('ApplicationEventObserver', () => {
subscriptionManager.acceptInvitation = jest.fn()
toastService = {} as jest.Mocked<ToastServiceInterface>
toastService.showToast = jest.fn()
toastService.showToast = jest.fn().mockReturnValue('1')
toastService.hideToast = jest.fn()
userService = {} as jest.Mocked<UserClientInterface>
userService.submitUserRequest = jest.fn().mockReturnValue(true)
})
describe('Upon Application Launched', () => {
@@ -184,6 +192,63 @@ describe('ApplicationEventObserver', () => {
expect(toastService.showToast).toHaveBeenCalledWith(ToastType.Error, 'Oops!')
expect(routeService.removeQueryParameterFromURL).toHaveBeenCalledWith(RootQueryParam.AcceptSubscriptionInvite)
})
it('should open up sign in if user is not logged in and tries to send request', async () => {
sessionManager.getUser = jest.fn().mockReturnValue(undefined)
routeService.getRoute = jest.fn().mockReturnValue({
type: RouteType.UserRequest,
userRequestParams: {
requestType: UserRequestType.ExitDiscount,
},
} as jest.Mocked<RouteParserInterface>)
await createObserver().handle(ApplicationEvent.Launched)
expect(accountMenuController.setShow).toHaveBeenCalledWith(true)
expect(accountMenuController.setCurrentPane).toHaveBeenCalledWith(AccountMenuPane.SignIn)
expect(userService.submitUserRequest).not.toHaveBeenCalled()
expect(toastService.showToast).not.toHaveBeenCalled()
})
it('should send user request if user is logged in', async () => {
userService.submitUserRequest = jest.fn().mockReturnValue(true)
routeService.getRoute = jest.fn().mockReturnValue({
type: RouteType.UserRequest,
userRequestParams: {
requestType: UserRequestType.ExitDiscount,
},
} as jest.Mocked<RouteParserInterface>)
await createObserver().handle(ApplicationEvent.Launched)
expect(userService.submitUserRequest).toHaveBeenCalledWith('exit-discount')
expect(toastService.showToast).toHaveBeenNthCalledWith(
2,
ToastType.Success,
'We have received your request. Please check your email for further instructions.',
)
expect(routeService.removeQueryParameterFromURL).toHaveBeenCalledWith(RootQueryParam.UserRequest)
})
it('should show sending request failure if user is logged in and sending fails', async () => {
userService.submitUserRequest = jest.fn().mockReturnValue(false)
routeService.getRoute = jest.fn().mockReturnValue({
type: RouteType.UserRequest,
userRequestParams: {
requestType: UserRequestType.ExitDiscount,
},
} as jest.Mocked<RouteParserInterface>)
await createObserver().handle(ApplicationEvent.Launched)
expect(userService.submitUserRequest).toHaveBeenCalledWith('exit-discount')
expect(toastService.showToast).toHaveBeenNthCalledWith(
2,
ToastType.Success,
'We could not process your request. Please try again or contact support if the issue persists.',
)
expect(routeService.removeQueryParameterFromURL).toHaveBeenCalledWith(RootQueryParam.UserRequest)
})
})
describe('Upon Signing In', () => {
@@ -219,6 +284,26 @@ describe('ApplicationEventObserver', () => {
)
expect(routeService.removeQueryParameterFromURL).toHaveBeenCalledWith(RootQueryParam.AcceptSubscriptionInvite)
})
it('should send user request', async () => {
userService.submitUserRequest = jest.fn().mockReturnValue(true)
routeService.getRoute = jest.fn().mockReturnValue({
type: RouteType.UserRequest,
userRequestParams: {
requestType: UserRequestType.ExitDiscount,
},
} as jest.Mocked<RouteParserInterface>)
await createObserver().handle(ApplicationEvent.SignedIn)
expect(userService.submitUserRequest).toHaveBeenCalledWith('exit-discount')
expect(toastService.showToast).toHaveBeenNthCalledWith(
2,
ToastType.Success,
'We have received your request. Please check your email for further instructions.',
)
expect(routeService.removeQueryParameterFromURL).toHaveBeenCalledWith(RootQueryParam.UserRequest)
})
})
describe('Upon Sync Status Changing', () => {
@@ -10,6 +10,7 @@ import {
SessionsClientInterface,
SubscriptionClientInterface,
SyncClientInterface,
UserClientInterface,
} from '@standardnotes/snjs'
import { ToastType } from '@standardnotes/toast'
@@ -34,6 +35,7 @@ export class ApplicationEventObserver implements EventObserverInterface {
private sessionManager: SessionsClientInterface,
private subscriptionManager: SubscriptionClientInterface,
private toastService: ToastServiceInterface,
private userService: UserClientInterface,
) {}
async handle(event: ApplicationEvent): Promise<void> {
@@ -67,6 +69,17 @@ export class ApplicationEventObserver implements EventObserverInterface {
}
await this.acceptSubscriptionInvitation(route)
break
}
case RouteType.UserRequest: {
const user = this.sessionManager.getUser()
if (user === undefined) {
this.promptUserSignIn()
break
}
await this.sendUserRequest(route)
break
}
}
@@ -84,6 +97,10 @@ export class ApplicationEventObserver implements EventObserverInterface {
case RouteType.AcceptSubscriptionInvite:
await this.acceptSubscriptionInvitation(route)
break
case RouteType.UserRequest:
await this.sendUserRequest(route)
break
}
}
@@ -105,8 +122,12 @@ export class ApplicationEventObserver implements EventObserverInterface {
}
private async acceptSubscriptionInvitation(route: RouteParserInterface): Promise<void> {
const processingToastId = this.toastService.showToast(ToastType.Loading, 'Accepting invitation...')
const acceptResult = await this.subscriptionManager.acceptInvitation(route.subscriptionInviteParams.inviteUuid)
this.toastService.hideToast(processingToastId)
const toastType = acceptResult.success ? ToastType.Success : ToastType.Error
const toastMessage = acceptResult.success ? 'Successfully joined a shared subscription' : acceptResult.message
@@ -114,4 +135,21 @@ export class ApplicationEventObserver implements EventObserverInterface {
this.routeService.removeQueryParameterFromURL(RootQueryParam.AcceptSubscriptionInvite)
}
private async sendUserRequest(route: RouteParserInterface): Promise<void> {
const processingToastId = this.toastService.showToast(ToastType.Loading, 'Processing your request...')
const requestSubmittedSuccessfully = await this.userService.submitUserRequest(route.userRequestParams.requestType)
this.toastService.hideToast(processingToastId)
const toastType = requestSubmittedSuccessfully ? ToastType.Success : ToastType.Error
const toastMessage = requestSubmittedSuccessfully
? 'We have received your request. Please check your email for further instructions.'
: 'We could not process your request. Please try again or contact support if the issue persists.'
this.toastService.showToast(toastType, toastMessage)
this.routeService.removeQueryParameterFromURL(RootQueryParam.UserRequest)
}
}