mirror of
https://github.com/spacebarchat/client.git
synced 2024-11-25 19:52:31 +01:00
attachment previews
This commit is contained in:
parent
f946108b21
commit
35d9bbfa6e
@ -1,3 +1,4 @@
|
|||||||
|
import { useModals } from "@mattjennings/react-modal-stack";
|
||||||
import { MessageType } from "@spacebarchat/spacebar-api-types/v9";
|
import { MessageType } from "@spacebarchat/spacebar-api-types/v9";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Moment from "react-moment";
|
import Moment from "react-moment";
|
||||||
@ -6,15 +7,13 @@ import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
|||||||
import useLogger from "../../hooks/useLogger";
|
import useLogger from "../../hooks/useLogger";
|
||||||
import { QueuedMessage } from "../../stores/MessageQueue";
|
import { QueuedMessage } from "../../stores/MessageQueue";
|
||||||
import { default as MessageObject } from "../../stores/objects/Message";
|
import { default as MessageObject } from "../../stores/objects/Message";
|
||||||
|
import { calculateImageRatio, calculateScaledDimensions } from "../../utils/Message";
|
||||||
import { calendarStrings } from "../../utils/i18n";
|
import { calendarStrings } from "../../utils/i18n";
|
||||||
import Avatar from "../Avatar";
|
import Avatar from "../Avatar";
|
||||||
import { Link } from "../Link";
|
import { Link } from "../Link";
|
||||||
|
import AttachmentPreviewModal from "../modals/AttachmentPreviewModal";
|
||||||
import { IContextMenuItem } from "./../ContextMenuItem";
|
import { IContextMenuItem } from "./../ContextMenuItem";
|
||||||
|
|
||||||
// max width/height for images
|
|
||||||
const maxWidth = 400;
|
|
||||||
const maxHeight = 300;
|
|
||||||
|
|
||||||
type MessageLike = MessageObject | QueuedMessage;
|
type MessageLike = MessageObject | QueuedMessage;
|
||||||
|
|
||||||
const MessageListItem = styled.li`
|
const MessageListItem = styled.li`
|
||||||
@ -68,43 +67,6 @@ const MessageAttachment = styled.div`
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function calculateImageRatio(width: number, height: number) {
|
|
||||||
let o = 1;
|
|
||||||
width > maxWidth && (o = maxWidth / width);
|
|
||||||
width = Math.round(width * o);
|
|
||||||
let a = 1;
|
|
||||||
(height = Math.round(height * o)) > maxHeight && (a = maxHeight / height);
|
|
||||||
return Math.min(o * a, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateScaledDimensions(
|
|
||||||
originalWidth: number,
|
|
||||||
originalHeight: number,
|
|
||||||
ratio: number,
|
|
||||||
): { scaledWidth: number; scaledHeight: number } {
|
|
||||||
const deviceResolution = window.devicePixelRatio ?? 1;
|
|
||||||
let scaledWidth = originalWidth;
|
|
||||||
let scaledHeight = originalHeight;
|
|
||||||
|
|
||||||
if (ratio < 1) {
|
|
||||||
scaledWidth = Math.round(originalWidth * ratio);
|
|
||||||
scaledHeight = Math.round(originalHeight * ratio);
|
|
||||||
}
|
|
||||||
|
|
||||||
scaledWidth = Math.min(scaledWidth, maxWidth);
|
|
||||||
scaledHeight = Math.min(scaledHeight, maxHeight);
|
|
||||||
|
|
||||||
if (scaledWidth !== originalWidth || scaledHeight !== originalHeight) {
|
|
||||||
scaledWidth |= 0;
|
|
||||||
scaledHeight |= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
scaledWidth *= deviceResolution;
|
|
||||||
scaledHeight *= deviceResolution;
|
|
||||||
|
|
||||||
return { scaledWidth, scaledHeight };
|
|
||||||
}
|
|
||||||
|
|
||||||
// converts URLs in a string to html links
|
// converts URLs in a string to html links
|
||||||
const Linkify = ({ children }: { children: string }) => {
|
const Linkify = ({ children }: { children: string }) => {
|
||||||
const urlPattern = /\bhttps?:\/\/\S+\b\/?/g;
|
const urlPattern = /\bhttps?:\/\/\S+\b\/?/g;
|
||||||
@ -146,6 +108,7 @@ interface Props {
|
|||||||
*/
|
*/
|
||||||
function Message({ message, isHeader, isSending, isFailed }: Props) {
|
function Message({ message, isHeader, isSending, isFailed }: Props) {
|
||||||
const logger = useLogger("Message.tsx");
|
const logger = useLogger("Message.tsx");
|
||||||
|
const { openModal } = useModals();
|
||||||
const contextMenu = React.useContext(ContextMenuContext);
|
const contextMenu = React.useContext(ContextMenuContext);
|
||||||
const [contextMenuItems, setContextMenuItems] = React.useState<IContextMenuItem[]>([
|
const [contextMenuItems, setContextMenuItems] = React.useState<IContextMenuItem[]>([
|
||||||
{
|
{
|
||||||
@ -285,6 +248,9 @@ function Message({ message, isHeader, isSending, isFailed }: Props) {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
openModal(AttachmentPreviewModal, { attachment });
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{a}
|
{a}
|
||||||
</MessageAttachment>
|
</MessageAttachment>
|
||||||
|
37
src/components/modals/AttachmentPreviewModal.tsx
Normal file
37
src/components/modals/AttachmentPreviewModal.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { APIAttachment } from "@spacebarchat/spacebar-api-types/v9";
|
||||||
|
import { calculateImageRatio, calculateScaledDimensions } from "../../utils/Message";
|
||||||
|
import { Modal } from "./ModalComponents";
|
||||||
|
import { AnimatedModalProps } from "./ModalRenderer";
|
||||||
|
|
||||||
|
const SCALE_FACTOR = 3.5;
|
||||||
|
|
||||||
|
interface Props extends AnimatedModalProps {
|
||||||
|
attachment: APIAttachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AttachmentPreviewModal(props: Props) {
|
||||||
|
const width = props.attachment.width ?? 0;
|
||||||
|
const height = props.attachment.height ?? 0;
|
||||||
|
const maxWidth = 400 * SCALE_FACTOR;
|
||||||
|
const maxHeight = 300 * SCALE_FACTOR;
|
||||||
|
|
||||||
|
const ratio = calculateImageRatio(width, height, maxWidth, maxHeight);
|
||||||
|
const { scaledWidth, scaledHeight } = calculateScaledDimensions(width, height, ratio, maxWidth, maxHeight);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
full
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<img src={props.attachment.url} width={scaledWidth} height={scaledHeight} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AttachmentPreviewModal;
|
@ -1,4 +1,4 @@
|
|||||||
import { type StackedModalProps } from "@mattjennings/react-modal-stack";
|
import { useModals, type StackedModalProps } from "@mattjennings/react-modal-stack";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
@ -16,16 +16,6 @@ export const ModalBase = styled(motion.div)`
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
// &::before {
|
|
||||||
// content: "";
|
|
||||||
// position: absolute;
|
|
||||||
// top: 0;
|
|
||||||
// left: 0;
|
|
||||||
// right: 0;
|
|
||||||
// bottom: 0;
|
|
||||||
// background-color: black;
|
|
||||||
// opacity: 0.85;
|
|
||||||
// }
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -182,9 +172,13 @@ export const ModalFullContent = styled.div`
|
|||||||
interface ModalProps extends StackedModalProps {
|
interface ModalProps extends StackedModalProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
full?: boolean;
|
full?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Modal(props: ModalProps) {
|
export function Modal(props: ModalProps) {
|
||||||
|
const { closeModal } = useModals();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{props.open && (
|
{props.open && (
|
||||||
@ -202,9 +196,14 @@ export function Modal(props: ModalProps) {
|
|||||||
initial="hide"
|
initial="hide"
|
||||||
animate="show"
|
animate="show"
|
||||||
exit="hide"
|
exit="hide"
|
||||||
|
onClick={() => {
|
||||||
|
closeModal();
|
||||||
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ModalWrapper full={props.full}>{props.children}</ModalWrapper>
|
<ModalWrapper full={props.full} style={props.style}>
|
||||||
|
{props.children}
|
||||||
|
</ModalWrapper>
|
||||||
</ModalBase>
|
</ModalBase>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
44
src/utils/Message.ts
Normal file
44
src/utils/Message.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
export function calculateImageRatio(width: number, height: number, maxWidth?: number, maxHeight?: number) {
|
||||||
|
const mw = maxWidth ?? 400;
|
||||||
|
const mh = maxHeight ?? 300;
|
||||||
|
|
||||||
|
let o = 1;
|
||||||
|
width > mw && (o = mw / width);
|
||||||
|
width = Math.round(width * o);
|
||||||
|
let a = 1;
|
||||||
|
(height = Math.round(height * o)) > mh && (a = mh / height);
|
||||||
|
return Math.min(o * a, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateScaledDimensions(
|
||||||
|
originalWidth: number,
|
||||||
|
originalHeight: number,
|
||||||
|
ratio: number,
|
||||||
|
maxWidth?: number,
|
||||||
|
maxHeight?: number,
|
||||||
|
): { scaledWidth: number; scaledHeight: number } {
|
||||||
|
const mw = maxWidth ?? 400;
|
||||||
|
const mh = maxHeight ?? 300;
|
||||||
|
|
||||||
|
const deviceResolution = window.devicePixelRatio ?? 1;
|
||||||
|
let scaledWidth = originalWidth;
|
||||||
|
let scaledHeight = originalHeight;
|
||||||
|
|
||||||
|
if (ratio < 1) {
|
||||||
|
scaledWidth = Math.round(originalWidth * ratio);
|
||||||
|
scaledHeight = Math.round(originalHeight * ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
scaledWidth = Math.min(scaledWidth, mw);
|
||||||
|
scaledHeight = Math.min(scaledHeight, mh);
|
||||||
|
|
||||||
|
if (scaledWidth !== originalWidth || scaledHeight !== originalHeight) {
|
||||||
|
scaledWidth |= 0;
|
||||||
|
scaledHeight |= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
scaledWidth *= deviceResolution;
|
||||||
|
scaledHeight *= deviceResolution;
|
||||||
|
|
||||||
|
return { scaledWidth, scaledHeight };
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user