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 <amanharwara@protonmail.com>
This commit is contained in:
Karol Sójko
2023-12-28 10:50:14 +01:00
committed by GitHub
parent f376607f27
commit bc845c1633
13 changed files with 198 additions and 109 deletions

View File

@@ -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<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)
@@ -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) => {
@@ -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],
)
@@ -124,100 +115,87 @@ const AdvancedOptions: FunctionComponent<Props> = ({
</div>
</button>
{showAdvanced ? (
<div className="my-2 px-3">
{children}
<>
<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}
/>
</>
)}
{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>
{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}
/>
</>
)}
{isRecoveryCodes && (
<>
<DecoratedInput
className={{ container: 'mb-2' }}
left={[<Icon type="security" className="text-neutral" />]}
type="text"
placeholder="Recovery code"
value={recoveryCodes}
onChange={handleRecoveryCodesChange}
{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}
/>
</>
)}
</div>
<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>
{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}
</>
)

View File

@@ -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<ServerType>('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 (
<div className={`flex h-full flex-grow flex-col px-3 pb-1.5 ${className}`}>
<div className="mb-2 flex font-bold">Sync Server</div>
<RadioButtonGroup value={currentType} items={options} onChange={selectTab} />
{currentType === 'custom' && (
<DecoratedInput
className={{
container: 'mt-1',
}}
type="text"
left={[<Icon type="server" className="text-neutral" />]}
placeholder={DefaultHost.Api}
value={customServerAddress}
onChange={handleCustomServerAddressChange}
/>
)}
</div>
)
}
export default ServerPicker

View File

@@ -0,0 +1 @@
export type ServerType = 'standard' | 'custom' | 'home server'

View File

@@ -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
}