mirror of
https://gitlab.com/MrFry/qmining-page
synced 2025-04-01 20:23:44 +02:00
Chat
This commit is contained in:
parent
c60ace4f9b
commit
602e16046e
18 changed files with 512 additions and 174 deletions
|
@ -6,31 +6,36 @@ 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 groupMessages(msgs, currUser) {
|
||||
function groupPrevMessages(msgs, currUser) {
|
||||
return msgs.reduce((acc, msg) => {
|
||||
const group =
|
||||
parseInt(msg.sender) !== parseInt(currUser) ? msg.sender : msg.reciever
|
||||
return {
|
||||
...acc,
|
||||
[group]: [msg],
|
||||
[group]: {
|
||||
msgs: [msg],
|
||||
loaded: false,
|
||||
lastLoaded: false,
|
||||
lastMessage: msg,
|
||||
},
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
||||
function addMsgsToGroup(msgGroup, msgs, user) {
|
||||
let res = { ...msgGroup }
|
||||
msgs.forEach((msg) => {
|
||||
res = addMsgToGroup(
|
||||
res,
|
||||
msg.reciever === user ? { ...msg, unread: 0 } : msg,
|
||||
user
|
||||
)
|
||||
msgs.reverse().forEach((msg) => {
|
||||
res = addMsgToGroup(res, msg, user)
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
@ -39,11 +44,24 @@ function addMsgToGroup(msgGroup, msg, user) {
|
|||
const group =
|
||||
parseInt(msg.sender) === parseInt(user) ? msg.reciever : msg.sender
|
||||
if (!msgGroup[group]) {
|
||||
msgGroup[group] = []
|
||||
msgGroup[group] = { msgs: [], lastLoaded: true, loaded: true }
|
||||
}
|
||||
|
||||
const currGroup = msgGroup[group]
|
||||
|
||||
return {
|
||||
...msgGroup,
|
||||
[group]: [...msgGroup[group], msg],
|
||||
[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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,6 +80,28 @@ function NewMarker() {
|
|||
)
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -77,16 +117,35 @@ export default class Chat extends React.Component {
|
|||
}
|
||||
if (props.globalData && !isNaN(props.globalData.userId)) {
|
||||
this.state.user = props.globalData.userId
|
||||
// this.connect(this.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)
|
||||
this.connect(this.props.globalData.userId)
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -112,8 +171,16 @@ export default class Chat extends React.Component {
|
|||
}
|
||||
|
||||
handleErrors(err) {
|
||||
alert(err.message)
|
||||
this.setState({
|
||||
connected: false,
|
||||
})
|
||||
console.error(err)
|
||||
alert(`Chat error: ${err.message}`)
|
||||
try {
|
||||
this.socket.disconnect()
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
|
||||
connect(user) {
|
||||
|
@ -141,23 +208,49 @@ export default class Chat extends React.Component {
|
|||
const { prevMsgs } = data
|
||||
const { user } = this.state
|
||||
this.setState({
|
||||
msgs: groupMessages(prevMsgs, user),
|
||||
msgs: groupPrevMessages(prevMsgs, user),
|
||||
connected: true,
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('chat message read', (data) => {
|
||||
const { userReadMsg } = data
|
||||
this.partnerReadChatMessage(userReadMsg)
|
||||
this.partnerSeenChatMessage(userReadMsg)
|
||||
})
|
||||
|
||||
socket.on('chat message open', (data) => {
|
||||
socket.on('get chat messages', (data) => {
|
||||
const { requestsdMsgs, hasMore } = data
|
||||
const { msgs, user, selectedUser } = this.state
|
||||
if (msgs[selectedUser].length <= 1) {
|
||||
this.setState({
|
||||
msgs: addMsgsToGroup(msgs, data, user),
|
||||
})
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
@ -170,8 +263,8 @@ export default class Chat extends React.Component {
|
|||
this.socket = socket
|
||||
}
|
||||
|
||||
sendMsg() {
|
||||
const { msgs, selectedUser, currMsg, user } = this.state
|
||||
sendMsg(currMsg, type) {
|
||||
const { msgs, selectedUser, user } = this.state
|
||||
if (!currMsg) {
|
||||
return
|
||||
}
|
||||
|
@ -182,6 +275,7 @@ export default class Chat extends React.Component {
|
|||
sender: user,
|
||||
date: new Date().getTime(),
|
||||
unread: 1,
|
||||
type: type || 'text',
|
||||
}
|
||||
this.socket.emit('chat message', msg)
|
||||
this.setState({
|
||||
|
@ -191,26 +285,31 @@ export default class Chat extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
partnerReadChatMessage(chatPartner) {
|
||||
partnerSeenChatMessage(chatPartner) {
|
||||
const { msgs } = this.state
|
||||
this.setState({
|
||||
msgs: {
|
||||
...msgs,
|
||||
[chatPartner]: msgs[chatPartner].map((msg) => {
|
||||
if (msg.reciever === chatPartner) {
|
||||
return {
|
||||
...msg,
|
||||
unread: 0,
|
||||
[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
|
||||
}
|
||||
} else {
|
||||
return msg
|
||||
}
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
this.scrollToChatBottom()
|
||||
}
|
||||
|
||||
chatMessageRead(chatPartner) {
|
||||
chatMessageSeen(chatPartner) {
|
||||
const { msgs, user } = this.state
|
||||
if (this.props.refetchGlobalData) {
|
||||
this.props.refetchGlobalData()
|
||||
|
@ -219,16 +318,20 @@ export default class Chat extends React.Component {
|
|||
this.setState({
|
||||
msgs: {
|
||||
...msgs,
|
||||
[chatPartner]: msgs[chatPartner].map((msg) => {
|
||||
if (msg.reciever === user) {
|
||||
return {
|
||||
...msg,
|
||||
unread: 0,
|
||||
[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
|
||||
}
|
||||
} else {
|
||||
return msg
|
||||
}
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -236,14 +339,14 @@ export default class Chat extends React.Component {
|
|||
selectedUserChange(val) {
|
||||
const { msgs, selectedUser, user } = this.state
|
||||
const prevLastMessage = msgs[selectedUser]
|
||||
? msgs[selectedUser][msgs[selectedUser].length - 1]
|
||||
? msgs[selectedUser].lastMessage
|
||||
: null
|
||||
if (
|
||||
prevLastMessage &&
|
||||
prevLastMessage.unread === 1 &&
|
||||
prevLastMessage.sender !== user
|
||||
) {
|
||||
this.chatMessageRead(selectedUser)
|
||||
this.chatMessageSeen(selectedUser)
|
||||
}
|
||||
this.setState({
|
||||
selectedUser: val,
|
||||
|
@ -255,14 +358,21 @@ export default class Chat extends React.Component {
|
|||
if (!currSelectedMsgs) {
|
||||
return
|
||||
}
|
||||
if (msgs[val].length <= 1) {
|
||||
this.socket.emit('chat message open', {
|
||||
if (!msgs[val].loaded) {
|
||||
this.socket.emit('get chat messages', {
|
||||
chatPartner: val,
|
||||
})
|
||||
}
|
||||
const lastMessage = currSelectedMsgs[currSelectedMsgs.length - 1]
|
||||
const lastMessage = currSelectedMsgs.lastMessage
|
||||
if (lastMessage.unread === 1 && lastMessage.sender !== user) {
|
||||
this.chatMessageRead(val)
|
||||
this.chatMessageSeen(val)
|
||||
}
|
||||
try {
|
||||
this.router.push(`${this.router.pathname}`, undefined, {
|
||||
shallow: true,
|
||||
})
|
||||
} catch (e) {
|
||||
// e
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -270,6 +380,38 @@ export default class Chat extends React.Component {
|
|||
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(
|
||||
`${constants.apiUrl}${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>
|
||||
<input
|
||||
autoFocus
|
||||
placeholder={'Message ...'}
|
||||
|
@ -278,17 +420,17 @@ export default class Chat extends React.Component {
|
|||
tabIndex={0}
|
||||
onKeyUp={(e) => {
|
||||
const lastMessage = msgs[selectedUser]
|
||||
? msgs[selectedUser][msgs[selectedUser].length - 1]
|
||||
? msgs[selectedUser].lastMessage
|
||||
: null
|
||||
if (
|
||||
lastMessage &&
|
||||
lastMessage.unread === 1 &&
|
||||
lastMessage.sender !== user
|
||||
) {
|
||||
this.chatMessageRead(selectedUser)
|
||||
this.chatMessageSeen(selectedUser)
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
this.sendMsg()
|
||||
this.sendMsg(currMsg)
|
||||
}
|
||||
}}
|
||||
onChange={(e) => {
|
||||
|
@ -300,10 +442,10 @@ export default class Chat extends React.Component {
|
|||
<div className={`buttonContainer ${styles.sendButton}`}>
|
||||
<div
|
||||
onClick={() => {
|
||||
this.sendMsg()
|
||||
this.sendMsg(currMsg)
|
||||
}}
|
||||
>
|
||||
Send
|
||||
Küldés
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -336,8 +478,8 @@ export default class Chat extends React.Component {
|
|||
alert('Érvényes User ID-t adj meg! (számot)')
|
||||
return
|
||||
}
|
||||
this.selectedUserChange(userInputVal)
|
||||
this.setState({
|
||||
selectedUser: userInputVal,
|
||||
userInputVal: null,
|
||||
})
|
||||
}}
|
||||
|
@ -345,68 +487,113 @@ export default class Chat extends React.Component {
|
|||
Chat!
|
||||
</div>
|
||||
</div>
|
||||
<i>Admin User ID-ja: {'"1"'}</i>
|
||||
<i style={{ fontSize: '12px' }}>Admin User ID-ja: {'"1"'}</i>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderMsgs() {
|
||||
const { msgs, selectedUser } = this.state
|
||||
const { msgs, selectedUser, user } = this.state
|
||||
|
||||
const selectedMsgs = msgs[selectedUser].msgs
|
||||
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>
|
||||
<>
|
||||
{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(<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} ${
|
||||
sender === user ? styles.ownMsg : styles.partnerMsg
|
||||
}`}
|
||||
>
|
||||
{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
|
||||
}, [])}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
renderMsg(message, key) {
|
||||
const { msg, type } = message
|
||||
|
||||
if (type === 'text') {
|
||||
return <div className={`${styles.messageEntry}`}>{msg}</div>
|
||||
} else if (type === 'img') {
|
||||
return (
|
||||
<a key={key} href={msg} rel="noreferrer" target="_blank">
|
||||
<img src={msg} />
|
||||
</a>
|
||||
)
|
||||
} else if (type === 'file') {
|
||||
return (
|
||||
<a key={key} href={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.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[group.length - 1]
|
||||
const lastMessage = group.lastMessage
|
||||
|
||||
const lastMsgDate = new Date(group.lastMessage.date)
|
||||
const date =
|
||||
lastMsgDate.getDate() === new Date().getDate()
|
||||
? lastMsgDate.toLocaleTimeString()
|
||||
: lastMsgDate.toLocaleDateString()
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -420,19 +607,34 @@ export default class Chat extends React.Component {
|
|||
}}
|
||||
key={i}
|
||||
>
|
||||
<div>#{key}</div>
|
||||
<div>{lastMessage.msg}</div>
|
||||
<div>
|
||||
<b>#{key}</b>
|
||||
<div style={{ fontSize: '12px' }}>{date}</div>
|
||||
</div>
|
||||
<div>
|
||||
{lastMessage.type === 'text' ? lastMessage.msg : 'Csatolt fájl'}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 { user, msgs, connected, selectedUser } = this.state
|
||||
const { msgs, connected, selectedUser } = this.state
|
||||
|
||||
return (
|
||||
<div className={styles.chat}>
|
||||
<Head>
|
||||
<title>{`Connected as ${user}`}</title>
|
||||
<title>Chat - Qmining | Frylabs.net</title>
|
||||
</Head>
|
||||
{connected ? (
|
||||
<>
|
||||
|
@ -466,24 +668,8 @@ export default class Chat extends React.Component {
|
|||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<div className={styles.loading}>
|
||||
<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