feat: port ImageViewer

This commit is contained in:
Paul Makles 2022-06-30 19:34:04 +01:00
parent 1664aaee15
commit 0d3f29515e
11 changed files with 58 additions and 116 deletions

View file

@ -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)}

View file

@ -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)}
/> />
); );

View file

@ -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!,
}) })
} }

View file

@ -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} />;

View file

@ -1,16 +0,0 @@
.info {
.header {
display: flex;
align-items: center;
flex-direction: row;
h1 {
margin: 0;
flex-grow: 1;
}
div {
cursor: pointer;
}
}
}

View file

@ -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>
);
});

View file

@ -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;
}
}

View file

@ -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,
}) })
} }

View file

@ -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,

View file

@ -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;
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;
} }
`;
type ImageMetadata = API.Metadata & { type: "Image" }; 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>
); );
} }

View file

@ -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 } & {