fix: show files option in linking menu even if not entitled

This commit is contained in:
Mo
2022-11-13 15:10:57 -06:00
parent 2b76d48392
commit f42657fa9e
10 changed files with 216 additions and 183 deletions

View File

@@ -39,6 +39,7 @@ import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { setCustomViewportHeight } from '@/setViewportHeightWithFallback'
import { WebServices } from './WebServices'
import { FeatureName } from '@/Controllers/FeatureName'
export type WebEventObserver = (event: WebAppEvent, data?: unknown) => void
@@ -347,6 +348,14 @@ export class WebApplication extends SNApplication implements WebApplicationInter
return this.hasValidSubscription()
}
get entitledToFiles(): boolean {
return this.getViewControllerManager().featuresController.entitledToFiles
}
showPremiumModal(featureName: FeatureName): void {
void this.getViewControllerManager().featuresController.showPremiumAlert(featureName)
}
hasValidSubscription(): boolean {
return this.getViewControllerManager().subscriptionController.hasValidSubscription()
}

View File

@@ -28,6 +28,7 @@ import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider
import { LinkingController } from '@/Controllers/LinkingController'
import DailyContentList from './Daily/DailyContentList'
import { ListableContentItem } from './Types/ListableContentItem'
import { FeatureName } from '@/Controllers/FeatureName'
type Props = {
accountMenuController: AccountMenuController
@@ -123,6 +124,11 @@ const ContentListView: FunctionComponent<Props> = ({
const addNewItem = useCallback(async () => {
if (isFilesSmartView) {
if (!application.entitledToFiles) {
application.showPremiumModal(FeatureName.Files)
return
}
if (StreamingFileReader.available()) {
void filesController.uploadNewFile()
return
@@ -133,7 +139,7 @@ const ContentListView: FunctionComponent<Props> = ({
await createNewNote()
toggleAppPane(AppPaneId.Editor)
}
}, [isFilesSmartView, filesController, createNewNote, toggleAppPane])
}, [isFilesSmartView, filesController, createNewNote, toggleAppPane, application])
useEffect(() => {
/**

View File

@@ -153,7 +153,7 @@ const ContentListHeader = ({
}, [OptionsMenu, AddButton, FolderName])
return (
<div className="section-title-bar-header items-start gap-1 overflow-hidden">
<div className="section-title-bar-header items-start gap-1">
{!isTablet && PhoneAndDesktopLayout}
{isTablet && TabletLayout}
</div>

View File

@@ -188,7 +188,7 @@ const FileDragNDropProvider = ({ application, children, featuresController, file
resetState()
if (!featuresController.hasFiles) {
if (!featuresController.entitledToFiles) {
premiumModal.activate('Files')
return
}
@@ -217,7 +217,7 @@ const FileDragNDropProvider = ({ application, children, featuresController, file
dragCounter.current = 0
}
},
[application, featuresController.hasFiles, filesController, premiumModal, resetState],
[application, featuresController.entitledToFiles, filesController, premiumModal, resetState],
)
useEffect(() => {

View File

@@ -1,156 +1,17 @@
import { FeatureName } from '@/Controllers/FeatureName'
import { FeaturesController } from '@/Controllers/FeaturesController'
import { FilesController } from '@/Controllers/FilesController'
import { LinkingController } from '@/Controllers/LinkingController'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { formatDateForContextMenu } from '@/Utils/DateUtils'
import { getIconForItem } from '@/Utils/Items/Icons/getIconForItem'
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import { formatSizeToReadableString } from '@standardnotes/filepicker'
import { FileItem } from '@standardnotes/snjs'
import { KeyboardKey } from '@standardnotes/ui-services'
import { observer } from 'mobx-react-lite'
import { ChangeEventHandler, useEffect, useRef, useState } from 'react'
import { useApplication } from '../ApplicationView/ApplicationProvider'
import { PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
import ClearInputButton from '../ClearInputButton/ClearInputButton'
import Icon from '../Icon/Icon'
import DecoratedInput from '../Input/DecoratedInput'
import MenuItem from '../Menu/MenuItem'
import { MenuItemType } from '../Menu/MenuItemType'
import Popover from '../Popover/Popover'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import LinkedFileMenuOptions from './LinkedFileMenuOptions'
import LinkedItemMeta from './LinkedItemMeta'
import LinkedItemSearchResults from './LinkedItemSearchResults'
const LinkedItemsSectionItem = ({
activateItem,
item,
searchQuery,
unlinkItem,
handleFileAction,
}: {
activateItem: LinkingController['activateItem']
item: LinkableItem
searchQuery?: string
unlinkItem: () => void
handleFileAction: FilesController['handleFileAction']
}) => {
const menuButtonRef = useRef<HTMLButtonElement>(null)
const application = useApplication()
const [isMenuOpen, setIsMenuOpen] = useState(false)
const toggleMenu = () => setIsMenuOpen((open) => !open)
const [isRenamingFile, setIsRenamingFile] = useState(false)
const [icon, className] = getIconForItem(item, application)
const title = item.title ?? ''
const renameFile = async (name: string) => {
if (!(item instanceof FileItem)) {
return
}
await handleFileAction({
type: PopoverFileItemActionType.RenameFile,
payload: {
file: item,
name: name,
},
})
setIsRenamingFile(false)
}
return (
<div className="relative flex items-center justify-between">
{isRenamingFile && item instanceof FileItem ? (
<div className="flex flex-grow items-center gap-4 py-2 pl-3 pr-12">
<Icon type={icon} className={classNames('flex-shrink-0', className)} />
<input
className="min-w-0 flex-grow text-sm"
defaultValue={title}
onKeyDown={(event) => {
if (event.key === KeyboardKey.Escape) {
setIsRenamingFile(false)
} else if (event.key === KeyboardKey.Enter) {
const newTitle = event.currentTarget.value
void renameFile(newTitle)
}
}}
ref={(node) => {
if (node) {
node.focus()
}
}}
/>
</div>
) : (
<button
className="flex max-w-full flex-grow items-center justify-between gap-4 py-2 pl-3 pr-12 text-sm hover:bg-info-backdrop focus:bg-info-backdrop"
onClick={() => activateItem(item)}
onContextMenu={(event) => {
event.preventDefault()
toggleMenu()
}}
>
<LinkedItemMeta item={item} searchQuery={searchQuery} />
</button>
)}
<button
className="absolute right-3 top-1/2 h-7 w-7 -translate-y-1/2 cursor-pointer rounded-full border-0 bg-transparent p-1 hover:bg-contrast"
onClick={toggleMenu}
ref={menuButtonRef}
>
<Icon type="more" className="text-neutral" />
</button>
<Popover
open={isMenuOpen}
togglePopover={toggleMenu}
anchorElement={menuButtonRef.current}
side="bottom"
align="center"
className="py-2"
>
<MenuItem
type={MenuItemType.IconButton}
onClick={() => {
unlinkItem()
toggleMenu()
}}
>
<Icon type="link-off" className="mr-2 text-danger" />
Unlink
</MenuItem>
{item instanceof FileItem && (
<LinkedFileMenuOptions
file={item}
closeMenu={toggleMenu}
handleFileAction={handleFileAction}
setIsRenamingFile={setIsRenamingFile}
/>
)}
<HorizontalSeparator classes="my-2" />
<div className="mt-1 px-3 py-1 text-xs font-medium text-neutral">
<div className="mb-1">
<span className="font-semibold">Created at:</span> {formatDateForContextMenu(item.created_at)}
</div>
<div className="mb-1">
<span className="font-semibold">Modified at:</span> {formatDateForContextMenu(item.userModifiedDate)}
</div>
<div className="mb-1">
<span className="font-semibold">ID:</span> {item.uuid}
</div>
{item instanceof FileItem && (
<div>
<span className="font-semibold">Size:</span> {formatSizeToReadableString(item.decryptedSize)}
</div>
)}
</div>
</Popover>
</div>
)
}
import { LinkedItemsSectionItem } from './LinkedItemsSectionItem'
const LinkedItemsPanel = ({
linkingController,
@@ -178,7 +39,7 @@ const LinkedItemsPanel = ({
activeItem,
} = linkingController
const { hasFiles } = featuresController
const { entitledToFiles } = featuresController
const application = useApplication()
const fileInputRef = useRef<HTMLInputElement | null>(null)
@@ -214,6 +75,11 @@ const LinkedItemsPanel = ({
}
const selectAndUploadFiles = () => {
if (!entitledToFiles) {
void featuresController.showPremiumAlert(FeatureName.Files)
return
}
if (!fileInputRef.current) {
return
}
@@ -307,37 +173,37 @@ const LinkedItemsPanel = ({
</div>
</div>
)}
{(!!linkedFiles.length || hasFiles) && (
<div>
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Linked Files</div>
<div className="my-1">
<input
type="file"
className="absolute top-0 left-0 -z-50 h-px w-px opacity-0"
multiple
ref={fileInputRef}
onChange={handleFileInputChange}
<div>
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Linked Files</div>
<div className="my-1">
<input
type="file"
className="absolute top-0 left-0 -z-50 h-px w-px opacity-0"
multiple
ref={fileInputRef}
onChange={handleFileInputChange}
/>
<button
className="flex w-full cursor-pointer items-center gap-3 bg-transparent px-3 py-2 text-left text-base text-text hover:bg-info-backdrop hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
onClick={selectAndUploadFiles}
>
<Icon type="add" />
Upload and link file(s)
</button>
{linkedFiles.map((link) => (
<LinkedItemsSectionItem
key={link.id}
item={link.item}
searchQuery={searchQuery}
unlinkItem={() => unlinkItemFromSelectedItem(link.item)}
activateItem={activateItem}
handleFileAction={filesController.handleFileAction}
/>
<button
className="flex w-full cursor-pointer items-center gap-3 bg-transparent px-3 py-2 text-left text-base text-text hover:bg-info-backdrop hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
onClick={selectAndUploadFiles}
>
<Icon type="add" />
Upload and link file(s)
</button>
{linkedFiles.map((link) => (
<LinkedItemsSectionItem
key={link.id}
item={link.item}
searchQuery={searchQuery}
unlinkItem={() => unlinkItemFromSelectedItem(link.item)}
activateItem={activateItem}
handleFileAction={filesController.handleFileAction}
/>
))}
</div>
))}
</div>
)}
</div>
{!!filesLinkingToActiveItem.length && (
<div>
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">

View File

@@ -0,0 +1,147 @@
import { FilesController } from '@/Controllers/FilesController'
import { LinkingController } from '@/Controllers/LinkingController'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { formatDateForContextMenu } from '@/Utils/DateUtils'
import { getIconForItem } from '@/Utils/Items/Icons/getIconForItem'
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import { formatSizeToReadableString } from '@standardnotes/filepicker'
import { FileItem } from '@standardnotes/snjs'
import { KeyboardKey } from '@standardnotes/ui-services'
import { useRef, useState } from 'react'
import { useApplication } from '../ApplicationView/ApplicationProvider'
import { PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
import Icon from '../Icon/Icon'
import MenuItem from '../Menu/MenuItem'
import { MenuItemType } from '../Menu/MenuItemType'
import Popover from '../Popover/Popover'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import LinkedFileMenuOptions from './LinkedFileMenuOptions'
import LinkedItemMeta from './LinkedItemMeta'
export const LinkedItemsSectionItem = ({
activateItem,
item,
searchQuery,
unlinkItem,
handleFileAction,
}: {
activateItem: LinkingController['activateItem']
item: LinkableItem
searchQuery?: string
unlinkItem: () => void
handleFileAction: FilesController['handleFileAction']
}) => {
const menuButtonRef = useRef<HTMLButtonElement>(null)
const application = useApplication()
const [isMenuOpen, setIsMenuOpen] = useState(false)
const toggleMenu = () => setIsMenuOpen((open) => !open)
const [isRenamingFile, setIsRenamingFile] = useState(false)
const [icon, className] = getIconForItem(item, application)
const title = item.title ?? ''
const renameFile = async (name: string) => {
if (!(item instanceof FileItem)) {
return
}
await handleFileAction({
type: PopoverFileItemActionType.RenameFile,
payload: {
file: item,
name: name,
},
})
setIsRenamingFile(false)
}
return (
<div className="relative flex items-center justify-between">
{isRenamingFile && item instanceof FileItem ? (
<div className="flex flex-grow items-center gap-4 py-2 pl-3 pr-12">
<Icon type={icon} className={classNames('flex-shrink-0', className)} />
<input
className="min-w-0 flex-grow text-sm"
defaultValue={title}
onKeyDown={(event) => {
if (event.key === KeyboardKey.Escape) {
setIsRenamingFile(false)
} else if (event.key === KeyboardKey.Enter) {
const newTitle = event.currentTarget.value
void renameFile(newTitle)
}
}}
ref={(node) => {
if (node) {
node.focus()
}
}}
/>
</div>
) : (
<button
className="flex max-w-full flex-grow items-center justify-between gap-4 py-2 pl-3 pr-12 text-sm hover:bg-info-backdrop focus:bg-info-backdrop"
onClick={() => activateItem(item)}
onContextMenu={(event) => {
event.preventDefault()
toggleMenu()
}}
>
<LinkedItemMeta item={item} searchQuery={searchQuery} />
</button>
)}
<button
className="absolute right-3 top-1/2 h-7 w-7 -translate-y-1/2 cursor-pointer rounded-full border-0 bg-transparent p-1 hover:bg-contrast"
onClick={toggleMenu}
ref={menuButtonRef}
>
<Icon type="more" className="text-neutral" />
</button>
<Popover
open={isMenuOpen}
togglePopover={toggleMenu}
anchorElement={menuButtonRef.current}
side="bottom"
align="center"
className="py-2"
>
<MenuItem
type={MenuItemType.IconButton}
onClick={() => {
unlinkItem()
toggleMenu()
}}
>
<Icon type="link-off" className="mr-2 text-danger" />
Unlink
</MenuItem>
{item instanceof FileItem && (
<LinkedFileMenuOptions
file={item}
closeMenu={toggleMenu}
handleFileAction={handleFileAction}
setIsRenamingFile={setIsRenamingFile}
/>
)}
<HorizontalSeparator classes="my-2" />
<div className="mt-1 px-3 py-1 text-xs font-medium text-neutral">
<div className="mb-1">
<span className="font-semibold">Created at:</span> {formatDateForContextMenu(item.created_at)}
</div>
<div className="mb-1">
<span className="font-semibold">Modified at:</span> {formatDateForContextMenu(item.userModifiedDate)}
</div>
<div className="mb-1">
<span className="font-semibold">ID:</span> {item.uuid}
</div>
{item instanceof FileItem && (
<div>
<span className="font-semibold">Size:</span> {formatSizeToReadableString(item.decryptedSize)}
</div>
)}
</div>
</Popover>
</div>
)
}

View File

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

View File

@@ -5,10 +5,11 @@ import { WebApplication } from '@/Application/Application'
import { openSubscriptionDashboard } from '@/Utils/ManageSubscription'
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
import { PremiumFeatureModalType } from './PremiumFeatureModalType'
import { FeatureName } from '@/Controllers/FeatureName'
type Props = {
application: WebApplication
featureName: string
featureName: FeatureName | string
hasSubscription: boolean
hasAccount: boolean
onClose: () => void

View File

@@ -0,0 +1,3 @@
export enum FeatureName {
Files = 'Encrypted File Storage',
}

View File

@@ -1,3 +1,4 @@
import { FeatureName } from './FeatureName'
import { WebApplication } from '@/Application/Application'
import { PremiumFeatureModalType } from '@/Components/PremiumFeaturesModal/PremiumFeatureModalType'
import { destroyAllObjectProperties } from '@/Utils'
@@ -15,7 +16,7 @@ import { CrossControllerEvent } from './CrossControllerEvent'
export class FeaturesController extends AbstractViewController {
hasFolders: boolean
hasSmartViews: boolean
hasFiles: boolean
entitledToFiles: boolean
premiumAlertFeatureName: string | undefined
premiumAlertType: PremiumFeatureModalType | undefined = undefined
@@ -25,7 +26,7 @@ export class FeaturesController extends AbstractViewController {
;(this.closePremiumAlert as unknown) = undefined
;(this.hasFolders as unknown) = undefined
;(this.hasSmartViews as unknown) = undefined
;(this.hasFiles as unknown) = undefined
;(this.entitledToFiles as unknown) = undefined
;(this.premiumAlertFeatureName as unknown) = undefined
;(this.premiumAlertType as unknown) = undefined
@@ -37,7 +38,7 @@ export class FeaturesController extends AbstractViewController {
this.hasFolders = this.isEntitledToFolders()
this.hasSmartViews = this.isEntitledToSmartViews()
this.hasFiles = this.isEntitledToFiles()
this.entitledToFiles = this.isEntitledToFiles()
this.premiumAlertFeatureName = undefined
eventBus.addEventHandler(this, CrossControllerEvent.DisplayPremiumModal)
@@ -45,7 +46,7 @@ export class FeaturesController extends AbstractViewController {
makeObservable(this, {
hasFolders: observable,
hasSmartViews: observable,
hasFiles: observable,
entitledToFiles: observable,
premiumAlertType: observable,
premiumAlertFeatureName: observable,
showPremiumAlert: action,
@@ -67,7 +68,7 @@ export class FeaturesController extends AbstractViewController {
runInAction(() => {
this.hasFolders = this.isEntitledToFolders()
this.hasSmartViews = this.isEntitledToSmartViews()
this.hasFiles = this.isEntitledToFiles()
this.entitledToFiles = this.isEntitledToFiles()
})
}
}),
@@ -81,7 +82,7 @@ export class FeaturesController extends AbstractViewController {
}
}
public async showPremiumAlert(featureName: string): Promise<void> {
public async showPremiumAlert(featureName: FeatureName | string): Promise<void> {
this.premiumAlertFeatureName = featureName
this.premiumAlertType = PremiumFeatureModalType.UpgradePrompt