refactor: native feature management (#2350)

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

View File

@@ -9,7 +9,7 @@ export class DevMode {
/** Valid only when running a mock event publisher on port 3124 */
async purchaseMockSubscription() {
const subscriptionId = 2000
const subscriptionId = 2002
const email = this.application.getUser()?.email
const response = await fetch('http://localhost:3124/events', {
method: 'POST',

View File

@@ -16,6 +16,7 @@ import {
WebAppEvent,
BackupServiceInterface,
DesktopWatchedDirectoriesChanges,
ComponentInterface,
} from '@standardnotes/snjs'
import { WebApplicationInterface } from '@standardnotes/ui-services'
@@ -122,11 +123,11 @@ export class DesktopManager
* Sending a component in its raw state is really slow for the desktop app
* Keys are not passed into ItemParams, so the result is not encrypted
*/
convertComponentForTransmission(component: SNComponent) {
convertComponentForTransmission(component: ComponentInterface) {
return component.payloadRepresentation().ejected()
}
syncComponentsInstallation(components: SNComponent[]) {
syncComponentsInstallation(components: ComponentInterface[]) {
Promise.all(
components.map((component) => {
return this.convertComponentForTransmission(component)
@@ -138,7 +139,7 @@ export class DesktopManager
.catch(console.error)
}
registerUpdateObserver(callback: (component: SNComponent) => void): () => void {
registerUpdateObserver(callback: (component: ComponentInterface) => void): () => void {
const observer = {
callback: callback,
}

View File

@@ -24,6 +24,7 @@ import {
BackupServiceInterface,
InternalFeatureService,
InternalFeatureServiceInterface,
PrefDefaults,
NoteContent,
SNNote,
} from '@standardnotes/snjs'
@@ -48,7 +49,6 @@ import {
} from '@standardnotes/ui-services'
import { MobileWebReceiver, NativeMobileEventListener } from '../NativeMobileWeb/MobileWebReceiver'
import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { setCustomViewportHeight } from '@/setViewportHeightWithFallback'
import { WebServices } from './WebServices'
import { FeatureName } from '@/Controllers/FeatureName'
@@ -121,7 +121,12 @@ export class WebApplication extends SNApplication implements WebApplicationInter
this.webServices = {} as WebServices
this.webServices.keyboardService = new KeyboardService(platform, this.environment)
this.webServices.archiveService = new ArchiveManager(this)
this.webServices.themeService = new ThemeManager(this, this.internalEventBus)
this.webServices.themeService = new ThemeManager(
this,
this.preferences,
this.componentManager,
this.internalEventBus,
)
this.webServices.autolockService = this.isNativeMobileWeb()
? undefined
: new AutolockService(this, this.internalEventBus)
@@ -232,7 +237,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
return this.webServices.vaultDisplayService
}
public getViewControllerManager(): ViewControllerManager {
public get controllers(): ViewControllerManager {
return this.webServices.viewControllerManager
}
@@ -265,7 +270,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
}
public get featuresController() {
return this.getViewControllerManager().featuresController
return this.controllers.featuresController
}
public get desktopDevice(): DesktopDeviceInterface | undefined {
@@ -388,14 +393,14 @@ export class WebApplication extends SNApplication implements WebApplicationInter
}
handleReceivedFileEvent(file: { name: string; mimeType: string; data: string }): void {
const filesController = this.getViewControllerManager().filesController
const filesController = this.controllers.filesController
const blob = getBlobFromBase64(file.data, file.mimeType)
const mappedFile = new File([blob], file.name, { type: file.mimeType })
void filesController.uploadNewFile(mappedFile, true)
}
async handleReceivedTextEvent({ text, title }: { text: string; title?: string | undefined }) {
const titleForNote = title || this.getViewControllerManager().itemListController.titleForNewNote()
const titleForNote = title || this.controllers.itemListController.titleForNewNote()
const note = this.items.createTemplateItem<NoteContent, SNNote>(ContentType.TYPES.Note, {
title: titleForNote,
@@ -405,7 +410,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
const insertedNote = await this.mutator.insertItem(note)
this.getViewControllerManager().selectionController.selectItem(insertedNote.uuid, true).catch(console.error)
this.controllers.selectionController.selectItem(insertedNote.uuid, true).catch(console.error)
}
private async lockApplicationAfterMobileEventIfApplicable(): Promise<void> {
@@ -457,23 +462,23 @@ export class WebApplication extends SNApplication implements WebApplicationInter
}
entitledToPerTagPreferences(): boolean {
return this.hasValidSubscription()
return this.hasValidFirstPartySubscription()
}
get entitledToFiles(): boolean {
return this.getViewControllerManager().featuresController.entitledToFiles
return this.controllers.featuresController.entitledToFiles
}
showPremiumModal(featureName?: FeatureName): void {
void this.getViewControllerManager().featuresController.showPremiumAlert(featureName)
void this.controllers.featuresController.showPremiumAlert(featureName)
}
hasValidSubscription(): boolean {
return this.getViewControllerManager().subscriptionController.hasValidSubscription()
hasValidFirstPartySubscription(): boolean {
return this.controllers.subscriptionController.hasFirstPartyOnlineOrOfflineSubscription
}
async openPurchaseFlow() {
await this.getViewControllerManager().purchaseFlowController.openPurchaseFlow()
await this.controllers.purchaseFlowController.openPurchaseFlow()
}
addNativeMobileEventListener = (listener: NativeMobileEventListener) => {
@@ -485,11 +490,11 @@ export class WebApplication extends SNApplication implements WebApplicationInter
}
showAccountMenu(): void {
this.getViewControllerManager().accountMenuController.setShow(true)
this.controllers.accountMenuController.setShow(true)
}
hideAccountMenu(): void {
this.getViewControllerManager().accountMenuController.setShow(false)
this.controllers.accountMenuController.setShow(false)
}
/**
@@ -510,9 +515,9 @@ export class WebApplication extends SNApplication implements WebApplicationInter
}
openPreferences(pane?: PreferenceId): void {
this.getViewControllerManager().preferencesController.openPreferences()
this.controllers.preferencesController.openPreferences()
if (pane) {
this.getViewControllerManager().preferencesController.setCurrentPane(pane)
this.controllers.preferencesController.setCurrentPane(pane)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,7 +27,6 @@ export const SYNC_TIMEOUT_DEBOUNCE = 350
export const SYNC_TIMEOUT_NO_DEBOUNCE = 100
type EditorMetadata = {
name: string
icon: IconType
subtleIcon?: IconType
iconClassName: string
@@ -35,16 +34,8 @@ type EditorMetadata = {
}
export const SuperEditorMetadata: EditorMetadata = {
name: 'Super',
icon: 'file-doc',
subtleIcon: 'format-align-left',
iconClassName: 'text-accessory-tint-1',
iconTintNumber: 1,
}
export const PlainEditorMetadata: EditorMetadata = {
name: 'Plain Text',
icon: 'plain-text',
iconClassName: 'text-accessory-tint-1',
iconTintNumber: 1,
}

View File

@@ -1,49 +0,0 @@
import {
PrefKey,
CollectionSort,
NewNoteTitleFormat,
EditorLineHeight,
EditorFontSize,
EditorLineWidth,
PrefValue,
} from '@standardnotes/models'
import { FeatureIdentifier } from '@standardnotes/snjs'
export const PrefDefaults = {
[PrefKey.TagsPanelWidth]: 220,
[PrefKey.NotesPanelWidth]: 350,
[PrefKey.EditorWidth]: null,
[PrefKey.EditorLeft]: null,
[PrefKey.EditorMonospaceEnabled]: false,
[PrefKey.EditorSpellcheck]: true,
[PrefKey.EditorResizersEnabled]: false,
[PrefKey.EditorLineHeight]: EditorLineHeight.Normal,
[PrefKey.EditorLineWidth]: EditorLineWidth.FullWidth,
[PrefKey.EditorFontSize]: EditorFontSize.Normal,
[PrefKey.SortNotesBy]: CollectionSort.CreatedAt,
[PrefKey.SortNotesReverse]: false,
[PrefKey.NotesShowArchived]: false,
[PrefKey.NotesShowTrashed]: false,
[PrefKey.NotesHidePinned]: false,
[PrefKey.NotesHideProtected]: false,
[PrefKey.NotesHideNotePreview]: false,
[PrefKey.NotesHideDate]: false,
[PrefKey.NotesHideTags]: false,
[PrefKey.NotesHideEditorIcon]: false,
[PrefKey.UseSystemColorScheme]: false,
[PrefKey.AutoLightThemeIdentifier]: 'Default',
[PrefKey.AutoDarkThemeIdentifier]: FeatureIdentifier.DarkTheme,
[PrefKey.NoteAddToParentFolders]: true,
[PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat.CurrentDateAndTime,
[PrefKey.CustomNoteTitleFormat]: 'YYYY-MM-DD [at] hh:mm A',
[PrefKey.UpdateSavingStatusIndicator]: true,
[PrefKey.PaneGesturesEnabled]: true,
[PrefKey.MomentsDefaultTagUuid]: undefined,
[PrefKey.ClipperDefaultTagUuid]: undefined,
[PrefKey.DefaultEditorIdentifier]: FeatureIdentifier.PlainEditor,
[PrefKey.SuperNoteExportFormat]: 'json',
[PrefKey.SystemViewPreferences]: {},
[PrefKey.AuthenticatorNames]: '',
} satisfies {
[key in PrefKey]: PrefValue[key]
}

View File

@@ -63,7 +63,7 @@ export class FeaturesController extends AbstractViewController {
case ApplicationEvent.DidPurchaseSubscription:
this.showPurchaseSuccessAlert()
break
case ApplicationEvent.FeaturesUpdated:
case ApplicationEvent.FeaturesAvailabilityChanged:
case ApplicationEvent.Launched:
case ApplicationEvent.LocalDataLoaded:
case ApplicationEvent.UserRolesChanged:

View File

@@ -22,6 +22,7 @@ import {
isSystemView,
NotesAndFilesDisplayControllerOptions,
InternalEventBusInterface,
PrefDefaults,
} from '@standardnotes/snjs'
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
import { WebApplication } from '../../Application/WebApplication'
@@ -32,7 +33,6 @@ import { SearchOptionsController } from '../SearchOptionsController'
import { SelectedItemsController } from '../SelectedItemsController'
import { NotesController } from '../NotesController/NotesController'
import { formatDateAndTimeForNote } from '@/Utils/DateUtils'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import dayjs from 'dayjs'
import dayjsAdvancedFormat from 'dayjs/plugin/advancedFormat'

View File

@@ -1,7 +1,7 @@
import { FileItemActionType } from '@/Components/AttachedFilesPopover/PopoverFileItemAction'
import { NoteViewController } from '@/Components/NoteView/Controller/NoteViewController'
import { AppPaneId } from '@/Components/Panes/AppPaneMetadata'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem'
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import {
@@ -16,6 +16,7 @@ import {
isNote,
InternalEventBusInterface,
isTag,
PrefDefaults,
} from '@standardnotes/snjs'
import { action, computed, makeObservable, observable } from 'mobx'
import { AbstractViewController } from './Abstract/AbstractViewController'
@@ -77,7 +78,7 @@ export class LinkingController extends AbstractViewController {
}
get isEntitledToNoteLinking() {
return !!this.subscriptionController.onlineSubscription
return !!this.subscriptionController.hasFirstPartyOnlineOrOfflineSubscription
}
setIsLinkingPanelOpen = (open: boolean) => {

View File

@@ -12,6 +12,7 @@ import {
EditorLineWidth,
InternalEventBusInterface,
MutationType,
PrefDefaults,
} from '@standardnotes/snjs'
import { makeObservable, observable, action, computed, runInAction } from 'mobx'
import { WebApplication } from '../../Application/WebApplication'
@@ -20,7 +21,6 @@ import { SelectedItemsController } from '../SelectedItemsController'
import { ItemListController } from '../ItemList/ItemListController'
import { NavigationController } from '../Navigation/NavigationController'
import { NotesControllerInterface } from './NotesControllerInterface'
import { PrefDefaults } from '@/Constants/PrefDefaults'
export class NotesController extends AbstractViewController implements NotesControllerInterface {
shouldLinkToParentFolders: boolean

View File

@@ -3,7 +3,13 @@ import {
TOGGLE_LIST_PANE_KEYBOARD_COMMAND,
TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND,
} from '@standardnotes/ui-services'
import { ApplicationEvent, InternalEventBusInterface, PrefKey, removeFromArray } from '@standardnotes/snjs'
import {
ApplicationEvent,
InternalEventBusInterface,
PrefKey,
removeFromArray,
PrefDefaults,
} from '@standardnotes/snjs'
import { AppPaneId } from '../../Components/Panes/AppPaneMetadata'
import { isMobileScreen } from '@/Utils'
import { makeObservable, observable, action, computed } from 'mobx'
@@ -11,7 +17,6 @@ import { Disposer } from '@/Types/Disposer'
import { MediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
import { WebApplication } from '@/Application/WebApplication'
import { AbstractViewController } from '../Abstract/AbstractViewController'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { log, LoggingDomain } from '@/Logging'
import { PaneLayout } from './PaneLayout'
import { panesForLayout } from './panesForLayout'

View File

@@ -1,32 +1,26 @@
import { Subscription } from '@standardnotes/security'
import { destroyAllObjectProperties } from '@/Utils'
import {
ApplicationEvent,
ClientDisplayableError,
convertTimestampToMilliseconds,
InternalEventBusInterface,
Invitation,
InvitationStatus,
SubscriptionClientInterface,
SubscriptionManagerEvent,
SubscriptionManagerInterface,
} from '@standardnotes/snjs'
import { action, computed, makeObservable, observable, runInAction } from 'mobx'
import { computed, makeObservable, observable, runInAction } from 'mobx'
import { WebApplication } from '../../Application/WebApplication'
import { AbstractViewController } from '../Abstract/AbstractViewController'
import { AvailableSubscriptions } from './AvailableSubscriptionsType'
import { Subscription } from './SubscriptionType'
export class SubscriptionController extends AbstractViewController {
private readonly ALLOWED_SUBSCRIPTION_INVITATIONS = 5
onlineSubscription: Subscription | undefined = undefined
availableSubscriptions: AvailableSubscriptions | undefined = undefined
subscriptionInvitations: Invitation[] | undefined = undefined
hasAccount: boolean
hasFirstPartySubscription: boolean
onlineSubscription: Subscription | undefined = undefined
override deinit() {
super.deinit()
;(this.onlineSubscription as unknown) = undefined
;(this.availableSubscriptions as unknown) = undefined
;(this.subscriptionInvitations as unknown) = undefined
destroyAllObjectProperties(this)
@@ -35,39 +29,28 @@ export class SubscriptionController extends AbstractViewController {
constructor(
application: WebApplication,
eventBus: InternalEventBusInterface,
private subscriptionManager: SubscriptionClientInterface,
private subscriptionManager: SubscriptionManagerInterface,
) {
super(application, eventBus)
this.hasAccount = application.hasAccount()
this.hasFirstPartySubscription = application.features.hasFirstPartySubscription()
makeObservable(this, {
onlineSubscription: observable,
availableSubscriptions: observable,
subscriptionInvitations: observable,
hasAccount: observable,
hasFirstPartySubscription: observable,
onlineSubscription: observable,
userSubscriptionName: computed,
userSubscriptionExpirationDate: computed,
isUserSubscriptionExpired: computed,
isUserSubscriptionCanceled: computed,
hasFirstPartyOnlineOrOfflineSubscription: computed,
usedInvitationsCount: computed,
allowedInvitationsCount: computed,
allInvitationsUsed: computed,
setUserSubscription: action,
setAvailableSubscriptions: action,
})
this.disposers.push(
application.addEventObserver(async () => {
if (application.hasAccount()) {
this.getSubscriptionInfo().catch(console.error)
this.reloadSubscriptionInvitations().catch(console.error)
}
runInAction(() => {
this.hasFirstPartySubscription = application.features.hasFirstPartySubscription()
this.hasAccount = application.hasAccount()
})
}, ApplicationEvent.Launched),
@@ -75,15 +58,6 @@ export class SubscriptionController extends AbstractViewController {
this.disposers.push(
application.addEventObserver(async () => {
runInAction(() => {
this.hasFirstPartySubscription = application.features.hasFirstPartySubscription()
})
}, ApplicationEvent.LocalDataLoaded),
)
this.disposers.push(
application.addEventObserver(async () => {
this.getSubscriptionInfo().catch(console.error)
this.reloadSubscriptionInvitations().catch(console.error)
runInAction(() => {
this.hasAccount = application.hasAccount()
@@ -91,50 +65,33 @@ export class SubscriptionController extends AbstractViewController {
}, ApplicationEvent.SignedIn),
)
this.disposers.push(
application.subscriptions.addEventObserver(async (event) => {
if (event === SubscriptionManagerEvent.DidFetchSubscription) {
runInAction(() => {
this.onlineSubscription = application.subscriptions.getOnlineSubscription()
})
}
}),
)
this.disposers.push(
application.addEventObserver(async () => {
this.getSubscriptionInfo().catch(console.error)
this.reloadSubscriptionInvitations().catch(console.error)
runInAction(() => {
this.hasFirstPartySubscription = application.features.hasFirstPartySubscription()
})
}, ApplicationEvent.UserRolesChanged),
)
}
get userSubscriptionName(): string {
if (
this.availableSubscriptions &&
this.onlineSubscription &&
this.availableSubscriptions[this.onlineSubscription.planName]
) {
return this.availableSubscriptions[this.onlineSubscription.planName].name
get hasFirstPartyOnlineOrOfflineSubscription(): boolean {
if (this.application.sessions.isSignedIn()) {
if (!this.application.sessions.isSignedIntoFirstPartyServer()) {
return false
}
return this.application.subscriptions.getOnlineSubscription() !== undefined
} else {
return this.application.features.hasFirstPartyOfflineSubscription()
}
return ''
}
get userSubscriptionExpirationDate(): Date | undefined {
if (!this.onlineSubscription) {
return undefined
}
return new Date(convertTimestampToMilliseconds(this.onlineSubscription.endsAt))
}
get isUserSubscriptionExpired(): boolean {
if (!this.userSubscriptionExpirationDate) {
return false
}
return this.userSubscriptionExpirationDate.getTime() < new Date().getTime()
}
get isUserSubscriptionCanceled(): boolean {
return Boolean(this.onlineSubscription?.cancelled)
}
hasValidSubscription(): boolean {
return this.onlineSubscription != undefined && !this.isUserSubscriptionExpired && !this.isUserSubscriptionCanceled
}
get usedInvitationsCount(): number {
@@ -153,14 +110,6 @@ export class SubscriptionController extends AbstractViewController {
return this.usedInvitationsCount === this.ALLOWED_SUBSCRIPTION_INVITATIONS
}
public setUserSubscription(subscription: Subscription): void {
this.onlineSubscription = subscription
}
public setAvailableSubscriptions(subscriptions: AvailableSubscriptions): void {
this.availableSubscriptions = subscriptions
}
async sendSubscriptionInvitation(inviteeEmail: string): Promise<boolean> {
const success = await this.subscriptionManager.inviteToSubscription(inviteeEmail)
@@ -181,33 +130,6 @@ export class SubscriptionController extends AbstractViewController {
return success
}
private async getAvailableSubscriptions() {
try {
const subscriptions = await this.application.getAvailableSubscriptions()
if (!(subscriptions instanceof ClientDisplayableError)) {
this.setAvailableSubscriptions(subscriptions)
}
} catch (error) {
void error
}
}
private async getSubscription() {
try {
const subscription = await this.application.getUserSubscription()
if (!(subscription instanceof ClientDisplayableError) && subscription) {
this.setUserSubscription(subscription)
}
} catch (error) {
console.error(error)
}
}
private async getSubscriptionInfo() {
await this.getSubscription()
await this.getAvailableSubscriptions()
}
private async reloadSubscriptionInvitations(): Promise<void> {
this.subscriptionInvitations = await this.subscriptionManager.listSubscriptionInvitations()
}

View File

@@ -13,7 +13,7 @@ import { destroyAllObjectProperties } from '@/Utils'
import {
DeinitSource,
WebOrDesktopDeviceInterface,
SubscriptionClientInterface,
SubscriptionManagerInterface,
InternalEventHandlerInterface,
InternalEventInterface,
} from '@standardnotes/snjs'
@@ -75,7 +75,7 @@ export class ViewControllerManager implements InternalEventHandlerInterface {
private appEventObserverRemovers: (() => void)[] = []
private subscriptionManager: SubscriptionClientInterface
private subscriptionManager: SubscriptionManagerInterface
private persistenceService: PersistenceService
private applicationEventObserver: EventObserverInterface
private toastService: ToastServiceInterface

View File

@@ -13,7 +13,7 @@ import { ToastType } from '@standardnotes/toast'
import {
ApplicationEvent,
SessionsClientInterface,
SubscriptionClientInterface,
SubscriptionManagerInterface,
SyncClientInterface,
SyncOpStatus,
User,
@@ -39,7 +39,7 @@ describe('ApplicationEventObserver', () => {
let syncStatusController: SyncStatusController
let syncClient: SyncClientInterface
let sessionManager: SessionsClientInterface
let subscriptionManager: SubscriptionClientInterface
let subscriptionManager: SubscriptionManagerInterface
let toastService: ToastServiceInterface
let userService: UserClientInterface
@@ -87,7 +87,7 @@ describe('ApplicationEventObserver', () => {
sessionManager = {} as jest.Mocked<SessionsClientInterface>
sessionManager.getUser = jest.fn().mockReturnValue({} as jest.Mocked<User>)
subscriptionManager = {} as jest.Mocked<SubscriptionClientInterface>
subscriptionManager = {} as jest.Mocked<SubscriptionManagerInterface>
subscriptionManager.acceptInvitation = jest.fn()
toastService = {} as jest.Mocked<ToastServiceInterface>

View File

@@ -8,7 +8,7 @@ import {
import {
ApplicationEvent,
SessionsClientInterface,
SubscriptionClientInterface,
SubscriptionManagerInterface,
SyncClientInterface,
UserClientInterface,
} from '@standardnotes/snjs'
@@ -35,7 +35,7 @@ export class ApplicationEventObserver implements EventObserverInterface {
private syncStatusController: SyncStatusController,
private syncClient: SyncClientInterface,
private sessionManager: SessionsClientInterface,
private subscriptionManager: SubscriptionClientInterface,
private subscriptionManager: SubscriptionManagerInterface,
private toastService: ToastServiceInterface,
private userService: UserClientInterface,
) {}

View File

@@ -1,6 +1,5 @@
import { useApplication } from '@/Components/ApplicationProvider'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { ApplicationEvent, PrefKey } from '@standardnotes/snjs'
import { ApplicationEvent, PrefKey, PrefDefaults } from '@standardnotes/snjs'
import { useEffect, useState } from 'react'
export default function usePreference<Key extends PrefKey>(preference: Key) {

View File

@@ -32,7 +32,7 @@ const PremiumModalProvider: FunctionComponent<Props> = observer(
({ application, featuresController, children }: Props) => {
const featureName = featuresController.premiumAlertFeatureName || ''
const hasSubscription = application.hasValidSubscription()
const hasSubscription = application.hasValidFirstPartySubscription()
const activate = useCallback(
(feature: string) => {

View File

@@ -1,60 +1,46 @@
import { FeatureIdentifier, FeatureStatus } from '@standardnotes/snjs'
import { ComponentArea, NoteType } from '@standardnotes/features'
import { WebApplication } from '@/Application/WebApplication'
import { PlainEditorMetadata, SuperEditorMetadata } from '@/Constants/Constants'
import { FeatureIdentifier } from '@standardnotes/snjs'
import { ComponentArea, FindNativeFeature, GetIframeAndNativeEditors } from '@standardnotes/features'
import { getIconAndTintForNoteType } from './Items/Icons/getIconAndTintForNoteType'
import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
import { WebApplicationInterface } from '@standardnotes/ui-services'
export type EditorOption = DropdownItem & {
value: FeatureIdentifier
isLabs?: boolean
}
export function noteTypeForEditorOptionValue(value: EditorOption['value'], application: WebApplication): NoteType {
if (value === FeatureIdentifier.PlainEditor) {
return NoteType.Plain
} else if (value === FeatureIdentifier.SuperEditor) {
return NoteType.Super
}
export function getDropdownItemsForAllEditors(application: WebApplicationInterface): EditorOption[] {
const options: EditorOption[] = []
const matchingEditor = application.componentManager
.componentsForArea(ComponentArea.Editor)
.find((editor) => editor.identifier === value)
options.push(
...GetIframeAndNativeEditors().map((editor) => {
const [iconType, tint] = getIconAndTintForNoteType(editor.note_type)
return matchingEditor ? matchingEditor.noteType : NoteType.Unknown
}
return {
label: editor.name,
value: editor.identifier,
...(iconType ? { icon: iconType } : null),
...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null),
}
}),
)
export function getDropdownItemsForAllEditors(application: WebApplication): EditorOption[] {
const plaintextOption: EditorOption = {
icon: PlainEditorMetadata.icon,
iconClassName: PlainEditorMetadata.iconClassName,
label: PlainEditorMetadata.name,
value: FeatureIdentifier.PlainEditor,
}
options.push(
...application.componentManager
.thirdPartyComponentsForArea(ComponentArea.Editor)
.filter((component) => FindNativeFeature(component.identifier) === undefined)
.map((editor): EditorOption => {
const identifier = editor.package_info.identifier
const [iconType, tint] = getIconAndTintForNoteType(editor.noteType)
const options = application.componentManager.componentsForArea(ComponentArea.Editor).map((editor): EditorOption => {
const identifier = editor.package_info.identifier
const [iconType, tint] = getIconAndTintForNoteType(editor.package_info.note_type)
return {
label: editor.displayName,
value: identifier,
...(iconType ? { icon: iconType } : null),
...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null),
}
})
options.push(plaintextOption)
if (application.features.getFeatureStatus(FeatureIdentifier.SuperEditor) === FeatureStatus.Entitled) {
options.push({
icon: SuperEditorMetadata.icon,
iconClassName: SuperEditorMetadata.iconClassName,
label: SuperEditorMetadata.name,
value: FeatureIdentifier.SuperEditor,
isLabs: true,
})
}
return {
label: editor.displayName,
value: identifier,
...(iconType ? { icon: iconType } : null),
...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null),
}
}),
)
options.sort((a, b) => {
return a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1

View File

@@ -1,4 +1,4 @@
import { PlainEditorMetadata, SuperEditorMetadata } from '@/Constants/Constants'
import { SuperEditorMetadata } from '@/Constants/Constants'
import { NoteType } from '@standardnotes/features'
import { IconType } from '@standardnotes/models'
@@ -6,7 +6,7 @@ export function getIconAndTintForNoteType(noteType?: NoteType, subtle?: boolean)
switch (noteType) {
case undefined:
case NoteType.Plain:
return [PlainEditorMetadata.icon, PlainEditorMetadata.iconTintNumber]
return ['plain-text', 1]
case NoteType.RichText:
return ['rich-text', 1]
case NoteType.Markdown:
@@ -26,6 +26,6 @@ export function getIconAndTintForNoteType(noteType?: NoteType, subtle?: boolean)
]
case NoteType.Unknown:
default:
return ['editor', PlainEditorMetadata.iconTintNumber]
return ['editor', 1]
}
}

View File

@@ -1,12 +1,12 @@
import { IconType, FileItem, SNNote, DecryptedItem, SNTag } from '@standardnotes/snjs'
import { IconType, FileItem, SNNote, SNTag, DecryptedItemInterface } from '@standardnotes/snjs'
import { getIconAndTintForNoteType } from './getIconAndTintForNoteType'
import { getIconForFileType } from './getIconForFileType'
import { WebApplicationInterface } from '@standardnotes/ui-services'
export function getIconForItem(item: DecryptedItem, application: WebApplicationInterface): [IconType, string] {
export function getIconForItem(item: DecryptedItemInterface, application: WebApplicationInterface): [IconType, string] {
if (item instanceof SNNote) {
const editorForNote = application.componentManager.editorForNote(item)
const [icon, tint] = getIconAndTintForNoteType(editorForNote?.package_info.note_type)
const [icon, tint] = getIconAndTintForNoteType(editorForNote.noteType)
const className = `text-accessory-tint-${tint}`
return [icon, className]
} else if (item instanceof FileItem) {

View File

@@ -1,6 +1,7 @@
import { Environment, SNApplication } from '@standardnotes/snjs'
import { Environment } from '@standardnotes/snjs'
import { WebApplicationInterface } from '@standardnotes/ui-services'
export async function openSubscriptionDashboard(application: SNApplication) {
export async function openSubscriptionDashboard(application: WebApplicationInterface) {
const token = await application.getNewSubscriptionToken()
if (!token) {
return

View File

@@ -1,14 +1,9 @@
import { WebApplication } from '@/Application/WebApplication'
import { HeadlessSuperConverter } from '@/Components/SuperEditor/Tools/HeadlessSuperConverter'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { NoteType, PrefKey, SNNote } from '@standardnotes/snjs'
import { NoteType, PrefKey, SNNote, PrefDefaults } from '@standardnotes/snjs'
import { WebApplicationInterface } from '@standardnotes/ui-services'
export const getNoteFormat = (application: WebApplication, note: SNNote) => {
const editor = application.componentManager.editorForNote(note)
const isSuperNote = note.noteType === NoteType.Super
if (isSuperNote) {
export const getNoteFormat = (application: WebApplicationInterface, note: SNNote) => {
if (note.noteType === NoteType.Super) {
const superNoteExportFormatPref = application.getPreference(
PrefKey.SuperNoteExportFormat,
PrefDefaults[PrefKey.SuperNoteExportFormat],
@@ -17,15 +12,16 @@ export const getNoteFormat = (application: WebApplication, note: SNNote) => {
return superNoteExportFormatPref
}
return editor?.package_info?.file_type || 'txt'
const editor = application.componentManager.editorForNote(note)
return editor.fileType
}
export const getNoteFileName = (application: WebApplication, note: SNNote): string => {
export const getNoteFileName = (application: WebApplicationInterface, note: SNNote): string => {
const format = getNoteFormat(application, note)
return `${note.title}.${format}`
}
export const getNoteBlob = (application: WebApplication, note: SNNote) => {
export const getNoteBlob = (application: WebApplicationInterface, note: SNNote) => {
const format = getNoteFormat(application, note)
let type: string
switch (format) {

View File

@@ -1,11 +1,14 @@
import { ThemeItem } from '@/Components/QuickSettingsMenu/ThemeItem'
import { FeatureIdentifier } from '@standardnotes/snjs'
import { ComponentOrNativeFeature, FeatureIdentifier, ThemeFeatureDescription } from '@standardnotes/snjs'
const isDarkModeTheme = (theme: ThemeItem) => theme.identifier === FeatureIdentifier.DarkTheme
const isDarkModeTheme = (theme: ComponentOrNativeFeature<ThemeFeatureDescription>) =>
theme.featureIdentifier === FeatureIdentifier.DarkTheme
export const sortThemes = (a: ThemeItem, b: ThemeItem) => {
const aIsLayerable = a.component?.isLayerable()
const bIsLayerable = b.component?.isLayerable()
export const sortThemes = (
a: ComponentOrNativeFeature<ThemeFeatureDescription>,
b: ComponentOrNativeFeature<ThemeFeatureDescription>,
) => {
const aIsLayerable = a.layerable
const bIsLayerable = b.layerable
if (aIsLayerable && !bIsLayerable) {
return 1
@@ -14,6 +17,6 @@ export const sortThemes = (a: ThemeItem, b: ThemeItem) => {
} else if (!isDarkModeTheme(a) && isDarkModeTheme(b)) {
return 1
} else {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1
return a.displayName.toLowerCase() < b.displayName.toLowerCase() ? -1 : 1
}
}

View File

@@ -1,79 +1,66 @@
import { WebApplication } from '@/Application/WebApplication'
import {
ContentType,
FeatureStatus,
SNComponent,
ComponentArea,
FeatureDescription,
GetFeatures,
FindNativeFeature,
NoteType,
FeatureIdentifier,
GetIframeAndNativeEditors,
ComponentArea,
GetSuperNoteFeature,
ComponentOrNativeFeature,
IframeComponentFeatureDescription,
} from '@standardnotes/snjs'
import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
import { PlainEditorMetadata, SuperEditorMetadata } from '@/Constants/Constants'
import { SuperEditorMetadata } from '@/Constants/Constants'
import { WebApplicationInterface } from '@standardnotes/ui-services'
type NoteTypeToEditorRowsMap = Record<NoteType, EditorMenuItem[]>
const getNoteTypeForFeatureDescription = (featureDescription: FeatureDescription): NoteType => {
if (featureDescription.note_type) {
return featureDescription.note_type
} else if (featureDescription.file_type) {
switch (featureDescription.file_type) {
case 'html':
return NoteType.RichText
case 'md':
return NoteType.Markdown
const insertNativeEditorsInMap = (map: NoteTypeToEditorRowsMap, application: WebApplicationInterface): void => {
for (const editorFeature of GetIframeAndNativeEditors()) {
const isExperimental = application.features.isExperimentalFeature(editorFeature.identifier)
if (isExperimental) {
continue
}
}
return NoteType.Unknown
}
const insertNonInstalledNativeComponentsInMap = (
map: NoteTypeToEditorRowsMap,
components: SNComponent[],
application: WebApplication,
): void => {
GetFeatures()
.filter((feature) => feature.content_type === ContentType.TYPES.Component && feature.area === ComponentArea.Editor)
.forEach((editorFeature) => {
const notInstalled = !components.find((editor) => editor.identifier === editorFeature.identifier)
const isExperimental = application.features.isExperimentalFeature(editorFeature.identifier)
const isDeprecated = editorFeature.deprecated
const isShowable = notInstalled && !isExperimental && !isDeprecated
const isDeprecated = editorFeature.deprecated
if (isDeprecated) {
continue
}
if (isShowable) {
const noteType = getNoteTypeForFeatureDescription(editorFeature)
map[noteType].push({
name: editorFeature.name as string,
isEntitled: false,
noteType,
})
}
const noteType = editorFeature.note_type
map[noteType].push({
isEntitled: application.features.getFeatureStatus(editorFeature.identifier) === FeatureStatus.Entitled,
uiFeature: new ComponentOrNativeFeature(editorFeature),
})
}
}
const insertInstalledComponentsInMap = (
map: NoteTypeToEditorRowsMap,
components: SNComponent[],
application: WebApplication,
) => {
components.forEach((editor) => {
const noteType = getNoteTypeForFeatureDescription(editor.package_info)
const insertInstalledComponentsInMap = (map: NoteTypeToEditorRowsMap, application: WebApplicationInterface) => {
const thirdPartyOrInstalledEditors = application.componentManager
.thirdPartyComponentsForArea(ComponentArea.Editor)
.sort((a, b) => {
return a.displayName.toLowerCase() < b.displayName.toLowerCase() ? -1 : 1
})
for (const editor of thirdPartyOrInstalledEditors) {
const nativeFeature = FindNativeFeature(editor.identifier)
if (nativeFeature && !nativeFeature.deprecated) {
continue
}
const noteType = editor.noteType
const editorItem: EditorMenuItem = {
name: editor.displayName,
component: editor,
uiFeature: new ComponentOrNativeFeature<IframeComponentFeatureDescription>(editor),
isEntitled: application.features.getFeatureStatus(editor.identifier) === FeatureStatus.Entitled,
noteType,
}
map[noteType].push(editorItem)
})
}
}
const createGroupsFromMap = (map: NoteTypeToEditorRowsMap, _application: WebApplication): EditorMenuGroup[] => {
const createGroupsFromMap = (map: NoteTypeToEditorRowsMap): EditorMenuGroup[] => {
const superNote = GetSuperNoteFeature()
const groups: EditorMenuGroup[] = [
{
icon: 'plain-text',
@@ -84,7 +71,7 @@ const createGroupsFromMap = (map: NoteTypeToEditorRowsMap, _application: WebAppl
{
icon: SuperEditorMetadata.icon,
iconClassName: SuperEditorMetadata.iconClassName,
title: SuperEditorMetadata.name,
title: superNote.name,
items: map[NoteType.Super],
featured: true,
},
@@ -135,23 +122,10 @@ const createGroupsFromMap = (map: NoteTypeToEditorRowsMap, _application: WebAppl
return groups
}
const createBaselineMap = (application: WebApplication): NoteTypeToEditorRowsMap => {
const createBaselineMap = (): NoteTypeToEditorRowsMap => {
const map: NoteTypeToEditorRowsMap = {
[NoteType.Plain]: [
{
name: PlainEditorMetadata.name,
isEntitled: true,
noteType: NoteType.Plain,
},
],
[NoteType.Super]: [
{
name: SuperEditorMetadata.name,
isEntitled: application.features.getFeatureStatus(FeatureIdentifier.SuperEditor) === FeatureStatus.Entitled,
noteType: NoteType.Super,
description: FindNativeFeature(FeatureIdentifier.SuperEditor)?.description,
},
],
[NoteType.Plain]: [],
[NoteType.Super]: [],
[NoteType.RichText]: [],
[NoteType.Markdown]: [],
[NoteType.Task]: [],
@@ -164,12 +138,12 @@ const createBaselineMap = (application: WebApplication): NoteTypeToEditorRowsMap
return map
}
export const createEditorMenuGroups = (application: WebApplication, components: SNComponent[]) => {
const map = createBaselineMap(application)
export const createEditorMenuGroups = (application: WebApplicationInterface): EditorMenuGroup[] => {
const map = createBaselineMap()
insertNonInstalledNativeComponentsInMap(map, components, application)
insertNativeEditorsInMap(map, application)
insertInstalledComponentsInMap(map, components, application)
insertInstalledComponentsInMap(map, application)
return createGroupsFromMap(map, application)
return createGroupsFromMap(map)
}