chat-pane.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import ChatFallbackError from '@/features/chat/components/chat-fallback-error'
  2. import InfiniteScroll from '@/features/chat/components/infinite-scroll'
  3. import MessageInput from '@/features/chat/components/message-input'
  4. import { useChatContext } from '@/features/chat/context/chat-context'
  5. import { FetchError } from '@/infrastructure/fetch-json'
  6. import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
  7. import MaterialIcon from '@/shared/components/material-icon'
  8. import { useUserContext } from '@/shared/context/user-context'
  9. import { lazy, Suspense, useEffect } from 'react'
  10. import { useTranslation } from 'react-i18next'
  11. import classNames from 'classnames'
  12. import RailPanelHeader from '@/features/ide-react/components/rail/rail-panel-header'
  13. import withErrorBoundary from '@/infrastructure/error-boundary'
  14. const MessageList = lazy(() => import('./message-list'))
  15. const Loading = () => <FullSizeLoadingSpinner delay={500} className="pt-4" />
  16. export const ChatPane = () => {
  17. const { t } = useTranslation()
  18. const user = useUserContext()
  19. const {
  20. status,
  21. messages,
  22. initialMessagesLoaded,
  23. atEnd,
  24. loadInitialMessages,
  25. loadMoreMessages,
  26. reset,
  27. sendMessage,
  28. markMessagesAsRead,
  29. error,
  30. } = useChatContext()
  31. useEffect(() => {
  32. if (!initialMessagesLoaded) {
  33. loadInitialMessages()
  34. }
  35. }, [loadInitialMessages, initialMessagesLoaded])
  36. const shouldDisplayPlaceholder = status !== 'pending' && messages.length === 0
  37. if (error) {
  38. // let user try recover from fetch errors
  39. if (error instanceof FetchError) {
  40. return <ChatFallbackError reconnect={reset} />
  41. }
  42. throw error
  43. }
  44. if (!user) {
  45. return null
  46. }
  47. return (
  48. <div className="chat-panel">
  49. <RailPanelHeader title={t('collaborator_chat')} />
  50. <div className="chat-wrapper">
  51. <aside className="chat" aria-label={t('chat')}>
  52. <InfiniteScroll
  53. atEnd={atEnd}
  54. className="messages"
  55. fetchData={loadMoreMessages}
  56. isLoading={status === 'pending'}
  57. itemCount={messages.length}
  58. >
  59. <div className={classNames({ 'h-100': shouldDisplayPlaceholder })}>
  60. <h2 className="visually-hidden">{t('chat')}</h2>
  61. <Suspense fallback={<Loading />}>
  62. {status === 'pending' && <Loading />}
  63. {shouldDisplayPlaceholder && <Placeholder />}
  64. <MessageList
  65. messages={messages}
  66. resetUnreadMessages={markMessagesAsRead}
  67. />
  68. </Suspense>
  69. </div>
  70. </InfiniteScroll>
  71. <MessageInput
  72. resetUnreadMessages={markMessagesAsRead}
  73. sendMessage={sendMessage}
  74. />
  75. </aside>
  76. </div>
  77. </div>
  78. )
  79. }
  80. function Placeholder() {
  81. const { t } = useTranslation()
  82. return (
  83. <div className="chat-empty-state-placeholder">
  84. <div>
  85. <span className="chat-empty-state-icon">
  86. <MaterialIcon type="forum" />
  87. </span>
  88. </div>
  89. <div>
  90. <div className="chat-empty-state-title">{t('no_messages_yet')}</div>
  91. <div className="chat-empty-state-body">
  92. {t('start_the_conversation_by_saying_hello_or_sharing_an_update')}
  93. </div>
  94. </div>
  95. </div>
  96. )
  97. }
  98. export default withErrorBoundary(ChatPane, () => <ChatFallbackError />)