]> cat aescling's git repositories - mastodon.git/blob - app/javascript/mastodon/features/ui/index.js
883466602dcda6b38652dfde1247d70d74e76315
[mastodon.git] / app / javascript / mastodon / features / ui / index.js
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 } from '../../actions/compose';
12 import { refreshHomeTimeline } from '../../actions/timelines';
13 import { refreshNotifications } from '../../actions/notifications';
14 import { clearStatusesHeight } from '../../actions/statuses';
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 } from './util/async-components';
39
40 // Dummy import, to make sure that <Status /> ends up in the application bundle.
41 // Without this it ends up in ~8 very commonly used bundles.
42 import '../../../glitch/components/status';
43
44 const mapStateToProps = state => ({
45 systemFontUi: state.getIn(['meta', 'system_font_ui']),
46 layout: state.getIn(['local_settings', 'layout']),
47 isWide: state.getIn(['local_settings', 'stretch']),
48 navbarUnder: state.getIn(['local_settings', 'navbar_under']),
49 isComposing: state.getIn(['compose', 'is_composing']),
50 });
51
52 @connect(mapStateToProps)
53 @withRouter
54 export default class UI extends React.PureComponent {
55
56 static contextTypes = {
57 router: PropTypes.object.isRequired,
58 }
59
60 static propTypes = {
61 dispatch: PropTypes.func.isRequired,
62 children: PropTypes.node,
63 layout: PropTypes.string,
64 isWide: PropTypes.bool,
65 systemFontUi: PropTypes.bool,
66 navbarUnder: PropTypes.bool,
67 isComposing: PropTypes.bool,
68 location: PropTypes.object,
69 };
70
71 state = {
72 width: window.innerWidth,
73 draggingOver: false,
74 };
75
76 handleResize = debounce(() => {
77 // The cached heights are no longer accurate, invalidate
78 this.props.dispatch(clearStatusesHeight());
79
80 this.setState({ width: window.innerWidth });
81 }, 500, {
82 trailing: true,
83 });
84
85 handleDragEnter = (e) => {
86 e.preventDefault();
87
88 if (!this.dragTargets) {
89 this.dragTargets = [];
90 }
91
92 if (this.dragTargets.indexOf(e.target) === -1) {
93 this.dragTargets.push(e.target);
94 }
95
96 if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
97 this.setState({ draggingOver: true });
98 }
99 }
100
101 handleDragOver = (e) => {
102 e.preventDefault();
103 e.stopPropagation();
104
105 try {
106 e.dataTransfer.dropEffect = 'copy';
107 } catch (err) {
108
109 }
110
111 return false;
112 }
113
114 handleDrop = (e) => {
115 e.preventDefault();
116
117 this.setState({ draggingOver: false });
118
119 if (e.dataTransfer && e.dataTransfer.files.length === 1) {
120 this.props.dispatch(uploadCompose(e.dataTransfer.files));
121 }
122 }
123
124 handleDragLeave = (e) => {
125 e.preventDefault();
126 e.stopPropagation();
127
128 this.dragTargets = this.dragTargets.filter(el => el !== e.target && this.node.contains(el));
129
130 if (this.dragTargets.length > 0) {
131 return;
132 }
133
134 this.setState({ draggingOver: false });
135 }
136
137 closeUploadModal = () => {
138 this.setState({ draggingOver: false });
139 }
140
141 handleServiceWorkerPostMessage = ({ data }) => {
142 if (data.type === 'navigate') {
143 this.context.router.history.push(data.path);
144 } else {
145 console.warn('Unknown message type:', data.type);
146 }
147 }
148
149 componentWillMount () {
150 window.addEventListener('resize', this.handleResize, { passive: true });
151 document.addEventListener('dragenter', this.handleDragEnter, false);
152 document.addEventListener('dragover', this.handleDragOver, false);
153 document.addEventListener('drop', this.handleDrop, false);
154 document.addEventListener('dragleave', this.handleDragLeave, false);
155 document.addEventListener('dragend', this.handleDragEnd, false);
156
157 if ('serviceWorker' in navigator) {
158 navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
159 }
160
161 this.props.dispatch(refreshHomeTimeline());
162 this.props.dispatch(refreshNotifications());
163 }
164
165 shouldComponentUpdate (nextProps) {
166 if (nextProps.isComposing !== this.props.isComposing) {
167 // Avoid expensive update just to toggle a class
168 this.node.classList.toggle('is-composing', nextProps.isComposing);
169 this.node.classList.toggle('navbar-under', nextProps.navbarUnder);
170
171 return false;
172 }
173
174 // Why isn't this working?!?
175 // return super.shouldComponentUpdate(nextProps, nextState);
176 return true;
177 }
178
179 componentDidUpdate (prevProps) {
180 if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
181 this.columnsAreaNode.handleChildrenContentChange();
182 }
183 }
184
185 componentWillUnmount () {
186 window.removeEventListener('resize', this.handleResize);
187 document.removeEventListener('dragenter', this.handleDragEnter);
188 document.removeEventListener('dragover', this.handleDragOver);
189 document.removeEventListener('drop', this.handleDrop);
190 document.removeEventListener('dragleave', this.handleDragLeave);
191 document.removeEventListener('dragend', this.handleDragEnd);
192 }
193
194 setRef = (c) => {
195 this.node = c;
196 }
197
198 setColumnsAreaRef = (c) => {
199 this.columnsAreaNode = c.getWrappedInstance().getWrappedInstance();
200 }
201
202 render () {
203 const { width, draggingOver } = this.state;
204 const { children, layout, isWide, navbarUnder } = this.props;
205
206 const columnsClass = layout => {
207 switch (layout) {
208 case 'single':
209 return 'single-column';
210 case 'multiple':
211 return 'multi-columns';
212 default:
213 return 'auto-columns';
214 }
215 };
216
217 const className = classNames('ui', columnsClass(layout), {
218 'wide': isWide,
219 'system-font': this.props.systemFontUi,
220 'navbar-under': navbarUnder,
221 });
222
223 return (
224 <div className={className} ref={this.setRef}>
225 {navbarUnder ? null : (<TabsBar />)}
226 <ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={isMobile(width, layout)}>
227 <WrappedSwitch>
228 <Redirect from='/' to='/getting-started' exact />
229 <WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
230 <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} />
231 <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} />
232 <WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} />
233 <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
234
235 <WrappedRoute path='/notifications' component={Notifications} content={children} />
236 <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
237
238 <WrappedRoute path='/statuses/new' component={Compose} content={children} />
239 <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
240 <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
241 <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
242
243 <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} />
244 <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} />
245 <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} />
246 <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} />
247
248 <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
249 <WrappedRoute path='/blocks' component={Blocks} content={children} />
250 <WrappedRoute path='/mutes' component={Mutes} content={children} />
251
252 <WrappedRoute component={GenericNotFound} content={children} />
253 </WrappedSwitch>
254 </ColumnsAreaContainer>
255 <NotificationsContainer />
256 {navbarUnder ? (<TabsBar />) : null}
257 <LoadingBarContainer className='loading-bar' />
258 <ModalContainer />
259 <UploadArea active={draggingOver} onClose={this.closeUploadModal} />
260 </div>
261 );
262 }
263
264 }
This page took 0.124991 seconds and 2 git commands to generate.