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.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  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. <TouchmoveHack isModal = { true }><PollsPane /></TouchmoveHack>
  121. <KeyboardAvoider />
  122. </>
  123. );
  124. }
  125. return (
  126. <>
  127. {this.props._isPollsEnabled && this._renderTabs()}
  128. <TouchmoveHack
  129. flex = { true }
  130. isModal = { this.props._isModal }>
  131. <MessageContainer
  132. messages = { this.props._messages }
  133. ref = { this._messageContainerRef } />
  134. </TouchmoveHack>
  135. <MessageRecipient />
  136. <ChatInput
  137. onResize = { this._onChatInputResize }
  138. onSend = { this._onSendMessage } />
  139. <KeyboardAvoider />
  140. </>
  141. );
  142. }
  143. /**
  144. * Returns a React Element showing the Chat and Polls tab.
  145. *
  146. * @private
  147. * @returns {ReactElement}
  148. */
  149. _renderTabs() {
  150. return (
  151. <div className = { 'chat-tabs-container' }>
  152. <div
  153. className = { `chat-tab ${
  154. this.props._isPollsTabFocused ? '' : 'chat-tab-focus'
  155. }` }
  156. onClick = { this._onToggleChatTab }>
  157. <span className = { 'chat-tab-title' }>
  158. {this.props.t('chat.tabs.chat')}
  159. </span>
  160. {this.props._isPollsTabFocused
  161. && this.props._nbUnreadMessages > 0 && (
  162. <span className = { 'chat-tab-badge' }>
  163. {this.props._nbUnreadMessages}
  164. </span>
  165. )}
  166. </div>
  167. <div
  168. className = { `chat-tab ${
  169. this.props._isPollsTabFocused ? 'chat-tab-focus' : ''
  170. }` }
  171. onClick = { this._onTogglePollsTab }>
  172. <span className = { 'chat-tab-title' }>
  173. {this.props.t('chat.tabs.polls')}
  174. </span>
  175. {!this.props._isPollsTabFocused
  176. && this.props._nbUnreadPolls > 0 && (
  177. <span className = { 'chat-tab-badge' }>
  178. {this.props._nbUnreadPolls}
  179. </span>
  180. )}
  181. </div>
  182. </div>
  183. );
  184. }
  185. /**
  186. * Instantiates a React Element to display at the top of {@code Chat} to
  187. * close {@code Chat}.
  188. *
  189. * @private
  190. * @returns {ReactElement}
  191. */
  192. _renderChatHeader() {
  193. return (
  194. <Header
  195. className = 'chat-header'
  196. id = 'chat-header'
  197. isPollsEnabled = { this.props._isPollsEnabled }
  198. onCancel = { this._onToggleChat } />
  199. );
  200. }
  201. _renderPanelContent: () => React$Node | null;
  202. /**
  203. * Renders the contents of the chat panel.
  204. *
  205. * @private
  206. * @returns {ReactElement | null}
  207. */
  208. _renderPanelContent() {
  209. const { _isModal, _isOpen, _showNamePrompt } = this.props;
  210. let ComponentToRender = null;
  211. if (_isOpen) {
  212. if (_isModal) {
  213. ComponentToRender = (
  214. <ChatDialog isPollsEnabled = { this.props._isPollsEnabled }>
  215. { _showNamePrompt
  216. ? <DisplayNameForm isPollsEnabled = { this.props._isPollsEnabled } />
  217. : this._renderChat() }
  218. </ChatDialog>
  219. );
  220. } else {
  221. ComponentToRender = (
  222. <>
  223. { this._renderChatHeader() }
  224. { _showNamePrompt
  225. ? <DisplayNameForm isPollsEnabled = { this.props._isPollsEnabled } />
  226. : this._renderChat() }
  227. </>
  228. );
  229. }
  230. }
  231. let className = '';
  232. if (_isOpen) {
  233. className = 'slideInExt';
  234. } else if (this._isExited) {
  235. className = 'invisible';
  236. }
  237. return (
  238. <div
  239. aria-haspopup = 'true'
  240. className = { `sideToolbarContainer ${className}` }
  241. id = 'sideToolbarContainer'
  242. onKeyDown = { this._onEscClick } >
  243. { ComponentToRender }
  244. </div>
  245. );
  246. }
  247. /**
  248. * Scrolls the chat messages so the latest message is visible.
  249. *
  250. * @param {boolean} withAnimation - Whether or not to show a scrolling
  251. * animation.
  252. * @private
  253. * @returns {void}
  254. */
  255. _scrollMessageContainerToBottom(withAnimation) {
  256. if (this._messageContainerRef.current) {
  257. this._messageContainerRef.current.scrollToBottom(withAnimation);
  258. }
  259. }
  260. _onSendMessage: (string) => void;
  261. _onToggleChat: () => void;
  262. /**
  263. * Toggles the chat window.
  264. *
  265. * @returns {Function}
  266. */
  267. _onToggleChat() {
  268. this.props.dispatch(toggleChat());
  269. }
  270. _onTogglePollsTab: () => void;
  271. _onToggleChatTab: () => void;
  272. }
  273. export default translate(connect(_mapStateToProps)(Chat));