src/components/Chat/ChatInput.jsx
import { Grid, IconButton, InputAdornment, TextField } from '@mui/material'
import InsertEmoticonIcon from '@mui/icons-material/InsertEmoticon'
import ImageIcon from '@mui/icons-material/Image'
import SendIcon from '@mui/icons-material/Send'
import React, { useCallback, useState } from 'react'
import '../../firebase'
import { getDatabase, push, ref, serverTimestamp, set } from 'firebase/database'
import { useSelector } from 'react-redux'
export default function ChatInput() {
// 채널 정보 가져오기
const { channel, user } = useSelector(state => state)
//message 상태
const [message, setMesssage] = useState('')
//loading 상태
const [loading, setLoading] = useState(false)
//message onChange로 저장
const handleChange = useCallback(e => {
setMesssage(e.target.value)
}, [])
// 메세지 생성
const createMessage = useCallback(
() => ({
timestamp: serverTimestamp(),
user: {
id: user.currentUser.uid,
name: user.currentUser.displayName,
avatar: user.currentUser.photoURL,
},
content: message,
}),
[
message,
user.currentUser.uid,
user.currentUser.displayName,
user.currentUser.photoURL,
]
)
// 메시지 전송
const clickSendMessage = useCallback(async () => {
// 메세지가 없을경우 방어
if (!message) return
setLoading(true)
// firebass realtimedatabase에 메세지 저장
try {
await set(
push(ref(getDatabase(), 'messages/' + channel.currentChannel.id)),
createMessage()
)
setLoading(false)
setMesssage('')
} catch (error) {
console.error(error)
setLoading(false)
}
}, [message, channel.currentChannel?.id, createMessage])
return (
<Grid container sx={{ p: '20px' }}>
<Grid item xs={12} sx={{ position: 'relative' }}>
<TextField
InputProps={{
startAdornment: (
<InputAdornment position="start">
<IconButton>
<InsertEmoticonIcon />
</IconButton>
<IconButton>
<ImageIcon />
</IconButton>
</InputAdornment>
),
endAdornment: (
<InputAdornment position="start">
<IconButton disabled={loading} onClick={clickSendMessage}>
<SendIcon />
</IconButton>
</InputAdornment>
),
}}
autoComplete="off"
label="메세지 입력"
fullWidth
value={message}
onChange={handleChange}
/>
</Grid>
</Grid>
)
}
firebase에 채널id와 같은 id로 메시지가 등록되도록 구현했다.
src/components/Chat.jsx
import { Divider, Grid, List, Paper, Toolbar } from '@mui/material'
import React, { useEffect, useState } from 'react'
import ChatHeader from './ChatHeader'
import { useSelector } from 'react-redux'
import ChatInput from './ChatInput'
import ChatMessage from './ChatMessage'
import '../../firebase'
import {
child,
get,
getDatabase,
onChildAdded,
orderByChild,
query,
ref,
startAt,
} from 'firebase/database'
export default function Chat() {
// redux에서 채널명 가져오기
const { channel, user } = useSelector(state => state)
// 메세지
const [messages, setMessages] = useState([])
// firebase에 저장된 메시지 가져오기
useEffect(() => {
// 채널, 유저정보 없을 경우 방어
if (!channel.currentChannel) return
// 메시지 가져오기
async function getMessages() {
const snapShot = await get(
child(ref(getDatabase()), 'messages/' + channel.currentChannel.id)
)
setMessages(snapShot.val() ? Object.values(snapShot.val()) : [])
}
getMessages()
return () => {
setMessages([])
}
}, [channel.currentChannel])
// 메시지 정렬 기능 / 최적화
useEffect(() => {
if (!channel.currentChannel) return
// timestamp를 기준으로 정렬
const sorted = query(
ref(getDatabase(), 'messages/' + channel.currentChannel.id),
orderByChild('timestamp')
)
// 현재시점부터 추가된 메세지를 콜백으로 추가.
const unsubscribe = onChildAdded(
// onChildAdded는 순차적으로 데이터를 가져온다. (동기)
query(sorted, startAt(Date.now())),
snapshot => setMessages(oldMessages => [...oldMessages, snapshot.val()])
)
return () => {
unsubscribe?.()
}
}, [channel.currentChannel])
return (
<>
<Toolbar />
<ChatHeader channelInfo={channel.currentChannel} />
<Grid
container
component={Paper}
variant="outlined"
sx={{ mt: 3, position: 'relative' }}
>
<List
sx={{
height: 'calc(100vh - 350px)',
overflow: 'scroll',
width: '100%',
position: 'relative',
}}
>
{messages.map(message => (
<ChatMessage
key={message.timestamp}
message={message}
user={user}
/>
))}
</List>
<Divider />
<ChatInput />
</Grid>
</>
)
}
src/components/Chat/ChatMessage.jsx
Chat.jsx에서 가져온 message를 ChatMessage.jsx에 전달해준다.
import {
Avatar,
Grid,
ListItem,
ListItemAvatar,
ListItemText,
} from '@mui/material'
import dayjs from 'dayjs'
import React from 'react'
const relativeTime = require('dayjs/plugin/relativeTime')
dayjs.extend(relativeTime)
export default function ChatMessage({ message, user }) {
return (
<ListItem>
<ListItemAvatar sx={{ alignSelf: 'stretch' }}>
<Avatar
variant="rounded"
sx={{ width: 50, height: 50 }}
alt="profileImage"
src={message.user.avatar}
/>
</ListItemAvatar>
<Grid container sx={{ ml: 2 }}>
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'left' }}>
<ListItemText
sx={{ display: 'flex' }}
primary={message.user.name}
primaryTypographyProps={{
fontWeight: 'bold',
color:
message.user.id === user.currentUser?.uid ? 'orange' : 'black',
}}
secondary={dayjs(message.timestamp).fromNow()}
secondaryTypographyProps={{ color: 'gray', ml: 1 }}
/>
</Grid>
<Grid item xs={12}>
<ListItemText
align="left"
xs={{ wordBreak: 'break-all' }}
primary={message.content}
/>
{/* TODO 이미지 추가 */}
{/* <img alt="이미지" src="" style={{ maxWidth: '100%' }} /> */}
</Grid>
</Grid>
</ListItem>
)
}