mirror of
https://github.com/mastodon/mastodon.git
synced 2024-12-27 02:54:19 +00:00
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
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue