1. navigation
- 채팅이 내가 있는 방에 도착하게 될 경우 네비게이션 오른쪽 상단의 messenger icon에 빨간색 원을 그려줌
- 처음 componenet가 렌더링 될 때, dispatch() 함수를 호출하여 client값을 store에 save함
- 로그인 성공 이후 localstorage에 저장된 email을 가지고 사용자 정보를 서버에 요청하며 로그아웃 시 email 값이 remove되므로
userdata가 undefined일 경우 login 창으로 redirect 시켜 로그인하도록 함
const Navigation = () => {
const email = localStorage.getItem("email");
const {
data: userData,
error,
revalidate,
mutate
} = useSWR<IChatUser | undefined>(email, fetchByEmail);
const dispatch = useDispatch()
const history = useHistory();
const [messengerAlarmed, setMessengerAlarmed] = useState(false)
const onLogout = useCallback(() => {
axios.post(`/api/logout`, null, {
withCredentials: true
})
.then(() => {
mutate(undefined, false);
localStorage.removeItem("email")
console.log("logout success");
console.log("cached user : " + JSON.stringify(userData));
})
}, [])
const onMessengerClick = useCallback(() => {
setMessengerAlarmed(false)
console.log("redirect to /messenger");
history.push('/messenger')
}, [])
const subscribeCallback = useCallback(({body}) => {
console.log("messageAlarm ! " + JSON.stringify(body))
console.log("current path : " + window.location.pathname)
if (!window.location.pathname.startsWith("/messenger")) {
setMessengerAlarmed(true)
} else {
console.log("your path is not allowd for messenger notification!")
}
}, [])
const client = useRef<Client>();
const [connect, disconnect] = useStomp(client, '/sub/messenger/icon/' + userData?.userId, subscribeCallback)
useEffect(() => {
connect();
dispatch(saveStompData(client))
return () => disconnect();
}, []);
if (userData === undefined) {
console.log("redirect to login");
return <Redirect to="/login"/>;
}
return (
<Nav>
<div className="nav-container">
<div className="nav-1">
<FaSatellite/>
</div>
<Input className="input-search" id="searchInput"
placeholder="검색"
type="search"/>
<div className="nav-2">
<span onClick={onLogout}>
<BiLogOut>로그아웃</BiLogOut>
</span>
<span><FaHome/></span>
{messengerAlarmed ?
<span onClick={onMessengerClick}
className="fa-stack fa-sm">
<FaRegCircle className="fa-stack-2x" color="red"/>
<FaFacebookMessenger className="fa-stack-1x"/>
</span>
:
<span onClick={onMessengerClick}>
<FaFacebookMessenger/>
</span>
}
<span><FaRegHeart/></span>
<span><FaRegUser/></span>
</div>
</div>
</Nav>
);
};
export default Navigation;
2. chatMenu
- messenger page에 왼쪽 사이드바 컴포넌트이며, 사용자의 채팅방 목록을 띄워줌
- 채팅방 목록을 서버에 요청한 후, 응답받은 list의 element 각각에 navlink 태그를 달아 채팅방의 채팅내역을 보도록 컴포넌트를 달아줌
- 각 element는 conversation 컴포넌트로 만들어 props를 전달해줌
const ChatMenu: FC = () => {
const email = localStorage.getItem("email");
const {
data: userData,
error,
revalidate,
mutate
} = useSWR<IChatUser>(email, fetchByEmail, {
dedupingInterval: 20000, // 20초
});
const {data: chatRoomsList} = useSWR<IChatRoom[]>(
`/api/chat/room/list?userId=${userData?.userId}`, fetchChatRooms, {
dedupingInterval: 5000, // 5초
});
return (
<Div>
<div className="chatMenuWrapper">
{chatRoomsList?.map(room => {
return (
<NavLink key={room.chatRoomId}
activeClassName="selected"
to={{
pathname: `/messenger/dm/${room.chatRoomId}`,
state: {chatRoom: room}
}}>
<Conversation chatRoom={room}
currentUserId={userData?.userId}/>
</NavLink>
)
})}
</div>
</Div>
)
}
export default ChatMenu
3. conversation
- 채팅방은 총 3개의 구성요소를 가지고 있음
- 맨 왼쪽에 나와 채팅하는 상대의 프로필 이미지를 띄워줌
- 그리고 오른쪽에 상대의 닉네임을 띄워주고
- 가장 마지막에 채팅했던 메시지를 띄워줌
interface Props {
chatRoom: IChatRoom;
currentUserId?: number;
}
const Conversation: FC<Props> = ({chatRoom, currentUserId}) => {
const [user, setUser] = useState<IChatUser>()
const PF: string | undefined = process.env.REACT_APP_PUBLIC_FOLDER
const friend: IChatUser | undefined = chatRoom.chatRoomMembers.find((m: IChatUser) => m.userId !== currentUserId);
const friendId = friend?.userId
useEffect(() => {
const getUser = async () => {
try {
const friend = await axios("/api/user/?userId=" + friendId);
setUser(friend.data.HCS.item.profile);
} catch (err) {
console.log(err)
}
}
getUser();
}, [currentUserId, chatRoom])
return (
<Div className="conversation">
<Img
className="conversationImg"
src={user?.profileImage ? PF + "person/noAvatar.png" : user?.profileImage}
/>
<div className="conversationContent">
<div className="conversationName">{user?.nickname}</div>
<div
className="lastMessage">{chatRoom.lastChatMesg.message}</div>
</div>
</Div>
)
}
export default Conversation
4. chatBox
- 채팅방에 들어갈 때 채팅 내역과 채팅입력이 가능한 컴포넌트로 구성된 컴포넌트이다
- 최근 15개의 채팅내역이 무엇인지 서버에 요청하여 응답값을 띄워주고
- dispatch()로 store에 저장된 stomp client를 fetching한다
- 리턴된 stomp client로 subscribe하여 새로운 메시지가 해당 방에 들어올 때마다 chatList에 메시지를 추가시킨다
- 또한 채팅메시지를 보낼 때 publish() 호출하여 서버에 메시지 전송
const ChatBox = () => {
const stompReducer = useSelector((state: RootReducerType) => state.StompReducer)
const dispatch = useDispatch()
const {roomId} = useParams<{ roomId?: string }>();
const chatRoomData = useLocation<any>().state.chatRoom
const email = localStorage.getItem("email");
const {
data: userData,
error,
revalidate,
mutate
} = useSWR<IChatUser>(email, fetchByEmail, {
dedupingInterval: 20000, // 20초
});
const friend: IChatUser = chatRoomData?.chatRoomMembers.find((m: IChatUser) => m.userId !== userData?.userId)!;
const [chatList, setChatList] = useState<IChatMessage[]>([])
const [chat, onChangeChat, setChat] = useInput('');
const fetchChatMesgs = useCallback(() => {
const chatListData = axios.get(
`/api/chat/room/?roomId=${roomId}`
)
chatListData.then((response) => setChatList(response.data.HCS.item.latestChatMessages))
}, []
)
const client = useRef<Client>();
useEffect(() => {
dispatch(fetchStompData())
if (stompReducer.success) {
console.log("dispatch for getting client is success! ")
client.current = stompReducer.stomp
client.current?.subscribe('/sub/chat/room/' + chatRoomData.chatRoomId, ({body}) => {
setChatList((_chatList: IChatMessage[]) => [..._chatList, JSON.parse(body)])
})
}
fetchChatMesgs()
}, []);
const onSubmitForm = useCallback(
(e) => {
e.preventDefault();
console.log(chat);
if (chat?.trim()) {
client.current?.publish({
destination: '/pub/chat/message',
body: JSON.stringify({
roomId: roomId,
authorId: userData?.userId,
message: chat
})
})
setChat('');
}
},
[chat, chatList, userData, friend, roomId]
);
return (
<Div>
<div className="chatBoxWrapper">
<div className="chatBoxTop">
{chatList && chatList.map((chat: IChatMessage, index) => {
if (chat.authorId === userData?.userId) {
return <Message key={index} isOwn
chatMessage={chat}
me={userData}/>;
} else {
return <Message key={index}
chatMessage={chat} me={friend}/>;
}
})}
</div>
<div className="chatBoxBottom">
<ChatInput chat={chat} friend={friend}
onChangeChat={onChangeChat}
onSubmitForm={onSubmitForm}/>
</div>
</div>
</Div>
);
};
export default ChatBox
5. message
- 채팅방의 메시지 각각을 message 컴포넌트로 렌더링하여 뿌림
- 채팅을 작성한 사람의 프로필 이미지, 이름, 메시지 내용을 가지고 태그로 씌워 컴포넌트를 완성한다
- 해당 메시지의 작가가 지금 로그인한 사람인지 아닌지에 따라 message 컴포넌트가 왼쪽에 배치될지 결정하고,
isOwn 속성 여부에 따라 style 을 다르게 하여 스타일을 준다
interface Props {
isOwn?: boolean;
chatMessage?: IChatMessage
me: IChatUser
}
const Message: FC<Props> = ({isOwn = false, chatMessage, me}) => {
const PF: string | undefined = process.env.REACT_APP_PUBLIC_FOLDER
return (
<Div className="message" isOwn={isOwn}>
<div className="messageTop">
<Img
className="conversationImg"
src={me?.profileImage ? PF + "person/noAvatar.png" : me?.profileImage}
/>
<p className="messageText">{chatMessage?.message}</p>
</div>
</Div>
)
}
export default Message
'Javascript > React' 카테고리의 다른 글
[React] Spring Boot with React 채팅 서버 : 4. redux (0) | 2022.03.13 |
---|---|
[React] Spring Boot with React 채팅 서버 : 2-3. utils (0) | 2022.03.10 |
[React] Spring Boot with React 채팅 서버 : 2-2. typings (0) | 2022.03.10 |
[React] Spring Boot with React 채팅 서버 : 2-1. hooks (0) | 2022.03.09 |
[React] Spring Boot with React 채팅 서버 : 3. pages (0) | 2022.03.09 |