chat.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  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 { RailIndicator } from '../rail-indicator'
  13. import RailPanelHeader from '../rail-panel-header'
  14. const MessageList = lazy(() => import('../../../chat/components/message-list'))
  15. export const ChatIndicator = () => {
  16. const { unreadMessageCount } = useChatContext()
  17. if (unreadMessageCount === 0) {
  18. return null
  19. }
  20. return <RailIndicator count={unreadMessageCount} type="info" />
  21. }
  22. const Loading = () => <FullSizeLoadingSpinner delay={500} className="pt-4" />
  23. export const ChatPane = () => {
  24. const { t } = useTranslation()
  25. const user = useUserContext()
  26. const {
  27. status,
  28. messages,
  29. initialMessagesLoaded,
  30. atEnd,
  31. loadInitialMessages,
  32. loadMoreMessages,
  33. reset,
  34. sendMessage,
  35. markMessagesAsRead,
  36. error,
  37. } = useChatContext()
  38. useEffect(() => {
  39. if (!initialMessagesLoaded) {
  40. loadInitialMessages()
  41. }
  42. }, [loadInitialMessages, initialMessagesLoaded])
  43. const shouldDisplayPlaceholder = status !== 'pending' && messages.length === 0
  44. const messageContentCount = messages.reduce(
  45. (acc, { contents }) => acc + contents.length,
  46. 0
  47. )
  48. if (error) {
  49. // let user try recover from fetch errors
  50. if (error instanceof FetchError) {
  51. return <ChatFallbackError reconnect={reset} />
  52. }
  53. throw error
  54. }
  55. if (!user) {
  56. return null
  57. }
  58. return (
  59. <div className="chat-panel">
  60. <RailPanelHeader title={t('collaborator_chat')} />
  61. <div className="chat-wrapper">
  62. <aside className="chat">
  63. <InfiniteScroll
  64. atEnd={atEnd}
  65. className="messages"
  66. fetchData={loadMoreMessages}
  67. isLoading={status === 'pending'}
  68. itemCount={messageContentCount}
  69. >
  70. <div className={classNames({ 'h-100': shouldDisplayPlaceholder })}>
  71. <h2 className="visually-hidden">{t('chat')}</h2>
  72. <Suspense fallback={<Loading />}>
  73. {status === 'pending' && <Loading />}
  74. {shouldDisplayPlaceholder && <Placeholder />}
  75. <MessageList
  76. messages={messages}
  77. resetUnreadMessages={markMessagesAsRead}
  78. newDesign
  79. />
  80. </Suspense>
  81. </div>
  82. </InfiniteScroll>
  83. <MessageInput
  84. resetUnreadMessages={markMessagesAsRead}
  85. sendMessage={sendMessage}
  86. />
  87. </aside>
  88. </div>
  89. </div>
  90. )
  91. }
  92. function Placeholder() {
  93. const { t } = useTranslation()
  94. return (
  95. <div className="chat-empty-state-placeholder">
  96. <div>
  97. <span className="chat-empty-state-icon">
  98. <MaterialIcon type="forum" />
  99. </span>
  100. </div>
  101. <div>
  102. <div className="chat-empty-state-title">{t('no_messages_yet')}</div>
  103. <div className="chat-empty-state-body">
  104. {t('start_the_conversation_by_saying_hello_or_sharing_an_update')}
  105. </div>
  106. </div>
  107. </div>
  108. )
  109. }