反应。聊天工具 API。 MessageList 组件错误 - 呈现来自其他房间的消息。组件生命周期和状态
React. Chatkit API. MessageList component bug - rendering messages from other rooms. Component Lifecycle and state
我使用 React 构建的 Chatkit 应用遇到了一些奇怪的问题 activity。本质上,我是在不同房间对两个不同的用户进行测试。当我从一个房间的用户发送消息时。其他用户能够看到该消息,尽管他们不在同一个房间。这是正在发生的事情的屏幕截图。
这似乎只有当用户至少在同一个房间里一次时才会发生。
Buggy Chat
我可以看出消息的创建是正确的,因为我在 ChatKit 的正确位置看到了它们 API。此外,如果我重新渲染组件,消息最终会出现在正确的位置。但是跨房间消息错误仍然存在。
Corrected Chat
我的印象是它肯定与 MessageList 组件的状态有关。我确保每次进入新房间时都更新组件状态,但我想真正的问题是应用程序的其他实例是否甚至关心不同实例的组件状态变化。
废话少说,这是我的代码:
ChatScreen(主应用)
import React from "react"
import Chatkit from "@pusher/chatkit"
import MessageList from "./MessageList"
import SendMessageForm from "./SendMessageForm"
import WhosOnlineList from "./WhosOnlineList"
import RoomList from "./RoomList"
import NewRoomForm from "./NewRoomForm"
import { getCurrentRoom } from "../../actions/chatkitActions"
import { connect } from "react-redux"
class ChatScreen extends React.Component{
constructor(props){
super(props)
this.state = {
messages: [],
currentRoom: {},
currentUser: {},
usersWhoAreTyping: [],
joinableRooms: [],
joinedRooms: [],
errors: {}
}
this.sendMessage = this.sendMessage.bind(this)
this.sendTypingEvent = this.sendTypingEvent.bind(this)
this.subscribeToRoom = this.subscribeToRoom.bind(this)
this.getRooms = this.getRooms.bind(this)
this.createRoom = this.createRoom.bind(this)
}
componentDidMount(){
//setup Chatkit
let tokenUrl
let instanceLocator = "somecode"
if(process.env.NODE_ENV === "production"){
tokenUrl = "somenedpoint"
} else {
tokenUrl = "http://localhost:3000/api/channels/authenticate"
}
const chatManager = new Chatkit.ChatManager({
instanceLocator: instanceLocator,
userId: this.props.chatUser.name,
connectionTimeout: 120000,
tokenProvider: new Chatkit.TokenProvider({
url: tokenUrl
})
})
//initiate Chatkit
chatManager.connect()
.then((currentUser) => {
this.setState({
currentUser: currentUser
})
//get all rooms
this.getRooms()
// if the user is returning to the chat, direct them to the room they last visited
if(this.props.chatkit.currentRoom.id > 0){
this.subscribeToRoom(this.props.chatkit.currentRoom.id)
}
})
}
sendMessage = (text) => {
this.state.currentUser.sendMessage({
roomId: this.state.currentRoom.id,
text: text
})
}
sendTypingEvent = () => {
this.state.currentUser
.isTypingIn({
roomId: this.state.currentRoom.id
})
.catch((errors) => {
this.setState({
errors: errors
})
})
}
getRooms = () => {
this.state.currentUser.getJoinableRooms()
.then((joinableRooms) => {
this.setState({
joinableRooms: joinableRooms,
joinedRooms: this.state.currentUser.rooms
})
})
.catch((errors) => {
this.setState({
errors: { error: "could not retrieve rooms"}
})
})
}
subscribeToRoom = (roomId) => {
this.setState({
messages: []
})
this.state.currentUser.subscribeToRoom({
roomId: roomId,
hooks: {
onNewMessage: (message) => {
this.setState({
messages: [...this.state.messages, message]
})
},
onUserStartedTyping: (currentUser) => {
this.setState({
usersWhoAreTyping: [...this.state.usersWhoAreTyping, currentUser.name]
})
},
onUserStoppedTyping: (currentUser) => {
this.setState({
usersWhoAreTyping: this.state.usersWhoAreTyping.filter((user) => {
return user !== currentUser.name
})
})
},
onUserCameOnline: () => this.forceUpdate(),
onUserWentOffline: () => this.forceUpdate(),
onUserJoined: () => this.forceUpdate()
}
})
.then((currentRoom) => {
this.setState({
currentRoom: currentRoom
})
this.getRooms()
//store currentRoom in redux state
this.props.getCurrentRoom(currentRoom)
})
.catch((errors) => {
this.setState({
errors: errors
})
})
}
createRoom = (roomName) => {
this.state.currentUser.createRoom({
name: roomName
})
.then((newRoom) => {
this.subscribeToRoom(newRoom.id)
})
.catch((errors) => {
this.setState({
errors: { error: "could not create room" }
})
})
}
render(){
const username = this.props.chatUser.name
return(
<div className="container" style={{ display: "flex", fontFamily: "Montserrat", height: "100vh"}}>
<div
className="col-md-3 bg-dark mr-2 p-0"
style={{display: "flex", flexDirection: "column", maxHeight: "80vh", padding: "24px 24px 0px"}}
>
<div style={{flex: "1"}} className="p-4">
<WhosOnlineList users={this.state.currentRoom.users}/>
<RoomList
roomId={this.state.currentRoom.id}
rooms={[...this.state.joinedRooms, ...this.state.joinableRooms]}
subscribeToRoom={this.subscribeToRoom}
/>
</div>
<NewRoomForm createRoom={this.createRoom} user={this.state.currentUser}/>
</div>
<div
className="col-md-9 border p-0"
style={{display: "flex", flexDirection: "column", maxHeight: "80vh"}}
>
<div className="mb-3">
{ this.state.currentRoom.name ? (
<h4
className="bg-black text-light m-0"
style={{padding: "1.0rem 1.2rem"}}
>
{this.state.currentRoom.name}
</h4>
) : (
this.props.chatkit.currentRoom.id > 0 ) ? (
<h3 className="text-dark p-4">Returning to room...</h3>
) : (
<h3 className="text-dark p-4">← Join a Room!</h3>
)}
</div>
<div style={{flex: "1"}}>
<MessageList messages={this.state.messages} room={this.state.currentRoom.id} usersWhoAreTyping={this.state.usersWhoAreTyping}/>
</div>
<SendMessageForm
sendMessage={this.sendMessage}
userTyping={this.sendTypingEvent}
currentRoom={this.state.currentRoom}
/>
</div>
</div>
)
}
}
const mapStateToProps = (state) => {
return{
chatkit: state.chatkit
}
}
const mapDispatchToProps = (dispatch) => {
return{
getCurrentRoom: (currentRoom) => {
dispatch(getCurrentRoom(currentRoom))
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatScreen)
消息列表(组件)
import React from "react"
import ReactDOM from "react-dom"
import TypingIndicator from "./TypingIndicator"
class MessageList extends React.Component{
constructor(props){
super(props)
this.state = {
currentRoom: {}
}
}
componentWillReceiveProps(nextProps){
if(nextProps.room){
console.log(nextProps.room)
this.setState({
currentRoom: nextProps.room
})
}
}
componentWillUpdate(){
const node = ReactDOM.findDOMNode(this)
//scrollTop is the distance from the top. clientHeight is the visible height. scrollHeight is the height on the component
this.shouldScrollToBottom = node.scrollTop + node.clientHeight + 100 >= node.scrollHeight
}
componentDidUpdate(){
//scroll to the bottom if we are close to the bottom of the component
if(this.shouldScrollToBottom){
const node = ReactDOM.findDOMNode(this)
node.scrollTop = node.scrollHeight
}
}
render(){
const messages = this.props.messages
let updatedMessages = []
for(var i = 0; i < messages.length; i++){
let previous = {}
if(i > 0){
previous = messages[i - 1]
}
if(messages[i].senderId === previous.senderId){
updatedMessages.push({...messages[i], senderId: ""})
} else{
updatedMessages.push(messages[i])
}
}
return(
<div>
{this.props.room && (
<div style={{overflow: "scroll", overflowX: "hidden", maxHeight: "65vh"}}>
<ul style={{listStyle: "none"}} className="p-3">
{updatedMessages.map((message, index) => {
return (
<li className="mb-1" key={index}>
<div>
{message.senderId && (
<span
className="text-dark d-block font-weight-bold mt-3"
>
{message.senderId}
</span>
)}
<span
className="bg-info text-light rounded d-inline-block"
style={{padding:".25rem .5rem"}}
>
{message.text}
</span>
</div>
</li>
)
})}
</ul>
<TypingIndicator usersWhoAreTyping={this.props.usersWhoAreTyping}/>
</div>
)}
</div>
)
}
}
export default MessageList
房间列表(组件)
import React from "react"
class RoomList extends React.Component{
render(){
const orderedRooms = [...this.props.rooms].sort((a, b) => {
return a.id - b.id
})
return(
<div>
{ this.props.rooms.length > 0 ? (
<div>
<div className="d-flex justify-content-between text-light mb-2">
<h6 className="font-weight-bold">Channels</h6><i className="fa fa-gamepad"></i>
</div>
<ul style={{listStyle: "none", overflow: "scroll", overflowX: "hidden", maxHeight: "27vh"}} className="p-2">
{orderedRooms.map((room, index) => {
return(
<li key={index} className="font-weight-bold mb-2">
<a
onClick={() => {
this.props.subscribeToRoom(room.id)
}}
href="#"
className={room.id === this.props.roomId ? "text-success": "text-info"}
style={{textDecoration: "none"}}
>
<span className="mr-2">#</span>{room.name}
</a>
</li>
)
})}
</ul>
</div>
) : (
<p className="text-muted p-2">Loading...</p>
)}
</div>
)
}
}
这是呈现 ChatScreen 的组件 (ChannelsContainer)
import React from "react"
import UsernameForm from "./UsernameForm"
import ChatScreen from "./ChatScreen"
import { connect } from "react-redux"
class ChannelsContainer extends React.Component{
constructor(props){
super(props)
this.state = {
chatScreen: false
}
}
componentWillMount(){
if(this.props.chatkit.chatInitialized){
this.setState({
chatScreen: true
})
}
}
componentWillReceiveProps(nextProps){
if(nextProps.chatkit.chatInitialized){
this.setState({
chatScreen: true
})
}
}
render(){
let chatStage
if(this.state.chatScreen){
chatStage = <ChatScreen chatUser={this.props.chatkit.chatUser}/>
} else{
chatStage = <UsernameForm/>
}
return(
<div style={{minHeight: "90vh"}}>
{chatStage}
</div>
)
}
}
const mapStateToProps = (state) => {
return{
chatkit: state.chatkit
}
}
export default connect(mapStateToProps)(ChannelsContainer)
请告诉我你们的想法。
已修复。我所要做的就是将消息的房间 ID 与当前房间 ID 进行比较。如果它们相同,那么我将更新我的组件状态消息字段。
onNewMessage: (message) => {
if(message.room.id === this.state.currentRoom.id){
this.setState({
messages: [...this.state.messages, message]
})
}
}
我使用 React 构建的 Chatkit 应用遇到了一些奇怪的问题 activity。本质上,我是在不同房间对两个不同的用户进行测试。当我从一个房间的用户发送消息时。其他用户能够看到该消息,尽管他们不在同一个房间。这是正在发生的事情的屏幕截图。
这似乎只有当用户至少在同一个房间里一次时才会发生。
Buggy Chat
我可以看出消息的创建是正确的,因为我在 ChatKit 的正确位置看到了它们 API。此外,如果我重新渲染组件,消息最终会出现在正确的位置。但是跨房间消息错误仍然存在。
Corrected Chat
我的印象是它肯定与 MessageList 组件的状态有关。我确保每次进入新房间时都更新组件状态,但我想真正的问题是应用程序的其他实例是否甚至关心不同实例的组件状态变化。
废话少说,这是我的代码:
ChatScreen(主应用)
import React from "react"
import Chatkit from "@pusher/chatkit"
import MessageList from "./MessageList"
import SendMessageForm from "./SendMessageForm"
import WhosOnlineList from "./WhosOnlineList"
import RoomList from "./RoomList"
import NewRoomForm from "./NewRoomForm"
import { getCurrentRoom } from "../../actions/chatkitActions"
import { connect } from "react-redux"
class ChatScreen extends React.Component{
constructor(props){
super(props)
this.state = {
messages: [],
currentRoom: {},
currentUser: {},
usersWhoAreTyping: [],
joinableRooms: [],
joinedRooms: [],
errors: {}
}
this.sendMessage = this.sendMessage.bind(this)
this.sendTypingEvent = this.sendTypingEvent.bind(this)
this.subscribeToRoom = this.subscribeToRoom.bind(this)
this.getRooms = this.getRooms.bind(this)
this.createRoom = this.createRoom.bind(this)
}
componentDidMount(){
//setup Chatkit
let tokenUrl
let instanceLocator = "somecode"
if(process.env.NODE_ENV === "production"){
tokenUrl = "somenedpoint"
} else {
tokenUrl = "http://localhost:3000/api/channels/authenticate"
}
const chatManager = new Chatkit.ChatManager({
instanceLocator: instanceLocator,
userId: this.props.chatUser.name,
connectionTimeout: 120000,
tokenProvider: new Chatkit.TokenProvider({
url: tokenUrl
})
})
//initiate Chatkit
chatManager.connect()
.then((currentUser) => {
this.setState({
currentUser: currentUser
})
//get all rooms
this.getRooms()
// if the user is returning to the chat, direct them to the room they last visited
if(this.props.chatkit.currentRoom.id > 0){
this.subscribeToRoom(this.props.chatkit.currentRoom.id)
}
})
}
sendMessage = (text) => {
this.state.currentUser.sendMessage({
roomId: this.state.currentRoom.id,
text: text
})
}
sendTypingEvent = () => {
this.state.currentUser
.isTypingIn({
roomId: this.state.currentRoom.id
})
.catch((errors) => {
this.setState({
errors: errors
})
})
}
getRooms = () => {
this.state.currentUser.getJoinableRooms()
.then((joinableRooms) => {
this.setState({
joinableRooms: joinableRooms,
joinedRooms: this.state.currentUser.rooms
})
})
.catch((errors) => {
this.setState({
errors: { error: "could not retrieve rooms"}
})
})
}
subscribeToRoom = (roomId) => {
this.setState({
messages: []
})
this.state.currentUser.subscribeToRoom({
roomId: roomId,
hooks: {
onNewMessage: (message) => {
this.setState({
messages: [...this.state.messages, message]
})
},
onUserStartedTyping: (currentUser) => {
this.setState({
usersWhoAreTyping: [...this.state.usersWhoAreTyping, currentUser.name]
})
},
onUserStoppedTyping: (currentUser) => {
this.setState({
usersWhoAreTyping: this.state.usersWhoAreTyping.filter((user) => {
return user !== currentUser.name
})
})
},
onUserCameOnline: () => this.forceUpdate(),
onUserWentOffline: () => this.forceUpdate(),
onUserJoined: () => this.forceUpdate()
}
})
.then((currentRoom) => {
this.setState({
currentRoom: currentRoom
})
this.getRooms()
//store currentRoom in redux state
this.props.getCurrentRoom(currentRoom)
})
.catch((errors) => {
this.setState({
errors: errors
})
})
}
createRoom = (roomName) => {
this.state.currentUser.createRoom({
name: roomName
})
.then((newRoom) => {
this.subscribeToRoom(newRoom.id)
})
.catch((errors) => {
this.setState({
errors: { error: "could not create room" }
})
})
}
render(){
const username = this.props.chatUser.name
return(
<div className="container" style={{ display: "flex", fontFamily: "Montserrat", height: "100vh"}}>
<div
className="col-md-3 bg-dark mr-2 p-0"
style={{display: "flex", flexDirection: "column", maxHeight: "80vh", padding: "24px 24px 0px"}}
>
<div style={{flex: "1"}} className="p-4">
<WhosOnlineList users={this.state.currentRoom.users}/>
<RoomList
roomId={this.state.currentRoom.id}
rooms={[...this.state.joinedRooms, ...this.state.joinableRooms]}
subscribeToRoom={this.subscribeToRoom}
/>
</div>
<NewRoomForm createRoom={this.createRoom} user={this.state.currentUser}/>
</div>
<div
className="col-md-9 border p-0"
style={{display: "flex", flexDirection: "column", maxHeight: "80vh"}}
>
<div className="mb-3">
{ this.state.currentRoom.name ? (
<h4
className="bg-black text-light m-0"
style={{padding: "1.0rem 1.2rem"}}
>
{this.state.currentRoom.name}
</h4>
) : (
this.props.chatkit.currentRoom.id > 0 ) ? (
<h3 className="text-dark p-4">Returning to room...</h3>
) : (
<h3 className="text-dark p-4">← Join a Room!</h3>
)}
</div>
<div style={{flex: "1"}}>
<MessageList messages={this.state.messages} room={this.state.currentRoom.id} usersWhoAreTyping={this.state.usersWhoAreTyping}/>
</div>
<SendMessageForm
sendMessage={this.sendMessage}
userTyping={this.sendTypingEvent}
currentRoom={this.state.currentRoom}
/>
</div>
</div>
)
}
}
const mapStateToProps = (state) => {
return{
chatkit: state.chatkit
}
}
const mapDispatchToProps = (dispatch) => {
return{
getCurrentRoom: (currentRoom) => {
dispatch(getCurrentRoom(currentRoom))
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatScreen)
消息列表(组件)
import React from "react"
import ReactDOM from "react-dom"
import TypingIndicator from "./TypingIndicator"
class MessageList extends React.Component{
constructor(props){
super(props)
this.state = {
currentRoom: {}
}
}
componentWillReceiveProps(nextProps){
if(nextProps.room){
console.log(nextProps.room)
this.setState({
currentRoom: nextProps.room
})
}
}
componentWillUpdate(){
const node = ReactDOM.findDOMNode(this)
//scrollTop is the distance from the top. clientHeight is the visible height. scrollHeight is the height on the component
this.shouldScrollToBottom = node.scrollTop + node.clientHeight + 100 >= node.scrollHeight
}
componentDidUpdate(){
//scroll to the bottom if we are close to the bottom of the component
if(this.shouldScrollToBottom){
const node = ReactDOM.findDOMNode(this)
node.scrollTop = node.scrollHeight
}
}
render(){
const messages = this.props.messages
let updatedMessages = []
for(var i = 0; i < messages.length; i++){
let previous = {}
if(i > 0){
previous = messages[i - 1]
}
if(messages[i].senderId === previous.senderId){
updatedMessages.push({...messages[i], senderId: ""})
} else{
updatedMessages.push(messages[i])
}
}
return(
<div>
{this.props.room && (
<div style={{overflow: "scroll", overflowX: "hidden", maxHeight: "65vh"}}>
<ul style={{listStyle: "none"}} className="p-3">
{updatedMessages.map((message, index) => {
return (
<li className="mb-1" key={index}>
<div>
{message.senderId && (
<span
className="text-dark d-block font-weight-bold mt-3"
>
{message.senderId}
</span>
)}
<span
className="bg-info text-light rounded d-inline-block"
style={{padding:".25rem .5rem"}}
>
{message.text}
</span>
</div>
</li>
)
})}
</ul>
<TypingIndicator usersWhoAreTyping={this.props.usersWhoAreTyping}/>
</div>
)}
</div>
)
}
}
export default MessageList
房间列表(组件)
import React from "react"
class RoomList extends React.Component{
render(){
const orderedRooms = [...this.props.rooms].sort((a, b) => {
return a.id - b.id
})
return(
<div>
{ this.props.rooms.length > 0 ? (
<div>
<div className="d-flex justify-content-between text-light mb-2">
<h6 className="font-weight-bold">Channels</h6><i className="fa fa-gamepad"></i>
</div>
<ul style={{listStyle: "none", overflow: "scroll", overflowX: "hidden", maxHeight: "27vh"}} className="p-2">
{orderedRooms.map((room, index) => {
return(
<li key={index} className="font-weight-bold mb-2">
<a
onClick={() => {
this.props.subscribeToRoom(room.id)
}}
href="#"
className={room.id === this.props.roomId ? "text-success": "text-info"}
style={{textDecoration: "none"}}
>
<span className="mr-2">#</span>{room.name}
</a>
</li>
)
})}
</ul>
</div>
) : (
<p className="text-muted p-2">Loading...</p>
)}
</div>
)
}
}
这是呈现 ChatScreen 的组件 (ChannelsContainer)
import React from "react"
import UsernameForm from "./UsernameForm"
import ChatScreen from "./ChatScreen"
import { connect } from "react-redux"
class ChannelsContainer extends React.Component{
constructor(props){
super(props)
this.state = {
chatScreen: false
}
}
componentWillMount(){
if(this.props.chatkit.chatInitialized){
this.setState({
chatScreen: true
})
}
}
componentWillReceiveProps(nextProps){
if(nextProps.chatkit.chatInitialized){
this.setState({
chatScreen: true
})
}
}
render(){
let chatStage
if(this.state.chatScreen){
chatStage = <ChatScreen chatUser={this.props.chatkit.chatUser}/>
} else{
chatStage = <UsernameForm/>
}
return(
<div style={{minHeight: "90vh"}}>
{chatStage}
</div>
)
}
}
const mapStateToProps = (state) => {
return{
chatkit: state.chatkit
}
}
export default connect(mapStateToProps)(ChannelsContainer)
请告诉我你们的想法。
已修复。我所要做的就是将消息的房间 ID 与当前房间 ID 进行比较。如果它们相同,那么我将更新我的组件状态消息字段。
onNewMessage: (message) => {
if(message.room.id === this.state.currentRoom.id){
this.setState({
messages: [...this.state.messages, message]
})
}
}