mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Fixed unread message count in react chat (#3529)
* Fixed unread message count in react chat The problem was caused by ChatStore being instantiated multiple times on each `useRef` call, plus also incorrectly cleaned-up, since it should be calling `socket.removeListener` instead of `socket.off` on effect destroy. * deferred loading messages until chat is opened GitOrigin-RevId: b990cd06cea6630472b0911b56219766717aaff6
This commit is contained in:
parent
fb8c70598c
commit
864a75c284
8 changed files with 51 additions and 29 deletions
|
@ -1,4 +1,4 @@
|
|||
aside.chat(
|
||||
ng-controller="ReactChatController"
|
||||
)
|
||||
chat(reset-unread-messages="resetUnreadMessages")
|
||||
chat(reset-unread-messages="resetUnreadMessages" chat-is-open="chatIsOpen")
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import MessageList from './message-list'
|
||||
import MessageInput from './message-input'
|
||||
|
@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
|
|||
import { useChatStore } from '../store/chat-store-effect'
|
||||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||
|
||||
function ChatPane({ resetUnreadMessages }) {
|
||||
function ChatPane({ resetUnreadMessages, chatIsOpen }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
|
@ -20,10 +20,14 @@ function ChatPane({ resetUnreadMessages }) {
|
|||
userId
|
||||
} = useChatStore()
|
||||
|
||||
const [initialMessagesLoaded, setInitialMessagesLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
loadMoreMessages()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
if (chatIsOpen && !initialMessagesLoaded) {
|
||||
loadMoreMessages()
|
||||
setInitialMessagesLoaded(true)
|
||||
}
|
||||
}, [initialMessagesLoaded, loadMoreMessages, chatIsOpen])
|
||||
|
||||
const shouldDisplayPlaceholder = !loading && messages.length === 0
|
||||
|
||||
|
@ -85,7 +89,8 @@ function Placeholder() {
|
|||
}
|
||||
|
||||
ChatPane.propTypes = {
|
||||
resetUnreadMessages: PropTypes.func.isRequired
|
||||
resetUnreadMessages: PropTypes.func.isRequired,
|
||||
chatIsOpen: PropTypes.bool
|
||||
}
|
||||
|
||||
export default withErrorBoundary(ChatPane)
|
||||
|
|
|
@ -5,11 +5,22 @@ import { rootContext } from '../../../shared/context/root-context'
|
|||
import ChatPane from '../components/chat-pane'
|
||||
|
||||
App.controller('ReactChatController', function($scope, ide) {
|
||||
$scope.chatIsOpen = ide.$scope.ui.chatOpen
|
||||
|
||||
ide.$scope.$watch('ui.chatOpen', newValue => {
|
||||
$scope.$applyAsync(() => {
|
||||
$scope.chatIsOpen = newValue
|
||||
})
|
||||
})
|
||||
|
||||
$scope.resetUnreadMessages = () =>
|
||||
ide.$scope.$broadcast('chat:resetUnreadMessages')
|
||||
})
|
||||
|
||||
App.component(
|
||||
'chat',
|
||||
react2angular(rootContext.use(ChatPane), ['resetUnreadMessages'])
|
||||
react2angular(rootContext.use(ChatPane), [
|
||||
'resetUnreadMessages',
|
||||
'chatIsOpen'
|
||||
])
|
||||
)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { ChatStore } from './chat-store'
|
||||
import { useApplicationContext } from '../../../shared/context/application-context'
|
||||
import { useEditorContext } from '../../../shared/context/editor-context'
|
||||
|
@ -7,29 +7,32 @@ export function useChatStore() {
|
|||
const { user } = useApplicationContext()
|
||||
const { projectId } = useEditorContext()
|
||||
|
||||
const chatStoreRef = useRef(new ChatStore(user, projectId))
|
||||
const [atEnd, setAtEnd] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [messages, setMessages] = useState([])
|
||||
|
||||
const [atEnd, setAtEnd] = useState(chatStoreRef.current.atEnd)
|
||||
const [loading, setLoading] = useState(chatStoreRef.current.loading)
|
||||
const [messages, setMessages] = useState(chatStoreRef.current.messages)
|
||||
const [store] = useState(() => new ChatStore(user, projectId))
|
||||
const loadMoreMessages = useCallback(() => store.loadMoreMessages(), [store])
|
||||
const sendMessage = useCallback(message => store.sendMessage(message), [
|
||||
store
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const chatStore = chatStoreRef.current
|
||||
function handleStoreUpdated() {
|
||||
setAtEnd(chatStore.atEnd)
|
||||
setLoading(chatStore.loading)
|
||||
setMessages(chatStore.messages)
|
||||
setAtEnd(store.atEnd)
|
||||
setLoading(store.loading)
|
||||
setMessages(store.messages)
|
||||
}
|
||||
chatStore.on('updated', handleStoreUpdated)
|
||||
return () => chatStore.destroy()
|
||||
}, [chatStoreRef])
|
||||
store.on('updated', handleStoreUpdated)
|
||||
return () => store.destroy()
|
||||
}, [store])
|
||||
|
||||
return {
|
||||
userId: user.id,
|
||||
atEnd,
|
||||
loading,
|
||||
messages,
|
||||
loadMoreMessages: () => chatStoreRef.current.loadMoreMessages(),
|
||||
sendMessage: message => chatStoreRef.current.sendMessage(message)
|
||||
loadMoreMessages,
|
||||
sendMessage
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,10 @@ export class ChatStore {
|
|||
}
|
||||
|
||||
destroy() {
|
||||
window._ide.socket.off('new-chat-message', this._onNewChatMessage)
|
||||
window._ide.socket.removeListener(
|
||||
'new-chat-message',
|
||||
this._onNewChatMessage
|
||||
)
|
||||
this._emitter.off() // removes all listeners
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ describe('<ChatPane />', function() {
|
|||
fetchMock.get(/messages/, testMessages)
|
||||
// unmounting before `beforeEach` block is executed is required to prevent cleanup errors
|
||||
const { unmount } = renderWithEditorContext(
|
||||
<ChatPane resetUnreadMessages={() => {}} />
|
||||
<ChatPane resetUnreadMessages={() => {}} chatIsOpen />
|
||||
)
|
||||
|
||||
await screen.findByText('a message')
|
||||
|
@ -63,7 +63,7 @@ describe('<ChatPane />', function() {
|
|||
it('A loading spinner is rendered while the messages are loading, then disappears', async function() {
|
||||
fetchMock.get(/messages/, [])
|
||||
const { unmount } = renderWithEditorContext(
|
||||
<ChatPane resetUnreadMessages={() => {}} />
|
||||
<ChatPane resetUnreadMessages={() => {}} chatIsOpen />
|
||||
)
|
||||
await waitForElementToBeRemoved(() => screen.getByText('Loading…'))
|
||||
unmount()
|
||||
|
@ -73,7 +73,7 @@ describe('<ChatPane />', function() {
|
|||
it('is rendered when there are no messages ', async function() {
|
||||
fetchMock.get(/messages/, [])
|
||||
const { unmount } = renderWithEditorContext(
|
||||
<ChatPane resetUnreadMessages={() => {}} />
|
||||
<ChatPane resetUnreadMessages={() => {}} chatIsOpen />
|
||||
)
|
||||
await screen.findByText('Send your first message to your collaborators')
|
||||
unmount()
|
||||
|
@ -82,7 +82,7 @@ describe('<ChatPane />', function() {
|
|||
it('is not rendered when messages are displayed', function() {
|
||||
fetchMock.get(/messages/, testMessages)
|
||||
const { unmount } = renderWithEditorContext(
|
||||
<ChatPane resetUnreadMessages={() => {}} />
|
||||
<ChatPane resetUnreadMessages={() => {}} chatIsOpen />
|
||||
)
|
||||
expect(
|
||||
screen.queryByText('Send your first message to your collaborators')
|
||||
|
|
|
@ -27,7 +27,7 @@ export function tearDownMathJaxStubs() {
|
|||
}
|
||||
|
||||
export function stubChatStore({ user }) {
|
||||
window._ide = { socket: { on: sinon.stub(), off: sinon.stub() } }
|
||||
window._ide = { socket: { on: sinon.stub(), removeListener: sinon.stub() } }
|
||||
window.user = user
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ describe('ChatStore', function() {
|
|||
|
||||
window.csrfToken = 'csrf_tok'
|
||||
|
||||
socket = { on: sinon.stub(), off: sinon.stub() }
|
||||
socket = { on: sinon.stub(), removeListener: sinon.stub() }
|
||||
window._ide = { socket }
|
||||
mockSocketMessage = message => socket.on.getCall(0).args[1](message)
|
||||
|
||||
|
|
Loading…
Reference in a new issue