improve livechat

This commit is contained in:
SrGooglo 2025-04-02 01:52:02 +00:00
parent bfd4e28d01
commit b43aa731ca
2 changed files with 399 additions and 372 deletions

View File

@ -8,327 +8,353 @@ import SessionModel from "@models/session"
import "./index.less" import "./index.less"
const Line = (props) => { const Line = (props) => {
const { user, content } = props const { user, content } = props
return <div className="textRoom_line"> return (
<div className="textRoom_line_user"> <div className="textRoom_line">
<h4>{user.fullName ?? user.username}</h4> <div className="textRoom_line_user">
</div> <h4>{user.fullName ?? user.username}</h4>
<div className="textRoom_line_content"> </div>
<span>{content}</span> <div className="textRoom_line_content">
</div> <span>{content}</span>
</div> </div>
</div>
)
} }
export default class LiveChat extends React.Component { export default class LiveChat extends React.Component {
state = { state = {
joining: true, joining: true,
roomInfo: null, roomInfo: null,
timeline: [], timeline: [],
temporalTimeline: [], temporalTimeline: [],
maxTemporalLines: this.props.maxTemporalLines ?? 10, maxTemporalLines: this.props.maxTemporalLines ?? 10,
lastSentMessage: null, lastSentMessage: null,
writtedMessage: "", writtedMessage: "",
} }
debouncedIntervalTimelinePurge = null debouncedIntervalTimelinePurge = null
timelineRef = React.createRef() timelineRef = React.createRef()
socket = app.cores.api.client().sockets.chats socket = app.cores.api.client().ws.sockets.get("chats")
roomEvents = { roomEvents = {
"room:message": (message) => { "room:message": (message) => {
if (message.content === this.state.lastSentMessage) { if (message.content === this.state.lastSentMessage) {
console.timeEnd("[CHATROOM] SUBMIT:MESSAGE") console.timeEnd("[CHATROOM] SUBMIT:MESSAGE")
} }
this.pushToTimeline(message) this.pushToTimeline(message)
}, },
"room:joined": (info) => { "room:joined": (info) => {
console.log("[CHATROOM] Room joined", info) console.log("[CHATROOM] Room joined", info)
this.setState({ this.setState({
joining: false, joining: false,
roomInfo: info, roomInfo: info,
}) })
}, },
"room:leave": (info) => { "room:leave": (info) => {
console.log("[CHATROOM] Room left", info) console.log("[CHATROOM] Room left", info)
this.setState({ this.setState({
joining: true, joining: true,
roomInfo: null, roomInfo: null,
}) })
} },
} }
joinSocketRoom = async () => { joinSocketRoom = async () => {
if (!SessionModel.token) { if (!SessionModel.token) {
return this.setState({ return this.setState({
noAuthed: true, noAuthed: true,
}) })
} }
console.log(`[CHATROOM] Joining socket room [${this.props.id}]...`)
for (const [eventName, eventHandler] of Object.entries(this.roomEvents)) { console.log(`[CHATROOM] Joining socket room [${this.props.id}]...`)
this.socket.on(eventName, eventHandler)
}
await this.setState({ for (const [eventName, eventHandler] of Object.entries(
joining: true, this.roomEvents,
}) )) {
this.socket.on(eventName, eventHandler)
}
this.socket.emit( await this.setState({
"join:room", joining: true,
{ })
room: this.props.id,
},
(error, info) => {
if (error) {
this.setState({ connectionEnd: true })
return console.error("Error joining room", error) this.socket.emit(
} "join:room",
} {
) room: this.props.id,
} },
(error, info) => {
if (error) {
this.setState({ connectionEnd: true })
leaveSocketRoom = () => { return console.error("Error joining room", error)
if (this.state.connectionEnd) { }
return false },
} )
}
console.log(`[CHATROOM] Leaving socket room [${this.props.id}]...`)
for (const [eventName, eventHandler] of Object.entries(this.roomEvents)) { leaveSocketRoom = () => {
this.socket.off(eventName, eventHandler) if (this.state.connectionEnd) {
} return false
}
this.socket.emit("leave:room") console.log(`[CHATROOM] Leaving socket room [${this.props.id}]...`)
}
submitMessage = (message) => { for (const [eventName, eventHandler] of Object.entries(
console.time("[CHATROOM] SUBMIT:MESSAGE") this.roomEvents,
)) {
this.socket.off(eventName, eventHandler)
}
this.socket.emit("room:send:message", { this.socket.emit("leave:room")
message }
})
// remove writted message submitMessage = (message) => {
this.setState({ console.time("[CHATROOM] SUBMIT:MESSAGE")
lastSentMessage: message,
writtedMessage: ""
})
}
pushToTimeline = (message) => { this.socket.emit("room:send:message", {
const { timeline } = this.state message,
})
if (typeof message.key === "undefined") { // remove writted message
message.key = this.state.timeline.length this.setState({
} lastSentMessage: message,
writtedMessage: "",
})
}
this.setState({ pushToTimeline = (message) => {
timeline: [...timeline, message] const { timeline } = this.state
})
if (this.props.floatingMode) { if (typeof message.key === "undefined") {
if (this.state.temporalTimeline.length >= this.state.maxTemporalLines) { message.key = this.state.timeline.length
this.setState({ }
temporalTimeline: this.state.temporalTimeline.slice(1)
})
}
// calculate duration based on message length (Minimum 3 second, maximum 10 seconds) this.setState({
const calculatedDuration = Math.min(Math.max(message.content.length * 0.1, 3), 10) * 1000 timeline: [...timeline, message],
})
const temporalLine = { if (this.props.floatingMode) {
expireTime: Date.now() + calculatedDuration, if (
duration: calculatedDuration, this.state.temporalTimeline.length >=
messageKey: message.key this.state.maxTemporalLines
} ) {
this.setState({
temporalTimeline: this.state.temporalTimeline.slice(1),
})
}
this.setState({ // calculate duration based on message length (Minimum 3 second, maximum 10 seconds)
temporalTimeline: [...this.state.temporalTimeline, temporalLine] const calculatedDuration =
}) Math.min(Math.max(message.content.length * 0.1, 3), 10) * 1000
if (this.debouncedIntervalTimelinePurge) { const temporalLine = {
clearInterval(this.debouncedIntervalTimelinePurge) expireTime: Date.now() + calculatedDuration,
} duration: calculatedDuration,
messageKey: message.key,
}
this.debouncedIntervalTimelinePurge = setInterval(this.purgeLastTemporalLine, 3000) this.setState({
} temporalTimeline: [
...this.state.temporalTimeline,
temporalLine,
],
})
this.scrollTimelineToBottom() if (this.debouncedIntervalTimelinePurge) {
} clearInterval(this.debouncedIntervalTimelinePurge)
}
purgeLastTemporalLine = () => { this.debouncedIntervalTimelinePurge = setInterval(
if (!this.props.floatingMode) { this.purgeLastTemporalLine,
return false 3000,
} )
}
const { temporalTimeline } = this.state this.scrollTimelineToBottom()
}
if (temporalTimeline.length === 0) { purgeLastTemporalLine = () => {
clearInterval(this.debouncedIntervalTimelinePurge) if (!this.props.floatingMode) {
return false return false
} }
const lastTemporalLine = temporalTimeline[0] const { temporalTimeline } = this.state
if (lastTemporalLine.expireTime < Date.now()) { if (temporalTimeline.length === 0) {
this.setState({ clearInterval(this.debouncedIntervalTimelinePurge)
temporalTimeline: temporalTimeline.slice(1) return false
}) }
}
}
handleInputChange = (e) => { const lastTemporalLine = temporalTimeline[0]
if (e.target.value[0] === " " || e.target.value[0] === "\n") {
e.target.value = e.target.value.slice(1)
}
this.setState({ if (lastTemporalLine.expireTime < Date.now()) {
writtedMessage: e.target.value this.setState({
}) temporalTimeline: temporalTimeline.slice(1),
} })
}
}
handleOnEnter = (e) => { handleInputChange = (e) => {
e.preventDefault() if (e.target.value[0] === " " || e.target.value[0] === "\n") {
e.stopPropagation() e.target.value = e.target.value.slice(1)
}
if (e.target.value.length === 0) { this.setState({
return writtedMessage: e.target.value,
} })
}
this.submitMessage(e.target.value) handleOnEnter = (e) => {
} e.preventDefault()
e.stopPropagation()
scrollTimelineToBottom = () => { if (e.target.value.length === 0) {
const scrollingElement = document.getElementById("liveChat_timeline") return
}
if (scrollingElement) { this.submitMessage(e.target.value)
scrollingElement.scrollTo({ }
top: scrollingElement.scrollHeight,
behavior: "smooth"
})
}
}
componentDidMount = async () => { scrollTimelineToBottom = () => {
this.joinSocketRoom() const scrollingElement = document.getElementById("liveChat_timeline")
app.ctx = { if (scrollingElement) {
submit: this.submitMessage scrollingElement.scrollTo({
} top: scrollingElement.scrollHeight,
} behavior: "smooth",
})
}
}
componentWillUnmount() { componentDidMount = async () => {
this.leaveSocketRoom() this.joinSocketRoom()
if (this.debouncedIntervalTimelinePurge) {
clearInterval(this.debouncedIntervalTimelinePurge)
}
delete app.ctx app.ctx = {
} submit: this.submitMessage,
}
}
render() { componentWillUnmount() {
if (this.state.connectionEnd) { this.leaveSocketRoom()
return <div className="liveChat">
<antd.Result
status="error"
title="Connection error"
subTitle="Cannot connect to the server"
/>
</div>
}
if (this.state.connecting) { if (this.debouncedIntervalTimelinePurge) {
return <div className="liveChat"> clearInterval(this.debouncedIntervalTimelinePurge)
<antd.Skeleton active /> }
</div>
}
if (this.props.floatingMode) { delete app.ctx
return <div className="liveChat floating"> }
<TransitionGroup
ref={this.timelineRef}
className="liveChat_timeline"
id="liveChat_timeline"
>
{
this.state.temporalTimeline.map((line, index) => {
return <CSSTransition
key={index}
timeout={300}
classNames={{
enterActive: "transverse-enter",
exitActive: "transverse-out"
}}
>
<Line {...this.state.timeline[line.messageKey]} />
</CSSTransition>
})
}
</TransitionGroup>
</div>
}
if (this.state.noAuthed) { render() {
return <div className="liveChat empty"> if (this.state.connectionEnd) {
<antd.Empty description="You must be logged in to use this feature" /> return (
</div> <div className="liveChat">
} <antd.Result
status="error"
title="Connection error"
subTitle="Cannot connect to the server"
/>
</div>
)
}
return <div if (this.state.connecting) {
className={classnames( return (
"liveChat", <div className="liveChat">
{ <antd.Skeleton active />
["empty"]: this.state.timeline.length === 0, </div>
["compact"]: this.props.compact )
} }
)}
>
{
!this.props.compact && this.state.timeline.length === 0 && <antd.Empty description="Welcome to the room" />
}
{ if (this.props.floatingMode) {
this.props.compact && this.state.timeline.length === 0 && <p> return (
Welcome to the room <div className="liveChat floating">
</p> <TransitionGroup
} ref={this.timelineRef}
className="liveChat_timeline"
id="liveChat_timeline"
>
{this.state.temporalTimeline.map((line, index) => {
return (
<CSSTransition
key={index}
timeout={300}
classNames={{
enterActive: "transverse-enter",
exitActive: "transverse-out",
}}
>
<Line
{...this.state.timeline[
line.messageKey
]}
/>
</CSSTransition>
)
})}
</TransitionGroup>
</div>
)
}
{ if (this.state.noAuthed) {
this.state.timeline.length !== 0 && <div return (
className="liveChat_timeline" <div className="liveChat empty">
ref={this.timelineRef} <antd.Empty description="You must be logged in to use this feature" />
id="liveChat_timeline" </div>
> )
{ }
this.state.timeline.map((line, index) => {
return <Line key={index} {...line} />
})
}
</div>
}
<div className="liveChat_textInput"> return (
<antd.Input.TextArea <div
placeholder="Type your message here" className={classnames("liveChat", {
autoSize={{ minRows: 1, maxRows: 3 }} ["empty"]: this.state.timeline.length === 0,
value={this.state.writtedMessage} ["compact"]: this.props.compact,
onChange={this.handleInputChange} })}
onPressEnter={this.handleOnEnter} >
maxLength={this.state.roomInfo?.limitations?.maxMessageLength ?? 100} {!this.props.compact && this.state.timeline.length === 0 && (
showCount <antd.Empty description="Welcome to the room" />
/> )}
</div>
</div> {this.props.compact && this.state.timeline.length === 0 && (
} <p>Welcome to the room</p>
} )}
{this.state.timeline.length !== 0 && (
<div
className="liveChat_timeline"
ref={this.timelineRef}
id="liveChat_timeline"
>
{this.state.timeline.map((line, index) => {
return <Line key={index} {...line} />
})}
</div>
)}
<div className="liveChat_textInput">
<antd.Input.TextArea
placeholder="Type your message here"
autoSize={{ minRows: 1, maxRows: 3 }}
value={this.state.writtedMessage}
onChange={this.handleInputChange}
onPressEnter={this.handleOnEnter}
maxLength={
this.state.roomInfo?.limitations
?.maxMessageLength ?? 100
}
showCount
/>
</div>
</div>
)
}
}

View File

@ -1,153 +1,154 @@
.liveChat { .liveChat {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
transition: all 250ms ease-in-out; transition: all 250ms ease-in-out;
&.empty { &.empty {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
&.compact { &.compact {
height: 5vh !important; height: 5vh !important;
} }
} }
&.compact { &.compact {
height: 15vh; height: 15vh;
justify-content: flex-end; justify-content: flex-end;
.liveChat_timeline { .liveChat_timeline {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0; padding: 0;
margin: 0; margin: 0;
margin-bottom: 10px; gap: 5px;
gap: 5px; .textRoom_line {
padding: 0;
margin: 0;
.textRoom_line { h4 {
padding: 0; font-size: 0.8rem;
margin: 0; }
}
}
h4 { .liveChat_textInput {
font-size: 0.8rem; position: relative;
}
}
}
.liveChat_textInput { bottom: 0;
position: relative; left: 0;
bottom: 0; margin-bottom: 0;
left: 0; }
}
margin-bottom: 0; &.floating {
} .textRoom_line {
} display: relative;
&.floating { background-color: rgba(var(--layoutBackgroundColor), 0.7);
.textRoom_line { backdrop-filter: blur(20px);
display: relative; }
background-color: rgba(var(--layoutBackgroundColor), 0.7); .liveChat_timeline {
backdrop-filter: blur(20px); display: flex;
}
.liveChat_timeline { flex-direction: column;
display: flex; padding-top: 0;
flex-direction: column; overflow-x: hidden;
padding-top: 0; overflow-y: hidden;
overflow-x: hidden; //justify-content: flex-end;
overflow-y: hidden;
//justify-content: flex-end; margin-bottom: 0;
margin-bottom: 0; transition: all 250ms ease-in-out;
}
}
transition: all 250ms ease-in-out; .liveChat_timeline {
} position: relative;
}
.liveChat_timeline { display: flex;
position: relative; flex-direction: column;
height: 100%;
overflow-y: scroll; gap: 10px;
margin-bottom: 70px; height: 100%;
} overflow-y: scroll;
.liveChat_textInput { margin-bottom: 70px;
position: absolute; }
bottom: 0;
left: 0;
margin-bottom: 20px; .liveChat_textInput {
position: absolute;
bottom: 0;
left: 0;
width: 100%; margin-bottom: 20px;
height: fit-content;
color: var(--text-color); width: 100%;
height: fit-content;
.ant-input-textarea-show-count::after { color: var(--text-color);
color: var(--text-color);
} .ant-input-textarea-show-count::after {
} color: var(--text-color);
}
}
} }
.textRoom_line { .textRoom_line {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 5px 10px; padding: 5px 7px;
margin-bottom: 10px; border-radius: 12px;
border-radius: 12px; background-color: rgba(var(--bg_color_1), 0.7);
background-color: var(--background-color-accent); h1,
h2,
h3,
h4,
span {
margin: 0;
user-select: text;
}
h1, .textRoom_line_user {
h2, font-weight: bold;
h3, font-size: 0.9rem;
h4,
span {
margin: 0;
user-select: text;
}
.textRoom_line_user { color: var(--text-color);
font-weight: bold; overflow: hidden;
font-size: 1rem;
color: var(--background-color-contrast);
overflow: hidden; text-overflow: ellipsis;
white-space: nowrap;
}
text-overflow: ellipsis; .textRoom_line_content {
white-space: nowrap; font-size: 0.8rem;
} color: var(--text-color);
.textRoom_line_content { padding: 0 10px;
font-size: 0.8rem;
color: var(--text-color);
padding: 0 10px; word-break: break-all;
white-space: pre-wrap;
word-break: break-all; }
white-space: pre-wrap; }
}
}