Skip to content

Commit

Permalink
feature: Load recent messages
Browse files Browse the repository at this point in the history
  • Loading branch information
seheon99 committed May 23, 2024
1 parent b5b5f1e commit 542c0cc
Show file tree
Hide file tree
Showing 23 changed files with 771 additions and 140 deletions.
269 changes: 226 additions & 43 deletions .pnp.cjs

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
"typescript.tsdk": ".yarn/sdks/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"prettier.prettierPath": ".yarn/sdks/prettier/index.cjs",
"svg.preview.background": "dark-transparent"
"svg.preview.background": "dark-transparent",
"sarif-viewer.connectToGithubCodeScanning": "off"
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"react": "^18",
"react-dom": "^18",
"return-fetch": "^0.4.5",
"socket.io-client": "^4.7.5",
"socket.io-client": "^2",
"swiper": "^11.0.5",
"swr": "^2.2.5",
"zustand": "^4.5.2"
Expand Down Expand Up @@ -61,6 +61,7 @@
"stylelint-config-clean-order": "^5.4.1",
"stylelint-config-standard": "^36.0.0",
"stylelint-order": "^6.0.4",
"supports-color": "^9.4.0",
"typescript": "5.3.2",
"webpack": "^5.89.0"
},
Expand Down
46 changes: 40 additions & 6 deletions src/components/molecules/ChatBubbles.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,58 @@
import { useState } from 'react';
import { forwardRef, useEffect, useRef } from 'react';
import Image from 'next/image';

import styled from '@emotion/styled';

import { exampleUsers } from '#/entities';
import { Message } from '#/types';
import { isImageMessage, isTextMessage } from '#/utilities/message';
import { ChatBubble } from './ChatBubble';

export const ChatBubbles: React.FC = () => {
const [messages, setMessages] = useState<string[]>(['Hi']);
interface ChatBubblesProps {
messages: Message[];
loadMore: () => void;
}

export const ChatBubbles = ({ messages, loadMore }: ChatBubblesProps) => {
const topRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadMore();
}
});
if (topRef.current) {
observer.observe(topRef.current);
}
return () => observer.disconnect();
}, [loadMore]);

return (
<Container>
{messages.map((message, index) => (
<ChatBubble key={index} user={exampleUsers[0]} text={message} />
))}
<div />
<div />
<div />
{messages.map((message, index) =>
isTextMessage(message) ? (
<ChatBubble key={index} user={exampleUsers[0]} text={message.content} />
) : isImageMessage(message) ? (
<Image src={message.imageUrl} alt={`Image from message ${message.id}`} />
) : null
)}
<div ref={topRef} />
</Container>
);
};

ChatBubbles.displayName = 'ChatBubbles';

const Container = styled.div`
overflow-y: scroll;
display: flex;
flex-direction: column-reverse;
gap: 40px;
height: 100%;
padding: 28px;
`;
13 changes: 9 additions & 4 deletions src/components/molecules/ChatToolbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,26 @@ import styled from '@emotion/styled';

import { Icons, Input } from '#/components/atoms';

export const ChatToolbox: React.FC = () => {
interface ChatToolboxProps {
sendMessage: (message: string) => void;
sendImage: (imageUrl: string) => void;
}

export const ChatToolbox = ({ sendMessage, sendImage }: ChatToolboxProps) => {
return (
<Container>
<Form>
<ToolIcon icon="image" size={36} />
<TextInput
placeholder="대기방의 팀원에게 메세지를 보내보세요"
typo="typo5"
weight="regular"
/>
<SendIcon icon="upload" size={36} color="#ff908d" />
</Container>
</Form>
);
};

const Container = styled.div`
const Form = styled.form`
position: relative;
display: flex;
gap: 10px;
Expand Down
2 changes: 1 addition & 1 deletion src/components/molecules/ProjectSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface ProjectSummaryProps {
}

export const ProjectSummary = ({ projectId }: ProjectSummaryProps) => {
const project = useProject(projectId);
const { data: project } = useProject(projectId);
return (
<Container>
<Txt>{project?.name}</Txt>
Expand Down
73 changes: 63 additions & 10 deletions src/components/organisms/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,82 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

import styled from '@emotion/styled';

import { Txt } from '#/components/atoms';
import { Loading, Txt } from '#/components/atoms';
import { ChatBubbles } from '#/components/molecules/ChatBubbles';
import { ChatToolbox } from '#/components/molecules/ChatToolbox';
import { useChatMessagesQuery } from '#/hooks/use-chat';
import { useChat } from '#/stores';
import { Message } from '#/types';
import { fitSocket } from '#/utilities/socket';

interface ChatProps {
chatId: number;
}

export const Chat = ({ chatId }: ChatProps) => {
const [isTopReached, setIsTopReached] = useState(true);

const { chat, addNewMessage, addPrevMessages } = useChat(chatId);

const lastMessage = useMemo(() => chat?.messages[chat.messages.length - 1], [chat?.messages]);

const { data: messageBundle } = useChatMessagesQuery(
isTopReached ? chatId : null,
lastMessage?.id
);

const socket = useRef<ReturnType<typeof fitSocket>>();

const sendMessage = useCallback((message: string) => {
if (socket.current) {
socket.current.emit('/chat/text', { content: message });
}
}, []);

const sendImage = useCallback((imageUrl: string) => {
if (socket.current) {
socket.current.emit('/chat/image', { content: imageUrl });
}
}, []);

useEffect(() => {
socket.current = fitSocket({ roomId: chatId });
socket.current.on('get_message', (message: Message) => {
addNewMessage(message);
});
return () => socket.current.close();
}, [addNewMessage, chatId]);

useEffect(() => {
if (messageBundle) {
addPrevMessages(messageBundle);
setIsTopReached(false);
}
}, [addPrevMessages, messageBundle]);

export const Chat: React.FC = () => {
return (
<Container>
<Header>
<Txt size="typo4" weight="bold" color="#757575">
채팅방
</Txt>
</Header>
<ChatBubblesContainer>
<ChatBubbles />
</ChatBubblesContainer>
{chat ? (
<ChatBubbles messages={chat?.messages} loadMore={() => setIsTopReached(true)} />
) : (
<Loading />
)}
<ChatToolboxContainer>
<ChatToolbox />
<ChatToolbox sendMessage={sendMessage} sendImage={sendImage} />
</ChatToolboxContainer>
</Container>
);
};

const Container = styled.div`
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
Expand All @@ -41,10 +95,9 @@ const Header = styled.div`
box-shadow: 0 0 32px rgb(0 0 0 / 5%);
`;

const ChatBubblesContainer = styled.div`
flex: 1;
`;

const ChatToolboxContainer = styled.div`
position: absolute;
bottom: 0;
width: 100%;
padding: 30px;
`;
2 changes: 2 additions & 0 deletions src/components/organisms/ChatParticipants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ const Container = styled.div`
display: flex;
flex-direction: column;
gap: 60px;
width: 100%;
height: fit-content;
`;

const HeaderContainer = styled.div`
Expand Down
29 changes: 22 additions & 7 deletions src/components/organisms/ChatRoom.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';

import styled from '@emotion/styled';

Expand All @@ -14,10 +14,18 @@ interface ChatRoomProps {

export const ChatRoom = ({ projectId, matchingId }: ChatRoomProps) => {
const [participants, setParticipants] = useState<ChatUser[]>([]);
const [height, setHeight] = useState<number | null>(null);

const matchingRoomState = useMatchingRoom(matchingId);
const projectState = useProject(projectId);

const chatId = useMemo(
() => matchingRoomState.data?.chatId ?? projectState.data?.chatId,
[matchingRoomState.data?.chatId, projectState.data?.chatId]
);

const participantsContainerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (projectState?.data?.members) {
setParticipants(projectState.data.members);
Expand All @@ -26,33 +34,40 @@ export const ChatRoom = ({ projectId, matchingId }: ChatRoomProps) => {
}
}, [matchingRoomState.data?.matchingUsers, projectState.data?.members]);

useEffect(() => {
if (participantsContainerRef.current) {
const targetHeight = participantsContainerRef.current.offsetHeight;
setHeight(targetHeight);
}
}, [participantsContainerRef.current?.offsetHeight]);

return (
<Container>
<ChatParticipantsContainer>
<Container height={height}>
<ChatParticipantsContainer ref={participantsContainerRef}>
<ChatParticipants projectId={projectId} participants={participants} />
</ChatParticipantsContainer>
<ChatContainer>
<Chat />
</ChatContainer>
<ChatContainer>{chatId && <Chat chatId={chatId} />}</ChatContainer>
</Container>
);
};

const Container = styled.div`
const Container = styled.div<{ height?: number | null }>`
position: relative;
overflow: hidden;
display: flex;
width: 100%;
max-width: 1200px;
height: ${({ height }) => (height ? `${height}px` : 'auto')};
border: 1px solid #e0e0e0;
border-radius: 12px;
`;

const ChatParticipantsContainer = styled.div`
flex: 1 1 50%;
height: fit-content;
padding: 30px 60px;
`;

Expand Down
27 changes: 24 additions & 3 deletions src/components/organisms/MatchingChatRoom.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,45 @@
import { useMemo } from 'react';

import styled from '@emotion/styled';

import { Button } from '#/components/atoms';
import { ChatRoom } from '#/components/organisms/ChatRoom';
import { useMatchingRoomReadyMutation } from '#/hooks/use-matching-room';
import {
useMatchingRoomCancelMutation,
useMatchingRoomReadyMutation,
} from '#/hooks/use-matching-room';
import { useAuthStore, useMatching, useMatchingRoom } from '#/stores';
import { MatchingRoom } from '#/types';

interface MatchingChatRoomProps {
matchingId: MatchingRoom['id'];
}

export const MatchingChatRoom = ({ matchingId }: MatchingChatRoomProps) => {
const userId = useAuthStore((store) => store.user?.id);
const { data: matching } = useMatching();
const { data: matchingRoom } = useMatchingRoom(matching?.roomId);
const isReady = useMemo(
() => matchingRoom?.matchingUsers.find((u) => u.id === userId)?.isReady,
[matchingRoom?.matchingUsers, userId]
);

const { trigger: readyMatching } = useMatchingRoomReadyMutation(matchingId);
const { trigger: cancelMatching } = useMatchingRoomCancelMutation(matchingId);

return (
<Container>
<ChatRoom matchingId={matchingId} />
<ButtonContainer>
<Button variant="round" height="70" color="secondary">
<Button variant="round" height="70" color="secondary" onClick={() => cancelMatching()}>
대기방에서 나가기
</Button>
<Button variant="round" height="70" color="primary" onClick={() => readyMatching()}>
<Button
variant="round"
height="70"
color="primary"
onClick={() => readyMatching({ isReady: !isReady })}
>
프로젝트 시작하기
</Button>
</ButtonContainer>
Expand Down
Loading

0 comments on commit 542c0cc

Please sign in to comment.