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' const byDate = (a, b) => { return a.date - b.date } function countAllMessages(msgs) { return Object.keys(msgs).reduce((acc, key) => { return acc + msgs[key].length }, 0) } function groupPrevMessages(msgs, currUser) { return msgs.reduce((acc, msg) => { const group = parseInt(msg.sender) !== parseInt(currUser) ? msg.sender : msg.reciever return { ...acc, [group]: { msgs: [msg], loaded: false, lastLoaded: false, lastMessage: msg, }, } }, {}) } function addMsgsToGroup(msgGroup, msgs, user) { let res = { ...msgGroup } msgs.reverse().forEach((msg) => { res = addMsgToGroup(res, 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] = { msgs: [], lastLoaded: true, loaded: true } } const currGroup = msgGroup[group] return { ...msgGroup, [group]: { ...currGroup, loaded: true, msgs: currGroup.loaded ? [...currGroup.msgs, msg].sort(byDate) : [msg].sort(byDate), lastMessage: !currGroup.lastMessage || msg.date > currGroup.lastMessage.date ? msg : currGroup.lastMessage, }, } } function SeenMarker() { return ( Látta ) } function NewMarker() { return ( Új üzenet ) } function uploadFile(file) { return new Promise((resolve) => { const formData = new FormData() // eslint-disable-line formData.append('file', file) fetch(constants.apiUrl + 'postchatfile', { method: 'POST', credentials: 'include', headers: { Accept: 'application/json', }, body: formData, }) .then((res) => { return res.json() }) .then((res) => { resolve(res) }) }) } 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) } this.router = props.router } componentDidUpdate(prevProps, prevState) { try { const user = this.props.router.query.user if (this.state.userFromQuery !== user) { if (isNaN(user)) { this.setState({ userFromQuery: user, }) } else { this.setState({ selectedUser: user, userFromQuery: user, }) } } } catch (e) { // e } 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) { this.setState({ connected: false, }) console.error(err) alert(`Chat error: ${err.message}`) try { this.socket.disconnect() } catch (e) { console.warn(e) } } 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: groupPrevMessages(prevMsgs, user), connected: true, }) }) socket.on('chat message read', (data) => { const { userReadMsg } = data this.partnerSeenChatMessage(userReadMsg) }) socket.on('get chat messages', (data) => { const { requestsdMsgs, hasMore } = data const { msgs, user, selectedUser } = this.state this.setState({ msgs: addMsgsToGroup( Object.keys(msgs).reduce((acc, key) => { const msgGroup = msgs[key] if (parseInt(key) === selectedUser) { acc[key] = { ...msgGroup, lastLoaded: !hasMore, msgs: msgGroup.msgs.map((msg) => { return { ...msg, unread: 0, } }), } } else { acc[key] = msgGroup } return acc }, {}), requestsdMsgs.map((msg) => { return { ...msg, isFirstMessage: !hasMore, } }), user ), }) }) socket.on('chat message', (data) => { const { msgs, user } = this.state this.setState({ msgs: addMsgToGroup(msgs, data, user), }) }) this.socket = socket } sendMsg(currMsg, type) { const { msgs, selectedUser, user } = this.state if (!currMsg) { return } if (this.socket && selectedUser) { const msg = { msg: currMsg, reciever: selectedUser, sender: user, date: new Date().getTime(), unread: 1, type: type || 'text', } this.socket.emit('chat message', msg) this.setState({ currMsg: '', msgs: addMsgToGroup(msgs, msg, user), }) } } partnerSeenChatMessage(chatPartner) { const { msgs } = this.state this.setState({ msgs: { ...msgs, [chatPartner]: { ...msgs[chatPartner], lastMessage: { ...msgs[chatPartner].lastMessage, unread: 0 }, msgs: msgs[chatPartner].msgs.map((msg) => { if (msg.reciever === chatPartner) { return { ...msg, unread: 0, } } else { return msg } }), }, }, }) this.scrollToChatBottom() } chatMessageSeen(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], lastMessage: { ...msgs[chatPartner].lastMessage, unread: 0 }, msgs: msgs[chatPartner].msgs.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].lastMessage : null if ( prevLastMessage && prevLastMessage.unread === 1 && prevLastMessage.sender !== user ) { this.chatMessageSeen(selectedUser) } this.setState({ selectedUser: val, }) if (!val || isNaN(val)) { return } const currSelectedMsgs = msgs[val] if (!currSelectedMsgs) { return } if (!msgs[val].loaded) { this.socket.emit('get chat messages', { chatPartner: val, }) } const lastMessage = currSelectedMsgs.lastMessage if (lastMessage.unread === 1 && lastMessage.sender !== user) { this.chatMessageSeen(val) } try { this.router.push(`${this.router.pathname}`, undefined, { shallow: true, }) } catch (e) { // e } } renderChatInput() { const { currMsg, msgs, selectedUser, user } = this.state return (
{ const file = e.target.files[0] const isImage = ['png', 'jpg', 'jpeg', 'gif'].some((ext) => { return file.name.toLowerCase().includes(ext) }) uploadFile(file).then((res) => { const { path, success } = res if (success) { this.sendMsg( `${constants.apiUrl}${path}`, isImage ? 'img' : 'file' ) } else { alert('Error uploading image :/') console.error(res) } }) }} type="file" id="actual-btn" style={{ display: 'none' }} />
{ const lastMessage = msgs[selectedUser] ? msgs[selectedUser].lastMessage : null if ( lastMessage && lastMessage.unread === 1 && lastMessage.sender !== user ) { this.chatMessageSeen(selectedUser) } if (e.key === 'Enter') { this.sendMsg(currMsg) } }} onChange={(e) => { this.setState({ currMsg: e.target.value, }) }} />
{ this.sendMsg(currMsg) }} > Küldés
) } renderHome() { return (
{ const val = parseInt(e.target.value) if (!isNaN(val)) { this.setState({ userInputVal: val, }) } }} placeholder={'Címzett User ID-ja ...'} />
{ const { userInputVal } = this.state if (isNaN(userInputVal) || userInputVal <= 0) { alert('Érvényes User ID-t adj meg! (számot)') return } this.selectedUserChange(userInputVal) this.setState({ userInputVal: null, }) }} > Chat!
Admin User ID-ja: {'"1"'}
) } renderMsgs() { const { msgs, selectedUser, user } = this.state const selectedMsgs = msgs[selectedUser].msgs let prevMsg return ( <> {this.renderLoadMore()} {selectedMsgs.reduce((acc, currMessage, i) => { const { date, sender, unread } = currMessage const timeString = new Date(date).toLocaleString() if (prevMsg && prevMsg.unread === 0 && unread === 1) { if (sender === selectedUser) { acc.push() } else if (prevMsg.sender !== selectedUser) { acc.push() } } acc.push(
{this.renderMsg(currMessage, i)}
) if (i === selectedMsgs.length - 1 && unread === 0) { if (sender !== selectedUser) { acc.push() } } prevMsg = currMessage return acc }, [])} ) } renderMsg(message, key) { const { msg, type } = message if (type === 'text') { return
{msg}
} else if (type === 'img') { return ( ) } else if (type === 'file') { return ( {msg.split('/').slice(-1)} ) } else { console.error(message) return
Invalid msg type {type}
} } renderLoadMore() { const { selectedUser, msgs } = this.state const group = msgs[selectedUser] const firstMessage = group.msgs[0] if (group.lastLoaded) { return (
Beszélgetés eleje
) } return (
{ this.socket.emit('get chat messages', { chatPartner: selectedUser, from: firstMessage.date, }) }} >
Több betöltése ...
) } renderSidebarEntry(i, key) { const { selectedUser, msgs, user } = this.state const group = msgs[key] const lastMessage = group.lastMessage const lastMsgDate = new Date(group.lastMessage.date) const date = lastMsgDate.getDate() === new Date().getDate() ? lastMsgDate.toLocaleTimeString() : lastMsgDate.toLocaleDateString() return (
{ this.selectedUserChange(parseInt(key)) }} key={i} >
#{key}
{date}
{lastMessage.type === 'text' ? lastMessage.msg : 'Csatolt fájl'}
) } renderSidebarEntryes() { const { msgs } = this.state const sorted = Object.keys(msgs).sort((a, b) => { return msgs[b].lastMessage.date - msgs[a].lastMessage.date }) return sorted.map((key, i) => { return this.renderSidebarEntry(i, key) }) } render() { const { msgs, connected, selectedUser } = this.state return (
Chat - Qmining | Frylabs.net {connected ? ( <>
{selectedUser ? `User #${selectedUser}` : ''}
{selectedUser === 0 ? this.renderHome() : null} {selectedUser && msgs[selectedUser] ? this.renderMsgs() : null}
{selectedUser !== 0 ? this.renderChatInput() : null}
{ this.selectedUserChange(0) }} >
Új beszélgetés

{msgs && this.renderSidebarEntryes()}
) : (
)}
) } }