refactor: native feature management (#2350)
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export abstract class AbstractComponent<P = PureComponentProps, S = PureComponen
|
||||
}
|
||||
|
||||
public get viewControllerManager(): ViewControllerManager {
|
||||
return this.application.getViewControllerManager()
|
||||
return this.application.controllers
|
||||
}
|
||||
|
||||
autorun(view: (r: IReactionPublic) => void): void {
|
||||
|
||||
@@ -47,7 +47,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
const currentWriteErrorDialog = useRef<Promise<void> | null>(null)
|
||||
const currentLoadErrorDialog = useRef<Promise<void> | null>(null)
|
||||
|
||||
const viewControllerManager = application.getViewControllerManager()
|
||||
const viewControllerManager = application.controllers
|
||||
|
||||
useEffect(() => {
|
||||
const desktopService = application.getDesktopService()
|
||||
@@ -198,7 +198,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
<ApplicationProvider application={application}>
|
||||
<CommandProvider service={application.keyboardService}>
|
||||
<AndroidBackHandlerProvider application={application}>
|
||||
<ResponsivePaneProvider paneController={application.getViewControllerManager().paneController}>
|
||||
<ResponsivePaneProvider paneController={application.controllers.paneController}>
|
||||
<PremiumModalProvider
|
||||
application={application}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
@@ -230,7 +230,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
<ApplicationProvider application={application}>
|
||||
<CommandProvider service={application.keyboardService}>
|
||||
<AndroidBackHandlerProvider application={application}>
|
||||
<ResponsivePaneProvider paneController={application.getViewControllerManager().paneController}>
|
||||
<ResponsivePaneProvider paneController={application.controllers.paneController}>
|
||||
<PremiumModalProvider
|
||||
application={application}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
@@ -252,7 +252,6 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
application={application}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
subscriptionController={viewControllerManager.subscriptionController}
|
||||
/>
|
||||
</>
|
||||
{renderChallenges()}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintFor
|
||||
import { CHANGE_EDITOR_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { NoteViewController } from '../NoteView/Controller/NoteViewController'
|
||||
import { noteTypeForEditorIdentifier } from '@standardnotes/snjs'
|
||||
import { NoteType, noteTypeForEditorIdentifier } from '@standardnotes/snjs'
|
||||
|
||||
type Props = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
@@ -30,6 +30,7 @@ const ChangeEditorButton: FunctionComponent<Props> = ({
|
||||
const [selectedEditor, setSelectedEditor] = useState(() => {
|
||||
return note ? application.componentManager.editorForNote(note) : undefined
|
||||
})
|
||||
|
||||
const noteType = noteViewController?.isTemplateNote
|
||||
? noteTypeForEditorIdentifier(
|
||||
application.geDefaultEditorIdentifier(
|
||||
@@ -38,9 +39,12 @@ const ChangeEditorButton: FunctionComponent<Props> = ({
|
||||
: undefined,
|
||||
),
|
||||
)
|
||||
: note
|
||||
: note && note.noteType != NoteType.Unknown
|
||||
? note.noteType
|
||||
: selectedEditor?.package_info.note_type
|
||||
: selectedEditor
|
||||
? selectedEditor.noteType
|
||||
: NoteType.Unknown
|
||||
|
||||
const [selectedEditorIcon, selectedEditorIconTint] = getIconAndTintForNoteType(noteType, true)
|
||||
const [isClickOutsideDisabled, setIsClickOutsideDisabled] = useState(false)
|
||||
|
||||
|
||||
@@ -3,7 +3,16 @@ import Menu from '@/Components/Menu/Menu'
|
||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
import { STRING_EDIT_LOCKED_ATTEMPT } from '@/Constants/Strings'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { ComponentArea, NoteMutator, NoteType, PrefKey, SNComponent, SNNote } from '@standardnotes/snjs'
|
||||
import {
|
||||
ComponentOrNativeFeature,
|
||||
EditorFeatureDescription,
|
||||
FeatureIdentifier,
|
||||
IframeComponentFeatureDescription,
|
||||
NoteMutator,
|
||||
NoteType,
|
||||
PrefKey,
|
||||
SNNote,
|
||||
} from '@standardnotes/snjs'
|
||||
import { Fragment, FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
|
||||
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
|
||||
@@ -21,7 +30,7 @@ type ChangeEditorMenuProps = {
|
||||
closeMenu: () => void
|
||||
isVisible: boolean
|
||||
note: SNNote | undefined
|
||||
onSelect?: (component: SNComponent | undefined) => void
|
||||
onSelect?: (component: ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>) => void
|
||||
setDisableClickOutside?: (value: boolean) => void
|
||||
}
|
||||
|
||||
@@ -35,25 +44,23 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
onSelect,
|
||||
setDisableClickOutside,
|
||||
}) => {
|
||||
const editors = useMemo(
|
||||
() =>
|
||||
application.componentManager.componentsForArea(ComponentArea.Editor).sort((a, b) => {
|
||||
return a.displayName.toLowerCase() < b.displayName.toLowerCase() ? -1 : 1
|
||||
}),
|
||||
[application.componentManager],
|
||||
)
|
||||
const groups = useMemo(() => createEditorMenuGroups(application, editors), [application, editors])
|
||||
const [currentComponent, setCurrentComponent] = useState<SNComponent>()
|
||||
const groups = useMemo(() => createEditorMenuGroups(application), [application])
|
||||
const [currentFeature, setCurrentFeature] =
|
||||
useState<ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>>()
|
||||
const [pendingConversionItem, setPendingConversionItem] = useState<EditorMenuItem | null>(null)
|
||||
|
||||
const showSuperNoteImporter =
|
||||
!!pendingConversionItem && note?.noteType !== NoteType.Super && pendingConversionItem.noteType === NoteType.Super
|
||||
!!pendingConversionItem &&
|
||||
note?.noteType !== NoteType.Super &&
|
||||
pendingConversionItem.uiFeature.noteType === NoteType.Super
|
||||
const showSuperNoteConverter =
|
||||
!!pendingConversionItem && note?.noteType === NoteType.Super && pendingConversionItem.noteType !== NoteType.Super
|
||||
!!pendingConversionItem &&
|
||||
note?.noteType === NoteType.Super &&
|
||||
pendingConversionItem.uiFeature.noteType !== NoteType.Super
|
||||
|
||||
useEffect(() => {
|
||||
if (note) {
|
||||
setCurrentComponent(application.componentManager.editorForNote(note))
|
||||
setCurrentFeature(application.componentManager.editorForNote(note))
|
||||
}
|
||||
}, [application, note])
|
||||
|
||||
@@ -61,61 +68,52 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
|
||||
const isSelected = useCallback(
|
||||
(item: EditorMenuItem) => {
|
||||
if (currentComponent) {
|
||||
return item.component?.identifier === currentComponent.identifier
|
||||
if (currentFeature) {
|
||||
return item.uiFeature.featureIdentifier === currentFeature.featureIdentifier
|
||||
}
|
||||
|
||||
const itemNoteTypeIsSameAsCurrentNoteType = item.noteType === note?.noteType
|
||||
const noteDoesntHaveTypeAndItemIsPlain = !note?.noteType && item.noteType === NoteType.Plain
|
||||
const unknownNoteTypeAndItemIsPlain = note?.noteType === NoteType.Unknown && item.noteType === NoteType.Plain
|
||||
const itemNoteTypeIsSameAsCurrentNoteType = item.uiFeature.noteType === note?.noteType
|
||||
const noteDoesntHaveTypeAndItemIsPlain = !note?.noteType && item.uiFeature.noteType === NoteType.Plain
|
||||
const unknownNoteTypeAndItemIsPlain =
|
||||
note?.noteType === NoteType.Unknown && item.uiFeature.noteType === NoteType.Plain
|
||||
|
||||
return itemNoteTypeIsSameAsCurrentNoteType || noteDoesntHaveTypeAndItemIsPlain || unknownNoteTypeAndItemIsPlain
|
||||
},
|
||||
[currentComponent, note],
|
||||
[currentFeature, note],
|
||||
)
|
||||
|
||||
const selectComponent = useCallback(
|
||||
async (component: SNComponent, note: SNNote) => {
|
||||
if (component.conflictOf) {
|
||||
void application.changeAndSaveItem(component, (mutator) => {
|
||||
async (
|
||||
uiFeature: ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>,
|
||||
note: SNNote,
|
||||
) => {
|
||||
if (uiFeature.isComponent && uiFeature.asComponent.conflictOf) {
|
||||
void application.changeAndSaveItem(uiFeature.asComponent, (mutator) => {
|
||||
mutator.conflictOf = undefined
|
||||
})
|
||||
}
|
||||
|
||||
await application.getViewControllerManager().itemListController.insertCurrentIfTemplate()
|
||||
await application.controllers.itemListController.insertCurrentIfTemplate()
|
||||
|
||||
await application.changeAndSaveItem(note, (mutator) => {
|
||||
const noteMutator = mutator as NoteMutator
|
||||
noteMutator.noteType = component.noteType
|
||||
noteMutator.editorIdentifier = component.identifier
|
||||
noteMutator.noteType = uiFeature.noteType
|
||||
noteMutator.editorIdentifier = uiFeature.featureIdentifier
|
||||
})
|
||||
|
||||
setCurrentComponent(application.componentManager.editorForNote(note))
|
||||
setCurrentFeature(application.componentManager.editorForNote(note))
|
||||
|
||||
if (uiFeature.featureIdentifier === FeatureIdentifier.PlainEditor) {
|
||||
reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled))
|
||||
}
|
||||
},
|
||||
[application],
|
||||
)
|
||||
|
||||
const selectNonComponent = useCallback(
|
||||
async (item: EditorMenuItem, note: SNNote) => {
|
||||
await application.getViewControllerManager().itemListController.insertCurrentIfTemplate()
|
||||
|
||||
reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled))
|
||||
|
||||
await application.changeAndSaveItem(note, (mutator) => {
|
||||
const noteMutator = mutator as NoteMutator
|
||||
noteMutator.noteType = item.noteType
|
||||
noteMutator.editorIdentifier = undefined
|
||||
})
|
||||
|
||||
setCurrentComponent(undefined)
|
||||
},
|
||||
[application],
|
||||
)
|
||||
|
||||
const selectItem = useCallback(
|
||||
async (itemToBeSelected: EditorMenuItem) => {
|
||||
if (!itemToBeSelected.isEntitled) {
|
||||
premiumModal.activate(itemToBeSelected.name)
|
||||
const handleMenuSelection = useCallback(
|
||||
async (menuItem: EditorMenuItem) => {
|
||||
if (!menuItem.isEntitled) {
|
||||
premiumModal.activate(menuItem.uiFeature.displayName)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -128,28 +126,28 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
return
|
||||
}
|
||||
|
||||
if (itemToBeSelected.noteType === NoteType.Super) {
|
||||
if (menuItem.uiFeature.noteType === NoteType.Super) {
|
||||
if (note.noteType === NoteType.Super) {
|
||||
return
|
||||
}
|
||||
|
||||
setPendingConversionItem(itemToBeSelected)
|
||||
setPendingConversionItem(menuItem)
|
||||
setDisableClickOutside?.(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (note.noteType === NoteType.Super && note.text.length > 0) {
|
||||
setPendingConversionItem(itemToBeSelected)
|
||||
setPendingConversionItem(menuItem)
|
||||
setDisableClickOutside?.(true)
|
||||
return
|
||||
}
|
||||
|
||||
let shouldMakeSelection = true
|
||||
|
||||
if (itemToBeSelected.component) {
|
||||
if (menuItem.uiFeature) {
|
||||
const changeRequiresAlert = application.componentManager.doesEditorChangeRequireAlert(
|
||||
currentComponent,
|
||||
itemToBeSelected.component,
|
||||
currentFeature,
|
||||
menuItem.uiFeature,
|
||||
)
|
||||
|
||||
if (changeRequiresAlert) {
|
||||
@@ -158,17 +156,13 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
}
|
||||
|
||||
if (shouldMakeSelection) {
|
||||
if (itemToBeSelected.component) {
|
||||
selectComponent(itemToBeSelected.component, note).catch(console.error)
|
||||
} else {
|
||||
selectNonComponent(itemToBeSelected, note).catch(console.error)
|
||||
}
|
||||
selectComponent(menuItem.uiFeature, note).catch(console.error)
|
||||
}
|
||||
|
||||
closeMenu()
|
||||
|
||||
if (onSelect) {
|
||||
onSelect(itemToBeSelected.component)
|
||||
onSelect(menuItem.uiFeature)
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -179,9 +173,8 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
application.alertService,
|
||||
application.componentManager,
|
||||
setDisableClickOutside,
|
||||
currentComponent,
|
||||
currentFeature,
|
||||
selectComponent,
|
||||
selectNonComponent,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -190,15 +183,9 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
return
|
||||
}
|
||||
|
||||
if (pendingConversionItem.component) {
|
||||
selectComponent(pendingConversionItem.component, note).catch(console.error)
|
||||
closeMenu()
|
||||
return
|
||||
}
|
||||
|
||||
selectNonComponent(pendingConversionItem, note).catch(console.error)
|
||||
selectComponent(pendingConversionItem.uiFeature, note).catch(console.error)
|
||||
closeMenu()
|
||||
}, [pendingConversionItem, note, selectNonComponent, closeMenu, selectComponent])
|
||||
}, [pendingConversionItem, note, closeMenu, selectComponent])
|
||||
|
||||
const closeSuperNoteImporter = () => {
|
||||
setPendingConversionItem(null)
|
||||
@@ -220,30 +207,30 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
return (
|
||||
<Fragment key={groupId}>
|
||||
<div className={`border-0 border-t border-solid border-border py-1 ${index === 0 ? 'border-t-0' : ''}`}>
|
||||
{group.items.map((item) => {
|
||||
{group.items.map((menuItem) => {
|
||||
const onClickEditorItem = () => {
|
||||
selectItem(item).catch(console.error)
|
||||
handleMenuSelection(menuItem).catch(console.error)
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuRadioButtonItem
|
||||
key={item.name}
|
||||
key={menuItem.uiFeature.uniqueIdentifier}
|
||||
onClick={onClickEditorItem}
|
||||
className={'flex-row-reversed py-2'}
|
||||
checked={item.isEntitled ? isSelected(item) : false}
|
||||
info={item.description}
|
||||
checked={isSelected(menuItem)}
|
||||
info={menuItem.uiFeature.description}
|
||||
>
|
||||
<div className="flex flex-grow items-center justify-between">
|
||||
<div className={`flex items-center ${group.featured ? 'font-bold' : ''}`}>
|
||||
{group.icon && <Icon type={group.icon} className={`mr-2 ${group.iconClassName}`} />}
|
||||
{item.name}
|
||||
{item.isLabs && (
|
||||
{menuItem.uiFeature.displayName}
|
||||
{menuItem.isLabs && (
|
||||
<Pill className="py-0.5 px-1.5" style="success">
|
||||
Labs
|
||||
</Pill>
|
||||
)}
|
||||
</div>
|
||||
{!item.isEntitled && (
|
||||
{!menuItem.isEntitled && (
|
||||
<Icon type={PremiumFeatureIconName} className={PremiumFeatureIconClass} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,14 @@ import { WebApplication } from '@/Application/WebApplication'
|
||||
import { STRING_EDIT_LOCKED_ATTEMPT } from '@/Constants/Strings'
|
||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
import { createEditorMenuGroups } from '@/Utils/createEditorMenuGroups'
|
||||
import { ComponentArea, NoteMutator, NoteType, SNComponent, SNNote } from '@standardnotes/snjs'
|
||||
import {
|
||||
ComponentOrNativeFeature,
|
||||
EditorFeatureDescription,
|
||||
IframeComponentFeatureDescription,
|
||||
NoteMutator,
|
||||
NoteType,
|
||||
SNNote,
|
||||
} from '@standardnotes/snjs'
|
||||
import { Fragment, useCallback, useMemo, useState } from 'react'
|
||||
import Icon from '../Icon/Icon'
|
||||
import { PremiumFeatureIconName, PremiumFeatureIconClass } from '../Icon/PremiumFeatureIcon'
|
||||
@@ -22,7 +29,7 @@ type Props = {
|
||||
setDisableClickOutside: (value: boolean) => void
|
||||
}
|
||||
|
||||
const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Props) => {
|
||||
const ChangeEditorMultipleMenu = ({ application, notes, setDisableClickOutside }: Props) => {
|
||||
const premiumModal = usePremiumModal()
|
||||
|
||||
const [itemToBeSelected, setItemToBeSelected] = useState<EditorMenuItem | undefined>()
|
||||
@@ -30,47 +37,32 @@ const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Prop
|
||||
|
||||
const hasSelectedLockedNotes = useMemo(() => notes.some((note) => note.locked), [notes])
|
||||
|
||||
const editors = useMemo(
|
||||
() =>
|
||||
application.componentManager.componentsForArea(ComponentArea.Editor).sort((a, b) => {
|
||||
return a.displayName.toLowerCase() < b.displayName.toLowerCase() ? -1 : 1
|
||||
}),
|
||||
[application.componentManager],
|
||||
)
|
||||
const groups = useMemo(() => createEditorMenuGroups(application, editors), [application, editors])
|
||||
const groups = useMemo(() => createEditorMenuGroups(application), [application])
|
||||
|
||||
const selectComponent = useCallback(
|
||||
async (component: SNComponent, note: SNNote) => {
|
||||
if (component.conflictOf) {
|
||||
void application.changeAndSaveItem(component, (mutator) => {
|
||||
async (
|
||||
uiFeature: ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>,
|
||||
note: SNNote,
|
||||
) => {
|
||||
if (uiFeature.isComponent && uiFeature.asComponent.conflictOf) {
|
||||
void application.changeAndSaveItem(uiFeature.asComponent, (mutator) => {
|
||||
mutator.conflictOf = undefined
|
||||
})
|
||||
}
|
||||
|
||||
await application.changeAndSaveItem(note, (mutator) => {
|
||||
const noteMutator = mutator as NoteMutator
|
||||
noteMutator.noteType = component.noteType
|
||||
noteMutator.editorIdentifier = component.identifier
|
||||
noteMutator.noteType = uiFeature.noteType
|
||||
noteMutator.editorIdentifier = uiFeature.featureIdentifier
|
||||
})
|
||||
},
|
||||
[application],
|
||||
)
|
||||
|
||||
const selectNonComponent = useCallback(
|
||||
async (item: EditorMenuItem, note: SNNote) => {
|
||||
await application.changeAndSaveItem(note, (mutator) => {
|
||||
const noteMutator = mutator as NoteMutator
|
||||
noteMutator.noteType = item.noteType
|
||||
noteMutator.editorIdentifier = undefined
|
||||
})
|
||||
},
|
||||
[application],
|
||||
)
|
||||
|
||||
const selectItem = useCallback(
|
||||
const handleMenuSelection = useCallback(
|
||||
async (itemToBeSelected: EditorMenuItem) => {
|
||||
if (!itemToBeSelected.isEntitled) {
|
||||
premiumModal.activate(itemToBeSelected.name)
|
||||
premiumModal.activate(itemToBeSelected.uiFeature.displayName)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -79,35 +71,27 @@ const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Prop
|
||||
return
|
||||
}
|
||||
|
||||
if (itemToBeSelected.noteType === NoteType.Super) {
|
||||
if (itemToBeSelected.uiFeature.noteType === NoteType.Super) {
|
||||
setDisableClickOutside(true)
|
||||
setItemToBeSelected(itemToBeSelected)
|
||||
setConfirmationQueue(notes)
|
||||
return
|
||||
}
|
||||
|
||||
if (itemToBeSelected.component) {
|
||||
const changeRequiresConfirmation = notes.some((note) => {
|
||||
const editorForNote = application.componentManager.editorForNote(note)
|
||||
return application.componentManager.doesEditorChangeRequireAlert(editorForNote, itemToBeSelected.component)
|
||||
})
|
||||
const changeRequiresConfirmation = notes.some((note) => {
|
||||
const editorForNote = application.componentManager.editorForNote(note)
|
||||
return application.componentManager.doesEditorChangeRequireAlert(editorForNote, itemToBeSelected.uiFeature)
|
||||
})
|
||||
|
||||
if (changeRequiresConfirmation) {
|
||||
const canChange = await application.componentManager.showEditorChangeAlert()
|
||||
if (!canChange) {
|
||||
return
|
||||
}
|
||||
if (changeRequiresConfirmation) {
|
||||
const canChange = await application.componentManager.showEditorChangeAlert()
|
||||
if (!canChange) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const note of notes) {
|
||||
void selectComponent(itemToBeSelected.component, note)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for (const note of notes) {
|
||||
void selectNonComponent(itemToBeSelected, note)
|
||||
void selectComponent(itemToBeSelected.uiFeature, note)
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -117,14 +101,13 @@ const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Prop
|
||||
notes,
|
||||
premiumModal,
|
||||
selectComponent,
|
||||
selectNonComponent,
|
||||
setDisableClickOutside,
|
||||
],
|
||||
)
|
||||
|
||||
const groupsWithItems = groups.filter((group) => group.items && group.items.length)
|
||||
|
||||
const showSuperImporter = itemToBeSelected?.noteType === NoteType.Super && confirmationQueue.length > 0
|
||||
const showSuperImporter = itemToBeSelected?.uiFeature.noteType === NoteType.Super && confirmationQueue.length > 0
|
||||
|
||||
const closeCurrentSuperNoteImporter = useCallback(() => {
|
||||
const remainingNotes = confirmationQueue.slice(1)
|
||||
@@ -144,10 +127,10 @@ const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Prop
|
||||
return
|
||||
}
|
||||
|
||||
void selectNonComponent(itemToBeSelected, confirmationQueue[0])
|
||||
void selectComponent(itemToBeSelected.uiFeature, confirmationQueue[0])
|
||||
|
||||
closeCurrentSuperNoteImporter()
|
||||
}, [closeCurrentSuperNoteImporter, confirmationQueue, itemToBeSelected, selectNonComponent])
|
||||
}, [closeCurrentSuperNoteImporter, confirmationQueue, itemToBeSelected, selectComponent])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -157,14 +140,18 @@ const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Prop
|
||||
<div className={`border-0 border-t border-solid border-border py-1 ${index === 0 ? 'border-t-0' : ''}`}>
|
||||
{group.items.map((item) => {
|
||||
const onClickEditorItem = () => {
|
||||
selectItem(item).catch(console.error)
|
||||
handleMenuSelection(item).catch(console.error)
|
||||
}
|
||||
return (
|
||||
<MenuItem key={item.name} onClick={onClickEditorItem} className={'flex-row-reversed py-2'}>
|
||||
<MenuItem
|
||||
key={item.uiFeature.uniqueIdentifier}
|
||||
onClick={onClickEditorItem}
|
||||
className={'flex-row-reversed py-2'}
|
||||
>
|
||||
<div className="flex flex-grow items-center justify-between">
|
||||
<div className={'flex items-center'}>
|
||||
{group.icon && <Icon type={group.icon} className={`mr-2 ${group.iconClassName}`} />}
|
||||
{item.name}
|
||||
{item.uiFeature.displayName}
|
||||
{item.isLabs && (
|
||||
<Pill className="py-0.5 px-1.5" style="success">
|
||||
Labs
|
||||
@@ -194,4 +181,4 @@ const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Prop
|
||||
)
|
||||
}
|
||||
|
||||
export default ChangeMultipleMenu
|
||||
export default ChangeEditorMultipleMenu
|
||||
@@ -3,7 +3,7 @@ import { NotesController } from '@/Controllers/NotesController/NotesController'
|
||||
import { useRef, useState } from 'react'
|
||||
import RoundIconButton from '../Button/RoundIconButton'
|
||||
import Popover from '../Popover/Popover'
|
||||
import ChangeMultipleMenu from './ChangeMultipleMenu'
|
||||
import ChangeEditorMultipleMenu from './ChangeEditorMultipleMenu'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -27,7 +27,7 @@ const ChangeMultipleButton = ({ application, notesController }: Props) => {
|
||||
open={isChangeMenuOpen}
|
||||
className="pt-2 md:pt-0"
|
||||
>
|
||||
<ChangeMultipleMenu
|
||||
<ChangeEditorMultipleMenu
|
||||
application={application}
|
||||
notes={notesController.selectedNotes}
|
||||
setDisableClickOutside={setDisableClickOutside}
|
||||
|
||||
@@ -64,7 +64,7 @@ const ClipperView = ({
|
||||
() => application.features.getFeatureStatus(FeatureIdentifier.Extension) === FeatureStatus.Entitled,
|
||||
)
|
||||
const isEntitledRef = useStateRef(isEntitledToExtension)
|
||||
const hasSubscription = application.hasValidSubscription()
|
||||
const hasSubscription = application.hasValidFirstPartySubscription()
|
||||
useEffect(() => {
|
||||
return application.addEventObserver(async (event) => {
|
||||
switch (event) {
|
||||
@@ -74,7 +74,7 @@ const ClipperView = ({
|
||||
setUser(application.getUser())
|
||||
setIsEntitled(application.features.getFeatureStatus(FeatureIdentifier.Extension) === FeatureStatus.Entitled)
|
||||
break
|
||||
case ApplicationEvent.FeaturesUpdated:
|
||||
case ApplicationEvent.FeaturesAvailabilityChanged:
|
||||
setIsEntitled(application.features.getFeatureStatus(FeatureIdentifier.Extension) === FeatureStatus.Entitled)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
import {
|
||||
ComponentAction,
|
||||
FeatureStatus,
|
||||
SNComponent,
|
||||
dateToLocalizedString,
|
||||
ComponentViewerInterface,
|
||||
ComponentViewerEvent,
|
||||
ComponentViewerError,
|
||||
ComponentInterface,
|
||||
SubscriptionManagerEvent,
|
||||
} from '@standardnotes/snjs'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import OfflineRestricted from '@/Components/ComponentView/OfflineRestricted'
|
||||
import UrlMissing from '@/Components/ComponentView/UrlMissing'
|
||||
import IsDeprecated from '@/Components/ComponentView/IsDeprecated'
|
||||
import IsExpired from '@/Components/ComponentView/IsExpired'
|
||||
import NotEntitledBanner from '@/Components/ComponentView/NotEntitledBanner'
|
||||
import IssueOnLoading from '@/Components/ComponentView/IssueOnLoading'
|
||||
import { openSubscriptionDashboard } from '@/Utils/ManageSubscription'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
|
||||
interface IProps {
|
||||
application: WebApplication
|
||||
interface Props {
|
||||
componentViewer: ComponentViewerInterface
|
||||
requestReload?: (viewer: ComponentViewerInterface, force?: boolean) => void
|
||||
onLoad?: (component: SNComponent) => void
|
||||
onLoad?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,7 +30,9 @@ const MaxLoadThreshold = 4000
|
||||
const VisibilityChangeKey = 'visibilitychange'
|
||||
const MSToWaitAfterIframeLoadToAvoidFlicker = 35
|
||||
|
||||
const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, componentViewer, requestReload }) => {
|
||||
const IframeFeatureView: FunctionComponent<Props> = ({ onLoad, componentViewer, requestReload }) => {
|
||||
const application = useApplication()
|
||||
|
||||
const iframeRef = useRef<HTMLIFrameElement | null>(null)
|
||||
const [loadTimeout, setLoadTimeout] = useState<ReturnType<typeof setTimeout> | undefined>(undefined)
|
||||
|
||||
@@ -45,11 +45,7 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
|
||||
const [isDeprecationMessageDismissed, setIsDeprecationMessageDismissed] = useState(false)
|
||||
const [didAttemptReload, setDidAttemptReload] = useState(false)
|
||||
|
||||
const component: SNComponent = componentViewer.component
|
||||
|
||||
const manageSubscription = useCallback(() => {
|
||||
void openSubscriptionDashboard(application)
|
||||
}, [application])
|
||||
const uiFeature = componentViewer.getComponentOrFeatureItem()
|
||||
|
||||
const reloadValidityStatus = useCallback(() => {
|
||||
setFeatureStatus(componentViewer.getFeatureStatus())
|
||||
@@ -63,13 +59,21 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
|
||||
}
|
||||
|
||||
setError(componentViewer.getError())
|
||||
setDeprecationMessage(component.deprecationMessage)
|
||||
}, [componentViewer, component.deprecationMessage, featureStatus, isComponentValid, isLoading])
|
||||
setDeprecationMessage(uiFeature.deprecationMessage)
|
||||
}, [componentViewer, uiFeature, featureStatus, isComponentValid, isLoading])
|
||||
|
||||
useEffect(() => {
|
||||
reloadValidityStatus()
|
||||
}, [reloadValidityStatus])
|
||||
|
||||
useEffect(() => {
|
||||
return application.subscriptions.addEventObserver((event) => {
|
||||
if (event === SubscriptionManagerEvent.DidFetchSubscription) {
|
||||
reloadValidityStatus()
|
||||
}
|
||||
})
|
||||
}, [application.subscriptions, reloadValidityStatus])
|
||||
|
||||
const dismissDeprecationMessage = () => {
|
||||
setIsDeprecationMessageDismissed(true)
|
||||
}
|
||||
@@ -123,9 +127,9 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
setHasIssueLoading(false)
|
||||
onLoad?.(component)
|
||||
onLoad?.()
|
||||
}, MSToWaitAfterIframeLoadToAvoidFlicker)
|
||||
}, [componentViewer, onLoad, component, loadTimeout])
|
||||
}, [componentViewer, onLoad, loadTimeout])
|
||||
|
||||
useEffect(() => {
|
||||
const removeFeaturesChangedObserver = componentViewer.addEventObserver((event) => {
|
||||
@@ -149,7 +153,7 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
|
||||
application.keyboardService.handleComponentKeyUp(data.keyboardModifier)
|
||||
break
|
||||
case ComponentAction.Click:
|
||||
application.getViewControllerManager().notesController.setContextMenuOpen(false)
|
||||
application.controllers.notesController.setContextMenuOpen(false)
|
||||
break
|
||||
default:
|
||||
return
|
||||
@@ -163,8 +167,8 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
|
||||
useEffect(() => {
|
||||
const unregisterDesktopObserver = application
|
||||
.getDesktopService()
|
||||
?.registerUpdateObserver((updatedComponent: SNComponent) => {
|
||||
if (updatedComponent.uuid === component.uuid && updatedComponent.active) {
|
||||
?.registerUpdateObserver((updatedComponent: ComponentInterface) => {
|
||||
if (updatedComponent.uuid === uiFeature.uniqueIdentifier) {
|
||||
requestReload?.(componentViewer)
|
||||
}
|
||||
})
|
||||
@@ -172,13 +176,13 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
|
||||
return () => {
|
||||
unregisterDesktopObserver?.()
|
||||
}
|
||||
}, [application, requestReload, componentViewer, component.uuid])
|
||||
}, [application, requestReload, componentViewer, uiFeature])
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasIssueLoading && (
|
||||
<IssueOnLoading
|
||||
componentName={component.displayName}
|
||||
componentName={uiFeature.displayName}
|
||||
reloadIframe={() => {
|
||||
reloadValidityStatus(), requestReload?.(componentViewer, true)
|
||||
}}
|
||||
@@ -186,19 +190,17 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
|
||||
)}
|
||||
|
||||
{featureStatus !== FeatureStatus.Entitled && (
|
||||
<IsExpired
|
||||
expiredDate={dateToLocalizedString(component.valid_until)}
|
||||
featureStatus={featureStatus}
|
||||
componentName={component.displayName}
|
||||
manageSubscription={manageSubscription}
|
||||
/>
|
||||
<NotEntitledBanner featureStatus={featureStatus} feature={uiFeature.featureDescription} />
|
||||
)}
|
||||
{deprecationMessage && !isDeprecationMessageDismissed && (
|
||||
<IsDeprecated deprecationMessage={deprecationMessage} dismissDeprecationMessage={dismissDeprecationMessage} />
|
||||
)}
|
||||
|
||||
{error === ComponentViewerError.OfflineRestricted && <OfflineRestricted />}
|
||||
{error === ComponentViewerError.MissingUrl && <UrlMissing componentName={component.displayName} />}
|
||||
{component.uuid && isComponentValid && (
|
||||
|
||||
{error === ComponentViewerError.MissingUrl && <UrlMissing componentName={uiFeature.displayName} />}
|
||||
|
||||
{uiFeature.uniqueIdentifier && isComponentValid && (
|
||||
<iframe
|
||||
className="h-full w-full flex-grow bg-transparent"
|
||||
ref={iframeRef}
|
||||
@@ -216,4 +218,4 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(ComponentView)
|
||||
export default observer(IframeFeatureView)
|
||||
@@ -1,49 +0,0 @@
|
||||
import { FeatureStatus } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'react'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import IndicatorCircle from '../IndicatorCircle/IndicatorCircle'
|
||||
|
||||
type Props = {
|
||||
expiredDate: string
|
||||
componentName: string
|
||||
featureStatus: FeatureStatus
|
||||
manageSubscription: () => void
|
||||
}
|
||||
|
||||
const statusString = (featureStatus: FeatureStatus, expiredDate: string, componentName: string) => {
|
||||
switch (featureStatus) {
|
||||
case FeatureStatus.InCurrentPlanButExpired:
|
||||
return `Your subscription expired on ${expiredDate}`
|
||||
case FeatureStatus.NoUserSubscription:
|
||||
return 'You do not have an active subscription'
|
||||
case FeatureStatus.NotInCurrentPlan:
|
||||
return `Please upgrade your plan to access ${componentName}`
|
||||
default:
|
||||
return `${componentName} is valid and you should not be seeing this message`
|
||||
}
|
||||
}
|
||||
|
||||
const IsExpired: FunctionComponent<Props> = ({ expiredDate, featureStatus, componentName, manageSubscription }) => {
|
||||
return (
|
||||
<div className={'sn-component'}>
|
||||
<div className="flex min-h-[1.625rem] w-full select-none items-center justify-between border-b border-border bg-contrast py-2.5 px-2 text-text">
|
||||
<div className={'left'}>
|
||||
<div className="flex items-center">
|
||||
<IndicatorCircle style="danger" />
|
||||
<div className="ml-2">
|
||||
<strong>{statusString(featureStatus, expiredDate, componentName)}</strong>
|
||||
<div className={'sk-p'}>{componentName} is in a read-only state.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'right'}>
|
||||
<Button onClick={manageSubscription} primary colorStyle="success" small>
|
||||
Manage subscription
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default IsExpired
|
||||
@@ -0,0 +1,63 @@
|
||||
import { AnyFeatureDescription, FeatureStatus, dateToLocalizedString } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useCallback } from 'react'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { WarningCircle } from '../UIElements/WarningCircle'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { openSubscriptionDashboard } from '@/Utils/ManageSubscription'
|
||||
|
||||
type Props = {
|
||||
feature: AnyFeatureDescription
|
||||
featureStatus: FeatureStatus
|
||||
}
|
||||
|
||||
const statusString = (featureStatus: FeatureStatus, expiredDate: Date | undefined, featureName: string) => {
|
||||
switch (featureStatus) {
|
||||
case FeatureStatus.InCurrentPlanButExpired:
|
||||
if (expiredDate) {
|
||||
return `Your subscription expired on ${dateToLocalizedString(expiredDate)}`
|
||||
} else {
|
||||
return 'Your subscription expired.'
|
||||
}
|
||||
case FeatureStatus.NoUserSubscription:
|
||||
return 'You do not have an active subscription'
|
||||
case FeatureStatus.NotInCurrentPlan:
|
||||
return `Please upgrade your plan to access ${featureName}`
|
||||
default:
|
||||
return `${featureName} is valid and you should not be seeing this message`
|
||||
}
|
||||
}
|
||||
|
||||
const NotEntitledBanner: FunctionComponent<Props> = ({ featureStatus, feature }) => {
|
||||
const application = useApplication()
|
||||
|
||||
const expiredDate = application.subscriptions.userSubscriptionExpirationDate
|
||||
|
||||
const manageSubscription = useCallback(() => {
|
||||
void openSubscriptionDashboard(application)
|
||||
}, [application])
|
||||
|
||||
return (
|
||||
<div className={'sn-component'}>
|
||||
<div className="flex min-h-[1.625rem] w-full select-none items-center justify-between border-b border-border bg-contrast py-2.5 px-2 text-text">
|
||||
<div className={'left'}>
|
||||
<div className="flex items-start">
|
||||
<div className="mt-1">
|
||||
<WarningCircle />
|
||||
</div>
|
||||
<div className="ml-2">
|
||||
<strong>{statusString(featureStatus, expiredDate, feature.name)}</strong>
|
||||
<div className={'sk-p'}>{feature.name} is in a read-only state.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'right'}>
|
||||
<Button onClick={manageSubscription} primary colorStyle="success" small>
|
||||
Manage subscription
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotEntitledBanner
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
TagMutator,
|
||||
TagPreferences,
|
||||
VectorIconNameOrEmoji,
|
||||
PrefDefaults,
|
||||
} from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
@@ -16,7 +17,6 @@ import Icon from '@/Components/Icon/Icon'
|
||||
import Menu from '@/Components/Menu/Menu'
|
||||
import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator'
|
||||
import { DisplayOptionsMenuProps } from './DisplayOptionsMenuProps'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import NewNotePreferences from './NewNotePreferences'
|
||||
import { PreferenceMode } from './PreferenceMode'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
@@ -93,7 +93,7 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
||||
: selectedTag.preferences
|
||||
const [currentMode, setCurrentMode] = useState<PreferenceMode>(selectedTagPreferences ? 'tag' : 'global')
|
||||
const [preferences, setPreferences] = useState<TagPreferences>({})
|
||||
const hasSubscription = application.features.hasFirstPartySubscription()
|
||||
const hasSubscription = application.controllers.subscriptionController.hasFirstPartyOnlineOrOfflineSubscription
|
||||
const controlsDisabled = currentMode === 'tag' && !hasSubscription
|
||||
const isDailyEntry = selectedTagPreferences?.entryMode === 'daily'
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@ import {
|
||||
isSmartView,
|
||||
isSystemView,
|
||||
SystemViewId,
|
||||
PrefDefaults,
|
||||
FeatureStatus,
|
||||
} from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { ChangeEventHandler, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import Dropdown from '@/Components/Dropdown/Dropdown'
|
||||
import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
@@ -27,6 +28,8 @@ import { EditorOption, getDropdownItemsForAllEditors } from '@/Utils/DropdownIte
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { NoteTitleFormatOptions } from './NoteTitleFormatOptions'
|
||||
|
||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
|
||||
const PrefChangeDebounceTimeInMs = 25
|
||||
|
||||
const HelpPageUrl = 'https://day.js.org/docs/en/display/format#list-of-all-available-formats'
|
||||
@@ -46,6 +49,8 @@ const NewNotePreferences: FunctionComponent<Props> = ({
|
||||
changePreferencesCallback,
|
||||
disabled,
|
||||
}: Props) => {
|
||||
const premiumModal = usePremiumModal()
|
||||
|
||||
const isSystemTag = isSmartView(selectedTag) && isSystemView(selectedTag)
|
||||
const selectedTagPreferences = isSystemTag
|
||||
? application.getPreference(PrefKey.SystemViewPreferences)?.[selectedTag.uuid as SystemViewId]
|
||||
@@ -114,8 +119,15 @@ const NewNotePreferences: FunctionComponent<Props> = ({
|
||||
setEditorItems(getDropdownItemsForAllEditors(application))
|
||||
}, [application])
|
||||
|
||||
const setDefaultEditor = useCallback(
|
||||
const selectEditorForNewNoteDefault = useCallback(
|
||||
(value: EditorOption['value']) => {
|
||||
if (application.features.getFeatureStatus(value) !== FeatureStatus.Entitled) {
|
||||
const editorItem = editorItems.find((item) => item.value === value)
|
||||
if (editorItem) {
|
||||
premiumModal.activate(editorItem.label)
|
||||
}
|
||||
return
|
||||
}
|
||||
setDefaultEditorIdentifier(value as FeatureIdentifier)
|
||||
|
||||
if (mode === 'global') {
|
||||
@@ -124,7 +136,7 @@ const NewNotePreferences: FunctionComponent<Props> = ({
|
||||
void changePreferencesCallback({ editorIdentifier: value })
|
||||
}
|
||||
},
|
||||
[application, changePreferencesCallback, mode],
|
||||
[application, mode, editorItems, premiumModal, changePreferencesCallback],
|
||||
)
|
||||
|
||||
const debounceTimeoutRef = useRef<number>()
|
||||
@@ -158,7 +170,7 @@ const NewNotePreferences: FunctionComponent<Props> = ({
|
||||
label="Select the default note type"
|
||||
items={editorItems}
|
||||
value={defaultEditorIdentifier}
|
||||
onChange={(value) => setDefaultEditor(value as EditorOption['value'])}
|
||||
onChange={(value) => selectEditorForNewNoteDefault(value as EditorOption['value'])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintFor
|
||||
import ListItemVaultInfo from './ListItemVaultInfo'
|
||||
import { NoteDragDataFormat } from '../Tags/DragNDrop'
|
||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
import useItem from '@/Hooks/useItem'
|
||||
|
||||
const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
|
||||
application,
|
||||
@@ -33,8 +34,11 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
|
||||
isNextItemTiled,
|
||||
}) => {
|
||||
const listItemRef = useRef<HTMLDivElement>(null)
|
||||
const liveItem = useItem<SNNote>(item.uuid)
|
||||
|
||||
const editor = liveItem ? application.componentManager.editorForNote(liveItem) : undefined
|
||||
const noteType = liveItem?.noteType ? liveItem.noteType : editor ? editor.noteType : undefined
|
||||
|
||||
const noteType = item.noteType || application.componentManager.editorForNote(item)?.package_info.note_type
|
||||
const [icon, tint] = getIconAndTintForNoteType(noteType)
|
||||
const hasFiles = application.items.itemsReferencingItem(item).filter(isFile).length > 0
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
TagMutator,
|
||||
isSystemView,
|
||||
isSmartView,
|
||||
isNote,
|
||||
} from '@standardnotes/snjs'
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
||||
import { FileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
|
||||
@@ -182,10 +183,9 @@ const ItemNameCell = ({ item, hideIcon }: { item: DecryptedItemInterface; hideIc
|
||||
const [backupInfo, setBackupInfo] = useState<FileBackupRecord | undefined>(undefined)
|
||||
const isItemFile = item instanceof FileItem
|
||||
|
||||
const noteType =
|
||||
item instanceof SNNote
|
||||
? item.noteType || application.componentManager.editorForNote(item)?.package_info.note_type
|
||||
: undefined
|
||||
const editor = isNote(item) ? application.componentManager.editorForNote(item) : undefined
|
||||
const noteType = isNote(item) ? item.noteType : editor ? editor.noteType : undefined
|
||||
|
||||
const [noteIcon, noteIconTint] = getIconAndTintForNoteType(noteType)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
import { classNames, EditorLineWidth, PrefKey, SNNote } from '@standardnotes/snjs'
|
||||
import { classNames, EditorLineWidth, PrefKey, SNNote, PrefDefaults } from '@standardnotes/snjs'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import Button from '../Button/Button'
|
||||
import Modal, { ModalAction } from '../Modal/Modal'
|
||||
@@ -9,7 +9,6 @@ import { EditorMargins, EditorMaxWidths } from './EditorWidths'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import ModalOverlay from '../Modal/ModalOverlay'
|
||||
import { CHANGE_EDITOR_WIDTH_COMMAND } from '@standardnotes/ui-services'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import Switch from '../Switch/Switch'
|
||||
|
||||
@@ -155,7 +154,7 @@ const EditorWidthSelectionModal = ({
|
||||
|
||||
const EditorWidthSelectionModalWrapper = () => {
|
||||
const application = useApplication()
|
||||
const { notesController } = application.getViewControllerManager()
|
||||
const { notesController } = application.controllers
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isGlobal, setIsGlobal] = useState(false)
|
||||
|
||||
@@ -141,7 +141,7 @@ const FilePreview = ({ file, application, isEmbeddedInSuper = false, imageZoomLe
|
||||
) : (
|
||||
<FilePreviewError
|
||||
file={file}
|
||||
filesController={application.getViewControllerManager().filesController}
|
||||
filesController={application.controllers.filesController}
|
||||
tryAgainCallback={() => {
|
||||
setDownloadedBytes(undefined)
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { QuickSettingsController } from '@/Controllers/QuickSettingsController'
|
||||
import { FeatureIdentifier, SNTheme } from '@standardnotes/snjs'
|
||||
import { ComponentOrNativeFeature, GetDarkThemeFeature } from '@standardnotes/snjs'
|
||||
import { TOGGLE_DARK_MODE_COMMAND } from '@standardnotes/ui-services'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { useEffect, useRef } from 'react'
|
||||
@@ -25,12 +25,7 @@ const QuickSettingsButton = ({ application, isOpen, toggleMenu, quickSettingsMen
|
||||
return commandService.addCommandHandler({
|
||||
command: TOGGLE_DARK_MODE_COMMAND,
|
||||
onKeyDown: () => {
|
||||
const darkTheme = application.items
|
||||
.getDisplayableComponents()
|
||||
.find((theme) => theme.package_info.identifier === FeatureIdentifier.DarkTheme) as SNTheme | undefined
|
||||
if (darkTheme) {
|
||||
void application.componentManager.toggleTheme(darkTheme.uuid)
|
||||
}
|
||||
void application.componentManager.toggleTheme(new ComponentOrNativeFeature(GetDarkThemeFeature()))
|
||||
},
|
||||
})
|
||||
}, [application, commandService])
|
||||
@@ -57,7 +52,7 @@ const QuickSettingsButton = ({ application, isOpen, toggleMenu, quickSettingsMen
|
||||
align="start"
|
||||
className="py-2"
|
||||
>
|
||||
<QuickSettingsMenu quickSettingsMenuController={quickSettingsMenuController} application={application} />
|
||||
<QuickSettingsMenu quickSettingsMenuController={quickSettingsMenuController} />
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ type Props = {
|
||||
const UpgradeNow = ({ application, featuresController, subscriptionContoller }: Props) => {
|
||||
const shouldShowCTA = !featuresController.hasFolders
|
||||
const hasAccount = subscriptionContoller.hasAccount
|
||||
const hasAccessToFeatures = subscriptionContoller.hasFirstPartySubscription
|
||||
const hasAccessToFeatures = subscriptionContoller.hasFirstPartyOnlineOrOfflineSubscription
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (hasAccount && application.isNativeIOS()) {
|
||||
|
||||
@@ -14,33 +14,44 @@ import Icon from '../Icon/Icon'
|
||||
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
|
||||
import RadioIndicator from '../Radio/RadioIndicator'
|
||||
import MenuListItem from './MenuListItem'
|
||||
import Popover from '../Popover/Popover'
|
||||
|
||||
const Tooltip = ({ text }: { text: string }) => {
|
||||
const [mobileVisible, setMobileVisible] = useState(false)
|
||||
const [visible, setVisible] = useState(false)
|
||||
const onClickMobile: MouseEventHandler<HTMLDivElement> = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setMobileVisible(!mobileVisible)
|
||||
setVisible(!visible)
|
||||
},
|
||||
[mobileVisible],
|
||||
[visible],
|
||||
)
|
||||
|
||||
const [anchorElement, setAnchorElement] = useState<HTMLDivElement | null>(null)
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className={classNames('peer flex h-5 w-5 items-center justify-center rounded-full')} onClick={onClickMobile}>
|
||||
<Icon type={'help'} className="text-neutral" size="large" />
|
||||
<span className="sr-only">Note sync status</span>
|
||||
</div>
|
||||
<div
|
||||
ref={setAnchorElement}
|
||||
className={classNames('peer z-0 flex h-5 w-5 items-center justify-center rounded-full')}
|
||||
onClick={onClickMobile}
|
||||
onMouseEnter={() => setVisible(true)}
|
||||
onMouseLeave={() => setVisible(false)}
|
||||
>
|
||||
<Icon type={'notes'} className="text-border" size="large" />
|
||||
</div>
|
||||
<Popover
|
||||
open={visible}
|
||||
title="Info"
|
||||
anchorElement={anchorElement}
|
||||
disableMobileFullscreenTakeover
|
||||
className={classNames(
|
||||
'hidden',
|
||||
'absolute top-full right-0 w-60 translate-x-2 translate-y-1 select-none rounded border border-border shadow-main',
|
||||
'bg-default py-1.5 px-3 text-left peer-hover:block peer-focus:block',
|
||||
'w-60 translate-x-2 translate-y-1 select-none rounded border border-border shadow-main',
|
||||
'z-modal bg-default py-1.5 px-3 text-left',
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('note view controller', () => {
|
||||
identifier: FeatureIdentifier.MarkdownProEditor,
|
||||
} as SNComponent)
|
||||
|
||||
componentManager.componentWithIdentifier = jest.fn().mockReturnValue({
|
||||
componentManager.componentOrNativeFeatureForIdentifier = jest.fn().mockReturnValue({
|
||||
identifier: FeatureIdentifier.MarkdownProEditor,
|
||||
} as SNComponent)
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ const NoteConflictResolutionModal = ({
|
||||
mutator.conflictOf = undefined
|
||||
})
|
||||
setIsPerformingAction(false)
|
||||
void application.getViewControllerManager().selectionController.selectItem(selectedNotes[0].uuid, true)
|
||||
void application.controllers.selectionController.selectItem(selectedNotes[0].uuid, true)
|
||||
void application.sync.sync()
|
||||
close()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { ReactNode, useCallback, useState } from 'react'
|
||||
import { IconType, PrefKey } from '@standardnotes/snjs'
|
||||
import { IconType, PrefKey, PrefDefaults } from '@standardnotes/snjs'
|
||||
import Icon from '../Icon/Icon'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
|
||||
|
||||
@@ -41,8 +41,9 @@ describe('NoteView', () => {
|
||||
notesController: notesController,
|
||||
} as jest.Mocked<ViewControllerManager>
|
||||
|
||||
application = {} as jest.Mocked<WebApplication>
|
||||
application.getViewControllerManager = jest.fn().mockReturnValue(viewControllerManager)
|
||||
application = {
|
||||
controllers: viewControllerManager,
|
||||
} as jest.Mocked<WebApplication>
|
||||
application.hasProtectionSources = jest.fn().mockReturnValue(true)
|
||||
application.authorizeNoteAccess = jest.fn()
|
||||
application.addWebEventObserver = jest.fn()
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
||||
import ChangeEditorButton from '@/Components/ChangeEditor/ChangeEditorButton'
|
||||
import ComponentView from '@/Components/ComponentView/ComponentView'
|
||||
import IframeFeatureView from '@/Components/ComponentView/IframeFeatureView'
|
||||
import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel'
|
||||
import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton'
|
||||
import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedItemOverlay'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
import { debounce, isDesktopApplication, isMobileScreen } from '@/Utils'
|
||||
@@ -13,16 +12,20 @@ import { classNames, pluralize } from '@standardnotes/utils'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
ComponentArea,
|
||||
ComponentInterface,
|
||||
ComponentOrNativeFeature,
|
||||
ComponentViewerInterface,
|
||||
ContentType,
|
||||
EditorLineWidth,
|
||||
IframeComponentFeatureDescription,
|
||||
isIframeUIFeature,
|
||||
isPayloadSourceInternalChange,
|
||||
isPayloadSourceRetrieved,
|
||||
NoteType,
|
||||
PayloadEmitSource,
|
||||
PrefDefaults,
|
||||
PrefKey,
|
||||
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
|
||||
SNComponent,
|
||||
SNNote,
|
||||
} from '@standardnotes/snjs'
|
||||
import { confirmDialog, DELETE_NOTE_KEYBOARD_COMMAND, KeyboardKey } from '@standardnotes/ui-services'
|
||||
@@ -53,12 +56,12 @@ import Icon from '../Icon/Icon'
|
||||
|
||||
const MinimumStatusDuration = 400
|
||||
|
||||
function sortAlphabetically(array: SNComponent[]): SNComponent[] {
|
||||
function sortAlphabetically(array: ComponentInterface[]): ComponentInterface[] {
|
||||
return array.sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1))
|
||||
}
|
||||
|
||||
type State = {
|
||||
availableStackComponents: SNComponent[]
|
||||
availableStackComponents: ComponentInterface[]
|
||||
editorComponentViewer?: ComponentViewerInterface
|
||||
editorComponentViewerDidAlreadyReload?: boolean
|
||||
editorStateDidLoad: boolean
|
||||
@@ -443,14 +446,21 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
)
|
||||
|
||||
this.removeNoteStreamObserver = this.application.streamItems<SNNote>(ContentType.TYPES.Note, async () => {
|
||||
if (!this.note) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
conflictedNotes: this.application.items.conflictsOf(this.note.uuid) as SNNote[],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private createComponentViewer(component: SNComponent) {
|
||||
const viewer = this.application.componentManager.createComponentViewer(component, this.note.uuid)
|
||||
private createComponentViewer(component: ComponentOrNativeFeature<IframeComponentFeatureDescription>) {
|
||||
if (!component) {
|
||||
throw Error('Cannot create component viewer for undefined component')
|
||||
}
|
||||
const viewer = this.application.componentManager.createComponentViewer(component, { uuid: this.note.uuid })
|
||||
return viewer
|
||||
}
|
||||
|
||||
@@ -462,7 +472,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
return
|
||||
}
|
||||
|
||||
const component = viewer.component
|
||||
const component = viewer.getComponentOrFeatureItem()
|
||||
this.application.componentManager.destroyComponentViewer(viewer)
|
||||
this.setState(
|
||||
{
|
||||
@@ -496,35 +506,36 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
}
|
||||
}
|
||||
|
||||
async reloadEditorComponent() {
|
||||
async reloadEditorComponent(): Promise<void> {
|
||||
log(LoggingDomain.NoteView, 'Reload editor component')
|
||||
if (this.state.showProtectedWarning) {
|
||||
this.destroyCurrentEditorComponent()
|
||||
return
|
||||
}
|
||||
|
||||
const newEditor = this.application.componentManager.editorForNote(this.note)
|
||||
const newUIFeature = this.application.componentManager.editorForNote(this.note)
|
||||
|
||||
/** Editors cannot interact with template notes so the note must be inserted */
|
||||
if (newEditor && this.controller.isTemplateNote) {
|
||||
/** Component editors cannot interact with template notes so the note must be inserted */
|
||||
if (isIframeUIFeature(newUIFeature) && this.controller.isTemplateNote) {
|
||||
await this.controller.insertTemplatedNote()
|
||||
}
|
||||
|
||||
const currentComponentViewer = this.state.editorComponentViewer
|
||||
|
||||
if (currentComponentViewer?.componentUuid !== newEditor?.uuid) {
|
||||
if (currentComponentViewer) {
|
||||
if (currentComponentViewer) {
|
||||
const needsDestroy = currentComponentViewer.componentUniqueIdentifier !== newUIFeature.uniqueIdentifier
|
||||
if (needsDestroy) {
|
||||
this.destroyCurrentEditorComponent()
|
||||
}
|
||||
}
|
||||
|
||||
if (newEditor) {
|
||||
this.setState({
|
||||
editorComponentViewer: this.createComponentViewer(newEditor),
|
||||
editorStateDidLoad: true,
|
||||
})
|
||||
}
|
||||
reloadFont(this.state.monospaceFont)
|
||||
if (isIframeUIFeature(newUIFeature)) {
|
||||
this.setState({
|
||||
editorComponentViewer: this.createComponentViewer(newUIFeature),
|
||||
editorStateDidLoad: true,
|
||||
})
|
||||
} else {
|
||||
reloadFont(this.state.monospaceFont)
|
||||
this.setState({
|
||||
editorStateDidLoad: true,
|
||||
})
|
||||
@@ -731,8 +742,8 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
log(LoggingDomain.NoteView, 'Reload stack components')
|
||||
const stackComponents = sortAlphabetically(
|
||||
this.application.componentManager
|
||||
.componentsForArea(ComponentArea.EditorStack)
|
||||
.filter((component) => component.active),
|
||||
.thirdPartyComponentsForArea(ComponentArea.EditorStack)
|
||||
.filter((component) => this.application.componentManager.isComponentActive(component)),
|
||||
)
|
||||
const enabledComponents = stackComponents.filter((component) => {
|
||||
return component.isExplicitlyEnabledForItem(this.note.uuid)
|
||||
@@ -740,21 +751,28 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
|
||||
const needsNewViewer = enabledComponents.filter((component) => {
|
||||
const hasExistingViewer = this.state.stackComponentViewers.find(
|
||||
(viewer) => viewer.componentUuid === component.uuid,
|
||||
(viewer) => viewer.componentUniqueIdentifier === component.uuid,
|
||||
)
|
||||
return !hasExistingViewer
|
||||
})
|
||||
|
||||
const needsDestroyViewer = this.state.stackComponentViewers.filter((viewer) => {
|
||||
const viewerComponentExistsInEnabledComponents = enabledComponents.find((component) => {
|
||||
return component.uuid === viewer.componentUuid
|
||||
return component.uuid === viewer.componentUniqueIdentifier
|
||||
})
|
||||
return !viewerComponentExistsInEnabledComponents
|
||||
})
|
||||
|
||||
const newViewers: ComponentViewerInterface[] = []
|
||||
for (const component of needsNewViewer) {
|
||||
newViewers.push(this.application.componentManager.createComponentViewer(component, this.note.uuid))
|
||||
newViewers.push(
|
||||
this.application.componentManager.createComponentViewer(
|
||||
new ComponentOrNativeFeature<IframeComponentFeatureDescription>(component),
|
||||
{
|
||||
uuid: this.note.uuid,
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
for (const viewer of needsDestroyViewer) {
|
||||
@@ -766,11 +784,11 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
})
|
||||
}
|
||||
|
||||
stackComponentExpanded = (component: SNComponent): boolean => {
|
||||
return !!this.state.stackComponentViewers.find((viewer) => viewer.componentUuid === component.uuid)
|
||||
stackComponentExpanded = (component: ComponentInterface): boolean => {
|
||||
return !!this.state.stackComponentViewers.find((viewer) => viewer.componentUniqueIdentifier === component.uuid)
|
||||
}
|
||||
|
||||
toggleStackComponent = async (component: SNComponent) => {
|
||||
toggleStackComponent = async (component: ComponentInterface) => {
|
||||
if (!component.isExplicitlyEnabledForItem(this.note.uuid)) {
|
||||
await this.application.mutator.runTransactionalMutation(
|
||||
transactionForAssociateComponentWithCurrentNote(component, this.note),
|
||||
@@ -962,12 +980,11 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
{editorMode === 'component' && this.state.editorComponentViewer && (
|
||||
<div className="component-view relative flex-grow">
|
||||
{this.state.paneGestureEnabled && <div className="absolute top-0 left-0 h-full w-[20px] md:hidden" />}
|
||||
<ComponentView
|
||||
<IframeFeatureView
|
||||
key={this.state.editorComponentViewer.identifier}
|
||||
componentViewer={this.state.editorComponentViewer}
|
||||
onLoad={this.onEditorComponentLoad}
|
||||
requestReload={this.editorComponentViewerRequestsReload}
|
||||
application={this.application}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -1006,6 +1023,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
>
|
||||
<div className="flex h-full">
|
||||
{this.state.availableStackComponents.map((component) => {
|
||||
const active = this.application.componentManager.isComponentActive(component)
|
||||
return (
|
||||
<div
|
||||
key={component.uuid}
|
||||
@@ -1015,7 +1033,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
className="flex flex-grow cursor-pointer items-center justify-center [&:not(:first-child)]:ml-3"
|
||||
>
|
||||
<div className="flex h-full items-center [&:not(:first-child)]:ml-2">
|
||||
{this.stackComponentExpanded(component) && component.active && <IndicatorCircle style="info" />}
|
||||
{this.stackComponentExpanded(component) && active && <IndicatorCircle style="info" />}
|
||||
{!this.stackComponentExpanded(component) && <IndicatorCircle style="neutral" />}
|
||||
</div>
|
||||
<div className="flex h-full items-center [&:not(:first-child)]:ml-2">
|
||||
@@ -1032,7 +1050,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
||||
{this.state.stackComponentViewers.map((viewer) => {
|
||||
return (
|
||||
<div className="component-view component-stack-item" key={viewer.identifier}>
|
||||
<ComponentView key={viewer.identifier} componentViewer={viewer} application={this.application} />
|
||||
<IframeFeatureView key={viewer.identifier} componentViewer={viewer} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { usePrevious } from '@/Components/ContentListView/Calendar/usePrevious'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
import { Disposer } from '@/Types/Disposer'
|
||||
import { EditorEventSource } from '@/Types/EditorEventSource'
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
isPayloadSourceRetrieved,
|
||||
PrefKey,
|
||||
WebAppEvent,
|
||||
PrefDefaults,
|
||||
} from '@standardnotes/snjs'
|
||||
import { TAB_COMMAND } from '@standardnotes/ui-services'
|
||||
import { ChangeEventHandler, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ContentType, NoteContent, NoteType, SNNote, classNames } from '@standardnotes/snjs'
|
||||
import { ContentType, NoteContent, NoteType, SNNote, classNames, isIframeUIFeature } from '@standardnotes/snjs'
|
||||
import { UIEventHandler, useEffect, useMemo, useRef } from 'react'
|
||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import ComponentView from '../ComponentView/ComponentView'
|
||||
import IframeFeatureView from '../ComponentView/IframeFeatureView'
|
||||
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
|
||||
import { BlocksEditor } from '../SuperEditor/BlocksEditor'
|
||||
import { BlocksEditorComposer } from '../SuperEditor/BlocksEditorComposer'
|
||||
@@ -30,18 +30,17 @@ export const ReadonlyNoteContent = ({
|
||||
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
||||
|
||||
const componentViewer = useMemo(() => {
|
||||
const editorForCurrentNote = note ? application.componentManager.editorForNote(note) : undefined
|
||||
|
||||
if (!editorForCurrentNote) {
|
||||
const editorForCurrentNote = application.componentManager.editorForNote(note)
|
||||
if (!isIframeUIFeature(editorForCurrentNote)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const templateNoteForRevision = application.items.createTemplateItem(ContentType.TYPES.Note, note.content) as SNNote
|
||||
|
||||
const componentViewer = application.componentManager.createComponentViewer(editorForCurrentNote)
|
||||
componentViewer.setReadonly(true)
|
||||
componentViewer.lockReadonly = true
|
||||
componentViewer.overrideContextItem = templateNoteForRevision
|
||||
const componentViewer = application.componentManager.createComponentViewer(editorForCurrentNote, {
|
||||
readonlyItem: templateNoteForRevision,
|
||||
})
|
||||
|
||||
return componentViewer
|
||||
}, [application.componentManager, application.items, note])
|
||||
|
||||
@@ -91,7 +90,7 @@ export const ReadonlyNoteContent = ({
|
||||
)}
|
||||
{componentViewer ? (
|
||||
<div className="component-view">
|
||||
<ComponentView key={componentViewer.identifier} componentViewer={componentViewer} application={application} />
|
||||
<IframeFeatureView key={componentViewer.identifier} componentViewer={componentViewer} />
|
||||
</div>
|
||||
) : content.noteType === NoteType.Super ? (
|
||||
<ErrorBoundary>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SNComponent, SNNote, ComponentMutator, TransactionalMutation, ItemMutator } from '@standardnotes/snjs'
|
||||
import { SNNote, ComponentMutator, TransactionalMutation, ItemMutator, ComponentInterface } from '@standardnotes/snjs'
|
||||
|
||||
export const transactionForAssociateComponentWithCurrentNote = (component: SNComponent, note: SNNote) => {
|
||||
export const transactionForAssociateComponentWithCurrentNote = (component: ComponentInterface, note: SNNote) => {
|
||||
const transaction: TransactionalMutation = {
|
||||
itemUuid: component.uuid,
|
||||
mutate: (m: ItemMutator) => {
|
||||
@@ -12,7 +12,7 @@ export const transactionForAssociateComponentWithCurrentNote = (component: SNCom
|
||||
return transaction
|
||||
}
|
||||
|
||||
export const transactionForDisassociateComponentWithCurrentNote = (component: SNComponent, note: SNNote) => {
|
||||
export const transactionForDisassociateComponentWithCurrentNote = (component: ComponentInterface, note: SNNote) => {
|
||||
const transaction: TransactionalMutation = {
|
||||
itemUuid: component.uuid,
|
||||
mutate: (m: ItemMutator) => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { NoteType, SNComponent } from '@standardnotes/snjs'
|
||||
import {
|
||||
ComponentOrNativeFeature,
|
||||
EditorFeatureDescription,
|
||||
IframeComponentFeatureDescription,
|
||||
} from '@standardnotes/snjs'
|
||||
|
||||
export type EditorMenuItem = {
|
||||
name: string
|
||||
component?: SNComponent
|
||||
uiFeature: ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>
|
||||
isEntitled: boolean
|
||||
noteType: NoteType
|
||||
isLabs?: boolean
|
||||
description?: string
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useMemo, FunctionComponent } from 'react'
|
||||
import { SNApplication, SNNote, classNames } from '@standardnotes/snjs'
|
||||
import { SNNote, classNames } from '@standardnotes/snjs'
|
||||
import { formatDateForContextMenu } from '@/Utils/DateUtils'
|
||||
import { calculateReadTime } from './Utils/calculateReadTime'
|
||||
import { countNoteAttributes } from './Utils/countNoteAttributes'
|
||||
import { WebApplicationInterface } from '@standardnotes/ui-services'
|
||||
|
||||
export const useNoteAttributes = (application: SNApplication, note: SNNote) => {
|
||||
export const useNoteAttributes = (application: WebApplicationInterface, note: SNNote) => {
|
||||
const { words, characters, paragraphs } = useMemo(() => countNoteAttributes(note.text), [note.text])
|
||||
|
||||
const readTime = useMemo(() => (typeof words === 'number' ? calculateReadTime(words) : 'N/A'), [words])
|
||||
@@ -14,7 +15,7 @@ export const useNoteAttributes = (application: SNApplication, note: SNNote) => {
|
||||
const dateCreated = useMemo(() => formatDateForContextMenu(note.created_at), [note.created_at])
|
||||
|
||||
const editor = application.componentManager.editorForNote(note)
|
||||
const format = editor?.package_info?.file_type || 'txt'
|
||||
const format = editor.fileType
|
||||
|
||||
return {
|
||||
words,
|
||||
@@ -28,7 +29,7 @@ export const useNoteAttributes = (application: SNApplication, note: SNNote) => {
|
||||
}
|
||||
|
||||
export const NoteAttributes: FunctionComponent<{
|
||||
application: SNApplication
|
||||
application: WebApplicationInterface
|
||||
note: SNNote
|
||||
className?: string
|
||||
}> = ({ application, note, className }) => {
|
||||
|
||||
@@ -409,7 +409,9 @@ const NotesOptions = ({
|
||||
|
||||
<HorizontalSeparator classes="my-2" />
|
||||
|
||||
<SpellcheckOptions editorForNote={editorForNote} notesController={notesController} note={notes[0]} />
|
||||
{editorForNote && (
|
||||
<SpellcheckOptions editorForNote={editorForNote} notesController={notesController} note={notes[0]} />
|
||||
)}
|
||||
|
||||
<HorizontalSeparator classes="my-2" />
|
||||
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { SNComponent, SNNote } from '@standardnotes/snjs'
|
||||
import {
|
||||
ComponentOrNativeFeature,
|
||||
EditorFeatureDescription,
|
||||
IframeComponentFeatureDescription,
|
||||
SNNote,
|
||||
} from '@standardnotes/snjs'
|
||||
import { NotesController } from '@/Controllers/NotesController/NotesController'
|
||||
import { iconClass } from './ClassNames'
|
||||
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
||||
|
||||
export const SpellcheckOptions: FunctionComponent<{
|
||||
editorForNote: SNComponent | undefined
|
||||
editorForNote: ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>
|
||||
notesController: NotesController
|
||||
note: SNNote
|
||||
}> = ({ editorForNote, notesController, note }) => {
|
||||
const spellcheckControllable = Boolean(!editorForNote || editorForNote.package_info.spellcheckControl)
|
||||
const spellcheckControllable = editorForNote.featureDescription.spellcheckControl
|
||||
const noteSpellcheck = !spellcheckControllable
|
||||
? true
|
||||
: note
|
||||
|
||||
@@ -38,7 +38,7 @@ const PanesSystemComponent = () => {
|
||||
const [panesPendingEntrance, setPanesPendingEntrance] = useState<AppPaneId[]>([])
|
||||
const [panesPendingExit, setPanesPendingExit] = useState<AppPaneId[]>([])
|
||||
|
||||
const viewControllerManager = application.getViewControllerManager()
|
||||
const viewControllerManager = application.controllers
|
||||
|
||||
const [navigationPanelWidth, setNavigationPanelWidth] = useState<number>(
|
||||
application.getPreference(PrefKey.TagsPanelWidth, PLACEHOLDER_NAVIGATION_PANEL_WIDTH),
|
||||
|
||||
@@ -2,8 +2,7 @@ import { useStateRef } from '@/Hooks/useStateRef'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { ApplicationEvent, PrefKey } from '@standardnotes/snjs'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { ApplicationEvent, PrefKey, PrefDefaults } from '@standardnotes/snjs'
|
||||
|
||||
function getScrollParent(node: HTMLElement | null): HTMLElement | null {
|
||||
if (!node) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SNComponent } from '@standardnotes/snjs'
|
||||
import { ComponentInterface } from '@standardnotes/snjs'
|
||||
import { useCallback } from 'react'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import ModalDialogButtons from '../Modal/ModalDialogButtons'
|
||||
@@ -7,7 +7,7 @@ import Modal from '../Modal/Modal'
|
||||
type Props = {
|
||||
callback: (approved: boolean) => void
|
||||
dismiss: () => void
|
||||
component: SNComponent
|
||||
component: ComponentInterface
|
||||
permissionsString: string
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { ApplicationEvent, PermissionDialog } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import ModalOverlay from '../Modal/ModalOverlay'
|
||||
import PermissionsModal from './PermissionsModal'
|
||||
import { WebApplicationInterface } from '@standardnotes/ui-services'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
application: WebApplicationInterface
|
||||
}
|
||||
|
||||
const PermissionsModalWrapper: FunctionComponent<Props> = ({ application }) => {
|
||||
|
||||
@@ -31,7 +31,7 @@ const AccountPreferences = ({ application, viewControllerManager }: Props) => {
|
||||
<Sync application={application} />
|
||||
</>
|
||||
)}
|
||||
<Subscription application={application} viewControllerManager={viewControllerManager} />
|
||||
<Subscription />
|
||||
<SubscriptionSharing application={application} viewControllerManager={viewControllerManager} />
|
||||
{application.hasAccount() && viewControllerManager.featuresController.entitledToFiles && (
|
||||
<FilesSection application={application} />
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
import { Title } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import SubscriptionInformation from './SubscriptionInformation'
|
||||
import NoSubscription from './NoSubscription'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
||||
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
viewControllerManager: ViewControllerManager
|
||||
}
|
||||
const Subscription: FunctionComponent = () => {
|
||||
const application = useApplication()
|
||||
|
||||
const Subscription: FunctionComponent<Props> = ({ application, viewControllerManager }: Props) => {
|
||||
const subscriptionState = viewControllerManager.subscriptionController
|
||||
const { onlineSubscription } = subscriptionState
|
||||
|
||||
const now = new Date().getTime()
|
||||
const onlineSubscription = application.controllers.subscriptionController.onlineSubscription
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
@@ -25,11 +18,7 @@ const Subscription: FunctionComponent<Props> = ({ application, viewControllerMan
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex flex-grow flex-col">
|
||||
<Title>Subscription</Title>
|
||||
{onlineSubscription && onlineSubscription.endsAt > now ? (
|
||||
<SubscriptionInformation subscriptionState={subscriptionState} application={application} />
|
||||
) : (
|
||||
<NoSubscription application={application} />
|
||||
)}
|
||||
{onlineSubscription ? <SubscriptionInformation /> : <NoSubscription application={application} />}
|
||||
</div>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { openSubscriptionDashboard } from '@/Utils/ManageSubscription'
|
||||
import StatusText from './StatusText'
|
||||
import SubscriptionStatusText from './SubscriptionStatusText'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
|
||||
type Props = {
|
||||
subscriptionState: SubscriptionController
|
||||
application: WebApplication
|
||||
}
|
||||
const SubscriptionInformation = () => {
|
||||
const application = useApplication()
|
||||
|
||||
const SubscriptionInformation = ({ subscriptionState, application }: Props) => {
|
||||
const manageSubscription = async () => {
|
||||
void openSubscriptionDashboard(application)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatusText subscriptionState={subscriptionState} />
|
||||
<SubscriptionStatusText />
|
||||
<Button className="mt-3 mr-3 min-w-20" label="Manage subscription" onClick={manageSubscription} />
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Text } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
|
||||
type Props = { subscriptionState: SubscriptionController }
|
||||
const SubscriptionStatusText = () => {
|
||||
const application = useApplication()
|
||||
|
||||
const StatusText = ({ subscriptionState }: Props) => {
|
||||
const {
|
||||
userSubscriptionName,
|
||||
userSubscriptionExpirationDate,
|
||||
isUserSubscriptionExpired,
|
||||
isUserSubscriptionCanceled,
|
||||
} = subscriptionState
|
||||
} = application.subscriptions
|
||||
|
||||
const expirationDateString = userSubscriptionExpirationDate?.toLocaleString()
|
||||
|
||||
if (isUserSubscriptionCanceled) {
|
||||
@@ -58,4 +59,4 @@ const StatusText = ({ subscriptionState }: Props) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(StatusText)
|
||||
export default observer(SubscriptionStatusText)
|
||||
@@ -4,16 +4,7 @@ import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import {
|
||||
ContentType,
|
||||
FeatureIdentifier,
|
||||
PrefKey,
|
||||
GetFeatures,
|
||||
SNTheme,
|
||||
FindNativeFeature,
|
||||
FeatureStatus,
|
||||
naturalSort,
|
||||
} from '@standardnotes/snjs'
|
||||
import { FeatureIdentifier, PrefKey, FeatureStatus, naturalSort, PrefDefaults } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useEffect, useState } from 'react'
|
||||
import { Subtitle, Title, Text } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
@@ -21,8 +12,8 @@ import PreferencesPane from '../PreferencesComponents/PreferencesPane'
|
||||
import PreferencesGroup from '../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../PreferencesComponents/PreferencesSegment'
|
||||
import { PremiumFeatureIconName } from '@/Components/Icon/PremiumFeatureIcon'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import EditorAppearance from './Appearance/EditorAppearance'
|
||||
import { GetAllThemesUseCase } from '@standardnotes/ui-services'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -43,36 +34,39 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const themesAsItems: DropdownItem[] = application.items
|
||||
.getDisplayableComponents()
|
||||
.filter((component) => component.isTheme() && !(component as SNTheme).isLayerable())
|
||||
.filter((theme) => !FindNativeFeature(theme.identifier))
|
||||
.map((theme) => {
|
||||
return {
|
||||
label: theme.name,
|
||||
value: theme.identifier as string,
|
||||
}
|
||||
})
|
||||
const usecase = new GetAllThemesUseCase(application.items)
|
||||
const { thirdParty, native } = usecase.execute({ excludeLayerable: true })
|
||||
|
||||
GetFeatures()
|
||||
.filter((feature) => feature.content_type === ContentType.TYPES.Theme && !feature.layerable)
|
||||
.forEach((theme) => {
|
||||
themesAsItems.push({
|
||||
label: theme.name as string,
|
||||
value: theme.identifier,
|
||||
icon:
|
||||
application.features.getFeatureStatus(theme.identifier) !== FeatureStatus.Entitled
|
||||
? PremiumFeatureIconName
|
||||
: undefined,
|
||||
})
|
||||
})
|
||||
const dropdownItems: DropdownItem[] = []
|
||||
|
||||
themesAsItems.unshift({
|
||||
dropdownItems.push({
|
||||
label: 'Default',
|
||||
value: 'Default',
|
||||
})
|
||||
|
||||
setThemeItems(naturalSort(themesAsItems, 'label'))
|
||||
dropdownItems.push(
|
||||
...native.map((theme) => {
|
||||
return {
|
||||
label: theme.displayName as string,
|
||||
value: theme.featureIdentifier,
|
||||
icon:
|
||||
application.features.getFeatureStatus(theme.featureIdentifier) !== FeatureStatus.Entitled
|
||||
? PremiumFeatureIconName
|
||||
: undefined,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
dropdownItems.push(
|
||||
...thirdParty.map((theme) => {
|
||||
return {
|
||||
label: theme.displayName,
|
||||
value: theme.featureIdentifier,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
setThemeItems(naturalSort(dropdownItems, 'label'))
|
||||
}, [application])
|
||||
|
||||
const toggleUseDeviceSettings = () => {
|
||||
|
||||
@@ -3,8 +3,14 @@ import Dropdown from '@/Components/Dropdown/Dropdown'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { ApplicationEvent, EditorFontSize, EditorLineHeight, EditorLineWidth, PrefKey } from '@standardnotes/snjs'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
EditorFontSize,
|
||||
EditorLineHeight,
|
||||
EditorLineWidth,
|
||||
PrefKey,
|
||||
PrefDefaults,
|
||||
} from '@standardnotes/snjs'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Subtitle, Title, Text } from '../../PreferencesComponents/Content'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
@@ -78,7 +84,7 @@ const EditorDefaults = ({ application }: Props) => {
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Editor appearance</Title>
|
||||
<Title>Editor</Title>
|
||||
<div className="mt-2">
|
||||
<div className="flex justify-between gap-2 md:items-center">
|
||||
<div className="flex flex-col">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FunctionComponent, useState } from 'react'
|
||||
import { ComponentMutator, SNComponent } from '@standardnotes/snjs'
|
||||
import { ComponentInterface, ComponentMutator, SNComponent } from '@standardnotes/snjs'
|
||||
import { SubtitleLight } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import Button from '@/Components/Button/Button'
|
||||
@@ -39,7 +39,7 @@ const PackageEntry: FunctionComponent<PackageEntryProps> = ({ application, exten
|
||||
mutator.offlineOnly = newOfflineOnly
|
||||
})
|
||||
.then((item) => {
|
||||
const component = item as SNComponent
|
||||
const component = item as ComponentInterface
|
||||
setOfflineOnly(component.offlineOnly)
|
||||
})
|
||||
.catch((e) => {
|
||||
@@ -54,7 +54,7 @@ const PackageEntry: FunctionComponent<PackageEntryProps> = ({ application, exten
|
||||
mutator.name = newName
|
||||
})
|
||||
.then((item) => {
|
||||
const component = item as SNComponent
|
||||
const component = item as ComponentInterface
|
||||
setExtensionName(component.name)
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ButtonType, ContentType, SNComponent } from '@standardnotes/snjs'
|
||||
import { ButtonType, ContentType } from '@standardnotes/snjs'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
@@ -63,7 +63,7 @@ const PackagesPreferencesSection: FunctionComponent<Props> = ({
|
||||
}
|
||||
|
||||
const submitExtensionUrl = async (url: string) => {
|
||||
const component = await application.features.downloadExternalFeature(url)
|
||||
const component = await application.features.downloadRemoteThirdPartyFeature(url)
|
||||
if (component) {
|
||||
setConfirmableExtension(component)
|
||||
}
|
||||
@@ -90,10 +90,6 @@ const PackagesPreferencesSection: FunctionComponent<Props> = ({
|
||||
return false
|
||||
}
|
||||
|
||||
if (extension instanceof SNComponent) {
|
||||
return !['modal', 'rooms'].includes(extension.area)
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { SNActionsExtension, SNComponent, SNTheme } from '@standardnotes/snjs'
|
||||
import { ComponentInterface, SNActionsExtension, ThemeInterface } from '@standardnotes/snjs'
|
||||
|
||||
export type AnyPackageType = SNComponent | SNTheme | SNActionsExtension
|
||||
export type AnyPackageType = ComponentInterface | ThemeInterface | SNActionsExtension
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PrefKey, Platform } from '@standardnotes/snjs'
|
||||
import { PrefKey, Platform, PrefDefaults } from '@standardnotes/snjs'
|
||||
import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { FunctionComponent, useState } from 'react'
|
||||
@@ -6,7 +6,6 @@ import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { ApplicationEvent, FeatureIdentifier, FeatureStatus, FindNativeFeature, PrefKey } from '@standardnotes/snjs'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
FeatureIdentifier,
|
||||
FeatureStatus,
|
||||
FindNativeFeature,
|
||||
PrefKey,
|
||||
PrefDefaults,
|
||||
} from '@standardnotes/snjs'
|
||||
import { Fragment, FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
import PreferencesGroup from '../../../PreferencesComponents/PreferencesGroup'
|
||||
@@ -8,7 +15,6 @@ import PreferencesSegment from '../../../PreferencesComponents/PreferencesSegmen
|
||||
import LabsFeature from './LabsFeature'
|
||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
|
||||
type ExperimentalFeatureItem = {
|
||||
identifier: FeatureIdentifier
|
||||
|
||||
@@ -21,9 +21,9 @@ const Persistence = ({ application }: Props) => {
|
||||
setShouldPersistNoteState(shouldPersist)
|
||||
|
||||
if (shouldPersist) {
|
||||
application.getViewControllerManager().persistValues()
|
||||
application.controllers.persistValues()
|
||||
} else {
|
||||
application.getViewControllerManager().clearPersistedValues()
|
||||
application.controllers.clearPersistedValues()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { PrefKey } from '@standardnotes/snjs'
|
||||
import { PrefKey, PrefDefaults } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useState } from 'react'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
|
||||
@@ -25,7 +25,7 @@ const HomeServerSettings = () => {
|
||||
const homeServerService = application.homeServer as HomeServerServiceInterface
|
||||
const featuresService = application.features
|
||||
const sessionsService = application.sessions
|
||||
const viewControllerManager = application.getViewControllerManager()
|
||||
const viewControllerManager = application.controllers
|
||||
|
||||
const logsTextarea = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const CreateAccount: FunctionComponent<Props> = ({ viewControllerManager, applic
|
||||
}
|
||||
|
||||
const subscribeWithoutAccount = () => {
|
||||
application.getViewControllerManager().purchaseFlowController.openPurchaseWebpage()
|
||||
application.controllers.purchaseFlowController.openPurchaseWebpage()
|
||||
}
|
||||
|
||||
const handleCreateAccount = async () => {
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { ComponentArea, ContentType, FeatureIdentifier, GetFeatures, SNComponent } from '@standardnotes/snjs'
|
||||
import {
|
||||
ComponentArea,
|
||||
ComponentInterface,
|
||||
ComponentOrNativeFeature,
|
||||
ContentType,
|
||||
FeatureIdentifier,
|
||||
PreferencesServiceEvent,
|
||||
ThemeFeatureDescription,
|
||||
} from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import FocusModeSwitch from './FocusModeSwitch'
|
||||
import ThemesMenuButton from './ThemesMenuButton'
|
||||
import { ThemeItem } from './ThemeItem'
|
||||
import { sortThemes } from '@/Utils/SortThemes'
|
||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||
import { QuickSettingsController } from '@/Controllers/QuickSettingsController'
|
||||
@@ -13,50 +19,35 @@ import PanelSettingsSection from './PanelSettingsSection'
|
||||
import Menu from '../Menu/Menu'
|
||||
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
||||
import MenuRadioButtonItem from '../Menu/MenuRadioButtonItem'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { GetAllThemesUseCase } from '@standardnotes/ui-services'
|
||||
|
||||
type MenuProps = {
|
||||
quickSettingsMenuController: QuickSettingsController
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, quickSettingsMenuController }) => {
|
||||
const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ quickSettingsMenuController }) => {
|
||||
const application = useApplication()
|
||||
|
||||
const { focusModeEnabled, setFocusModeEnabled } = application.paneController
|
||||
const { closeQuickSettingsMenu } = quickSettingsMenuController
|
||||
const [themes, setThemes] = useState<ThemeItem[]>([])
|
||||
const [toggleableComponents, setToggleableComponents] = useState<SNComponent[]>([])
|
||||
const [themes, setThemes] = useState<ComponentOrNativeFeature<ThemeFeatureDescription>[]>([])
|
||||
const [editorStackComponents, setEditorStackComponents] = useState<ComponentInterface[]>([])
|
||||
|
||||
const defaultThemeOn = !themes.map((item) => item?.component).find((theme) => theme?.active && !theme.isLayerable())
|
||||
const activeThemes = application.componentManager.getActiveThemes()
|
||||
const hasNonLayerableActiveTheme = activeThemes.find((theme) => !theme.layerable)
|
||||
const defaultThemeOn = !hasNonLayerableActiveTheme
|
||||
|
||||
const prefsButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const defaultThemeButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const reloadThemes = useCallback(() => {
|
||||
const themes = application.items
|
||||
.getDisplayableComponents()
|
||||
.filter((component) => component.isTheme())
|
||||
.map((item) => {
|
||||
return {
|
||||
name: item.displayName,
|
||||
identifier: item.identifier,
|
||||
component: item,
|
||||
}
|
||||
}) as ThemeItem[]
|
||||
|
||||
GetFeatures()
|
||||
.filter((feature) => feature.content_type === ContentType.TYPES.Theme && !feature.layerable)
|
||||
.forEach((theme) => {
|
||||
if (themes.findIndex((item) => item.identifier === theme.identifier) === -1) {
|
||||
themes.push({
|
||||
name: theme.name as string,
|
||||
identifier: theme.identifier,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
setThemes(themes.sort(sortThemes))
|
||||
const usecase = new GetAllThemesUseCase(application.items)
|
||||
const { thirdParty, native } = usecase.execute({ excludeLayerable: false })
|
||||
setThemes([...thirdParty, ...native].sort(sortThemes))
|
||||
}, [application])
|
||||
|
||||
const reloadToggleableComponents = useCallback(() => {
|
||||
const reloadEditorStackComponents = useCallback(() => {
|
||||
const toggleableComponents = application.items
|
||||
.getDisplayableComponents()
|
||||
.filter(
|
||||
@@ -66,7 +57,7 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, quickSet
|
||||
component.identifier !== FeatureIdentifier.DeprecatedFoldersComponent,
|
||||
)
|
||||
|
||||
setToggleableComponents(toggleableComponents)
|
||||
setEditorStackComponents(toggleableComponents)
|
||||
}, [application])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -85,37 +76,41 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, quickSet
|
||||
}
|
||||
}, [application, reloadThemes])
|
||||
|
||||
useEffect(() => {
|
||||
return application.preferences.addEventObserver((event) => {
|
||||
if (event === PreferencesServiceEvent.PreferencesChanged) {
|
||||
reloadThemes()
|
||||
}
|
||||
})
|
||||
}, [application, reloadThemes])
|
||||
|
||||
useEffect(() => {
|
||||
const cleanupItemStream = application.streamItems(ContentType.TYPES.Component, () => {
|
||||
reloadToggleableComponents()
|
||||
reloadEditorStackComponents()
|
||||
})
|
||||
|
||||
return () => {
|
||||
cleanupItemStream()
|
||||
}
|
||||
}, [application, reloadToggleableComponents])
|
||||
}, [application, reloadEditorStackComponents])
|
||||
|
||||
useEffect(() => {
|
||||
prefsButtonRef.current?.focus()
|
||||
}, [])
|
||||
|
||||
const toggleComponent = useCallback(
|
||||
(component: SNComponent) => {
|
||||
if (component.isTheme()) {
|
||||
application.componentManager.toggleTheme(component.uuid).catch(console.error)
|
||||
} else {
|
||||
application.componentManager.toggleComponent(component.uuid).catch(console.error)
|
||||
}
|
||||
const toggleEditorStackComponent = useCallback(
|
||||
(component: ComponentInterface) => {
|
||||
application.componentManager.toggleComponent(component).catch(console.error)
|
||||
},
|
||||
[application],
|
||||
)
|
||||
|
||||
const deactivateAnyNonLayerableTheme = useCallback(() => {
|
||||
const activeTheme = themes.map((item) => item.component).find((theme) => theme?.active && !theme.isLayerable())
|
||||
if (activeTheme) {
|
||||
application.componentManager.toggleTheme(activeTheme.uuid).catch(console.error)
|
||||
const nonLayerableActiveTheme = application.componentManager.getActiveThemes().find((theme) => !theme.layerable)
|
||||
if (nonLayerableActiveTheme) {
|
||||
void application.componentManager.toggleTheme(nonLayerableActiveTheme)
|
||||
}
|
||||
}, [application, themes])
|
||||
}, [application])
|
||||
|
||||
const toggleDefaultTheme = useCallback(() => {
|
||||
deactivateAnyNonLayerableTheme()
|
||||
@@ -123,15 +118,15 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, quickSet
|
||||
|
||||
return (
|
||||
<Menu a11yLabel="Quick settings menu" isOpen>
|
||||
{toggleableComponents.length > 0 && (
|
||||
{editorStackComponents.length > 0 && (
|
||||
<>
|
||||
<div className="my-1 px-3 text-sm font-semibold uppercase text-text">Tools</div>
|
||||
{toggleableComponents.map((component) => (
|
||||
{editorStackComponents.map((component) => (
|
||||
<MenuSwitchButtonItem
|
||||
onChange={() => {
|
||||
toggleComponent(component)
|
||||
toggleEditorStackComponent(component)
|
||||
}}
|
||||
checked={component.active}
|
||||
checked={application.componentManager.isComponentActive(component)}
|
||||
key={component.uuid}
|
||||
>
|
||||
<Icon type="window" className="mr-2 text-neutral" />
|
||||
@@ -146,7 +141,7 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, quickSet
|
||||
Default
|
||||
</MenuRadioButtonItem>
|
||||
{themes.map((theme) => (
|
||||
<ThemesMenuButton item={theme} application={application} key={theme.component?.uuid ?? theme.identifier} />
|
||||
<ThemesMenuButton uiFeature={theme} key={theme.uniqueIdentifier} />
|
||||
))}
|
||||
<HorizontalSeparator classes="my-2" />
|
||||
<FocusModeSwitch
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { FeatureIdentifier, SNTheme } from '@standardnotes/snjs'
|
||||
|
||||
export type ThemeItem = {
|
||||
name: string
|
||||
identifier: FeatureIdentifier
|
||||
component?: SNTheme
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { FeatureIdentifier, FeatureStatus } from '@standardnotes/snjs'
|
||||
import {
|
||||
ComponentOrNativeFeature,
|
||||
FeatureIdentifier,
|
||||
FeatureStatus,
|
||||
ThemeFeatureDescription,
|
||||
} from '@standardnotes/snjs'
|
||||
import { FunctionComponent, MouseEventHandler, useCallback, useMemo } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
import { ThemeItem } from './ThemeItem'
|
||||
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
|
||||
import { isMobileScreen } from '@/Utils'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
@@ -12,38 +15,41 @@ import MenuRadioButtonItem from '../Menu/MenuRadioButtonItem'
|
||||
import { useCommandService } from '../CommandProvider'
|
||||
import { TOGGLE_DARK_MODE_COMMAND } from '@standardnotes/ui-services'
|
||||
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
|
||||
type Props = {
|
||||
item: ThemeItem
|
||||
application: WebApplication
|
||||
uiFeature: ComponentOrNativeFeature<ThemeFeatureDescription>
|
||||
}
|
||||
|
||||
const ThemesMenuButton: FunctionComponent<Props> = ({ application, item }) => {
|
||||
const ThemesMenuButton: FunctionComponent<Props> = ({ uiFeature }) => {
|
||||
const application = useApplication()
|
||||
const commandService = useCommandService()
|
||||
const premiumModal = usePremiumModal()
|
||||
|
||||
const isThirdPartyTheme = useMemo(
|
||||
() => application.features.isThirdPartyFeature(item.identifier),
|
||||
[application, item.identifier],
|
||||
() => application.features.isThirdPartyFeature(uiFeature.featureIdentifier),
|
||||
[application, uiFeature.featureIdentifier],
|
||||
)
|
||||
const isEntitledToTheme = useMemo(
|
||||
() => application.features.getFeatureStatus(item.identifier) === FeatureStatus.Entitled,
|
||||
[application, item.identifier],
|
||||
() => application.features.getFeatureStatus(uiFeature.featureIdentifier) === FeatureStatus.Entitled,
|
||||
[application, uiFeature.featureIdentifier],
|
||||
)
|
||||
const canActivateTheme = useMemo(() => isEntitledToTheme || isThirdPartyTheme, [isEntitledToTheme, isThirdPartyTheme])
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
if (item.component && canActivateTheme) {
|
||||
const isThemeLayerable = item.component.isLayerable()
|
||||
const themeIsLayerableOrNotActive = isThemeLayerable || !item.component.active
|
||||
|
||||
if (themeIsLayerableOrNotActive) {
|
||||
application.componentManager.toggleTheme(item.component.uuid).catch(console.error)
|
||||
}
|
||||
} else {
|
||||
premiumModal.activate(`${item.name} theme`)
|
||||
if (!canActivateTheme) {
|
||||
premiumModal.activate(`${uiFeature.displayName} theme`)
|
||||
return
|
||||
}
|
||||
}, [application, canActivateTheme, item, premiumModal])
|
||||
|
||||
const isThemeLayerable = uiFeature.layerable
|
||||
|
||||
const themeIsLayerableOrNotActive = isThemeLayerable || !application.componentManager.isThemeActive(uiFeature)
|
||||
|
||||
if (themeIsLayerableOrNotActive) {
|
||||
void application.componentManager.toggleTheme(uiFeature)
|
||||
}
|
||||
}, [application, canActivateTheme, uiFeature, premiumModal])
|
||||
|
||||
const onClick: MouseEventHandler<HTMLButtonElement> = useCallback(
|
||||
(event) => {
|
||||
@@ -54,34 +60,38 @@ const ThemesMenuButton: FunctionComponent<Props> = ({ application, item }) => {
|
||||
)
|
||||
|
||||
const isMobile = application.isNativeMobileWeb() || isMobileScreen()
|
||||
const shouldHideButton = item.identifier === FeatureIdentifier.DynamicTheme && isMobile
|
||||
const shouldHideButton = uiFeature.featureIdentifier === FeatureIdentifier.DynamicTheme && isMobile
|
||||
|
||||
const darkThemeShortcut = useMemo(() => {
|
||||
if (item.identifier === FeatureIdentifier.DarkTheme) {
|
||||
if (uiFeature.featureIdentifier === FeatureIdentifier.DarkTheme) {
|
||||
return commandService.keyboardShortcutForCommand(TOGGLE_DARK_MODE_COMMAND)
|
||||
}
|
||||
}, [commandService, item.identifier])
|
||||
}, [commandService, uiFeature.featureIdentifier])
|
||||
|
||||
if (shouldHideButton) {
|
||||
return null
|
||||
}
|
||||
|
||||
return item.component?.isLayerable() ? (
|
||||
<MenuSwitchButtonItem checked={item.component.active} onChange={() => toggleTheme()}>
|
||||
const themeActive = uiFeature ? application.componentManager.isThemeActive(uiFeature) : false
|
||||
|
||||
const dockIcon = uiFeature.dockIcon
|
||||
|
||||
return uiFeature.layerable ? (
|
||||
<MenuSwitchButtonItem checked={themeActive} onChange={() => toggleTheme()}>
|
||||
{!canActivateTheme && (
|
||||
<Icon type={PremiumFeatureIconName} className={classNames(PremiumFeatureIconClass, 'mr-2')} />
|
||||
)}
|
||||
{item.name}
|
||||
{uiFeature.displayName}
|
||||
</MenuSwitchButtonItem>
|
||||
) : (
|
||||
<MenuRadioButtonItem checked={Boolean(item.component?.active)} onClick={onClick}>
|
||||
<span className={classNames('mr-auto', item.component?.active ? 'font-semibold' : undefined)}>{item.name}</span>
|
||||
<MenuRadioButtonItem checked={themeActive} onClick={onClick}>
|
||||
<span className={classNames('mr-auto', themeActive ? 'font-semibold' : undefined)}>{uiFeature.displayName}</span>
|
||||
{darkThemeShortcut && <KeyboardShortcutIndicator className="mr-2" shortcut={darkThemeShortcut} />}
|
||||
{item.component && canActivateTheme ? (
|
||||
{uiFeature && canActivateTheme ? (
|
||||
<div
|
||||
className="h-5 w-5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: item.component.package_info?.dock_icon?.background_color,
|
||||
backgroundColor: dockIcon?.background_color,
|
||||
}}
|
||||
></div>
|
||||
) : (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import RevisionContentLocked from './RevisionContentLocked'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
|
||||
import { NoteHistoryController, RevisionContentState } from '@/Controllers/NoteHistory/NoteHistoryController'
|
||||
import Spinner from '@/Components/Spinner/Spinner'
|
||||
import { ReadonlyNoteContent } from '../NoteView/ReadonlyNoteContent'
|
||||
@@ -9,10 +8,9 @@ import { SNNote } from '@standardnotes/snjs'
|
||||
type Props = {
|
||||
noteHistoryController: NoteHistoryController
|
||||
note: SNNote
|
||||
subscriptionController: SubscriptionController
|
||||
}
|
||||
|
||||
const HistoryModalContentPane = ({ noteHistoryController, subscriptionController, note }: Props) => {
|
||||
const HistoryModalContentPane = ({ noteHistoryController, note }: Props) => {
|
||||
const { selectedRevision, contentState } = noteHistoryController
|
||||
|
||||
switch (contentState) {
|
||||
@@ -30,7 +28,7 @@ const HistoryModalContentPane = ({ noteHistoryController, subscriptionController
|
||||
}
|
||||
return <ReadonlyNoteContent note={note} content={selectedRevision.payload.content} showLinkedItems={false} />
|
||||
case RevisionContentState.NotEntitled:
|
||||
return <RevisionContentLocked subscriptionController={subscriptionController} />
|
||||
return <RevisionContentLocked />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import MobileModalHeader from '../Modal/MobileModalHeader'
|
||||
const HistoryModalDialogContent = ({
|
||||
application,
|
||||
dismissModal,
|
||||
subscriptionController,
|
||||
note,
|
||||
selectionController,
|
||||
}: RevisionHistoryModalContentProps) => {
|
||||
@@ -87,11 +86,7 @@ const HistoryModalDialogContent = ({
|
||||
selectedMobileTab === 'Content' ? 'flex' : 'hidden',
|
||||
)}
|
||||
>
|
||||
<HistoryModalContentPane
|
||||
noteHistoryController={noteHistoryController}
|
||||
note={note}
|
||||
subscriptionController={subscriptionController}
|
||||
/>
|
||||
<HistoryModalContentPane noteHistoryController={noteHistoryController} note={note} />
|
||||
</div>
|
||||
</div>
|
||||
<HistoryModalFooter dismissModal={dismissModal} noteHistoryController={noteHistoryController} />
|
||||
|
||||
@@ -2,7 +2,7 @@ import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { HistoryLockedIllustration } from '@standardnotes/icons'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
|
||||
const getPlanHistoryDuration = (planName: string | undefined) => {
|
||||
switch (planName) {
|
||||
@@ -19,12 +19,15 @@ const getPremiumContentCopy = (planName: string | undefined) => {
|
||||
return `Version history is limited to ${getPlanHistoryDuration(planName)} in the ${planName} plan`
|
||||
}
|
||||
|
||||
type Props = {
|
||||
subscriptionController: SubscriptionController
|
||||
}
|
||||
const RevisionContentLocked: FunctionComponent = () => {
|
||||
const application = useApplication()
|
||||
|
||||
const RevisionContentLocked: FunctionComponent<Props> = ({ subscriptionController }) => {
|
||||
const { userSubscriptionName, isUserSubscriptionExpired, isUserSubscriptionCanceled } = subscriptionController
|
||||
let planName = 'free'
|
||||
if (application.subscriptions.hasOnlineSubscription()) {
|
||||
if (!application.subscriptions.isUserSubscriptionCanceled && !application.subscriptions.isUserSubscriptionExpired) {
|
||||
planName = application.subscriptions.userSubscriptionName
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
@@ -32,12 +35,7 @@ const RevisionContentLocked: FunctionComponent<Props> = ({ subscriptionControlle
|
||||
<HistoryLockedIllustration />
|
||||
<div className="mt-2 mb-1 text-lg font-bold">Can't access this version</div>
|
||||
<div className="leading-140% mb-4 text-passive-0">
|
||||
{getPremiumContentCopy(
|
||||
!isUserSubscriptionCanceled && !isUserSubscriptionExpired && userSubscriptionName
|
||||
? userSubscriptionName
|
||||
: 'free',
|
||||
)}
|
||||
. Learn more about our other plans to upgrade your history capacity.
|
||||
{getPremiumContentCopy(planName)}. Learn more about our other plans to upgrade your history capacity.
|
||||
</div>
|
||||
<Button
|
||||
primary
|
||||
|
||||
@@ -10,7 +10,6 @@ const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps> = ({
|
||||
application,
|
||||
historyModalController,
|
||||
selectionController,
|
||||
subscriptionController,
|
||||
}) => {
|
||||
const addAndroidBackHandler = useAndroidBackHandler()
|
||||
|
||||
@@ -49,7 +48,6 @@ const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps> = ({
|
||||
dismissModal={historyModalController.dismissModal}
|
||||
note={historyModalController.note}
|
||||
selectionController={selectionController}
|
||||
subscriptionController={subscriptionController}
|
||||
/>
|
||||
)}
|
||||
</HistoryModalDialog>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
|
||||
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
|
||||
import { SNNote } from '@standardnotes/snjs'
|
||||
|
||||
type CommonProps = {
|
||||
application: WebApplication
|
||||
selectionController: SelectedItemsController
|
||||
subscriptionController: SubscriptionController
|
||||
}
|
||||
|
||||
export type RevisionHistoryModalProps = CommonProps & {
|
||||
|
||||
@@ -41,7 +41,7 @@ const RemoteImageComponent = ({ className, src, alt, node, format, nodeKey }: Pr
|
||||
const blob = await response.blob()
|
||||
const file = new File([blob], src, { type: blob.type })
|
||||
|
||||
const { filesController, linkingController } = application.getViewControllerManager()
|
||||
const { filesController, linkingController } = application.controllers
|
||||
|
||||
const uploadedFile = await filesController.uploadNewFile(file, false)
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
EditorLineHeight,
|
||||
isPayloadSourceRetrieved,
|
||||
PrefKey,
|
||||
PrefDefaults,
|
||||
FeatureIdentifier,
|
||||
FeatureStatus,
|
||||
GetSuperNoteFeature,
|
||||
} from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { BlocksEditor } from './BlocksEditor'
|
||||
@@ -30,7 +34,6 @@ import {
|
||||
ChangeEditorFunction,
|
||||
} from './Plugins/ChangeContentCallback/ChangeContentCallback'
|
||||
import PasswordPlugin from './Plugins/PasswordPlugin/PasswordPlugin'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { useCommandService } from '@/Components/CommandProvider'
|
||||
import { SUPER_SHOW_MARKDOWN_PREVIEW } from '@standardnotes/ui-services'
|
||||
import { SuperNoteMarkdownPreview } from './SuperNoteMarkdownPreview'
|
||||
@@ -45,6 +48,7 @@ import ModalOverlay from '@/Components/Modal/ModalOverlay'
|
||||
import MobileToolbarPlugin from './Plugins/MobileToolbarPlugin/MobileToolbarPlugin'
|
||||
import CodeOptionsPlugin from './Plugins/CodeOptionsPlugin/CodeOptions'
|
||||
import RemoteImagePlugin from './Plugins/RemoteImagePlugin/RemoteImagePlugin'
|
||||
import NotEntitledBanner from '../ComponentView/NotEntitledBanner'
|
||||
|
||||
export const SuperNotePreviewCharLimit = 160
|
||||
|
||||
@@ -68,6 +72,11 @@ export const SuperEditor: FunctionComponent<Props> = ({
|
||||
const ignoreNextChange = useRef(false)
|
||||
const [showMarkdownPreview, setShowMarkdownPreview] = useState(false)
|
||||
const getMarkdownPlugin = useRef<GetMarkdownPluginInterface | null>(null)
|
||||
const [featureStatus, setFeatureStatus] = useState<FeatureStatus>(FeatureStatus.Entitled)
|
||||
|
||||
useEffect(() => {
|
||||
setFeatureStatus(application.features.getFeatureStatus(FeatureIdentifier.SuperEditor))
|
||||
}, [application.features])
|
||||
|
||||
const commandService = useCommandService()
|
||||
|
||||
@@ -194,6 +203,9 @@ export const SuperEditor: FunctionComponent<Props> = ({
|
||||
|
||||
return (
|
||||
<div className="font-editor relative flex h-full w-full flex-col md:block" ref={ref}>
|
||||
{featureStatus !== FeatureStatus.Entitled && (
|
||||
<NotEntitledBanner featureStatus={featureStatus} feature={GetSuperNoteFeature()} />
|
||||
)}
|
||||
<ErrorBoundary>
|
||||
<LinkingControllerProvider controller={linkingController}>
|
||||
<FilesControllerProvider controller={filesController}>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { ContentType, NoteContent, NoteType, SNNote, spaceSeparatedStrings } from '@standardnotes/snjs'
|
||||
import {
|
||||
ContentType,
|
||||
NoteContent,
|
||||
NoteType,
|
||||
SNNote,
|
||||
isIframeUIFeature,
|
||||
spaceSeparatedStrings,
|
||||
} from '@standardnotes/snjs'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import ComponentView from '../ComponentView/ComponentView'
|
||||
import IframeFeatureView from '../ComponentView/IframeFeatureView'
|
||||
import Icon from '../Icon/Icon'
|
||||
import Modal, { ModalAction } from '../Modal/Modal'
|
||||
import { EditorMenuItem } from '../NotesOptions/EditorMenuItem'
|
||||
@@ -20,27 +27,30 @@ const SuperNoteConverter = ({
|
||||
onComplete: () => void
|
||||
}) => {
|
||||
const application = useApplication()
|
||||
const { name, noteType, component } = convertTo
|
||||
const { uiFeature } = convertTo
|
||||
|
||||
const format = useMemo(() => {
|
||||
if (component && component.package_info.file_type) {
|
||||
return component.package_info.file_type
|
||||
if (uiFeature) {
|
||||
const fileType = uiFeature.fileType
|
||||
if (fileType) {
|
||||
return fileType
|
||||
}
|
||||
}
|
||||
|
||||
if (noteType === NoteType.Markdown) {
|
||||
if (uiFeature.noteType === NoteType.Markdown) {
|
||||
return 'md'
|
||||
}
|
||||
|
||||
if (noteType === NoteType.RichText) {
|
||||
if (uiFeature.noteType === NoteType.RichText) {
|
||||
return 'html'
|
||||
}
|
||||
|
||||
if (noteType === NoteType.Plain) {
|
||||
if (uiFeature.noteType === NoteType.Plain) {
|
||||
return 'txt'
|
||||
}
|
||||
|
||||
return 'json'
|
||||
}, [component, noteType])
|
||||
}, [uiFeature])
|
||||
|
||||
const convertedContent = useMemo(() => {
|
||||
if (note.text.length === 0) {
|
||||
@@ -51,7 +61,7 @@ const SuperNoteConverter = ({
|
||||
}, [format, note])
|
||||
|
||||
const componentViewer = useMemo(() => {
|
||||
if (!component) {
|
||||
if (!uiFeature || !isIframeUIFeature(uiFeature)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -61,12 +71,12 @@ const SuperNoteConverter = ({
|
||||
references: note.references,
|
||||
})
|
||||
|
||||
const componentViewer = application.componentManager.createComponentViewer(component)
|
||||
componentViewer.setReadonly(true)
|
||||
componentViewer.lockReadonly = true
|
||||
componentViewer.overrideContextItem = templateNoteForRevision
|
||||
const componentViewer = application.componentManager.createComponentViewer(uiFeature, {
|
||||
readonlyItem: templateNoteForRevision,
|
||||
})
|
||||
|
||||
return componentViewer
|
||||
}, [application.componentManager, application.items, component, convertedContent, note.references, note.title])
|
||||
}, [application.componentManager, application.items, uiFeature, convertedContent, note.references, note.title])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -163,7 +173,7 @@ const SuperNoteConverter = ({
|
||||
) : null}
|
||||
{componentViewer ? (
|
||||
<div className="component-view min-h-0">
|
||||
<ComponentView key={componentViewer.identifier} componentViewer={componentViewer} application={application} />
|
||||
<IframeFeatureView key={componentViewer.identifier} componentViewer={componentViewer} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full min-h-0 overflow-hidden">
|
||||
|
||||
@@ -24,7 +24,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const Navigation = forwardRef<HTMLDivElement, Props>(({ application, className, children, id }, ref) => {
|
||||
const viewControllerManager = useMemo(() => application.getViewControllerManager(), [application])
|
||||
const viewControllerManager = useMemo(() => application.controllers, [application])
|
||||
const { setPaneLayout } = useResponsiveAppPane()
|
||||
|
||||
const [hasPasscode, setHasPasscode] = useState(() => application.hasPasscode())
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
|
||||
export const WarningCircle = () => {
|
||||
return (
|
||||
<button
|
||||
className={
|
||||
'peer flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-warning text-warning-contrast'
|
||||
}
|
||||
>
|
||||
<Icon type={'warning'} size="small" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
) {}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user