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 classNames
from 'classnames';
39 } from './util/async-components';
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 '../../../glitch/components/status';
45 const mapStateToProps
= state
=> ({
46 systemFontUi: state
.getIn(['meta', 'system_font_ui']),
47 layout: state
.getIn(['local_settings', 'layout']),
48 isWide: state
.getIn(['local_settings', 'stretch']),
49 navbarUnder: state
.getIn(['local_settings', 'navbar_under']),
50 isComposing: state
.getIn(['compose', 'is_composing']),
53 @connect(mapStateToProps
)
55 export default class UI
extends React
.PureComponent
{
57 static contextTypes
= {
58 router: PropTypes
.object
.isRequired
,
62 dispatch: PropTypes
.func
.isRequired
,
63 children: PropTypes
.node
,
64 layout: PropTypes
.string
,
65 isWide: PropTypes
.bool
,
66 systemFontUi: PropTypes
.bool
,
67 navbarUnder: PropTypes
.bool
,
68 isComposing: PropTypes
.bool
,
69 location: PropTypes
.object
,
73 width: window
.innerWidth
,
77 handleResize
= debounce(() => {
78 // The cached heights are no longer accurate, invalidate
79 this.props
.dispatch(clearStatusesHeight());
81 this.setState({ width: window
.innerWidth
});
86 handleDragEnter
= (e
) => {
89 if (!this.dragTargets
) {
90 this.dragTargets
= [];
93 if (this.dragTargets
.indexOf(e
.target
) === -1) {
94 this.dragTargets
.push(e
.target
);
97 if (e
.dataTransfer
&& e
.dataTransfer
.types
.includes('Files')) {
98 this.setState({ draggingOver: true });
102 handleDragOver
= (e
) => {
107 e
.dataTransfer
.dropEffect
= 'copy';
115 handleDrop
= (e
) => {
118 this.setState({ draggingOver: false });
120 if (e
.dataTransfer
&& e
.dataTransfer
.files
.length
=== 1) {
121 this.props
.dispatch(uploadCompose(e
.dataTransfer
.files
));
125 handleDragLeave
= (e
) => {
129 this.dragTargets
= this.dragTargets
.filter(el
=> el
!== e
.target
&& this.node
.contains(el
));
131 if (this.dragTargets
.length
> 0) {
135 this.setState({ draggingOver: false });
138 closeUploadModal
= () => {
139 this.setState({ draggingOver: false });
142 handleServiceWorkerPostMessage
= ({ data
}) => {
143 if (data
.type
=== 'navigate') {
144 this.context
.router
.history
.push(data
.path
);
146 console
.warn('Unknown message type:', data
.type
);
150 componentWillMount () {
151 window
.addEventListener('resize', this.handleResize
, { passive: true });
152 document
.addEventListener('dragenter', this.handleDragEnter
, false);
153 document
.addEventListener('dragover', this.handleDragOver
, false);
154 document
.addEventListener('drop', this.handleDrop
, false);
155 document
.addEventListener('dragleave', this.handleDragLeave
, false);
156 document
.addEventListener('dragend', this.handleDragEnd
, false);
158 if ('serviceWorker' in navigator
) {
159 navigator
.serviceWorker
.addEventListener('message', this.handleServiceWorkerPostMessage
);
162 this.props
.dispatch(refreshHomeTimeline());
163 this.props
.dispatch(refreshNotifications());
166 shouldComponentUpdate (nextProps
) {
167 if (nextProps
.isComposing
!== this.props
.isComposing
) {
168 // Avoid expensive update just to toggle a class
169 this.node
.classList
.toggle('is-composing', nextProps
.isComposing
);
170 this.node
.classList
.toggle('navbar-under', nextProps
.navbarUnder
);
175 // Why isn't this working?!?
176 // return super.shouldComponentUpdate(nextProps, nextState);
180 componentDidUpdate (prevProps
) {
181 if (![this.props
.location
.pathname
, '/'].includes(prevProps
.location
.pathname
)) {
182 this.columnsAreaNode
.handleChildrenContentChange();
186 componentWillUnmount () {
187 window
.removeEventListener('resize', this.handleResize
);
188 document
.removeEventListener('dragenter', this.handleDragEnter
);
189 document
.removeEventListener('dragover', this.handleDragOver
);
190 document
.removeEventListener('drop', this.handleDrop
);
191 document
.removeEventListener('dragleave', this.handleDragLeave
);
192 document
.removeEventListener('dragend', this.handleDragEnd
);
199 setColumnsAreaRef
= (c
) => {
200 this.columnsAreaNode
= c
.getWrappedInstance().getWrappedInstance();
204 const { width
, draggingOver
} = this.state
;
205 const { children
, layout
, isWide
, navbarUnder
} = this.props
;
207 const columnsClass
= layout
=> {
210 return 'single-column';
212 return 'multi-columns';
214 return 'auto-columns';
218 const className
= classNames('ui', columnsClass(layout
), {
220 'system-font': this.props
.systemFontUi
,
221 'navbar-under': navbarUnder
,
225 <div className
={className
} ref
={this.setRef
}>
226 {navbarUnder
? null : (<TabsBar
/>)}
227 <ColumnsAreaContainer ref
={this.setColumnsAreaRef
} singleColumn
={isMobile(width
, layout
)}>
229 <Redirect
from='/' to
='/getting-started' exact
/>
230 <WrappedRoute path
='/getting-started' component
={GettingStarted
} content
={children
} />
231 <WrappedRoute path
='/timelines/home' component
={HomeTimeline
} content
={children
} />
232 <WrappedRoute path
='/timelines/public' exact component
={PublicTimeline
} content
={children
} />
233 <WrappedRoute path
='/timelines/public/local' component
={CommunityTimeline
} content
={children
} />
234 <WrappedRoute path
='/timelines/tag/:id' component
={HashtagTimeline
} content
={children
} />
236 <WrappedRoute path
='/notifications' component
={Notifications
} content
={children
} />
237 <WrappedRoute path
='/favourites' component
={FavouritedStatuses
} content
={children
} />
239 <WrappedRoute path
='/statuses/new' component
={Compose
} content
={children
} />
240 <WrappedRoute path
='/statuses/:statusId' exact component
={Status
} content
={children
} />
241 <WrappedRoute path
='/statuses/:statusId/reblogs' component
={Reblogs
} content
={children
} />
242 <WrappedRoute path
='/statuses/:statusId/favourites' component
={Favourites
} content
={children
} />
244 <WrappedRoute path
='/accounts/:accountId' exact component
={AccountTimeline
} content
={children
} />
245 <WrappedRoute path
='/accounts/:accountId/followers' component
={Followers
} content
={children
} />
246 <WrappedRoute path
='/accounts/:accountId/following' component
={Following
} content
={children
} />
247 <WrappedRoute path
='/accounts/:accountId/media' component
={AccountGallery
} content
={children
} />
249 <WrappedRoute path
='/follow_requests' component
={FollowRequests
} content
={children
} />
250 <WrappedRoute path
='/blocks' component
={Blocks
} content
={children
} />
251 <WrappedRoute path
='/mutes' component
={Mutes
} content
={children
} />
253 <WrappedRoute component
={GenericNotFound
} content
={children
} />
255 </ColumnsAreaContainer
>
256 <NotificationsContainer
/>
257 {navbarUnder
? (<TabsBar
/>) : null}
258 <LoadingBarContainer className
='loading-bar' />
260 <UploadArea active
={draggingOver
} onClose
={this.closeUploadModal
} />