* Move TabsBar rendering logic from CSS to the ColumnsArea component * Add forceSingleColumn mode * Add unread notifications counter to tabs bar * Add toggle to control `forceSingleColumn` * Increase paddings in mobile layout responsively at large sizesmaster^2
@@ -49,7 +49,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { | |||
autoFocus: PropTypes.bool, | |||
className: PropTypes.string, | |||
id: PropTypes.string, | |||
searchTokens: PropTypes.list, | |||
searchTokens: ImmutablePropTypes.list, | |||
maxLength: PropTypes.number, | |||
}; | |||
@@ -106,12 +106,12 @@ class Compose extends React.PureComponent { | |||
<div className='drawer__pager'> | |||
{!isSearchPage && <div className='drawer__inner' onFocus={this.onFocus}> | |||
<NavigationContainer onClose={this.onBlur} /> | |||
<ComposeFormContainer /> | |||
{multiColumn && ( | |||
<div className='drawer__inner__mastodon'> | |||
<img alt='' draggable='false' src={mascot || elephantUIPlane} /> | |||
</div> | |||
)} | |||
<div className='drawer__inner__mastodon'> | |||
<img alt='' draggable='false' src={mascot || elephantUIPlane} /> | |||
</div> | |||
</div>} | |||
<Motion defaultStyle={{ x: isSearchPage ? 0 : -100 }} style={{ x: spring(showSearch || isSearchPage ? 0 : -100, { stiffness: 210, damping: 20 }) }}> | |||
@@ -8,11 +8,13 @@ import PropTypes from 'prop-types'; | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import ImmutablePureComponent from 'react-immutable-pure-component'; | |||
import { me, invitesEnabled, version, profile_directory, repository, source_url } from '../../initial_state'; | |||
import { fetchFollowRequests } from '../../actions/accounts'; | |||
import { fetchFollowRequests } from 'mastodon/actions/accounts'; | |||
import { changeSetting } from 'mastodon/actions/settings'; | |||
import { List as ImmutableList } from 'immutable'; | |||
import { Link } from 'react-router-dom'; | |||
import NavigationBar from '../compose/components/navigation_bar'; | |||
import Icon from 'mastodon/components/icon'; | |||
import Toggle from 'react-toggle'; | |||
const messages = defineMessages({ | |||
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, | |||
@@ -39,10 +41,12 @@ const messages = defineMessages({ | |||
const mapStateToProps = state => ({ | |||
myAccount: state.getIn(['accounts', me]), | |||
unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size, | |||
forceSingleColumn: state.getIn(['settings', 'forceSingleColumn'], false), | |||
}); | |||
const mapDispatchToProps = dispatch => ({ | |||
fetchFollowRequests: () => dispatch(fetchFollowRequests()), | |||
changeForceSingleColumn: checked => dispatch(changeSetting(['forceSingleColumn'], checked)), | |||
}); | |||
const badgeDisplay = (number, limit) => { | |||
@@ -67,6 +71,8 @@ class GettingStarted extends ImmutablePureComponent { | |||
fetchFollowRequests: PropTypes.func.isRequired, | |||
unreadFollowRequests: PropTypes.number, | |||
unreadNotifications: PropTypes.number, | |||
forceSingleColumn: PropTypes.bool, | |||
changeForceSingleColumn: PropTypes.func.isRequired, | |||
}; | |||
componentDidMount () { | |||
@@ -77,8 +83,12 @@ class GettingStarted extends ImmutablePureComponent { | |||
} | |||
} | |||
handleForceSingleColumnChange = ({ target }) => { | |||
this.props.changeForceSingleColumn(target.checked); | |||
} | |||
render () { | |||
const { intl, myAccount, multiColumn, unreadFollowRequests } = this.props; | |||
const { intl, myAccount, multiColumn, unreadFollowRequests, forceSingleColumn } = this.props; | |||
const navItems = []; | |||
let i = 1; | |||
@@ -177,6 +187,11 @@ class GettingStarted extends ImmutablePureComponent { | |||
</p> | |||
</div> | |||
</div> | |||
<label className='navigational-toggle'> | |||
<FormattedMessage id='getting_started.use_simple_layout' defaultMessage='Use simple layout' /> | |||
<Toggle checked={forceSingleColumn} onChange={this.handleForceSingleColumnChange} /> | |||
</label> | |||
</Column> | |||
); | |||
} | |||
@@ -5,7 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import ImmutablePureComponent from 'react-immutable-pure-component'; | |||
import ReactSwipeableViews from 'react-swipeable-views'; | |||
import { links, getIndex, getLink } from './tabs_bar'; | |||
import TabsBar, { links, getIndex, getLink } from './tabs_bar'; | |||
import { Link } from 'react-router-dom'; | |||
import BundleContainer from '../containers/bundle_container'; | |||
@@ -139,7 +139,7 @@ class ColumnsArea extends ImmutablePureComponent { | |||
<ColumnLoading title={title} icon={icon} />; | |||
return ( | |||
<div className='columns-area' key={index}> | |||
<div className='columns-area columns-area--mobile' key={index}> | |||
{view} | |||
</div> | |||
); | |||
@@ -164,13 +164,17 @@ class ColumnsArea extends ImmutablePureComponent { | |||
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>; | |||
return columnIndex !== -1 ? [ | |||
<TabsBar key='tabs' />, | |||
<ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}> | |||
{links.map(this.renderView)} | |||
</ReactSwipeableViews>, | |||
floatingActionButton, | |||
] : [ | |||
<div className='columns-area'>{children}</div>, | |||
<TabsBar key='tabs' />, | |||
<div key='content' className='columns-area columns-area--mobile'>{children}</div>, | |||
floatingActionButton, | |||
]; | |||
@@ -0,0 +1,23 @@ | |||
import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import { connect } from 'react-redux'; | |||
import Icon from 'mastodon/components/icon'; | |||
const mapStateToProps = state => ({ | |||
count: state.getIn(['notifications', 'unread']), | |||
}); | |||
const formatNumber = num => num > 99 ? '99+' : num; | |||
const NotificationsCounterIcon = ({ count }) => ( | |||
<i className='icon-with-badge'> | |||
<Icon id='bell' fixedWidth /> | |||
{count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>} | |||
</i> | |||
); | |||
NotificationsCounterIcon.propTypes = { | |||
count: PropTypes.number.isRequired, | |||
}; | |||
export default connect(mapStateToProps)(NotificationsCounterIcon); |
@@ -5,10 +5,11 @@ import { FormattedMessage, injectIntl } from 'react-intl'; | |||
import { debounce } from 'lodash'; | |||
import { isUserTouching } from '../../../is_mobile'; | |||
import Icon from 'mastodon/components/icon'; | |||
import NotificationsCounterIcon from './notifications_counter_icon'; | |||
export const links = [ | |||
<NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, | |||
<NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><Icon id='bell' fixedWidth /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, | |||
<NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, | |||
<NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>, | |||
<NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>, | |||
@@ -7,7 +7,6 @@ import { Redirect, withRouter } from 'react-router-dom'; | |||
import PropTypes from 'prop-types'; | |||
import NotificationsContainer from './containers/notifications_container'; | |||
import LoadingBarContainer from './containers/loading_bar_container'; | |||
import TabsBar from './components/tabs_bar'; | |||
import ModalContainer from './containers/modal_container'; | |||
import { isMobile } from '../../is_mobile'; | |||
import { debounce } from 'lodash'; | |||
@@ -63,6 +62,7 @@ const mapStateToProps = state => ({ | |||
hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0, | |||
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0, | |||
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null, | |||
forceSingleColumn: state.getIn(['settings', 'forceSingleColumn'], false), | |||
}); | |||
const keyMap = { | |||
@@ -101,6 +101,7 @@ class SwitchingColumnsArea extends React.PureComponent { | |||
children: PropTypes.node, | |||
location: PropTypes.object, | |||
onLayoutChange: PropTypes.func.isRequired, | |||
forceSingleColumn: PropTypes.bool, | |||
}; | |||
state = { | |||
@@ -139,12 +140,13 @@ class SwitchingColumnsArea extends React.PureComponent { | |||
} | |||
render () { | |||
const { children } = this.props; | |||
const { children, forceSingleColumn } = this.props; | |||
const { mobile } = this.state; | |||
const redirect = mobile ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />; | |||
const singleColumn = forceSingleColumn || mobile; | |||
const redirect = singleColumn ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />; | |||
return ( | |||
<ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}> | |||
<ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}> | |||
<WrappedSwitch> | |||
{redirect} | |||
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} /> | |||
@@ -205,6 +207,7 @@ class UI extends React.PureComponent { | |||
location: PropTypes.object, | |||
intl: PropTypes.object.isRequired, | |||
dropdownMenuIsOpen: PropTypes.bool, | |||
forceSingleColumn: PropTypes.bool, | |||
}; | |||
state = { | |||
@@ -453,7 +456,7 @@ class UI extends React.PureComponent { | |||
render () { | |||
const { draggingOver } = this.state; | |||
const { children, isComposing, location, dropdownMenuIsOpen } = this.props; | |||
const { children, isComposing, location, dropdownMenuIsOpen, forceSingleColumn } = this.props; | |||
const handlers = { | |||
help: this.handleHotkeyToggleHelp, | |||
@@ -479,9 +482,7 @@ class UI extends React.PureComponent { | |||
return ( | |||
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused> | |||
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}> | |||
<TabsBar /> | |||
<SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange}> | |||
<SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange} forceSingleColumn={forceSingleColumn}> | |||
{children} | |||
</SwitchingColumnsArea> | |||
@@ -14,6 +14,8 @@ const initialState = ImmutableMap({ | |||
skinTone: 1, | |||
forceSingleColumn: false, | |||
home: ImmutableMap({ | |||
shows: ImmutableMap({ | |||
reblog: true, | |||
@@ -1788,16 +1788,6 @@ a.account__display-name { | |||
} | |||
} | |||
@media screen and (min-width: 360px) { | |||
.columns-area { | |||
padding: 10px; | |||
} | |||
.react-swipeable-view-container .columns-area { | |||
height: calc(100% - 20px) !important; | |||
} | |||
} | |||
.react-swipeable-view-container { | |||
&, | |||
.columns-area, | |||
@@ -1860,36 +1850,6 @@ a.account__display-name { | |||
overflow: hidden; | |||
} | |||
@media screen and (min-width: 360px) { | |||
.tabs-bar { | |||
margin: 10px; | |||
margin-bottom: 0; | |||
} | |||
.getting-started__wrapper, | |||
.getting-started__trends, | |||
.search { | |||
margin-bottom: 10px; | |||
} | |||
} | |||
@media screen and (max-width: 630px) { | |||
.column, | |||
.drawer { | |||
width: 100%; | |||
padding: 0; | |||
} | |||
.columns-area { | |||
flex-direction: column; | |||
} | |||
.search__input, | |||
.autosuggest-textarea__textarea { | |||
font-size: 16px; | |||
} | |||
} | |||
@media screen and (min-width: 631px) { | |||
.columns-area { | |||
padding: 0; | |||
@@ -1920,6 +1880,172 @@ a.account__display-name { | |||
} | |||
} | |||
.tabs-bar { | |||
box-sizing: border-box; | |||
display: flex; | |||
background: lighten($ui-base-color, 8%); | |||
flex: 0 0 auto; | |||
overflow-y: auto; | |||
} | |||
.tabs-bar__link { | |||
display: block; | |||
flex: 1 1 auto; | |||
padding: 15px 10px; | |||
color: $primary-text-color; | |||
text-decoration: none; | |||
text-align: center; | |||
font-size: 14px; | |||
font-weight: 500; | |||
border-bottom: 2px solid lighten($ui-base-color, 8%); | |||
transition: all 50ms linear; | |||
transition-property: border-bottom, background, color; | |||
.fa { | |||
font-weight: 400; | |||
font-size: 16px; | |||
} | |||
&.active { | |||
border-bottom: 2px solid $highlight-text-color; | |||
color: $highlight-text-color; | |||
} | |||
&:hover, | |||
&:focus, | |||
&:active { | |||
@media screen and (min-width: 631px) { | |||
background: lighten($ui-base-color, 14%); | |||
} | |||
} | |||
span { | |||
margin-left: 5px; | |||
display: none; | |||
} | |||
} | |||
@media screen and (min-width: 600px) { | |||
.tabs-bar__link { | |||
span { | |||
display: inline; | |||
} | |||
} | |||
} | |||
.columns-area--mobile { | |||
flex-direction: column; | |||
width: 100%; | |||
max-width: 600px; | |||
margin: 0 auto; | |||
.column, | |||
.drawer { | |||
width: 100%; | |||
height: 100%; | |||
padding: 0; | |||
} | |||
.search__input, | |||
.autosuggest-textarea__textarea { | |||
font-size: 16px; | |||
} | |||
@media screen and (min-width: 360px) { | |||
padding: 10px; | |||
} | |||
@media screen and (min-width: 630px) { | |||
.detailed-status { | |||
padding: 15px; | |||
.media-gallery, | |||
.video-player { | |||
margin-top: 15px; | |||
} | |||
} | |||
.account__header__bar { | |||
padding: 5px 10px; | |||
} | |||
.navigation-bar, | |||
.compose-form { | |||
padding: 15px; | |||
} | |||
.compose-form .compose-form__publish .compose-form__publish-button-wrapper { | |||
padding-top: 15px; | |||
} | |||
.status { | |||
padding: 15px 15px 15px (48px + 15px * 2); | |||
min-height: 48px + 2px; | |||
&__avatar { | |||
left: 15px; | |||
top: 17px; | |||
} | |||
&__content { | |||
padding-top: 5px; | |||
} | |||
&__prepend { | |||
margin-left: 48px + 15px * 2; | |||
padding-top: 15px; | |||
} | |||
&__prepend-icon-wrapper { | |||
left: -32px; | |||
} | |||
.media-gallery, | |||
&__action-bar, | |||
.video-player { | |||
margin-top: 10px; | |||
} | |||
} | |||
} | |||
} | |||
@media screen and (min-width: 360px) { | |||
.tabs-bar { | |||
margin: 10px auto; | |||
margin-bottom: 0; | |||
width: calc(100% - 20px); | |||
max-width: 600px; | |||
} | |||
.react-swipeable-view-container .columns-area--mobile { | |||
height: calc(100% - 20px) !important; | |||
} | |||
.getting-started__wrapper, | |||
.getting-started__trends, | |||
.search { | |||
margin-bottom: 10px; | |||
} | |||
} | |||
.icon-with-badge { | |||
position: relative; | |||
&__badge { | |||
position: absolute; | |||
right: -13px; | |||
top: -13px; | |||
background: $ui-highlight-color; | |||
border: 2px solid lighten($ui-base-color, 8%); | |||
padding: 1px 6px; | |||
border-radius: 6px; | |||
font-size: 10px; | |||
font-weight: 500; | |||
line-height: 14px; | |||
color: $primary-text-color; | |||
} | |||
} | |||
.drawer__pager { | |||
box-sizing: border-box; | |||
padding: 0; | |||
@@ -1952,6 +2078,7 @@ a.account__display-name { | |||
background: lighten($ui-base-color, 13%) url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>') no-repeat bottom / 100% auto; | |||
flex: 1; | |||
min-height: 47px; | |||
display: none; | |||
> img { | |||
display: block; | |||
@@ -1963,6 +2090,19 @@ a.account__display-name { | |||
user-drag: none; | |||
user-select: none; | |||
} | |||
@media screen and (min-height: 640px) { | |||
display: block; | |||
} | |||
} | |||
.navigational-toggle { | |||
padding: 10px; | |||
display: flex; | |||
align-items: center; | |||
justify-content: space-between; | |||
font-size: 14px; | |||
color: $dark-text-color; | |||
} | |||
.pseudo-drawer { | |||
@@ -1989,64 +2129,6 @@ a.account__display-name { | |||
} | |||
} | |||
.tabs-bar { | |||
display: flex; | |||
background: lighten($ui-base-color, 8%); | |||
flex: 0 0 auto; | |||
overflow-y: auto; | |||
} | |||
.tabs-bar__link { | |||
display: block; | |||
flex: 1 1 auto; | |||
padding: 15px 10px; | |||
color: $primary-text-color; | |||
text-decoration: none; | |||
text-align: center; | |||
font-size: 14px; | |||
font-weight: 500; | |||
border-bottom: 2px solid lighten($ui-base-color, 8%); | |||
transition: all 50ms linear; | |||
transition-property: border-bottom, background, color; | |||
.fa { | |||
font-weight: 400; | |||
font-size: 16px; | |||
} | |||
&.active { | |||
border-bottom: 2px solid $highlight-text-color; | |||
color: $highlight-text-color; | |||
} | |||
&:hover, | |||
&:focus, | |||
&:active { | |||
@media screen and (min-width: 631px) { | |||
background: lighten($ui-base-color, 14%); | |||
} | |||
} | |||
span { | |||
margin-left: 5px; | |||
display: none; | |||
} | |||
} | |||
@media screen and (min-width: 600px) { | |||
.tabs-bar__link { | |||
span { | |||
display: inline; | |||
} | |||
} | |||
} | |||
@media screen and (min-width: 631px) { | |||
.tabs-bar { | |||
display: none; | |||
} | |||
} | |||
.scrollable { | |||
overflow-y: scroll; | |||
overflow-x: hidden; | |||
@@ -3190,6 +3272,10 @@ a.status-card.compact:hover { | |||
contain: strict; | |||
} | |||
& > span { | |||
max-width: 400px; | |||
} | |||
a { | |||
color: $highlight-text-color; | |||
text-decoration: none; | |||
@@ -5611,3 +5697,49 @@ noscript { | |||
} | |||
} | |||
} | |||
.layout-toggle { | |||
display: flex; | |||
padding: 5px; | |||
button { | |||
box-sizing: border-box; | |||
flex: 0 0 50%; | |||
background: transparent; | |||
padding: 5px; | |||
border: 0; | |||
position: relative; | |||
&:hover, | |||
&:focus, | |||
&:active { | |||
svg path:first-child { | |||
fill: lighten($ui-base-color, 16%); | |||
} | |||
} | |||
} | |||
svg { | |||
width: 100%; | |||
height: auto; | |||
path:first-child { | |||
fill: lighten($ui-base-color, 12%); | |||
} | |||
path:last-child { | |||
fill: darken($ui-base-color, 14%); | |||
} | |||
} | |||
&__active { | |||
color: $ui-highlight-color; | |||
position: absolute; | |||
top: 50%; | |||
left: 50%; | |||
transform: translate(-50%, -50%); | |||
background: lighten($ui-base-color, 12%); | |||
border-radius: 50%; | |||
padding: 0.35rem; | |||
} | |||
} |