Added chat page

This commit is contained in:
mrfry 2021-05-26 18:37:06 +02:00
parent 3a67f2a1aa
commit c60ace4f9b
2 changed files with 664 additions and 0 deletions

492
src/pages/chat.js Normal file
View file

@ -0,0 +1,492 @@
import React from 'react'
import Head from 'next/head'
import io from 'socket.io-client'
import constants from '../constants.json'
import LoadingIndicator from '../components/LoadingIndicator'
import styles from './chat.module.css'
function countAllMessages(msgs) {
return Object.keys(msgs).reduce((acc, key) => {
return acc + msgs[key].length
}, 0)
}
function groupMessages(msgs, currUser) {
return msgs.reduce((acc, msg) => {
const group =
parseInt(msg.sender) !== parseInt(currUser) ? msg.sender : msg.reciever
return {
...acc,
[group]: [msg],
}
}, {})
}
function addMsgsToGroup(msgGroup, msgs, user) {
let res = { ...msgGroup }
msgs.forEach((msg) => {
res = addMsgToGroup(
res,
msg.reciever === user ? { ...msg, unread: 0 } : msg,
user
)
})
return res
}
function addMsgToGroup(msgGroup, msg, user) {
const group =
parseInt(msg.sender) === parseInt(user) ? msg.reciever : msg.sender
if (!msgGroup[group]) {
msgGroup[group] = []
}
return {
...msgGroup,
[group]: [...msgGroup[group], msg],
}
}
function SeenMarker() {
return (
<span className={styles.unreadMarker} key={`unread_marker`}>
Látta
</span>
)
}
function NewMarker() {
return (
<span className={styles.newMarker} key={`unread_marker`}>
Új üzenet
</span>
)
}
export default class Chat extends React.Component {
constructor(props) {
super(props)
this.state = {
msgs: {},
currMsg: '',
connected: false,
selectedUser: 0,
}
if (props.refetchGlobalData) {
props.refetchGlobalData()
}
if (props.globalData && !isNaN(props.globalData.userId)) {
this.state.user = props.globalData.userId
// this.connect(this.props.globalData.userId)
}
}
componentDidUpdate(prevProps, prevState) {
if (!prevProps.globalData.userId && this.props.globalData.userId) {
this.setState({
user: this.props.globalData.userId,
})
// this.connect(this.props.globalData.userId)
}
if (
countAllMessages(prevState.msgs) !== countAllMessages(this.state.msgs) ||
prevState.selectedUser !== this.state.selectedUser
) {
this.scrollToChatBottom()
}
}
componentWillUnmount() {
console.info('Chat disconnect')
if (this.socket) {
this.socket.disconnect()
}
}
scrollToChatBottom() {
const objDiv = document.getElementById('messages')
if (objDiv) {
objDiv.scrollTop = objDiv.scrollHeight
}
}
handleErrors(err) {
alert(err.message)
console.error(err)
}
connect(user) {
const { connected } = this.state
if (connected) {
console.warn('Already connected ...')
return
}
// https://socket.io/docs/v4/handling-cors/#Configuration
const socket = io(`${constants.apiUrl}`, {
withCredentials: true,
extraHeaders: {
'qmining-chat': 'qmining-chat',
},
})
socket.on('connect', () => {
console.info(`Connected as user ${user}`)
socket.emit('join', { id: user })
})
socket.on('connect_error', (err) => this.handleErrors(err))
socket.on('connect_failed', (err) => this.handleErrors(err))
socket.on('prev messages', (data) => {
const { prevMsgs } = data
const { user } = this.state
this.setState({
msgs: groupMessages(prevMsgs, user),
connected: true,
})
})
socket.on('chat message read', (data) => {
const { userReadMsg } = data
this.partnerReadChatMessage(userReadMsg)
})
socket.on('chat message open', (data) => {
const { msgs, user, selectedUser } = this.state
if (msgs[selectedUser].length <= 1) {
this.setState({
msgs: addMsgsToGroup(msgs, data, user),
})
}
})
socket.on('chat message', (data) => {
const { msgs, user } = this.state
this.setState({
msgs: addMsgToGroup(msgs, data, user),
})
})
this.socket = socket
}
sendMsg() {
const { msgs, selectedUser, currMsg, user } = this.state
if (!currMsg) {
return
}
if (this.socket && selectedUser) {
const msg = {
msg: currMsg,
reciever: selectedUser,
sender: user,
date: new Date().getTime(),
unread: 1,
}
this.socket.emit('chat message', msg)
this.setState({
currMsg: '',
msgs: addMsgToGroup(msgs, msg, user),
})
}
}
partnerReadChatMessage(chatPartner) {
const { msgs } = this.state
this.setState({
msgs: {
...msgs,
[chatPartner]: msgs[chatPartner].map((msg) => {
if (msg.reciever === chatPartner) {
return {
...msg,
unread: 0,
}
} else {
return msg
}
}),
},
})
}
chatMessageRead(chatPartner) {
const { msgs, user } = this.state
if (this.props.refetchGlobalData) {
this.props.refetchGlobalData()
}
this.socket.emit('chat message read', { chatPartner: chatPartner })
this.setState({
msgs: {
...msgs,
[chatPartner]: msgs[chatPartner].map((msg) => {
if (msg.reciever === user) {
return {
...msg,
unread: 0,
}
} else {
return msg
}
}),
},
})
}
selectedUserChange(val) {
const { msgs, selectedUser, user } = this.state
const prevLastMessage = msgs[selectedUser]
? msgs[selectedUser][msgs[selectedUser].length - 1]
: null
if (
prevLastMessage &&
prevLastMessage.unread === 1 &&
prevLastMessage.sender !== user
) {
this.chatMessageRead(selectedUser)
}
this.setState({
selectedUser: val,
})
if (!val || isNaN(val)) {
return
}
const currSelectedMsgs = msgs[val]
if (!currSelectedMsgs) {
return
}
if (msgs[val].length <= 1) {
this.socket.emit('chat message open', {
chatPartner: val,
})
}
const lastMessage = currSelectedMsgs[currSelectedMsgs.length - 1]
if (lastMessage.unread === 1 && lastMessage.sender !== user) {
this.chatMessageRead(val)
}
}
renderChatInput() {
const { currMsg, msgs, selectedUser, user } = this.state
return (
<div className={styles.chatInput}>
<input
autoFocus
placeholder={'Message ...'}
type={'text'}
value={currMsg}
tabIndex={0}
onKeyUp={(e) => {
const lastMessage = msgs[selectedUser]
? msgs[selectedUser][msgs[selectedUser].length - 1]
: null
if (
lastMessage &&
lastMessage.unread === 1 &&
lastMessage.sender !== user
) {
this.chatMessageRead(selectedUser)
}
if (e.key === 'Enter') {
this.sendMsg()
}
}}
onChange={(e) => {
this.setState({
currMsg: e.target.value,
})
}}
/>
<div className={`buttonContainer ${styles.sendButton}`}>
<div
onClick={() => {
this.sendMsg()
}}
>
Send
</div>
</div>
</div>
)
}
renderHome() {
return (
<div className={styles.home}>
<div>
<input
autoFocus
type={'text'}
onChange={(e) => {
const val = parseInt(e.target.value)
if (!isNaN(val)) {
this.setState({
userInputVal: val,
})
}
}}
placeholder={'Címzett User ID-ja ...'}
/>
</div>
<div className={'buttonContainer'}>
<div
onClick={() => {
const { userInputVal } = this.state
if (isNaN(userInputVal) || userInputVal <= 0) {
alert('Érvényes User ID-t adj meg! (számot)')
return
}
this.setState({
selectedUser: userInputVal,
userInputVal: null,
})
}}
>
Chat!
</div>
</div>
<i>Admin User ID-ja: {'"1"'}</i>
</div>
)
}
renderMsgs() {
const { msgs, selectedUser } = this.state
let prevMsg
return msgs[selectedUser].reduce((acc, msg, i) => {
if (prevMsg && prevMsg.unread === 0 && msg.unread === 1) {
if (msg.sender === selectedUser) {
acc.push(<NewMarker key={`marker_${i}`} />)
} else {
acc.push(<SeenMarker key={`marker_${i}`} />)
}
}
acc.push(this.renderMsg(msg, i))
prevMsg = msg
if (i === msgs[selectedUser].length - 1 && msg.unread === 0) {
if (msg.sender !== selectedUser) {
acc.push(<SeenMarker key={`marker_${i}`} />)
}
}
return acc
}, [])
}
renderMsg(message, key) {
const { date, sender, msg /* reciever */ } = message
const { user } = this.state
const timeString = new Date(date).toLocaleString()
return (
<div
key={key}
title={timeString}
className={`${styles.messageContainer} ${
sender === user ? styles.ownMsg : styles.partnerMsg
}`}
>
<div className={`${styles.messageEntry}`}>{msg}</div>
</div>
)
}
renderSidebarEntryes() {
const { msgs } = this.state
const sorted = Object.keys(msgs).sort((a, b) => {
const lastA = msgs[a].slice(-1)[0]
const lastB = msgs[b].slice(-1)[0]
return lastB.date - lastA.date
})
return sorted.map((key, i) => {
return this.renderSidebarEntry(i, key)
})
}
renderSidebarEntry(i, key) {
const { selectedUser, msgs, user } = this.state
const group = msgs[key]
const lastMessage = group[group.length - 1]
return (
<div
className={`${styles.group} ${
lastMessage.unread === 1 && lastMessage.sender !== user
? styles.unread
: ''
} ${selectedUser === parseInt(key) ? styles.activeSidebarItem : ''}`}
onClick={() => {
this.selectedUserChange(parseInt(key))
}}
key={i}
>
<div>#{key}</div>
<div>{lastMessage.msg}</div>
</div>
)
}
render() {
const { user, msgs, connected, selectedUser } = this.state
return (
<div className={styles.chat}>
<Head>
<title>{`Connected as ${user}`}</title>
</Head>
{connected ? (
<>
<div className={styles.main}>
<div className={styles.header}>
{selectedUser ? `User #${selectedUser}` : ''}
</div>
<div className={styles.messages} id={'messages'}>
{selectedUser === 0 ? this.renderHome() : null}
{selectedUser && msgs[selectedUser] ? this.renderMsgs() : null}
</div>
{selectedUser !== 0 ? this.renderChatInput() : null}
</div>
<div className={styles.sidebar}>
<div className={styles.usersContainer}>
<div
className={`${styles.group} ${
selectedUser === 0 ? styles.activeSidebarItem : ''
}`}
onClick={() => {
this.selectedUserChange(0)
}}
>
<div>Új beszélgetés</div>
</div>
<hr />
{msgs && this.renderSidebarEntryes()}
</div>
<span className={styles.spacer} />
<div></div>
</div>
</>
) : (
<div>
<LoadingIndicator />
<input
onChange={(e) => {
this.setState({
user: parseInt(e.target.value),
})
}}
type={'text'}
placeholder={'user'}
/>
<div
onClick={() => {
this.connect(this.state.user)
}}
>
connect
</div>
</div>
)}
</div>
)
}
}