refactor: native feature management (#2350)

This commit is contained in:
Mo
2023-07-12 12:56:08 -05:00
committed by GitHub
parent 49f7581cd8
commit 078ef3772c
223 changed files with 3996 additions and 3438 deletions

View File

@@ -38,7 +38,7 @@ export abstract class AbstractComponent<P = PureComponentProps, S = PureComponen
}
public get viewControllerManager(): ViewControllerManager {
return this.application.getViewControllerManager()
return this.application.controllers
}
autorun(view: (r: IReactionPublic) => void): void {

View File

@@ -47,7 +47,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
const currentWriteErrorDialog = useRef<Promise<void> | null>(null)
const currentLoadErrorDialog = useRef<Promise<void> | null>(null)
const viewControllerManager = application.getViewControllerManager()
const viewControllerManager = application.controllers
useEffect(() => {
const desktopService = application.getDesktopService()
@@ -198,7 +198,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
<ApplicationProvider application={application}>
<CommandProvider service={application.keyboardService}>
<AndroidBackHandlerProvider application={application}>
<ResponsivePaneProvider paneController={application.getViewControllerManager().paneController}>
<ResponsivePaneProvider paneController={application.controllers.paneController}>
<PremiumModalProvider
application={application}
featuresController={viewControllerManager.featuresController}
@@ -230,7 +230,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
<ApplicationProvider application={application}>
<CommandProvider service={application.keyboardService}>
<AndroidBackHandlerProvider application={application}>
<ResponsivePaneProvider paneController={application.getViewControllerManager().paneController}>
<ResponsivePaneProvider paneController={application.controllers.paneController}>
<PremiumModalProvider
application={application}
featuresController={viewControllerManager.featuresController}
@@ -252,7 +252,6 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
application={application}
historyModalController={viewControllerManager.historyModalController}
selectionController={viewControllerManager.selectionController}
subscriptionController={viewControllerManager.subscriptionController}
/>
</>
{renderChallenges()}

View File

@@ -8,7 +8,7 @@ import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintFor
import { CHANGE_EDITOR_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services'
import { useApplication } from '../ApplicationProvider'
import { NoteViewController } from '../NoteView/Controller/NoteViewController'
import { noteTypeForEditorIdentifier } from '@standardnotes/snjs'
import { NoteType, noteTypeForEditorIdentifier } from '@standardnotes/snjs'
type Props = {
viewControllerManager: ViewControllerManager
@@ -30,6 +30,7 @@ const ChangeEditorButton: FunctionComponent<Props> = ({
const [selectedEditor, setSelectedEditor] = useState(() => {
return note ? application.componentManager.editorForNote(note) : undefined
})
const noteType = noteViewController?.isTemplateNote
? noteTypeForEditorIdentifier(
application.geDefaultEditorIdentifier(
@@ -38,9 +39,12 @@ const ChangeEditorButton: FunctionComponent<Props> = ({
: undefined,
),
)
: note
: note && note.noteType != NoteType.Unknown
? note.noteType
: selectedEditor?.package_info.note_type
: selectedEditor
? selectedEditor.noteType
: NoteType.Unknown
const [selectedEditorIcon, selectedEditorIconTint] = getIconAndTintForNoteType(noteType, true)
const [isClickOutsideDisabled, setIsClickOutsideDisabled] = useState(false)

View File

@@ -3,7 +3,16 @@ import Menu from '@/Components/Menu/Menu'
import { usePremiumModal } from '@/Hooks/usePremiumModal'
import { STRING_EDIT_LOCKED_ATTEMPT } from '@/Constants/Strings'
import { WebApplication } from '@/Application/WebApplication'
import { ComponentArea, NoteMutator, NoteType, PrefKey, SNComponent, SNNote } from '@standardnotes/snjs'
import {
ComponentOrNativeFeature,
EditorFeatureDescription,
FeatureIdentifier,
IframeComponentFeatureDescription,
NoteMutator,
NoteType,
PrefKey,
SNNote,
} from '@standardnotes/snjs'
import { Fragment, FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
@@ -21,7 +30,7 @@ type ChangeEditorMenuProps = {
closeMenu: () => void
isVisible: boolean
note: SNNote | undefined
onSelect?: (component: SNComponent | undefined) => void
onSelect?: (component: ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>) => void
setDisableClickOutside?: (value: boolean) => void
}
@@ -35,25 +44,23 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
onSelect,
setDisableClickOutside,
}) => {
const editors = useMemo(
() =>
application.componentManager.componentsForArea(ComponentArea.Editor).sort((a, b) => {
return a.displayName.toLowerCase() < b.displayName.toLowerCase() ? -1 : 1
}),
[application.componentManager],
)
const groups = useMemo(() => createEditorMenuGroups(application, editors), [application, editors])
const [currentComponent, setCurrentComponent] = useState<SNComponent>()
const groups = useMemo(() => createEditorMenuGroups(application), [application])
const [currentFeature, setCurrentFeature] =
useState<ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>>()
const [pendingConversionItem, setPendingConversionItem] = useState<EditorMenuItem | null>(null)
const showSuperNoteImporter =
!!pendingConversionItem && note?.noteType !== NoteType.Super && pendingConversionItem.noteType === NoteType.Super
!!pendingConversionItem &&
note?.noteType !== NoteType.Super &&
pendingConversionItem.uiFeature.noteType === NoteType.Super
const showSuperNoteConverter =
!!pendingConversionItem && note?.noteType === NoteType.Super && pendingConversionItem.noteType !== NoteType.Super
!!pendingConversionItem &&
note?.noteType === NoteType.Super &&
pendingConversionItem.uiFeature.noteType !== NoteType.Super
useEffect(() => {
if (note) {
setCurrentComponent(application.componentManager.editorForNote(note))
setCurrentFeature(application.componentManager.editorForNote(note))
}
}, [application, note])
@@ -61,61 +68,52 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
const isSelected = useCallback(
(item: EditorMenuItem) => {
if (currentComponent) {
return item.component?.identifier === currentComponent.identifier
if (currentFeature) {
return item.uiFeature.featureIdentifier === currentFeature.featureIdentifier
}
const itemNoteTypeIsSameAsCurrentNoteType = item.noteType === note?.noteType
const noteDoesntHaveTypeAndItemIsPlain = !note?.noteType && item.noteType === NoteType.Plain
const unknownNoteTypeAndItemIsPlain = note?.noteType === NoteType.Unknown && item.noteType === NoteType.Plain
const itemNoteTypeIsSameAsCurrentNoteType = item.uiFeature.noteType === note?.noteType
const noteDoesntHaveTypeAndItemIsPlain = !note?.noteType && item.uiFeature.noteType === NoteType.Plain
const unknownNoteTypeAndItemIsPlain =
note?.noteType === NoteType.Unknown && item.uiFeature.noteType === NoteType.Plain
return itemNoteTypeIsSameAsCurrentNoteType || noteDoesntHaveTypeAndItemIsPlain || unknownNoteTypeAndItemIsPlain
},
[currentComponent, note],
[currentFeature, note],
)
const selectComponent = useCallback(
async (component: SNComponent, note: SNNote) => {
if (component.conflictOf) {
void application.changeAndSaveItem(component, (mutator) => {
async (
uiFeature: ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>,
note: SNNote,
) => {
if (uiFeature.isComponent && uiFeature.asComponent.conflictOf) {
void application.changeAndSaveItem(uiFeature.asComponent, (mutator) => {
mutator.conflictOf = undefined
})
}
await application.getViewControllerManager().itemListController.insertCurrentIfTemplate()
await application.controllers.itemListController.insertCurrentIfTemplate()
await application.changeAndSaveItem(note, (mutator) => {
const noteMutator = mutator as NoteMutator
noteMutator.noteType = component.noteType
noteMutator.editorIdentifier = component.identifier
noteMutator.noteType = uiFeature.noteType
noteMutator.editorIdentifier = uiFeature.featureIdentifier
})
setCurrentComponent(application.componentManager.editorForNote(note))
setCurrentFeature(application.componentManager.editorForNote(note))
if (uiFeature.featureIdentifier === FeatureIdentifier.PlainEditor) {
reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled))
}
},
[application],
)
const selectNonComponent = useCallback(
async (item: EditorMenuItem, note: SNNote) => {
await application.getViewControllerManager().itemListController.insertCurrentIfTemplate()
reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled))
await application.changeAndSaveItem(note, (mutator) => {
const noteMutator = mutator as NoteMutator
noteMutator.noteType = item.noteType
noteMutator.editorIdentifier = undefined
})
setCurrentComponent(undefined)
},
[application],
)
const selectItem = useCallback(
async (itemToBeSelected: EditorMenuItem) => {
if (!itemToBeSelected.isEntitled) {
premiumModal.activate(itemToBeSelected.name)
const handleMenuSelection = useCallback(
async (menuItem: EditorMenuItem) => {
if (!menuItem.isEntitled) {
premiumModal.activate(menuItem.uiFeature.displayName)
return
}
@@ -128,28 +126,28 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
return
}
if (itemToBeSelected.noteType === NoteType.Super) {
if (menuItem.uiFeature.noteType === NoteType.Super) {
if (note.noteType === NoteType.Super) {
return
}
setPendingConversionItem(itemToBeSelected)
setPendingConversionItem(menuItem)
setDisableClickOutside?.(true)
return
}
if (note.noteType === NoteType.Super && note.text.length > 0) {
setPendingConversionItem(itemToBeSelected)
setPendingConversionItem(menuItem)
setDisableClickOutside?.(true)
return
}
let shouldMakeSelection = true
if (itemToBeSelected.component) {
if (menuItem.uiFeature) {
const changeRequiresAlert = application.componentManager.doesEditorChangeRequireAlert(
currentComponent,
itemToBeSelected.component,
currentFeature,
menuItem.uiFeature,
)
if (changeRequiresAlert) {
@@ -158,17 +156,13 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
}
if (shouldMakeSelection) {
if (itemToBeSelected.component) {
selectComponent(itemToBeSelected.component, note).catch(console.error)
} else {
selectNonComponent(itemToBeSelected, note).catch(console.error)
}
selectComponent(menuItem.uiFeature, note).catch(console.error)
}
closeMenu()
if (onSelect) {
onSelect(itemToBeSelected.component)
onSelect(menuItem.uiFeature)
}
},
[
@@ -179,9 +173,8 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
application.alertService,
application.componentManager,
setDisableClickOutside,
currentComponent,
currentFeature,
selectComponent,
selectNonComponent,
],
)
@@ -190,15 +183,9 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
return
}
if (pendingConversionItem.component) {
selectComponent(pendingConversionItem.component, note).catch(console.error)
closeMenu()
return
}
selectNonComponent(pendingConversionItem, note).catch(console.error)
selectComponent(pendingConversionItem.uiFeature, note).catch(console.error)
closeMenu()
}, [pendingConversionItem, note, selectNonComponent, closeMenu, selectComponent])
}, [pendingConversionItem, note, closeMenu, selectComponent])
const closeSuperNoteImporter = () => {
setPendingConversionItem(null)
@@ -220,30 +207,30 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
return (
<Fragment key={groupId}>
<div className={`border-0 border-t border-solid border-border py-1 ${index === 0 ? 'border-t-0' : ''}`}>
{group.items.map((item) => {
{group.items.map((menuItem) => {
const onClickEditorItem = () => {
selectItem(item).catch(console.error)
handleMenuSelection(menuItem).catch(console.error)
}
return (
<MenuRadioButtonItem
key={item.name}
key={menuItem.uiFeature.uniqueIdentifier}
onClick={onClickEditorItem}
className={'flex-row-reversed py-2'}
checked={item.isEntitled ? isSelected(item) : false}
info={item.description}
checked={isSelected(menuItem)}
info={menuItem.uiFeature.description}
>
<div className="flex flex-grow items-center justify-between">
<div className={`flex items-center ${group.featured ? 'font-bold' : ''}`}>
{group.icon && <Icon type={group.icon} className={`mr-2 ${group.iconClassName}`} />}
{item.name}
{item.isLabs && (
{menuItem.uiFeature.displayName}
{menuItem.isLabs && (
<Pill className="py-0.5 px-1.5" style="success">
Labs
</Pill>
)}
</div>
{!item.isEntitled && (
{!menuItem.isEntitled && (
<Icon type={PremiumFeatureIconName} className={PremiumFeatureIconClass} />
)}
</div>

View File

@@ -2,7 +2,14 @@ import { WebApplication } from '@/Application/WebApplication'
import { STRING_EDIT_LOCKED_ATTEMPT } from '@/Constants/Strings'
import { usePremiumModal } from '@/Hooks/usePremiumModal'
import { createEditorMenuGroups } from '@/Utils/createEditorMenuGroups'
import { ComponentArea, NoteMutator, NoteType, SNComponent, SNNote } from '@standardnotes/snjs'
import {
ComponentOrNativeFeature,
EditorFeatureDescription,
IframeComponentFeatureDescription,
NoteMutator,
NoteType,
SNNote,
} from '@standardnotes/snjs'
import { Fragment, useCallback, useMemo, useState } from 'react'
import Icon from '../Icon/Icon'
import { PremiumFeatureIconName, PremiumFeatureIconClass } from '../Icon/PremiumFeatureIcon'
@@ -22,7 +29,7 @@ type Props = {
setDisableClickOutside: (value: boolean) => void
}
const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Props) => {
const ChangeEditorMultipleMenu = ({ application, notes, setDisableClickOutside }: Props) => {
const premiumModal = usePremiumModal()
const [itemToBeSelected, setItemToBeSelected] = useState<EditorMenuItem | undefined>()
@@ -30,47 +37,32 @@ const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Prop
const hasSelectedLockedNotes = useMemo(() => notes.some((note) => note.locked), [notes])
const editors = useMemo(
() =>
application.componentManager.componentsForArea(ComponentArea.Editor).sort((a, b) => {
return a.displayName.toLowerCase() < b.displayName.toLowerCase() ? -1 : 1
}),
[application.componentManager],
)
const groups = useMemo(() => createEditorMenuGroups(application, editors), [application, editors])
const groups = useMemo(() => createEditorMenuGroups(application), [application])
const selectComponent = useCallback(
async (component: SNComponent, note: SNNote) => {
if (component.conflictOf) {
void application.changeAndSaveItem(component, (mutator) => {
async (
uiFeature: ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>,
note: SNNote,
) => {
if (uiFeature.isComponent && uiFeature.asComponent.conflictOf) {
void application.changeAndSaveItem(uiFeature.asComponent, (mutator) => {
mutator.conflictOf = undefined
})
}
await application.changeAndSaveItem(note, (mutator) => {
const noteMutator = mutator as NoteMutator
noteMutator.noteType = component.noteType
noteMutator.editorIdentifier = component.identifier
noteMutator.noteType = uiFeature.noteType
noteMutator.editorIdentifier = uiFeature.featureIdentifier
})
},
[application],
)
const selectNonComponent = useCallback(
async (item: EditorMenuItem, note: SNNote) => {
await application.changeAndSaveItem(note, (mutator) => {
const noteMutator = mutator as NoteMutator
noteMutator.noteType = item.noteType
noteMutator.editorIdentifier = undefined
})
},
[application],
)
const selectItem = useCallback(
const handleMenuSelection = useCallback(
async (itemToBeSelected: EditorMenuItem) => {
if (!itemToBeSelected.isEntitled) {
premiumModal.activate(itemToBeSelected.name)
premiumModal.activate(itemToBeSelected.uiFeature.displayName)
return
}
@@ -79,35 +71,27 @@ const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Prop
return
}
if (itemToBeSelected.noteType === NoteType.Super) {
if (itemToBeSelected.uiFeature.noteType === NoteType.Super) {
setDisableClickOutside(true)
setItemToBeSelected(itemToBeSelected)
setConfirmationQueue(notes)
return
}
if (itemToBeSelected.component) {
const changeRequiresConfirmation = notes.some((note) => {
const editorForNote = application.componentManager.editorForNote(note)
return application.componentManager.doesEditorChangeRequireAlert(editorForNote, itemToBeSelected.component)
})
const changeRequiresConfirmation = notes.some((note) => {
const editorForNote = application.componentManager.editorForNote(note)
return application.componentManager.doesEditorChangeRequireAlert(editorForNote, itemToBeSelected.uiFeature)
})
if (changeRequiresConfirmation) {
const canChange = await application.componentManager.showEditorChangeAlert()
if (!canChange) {
return
}
if (changeRequiresConfirmation) {
const canChange = await application.componentManager.showEditorChangeAlert()
if (!canChange) {
return
}
for (const note of notes) {
void selectComponent(itemToBeSelected.component, note)
}
return
}
for (const note of notes) {
void selectNonComponent(itemToBeSelected, note)
void selectComponent(itemToBeSelected.uiFeature, note)
}
},
[
@@ -117,14 +101,13 @@ const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Prop
notes,
premiumModal,
selectComponent,
selectNonComponent,
setDisableClickOutside,
],
)
const groupsWithItems = groups.filter((group) => group.items && group.items.length)
const showSuperImporter = itemToBeSelected?.noteType === NoteType.Super && confirmationQueue.length > 0
const showSuperImporter = itemToBeSelected?.uiFeature.noteType === NoteType.Super && confirmationQueue.length > 0
const closeCurrentSuperNoteImporter = useCallback(() => {
const remainingNotes = confirmationQueue.slice(1)
@@ -144,10 +127,10 @@ const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Prop
return
}
void selectNonComponent(itemToBeSelected, confirmationQueue[0])
void selectComponent(itemToBeSelected.uiFeature, confirmationQueue[0])
closeCurrentSuperNoteImporter()
}, [closeCurrentSuperNoteImporter, confirmationQueue, itemToBeSelected, selectNonComponent])
}, [closeCurrentSuperNoteImporter, confirmationQueue, itemToBeSelected, selectComponent])
return (
<>
@@ -157,14 +140,18 @@ const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Prop
<div className={`border-0 border-t border-solid border-border py-1 ${index === 0 ? 'border-t-0' : ''}`}>
{group.items.map((item) => {
const onClickEditorItem = () => {
selectItem(item).catch(console.error)
handleMenuSelection(item).catch(console.error)
}
return (
<MenuItem key={item.name} onClick={onClickEditorItem} className={'flex-row-reversed py-2'}>
<MenuItem
key={item.uiFeature.uniqueIdentifier}
onClick={onClickEditorItem}
className={'flex-row-reversed py-2'}
>
<div className="flex flex-grow items-center justify-between">
<div className={'flex items-center'}>
{group.icon && <Icon type={group.icon} className={`mr-2 ${group.iconClassName}`} />}
{item.name}
{item.uiFeature.displayName}
{item.isLabs && (
<Pill className="py-0.5 px-1.5" style="success">
Labs
@@ -194,4 +181,4 @@ const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Prop
)
}
export default ChangeMultipleMenu
export default ChangeEditorMultipleMenu

View File

@@ -3,7 +3,7 @@ import { NotesController } from '@/Controllers/NotesController/NotesController'
import { useRef, useState } from 'react'
import RoundIconButton from '../Button/RoundIconButton'
import Popover from '../Popover/Popover'
import ChangeMultipleMenu from './ChangeMultipleMenu'
import ChangeEditorMultipleMenu from './ChangeEditorMultipleMenu'
type Props = {
application: WebApplication
@@ -27,7 +27,7 @@ const ChangeMultipleButton = ({ application, notesController }: Props) => {
open={isChangeMenuOpen}
className="pt-2 md:pt-0"
>
<ChangeMultipleMenu
<ChangeEditorMultipleMenu
application={application}
notes={notesController.selectedNotes}
setDisableClickOutside={setDisableClickOutside}

View File

@@ -64,7 +64,7 @@ const ClipperView = ({
() => application.features.getFeatureStatus(FeatureIdentifier.Extension) === FeatureStatus.Entitled,
)
const isEntitledRef = useStateRef(isEntitledToExtension)
const hasSubscription = application.hasValidSubscription()
const hasSubscription = application.hasValidFirstPartySubscription()
useEffect(() => {
return application.addEventObserver(async (event) => {
switch (event) {
@@ -74,7 +74,7 @@ const ClipperView = ({
setUser(application.getUser())
setIsEntitled(application.features.getFeatureStatus(FeatureIdentifier.Extension) === FeatureStatus.Entitled)
break
case ApplicationEvent.FeaturesUpdated:
case ApplicationEvent.FeaturesAvailabilityChanged:
setIsEntitled(application.features.getFeatureStatus(FeatureIdentifier.Extension) === FeatureStatus.Entitled)
break
}

View File

@@ -1,27 +1,25 @@
import {
ComponentAction,
FeatureStatus,
SNComponent,
dateToLocalizedString,
ComponentViewerInterface,
ComponentViewerEvent,
ComponentViewerError,
ComponentInterface,
SubscriptionManagerEvent,
} from '@standardnotes/snjs'
import { WebApplication } from '@/Application/WebApplication'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import { observer } from 'mobx-react-lite'
import OfflineRestricted from '@/Components/ComponentView/OfflineRestricted'
import UrlMissing from '@/Components/ComponentView/UrlMissing'
import IsDeprecated from '@/Components/ComponentView/IsDeprecated'
import IsExpired from '@/Components/ComponentView/IsExpired'
import NotEntitledBanner from '@/Components/ComponentView/NotEntitledBanner'
import IssueOnLoading from '@/Components/ComponentView/IssueOnLoading'
import { openSubscriptionDashboard } from '@/Utils/ManageSubscription'
import { useApplication } from '../ApplicationProvider'
interface IProps {
application: WebApplication
interface Props {
componentViewer: ComponentViewerInterface
requestReload?: (viewer: ComponentViewerInterface, force?: boolean) => void
onLoad?: (component: SNComponent) => void
onLoad?: () => void
}
/**
@@ -32,7 +30,9 @@ const MaxLoadThreshold = 4000
const VisibilityChangeKey = 'visibilitychange'
const MSToWaitAfterIframeLoadToAvoidFlicker = 35
const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, componentViewer, requestReload }) => {
const IframeFeatureView: FunctionComponent<Props> = ({ onLoad, componentViewer, requestReload }) => {
const application = useApplication()
const iframeRef = useRef<HTMLIFrameElement | null>(null)
const [loadTimeout, setLoadTimeout] = useState<ReturnType<typeof setTimeout> | undefined>(undefined)
@@ -45,11 +45,7 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
const [isDeprecationMessageDismissed, setIsDeprecationMessageDismissed] = useState(false)
const [didAttemptReload, setDidAttemptReload] = useState(false)
const component: SNComponent = componentViewer.component
const manageSubscription = useCallback(() => {
void openSubscriptionDashboard(application)
}, [application])
const uiFeature = componentViewer.getComponentOrFeatureItem()
const reloadValidityStatus = useCallback(() => {
setFeatureStatus(componentViewer.getFeatureStatus())
@@ -63,13 +59,21 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
}
setError(componentViewer.getError())
setDeprecationMessage(component.deprecationMessage)
}, [componentViewer, component.deprecationMessage, featureStatus, isComponentValid, isLoading])
setDeprecationMessage(uiFeature.deprecationMessage)
}, [componentViewer, uiFeature, featureStatus, isComponentValid, isLoading])
useEffect(() => {
reloadValidityStatus()
}, [reloadValidityStatus])
useEffect(() => {
return application.subscriptions.addEventObserver((event) => {
if (event === SubscriptionManagerEvent.DidFetchSubscription) {
reloadValidityStatus()
}
})
}, [application.subscriptions, reloadValidityStatus])
const dismissDeprecationMessage = () => {
setIsDeprecationMessageDismissed(true)
}
@@ -123,9 +127,9 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
setTimeout(() => {
setIsLoading(false)
setHasIssueLoading(false)
onLoad?.(component)
onLoad?.()
}, MSToWaitAfterIframeLoadToAvoidFlicker)
}, [componentViewer, onLoad, component, loadTimeout])
}, [componentViewer, onLoad, loadTimeout])
useEffect(() => {
const removeFeaturesChangedObserver = componentViewer.addEventObserver((event) => {
@@ -149,7 +153,7 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
application.keyboardService.handleComponentKeyUp(data.keyboardModifier)
break
case ComponentAction.Click:
application.getViewControllerManager().notesController.setContextMenuOpen(false)
application.controllers.notesController.setContextMenuOpen(false)
break
default:
return
@@ -163,8 +167,8 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
useEffect(() => {
const unregisterDesktopObserver = application
.getDesktopService()
?.registerUpdateObserver((updatedComponent: SNComponent) => {
if (updatedComponent.uuid === component.uuid && updatedComponent.active) {
?.registerUpdateObserver((updatedComponent: ComponentInterface) => {
if (updatedComponent.uuid === uiFeature.uniqueIdentifier) {
requestReload?.(componentViewer)
}
})
@@ -172,13 +176,13 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
return () => {
unregisterDesktopObserver?.()
}
}, [application, requestReload, componentViewer, component.uuid])
}, [application, requestReload, componentViewer, uiFeature])
return (
<>
{hasIssueLoading && (
<IssueOnLoading
componentName={component.displayName}
componentName={uiFeature.displayName}
reloadIframe={() => {
reloadValidityStatus(), requestReload?.(componentViewer, true)
}}
@@ -186,19 +190,17 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
)}
{featureStatus !== FeatureStatus.Entitled && (
<IsExpired
expiredDate={dateToLocalizedString(component.valid_until)}
featureStatus={featureStatus}
componentName={component.displayName}
manageSubscription={manageSubscription}
/>
<NotEntitledBanner featureStatus={featureStatus} feature={uiFeature.featureDescription} />
)}
{deprecationMessage && !isDeprecationMessageDismissed && (
<IsDeprecated deprecationMessage={deprecationMessage} dismissDeprecationMessage={dismissDeprecationMessage} />
)}
{error === ComponentViewerError.OfflineRestricted && <OfflineRestricted />}
{error === ComponentViewerError.MissingUrl && <UrlMissing componentName={component.displayName} />}
{component.uuid && isComponentValid && (
{error === ComponentViewerError.MissingUrl && <UrlMissing componentName={uiFeature.displayName} />}
{uiFeature.uniqueIdentifier && isComponentValid && (
<iframe
className="h-full w-full flex-grow bg-transparent"
ref={iframeRef}
@@ -216,4 +218,4 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
)
}
export default observer(ComponentView)
export default observer(IframeFeatureView)

View File

@@ -1,49 +0,0 @@
import { FeatureStatus } from '@standardnotes/snjs'
import { FunctionComponent } from 'react'
import Button from '@/Components/Button/Button'
import IndicatorCircle from '../IndicatorCircle/IndicatorCircle'
type Props = {
expiredDate: string
componentName: string
featureStatus: FeatureStatus
manageSubscription: () => void
}
const statusString = (featureStatus: FeatureStatus, expiredDate: string, componentName: string) => {
switch (featureStatus) {
case FeatureStatus.InCurrentPlanButExpired:
return `Your subscription expired on ${expiredDate}`
case FeatureStatus.NoUserSubscription:
return 'You do not have an active subscription'
case FeatureStatus.NotInCurrentPlan:
return `Please upgrade your plan to access ${componentName}`
default:
return `${componentName} is valid and you should not be seeing this message`
}
}
const IsExpired: FunctionComponent<Props> = ({ expiredDate, featureStatus, componentName, manageSubscription }) => {
return (
<div className={'sn-component'}>
<div className="flex min-h-[1.625rem] w-full select-none items-center justify-between border-b border-border bg-contrast py-2.5 px-2 text-text">
<div className={'left'}>
<div className="flex items-center">
<IndicatorCircle style="danger" />
<div className="ml-2">
<strong>{statusString(featureStatus, expiredDate, componentName)}</strong>
<div className={'sk-p'}>{componentName} is in a read-only state.</div>
</div>
</div>
</div>
<div className={'right'}>
<Button onClick={manageSubscription} primary colorStyle="success" small>
Manage subscription
</Button>
</div>
</div>
</div>
)
}
export default IsExpired

View File

@@ -0,0 +1,63 @@
import { AnyFeatureDescription, FeatureStatus, dateToLocalizedString } from '@standardnotes/snjs'
import { FunctionComponent, useCallback } from 'react'
import Button from '@/Components/Button/Button'
import { WarningCircle } from '../UIElements/WarningCircle'
import { useApplication } from '../ApplicationProvider'
import { openSubscriptionDashboard } from '@/Utils/ManageSubscription'
type Props = {
feature: AnyFeatureDescription
featureStatus: FeatureStatus
}
const statusString = (featureStatus: FeatureStatus, expiredDate: Date | undefined, featureName: string) => {
switch (featureStatus) {
case FeatureStatus.InCurrentPlanButExpired:
if (expiredDate) {
return `Your subscription expired on ${dateToLocalizedString(expiredDate)}`
} else {
return 'Your subscription expired.'
}
case FeatureStatus.NoUserSubscription:
return 'You do not have an active subscription'
case FeatureStatus.NotInCurrentPlan:
return `Please upgrade your plan to access ${featureName}`
default:
return `${featureName} is valid and you should not be seeing this message`
}
}
const NotEntitledBanner: FunctionComponent<Props> = ({ featureStatus, feature }) => {
const application = useApplication()
const expiredDate = application.subscriptions.userSubscriptionExpirationDate
const manageSubscription = useCallback(() => {
void openSubscriptionDashboard(application)
}, [application])
return (
<div className={'sn-component'}>
<div className="flex min-h-[1.625rem] w-full select-none items-center justify-between border-b border-border bg-contrast py-2.5 px-2 text-text">
<div className={'left'}>
<div className="flex items-start">
<div className="mt-1">
<WarningCircle />
</div>
<div className="ml-2">
<strong>{statusString(featureStatus, expiredDate, feature.name)}</strong>
<div className={'sk-p'}>{feature.name} is in a read-only state.</div>
</div>
</div>
</div>
<div className={'right'}>
<Button onClick={manageSubscription} primary colorStyle="success" small>
Manage subscription
</Button>
</div>
</div>
</div>
)
}
export default NotEntitledBanner

View File

@@ -9,6 +9,7 @@ import {
TagMutator,
TagPreferences,
VectorIconNameOrEmoji,
PrefDefaults,
} from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
@@ -16,7 +17,6 @@ import Icon from '@/Components/Icon/Icon'
import Menu from '@/Components/Menu/Menu'
import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator'
import { DisplayOptionsMenuProps } from './DisplayOptionsMenuProps'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import NewNotePreferences from './NewNotePreferences'
import { PreferenceMode } from './PreferenceMode'
import { classNames } from '@standardnotes/utils'
@@ -93,7 +93,7 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
: selectedTag.preferences
const [currentMode, setCurrentMode] = useState<PreferenceMode>(selectedTagPreferences ? 'tag' : 'global')
const [preferences, setPreferences] = useState<TagPreferences>({})
const hasSubscription = application.features.hasFirstPartySubscription()
const hasSubscription = application.controllers.subscriptionController.hasFirstPartyOnlineOrOfflineSubscription
const controlsDisabled = currentMode === 'tag' && !hasSubscription
const isDailyEntry = selectedTagPreferences?.entryMode === 'daily'

View File

@@ -7,10 +7,11 @@ import {
isSmartView,
isSystemView,
SystemViewId,
PrefDefaults,
FeatureStatus,
} from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { ChangeEventHandler, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import Dropdown from '@/Components/Dropdown/Dropdown'
import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
import { WebApplication } from '@/Application/WebApplication'
@@ -27,6 +28,8 @@ import { EditorOption, getDropdownItemsForAllEditors } from '@/Utils/DropdownIte
import { classNames } from '@standardnotes/utils'
import { NoteTitleFormatOptions } from './NoteTitleFormatOptions'
import { usePremiumModal } from '@/Hooks/usePremiumModal'
const PrefChangeDebounceTimeInMs = 25
const HelpPageUrl = 'https://day.js.org/docs/en/display/format#list-of-all-available-formats'
@@ -46,6 +49,8 @@ const NewNotePreferences: FunctionComponent<Props> = ({
changePreferencesCallback,
disabled,
}: Props) => {
const premiumModal = usePremiumModal()
const isSystemTag = isSmartView(selectedTag) && isSystemView(selectedTag)
const selectedTagPreferences = isSystemTag
? application.getPreference(PrefKey.SystemViewPreferences)?.[selectedTag.uuid as SystemViewId]
@@ -114,8 +119,15 @@ const NewNotePreferences: FunctionComponent<Props> = ({
setEditorItems(getDropdownItemsForAllEditors(application))
}, [application])
const setDefaultEditor = useCallback(
const selectEditorForNewNoteDefault = useCallback(
(value: EditorOption['value']) => {
if (application.features.getFeatureStatus(value) !== FeatureStatus.Entitled) {
const editorItem = editorItems.find((item) => item.value === value)
if (editorItem) {
premiumModal.activate(editorItem.label)
}
return
}
setDefaultEditorIdentifier(value as FeatureIdentifier)
if (mode === 'global') {
@@ -124,7 +136,7 @@ const NewNotePreferences: FunctionComponent<Props> = ({
void changePreferencesCallback({ editorIdentifier: value })
}
},
[application, changePreferencesCallback, mode],
[application, mode, editorItems, premiumModal, changePreferencesCallback],
)
const debounceTimeoutRef = useRef<number>()
@@ -158,7 +170,7 @@ const NewNotePreferences: FunctionComponent<Props> = ({
label="Select the default note type"
items={editorItems}
value={defaultEditorIdentifier}
onChange={(value) => setDefaultEditor(value as EditorOption['value'])}
onChange={(value) => selectEditorForNewNoteDefault(value as EditorOption['value'])}
/>
</div>
</div>

View File

@@ -16,6 +16,7 @@ import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintFor
import ListItemVaultInfo from './ListItemVaultInfo'
import { NoteDragDataFormat } from '../Tags/DragNDrop'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import useItem from '@/Hooks/useItem'
const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
application,
@@ -33,8 +34,11 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
isNextItemTiled,
}) => {
const listItemRef = useRef<HTMLDivElement>(null)
const liveItem = useItem<SNNote>(item.uuid)
const editor = liveItem ? application.componentManager.editorForNote(liveItem) : undefined
const noteType = liveItem?.noteType ? liveItem.noteType : editor ? editor.noteType : undefined
const noteType = item.noteType || application.componentManager.editorForNote(item)?.package_info.note_type
const [icon, tint] = getIconAndTintForNoteType(noteType)
const hasFiles = application.items.itemsReferencingItem(item).filter(isFile).length > 0

View File

@@ -14,6 +14,7 @@ import {
TagMutator,
isSystemView,
isSmartView,
isNote,
} from '@standardnotes/snjs'
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import { FileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
@@ -182,10 +183,9 @@ const ItemNameCell = ({ item, hideIcon }: { item: DecryptedItemInterface; hideIc
const [backupInfo, setBackupInfo] = useState<FileBackupRecord | undefined>(undefined)
const isItemFile = item instanceof FileItem
const noteType =
item instanceof SNNote
? item.noteType || application.componentManager.editorForNote(item)?.package_info.note_type
: undefined
const editor = isNote(item) ? application.componentManager.editorForNote(item) : undefined
const noteType = isNote(item) ? item.noteType : editor ? editor.noteType : undefined
const [noteIcon, noteIconTint] = getIconAndTintForNoteType(noteType)
useEffect(() => {

View File

@@ -1,5 +1,5 @@
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { classNames, EditorLineWidth, PrefKey, SNNote } from '@standardnotes/snjs'
import { classNames, EditorLineWidth, PrefKey, SNNote, PrefDefaults } from '@standardnotes/snjs'
import { useCallback, useEffect, useMemo, useState } from 'react'
import Button from '../Button/Button'
import Modal, { ModalAction } from '../Modal/Modal'
@@ -9,7 +9,6 @@ import { EditorMargins, EditorMaxWidths } from './EditorWidths'
import { useApplication } from '../ApplicationProvider'
import ModalOverlay from '../Modal/ModalOverlay'
import { CHANGE_EDITOR_WIDTH_COMMAND } from '@standardnotes/ui-services'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { observer } from 'mobx-react-lite'
import Switch from '../Switch/Switch'
@@ -155,7 +154,7 @@ const EditorWidthSelectionModal = ({
const EditorWidthSelectionModalWrapper = () => {
const application = useApplication()
const { notesController } = application.getViewControllerManager()
const { notesController } = application.controllers
const [isOpen, setIsOpen] = useState(false)
const [isGlobal, setIsGlobal] = useState(false)

View File

@@ -141,7 +141,7 @@ const FilePreview = ({ file, application, isEmbeddedInSuper = false, imageZoomLe
) : (
<FilePreviewError
file={file}
filesController={application.getViewControllerManager().filesController}
filesController={application.controllers.filesController}
tryAgainCallback={() => {
setDownloadedBytes(undefined)
}}

View File

@@ -1,6 +1,6 @@
import { WebApplication } from '@/Application/WebApplication'
import { QuickSettingsController } from '@/Controllers/QuickSettingsController'
import { FeatureIdentifier, SNTheme } from '@standardnotes/snjs'
import { ComponentOrNativeFeature, GetDarkThemeFeature } from '@standardnotes/snjs'
import { TOGGLE_DARK_MODE_COMMAND } from '@standardnotes/ui-services'
import { classNames } from '@standardnotes/utils'
import { useEffect, useRef } from 'react'
@@ -25,12 +25,7 @@ const QuickSettingsButton = ({ application, isOpen, toggleMenu, quickSettingsMen
return commandService.addCommandHandler({
command: TOGGLE_DARK_MODE_COMMAND,
onKeyDown: () => {
const darkTheme = application.items
.getDisplayableComponents()
.find((theme) => theme.package_info.identifier === FeatureIdentifier.DarkTheme) as SNTheme | undefined
if (darkTheme) {
void application.componentManager.toggleTheme(darkTheme.uuid)
}
void application.componentManager.toggleTheme(new ComponentOrNativeFeature(GetDarkThemeFeature()))
},
})
}, [application, commandService])
@@ -57,7 +52,7 @@ const QuickSettingsButton = ({ application, isOpen, toggleMenu, quickSettingsMen
align="start"
className="py-2"
>
<QuickSettingsMenu quickSettingsMenuController={quickSettingsMenuController} application={application} />
<QuickSettingsMenu quickSettingsMenuController={quickSettingsMenuController} />
</Popover>
</>
)

View File

@@ -13,7 +13,7 @@ type Props = {
const UpgradeNow = ({ application, featuresController, subscriptionContoller }: Props) => {
const shouldShowCTA = !featuresController.hasFolders
const hasAccount = subscriptionContoller.hasAccount
const hasAccessToFeatures = subscriptionContoller.hasFirstPartySubscription
const hasAccessToFeatures = subscriptionContoller.hasFirstPartyOnlineOrOfflineSubscription
const onClick = useCallback(() => {
if (hasAccount && application.isNativeIOS()) {

View File

@@ -14,33 +14,44 @@ import Icon from '../Icon/Icon'
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
import RadioIndicator from '../Radio/RadioIndicator'
import MenuListItem from './MenuListItem'
import Popover from '../Popover/Popover'
const Tooltip = ({ text }: { text: string }) => {
const [mobileVisible, setMobileVisible] = useState(false)
const [visible, setVisible] = useState(false)
const onClickMobile: MouseEventHandler<HTMLDivElement> = useCallback(
(event) => {
event.preventDefault()
event.stopPropagation()
setMobileVisible(!mobileVisible)
setVisible(!visible)
},
[mobileVisible],
[visible],
)
const [anchorElement, setAnchorElement] = useState<HTMLDivElement | null>(null)
return (
<div className="relative">
<div className={classNames('peer flex h-5 w-5 items-center justify-center rounded-full')} onClick={onClickMobile}>
<Icon type={'help'} className="text-neutral" size="large" />
<span className="sr-only">Note sync status</span>
</div>
<div
ref={setAnchorElement}
className={classNames('peer z-0 flex h-5 w-5 items-center justify-center rounded-full')}
onClick={onClickMobile}
onMouseEnter={() => setVisible(true)}
onMouseLeave={() => setVisible(false)}
>
<Icon type={'notes'} className="text-border" size="large" />
</div>
<Popover
open={visible}
title="Info"
anchorElement={anchorElement}
disableMobileFullscreenTakeover
className={classNames(
'hidden',
'absolute top-full right-0 w-60 translate-x-2 translate-y-1 select-none rounded border border-border shadow-main',
'bg-default py-1.5 px-3 text-left peer-hover:block peer-focus:block',
'w-60 translate-x-2 translate-y-1 select-none rounded border border-border shadow-main',
'z-modal bg-default py-1.5 px-3 text-left',
)}
>
{text}
</div>
</Popover>
</div>
)
}

View File

@@ -58,7 +58,7 @@ describe('note view controller', () => {
identifier: FeatureIdentifier.MarkdownProEditor,
} as SNComponent)
componentManager.componentWithIdentifier = jest.fn().mockReturnValue({
componentManager.componentOrNativeFeatureForIdentifier = jest.fn().mockReturnValue({
identifier: FeatureIdentifier.MarkdownProEditor,
} as SNComponent)

View File

@@ -105,7 +105,7 @@ const NoteConflictResolutionModal = ({
mutator.conflictOf = undefined
})
setIsPerformingAction(false)
void application.getViewControllerManager().selectionController.selectItem(selectedNotes[0].uuid, true)
void application.controllers.selectionController.selectItem(selectedNotes[0].uuid, true)
void application.sync.sync()
close()
}

View File

@@ -1,8 +1,7 @@
import { ElementIds } from '@/Constants/ElementIDs'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { classNames } from '@standardnotes/utils'
import { ReactNode, useCallback, useState } from 'react'
import { IconType, PrefKey } from '@standardnotes/snjs'
import { IconType, PrefKey, PrefDefaults } from '@standardnotes/snjs'
import Icon from '../Icon/Icon'
import { useApplication } from '../ApplicationProvider'

View File

@@ -41,8 +41,9 @@ describe('NoteView', () => {
notesController: notesController,
} as jest.Mocked<ViewControllerManager>
application = {} as jest.Mocked<WebApplication>
application.getViewControllerManager = jest.fn().mockReturnValue(viewControllerManager)
application = {
controllers: viewControllerManager,
} as jest.Mocked<WebApplication>
application.hasProtectionSources = jest.fn().mockReturnValue(true)
application.authorizeNoteAccess = jest.fn()
application.addWebEventObserver = jest.fn()

View File

@@ -1,11 +1,10 @@
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
import ChangeEditorButton from '@/Components/ChangeEditor/ChangeEditorButton'
import ComponentView from '@/Components/ComponentView/ComponentView'
import IframeFeatureView from '@/Components/ComponentView/IframeFeatureView'
import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel'
import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton'
import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedItemOverlay'
import { ElementIds } from '@/Constants/ElementIDs'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings'
import { log, LoggingDomain } from '@/Logging'
import { debounce, isDesktopApplication, isMobileScreen } from '@/Utils'
@@ -13,16 +12,20 @@ import { classNames, pluralize } from '@standardnotes/utils'
import {
ApplicationEvent,
ComponentArea,
ComponentInterface,
ComponentOrNativeFeature,
ComponentViewerInterface,
ContentType,
EditorLineWidth,
IframeComponentFeatureDescription,
isIframeUIFeature,
isPayloadSourceInternalChange,
isPayloadSourceRetrieved,
NoteType,
PayloadEmitSource,
PrefDefaults,
PrefKey,
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
SNComponent,
SNNote,
} from '@standardnotes/snjs'
import { confirmDialog, DELETE_NOTE_KEYBOARD_COMMAND, KeyboardKey } from '@standardnotes/ui-services'
@@ -53,12 +56,12 @@ import Icon from '../Icon/Icon'
const MinimumStatusDuration = 400
function sortAlphabetically(array: SNComponent[]): SNComponent[] {
function sortAlphabetically(array: ComponentInterface[]): ComponentInterface[] {
return array.sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1))
}
type State = {
availableStackComponents: SNComponent[]
availableStackComponents: ComponentInterface[]
editorComponentViewer?: ComponentViewerInterface
editorComponentViewerDidAlreadyReload?: boolean
editorStateDidLoad: boolean
@@ -443,14 +446,21 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
)
this.removeNoteStreamObserver = this.application.streamItems<SNNote>(ContentType.TYPES.Note, async () => {
if (!this.note) {
return
}
this.setState({
conflictedNotes: this.application.items.conflictsOf(this.note.uuid) as SNNote[],
})
})
}
private createComponentViewer(component: SNComponent) {
const viewer = this.application.componentManager.createComponentViewer(component, this.note.uuid)
private createComponentViewer(component: ComponentOrNativeFeature<IframeComponentFeatureDescription>) {
if (!component) {
throw Error('Cannot create component viewer for undefined component')
}
const viewer = this.application.componentManager.createComponentViewer(component, { uuid: this.note.uuid })
return viewer
}
@@ -462,7 +472,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
return
}
const component = viewer.component
const component = viewer.getComponentOrFeatureItem()
this.application.componentManager.destroyComponentViewer(viewer)
this.setState(
{
@@ -496,35 +506,36 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
}
}
async reloadEditorComponent() {
async reloadEditorComponent(): Promise<void> {
log(LoggingDomain.NoteView, 'Reload editor component')
if (this.state.showProtectedWarning) {
this.destroyCurrentEditorComponent()
return
}
const newEditor = this.application.componentManager.editorForNote(this.note)
const newUIFeature = this.application.componentManager.editorForNote(this.note)
/** Editors cannot interact with template notes so the note must be inserted */
if (newEditor && this.controller.isTemplateNote) {
/** Component editors cannot interact with template notes so the note must be inserted */
if (isIframeUIFeature(newUIFeature) && this.controller.isTemplateNote) {
await this.controller.insertTemplatedNote()
}
const currentComponentViewer = this.state.editorComponentViewer
if (currentComponentViewer?.componentUuid !== newEditor?.uuid) {
if (currentComponentViewer) {
if (currentComponentViewer) {
const needsDestroy = currentComponentViewer.componentUniqueIdentifier !== newUIFeature.uniqueIdentifier
if (needsDestroy) {
this.destroyCurrentEditorComponent()
}
}
if (newEditor) {
this.setState({
editorComponentViewer: this.createComponentViewer(newEditor),
editorStateDidLoad: true,
})
}
reloadFont(this.state.monospaceFont)
if (isIframeUIFeature(newUIFeature)) {
this.setState({
editorComponentViewer: this.createComponentViewer(newUIFeature),
editorStateDidLoad: true,
})
} else {
reloadFont(this.state.monospaceFont)
this.setState({
editorStateDidLoad: true,
})
@@ -731,8 +742,8 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
log(LoggingDomain.NoteView, 'Reload stack components')
const stackComponents = sortAlphabetically(
this.application.componentManager
.componentsForArea(ComponentArea.EditorStack)
.filter((component) => component.active),
.thirdPartyComponentsForArea(ComponentArea.EditorStack)
.filter((component) => this.application.componentManager.isComponentActive(component)),
)
const enabledComponents = stackComponents.filter((component) => {
return component.isExplicitlyEnabledForItem(this.note.uuid)
@@ -740,21 +751,28 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
const needsNewViewer = enabledComponents.filter((component) => {
const hasExistingViewer = this.state.stackComponentViewers.find(
(viewer) => viewer.componentUuid === component.uuid,
(viewer) => viewer.componentUniqueIdentifier === component.uuid,
)
return !hasExistingViewer
})
const needsDestroyViewer = this.state.stackComponentViewers.filter((viewer) => {
const viewerComponentExistsInEnabledComponents = enabledComponents.find((component) => {
return component.uuid === viewer.componentUuid
return component.uuid === viewer.componentUniqueIdentifier
})
return !viewerComponentExistsInEnabledComponents
})
const newViewers: ComponentViewerInterface[] = []
for (const component of needsNewViewer) {
newViewers.push(this.application.componentManager.createComponentViewer(component, this.note.uuid))
newViewers.push(
this.application.componentManager.createComponentViewer(
new ComponentOrNativeFeature<IframeComponentFeatureDescription>(component),
{
uuid: this.note.uuid,
},
),
)
}
for (const viewer of needsDestroyViewer) {
@@ -766,11 +784,11 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
})
}
stackComponentExpanded = (component: SNComponent): boolean => {
return !!this.state.stackComponentViewers.find((viewer) => viewer.componentUuid === component.uuid)
stackComponentExpanded = (component: ComponentInterface): boolean => {
return !!this.state.stackComponentViewers.find((viewer) => viewer.componentUniqueIdentifier === component.uuid)
}
toggleStackComponent = async (component: SNComponent) => {
toggleStackComponent = async (component: ComponentInterface) => {
if (!component.isExplicitlyEnabledForItem(this.note.uuid)) {
await this.application.mutator.runTransactionalMutation(
transactionForAssociateComponentWithCurrentNote(component, this.note),
@@ -962,12 +980,11 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
{editorMode === 'component' && this.state.editorComponentViewer && (
<div className="component-view relative flex-grow">
{this.state.paneGestureEnabled && <div className="absolute top-0 left-0 h-full w-[20px] md:hidden" />}
<ComponentView
<IframeFeatureView
key={this.state.editorComponentViewer.identifier}
componentViewer={this.state.editorComponentViewer}
onLoad={this.onEditorComponentLoad}
requestReload={this.editorComponentViewerRequestsReload}
application={this.application}
/>
</div>
)}
@@ -1006,6 +1023,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
>
<div className="flex h-full">
{this.state.availableStackComponents.map((component) => {
const active = this.application.componentManager.isComponentActive(component)
return (
<div
key={component.uuid}
@@ -1015,7 +1033,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
className="flex flex-grow cursor-pointer items-center justify-center [&:not(:first-child)]:ml-3"
>
<div className="flex h-full items-center [&:not(:first-child)]:ml-2">
{this.stackComponentExpanded(component) && component.active && <IndicatorCircle style="info" />}
{this.stackComponentExpanded(component) && active && <IndicatorCircle style="info" />}
{!this.stackComponentExpanded(component) && <IndicatorCircle style="neutral" />}
</div>
<div className="flex h-full items-center [&:not(:first-child)]:ml-2">
@@ -1032,7 +1050,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
{this.state.stackComponentViewers.map((viewer) => {
return (
<div className="component-view component-stack-item" key={viewer.identifier}>
<ComponentView key={viewer.identifier} componentViewer={viewer} application={this.application} />
<IframeFeatureView key={viewer.identifier} componentViewer={viewer} />
</div>
)
})}

View File

@@ -1,7 +1,6 @@
import { WebApplication } from '@/Application/WebApplication'
import { usePrevious } from '@/Components/ContentListView/Calendar/usePrevious'
import { ElementIds } from '@/Constants/ElementIDs'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { log, LoggingDomain } from '@/Logging'
import { Disposer } from '@/Types/Disposer'
import { EditorEventSource } from '@/Types/EditorEventSource'
@@ -14,6 +13,7 @@ import {
isPayloadSourceRetrieved,
PrefKey,
WebAppEvent,
PrefDefaults,
} from '@standardnotes/snjs'
import { TAB_COMMAND } from '@standardnotes/ui-services'
import { ChangeEventHandler, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'

View File

@@ -1,8 +1,8 @@
import { ContentType, NoteContent, NoteType, SNNote, classNames } from '@standardnotes/snjs'
import { ContentType, NoteContent, NoteType, SNNote, classNames, isIframeUIFeature } from '@standardnotes/snjs'
import { UIEventHandler, useEffect, useMemo, useRef } from 'react'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { useApplication } from '../ApplicationProvider'
import ComponentView from '../ComponentView/ComponentView'
import IframeFeatureView from '../ComponentView/IframeFeatureView'
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
import { BlocksEditor } from '../SuperEditor/BlocksEditor'
import { BlocksEditorComposer } from '../SuperEditor/BlocksEditorComposer'
@@ -30,18 +30,17 @@ export const ReadonlyNoteContent = ({
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
const componentViewer = useMemo(() => {
const editorForCurrentNote = note ? application.componentManager.editorForNote(note) : undefined
if (!editorForCurrentNote) {
const editorForCurrentNote = application.componentManager.editorForNote(note)
if (!isIframeUIFeature(editorForCurrentNote)) {
return undefined
}
const templateNoteForRevision = application.items.createTemplateItem(ContentType.TYPES.Note, note.content) as SNNote
const componentViewer = application.componentManager.createComponentViewer(editorForCurrentNote)
componentViewer.setReadonly(true)
componentViewer.lockReadonly = true
componentViewer.overrideContextItem = templateNoteForRevision
const componentViewer = application.componentManager.createComponentViewer(editorForCurrentNote, {
readonlyItem: templateNoteForRevision,
})
return componentViewer
}, [application.componentManager, application.items, note])
@@ -91,7 +90,7 @@ export const ReadonlyNoteContent = ({
)}
{componentViewer ? (
<div className="component-view">
<ComponentView key={componentViewer.identifier} componentViewer={componentViewer} application={application} />
<IframeFeatureView key={componentViewer.identifier} componentViewer={componentViewer} />
</div>
) : content.noteType === NoteType.Super ? (
<ErrorBoundary>

View File

@@ -1,6 +1,6 @@
import { SNComponent, SNNote, ComponentMutator, TransactionalMutation, ItemMutator } from '@standardnotes/snjs'
import { SNNote, ComponentMutator, TransactionalMutation, ItemMutator, ComponentInterface } from '@standardnotes/snjs'
export const transactionForAssociateComponentWithCurrentNote = (component: SNComponent, note: SNNote) => {
export const transactionForAssociateComponentWithCurrentNote = (component: ComponentInterface, note: SNNote) => {
const transaction: TransactionalMutation = {
itemUuid: component.uuid,
mutate: (m: ItemMutator) => {
@@ -12,7 +12,7 @@ export const transactionForAssociateComponentWithCurrentNote = (component: SNCom
return transaction
}
export const transactionForDisassociateComponentWithCurrentNote = (component: SNComponent, note: SNNote) => {
export const transactionForDisassociateComponentWithCurrentNote = (component: ComponentInterface, note: SNNote) => {
const transaction: TransactionalMutation = {
itemUuid: component.uuid,
mutate: (m: ItemMutator) => {

View File

@@ -1,10 +1,11 @@
import { NoteType, SNComponent } from '@standardnotes/snjs'
import {
ComponentOrNativeFeature,
EditorFeatureDescription,
IframeComponentFeatureDescription,
} from '@standardnotes/snjs'
export type EditorMenuItem = {
name: string
component?: SNComponent
uiFeature: ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>
isEntitled: boolean
noteType: NoteType
isLabs?: boolean
description?: string
}

View File

@@ -1,10 +1,11 @@
import { useMemo, FunctionComponent } from 'react'
import { SNApplication, SNNote, classNames } from '@standardnotes/snjs'
import { SNNote, classNames } from '@standardnotes/snjs'
import { formatDateForContextMenu } from '@/Utils/DateUtils'
import { calculateReadTime } from './Utils/calculateReadTime'
import { countNoteAttributes } from './Utils/countNoteAttributes'
import { WebApplicationInterface } from '@standardnotes/ui-services'
export const useNoteAttributes = (application: SNApplication, note: SNNote) => {
export const useNoteAttributes = (application: WebApplicationInterface, note: SNNote) => {
const { words, characters, paragraphs } = useMemo(() => countNoteAttributes(note.text), [note.text])
const readTime = useMemo(() => (typeof words === 'number' ? calculateReadTime(words) : 'N/A'), [words])
@@ -14,7 +15,7 @@ export const useNoteAttributes = (application: SNApplication, note: SNNote) => {
const dateCreated = useMemo(() => formatDateForContextMenu(note.created_at), [note.created_at])
const editor = application.componentManager.editorForNote(note)
const format = editor?.package_info?.file_type || 'txt'
const format = editor.fileType
return {
words,
@@ -28,7 +29,7 @@ export const useNoteAttributes = (application: SNApplication, note: SNNote) => {
}
export const NoteAttributes: FunctionComponent<{
application: SNApplication
application: WebApplicationInterface
note: SNNote
className?: string
}> = ({ application, note, className }) => {

View File

@@ -409,7 +409,9 @@ const NotesOptions = ({
<HorizontalSeparator classes="my-2" />
<SpellcheckOptions editorForNote={editorForNote} notesController={notesController} note={notes[0]} />
{editorForNote && (
<SpellcheckOptions editorForNote={editorForNote} notesController={notesController} note={notes[0]} />
)}
<HorizontalSeparator classes="my-2" />

View File

@@ -1,16 +1,21 @@
import Icon from '@/Components/Icon/Icon'
import { FunctionComponent } from 'react'
import { SNComponent, SNNote } from '@standardnotes/snjs'
import {
ComponentOrNativeFeature,
EditorFeatureDescription,
IframeComponentFeatureDescription,
SNNote,
} from '@standardnotes/snjs'
import { NotesController } from '@/Controllers/NotesController/NotesController'
import { iconClass } from './ClassNames'
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
export const SpellcheckOptions: FunctionComponent<{
editorForNote: SNComponent | undefined
editorForNote: ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>
notesController: NotesController
note: SNNote
}> = ({ editorForNote, notesController, note }) => {
const spellcheckControllable = Boolean(!editorForNote || editorForNote.package_info.spellcheckControl)
const spellcheckControllable = editorForNote.featureDescription.spellcheckControl
const noteSpellcheck = !spellcheckControllable
? true
: note

View File

@@ -38,7 +38,7 @@ const PanesSystemComponent = () => {
const [panesPendingEntrance, setPanesPendingEntrance] = useState<AppPaneId[]>([])
const [panesPendingExit, setPanesPendingExit] = useState<AppPaneId[]>([])
const viewControllerManager = application.getViewControllerManager()
const viewControllerManager = application.controllers
const [navigationPanelWidth, setNavigationPanelWidth] = useState<number>(
application.getPreference(PrefKey.TagsPanelWidth, PLACEHOLDER_NAVIGATION_PANEL_WIDTH),

View File

@@ -2,8 +2,7 @@ import { useStateRef } from '@/Hooks/useStateRef'
import { useEffect, useRef, useState } from 'react'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { useApplication } from '../ApplicationProvider'
import { ApplicationEvent, PrefKey } from '@standardnotes/snjs'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { ApplicationEvent, PrefKey, PrefDefaults } from '@standardnotes/snjs'
function getScrollParent(node: HTMLElement | null): HTMLElement | null {
if (!node) {

View File

@@ -1,4 +1,4 @@
import { SNComponent } from '@standardnotes/snjs'
import { ComponentInterface } from '@standardnotes/snjs'
import { useCallback } from 'react'
import Button from '@/Components/Button/Button'
import ModalDialogButtons from '../Modal/ModalDialogButtons'
@@ -7,7 +7,7 @@ import Modal from '../Modal/Modal'
type Props = {
callback: (approved: boolean) => void
dismiss: () => void
component: SNComponent
component: ComponentInterface
permissionsString: string
}

View File

@@ -1,11 +1,11 @@
import { WebApplication } from '@/Application/WebApplication'
import { ApplicationEvent, PermissionDialog } from '@standardnotes/snjs'
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
import ModalOverlay from '../Modal/ModalOverlay'
import PermissionsModal from './PermissionsModal'
import { WebApplicationInterface } from '@standardnotes/ui-services'
type Props = {
application: WebApplication
application: WebApplicationInterface
}
const PermissionsModalWrapper: FunctionComponent<Props> = ({ application }) => {

View File

@@ -31,7 +31,7 @@ const AccountPreferences = ({ application, viewControllerManager }: Props) => {
<Sync application={application} />
</>
)}
<Subscription application={application} viewControllerManager={viewControllerManager} />
<Subscription />
<SubscriptionSharing application={application} viewControllerManager={viewControllerManager} />
{application.hasAccount() && viewControllerManager.featuresController.entitledToFiles && (
<FilesSection application={application} />

View File

@@ -1,23 +1,16 @@
import { Title } from '@/Components/Preferences/PreferencesComponents/Content'
import { WebApplication } from '@/Application/WebApplication'
import SubscriptionInformation from './SubscriptionInformation'
import NoSubscription from './NoSubscription'
import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'react'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
import { useApplication } from '@/Components/ApplicationProvider'
type Props = {
application: WebApplication
viewControllerManager: ViewControllerManager
}
const Subscription: FunctionComponent = () => {
const application = useApplication()
const Subscription: FunctionComponent<Props> = ({ application, viewControllerManager }: Props) => {
const subscriptionState = viewControllerManager.subscriptionController
const { onlineSubscription } = subscriptionState
const now = new Date().getTime()
const onlineSubscription = application.controllers.subscriptionController.onlineSubscription
return (
<PreferencesGroup>
@@ -25,11 +18,7 @@ const Subscription: FunctionComponent<Props> = ({ application, viewControllerMan
<div className="flex flex-row items-center">
<div className="flex flex-grow flex-col">
<Title>Subscription</Title>
{onlineSubscription && onlineSubscription.endsAt > now ? (
<SubscriptionInformation subscriptionState={subscriptionState} application={application} />
) : (
<NoSubscription application={application} />
)}
{onlineSubscription ? <SubscriptionInformation /> : <NoSubscription application={application} />}
</div>
</div>
</PreferencesSegment>

View File

@@ -1,23 +1,19 @@
import { observer } from 'mobx-react-lite'
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
import Button from '@/Components/Button/Button'
import { WebApplication } from '@/Application/WebApplication'
import { openSubscriptionDashboard } from '@/Utils/ManageSubscription'
import StatusText from './StatusText'
import SubscriptionStatusText from './SubscriptionStatusText'
import { useApplication } from '@/Components/ApplicationProvider'
type Props = {
subscriptionState: SubscriptionController
application: WebApplication
}
const SubscriptionInformation = () => {
const application = useApplication()
const SubscriptionInformation = ({ subscriptionState, application }: Props) => {
const manageSubscription = async () => {
void openSubscriptionDashboard(application)
}
return (
<>
<StatusText subscriptionState={subscriptionState} />
<SubscriptionStatusText />
<Button className="mt-3 mr-3 min-w-20" label="Manage subscription" onClick={manageSubscription} />
</>
)

View File

@@ -1,16 +1,17 @@
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
import { observer } from 'mobx-react-lite'
import { Text } from '@/Components/Preferences/PreferencesComponents/Content'
import { useApplication } from '@/Components/ApplicationProvider'
type Props = { subscriptionState: SubscriptionController }
const SubscriptionStatusText = () => {
const application = useApplication()
const StatusText = ({ subscriptionState }: Props) => {
const {
userSubscriptionName,
userSubscriptionExpirationDate,
isUserSubscriptionExpired,
isUserSubscriptionCanceled,
} = subscriptionState
} = application.subscriptions
const expirationDateString = userSubscriptionExpirationDate?.toLocaleString()
if (isUserSubscriptionCanceled) {
@@ -58,4 +59,4 @@ const StatusText = ({ subscriptionState }: Props) => {
)
}
export default observer(StatusText)
export default observer(SubscriptionStatusText)

View File

@@ -4,16 +4,7 @@ import { usePremiumModal } from '@/Hooks/usePremiumModal'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import Switch from '@/Components/Switch/Switch'
import { WebApplication } from '@/Application/WebApplication'
import {
ContentType,
FeatureIdentifier,
PrefKey,
GetFeatures,
SNTheme,
FindNativeFeature,
FeatureStatus,
naturalSort,
} from '@standardnotes/snjs'
import { FeatureIdentifier, PrefKey, FeatureStatus, naturalSort, PrefDefaults } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useEffect, useState } from 'react'
import { Subtitle, Title, Text } from '@/Components/Preferences/PreferencesComponents/Content'
@@ -21,8 +12,8 @@ import PreferencesPane from '../PreferencesComponents/PreferencesPane'
import PreferencesGroup from '../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../PreferencesComponents/PreferencesSegment'
import { PremiumFeatureIconName } from '@/Components/Icon/PremiumFeatureIcon'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import EditorAppearance from './Appearance/EditorAppearance'
import { GetAllThemesUseCase } from '@standardnotes/ui-services'
type Props = {
application: WebApplication
@@ -43,36 +34,39 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
)
useEffect(() => {
const themesAsItems: DropdownItem[] = application.items
.getDisplayableComponents()
.filter((component) => component.isTheme() && !(component as SNTheme).isLayerable())
.filter((theme) => !FindNativeFeature(theme.identifier))
.map((theme) => {
return {
label: theme.name,
value: theme.identifier as string,
}
})
const usecase = new GetAllThemesUseCase(application.items)
const { thirdParty, native } = usecase.execute({ excludeLayerable: true })
GetFeatures()
.filter((feature) => feature.content_type === ContentType.TYPES.Theme && !feature.layerable)
.forEach((theme) => {
themesAsItems.push({
label: theme.name as string,
value: theme.identifier,
icon:
application.features.getFeatureStatus(theme.identifier) !== FeatureStatus.Entitled
? PremiumFeatureIconName
: undefined,
})
})
const dropdownItems: DropdownItem[] = []
themesAsItems.unshift({
dropdownItems.push({
label: 'Default',
value: 'Default',
})
setThemeItems(naturalSort(themesAsItems, 'label'))
dropdownItems.push(
...native.map((theme) => {
return {
label: theme.displayName as string,
value: theme.featureIdentifier,
icon:
application.features.getFeatureStatus(theme.featureIdentifier) !== FeatureStatus.Entitled
? PremiumFeatureIconName
: undefined,
}
}),
)
dropdownItems.push(
...thirdParty.map((theme) => {
return {
label: theme.displayName,
value: theme.featureIdentifier,
}
}),
)
setThemeItems(naturalSort(dropdownItems, 'label'))
}, [application])
const toggleUseDeviceSettings = () => {

View File

@@ -3,8 +3,14 @@ import Dropdown from '@/Components/Dropdown/Dropdown'
import Icon from '@/Components/Icon/Icon'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import Switch from '@/Components/Switch/Switch'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { ApplicationEvent, EditorFontSize, EditorLineHeight, EditorLineWidth, PrefKey } from '@standardnotes/snjs'
import {
ApplicationEvent,
EditorFontSize,
EditorLineHeight,
EditorLineWidth,
PrefKey,
PrefDefaults,
} from '@standardnotes/snjs'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Subtitle, Title, Text } from '../../PreferencesComponents/Content'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
@@ -78,7 +84,7 @@ const EditorDefaults = ({ application }: Props) => {
return (
<PreferencesGroup>
<PreferencesSegment>
<Title>Editor appearance</Title>
<Title>Editor</Title>
<div className="mt-2">
<div className="flex justify-between gap-2 md:items-center">
<div className="flex flex-col">

View File

@@ -1,5 +1,5 @@
import { FunctionComponent, useState } from 'react'
import { ComponentMutator, SNComponent } from '@standardnotes/snjs'
import { ComponentInterface, ComponentMutator, SNComponent } from '@standardnotes/snjs'
import { SubtitleLight } from '@/Components/Preferences/PreferencesComponents/Content'
import Switch from '@/Components/Switch/Switch'
import Button from '@/Components/Button/Button'
@@ -39,7 +39,7 @@ const PackageEntry: FunctionComponent<PackageEntryProps> = ({ application, exten
mutator.offlineOnly = newOfflineOnly
})
.then((item) => {
const component = item as SNComponent
const component = item as ComponentInterface
setOfflineOnly(component.offlineOnly)
})
.catch((e) => {
@@ -54,7 +54,7 @@ const PackageEntry: FunctionComponent<PackageEntryProps> = ({ application, exten
mutator.name = newName
})
.then((item) => {
const component = item as SNComponent
const component = item as ComponentInterface
setExtensionName(component.name)
})
.catch(console.error)

View File

@@ -1,4 +1,4 @@
import { ButtonType, ContentType, SNComponent } from '@standardnotes/snjs'
import { ButtonType, ContentType } from '@standardnotes/snjs'
import Button from '@/Components/Button/Button'
import DecoratedInput from '@/Components/Input/DecoratedInput'
import { WebApplication } from '@/Application/WebApplication'
@@ -63,7 +63,7 @@ const PackagesPreferencesSection: FunctionComponent<Props> = ({
}
const submitExtensionUrl = async (url: string) => {
const component = await application.features.downloadExternalFeature(url)
const component = await application.features.downloadRemoteThirdPartyFeature(url)
if (component) {
setConfirmableExtension(component)
}
@@ -90,10 +90,6 @@ const PackagesPreferencesSection: FunctionComponent<Props> = ({
return false
}
if (extension instanceof SNComponent) {
return !['modal', 'rooms'].includes(extension.area)
}
return true
})

View File

@@ -1,3 +1,3 @@
import { SNActionsExtension, SNComponent, SNTheme } from '@standardnotes/snjs'
import { ComponentInterface, SNActionsExtension, ThemeInterface } from '@standardnotes/snjs'
export type AnyPackageType = SNComponent | SNTheme | SNActionsExtension
export type AnyPackageType = ComponentInterface | ThemeInterface | SNActionsExtension

View File

@@ -1,4 +1,4 @@
import { PrefKey, Platform } from '@standardnotes/snjs'
import { PrefKey, Platform, PrefDefaults } from '@standardnotes/snjs'
import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
import { WebApplication } from '@/Application/WebApplication'
import { FunctionComponent, useState } from 'react'
@@ -6,7 +6,6 @@ import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import Switch from '@/Components/Switch/Switch'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import { PrefDefaults } from '@/Constants/PrefDefaults'
type Props = {
application: WebApplication

View File

@@ -1,6 +1,13 @@
import { Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
import { WebApplication } from '@/Application/WebApplication'
import { ApplicationEvent, FeatureIdentifier, FeatureStatus, FindNativeFeature, PrefKey } from '@standardnotes/snjs'
import {
ApplicationEvent,
FeatureIdentifier,
FeatureStatus,
FindNativeFeature,
PrefKey,
PrefDefaults,
} from '@standardnotes/snjs'
import { Fragment, FunctionComponent, useCallback, useEffect, useState } from 'react'
import { usePremiumModal } from '@/Hooks/usePremiumModal'
import PreferencesGroup from '../../../PreferencesComponents/PreferencesGroup'
@@ -8,7 +15,6 @@ import PreferencesSegment from '../../../PreferencesComponents/PreferencesSegmen
import LabsFeature from './LabsFeature'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { PrefDefaults } from '@/Constants/PrefDefaults'
type ExperimentalFeatureItem = {
identifier: FeatureIdentifier

View File

@@ -21,9 +21,9 @@ const Persistence = ({ application }: Props) => {
setShouldPersistNoteState(shouldPersist)
if (shouldPersist) {
application.getViewControllerManager().persistValues()
application.controllers.persistValues()
} else {
application.getViewControllerManager().clearPersistedValues()
application.controllers.clearPersistedValues()
}
}

View File

@@ -1,12 +1,11 @@
import Switch from '@/Components/Switch/Switch'
import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
import { WebApplication } from '@/Application/WebApplication'
import { PrefKey } from '@standardnotes/snjs'
import { PrefKey, PrefDefaults } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useState } from 'react'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import { PrefDefaults } from '@/Constants/PrefDefaults'
type Props = {
application: WebApplication

View File

@@ -25,7 +25,7 @@ const HomeServerSettings = () => {
const homeServerService = application.homeServer as HomeServerServiceInterface
const featuresService = application.features
const sessionsService = application.sessions
const viewControllerManager = application.getViewControllerManager()
const viewControllerManager = application.controllers
const logsTextarea = useRef<HTMLTextAreaElement>(null)

View File

@@ -51,7 +51,7 @@ const CreateAccount: FunctionComponent<Props> = ({ viewControllerManager, applic
}
const subscribeWithoutAccount = () => {
application.getViewControllerManager().purchaseFlowController.openPurchaseWebpage()
application.controllers.purchaseFlowController.openPurchaseWebpage()
}
const handleCreateAccount = async () => {

View File

@@ -1,11 +1,17 @@
import { WebApplication } from '@/Application/WebApplication'
import { ComponentArea, ContentType, FeatureIdentifier, GetFeatures, SNComponent } from '@standardnotes/snjs'
import {
ComponentArea,
ComponentInterface,
ComponentOrNativeFeature,
ContentType,
FeatureIdentifier,
PreferencesServiceEvent,
ThemeFeatureDescription,
} from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon'
import FocusModeSwitch from './FocusModeSwitch'
import ThemesMenuButton from './ThemesMenuButton'
import { ThemeItem } from './ThemeItem'
import { sortThemes } from '@/Utils/SortThemes'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { QuickSettingsController } from '@/Controllers/QuickSettingsController'
@@ -13,50 +19,35 @@ import PanelSettingsSection from './PanelSettingsSection'
import Menu from '../Menu/Menu'
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
import MenuRadioButtonItem from '../Menu/MenuRadioButtonItem'
import { useApplication } from '../ApplicationProvider'
import { GetAllThemesUseCase } from '@standardnotes/ui-services'
type MenuProps = {
quickSettingsMenuController: QuickSettingsController
application: WebApplication
}
const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, quickSettingsMenuController }) => {
const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ quickSettingsMenuController }) => {
const application = useApplication()
const { focusModeEnabled, setFocusModeEnabled } = application.paneController
const { closeQuickSettingsMenu } = quickSettingsMenuController
const [themes, setThemes] = useState<ThemeItem[]>([])
const [toggleableComponents, setToggleableComponents] = useState<SNComponent[]>([])
const [themes, setThemes] = useState<ComponentOrNativeFeature<ThemeFeatureDescription>[]>([])
const [editorStackComponents, setEditorStackComponents] = useState<ComponentInterface[]>([])
const defaultThemeOn = !themes.map((item) => item?.component).find((theme) => theme?.active && !theme.isLayerable())
const activeThemes = application.componentManager.getActiveThemes()
const hasNonLayerableActiveTheme = activeThemes.find((theme) => !theme.layerable)
const defaultThemeOn = !hasNonLayerableActiveTheme
const prefsButtonRef = useRef<HTMLButtonElement>(null)
const defaultThemeButtonRef = useRef<HTMLButtonElement>(null)
const reloadThemes = useCallback(() => {
const themes = application.items
.getDisplayableComponents()
.filter((component) => component.isTheme())
.map((item) => {
return {
name: item.displayName,
identifier: item.identifier,
component: item,
}
}) as ThemeItem[]
GetFeatures()
.filter((feature) => feature.content_type === ContentType.TYPES.Theme && !feature.layerable)
.forEach((theme) => {
if (themes.findIndex((item) => item.identifier === theme.identifier) === -1) {
themes.push({
name: theme.name as string,
identifier: theme.identifier,
})
}
})
setThemes(themes.sort(sortThemes))
const usecase = new GetAllThemesUseCase(application.items)
const { thirdParty, native } = usecase.execute({ excludeLayerable: false })
setThemes([...thirdParty, ...native].sort(sortThemes))
}, [application])
const reloadToggleableComponents = useCallback(() => {
const reloadEditorStackComponents = useCallback(() => {
const toggleableComponents = application.items
.getDisplayableComponents()
.filter(
@@ -66,7 +57,7 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, quickSet
component.identifier !== FeatureIdentifier.DeprecatedFoldersComponent,
)
setToggleableComponents(toggleableComponents)
setEditorStackComponents(toggleableComponents)
}, [application])
useEffect(() => {
@@ -85,37 +76,41 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, quickSet
}
}, [application, reloadThemes])
useEffect(() => {
return application.preferences.addEventObserver((event) => {
if (event === PreferencesServiceEvent.PreferencesChanged) {
reloadThemes()
}
})
}, [application, reloadThemes])
useEffect(() => {
const cleanupItemStream = application.streamItems(ContentType.TYPES.Component, () => {
reloadToggleableComponents()
reloadEditorStackComponents()
})
return () => {
cleanupItemStream()
}
}, [application, reloadToggleableComponents])
}, [application, reloadEditorStackComponents])
useEffect(() => {
prefsButtonRef.current?.focus()
}, [])
const toggleComponent = useCallback(
(component: SNComponent) => {
if (component.isTheme()) {
application.componentManager.toggleTheme(component.uuid).catch(console.error)
} else {
application.componentManager.toggleComponent(component.uuid).catch(console.error)
}
const toggleEditorStackComponent = useCallback(
(component: ComponentInterface) => {
application.componentManager.toggleComponent(component).catch(console.error)
},
[application],
)
const deactivateAnyNonLayerableTheme = useCallback(() => {
const activeTheme = themes.map((item) => item.component).find((theme) => theme?.active && !theme.isLayerable())
if (activeTheme) {
application.componentManager.toggleTheme(activeTheme.uuid).catch(console.error)
const nonLayerableActiveTheme = application.componentManager.getActiveThemes().find((theme) => !theme.layerable)
if (nonLayerableActiveTheme) {
void application.componentManager.toggleTheme(nonLayerableActiveTheme)
}
}, [application, themes])
}, [application])
const toggleDefaultTheme = useCallback(() => {
deactivateAnyNonLayerableTheme()
@@ -123,15 +118,15 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, quickSet
return (
<Menu a11yLabel="Quick settings menu" isOpen>
{toggleableComponents.length > 0 && (
{editorStackComponents.length > 0 && (
<>
<div className="my-1 px-3 text-sm font-semibold uppercase text-text">Tools</div>
{toggleableComponents.map((component) => (
{editorStackComponents.map((component) => (
<MenuSwitchButtonItem
onChange={() => {
toggleComponent(component)
toggleEditorStackComponent(component)
}}
checked={component.active}
checked={application.componentManager.isComponentActive(component)}
key={component.uuid}
>
<Icon type="window" className="mr-2 text-neutral" />
@@ -146,7 +141,7 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, quickSet
Default
</MenuRadioButtonItem>
{themes.map((theme) => (
<ThemesMenuButton item={theme} application={application} key={theme.component?.uuid ?? theme.identifier} />
<ThemesMenuButton uiFeature={theme} key={theme.uniqueIdentifier} />
))}
<HorizontalSeparator classes="my-2" />
<FocusModeSwitch

View File

@@ -1,7 +0,0 @@
import { FeatureIdentifier, SNTheme } from '@standardnotes/snjs'
export type ThemeItem = {
name: string
identifier: FeatureIdentifier
component?: SNTheme
}

View File

@@ -1,9 +1,12 @@
import { WebApplication } from '@/Application/WebApplication'
import { FeatureIdentifier, FeatureStatus } from '@standardnotes/snjs'
import {
ComponentOrNativeFeature,
FeatureIdentifier,
FeatureStatus,
ThemeFeatureDescription,
} from '@standardnotes/snjs'
import { FunctionComponent, MouseEventHandler, useCallback, useMemo } from 'react'
import Icon from '@/Components/Icon/Icon'
import { usePremiumModal } from '@/Hooks/usePremiumModal'
import { ThemeItem } from './ThemeItem'
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
import { isMobileScreen } from '@/Utils'
import { classNames } from '@standardnotes/utils'
@@ -12,38 +15,41 @@ import MenuRadioButtonItem from '../Menu/MenuRadioButtonItem'
import { useCommandService } from '../CommandProvider'
import { TOGGLE_DARK_MODE_COMMAND } from '@standardnotes/ui-services'
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
import { useApplication } from '../ApplicationProvider'
type Props = {
item: ThemeItem
application: WebApplication
uiFeature: ComponentOrNativeFeature<ThemeFeatureDescription>
}
const ThemesMenuButton: FunctionComponent<Props> = ({ application, item }) => {
const ThemesMenuButton: FunctionComponent<Props> = ({ uiFeature }) => {
const application = useApplication()
const commandService = useCommandService()
const premiumModal = usePremiumModal()
const isThirdPartyTheme = useMemo(
() => application.features.isThirdPartyFeature(item.identifier),
[application, item.identifier],
() => application.features.isThirdPartyFeature(uiFeature.featureIdentifier),
[application, uiFeature.featureIdentifier],
)
const isEntitledToTheme = useMemo(
() => application.features.getFeatureStatus(item.identifier) === FeatureStatus.Entitled,
[application, item.identifier],
() => application.features.getFeatureStatus(uiFeature.featureIdentifier) === FeatureStatus.Entitled,
[application, uiFeature.featureIdentifier],
)
const canActivateTheme = useMemo(() => isEntitledToTheme || isThirdPartyTheme, [isEntitledToTheme, isThirdPartyTheme])
const toggleTheme = useCallback(() => {
if (item.component && canActivateTheme) {
const isThemeLayerable = item.component.isLayerable()
const themeIsLayerableOrNotActive = isThemeLayerable || !item.component.active
if (themeIsLayerableOrNotActive) {
application.componentManager.toggleTheme(item.component.uuid).catch(console.error)
}
} else {
premiumModal.activate(`${item.name} theme`)
if (!canActivateTheme) {
premiumModal.activate(`${uiFeature.displayName} theme`)
return
}
}, [application, canActivateTheme, item, premiumModal])
const isThemeLayerable = uiFeature.layerable
const themeIsLayerableOrNotActive = isThemeLayerable || !application.componentManager.isThemeActive(uiFeature)
if (themeIsLayerableOrNotActive) {
void application.componentManager.toggleTheme(uiFeature)
}
}, [application, canActivateTheme, uiFeature, premiumModal])
const onClick: MouseEventHandler<HTMLButtonElement> = useCallback(
(event) => {
@@ -54,34 +60,38 @@ const ThemesMenuButton: FunctionComponent<Props> = ({ application, item }) => {
)
const isMobile = application.isNativeMobileWeb() || isMobileScreen()
const shouldHideButton = item.identifier === FeatureIdentifier.DynamicTheme && isMobile
const shouldHideButton = uiFeature.featureIdentifier === FeatureIdentifier.DynamicTheme && isMobile
const darkThemeShortcut = useMemo(() => {
if (item.identifier === FeatureIdentifier.DarkTheme) {
if (uiFeature.featureIdentifier === FeatureIdentifier.DarkTheme) {
return commandService.keyboardShortcutForCommand(TOGGLE_DARK_MODE_COMMAND)
}
}, [commandService, item.identifier])
}, [commandService, uiFeature.featureIdentifier])
if (shouldHideButton) {
return null
}
return item.component?.isLayerable() ? (
<MenuSwitchButtonItem checked={item.component.active} onChange={() => toggleTheme()}>
const themeActive = uiFeature ? application.componentManager.isThemeActive(uiFeature) : false
const dockIcon = uiFeature.dockIcon
return uiFeature.layerable ? (
<MenuSwitchButtonItem checked={themeActive} onChange={() => toggleTheme()}>
{!canActivateTheme && (
<Icon type={PremiumFeatureIconName} className={classNames(PremiumFeatureIconClass, 'mr-2')} />
)}
{item.name}
{uiFeature.displayName}
</MenuSwitchButtonItem>
) : (
<MenuRadioButtonItem checked={Boolean(item.component?.active)} onClick={onClick}>
<span className={classNames('mr-auto', item.component?.active ? 'font-semibold' : undefined)}>{item.name}</span>
<MenuRadioButtonItem checked={themeActive} onClick={onClick}>
<span className={classNames('mr-auto', themeActive ? 'font-semibold' : undefined)}>{uiFeature.displayName}</span>
{darkThemeShortcut && <KeyboardShortcutIndicator className="mr-2" shortcut={darkThemeShortcut} />}
{item.component && canActivateTheme ? (
{uiFeature && canActivateTheme ? (
<div
className="h-5 w-5 rounded-full"
style={{
backgroundColor: item.component.package_info?.dock_icon?.background_color,
backgroundColor: dockIcon?.background_color,
}}
></div>
) : (

View File

@@ -1,6 +1,5 @@
import RevisionContentLocked from './RevisionContentLocked'
import { observer } from 'mobx-react-lite'
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
import { NoteHistoryController, RevisionContentState } from '@/Controllers/NoteHistory/NoteHistoryController'
import Spinner from '@/Components/Spinner/Spinner'
import { ReadonlyNoteContent } from '../NoteView/ReadonlyNoteContent'
@@ -9,10 +8,9 @@ import { SNNote } from '@standardnotes/snjs'
type Props = {
noteHistoryController: NoteHistoryController
note: SNNote
subscriptionController: SubscriptionController
}
const HistoryModalContentPane = ({ noteHistoryController, subscriptionController, note }: Props) => {
const HistoryModalContentPane = ({ noteHistoryController, note }: Props) => {
const { selectedRevision, contentState } = noteHistoryController
switch (contentState) {
@@ -30,7 +28,7 @@ const HistoryModalContentPane = ({ noteHistoryController, subscriptionController
}
return <ReadonlyNoteContent note={note} content={selectedRevision.payload.content} showLinkedItems={false} />
case RevisionContentState.NotEntitled:
return <RevisionContentLocked subscriptionController={subscriptionController} />
return <RevisionContentLocked />
default:
return null
}

View File

@@ -15,7 +15,6 @@ import MobileModalHeader from '../Modal/MobileModalHeader'
const HistoryModalDialogContent = ({
application,
dismissModal,
subscriptionController,
note,
selectionController,
}: RevisionHistoryModalContentProps) => {
@@ -87,11 +86,7 @@ const HistoryModalDialogContent = ({
selectedMobileTab === 'Content' ? 'flex' : 'hidden',
)}
>
<HistoryModalContentPane
noteHistoryController={noteHistoryController}
note={note}
subscriptionController={subscriptionController}
/>
<HistoryModalContentPane noteHistoryController={noteHistoryController} note={note} />
</div>
</div>
<HistoryModalFooter dismissModal={dismissModal} noteHistoryController={noteHistoryController} />

View File

@@ -2,7 +2,7 @@ import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'react'
import { HistoryLockedIllustration } from '@standardnotes/icons'
import Button from '@/Components/Button/Button'
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
import { useApplication } from '../ApplicationProvider'
const getPlanHistoryDuration = (planName: string | undefined) => {
switch (planName) {
@@ -19,12 +19,15 @@ const getPremiumContentCopy = (planName: string | undefined) => {
return `Version history is limited to ${getPlanHistoryDuration(planName)} in the ${planName} plan`
}
type Props = {
subscriptionController: SubscriptionController
}
const RevisionContentLocked: FunctionComponent = () => {
const application = useApplication()
const RevisionContentLocked: FunctionComponent<Props> = ({ subscriptionController }) => {
const { userSubscriptionName, isUserSubscriptionExpired, isUserSubscriptionCanceled } = subscriptionController
let planName = 'free'
if (application.subscriptions.hasOnlineSubscription()) {
if (!application.subscriptions.isUserSubscriptionCanceled && !application.subscriptions.isUserSubscriptionExpired) {
planName = application.subscriptions.userSubscriptionName
}
}
return (
<div className="flex h-full w-full items-center justify-center">
@@ -32,12 +35,7 @@ const RevisionContentLocked: FunctionComponent<Props> = ({ subscriptionControlle
<HistoryLockedIllustration />
<div className="mt-2 mb-1 text-lg font-bold">Can't access this version</div>
<div className="leading-140% mb-4 text-passive-0">
{getPremiumContentCopy(
!isUserSubscriptionCanceled && !isUserSubscriptionExpired && userSubscriptionName
? userSubscriptionName
: 'free',
)}
. Learn more about our other plans to upgrade your history capacity.
{getPremiumContentCopy(planName)}. Learn more about our other plans to upgrade your history capacity.
</div>
<Button
primary

View File

@@ -10,7 +10,6 @@ const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps> = ({
application,
historyModalController,
selectionController,
subscriptionController,
}) => {
const addAndroidBackHandler = useAndroidBackHandler()
@@ -49,7 +48,6 @@ const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps> = ({
dismissModal={historyModalController.dismissModal}
note={historyModalController.note}
selectionController={selectionController}
subscriptionController={subscriptionController}
/>
)}
</HistoryModalDialog>

View File

@@ -1,13 +1,11 @@
import { WebApplication } from '@/Application/WebApplication'
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
import { SNNote } from '@standardnotes/snjs'
type CommonProps = {
application: WebApplication
selectionController: SelectedItemsController
subscriptionController: SubscriptionController
}
export type RevisionHistoryModalProps = CommonProps & {

View File

@@ -41,7 +41,7 @@ const RemoteImageComponent = ({ className, src, alt, node, format, nodeKey }: Pr
const blob = await response.blob()
const file = new File([blob], src, { type: blob.type })
const { filesController, linkingController } = application.getViewControllerManager()
const { filesController, linkingController } = application.controllers
const uploadedFile = await filesController.uploadNewFile(file, false)

View File

@@ -6,6 +6,10 @@ import {
EditorLineHeight,
isPayloadSourceRetrieved,
PrefKey,
PrefDefaults,
FeatureIdentifier,
FeatureStatus,
GetSuperNoteFeature,
} from '@standardnotes/snjs'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import { BlocksEditor } from './BlocksEditor'
@@ -30,7 +34,6 @@ import {
ChangeEditorFunction,
} from './Plugins/ChangeContentCallback/ChangeContentCallback'
import PasswordPlugin from './Plugins/PasswordPlugin/PasswordPlugin'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { useCommandService } from '@/Components/CommandProvider'
import { SUPER_SHOW_MARKDOWN_PREVIEW } from '@standardnotes/ui-services'
import { SuperNoteMarkdownPreview } from './SuperNoteMarkdownPreview'
@@ -45,6 +48,7 @@ import ModalOverlay from '@/Components/Modal/ModalOverlay'
import MobileToolbarPlugin from './Plugins/MobileToolbarPlugin/MobileToolbarPlugin'
import CodeOptionsPlugin from './Plugins/CodeOptionsPlugin/CodeOptions'
import RemoteImagePlugin from './Plugins/RemoteImagePlugin/RemoteImagePlugin'
import NotEntitledBanner from '../ComponentView/NotEntitledBanner'
export const SuperNotePreviewCharLimit = 160
@@ -68,6 +72,11 @@ export const SuperEditor: FunctionComponent<Props> = ({
const ignoreNextChange = useRef(false)
const [showMarkdownPreview, setShowMarkdownPreview] = useState(false)
const getMarkdownPlugin = useRef<GetMarkdownPluginInterface | null>(null)
const [featureStatus, setFeatureStatus] = useState<FeatureStatus>(FeatureStatus.Entitled)
useEffect(() => {
setFeatureStatus(application.features.getFeatureStatus(FeatureIdentifier.SuperEditor))
}, [application.features])
const commandService = useCommandService()
@@ -194,6 +203,9 @@ export const SuperEditor: FunctionComponent<Props> = ({
return (
<div className="font-editor relative flex h-full w-full flex-col md:block" ref={ref}>
{featureStatus !== FeatureStatus.Entitled && (
<NotEntitledBanner featureStatus={featureStatus} feature={GetSuperNoteFeature()} />
)}
<ErrorBoundary>
<LinkingControllerProvider controller={linkingController}>
<FilesControllerProvider controller={filesController}>

View File

@@ -1,7 +1,14 @@
import { ContentType, NoteContent, NoteType, SNNote, spaceSeparatedStrings } from '@standardnotes/snjs'
import {
ContentType,
NoteContent,
NoteType,
SNNote,
isIframeUIFeature,
spaceSeparatedStrings,
} from '@standardnotes/snjs'
import { useCallback, useEffect, useMemo } from 'react'
import { useApplication } from '../ApplicationProvider'
import ComponentView from '../ComponentView/ComponentView'
import IframeFeatureView from '../ComponentView/IframeFeatureView'
import Icon from '../Icon/Icon'
import Modal, { ModalAction } from '../Modal/Modal'
import { EditorMenuItem } from '../NotesOptions/EditorMenuItem'
@@ -20,27 +27,30 @@ const SuperNoteConverter = ({
onComplete: () => void
}) => {
const application = useApplication()
const { name, noteType, component } = convertTo
const { uiFeature } = convertTo
const format = useMemo(() => {
if (component && component.package_info.file_type) {
return component.package_info.file_type
if (uiFeature) {
const fileType = uiFeature.fileType
if (fileType) {
return fileType
}
}
if (noteType === NoteType.Markdown) {
if (uiFeature.noteType === NoteType.Markdown) {
return 'md'
}
if (noteType === NoteType.RichText) {
if (uiFeature.noteType === NoteType.RichText) {
return 'html'
}
if (noteType === NoteType.Plain) {
if (uiFeature.noteType === NoteType.Plain) {
return 'txt'
}
return 'json'
}, [component, noteType])
}, [uiFeature])
const convertedContent = useMemo(() => {
if (note.text.length === 0) {
@@ -51,7 +61,7 @@ const SuperNoteConverter = ({
}, [format, note])
const componentViewer = useMemo(() => {
if (!component) {
if (!uiFeature || !isIframeUIFeature(uiFeature)) {
return undefined
}
@@ -61,12 +71,12 @@ const SuperNoteConverter = ({
references: note.references,
})
const componentViewer = application.componentManager.createComponentViewer(component)
componentViewer.setReadonly(true)
componentViewer.lockReadonly = true
componentViewer.overrideContextItem = templateNoteForRevision
const componentViewer = application.componentManager.createComponentViewer(uiFeature, {
readonlyItem: templateNoteForRevision,
})
return componentViewer
}, [application.componentManager, application.items, component, convertedContent, note.references, note.title])
}, [application.componentManager, application.items, uiFeature, convertedContent, note.references, note.title])
useEffect(() => {
return () => {
@@ -163,7 +173,7 @@ const SuperNoteConverter = ({
) : null}
{componentViewer ? (
<div className="component-view min-h-0">
<ComponentView key={componentViewer.identifier} componentViewer={componentViewer} application={application} />
<IframeFeatureView key={componentViewer.identifier} componentViewer={componentViewer} />
</div>
) : (
<div className="h-full min-h-0 overflow-hidden">

View File

@@ -24,7 +24,7 @@ type Props = {
}
const Navigation = forwardRef<HTMLDivElement, Props>(({ application, className, children, id }, ref) => {
const viewControllerManager = useMemo(() => application.getViewControllerManager(), [application])
const viewControllerManager = useMemo(() => application.controllers, [application])
const { setPaneLayout } = useResponsiveAppPane()
const [hasPasscode, setHasPasscode] = useState(() => application.hasPasscode())

View File

@@ -0,0 +1,13 @@
import Icon from '@/Components/Icon/Icon'
export const WarningCircle = () => {
return (
<button
className={
'peer flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-warning text-warning-contrast'
}
>
<Icon type={'warning'} size="small" />
</button>
)
}