Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add screen shield #1402

Merged
merged 2 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions components/links/link-sheet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -103,6 +104,7 @@ export type DEFAULT_LINK_TYPE = {
watermarkConfig: WatermarkConfig | null;
audienceType: LinkAudienceType;
groupId: string | null;
screenShieldPercentage: number | null;
};

export default function LinkSheet({
Expand Down
6 changes: 6 additions & 0 deletions components/links/link-sheet/link-options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -128,6 +129,11 @@ export const LinkOptions = ({
}
handleUpgradeStateChange={handleUpgradeStateChange}
/>
<ScreenShieldSection
{...{ data, setData }}
isAllowed={isTrial || isBusiness || isDatarooms}
handleUpgradeStateChange={handleUpgradeStateChange}
/>
<WatermarkSection
{...{ data, setData }}
isAllowed={isTrial || isDatarooms}
Expand Down
92 changes: 92 additions & 0 deletions components/links/link-sheet/screen-shield-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useEffect, useState } from "react";

import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";

import { DEFAULT_LINK_TYPE } from ".";
import LinkItem from "./link-item";
import { LinkUpgradeOptions } from "./link-options";

export default function ScreenShieldSection({
data,
setData,
isAllowed,
handleUpgradeStateChange,
}: {
data: DEFAULT_LINK_TYPE;
setData: React.Dispatch<React.SetStateAction<DEFAULT_LINK_TYPE>>;
isAllowed: boolean;
handleUpgradeStateChange: ({
state,
trigger,
plan,
}: LinkUpgradeOptions) => void;
}) {
const { screenShieldPercentage } = data;
const [enabled, setEnabled] = useState<boolean>(!!screenShieldPercentage);

useEffect(() => {
setEnabled(!!screenShieldPercentage);
}, [screenShieldPercentage]);

const handleEnableScreenShield = () => {
const updatedEnabled = !enabled;
setData({
...data,
screenShieldPercentage: updatedEnabled ? 35 : null, // Default to 35% when enabling
});
setEnabled(updatedEnabled);
};

return (
<div className="pb-5">
<LinkItem
title="Enable Screen Shield"
tooltipContent="Add a draggable shield that limits the visible area of your content"
enabled={enabled}
action={handleEnableScreenShield}
isAllowed={isAllowed}
requiredPlan="business"
upgradeAction={() =>
handleUpgradeStateChange({
state: true,
trigger: "link_sheet_screen_shield_section",
plan: "Business",
})
}
/>

{enabled && (
<div className="mt-4 space-y-3">
<div className="space-y-2">
<Label>Visible Area</Label>
<Select
value={String(screenShieldPercentage)}
onValueChange={(value) =>
setData({
...data,
screenShieldPercentage: parseInt(value),
})
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="20">20% - Most Restrictive</SelectItem>
<SelectItem value="35">35% - Recommended</SelectItem>
<SelectItem value="50">50% - Most Permissive</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}
</div>
);
}
1 change: 1 addition & 0 deletions components/links/links-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
8 changes: 7 additions & 1 deletion components/view/PagesViewerNew.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -76,6 +77,7 @@ export default function PagesViewer({
allowDownload,
feedbackEnabled,
screenshotProtectionEnabled,
screenShieldPercentage,
versionNumber,
brand,
documentName,
Expand Down Expand Up @@ -106,6 +108,7 @@ export default function PagesViewer({
allowDownload: boolean;
feedbackEnabled: boolean;
screenshotProtectionEnabled: boolean;
screenShieldPercentage: number | null;
versionNumber: number;
brand?: Partial<Brand> | Partial<DataroomBrand> | null;
documentName?: string;
Expand Down Expand Up @@ -493,7 +496,7 @@ export default function PagesViewer({

// Function to handle context for screenshotting
const handleContextMenu = (event: React.MouseEvent<HTMLDivElement>) => {
if (!screenshotProtectionEnabled) {
if (!screenshotProtectionEnabled && !screenShieldPercentage) {
return null;
}

Expand Down Expand Up @@ -1084,6 +1087,9 @@ export default function PagesViewer({
isPreview={isPreview}
/>
) : null}
{!!screenShieldPercentage ? (
<ScreenShield visiblePercentage={screenShieldPercentage} />
) : null}
{screenshotProtectionEnabled ? <ScreenProtector /> : null}
{showPoweredByBanner ? <PoweredBy linkId={linkId} /> : null}
</div>
Expand Down
2 changes: 2 additions & 0 deletions components/view/dataroom/dataroom-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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}
Expand Down
4 changes: 2 additions & 2 deletions components/view/powered-by.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { createPortal } from "react-dom";

export const PoweredBy = ({ linkId }: { linkId: string }) => {
return createPortal(
<div className="absolute bottom-0 right-0 w-fit">
<div className="absolute bottom-0 right-0 z-[100] w-fit">
<div className="p-6">
<div className="pointer-events-auto relative z-20 flex min-h-8 w-auto items-center justify-end whitespace-nowrap rounded-md bg-black text-white ring-1 ring-white/40 hover:ring-white/90">
<a
href={`https://www.papermark.io?utm_campaign=poweredby&utm_medium=poweredby&utm_source=papermark-${linkId}`}
target="_blank"
rel="noopener noreferrer"
className="rounded-sm text-sm "
className="rounded-sm text-sm"
style={{ paddingInlineStart: "12px", paddingInlineEnd: "12px" }}
>
Share docs via{" "}
Expand Down
150 changes: 150 additions & 0 deletions components/view/screen-shield.tsx
Original file line number Diff line number Diff line change
@@ -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 */}
<div
onMouseDown={handleMouseDown}
onTouchStart={handleMouseDown}
onContextMenu={(e) => 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",
}}
>
<div
onMouseDown={handleMouseDown}
onTouchStart={handleMouseDown}
className="absolute inset-x-0 bottom-0 cursor-move bg-black/90 p-2 text-center text-white hover:bg-gray-500 hover:opacity-80"
>
<div className="flex items-center justify-center gap-2">
<span className="select-none text-xs font-medium">
PRIVATE AND CONFIDENTIAL
</span>
<ButtonTooltip
content="The sender has reduced the viewing area to protect the contents of this confidential file. Click and drag this bar to adjust your view."
sideOffset={16}
>
<CircleHelpIcon className="h-4 w-4" />
</ButtonTooltip>
</div>
</div>
</div>

{/* Bottom Shield */}
<div
onMouseDown={handleMouseDown}
onTouchStart={handleMouseDown}
onContextMenu={(e) => 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",
}}
>
<div
onMouseDown={handleMouseDown}
onTouchStart={handleMouseDown}
className="absolute inset-x-0 top-0 cursor-move bg-black/90 p-2 text-center text-white hover:bg-gray-500 hover:opacity-80"
>
<div className="select-none text-xs">
<span>Drag to adjust view</span>
</div>
</div>
</div>
</>
);
}
2 changes: 2 additions & 0 deletions components/view/view-data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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}
Expand Down
Loading
Loading