fix(gallery): remove the inert image-protection prop surface from AuthenticatedImage
AuthenticatedImage accepted the whole image-protection prop surface and discarded it in a `void unusedProps` block. Callers computed those props from the event's protection level and passed them in good faith, so raising the level produced canvas rendering (via the layouts' own OR on `protectionLevel === 'maximum'`) and nothing else the level implies. Removes them from the interface and from every call site, so the props state what the component actually does. Two survive because they are real: `useCanvasRendering`, and `onProtectionViolation` — which #1297 listed as inert but which does fire, from the canvas context-menu handler. `useWatermark` is removed as well; #1297 did not list it (it sat outside the `unusedProps` block) but it was equally dead. Removal rather than implementation is deliberate. The implementation these props describe already exists in `ProtectedImage`, which is exported from the barrel and rendered nowhere. Wiring it in is a product decision about what protection level should mean, not a side effect of a cleanup. Analytics payloads inside the surviving onProtectionViolation handlers keep their photoId/protectionLevel fields. Refs #1297
This commit is contained in:
@@ -11,25 +11,12 @@ import {
|
||||
interface AuthenticatedImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'onLoad'> {
|
||||
src: string;
|
||||
fallbackSrc?: string;
|
||||
useWatermark?: boolean;
|
||||
isGallery?: boolean;
|
||||
protectFromDownload?: boolean;
|
||||
slug?: string;
|
||||
photoId?: number;
|
||||
requiresToken?: boolean;
|
||||
secureUrlTemplate?: string;
|
||||
downloadUrlTemplate?: string;
|
||||
onProtectionViolation?: (violationType: string) => void;
|
||||
watermarkText?: string;
|
||||
overlayProtection?: boolean;
|
||||
fragmentGrid?: boolean;
|
||||
scrambleFragments?: boolean;
|
||||
useCanvasRendering?: boolean;
|
||||
blockKeyboardShortcuts?: boolean;
|
||||
detectPrintScreen?: boolean;
|
||||
detectDevTools?: boolean;
|
||||
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
|
||||
useEnhancedProtection?: boolean;
|
||||
/** Fired when the canvas branch blocks a context-menu attempt. The only
|
||||
* protection callback this component actually implements (#1297). */
|
||||
onProtectionViolation?: (violationType: string) => void;
|
||||
onLoad?: () => void;
|
||||
/**
|
||||
* Priority in the shared fetch queue (#1287). NOT the native `fetchPriority`
|
||||
@@ -43,51 +30,44 @@ interface AuthenticatedImageProps extends Omit<React.ImgHTMLAttributes<HTMLImage
|
||||
queuePriority?: 'high' | 'prefetch' | 'normal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an image with the gallery's bearer token and renders it.
|
||||
*
|
||||
* IMAGE PROTECTION IS NOT IMPLEMENTED HERE (#1297). This component used to
|
||||
* accept the whole protection prop surface — protectFromDownload,
|
||||
* watermarkText, fragmentGrid, blockKeyboardShortcuts, detectPrintScreen,
|
||||
* detectDevTools, protectionLevel and the rest — and discard every one of
|
||||
* them in a `void unusedProps` block. Callers computed them from the event's
|
||||
* protection level and passed them in good faith, so raising that level
|
||||
* produced canvas rendering (via the layouts' own OR on
|
||||
* `protectionLevel === 'maximum'`) and nothing else it implies.
|
||||
*
|
||||
* They are removed rather than implemented, so the interface states what the
|
||||
* component actually does. The implementation those props describe already
|
||||
* exists in `ProtectedImage` — which is exported and currently rendered
|
||||
* nowhere. Wiring that in is a deliberate product decision about what
|
||||
* protection level should mean, not a silent side effect of a cleanup.
|
||||
*
|
||||
* Two props survive because they are real:
|
||||
* useCanvasRendering draws to a canvas instead of an <img>
|
||||
* onProtectionViolation fires from the canvas context-menu handler below
|
||||
*
|
||||
* `useWatermark` was removed too. #1297 did not list it — it sat outside the
|
||||
* `unusedProps` block — but it was equally inert: declared, defaulted, never
|
||||
* read.
|
||||
*/
|
||||
export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
src,
|
||||
fallbackSrc,
|
||||
alt,
|
||||
useWatermark = false,
|
||||
isGallery = false,
|
||||
protectFromDownload,
|
||||
slug,
|
||||
photoId,
|
||||
requiresToken,
|
||||
secureUrlTemplate,
|
||||
downloadUrlTemplate,
|
||||
onProtectionViolation,
|
||||
watermarkText,
|
||||
overlayProtection,
|
||||
fragmentGrid,
|
||||
scrambleFragments,
|
||||
useCanvasRendering,
|
||||
blockKeyboardShortcuts,
|
||||
detectPrintScreen,
|
||||
detectDevTools,
|
||||
protectionLevel,
|
||||
useEnhancedProtection,
|
||||
onProtectionViolation,
|
||||
onLoad,
|
||||
queuePriority = 'normal',
|
||||
...props
|
||||
}) => {
|
||||
const unusedProps = {
|
||||
protectFromDownload,
|
||||
photoId,
|
||||
requiresToken,
|
||||
secureUrlTemplate,
|
||||
downloadUrlTemplate,
|
||||
onProtectionViolation,
|
||||
watermarkText,
|
||||
overlayProtection,
|
||||
fragmentGrid,
|
||||
scrambleFragments,
|
||||
blockKeyboardShortcuts,
|
||||
detectPrintScreen,
|
||||
detectDevTools,
|
||||
protectionLevel,
|
||||
useEnhancedProtection
|
||||
};
|
||||
void unusedProps;
|
||||
|
||||
const [imageSrc, setImageSrc] = useState<string>('');
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
@@ -41,9 +41,7 @@ export const GalleryFolderTiles: React.FC<GalleryFolderTilesProps> = ({
|
||||
compact = false,
|
||||
slug,
|
||||
protectionLevel,
|
||||
useEnhancedProtection,
|
||||
useCanvasRendering,
|
||||
allowDownloads = true,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -96,12 +94,6 @@ export const GalleryFolderTiles: React.FC<GalleryFolderTilesProps> = ({
|
||||
className="w-full h-full object-cover group-hover:scale-[1.02] transition-transform"
|
||||
isGallery
|
||||
slug={slug}
|
||||
photoId={coverPhoto.id}
|
||||
requiresToken={coverPhoto.requires_token}
|
||||
secureUrlTemplate={coverPhoto.secure_url_template}
|
||||
protectFromDownload={!allowDownloads || useEnhancedProtection}
|
||||
protectionLevel={protectionLevel}
|
||||
useEnhancedProtection={useEnhancedProtection}
|
||||
// Same rule as every other gallery image path: maximum
|
||||
// protection implies canvas rendering even when the separate
|
||||
// toggle is off (its default), otherwise a cover silently
|
||||
|
||||
@@ -43,9 +43,7 @@ export const HeroHeader: React.FC<HeroHeaderProps> = ({
|
||||
heroLogoSize = 'medium',
|
||||
heroLogoPosition = 'top',
|
||||
dividerStyle = 'wave',
|
||||
allowDownloads = true,
|
||||
protectionLevel = 'standard',
|
||||
useEnhancedProtection = false,
|
||||
useCanvasRendering = false,
|
||||
onScrollToContent,
|
||||
heroImageAnchor = 'center'
|
||||
@@ -144,10 +142,6 @@ export const HeroHeader: React.FC<HeroHeaderProps> = ({
|
||||
style={{ objectPosition: heroImageAnchor }}
|
||||
isGallery={true}
|
||||
slug={slug}
|
||||
photoId={heroPhoto.id}
|
||||
protectFromDownload={!allowDownloads || useEnhancedProtection}
|
||||
protectionLevel={protectionLevel}
|
||||
useEnhancedProtection={useEnhancedProtection}
|
||||
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
|
||||
/>
|
||||
|
||||
|
||||
@@ -144,9 +144,6 @@ export const PeopleSheet: React.FC<PeopleSheetProps> = ({
|
||||
alt=""
|
||||
isGallery
|
||||
slug={slug}
|
||||
photoId={photo.id}
|
||||
requiresToken={photo.requires_token}
|
||||
secureUrlTemplate={photo.secure_url_template}
|
||||
// Crop to the face, exactly as the strip does. Without
|
||||
// this a group photo shows whoever is centred — often
|
||||
// not the person being labelled, and identical for two
|
||||
|
||||
@@ -86,9 +86,6 @@ const PersonAvatar: React.FC<PersonAvatarProps> = ({
|
||||
alt=""
|
||||
isGallery
|
||||
slug={slug}
|
||||
photoId={photo.id}
|
||||
requiresToken={photo.requires_token}
|
||||
secureUrlTemplate={photo.secure_url_template}
|
||||
style={cropStyle || { width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -244,7 +244,6 @@ const PhotoThumbnail: React.FC<PhotoThumbnailProps> = ({
|
||||
onDownload,
|
||||
allowDownloads = true,
|
||||
protectionLevel = 'standard',
|
||||
useEnhancedProtection = false,
|
||||
useCanvasRendering = false,
|
||||
slug,
|
||||
feedbackEnabled = false
|
||||
@@ -269,18 +268,7 @@ const PhotoThumbnail: React.FC<PhotoThumbnailProps> = ({
|
||||
loading="lazy"
|
||||
isGallery={true}
|
||||
slug={slug}
|
||||
photoId={photo.id}
|
||||
requiresToken={photo.requires_token}
|
||||
secureUrlTemplate={photo.secure_url_template}
|
||||
protectFromDownload={!allowDownloads || useEnhancedProtection}
|
||||
protectionLevel={protectionLevel}
|
||||
useEnhancedProtection={useEnhancedProtection}
|
||||
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
|
||||
fragmentGrid={protectionLevel === 'enhanced' || protectionLevel === 'maximum'}
|
||||
blockKeyboardShortcuts={useEnhancedProtection}
|
||||
detectPrintScreen={useEnhancedProtection}
|
||||
detectDevTools={protectionLevel === 'maximum'}
|
||||
watermarkText={useEnhancedProtection ? 'Protected' : undefined}
|
||||
onProtectionViolation={(violationType) => {
|
||||
// Track analytics
|
||||
if (typeof window !== 'undefined' && (window as any).umami) {
|
||||
|
||||
@@ -1159,9 +1159,6 @@ export const PhotoLightbox: React.FC<PhotoLightboxProps> = ({
|
||||
draggable={false}
|
||||
isGallery={true}
|
||||
slug={slug}
|
||||
photoId={photo.id}
|
||||
requiresToken={photo.requires_token}
|
||||
secureUrlTemplate={photo.secure_url_template}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -1189,21 +1186,9 @@ export const PhotoLightbox: React.FC<PhotoLightboxProps> = ({
|
||||
transition: isDragging ? 'none' : 'transform 0.2s',
|
||||
}}
|
||||
draggable={false}
|
||||
useWatermark={useEnhancedProtection}
|
||||
watermarkText={useEnhancedProtection ? `${photo.filename} - Protected` : undefined}
|
||||
isGallery={true}
|
||||
slug={slug}
|
||||
photoId={photo.id}
|
||||
requiresToken={photo.requires_token}
|
||||
secureUrlTemplate={photo.secure_url_template}
|
||||
protectFromDownload={!allowDownloads || useEnhancedProtection}
|
||||
protectionLevel={protectionLevel}
|
||||
useEnhancedProtection={useEnhancedProtection}
|
||||
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
|
||||
fragmentGrid={protectionLevel === 'enhanced' || protectionLevel === 'maximum'}
|
||||
blockKeyboardShortcuts={useEnhancedProtection}
|
||||
detectPrintScreen={useEnhancedProtection}
|
||||
detectDevTools={protectionLevel === 'enhanced' || protectionLevel === 'maximum'}
|
||||
onProtectionViolation={(violationType) => {
|
||||
console.warn(`Protection violation in lightbox for photo ${photo.id}: ${violationType}`);
|
||||
|
||||
|
||||
@@ -89,7 +89,6 @@ export const CarouselGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
alt={currentPhoto.filename}
|
||||
className="w-full h-full object-contain"
|
||||
isGallery={true}
|
||||
protectFromDownload={!allowDownloads}
|
||||
/>
|
||||
|
||||
{/* Colour labels for the photo in view (#1189). Bottom-left because it
|
||||
@@ -267,7 +266,6 @@ export const CarouselGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
className="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
isGallery={true}
|
||||
protectFromDownload={!allowDownloads}
|
||||
/>
|
||||
{/* The strip is the only place this layout shows more than one
|
||||
photo at a time, so it is the only place a label can
|
||||
|
||||
@@ -68,9 +68,7 @@ const PhotoCard: React.FC<PhotoCardProps> = ({
|
||||
isSelectionMode,
|
||||
isLiked,
|
||||
slug,
|
||||
allowDownloads = true,
|
||||
protectionLevel = 'standard',
|
||||
useEnhancedProtection = false,
|
||||
useCanvasRendering = false,
|
||||
feedbackEnabled = false,
|
||||
allowLikes = false,
|
||||
@@ -124,12 +122,6 @@ const PhotoCard: React.FC<PhotoCardProps> = ({
|
||||
loading="lazy"
|
||||
isGallery={true}
|
||||
slug={slug}
|
||||
photoId={photo.id}
|
||||
requiresToken={photo.requires_token}
|
||||
secureUrlTemplate={photo.secure_url_template}
|
||||
protectFromDownload={!allowDownloads || useEnhancedProtection}
|
||||
protectionLevel={protectionLevel}
|
||||
useEnhancedProtection={useEnhancedProtection}
|
||||
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
|
||||
/>
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ const GridPhoto: React.FC<GridPhotoProps> = ({
|
||||
allowDownloads = true,
|
||||
slug,
|
||||
protectionLevel = 'standard',
|
||||
useEnhancedProtection = false,
|
||||
useCanvasRendering = false,
|
||||
feedbackEnabled = false,
|
||||
feedbackOptions,
|
||||
@@ -93,18 +92,7 @@ const GridPhoto: React.FC<GridPhotoProps> = ({
|
||||
loading: 'lazy',
|
||||
isGallery: true,
|
||||
slug,
|
||||
photoId: photo.id,
|
||||
requiresToken: photo.requires_token,
|
||||
secureUrlTemplate: photo.secure_url_template,
|
||||
protectFromDownload: !allowDownloads || useEnhancedProtection,
|
||||
protectionLevel,
|
||||
useEnhancedProtection,
|
||||
useCanvasRendering: useCanvasRendering || protectionLevel === 'maximum',
|
||||
fragmentGrid: protectionLevel === 'enhanced' || protectionLevel === 'maximum',
|
||||
blockKeyboardShortcuts: useEnhancedProtection,
|
||||
detectPrintScreen: useEnhancedProtection,
|
||||
detectDevTools: protectionLevel === 'maximum',
|
||||
watermarkText: useEnhancedProtection ? 'Protected' : undefined,
|
||||
onProtectionViolation: (violationType: string) => {
|
||||
console.warn(`Protection violation on grid photo ${photo.id}: ${violationType}`);
|
||||
},
|
||||
|
||||
@@ -70,7 +70,6 @@ const JustifiedPhoto: React.FC<JustifiedPhotoProps> = ({
|
||||
allowDownloads = true,
|
||||
slug,
|
||||
protectionLevel = 'standard',
|
||||
useEnhancedProtection = false,
|
||||
useCanvasRendering = false,
|
||||
feedbackEnabled = false,
|
||||
feedbackOptions,
|
||||
@@ -134,18 +133,7 @@ const JustifiedPhoto: React.FC<JustifiedPhotoProps> = ({
|
||||
loading: 'lazy',
|
||||
isGallery: true,
|
||||
slug,
|
||||
photoId: photo.id,
|
||||
requiresToken: photo.requires_token,
|
||||
secureUrlTemplate: photo.secure_url_template,
|
||||
protectFromDownload: !allowDownloads || useEnhancedProtection,
|
||||
protectionLevel,
|
||||
useEnhancedProtection,
|
||||
useCanvasRendering: useCanvasRendering || protectionLevel === 'maximum',
|
||||
fragmentGrid: protectionLevel === 'enhanced' || protectionLevel === 'maximum',
|
||||
blockKeyboardShortcuts: useEnhancedProtection,
|
||||
detectPrintScreen: useEnhancedProtection,
|
||||
detectDevTools: protectionLevel === 'maximum',
|
||||
watermarkText: useEnhancedProtection ? 'Protected' : undefined,
|
||||
onProtectionViolation: (violationType: string) => {
|
||||
console.warn(`Protection violation on justified photo ${photo.id}: ${violationType}`);
|
||||
},
|
||||
@@ -411,10 +399,6 @@ export const JustifiedGalleryLayout: React.FC<JustifiedGalleryLayoutProps> = ({
|
||||
className="w-full h-full object-cover"
|
||||
isGallery={true}
|
||||
slug={slug}
|
||||
photoId={heroPhoto.id}
|
||||
protectFromDownload={!allowDownloads || useEnhancedProtection}
|
||||
protectionLevel={protectionLevel}
|
||||
useEnhancedProtection={useEnhancedProtection}
|
||||
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
|
||||
/>
|
||||
|
||||
|
||||
@@ -120,7 +120,6 @@ const MasonryPhoto: React.FC<MasonryPhotoProps> = ({
|
||||
className: 'w-full h-full object-cover rounded-lg',
|
||||
loading: 'lazy',
|
||||
isGallery: true,
|
||||
protectFromDownload: !allowDownloads,
|
||||
}}
|
||||
overlayBaseClassName="absolute inset-0 bg-black/40 transition-opacity duration-200 rounded-lg flex items-center justify-center gap-2"
|
||||
allowDownloads={allowDownloads}
|
||||
@@ -376,7 +375,6 @@ export const MasonryGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
className: 'w-full h-full object-cover rounded-lg transition-transform duration-300 group-hover:scale-[1.02]',
|
||||
loading: 'lazy',
|
||||
isGallery: true,
|
||||
protectFromDownload: !allowDownloads,
|
||||
}}
|
||||
overlayBaseClassName="absolute inset-0 bg-black/40 transition-opacity duration-200 rounded-lg flex items-center justify-center gap-2"
|
||||
actionVariant="dark"
|
||||
@@ -434,7 +432,6 @@ export const MasonryGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
className: 'w-full h-full object-cover rounded-lg transition-transform duration-300 group-hover:scale-[1.02]',
|
||||
loading: 'lazy',
|
||||
isGallery: true,
|
||||
protectFromDownload: !allowDownloads,
|
||||
}}
|
||||
overlayBaseClassName="absolute inset-0 bg-black/40 transition-opacity duration-200 rounded-lg flex items-center justify-center gap-2"
|
||||
actionVariant="dark"
|
||||
@@ -501,7 +498,6 @@ export const MasonryGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
className: 'w-full h-full object-cover transition-transform duration-300 group-hover:scale-105',
|
||||
loading: 'lazy',
|
||||
isGallery: true,
|
||||
protectFromDownload: !allowDownloads,
|
||||
}}
|
||||
overlayBaseClassName="absolute inset-0 bg-black/40 transition-opacity duration-200 flex items-center justify-center gap-2"
|
||||
actionVariant="dark"
|
||||
|
||||
@@ -83,7 +83,6 @@ const MosaicPhoto: React.FC<MosaicPhotoProps> = ({
|
||||
className: 'w-full h-full object-cover transition-transform duration-300 group-hover:scale-105',
|
||||
loading: 'lazy',
|
||||
isGallery: true,
|
||||
protectFromDownload: !allowDownloads,
|
||||
}}
|
||||
overlayBaseClassName="absolute inset-0 bg-black/40 transition-opacity duration-200 flex items-center justify-center gap-2"
|
||||
allowDownloads={allowDownloads}
|
||||
|
||||
@@ -130,7 +130,6 @@ export const TimelineGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
className: 'w-full h-full object-cover rounded-lg',
|
||||
loading: 'lazy',
|
||||
isGallery: true,
|
||||
protectFromDownload: !allowDownloads,
|
||||
}}
|
||||
overlayBaseClassName="absolute inset-0 bg-black/40 transition-opacity duration-200 rounded-lg flex items-center justify-center gap-2"
|
||||
allowDownloads={allowDownloads}
|
||||
|
||||
@@ -22,9 +22,7 @@ export const StoryHero: React.FC<StoryHeroProps> = ({
|
||||
stats,
|
||||
photo,
|
||||
slug,
|
||||
allowDownloads = true,
|
||||
protectionLevel = 'standard',
|
||||
useEnhancedProtection = false,
|
||||
useCanvasRendering = false
|
||||
}) => {
|
||||
const formattedDate = date
|
||||
@@ -51,12 +49,6 @@ export const StoryHero: React.FC<StoryHeroProps> = ({
|
||||
className="w-full h-full object-cover"
|
||||
isGallery={true}
|
||||
slug={slug}
|
||||
photoId={photo.id}
|
||||
requiresToken={photo.requires_token}
|
||||
secureUrlTemplate={photo.secure_url_template}
|
||||
protectFromDownload={!allowDownloads || useEnhancedProtection}
|
||||
protectionLevel={protectionLevel}
|
||||
useEnhancedProtection={useEnhancedProtection}
|
||||
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -28,9 +28,7 @@ export const StoryPhotoCard: React.FC<StoryPhotoCardProps> = ({
|
||||
onToggleFavorite,
|
||||
onClick,
|
||||
slug,
|
||||
allowDownloads = true,
|
||||
protectionLevel = 'standard',
|
||||
useEnhancedProtection = false,
|
||||
useCanvasRendering = false,
|
||||
featured = false,
|
||||
galleryId: _galleryId
|
||||
@@ -105,12 +103,6 @@ export const StoryPhotoCard: React.FC<StoryPhotoCardProps> = ({
|
||||
}`}
|
||||
isGallery={true}
|
||||
slug={slug}
|
||||
photoId={photo.id}
|
||||
requiresToken={photo.requires_token}
|
||||
secureUrlTemplate={photo.secure_url_template}
|
||||
protectFromDownload={!allowDownloads || useEnhancedProtection}
|
||||
protectionLevel={protectionLevel}
|
||||
useEnhancedProtection={useEnhancedProtection}
|
||||
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user