mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-06 17:11:55 +00:00
Use tabWidth 4 without actual tabs.
This commit is contained in:
@@ -13,121 +13,121 @@ import AttachmentActions from "./AttachmentActions";
|
||||
import TextFile from "./TextFile";
|
||||
|
||||
interface Props {
|
||||
attachment: AttachmentRJS;
|
||||
hasContent: boolean;
|
||||
attachment: AttachmentRJS;
|
||||
hasContent: boolean;
|
||||
}
|
||||
|
||||
const MAX_ATTACHMENT_WIDTH = 480;
|
||||
|
||||
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 [loaded, setLoaded] = useState(false);
|
||||
const client = useContext(AppContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
const { filename, metadata } = attachment;
|
||||
const [spoiler, setSpoiler] = useState(filename.startsWith("SPOILER_"));
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
const url = client.generateFileURL(
|
||||
attachment,
|
||||
{ width: MAX_ATTACHMENT_WIDTH * 1.5 },
|
||||
true,
|
||||
);
|
||||
const url = client.generateFileURL(
|
||||
attachment,
|
||||
{ width: MAX_ATTACHMENT_WIDTH * 1.5 },
|
||||
true,
|
||||
);
|
||||
|
||||
switch (metadata.type) {
|
||||
case "Image": {
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
onClick={() => spoiler && setSpoiler(false)}>
|
||||
{spoiler && (
|
||||
<div className={styles.overflow}>
|
||||
<span>
|
||||
<Text id="app.main.channel.misc.spoiler_attachment" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
src={url}
|
||||
alt={filename}
|
||||
width={metadata.width}
|
||||
height={metadata.height}
|
||||
data-spoiler={spoiler}
|
||||
data-has-content={hasContent}
|
||||
className={classNames(
|
||||
styles.attachment,
|
||||
styles.image,
|
||||
loaded && styles.loaded,
|
||||
)}
|
||||
onClick={() =>
|
||||
openScreen({ id: "image_viewer", attachment })
|
||||
}
|
||||
onMouseDown={(ev) =>
|
||||
ev.button === 1 && window.open(url, "_blank")
|
||||
}
|
||||
onLoad={() => setLoaded(true)}
|
||||
/>
|
||||
</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}>
|
||||
<span>
|
||||
<Text id="app.main.channel.misc.spoiler_attachment" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
data-spoiler={spoiler}
|
||||
data-has-content={hasContent}
|
||||
className={classNames(styles.attachment, styles.video)}>
|
||||
<AttachmentActions attachment={attachment} />
|
||||
<video
|
||||
src={url}
|
||||
width={metadata.width}
|
||||
height={metadata.height}
|
||||
className={classNames(loaded && styles.loaded)}
|
||||
controls
|
||||
onMouseDown={(ev) =>
|
||||
ev.button === 1 && window.open(url, "_blank")
|
||||
}
|
||||
onLoadedMetadata={() => setLoaded(true)}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
}
|
||||
switch (metadata.type) {
|
||||
case "Image": {
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
onClick={() => spoiler && setSpoiler(false)}>
|
||||
{spoiler && (
|
||||
<div className={styles.overflow}>
|
||||
<span>
|
||||
<Text id="app.main.channel.misc.spoiler_attachment" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
src={url}
|
||||
alt={filename}
|
||||
width={metadata.width}
|
||||
height={metadata.height}
|
||||
data-spoiler={spoiler}
|
||||
data-has-content={hasContent}
|
||||
className={classNames(
|
||||
styles.attachment,
|
||||
styles.image,
|
||||
loaded && styles.loaded,
|
||||
)}
|
||||
onClick={() =>
|
||||
openScreen({ id: "image_viewer", attachment })
|
||||
}
|
||||
onMouseDown={(ev) =>
|
||||
ev.button === 1 && window.open(url, "_blank")
|
||||
}
|
||||
onLoad={() => setLoaded(true)}
|
||||
/>
|
||||
</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}>
|
||||
<span>
|
||||
<Text id="app.main.channel.misc.spoiler_attachment" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
data-spoiler={spoiler}
|
||||
data-has-content={hasContent}
|
||||
className={classNames(styles.attachment, styles.video)}>
|
||||
<AttachmentActions attachment={attachment} />
|
||||
<video
|
||||
src={url}
|
||||
width={metadata.width}
|
||||
height={metadata.height}
|
||||
className={classNames(loaded && styles.loaded)}
|
||||
controls
|
||||
onMouseDown={(ev) =>
|
||||
ev.button === 1 && window.open(url, "_blank")
|
||||
}
|
||||
onLoadedMetadata={() => setLoaded(true)}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
Download,
|
||||
LinkExternal,
|
||||
File,
|
||||
Headphone,
|
||||
Video,
|
||||
Download,
|
||||
LinkExternal,
|
||||
File,
|
||||
Headphone,
|
||||
Video,
|
||||
} from "@styled-icons/boxicons-regular";
|
||||
import { Attachment } from "revolt.js/dist/api/objects";
|
||||
|
||||
@@ -18,98 +18,98 @@ import { AppContext } from "../../../../context/revoltjs/RevoltClient";
|
||||
import IconButton from "../../../ui/IconButton";
|
||||
|
||||
interface Props {
|
||||
attachment: Attachment;
|
||||
attachment: Attachment;
|
||||
}
|
||||
|
||||
export default function AttachmentActions({ attachment }: Props) {
|
||||
const client = useContext(AppContext);
|
||||
const { filename, metadata, size } = attachment;
|
||||
const client = useContext(AppContext);
|
||||
const { filename, metadata, size } = attachment;
|
||||
|
||||
const url = client.generateFileURL(attachment)!;
|
||||
const open_url = `${url}/${filename}`;
|
||||
const download_url = url.replace("attachments", "attachments/download");
|
||||
const url = client.generateFileURL(attachment)!;
|
||||
const open_url = `${url}/${filename}`;
|
||||
const download_url = url.replace("attachments", "attachments/download");
|
||||
|
||||
const filesize = determineFileSize(size);
|
||||
const filesize = determineFileSize(size);
|
||||
|
||||
switch (metadata.type) {
|
||||
case "Image":
|
||||
return (
|
||||
<div className={classNames(styles.actions, styles.imageAction)}>
|
||||
<span className={styles.filename}>{filename}</span>
|
||||
<span className={styles.filesize}>
|
||||
{metadata.width + "x" + metadata.height} ({filesize})
|
||||
</span>
|
||||
<a
|
||||
href={open_url}
|
||||
target="_blank"
|
||||
className={styles.iconType}>
|
||||
<IconButton>
|
||||
<LinkExternal size={24} />
|
||||
</IconButton>
|
||||
</a>
|
||||
<a
|
||||
href={download_url}
|
||||
className={styles.downloadIcon}
|
||||
download
|
||||
target="_blank">
|
||||
<IconButton>
|
||||
<Download size={24} />
|
||||
</IconButton>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
case "Audio":
|
||||
return (
|
||||
<div className={classNames(styles.actions, styles.audioAction)}>
|
||||
<Headphone size={24} className={styles.iconType} />
|
||||
<span className={styles.filename}>{filename}</span>
|
||||
<span className={styles.filesize}>{filesize}</span>
|
||||
<a
|
||||
href={download_url}
|
||||
className={styles.downloadIcon}
|
||||
download
|
||||
target="_blank">
|
||||
<IconButton>
|
||||
<Download size={24} />
|
||||
</IconButton>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
case "Video":
|
||||
return (
|
||||
<div className={classNames(styles.actions, styles.videoAction)}>
|
||||
<Video size={24} className={styles.iconType} />
|
||||
<span className={styles.filename}>{filename}</span>
|
||||
<span className={styles.filesize}>
|
||||
{metadata.width + "x" + metadata.height} ({filesize})
|
||||
</span>
|
||||
<a
|
||||
href={download_url}
|
||||
className={styles.downloadIcon}
|
||||
download
|
||||
target="_blank">
|
||||
<IconButton>
|
||||
<Download size={24} />
|
||||
</IconButton>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div className={styles.actions}>
|
||||
<File size={24} className={styles.iconType} />
|
||||
<span className={styles.filename}>{filename}</span>
|
||||
<span className={styles.filesize}>{filesize}</span>
|
||||
<a
|
||||
href={download_url}
|
||||
className={styles.downloadIcon}
|
||||
download
|
||||
target="_blank">
|
||||
<IconButton>
|
||||
<Download size={24} />
|
||||
</IconButton>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
switch (metadata.type) {
|
||||
case "Image":
|
||||
return (
|
||||
<div className={classNames(styles.actions, styles.imageAction)}>
|
||||
<span className={styles.filename}>{filename}</span>
|
||||
<span className={styles.filesize}>
|
||||
{metadata.width + "x" + metadata.height} ({filesize})
|
||||
</span>
|
||||
<a
|
||||
href={open_url}
|
||||
target="_blank"
|
||||
className={styles.iconType}>
|
||||
<IconButton>
|
||||
<LinkExternal size={24} />
|
||||
</IconButton>
|
||||
</a>
|
||||
<a
|
||||
href={download_url}
|
||||
className={styles.downloadIcon}
|
||||
download
|
||||
target="_blank">
|
||||
<IconButton>
|
||||
<Download size={24} />
|
||||
</IconButton>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
case "Audio":
|
||||
return (
|
||||
<div className={classNames(styles.actions, styles.audioAction)}>
|
||||
<Headphone size={24} className={styles.iconType} />
|
||||
<span className={styles.filename}>{filename}</span>
|
||||
<span className={styles.filesize}>{filesize}</span>
|
||||
<a
|
||||
href={download_url}
|
||||
className={styles.downloadIcon}
|
||||
download
|
||||
target="_blank">
|
||||
<IconButton>
|
||||
<Download size={24} />
|
||||
</IconButton>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
case "Video":
|
||||
return (
|
||||
<div className={classNames(styles.actions, styles.videoAction)}>
|
||||
<Video size={24} className={styles.iconType} />
|
||||
<span className={styles.filename}>{filename}</span>
|
||||
<span className={styles.filesize}>
|
||||
{metadata.width + "x" + metadata.height} ({filesize})
|
||||
</span>
|
||||
<a
|
||||
href={download_url}
|
||||
className={styles.downloadIcon}
|
||||
download
|
||||
target="_blank">
|
||||
<IconButton>
|
||||
<Download size={24} />
|
||||
</IconButton>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div className={styles.actions}>
|
||||
<File size={24} className={styles.iconType} />
|
||||
<span className={styles.filename}>{filename}</span>
|
||||
<span className={styles.filesize}>{filesize}</span>
|
||||
<a
|
||||
href={download_url}
|
||||
className={styles.downloadIcon}
|
||||
download
|
||||
target="_blank">
|
||||
<IconButton>
|
||||
<Download size={24} />
|
||||
</IconButton>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,83 +11,83 @@ import Markdown from "../../../markdown/Markdown";
|
||||
import UserShort from "../../user/UserShort";
|
||||
|
||||
interface Props {
|
||||
channel: string;
|
||||
index: number;
|
||||
id: string;
|
||||
channel: string;
|
||||
index: number;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const ReplyBase = styled.div<{
|
||||
head?: boolean;
|
||||
fail?: boolean;
|
||||
preview?: boolean;
|
||||
head?: boolean;
|
||||
fail?: boolean;
|
||||
preview?: boolean;
|
||||
}>`
|
||||
gap: 4px;
|
||||
display: flex;
|
||||
font-size: 0.8em;
|
||||
margin-left: 30px;
|
||||
user-select: none;
|
||||
margin-bottom: 4px;
|
||||
align-items: center;
|
||||
color: var(--secondary-foreground);
|
||||
gap: 4px;
|
||||
display: flex;
|
||||
font-size: 0.8em;
|
||||
margin-left: 30px;
|
||||
user-select: none;
|
||||
margin-bottom: 4px;
|
||||
align-items: center;
|
||||
color: var(--secondary-foreground);
|
||||
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
svg:first-child {
|
||||
flex-shrink: 0;
|
||||
transform: scaleX(-1);
|
||||
color: var(--tertiary-foreground);
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
props.fail &&
|
||||
css`
|
||||
color: var(--tertiary-foreground);
|
||||
`}
|
||||
|
||||
${(props) =>
|
||||
props.head &&
|
||||
css`
|
||||
margin-top: 12px;
|
||||
`}
|
||||
svg:first-child {
|
||||
flex-shrink: 0;
|
||||
transform: scaleX(-1);
|
||||
color: var(--tertiary-foreground);
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
props.preview &&
|
||||
css`
|
||||
margin-left: 0;
|
||||
`}
|
||||
props.fail &&
|
||||
css`
|
||||
color: var(--tertiary-foreground);
|
||||
`}
|
||||
|
||||
${(props) =>
|
||||
props.head &&
|
||||
css`
|
||||
margin-top: 12px;
|
||||
`}
|
||||
|
||||
${(props) =>
|
||||
props.preview &&
|
||||
css`
|
||||
margin-left: 0;
|
||||
`}
|
||||
`;
|
||||
|
||||
export function MessageReply({ index, channel, id }: Props) {
|
||||
const view = useRenderState(channel);
|
||||
if (view?.type !== "RENDER") return null;
|
||||
const view = useRenderState(channel);
|
||||
if (view?.type !== "RENDER") return null;
|
||||
|
||||
const message = view.messages.find((x) => x._id === id);
|
||||
if (!message) {
|
||||
return (
|
||||
<ReplyBase head={index === 0} fail>
|
||||
<Reply size={16} />
|
||||
<span>
|
||||
<Text id="app.main.channel.misc.failed_load" />
|
||||
</span>
|
||||
</ReplyBase>
|
||||
);
|
||||
}
|
||||
const message = view.messages.find((x) => x._id === id);
|
||||
if (!message) {
|
||||
return (
|
||||
<ReplyBase head={index === 0} fail>
|
||||
<Reply size={16} />
|
||||
<span>
|
||||
<Text id="app.main.channel.misc.failed_load" />
|
||||
</span>
|
||||
</ReplyBase>
|
||||
);
|
||||
}
|
||||
|
||||
const user = useUser(message.author);
|
||||
const user = useUser(message.author);
|
||||
|
||||
return (
|
||||
<ReplyBase head={index === 0}>
|
||||
<Reply size={16} />
|
||||
<UserShort user={user} size={16} />
|
||||
{message.attachments && message.attachments.length > 0 && (
|
||||
<File size={16} />
|
||||
)}
|
||||
<Markdown
|
||||
disallowBigEmoji
|
||||
content={(message.content as string).replace(/\n/g, " ")}
|
||||
/>
|
||||
</ReplyBase>
|
||||
);
|
||||
return (
|
||||
<ReplyBase head={index === 0}>
|
||||
<Reply size={16} />
|
||||
<UserShort user={user} size={16} />
|
||||
{message.attachments && message.attachments.length > 0 && (
|
||||
<File size={16} />
|
||||
)}
|
||||
<Markdown
|
||||
disallowBigEmoji
|
||||
content={(message.content as string).replace(/\n/g, " ")}
|
||||
/>
|
||||
</ReplyBase>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,67 +6,67 @@ import { useContext, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import RequiresOnline from "../../../../context/revoltjs/RequiresOnline";
|
||||
import {
|
||||
AppContext,
|
||||
StatusContext,
|
||||
AppContext,
|
||||
StatusContext,
|
||||
} from "../../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import Preloader from "../../../ui/Preloader";
|
||||
|
||||
interface Props {
|
||||
attachment: Attachment;
|
||||
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 [content, setContent] = useState<undefined | string>(undefined);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const status = useContext(StatusContext);
|
||||
const client = useContext(AppContext);
|
||||
|
||||
const url = client.generateFileURL(attachment)!;
|
||||
const url = client.generateFileURL(attachment)!;
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof content !== "undefined") return;
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
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]);
|
||||
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 type="ring" />
|
||||
</RequiresOnline>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={styles.textContent}
|
||||
data-loading={typeof content === "undefined"}>
|
||||
{content ? (
|
||||
<pre>
|
||||
<code>{content}</code>
|
||||
</pre>
|
||||
) : (
|
||||
<RequiresOnline>
|
||||
<Preloader type="ring" />
|
||||
</RequiresOnline>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user