autoFocus: PropTypes.bool,
className: PropTypes.string,
id: PropTypes.string,
- searchTokens: PropTypes.list,
+ searchTokens: ImmutablePropTypes.list,
maxLength: PropTypes.number,
};
<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 }) }}>
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' },
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) => {
fetchFollowRequests: PropTypes.func.isRequired,
unreadFollowRequests: PropTypes.number,
unreadNotifications: PropTypes.number,
+ forceSingleColumn: PropTypes.bool,
+ changeForceSingleColumn: PropTypes.func.isRequired,
};
componentDidMount () {
}
}
+ 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;
</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>
);
}
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';
<ColumnLoading title={title} icon={icon} />;
return (
- <div className='columns-area' key={index}>
+ <div className='columns-area columns-area--mobile' key={index}>
{view}
</div>
);
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,
];
--- /dev/null
+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);
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>,
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';
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 = {
children: PropTypes.node,
location: PropTypes.object,
onLayoutChange: PropTypes.func.isRequired,
+ forceSingleColumn: PropTypes.bool,
};
state = {
}
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} />
location: PropTypes.object,
intl: PropTypes.object.isRequired,
dropdownMenuIsOpen: PropTypes.bool,
+ forceSingleColumn: PropTypes.bool,
};
state = {
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,
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>
skinTone: 1,
+ forceSingleColumn: false,
+
home: ImmutableMap({
shows: ImmutableMap({
reblog: true,
}
}
-@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,
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;
}
}
+.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;
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;
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 {
}
}
-.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;
contain: strict;
}
+ & > span {
+ max-width: 400px;
+ }
+
a {
color: $highlight-text-color;
text-decoration: none;
}
}
}
+
+.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;
+ }
+}