mirror of
https://gitlab.com/MrFry/qmining-page
synced 2025-04-01 20:23:44 +02:00
Added chat page
This commit is contained in:
parent
3a67f2a1aa
commit
c60ace4f9b
2 changed files with 664 additions and 0 deletions
492
src/pages/chat.js
Normal file
492
src/pages/chat.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue