From 8fb917c7f2bb94de0f652b6b53d687eadfe4acf0 Mon Sep 17 00:00:00 2001 From: Marc Seitz Date: Tue, 26 Nov 2024 17:47:48 +0900 Subject: [PATCH 1/2] feat: add screen shield for discouraging screenshots --- components/links/link-sheet/index.tsx | 2 + components/links/link-sheet/link-options.tsx | 6 + .../link-sheet/screen-shield-section.tsx | 92 +++++++++++ components/links/links-table.tsx | 1 + components/view/PagesViewerNew.tsx | 8 +- components/view/dataroom/dataroom-view.tsx | 2 + components/view/powered-by.tsx | 4 +- components/view/screen-shield.tsx | 150 ++++++++++++++++++ components/view/view-data.tsx | 2 + components/view/viewer/image-viewer.tsx | 8 +- pages/api/links/[id]/index.ts | 2 + pages/api/links/domains/[...domainSlug].ts | 1 + pages/api/links/index.ts | 1 + .../migration.sql | 3 + prisma/schema.prisma | 1 + 15 files changed, 279 insertions(+), 4 deletions(-) create mode 100644 components/links/link-sheet/screen-shield-section.tsx create mode 100644 components/view/screen-shield.tsx create mode 100644 prisma/migrations/20241126000000_add_screen_shield/migration.sql diff --git a/components/links/link-sheet/index.tsx b/components/links/link-sheet/index.tsx index 6253a6d22..bc7c1abaa 100644 --- a/components/links/link-sheet/index.tsx +++ b/components/links/link-sheet/index.tsx @@ -71,6 +71,7 @@ export const DEFAULT_LINK_PROPS = (linkType: LinkType) => ({ watermarkConfig: null, audienceType: LinkAudienceType.GENERAL, groupId: null, + screenShieldPercentage: null, }); export type DEFAULT_LINK_TYPE = { @@ -103,6 +104,7 @@ export type DEFAULT_LINK_TYPE = { watermarkConfig: WatermarkConfig | null; audienceType: LinkAudienceType; groupId: string | null; + screenShieldPercentage: number | null; }; export default function LinkSheet({ diff --git a/components/links/link-sheet/link-options.tsx b/components/links/link-sheet/link-options.tsx index bf2ba5d8f..ea0f6a678 100644 --- a/components/links/link-sheet/link-options.tsx +++ b/components/links/link-sheet/link-options.tsx @@ -21,6 +21,7 @@ import useLimits from "@/lib/swr/use-limits"; import AgreementSection from "./agreement-section"; import QuestionSection from "./question-section"; +import ScreenShieldSection from "./screen-shield-section"; import ScreenshotProtectionSection from "./screenshot-protection-section"; import WatermarkSection from "./watermark-section"; @@ -128,6 +129,11 @@ export const LinkOptions = ({ } handleUpgradeStateChange={handleUpgradeStateChange} /> + >; + isAllowed: boolean; + handleUpgradeStateChange: ({ + state, + trigger, + plan, + }: LinkUpgradeOptions) => void; +}) { + const { screenShieldPercentage } = data; + const [enabled, setEnabled] = useState(!!screenShieldPercentage); + + useEffect(() => { + setEnabled(!!screenShieldPercentage); + }, [screenShieldPercentage]); + + const handleEnableScreenShield = () => { + const updatedEnabled = !enabled; + setData({ + ...data, + screenShieldPercentage: updatedEnabled ? 35 : null, // Default to 35% when enabling + }); + setEnabled(updatedEnabled); + }; + + return ( +
+ + handleUpgradeStateChange({ + state: true, + trigger: "link_sheet_screen_shield_section", + plan: "Business", + }) + } + /> + + {enabled && ( +
+
+ + +
+
+ )} +
+ ); +} diff --git a/components/links/links-table.tsx b/components/links/links-table.tsx index 91af9519f..55ab92de6 100644 --- a/components/links/links-table.tsx +++ b/components/links/links-table.tsx @@ -111,6 +111,7 @@ export default function LinksTable({ watermarkConfig: link.watermarkConfig as WatermarkConfig | null, audienceType: link.audienceType, groupId: link.groupId, + screenShieldPercentage: link.screenShieldPercentage, }); //wait for dropdown to close before opening the link sheet setTimeout(() => { diff --git a/components/view/PagesViewerNew.tsx b/components/view/PagesViewerNew.tsx index c63ccc693..31f50bbda 100644 --- a/components/view/PagesViewerNew.tsx +++ b/components/view/PagesViewerNew.tsx @@ -27,6 +27,7 @@ import { TDocumentData } from "./dataroom/dataroom-view"; import Nav from "./nav"; import { PoweredBy } from "./powered-by"; import Question from "./question"; +import { ScreenShield } from "./screen-shield"; import Toolbar from "./toolbar"; import ViewDurationSummary from "./visitor-graph"; import { SVGWatermark } from "./watermark-svg"; @@ -76,6 +77,7 @@ export default function PagesViewer({ allowDownload, feedbackEnabled, screenshotProtectionEnabled, + screenShieldPercentage, versionNumber, brand, documentName, @@ -106,6 +108,7 @@ export default function PagesViewer({ allowDownload: boolean; feedbackEnabled: boolean; screenshotProtectionEnabled: boolean; + screenShieldPercentage: number | null; versionNumber: number; brand?: Partial | Partial | null; documentName?: string; @@ -493,7 +496,7 @@ export default function PagesViewer({ // Function to handle context for screenshotting const handleContextMenu = (event: React.MouseEvent) => { - if (!screenshotProtectionEnabled) { + if (!screenshotProtectionEnabled && !screenShieldPercentage) { return null; } @@ -1084,6 +1087,9 @@ export default function PagesViewer({ isPreview={isPreview} /> ) : null} + {!!screenShieldPercentage ? ( + + ) : null} {screenshotProtectionEnabled ? : null} {showPoweredByBanner ? : null} diff --git a/components/view/dataroom/dataroom-view.tsx b/components/view/dataroom/dataroom-view.tsx index 3f14b5d91..e8799fae8 100644 --- a/components/view/dataroom/dataroom-view.tsx +++ b/components/view/dataroom/dataroom-view.tsx @@ -395,6 +395,7 @@ export default function DataroomView({ allowDownload={viewData.canDownload ?? link.allowDownload!} feedbackEnabled={link.enableFeedback!} screenshotProtectionEnabled={link.enableScreenshotProtection!} + screenShieldPercentage={link.screenShieldPercentage} versionNumber={documentData.documentVersionNumber} brand={brand} dataroomId={dataroom.id} @@ -421,6 +422,7 @@ export default function DataroomView({ allowDownload={viewData.canDownload ?? link.allowDownload!} feedbackEnabled={link.enableFeedback!} screenshotProtectionEnabled={link.enableScreenshotProtection!} + screenShieldPercentage={link.screenShieldPercentage} versionNumber={documentData.documentVersionNumber} brand={brand} dataroomId={dataroom.id} diff --git a/components/view/powered-by.tsx b/components/view/powered-by.tsx index 643e48020..2057be68a 100644 --- a/components/view/powered-by.tsx +++ b/components/view/powered-by.tsx @@ -2,14 +2,14 @@ import { createPortal } from "react-dom"; export const PoweredBy = ({ linkId }: { linkId: string }) => { return createPortal( -
+
Share docs via{" "} diff --git a/components/view/screen-shield.tsx b/components/view/screen-shield.tsx new file mode 100644 index 000000000..52e7ffb4e --- /dev/null +++ b/components/view/screen-shield.tsx @@ -0,0 +1,150 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; + +import { CircleHelpIcon } from "lucide-react"; + +import { ButtonTooltip } from "../ui/tooltip"; + +export function ScreenShield({ + visiblePercentage = 35, +}: { + visiblePercentage?: number; +}) { + const navbarHeight = 64; + const minShieldHeight = 32; + + const navbarHeightVh = (navbarHeight / window.innerHeight) * 100; + const minShieldHeightVh = (minShieldHeight / window.innerHeight) * 100; + + const availableHeight = 100 - navbarHeightVh; + + const [handlePosition, setHandlePosition] = useState( + (availableHeight - visiblePercentage) / 2, + ); + + const [isDragging, setIsDragging] = useState(false); + const [startY, setStartY] = useState(0); + + const handleMouseDown = (e: React.MouseEvent | React.TouchEvent) => { + setIsDragging(true); + setStartY("touches" in e ? e.touches[0].clientY : e.clientY); + }; + + const handleMove = useCallback( + (clientY: number) => { + if (!isDragging) return; + + const delta = clientY - startY; + const viewportHeight = window.innerHeight; + const deltaPercentage = (delta / viewportHeight) * 100; + + const newPosition = Math.min( + availableHeight - visiblePercentage - minShieldHeightVh, + Math.max(minShieldHeightVh, handlePosition + deltaPercentage), + ); + + setHandlePosition(newPosition); + setStartY(clientY); + }, + [isDragging, startY, handlePosition, availableHeight, visiblePercentage], + ); + + const handleMouseMove = useCallback( + (e: MouseEvent) => { + handleMove(e.clientY); + }, + [handleMove], + ); + + const handleTouchMove = useCallback( + (e: TouchEvent) => { + handleMove(e.touches[0].clientY); + }, + [handleMove], + ); + + const handleEnd = useCallback(() => { + setIsDragging(false); + }, []); + + useEffect(() => { + if (isDragging) { + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("touchmove", handleTouchMove); + window.addEventListener("mouseup", handleEnd); + window.addEventListener("touchend", handleEnd); + } + + return () => { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("touchmove", handleTouchMove); + window.removeEventListener("mouseup", handleEnd); + window.removeEventListener("touchend", handleEnd); + }; + }, [isDragging, handleMouseMove, handleTouchMove, handleEnd]); + + const patternBg = `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M30 0l30 30-30 30L0 30 30 0zm0 10L10 30l20 20 20-20-20-20z' fill='%23ffffff' fill-opacity='0.1'/%3E%3C/svg%3E")`; + + console.log("total height", availableHeight); + console.log("handle position", handlePosition); + console.log("visible percentage", visiblePercentage); + + return ( + <> + {/* Top Shield */} +
e.preventDefault()} + className="fixed inset-x-0 top-16 w-full origin-top cursor-move bg-black/90 backdrop-blur-sm transition-transform duration-300 ease-out" + style={{ + height: `${handlePosition}svh`, + backgroundImage: patternBg, + backgroundSize: "30px 30px", + }} + > +
+
+ + PRIVATE AND CONFIDENTIAL + + + + +
+
+
+ + {/* Bottom Shield */} +
e.preventDefault()} + className="fixed inset-x-0 bottom-0 w-full origin-bottom cursor-move bg-black/90 backdrop-blur-sm transition-transform duration-300 ease-out" + style={{ + height: `${availableHeight - visiblePercentage - handlePosition}svh`, + backgroundImage: patternBg, + backgroundSize: "30px 30px", + }} + > +
+
+ Drag to adjust view +
+
+
+ + ); +} diff --git a/components/view/view-data.tsx b/components/view/view-data.tsx index b6d8b2bef..c9ce967c2 100644 --- a/components/view/view-data.tsx +++ b/components/view/view-data.tsx @@ -92,6 +92,7 @@ export default function ViewData({ allowDownload={link.allowDownload!} feedbackEnabled={link.enableFeedback!} screenshotProtectionEnabled={link.enableScreenshotProtection!} + screenShieldPercentage={link.screenShieldPercentage} versionNumber={document.versions[0].versionNumber} brand={brand} showPoweredByBanner={showPoweredByBanner} @@ -118,6 +119,7 @@ export default function ViewData({ allowDownload={link.allowDownload!} feedbackEnabled={link.enableFeedback!} screenshotProtectionEnabled={link.enableScreenshotProtection!} + screenShieldPercentage={link.screenShieldPercentage} versionNumber={document.versions[0].versionNumber} brand={brand} showPoweredByBanner={showPoweredByBanner} diff --git a/components/view/viewer/image-viewer.tsx b/components/view/viewer/image-viewer.tsx index 5534c104a..6c18863f5 100644 --- a/components/view/viewer/image-viewer.tsx +++ b/components/view/viewer/image-viewer.tsx @@ -19,6 +19,7 @@ import { ScreenProtector } from "../ScreenProtection"; import { TDocumentData } from "../dataroom/dataroom-view"; import Nav from "../nav"; import { PoweredBy } from "../powered-by"; +import { ScreenShield } from "../screen-shield"; import { SVGWatermark } from "../watermark-svg"; const trackPageView = async (data: { @@ -52,6 +53,7 @@ export default function ImageViewer({ allowDownload, feedbackEnabled, screenshotProtectionEnabled, + screenShieldPercentage, versionNumber, brand, documentName, @@ -76,6 +78,7 @@ export default function ImageViewer({ allowDownload: boolean; feedbackEnabled: boolean; screenshotProtectionEnabled: boolean; + screenShieldPercentage: number | null; versionNumber: number; brand?: Partial | Partial | null; documentName?: string; @@ -252,7 +255,7 @@ export default function ImageViewer({ // Function to handle context for screenshotting const handleContextMenu = (event: React.MouseEvent) => { - if (!screenshotProtectionEnabled) { + if (!screenshotProtectionEnabled && !screenShieldPercentage) { return null; } @@ -382,6 +385,9 @@ export default function ImageViewer({
+ {!!screenShieldPercentage ? ( + + ) : null} {screenshotProtectionEnabled ? : null} {showPoweredByBanner ? : null}
diff --git a/pages/api/links/[id]/index.ts b/pages/api/links/[id]/index.ts index b5ced41dd..1e20cbdb4 100644 --- a/pages/api/links/[id]/index.ts +++ b/pages/api/links/[id]/index.ts @@ -38,6 +38,7 @@ export default async function handle( allowDownload: true, enableFeedback: true, enableScreenshotProtection: true, + screenShieldPercentage: true, password: true, isArchived: true, enableCustomMetatag: true, @@ -241,6 +242,7 @@ export default async function handle( enableNotification: linkData.enableNotification, enableFeedback: linkData.enableFeedback, enableScreenshotProtection: linkData.enableScreenshotProtection, + screenShieldPercentage: linkData.screenShieldPercentage, enableCustomMetatag: linkData.enableCustomMetatag, metaTitle: linkData.metaTitle || null, metaDescription: linkData.metaDescription || null, diff --git a/pages/api/links/domains/[...domainSlug].ts b/pages/api/links/domains/[...domainSlug].ts index 9fb9f3779..a6715b30b 100644 --- a/pages/api/links/domains/[...domainSlug].ts +++ b/pages/api/links/domains/[...domainSlug].ts @@ -48,6 +48,7 @@ export default async function handle( enableCustomMetatag: true, enableFeedback: true, enableScreenshotProtection: true, + screenShieldPercentage: true, metaTitle: true, metaDescription: true, metaImage: true, diff --git a/pages/api/links/index.ts b/pages/api/links/index.ts index 12c6f4bd4..499c510dd 100644 --- a/pages/api/links/index.ts +++ b/pages/api/links/index.ts @@ -132,6 +132,7 @@ export default async function handler( enableNotification: linkData.enableNotification, enableFeedback: linkData.enableFeedback, enableScreenshotProtection: linkData.enableScreenshotProtection, + screenShieldPercentage: linkData.screenShieldPercentage, enableCustomMetatag: linkData.enableCustomMetatag, metaTitle: linkData.metaTitle || null, metaDescription: linkData.metaDescription || null, diff --git a/prisma/migrations/20241126000000_add_screen_shield/migration.sql b/prisma/migrations/20241126000000_add_screen_shield/migration.sql new file mode 100644 index 000000000..f772ea49e --- /dev/null +++ b/prisma/migrations/20241126000000_add_screen_shield/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Link" ADD COLUMN "screenShieldPercentage" INTEGER; + diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6ae2fcc45..149be35f6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -251,6 +251,7 @@ model Link { enableFeedback Boolean? @default(false) // Optional give user a option to enable the reactions toolbar enableQuestion Boolean? @default(false) // Optional give user a option to enable the question feedback enableScreenshotProtection Boolean? @default(false) // Optional give user a option to enable the screenshot protection + screenShieldPercentage Int? // Optional give user a option to enable the screen shield percentage feedback Feedback? enableAgreement Boolean? @default(false) // Optional give user a option to enable the terms and conditions agreement Agreement? @relation(fields: [agreementId], references: [id], onDelete: SetNull) From 3d4118de81549b8594d8fbecd804e7258cc49247 Mon Sep 17 00:00:00 2001 From: Marc Seitz Date: Tue, 26 Nov 2024 17:48:02 +0900 Subject: [PATCH 2/2] chore: update deps --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index ed910a413..e4d6f9bf1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1870,9 +1870,9 @@ } }, "node_modules/@dnd-kit/accessibility": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz", - "integrity": "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", "dependencies": { "tslib": "^2.0.0" }, @@ -1881,11 +1881,11 @@ } }, "node_modules/@dnd-kit/core": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.1.0.tgz", - "integrity": "sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.2.0.tgz", + "integrity": "sha512-KVK/CJmaYGTxTPU6P0+Oy4itgffTUa80B8317sXzfOr1qUzSL29jE7Th11llXiu2haB7B9Glpzo2CDElin+geQ==", "dependencies": { - "@dnd-kit/accessibility": "^3.1.0", + "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, diff --git a/package.json b/package.json index 0b2e35e56..5074775b0 100644 --- a/package.json +++ b/package.json @@ -160,4 +160,4 @@ "react-pdf": "8.0.2" } } -} \ No newline at end of file +}