refactor: native feature management (#2350)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const CreateAccount: FunctionComponent<Props> = ({ viewControllerManager, applic
|
||||
}
|
||||
|
||||
const subscribeWithoutAccount = () => {
|
||||
application.getViewControllerManager().purchaseFlowController.openPurchaseWebpage()
|
||||
application.controllers.purchaseFlowController.openPurchaseWebpage()
|
||||
}
|
||||
|
||||
const handleCreateAccount = async () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { FeatureIdentifier, SNTheme } from '@standardnotes/snjs'
|
||||
|
||||
export type ThemeItem = {
|
||||
name: string
|
||||
identifier: FeatureIdentifier
|
||||
component?: SNTheme
|
||||
}
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user