import React from 'react' import io from 'socket.io-client' import linkifyString from 'linkify-string' import constants from '../constants' import LoadingIndicator from '../components/LoadingIndicator' import { queryClient } from '../pages/_app' import Header from '../components/header' import styles from './chat.module.css' const byDate = (a, b) => { return a.date - b.date } 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.globalData && !isNaN(props.globalData.userId)) { this.state.user = props.globalData.userId this.connect(this.props.globalData.userId) } this.router = props.router this.chatInputRef = React.createRef() } 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 ( prevState.msgs && prevState.msgs[prevState.selectedUser] && this.state.msgs && this.state.msgs[this.state.selectedUser] ) { const prevLatest = prevState.msgs[prevState.selectedUser].lastMessage const newLatest = this.state.msgs[this.state.selectedUser].lastMessage if (prevLatest && newLatest && prevLatest.date !== newLatest.date) { this.scrollToChatBottom() } if (prevState.msgs[prevState.selectedUser].msgs.length === 1) { this.scrollToChatBottom() } } } componentWillUnmount() { console.info('Chat disconnect') if (this.socket) { this.socket.disconnect() } } resizeInputBar() { if (this.chatInputRef.current) { const input = this.chatInputRef.current input.style.height = input.scrollHeight + 'px' } } 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.chatUrl}`, { 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 }), } } 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) { this.chatInputRef.current.style.height = '40px' const { msgs, selectedUser, user } = this.state if (!currMsg || !currMsg.trim()) { return } if (this.socket && selectedUser) { const msg = { msg: currMsg.trim(), 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 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 } queryClient.setQueryData('infos', (oldData) => ({ ...oldData, unreads: oldData.unreads.filter((x) => x !== val), })) } 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(`${path}`, isImage ? 'img' : 'file') } else { alert('Error uploading image :/') console.error(res) } }) }} type="file" id="actual-btn" style={{ display: 'none' }} />