Skip to content

Commit

Permalink
chore: add server switcher (#2717)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
karolsojko and amanharwara authored Dec 28, 2023
1 parent f376607 commit bc845c1
Show file tree
Hide file tree
Showing 13 changed files with 200 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export interface ApplicationInterface {
setPreference<K extends PrefKey>(key: K, value: PrefValue[K]): Promise<void>

hasAccount(): boolean
setCustomHost(host: string): Promise<void>
setCustomHost(host: string, websocketUrl?: string): Promise<void>
isUsingHomeServer(): Promise<boolean>

importData(data: BackupFile, awaitSync?: boolean): Promise<Result<ImportDataResult>>
Expand Down
4 changes: 2 additions & 2 deletions packages/snjs/lib/Application/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,10 +576,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return compareVersions(userVersion, ProtocolVersion.V004) >= 0
}

public async setCustomHost(host: string): Promise<void> {
public async setCustomHost(host: string, websocketUrl?: string): Promise<void> {
await this.setHost.execute(host)

this.sockets.setWebSocketUrl(undefined)
this.sockets.setWebSocketUrl(websocketUrl)
}

public getUserPasswordCreationDate(): Date | undefined {
Expand Down
1 change: 1 addition & 0 deletions packages/snjs/lib/Application/Dependencies/Dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1528,6 +1528,7 @@ export class Dependencies {
this.get<HttpService>(TYPES.HttpService),
this.get<DiskStorageService>(TYPES.DiskStorageService),
this.options.defaultHost,
this.options.identifier,
this.get<InMemoryStore>(TYPES.InMemoryStore),
this.get<PureCryptoInterface>(TYPES.Crypto),
this.get<SessionStorageMapper>(TYPES.SessionStorageMapper),
Expand Down
27 changes: 27 additions & 0 deletions packages/snjs/lib/Migrations/Versions/2_204_8.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
const existingHostKeyValue = this.services.storageService.getValue<string | undefined>(StorageKey.ServerHost)
if (existingHostKeyValue === undefined) {
return
}

this.services.storageService.setValue(`${StorageKey.ServerHost}:${this.services.identifier}`, existingHostKeyValue)

await this.services.storageService.removeValue(StorageKey.ServerHost)
}
}
7 changes: 5 additions & 2 deletions packages/snjs/lib/Services/Api/ApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export class LegacyApiService
private httpService: HttpServiceInterface,
private storageService: DiskStorageService,
private host: string,
private workspaceIdentifier: string,
private inMemoryStore: KeyValueStoreInterface<string>,
private crypto: PureCryptoInterface,
private sessionStorageMapper: MapperInterface<Session, Record<string, unknown>>,
Expand Down Expand Up @@ -142,14 +143,16 @@ export class LegacyApiService
}

public loadHost(): string {
const storedValue = this.storageService.getValue<string | undefined>(StorageKey.ServerHost)
const storedValue = this.storageService.getValue<string | undefined>(
`${StorageKey.ServerHost}:${this.workspaceIdentifier}`,
)
this.host = storedValue || this.host
return this.host
}

public async setHost(host: string): Promise<void> {
this.host = host
this.storageService.setValue(StorageKey.ServerHost, host)
this.storageService.setValue(`${StorageKey.ServerHost}:${this.workspaceIdentifier}`, host)
}

public getHost(): string {
Expand Down
2 changes: 1 addition & 1 deletion packages/snjs/lib/Services/Session/SessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export class SessionManager
}
}

const serverHost = this.storage.getValue<string>(StorageKey.ServerHost)
const serverHost = this.storage.getValue<string | undefined>(`${StorageKey.ServerHost}:${this.workspaceIdentifier}`)
if (serverHost) {
void this.apiService.setHost(serverHost)
this.httpService.setHost(serverHost)
Expand Down
4 changes: 4 additions & 0 deletions packages/snjs/lib/Url/DefaultHost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum DefaultHost {
Api = 'https://api.standardnotes.com',
WebSocket = 'wss://sockets.standardnotes.com',
}
1 change: 1 addition & 0 deletions packages/snjs/lib/Url/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './DefaultHost'
1 change: 1 addition & 0 deletions packages/snjs/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -22,7 +23,7 @@ const AdvancedOptions: FunctionComponent<Props> = ({
}) => {
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)
Expand Down Expand Up @@ -71,9 +72,8 @@ const AdvancedOptions: FunctionComponent<Props> = ({
if (!isRecoveryCodes) {
setIsPrivateUsername(false)
setIsStrictSignin(false)
setEnableServerOption(false)
}
}, [isRecoveryCodes, setIsPrivateUsername, setIsStrictSignin, setEnableServerOption, onRecoveryCodesChange])
}, [isRecoveryCodes, setIsPrivateUsername, setIsStrictSignin, onRecoveryCodesChange])

const handleRecoveryCodesChange = useCallback(
(recoveryCodes: string) => {
Expand All @@ -85,19 +85,10 @@ const AdvancedOptions: FunctionComponent<Props> = ({
[onRecoveryCodesChange],
)

const handleServerOptionChange: ChangeEventHandler<HTMLInputElement> = 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],
)
Expand All @@ -124,100 +115,87 @@ const AdvancedOptions: FunctionComponent<Props> = ({
</div>
</button>
{showAdvanced ? (
<div className="my-2 px-3">
{children}

<div className="mb-1 flex items-center justify-between">
<Checkbox
name="private-workspace"
label="Private username mode"
checked={isPrivateUsername}
disabled={disabled || isRecoveryCodes}
onChange={handleIsPrivateUsernameChange}
/>
<a href="https://standardnotes.com/help/80" target="_blank" rel="noopener noreferrer" title="Learn more">
<Icon type="info" className="text-neutral" />
</a>
</div>

{isPrivateUsername && (
<>
<DecoratedInput
className={{ container: 'mb-2' }}
left={[<Icon type="account-circle" className="text-neutral" />]}
type="text"
placeholder="Username"
value={privateUsername}
onChange={handlePrivateUsernameNameChange}
disabled={disabled || isRecoveryCodes}
spellcheck={false}
autocomplete={false}
/>
</>
)}
<>
<div className="my-2 px-3">
{children}

{onStrictSignInChange && (
<div className="mb-1 flex items-center justify-between">
<Checkbox
name="use-strict-signin"
label="Use strict sign-in"
checked={isStrictSignin}
name="private-workspace"
label="Private username mode"
checked={isPrivateUsername}
disabled={disabled || isRecoveryCodes}
onChange={handleStrictSigninChange}
onChange={handleIsPrivateUsernameChange}
/>
<a
href="https://standardnotes.com/help/security"
target="_blank"
rel="noopener noreferrer"
title="Learn more"
>
<a href="https://standardnotes.com/help/80" target="_blank" rel="noopener noreferrer" title="Learn more">
<Icon type="info" className="text-neutral" />
</a>
</div>
)}

<div className="mb-1 flex items-center justify-between">
<Checkbox
name="recovery-codes"
label="Use recovery code"
checked={isRecoveryCodes}
disabled={disabled}
onChange={handleIsRecoveryCodesChange}
/>
</div>

{isRecoveryCodes && (
<>
<DecoratedInput
className={{ container: 'mb-2' }}
left={[<Icon type="security" className="text-neutral" />]}
type="text"
placeholder="Recovery code"
value={recoveryCodes}
onChange={handleRecoveryCodesChange}
{isPrivateUsername && (
<>
<DecoratedInput
className={{ container: 'mb-2' }}
left={[<Icon type="account-circle" className="text-neutral" />]}
type="text"
placeholder="Username"
value={privateUsername}
onChange={handlePrivateUsernameNameChange}
disabled={disabled || isRecoveryCodes}
spellcheck={false}
autocomplete={false}
/>
</>
)}

{onStrictSignInChange && (
<div className="mb-1 flex items-center justify-between">
<Checkbox
name="use-strict-signin"
label="Use strict sign-in"
checked={isStrictSignin}
disabled={disabled || isRecoveryCodes}
onChange={handleStrictSigninChange}
/>
<a
href="https://standardnotes.com/help/security"
target="_blank"
rel="noopener noreferrer"
title="Learn more"
>
<Icon type="info" className="text-neutral" />
</a>
</div>
)}

<div className="mb-1 flex items-center justify-between">
<Checkbox
name="recovery-codes"
label="Use recovery code"
checked={isRecoveryCodes}
disabled={disabled}
spellcheck={false}
autocomplete={false}
onChange={handleIsRecoveryCodesChange}
/>
</>
)}

<Checkbox
name="custom-sync-server"
label="Custom sync server"
checked={enableServerOption}
onChange={handleServerOptionChange}
disabled={disabled || isRecoveryCodes}
/>
<DecoratedInput
type="text"
left={[<Icon type="server" className="text-neutral" />]}
placeholder="https://api.standardnotes.com"
value={server}
onChange={handleSyncServerChange}
disabled={!enableServerOption && !disabled && !isRecoveryCodes}
/>
</div>
</div>

{isRecoveryCodes && (
<>
<DecoratedInput
className={{ container: 'mb-2' }}
left={[<Icon type="security" className="text-neutral" />]}
type="text"
placeholder="Recovery code"
value={recoveryCodes}
onChange={handleRecoveryCodesChange}
disabled={disabled}
spellcheck={false}
autocomplete={false}
/>
</>
)}
</div>
<ServerPicker customServerAddress={server} handleCustomServerAddressChange={handleSyncServerChange} />
</>
) : null}
</>
)
Expand Down
Loading

0 comments on commit bc845c1

Please sign in to comment.