From f42657fa9e01b3b8a4ca555ea5c06236503b40a4 Mon Sep 17 00:00:00 2001 From: Mo Date: Sun, 13 Nov 2022 15:10:57 -0600 Subject: [PATCH] fix: show files option in linking menu even if not entitled --- .../javascripts/Application/Application.ts | 9 + .../ContentListView/ContentListView.tsx | 8 +- .../Header/ContentListHeader.tsx | 2 +- .../FileDragNDropProvider.tsx | 4 +- .../LinkedItems/LinkedItemsPanel.tsx | 208 ++++-------------- .../LinkedItems/LinkedItemsSectionItem.tsx | 147 +++++++++++++ .../Panes/Account/AccountPreferences.tsx | 2 +- .../PremiumFeaturesModal.tsx | 3 +- .../javascripts/Controllers/FeatureName.ts | 3 + .../Controllers/FeaturesController.ts | 13 +- 10 files changed, 216 insertions(+), 183 deletions(-) create mode 100644 packages/web/src/javascripts/Components/LinkedItems/LinkedItemsSectionItem.tsx create mode 100644 packages/web/src/javascripts/Controllers/FeatureName.ts diff --git a/packages/web/src/javascripts/Application/Application.ts b/packages/web/src/javascripts/Application/Application.ts index 7cde51a4f..c01a3f8c5 100644 --- a/packages/web/src/javascripts/Application/Application.ts +++ b/packages/web/src/javascripts/Application/Application.ts @@ -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() } diff --git a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx index 91ac7c59d..af7d77570 100644 --- a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx @@ -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 = ({ 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 = ({ await createNewNote() toggleAppPane(AppPaneId.Editor) } - }, [isFilesSmartView, filesController, createNewNote, toggleAppPane]) + }, [isFilesSmartView, filesController, createNewNote, toggleAppPane, application]) useEffect(() => { /** diff --git a/packages/web/src/javascripts/Components/ContentListView/Header/ContentListHeader.tsx b/packages/web/src/javascripts/Components/ContentListView/Header/ContentListHeader.tsx index ce799f20d..63d1b709d 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Header/ContentListHeader.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/Header/ContentListHeader.tsx @@ -153,7 +153,7 @@ const ContentListHeader = ({ }, [OptionsMenu, AddButton, FolderName]) return ( -
+
{!isTablet && PhoneAndDesktopLayout} {isTablet && TabletLayout}
diff --git a/packages/web/src/javascripts/Components/FileDragNDropProvider/FileDragNDropProvider.tsx b/packages/web/src/javascripts/Components/FileDragNDropProvider/FileDragNDropProvider.tsx index 8cdc43020..4910c913d 100644 --- a/packages/web/src/javascripts/Components/FileDragNDropProvider/FileDragNDropProvider.tsx +++ b/packages/web/src/javascripts/Components/FileDragNDropProvider/FileDragNDropProvider.tsx @@ -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(() => { diff --git a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsPanel.tsx b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsPanel.tsx index fe6e6e5e2..a1048345e 100644 --- a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsPanel.tsx +++ b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsPanel.tsx @@ -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(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 ( -
- {isRenamingFile && item instanceof FileItem ? ( -
- - { - 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() - } - }} - /> -
- ) : ( - - )} - - - { - unlinkItem() - toggleMenu() - }} - > - - Unlink - - {item instanceof FileItem && ( - - )} - -
-
- Created at: {formatDateForContextMenu(item.created_at)} -
-
- Modified at: {formatDateForContextMenu(item.userModifiedDate)} -
-
- ID: {item.uuid} -
- {item instanceof FileItem && ( -
- Size: {formatSizeToReadableString(item.decryptedSize)} -
- )} -
-
-
- ) -} +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(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 = ({
)} - {(!!linkedFiles.length || hasFiles) && ( -
-
Linked Files
-
- +
Linked Files
+
+ + + {linkedFiles.map((link) => ( + unlinkItemFromSelectedItem(link.item)} + activateItem={activateItem} + handleFileAction={filesController.handleFileAction} /> - - {linkedFiles.map((link) => ( - unlinkItemFromSelectedItem(link.item)} - activateItem={activateItem} - handleFileAction={filesController.handleFileAction} - /> - ))} -
+ ))}
- )} +
+ {!!filesLinkingToActiveItem.length && (
diff --git a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsSectionItem.tsx b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsSectionItem.tsx new file mode 100644 index 000000000..7f3f71dcd --- /dev/null +++ b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsSectionItem.tsx @@ -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(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 ( +
+ {isRenamingFile && item instanceof FileItem ? ( +
+ + { + 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() + } + }} + /> +
+ ) : ( + + )} + + + { + unlinkItem() + toggleMenu() + }} + > + + Unlink + + {item instanceof FileItem && ( + + )} + +
+
+ Created at: {formatDateForContextMenu(item.created_at)} +
+
+ Modified at: {formatDateForContextMenu(item.userModifiedDate)} +
+
+ ID: {item.uuid} +
+ {item instanceof FileItem && ( +
+ Size: {formatSizeToReadableString(item.decryptedSize)} +
+ )} +
+
+
+ ) +} diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/AccountPreferences.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/AccountPreferences.tsx index a9b20192d..41af64f33 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/AccountPreferences.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/AccountPreferences.tsx @@ -29,7 +29,7 @@ const AccountPreferences = ({ application, viewControllerManager }: Props) => ( )} - {application.hasAccount() && viewControllerManager.featuresController.hasFiles && ( + {application.hasAccount() && viewControllerManager.featuresController.entitledToFiles && ( )} {application.hasAccount() && } diff --git a/packages/web/src/javascripts/Components/PremiumFeaturesModal/PremiumFeaturesModal.tsx b/packages/web/src/javascripts/Components/PremiumFeaturesModal/PremiumFeaturesModal.tsx index 0b66a7ac9..4b4b15719 100644 --- a/packages/web/src/javascripts/Components/PremiumFeaturesModal/PremiumFeaturesModal.tsx +++ b/packages/web/src/javascripts/Components/PremiumFeaturesModal/PremiumFeaturesModal.tsx @@ -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 diff --git a/packages/web/src/javascripts/Controllers/FeatureName.ts b/packages/web/src/javascripts/Controllers/FeatureName.ts new file mode 100644 index 000000000..fef41c61c --- /dev/null +++ b/packages/web/src/javascripts/Controllers/FeatureName.ts @@ -0,0 +1,3 @@ +export enum FeatureName { + Files = 'Encrypted File Storage', +} diff --git a/packages/web/src/javascripts/Controllers/FeaturesController.ts b/packages/web/src/javascripts/Controllers/FeaturesController.ts index 405ce154d..24f84460b 100644 --- a/packages/web/src/javascripts/Controllers/FeaturesController.ts +++ b/packages/web/src/javascripts/Controllers/FeaturesController.ts @@ -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 { + public async showPremiumAlert(featureName: FeatureName | string): Promise { this.premiumAlertFeatureName = featureName this.premiumAlertType = PremiumFeatureModalType.UpgradePrompt