@ -8,7 +8,7 @@ import { connect } from 'react-redux';
import { Redirect , withRouter } from 'react-router-dom' ;
import { isMobile } from '../../is_mobile' ;
import { debounce } from 'lodash' ;
import { uploadCompose } from '../../actions/compose' ;
import { uploadCompose , resetCompose } from '../../actions/compose' ;
import { refreshHomeTimeline } from '../../actions/timelines' ;
import { refreshNotifications } from '../../actions/notifications' ;
import { clearHeight } from '../../actions/height_cache' ;
@ -37,15 +37,43 @@ import {
Mutes ,
PinnedStatuses ,
} from './util/async-components' ;
import { HotKeys } from 'react-hotkeys' ;
// Dummy import, to make sure that <Status /> ends up in the application bundle.
// Without this it ends up in ~8 very commonly used bundles.
import '../../components/status' ;
const mapStateToProps = state => ( {
me : state . getIn ( [ 'meta' , 'me' ] ) ,
isComposing : state . getIn ( [ 'compose' , 'is_composing' ] ) ,
} ) ;
const keyMap = {
new : 'n' ,
search : 's' ,
forceNew : 'option+n' ,
focusColumn : [ '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' ] ,
reply : 'r' ,
favourite : 'f' ,
boost : 'b' ,
mention : 'm' ,
open : [ 'enter' , 'o' ] ,
openProfile : 'p' ,
moveDown : [ 'down' , 'j' ] ,
moveUp : [ 'up' , 'k' ] ,
back : 'backspace' ,
goToHome : 'g h' ,
goToNotifications : 'g n' ,
goToLocal : 'g l' ,
goToFederated : 'g t' ,
goToStart : 'g s' ,
goToFavourites : 'g f' ,
goToPinned : 'g p' ,
goToProfile : 'g u' ,
goToBlocked : 'g b' ,
goToMuted : 'g m' ,
} ;
@ connect ( mapStateToProps )
@ withRouter
export default class UI extends React . Component {
@ -58,6 +86,7 @@ export default class UI extends React.Component {
dispatch : PropTypes . func . isRequired ,
children : PropTypes . node ,
isComposing : PropTypes . bool ,
me : PropTypes . string ,
location : PropTypes . object ,
} ;
@ -155,6 +184,12 @@ export default class UI extends React.Component {
this . props . dispatch ( refreshNotifications ( ) ) ;
}
componentDidMount ( ) {
this . hotkeys . _ _mousetrap _ _ . stopCallback = ( e , element ) => {
return ! ( e . altKey || e . ctrlKey || e . shiftKey || e . metaKey ) && [ 'TEXTAREA' , 'SELECT' , 'INPUT' ] . includes ( element . tagName ) ;
} ;
}
shouldComponentUpdate ( nextProps ) {
if ( nextProps . isComposing !== this . props . isComposing ) {
// Avoid expensive update just to toggle a class
@ -191,52 +226,160 @@ export default class UI extends React.Component {
this . columnsAreaNode = c . getWrappedInstance ( ) . getWrappedInstance ( ) ;
}
setOverlayRef = c => {
this . overlay = c ;
handleHotkeyNew = e => {
e . preventDefault ( ) ;
const element = this . node . querySelector ( '.compose-form__autosuggest-wrapper textarea' ) ;
if ( element ) {
element . focus ( ) ;
}
}
handleHotkeySearch = e => {
e . preventDefault ( ) ;
const element = this . node . querySelector ( '.search__input' ) ;
if ( element ) {
element . focus ( ) ;
}
}
handleHotkeyForceNew = e => {
this . handleHotkeyNew ( e ) ;
this . props . dispatch ( resetCompose ( ) ) ;
}
handleHotkeyFocusColumn = e => {
const index = ( e . key * 1 ) + 1 ; // First child is drawer, skip that
const column = this . node . querySelector ( ` .column:nth-child( ${ index } ) ` ) ;
if ( column ) {
const status = column . querySelector ( '.focusable' ) ;
if ( status ) {
status . focus ( ) ;
}
}
}
handleHotkeyBack = ( ) => {
if ( window . history && window . history . length === 1 ) {
this . context . router . history . push ( '/' ) ;
} else {
this . context . router . history . goBack ( ) ;
}
}
setHotkeysRef = c => {
this . hotkeys = c ;
}
handleHotkeyGoToHome = ( ) => {
this . context . router . history . push ( '/timelines/home' ) ;
}
handleHotkeyGoToNotifications = ( ) => {
this . context . router . history . push ( '/notifications' ) ;
}
handleHotkeyGoToLocal = ( ) => {
this . context . router . history . push ( '/timelines/public/local' ) ;
}
handleHotkeyGoToFederated = ( ) => {
this . context . router . history . push ( '/timelines/public' ) ;
}
handleHotkeyGoToStart = ( ) => {
this . context . router . history . push ( '/getting-started' ) ;
}
handleHotkeyGoToFavourites = ( ) => {
this . context . router . history . push ( '/favourites' ) ;
}
handleHotkeyGoToPinned = ( ) => {
this . context . router . history . push ( '/pinned' ) ;
}
handleHotkeyGoToProfile = ( ) => {
this . context . router . history . push ( ` /accounts/ ${ this . props . me } ` ) ;
}
handleHotkeyGoToBlocked = ( ) => {
this . context . router . history . push ( '/blocks' ) ;
}
handleHotkeyGoToMuted = ( ) => {
this . context . router . history . push ( '/mutes' ) ;
}
render ( ) {
const { width , draggingOver } = this . state ;
const { children } = this . props ;
const handlers = {
new : this . handleHotkeyNew ,
search : this . handleHotkeySearch ,
forceNew : this . handleHotkeyForceNew ,
focusColumn : this . handleHotkeyFocusColumn ,
back : this . handleHotkeyBack ,
goToHome : this . handleHotkeyGoToHome ,
goToNotifications : this . handleHotkeyGoToNotifications ,
goToLocal : this . handleHotkeyGoToLocal ,
goToFederated : this . handleHotkeyGoToFederated ,
goToStart : this . handleHotkeyGoToStart ,
goToFavourites : this . handleHotkeyGoToFavourites ,
goToPinned : this . handleHotkeyGoToPinned ,
goToProfile : this . handleHotkeyGoToProfile ,
goToBlocked : this . handleHotkeyGoToBlocked ,
goToMuted : this . handleHotkeyGoToMuted ,
} ;
return (
< div className = 'ui' ref = { this . setRef } >
< TabsBar / >
< ColumnsAreaContainer ref = { this . setColumnsAreaRef } singleColumn = { isMobile ( width ) } >
< WrappedSwitch >
< Redirect from = '/' to = '/getting-started' exact / >
< WrappedRoute path = '/getting-started' component = { GettingStarted } content = { children } / >
< WrappedRoute path = '/timelines/home' component = { HomeTimeline } content = { children } / >
< WrappedRoute path = '/timelines/public' exact component = { PublicTimeline } content = { children } / >
< WrappedRoute path = '/timelines/public/local' component = { CommunityTimeline } content = { children } / >
< WrappedRoute path = '/timelines/tag/:id' component = { HashtagTimeline } content = { children } / >
< WrappedRoute path = '/notifications' component = { Notifications } content = { children } / >
< WrappedRoute path = '/favourites' component = { FavouritedStatuses } content = { children } / >
< WrappedRoute path = '/pinned' component = { PinnedStatuses } content = { children } / >
< WrappedRoute path = '/statuses/new' component = { Compose } content = { children } / >
< WrappedRoute path = '/statuses/:statusId' exact component = { Status } content = { children } / >
< WrappedRoute path = '/statuses/:statusId/reblogs' component = { Reblogs } content = { children } / >
< WrappedRoute path = '/statuses/:statusId/favourites' component = { Favourites } content = { children } / >
< WrappedRoute path = '/accounts/:accountId' exact component = { AccountTimeline } content = { children } / >
< WrappedRoute path = '/accounts/:accountId/followers' component = { Followers } content = { children } / >
< WrappedRoute path = '/accounts/:accountId/following' component = { Following } content = { children } / >
< WrappedRoute path = '/accounts/:accountId/media' component = { AccountGallery } content = { children } / >
< WrappedRoute path = '/follow_requests' component = { FollowRequests } content = { children } / >
< WrappedRoute path = '/blocks' component = { Blocks } content = { children } / >
< WrappedRoute path = '/mutes' component = { Mutes } content = { children } / >
< WrappedRoute component = { GenericNotFound } content = { children } / >
< / W r a p p e d S w i t c h >
< / C o l u m n s A r e a C o n t a i n e r >
< NotificationsContainer / >
< LoadingBarContainer className = 'loading-bar' / >
< ModalContainer / >
< UploadArea active = { draggingOver } onClose = { this . closeUploadModal } / >
< / d i v >
< HotKeys keyMap = { keyMap } handlers = { handlers } ref = { this . setHotkeysRef } >
< div className = 'ui' ref = { this . setRef } >
< TabsBar / >
< ColumnsAreaContainer ref = { this . setColumnsAreaRef } singleColumn = { isMobile ( width ) } >
< WrappedSwitch >
< Redirect from = '/' to = '/getting-started' exact / >
< WrappedRoute path = '/getting-started' component = { GettingStarted } content = { children } / >
< WrappedRoute path = '/timelines/home' component = { HomeTimeline } content = { children } / >
< WrappedRoute path = '/timelines/public' exact component = { PublicTimeline } content = { children } / >
< WrappedRoute path = '/timelines/public/local' component = { CommunityTimeline } content = { children } / >
< WrappedRoute path = '/timelines/tag/:id' component = { HashtagTimeline } content = { children } / >
< WrappedRoute path = '/notifications' component = { Notifications } content = { children } / >
< WrappedRoute path = '/favourites' component = { FavouritedStatuses } content = { children } / >
< WrappedRoute path = '/pinned' component = { PinnedStatuses } content = { children } / >
< WrappedRoute path = '/statuses/new' component = { Compose } content = { children } / >
< WrappedRoute path = '/statuses/:statusId' exact component = { Status } content = { children } / >
< WrappedRoute path = '/statuses/:statusId/reblogs' component = { Reblogs } content = { children } / >
< WrappedRoute path = '/statuses/:statusId/favourites' component = { Favourites } content = { children } / >
< WrappedRoute path = '/accounts/:accountId' exact component = { AccountTimeline } content = { children } / >
< WrappedRoute path = '/accounts/:accountId/followers' component = { Followers } content = { children } / >
< WrappedRoute path = '/accounts/:accountId/following' component = { Following } content = { children } / >
< WrappedRoute path = '/accounts/:accountId/media' component = { AccountGallery } content = { children } / >
< WrappedRoute path = '/follow_requests' component = { FollowRequests } content = { children } / >
< WrappedRoute path = '/blocks' component = { Blocks } content = { children } / >
< WrappedRoute path = '/mutes' component = { Mutes } content = { children } / >
< WrappedRoute component = { GenericNotFound } content = { children } / >
< / W r a p p e d S w i t c h >
< / C o l u m n s A r e a C o n t a i n e r >
< NotificationsContainer / >
< LoadingBarContainer className = 'loading-bar' / >
< ModalContainer / >
< UploadArea active = { draggingOver } onClose = { this . closeUploadModal } / >
< / d i v >
< / H o t K e y s >
) ;
}