diff options
Diffstat (limited to 'app/javascript')
9 files changed, 296 insertions, 118 deletions
diff --git a/app/javascript/mastodon/components/autosuggest_input.js b/app/javascript/mastodon/components/autosuggest_input.js index bb8ab60db..4b4aa8f0e 100644 --- a/app/javascript/mastodon/components/autosuggest_input.js +++ b/app/javascript/mastodon/components/autosuggest_input.js @@ -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, }; diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index fff329106..0731abcf4 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -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 }) }}> diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index 77c27ac6b..a671578a0 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -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> ); } diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 63feeac45..47cea3e3a 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -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, ]; diff --git a/app/javascript/mastodon/features/ui/components/notifications_counter_icon.js b/app/javascript/mastodon/features/ui/components/notifications_counter_icon.js new file mode 100644 index 000000000..deb907866 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/notifications_counter_icon.js @@ -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); diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js index 1b2bb7781..979b782bb 100644 --- a/app/javascript/mastodon/features/ui/components/tabs_bar.js +++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js @@ -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>, diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 1fcea779d..6d5279157 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -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> diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index a0eea137f..419c313af 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -14,6 +14,8 @@ const initialState = ImmutableMap({ skinTone: 1, + forceSingleColumn: false, + home: ImmutableMap({ shows: ImmutableMap({ reblog: true, diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index ab466f944..9acf0acfc 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -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; + } +} |