Add responsive panels to the single-column layout (#10820)
* Add responsive panels to the single-column layout * Fixes * Fix not being able to save the preference * Fix code style issues * Set max-height on the compose textarea and add a link to relationship manager
This commit is contained in:
		
							parent
							
								
									5cdb4c483f
								
							
						
					
					
						commit
						1e5532e693
					
				
					 26 changed files with 389 additions and 96 deletions
				
			
		|  | @ -49,6 +49,7 @@ class Settings::PreferencesController < Settings::BaseController | |||
|       :setting_hide_network, | ||||
|       :setting_aggregate_reblogs, | ||||
|       :setting_show_application, | ||||
|       :setting_advanced_layout, | ||||
|       notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account), | ||||
|       interactions: %i(must_be_follower must_be_following) | ||||
|     ) | ||||
|  |  | |||
|  | @ -63,6 +63,14 @@ const messages = defineMessages({ | |||
|   uploadErrorPoll:  { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, | ||||
| }); | ||||
| 
 | ||||
| const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 3); | ||||
| 
 | ||||
| export const ensureComposeIsVisible = (getState, routerHistory) => { | ||||
|   if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) { | ||||
|     routerHistory.push('/statuses/new'); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export function changeCompose(text) { | ||||
|   return { | ||||
|     type: COMPOSE_CHANGE, | ||||
|  | @ -77,9 +85,7 @@ export function replyCompose(status, routerHistory) { | |||
|       status: status, | ||||
|     }); | ||||
| 
 | ||||
|     if (!getState().getIn(['compose', 'mounted'])) { | ||||
|       routerHistory.push('/statuses/new'); | ||||
|     } | ||||
|     ensureComposeIsVisible(getState, routerHistory); | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
|  | @ -102,9 +108,7 @@ export function mentionCompose(account, routerHistory) { | |||
|       account: account, | ||||
|     }); | ||||
| 
 | ||||
|     if (!getState().getIn(['compose', 'mounted'])) { | ||||
|       routerHistory.push('/statuses/new'); | ||||
|     } | ||||
|     ensureComposeIsVisible(getState, routerHistory); | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
|  | @ -115,9 +119,7 @@ export function directCompose(account, routerHistory) { | |||
|       account: account, | ||||
|     }); | ||||
| 
 | ||||
|     if (!getState().getIn(['compose', 'mounted'])) { | ||||
|       routerHistory.push('/statuses/new'); | ||||
|     } | ||||
|     ensureComposeIsVisible(getState, routerHistory); | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import { evictStatus } from '../storage/modifier'; | |||
| 
 | ||||
| import { deleteFromTimelines } from './timelines'; | ||||
| import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer'; | ||||
| import { ensureComposeIsVisible } from './compose'; | ||||
| 
 | ||||
| export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; | ||||
| export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; | ||||
|  | @ -139,7 +140,7 @@ export function redraft(status, raw_text) { | |||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function deleteStatus(id, router, withRedraft = false) { | ||||
| export function deleteStatus(id, routerHistory, withRedraft = false) { | ||||
|   return (dispatch, getState) => { | ||||
|     let status = getState().getIn(['statuses', id]); | ||||
| 
 | ||||
|  | @ -156,10 +157,7 @@ export function deleteStatus(id, router, withRedraft = false) { | |||
| 
 | ||||
|       if (withRedraft) { | ||||
|         dispatch(redraft(status, response.data.text)); | ||||
| 
 | ||||
|         if (!getState().getIn(['compose', 'mounted'])) { | ||||
|           router.push('/statuses/new'); | ||||
|         } | ||||
|         ensureComposeIsVisible(getState, routerHistory); | ||||
|       } | ||||
|     }).catch(error => { | ||||
|       dispatch(deleteStatusFail(id, error)); | ||||
|  |  | |||
|  | @ -49,7 +49,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { | |||
|     autoFocus: PropTypes.bool, | ||||
|     className: PropTypes.string, | ||||
|     id: PropTypes.string, | ||||
|     searchTokens: ImmutablePropTypes.list, | ||||
|     searchTokens: PropTypes.arrayOf(PropTypes.string), | ||||
|     maxLength: PropTypes.number, | ||||
|   }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -46,7 +46,7 @@ class ActionBar extends React.PureComponent { | |||
|     return ( | ||||
|       <div className='compose__action-bar'> | ||||
|         <div className='compose__action-bar-dropdown'> | ||||
|           <DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' /> | ||||
|           <DropdownMenuContainer items={menu} icon='chevron-down' size={16} direction='right' /> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ export default class NavigationBar extends ImmutablePureComponent { | |||
|       <div className='navigation-bar'> | ||||
|         <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> | ||||
|           <span style={{ display: 'none' }}>{this.props.account.get('acct')}</span> | ||||
|           <Avatar account={this.props.account} size={40} /> | ||||
|           <Avatar account={this.props.account} size={48} /> | ||||
|         </Permalink> | ||||
| 
 | ||||
|         <div className='navigation-bar__profile'> | ||||
|  |  | |||
|  | @ -47,6 +47,10 @@ class SearchPopout extends React.PureComponent { | |||
| export default @injectIntl | ||||
| class Search extends React.PureComponent { | ||||
| 
 | ||||
|   static contextTypes = { | ||||
|     router: PropTypes.object.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     value: PropTypes.string.isRequired, | ||||
|     submitted: PropTypes.bool, | ||||
|  | @ -54,6 +58,7 @@ class Search extends React.PureComponent { | |||
|     onSubmit: PropTypes.func.isRequired, | ||||
|     onClear: PropTypes.func.isRequired, | ||||
|     onShow: PropTypes.func.isRequired, | ||||
|     openInRoute: PropTypes.bool, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|  | @ -76,7 +81,12 @@ class Search extends React.PureComponent { | |||
|   handleKeyUp = (e) => { | ||||
|     if (e.key === 'Enter') { | ||||
|       e.preventDefault(); | ||||
| 
 | ||||
|       this.props.onSubmit(); | ||||
| 
 | ||||
|       if (this.props.openInRoute) { | ||||
|         this.context.router.history.push('/search'); | ||||
|       } | ||||
|     } else if (e.key === 'Escape') { | ||||
|       document.querySelector('.ui').parentElement.focus(); | ||||
|     } | ||||
|  |  | |||
|  | @ -9,12 +9,10 @@ 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 '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' }, | ||||
|  | @ -41,12 +39,10 @@ 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) => { | ||||
|  | @ -71,8 +67,6 @@ class GettingStarted extends ImmutablePureComponent { | |||
|     fetchFollowRequests: PropTypes.func.isRequired, | ||||
|     unreadFollowRequests: PropTypes.number, | ||||
|     unreadNotifications: PropTypes.number, | ||||
|     forceSingleColumn: PropTypes.bool, | ||||
|     changeForceSingleColumn: PropTypes.func.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   componentDidMount () { | ||||
|  | @ -83,12 +77,8 @@ class GettingStarted extends ImmutablePureComponent { | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   handleForceSingleColumnChange = ({ target }) => { | ||||
|     this.props.changeForceSingleColumn(target.checked); | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { intl, myAccount, multiColumn, unreadFollowRequests, forceSingleColumn } = this.props; | ||||
|     const { intl, myAccount, multiColumn, unreadFollowRequests } = this.props; | ||||
| 
 | ||||
|     const navItems = []; | ||||
|     let i = 1; | ||||
|  | @ -187,11 +177,6 @@ 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> | ||||
|     ); | ||||
|   } | ||||
|  |  | |||
							
								
								
									
										17
									
								
								app/javascript/mastodon/features/search/index.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/javascript/mastodon/features/search/index.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | |||
| import React from 'react'; | ||||
| import SearchContainer from 'mastodon/features/compose/containers/search_container'; | ||||
| import SearchResultsContainer from 'mastodon/features/compose/containers/search_results_container'; | ||||
| 
 | ||||
| const Search = () => ( | ||||
|   <div className='column search-page'> | ||||
|     <SearchContainer /> | ||||
| 
 | ||||
|     <div className='drawer__pager'> | ||||
|       <div className='drawer__inner darker'> | ||||
|         <SearchResultsContainer /> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| ); | ||||
| 
 | ||||
| export default Search; | ||||
|  | @ -14,6 +14,8 @@ import DrawerLoading from './drawer_loading'; | |||
| import BundleColumnError from './bundle_column_error'; | ||||
| import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components'; | ||||
| import Icon from 'mastodon/components/icon'; | ||||
| import ComposePanel from './compose_panel'; | ||||
| import NavigationPanel from './navigation_panel'; | ||||
| 
 | ||||
| import detectPassiveEvents from 'detect-passive-events'; | ||||
| import { scrollRight } from '../../../scroll'; | ||||
|  | @ -173,14 +175,22 @@ class ColumnsArea extends ImmutablePureComponent { | |||
| 
 | ||||
|       return ( | ||||
|         <div className='columns-area__panels'> | ||||
|           <div className='columns-area__panels__pane' /> | ||||
|           <div className='columns-area__panels__pane columns-area__panels__pane--compositional'> | ||||
|             <div className='columns-area__panels__pane__inner'> | ||||
|               <ComposePanel /> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div className='columns-area__panels__main'> | ||||
|             <TabsBar key='tabs' /> | ||||
|             {content} | ||||
|           </div> | ||||
| 
 | ||||
|           <div className='columns-area__panels__pane' /> | ||||
|           <div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'> | ||||
|             <div className='columns-area__panels__pane__inner'> | ||||
|               <NavigationPanel /> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           {floatingActionButton} | ||||
|         </div> | ||||
|  |  | |||
|  | @ -0,0 +1,41 @@ | |||
| import React from 'react'; | ||||
| import SearchContainer from 'mastodon/features/compose/containers/search_container'; | ||||
| import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container'; | ||||
| import NavigationContainer from 'mastodon/features/compose/containers/navigation_container'; | ||||
| import { invitesEnabled, version, repository, source_url } from 'mastodon/initial_state'; | ||||
| import { Link } from 'react-router-dom'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| 
 | ||||
| const ComposePanel = () => ( | ||||
|   <div className='compose-panel'> | ||||
|     <SearchContainer openInRoute /> | ||||
|     <NavigationContainer /> | ||||
|     <ComposeFormContainer /> | ||||
| 
 | ||||
|     <div className='flex-spacer' /> | ||||
| 
 | ||||
|     <div className='getting-started__footer'> | ||||
|       <ul> | ||||
|         {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>} | ||||
|         <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li> | ||||
|         <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li> | ||||
|         <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li> | ||||
|         <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li> | ||||
|         <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li> | ||||
|         <li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li> | ||||
|         <li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li> | ||||
|         <li><a href='/auth/sign_out' data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li> | ||||
|       </ul> | ||||
| 
 | ||||
|       <p> | ||||
|         <FormattedMessage | ||||
|           id='getting_started.open_source_notice' | ||||
|           defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.' | ||||
|           values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }} | ||||
|         /> | ||||
|       </p> | ||||
|     </div> | ||||
|   </div> | ||||
| ); | ||||
| 
 | ||||
| export default ComposePanel; | ||||
							
								
								
									
										55
									
								
								app/javascript/mastodon/features/ui/components/list_panel.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								app/javascript/mastodon/features/ui/components/list_panel.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,55 @@ | |||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||
| import { fetchLists } from 'mastodon/actions/lists'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { createSelector } from 'reselect'; | ||||
| import { NavLink, withRouter } from 'react-router-dom'; | ||||
| import Icon from 'mastodon/components/icon'; | ||||
| 
 | ||||
| const getOrderedLists = createSelector([state => state.get('lists')], lists => { | ||||
|   if (!lists) { | ||||
|     return lists; | ||||
|   } | ||||
| 
 | ||||
|   return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))); | ||||
| }); | ||||
| 
 | ||||
| const mapStateToProps = state => ({ | ||||
|   lists: getOrderedLists(state), | ||||
| }); | ||||
| 
 | ||||
| export default @withRouter | ||||
| @connect(mapStateToProps) | ||||
| class ListPanel extends ImmutablePureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     dispatch: PropTypes.func.isRequired, | ||||
|     lists: ImmutablePropTypes.list, | ||||
|   }; | ||||
| 
 | ||||
|   componentDidMount () { | ||||
|     const { dispatch } = this.props; | ||||
|     dispatch(fetchLists()); | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { lists } = this.props; | ||||
| 
 | ||||
|     if (!lists) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div> | ||||
|         <hr /> | ||||
| 
 | ||||
|         {lists.map(list => ( | ||||
|           <NavLink key={list.get('id')} className='column-link column-link--transparent' strict to={`/timelines/list/${list.get('id')}`}><Icon className='column-link__icon' id='list-ul' fixedWidth />{list.get('title')}</NavLink> | ||||
|         ))} | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,27 @@ | |||
| import React from 'react'; | ||||
| import { NavLink, withRouter } from 'react-router-dom'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| import Icon from 'mastodon/components/icon'; | ||||
| import NotificationsCounterIcon from './notifications_counter_icon'; | ||||
| import ListPanel from './list_panel'; | ||||
| 
 | ||||
| const NavigationPanel = () => ( | ||||
|   <div className='navigation-panel'> | ||||
|     <NavLink className='column-link column-link--transparent' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink> | ||||
|     <NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink> | ||||
|     <NavLink className='column-link column-link--transparent' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink> | ||||
|     <NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink> | ||||
|     <NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink> | ||||
|     <NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink> | ||||
|     <NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink> | ||||
| 
 | ||||
|     <ListPanel /> | ||||
| 
 | ||||
|     <hr /> | ||||
| 
 | ||||
|     <a className='column-link column-link--transparent' href='/settings/preferences' target='_blank'><Icon className='column-link__icon' id='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a> | ||||
|     <a className='column-link column-link--transparent' href='/relationships' target='_blank'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a> | ||||
|   </div> | ||||
| ); | ||||
| 
 | ||||
| export default withRouter(NavigationPanel); | ||||
|  | @ -9,15 +9,16 @@ const mapStateToProps = state => ({ | |||
| 
 | ||||
| const formatNumber = num => num > 99 ? '99+' : num; | ||||
| 
 | ||||
| const NotificationsCounterIcon = ({ count }) => ( | ||||
| const NotificationsCounterIcon = ({ count, className }) => ( | ||||
|   <i className='icon-with-badge'> | ||||
|     <Icon id='bell' fixedWidth /> | ||||
|     <Icon id='bell' fixedWidth className={className} /> | ||||
|     {count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>} | ||||
|   </i> | ||||
| ); | ||||
| 
 | ||||
| NotificationsCounterIcon.propTypes = { | ||||
|   count: PropTypes.number.isRequired, | ||||
|   className: PropTypes.string, | ||||
| }; | ||||
| 
 | ||||
| export default connect(mapStateToProps)(NotificationsCounterIcon); | ||||
|  |  | |||
|  | @ -8,14 +8,12 @@ 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' ><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>, | ||||
|   <NavLink className='tabs-bar__link primary' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>, | ||||
| 
 | ||||
|   <NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>, | ||||
|   <NavLink className='tabs-bar__link' 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' 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' 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' 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>, | ||||
|   <NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>, | ||||
|   <NavLink className='tabs-bar__link' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>, | ||||
| ]; | ||||
| 
 | ||||
| export function getIndex (path) { | ||||
|  |  | |||
|  | @ -44,8 +44,9 @@ import { | |||
|   Mutes, | ||||
|   PinnedStatuses, | ||||
|   Lists, | ||||
|   Search, | ||||
| } from './util/async-components'; | ||||
| import { me } from '../../initial_state'; | ||||
| import { me, forceSingleColumn } from '../../initial_state'; | ||||
| import { previewState as previewMediaState } from './components/media_modal'; | ||||
| import { previewState as previewVideoState } from './components/video_modal'; | ||||
| 
 | ||||
|  | @ -62,7 +63,6 @@ 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,7 +101,6 @@ class SwitchingColumnsArea extends React.PureComponent { | |||
|     children: PropTypes.node, | ||||
|     location: PropTypes.object, | ||||
|     onLayoutChange: PropTypes.func.isRequired, | ||||
|     forceSingleColumn: PropTypes.bool, | ||||
|   }; | ||||
| 
 | ||||
|   state = { | ||||
|  | @ -140,7 +139,7 @@ class SwitchingColumnsArea extends React.PureComponent { | |||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { children, forceSingleColumn } = this.props; | ||||
|     const { children } = this.props; | ||||
|     const { mobile } = this.state; | ||||
|     const singleColumn = forceSingleColumn || mobile; | ||||
|     const redirect = singleColumn ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />; | ||||
|  | @ -162,7 +161,7 @@ class SwitchingColumnsArea extends React.PureComponent { | |||
|           <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> | ||||
|           <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> | ||||
| 
 | ||||
|           <WrappedRoute path='/search' component={Compose} content={children} componentParams={{ isSearchPage: true }} /> | ||||
|           <WrappedRoute path='/search' component={Search} content={children} /> | ||||
| 
 | ||||
|           <WrappedRoute path='/statuses/new' component={Compose} content={children} /> | ||||
|           <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> | ||||
|  | @ -207,7 +206,6 @@ class UI extends React.PureComponent { | |||
|     location: PropTypes.object, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|     dropdownMenuIsOpen: PropTypes.bool, | ||||
|     forceSingleColumn: PropTypes.bool, | ||||
|   }; | ||||
| 
 | ||||
|   state = { | ||||
|  | @ -456,7 +454,7 @@ class UI extends React.PureComponent { | |||
| 
 | ||||
|   render () { | ||||
|     const { draggingOver } = this.state; | ||||
|     const { children, isComposing, location, dropdownMenuIsOpen, forceSingleColumn } = this.props; | ||||
|     const { children, isComposing, location, dropdownMenuIsOpen } = this.props; | ||||
| 
 | ||||
|     const handlers = { | ||||
|       help: this.handleHotkeyToggleHelp, | ||||
|  | @ -482,7 +480,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 }}> | ||||
|           <SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange} forceSingleColumn={forceSingleColumn}> | ||||
|           <SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange}> | ||||
|             {children} | ||||
|           </SwitchingColumnsArea> | ||||
| 
 | ||||
|  |  | |||
|  | @ -129,3 +129,7 @@ export function ListEditor () { | |||
| export function ListAdder () { | ||||
|   return import(/*webpackChunkName: "features/list_adder" */'../../list_adder'); | ||||
| } | ||||
| 
 | ||||
| export function Search () { | ||||
|   return import(/*webpackChunkName: "features/search" */'../../search'); | ||||
| } | ||||
|  |  | |||
|  | @ -19,5 +19,6 @@ export const version = getMeta('version'); | |||
| export const mascot = getMeta('mascot'); | ||||
| export const profile_directory = getMeta('profile_directory'); | ||||
| export const isStaff = getMeta('is_staff'); | ||||
| export const forceSingleColumn = !getMeta('advanced_layout'); | ||||
| 
 | ||||
| export default initialState; | ||||
|  |  | |||
|  | @ -14,8 +14,6 @@ const initialState = ImmutableMap({ | |||
| 
 | ||||
|   skinTone: 1, | ||||
| 
 | ||||
|   forceSingleColumn: false, | ||||
| 
 | ||||
|   home: ImmutableMap({ | ||||
|     shows: ImmutableMap({ | ||||
|       reblog: true, | ||||
|  |  | |||
|  | @ -1801,7 +1801,12 @@ a.account__display-name { | |||
|       display: flex; | ||||
|       justify-content: flex-end; | ||||
| 
 | ||||
|       &--start { | ||||
|         justify-content: flex-start; | ||||
|       } | ||||
| 
 | ||||
|       &__inner { | ||||
|         width: 285px; | ||||
|         pointer-events: auto; | ||||
|         height: 100%; | ||||
|       } | ||||
|  | @ -1925,6 +1930,7 @@ a.account__display-name { | |||
|   display: block; | ||||
|   flex: 1 1 auto; | ||||
|   padding: 15px 10px; | ||||
|   padding-bottom: 13px; | ||||
|   color: $primary-text-color; | ||||
|   text-decoration: none; | ||||
|   text-align: center; | ||||
|  | @ -1949,6 +1955,7 @@ a.account__display-name { | |||
|   &:active { | ||||
|     @media screen and (min-width: 631px) { | ||||
|       background: lighten($ui-base-color, 14%); | ||||
|       border-bottom-color: lighten($ui-base-color, 14%); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | @ -1978,11 +1985,21 @@ a.account__display-name { | |||
|     padding: 0; | ||||
|   } | ||||
| 
 | ||||
|   .search__input, | ||||
|   .autosuggest-textarea__textarea { | ||||
|     font-size: 16px; | ||||
|   } | ||||
| 
 | ||||
|   .search__input { | ||||
|     line-height: 18px; | ||||
|     font-size: 16px; | ||||
|     padding: 15px; | ||||
|     padding-right: 30px; | ||||
|   } | ||||
| 
 | ||||
|   .search__icon .fa { | ||||
|     top: 15px; | ||||
|   } | ||||
| 
 | ||||
|   @media screen and (min-width: 360px) { | ||||
|     padding: 10px 0; | ||||
|   } | ||||
|  | @ -2038,6 +2055,58 @@ a.account__display-name { | |||
|         margin-top: 10px; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     .account { | ||||
|       padding: 15px 10px; | ||||
|     } | ||||
| 
 | ||||
|     .notification { | ||||
|       &__message { | ||||
|         margin-left: 48px + 15px * 2; | ||||
|         padding-top: 15px; | ||||
|       } | ||||
| 
 | ||||
|       &__favourite-icon-wrapper { | ||||
|         left: -32px; | ||||
|       } | ||||
| 
 | ||||
|       .status { | ||||
|         padding-top: 8px; | ||||
|       } | ||||
| 
 | ||||
|       .account { | ||||
|         padding-top: 8px; | ||||
|       } | ||||
| 
 | ||||
|       .account__avatar-wrapper { | ||||
|         margin-left: 17px; | ||||
|         margin-right: 15px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .floating-action-button { | ||||
|   position: fixed; | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|   width: 3.9375rem; | ||||
|   height: 3.9375rem; | ||||
|   bottom: 1.3125rem; | ||||
|   right: 1.3125rem; | ||||
|   background: darken($ui-highlight-color, 3%); | ||||
|   color: $white; | ||||
|   border-radius: 50%; | ||||
|   font-size: 21px; | ||||
|   line-height: 21px; | ||||
|   text-decoration: none; | ||||
|   box-shadow: 2px 3px 9px rgba($base-shadow-color, 0.4); | ||||
| 
 | ||||
|   &:hover, | ||||
|   &:focus, | ||||
|   &:active { | ||||
|     background: lighten($ui-highlight-color, 7%); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | @ -2059,12 +2128,41 @@ a.account__display-name { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| @media screen and (max-width: 600px + (285px * 1) + (10px * 1)) { | ||||
|   .columns-area__panels__pane--compositional { | ||||
|     display: none; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @media screen and (min-width: 600px + (285px * 1) + (10px * 1)) { | ||||
|   .floating-action-button, | ||||
|   .tabs-bar__link.optional { | ||||
|     display: none; | ||||
|   } | ||||
| 
 | ||||
|   .search-page .search { | ||||
|     display: none; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @media screen and (max-width: 600px + (285px * 2) + (10px * 2)) { | ||||
|   .columns-area__panels__pane--navigational { | ||||
|     display: none; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @media screen and (min-width: 600px + (285px * 2) + (10px * 2)) { | ||||
|   .tabs-bar { | ||||
|     display: none; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .icon-with-badge { | ||||
|   position: relative; | ||||
| 
 | ||||
|   &__badge { | ||||
|     position: absolute; | ||||
|     right: -13px; | ||||
|     left: 9px; | ||||
|     top: -13px; | ||||
|     background: $ui-highlight-color; | ||||
|     border: 2px solid lighten($ui-base-color, 8%); | ||||
|  | @ -2077,6 +2175,57 @@ a.account__display-name { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| .column-link--transparent .icon-with-badge__badge { | ||||
|   border-color: darken($ui-base-color, 8%); | ||||
| } | ||||
| 
 | ||||
| .compose-panel { | ||||
|   width: 285px; | ||||
|   margin-top: 10px; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   height: 100%; | ||||
| 
 | ||||
|   .search__input { | ||||
|     line-height: 18px; | ||||
|     font-size: 16px; | ||||
|     padding: 15px; | ||||
|     padding-right: 30px; | ||||
|   } | ||||
| 
 | ||||
|   .search__icon .fa { | ||||
|     top: 15px; | ||||
|   } | ||||
| 
 | ||||
|   .navigation-bar { | ||||
|     padding-top: 20px; | ||||
|     padding-bottom: 20px; | ||||
|   } | ||||
| 
 | ||||
|   .flex-spacer { | ||||
|     background: transparent; | ||||
|   } | ||||
| 
 | ||||
|   .autosuggest-textarea__textarea { | ||||
|     max-height: 200px; | ||||
|   } | ||||
| 
 | ||||
|   .compose-form__upload-thumbnail { | ||||
|     height: 80px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .navigation-panel { | ||||
|   margin-top: 10px; | ||||
| 
 | ||||
|   hr { | ||||
|     border: 0; | ||||
|     background: transparent; | ||||
|     border-top: 1px solid lighten($ui-base-color, 4%); | ||||
|     margin: 10px 0; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .drawer__pager { | ||||
|   box-sizing: border-box; | ||||
|   padding: 0; | ||||
|  | @ -2127,15 +2276,6 @@ a.account__display-name { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| .navigational-toggle { | ||||
|   padding: 10px; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: space-between; | ||||
|   font-size: 14px; | ||||
|   color: $dark-text-color; | ||||
| } | ||||
| 
 | ||||
| .pseudo-drawer { | ||||
|   background: lighten($ui-base-color, 13%); | ||||
|   font-size: 13px; | ||||
|  | @ -2365,9 +2505,31 @@ a.account__display-name { | |||
|   padding: 15px; | ||||
|   text-decoration: none; | ||||
| 
 | ||||
|   &:hover { | ||||
|   &:hover, | ||||
|   &:focus, | ||||
|   &:active { | ||||
|     background: lighten($ui-base-color, 11%); | ||||
|   } | ||||
| 
 | ||||
|   &:focus { | ||||
|     outline: 0; | ||||
|   } | ||||
| 
 | ||||
|   &--transparent { | ||||
|     background: transparent; | ||||
|     color: $ui-secondary-color; | ||||
| 
 | ||||
|     &:hover, | ||||
|     &:focus, | ||||
|     &:active { | ||||
|       background: transparent; | ||||
|       color: $primary-text-color; | ||||
|     } | ||||
| 
 | ||||
|     &.active { | ||||
|       color: $ui-highlight-color; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .column-link__icon { | ||||
|  | @ -5436,34 +5598,6 @@ noscript { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| .floating-action-button { | ||||
|   position: fixed; | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|   width: 3.9375rem; | ||||
|   height: 3.9375rem; | ||||
|   bottom: 1.3125rem; | ||||
|   right: 1.3125rem; | ||||
|   background: darken($ui-highlight-color, 3%); | ||||
|   color: $white; | ||||
|   border-radius: 50%; | ||||
|   font-size: 21px; | ||||
|   line-height: 21px; | ||||
|   text-decoration: none; | ||||
|   box-shadow: 2px 3px 9px rgba($base-shadow-color, 0.4); | ||||
| 
 | ||||
|   &:hover, | ||||
|   &:focus, | ||||
|   &:active { | ||||
|     background: lighten($ui-highlight-color, 7%); | ||||
|   } | ||||
| 
 | ||||
|   @media screen and (min-width: 630px) { | ||||
|     display: none; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .account__header__content { | ||||
|   color: $darker-text-color; | ||||
|   font-size: 14px; | ||||
|  |  | |||
|  | @ -33,6 +33,7 @@ class UserSettingsDecorator | |||
|     user.settings['hide_network']        = hide_network_preference if change?('setting_hide_network') | ||||
|     user.settings['aggregate_reblogs']   = aggregate_reblogs_preference if change?('setting_aggregate_reblogs') | ||||
|     user.settings['show_application']    = show_application_preference if change?('setting_show_application') | ||||
|     user.settings['advanced_layout']     = advanced_layout_preference if change?('setting_advanced_layout') | ||||
|   end | ||||
| 
 | ||||
|   def merged_notification_emails | ||||
|  | @ -107,6 +108,10 @@ class UserSettingsDecorator | |||
|     boolean_cast_setting 'setting_aggregate_reblogs' | ||||
|   end | ||||
| 
 | ||||
|   def advanced_layout_preference | ||||
|     boolean_cast_setting 'setting_advanced_layout' | ||||
|   end | ||||
| 
 | ||||
|   def boolean_cast_setting(key) | ||||
|     ActiveModel::Type::Boolean.new.cast(settings[key]) | ||||
|   end | ||||
|  |  | |||
|  | @ -104,7 +104,8 @@ class User < ApplicationRecord | |||
| 
 | ||||
|   delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal, | ||||
|            :reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network, | ||||
|            :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, to: :settings, prefix: :setting, allow_nil: false | ||||
|            :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, | ||||
|            :advanced_layout, to: :settings, prefix: :setting, allow_nil: false | ||||
| 
 | ||||
|   attr_reader :invite_code | ||||
|   attr_writer :external | ||||
|  |  | |||
|  | @ -31,6 +31,7 @@ class InitialStateSerializer < ActiveModel::Serializer | |||
|       store[:display_media]   = object.current_account.user.setting_display_media | ||||
|       store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers | ||||
|       store[:reduce_motion]   = object.current_account.user.setting_reduce_motion | ||||
|       store[:advanced_layout] = object.current_account.user.setting_advanced_layout | ||||
|       store[:is_staff]        = object.current_account.user.staff? | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -46,6 +46,9 @@ | |||
|     .fields-group.fields-row__column.fields-row__column-6 | ||||
|       = f.input :setting_display_media, collection: ['default', 'show_all', 'hide_all'], wrapper: :with_label, include_blank: false, label_method: lambda { |item| t("simple_form.hints.defaults.setting_display_media_#{item}") }, hint: false | ||||
| 
 | ||||
|   .fields-group | ||||
|     = f.input :setting_advanced_layout, as: :boolean, wrapper: :with_label | ||||
| 
 | ||||
|   .fields-group | ||||
|     = f.input :setting_unfollow_modal, as: :boolean, wrapper: :with_label | ||||
|     = f.input :setting_boost_modal, as: :boolean, wrapper: :with_label | ||||
|  |  | |||
		Reference in a new issue