diff --git a/packages/models/src/Domain/Syncable/UserPrefs/AutoDownloadLimit.ts b/packages/models/src/Domain/Syncable/UserPrefs/AutoDownloadLimit.ts new file mode 100644 index 00000000000..faab4dc9ef7 --- /dev/null +++ b/packages/models/src/Domain/Syncable/UserPrefs/AutoDownloadLimit.ts @@ -0,0 +1,9 @@ +const BytesInOneMB = 1024 * 1024 + +export enum AutoDownloadLimit { + TwoAndHalfMB = 2.5 * BytesInOneMB, + FiveMB = 5 * BytesInOneMB, + TenMB = 10 * BytesInOneMB, + TwentyMB = 20 * BytesInOneMB, + FiftyMB = 50 * BytesInOneMB, +} diff --git a/packages/models/src/Domain/Syncable/UserPrefs/index.ts b/packages/models/src/Domain/Syncable/UserPrefs/index.ts index 859f7e4de00..2ac9b59102e 100644 --- a/packages/models/src/Domain/Syncable/UserPrefs/index.ts +++ b/packages/models/src/Domain/Syncable/UserPrefs/index.ts @@ -7,3 +7,4 @@ export * from './EditorLineWidth' export * from './NewNoteTitleFormat' export * from './ComponentPreferences' export * from './PrefDefaults' +export * from './AutoDownloadLimit' diff --git a/packages/services/src/Domain/Preferences/LocalPrefKey.ts b/packages/services/src/Domain/Preferences/LocalPrefKey.ts index af2e438a3f1..6fa3f4cedcd 100644 --- a/packages/services/src/Domain/Preferences/LocalPrefKey.ts +++ b/packages/services/src/Domain/Preferences/LocalPrefKey.ts @@ -1,4 +1,10 @@ -import { EditorFontSize, EditorLineHeight, EditorLineWidth } from '@standardnotes/models' +import { + AutoDownloadLimit, + PrefDefaults, + EditorFontSize, + EditorLineHeight, + EditorLineWidth, +} from '@standardnotes/models' export enum LocalPrefKey { ActiveThemes = 'activeThemes', @@ -7,6 +13,9 @@ export enum LocalPrefKey { AutoLightThemeIdentifier = 'autoLightThemeIdentifier', AutoDarkThemeIdentifier = 'autoDarkThemeIdentifier', + AlwaysAutoDownloadSuperEmbeds = 'alwaysAutoDownloadSuperEmbeds', + SuperEmbedAutoDownloadLimit = 'superEmbedAutoDownloadLimit', + EditorMonospaceEnabled = 'monospaceFont', EditorLineHeight = 'editorLineHeight', EditorLineWidth = 'editorLineWidth', @@ -20,8 +29,29 @@ export type LocalPrefValue = { [LocalPrefKey.AutoLightThemeIdentifier]: string [LocalPrefKey.AutoDarkThemeIdentifier]: string + [LocalPrefKey.AlwaysAutoDownloadSuperEmbeds]: boolean + [LocalPrefKey.SuperEmbedAutoDownloadLimit]: AutoDownloadLimit + [LocalPrefKey.EditorMonospaceEnabled]: boolean [LocalPrefKey.EditorLineHeight]: EditorLineHeight [LocalPrefKey.EditorLineWidth]: EditorLineWidth [LocalPrefKey.EditorFontSize]: EditorFontSize } + +export const LocalPrefDefaults = { + [LocalPrefKey.ActiveThemes]: PrefDefaults[LocalPrefKey.ActiveThemes], + [LocalPrefKey.UseSystemColorScheme]: PrefDefaults[LocalPrefKey.UseSystemColorScheme], + [LocalPrefKey.UseTranslucentUI]: PrefDefaults[LocalPrefKey.UseTranslucentUI], + [LocalPrefKey.AutoLightThemeIdentifier]: PrefDefaults[LocalPrefKey.AutoLightThemeIdentifier], + [LocalPrefKey.AutoDarkThemeIdentifier]: PrefDefaults[LocalPrefKey.AutoDarkThemeIdentifier], + + [LocalPrefKey.AlwaysAutoDownloadSuperEmbeds]: false, + [LocalPrefKey.SuperEmbedAutoDownloadLimit]: AutoDownloadLimit.FiveMB, + + [LocalPrefKey.EditorMonospaceEnabled]: PrefDefaults[LocalPrefKey.EditorMonospaceEnabled], + [LocalPrefKey.EditorLineHeight]: PrefDefaults[LocalPrefKey.EditorLineHeight], + [LocalPrefKey.EditorLineWidth]: PrefDefaults[LocalPrefKey.EditorLineWidth], + [LocalPrefKey.EditorFontSize]: PrefDefaults[LocalPrefKey.EditorFontSize], +} satisfies { + [key in LocalPrefKey]: LocalPrefValue[key] +} diff --git a/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx b/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx index 26cab51bdd9..c3d137e0a50 100644 --- a/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx +++ b/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx @@ -4,6 +4,7 @@ import { ApplicationEvent, FileDownloadProgress, FileItem, + LocalPrefKey, fileProgressToHumanReadableString, } from '@standardnotes/snjs' import { useEffect, useMemo, useState } from 'react' @@ -14,6 +15,9 @@ import PreviewComponent from './PreviewComponent' import Button from '../Button/Button' import { ProtectedIllustration } from '@standardnotes/icons' import { ImageZoomLevelProps } from './ImageZoomLevelProps' +import { useLocalPreference } from '../../Hooks/usePreference' +import { formatSizeToReadableString } from '@standardnotes/filepicker' +import { ElementIds } from '@/Constants/ElementIDs' type Props = { application: WebApplication @@ -22,13 +26,25 @@ type Props = { } & ImageZoomLevelProps const FilePreview = ({ file, application, isEmbeddedInSuper = false, imageZoomLevel, setImageZoomLevel }: Props) => { + const [alwaysAutoDownload] = useLocalPreference(LocalPrefKey.AlwaysAutoDownloadSuperEmbeds) + const [autoDownloadLimit] = useLocalPreference(LocalPrefKey.SuperEmbedAutoDownloadLimit) + const isOverLimit = file.decryptedSize > autoDownloadLimit + + const [shouldDownload, setShouldDownload] = useState(isEmbeddedInSuper ? alwaysAutoDownload || !isOverLimit : true) + useEffect(() => { + if (!isEmbeddedInSuper) { + return + } + setShouldDownload(alwaysAutoDownload || !isOverLimit) + }, [alwaysAutoDownload, isEmbeddedInSuper, isOverLimit]) + const [isAuthorized, setIsAuthorized] = useState(application.isAuthorizedToRenderItem(file)) const isFilePreviewable = useMemo(() => { return isFileTypePreviewable(file.mimeType) }, [file.mimeType]) - const [isDownloading, setIsDownloading] = useState(true) + const [isDownloading, setIsDownloading] = useState(shouldDownload) const [downloadProgress, setDownloadProgress] = useState() const [downloadedBytes, setDownloadedBytes] = useState() @@ -49,6 +65,10 @@ const FilePreview = ({ file, application, isEmbeddedInSuper = false, imageZoomLe }, [application, file]) useEffect(() => { + if (!shouldDownload) { + return + } + if (!isFilePreviewable || !isAuthorized) { setIsDownloading(false) setDownloadProgress(undefined) @@ -85,7 +105,7 @@ const FilePreview = ({ file, application, isEmbeddedInSuper = false, imageZoomLe } void downloadFileForPreview() - }, [application.files, downloadedBytes, file, isFilePreviewable, isAuthorized]) + }, [application.files, downloadedBytes, file, isFilePreviewable, isAuthorized, shouldDownload]) if (!isAuthorized) { const hasProtectionSources = application.hasProtectionSources() @@ -113,6 +133,58 @@ const FilePreview = ({ file, application, isEmbeddedInSuper = false, imageZoomLe ) } + if (!shouldDownload && !isDownloading && !downloadedBytes) { + return ( +
+
{file.name}
+

+ This file was not automatically loaded because it was larger ({formatSizeToReadableString(file.decryptedSize)} + ) than the set limit for auto-downloading embedded files ({formatSizeToReadableString(autoDownloadLimit)}) +

+
+ + +
+
+ ) + } + return isDownloading ? (
diff --git a/packages/web/src/javascripts/Components/Preferences/Controller/PreferencesSessionController.ts b/packages/web/src/javascripts/Components/Preferences/Controller/PreferencesSessionController.ts index ae0d89e3ee8..bbb1c53e569 100644 --- a/packages/web/src/javascripts/Components/Preferences/Controller/PreferencesSessionController.ts +++ b/packages/web/src/javascripts/Components/Preferences/Controller/PreferencesSessionController.ts @@ -1,4 +1,4 @@ -import { action, makeAutoObservable, observable } from 'mobx' +import { action, autorun, makeAutoObservable, observable } from 'mobx' import { WebApplication } from '@/Application/WebApplication' import { PackageProvider } from '../Panes/Plugins/PackageProvider' import { securityPrefsHasBubble } from '../Panes/Security/securityPrefsHasBubble' @@ -59,6 +59,10 @@ export class PreferencesSessionController { this.updateMenuBubbleCounts() } }) + + autorun(() => { + this.selectPane(application.preferencesController.currentPane) + }) } private updateMenuBubbleCounts(): void { diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/Defaults.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/Defaults.tsx index 15e4991c381..fc27c6c8c93 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/Defaults.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/Defaults.tsx @@ -7,7 +7,6 @@ import Switch from '@/Components/Switch/Switch' import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' import usePreference from '@/Hooks/usePreference' -import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' type Props = { application: WebApplication @@ -20,14 +19,10 @@ const Defaults: FunctionComponent = ({ application }) => { () => (application.getValue(AndroidConfirmBeforeExitKey) as boolean) ?? true, ) - const isMobile = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm) - const spellcheck = usePreference(PrefKey.EditorSpellcheck) const addNoteToParentFolders = usePreference(PrefKey.NoteAddToParentFolders) - const alwaysShowSuperToolbar = usePreference(PrefKey.AlwaysShowSuperToolbar) - const toggleSpellcheck = () => { application.toggleGlobalSpellcheck().catch(console.error) } @@ -77,25 +72,6 @@ const Defaults: FunctionComponent = ({ application }) => { checked={addNoteToParentFolders} />
- - {!isMobile && ( -
-
- Use always-visible toolbar in Super notes - - When enabled, the Super toolbar will always be shown at the top of the note. It can be temporarily - toggled using Cmd/Ctrl+Shift+K. When disabled, the Super toolbar will only be shown as a floating - toolbar when text is selected. - -
- { - application.setPreference(PrefKey.AlwaysShowSuperToolbar, !alwaysShowSuperToolbar).catch(console.error) - }} - checked={alwaysShowSuperToolbar} - /> -
- )} ) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/General.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/General.tsx index 622937ddc08..a22ca8654c0 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/General.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/General.tsx @@ -10,6 +10,7 @@ import SmartViews from './SmartViews/SmartViews' import Moments from './Moments' import NewNoteDefaults from './NewNoteDefaults' import { useApplication } from '@/Components/ApplicationProvider' +import SuperNotes from './SuperNotes' const General: FunctionComponent = () => { const application = useApplication() @@ -18,6 +19,7 @@ const General: FunctionComponent = () => { + diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/SuperNotes.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/SuperNotes.tsx new file mode 100644 index 00000000000..6faf1b5c212 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/SuperNotes.tsx @@ -0,0 +1,126 @@ +import { AutoDownloadLimit, LocalPrefKey, PrefKey } from '@standardnotes/snjs' +import { useMediaQuery, MutuallyExclusiveMediaQueryBreakpoints } from '../../../../Hooks/useMediaQuery' +import usePreference, { useLocalPreference } from '../../../../Hooks/usePreference' +import Switch from '../../../Switch/Switch' +import { Subtitle, Title, Text } from '../../PreferencesComponents/Content' +import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' +import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' +import { WebApplication } from '../../../../Application/WebApplication' +import HorizontalSeparator from '../../../Shared/HorizontalSeparator' +import Dropdown from '../../../Dropdown/Dropdown' +import { useMemo } from 'react' +import { ElementIds } from '@/Constants/ElementIDs' + +const SuperNotes = ({ application }: { application: WebApplication }) => { + const isMobile = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm) + + const alwaysShowSuperToolbar = usePreference(PrefKey.AlwaysShowSuperToolbar) + + const [alwaysAutoDownloadEmbeds, setAlwaysAutoDownloadEmbeds] = useLocalPreference( + LocalPrefKey.AlwaysAutoDownloadSuperEmbeds, + ) + const [autoDownloadLimit, setAutoDownloadLimit] = useLocalPreference(LocalPrefKey.SuperEmbedAutoDownloadLimit) + + const autoDownloadLimitOptions = useMemo( + () => [ + { + label: '2.5 MB', + value: 'TwoAndHalfMB', + }, + { + label: '5 MB', + value: 'FiveMB', + }, + { + label: '10 MB', + value: 'TenMB', + }, + { + label: '20 MB', + value: 'TwentyMB', + }, + { + label: '50 MB', + value: 'FiftyMB', + }, + ], + [], + ) + + const dropdownValue: keyof typeof AutoDownloadLimit = useMemo(() => { + switch (autoDownloadLimit) { + case AutoDownloadLimit.TwoAndHalfMB: + return 'TwoAndHalfMB' + case AutoDownloadLimit.FiveMB: + return 'FiveMB' + case AutoDownloadLimit.TenMB: + return 'TenMB' + case AutoDownloadLimit.TwentyMB: + return 'TwentyMB' + case AutoDownloadLimit.FiftyMB: + return 'FiftyMB' + } + }, [autoDownloadLimit]) + + return ( + + + Super notes + {!isMobile && ( + <> +
+
+ Use always-visible toolbar in Super notes + + When enabled, the Super toolbar will always be shown at the top of the note. It can be temporarily + toggled using Cmd/Ctrl+Shift+K. When disabled, the Super toolbar will only be shown as a floating + toolbar when text is selected. + +
+ { + application + .setPreference(PrefKey.AlwaysShowSuperToolbar, !alwaysShowSuperToolbar) + .catch(console.error) + }} + checked={alwaysShowSuperToolbar} + /> +
+ + + )} +
+
+ Always auto-download embedded files + + When enabled, embedded files will always be automatically downloaded regardless of their file size. + +
+ { + setAlwaysAutoDownloadEmbeds(!alwaysAutoDownloadEmbeds) + }} + checked={alwaysAutoDownloadEmbeds} + /> +
+ +
+ Auto-download limit for embedded files + Only embedded files below the set limit will be automatically downloaded +
+ { + setAutoDownloadLimit(AutoDownloadLimit[value as keyof typeof AutoDownloadLimit]) + }} + /> +
+
+
+
+ ) +} + +export default SuperNotes diff --git a/packages/web/src/javascripts/Components/Preferences/PreferencesView.tsx b/packages/web/src/javascripts/Components/Preferences/PreferencesView.tsx index 9380526845a..4da00ca4f25 100644 --- a/packages/web/src/javascripts/Components/Preferences/PreferencesView.tsx +++ b/packages/web/src/javascripts/Components/Preferences/PreferencesView.tsx @@ -17,10 +17,6 @@ const PreferencesView: FunctionComponent = ({ application, clo [application], ) - useEffect(() => { - menu.selectPane(application.preferencesController.currentPane) - }, [menu, application.preferencesController.currentPane]) - const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm) const addAndroidBackHandler = useAndroidBackHandler() diff --git a/packages/web/src/javascripts/Constants/ElementIDs.ts b/packages/web/src/javascripts/Constants/ElementIDs.ts index 19ca5fcad76..08aa3d591f2 100644 --- a/packages/web/src/javascripts/Constants/ElementIDs.ts +++ b/packages/web/src/javascripts/Constants/ElementIDs.ts @@ -15,4 +15,5 @@ export const ElementIds = { SearchBar: 'search-bar', ConflictResolutionButton: 'conflict-resolution-button', SuperEditor: 'super-editor', + AutoDownloadLimitPreference: 'auto-download-limit-pref', } as const diff --git a/packages/web/src/javascripts/Hooks/usePreference.tsx b/packages/web/src/javascripts/Hooks/usePreference.tsx index 9402aab9ec4..7a040064cab 100644 --- a/packages/web/src/javascripts/Hooks/usePreference.tsx +++ b/packages/web/src/javascripts/Hooks/usePreference.tsx @@ -1,11 +1,18 @@ import { useApplication } from '@/Components/ApplicationProvider' -import { ApplicationEvent, PrefKey, PrefDefaults, LocalPrefKey, LocalPrefValue } from '@standardnotes/snjs' +import { + ApplicationEvent, + PrefKey, + PrefDefaults, + LocalPrefKey, + LocalPrefValue, + LocalPrefDefaults, +} from '@standardnotes/snjs' import { useCallback, useEffect, useState } from 'react' export function useLocalPreference(preference: Key) { const application = useApplication() - const [value, setValue] = useState(application.preferences.getLocalValue(preference, PrefDefaults[preference])) + const [value, setValue] = useState(application.preferences.getLocalValue(preference, LocalPrefDefaults[preference])) const setNewValue = useCallback( (newValue: LocalPrefValue[Key]) => { @@ -16,7 +23,7 @@ export function useLocalPreference(preference: Key) { useEffect(() => { return application.addEventObserver(async () => { - const latestValue = application.preferences.getLocalValue(preference, PrefDefaults[preference]) + const latestValue = application.preferences.getLocalValue(preference, LocalPrefDefaults[preference]) setValue(latestValue) }, ApplicationEvent.LocalPreferencesChanged)