You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Chat.js 8.4KB

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