mirror of
https://gitlab.com/MrFry/qmining-page
synced 2025-04-01 20:23:44 +02:00
716 lines
18 KiB
JavaScript
716 lines
18 KiB
JavaScript
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 (
|
|
<span className={styles.unreadMarker} key={`unread_marker`}>
|
|
Látta
|
|
</span>
|
|
)
|
|
}
|
|
function NewMarker() {
|
|
return (
|
|
<span className={styles.newMarker} key={`unread_marker`}>
|
|
Új üzenet
|
|
</span>
|
|
)
|
|
}
|
|
|
|
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 (
|
|
<div className={styles.chatInput}>
|
|
<div>
|
|
<input
|
|
onChange={(e) => {
|
|
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' }}
|
|
/>
|
|
<label
|
|
className={styles.file}
|
|
htmlFor="actual-btn"
|
|
onClick={() => {}}
|
|
>
|
|
📂
|
|
</label>
|
|
</div>
|
|
<textarea
|
|
ref={this.chatInputRef}
|
|
className={styles.msgInput}
|
|
autoFocus
|
|
placeholder={'Üzenet ...'}
|
|
type={'text'}
|
|
value={currMsg}
|
|
tabIndex={0}
|
|
onKeyDown={(e) => {
|
|
const lastMessage = msgs[selectedUser]
|
|
? msgs[selectedUser].lastMessage
|
|
: null
|
|
if (
|
|
lastMessage &&
|
|
lastMessage.unread === 1 &&
|
|
lastMessage.sender !== user
|
|
) {
|
|
this.chatMessageSeen(selectedUser)
|
|
}
|
|
if (e.key === 'Enter') {
|
|
if (!e.shiftKey) {
|
|
this.sendMsg(currMsg)
|
|
e.preventDefault()
|
|
}
|
|
}
|
|
this.resizeInputBar()
|
|
}}
|
|
onChange={(e) => {
|
|
this.setState({
|
|
currMsg: e.target.value,
|
|
})
|
|
}}
|
|
/>
|
|
<div className={`buttonContainer ${styles.sendButton}`}>
|
|
<div
|
|
onClick={() => {
|
|
this.sendMsg(currMsg)
|
|
}}
|
|
>
|
|
Küldés
|
|
</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.selectedUserChange(userInputVal)
|
|
this.setState({
|
|
userInputVal: null,
|
|
})
|
|
}}
|
|
>
|
|
Chat!
|
|
</div>
|
|
</div>
|
|
<i style={{ fontSize: '12px' }}>Admin User ID-ja: {'"1"'}</i>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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 msgStyle = sender === user ? styles.ownMsg : styles.partnerMsg
|
|
const timeString = new Date(date).toLocaleString()
|
|
if (prevMsg && prevMsg.unread === 0 && unread === 1) {
|
|
if (sender === selectedUser) {
|
|
acc.push(<NewMarker key={`marker_${i}`} />)
|
|
} else if (prevMsg.sender !== selectedUser) {
|
|
acc.push(<SeenMarker key={`marker_${i}`} />)
|
|
}
|
|
}
|
|
acc.push(
|
|
<div
|
|
key={i}
|
|
title={timeString}
|
|
className={`${styles.messageContainer} ${
|
|
currMessage.type !== 'img' ? msgStyle : ''
|
|
}`}
|
|
>
|
|
{this.renderMsg(currMessage, i)}
|
|
</div>
|
|
)
|
|
if (i === selectedMsgs.length - 1 && unread === 0) {
|
|
if (sender !== selectedUser) {
|
|
acc.push(<SeenMarker key={`marker_${i}`} />)
|
|
}
|
|
}
|
|
prevMsg = currMessage
|
|
return acc
|
|
}, [])}
|
|
</>
|
|
)
|
|
}
|
|
|
|
renderMsg(message, key) {
|
|
const { msg, type } = message
|
|
|
|
if (type === 'text') {
|
|
return (
|
|
<div
|
|
className={`${styles.messageEntry}`}
|
|
dangerouslySetInnerHTML={{
|
|
__html: linkifyString(msg, {
|
|
defaultProtocol: 'https',
|
|
target: '_blank',
|
|
}),
|
|
}}
|
|
/>
|
|
)
|
|
} else if (type === 'img') {
|
|
return (
|
|
<a key={key} href={msg} rel="noreferrer" target="_blank">
|
|
<img
|
|
onLoad={() => {
|
|
if (!this.state.skipBottomScroll) {
|
|
this.scrollToChatBottom()
|
|
}
|
|
}}
|
|
src={`${constants.apiUrl}${msg}`}
|
|
/>
|
|
</a>
|
|
)
|
|
} else if (type === 'file') {
|
|
return (
|
|
<a
|
|
className={`${styles.messageEntry}`}
|
|
key={key}
|
|
href={`${constants.apiUrl}${msg}`}
|
|
rel="noreferrer"
|
|
target="_blank"
|
|
>
|
|
{msg.split('/').slice(-1)}
|
|
</a>
|
|
)
|
|
} else {
|
|
console.error(message)
|
|
return <div key={key}>Invalid msg type {type}</div>
|
|
}
|
|
}
|
|
|
|
renderLoadMore() {
|
|
const { selectedUser, msgs } = this.state
|
|
const group = msgs[selectedUser]
|
|
const firstMessage = group.msgs[0]
|
|
|
|
if (group.lastLoaded) {
|
|
return (
|
|
<div className={styles.loadMore}>
|
|
<div>Beszélgetés eleje</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={`${styles.loadMore} ${styles.loadMoreActive}`}
|
|
onClick={() => {
|
|
this.setState({ skipBottomScroll: true })
|
|
this.socket.emit('get chat messages', {
|
|
chatPartner: selectedUser,
|
|
from: firstMessage.date,
|
|
})
|
|
}}
|
|
>
|
|
<div>Több betöltése ...</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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 (
|
|
<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>
|
|
<b>#{key}</b>
|
|
<div style={{ fontSize: '12px' }}>{date}</div>
|
|
</div>
|
|
<div>
|
|
{lastMessage.type === 'text' ? lastMessage.msg : 'Csatolt fájl'}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
renderSidebarEntries() {
|
|
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 (
|
|
<div className={styles.chat}>
|
|
<Header title={'Chat'} />
|
|
{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.renderSidebarEntries()}
|
|
</div>
|
|
<span className={styles.spacer} />
|
|
<div></div>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className={styles.loading}>
|
|
<LoadingIndicator />
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
}
|