diff --git a/src/components/Chat/ChatImageMessage/ChatImageMessage.styled.ts b/src/components/Chat/ChatImageMessage/ChatImageMessage.styled.ts new file mode 100644 index 0000000..86e2376 --- /dev/null +++ b/src/components/Chat/ChatImageMessage/ChatImageMessage.styled.ts @@ -0,0 +1,42 @@ +import styled from 'styled-components'; + +type Orientation = 'landscape' | 'portrait' | 'square'; + +const IMAGE_CARD_SIZE: Record = { + landscape: { + width: '260px', + height: '174px', + }, + portrait: { + width: '260px', + height: '325px', + }, + square: { + width: '260px', + height: '260px', + }, +}; + +export const ImageLink = styled.a<{ + $isLoaded: boolean; + $orientation: Orientation; +}>` + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; + + width: ${(props) => IMAGE_CARD_SIZE[props.$orientation].width}; + height: ${(props) => IMAGE_CARD_SIZE[props.$orientation].height}; +`; + +export const PreviewImage = styled.img<{ $isLoaded: boolean }>` + display: block; + width: auto; + height: auto; + max-width: 100%; + max-height: 100%; + object-fit: contain; + opacity: ${(props) => (props.$isLoaded ? 1 : 0)}; +`; diff --git a/src/components/Chat/ChatImageMessage/ChatImageMessage.tsx b/src/components/Chat/ChatImageMessage/ChatImageMessage.tsx new file mode 100644 index 0000000..69655d9 --- /dev/null +++ b/src/components/Chat/ChatImageMessage/ChatImageMessage.tsx @@ -0,0 +1,53 @@ +import { useState } from 'react'; +import { openImagePreviewWindow } from '@utils/openImagePreviewWindow'; +import * as S from './ChatImageMessage.styled'; + +type Orientation = 'landscape' | 'portrait' | 'square'; + +interface ChatImageMessageProps { + src: string; + alt: string; +} + +const ChatImageMessage = ({ src, alt }: ChatImageMessageProps) => { + const [isLoaded, setIsLoaded] = useState(false); + const [orientation, setOrientation] = useState('portrait'); + + const handleImageLoad = (event: React.SyntheticEvent) => { + const { naturalWidth, naturalHeight } = event.currentTarget; + + if (naturalWidth > naturalHeight) setOrientation('landscape'); + else if (naturalWidth < naturalHeight) setOrientation('portrait'); + else setOrientation('square'); + + setIsLoaded(true); + }; + + const handleImageClick = (event: React.MouseEvent) => { + if (event.ctrlKey || event.metaKey || event.shiftKey) return; + + event.preventDefault(); + openImagePreviewWindow({ src, alt }); + }; + + return ( + + + + ); +}; + +export default ChatImageMessage; diff --git a/src/components/Chat/MyMessageBox/MyMessageBox.styled.ts b/src/components/Chat/MyMessageBox/MyMessageBox.styled.ts index 7a19bc5..b7d75df 100644 --- a/src/components/Chat/MyMessageBox/MyMessageBox.styled.ts +++ b/src/components/Chat/MyMessageBox/MyMessageBox.styled.ts @@ -17,8 +17,7 @@ export const MyMessageBoxWrapper = styled.div<{ state: boolean; type: string }>` props.type === 'TEXT' ? `${theme.units.spacing.space10} ${theme.units.spacing.space14}` : `${theme.units.spacing.space10} 0 0 0`}; - background-color: ${(props) => - props.type === 'TEXT' ? theme.color.bg.iPrimary : 'none'}; + background-color: ${(props) => (props.type === 'TEXT' ? theme.color.bg.iPrimary : 'none')}; border-radius: ${(props) => props.state ? `${theme.units.radius.radius20} 0 ${theme.units.radius.radius20} ${theme.units.radius.radius20}` @@ -47,11 +46,6 @@ export const TimeWrapper = styled.div` export const ImageWrapper = styled.div` display: flex; + flex-wrap: wrap; gap: ${theme.units.spacing.space10}; - - img { - width: 100%; - height: 100%; - object-fit: cover; - } `; diff --git a/src/components/Chat/MyMessageBox/MyMessageBox.tsx b/src/components/Chat/MyMessageBox/MyMessageBox.tsx index ce6b807..1bb3427 100644 --- a/src/components/Chat/MyMessageBox/MyMessageBox.tsx +++ b/src/components/Chat/MyMessageBox/MyMessageBox.tsx @@ -1,7 +1,8 @@ +import ChatAttachments from '@components/Chat/ChatAttachment/ChatAttachments'; +import ChatImageMessage from '@components/Chat/ChatImageMessage/ChatImageMessage'; import Flex from '@components/common/Flex/Flex'; import Text from '@components/common/Text/Text'; import type { Attachment } from '@type/chat'; -import ChatAttachments from '../ChatAttachment/ChatAttachments'; import * as S from './MyMessageBox.styled'; export interface MyMessageBoxProps { @@ -33,15 +34,7 @@ const MyMessageBox = (props: MyMessageBoxProps) => { )} {type === 'IMAGE' && ( - {file?.map((img) => ( - - {img.filename} - - ))} + {file?.map((img) => )} )} {type === 'FILE' && ( diff --git a/src/components/Chat/OtherMessageBox/OtherMessageBox.styled.ts b/src/components/Chat/OtherMessageBox/OtherMessageBox.styled.ts index 48c01b4..fdbbaed 100644 --- a/src/components/Chat/OtherMessageBox/OtherMessageBox.styled.ts +++ b/src/components/Chat/OtherMessageBox/OtherMessageBox.styled.ts @@ -7,9 +7,7 @@ export const OtherMessageBoxContainer = styled.div<{ display: flex; width: 100%; padding-top: ${(props) => - props.state - ? `${theme.units.spacing.space16}` - : `${theme.units.spacing.space6}`}; + props.state ? `${theme.units.spacing.space16}` : `${theme.units.spacing.space6}`}; gap: ${theme.units.spacing.space8}; `; @@ -25,8 +23,7 @@ export const OtherMessageBoxWrapper = styled.div<{ props.type === 'TEXT' ? `${theme.units.spacing.space10} ${theme.units.spacing.space14}` : `${theme.units.spacing.space10} 0 0 0`}; - background-color: ${(props) => - props.type === 'TEXT' ? theme.color.bg.secondary : 'none'}; + background-color: ${(props) => (props.type === 'TEXT' ? theme.color.bg.secondary : 'none')}; border-radius: ${(props) => props.state ? `0 ${theme.units.radius.radius20} ${theme.units.radius.radius20} ${theme.units.radius.radius20}` @@ -62,11 +59,6 @@ export const AvatarSpacer = styled.div` export const ImageWrapper = styled.div` display: flex; + flex-wrap: wrap; gap: ${theme.units.spacing.space10}; - - img { - width: 100%; - height: 100%; - object-fit: cover; - } `; diff --git a/src/components/Chat/OtherMessageBox/OtherMessageBox.tsx b/src/components/Chat/OtherMessageBox/OtherMessageBox.tsx index 5092b58..128f2fa 100644 --- a/src/components/Chat/OtherMessageBox/OtherMessageBox.tsx +++ b/src/components/Chat/OtherMessageBox/OtherMessageBox.tsx @@ -1,8 +1,9 @@ +import ChatAttachments from '@components/Chat/ChatAttachment/ChatAttachments'; +import ChatImageMessage from '@components/Chat/ChatImageMessage/ChatImageMessage'; import Avatar from '@components/common/Avatar/Avatar'; import Flex from '@components/common/Flex/Flex'; import Text from '@components/common/Text/Text'; import type { Attachment } from '@type/chat'; -import ChatAttachments from '../ChatAttachment/ChatAttachments'; import * as S from './OtherMessageBox.styled'; export interface OtherMessageBoxProps { @@ -43,13 +44,7 @@ const OtherMessageBox = (props: OtherMessageBoxProps) => { {type === 'IMAGE' && ( {file?.map((img) => ( - - {img.filename} - + ))} )} diff --git a/src/hooks/chatting/useChatMessages.ts b/src/hooks/chatting/useChatMessages.ts index d1faccd..b14fb2a 100644 --- a/src/hooks/chatting/useChatMessages.ts +++ b/src/hooks/chatting/useChatMessages.ts @@ -28,10 +28,10 @@ const useChatMessages = (props: useChatMessagesProps) => { const latestMessages = messagePages[0]?.chatChannelMessages ?? []; const latestMessageId = latestMessages[0]?.id; - if (!latestMessageId || !userStatus) return; + if (!latestMessageId || !userStatus || !stompClient) return; if (lastReadMessageIdRef.current === latestMessageId) return; - stompClient?.send( + stompClient.send( END_POINTS.READ_MESSAGE(userStatus.profile.lastSeenTeamspaceId, selectedChat, latestMessageId) ); lastReadMessageIdRef.current = latestMessageId; diff --git a/src/utils/openImagePreviewWindow.ts b/src/utils/openImagePreviewWindow.ts new file mode 100644 index 0000000..b6c3031 --- /dev/null +++ b/src/utils/openImagePreviewWindow.ts @@ -0,0 +1,29 @@ +interface OpenImagePreviewWindowParams { + src: string; + alt: string; +} + +export const openImagePreviewWindow = ({ src, alt }: OpenImagePreviewWindowParams) => { + const imageWindow = window.open('', '_blank'); + + if (!imageWindow) return; + + imageWindow.opener = null; + imageWindow.document.title = alt; + imageWindow.document.body.style.margin = '0'; + imageWindow.document.body.style.backgroundColor = '#0f1115'; + imageWindow.document.body.style.display = 'flex'; + imageWindow.document.body.style.alignItems = 'center'; + imageWindow.document.body.style.justifyContent = 'center'; + imageWindow.document.body.style.minHeight = '100vh'; + + const previewImage = imageWindow.document.createElement('img'); + + previewImage.src = src; + previewImage.alt = alt; + previewImage.style.maxWidth = '100vw'; + previewImage.style.maxHeight = '100vh'; + previewImage.style.objectFit = 'contain'; + + imageWindow.document.body.replaceChildren(previewImage); +};