diff --git a/src/pages/chat.js b/src/pages/chat.js new file mode 100644 index 0000000..623b1b8 --- /dev/null +++ b/src/pages/chat.js @@ -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 ( + + Látta + + ) +} +function NewMarker() { + return ( + + Új üzenet + + ) +} + +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 ( +
+ { + 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, + }) + }} + /> +
+
{ + this.sendMsg() + }} + > + Send +
+
+
+ ) + } + + 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.setState({ + selectedUser: userInputVal, + userInputVal: null, + }) + }} + > + Chat! +
+
+ Admin User ID-ja: {'"1"'} +
+ ) + } + + 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() + } else { + acc.push() + } + } + acc.push(this.renderMsg(msg, i)) + prevMsg = msg + if (i === msgs[selectedUser].length - 1 && msg.unread === 0) { + if (msg.sender !== selectedUser) { + acc.push() + } + } + return acc + }, []) + } + + renderMsg(message, key) { + const { date, sender, msg /* reciever */ } = message + const { user } = this.state + const timeString = new Date(date).toLocaleString() + + return ( +
+
{msg}
+
+ ) + } + + 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 ( +
{ + this.selectedUserChange(parseInt(key)) + }} + key={i} + > +
#{key}
+
{lastMessage.msg}
+
+ ) + } + + render() { + const { user, msgs, connected, selectedUser } = this.state + + return ( +
+ + {`Connected as ${user}`} + + {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()} +
+ +
+
+ + ) : ( +
+ + { + this.setState({ + user: parseInt(e.target.value), + }) + }} + type={'text'} + placeholder={'user'} + /> +
{ + this.connect(this.state.user) + }} + > + connect +
+
+ )} +
+ ) + } +} diff --git a/src/pages/chat.module.css b/src/pages/chat.module.css new file mode 100644 index 0000000..e267084 --- /dev/null +++ b/src/pages/chat.module.css @@ -0,0 +1,172 @@ +.chat { + display: flex; + align-items: stretch; + height: 100%; +} + +.main { + display: flex; + flex-flow: column; + flex: 1 0; +} + +.sidebar { + display: flex; + flex-direction: column; + + width: calc(20vw); + z-index: 1; + right: 0; + overflow-x: hidden; +} + +.sidebar > div:last-child { + text-align: center; + cursor: pointer; +} + +.usersContainer { + padding: 0px 5px; + user-select: none; +} + +.usersContainer > div { + cursor: pointer; +} + +.spacer { + flex: 1 1; +} + +.new { + display: flex; + flex-direction: column; + align-self: center; + margin: 5px; + width: 140px; + text-align: center; +} + +.new > input { + text-align: center; +} + +.group { + padding: 15px; + cursor: pointer; + user-select: none; + + transition: width 0.5s, height 0.5s, ease-in 0.5s; +} + +.group > div { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.group:hover { + background-color: var(--hoover-color); +} + +.activeSidebarItem { + color: black; + background-color: var(--primary-color); +} + +.activeSidebarItem:hover { + background-color: var(--primary-color); +} + +.unread { + color: var(--text-color); +} + +.header { + display: flex; + justify-content: center; + + color: var(--text-color); + display: flex; + align-items: center; + padding: 10px 0px; +} + +.messages { + display: flex; + flex-flow: column; + word-break: break-all; + padding: 5px 0px; + + overflow-x: none; + overflow-y: auto; + flex: 1 0 200px; +} + +.chatInput { + display: flex; + align-items: center; + padding: 4px 0px; +} + +.chatInput > input { + padding: 2px; + height: 30px; +} + +.messageContainer { + display: flex; + + margin: 2px 10px; + max-width: 300px; +} + +.ownMsg { + align-self: flex-end; + justify-content: flex-end; +} + +.ownMsg > div { + background-color: #99f; +} + +.partnerMsg > div { + background-color: var(--primary-color); +} + +.unreadMarker { + align-self: flex-end; + justify-content: flex-end; + font-size: 14px; + padding: 0px 5px; +} + +.newMarker { + font-size: 14px; + padding: 0px 5px; +} + +.messageEntry { + padding: 5px 8px; + border-radius: 4px; + color: black; +} + +.sendButton { + width: 90px; +} + +.home { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + width: 100%; + height: 100%; +} + +.home input { + text-align: center; + width: 300px; +}