mirror of
https://github.com/revoltchat/revite.git
synced 2024-11-22 07:00:58 -05:00
feat: port ImageViewer
This commit is contained in:
parent
1664aaee15
commit
0d3f29515e
11 changed files with 58 additions and 116 deletions
|
@ -2,11 +2,10 @@ import { API } from "revolt.js";
|
||||||
|
|
||||||
import styles from "./Attachment.module.scss";
|
import styles from "./Attachment.module.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useContext, useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
|
||||||
|
|
||||||
import { useClient } from "../../../../controllers/client/ClientController";
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
|
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
enum ImageLoadingState {
|
enum ImageLoadingState {
|
||||||
Loading,
|
Loading,
|
||||||
|
@ -21,7 +20,6 @@ type Props = JSX.HTMLAttributes<HTMLImageElement> & {
|
||||||
export default function ImageFile({ attachment, ...props }: Props) {
|
export default function ImageFile({ attachment, ...props }: Props) {
|
||||||
const [loading, setLoading] = useState(ImageLoadingState.Loading);
|
const [loading, setLoading] = useState(ImageLoadingState.Loading);
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
const url = client.generateFileURL(attachment)!;
|
const url = client.generateFileURL(attachment)!;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -33,7 +31,9 @@ export default function ImageFile({ attachment, ...props }: Props) {
|
||||||
className={classNames(styles.image, {
|
className={classNames(styles.image, {
|
||||||
[styles.loading]: loading !== ImageLoadingState.Loaded,
|
[styles.loading]: loading !== ImageLoadingState.Loaded,
|
||||||
})}
|
})}
|
||||||
onClick={() => openScreen({ id: "image_viewer", attachment })}
|
onClick={() =>
|
||||||
|
modalController.push({ type: "image_viewer", attachment })
|
||||||
|
}
|
||||||
onMouseDown={(ev) => ev.button === 1 && window.open(url, "_blank")}
|
onMouseDown={(ev) => ev.button === 1 && window.open(url, "_blank")}
|
||||||
onLoad={() => setLoading(ImageLoadingState.Loaded)}
|
onLoad={() => setLoading(ImageLoadingState.Loaded)}
|
||||||
onError={() => setLoading(ImageLoadingState.Error)}
|
onError={() => setLoading(ImageLoadingState.Error)}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { useContext } from "preact/hooks";
|
||||||
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
||||||
|
|
||||||
import { useClient } from "../../../../controllers/client/ClientController";
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
|
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||||
import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/MessageArea";
|
import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/MessageArea";
|
||||||
import Markdown from "../../../markdown/Markdown";
|
import Markdown from "../../../markdown/Markdown";
|
||||||
import Attachment from "../attachments/Attachment";
|
import Attachment from "../attachments/Attachment";
|
||||||
|
@ -24,7 +25,7 @@ const MAX_PREVIEW_SIZE = 150;
|
||||||
export default function Embed({ embed }: Props) {
|
export default function Embed({ embed }: Props) {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|
||||||
const { openScreen, openLink } = useIntermediate();
|
const { openLink } = useIntermediate();
|
||||||
const maxWidth = Math.min(
|
const maxWidth = Math.min(
|
||||||
useContext(MessageAreaWidthContext) - CONTAINER_PADDING,
|
useContext(MessageAreaWidthContext) - CONTAINER_PADDING,
|
||||||
MAX_EMBED_WIDTH,
|
MAX_EMBED_WIDTH,
|
||||||
|
@ -191,7 +192,9 @@ export default function Embed({ embed }: Props) {
|
||||||
type="text/html"
|
type="text/html"
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
onClick={() => openScreen({ id: "image_viewer", embed })}
|
onClick={() =>
|
||||||
|
modalController.push({ type: "image_viewer", embed })
|
||||||
|
}
|
||||||
onMouseDown={(ev) => ev.button === 1 && openLink(embed.url)}
|
onMouseDown={(ev) => ev.button === 1 && openLink(embed.url)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,9 +3,8 @@ import { API } from "revolt.js";
|
||||||
|
|
||||||
import styles from "./Embed.module.scss";
|
import styles from "./Embed.module.scss";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
|
||||||
|
|
||||||
import { useClient } from "../../../../controllers/client/ClientController";
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
|
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
embed: API.Embed;
|
embed: API.Embed;
|
||||||
|
@ -15,7 +14,6 @@ interface Props {
|
||||||
|
|
||||||
export default function EmbedMedia({ embed, width, height }: Props) {
|
export default function EmbedMedia({ embed, width, height }: Props) {
|
||||||
if (embed.type !== "Website") return null;
|
if (embed.type !== "Website") return null;
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|
||||||
switch (embed.special?.type) {
|
switch (embed.special?.type) {
|
||||||
|
@ -118,8 +116,8 @@ export default function EmbedMedia({ embed, width, height }: Props) {
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
style={{ width, height }}
|
style={{ width, height }}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openScreen({
|
modalController.push({
|
||||||
id: "image_viewer",
|
type: "image_viewer",
|
||||||
embed: embed.image!,
|
embed: embed.image!,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { IntermediateContext, useIntermediate } from "./Intermediate";
|
||||||
import { SpecialInputModal } from "./modals/Input";
|
import { SpecialInputModal } from "./modals/Input";
|
||||||
import { SpecialPromptModal } from "./modals/Prompt";
|
import { SpecialPromptModal } from "./modals/Prompt";
|
||||||
import { CreateBotModal } from "./popovers/CreateBot";
|
import { CreateBotModal } from "./popovers/CreateBot";
|
||||||
import { ImageViewer } from "./popovers/ImageViewer";
|
|
||||||
import { UserPicker } from "./popovers/UserPicker";
|
import { UserPicker } from "./popovers/UserPicker";
|
||||||
import { UserProfile } from "./popovers/UserProfile";
|
import { UserProfile } from "./popovers/UserProfile";
|
||||||
|
|
||||||
|
@ -24,8 +23,6 @@ export default function Popovers() {
|
||||||
case "user_picker":
|
case "user_picker":
|
||||||
// @ts-expect-error someone figure this out :)
|
// @ts-expect-error someone figure this out :)
|
||||||
return <UserPicker {...screen} onClose={onClose} />;
|
return <UserPicker {...screen} onClose={onClose} />;
|
||||||
case "image_viewer":
|
|
||||||
return <ImageViewer {...screen} onClose={onClose} />;
|
|
||||||
case "create_bot":
|
case "create_bot":
|
||||||
// @ts-expect-error someone figure this out :)
|
// @ts-expect-error someone figure this out :)
|
||||||
return <CreateBotModal onClose={onClose} {...screen} />;
|
return <CreateBotModal onClose={onClose} {...screen} />;
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
.info {
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { X } from "@styled-icons/boxicons-regular";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { Channel } from "revolt.js";
|
|
||||||
|
|
||||||
import styles from "./ChannelInfo.module.scss";
|
|
||||||
|
|
||||||
import { Modal } from "@revoltchat/ui";
|
|
||||||
|
|
||||||
import Markdown from "../../../components/markdown/Markdown";
|
|
||||||
import { getChannelName } from "../../revoltjs/util";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
channel: Channel;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChannelInfo = observer(({ channel, onClose }: Props) => {
|
|
||||||
if (
|
|
||||||
channel.channel_type === "DirectMessage" ||
|
|
||||||
channel.channel_type === "SavedMessages"
|
|
||||||
) {
|
|
||||||
onClose();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal onClose={onClose}>
|
|
||||||
<div className={styles.info}>
|
|
||||||
<div className={styles.header}>
|
|
||||||
<h1>{getChannelName(channel, true)}</h1>
|
|
||||||
<div onClick={onClose}>
|
|
||||||
<X size={36} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<Markdown content={channel.description!} />
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
});
|
|
|
@ -1,20 +0,0 @@
|
||||||
.viewer {
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-direction: column;
|
|
||||||
border-end-end-radius: 4px;
|
|
||||||
border-end-start-radius: 4px;
|
|
||||||
|
|
||||||
max-width: 100vw;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
max-width: 90vw;
|
|
||||||
max-height: 75vh;
|
|
||||||
object-fit: contain;
|
|
||||||
border-bottom: thin solid var(--tertiary-foreground);
|
|
||||||
|
|
||||||
-webkit-touch-callout: default;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -13,7 +13,7 @@ import { UserPermission, API } from "revolt.js";
|
||||||
|
|
||||||
import styles from "./UserProfile.module.scss";
|
import styles from "./UserProfile.module.scss";
|
||||||
import { Localizer, Text } from "preact-i18n";
|
import { Localizer, Text } from "preact-i18n";
|
||||||
import { useContext, useEffect, useLayoutEffect, useState } from "preact/hooks";
|
import { useEffect, useLayoutEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
@ -35,6 +35,7 @@ import { Username } from "../../../components/common/user/UserShort";
|
||||||
import UserStatus from "../../../components/common/user/UserStatus";
|
import UserStatus from "../../../components/common/user/UserStatus";
|
||||||
import Markdown from "../../../components/markdown/Markdown";
|
import Markdown from "../../../components/markdown/Markdown";
|
||||||
import { useSession } from "../../../controllers/client/ClientController";
|
import { useSession } from "../../../controllers/client/ClientController";
|
||||||
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
import { useIntermediate } from "../Intermediate";
|
import { useIntermediate } from "../Intermediate";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -159,8 +160,8 @@ export const UserProfile = observer(
|
||||||
hover={typeof user.avatar !== "undefined"}
|
hover={typeof user.avatar !== "undefined"}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
user.avatar &&
|
user.avatar &&
|
||||||
openScreen({
|
modalController.push({
|
||||||
id: "image_viewer",
|
type: "image_viewer",
|
||||||
attachment: user.avatar,
|
attachment: user.avatar,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import Changelog from "./components/Changelog";
|
||||||
import ChannelInfo from "./components/ChannelInfo";
|
import ChannelInfo from "./components/ChannelInfo";
|
||||||
import Clipboard from "./components/Clipboard";
|
import Clipboard from "./components/Clipboard";
|
||||||
import Error from "./components/Error";
|
import Error from "./components/Error";
|
||||||
|
import ImageViewer from "./components/ImageViewer";
|
||||||
import LinkWarning from "./components/LinkWarning";
|
import LinkWarning from "./components/LinkWarning";
|
||||||
import MFAEnableTOTP from "./components/MFAEnableTOTP";
|
import MFAEnableTOTP from "./components/MFAEnableTOTP";
|
||||||
import MFAFlow from "./components/MFAFlow";
|
import MFAFlow from "./components/MFAFlow";
|
||||||
|
@ -222,6 +223,7 @@ export const modalController = new ModalControllerExtended({
|
||||||
channel_info: ChannelInfo,
|
channel_info: ChannelInfo,
|
||||||
clipboard: Clipboard,
|
clipboard: Clipboard,
|
||||||
error: Error,
|
error: Error,
|
||||||
|
image_viewer: ImageViewer,
|
||||||
link_warning: LinkWarning,
|
link_warning: LinkWarning,
|
||||||
mfa_flow: MFAFlow,
|
mfa_flow: MFAFlow,
|
||||||
mfa_recovery: MFARecovery,
|
mfa_recovery: MFARecovery,
|
||||||
|
|
|
@ -1,23 +1,40 @@
|
||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
import styled from "styled-components";
|
||||||
import { API } from "revolt.js";
|
|
||||||
|
|
||||||
import styles from "./ImageViewer.module.scss";
|
|
||||||
|
|
||||||
import { Modal } from "@revoltchat/ui";
|
import { Modal } from "@revoltchat/ui";
|
||||||
|
|
||||||
import AttachmentActions from "../../../components/common/messaging/attachments/AttachmentActions";
|
import AttachmentActions from "../../../components/common/messaging/attachments/AttachmentActions";
|
||||||
import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions";
|
import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions";
|
||||||
import { useClient } from "../../../controllers/client/ClientController";
|
import { useClient } from "../../client/ClientController";
|
||||||
|
import { ModalProps } from "../types";
|
||||||
|
|
||||||
interface Props {
|
const Viewer = styled.div`
|
||||||
onClose: () => void;
|
display: flex;
|
||||||
embed?: API.Image;
|
overflow: hidden;
|
||||||
attachment?: API.File;
|
flex-direction: column;
|
||||||
}
|
border-end-end-radius: 4px;
|
||||||
|
border-end-start-radius: 4px;
|
||||||
|
|
||||||
type ImageMetadata = API.Metadata & { type: "Image" };
|
max-width: 100vw;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 75vh;
|
||||||
|
object-fit: contain;
|
||||||
|
border-bottom: thin solid var(--tertiary-foreground);
|
||||||
|
|
||||||
|
-webkit-touch-callout: default;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function ImageViewer({
|
||||||
|
embed,
|
||||||
|
attachment,
|
||||||
|
...props
|
||||||
|
}: ModalProps<"image_viewer">) {
|
||||||
|
const client = useClient();
|
||||||
|
|
||||||
export function ImageViewer({ attachment, embed, onClose }: Props) {
|
|
||||||
if (attachment && attachment.metadata.type !== "Image") {
|
if (attachment && attachment.metadata.type !== "Image") {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Attempted to use a non valid attatchment type in the image viewer: ${attachment.metadata.type}`,
|
`Attempted to use a non valid attatchment type in the image viewer: ${attachment.metadata.type}`,
|
||||||
|
@ -25,20 +42,16 @@ export function ImageViewer({ attachment, embed, onClose }: Props) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = useClient();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal onClose={onClose} transparent maxHeight="100vh" maxWidth="100vw">
|
<Modal {...props} transparent maxHeight="100vh" maxWidth="100vw">
|
||||||
<div className={styles.viewer}>
|
<Viewer>
|
||||||
{attachment && (
|
{attachment && (
|
||||||
<>
|
<>
|
||||||
<img
|
<img
|
||||||
loading="eager"
|
loading="eager"
|
||||||
src={client.generateFileURL(attachment)}
|
src={client.generateFileURL(attachment)}
|
||||||
width={(attachment.metadata as ImageMetadata).width}
|
width={(attachment.metadata as any).width}
|
||||||
height={
|
height={(attachment.metadata as any).height}
|
||||||
(attachment.metadata as ImageMetadata).height
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<AttachmentActions attachment={attachment} />
|
<AttachmentActions attachment={attachment} />
|
||||||
</>
|
</>
|
||||||
|
@ -54,7 +67,7 @@ export function ImageViewer({ attachment, embed, onClose }: Props) {
|
||||||
<EmbedMediaActions embed={embed} />
|
<EmbedMediaActions embed={embed} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Viewer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -80,6 +80,11 @@ export type Modal = {
|
||||||
type: "server_info";
|
type: "server_info";
|
||||||
server: Server;
|
server: Server;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: "image_viewer";
|
||||||
|
embed?: API.Image;
|
||||||
|
attachment?: API.File;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ModalProps<T extends Modal["type"]> = Modal & { type: T } & {
|
export type ModalProps<T extends Modal["type"]> = Modal & { type: T } & {
|
||||||
|
|
Loading…
Reference in a new issue