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

A thin compatibility layer for the new media processing #1707

Draft
wants to merge 10 commits into
base: stable
Choose a base branch
from
8 changes: 7 additions & 1 deletion src/components/post/post-attachment-audio.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { faTimes } from '@fortawesome/free-solid-svg-icons';

import { formatFileSize } from '../../utils';
import { Icon } from '../fontawesome-icons';
import { attachmentPreviewUrl } from '../../services/api';

class AudioAttachment extends PureComponent {
handleClickOnRemoveAttachment = () => {
Expand All @@ -26,7 +27,12 @@ class AudioAttachment extends PureComponent {
return (
<div className="attachment" role="figure" aria-label={`Audio attachment ${artistAndTitle}`}>
<div>
<audio src={props.url} title={artistAndTitle} preload="none" controls />
<audio
src={attachmentPreviewUrl(props.id, 'audio')}
title={artistAndTitle}
preload="none"
controls
/>
</div>
<div>
<a href={props.url} title={artistAndTitle} target="_blank">
Expand Down
21 changes: 19 additions & 2 deletions src/components/post/post-attachment-general.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,37 @@ import { faTimes } from '@fortawesome/free-solid-svg-icons';

import { formatFileSize } from '../../utils';
import { Icon } from '../fontawesome-icons';
import { attachmentPreviewUrl } from '../../services/api';

class GeneralAttachment extends PureComponent {
handleClickOnRemoveAttachment = () => {
this.props.removeAttachment(this.props.id);
};

handleClick = (e) => {
if (e.button !== 0 || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
return;
}
if (this.props.meta?.inProgress) {
e.preventDefault();
alert('This file is still being processed');
}
};

render() {
const { props } = this;
const { inProgress = false } = props.meta ?? {};
const formattedFileSize = formatFileSize(props.fileSize);
const nameAndSize = `${props.fileName} (${formattedFileSize})`;
const nameAndSize = `${props.fileName} (${inProgress ? 'processing...' : formattedFileSize})`;

return (
<div className="attachment" role="figure" aria-label={`Attachment ${nameAndSize}`}>
<a href={props.url} title={nameAndSize} target="_blank">
<a
href={attachmentPreviewUrl(props.id, 'original')}
onClick={this.handleClick}
title={nameAndSize}
target="_blank"
>
<Icon icon={faFile} className="attachment-icon" />
<span className="attachment-title">{nameAndSize}</span>
</a>
Expand Down
27 changes: 27 additions & 0 deletions src/components/post/post-attachment-geometry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const thumbnailMaxWidth = 525;
const thumbnailMaxHeight = 175;

const videoMaxWidth = 500;
const videoMaxHeight = 400;

export function thumbnailSize(att) {
return fitIntoBox(att, thumbnailMaxWidth, thumbnailMaxHeight);
}

export function videoSize(att) {
return fitIntoBox(att, videoMaxWidth, videoMaxHeight);
}

function fitIntoBox(att, boxWidth, boxHeight) {
const [width, height] = [att.previewWidth ?? att.width, att.previewHeight ?? att.height];
boxWidth = Math.min(boxWidth, width);
boxHeight = Math.min(boxHeight, height);
const wRatio = width / boxWidth;
const hRatio = height / boxHeight;

if (wRatio > hRatio) {
return { width: boxWidth, height: Math.round(height / wRatio) };
}

return { width: Math.round(width / hRatio), height: boxHeight };
}
11 changes: 6 additions & 5 deletions src/components/post/post-attachment-image-container.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { faChevronCircleRight } from '@fortawesome/free-solid-svg-icons';
import { Icon } from '../fontawesome-icons';
import { lazyComponent } from '../lazy-component';
import { openLightbox } from '../../services/lightbox';
import { attachmentPreviewUrl } from '../../services/api';
import ImageAttachment from './post-attachment-image';
import { thumbnailSize } from './post-attachment-geometry';

const bordersSize = 4;
const spaceSize = 8;
Expand Down Expand Up @@ -38,7 +40,7 @@ export default class ImageAttachmentsContainer extends Component {

getItemWidths() {
return this.props.attachments
.map(({ imageSizes: { t, o } }) => (t ? t.w : o ? o.w : 0))
.map((att) => thumbnailSize(att).width)
.map((w) => w + bordersSize + spaceSize);
}

Expand Down Expand Up @@ -72,11 +74,10 @@ export default class ImageAttachmentsContainer extends Component {

getPswpItems() {
return this.props.attachments.map((a) => ({
src: a.url,
width: a.imageSizes?.o?.w ?? 1,
height: a.imageSizes?.o?.h ?? 1,
src: attachmentPreviewUrl(a.id, 'image'),
width: a.previewWidth ?? a.width,
height: a.previewHeight ?? a.height,
pid: this.getPictureId(a),
autoSize: !a.imageSizes?.o?.w,
}));
}

Expand Down
64 changes: 21 additions & 43 deletions src/components/post/post-attachment-image.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { faTimes } from '@fortawesome/free-solid-svg-icons';

import { formatFileSize } from '../../utils';
import { Icon } from '../fontawesome-icons';
import { attachmentPreviewUrl } from '../../services/api';
import { thumbnailSize } from './post-attachment-geometry';

const NSFW_PREVIEW_AREA = 20;

Expand All @@ -19,57 +21,39 @@ class PostAttachmentImage extends PureComponent {
if (!nsfwCanvas) {
return;
}
const { width, height } = thumbnailSize(this.props);
const ctx = nsfwCanvas.getContext('2d');
ctx.fillStyle = '#cccccc';
ctx.fillRect(0, 0, nsfwCanvas.width, nsfwCanvas.height);
const img = new Image();
img.onload = () =>
nsfwCanvas.isConnected && ctx.drawImage(img, 0, 0, nsfwCanvas.width, nsfwCanvas.height);
img.src = this.props.imageSizes.t?.url ?? this.props.thumbnailUrl;
img.src = attachmentPreviewUrl(this.props.id, 'image', width, height);
}

render() {
const { props } = this;

const formattedFileSize = formatFileSize(props.fileSize);
const formattedImageSize = props.imageSizes.o
? `, ${props.imageSizes.o.w}×${props.imageSizes.o.h}px`
: '';
const formattedImageSize = `, ${props.width}×${props.height}px`;
const nameAndSize = `${props.fileName} (${formattedFileSize}${formattedImageSize})`;
const alt = `Image attachment ${props.fileName}`;

let srcSet;
if (props.imageSizes.t2 && props.imageSizes.t2.url) {
srcSet = `${props.imageSizes.t2.url} 2x`;
} else if (
props.imageSizes.o &&
props.imageSizes.t &&
props.imageSizes.o.w <= props.imageSizes.t.w * 2
) {
srcSet = `${props.imageSizes.o.url || props.url} 2x`;
}
const { width, height } = thumbnailSize(this.props);

const imageAttributes = {
src: (props.imageSizes.t && props.imageSizes.t.url) || props.thumbnailUrl,
srcSet,
src: attachmentPreviewUrl(props.id, 'image', width, height),
srcSet: `${attachmentPreviewUrl(props.id, 'image', width * 2, height * 2)} 2x`,
alt,
id: props.pictureId,
loading: 'lazy',
width: props.imageSizes.t
? props.imageSizes.t.w
: props.imageSizes.o
? props.imageSizes.o.w
: undefined,
height: props.imageSizes.t
? props.imageSizes.t.h
: props.imageSizes.o
? props.imageSizes.o.h
: undefined,
width,
height,
};

const area = imageAttributes.width * imageAttributes.height;
const canvasWidth = Math.round(imageAttributes.width * Math.sqrt(NSFW_PREVIEW_AREA / area));
const canvasHeight = Math.round(imageAttributes.height * Math.sqrt(NSFW_PREVIEW_AREA / area));
const area = width * height;
const canvasWidth = Math.round(width * Math.sqrt(NSFW_PREVIEW_AREA / area));
const canvasHeight = Math.round(height * Math.sqrt(NSFW_PREVIEW_AREA / area));

return (
<div
Expand All @@ -84,21 +68,15 @@ class PostAttachmentImage extends PureComponent {
target="_blank"
className="image-attachment-link"
>
{props.thumbnailUrl ? (
<>
{props.isNSFW && (
<canvas
ref={this.canvasRef}
className="image-attachment-nsfw-canvas"
width={canvasWidth}
height={canvasHeight}
/>
)}
<img className="image-attachment-img" {...imageAttributes} />
</>
) : (
props.id
{props.isNSFW && (
<canvas
ref={this.canvasRef}
className="image-attachment-nsfw-canvas"
width={canvasWidth}
height={canvasHeight}
/>
)}
<img className="image-attachment-img" {...imageAttributes} />
</a>

{props.isEditing && (
Expand Down
94 changes: 94 additions & 0 deletions src/components/post/post-attachment-like-a-video.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useEffect, useRef, useState } from 'react';
import { faFileVideo, faPlayCircle } from '@fortawesome/free-regular-svg-icons';
import { faTimes } from '@fortawesome/free-solid-svg-icons';

import { useEvent } from 'react-use-event-hook';
import { formatFileSize } from '../../utils';
import { ButtonLink } from '../button-link';
import { Icon } from '../fontawesome-icons';
import { attachmentPreviewUrl } from '../../services/api';

export default function LikeAVideoAttachment({
id,
fileName,
fileSize,
removeAttachment,
isEditing,
}) {
const url = attachmentPreviewUrl(id, 'original');
const [isOpen, setIsOpen] = useState(false);

const handleClickOnRemoveAttachment = useEvent(() => removeAttachment(id));
const toggleOpen = useEvent(() => setIsOpen(true));

const formattedFileSize = formatFileSize(fileSize);
const title = `${fileName} (${formattedFileSize})`;

const videoRef = useRef(null);

// Prevent video from playing infinitely (we has this situation once and don't
// want it to happen again)
useEffect(() => {
if (!isOpen || !videoRef.current) {
return;
}
const videoEl = videoRef.current;

// By default, the video playback should be paused after 5 minutes
let maxPlayTime = 300 * 1000;
let playTimer = 0;
const onPlay = () => {
clearTimeout(playTimer);
playTimer = setTimeout(() => videoEl.pause(), maxPlayTime);
};
const onPause = () => clearTimeout(playTimer);
const onDurationChange = () => {
// Video in playback mode should not be longer than 10 times of the video duration
maxPlayTime = videoEl.duration * 10 * 1000;
};
const abortController = new AbortController();
const { signal } = abortController;

videoEl.addEventListener('durationchange', onDurationChange, { once: true, signal });
videoEl.addEventListener('play', onPlay, { signal });
videoEl.addEventListener('pause', onPause, { signal });
signal.addEventListener('abort', onPause);
return () => abortController.abort();
}, [isOpen]);

return (
<div className="attachment" role="figure" aria-label={`Video attachment ${title}`}>
{isOpen ? (
<div>
<video title={title} autoPlay controls ref={videoRef}>
<source src={url} />
Your browser does not support HTML5 video tag.
</video>
</div>
) : (
<ButtonLink
onClick={toggleOpen}
className="video-attachment-click-to-play"
title="Click to play video"
>
<Icon icon={faPlayCircle} />
</ButtonLink>
)}
<div>
<a href={url} title={title} target="_blank">
<Icon icon={faFileVideo} className="attachment-icon" />
<span>{title}</span>
</a>

{isEditing && (
<Icon
icon={faTimes}
className="remove-attachment"
title="Remove video file"
onClick={handleClickOnRemoveAttachment}
/>
)}
</div>
</div>
);
}
Loading
Loading