Port attachments and embeds.

This commit is contained in:
Paul
2021-06-20 22:09:18 +01:00
parent a24bcf9f86
commit d1bff98635
9 changed files with 781 additions and 2 deletions

View File

@@ -0,0 +1,119 @@
.attachment {
border-radius: 6px;
margin: .125rem 0 .125rem;
&[data-spoiler="true"] {
filter: blur(30px);
pointer-events: none;
}
&[data-has-content="true"] {
margin-top: 4px;
}
&.image {
cursor: pointer;
}
&.video {
.actions {
padding: 10px 12px;
border-radius: 6px 6px 0 0;
}
video {
width: 100%;
border-radius: 0 0 6px 6px;
}
}
&.audio {
gap: 4px;
padding: 6px;
display: flex;
border-radius: 6px;
flex-direction: column;
background: var(--secondary-background);
max-width: 400px;
> audio {
width: 100%;
}
}
&.file {
> div {
width: 400px;
padding: 12px;
user-select: none;
width: fit-content;
border-radius: 6px;
}
}
&.text {
display: flex;
overflow: hidden;
max-width: 800px;
border-radius: 6px;
flex-direction: column;
.textContent {
height: 140px;
padding: 12px;
overflow-x: auto;
overflow-y: auto;
border-radius: 0 !important;
background: var(--secondary-header);
pre {
margin: 0;
}
pre code {
font-family: "Fira Mono", sans-serif;
}
&[data-loading="true"] {
display: flex;
> * {
flex-grow: 1;
}
}
}
}
}
.actions {
gap: 8px;
padding: 8px;
display: flex;
overflow: none;
max-width: 100%;
align-items: center;
flex-direction: row;
color: var(--foreground);
background: var(--secondary-background);
> svg {
flex-shrink: 0;
}
.info {
display: flex;
flex-direction: column;
flex-grow: 1;
> span {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.filesize {
font-size: 10px;
color: var(--secondary-foreground);
}
}
}

View File

@@ -0,0 +1,152 @@
import TextFile from "./TextFile";
import { Text } from "preact-i18n";
import classNames from "classnames";
import styles from "./Attachment.module.scss";
import AttachmentActions from "./AttachmentActions";
import { useContext, useState } from "preact/hooks";
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
import { Attachment as AttachmentRJS } from "revolt.js/dist/api/objects";
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/MessageArea";
interface Props {
attachment: AttachmentRJS;
hasContent: boolean;
}
const MAX_ATTACHMENT_WIDTH = 480;
const MAX_ATTACHMENT_HEIGHT = 640;
export default function Attachment({ attachment, hasContent }: Props) {
const client = useContext(AppContext);
const { openScreen } = useIntermediate();
const { filename, metadata } = attachment;
const [ spoiler, setSpoiler ] = useState(filename.startsWith("SPOILER_"));
const maxWidth = Math.min(useContext(MessageAreaWidthContext), MAX_ATTACHMENT_WIDTH);
const url = client.generateFileURL(attachment, { width: MAX_ATTACHMENT_WIDTH * 1.5 }, true);
let width = 0,
height = 0;
if (metadata.type === 'Image' || metadata.type === 'Video') {
let limitingWidth = Math.min(
maxWidth,
metadata.width
);
let limitingHeight = Math.min(
MAX_ATTACHMENT_HEIGHT,
metadata.height
);
// Calculate smallest possible WxH.
width = Math.min(
limitingWidth,
limitingHeight * (metadata.width / metadata.height)
);
height = Math.min(
limitingHeight,
limitingWidth * (metadata.height / metadata.width)
);
}
switch (metadata.type) {
case "Image": {
return (
<div
style={{ width }}
className={styles.container}
onClick={() => spoiler && setSpoiler(false)}
>
{spoiler && (
<div className={styles.overflow}>
<div style={{ width, height }}>
<span><Text id="app.main.channel.misc.spoiler_attachment" /></span>
</div>
</div>
)}
<img
src={url}
alt={filename}
data-spoiler={spoiler}
data-has-content={hasContent}
className={classNames(styles.attachment, styles.image)}
onClick={() =>
openScreen({ id: "image_viewer", attachment })
}
onMouseDown={ev =>
ev.button === 1 &&
window.open(url, "_blank")
}
style={{ width, height }}
/>
</div>
);
}
case "Audio": {
return (
<div
className={classNames(styles.attachment, styles.audio)}
data-has-content={hasContent}
>
<AttachmentActions attachment={attachment} />
<audio src={url} controls />
</div>
);
}
case "Video": {
return (
<div
className={styles.container}
onClick={() => spoiler && setSpoiler(false)}>
{spoiler && (
<div className={styles.overflow}>
<div style={{ width, height }}>
<span><Text id="app.main.channel.misc.spoiler_attachment" /></span>
</div>
</div>
)}
<div
style={{ width }}
data-spoiler={spoiler}
data-has-content={hasContent}
className={classNames(styles.attachment, styles.video)}
>
<AttachmentActions attachment={attachment} />
<video
src={url}
controls
style={{ width, height }}
onMouseDown={ev =>
ev.button === 1 &&
window.open(url, "_blank")
}
/>
</div>
</div>
);
}
case 'Text': {
return (
<div
className={classNames(styles.attachment, styles.text)}
data-has-content={hasContent}
>
<TextFile attachment={attachment} />
<AttachmentActions attachment={attachment} />
</div>
);
}
default: {
return (
<div
className={classNames(styles.attachment, styles.file)}
data-has-content={hasContent}
>
<AttachmentActions attachment={attachment} />
</div>
);
}
}
}

View File

@@ -0,0 +1,98 @@
import { useContext } from 'preact/hooks';
import styles from './Attachment.module.scss';
import IconButton from '../../../ui/IconButton';
import { Attachment } from "revolt.js/dist/api/objects";
import { AppContext } from '../../../../context/revoltjs/RevoltClient';
import { Download, ExternalLink, File, Headphones, Video } from '@styled-icons/feather';
interface Props {
attachment: Attachment;
}
export function determineFileSize(size: number) {
if (size > 1e6) {
return `${(size / 1e6).toFixed(2)} MB`;
} else if (size > 1e3) {
return `${(size / 1e3).toFixed(2)} KB`;
}
return `${size} B`;
}
export default function AttachmentActions({ attachment }: Props) {
const client = useContext(AppContext);
const { filename, metadata, size } = attachment;
const url = client.generateFileURL(attachment) as string;
const open_url = `${url}/${filename}`;
const download_url = url.replace('attachments', 'attachments/download')
const filesize = determineFileSize(size as any);
switch (metadata.type) {
case 'Image':
return (
<div className={styles.actions}>
<div className={styles.info}>
<span className={styles.filename}>{filename}</span>
<span className={styles.filesize}>{metadata.width + 'x' + metadata.height} ({filesize})</span>
</div>
<a href={open_url} target="_blank">
<IconButton>
<ExternalLink size={24} />
</IconButton>
</a>
<a href={download_url} download target="_blank">
<IconButton>
<Download size={24} />
</IconButton>
</a>
</div>
)
case 'Audio':
return (
<div className={styles.actions}>
<Headphones size={24} strokeWidth={1.5} />
<div className={styles.info}>
<span className={styles.filename}>{filename}</span>
<span className={styles.filesize}>{filesize}</span>
</div>
<a href={download_url} download target="_blank">
<IconButton>
<Download size={24} strokeWidth={1.5} />
</IconButton>
</a>
</div>
)
case 'Video':
return (
<div className={styles.actions}>
<Video size={24} strokeWidth={1.5} />
<div className={styles.info}>
<span className={styles.filename}>{filename}</span>
<span className={styles.filesize}>{metadata.width + 'x' + metadata.height} ({filesize})</span>
</div>
<a href={download_url} download target="_blank">
<IconButton>
<Download size={24} strokeWidth={1.5}/>
</IconButton>
</a>
</div>
)
default:
return (
<div className={styles.actions}>
<File size={24} strokeWidth={1.5} />
<div className={styles.info}>
<span className={styles.filename}>{filename}</span>
<span className={styles.filesize}>{filesize}</span>
</div>
<a href={download_url} download target="_blank">
<IconButton>
<Download size={24} strokeWidth={1.5} />
</IconButton>
</a>
</div>
)
}
}

View File

@@ -0,0 +1,57 @@
import axios from 'axios';
import Preloader from '../../../ui/Preloader';
import styles from './Attachment.module.scss';
import { Attachment } from 'revolt.js/dist/api/objects';
import { useContext, useEffect, useState } from 'preact/hooks';
import RequiresOnline from '../../../../context/revoltjs/RequiresOnline';
import { AppContext, StatusContext } from '../../../../context/revoltjs/RevoltClient';
interface Props {
attachment: Attachment;
}
const fileCache: { [key: string]: string } = {};
export default function TextFile({ attachment }: Props) {
const [ content, setContent ] = useState<undefined | string>(undefined);
const [ loading, setLoading ] = useState(false);
const status = useContext(StatusContext);
const client = useContext(AppContext);
const url = client.generateFileURL(attachment);
useEffect(() => {
if (typeof content !== 'undefined') return;
if (loading) return;
setLoading(true);
let cached = fileCache[attachment._id];
if (cached) {
setContent(cached);
setLoading(false);
} else {
axios.get(url)
.then(res => {
setContent(res.data);
fileCache[attachment._id] = res.data;
setLoading(false);
})
.catch(() => {
console.error("Failed to load text file. [", attachment._id, "]");
setLoading(false)
})
}
}, [ content, loading, status ]);
return (
<div className={styles.textContent} data-loading={typeof content === 'undefined'}>
{
content ?
<pre><code>{ content }</code></pre>
: <RequiresOnline>
<Preloader />
</RequiresOnline>
}
</div>
)
}