* 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 managermaster^2
@@ -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> | |||
); | |||
} | |||
@@ -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; |
@@ -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 | |||
@@ -47,6 +47,9 @@ | |||
= 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 | |||
= f.input :setting_delete_modal, as: :boolean, wrapper: :with_label | |||
@@ -26,6 +26,7 @@ en: | |||
password: Use at least 8 characters | |||
phrase: Will be matched regardless of casing in text or content warning of a toot | |||
scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones. | |||
setting_advanced_layout: The advanced UI consists of multiple customizable columns | |||
setting_aggregate_reblogs: Do not show new boosts for toots that have been recently boosted (only affects newly-received boosts) | |||
setting_default_language: The language of your toots can be detected automatically, but it's not always accurate | |||
setting_display_media_default: Hide media marked as sensitive | |||
@@ -90,6 +91,7 @@ en: | |||
otp_attempt: Two-factor code | |||
password: Password | |||
phrase: Keyword or phrase | |||
setting_advanced_layout: Enable advanced web interface | |||
setting_aggregate_reblogs: Group boosts in timelines | |||
setting_auto_play_gif: Auto-play animated GIFs | |||
setting_boost_modal: Show confirmation dialog before boosting | |||
@@ -31,6 +31,7 @@ defaults: &defaults | |||
noindex: false | |||
theme: 'default' | |||
aggregate_reblogs: true | |||
advanced_layout: true | |||
notification_emails: | |||
follow: false | |||
reblog: false | |||