chore: show Super demo modal if user doesn't have subscription when switching editor to Super

This commit is contained in:
Aman Harwara
2023-12-22 16:40:42 +05:30
committed by GitHub
parent 485339be86
commit 29b7e989a6
26 changed files with 482 additions and 119 deletions

View File

@@ -1,6 +1,14 @@
export interface SuperConverterServiceInterface {
isValidSuperString(superString: string): boolean
convertSuperStringToOtherFormat: (superString: string, toFormat: 'txt' | 'md' | 'html' | 'json') => Promise<string>
convertOtherFormatToSuperString: (otherFormatString: string, fromFormat: 'txt' | 'md' | 'html' | 'json') => string
convertOtherFormatToSuperString: (
otherFormatString: string,
fromFormat: 'txt' | 'md' | 'html' | 'json',
options?: {
html?: {
addLineBreaks?: boolean
}
},
) => string
getEmbeddedFileIDsFromSuperString(superString: string): string[]
}

View File

@@ -736,7 +736,7 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
boost: a7c83b31436843459a1961bfd74b96033dc77234
boost: 57d2868c099736d80fcd648bf211b4431e51a558
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: 71803c074f6325f10b5ec891c443b6bbabef0ca7
@@ -756,7 +756,7 @@ SPEC CHECKSUMS:
MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd
MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
RCTRequired: df81ab637d35fac9e6eb94611cfd20f0feb05455
RCTTypeSafety: 4636e4a36c7c2df332bda6d59b19b41c443d4287
React: e0cc5197a804031a6c53fb38483c3485fcb9d6f3

View File

@@ -991,6 +991,7 @@
OTHER_LDFLAGS = (
"$(inherited)",
" ",
"-Wl -ld_classic ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
@@ -1059,6 +1060,7 @@
OTHER_LDFLAGS = (
"$(inherited)",
" ",
"-Wl -ld_classic ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;

View File

@@ -154,6 +154,11 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
const handleMenuSelection = useCallback(
async (menuItem: EditorMenuItem) => {
if (!menuItem.isEntitled) {
if (menuItem.uiFeature.featureIdentifier === NativeFeatureIdentifier.TYPES.SuperEditor) {
premiumModal.showSuperDemo()
return
}
premiumModal.activate(menuItem.uiFeature.displayName)
return
}
@@ -249,7 +254,7 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
<>
<Menu className="pb-1 pt-0.5" a11yLabel="Change note type menu">
<MenuSection>
<div className="flex items-center justify-between pr-4 py-3 md:pt-0 md:pb-1">
<div className="flex items-center justify-between py-3 pr-4 md:pb-1 md:pt-0">
<div className="px-3">
<h2 className="text-base font-bold">Choose a note type</h2>
{unableToFindEditor && (

View File

@@ -9,6 +9,7 @@ import {
NoteMutator,
NoteType,
SNNote,
NativeFeatureIdentifier,
} from '@standardnotes/snjs'
import { useCallback, useMemo, useState } from 'react'
import Icon from '../Icon/Icon'
@@ -60,6 +61,11 @@ const ChangeEditorMultipleMenu = ({ application, notes, setDisableClickOutside }
const handleMenuSelection = useCallback(
async (itemToBeSelected: EditorMenuItem) => {
if (!itemToBeSelected.isEntitled) {
if (itemToBeSelected.uiFeature.featureIdentifier === NativeFeatureIdentifier.TYPES.SuperEditor) {
premiumModal.showSuperDemo()
return
}
premiumModal.activate(itemToBeSelected.uiFeature.displayName)
return
}

View File

@@ -33,6 +33,7 @@ const PositionedPopoverContent = ({
hideOnClickInModal = false,
setAnimationElement,
containerClassName,
documentElement,
}: PopoverContentProps) => {
const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
const popoverRect = useAutoElementRect(popoverElement)
@@ -45,13 +46,13 @@ const PositionedPopoverContent = ({
y: anchorPoint?.y,
})
const anchorRect = anchorPoint ? anchorPointRect : anchorElementRect
const documentRect = useDocumentRect()
const _documentRect = useDocumentRect()
const isDesktopScreen = useMediaQuery(MediaQueryBreakpoints.md)
const styles = getPositionedPopoverStyles({
align,
anchorRect,
documentRect,
documentRect: documentElement?.getBoundingClientRect() ?? _documentRect,
popoverRect: popoverRect ?? popoverElement?.getBoundingClientRect(),
side,
disableMobileFullscreenTakeover,
@@ -73,7 +74,7 @@ const PositionedPopoverContent = ({
let adjustedStyles: PopoverCSSProperties | undefined = undefined
if (!portal && popoverElement && styles) {
adjustedStyles = getAdjustedStylesForNonPortalPopover(popoverElement, styles)
adjustedStyles = getAdjustedStylesForNonPortalPopover(popoverElement, styles, documentElement)
}
usePopoverCloseOnClickOutside({

View File

@@ -50,6 +50,7 @@ type CommonPopoverProps = {
offset?: number
hideOnClickInModal?: boolean
open: boolean
documentElement?: HTMLElement
}
export type PopoverContentProps = CommonPopoverProps & {

View File

@@ -6,7 +6,7 @@ export const getAdjustedStylesForNonPortalPopover = (
styles: PopoverCSSProperties,
parent?: HTMLElement,
) => {
const absoluteParent = parent || getAbsolutePositionedParent(popoverElement)
const absoluteParent = parent || getAbsolutePositionedParent(popoverElement) || popoverElement.parentElement
const translateXProperty = styles?.['--translate-x']
const translateYProperty = styles?.['--translate-y']

View File

@@ -77,6 +77,11 @@ const NewNoteDefaults = () => {
}
if (application.features.getFeatureStatus(identifier) !== FeatureStatus.Entitled) {
if (feature.getValue().value === NativeFeatureIdentifier.TYPES.SuperEditor) {
premiumModal.showSuperDemo()
return
}
const editorItem = editorItems.find((item) => item.value === value)
if (editorItem) {
premiumModal.activate(editorItem.label)

View File

@@ -1,4 +1,5 @@
export enum PremiumFeatureModalType {
UpgradePrompt,
UpgradeSuccess,
SuperDemo,
}

View File

@@ -5,6 +5,8 @@ import { FeatureName } from '@/Controllers/FeatureName'
import { SuccessPrompt } from './Subviews/SuccessPrompt'
import { UpgradePrompt } from './Subviews/UpgradePrompt'
import Modal from '../Modal/Modal'
import SuperDemo from './Subviews/SuperDemo'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
type Props = {
application: WebApplication
@@ -23,23 +25,41 @@ const PremiumFeaturesModal: FunctionComponent<Props> = ({
}) => {
const ctaButtonRef = useRef<HTMLButtonElement>(null)
return (
<Modal close={onClose} title="Upgrade" className="px-6 py-5" customHeader={<></>}>
<div tabIndex={-1} className="sn-component">
<div tabIndex={0}>
{type === PremiumFeatureModalType.UpgradePrompt && (
<UpgradePrompt
featureName={featureName}
ctaRef={ctaButtonRef}
application={application}
hasSubscription={hasSubscription}
onClose={onClose}
/>
)}
const isShowingSuperDemo = type === PremiumFeatureModalType.SuperDemo
{type === PremiumFeatureModalType.UpgradeSuccess && <SuccessPrompt ctaRef={ctaButtonRef} onClose={onClose} />}
</div>
</div>
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
return (
<Modal
close={onClose}
title={isShowingSuperDemo ? 'Try out Super' : 'Upgrade'}
className={isShowingSuperDemo ? '' : 'px-6 py-5'}
customHeader={isShowingSuperDemo ? undefined : <></>}
actions={
isShowingSuperDemo
? [
{
label: 'Done',
type: 'primary',
onClick: onClose,
hidden: !isMobileScreen,
mobileSlot: 'right',
},
]
: undefined
}
>
{type === PremiumFeatureModalType.UpgradePrompt && (
<UpgradePrompt
featureName={featureName}
ctaRef={ctaButtonRef}
application={application}
hasSubscription={hasSubscription}
onClose={onClose}
/>
)}
{type === PremiumFeatureModalType.UpgradeSuccess && <SuccessPrompt ctaRef={ctaButtonRef} onClose={onClose} />}
{type === PremiumFeatureModalType.SuperDemo && <SuperDemo hasSubscription={hasSubscription} onClose={onClose} />}
</Modal>
)
}

View File

@@ -0,0 +1,64 @@
import { BlocksEditor } from '@/Components/SuperEditor/BlocksEditor'
import { BlocksEditorComposer } from '@/Components/SuperEditor/BlocksEditorComposer'
import BlockPickerMenuPlugin from '@/Components/SuperEditor/Plugins/BlockPickerPlugin/BlockPickerPlugin'
import usePreference from '@/Hooks/usePreference'
import { useResponsiveEditorFontSize } from '@/Utils/getPlaintextFontSize'
import { EditorLineHeightValues, PrefKey, classNames } from '@standardnotes/snjs'
import { CSSProperties, useRef, useState } from 'react'
import { SuperDemoInitialValue } from './SuperDemoInitialValue'
import { UpgradePrompt } from './UpgradePrompt'
import { useApplication } from '@/Components/ApplicationProvider'
import { useAutoElementRect } from '@/Hooks/useElementRect'
const SuperDemo = ({ hasSubscription, onClose }: { hasSubscription: boolean; onClose: () => void }) => {
const application = useApplication()
const lineHeight = usePreference(PrefKey.EditorLineHeight)
const fontSize = usePreference(PrefKey.EditorFontSize)
const responsiveFontSize = useResponsiveEditorFontSize(fontSize, false)
const ctaRef = useRef<HTMLButtonElement>(null)
const [demoContainer, setDemoContainer] = useState<HTMLDivElement | null>(null)
const demoContainerRect = useAutoElementRect(demoContainer, {
updateOnWindowResize: true,
})
return (
<div className="flex h-full flex-col" ref={setDemoContainer}>
<div
className={classNames(
'flex-shrink-0 border-b border-border p-4',
demoContainerRect && demoContainerRect.height < 500 ? 'hidden md:block' : '',
)}
>
<UpgradePrompt
featureName="Super notes"
ctaRef={ctaRef}
application={application}
hasSubscription={hasSubscription}
inline
preferHorizontalLayout
onClick={onClose}
/>
</div>
<div
className="relative flex h-full min-h-0 flex-col"
style={
{
'--line-height': EditorLineHeightValues[lineHeight],
'--font-size': responsiveFontSize,
} as CSSProperties
}
>
<BlocksEditorComposer initialValue={SuperDemoInitialValue()}>
<BlocksEditor className="blocks-editor h-full bg-default">
<BlockPickerMenuPlugin popoverZIndex="z-modal" />
</BlocksEditor>
</BlocksEditorComposer>
</div>
</div>
)
}
export default SuperDemo

View File

@@ -0,0 +1,164 @@
import { HeadlessSuperConverter } from '@/Components/SuperEditor/Tools/HeadlessSuperConverter'
const InitialHTML = `<div>
<h1>This is a demo of Super notes</h1>
<p><br></p>
<p>Super notes are our new <b>rich text</b> experience. With Super notes, you can create <b>rich</b>, <i>dynamic</i> text with powerful options.</p>
<p><br></p>
<h2><span>Images</span></h2>
<p><br></p>
<p>You can add images to your note by selecting the "Image from URL" option from the <code spellcheck="false"><span>/</span></code> menu or Insert menu in the toolbar.</p>
<p><br></p>
<p><img src="https://standardnotes.com/static/292c6ba50c69a3ae4f8b1883e7f505f6/1f7f6/vault-wide.jpg" /></p>
<p><br></p>
<h2><span>Lists</span></h2>
<p><br></p>
<ul>
<li value="1"><span>Type </span><code spellcheck="false"><span>-</span></code><span> followed by a space in begin a
list</span></li>
<li value="2"><span>Type </span><code spellcheck="false"><span>1.</span></code><span> followed by a space in begin a numbered
list</span></li>
<li value="3"><span>Type </span><code spellcheck="false"><span>[]</span></code><span> followed by a space
to begin a checklist </span></li>
</ul>
<p><br></p>
<ul>
<li value="1"><span>A list</span></li>
<li value="2">
<ul>
<li value="1"><span>Indent the list</span></li>
<li value="2">
<ul>
<li value="1"><span>And even more</span></li>
</ul>
</li>
</ul>
</li>
</ul>
<p><br></p>
<ol>
<li value="1"><span>A numbered list</span></li>
<li value="2"><span>With multiple levels</span></li>
<li value="3"><span>And even more</span></li>
</ol>
<p><br></p>
<ul __lexicallisttype="check">
<li role="checkbox" tabindex="-1" aria-checked="false" value="1">
<span>Create</span>
</li>
<li role="checkbox" tabindex="-1" aria-checked="true" value="2">
<span>a</span>
</li>
<li role="checkbox" tabindex="-1" aria-checked="true" value="3">
<span>checklist</span>
</li>
</ul>
<p><br></p>
<h2><span>Collapsible sections</span></h2>
<p><br></p>
<details open="">
<summary><span>Collapsible section</span></summary>
<div data-lexical-collapsible-content="true">
<p><span>Collapsible sections can include all
other types of content like</span></p>
<p><br></p>
<h2><span>Heading</span></h2>
<p><br></p>
<ul>
<li value="1"><span>a list</span></li>
</ul>
<ol>
<li value="1"><span>numbered</span></li>
</ol>
<ul __lexicallisttype="check">
<li role="checkbox" tabindex="-1" aria-checked="false" value="1"><span>check
list</span>
</li>
</ul>
<p><br></p>
<pre spellcheck="false" data-highlight-language="javascript"><span>A</span><span> code block</span></pre>
<p><br></p>
<p><span>You can even nest collapsible
sections.</span></p>
<p><br></p>
<details open="">
<summary><span>Nested collapsible section</span></summary>
<div data-lexical-collapsible-content="true">
<blockquote><span>Quote</span></blockquote>
</div>
</details>
</div>
</details>
<p><br></p>
<h2><span>Code blocks</span></h2>
<p><br></p>
<p><span>Type </span><code spellcheck="false"><span >\`\`\`</span></code><span> followed by a space to create a code
block. You can choose the language when your
cursor is within the code block.</span></p>
<p><br></p>
<pre spellcheck="false"
data-highlight-language="js"><span >function</span><span> </span><span >main</span><span >(</span><span >)</span><span> </span><span >{</span><br><span> </span><span >const</span><span> variable </span><span >=</span><span> </span><span >"string"</span><span >;</span><br><span> </span><span >return</span><span> </span><span >TEST</span><span >;</span><br><span >}</span></pre>
<p><br></p>
<h2><span>Tables</span></h2>
<table>
<colgroup>
<col>
<col>
<col>
</colgroup>
<tbody>
<tr>
<th>
<p><span>Header</span></p>
</th>
<th>
<p><span>Column 1</span></p>
</th>
<th>
<p><span>Column 2</span></p>
</th>
</tr>
<tr>
<th>
<p><span>Row 1</span></p>
</th>
<td>
<p><span>Row 1 x Column 1</span></p>
</td>
<td>
<p><span>Row 1 x Column 2</span></p>
</td>
</tr>
<tr>
<th>
<p><span>Row 2</span></p>
</th>
<td>
<p><span>Row 2 x Column 1</span></p>
</td>
<td>
<p><span>Row 2 x Column 2</span></p>
</td>
</tr>
</tbody>
</table>
<p><br></p>
<h2><span>Passwords</span></h2>
<p><span>You can generate a secure password using
the "Generate password" command using the </span><code spellcheck="false"><span >/</span></code><span>
menu.</span></p>
<p><br></p>
<ul>
<li value="1"><span>}:hcMrIFgaijpkyz</span></li>
<li value="2"><span>*raF/qi$m?y?iiBS</span></li>
<li value="3"><span>YuVmWf(gOD&amp;=vjbB</span></li>
</ul>
</div>`
export function SuperDemoInitialValue() {
return new HeadlessSuperConverter().convertOtherFormatToSuperString(InitialHTML, 'html', {
html: {
addLineBreaks: false,
},
})
}

View File

@@ -2,6 +2,8 @@ import { useCallback } from 'react'
import { WebApplication } from '@/Application/WebApplication'
import Icon from '@/Components/Icon/Icon'
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '@/Components/Icon/PremiumFeatureIcon'
import { classNames } from '@standardnotes/snjs'
import { requestCloseAllOpenModalsAndPopovers } from '@/Utils/CloseOpenModalsAndPopovers'
type Props = {
featureName?: string
@@ -12,10 +14,12 @@ type Props = {
} & (
| {
inline: true
preferHorizontalLayout?: boolean
onClose?: never
}
| {
inline?: false
preferHorizontalLayout?: never
onClose: () => void
}
)
@@ -28,11 +32,13 @@ export const UpgradePrompt = ({
onClose,
onClick,
inline,
preferHorizontalLayout = false,
}: Props) => {
const handleClick = useCallback(() => {
if (onClick) {
onClick()
}
requestCloseAllOpenModalsAndPopovers()
if (hasSubscription && !application.isNativeIOS()) {
void application.openSubscriptionDashboard.execute()
} else {
@@ -44,65 +50,76 @@ export const UpgradePrompt = ({
}, [application, hasSubscription, onClose, onClick])
return (
<>
<div>
<div className={preferHorizontalLayout ? 'flex flex-wrap items-center gap-4 md:flex-nowrap' : ''}>
{!inline && (
<div className="flex justify-end p-1">
{!inline && (
<button
className="flex cursor-pointer border-0 bg-transparent p-0"
onClick={onClose}
aria-label="Close modal"
>
<Icon className="text-neutral" type="close" />
</button>
)}
<button
className="flex cursor-pointer border-0 bg-transparent p-0"
onClick={onClose}
aria-label="Close modal"
>
<Icon className="text-neutral" type="close" />
</button>
</div>
)}
<div
className={classNames(
'flex items-center justify-center rounded-[50%] bg-contrast',
preferHorizontalLayout ? 'h-12 w-12 flex-shrink-0' : 'mx-auto mb-5 h-24 w-24',
)}
aria-hidden={true}
>
<Icon
className={classNames(preferHorizontalLayout ? 'h-8 w-8' : 'h-12 w-12', PremiumFeatureIconClass)}
size={'custom'}
type={PremiumFeatureIconName}
/>
</div>
<div className={preferHorizontalLayout ? '' : 'mb-2'}>
<div className={classNames('mb-1 text-lg font-bold', preferHorizontalLayout ? 'text-left' : 'text-center')}>
Enable Advanced Features
</div>
<div
className="mx-auto mb-5 flex h-24 w-24 items-center justify-center rounded-[50%] bg-contrast"
aria-hidden={true}
className={classNames('text-sm text-passive-1', preferHorizontalLayout ? 'text-left' : 'px-4.5 text-center')}
>
<Icon className={`h-12 w-12 ${PremiumFeatureIconClass}`} size={'custom'} type={PremiumFeatureIconName} />
{featureName && (
<span>
To take advantage of <span className="font-semibold">{featureName}</span> and other advanced features,
upgrade your current plan.
</span>
)}
{!featureName && (
<span>
To take advantage of all the advanced features Standard Notes has to offer, upgrade your current plan.
</span>
)}
{application.isNativeIOS() && (
<div className="mt-2">
<div className="mb-2 font-bold">The Professional Plan costs $119.99/year and includes benefits like</div>
<ul className="list-inside list-[circle]">
<li>100GB encrypted file storage</li>
<li>
Access to all note types, including Super, markdown, rich text, authenticator, tasks, and spreadsheets
</li>
<li>Access to Daily Notebooks and Moments journals</li>
<li>Note history going back indefinitely</li>
<li>Nested folders for your tags</li>
<li>Premium support</li>
</ul>
</div>
)}
</div>
<div className="mb-1 text-center text-lg font-bold">Enable Advanced Features</div>
</div>
<div className="mb-2 px-4.5 text-center text-sm text-passive-1">
{featureName && (
<span>
To take advantage of <span className="font-semibold">{featureName}</span> and other advanced features,
upgrade your current plan.
</span>
<button
onClick={handleClick}
className={classNames(
'no-border cursor-pointer rounded bg-info py-2 font-bold text-info-contrast hover:brightness-125 focus:brightness-125',
preferHorizontalLayout ? 'w-full px-4 md:ml-auto md:w-auto' : 'my-2 w-full',
)}
{!featureName && (
<span>
To take advantage of all the advanced features Standard Notes has to offer, upgrade your current plan.
</span>
)}
{application.isNativeIOS() && (
<div className="mt-2">
<div className="mb-2 font-bold">The Professional Plan costs $119.99/year and includes benefits like</div>
<ul className="list-inside list-[circle]">
<li>100GB encrypted file storage</li>
<li>
Access to all note types, including Super, markdown, rich text, authenticator, tasks, and spreadsheets
</li>
<li>Access to Daily Notebooks and Moments journals</li>
<li>Note history going back indefinitely</li>
<li>Nested folders for your tags</li>
<li>Premium support</li>
</ul>
</div>
)}
</div>
<div className="p-4">
<button
onClick={handleClick}
className="no-border w-full cursor-pointer rounded bg-info py-2 font-bold text-info-contrast hover:brightness-125 focus:brightness-125"
ref={ctaRef}
>
Upgrade
</button>
</div>
</>
ref={ctaRef}
>
Upgrade
</button>
</div>
)
}

View File

@@ -18,6 +18,7 @@ const StyledTooltip = ({
interactive = false,
type = 'label',
side,
documentElement,
...props
}: {
children: ReactNode
@@ -28,6 +29,7 @@ const StyledTooltip = ({
interactive?: boolean
type?: TooltipStoreProps['type']
side?: PopoverSide
documentElement?: HTMLElement
} & Partial<TooltipOptions>) => {
const [forceOpen, setForceOpen] = useState<boolean | undefined>()
@@ -123,15 +125,15 @@ const StyledTooltip = ({
popoverElement.style.pointerEvents = 'none'
}
const documentElement = document.querySelector('.main-ui-view')
const documentElementForPopover = documentElement || document.querySelector('.main-ui-view')
if (!popoverElement || !anchorElement || !documentElement || !open) {
if (!popoverElement || !anchorElement || !documentElementForPopover || !open) {
return
}
const anchorRect = anchorElement.getBoundingClientRect()
const popoverRect = popoverElement.getBoundingClientRect()
const documentRect = documentElement.getBoundingClientRect()
const documentRect = documentElementForPopover.getBoundingClientRect()
const styles = getPositionedPopoverStyles({
align: 'center',
@@ -151,7 +153,11 @@ const StyledTooltip = ({
Object.assign(popoverElement.style, styles)
if (!props.portal) {
const adjustedStyles = getAdjustedStylesForNonPortalPopover(popoverElement, styles)
const adjustedStyles = getAdjustedStylesForNonPortalPopover(
popoverElement,
styles,
props.portalElement instanceof HTMLElement ? props.portalElement : undefined,
)
popoverElement.style.setProperty('--translate-x', adjustedStyles['--translate-x'])
popoverElement.style.setProperty('--translate-y', adjustedStyles['--translate-y'])
}

View File

@@ -28,6 +28,13 @@ import TableActionMenuPlugin from './Plugins/TableCellActionMenuPlugin'
import ToolbarPlugin from './Plugins/ToolbarPlugin/ToolbarPlugin'
import { useMediaQuery, MutuallyExclusiveMediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
import { CheckListPlugin } from './Plugins/List/CheckListPlugin'
import RemoteImagePlugin from './Plugins/RemoteImagePlugin/RemoteImagePlugin'
import CodeOptionsPlugin from './Plugins/CodeOptionsPlugin/CodeOptions'
import { SuperSearchContextProvider } from './Plugins/SearchPlugin/Context'
import { SearchPlugin } from './Plugins/SearchPlugin/SearchPlugin'
import AutoLinkPlugin from './Plugins/AutoLinkPlugin/AutoLinkPlugin'
import DatetimePlugin from './Plugins/DateTimePlugin/DateTimePlugin'
import PasswordPlugin from './Plugins/PasswordPlugin/PasswordPlugin'
type BlocksEditorProps = {
onChange?: (value: string, preview: string) => void
@@ -83,7 +90,10 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
<div className="editor z-0 overflow-hidden" ref={onRef}>
<ContentEditable
id={SuperEditorContentId}
className={classNames('ContentEditable__root overflow-y-auto', className)}
className={classNames(
'ContentEditable__root relative overflow-y-auto p-4 text-[length:--font-size] leading-[--line-height] focus:shadow-none focus:outline-none',
className,
)}
spellCheck={spellcheck}
/>
<div className="search-highlight-container pointer-events-none absolute left-0 top-0 h-full w-full" />
@@ -116,6 +126,14 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
<CollapsiblePlugin />
<TabIndentationPlugin />
<RemoveBrokenTablesPlugin />
<RemoteImagePlugin />
<CodeOptionsPlugin />
<SuperSearchContextProvider>
<SearchPlugin />
</SuperSearchContextProvider>
<DatetimePlugin />
<PasswordPlugin />
<AutoLinkPlugin />
{!readonly && floatingAnchorElem && (
<>
<DraggableBlockPlugin anchorElem={floatingAnchorElem} />

View File

@@ -1,11 +1,11 @@
import { FunctionComponent } from 'react'
import { LexicalComposer } from '@lexical/react/LexicalComposer'
import { LexicalComposer, InitialEditorStateType } from '@lexical/react/LexicalComposer'
import BlocksEditorTheme from './Lexical/Theme/Theme'
import { BlockEditorNodes } from './Lexical/Nodes/AllNodes'
import { Klass, LexicalNode } from 'lexical'
type BlocksEditorComposerProps = {
initialValue: string | undefined
initialValue: InitialEditorStateType | undefined
children: React.ReactNode
nodes?: Array<Klass<LexicalNode>>
readonly?: boolean
@@ -24,7 +24,7 @@ export const BlocksEditorComposer: FunctionComponent<BlocksEditorComposerProps>
theme: BlocksEditorTheme,
editable: !readonly,
onError: (error: Error) => console.error(error),
editorState: initialValue && initialValue.length > 0 ? initialValue : undefined,
editorState: initialValue,
nodes: [...nodes, ...BlockEditorNodes],
}}
>

View File

@@ -11,7 +11,7 @@
display: flex;
justify-content: center;
align-items: center;
position: fixed;
position: absolute;
flex-direction: column;
top: 0px;
bottom: 0px;

View File

@@ -6,9 +6,10 @@
*
*/
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import './Modal.css'
import { ReactNode, useEffect, useRef } from 'react'
import { ReactNode, useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
function PortalImpl({
@@ -83,11 +84,22 @@ export default function Modal({
closeOnClickOutside?: boolean
onClose: () => void
title: string
}): JSX.Element {
}): ReactNode {
const [containerElement, setContainerElement] = useState<HTMLElement | undefined>()
const [editor] = useLexicalComposerContext()
useEffect(() => {
setContainerElement(editor.getRootElement()?.parentElement ?? document.body)
}, [editor])
if (!containerElement) {
return null
}
return createPortal(
<PortalImpl onClose={onClose} title={title} closeOnClickOutside={closeOnClickOutside}>
{children}
</PortalImpl>,
document.body,
containerElement,
)
}

View File

@@ -19,7 +19,7 @@ export function BlockPickerMenuItem({
<li
key={option.key}
tabIndex={-1}
className={`border-bottom gap-3 border-[0.5px] border-border ${PopoverItemClassNames} ${
className={`gap-3 border-b-[0.5px] border-border ${PopoverItemClassNames} ${
isSelected ? PopoverItemSelectedClassNames : ''
}`}
ref={option.setRefElement}

View File

@@ -30,7 +30,7 @@ import { GetDividerBlockOption } from '../Blocks/Divider'
import { GetCollapsibleBlockOption } from '../Blocks/Collapsible'
import { GetEmbedsBlockOptions } from '../Blocks/Embeds'
export default function BlockPickerMenuPlugin(): JSX.Element {
export default function BlockPickerMenuPlugin({ popoverZIndex }: { popoverZIndex?: string }): JSX.Element {
const [editor] = useLexicalComposerContext()
const application = useApplication()
const [modal, showModal] = useModal()
@@ -130,6 +130,7 @@ export default function BlockPickerMenuPlugin(): JSX.Element {
disableMobileFullscreenTakeover={true}
side={isMobileScreen() ? 'top' : 'bottom'}
maxHeight={(mh) => mh / 2}
overrideZIndex={popoverZIndex}
>
<ul>
{options.map((option, i: number) => (

View File

@@ -113,8 +113,19 @@ const ToolbarButton = forwardRef(
) => {
const [editor] = useLexicalComposerContext()
const isMobile = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
const parentElement = editor.getRootElement()?.parentElement ?? document.body
return (
<StyledTooltip showOnMobile showOnHover label={name} side="top">
<StyledTooltip
showOnMobile
showOnHover
label={name}
side="top"
portal={false}
portalElement={isMobile ? parentElement : undefined}
documentElement={parentElement}
>
<ToolbarItem
className={classNames(
'flex select-none items-center justify-center rounded p-0.5 focus:shadow-none focus:outline-none enabled:hover:bg-default enabled:focus-visible:bg-default disabled:opacity-50 md:border md:border-transparent enabled:hover:md:translucent-ui:border-[--popover-border-color]',
@@ -572,6 +583,8 @@ const ToolbarPlugin = () => {
toolbarStore,
])
const popoverDocumentElement = editor.getRootElement()?.parentElement ?? document.body
return (
<>
{modal}
@@ -746,6 +759,8 @@ const ToolbarPlugin = () => {
className="py-1"
disableMobileFullscreenTakeover
disableFlip
portal={false}
documentElement={isMobile ? popoverDocumentElement : undefined}
>
<div className="mb-1.5 mt-1 px-3 text-sm font-semibold uppercase text-text">Table of Contents</div>
<LexicalTableOfContents>
@@ -800,6 +815,8 @@ const ToolbarPlugin = () => {
disableMobileFullscreenTakeover
disableFlip
containerClassName="md:!min-w-60 md:!w-auto"
portal={false}
documentElement={isMobile ? popoverDocumentElement : undefined}
>
<Menu a11yLabel="Text formatting options" className="!px-0" onClick={() => setIsTextFormatMenuOpen(false)}>
<ToolbarMenuItem
@@ -839,6 +856,8 @@ const ToolbarPlugin = () => {
disableMobileFullscreenTakeover
disableFlip
containerClassName="md:!min-w-60 md:!w-auto"
portal={false}
documentElement={isMobile ? popoverDocumentElement : undefined}
>
<Menu a11yLabel="Text style" className="!px-0" onClick={() => setIsTextStyleMenuOpen(false)}>
<ToolbarMenuItem
@@ -910,6 +929,8 @@ const ToolbarPlugin = () => {
disableMobileFullscreenTakeover
disableFlip
containerClassName="md:!min-w-60 md:!w-auto"
portal={false}
documentElement={isMobile ? popoverDocumentElement : undefined}
>
<Menu a11yLabel="Alignment" className="!px-0" onClick={() => setIsAlignmentMenuOpen(false)}>
<ToolbarMenuItem
@@ -949,6 +970,8 @@ const ToolbarPlugin = () => {
disableMobileFullscreenTakeover
disableFlip
containerClassName="md:!min-w-60 md:!w-auto"
portal={false}
documentElement={isMobile ? popoverDocumentElement : undefined}
>
<Menu a11yLabel="Insert" className="!px-0" onClick={() => setIsInsertMenuOpen(false)}>
<ToolbarMenuItem

View File

@@ -1,6 +1,5 @@
import { WebApplication } from '@/Application/WebApplication'
import {
classNames,
isPayloadSourceRetrieved,
PrefKey,
NativeFeatureIdentifier,
@@ -14,7 +13,6 @@ import { BlocksEditorComposer } from './BlocksEditorComposer'
import { ItemSelectionPlugin } from './Plugins/ItemSelectionPlugin/ItemSelectionPlugin'
import { FileNode } from './Plugins/EncryptedFilePlugin/Nodes/FileNode'
import FilePlugin from './Plugins/EncryptedFilePlugin/FilePlugin'
import BlockPickerMenuPlugin from './Plugins/BlockPickerPlugin/BlockPickerPlugin'
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
import { LinkingController } from '@/Controllers/LinkingController'
import LinkingControllerProvider from '../../Controllers/LinkingControllerProvider'
@@ -23,28 +21,22 @@ import ItemBubblePlugin from './Plugins/ItemBubblePlugin/ItemBubblePlugin'
import { NodeObserverPlugin } from './Plugins/NodeObserverPlugin/NodeObserverPlugin'
import { FilesController } from '@/Controllers/FilesController'
import FilesControllerProvider from '@/Controllers/FilesControllerProvider'
import DatetimePlugin from './Plugins/DateTimePlugin/DateTimePlugin'
import AutoLinkPlugin from './Plugins/AutoLinkPlugin/AutoLinkPlugin'
import { NoteViewController } from '../NoteView/Controller/NoteViewController'
import {
ChangeContentCallbackPlugin,
ChangeEditorFunction,
} from './Plugins/ChangeContentCallback/ChangeContentCallback'
import PasswordPlugin from './Plugins/PasswordPlugin/PasswordPlugin'
import { useCommandService } from '@/Components/CommandProvider'
import { SUPER_SHOW_MARKDOWN_PREVIEW } from '@standardnotes/ui-services'
import { SuperNoteMarkdownPreview } from './SuperNoteMarkdownPreview'
import GetMarkdownPlugin, { GetMarkdownPluginInterface } from './Plugins/GetMarkdownPlugin/GetMarkdownPlugin'
import { useResponsiveEditorFontSize } from '@/Utils/getPlaintextFontSize'
import ReadonlyPlugin from './Plugins/ReadonlyPlugin/ReadonlyPlugin'
import { SuperSearchContextProvider } from './Plugins/SearchPlugin/Context'
import { SearchPlugin } from './Plugins/SearchPlugin/SearchPlugin'
import ModalOverlay from '@/Components/Modal/ModalOverlay'
import CodeOptionsPlugin from './Plugins/CodeOptionsPlugin/CodeOptions'
import RemoteImagePlugin from './Plugins/RemoteImagePlugin/RemoteImagePlugin'
import NotEntitledBanner from '../ComponentView/NotEntitledBanner'
import AutoFocusPlugin from './Plugins/AutoFocusPlugin'
import usePreference from '@/Hooks/usePreference'
import BlockPickerMenuPlugin from './Plugins/BlockPickerPlugin/BlockPickerPlugin'
export const SuperNotePreviewCharLimit = 160
@@ -207,10 +199,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
<BlocksEditorComposer readonly={note.current.locked || readonly} initialValue={note.current.text}>
<BlocksEditor
onChange={handleChange}
className={classNames(
'blocks-editor relative h-full resize-none px-4 py-4 text-[length:--font-size] focus:shadow-none focus:outline-none',
lineHeight && 'leading-[--line-height]',
)}
className="blocks-editor h-full resize-none"
previewLength={SuperNotePreviewCharLimit}
spellcheck={spellcheck}
readonly={note.current.locked || readonly}
@@ -218,11 +207,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
<ItemSelectionPlugin currentNote={note.current} />
<FilePlugin currentNote={note.current} />
<ItemBubblePlugin />
<BlockPickerMenuPlugin />
<GetMarkdownPlugin ref={getMarkdownPlugin} />
<DatetimePlugin />
<PasswordPlugin />
<AutoLinkPlugin />
<ChangeContentCallbackPlugin
providerCallback={(callback) => (changeEditorFunction.current = callback)}
/>
@@ -230,11 +215,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
<NodeObserverPlugin nodeType={FileNode} onRemove={handleBubbleRemove} />
{readonly === undefined && <ReadonlyPlugin note={note.current} />}
<AutoFocusPlugin isEnabled={controller.isTemplateNote} />
<SuperSearchContextProvider>
<SearchPlugin />
</SuperSearchContextProvider>
<CodeOptionsPlugin />
<RemoteImagePlugin />
<BlockPickerMenuPlugin />
</BlocksEditor>
</BlocksEditorComposer>
</FilesControllerProvider>

View File

@@ -155,7 +155,11 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface {
return content
}
convertOtherFormatToSuperString(otherFormatString: string, fromFormat: 'txt' | 'md' | 'html' | 'json'): string {
convertOtherFormatToSuperString: SuperConverterServiceInterface['convertOtherFormatToSuperString'] = (
otherFormatString,
fromFormat,
options,
) => {
if (otherFormatString.length === 0) {
return otherFormatString
}
@@ -175,6 +179,10 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface {
let didThrow = false
if (fromFormat === 'html') {
const htmlOptions = options?.html || {
addLineBreaks: true,
}
this.importEditor.update(
() => {
try {
@@ -203,7 +211,9 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface {
nodesToInsert.push(node)
}
nodesToInsert.push($createParagraphNode())
if (htmlOptions.addLineBreaks) {
nodesToInsert.push($createParagraphNode())
}
})
$getRoot().selectEnd()
$insertNodes(nodesToInsert.concat($createParagraphNode()))

View File

@@ -103,6 +103,10 @@ export class FeaturesController extends AbstractViewController implements Intern
this.premiumAlertType = PremiumFeatureModalType.UpgradeSuccess
}
showSuperDemoModal = () => {
this.premiumAlertType = PremiumFeatureModalType.SuperDemo
}
public closePremiumAlert() {
this.premiumAlertType = undefined
}

View File

@@ -1,11 +1,14 @@
import { WebApplication } from '@/Application/WebApplication'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, createContext, useCallback, useContext, ReactNode } from 'react'
import { FunctionComponent, createContext, useCallback, useContext, ReactNode, useMemo } from 'react'
import PremiumFeaturesModal from '@/Components/PremiumFeaturesModal/PremiumFeaturesModal'
import ModalOverlay from '@/Components/Modal/ModalOverlay'
import { classNames } from '@standardnotes/snjs'
import { PremiumFeatureModalType } from '@/Components/PremiumFeaturesModal/PremiumFeatureModalType'
type PremiumModalContextData = {
activate: (featureName: string) => void
showSuperDemo: () => void
}
const PremiumModalContext = createContext<PremiumModalContextData | null>(null)
@@ -43,12 +46,23 @@ const PremiumModalProvider: FunctionComponent<Props> = observer(({ application,
application.featuresController.closePremiumAlert()
}, [application.featuresController])
const showSuperDemo = useCallback(() => {
application.featuresController.showSuperDemoModal()
}, [application.featuresController])
const value: PremiumModalContextData = useMemo(() => ({ activate, showSuperDemo }), [activate, showSuperDemo])
return (
<>
<ModalOverlay
isOpen={application.featuresController.premiumAlertType != undefined}
close={close}
className="w-full max-w-[90vw] !h-auto md:max-w-89"
className={classNames(
'w-full',
application.featuresController.premiumAlertType === PremiumFeatureModalType.SuperDemo
? 'md:!h-full md:w-full md:max-w-[70vw]'
: '!h-auto max-w-[90vw] md:max-w-89',
)}
backdropClassName="!opacity-50"
>
<PremiumFeaturesModal
@@ -59,7 +73,7 @@ const PremiumModalProvider: FunctionComponent<Props> = observer(({ application,
type={application.featuresController.premiumAlertType!}
/>
</ModalOverlay>
<PremiumModalProvider_ value={{ activate }}>{children}</PremiumModalProvider_>
<PremiumModalProvider_ value={value}>{children}</PremiumModalProvider_>
</>
)
})