From bc845c16336d181100405be825bc87a8ee676fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Thu, 28 Dec 2023 10:50:14 +0100 Subject: [PATCH] chore: add server switcher (#2717) * wip * wip2 * refactor: use radio button group * chore: add persisting host per workspace * chore: header style * chore: update server picker title style * chore: margin * chore: label * chore: remove separator * rename migration to latest snjs version --------- Co-authored-by: Aman Harwara --- .../Application/ApplicationInterface.ts | 2 +- packages/snjs/lib/Application/Application.ts | 4 +- .../Application/Dependencies/Dependencies.ts | 1 + .../snjs/lib/Migrations/Versions/2_204_8.ts | 27 +++ packages/snjs/lib/Services/Api/ApiService.ts | 7 +- .../lib/Services/Session/SessionManager.ts | 2 +- packages/snjs/lib/Url/DefaultHost.ts | 4 + packages/snjs/lib/Url/index.ts | 1 + packages/snjs/lib/index.ts | 1 + .../AccountMenu/AdvancedOptions.tsx | 174 ++++++++---------- .../AccountMenu/ServerPicker/ServerPicker.tsx | 80 ++++++++ .../AccountMenu/ServerPicker/ServerType.ts | 1 + .../AccountMenu/AccountMenuController.ts | 7 - 13 files changed, 200 insertions(+), 111 deletions(-) create mode 100644 packages/snjs/lib/Migrations/Versions/2_204_8.ts create mode 100644 packages/snjs/lib/Url/DefaultHost.ts create mode 100644 packages/snjs/lib/Url/index.ts create mode 100644 packages/web/src/javascripts/Components/AccountMenu/ServerPicker/ServerPicker.tsx create mode 100644 packages/web/src/javascripts/Components/AccountMenu/ServerPicker/ServerType.ts diff --git a/packages/services/src/Domain/Application/ApplicationInterface.ts b/packages/services/src/Domain/Application/ApplicationInterface.ts index 28c145686d3..6cba3a6eb9a 100644 --- a/packages/services/src/Domain/Application/ApplicationInterface.ts +++ b/packages/services/src/Domain/Application/ApplicationInterface.ts @@ -64,7 +64,7 @@ export interface ApplicationInterface { setPreference(key: K, value: PrefValue[K]): Promise hasAccount(): boolean - setCustomHost(host: string): Promise + setCustomHost(host: string, websocketUrl?: string): Promise isUsingHomeServer(): Promise importData(data: BackupFile, awaitSync?: boolean): Promise> diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index 019241bfac0..0ac889ec8c9 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -576,10 +576,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return compareVersions(userVersion, ProtocolVersion.V004) >= 0 } - public async setCustomHost(host: string): Promise { + public async setCustomHost(host: string, websocketUrl?: string): Promise { await this.setHost.execute(host) - this.sockets.setWebSocketUrl(undefined) + this.sockets.setWebSocketUrl(websocketUrl) } public getUserPasswordCreationDate(): Date | undefined { diff --git a/packages/snjs/lib/Application/Dependencies/Dependencies.ts b/packages/snjs/lib/Application/Dependencies/Dependencies.ts index a5e89f47292..a9f15520a1f 100644 --- a/packages/snjs/lib/Application/Dependencies/Dependencies.ts +++ b/packages/snjs/lib/Application/Dependencies/Dependencies.ts @@ -1528,6 +1528,7 @@ export class Dependencies { this.get(TYPES.HttpService), this.get(TYPES.DiskStorageService), this.options.defaultHost, + this.options.identifier, this.get(TYPES.InMemoryStore), this.get(TYPES.Crypto), this.get(TYPES.SessionStorageMapper), diff --git a/packages/snjs/lib/Migrations/Versions/2_204_8.ts b/packages/snjs/lib/Migrations/Versions/2_204_8.ts new file mode 100644 index 00000000000..735e33e1e92 --- /dev/null +++ b/packages/snjs/lib/Migrations/Versions/2_204_8.ts @@ -0,0 +1,27 @@ +import { ApplicationStage, StorageKey } from '@standardnotes/services' +import { Migration } from '@Lib/Migrations/Migration' + +export class Migration2_204_8 extends Migration { + static override version(): string { + return '2.204.8' + } + + protected registerStageHandlers(): void { + this.registerStageHandler(ApplicationStage.Launched_10, async () => { + await this.migrateHostKeyStoredToWorkspaceIdentified() + + this.markDone() + }) + } + + private async migrateHostKeyStoredToWorkspaceIdentified(): Promise { + const existingHostKeyValue = this.services.storageService.getValue(StorageKey.ServerHost) + if (existingHostKeyValue === undefined) { + return + } + + this.services.storageService.setValue(`${StorageKey.ServerHost}:${this.services.identifier}`, existingHostKeyValue) + + await this.services.storageService.removeValue(StorageKey.ServerHost) + } +} diff --git a/packages/snjs/lib/Services/Api/ApiService.ts b/packages/snjs/lib/Services/Api/ApiService.ts index 3ff24fb22ee..ca127c99e74 100644 --- a/packages/snjs/lib/Services/Api/ApiService.ts +++ b/packages/snjs/lib/Services/Api/ApiService.ts @@ -106,6 +106,7 @@ export class LegacyApiService private httpService: HttpServiceInterface, private storageService: DiskStorageService, private host: string, + private workspaceIdentifier: string, private inMemoryStore: KeyValueStoreInterface, private crypto: PureCryptoInterface, private sessionStorageMapper: MapperInterface>, @@ -142,14 +143,16 @@ export class LegacyApiService } public loadHost(): string { - const storedValue = this.storageService.getValue(StorageKey.ServerHost) + const storedValue = this.storageService.getValue( + `${StorageKey.ServerHost}:${this.workspaceIdentifier}`, + ) this.host = storedValue || this.host return this.host } public async setHost(host: string): Promise { this.host = host - this.storageService.setValue(StorageKey.ServerHost, host) + this.storageService.setValue(`${StorageKey.ServerHost}:${this.workspaceIdentifier}`, host) } public getHost(): string { diff --git a/packages/snjs/lib/Services/Session/SessionManager.ts b/packages/snjs/lib/Services/Session/SessionManager.ts index 84fb3815003..68df3e3d702 100644 --- a/packages/snjs/lib/Services/Session/SessionManager.ts +++ b/packages/snjs/lib/Services/Session/SessionManager.ts @@ -169,7 +169,7 @@ export class SessionManager } } - const serverHost = this.storage.getValue(StorageKey.ServerHost) + const serverHost = this.storage.getValue(`${StorageKey.ServerHost}:${this.workspaceIdentifier}`) if (serverHost) { void this.apiService.setHost(serverHost) this.httpService.setHost(serverHost) diff --git a/packages/snjs/lib/Url/DefaultHost.ts b/packages/snjs/lib/Url/DefaultHost.ts new file mode 100644 index 00000000000..554ce381fc6 --- /dev/null +++ b/packages/snjs/lib/Url/DefaultHost.ts @@ -0,0 +1,4 @@ +export enum DefaultHost { + Api = 'https://api.standardnotes.com', + WebSocket = 'wss://sockets.standardnotes.com', +} diff --git a/packages/snjs/lib/Url/index.ts b/packages/snjs/lib/Url/index.ts new file mode 100644 index 00000000000..d9698b531a1 --- /dev/null +++ b/packages/snjs/lib/Url/index.ts @@ -0,0 +1 @@ +export * from './DefaultHost' diff --git a/packages/snjs/lib/index.ts b/packages/snjs/lib/index.ts index e4eb70b8cb6..56583b4f4d7 100644 --- a/packages/snjs/lib/index.ts +++ b/packages/snjs/lib/index.ts @@ -7,6 +7,7 @@ export * from './Log' export * from './Migrations' export * from './Services' export * from './Types' +export * from './Url' export * from './Version' export { KeyParamsOrigination } from '@standardnotes/common' export * from '@standardnotes/domain-core' diff --git a/packages/web/src/javascripts/Components/AccountMenu/AdvancedOptions.tsx b/packages/web/src/javascripts/Components/AccountMenu/AdvancedOptions.tsx index 93f652c7ddb..2a63de27718 100644 --- a/packages/web/src/javascripts/Components/AccountMenu/AdvancedOptions.tsx +++ b/packages/web/src/javascripts/Components/AccountMenu/AdvancedOptions.tsx @@ -1,9 +1,10 @@ import { observer } from 'mobx-react-lite' -import { ChangeEventHandler, FunctionComponent, ReactNode, useCallback, useEffect, useState } from 'react' +import { FunctionComponent, ReactNode, useCallback, useEffect, useState } from 'react' import Checkbox from '@/Components/Checkbox/Checkbox' import DecoratedInput from '@/Components/Input/DecoratedInput' import Icon from '@/Components/Icon/Icon' import { useApplication } from '../ApplicationProvider' +import ServerPicker from './ServerPicker/ServerPicker' type Props = { disabled?: boolean @@ -22,7 +23,7 @@ const AdvancedOptions: FunctionComponent = ({ }) => { const application = useApplication() - const { server, setServer, enableServerOption, setEnableServerOption } = application.accountMenuController + const { server, setServer } = application.accountMenuController const [showAdvanced, setShowAdvanced] = useState(false) const [isPrivateUsername, setIsPrivateUsername] = useState(false) @@ -71,9 +72,8 @@ const AdvancedOptions: FunctionComponent = ({ if (!isRecoveryCodes) { setIsPrivateUsername(false) setIsStrictSignin(false) - setEnableServerOption(false) } - }, [isRecoveryCodes, setIsPrivateUsername, setIsStrictSignin, setEnableServerOption, onRecoveryCodesChange]) + }, [isRecoveryCodes, setIsPrivateUsername, setIsStrictSignin, onRecoveryCodesChange]) const handleRecoveryCodesChange = useCallback( (recoveryCodes: string) => { @@ -85,19 +85,10 @@ const AdvancedOptions: FunctionComponent = ({ [onRecoveryCodesChange], ) - const handleServerOptionChange: ChangeEventHandler = useCallback( - (e) => { - if (e.target instanceof HTMLInputElement) { - setEnableServerOption(e.target.checked) - } - }, - [setEnableServerOption], - ) - const handleSyncServerChange = useCallback( - (server: string) => { + (server: string, websocketUrl?: string) => { setServer(server) - application.setCustomHost(server).catch(console.error) + application.setCustomHost(server, websocketUrl).catch(console.error) }, [application, setServer], ) @@ -124,100 +115,87 @@ const AdvancedOptions: FunctionComponent = ({ {showAdvanced ? ( -
- {children} - -
- - - - -
- - {isPrivateUsername && ( - <> - ]} - type="text" - placeholder="Username" - value={privateUsername} - onChange={handlePrivateUsernameNameChange} - disabled={disabled || isRecoveryCodes} - spellcheck={false} - autocomplete={false} - /> - - )} + <> +
+ {children} - {onStrictSignInChange && ( - )} - -
- -
- {isRecoveryCodes && ( - <> - ]} - type="text" - placeholder="Recovery code" - value={recoveryCodes} - onChange={handleRecoveryCodesChange} + {isPrivateUsername && ( + <> + ]} + type="text" + placeholder="Username" + value={privateUsername} + onChange={handlePrivateUsernameNameChange} + disabled={disabled || isRecoveryCodes} + spellcheck={false} + autocomplete={false} + /> + + )} + + {onStrictSignInChange && ( +
+ + + + +
+ )} + +
+ - - )} - - - ]} - placeholder="https://api.standardnotes.com" - value={server} - onChange={handleSyncServerChange} - disabled={!enableServerOption && !disabled && !isRecoveryCodes} - /> -
+
+ + {isRecoveryCodes && ( + <> + ]} + type="text" + placeholder="Recovery code" + value={recoveryCodes} + onChange={handleRecoveryCodesChange} + disabled={disabled} + spellcheck={false} + autocomplete={false} + /> + + )} +
+ + ) : null} ) diff --git a/packages/web/src/javascripts/Components/AccountMenu/ServerPicker/ServerPicker.tsx b/packages/web/src/javascripts/Components/AccountMenu/ServerPicker/ServerPicker.tsx new file mode 100644 index 00000000000..6c8c73ab878 --- /dev/null +++ b/packages/web/src/javascripts/Components/AccountMenu/ServerPicker/ServerPicker.tsx @@ -0,0 +1,80 @@ +import { useMemo, useState } from 'react' +import { ServerType } from './ServerType' +import DecoratedInput from '@/Components/Input/DecoratedInput' +import Icon from '@/Components/Icon/Icon' +import { useApplication } from '@/Components/ApplicationProvider' +import { isDesktopApplication } from '@/Utils' +import RadioButtonGroup from '@/Components/RadioButtonGroup/RadioButtonGroup' +import { DefaultHost } from '@standardnotes/snjs' + +type Props = { + customServerAddress?: string + handleCustomServerAddressChange: (value: string, websocketUrl?: string) => void + className?: string +} + +const ServerPicker = ({ className, customServerAddress, handleCustomServerAddressChange }: Props) => { + const application = useApplication() + + const [currentType, setCurrentType] = useState('standard') + + const selectTab = async (type: ServerType) => { + setCurrentType(type) + if (type === 'standard') { + handleCustomServerAddressChange(DefaultHost.Api, DefaultHost.WebSocket) + } + if (type === 'home server') { + if (!application.homeServer) { + application.alerts + .alert('Home server is not running. Please open the prefences and home server tab to start it.') + .catch(console.error) + + return + } + + const homeServerUrl = await application.homeServer.getHomeServerUrl() + if (!homeServerUrl) { + application.alerts + .alert('Home server is not running. Please open the prefences and home server tab to start it.') + .catch(console.error) + + return + } + + handleCustomServerAddressChange(homeServerUrl) + } + } + + const options = useMemo( + () => + [ + { label: 'Default', value: 'standard' }, + { label: 'Custom', value: 'custom' }, + ].concat(isDesktopApplication() ? [{ label: 'Home Server', value: 'home server' }] : []) as { + label: string + value: ServerType + }[], + [], + ) + + return ( +
+
Sync Server
+ + {currentType === 'custom' && ( + ]} + placeholder={DefaultHost.Api} + value={customServerAddress} + onChange={handleCustomServerAddressChange} + /> + )} +
+ ) +} + +export default ServerPicker diff --git a/packages/web/src/javascripts/Components/AccountMenu/ServerPicker/ServerType.ts b/packages/web/src/javascripts/Components/AccountMenu/ServerPicker/ServerType.ts new file mode 100644 index 00000000000..45ff66d8e9d --- /dev/null +++ b/packages/web/src/javascripts/Components/AccountMenu/ServerPicker/ServerType.ts @@ -0,0 +1 @@ +export type ServerType = 'standard' | 'custom' | 'home server' diff --git a/packages/web/src/javascripts/Controllers/AccountMenu/AccountMenuController.ts b/packages/web/src/javascripts/Controllers/AccountMenu/AccountMenuController.ts index 6c24975d61d..d665938e735 100644 --- a/packages/web/src/javascripts/Controllers/AccountMenu/AccountMenuController.ts +++ b/packages/web/src/javascripts/Controllers/AccountMenu/AccountMenuController.ts @@ -19,7 +19,6 @@ export class AccountMenuController extends AbstractViewController implements Int signingOut = false otherSessionsSignOut = false server: string | undefined = undefined - enableServerOption = false notesAndTags: (SNNote | SNTag)[] = [] isEncryptionEnabled = false encryptionStatusString = '' @@ -48,7 +47,6 @@ export class AccountMenuController extends AbstractViewController implements Int signingOut: observable, otherSessionsSignOut: observable, server: observable, - enableServerOption: observable, notesAndTags: observable, isEncryptionEnabled: observable, encryptionStatusString: observable, @@ -66,7 +64,6 @@ export class AccountMenuController extends AbstractViewController implements Int setIsBackupEncrypted: action, setOtherSessionsSignOut: action, setCurrentPane: action, - setEnableServerOption: action, setServer: action, setDeletingAccount: action, @@ -113,10 +110,6 @@ export class AccountMenuController extends AbstractViewController implements Int this.server = server } - setEnableServerOption = (enableServerOption: boolean): void => { - this.enableServerOption = enableServerOption - } - setIsEncryptionEnabled = (isEncryptionEnabled: boolean): void => { this.isEncryptionEnabled = isEncryptionEnabled }