chat-pane.js 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import React, { useEffect } from 'react'
  2. import PropTypes from 'prop-types'
  3. import { useTranslation } from 'react-i18next'
  4. import MessageList from './message-list'
  5. import MessageInput from './message-input'
  6. import InfiniteScroll from './infinite-scroll'
  7. import ChatFallbackError from './chat-fallback-error'
  8. import Icon from '../../../shared/components/icon'
  9. import { useLayoutContext } from '../../../shared/context/layout-context'
  10. import { useUserContext } from '../../../shared/context/user-context'
  11. import withErrorBoundary from '../../../infrastructure/error-boundary'
  12. import { FetchError } from '../../../infrastructure/fetch-json'
  13. import { useChatContext } from '../context/chat-context'
  14. const ChatPane = React.memo(function ChatPane() {
  15. const { t } = useTranslation()
  16. const { chatIsOpen } = useLayoutContext({ chatIsOpen: PropTypes.bool })
  17. const user = useUserContext({
  18. id: PropTypes.string.isRequired,
  19. })
  20. const {
  21. status,
  22. messages,
  23. initialMessagesLoaded,
  24. atEnd,
  25. loadInitialMessages,
  26. loadMoreMessages,
  27. reset,
  28. sendMessage,
  29. markMessagesAsRead,
  30. error,
  31. } = useChatContext()
  32. useEffect(() => {
  33. if (chatIsOpen && !initialMessagesLoaded) {
  34. loadInitialMessages()
  35. }
  36. }, [chatIsOpen, loadInitialMessages, initialMessagesLoaded])
  37. const shouldDisplayPlaceholder = status !== 'pending' && messages.length === 0
  38. const messageContentCount = messages.reduce(
  39. (acc, { contents }) => acc + contents.length,
  40. 0
  41. )
  42. if (error) {
  43. // let user try recover from fetch errors
  44. if (error instanceof FetchError) {
  45. return <ChatFallbackError reconnect={reset} />
  46. }
  47. throw error
  48. }
  49. if (!user) {
  50. return null
  51. }
  52. return (
  53. <aside className="chat">
  54. <InfiniteScroll
  55. atEnd={atEnd}
  56. className="messages"
  57. fetchData={loadMoreMessages}
  58. isLoading={status === 'pending'}
  59. itemCount={messageContentCount}
  60. >
  61. <div>
  62. <h2 className="sr-only">{t('chat')}</h2>
  63. {status === 'pending' && <LoadingSpinner />}
  64. {shouldDisplayPlaceholder && <Placeholder />}
  65. <MessageList
  66. messages={messages}
  67. userId={user.id}
  68. resetUnreadMessages={markMessagesAsRead}
  69. />
  70. </div>
  71. </InfiniteScroll>
  72. <MessageInput
  73. resetUnreadMessages={markMessagesAsRead}
  74. sendMessage={sendMessage}
  75. />
  76. </aside>
  77. )
  78. })
  79. function LoadingSpinner() {
  80. const { t } = useTranslation()
  81. return (
  82. <div className="loading">
  83. <Icon type="fw" modifier="refresh" spin />
  84. {` ${t('loading')}…`}
  85. </div>
  86. )
  87. }
  88. function Placeholder() {
  89. const { t } = useTranslation()
  90. return (
  91. <>
  92. <div className="no-messages text-center small">{t('no_messages')}</div>
  93. <div className="first-message text-center">
  94. {t('send_first_message')}
  95. <br />
  96. <Icon type="arrow-down" />
  97. </div>
  98. </>
  99. )
  100. }
  101. export default withErrorBoundary(ChatPane, ChatFallbackError)