您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

Chat.js 6.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. // @flow
  2. import React from 'react';
  3. import { translate } from '../../../base/i18n';
  4. import { connect } from '../../../base/redux';
  5. import { toggleChat } from '../../actions.web';
  6. import AbstractChat, {
  7. _mapStateToProps,
  8. type Props
  9. } from '../AbstractChat';
  10. import ChatDialog from './ChatDialog';
  11. import Header from './ChatDialogHeader';
  12. import ChatInput from './ChatInput';
  13. import DisplayNameForm from './DisplayNameForm';
  14. import KeyboardAvoider from './KeyboardAvoider';
  15. import MessageContainer from './MessageContainer';
  16. import MessageRecipient from './MessageRecipient';
  17. import TouchmoveHack from './TouchmoveHack';
  18. /**
  19. * React Component for holding the chat feature in a side panel that slides in
  20. * and out of view.
  21. */
  22. class Chat extends AbstractChat<Props> {
  23. /**
  24. * Whether or not the {@code Chat} component is off-screen, having finished
  25. * its hiding animation.
  26. */
  27. _isExited: boolean;
  28. /**
  29. * Reference to the React Component for displaying chat messages. Used for
  30. * scrolling to the end of the chat messages.
  31. */
  32. _messageContainerRef: Object;
  33. /**
  34. * Initializes a new {@code Chat} instance.
  35. *
  36. * @param {Object} props - The read-only properties with which the new
  37. * instance is to be initialized.
  38. */
  39. constructor(props: Props) {
  40. super(props);
  41. this._isExited = true;
  42. this._messageContainerRef = React.createRef();
  43. // Bind event handlers so they are only bound once for every instance.
  44. this._renderPanelContent = this._renderPanelContent.bind(this);
  45. this._onChatInputResize = this._onChatInputResize.bind(this);
  46. this._onEscClick = this._onEscClick.bind(this);
  47. this._onToggleChat = this._onToggleChat.bind(this);
  48. }
  49. /**
  50. * Implements {@code Component#componentDidMount}.
  51. *
  52. * @inheritdoc
  53. */
  54. componentDidMount() {
  55. this._scrollMessageContainerToBottom(true);
  56. }
  57. /**
  58. * Implements {@code Component#componentDidUpdate}.
  59. *
  60. * @inheritdoc
  61. */
  62. componentDidUpdate(prevProps) {
  63. if (this.props._messages !== prevProps._messages) {
  64. this._scrollMessageContainerToBottom(true);
  65. } else if (this.props._isOpen && !prevProps._isOpen) {
  66. this._scrollMessageContainerToBottom(false);
  67. }
  68. }
  69. _onEscClick: (KeyboardEvent) => void;
  70. /**
  71. * Click handler for the chat sidenav.
  72. *
  73. * @param {KeyboardEvent} event - Esc key click to close the popup.
  74. * @returns {void}
  75. */
  76. _onEscClick(event) {
  77. if (event.key === 'Escape' && this.props._isOpen) {
  78. event.preventDefault();
  79. event.stopPropagation();
  80. this._onToggleChat();
  81. }
  82. }
  83. /**
  84. * Implements React's {@link Component#render()}.
  85. *
  86. * @inheritdoc
  87. * @returns {ReactElement}
  88. */
  89. render() {
  90. return (
  91. <>
  92. { this._renderPanelContent() }
  93. </>
  94. );
  95. }
  96. _onChatInputResize: () => void;
  97. /**
  98. * Callback invoked when {@code ChatInput} changes height. Preserves
  99. * displaying the latest message if it is scrolled to.
  100. *
  101. * @private
  102. * @returns {void}
  103. */
  104. _onChatInputResize() {
  105. this._messageContainerRef.current.maybeUpdateBottomScroll();
  106. }
  107. /**
  108. * Returns a React Element for showing chat messages and a form to send new
  109. * chat messages.
  110. *
  111. * @private
  112. * @returns {ReactElement}
  113. */
  114. _renderChat() {
  115. return (
  116. <>
  117. <TouchmoveHack isModal = { this.props._isModal }>
  118. <MessageContainer
  119. messages = { this.props._messages }
  120. ref = { this._messageContainerRef } />
  121. </TouchmoveHack>
  122. <MessageRecipient />
  123. <ChatInput
  124. onResize = { this._onChatInputResize }
  125. onSend = { this._onSendMessage } />
  126. <KeyboardAvoider />
  127. </>
  128. );
  129. }
  130. /**
  131. * Instantiates a React Element to display at the top of {@code Chat} to
  132. * close {@code Chat}.
  133. *
  134. * @private
  135. * @returns {ReactElement}
  136. */
  137. _renderChatHeader() {
  138. return (
  139. <Header
  140. className = 'chat-header'
  141. id = 'chat-header'
  142. onCancel = { this._onToggleChat } />
  143. );
  144. }
  145. _renderPanelContent: () => React$Node | null;
  146. /**
  147. * Renders the contents of the chat panel.
  148. *
  149. * @private
  150. * @returns {ReactElement | null}
  151. */
  152. _renderPanelContent() {
  153. const { _isModal, _isOpen, _showNamePrompt } = this.props;
  154. let ComponentToRender = null;
  155. if (_isOpen) {
  156. if (_isModal) {
  157. ComponentToRender = (
  158. <ChatDialog>
  159. { _showNamePrompt ? <DisplayNameForm /> : this._renderChat() }
  160. </ChatDialog>
  161. );
  162. } else {
  163. ComponentToRender = (
  164. <>
  165. { this._renderChatHeader() }
  166. { _showNamePrompt ? <DisplayNameForm /> : this._renderChat() }
  167. </>
  168. );
  169. }
  170. }
  171. let className = '';
  172. if (_isOpen) {
  173. className = 'slideInExt';
  174. } else if (this._isExited) {
  175. className = 'invisible';
  176. }
  177. return (
  178. <div
  179. aria-haspopup = 'true'
  180. className = { `sideToolbarContainer ${className}` }
  181. id = 'sideToolbarContainer'
  182. onKeyDown = { this._onEscClick } >
  183. { ComponentToRender }
  184. </div>
  185. );
  186. }
  187. /**
  188. * Scrolls the chat messages so the latest message is visible.
  189. *
  190. * @param {boolean} withAnimation - Whether or not to show a scrolling
  191. * animation.
  192. * @private
  193. * @returns {void}
  194. */
  195. _scrollMessageContainerToBottom(withAnimation) {
  196. if (this._messageContainerRef.current) {
  197. this._messageContainerRef.current.scrollToBottom(withAnimation);
  198. }
  199. }
  200. _onSendMessage: (string) => void;
  201. _onToggleChat: () => void;
  202. /**
  203. * Toggles the chat window.
  204. *
  205. * @returns {Function}
  206. */
  207. _onToggleChat() {
  208. this.props.dispatch(toggleChat());
  209. }
  210. }
  211. export default translate(connect(_mapStateToProps)(Chat));