Skip to content

Commit

Permalink
Merge branch 'feat/chat-socket-file'
Browse files Browse the repository at this point in the history
  • Loading branch information
bluejoyq committed Nov 7, 2023
2 parents 02e6856 + d45e7cf commit 22759fa
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 35 deletions.
13 changes: 11 additions & 2 deletions src/data/backend/socket.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { SendMessageDto } from "@/domain/dtos/socket";
import { SendFileDto, SendMessageDto } from "@/domain/dtos/socket";
import { AppMessage } from "@/domain/models/appMessage";
import { io, Socket } from "socket.io-client";
export class SocketRepository {
socket: Socket;
constructor(roomId: number, onMessage: (message: SendMessageDto) => void) {
constructor(roomId: number, onMessage: (message: AppMessage) => void) {
this.socket = io(import.meta.env.VITE_API_BASE_URL);

this.socket.emit("join", `${roomId}`);
Expand All @@ -12,6 +13,14 @@ export class SocketRepository {
this.socket.emit("sendChat", dto);
}

async sendFile(dto: SendFileDto) {
this.socket.emit("sendFile", dto);
}

async sendAccountRequest(dto: SendMessageDto) {
this.socket.emit("sendAccount", dto);
}

async disconnect() {
this.socket.disconnect();
this.socket.removeAllListeners();
Expand Down
12 changes: 3 additions & 9 deletions src/data/hooks/chat.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { accidentObserverRepository, chatRepository } from "../backend";
import { GetMessageData, PostRoomDto } from "@/domain/dtos/chat";
import { SendMessageDto } from "@/domain/dtos/socket";
import { useCallback } from "react";
import { AppMessage } from "@/domain/models/appMessage";

export const useReadRoomsQuery = () => {
return useQuery({
Expand Down Expand Up @@ -56,7 +56,7 @@ export const useReadMessageQuery = (roomId: number) => {
export const useReadMessageQueryUpdater = () => {
const queryClient = useQueryClient();
return useCallback(
(roomId: number, dto: SendMessageDto) => {
(roomId: number, dto: AppMessage) => {
queryClient.setQueryData(
["message", roomId],
(oldData: GetMessageData | undefined) => {
Expand All @@ -65,13 +65,7 @@ export const useReadMessageQueryUpdater = () => {
}
return {
...oldData,
chatList: [
{
...dto,
createdAt: new Date().toISOString(),
},
...oldData.chatList,
],
chatList: [dto, ...oldData.chatList],
};
}
);
Expand Down
15 changes: 15 additions & 0 deletions src/domain/dtos/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,18 @@ export interface SendMessageDto {
sendUserUid: string;
message: string;
}

export interface FileObjectType {
fileData: ArrayBuffer;
filename: string;
}
export interface SendFileDto {
roomId: number;
sendUserUid: string;
file: FileObjectType;
}

export interface SendAccountRequest {
roomId: number;
sendUserUid: string;
}
3 changes: 3 additions & 0 deletions src/domain/models/appMessage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export type MessageType = "message" | "file" | "account";
export interface AppMessage {
roomId: number;
sendUserUid: string;
messageType: MessageType;
message: string;
createdAt: string;
}
78 changes: 69 additions & 9 deletions src/presentation/chat/components/ChatBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Button, css } from "@mui/material";
import { Box, css } from "@mui/material";
import { ReactElement, useRef } from "react";
import { ReactComponent as SendIcon } from "@/presentation/common/icons/outlined/Send 3.svg";
import { ReactComponent as ImageIcon } from "@/presentation/common/icons/outlined/Image.svg";
Expand All @@ -7,52 +7,95 @@ import { SocketRepository } from "@/data/backend/socket";
import { useAuthStore } from "@/store/authStore";
import { LoadingButton } from "@mui/lab";
import { useMutation } from "@tanstack/react-query";
import { SendMessageDto } from "@/domain/dtos/socket";
import { SendFileDto, SendMessageDto } from "@/domain/dtos/socket";
interface ChatBarProps {
roomId: number;
socketRepository: SocketRepository;
}

const useSendMutation = (socketRepository: SocketRepository) => {
const useSendMessageMutation = (socketRepository: SocketRepository) => {
return useMutation({
mutationFn: async (dto: SendMessageDto) => {
socketRepository.sendMessage(dto);
},
});
};

const useSendFileMutation = (socketRepository: SocketRepository) => {
return useMutation({
mutationFn: async (
dto: Omit<SendFileDto, "file"> & {
rawFile: File;
}
) => {
const file = {
filename: dto.rawFile.name,
fileData: await dto.rawFile.arrayBuffer(),
};
socketRepository.sendFile({
...dto,
file,
});
},
});
};

export const ChatBar = ({
roomId,
socketRepository,
}: ChatBarProps): ReactElement => {
const ref = useRef<HTMLInputElement>(null);
const fileRef = useRef<HTMLInputElement>(null);
const { appUser } = useAuthStore();
const { mutateAsync, isLoading } = useSendMutation(socketRepository);
if (appUser == null) {
throw new Error("appUser is null");
}
const { mutateAsync: mutateMessage, isLoading } =
useSendMessageMutation(socketRepository);
const { mutateAsync: mutateFile, isLoading: isFileLoading } =
useSendFileMutation(socketRepository);

const handleSendMessage = () => {
if (ref.current == null) return;
const message = ref.current.value;
if (message == null || message == "") return;
mutateAsync({
mutateMessage({
message,
sendUserUid: appUser.uid,
roomId: roomId,
});
ref.current.value = "";
};

const handleSendAccount = () => {
mutateMessage({
message: "농협 302-1234-1234-12",
sendUserUid: appUser.uid,
roomId: roomId,
});
};
return (
<Box css={styles.barContainer}>
<Box css={styles.actionsContainer}>
<Button css={styles.actionButton}>
<LoadingButton
css={styles.actionButton}
onClick={() => {
if (fileRef.current == null) return;
fileRef.current.click();
}}
loading={isFileLoading}
>
<ImageIcon />
자료 보내기
</Button>
<Button css={styles.actionButton}>
</LoadingButton>
<LoadingButton
css={styles.actionButton}
loading={isLoading}
onClick={handleSendAccount}
>
<PasswordIcon />
계좌 보내기
</Button>
</LoadingButton>
</Box>
<input
placeholder="메시지를 입력해주세요!"
Expand All @@ -66,6 +109,23 @@ export const ChatBar = ({
>
<SendIcon />
</LoadingButton>

<input
hidden
ref={fileRef}
type="file"
accept="image/*, video/*"
multiple={false}
onChange={(e) => {
if (e.target.files == null) return;
const file = e.target.files[0];
mutateFile({
rawFile: file,
sendUserUid: appUser.uid,
roomId: roomId,
});
}}
/>
</Box>
);
};
Expand Down
24 changes: 23 additions & 1 deletion src/presentation/chat/components/ChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AppMessage } from "@/domain/models/appMessage";
import { useAuthStore } from "@/store/authStore";
import { Avatar, Box, Typography, css } from "@mui/material";
import { ReactElement } from "react";
import { ImageOrVideo } from "./ImageOrVideo";

interface ChatMessageProps {
message: AppMessage;
Expand All @@ -19,7 +20,14 @@ export const ChatMessage = ({ message }: ChatMessageProps): ReactElement => {
)}
<Box css={styles.typoContainer}>
{!isSelf && <Typography>{sendor?.displayName}</Typography>}
<Typography css={styles.message}>{message.message}</Typography>
{message.messageType == "message" && (
<Typography css={styles.message}>{message.message}</Typography>
)}
{message.messageType == "file" && (
<Box css={styles.imageContainer(isSelf)}>
<ImageOrVideo src={message.message} css={styles.image} />
</Box>
)}
</Box>
</Box>
);
Expand All @@ -43,6 +51,7 @@ const styles = {
`,

message: css`
width: fit-content;
display: inline-flex;
padding: 5px 10px;
justify-content: center;
Expand All @@ -60,4 +69,17 @@ const styles = {
overflow-x: hidden;
word-break: break-all;
`,
imageContainer: (isSelf: boolean) => css`
display: flex;
justify-content: ${isSelf ? "flex-end" : "flex-start"};
`,
image: css`
width: 200px;
height: 200px;
object-fit: cover;
aspect-ratio: 1;
border-radius: 8px;
padding: 5px;
background: #fff;
`,
};
78 changes: 78 additions & 0 deletions src/presentation/chat/components/ImageOrVideo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Button, Modal, Skeleton, css } from "@mui/material";
import { ReactElement, useState } from "react";
interface ImageOrVideoProps {
src: string;
className?: string;
}
export const ImageOrVideo = ({
src,
className,
}: ImageOrVideoProps): ReactElement => {
const getFileType = (filePath: string): string => {
// 확장자를 추출합니다
const extension = filePath.split(".").pop()?.toLowerCase() ?? "";

// 이미지 확장자 목록
const imageExtensions = ["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp"];
// 비디오 확장자 목록
const videoExtensions = ["mp4", "mov", "wmv", "flv", "avi", "mkv", "webm"];

// 확장자에 따라 파일 타입을 결정합니다
if (imageExtensions.includes(extension)) {
return "image";
} else if (videoExtensions.includes(extension)) {
return "video";
} else {
return "unknown"; // 알 수 없는 파일 타입
}
};

const [modalOpen, setModalOpen] = useState(false);
const fileType = getFileType(src);
return (
<>
<Modal
open={modalOpen}
onClose={() => setModalOpen(false)}
css={styles.modal}
>
<>
{fileType == "image" && <img src={src} css={styles.modalImage} />}
{fileType == "video" && (
<video src={src} css={styles.modalImage} controls />
)}
{fileType == "unknown" && (
<Skeleton variant="rectangular" css={styles.modalImage} />
)}
</>
</Modal>
<Button onClick={() => setModalOpen(true)}>
{fileType == "image" && <img src={src} className={className} />}
{fileType == "video" && (
<video
src={src}
className={className}
controls
controlsList="nofullscreen"
/>
)}
{fileType == "unknown" && (
<Skeleton variant="rectangular" className={className} />
)}
</Button>
</>
);
};

const styles = {
modal: css`
display: flex;
align-items: center;
justify-content: center;
padding: 64px;
`,
modalImage: css`
width: 100%;
object-fit: contain;
`,
};
3 changes: 1 addition & 2 deletions src/presentation/chat/pages/ChatDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
} from "../components/ChatDetailHeader";
import { ChatBar } from "../components/ChatBar";
import { SocketRepository } from "@/data/backend/socket";
import { SendMessageDto } from "@/domain/dtos/socket";
import { ChatMessage } from "../components/ChatMessage";
import { AppMessage } from "@/domain/models/appMessage";

Expand Down Expand Up @@ -71,7 +70,7 @@ const ChatDetail = ({ roomId }: ChatDetailProps): ReactElement => {
scrollToBottom(isPreviousData);
}, [messages, isPreviousData]);
const handleReceive = useCallback(
(dto: SendMessageDto) => {
(dto: AppMessage) => {
updateQueryData(roomId, dto);
},
[updateQueryData, roomId]
Expand Down
15 changes: 12 additions & 3 deletions src/presentation/detail/pages/AccidentDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ReactComponent as Left1 } from "@/presentation/common/icons/outlined/Le
import { useKakaoMapAddressSearch } from "@/hooks/useKakaoMapSearch";
import { useReadUserById } from "@/data/hooks/user";
import { useCreateRoomMutation } from "@/data/hooks/chat";
import { enqueueSnackbar } from "notistack";

interface AccidentDetailPageProps {
accident: Accident;
Expand All @@ -31,11 +32,19 @@ export const AccdientDetailPage = ({ accident }: AccidentDetailPageProps) => {
const { mutateAsync, isLoading } = useCreateRoomMutation();

const handleConnectChat = async () => {
const data = await mutateAsync({
mutateAsync({
id: accident.id,
isAccident: true,
});
navigate(`/chat/${data.roomId}`);
})
.catch((e) => {
enqueueSnackbar(e.message, { variant: "error" });
})
.then((data) => {
if (!data) {
return;
}
navigate(`/chat/${data.roomId}`);
});
};

const placeName = addressData?.address_name ?? "주소를 불러오는 중입니다.";
Expand Down
Loading

0 comments on commit 22759fa

Please sign in to comment.