mirror of
https://github.com/revoltchat/revite.git
synced 2024-12-24 22:52:09 -05:00
Port attachments and embeds.
This commit is contained in:
parent
a24bcf9f86
commit
d1bff98635
9 changed files with 781 additions and 2 deletions
|
@ -1,7 +1,9 @@
|
||||||
|
import Embed from "./embed/Embed";
|
||||||
import UserIcon from "../user/UserIcon";
|
import UserIcon from "../user/UserIcon";
|
||||||
import { Username } from "../user/UserShort";
|
import { Username } from "../user/UserShort";
|
||||||
import Markdown from "../../markdown/Markdown";
|
import Markdown from "../../markdown/Markdown";
|
||||||
import { Children } from "../../../types/Preact";
|
import { Children } from "../../../types/Preact";
|
||||||
|
import Attachment from "./attachments/Attachment";
|
||||||
import { attachContextMenu } from "preact-context-menu";
|
import { attachContextMenu } from "preact-context-menu";
|
||||||
import { useUser } from "../../../context/revoltjs/hooks";
|
import { useUser } from "../../../context/revoltjs/hooks";
|
||||||
import { MessageObject } from "../../../context/revoltjs/util";
|
import { MessageObject } from "../../../context/revoltjs/util";
|
||||||
|
@ -15,11 +17,12 @@ interface Props {
|
||||||
head?: boolean
|
head?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Message({ attachContext, message, contrast, content, head }: Props) {
|
export default function Message({ attachContext, message, contrast, content: replacement, head }: Props) {
|
||||||
// TODO: Can improve re-renders here by providing a list
|
// TODO: Can improve re-renders here by providing a list
|
||||||
// TODO: of dependencies. We only need to update on u/avatar.
|
// TODO: of dependencies. We only need to update on u/avatar.
|
||||||
let user = useUser(message.author);
|
let user = useUser(message.author);
|
||||||
|
|
||||||
|
const content = message.content as string;
|
||||||
return (
|
return (
|
||||||
<MessageBase contrast={contrast}
|
<MessageBase contrast={contrast}
|
||||||
onContextMenu={attachContext ? attachContextMenu('Menu', { message, contextualChannel: message.channel }) : undefined}>
|
onContextMenu={attachContext ? attachContextMenu('Menu', { message, contextualChannel: message.channel }) : undefined}>
|
||||||
|
@ -30,7 +33,11 @@ export default function Message({ attachContext, message, contrast, content, hea
|
||||||
</MessageInfo>
|
</MessageInfo>
|
||||||
<MessageContent>
|
<MessageContent>
|
||||||
{ head && <Username user={user} /> }
|
{ head && <Username user={user} /> }
|
||||||
{ content ?? <Markdown content={message.content as string} /> }
|
{ content ?? <Markdown content={content} /> }
|
||||||
|
{ message.attachments?.map((attachment, index) =>
|
||||||
|
<Attachment key={index} attachment={attachment} hasContent={ index > 0 || content.length > 0 } />) }
|
||||||
|
{ message.embeds?.map((embed, index) =>
|
||||||
|
<Embed key={index} embed={embed} />) }
|
||||||
</MessageContent>
|
</MessageContent>
|
||||||
</MessageBase>
|
</MessageBase>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
97
src/components/common/messaging/embed/Embed.module.scss
Normal file
97
src/components/common/messaging/embed/Embed.module.scss
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
.embed {
|
||||||
|
margin: .2em 0;
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.image {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.website {
|
||||||
|
gap: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
> div:nth-child(1) {
|
||||||
|
gap: 6px;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
border-inline-start-width: 4px;
|
||||||
|
border-inline-start-style: solid;
|
||||||
|
|
||||||
|
padding: 12px;
|
||||||
|
width: fit-content;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--primary-header);
|
||||||
|
|
||||||
|
.siteinfo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
.favicon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site {
|
||||||
|
font-size: 11px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: var(--secondary-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.author {
|
||||||
|
font-size: 1em;
|
||||||
|
color: var(--primary-text);
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 1.1em;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
// -webkit-line-clamp: 6;
|
||||||
|
// -webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.image {
|
||||||
|
cursor: pointer;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
145
src/components/common/messaging/embed/Embed.tsx
Normal file
145
src/components/common/messaging/embed/Embed.tsx
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import EmbedMedia from './EmbedMedia';
|
||||||
|
import styles from "./Embed.module.scss";
|
||||||
|
import { useContext } from 'preact/hooks';
|
||||||
|
import { Embed as EmbedRJS } from "revolt.js/dist/api/objects";
|
||||||
|
import { useIntermediate } from '../../../../context/intermediate/Intermediate';
|
||||||
|
import { MessageAreaWidthContext } from '../../../../pages/channels/messaging/MessageArea';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
embed: EmbedRJS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_EMBED_WIDTH = 480;
|
||||||
|
const MAX_EMBED_HEIGHT = 640;
|
||||||
|
const CONTAINER_PADDING = 24;
|
||||||
|
const MAX_PREVIEW_SIZE = 150;
|
||||||
|
|
||||||
|
export default function Embed({ embed }: Props) {
|
||||||
|
// ! FIXME: temp code
|
||||||
|
// ! add proxy function to client
|
||||||
|
function proxyImage(url: string) {
|
||||||
|
return 'https://jan.revolt.chat/proxy?url=' + encodeURIComponent(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { openScreen } = useIntermediate();
|
||||||
|
const maxWidth = Math.min(useContext(MessageAreaWidthContext) - CONTAINER_PADDING, MAX_EMBED_WIDTH);
|
||||||
|
|
||||||
|
function calculateSize(w: number, h: number): { width: number, height: number } {
|
||||||
|
let limitingWidth = Math.min(
|
||||||
|
maxWidth,
|
||||||
|
w
|
||||||
|
);
|
||||||
|
|
||||||
|
let limitingHeight = Math.min(
|
||||||
|
MAX_EMBED_HEIGHT,
|
||||||
|
h
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate smallest possible WxH.
|
||||||
|
let width = Math.min(
|
||||||
|
limitingWidth,
|
||||||
|
limitingHeight * (w / h)
|
||||||
|
);
|
||||||
|
|
||||||
|
let height = Math.min(
|
||||||
|
limitingHeight,
|
||||||
|
limitingWidth * (h / w)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (embed.type) {
|
||||||
|
case 'Website': {
|
||||||
|
// ! FIXME: move this to january
|
||||||
|
/*if (embed.url && YOUTUBE_RE.test(embed.url)) {
|
||||||
|
embed.color = '#FF424F';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (embed.url && TWITCH_RE.test(embed.url)) {
|
||||||
|
embed.color = '#7B68EE';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (embed.url && SPOTIFY_RE.test(embed.url)) {
|
||||||
|
embed.color = '#1ABC9C';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (embed.url && SOUNDCLOUD_RE.test(embed.url)) {
|
||||||
|
embed.color = '#FF7F50';
|
||||||
|
}*/
|
||||||
|
|
||||||
|
// Determine special embed size.
|
||||||
|
let mw, mh;
|
||||||
|
let largeMedia = (embed.special && embed.special.type !== 'None') || embed.image?.size === 'Large';
|
||||||
|
switch (embed.special?.type) {
|
||||||
|
case 'YouTube':
|
||||||
|
case 'Bandcamp': {
|
||||||
|
mw = embed.video?.width ?? 1280;
|
||||||
|
mh = embed.video?.height ?? 720;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'Twitch': {
|
||||||
|
mw = 1280;
|
||||||
|
mh = 720;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
if (embed.image?.size === 'Preview') {
|
||||||
|
mw = MAX_EMBED_WIDTH;
|
||||||
|
mh = Math.min(embed.image.height ?? 0, MAX_PREVIEW_SIZE);
|
||||||
|
} else {
|
||||||
|
mw = embed.image?.width ?? MAX_EMBED_WIDTH;
|
||||||
|
mh = embed.image?.height ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let { width, height } = calculateSize(mw, mh);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(styles.embed, styles.website)}
|
||||||
|
style={{
|
||||||
|
borderInlineStartColor: embed.color ?? 'var(--tertiary-background)',
|
||||||
|
width: width + CONTAINER_PADDING
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
{ embed.site_name && <div className={styles.siteinfo}>
|
||||||
|
{ embed.icon_url && <img className={styles.favicon} src={proxyImage(embed.icon_url)} draggable={false} onError={e => e.currentTarget.style.display = 'none'} /> }
|
||||||
|
<div className={styles.site}>{ embed.site_name } </div>
|
||||||
|
</div> }
|
||||||
|
|
||||||
|
{/*<span><a href={embed.url} target={"_blank"} className={styles.author}>Author</a></span>*/}
|
||||||
|
{ embed.title && <span><a href={embed.url} target={"_blank"} className={styles.title}>{ embed.title }</a></span> }
|
||||||
|
{ embed.description && <div className={styles.description}>{ embed.description }</div> }
|
||||||
|
|
||||||
|
{ largeMedia && <EmbedMedia embed={embed} height={height} /> }
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
!largeMedia && <div>
|
||||||
|
<EmbedMedia embed={embed} width={height * ((embed.image?.width ?? 0) / (embed.image?.height ?? 0))} height={height} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case 'Image': {
|
||||||
|
return (
|
||||||
|
<img className={classNames(styles.embed, styles.image)}
|
||||||
|
style={calculateSize(embed.width, embed.height)}
|
||||||
|
src={proxyImage(embed.url)}
|
||||||
|
type="text/html"
|
||||||
|
frameBorder="0"
|
||||||
|
onClick={() =>
|
||||||
|
openScreen({ id: "image_viewer", embed })
|
||||||
|
}
|
||||||
|
onMouseDown={ev =>
|
||||||
|
ev.button === 1 &&
|
||||||
|
window.open(embed.url, "_blank")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
78
src/components/common/messaging/embed/EmbedMedia.tsx
Normal file
78
src/components/common/messaging/embed/EmbedMedia.tsx
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import styles from './Embed.module.scss';
|
||||||
|
import { Embed } from "revolt.js/dist/api/objects";
|
||||||
|
import { useIntermediate } from '../../../../context/intermediate/Intermediate';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
embed: Embed;
|
||||||
|
width?: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EmbedMedia({ embed, width, height }: Props) {
|
||||||
|
// ! FIXME: temp code
|
||||||
|
// ! add proxy function to client
|
||||||
|
function proxyImage(url: string) {
|
||||||
|
return 'https://jan.revolt.chat/proxy?url=' + encodeURIComponent(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (embed.type !== 'Website') return null;
|
||||||
|
const { openScreen } = useIntermediate();
|
||||||
|
|
||||||
|
switch (embed.special?.type) {
|
||||||
|
case 'YouTube': return (
|
||||||
|
<iframe
|
||||||
|
src={`https://www.youtube-nocookie.com/embed/${embed.special.id}?modestbranding=1`}
|
||||||
|
allowFullScreen
|
||||||
|
style={{ height }} />
|
||||||
|
)
|
||||||
|
case 'Twitch': return (
|
||||||
|
<iframe
|
||||||
|
src={`https://player.twitch.tv/?${embed.special.content_type.toLowerCase()}=${embed.special.id}&parent=${window.location.hostname}&autoplay=false`}
|
||||||
|
frameBorder="0"
|
||||||
|
allowFullScreen
|
||||||
|
scrolling="no"
|
||||||
|
style={{ height, }} />
|
||||||
|
)
|
||||||
|
case 'Spotify': return (
|
||||||
|
<iframe
|
||||||
|
src={`https://open.spotify.com/embed/${embed.special.content_type}/${embed.special.id}`}
|
||||||
|
frameBorder="0"
|
||||||
|
allowFullScreen
|
||||||
|
allowTransparency
|
||||||
|
style={{ height }} />
|
||||||
|
)
|
||||||
|
case 'Soundcloud': return (
|
||||||
|
<iframe
|
||||||
|
src={`https://w.soundcloud.com/player/?url=${encodeURIComponent(embed.url as string)}&color=%23FF7F50&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true&visual=true`}
|
||||||
|
frameBorder="0"
|
||||||
|
scrolling="no"
|
||||||
|
style={{ height }} />
|
||||||
|
)
|
||||||
|
case 'Bandcamp': {
|
||||||
|
return <iframe
|
||||||
|
src={`https://bandcamp.com/EmbeddedPlayer/${embed.special.content_type.toLowerCase()}=${embed.special.id}/size=large/bgcol=181a1b/linkcol=056cc4/tracklist=false/transparent=true/`}
|
||||||
|
seamless
|
||||||
|
style={{ height }} />;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
if (embed.image) {
|
||||||
|
let url = embed.image.url;
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
className={styles.image}
|
||||||
|
src={proxyImage(url)}
|
||||||
|
style={{ width, height }}
|
||||||
|
onClick={() =>
|
||||||
|
openScreen({ id: "image_viewer", embed: embed.image })
|
||||||
|
}
|
||||||
|
onMouseDown={ev =>
|
||||||
|
ev.button === 1 &&
|
||||||
|
window.open(url, "_blank")
|
||||||
|
} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
26
src/components/common/messaging/embed/EmbedMediaActions.tsx
Normal file
26
src/components/common/messaging/embed/EmbedMediaActions.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import styles from './Embed.module.scss';
|
||||||
|
import IconButton from '../../../ui/IconButton';
|
||||||
|
import { ExternalLink } from '@styled-icons/feather';
|
||||||
|
import { EmbedImage } from "revolt.js/dist/api/objects";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
embed: EmbedImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EmbedMediaActions({ embed }: Props) {
|
||||||
|
const filename = embed.url.split('/').pop();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<div className={styles.info}>
|
||||||
|
<span className={styles.filename}>{filename}</span>
|
||||||
|
<span className={styles.filesize}>{embed.width + 'x' + embed.height}</span>
|
||||||
|
</div>
|
||||||
|
<a href={embed.url} target="_blank">
|
||||||
|
<IconButton>
|
||||||
|
<ExternalLink size={24} />
|
||||||
|
</IconButton>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in a new issue