Added App Setttings Modal

This commit is contained in:
kibigo! 2017-06-28 22:00:54 -07:00
parent 6cbbdc805f
commit 595c6de32c
19 changed files with 561 additions and 94 deletions

View file

@ -14,7 +14,7 @@ export function changeLocalSetting(key, value) {
export function saveLocalSettings() {
return (_, getState) => {
const localSettings = getState().get('localSettings').toJS();
const localSettings = getState().get('local_settings').toJS();
localStorage.setItem('mastodon-settings', JSON.stringify(localSettings));
};
};

View file

@ -25,6 +25,7 @@ export default class StatusOrReblog extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map,
settings: ImmutablePropTypes.map,
wrapped: PropTypes.bool,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
@ -47,6 +48,7 @@ export default class StatusOrReblog extends ImmutablePureComponent {
updateOnProps = [
'status',
'account',
'settings',
'wrapped',
'me',
'boostModal',
@ -97,6 +99,7 @@ class Status extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map,
settings: ImmutablePropTypes.map,
wrapped: PropTypes.bool,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
@ -126,6 +129,7 @@ class Status extends ImmutablePureComponent {
updateOnProps = [
'status',
'account',
'settings',
'wrapped',
'me',
'boostModal',
@ -140,7 +144,8 @@ class Status extends ImmutablePureComponent {
]
componentWillReceiveProps (nextProps) {
if (nextProps.collapse !== this.props.collapse && nextProps.collapse !== undefined) this.setState({ isCollapsed: !!nextProps.collapse });
if (!nextProps.settings.getIn(['collapsed', 'enabled'])) this.collapse(false);
else if (nextProps.collapse !== this.props.collapse && nextProps.collapse !== undefined) this.collapse(this.props.collapse);
}
shouldComponentUpdate (nextProps, nextState) {
@ -165,8 +170,13 @@ class Status extends ImmutablePureComponent {
componentDidMount () {
const node = this.node;
if (this.props.collapse !== undefined) this.setState({ isCollapsed: !!this.props.collapse });
else if (node.clientHeight > 400) this.setState({ isCollapsed: true });
const { collapse, settings, status } = this.props;
if (collapse !== undefined) this.collapse(collapse);
else if (settings.getIn(['collapsed', 'auto', 'all'])) this.collapse();
else if (settings.getIn(['collapsed', 'auto', 'lengthy']) && node.clientHeight > 400) this.collapse();
else if (settings.getIn(['collapsed', 'auto', 'replies']) && status.get('in_reply_to_id', null) !== null) this.collapse();
else if (settings.getIn(['collapsed', 'auto', 'media']) && status.get('media_attachments').size > 0) this.collapse();
if (!this.props.intersectionObserverWrapper) {
// TODO: enable IntersectionObserver optimization for notification statuses.
@ -186,6 +196,11 @@ class Status extends ImmutablePureComponent {
this.componentMounted = false;
}
collapse = (collapsedOrNot) => {
if (collapsedOrNot === undefined) collapsedOrNot = true;
if (this.props.settings.getIn(['collapsed', 'enabled'])) this.setState({ isCollapsed: !!collapsedOrNot });
}
handleIntersection = (entry) => {
// Edge 15 doesn't support isIntersecting, but we can infer it
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12156111/
@ -247,20 +262,23 @@ class Status extends ImmutablePureComponent {
};
handleCollapsedClick = () => {
this.setState({ isCollapsed: !this.state.isCollapsed, isExpanded: false });
this.collapse(!this.state.isCollapsed);
this.setState({ isExpanded: false });
}
render () {
let media = null;
let mediaType = null;
let thumb = null;
let statusAvatar;
// Exclude intersectionObserverWrapper from `other` variable
// because intersection is managed in here.
const { status, account, intersectionObserverWrapper, intl, ...other } = this.props;
const { status, account, settings, intersectionObserverWrapper, intl, ...other } = this.props;
const { isExpanded, isIntersecting, isHidden, isCollapsed } = this.state;
let background = settings.getIn(['collapsed', 'backgrounds', 'user_backgrounds']) ? status.getIn(['account', 'header']) : null;
if (status === null) {
return null;
}
@ -280,12 +298,12 @@ class Status extends ImmutablePureComponent {
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />;
mediaType = <i className='fa fa-fw fa-video-camera' aria-hidden='true' />;
if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0)) thumb = status.getIn(['media_attachments', 0]).get('preview_url');
} else {
media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />;
mediaType = status.get('media_attachments').size > 1 ? <i className='fa fa-fw fa-th-large' aria-hidden='true' /> : <i className='fa fa-fw fa-picture-o' aria-hidden='true' />;
if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0)) thumb = status.getIn(['media_attachments', 0]).get('preview_url');
}
if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) background = status.getIn(['media_attachments', 0]).get('preview_url');
}
if (account === undefined || account === null) {
@ -295,19 +313,19 @@ class Status extends ImmutablePureComponent {
}
return (
<div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')} ${isCollapsed ? 'status-collapsed' : ''}`} data-id={status.get('id')} ref={this.handleRef} style={{ backgroundImage: thumb && isCollapsed ? 'url(' + thumb + ')' : 'none' }}>
<div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')} ${isCollapsed ? 'status-collapsed' : ''}`} data-id={status.get('id')} ref={this.handleRef} style={{ backgroundImage: background && isCollapsed ? 'url(' + background + ')' : 'none' }}>
<div className='status__info'>
<div className='status__info__icons'>
{mediaType}
<IconButton
{settings.getIn(['collapsed', 'enabled']) ? <IconButton
className='status__collapse-button'
animate flip
active={isCollapsed}
title={isCollapsed ? intl.formatMessage(messages.uncollapse) : intl.formatMessage(messages.collapse)}
icon='angle-double-up'
onClick={this.handleCollapsedClick}
/>
/> : null}
</div>
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name'>

View file

@ -25,9 +25,9 @@ addLocaleData(localeData);
const store = configureStore();
const initialState = JSON.parse(document.getElementById('initial-state').textContent);
try {
initialState.localSettings = JSON.parse(localStorage.getItem('mastodon-settings'));
initialState.local_settings = JSON.parse(localStorage.getItem('mastodon-settings'));
} catch (e) {
initialState.localSettings = {};
initialState.local_settings = {};
}
store.dispatch(hydrateStore(initialState));

View file

@ -34,6 +34,7 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({
status: getStatus(state, props.id),
me: state.getIn(['meta', 'me']),
settings: state.get('local_settings'),
boostModal: state.getIn(['meta', 'boost_modal']),
deleteModal: state.getIn(['meta', 'delete_modal']),
autoPlayGif: state.getIn(['meta', 'auto_play_gif']),

View file

@ -4,6 +4,7 @@ import NavigationContainer from './containers/navigation_container';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { mountCompose, unmountCompose } from '../../actions/compose';
import { openModal } from '../../actions/modal';
import { changeLocalSetting } from '../../actions/local_settings';
import Link from 'react-router-dom/Link';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
@ -16,13 +17,13 @@ const messages = defineMessages({
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
});
const mapStateToProps = state => ({
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
layout: state.getIn(['localSettings', 'layout']),
layout: state.getIn(['local_settings', 'layout']),
});
@connect(mapStateToProps)
@ -51,6 +52,10 @@ export default class Compose extends React.PureComponent {
e.preventDefault();
}
openSettings = () => {
this.props.dispatch(openModal('SETTINGS', {}));
}
render () {
const { multiColumn, showSearch, intl, layout } = this.props;
@ -62,7 +67,7 @@ export default class Compose extends React.PureComponent {
<Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)}><i role='img' aria-label={intl.formatMessage(messages.start)} className='fa fa-fw fa-asterisk' /></Link>
<Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role='img' aria-label={intl.formatMessage(messages.community)} className='fa fa-fw fa-users' /></Link>
<Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role='img' aria-label={intl.formatMessage(messages.public)} className='fa fa-fw fa-globe' /></Link>
<a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)}><i role='img' aria-label={intl.formatMessage(messages.preferences)} className='fa fa-fw fa-cog' /></a>
<a onClick={this.openSettings} role='button' tabIndex='0' className='drawer__tab' title={intl.formatMessage(messages.settings)}><i role='img' aria-label={intl.formatMessage(messages.settings)} className='fa fa-fw fa-cogs' /></a>
<a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)}><i role='img' aria-label={intl.formatMessage(messages.logout)} className='fa fa-fw fa-sign-out' /></a>
</div>
);

View file

@ -4,6 +4,7 @@ import ColumnLink from '../ui/components/column_link';
import ColumnSubheading from '../ui/components/column_subheading';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { openModal } from '../../actions/modal';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
@ -17,6 +18,7 @@ const messages = defineMessages({
settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
@ -39,8 +41,13 @@ export default class GettingStarted extends ImmutablePureComponent {
me: ImmutablePropTypes.map.isRequired,
columns: ImmutablePropTypes.list,
multiColumn: PropTypes.bool,
dispatch: PropTypes.func.isRequired,
};
openSettings = () => {
this.props.dispatch(openModal('SETTINGS', {}));
}
render () {
const { intl, me, columns, multiColumn } = this.props;
@ -79,16 +86,18 @@ export default class GettingStarted extends ImmutablePureComponent {
return (
<Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile>
<div className='scrollable optionally-scrollable'>
<div className='getting-started__wrapper'>
<ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />
{navItems}
<ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
<ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
<ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
<ColumnLink icon='cogs' text={intl.formatMessage(messages.settings)} onClick={this.openSettings} />
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
</div>
<div className='getting-started__footer scrollable optionally-scrollable'>
<div className='getting-started__footer'>
<div className='static-content getting-started'>
<p>
<a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.faq' defaultMessage='FAQ' /></a> • <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.userguide' defaultMessage='User Guide' /></a> • <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.appsshort' defaultMessage='Apps' /></a>
@ -102,6 +111,7 @@ export default class GettingStarted extends ImmutablePureComponent {
</p>
</div>
</div>
</div>
</Column>
);
}

View file

@ -12,6 +12,7 @@ export default class Notification extends ImmutablePureComponent {
static propTypes = {
notification: ImmutablePropTypes.map.isRequired,
settings: ImmutablePropTypes.map.isRequired,
};
renderFollow (account, link) {
@ -34,7 +35,7 @@ export default class Notification extends ImmutablePureComponent {
return <StatusContainer id={notification.get('status')} withDismiss />;
}
renderFavourite (notification, link) {
renderFavourite (notification, settings, link) {
return (
<div className='notification notification-favourite'>
<div className='notification__message'>
@ -44,12 +45,12 @@ export default class Notification extends ImmutablePureComponent {
<FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} />
</div>
<StatusContainer id={notification.get('status')} account={notification.get('account')} muted collapse withDismiss />
<StatusContainer id={notification.get('status')} account={notification.get('account')} muted collapse={settings.getIn(['collapsed', 'auto', 'notifications'])} withDismiss />
</div>
);
}
renderReblog (notification, link) {
renderReblog (notification, settings, link) {
return (
<div className='notification notification-reblog'>
<div className='notification__message'>
@ -59,13 +60,13 @@ export default class Notification extends ImmutablePureComponent {
<FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} />
</div>
<StatusContainer id={notification.get('status')} account={notification.get('account')} muted collapse withDismiss />
<StatusContainer id={notification.get('status')} account={notification.get('account')} muted collapse={settings.getIn(['collapsed', 'auto', 'notifications'])} withDismiss />
</div>
);
}
render () {
const { notification } = this.props;
const { notification, settings } = this.props;
const account = notification.get('account');
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
@ -77,9 +78,9 @@ export default class Notification extends ImmutablePureComponent {
case 'mention':
return this.renderMention(notification);
case 'favourite':
return this.renderFavourite(notification, link);
return this.renderFavourite(notification, settings, link);
case 'reblog':
return this.renderReblog(notification, link);
return this.renderReblog(notification, settings, link);
}
return null;

View file

@ -7,6 +7,7 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({
notification: getNotification(state, props.notification, props.accountId),
settings: state.get('local_settings'),
});
return mapStateToProps;

View file

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import Link from 'react-router-dom/Link';
const ColumnLink = ({ icon, text, to, href, method, hideOnMobile }) => {
const ColumnLink = ({ icon, text, to, onClick, href, method, hideOnMobile }) => {
if (href) {
return (
<a href={href} className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`} data-method={method}>
@ -10,13 +10,20 @@ const ColumnLink = ({ icon, text, to, href, method, hideOnMobile }) => {
{text}
</a>
);
} else {
} else if (to) {
return (
<Link to={to} className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`}>
<i className={`fa fa-fw fa-${icon} column-link__icon`} />
{text}
</Link>
);
} else {
return (
<a onClick={onClick} role='button' tabIndex='0' className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`} data-method={method}>
<i className={`fa fa-fw fa-${icon} column-link__icon`} />
{text}
</a>
);
}
};
@ -24,6 +31,7 @@ ColumnLink.propTypes = {
icon: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
to: PropTypes.string,
onClick: PropTypes.func,
href: PropTypes.string,
method: PropTypes.string,
hideOnMobile: PropTypes.bool,

View file

@ -6,6 +6,7 @@ import VideoModal from './video_modal';
import BoostModal from './boost_modal';
import ConfirmationModal from './confirmation_modal';
import ReportModal from './report_modal';
import SettingsModal from '../containers/settings_modal_container';
import TransitionMotion from 'react-motion/lib/TransitionMotion';
import spring from 'react-motion/lib/spring';
@ -16,6 +17,7 @@ const MODAL_COMPONENTS = {
'BOOST': BoostModal,
'CONFIRM': ConfirmationModal,
'REPORT': ReportModal,
'SETTINGS': SettingsModal,
};
export default class ModalRoot extends React.PureComponent {

View file

@ -0,0 +1,212 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
class SettingsItem extends React.PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,
item: PropTypes.array.isRequired,
id: PropTypes.string.isRequired,
dependsOn: PropTypes.array,
dependsOnNot: PropTypes.array,
children: PropTypes.element.isRequired,
onChange: PropTypes.func.isRequired,
};
handleChange = (e) => {
const { item, onChange } = this.props;
onChange(item, e);
}
render () {
const { settings, item, id, children, dependsOn, dependsOnNot } = this.props;
let enabled = true;
if (dependsOn) {
for (let i = 0; i < dependsOn.length; i++) {
enabled = enabled && settings.getIn(dependsOn[i]);
}
}
if (dependsOnNot) {
for (let i = 0; i < dependsOnNot.length; i++) {
enabled = enabled && !settings.getIn(dependsOnNot[i]);
}
}
return (
<label htmlFor={id}>
<input
id={id}
type='checkbox'
checked={settings.getIn(item)}
onChange={this.handleChange}
disabled={!enabled}
/>
{children}
</label>
);
}
}
export default class SettingsModal extends React.PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,
toggleSetting: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
state = {
currentIndex: 0,
};
General = () => {
return (
<div>
<h1><FormattedMessage id='settings.general' defaultMessage='General' /></h1>
<SettingsItem
settings={this.props.settings}
item={['stretch']}
id='mastodon-settings--stretch'
onChange={this.props.toggleSetting}
>
<FormattedMessage id='settings.wide_view' defaultMessage='Wide view (Desktop mode only)' />
</SettingsItem>
</div>
);
}
CollapsedStatuses = () => {
return (
<div>
<h1><FormattedMessage id='settings.collapsed_statuses' defaultMessage='Collapsed toots' /></h1>
<SettingsItem
settings={this.props.settings}
item={['collapsed', 'enabled']}
id='mastodon-settings--collapsed-enabled'
onChange={this.props.toggleSetting}
>
<FormattedMessage id='settings.enable_collapsed' defaultMessage='Enable collapsed toots' />
</SettingsItem>
<section>
<h2><FormattedMessage id='settings.auto_collapse' defaultMessage='Automatic collapsing' /></h2>
<SettingsItem
settings={this.props.settings}
item={['collapsed', 'auto', 'all']}
id='mastodon-settings--collapsed-auto-all'
onChange={this.props.toggleSetting}
dependsOn={[['collapsed', 'enabled']]}
>
<FormattedMessage id='settings.auto_collapse_all' defaultMessage='Everything' />
</SettingsItem>
<SettingsItem
settings={this.props.settings}
item={['collapsed', 'auto', 'notifications']}
id='mastodon-settings--collapsed-auto-notifications'
onChange={this.props.toggleSetting}
dependsOn={[['collapsed', 'enabled']]}
dependsOnNot={[['collapsed', 'auto', 'all']]}
>
<FormattedMessage id='settings.auto_collapse_notifications' defaultMessage='Notifications' />
</SettingsItem>
<SettingsItem
settings={this.props.settings}
item={['collapsed', 'auto', 'lengthy']}
id='mastodon-settings--collapsed-auto-lengthy'
onChange={this.props.toggleSetting}
dependsOn={[['collapsed', 'enabled']]}
dependsOnNot={[['collapsed', 'auto', 'all']]}
>
<FormattedMessage id='settings.auto_collapse_lengthy' defaultMessage='Lengthy toots' />
</SettingsItem>
<SettingsItem
settings={this.props.settings}
item={['collapsed', 'auto', 'replies']}
id='mastodon-settings--collapsed-auto-replies'
onChange={this.props.toggleSetting}
dependsOn={[['collapsed', 'enabled']]}
dependsOnNot={[['collapsed', 'auto', 'all']]}
>
<FormattedMessage id='settings.auto_collapse_replies' defaultMessage='Replies' />
</SettingsItem>
<SettingsItem
settings={this.props.settings}
item={['collapsed', 'auto', 'media']}
id='mastodon-settings--collapsed-auto-media'
onChange={this.props.toggleSetting}
dependsOn={[['collapsed', 'enabled']]}
dependsOnNot={[['collapsed', 'auto', 'all']]}
>
<FormattedMessage id='settings.auto_collapse_media' defaultMessage='Toots with media' />
</SettingsItem>
</section>
<section>
<h2><FormattedMessage id='settings.image_backgrounds' defaultMessage='Image backgrounds' /></h2>
<SettingsItem
settings={this.props.settings}
item={['collapsed', 'backgrounds', 'user_backgrounds']}
id='mastodon-settings--collapsed-user-backgrouns'
onChange={this.props.toggleSetting}
dependsOn={[['collapsed', 'enabled']]}
>
<FormattedMessage id='settings.image_backgrounds_users' defaultMessage='Give collapsed toots an image background' />
</SettingsItem>
<SettingsItem
settings={this.props.settings}
item={['collapsed', 'backgrounds', 'preview_images']}
id='mastodon-settings--collapsed-preview-images'
onChange={this.props.toggleSetting}
dependsOn={[['collapsed', 'enabled']]}
>
<FormattedMessage id='settings.image_backgrounds_media' defaultMessage='Preview collapsed toot media' />
</SettingsItem>
</section>
</div>
);
}
navigateTo = (e) =>
this.setState({ currentIndex: +e.currentTarget.getAttribute('data-mastodon-navigation_index') });
render () {
const { General, CollapsedStatuses, navigateTo } = this;
const { onClose } = this.props;
const { currentIndex } = this.state;
return (
<div className='modal-root__modal settings-modal'>
<nav className='settings-modal__navigation'>
<a onClick={navigateTo} role='button' data-mastodon-navigation_index='0' tabIndex='0' className={`settings-modal__navigation-item${currentIndex === 0 ? ' active' : ''}`}>
<FormattedMessage id='settings.general' defaultMessage='General' />
</a>
<a onClick={navigateTo} role='button' data-mastodon-navigation_index='1' tabIndex='0' className={`settings-modal__navigation-item${currentIndex === 1 ? ' active' : ''}`}>
<FormattedMessage id='settings.collapsed_statuses' defaultMessage='Collapsed toots' />
</a>
<a href='/settings/preferences' className='settings-modal__navigation-item'>
<i className='fa fa-fw fa-cogs' /> <FormattedMessage id='settings.preferences' defaultMessage='User preferences' />
</a>
<a onClick={onClose} role='button' tabIndex='0' className='settings-modal__navigation-close'>
<FormattedMessage id='settings.close' defaultMessage='Close' />
</a>
</nav>
<div className='settings-modal__content'>
{
[
<General />,
<CollapsedStatuses />,
][currentIndex] || <General />
}
</div>
</div>
);
}
}

View file

@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import { changeLocalSetting } from '../../../actions/local_settings';
import { closeModal } from '../../../actions/modal';
import SettingsModal from '../components/settings_modal';
const mapStateToProps = state => ({
settings: state.get('local_settings'),
});
const mapDispatchToProps = dispatch => ({
toggleSetting (setting, e) {
dispatch(changeLocalSetting(setting, e.target.checked));
},
onClose () {
dispatch(closeModal());
},
});
export default connect(mapStateToProps, mapDispatchToProps)(SettingsModal);

View file

@ -73,7 +73,8 @@ class WrappedRoute extends React.Component {
}
const mapStateToProps = state => ({
layout: state.getIn(['localSettings', 'layout']),
layout: state.getIn(['local_settings', 'layout']),
isWide: state.getIn(['local_settings', 'stretch']),
});
@connect(mapStateToProps)
@ -83,6 +84,7 @@ export default class UI extends React.PureComponent {
dispatch: PropTypes.func.isRequired,
children: PropTypes.node,
layout: PropTypes.string,
isWide: PropTypes.bool,
};
state = {
@ -179,7 +181,7 @@ export default class UI extends React.PureComponent {
render () {
const { width, draggingOver } = this.state;
const { children, layout } = this.props;
const { children, layout, isWide } = this.props;
const columnsClass = layout => {
switch (layout) {
@ -193,7 +195,7 @@ export default class UI extends React.PureComponent {
};
return (
<div className={'ui ' + columnsClass(layout)} ref={this.setRef}>
<div className={'ui ' + columnsClass(layout) + (isWide ? ' wide' : '')} ref={this.setRef}>
<TabsBar />
<ColumnsAreaContainer singleColumn={isMobile(width, layout)}>
<WrappedSwitch>

View file

@ -188,10 +188,6 @@
},
{
"descriptors": [
{
"defaultMessage": "{name} boosted",
"id": "status.reblogged_by"
},
{
"defaultMessage": "Collapse",
"id": "status.collapse"
@ -199,6 +195,10 @@
{
"defaultMessage": "Uncollapse",
"id": "status.uncollapse"
},
{
"defaultMessage": "{name} boosted",
"id": "status.reblogged_by"
}
],
"path": "app/javascript/mastodon/components/status.json"
@ -652,8 +652,8 @@
"id": "navigation_bar.community_timeline"
},
{
"defaultMessage": "Preferences",
"id": "navigation_bar.preferences"
"defaultMessage": "App settings",
"id": "navigation_bar.app_settings"
},
{
"defaultMessage": "Logout",
@ -667,13 +667,13 @@
"defaultMessage": "Mobile",
"id": "layout.mobile"
},
{
"defaultMessage": "Desktop",
"id": "layout.desktop"
},
{
"defaultMessage": "Auto",
"id": "layout.auto"
},
{
"defaultMessage": "Desktop",
"id": "layout.desktop"
}
],
"path": "app/javascript/mastodon/features/compose/index.json"
@ -743,6 +743,10 @@
"defaultMessage": "Preferences",
"id": "navigation_bar.preferences"
},
{
"defaultMessage": "App settings",
"id": "navigation_bar.app_settings"
},
{
"defaultMessage": "Follow requests",
"id": "navigation_bar.follow_requests"
@ -1073,7 +1077,7 @@
"id": "onboarding.page_one.welcome"
},
{
"defaultMessage": "{domain} is an 'instance' of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"defaultMessage": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"id": "onboarding.page_one.federation"
},
{
@ -1121,7 +1125,7 @@
"id": "onboarding.page_six.almost_done"
},
{
"defaultMessage": "{domain} runs on Glitchsoc, a friendly fork of {Mastodon}. Glitchsoc is fully compatible with any Mastodon instance or app. You can report bugs, request features, or contribute to the code on {github}.",
"defaultMessage": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"id": "onboarding.page_six.github"
},
{
@ -1168,6 +1172,71 @@
],
"path": "app/javascript/mastodon/features/ui/components/report_modal.json"
},
{
"descriptors": [
{
"defaultMessage": "General",
"id": "settings.general"
},
{
"defaultMessage": "Wide view (Desktop mode only)",
"id": "settings.wide_view"
},
{
"defaultMessage": "Collapsed toots",
"id": "settings.collapsed_statuses"
},
{
"defaultMessage": "Enable collapsed toots",
"id": "settings.enable_collapsed"
},
{
"defaultMessage": "Automatic collapsing",
"id": "settings.auto_collapse"
},
{
"defaultMessage": "Everything",
"id": "settings.auto_collapse_all"
},
{
"defaultMessage": "Notifications",
"id": "settings.auto_collapse_notifications"
},
{
"defaultMessage": "Lengthy toots",
"id": "settings.auto_collapse_lengthy"
},
{
"defaultMessage": "Replies",
"id": "settings.auto_collapse_replies"
},
{
"defaultMessage": "Toots with media",
"id": "settings.auto_collapse_media"
},
{
"defaultMessage": "Image backgrounds",
"id": "settings.image_backgrounds"
},
{
"defaultMessage": "Give collapsed toots an image background",
"id": "settings.image_backgrounds_users"
},
{
"defaultMessage": "Preview collapsed toot media",
"id": "settings.image_backgrounds_media"
},
{
"defaultMessage": "User preferences",
"id": "settings.global_settings"
},
{
"defaultMessage": "Close",
"id": "settings.close"
}
],
"path": "app/javascript/mastodon/features/ui/components/settings_modal.json"
},
{
"descriptors": [
{

View file

@ -87,6 +87,7 @@
"loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
"navigation_bar.app_settings": "App settings",
"navigation_bar.blocks": "Blocked users",
"navigation_bar.community_timeline": "Local timeline",
"navigation_bar.edit_profile": "Edit profile",
@ -146,6 +147,21 @@
"report.target": "Reporting {target}",
"search.placeholder": "Search",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"settings.auto_collapse": "Automatic collapsing",
"settings.auto_collapse_all": "Everything",
"settings.auto_collapse_lengthy": "Lengthy toots",
"settings.auto_collapse_media": "Toots with media",
"settings.auto_collapse_notifications": "Notifications",
"settings.auto_collapse_replies": "Replies",
"settings.close": "Close",
"settings.collapsed_statuses": "Collapsed toots",
"settings.enable_collapsed": "Enable collapsed toots",
"settings.general": "General",
"settings.global_settings": "User preferences",
"settings.image_backgrounds": "Image backgrounds",
"settings.image_backgrounds_media": "Preview collapsed toot media",
"settings.image_backgrounds_users": "Give collapsed toots an image background",
"settings.wide_view": "Wide view (Desktop mode only)",
"status.cannot_reblog": "This post cannot be boosted",
"status.collapse": "Collapse",
"status.delete": "Delete",

View file

@ -14,7 +14,7 @@ import relationships from './relationships';
import search from './search';
import notifications from './notifications';
import settings from './settings';
import localSettings from './local_settings';
import local_settings from './local_settings';
import status_lists from './status_lists';
import cards from './cards';
import reports from './reports';
@ -37,7 +37,7 @@ export default combineReducers({
search,
notifications,
settings,
localSettings,
local_settings,
cards,
reports,
contexts,

View file

@ -4,6 +4,21 @@ import Immutable from 'immutable';
const initialState = Immutable.Map({
layout : 'auto',
stretch : true,
collapsed : {
enabled : true,
auto : {
all : false,
notifications : true,
lengthy : true,
replies : false,
media : false,
},
backgrounds : {
user_backgrounds : false,
preview_images : false,
},
},
});
const hydrate = (state, localSettings) => state.mergeDeep(localSettings);
@ -11,7 +26,7 @@ const hydrate = (state, localSettings) => state.mergeDeep(localSettings);
export default function localSettings(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:
return hydrate(state, action.state.get('localSettings'));
return hydrate(state, action.state.get('local_settings'));
case LOCAL_SETTING_CHANGE:
return state.setIn(action.key, action.value);
default:

View file

@ -1351,6 +1351,10 @@
overflow-x: auto;
position: relative;
padding: 10px;
.wide & {
justify-content: center;
}
}
@include limited-single-column('screen and (max-width: 360px)', $parent: null) {
@ -1367,6 +1371,12 @@
flex-direction: column;
overflow: hidden;
.wide & {
flex: auto;
min-width: 330px;
max-width: 400px;
}
> .scrollable {
background: $ui-base-color;
}
@ -1387,6 +1397,12 @@
display: flex;
flex-direction: column;
overflow-y: auto;
.wide & {
flex: 1 1 200px;
min-width: 300px;
max-width: 400px;
}
}
.drawer__tab {
@ -1399,11 +1415,12 @@
text-align: center;
font-size: 16px;
border-bottom: 2px solid transparent;
outline: none;
cursor: pointer;
}
.column,
.drawer {
flex: 1 1 100%;
@supports(display: grid) { // hack to fix Chrome <57
contain: strict;
}
@ -1419,10 +1436,14 @@
}
}
:root { // Overrides .wide stylings for mobile view
@include single-column('screen and (max-width: 1024px)', $parent: null) {
.column,
.drawer {
flex: auto;
width: 100%;
min-width: 0;
max-width: none;
padding: 0;
}
@ -1435,6 +1456,7 @@
font-size: 16px;
}
}
}
@include multi-columns('screen and (min-width: 1025px)', $parent: null) {
.columns-area {
@ -1443,7 +1465,6 @@
.column,
.drawer {
flex: 0 0 auto;
padding: 10px;
padding-left: 5px;
padding-right: 5px;
@ -1771,6 +1792,8 @@
font-size: 16px;
padding: 15px;
text-decoration: none;
cursor: pointer;
outline: none;
&:hover {
background: lighten($ui-base-color, 11%);
@ -3312,6 +3335,85 @@ button.icon-button.active i.fa-retweet {
margin-bottom: 20px;
}
.settings-modal {
position: relative;
display: flex;
flex-direction: row;
background: $ui-secondary-color;
color: $ui-base-color;
border-radius: 8px;
height: 80vh;
width: 80vw;
max-width: 740px;
max-height: 450px;
overflow: hidden;
label {
display: block;
}
h1 {
font-size: 18px;
font-weight: 500;
line-height: 24px;
margin-bottom: 20px;
}
h2 {
font-size: 15px;
font-weight: 500;
line-height: 20px;
margin-top: 20px;
margin-bottom: 10px;
}
}
.settings-modal__navigation {
background: $primary-text-color;
color: $ui-base-color;
width: 200px;
font-size: 15px;
line-height: 20px;
overflow-y: auto;
.settings-modal__navigation-item, .settings-modal__navigation-close {
display: block;
padding: 15px 20px;
cursor: pointer;
outline: none;
text-decoration: none;
}
.settings-modal__navigation-item {
background: $primary-text-color;
color: inherit;
border-bottom: 1px $ui-primary-color solid;
transition: background .3s;
&:hover {
background: $ui-secondary-color;
}
&.active {
background: $ui-highlight-color;
color: $primary-text-color;
}
}
.settings-modal__navigation-close {
background: $error-value-color;
color: $primary-text-color;
}
}
.settings-modal__content {
display: block;
flex: auto;
padding: 15px 20px 15px 20px;
width: 360px;
overflow-y: auto;
}
.onboard-sliders {
display: inline-block;
max-width: 30px;

View file

@ -1,19 +1,5 @@
@import 'application';
@include multi-columns('screen and (min-width: 1300px)', $parent: null) {
.column {
flex-grow: 1 !important;
max-width: 400px;
}
.drawer {
flex-grow: 1 !important;
flex-basis: 200px !important;
min-width: 268px;
max-width: 400px;
}
}
.muted {
.status__content p, .status__content a {
color: lighten($ui-base-color, 35%);