forked from abner/for-legacy-web
Port attachments and embeds.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
152
src/components/common/messaging/attachments/Attachment.tsx
Normal file
152
src/components/common/messaging/attachments/Attachment.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
57
src/components/common/messaging/attachments/TextFile.tsx
Normal file
57
src/components/common/messaging/attachments/TextFile.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user