chat.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  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 '@/features/ide-react/components/rail/rail-indicator'
  13. import RailPanelHeader from '@/features/ide-react/components/rail/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. if (error) {
  45. // let user try recover from fetch errors
  46. if (error instanceof FetchError) {
  47. return <ChatFallbackError reconnect={reset} />
  48. }
  49. throw error
  50. }
  51. if (!user) {
  52. return null
  53. }
  54. return (
  55. <div className="chat-panel">
  56. <RailPanelHeader title={t('collaborator_chat')} />
  57. <div className="chat-wrapper">
  58. <aside className="chat" aria-label={t('chat')}>
  59. <InfiniteScroll
  60. atEnd={atEnd}
  61. className="messages"
  62. fetchData={loadMoreMessages}
  63. isLoading={status === 'pending'}
  64. itemCount={messages.length}
  65. >
  66. <div className={classNames({ 'h-100': shouldDisplayPlaceholder })}>
  67. <h2 className="visually-hidden">{t('chat')}</h2>
  68. <Suspense fallback={<Loading />}>
  69. {status === 'pending' && <Loading />}
  70. {shouldDisplayPlaceholder && <Placeholder />}
  71. <MessageList
  72. messages={messages}
  73. resetUnreadMessages={markMessagesAsRead}
  74. newDesign
  75. />
  76. </Suspense>
  77. </div>
  78. </InfiniteScroll>
  79. <MessageInput
  80. resetUnreadMessages={markMessagesAsRead}
  81. sendMessage={sendMessage}
  82. />
  83. </aside>
  84. </div>
  85. </div>
  86. )
  87. }
  88. function Placeholder() {
  89. const { t } = useTranslation()
  90. return (
  91. <div className="chat-empty-state-placeholder">
  92. <div>
  93. <span className="chat-empty-state-icon">
  94. <MaterialIcon type="forum" />
  95. </span>
  96. </div>
  97. <div>
  98. <div className="chat-empty-state-title">{t('no_messages_yet')}</div>
  99. <div className="chat-empty-state-body">
  100. {t('start_the_conversation_by_saying_hello_or_sharing_an_update')}
  101. </div>
  102. </div>
  103. </div>
  104. )
  105. }