Skip to content

Commit

Permalink
Add responsive panels to the single-column layout (mastodon#10820)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Gargron authored and multiple creatures committed Nov 19, 2019
1 parent f3d88f0 commit e9a90ca
Show file tree
Hide file tree
Showing 26 changed files with 440 additions and 133 deletions.
7 changes: 7 additions & 0 deletions app/controllers/settings/preferences_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# frozen_string_literal: true

class Settings::PreferencesController < Settings::BaseController
layout 'admin'

before_action :authenticate_user!

def show; end

def update
Expand Down Expand Up @@ -80,6 +84,9 @@ def user_settings_params
:setting_aggregate_reblogs,
:setting_show_application,
:setting_default_content_type,

:setting_theme,
:setting_advanced_layout,
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account),
interactions: %i(must_be_follower must_be_following)
)
Expand Down
20 changes: 11 additions & 9 deletions app/javascript/mastodon/actions/compose.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -77,9 +85,7 @@ export function replyCompose(status, routerHistory) {
status: status,
});

if (!getState().getIn(['compose', 'mounted'])) {
routerHistory.push('/statuses/new');
}
ensureComposeIsVisible(getState, routerHistory);
};
};

Expand All @@ -102,9 +108,7 @@ export function mentionCompose(account, routerHistory) {
account: account,
});

if (!getState().getIn(['compose', 'mounted'])) {
routerHistory.push('/statuses/new');
}
ensureComposeIsVisible(getState, routerHistory);
};
};

Expand All @@ -115,9 +119,7 @@ export function directCompose(account, routerHistory) {
account: account,
});

if (!getState().getIn(['compose', 'mounted'])) {
routerHistory.push('/statuses/new');
}
ensureComposeIsVisible(getState, routerHistory);
};
};

Expand Down
8 changes: 3 additions & 5 deletions app/javascript/mastodon/actions/statuses.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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]);

Expand All @@ -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));
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/mastodon/components/autosuggest_input.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'>
Expand Down
10 changes: 10 additions & 0 deletions app/javascript/mastodon/features/compose/components/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,18 @@ 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,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired,
onShow: PropTypes.func.isRequired,
openInRoute: PropTypes.bool,
intl: PropTypes.object.isRequired,
};

Expand All @@ -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();
}
Expand Down
17 changes: 1 addition & 16 deletions app/javascript/mastodon/features/getting_started/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand All @@ -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) => {
Expand All @@ -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 () {
Expand All @@ -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;
Expand Down Expand Up @@ -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>
);
}
Expand Down
17 changes: 17 additions & 0 deletions app/javascript/mastodon/features/search/index.js
Original file line number Diff line number Diff line change
@@ -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 changes: 12 additions & 2 deletions app/javascript/mastodon/features/ui/components/columns_area.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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>
Expand Down
41 changes: 41 additions & 0 deletions app/javascript/mastodon/features/ui/components/compose_panel.js
Original file line number Diff line number Diff line change
@@ -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 changes: 55 additions & 0 deletions app/javascript/mastodon/features/ui/components/list_panel.js
Original file line number Diff line number Diff line change
@@ -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>
);
}

}
27 changes: 27 additions & 0 deletions app/javascript/mastodon/features/ui/components/navigation_panel.js
Original file line number Diff line number Diff line change
@@ -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);
Loading

0 comments on commit e9a90ca

Please sign in to comment.