message-content.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import { useRef, useEffect, type FC, useCallback, useState } from 'react'
  2. import Linkify from 'react-linkify'
  3. import useIsMounted from '../../../shared/hooks/use-is-mounted'
  4. import { loadMathJax } from '../../mathjax/load-mathjax'
  5. import { debugConsole } from '@/utils/debugging'
  6. import { Message, useChatContext } from '@/features/chat/context/chat-context'
  7. import OLButton from '@/shared/components/ol/ol-button'
  8. import { useTranslation } from 'react-i18next'
  9. import AutoExpandingTextArea from '@/shared/components/auto-expanding-text-area'
  10. const MessageContent: FC<{
  11. content: Message['content']
  12. messageId: Message['id']
  13. edited: Message['edited']
  14. }> = ({ content, messageId, edited }) => {
  15. const { t } = useTranslation()
  16. const root = useRef<HTMLDivElement | null>(null)
  17. const mounted = useIsMounted()
  18. const { idOfMessageBeingEdited, cancelMessageEdit, editMessage } =
  19. useChatContext()
  20. const [editedContent, setEditedContent] = useState(content)
  21. const editing = idOfMessageBeingEdited === messageId
  22. useEffect(() => {
  23. if (root.current) {
  24. // adds attributes to all the links generated by <Linkify/>, required due to https://github.com/tasti/react-linkify/issues/99
  25. for (const a of root.current.getElementsByTagName('a')) {
  26. a.setAttribute('target', '_blank')
  27. a.setAttribute('rel', 'noreferrer noopener')
  28. }
  29. // MathJax v3 typesetting
  30. loadMathJax()
  31. .then(async MathJax => {
  32. if (mounted.current) {
  33. const element = root.current
  34. try {
  35. await MathJax.typesetPromise([element])
  36. MathJax.typesetClear([element])
  37. } catch (error) {
  38. debugConsole.error(error)
  39. }
  40. }
  41. })
  42. .catch(debugConsole.error)
  43. }
  44. }, [content, mounted])
  45. const completeEdit = useCallback(() => {
  46. editMessage(messageId, editedContent)
  47. }, [editMessage, editedContent, messageId])
  48. const handleKeyDown = useCallback(
  49. (e: React.KeyboardEvent) => {
  50. if (e.key === 'Enter') {
  51. e.preventDefault()
  52. completeEdit()
  53. } else if (e.key === 'Escape') {
  54. e.preventDefault()
  55. cancelMessageEdit()
  56. setEditedContent(content)
  57. }
  58. },
  59. [cancelMessageEdit, completeEdit, content]
  60. )
  61. const handleChange = useCallback(
  62. (e: React.ChangeEvent<HTMLTextAreaElement>) => {
  63. setEditedContent(e.target.value)
  64. },
  65. []
  66. )
  67. const handleAutoFocus = useCallback(
  68. (textarea: HTMLTextAreaElement) => textarea.select(),
  69. []
  70. )
  71. return editing ? (
  72. <>
  73. <AutoExpandingTextArea
  74. value={editedContent}
  75. style={{ width: '100%' }}
  76. onKeyDown={handleKeyDown}
  77. onChange={handleChange}
  78. autoFocus // eslint-disable-line jsx-a11y/no-autofocus
  79. onAutoFocus={handleAutoFocus}
  80. />
  81. <br />
  82. <OLButton
  83. size="sm"
  84. variant="secondary"
  85. onClick={() => {
  86. cancelMessageEdit()
  87. setEditedContent(content)
  88. }}
  89. >
  90. {t('cancel')}
  91. </OLButton>
  92. <OLButton size="sm" variant="secondary" onClick={() => completeEdit()}>
  93. {t('save')}
  94. </OLButton>
  95. </>
  96. ) : (
  97. <p ref={root} translate="no">
  98. <Linkify>{content}</Linkify>
  99. {edited ? (
  100. <>
  101. {' '}
  102. <span className="message-edited">({t('edited')})</span>
  103. </>
  104. ) : null}
  105. </p>
  106. )
  107. }
  108. export default MessageContent