mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
[ReactNavToolbar] Chat Toggle Button + chat-context
(#3625)
* Added toggle chat button to navigation header * new `useBrowserWindow` hook to work with browser title and focus * react2angular chat toggle button plumbing GitOrigin-RevId: 4380f1db9c7cc9a25bfb8d7a33e18d61b1d32993
This commit is contained in:
parent
7f6d439302
commit
260b878b7d
14 changed files with 347 additions and 79 deletions
|
@ -11,7 +11,11 @@ block content
|
|||
div(
|
||||
ng-controller="ReactRootContextController"
|
||||
)
|
||||
shared-context-react(editor-loading="editorLoading")
|
||||
shared-context-react(
|
||||
editor-loading="editorLoading"
|
||||
chat-is-open-angular="chatIsOpenAngular"
|
||||
set-chat-is-open-angular="setChatIsOpenAngular"
|
||||
)
|
||||
.loading-screen(ng-if="state.loading")
|
||||
.loading-screen-brand-container
|
||||
.loading-screen-brand(
|
||||
|
|
|
@ -1,24 +1,29 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import MessageList from './message-list'
|
||||
import MessageInput from './message-input'
|
||||
import InfiniteScroll from './infinite-scroll'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useChatStore } from '../store/chat-store-effect'
|
||||
import { useEditorContext } from '../../../shared/context/editor-context'
|
||||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||
import { useChatContext } from '../context/chat-context'
|
||||
|
||||
function ChatPane({ resetUnreadMessages, chatIsOpen }) {
|
||||
function ChatPane() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
ui: { chatIsOpen }
|
||||
} = useEditorContext()
|
||||
|
||||
const {
|
||||
userId,
|
||||
atEnd,
|
||||
loading,
|
||||
loadMoreMessages,
|
||||
messages,
|
||||
sendMessage,
|
||||
userId
|
||||
} = useChatStore()
|
||||
resetUnreadMessageCount
|
||||
} = useChatContext()
|
||||
|
||||
const [initialMessagesLoaded, setInitialMessagesLoaded] = useState(false)
|
||||
|
||||
|
@ -52,12 +57,12 @@ function ChatPane({ resetUnreadMessages, chatIsOpen }) {
|
|||
<MessageList
|
||||
messages={messages}
|
||||
userId={userId}
|
||||
resetUnreadMessages={resetUnreadMessages}
|
||||
resetUnreadMessages={resetUnreadMessageCount}
|
||||
/>
|
||||
</div>
|
||||
</InfiniteScroll>
|
||||
<MessageInput
|
||||
resetUnreadMessages={resetUnreadMessages}
|
||||
resetUnreadMessages={resetUnreadMessageCount}
|
||||
sendMessage={sendMessage}
|
||||
/>
|
||||
</aside>
|
||||
|
@ -88,9 +93,4 @@ function Placeholder() {
|
|||
)
|
||||
}
|
||||
|
||||
ChatPane.propTypes = {
|
||||
resetUnreadMessages: PropTypes.func.isRequired,
|
||||
chatIsOpen: PropTypes.bool
|
||||
}
|
||||
|
||||
export default withErrorBoundary(ChatPane)
|
||||
|
|
101
services/web/frontend/js/features/chat/context/chat-context.js
Normal file
101
services/web/frontend/js/features/chat/context/chat-context.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useState,
|
||||
useEffect
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useApplicationContext } from '../../../shared/context/application-context'
|
||||
import { useEditorContext } from '../../../shared/context/editor-context'
|
||||
import { ChatStore } from '../store/chat-store'
|
||||
import useBrowserWindow from '../../../infrastructure/browser-window-hook'
|
||||
|
||||
export const ChatContext = createContext()
|
||||
|
||||
export function ChatProvider({ children }) {
|
||||
const {
|
||||
hasFocus: windowHasFocus,
|
||||
flashTitle,
|
||||
stopFlashingTitle
|
||||
} = useBrowserWindow()
|
||||
const { user } = useApplicationContext()
|
||||
const {
|
||||
projectId,
|
||||
ui: { chatIsOpen }
|
||||
} = useEditorContext()
|
||||
|
||||
const [unreadMessageCount, setUnreadMessageCount] = useState(0)
|
||||
function resetUnreadMessageCount() {
|
||||
setUnreadMessageCount(0)
|
||||
}
|
||||
|
||||
const [atEnd, setAtEnd] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [messages, setMessages] = useState([])
|
||||
|
||||
const [store] = useState(() => new ChatStore(user, projectId))
|
||||
|
||||
useEffect(() => {
|
||||
if (windowHasFocus) {
|
||||
stopFlashingTitle()
|
||||
if (chatIsOpen) {
|
||||
setUnreadMessageCount(0)
|
||||
}
|
||||
}
|
||||
if (!windowHasFocus && unreadMessageCount > 0) {
|
||||
flashTitle('New Message')
|
||||
}
|
||||
}, [
|
||||
windowHasFocus,
|
||||
chatIsOpen,
|
||||
unreadMessageCount,
|
||||
flashTitle,
|
||||
stopFlashingTitle
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
function updateState() {
|
||||
setAtEnd(store.atEnd)
|
||||
setLoading(store.loading)
|
||||
setMessages(store.messages)
|
||||
}
|
||||
|
||||
function handleNewMessage() {
|
||||
setUnreadMessageCount(prevCount => prevCount + 1)
|
||||
}
|
||||
|
||||
store.on('updated', updateState)
|
||||
store.on('message-received', handleNewMessage)
|
||||
|
||||
updateState()
|
||||
|
||||
return () => store.destroy()
|
||||
}, [store])
|
||||
|
||||
const loadMoreMessages = useCallback(() => store.loadMoreMessages(), [store])
|
||||
const sendMessage = useCallback(message => store.sendMessage(message), [
|
||||
store
|
||||
])
|
||||
|
||||
const value = {
|
||||
userId: user.id,
|
||||
atEnd,
|
||||
loading,
|
||||
messages,
|
||||
unreadMessageCount,
|
||||
resetUnreadMessageCount,
|
||||
loadMoreMessages,
|
||||
sendMessage
|
||||
}
|
||||
|
||||
return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>
|
||||
}
|
||||
|
||||
ChatProvider.propTypes = {
|
||||
children: PropTypes.any
|
||||
}
|
||||
|
||||
export function useChatContext() {
|
||||
return useContext(ChatContext)
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
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'
|
||||
|
||||
export function useChatStore() {
|
||||
const { user } = useApplicationContext()
|
||||
const { projectId } = useEditorContext()
|
||||
|
||||
const [atEnd, setAtEnd] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [messages, setMessages] = useState([])
|
||||
|
||||
const [store] = useState(() => new ChatStore(user, projectId))
|
||||
const loadMoreMessages = useCallback(() => store.loadMoreMessages(), [store])
|
||||
const sendMessage = useCallback(message => store.sendMessage(message), [
|
||||
store
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
function handleStoreUpdated() {
|
||||
setAtEnd(store.atEnd)
|
||||
setLoading(store.loading)
|
||||
setMessages(store.messages)
|
||||
}
|
||||
store.on('updated', handleStoreUpdated)
|
||||
return () => store.destroy()
|
||||
}, [store])
|
||||
|
||||
return {
|
||||
userId: user.id,
|
||||
atEnd,
|
||||
loading,
|
||||
messages,
|
||||
loadMoreMessages,
|
||||
sendMessage
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
|
||||
function ChatToggleButton({ chatIsOpen, unreadMessageCount, onClick }) {
|
||||
const { t } = useTranslation()
|
||||
const classes = classNames(
|
||||
'btn',
|
||||
'btn-full-height',
|
||||
'btn-full-height-no-border',
|
||||
{ active: chatIsOpen }
|
||||
)
|
||||
|
||||
const hasUnreadMessages = unreadMessageCount > 0
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-is-valid
|
||||
<a role="button" className={classes} href="#" onClick={onClick}>
|
||||
<Icon
|
||||
type="fw"
|
||||
modifier="comment"
|
||||
classes={{ icon: hasUnreadMessages ? 'bounce' : undefined }}
|
||||
/>
|
||||
{hasUnreadMessages ? (
|
||||
<span className="label label-info">{unreadMessageCount}</span>
|
||||
) : null}
|
||||
<p className="toolbar-label">{t('chat')}</p>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
ChatToggleButton.propTypes = {
|
||||
chatIsOpen: PropTypes.bool,
|
||||
unreadMessageCount: PropTypes.number.isRequired,
|
||||
onClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default ChatToggleButton
|
|
@ -1,10 +1,19 @@
|
|||
import React from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ToolbarHeader from './toolbar-header'
|
||||
import { useEditorContext } from '../../../shared/context/editor-context'
|
||||
import { useChatContext } from '../../chat/context/chat-context'
|
||||
|
||||
function EditorNavigationToolbarRoot({ onShowLeftMenuClick }) {
|
||||
const { cobranding, loading } = useEditorContext()
|
||||
const { cobranding, loading, ui } = useEditorContext()
|
||||
const { resetUnreadMessageCount, unreadMessageCount } = useChatContext()
|
||||
|
||||
const toggleChatOpen = useCallback(() => {
|
||||
if (!ui.chatIsOpen) {
|
||||
resetUnreadMessageCount()
|
||||
}
|
||||
ui.toggleChatOpen()
|
||||
}, [ui, resetUnreadMessageCount])
|
||||
|
||||
// using {display: 'none'} as 1:1 migration from Angular's ng-hide. Using
|
||||
// `loading ? null : <ToolbarHeader/>` causes UI glitches
|
||||
|
@ -13,6 +22,9 @@ function EditorNavigationToolbarRoot({ onShowLeftMenuClick }) {
|
|||
style={loading ? { display: 'none' } : {}}
|
||||
cobranding={cobranding}
|
||||
onShowLeftMenuClick={onShowLeftMenuClick}
|
||||
chatIsOpen={ui.chatIsOpen}
|
||||
unreadMessageCount={unreadMessageCount}
|
||||
toggleChatOpen={toggleChatOpen}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,8 +3,15 @@ import PropTypes from 'prop-types'
|
|||
import MenuButton from './menu-button'
|
||||
import CobrandingLogo from './cobranding-logo'
|
||||
import BackToProjectsButton from './back-to-projects-button'
|
||||
import ChatToggleButton from './chat-toggle-button'
|
||||
|
||||
function ToolbarHeader({ cobranding, onShowLeftMenuClick }) {
|
||||
function ToolbarHeader({
|
||||
cobranding,
|
||||
onShowLeftMenuClick,
|
||||
chatIsOpen,
|
||||
toggleChatOpen,
|
||||
unreadMessageCount
|
||||
}) {
|
||||
return (
|
||||
<header className="toolbar toolbar-header toolbar-with-labels">
|
||||
<div className="toolbar-left">
|
||||
|
@ -12,13 +19,23 @@ function ToolbarHeader({ cobranding, onShowLeftMenuClick }) {
|
|||
{cobranding ? <CobrandingLogo {...cobranding} /> : null}
|
||||
<BackToProjectsButton />
|
||||
</div>
|
||||
<div className="toolbar-right">
|
||||
<ChatToggleButton
|
||||
chatIsOpen={chatIsOpen}
|
||||
onClick={toggleChatOpen}
|
||||
unreadMessageCount={unreadMessageCount}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
ToolbarHeader.propTypes = {
|
||||
onShowLeftMenuClick: PropTypes.func.isRequired,
|
||||
cobranding: PropTypes.object
|
||||
cobranding: PropTypes.object,
|
||||
chatIsOpen: PropTypes.bool,
|
||||
toggleChatOpen: PropTypes.func.isRequired,
|
||||
unreadMessageCount: PropTypes.number.isRequired
|
||||
}
|
||||
|
||||
export default ToolbarHeader
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
let titleIsFlashing = false
|
||||
let originalTitle
|
||||
let flashIntervalHandle
|
||||
|
||||
export function flashTitle(message) {
|
||||
if (document.hasFocus() || titleIsFlashing) {
|
||||
return
|
||||
}
|
||||
|
||||
function swapTitle() {
|
||||
if (window.document.title === originalTitle) {
|
||||
window.document.title = message
|
||||
} else {
|
||||
window.document.title = originalTitle
|
||||
}
|
||||
}
|
||||
|
||||
originalTitle = window.document.title
|
||||
window.document.title = message
|
||||
titleIsFlashing = true
|
||||
flashIntervalHandle = setInterval(swapTitle, 800)
|
||||
}
|
||||
|
||||
export function stopFlashingTitle() {
|
||||
if (!titleIsFlashing) {
|
||||
return
|
||||
}
|
||||
|
||||
clearInterval(flashIntervalHandle)
|
||||
window.document.title = originalTitle
|
||||
originalTitle = undefined
|
||||
titleIsFlashing = false
|
||||
}
|
||||
|
||||
function useBrowserWindow() {
|
||||
const [hasFocus, setHasFocus] = useState(document.hasFocus())
|
||||
|
||||
useEffect(() => {
|
||||
function handleFocusEvent() {
|
||||
setHasFocus(true)
|
||||
}
|
||||
|
||||
function handleBlurEvent() {
|
||||
setHasFocus(false)
|
||||
}
|
||||
|
||||
window.addEventListener('focus', handleFocusEvent)
|
||||
window.addEventListener('blur', handleBlurEvent)
|
||||
return () => {
|
||||
window.removeEventListener('focus', handleFocusEvent)
|
||||
window.removeEventListener('blur', handleBlurEvent)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { hasFocus, flashTitle, stopFlashingTitle }
|
||||
}
|
||||
|
||||
export default useBrowserWindow
|
|
@ -7,9 +7,29 @@ App.controller('ReactRootContextController', function($scope, ide) {
|
|||
ide.$scope.$watch('state.loading', editorLoading => {
|
||||
$scope.editorLoading = editorLoading
|
||||
})
|
||||
|
||||
$scope.setChatIsOpenAngular = value => {
|
||||
ide.$scope.$applyAsync(() => {
|
||||
ide.$scope.ui.chatOpen = value
|
||||
})
|
||||
}
|
||||
|
||||
// we need to pass `$scope.ui.chatOpen` to Angular while both React and Angular
|
||||
// Navigation Toolbars exist in the codebase. Once the Angular version is removed,
|
||||
// the React Navigation Toolbar will be the only source of truth for the open/closed state,
|
||||
// but `setChatIsOpenAngular` will still need to exist since Angular is responsible of the
|
||||
// global layout
|
||||
$scope.chatIsOpenAngular = ide.$scope.ui.chatOpen
|
||||
ide.$scope.$watch('ui.chatOpen', value => {
|
||||
$scope.chatIsOpenAngular = value
|
||||
})
|
||||
})
|
||||
|
||||
App.component(
|
||||
'sharedContextReact',
|
||||
react2angular(rootContext.component, ['editorLoading'])
|
||||
react2angular(rootContext.component, [
|
||||
'editorLoading',
|
||||
'setChatIsOpenAngular',
|
||||
'chatIsOpenAngular'
|
||||
])
|
||||
)
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
import React, { createContext, useContext } from 'react'
|
||||
import React, { createContext, useCallback, useContext, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import usePersistedState from '../../infrastructure/persisted-state-hook'
|
||||
|
||||
export const EditorContext = createContext()
|
||||
|
||||
export function EditorProvider({ children, loading }) {
|
||||
export function EditorProvider({
|
||||
children,
|
||||
loading,
|
||||
chatIsOpenAngular,
|
||||
setChatIsOpenAngular
|
||||
}) {
|
||||
const cobranding = window.brandVariation
|
||||
? {
|
||||
logoImgUrl: window.brandVariation.logo_url,
|
||||
|
@ -17,11 +23,37 @@ export function EditorProvider({ children, loading }) {
|
|||
? window._ide.$scope.project.owner._id
|
||||
: null
|
||||
|
||||
const [chatIsOpen, setChatIsOpen] = usePersistedState(
|
||||
'editor.ui.chat.open',
|
||||
false
|
||||
)
|
||||
|
||||
const toggleChatOpen = useCallback(() => {
|
||||
setChatIsOpen(!chatIsOpen)
|
||||
setChatIsOpenAngular(!chatIsOpen)
|
||||
}, [chatIsOpen, setChatIsOpenAngular, setChatIsOpen])
|
||||
|
||||
// updates React's `chatIsOpen` state when the chat is opened by Angular.
|
||||
// In order to prevent race conditions with `toggleChatOpen` it's not a 1:1 binding:
|
||||
// Angular forces the React state to `true`, but can only set it to `false` when
|
||||
// the React state is explicitly `true`.
|
||||
useEffect(() => {
|
||||
if (chatIsOpenAngular) {
|
||||
setChatIsOpen(true)
|
||||
} else if (chatIsOpen) {
|
||||
setChatIsOpen(false)
|
||||
}
|
||||
}, [chatIsOpenAngular, chatIsOpen, setChatIsOpen])
|
||||
|
||||
const editorContextValue = {
|
||||
cobranding,
|
||||
loading,
|
||||
projectId: window.project_id,
|
||||
isProjectOwner: ownerId === window.user.id
|
||||
isProjectOwner: ownerId === window.user.id,
|
||||
ui: {
|
||||
chatIsOpen,
|
||||
toggleChatOpen
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -33,7 +65,9 @@ export function EditorProvider({ children, loading }) {
|
|||
|
||||
EditorProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
loading: PropTypes.bool
|
||||
loading: PropTypes.bool,
|
||||
chatIsOpenAngular: PropTypes.bool,
|
||||
setChatIsOpenAngular: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export function useEditorContext() {
|
||||
|
|
|
@ -3,18 +3,32 @@ import PropTypes from 'prop-types'
|
|||
import { ApplicationProvider } from './application-context'
|
||||
import { EditorProvider } from './editor-context'
|
||||
import createSharedContext from 'react2angular-shared-context'
|
||||
import { ChatProvider } from '../../features/chat/context/chat-context'
|
||||
|
||||
export function ContextRoot({ children, editorLoading }) {
|
||||
export function ContextRoot({
|
||||
children,
|
||||
editorLoading,
|
||||
chatIsOpenAngular,
|
||||
setChatIsOpenAngular
|
||||
}) {
|
||||
return (
|
||||
<ApplicationProvider>
|
||||
<EditorProvider loading={editorLoading}>{children}</EditorProvider>
|
||||
<EditorProvider
|
||||
loading={editorLoading}
|
||||
chatIsOpenAngular={chatIsOpenAngular}
|
||||
setChatIsOpenAngular={setChatIsOpenAngular}
|
||||
>
|
||||
<ChatProvider>{children}</ChatProvider>
|
||||
</EditorProvider>
|
||||
</ApplicationProvider>
|
||||
)
|
||||
}
|
||||
|
||||
ContextRoot.propTypes = {
|
||||
children: PropTypes.any,
|
||||
editorLoading: PropTypes.bool
|
||||
editorLoading: PropTypes.bool,
|
||||
chatIsOpenAngular: PropTypes.bool,
|
||||
setChatIsOpenAngular: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export const rootContext = createSharedContext(ContextRoot)
|
||||
|
|
|
@ -4,7 +4,7 @@ import { screen, waitForElementToBeRemoved } from '@testing-library/react'
|
|||
import fetchMock from 'fetch-mock'
|
||||
|
||||
import ChatPane from '../../../../../frontend/js/features/chat/components/chat-pane'
|
||||
import { renderWithEditorContext } from '../../../helpers/render-with-context'
|
||||
import { renderWithChatContext } from '../../../helpers/render-with-context'
|
||||
import {
|
||||
stubChatStore,
|
||||
stubMathJax,
|
||||
|
@ -37,6 +37,7 @@ describe('<ChatPane />', function() {
|
|||
]
|
||||
|
||||
beforeEach(function() {
|
||||
global.localStorage.clear()
|
||||
stubChatStore({ user: currentUser })
|
||||
stubUIConfig()
|
||||
stubMathJax()
|
||||
|
@ -53,9 +54,7 @@ describe('<ChatPane />', function() {
|
|||
it('renders multiple messages', async function() {
|
||||
fetchMock.get(/messages/, testMessages)
|
||||
// unmounting before `beforeEach` block is executed is required to prevent cleanup errors
|
||||
const { unmount } = renderWithEditorContext(
|
||||
<ChatPane resetUnreadMessages={() => {}} chatIsOpen />
|
||||
)
|
||||
const { unmount } = renderWithChatContext(<ChatPane />)
|
||||
|
||||
await screen.findByText('a message')
|
||||
await screen.findByText('another message')
|
||||
|
@ -64,9 +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={() => {}} chatIsOpen />
|
||||
)
|
||||
const { unmount } = renderWithChatContext(<ChatPane />)
|
||||
await waitForElementToBeRemoved(() => screen.getByText('Loading…'))
|
||||
unmount()
|
||||
})
|
||||
|
@ -74,18 +71,14 @@ describe('<ChatPane />', function() {
|
|||
describe('"send your first message" placeholder', function() {
|
||||
it('is rendered when there are no messages ', async function() {
|
||||
fetchMock.get(/messages/, [])
|
||||
const { unmount } = renderWithEditorContext(
|
||||
<ChatPane resetUnreadMessages={() => {}} chatIsOpen />
|
||||
)
|
||||
const { unmount } = renderWithChatContext(<ChatPane />)
|
||||
await screen.findByText('Send your first message to your collaborators')
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('is not rendered when messages are displayed', function() {
|
||||
fetchMock.get(/messages/, testMessages)
|
||||
const { unmount } = renderWithEditorContext(
|
||||
<ChatPane resetUnreadMessages={() => {}} chatIsOpen />
|
||||
)
|
||||
const { unmount } = renderWithChatContext(<ChatPane />)
|
||||
expect(
|
||||
screen.queryByText('Send your first message to your collaborators')
|
||||
).to.not.exist
|
||||
|
|
|
@ -23,13 +23,13 @@ describe('<MessageList />', function() {
|
|||
function createMessages() {
|
||||
return [
|
||||
{
|
||||
id: 'test_msg_1',
|
||||
id: '1',
|
||||
contents: ['a message'],
|
||||
user: currentUser,
|
||||
timestamp: new Date().getTime()
|
||||
},
|
||||
{
|
||||
id: 'test_msg_2',
|
||||
id: '2',
|
||||
contents: ['another message'],
|
||||
user: currentUser,
|
||||
timestamp: new Date().getTime()
|
||||
|
|
|
@ -3,6 +3,7 @@ import { render } from '@testing-library/react'
|
|||
import { ApplicationProvider } from '../../../frontend/js/shared/context/application-context'
|
||||
import { EditorProvider } from '../../../frontend/js/shared/context/editor-context'
|
||||
import sinon from 'sinon'
|
||||
import { ChatProvider } from '../../../frontend/js/features/chat/context/chat-context'
|
||||
|
||||
export function renderWithEditorContext(
|
||||
children,
|
||||
|
@ -25,7 +26,17 @@ export function renderWithEditorContext(
|
|||
}
|
||||
return render(
|
||||
<ApplicationProvider>
|
||||
<EditorProvider>{children}</EditorProvider>
|
||||
<EditorProvider setChatIsOpen={() => {}} setChatIsOpenAngular={() => {}}>
|
||||
{children}
|
||||
</EditorProvider>
|
||||
</ApplicationProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export function renderWithChatContext(children, { user, projectId } = {}) {
|
||||
global.localStorage.setItem('editor.ui.chat.open', true)
|
||||
return renderWithEditorContext(<ChatProvider>{children}</ChatProvider>, {
|
||||
user,
|
||||
projectId
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue