import { WebApplicationGroup } from '@/Application/WebApplicationGroup' import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { useCallback, useEffect, useState } from 'react' import { AccountMenuPane } from '../AccountMenu/AccountMenuPane' import MenuPaneSelector from '../AccountMenu/MenuPaneSelector' import { useApplication } from '../ApplicationProvider' import Icon from '../Icon/Icon' import Menu from '../Menu/Menu' import MenuItem from '../Menu/MenuItem' import { storage as extensionStorage, windows } from 'webextension-polyfill' import sendMessageToActiveTab from '@standardnotes/clipper/src/utils/sendMessageToActiveTab' import { ClipPayload, RuntimeMessageTypes } from '@standardnotes/clipper/src/types/message' import { confirmDialog } from '@standardnotes/ui-services' import { ApplicationEvent, ContentType, DecryptedItem, FeatureIdentifier, FeatureStatus, NoteContent, NoteType, PrefKey, SNNote, SNTag, } from '@standardnotes/snjs' import { addToast, ToastType } from '@standardnotes/toast' import { getSuperJSONFromClipPayload } from './getSuperJSONFromClipHTML' import ClippedNoteView from './ClippedNoteView' import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon' import Button from '../Button/Button' import { openSubscriptionDashboard } from '@/Utils/ManageSubscription' import { useStateRef } from '@/Hooks/useStateRef' import usePreference from '@/Hooks/usePreference' import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem' import ItemSelectionDropdown from '../ItemSelectionDropdown/ItemSelectionDropdown' import LinkedItemBubble from '../LinkedItems/LinkedItemBubble' import StyledTooltip from '../StyledTooltip/StyledTooltip' import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem' const ClipperView = ({ viewControllerManager, applicationGroup, }: { viewControllerManager: ViewControllerManager applicationGroup: WebApplicationGroup }) => { const application = useApplication() const [currentWindow, setCurrentWindow] = useState>>() useEffect(() => { windows .getCurrent({ populate: true, }) .then((window) => { setCurrentWindow(window) }) .catch(console.error) }, []) const isFirefoxPopup = !!currentWindow && currentWindow.type === 'popup' && currentWindow.incognito === false const [user, setUser] = useState(() => application.getUser()) const [isEntitledToExtension, setIsEntitled] = useState( () => application.features.getFeatureStatus(FeatureIdentifier.Extension) === FeatureStatus.Entitled, ) const isEntitledRef = useStateRef(isEntitledToExtension) const hasSubscription = application.hasValidFirstPartySubscription() useEffect(() => { return application.addEventObserver(async (event) => { switch (event) { case ApplicationEvent.SignedIn: case ApplicationEvent.SignedOut: case ApplicationEvent.UserRolesChanged: setUser(application.getUser()) setIsEntitled(application.features.getFeatureStatus(FeatureIdentifier.Extension) === FeatureStatus.Entitled) break case ApplicationEvent.FeaturesAvailabilityChanged: setIsEntitled(application.features.getFeatureStatus(FeatureIdentifier.Extension) === FeatureStatus.Entitled) break } }) }, [application]) const defaultTagId = usePreference(PrefKey.ClipperDefaultTagUuid) const [defaultTag, setDefaultTag] = useState() const defaultTagRef = useStateRef(defaultTag) useEffect(() => { if (!defaultTagId) { setDefaultTag(undefined) return } const tag = application.items.findItem(defaultTagId) as SNTag | undefined setDefaultTag(tag) }, [defaultTagId, application]) const selectTag = useCallback( (tag: DecryptedItem) => { void application.setPreference(PrefKey.ClipperDefaultTagUuid, tag.uuid) }, [application], ) const unselectTag = useCallback(async () => { void application.setPreference(PrefKey.ClipperDefaultTagUuid, undefined) }, [application]) const [menuPane, setMenuPane] = useState() const activateRegisterPane = useCallback(() => { setMenuPane(AccountMenuPane.Register) }, [setMenuPane]) const activateSignInPane = useCallback(() => { setMenuPane(AccountMenuPane.SignIn) }, [setMenuPane]) const showSignOutConfirmation = useCallback(async () => { if ( await confirmDialog({ title: 'Sign Out', text: 'Are you sure you want to sign out?', confirmButtonText: 'Sign Out', confirmButtonStyle: 'danger', cancelButtonText: 'Cancel', }) ) { await application.user.signOut() } }, [application.user]) const [isScreenshotMode, setIsScreenshotMode] = useState(false) useEffect(() => { void sendMessageToActiveTab({ type: RuntimeMessageTypes.ToggleScreenshotMode, enabled: isScreenshotMode, }) }, [isScreenshotMode]) const [hasSelection, setHasSelection] = useState(false) useEffect(() => { if (!user) { return } try { const checkIfPageHasSelection = async () => { setHasSelection(Boolean(await sendMessageToActiveTab({ type: RuntimeMessageTypes.HasSelection }))) } void checkIfPageHasSelection() } catch (error) { console.error(error) } }, [user]) const [clipPayload, setClipPayload] = useState() useEffect(() => { const getClipFromStorage = async () => { const result = await extensionStorage.local.get('clip') if (!result.clip) { return } setClipPayload(result.clip) void extensionStorage.local.remove('clip') } void getClipFromStorage() }, []) const clearClip = useCallback(() => { setClipPayload(undefined) }, []) const [clippedNote, setClippedNote] = useState() useEffect(() => { if (!isEntitledRef.current) { return } async function createNoteFromClip() { if (!clipPayload) { setClippedNote(undefined) return } if (!clipPayload.content) { addToast({ type: ToastType.Error, message: 'No content to clip', }) return } if (clipPayload.isScreenshot) { const blob = await fetch(clipPayload.content).then((response) => response.blob()) const file = new File([blob], `${clipPayload.title} - ${clipPayload.url}.png`, { type: 'image/png', }) const uploadedFile = await viewControllerManager.filesController.uploadNewFile(file).catch(console.error) if (uploadedFile && defaultTagRef.current) { await application.linkingController.linkItems(uploadedFile, defaultTagRef.current) } return } const editorStateJSON = await getSuperJSONFromClipPayload(clipPayload) const note = application.items.createTemplateItem(ContentType.TYPES.Note, { title: clipPayload.title, text: editorStateJSON, editorIdentifier: FeatureIdentifier.SuperEditor, noteType: NoteType.Super, references: [], }) const insertedNote = await application.mutator.insertItem(note) if (defaultTagRef.current) { await application.linkingController.linkItems(insertedNote, defaultTagRef.current) } setClippedNote(insertedNote as SNNote) addToast({ type: ToastType.Success, message: 'Note clipped successfully', }) void application.sync.sync() } void createNoteFromClip() }, [ application.items, application.linkingController, application.mutator, application.sync, clipPayload, defaultTagRef, isEntitledRef, viewControllerManager.filesController, ]) const upgradePlan = useCallback(async () => { if (hasSubscription) { await openSubscriptionDashboard(application) } else { await application.openPurchaseFlow() } window.close() }, [application, hasSubscription]) if (user && !isEntitledToExtension) { return (
Enable Advanced Features
To take advantage of Web Clipper and other advanced features, upgrade your current plan.
) } if (clippedNote) { return ( ) } if (!user) { return menuPane ? (
setMenuPane(undefined)} />
) : ( Create free account Sign in ) } return (
{hasSelection && ( { const payload = await sendMessageToActiveTab({ type: RuntimeMessageTypes.GetSelection }) if (!payload) { return } setClipPayload(payload) }} > Clip text selection )} { const payload = await sendMessageToActiveTab({ type: RuntimeMessageTypes.GetFullPage }) if (!payload) { return } setClipPayload(payload) }} > {isScreenshotMode ? 'Capture visible' : 'Clip full page'} { const payload = await sendMessageToActiveTab({ type: RuntimeMessageTypes.GetArticle }) if (!payload) { return } setClipPayload(payload) }} > Clip article { void sendMessageToActiveTab({ type: RuntimeMessageTypes.StartNodeSelection }) window.close() }} > Select elements to {isScreenshotMode ? 'capture' : 'clip'} Clip as screenshot
{defaultTag && (
)}
{user.email}
) } export default ClipperView