The code powering m.abunchtell.com https://m.abunchtell.com
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.
 
 
 
 

387 lines
12 KiB

  1. import React from 'react';
  2. import NotificationsContainer from './containers/notifications_container';
  3. import PropTypes from 'prop-types';
  4. import LoadingBarContainer from './containers/loading_bar_container';
  5. import TabsBar from './components/tabs_bar';
  6. import ModalContainer from './containers/modal_container';
  7. import { connect } from 'react-redux';
  8. import { Redirect, withRouter } from 'react-router-dom';
  9. import { isMobile } from '../../is_mobile';
  10. import { debounce } from 'lodash';
  11. import { uploadCompose, resetCompose } from '../../actions/compose';
  12. import { refreshHomeTimeline } from '../../actions/timelines';
  13. import { refreshNotifications } from '../../actions/notifications';
  14. import { clearHeight } from '../../actions/height_cache';
  15. import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
  16. import UploadArea from './components/upload_area';
  17. import ColumnsAreaContainer from './containers/columns_area_container';
  18. import {
  19. Compose,
  20. Status,
  21. GettingStarted,
  22. PublicTimeline,
  23. CommunityTimeline,
  24. AccountTimeline,
  25. AccountGallery,
  26. HomeTimeline,
  27. Followers,
  28. Following,
  29. Reblogs,
  30. Favourites,
  31. HashtagTimeline,
  32. Notifications,
  33. FollowRequests,
  34. GenericNotFound,
  35. FavouritedStatuses,
  36. Blocks,
  37. Mutes,
  38. PinnedStatuses,
  39. } from './util/async-components';
  40. import { HotKeys } from 'react-hotkeys';
  41. // Dummy import, to make sure that <Status /> ends up in the application bundle.
  42. // Without this it ends up in ~8 very commonly used bundles.
  43. import '../../components/status';
  44. const mapStateToProps = state => ({
  45. me: state.getIn(['meta', 'me']),
  46. isComposing: state.getIn(['compose', 'is_composing']),
  47. });
  48. const keyMap = {
  49. new: 'n',
  50. search: 's',
  51. forceNew: 'option+n',
  52. focusColumn: ['1', '2', '3', '4', '5', '6', '7', '8', '9'],
  53. reply: 'r',
  54. favourite: 'f',
  55. boost: 'b',
  56. mention: 'm',
  57. open: ['enter', 'o'],
  58. openProfile: 'p',
  59. moveDown: ['down', 'j'],
  60. moveUp: ['up', 'k'],
  61. back: 'backspace',
  62. goToHome: 'g h',
  63. goToNotifications: 'g n',
  64. goToLocal: 'g l',
  65. goToFederated: 'g t',
  66. goToStart: 'g s',
  67. goToFavourites: 'g f',
  68. goToPinned: 'g p',
  69. goToProfile: 'g u',
  70. goToBlocked: 'g b',
  71. goToMuted: 'g m',
  72. };
  73. @connect(mapStateToProps)
  74. @withRouter
  75. export default class UI extends React.Component {
  76. static contextTypes = {
  77. router: PropTypes.object.isRequired,
  78. };
  79. static propTypes = {
  80. dispatch: PropTypes.func.isRequired,
  81. children: PropTypes.node,
  82. isComposing: PropTypes.bool,
  83. me: PropTypes.string,
  84. location: PropTypes.object,
  85. };
  86. state = {
  87. width: window.innerWidth,
  88. draggingOver: false,
  89. };
  90. handleResize = debounce(() => {
  91. // The cached heights are no longer accurate, invalidate
  92. this.props.dispatch(clearHeight());
  93. this.setState({ width: window.innerWidth });
  94. }, 500, {
  95. trailing: true,
  96. });
  97. handleDragEnter = (e) => {
  98. e.preventDefault();
  99. if (!this.dragTargets) {
  100. this.dragTargets = [];
  101. }
  102. if (this.dragTargets.indexOf(e.target) === -1) {
  103. this.dragTargets.push(e.target);
  104. }
  105. if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
  106. this.setState({ draggingOver: true });
  107. }
  108. }
  109. handleDragOver = (e) => {
  110. e.preventDefault();
  111. e.stopPropagation();
  112. try {
  113. e.dataTransfer.dropEffect = 'copy';
  114. } catch (err) {
  115. }
  116. return false;
  117. }
  118. handleDrop = (e) => {
  119. e.preventDefault();
  120. this.setState({ draggingOver: false });
  121. if (e.dataTransfer && e.dataTransfer.files.length === 1) {
  122. this.props.dispatch(uploadCompose(e.dataTransfer.files));
  123. }
  124. }
  125. handleDragLeave = (e) => {
  126. e.preventDefault();
  127. e.stopPropagation();
  128. this.dragTargets = this.dragTargets.filter(el => el !== e.target && this.node.contains(el));
  129. if (this.dragTargets.length > 0) {
  130. return;
  131. }
  132. this.setState({ draggingOver: false });
  133. }
  134. closeUploadModal = () => {
  135. this.setState({ draggingOver: false });
  136. }
  137. handleServiceWorkerPostMessage = ({ data }) => {
  138. if (data.type === 'navigate') {
  139. this.context.router.history.push(data.path);
  140. } else {
  141. console.warn('Unknown message type:', data.type);
  142. }
  143. }
  144. componentWillMount () {
  145. window.addEventListener('resize', this.handleResize, { passive: true });
  146. document.addEventListener('dragenter', this.handleDragEnter, false);
  147. document.addEventListener('dragover', this.handleDragOver, false);
  148. document.addEventListener('drop', this.handleDrop, false);
  149. document.addEventListener('dragleave', this.handleDragLeave, false);
  150. document.addEventListener('dragend', this.handleDragEnd, false);
  151. if ('serviceWorker' in navigator) {
  152. navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
  153. }
  154. this.props.dispatch(refreshHomeTimeline());
  155. this.props.dispatch(refreshNotifications());
  156. }
  157. componentDidMount () {
  158. this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
  159. return !(e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) && ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName);
  160. };
  161. }
  162. shouldComponentUpdate (nextProps) {
  163. if (nextProps.isComposing !== this.props.isComposing) {
  164. // Avoid expensive update just to toggle a class
  165. this.node.classList.toggle('is-composing', nextProps.isComposing);
  166. return false;
  167. }
  168. // Why isn't this working?!?
  169. // return super.shouldComponentUpdate(nextProps, nextState);
  170. return true;
  171. }
  172. componentDidUpdate (prevProps) {
  173. if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
  174. this.columnsAreaNode.handleChildrenContentChange();
  175. }
  176. }
  177. componentWillUnmount () {
  178. window.removeEventListener('resize', this.handleResize);
  179. document.removeEventListener('dragenter', this.handleDragEnter);
  180. document.removeEventListener('dragover', this.handleDragOver);
  181. document.removeEventListener('drop', this.handleDrop);
  182. document.removeEventListener('dragleave', this.handleDragLeave);
  183. document.removeEventListener('dragend', this.handleDragEnd);
  184. }
  185. setRef = c => {
  186. this.node = c;
  187. }
  188. setColumnsAreaRef = c => {
  189. this.columnsAreaNode = c.getWrappedInstance().getWrappedInstance();
  190. }
  191. handleHotkeyNew = e => {
  192. e.preventDefault();
  193. const element = this.node.querySelector('.compose-form__autosuggest-wrapper textarea');
  194. if (element) {
  195. element.focus();
  196. }
  197. }
  198. handleHotkeySearch = e => {
  199. e.preventDefault();
  200. const element = this.node.querySelector('.search__input');
  201. if (element) {
  202. element.focus();
  203. }
  204. }
  205. handleHotkeyForceNew = e => {
  206. this.handleHotkeyNew(e);
  207. this.props.dispatch(resetCompose());
  208. }
  209. handleHotkeyFocusColumn = e => {
  210. const index = (e.key * 1) + 1; // First child is drawer, skip that
  211. const column = this.node.querySelector(`.column:nth-child(${index})`);
  212. if (column) {
  213. const status = column.querySelector('.focusable');
  214. if (status) {
  215. status.focus();
  216. }
  217. }
  218. }
  219. handleHotkeyBack = () => {
  220. if (window.history && window.history.length === 1) {
  221. this.context.router.history.push('/');
  222. } else {
  223. this.context.router.history.goBack();
  224. }
  225. }
  226. setHotkeysRef = c => {
  227. this.hotkeys = c;
  228. }
  229. handleHotkeyGoToHome = () => {
  230. this.context.router.history.push('/timelines/home');
  231. }
  232. handleHotkeyGoToNotifications = () => {
  233. this.context.router.history.push('/notifications');
  234. }
  235. handleHotkeyGoToLocal = () => {
  236. this.context.router.history.push('/timelines/public/local');
  237. }
  238. handleHotkeyGoToFederated = () => {
  239. this.context.router.history.push('/timelines/public');
  240. }
  241. handleHotkeyGoToStart = () => {
  242. this.context.router.history.push('/getting-started');
  243. }
  244. handleHotkeyGoToFavourites = () => {
  245. this.context.router.history.push('/favourites');
  246. }
  247. handleHotkeyGoToPinned = () => {
  248. this.context.router.history.push('/pinned');
  249. }
  250. handleHotkeyGoToProfile = () => {
  251. this.context.router.history.push(`/accounts/${this.props.me}`);
  252. }
  253. handleHotkeyGoToBlocked = () => {
  254. this.context.router.history.push('/blocks');
  255. }
  256. handleHotkeyGoToMuted = () => {
  257. this.context.router.history.push('/mutes');
  258. }
  259. render () {
  260. const { width, draggingOver } = this.state;
  261. const { children } = this.props;
  262. const handlers = {
  263. new: this.handleHotkeyNew,
  264. search: this.handleHotkeySearch,
  265. forceNew: this.handleHotkeyForceNew,
  266. focusColumn: this.handleHotkeyFocusColumn,
  267. back: this.handleHotkeyBack,
  268. goToHome: this.handleHotkeyGoToHome,
  269. goToNotifications: this.handleHotkeyGoToNotifications,
  270. goToLocal: this.handleHotkeyGoToLocal,
  271. goToFederated: this.handleHotkeyGoToFederated,
  272. goToStart: this.handleHotkeyGoToStart,
  273. goToFavourites: this.handleHotkeyGoToFavourites,
  274. goToPinned: this.handleHotkeyGoToPinned,
  275. goToProfile: this.handleHotkeyGoToProfile,
  276. goToBlocked: this.handleHotkeyGoToBlocked,
  277. goToMuted: this.handleHotkeyGoToMuted,
  278. };
  279. return (
  280. <HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef}>
  281. <div className='ui' ref={this.setRef}>
  282. <TabsBar />
  283. <ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={isMobile(width)}>
  284. <WrappedSwitch>
  285. <Redirect from='/' to='/getting-started' exact />
  286. <WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
  287. <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} />
  288. <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} />
  289. <WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} />
  290. <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
  291. <WrappedRoute path='/notifications' component={Notifications} content={children} />
  292. <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
  293. <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
  294. <WrappedRoute path='/statuses/new' component={Compose} content={children} />
  295. <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
  296. <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
  297. <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
  298. <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} />
  299. <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} />
  300. <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} />
  301. <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} />
  302. <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
  303. <WrappedRoute path='/blocks' component={Blocks} content={children} />
  304. <WrappedRoute path='/mutes' component={Mutes} content={children} />
  305. <WrappedRoute component={GenericNotFound} content={children} />
  306. </WrappedSwitch>
  307. </ColumnsAreaContainer>
  308. <NotificationsContainer />
  309. <LoadingBarContainer className='loading-bar' />
  310. <ModalContainer />
  311. <UploadArea active={draggingOver} onClose={this.closeUploadModal} />
  312. </div>
  313. </HotKeys>
  314. );
  315. }
  316. }