chore: preferences changes

This commit is contained in:
Aman Harwara
2023-04-26 19:02:19 +05:30
parent 157ad0b052
commit d501ae4410
8 changed files with 10 additions and 492 deletions

View File

@@ -10,7 +10,6 @@ const PREFERENCE_IDS = [
'get-free-month',
'help-feedback',
'whats-new',
'filesend',
] as const
export type PreferenceId = typeof PREFERENCE_IDS[number]

View File

@@ -2,7 +2,6 @@ import { WebApplication } from '@/Application/Application'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { FunctionComponent } from 'react'
import PreferencesPane from '@/Components/Preferences/PreferencesComponents/PreferencesPane'
import CloudLink from './CloudBackups/CloudBackups'
import DataBackups from './DataBackups'
import EmailBackups from './EmailBackups'
import FileBackupsCrossPlatform from './Files/FileBackupsCrossPlatform'
@@ -19,7 +18,6 @@ const Backups: FunctionComponent<Props> = ({ application, viewControllerManager
<DataBackups application={application} viewControllerManager={viewControllerManager} />
<FileBackupsCrossPlatform application={application} />
<EmailBackups application={application} />
<CloudLink application={application} />
</PreferencesPane>
)
}

View File

@@ -1,220 +0,0 @@
import {
useCallback,
useEffect,
useState,
FunctionComponent,
KeyboardEventHandler,
ChangeEventHandler,
MouseEventHandler,
} from 'react'
import {
ButtonType,
SettingName,
CloudProvider,
DropboxBackupFrequency,
GoogleDriveBackupFrequency,
OneDriveBackupFrequency,
} from '@standardnotes/snjs'
import { WebApplication } from '@/Application/Application'
import Button from '@/Components/Button/Button'
import { openInNewTab } from '@/Utils'
import { Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
import { KeyboardKey } from '@standardnotes/ui-services'
type Props = {
application: WebApplication
providerName: CloudProvider
isEntitledToCloudBackups: boolean
}
const CloudBackupProvider: FunctionComponent<Props> = ({ application, providerName, isEntitledToCloudBackups }) => {
const [authBegan, setAuthBegan] = useState(false)
const [successfullyInstalled, setSuccessfullyInstalled] = useState(false)
const [backupFrequency, setBackupFrequency] = useState<string | undefined>(undefined)
const [confirmation, setConfirmation] = useState('')
const disable: MouseEventHandler = async (event) => {
event.stopPropagation()
try {
const shouldDisable = await application.alertService.confirm(
'Are you sure you want to disable this integration?',
'Disable?',
'Disable',
ButtonType.Danger,
'Cancel',
)
if (shouldDisable) {
await application.settings.deleteSetting(backupFrequencySettingName)
await application.settings.deleteSetting(backupTokenSettingName)
setBackupFrequency(undefined)
}
} catch (error) {
application.alertService.alert(error as string).catch(console.error)
}
}
const installIntegration: MouseEventHandler = (event) => {
if (!isEntitledToCloudBackups) {
return
}
event.stopPropagation()
const authUrl = application.getCloudProviderIntegrationUrl(providerName)
openInNewTab(authUrl)
setAuthBegan(true)
}
const performBackupNow = async () => {
// A backup is performed anytime the setting is updated with the integration token, so just update it here
try {
await application.settings.updateSetting(backupFrequencySettingName, backupFrequency as string)
void application.alertService.alert(
'A backup has been triggered for this provider. Please allow a couple minutes for your backup to be processed.',
)
} catch (err) {
application.alertService
.alert('There was an error while trying to trigger a backup for this provider. Please try again.')
.catch(console.error)
}
}
const backupSettingsData = {
[CloudProvider.Dropbox]: {
backupTokenSettingName: SettingName.create(SettingName.NAMES.DropboxBackupToken).getValue(),
backupFrequencySettingName: SettingName.create(SettingName.NAMES.DropboxBackupFrequency).getValue(),
defaultBackupFrequency: DropboxBackupFrequency.Daily,
},
[CloudProvider.Google]: {
backupTokenSettingName: SettingName.create(SettingName.NAMES.GoogleDriveBackupToken).getValue(),
backupFrequencySettingName: SettingName.create(SettingName.NAMES.GoogleDriveBackupFrequency).getValue(),
defaultBackupFrequency: GoogleDriveBackupFrequency.Daily,
},
[CloudProvider.OneDrive]: {
backupTokenSettingName: SettingName.create(SettingName.NAMES.OneDriveBackupToken).getValue(),
backupFrequencySettingName: SettingName.create(SettingName.NAMES.OneDriveBackupFrequency).getValue(),
defaultBackupFrequency: OneDriveBackupFrequency.Daily,
},
}
const { backupTokenSettingName, backupFrequencySettingName, defaultBackupFrequency } =
backupSettingsData[providerName]
const getCloudProviderIntegrationTokenFromUrl = (url: URL) => {
const urlSearchParams = new URLSearchParams(url.search)
let integrationTokenKeyInUrl = ''
switch (providerName) {
case CloudProvider.Dropbox:
integrationTokenKeyInUrl = 'dbt'
break
case CloudProvider.Google:
integrationTokenKeyInUrl = 'key'
break
case CloudProvider.OneDrive:
integrationTokenKeyInUrl = 'key'
break
default:
throw new Error('Invalid Cloud Provider name')
}
return urlSearchParams.get(integrationTokenKeyInUrl)
}
const handleKeyPress: KeyboardEventHandler = async (event) => {
if (event.key === KeyboardKey.Enter) {
try {
const decryptedCode = atob(confirmation)
const urlFromDecryptedCode = new URL(decryptedCode)
const cloudProviderToken = getCloudProviderIntegrationTokenFromUrl(urlFromDecryptedCode)
if (!cloudProviderToken) {
throw new Error()
}
await application.settings.updateSetting(backupTokenSettingName, cloudProviderToken)
await application.settings.updateSetting(backupFrequencySettingName, defaultBackupFrequency)
setBackupFrequency(defaultBackupFrequency)
setAuthBegan(false)
setSuccessfullyInstalled(true)
setConfirmation('')
await application.alertService.alert(
`${providerName} has been successfully installed. Your first backup has also been queued and should be reflected in your external cloud's folder within the next few minutes.`,
)
} catch (e) {
await application.alertService.alert('Invalid code. Please try again.')
}
}
}
const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {
setConfirmation(event.target.value)
}
const getIntegrationStatus = useCallback(async () => {
if (!application.getUser()) {
return
}
const frequency = await application.settings.getSetting(backupFrequencySettingName)
setBackupFrequency(frequency)
}, [application, backupFrequencySettingName])
useEffect(() => {
getIntegrationStatus().catch(console.error)
}, [getIntegrationStatus])
const isExpanded = authBegan || successfullyInstalled
const shouldShowEnableButton = !backupFrequency && !authBegan
const additionalClass = isEntitledToCloudBackups ? '' : 'opacity-50 cursor-default pointer-events-none'
return (
<div
className={`mr-1 ${isExpanded ? 'expanded' : ' '} ${
shouldShowEnableButton || backupFrequency ? 'flex items-center justify-between' : ''
}`}
>
<div>
<Subtitle className={additionalClass}>{providerName}</Subtitle>
{successfullyInstalled && <p>{providerName} has been successfully enabled.</p>}
</div>
{authBegan && (
<div>
<p className="sk-panel-row">
Complete authentication from the newly opened window. Upon completion, a confirmation code will be
displayed. Enter this code below:
</p>
<div className={'mt-1'}>
<input
className="sk-input sk-base center-text"
placeholder="Enter confirmation code"
value={confirmation}
onKeyPress={handleKeyPress}
onChange={handleChange}
/>
</div>
</div>
)}
{shouldShowEnableButton && (
<div>
<Button
label="Enable"
className={`min-w-40 px-1 text-xs ${additionalClass}`}
onClick={installIntegration}
disabled={!isEntitledToCloudBackups}
/>
</div>
)}
{backupFrequency && (
<div className={'flex flex-col items-end'}>
<Button className={`mb-2 min-w-40 ${additionalClass}`} label="Perform Backup" onClick={performBackupNow} />
<Button className="min-w-40" label="Disable" onClick={disable} />
</div>
)}
</div>
)
}
export default CloudBackupProvider

View File

@@ -1,155 +0,0 @@
import CloudBackupProvider from './CloudBackupProvider'
import { useCallback, useEffect, useState, FunctionComponent, Fragment } from 'react'
import { WebApplication } from '@/Application/Application'
import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import {
FeatureStatus,
FeatureIdentifier,
CloudProvider,
MuteFailedCloudBackupsEmailsOption,
SettingName,
} from '@standardnotes/snjs'
import Switch from '@/Components/Switch/Switch'
import { convertStringifiedBooleanToBoolean } from '@/Utils'
import { STRING_FAILED_TO_UPDATE_USER_SETTING } from '@/Constants/Strings'
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
import Spinner from '@/Components/Spinner/Spinner'
const providerData = [{ name: CloudProvider.Dropbox }, { name: CloudProvider.Google }, { name: CloudProvider.OneDrive }]
type Props = {
application: WebApplication
}
const CloudLink: FunctionComponent<Props> = ({ application }) => {
const [isEntitledToCloudBackups, setIsEntitledToCloudBackups] = useState(false)
const [isFailedCloudBackupEmailMuted, setIsFailedCloudBackupEmailMuted] = useState(true)
const [isLoading, setIsLoading] = useState(false)
const additionalClass = isEntitledToCloudBackups ? '' : 'opacity-50 cursor-default pointer-events-none'
const loadIsFailedCloudBackupEmailMutedSetting = useCallback(async () => {
if (!application.getUser()) {
return
}
setIsLoading(true)
try {
const userSettings = await application.settings.listSettings()
setIsFailedCloudBackupEmailMuted(
convertStringifiedBooleanToBoolean(
userSettings.getSettingValue(
SettingName.create(SettingName.NAMES.MuteFailedCloudBackupsEmails).getValue(),
MuteFailedCloudBackupsEmailsOption.NotMuted,
),
),
)
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
}
}, [application])
useEffect(() => {
const dailyDropboxBackupStatus = application.features.getFeatureStatus(FeatureIdentifier.DailyDropboxBackup)
const dailyGdriveBackupStatus = application.features.getFeatureStatus(FeatureIdentifier.DailyGDriveBackup)
const dailyOneDriveBackupStatus = application.features.getFeatureStatus(FeatureIdentifier.DailyOneDriveBackup)
const isCloudBackupsAllowed = [dailyDropboxBackupStatus, dailyGdriveBackupStatus, dailyOneDriveBackupStatus].every(
(status) => status === FeatureStatus.Entitled,
)
setIsEntitledToCloudBackups(isCloudBackupsAllowed)
loadIsFailedCloudBackupEmailMutedSetting().catch(console.error)
}, [application, loadIsFailedCloudBackupEmailMutedSetting])
const updateSetting = async (settingName: SettingName, payload: string): Promise<boolean> => {
try {
await application.settings.updateSetting(settingName, payload)
return true
} catch (e) {
application.alertService.alert(STRING_FAILED_TO_UPDATE_USER_SETTING).catch(console.error)
return false
}
}
const toggleMuteFailedCloudBackupEmails = async () => {
if (!isEntitledToCloudBackups) {
return
}
const previousValue = isFailedCloudBackupEmailMuted
setIsFailedCloudBackupEmailMuted(!isFailedCloudBackupEmailMuted)
const updateResult = await updateSetting(
SettingName.create(SettingName.NAMES.MuteFailedCloudBackupsEmails).getValue(),
`${!isFailedCloudBackupEmailMuted}`,
)
if (!updateResult) {
setIsFailedCloudBackupEmailMuted(previousValue)
}
}
return (
<PreferencesGroup>
<PreferencesSegment>
<Title>Cloud Backups</Title>
{!isEntitledToCloudBackups && (
<>
<Text>
A <span className={'font-bold'}>Plus</span> or <span className={'font-bold'}>Pro</span> subscription plan
is required to enable Cloud Backups.{' '}
<a target="_blank" href="https://standardnotes.com/features">
Learn more
</a>
.
</Text>
<HorizontalSeparator classes="mt-3 mb-3" />
</>
)}
<div>
<Text className={additionalClass}>
Configure the integrations below to enable automatic daily backups of your encrypted data set to your
third-party cloud provider.
</Text>
<div>
<HorizontalSeparator classes={`mt-3 mb-3 ${additionalClass}`} />
<div>
{providerData.map(({ name }) => (
<Fragment key={name}>
<CloudBackupProvider
application={application}
providerName={name}
isEntitledToCloudBackups={isEntitledToCloudBackups}
/>
<HorizontalSeparator classes={`mt-3 mb-3 ${additionalClass}`} />
</Fragment>
))}
</div>
</div>
<div className={additionalClass}>
<Subtitle>Email preferences</Subtitle>
<div className="mt-1 flex items-center justify-between">
<div className="flex flex-col">
<Text>Receive a notification email if a cloud backup fails.</Text>
</div>
{isLoading ? (
<Spinner className="h-4 w-4" />
) : (
<Switch
onChange={toggleMuteFailedCloudBackupEmails}
checked={!isFailedCloudBackupEmailMuted}
disabled={!isEntitledToCloudBackups}
/>
)}
</div>
</div>
</div>
</PreferencesSegment>
</PreferencesGroup>
)
}
export default CloudLink

View File

@@ -1,81 +0,0 @@
import { FunctionComponent } from 'react'
import { Title, Subtitle, Text, LinkButton } from '@/Components/Preferences/PreferencesComponents/Content'
import PreferencesPane from '../PreferencesComponents/PreferencesPane'
import PreferencesGroup from '../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../PreferencesComponents/PreferencesSegment'
const CloudLink: FunctionComponent = () => (
<PreferencesPane>
<PreferencesGroup>
<PreferencesSegment>
<Title>Frequently asked questions</Title>
<div className="h-2 w-full" />
<Subtitle>Who can read my private notes?</Subtitle>
<Text>
Quite simply: no one but you. Not us, not your ISP, not a hacker, and not a government agency. As long as you
keep your password safe, and your password is reasonably strong, then you are the only person in the world
with the ability to decrypt your notes. For more on how we handle your privacy and security, check out our
easy to read{' '}
<a target="_blank" href="https://standardnotes.com/privacy">
Privacy Manifesto.
</a>
</Text>
</PreferencesSegment>
<PreferencesSegment>
<Subtitle>Can I collaborate with others on a note?</Subtitle>
<Text>
Because of our encrypted architecture, Standard Notes does not currently provide a real-time collaboration
solution. Multiple users can share the same account however, but editing at the same time may result in sync
conflicts, which may result in the duplication of notes.
</Text>
</PreferencesSegment>
<PreferencesSegment>
<Subtitle>Can I use Standard Notes totally offline?</Subtitle>
<Text>
Standard Notes can be used totally offline without an account, and without an internet connection. You can
find{' '}
<a target="_blank" href="https://standardnotes.com/help/59/can-i-use-standard-notes-totally-offline">
more details here.
</a>
</Text>
</PreferencesSegment>
<PreferencesSegment>
<Subtitle>Cant find your question here?</Subtitle>
<LinkButton className="mt-3" label="Open FAQ" link="https://standardnotes.com/help" />
</PreferencesSegment>
</PreferencesGroup>
<PreferencesGroup>
<PreferencesSegment>
<Title>Community forum</Title>
<Text>
If you have an issue, found a bug or want to suggest a feature, you can browse or post to the forum. Its
recommended for non-account related issues. Please read our{' '}
<a target="_blank" href="https://standardnotes.com/longevity/">
Longevity statement
</a>{' '}
before advocating for a feature request.
</Text>
<LinkButton className="mt-3" label="Go to the forum" link="https://forum.standardnotes.org/" />
</PreferencesSegment>
</PreferencesGroup>
<PreferencesGroup>
<PreferencesSegment>
<Title>Community groups</Title>
<Text>
Want to meet other passionate note-takers and privacy enthusiasts? Want to share your feedback with us? Join
the Standard Notes community groups for discussions on security, themes, editors and more.
</Text>
<LinkButton className="mt-3" link="https://standardnotes.com/discord" label="Join our Discord" />
</PreferencesSegment>
</PreferencesGroup>
<PreferencesGroup>
<PreferencesSegment>
<Title>Account related issue?</Title>
<Text>Send an email to help@standardnotes.com and well sort it out.</Text>
<LinkButton className="mt-3" link="mailto: help@standardnotes.com" label="Email us" />
</PreferencesSegment>
</PreferencesGroup>
</PreferencesPane>
)
export default CloudLink

View File

@@ -30,11 +30,11 @@ const LabsPane: FunctionComponent<Props> = ({ application }) => {
const [experimentalFeatures, setExperimentalFeatures] = useState<ExperimentalFeatureItem[]>([])
const [isPaneGesturesEnabled, setIsPaneGesturesEnabled] = useState(() =>
application.getPreference(PrefKey.PaneGesturesEnabled, false),
application.getPreference(PrefKey.PaneGesturesEnabled),
)
useEffect(() => {
return application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => {
setIsPaneGesturesEnabled(application.getPreference(PrefKey.PaneGesturesEnabled, false))
setIsPaneGesturesEnabled(application.getPreference(PrefKey.PaneGesturesEnabled))
})
}, [application])
@@ -69,7 +69,7 @@ const LabsPane: FunctionComponent<Props> = ({ application }) => {
<LabsFeature
name="Pane switch gestures"
description="Allows using gestures to navigate"
isEnabled={isPaneGesturesEnabled}
isEnabled={!!isPaneGesturesEnabled}
toggleFeature={() => {
void application.setPreference(PrefKey.PaneGesturesEnabled, !isPaneGesturesEnabled)
}}

View File

@@ -42,7 +42,6 @@ const READY_PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
{ id: 'appearance', label: 'Appearance', icon: 'themes' },
{ id: 'listed', label: 'Listed', icon: 'listed' },
{ id: 'help-feedback', label: 'Help & feedback', icon: 'help' },
{ id: 'filesend', label: 'FileSend', icon: 'open-in' },
]
export class PreferencesMenu {

View File

@@ -1,12 +1,11 @@
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useMemo } from 'react'
import { FunctionComponent, useMemo } from 'react'
import Dropdown from '../Dropdown/Dropdown'
import { DropdownItem } from '../Dropdown/DropdownItem'
import PreferencesMenuItem from './PreferencesComponents/MenuItem'
import { PreferencesMenu } from './PreferencesMenu'
import { PreferenceId } from '@standardnotes/ui-services'
import { useApplication } from '../ApplicationProvider'
import { classNames, Environment } from '@standardnotes/snjs'
import { classNames } from '@standardnotes/snjs'
import { isIOS } from '@/Utils'
type Props = {
@@ -14,35 +13,18 @@ type Props = {
}
const PreferencesMenuView: FunctionComponent<Props> = ({ menu }) => {
const application = useApplication()
const { selectedPaneId, selectPane, menuItems } = menu
const dropdownMenuItems: DropdownItem[] = useMemo(
() =>
menuItems
.filter((pref) => pref.id !== 'filesend')
.map((menuItem) => ({
icon: menuItem.icon,
label: menuItem.label,
value: menuItem.id,
})),
menuItems.map((menuItem) => ({
icon: menuItem.icon,
label: menuItem.label,
value: menuItem.id,
})),
[menuItems],
)
const openFileSend = useCallback(() => {
const link = 'https://filesend.standardnotes.com/'
if (application.isNativeMobileWeb()) {
application.mobileDevice().openUrl(link)
return
} else if (application.environment === Environment.Desktop) {
application.desktopDevice?.openUrl(link)
return
}
window.open(link, '_blank')
}, [application])
return (
<div
className={classNames(
@@ -59,10 +41,6 @@ const PreferencesMenuView: FunctionComponent<Props> = ({ menu }) => {
selected={pref.selected}
hasBubble={pref.hasBubble}
onClick={() => {
if (pref.id === 'filesend') {
openFileSend()
return
}
selectPane(pref.id)
}}
/>