Merge branch 'master' into glitch-soc/merge-upstream

Conflicts:
- app/controllers/settings/preferences_controller.rb
- app/lib/user_settings_decorator.rb
- app/models/user.rb

Conflicts due to the addition of a new preference upstream,
“advanced layout”.
This commit is contained in:
Thibaut Girka 2019-05-26 15:41:40 +02:00
commit 20d01a954e
40 changed files with 623 additions and 135 deletions

View File

@ -3,6 +3,39 @@ Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [2.8.4] - 2019-05-24
### Fixed
- Fix delivery not retrying on some inbox errors that should be retriable ([ThibG](https://github.com/tootsuite/mastodon/pull/10812))
- Fix unnecessary 5 minute cooldowns on signature verifications in some cases ([ThibG](https://github.com/tootsuite/mastodon/pull/10813))
- Fix possible race condition when processing statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10815))
### Security
- Require specific OAuth scopes for specific endpoints of the streaming API, instead of merely requiring a token for all endpoints, and allow using WebSockets protocol negotiation to specify the access token instead of using a query string ([ThibG](https://github.com/tootsuite/mastodon/pull/10818))
## [2.8.3] - 2019-05-19
### Added
- Add `og:image:alt` OpenGraph tag ([BenLubar](https://github.com/tootsuite/mastodon/pull/10779))
- Add clickable area below avatar in statuses in web UI ([Dar13](https://github.com/tootsuite/mastodon/pull/10766))
- Add crossed-out eye icon on account gallery in web UI ([Kjwon15](https://github.com/tootsuite/mastodon/pull/10715))
- Add media description tooltip to thumbnails in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10713))
### Changed
- Change "mark as sensitive" button into a checkbox for clarity ([ThibG](https://github.com/tootsuite/mastodon/pull/10748))
### Fixed
- Fix bug allowing users to publicly boost their private statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10775), [ThibG](https://github.com/tootsuite/mastodon/pull/10783))
- Fix performance in formatter by a little ([ThibG](https://github.com/tootsuite/mastodon/pull/10765))
- Fix some colors in the light theme ([yuzulabo](https://github.com/tootsuite/mastodon/pull/10754))
- Fix some colors of the high contrast theme ([yuzulabo](https://github.com/tootsuite/mastodon/pull/10711))
- Fix ambivalent active state of poll refresh button in web UI ([MaciekBaron](https://github.com/tootsuite/mastodon/pull/10720))
- Fix duplicate posting being possible from web UI ([hinaloe](https://github.com/tootsuite/mastodon/pull/10785))
- Fix "invited by" not showing up in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10791))
## [2.8.2] - 2019-05-05 ## [2.8.2] - 2019-05-05
### Added ### Added

View File

@ -86,7 +86,7 @@ RUN apt update && \
useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \ useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \
echo "mastodon:`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256`" | chpasswd echo "mastodon:`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256`" | chpasswd
# Install masto runtime deps # Install mastodon runtime deps
RUN apt -y --no-install-recommends install \ RUN apt -y --no-install-recommends install \
libssl1.1 libpq5 imagemagick ffmpeg \ libssl1.1 libpq5 imagemagick ffmpeg \
libicu60 libprotobuf10 libidn11 libyaml-0-2 \ libicu60 libprotobuf10 libidn11 libyaml-0-2 \
@ -95,7 +95,7 @@ RUN apt -y --no-install-recommends install \
ln -s /opt/mastodon /mastodon && \ ln -s /opt/mastodon /mastodon && \
gem install bundler && \ gem install bundler && \
rm -rf /var/cache && \ rm -rf /var/cache && \
rm -rf /var/lib/apt rm -rf /var/lib/apt/lists/*
# Add tini # Add tini
ENV TINI_VERSION="0.18.0" ENV TINI_VERSION="0.18.0"
@ -104,11 +104,11 @@ ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tin
RUN echo "$TINI_SUM tini" | sha256sum -c - RUN echo "$TINI_SUM tini" | sha256sum -c -
RUN chmod +x /tini RUN chmod +x /tini
# Copy over masto source, and dependencies from building, and set permissions # Copy over mastodon source, and dependencies from building, and set permissions
COPY --chown=mastodon:mastodon . /opt/mastodon COPY --chown=mastodon:mastodon . /opt/mastodon
COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon
# Run masto services in prod mode # Run mastodon services in prod mode
ENV RAILS_ENV="production" ENV RAILS_ENV="production"
ENV NODE_ENV="production" ENV NODE_ENV="production"

View File

@ -46,6 +46,7 @@ class Settings::PreferencesController < Settings::BaseController
:setting_hide_followers_count, :setting_hide_followers_count,
:setting_aggregate_reblogs, :setting_aggregate_reblogs,
:setting_show_application, :setting_show_application,
:setting_advanced_layout,
:setting_default_content_type, :setting_default_content_type,
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account), notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account),
interactions: %i(must_be_follower must_be_following) interactions: %i(must_be_follower must_be_following)

View File

@ -63,6 +63,14 @@ const messages = defineMessages({
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
}); });
const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
export const ensureComposeIsVisible = (getState, routerHistory) => {
if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) {
routerHistory.push('/statuses/new');
}
};
export function changeCompose(text) { export function changeCompose(text) {
return { return {
type: COMPOSE_CHANGE, type: COMPOSE_CHANGE,
@ -77,9 +85,7 @@ export function replyCompose(status, routerHistory) {
status: status, status: status,
}); });
if (!getState().getIn(['compose', 'mounted'])) { ensureComposeIsVisible(getState, routerHistory);
routerHistory.push('/statuses/new');
}
}; };
}; };
@ -102,9 +108,7 @@ export function mentionCompose(account, routerHistory) {
account: account, account: account,
}); });
if (!getState().getIn(['compose', 'mounted'])) { ensureComposeIsVisible(getState, routerHistory);
routerHistory.push('/statuses/new');
}
}; };
}; };
@ -115,9 +119,7 @@ export function directCompose(account, routerHistory) {
account: account, account: account,
}); });
if (!getState().getIn(['compose', 'mounted'])) { ensureComposeIsVisible(getState, routerHistory);
routerHistory.push('/statuses/new');
}
}; };
}; };

View File

@ -4,6 +4,7 @@ import { evictStatus } from '../storage/modifier';
import { deleteFromTimelines } from './timelines'; import { deleteFromTimelines } from './timelines';
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer'; import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer';
import { ensureComposeIsVisible } from './compose';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; 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) => { return (dispatch, getState) => {
let status = getState().getIn(['statuses', id]); let status = getState().getIn(['statuses', id]);
@ -156,10 +157,7 @@ export function deleteStatus(id, router, withRedraft = false) {
if (withRedraft) { if (withRedraft) {
dispatch(redraft(status, response.data.text)); dispatch(redraft(status, response.data.text));
ensureComposeIsVisible(getState, routerHistory);
if (!getState().getIn(['compose', 'mounted'])) {
router.push('/statuses/new');
}
} }
}).catch(error => { }).catch(error => {
dispatch(deleteStatusFail(id, error)); dispatch(deleteStatusFail(id, error));

View File

@ -49,7 +49,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
autoFocus: PropTypes.bool, autoFocus: PropTypes.bool,
className: PropTypes.string, className: PropTypes.string,
id: PropTypes.string, id: PropTypes.string,
searchTokens: ImmutablePropTypes.list, searchTokens: PropTypes.arrayOf(PropTypes.string),
maxLength: PropTypes.number, maxLength: PropTypes.number,
}; };

View File

@ -0,0 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'mastodon/components/icon';
const formatNumber = num => num > 40 ? '40+' : num;
const IconWithBadge = ({ id, count, className }) => (
<i className='icon-with-badge'>
<Icon id={id} fixedWidth className={className} />
{count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>}
</i>
);
IconWithBadge.propTypes = {
id: PropTypes.string.isRequired,
count: PropTypes.number.isRequired,
className: PropTypes.string,
};
export default IconWithBadge;

View File

@ -244,6 +244,8 @@ class MediaGallery extends React.PureComponent {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
defaultWidth: PropTypes.number, defaultWidth: PropTypes.number,
cacheWidth: PropTypes.func, cacheWidth: PropTypes.func,
visible: PropTypes.bool,
onToggleVisibility: PropTypes.func,
}; };
static defaultProps = { static defaultProps = {
@ -251,18 +253,24 @@ class MediaGallery extends React.PureComponent {
}; };
state = { state = {
visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all', visible: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
width: this.props.defaultWidth, width: this.props.defaultWidth,
}; };
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (!is(nextProps.media, this.props.media)) { if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) {
this.setState({ visible: !nextProps.sensitive }); this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' });
} else if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
this.setState({ visible: nextProps.visible });
} }
} }
handleOpen = () => { handleOpen = () => {
this.setState({ visible: !this.state.visible }); if (this.props.onToggleVisibility) {
this.props.onToggleVisibility();
} else {
this.setState({ visible: !this.state.visible });
}
} }
handleClick = (index) => { handleClick = (index) => {

View File

@ -17,6 +17,8 @@ import { HotKeys } from 'react-hotkeys';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import PollContainer from 'mastodon/containers/poll_container'; import PollContainer from 'mastodon/containers/poll_container';
import { displayMedia } from '../initial_state';
import { is } from 'immutable';
// We use the component (and not the container) since we do not want // We use the component (and not the container) since we do not want
// to use the progress bar to show download progress // to use the progress bar to show download progress
@ -39,6 +41,18 @@ export const textForScreenReader = (intl, status, rebloggedByText = false) => {
return values.join(', '); return values.join(', ');
}; };
export const defaultMediaVisibility = (status) => {
if (!status) {
return undefined;
}
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
status = status.get('reblog');
}
return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
};
export default @injectIntl export default @injectIntl
class Status extends ImmutablePureComponent { class Status extends ImmutablePureComponent {
@ -85,6 +99,10 @@ class Status extends ImmutablePureComponent {
'hidden', 'hidden',
]; ];
state = {
showMedia: defaultMediaVisibility(this.props.status),
};
// Track height changes we know about to compensate scrolling // Track height changes we know about to compensate scrolling
componentDidMount () { componentDidMount () {
this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card'); this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
@ -98,11 +116,19 @@ class Status extends ImmutablePureComponent {
} }
} }
componentWillReceiveProps (nextProps) {
if (!is(nextProps.status, this.props.status) && nextProps.status) {
this.setState({ showMedia: defaultMediaVisibility(nextProps.status) });
}
}
// Compensate height changes // Compensate height changes
componentDidUpdate (prevProps, prevState, snapshot) { componentDidUpdate (prevProps, prevState, snapshot) {
const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card'); const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
if (doShowCard && !this.didShowCard) { if (doShowCard && !this.didShowCard) {
this.didShowCard = true; this.didShowCard = true;
if (snapshot !== null && this.props.updateScrollBottom) { if (snapshot !== null && this.props.updateScrollBottom) {
if (this.node && this.node.offsetTop < snapshot.top) { if (this.node && this.node.offsetTop < snapshot.top) {
this.props.updateScrollBottom(snapshot.height - snapshot.top); this.props.updateScrollBottom(snapshot.height - snapshot.top);
@ -122,6 +148,10 @@ class Status extends ImmutablePureComponent {
} }
} }
handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia });
}
handleClick = () => { handleClick = () => {
if (this.props.onClick) { if (this.props.onClick) {
this.props.onClick(); this.props.onClick();
@ -136,6 +166,17 @@ class Status extends ImmutablePureComponent {
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`); this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
} }
handleExpandClick = (e) => {
if (e.button === 0) {
if (!this.context.router) {
return;
}
const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
}
}
handleAccountClick = (e) => { handleAccountClick = (e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
const id = e.currentTarget.getAttribute('data-id'); const id = e.currentTarget.getAttribute('data-id');
@ -198,6 +239,10 @@ class Status extends ImmutablePureComponent {
this.props.onToggleHidden(this._properStatus()); this.props.onToggleHidden(this._properStatus());
} }
handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility();
}
_properStatus () { _properStatus () {
const { status } = this.props; const { status } = this.props;
@ -298,6 +343,8 @@ class Status extends ImmutablePureComponent {
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo} onOpenVideo={this.handleOpenVideo}
cacheWidth={this.props.cacheMediaWidth} cacheWidth={this.props.cacheMediaWidth}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/> />
)} )}
</Bundle> </Bundle>
@ -313,6 +360,8 @@ class Status extends ImmutablePureComponent {
onOpenMedia={this.props.onOpenMedia} onOpenMedia={this.props.onOpenMedia}
cacheWidth={this.props.cacheMediaWidth} cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth} defaultWidth={this.props.cachedMediaWidth}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/> />
)} )}
</Bundle> </Bundle>
@ -348,6 +397,7 @@ class Status extends ImmutablePureComponent {
moveUp: this.handleHotkeyMoveUp, moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown, moveDown: this.handleHotkeyMoveDown,
toggleHidden: this.handleHotkeyToggleHidden, toggleHidden: this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
}; };
return ( return (
@ -356,7 +406,7 @@ class Status extends ImmutablePureComponent {
{prepend} {prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}> <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
<div className='status__expand' onClick={this.handleClick} role='presentation' /> <div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
<div className='status__info'> <div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>

View File

@ -46,7 +46,7 @@ class ActionBar extends React.PureComponent {
return ( return (
<div className='compose__action-bar'> <div className='compose__action-bar'>
<div className='compose__action-bar-dropdown'> <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>
</div> </div>
); );

View File

@ -20,7 +20,7 @@ export default class NavigationBar extends ImmutablePureComponent {
<div className='navigation-bar'> <div className='navigation-bar'>
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span> <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> </Permalink>
<div className='navigation-bar__profile'> <div className='navigation-bar__profile'>

View File

@ -47,6 +47,10 @@ class SearchPopout extends React.PureComponent {
export default @injectIntl export default @injectIntl
class Search extends React.PureComponent { class Search extends React.PureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,
};
static propTypes = { static propTypes = {
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
submitted: PropTypes.bool, submitted: PropTypes.bool,
@ -54,6 +58,7 @@ class Search extends React.PureComponent {
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired, onClear: PropTypes.func.isRequired,
onShow: PropTypes.func.isRequired, onShow: PropTypes.func.isRequired,
openInRoute: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -76,7 +81,12 @@ class Search extends React.PureComponent {
handleKeyUp = (e) => { handleKeyUp = (e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
this.props.onSubmit(); this.props.onSubmit();
if (this.props.openInRoute) {
this.context.router.history.push('/search');
}
} else if (e.key === 'Escape') { } else if (e.key === 'Escape') {
document.querySelector('.ui').parentElement.focus(); document.querySelector('.ui').parentElement.focus();
} }

View File

@ -56,7 +56,7 @@ class FollowRequests extends ImmutablePureComponent {
const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />; const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />;
return ( return (
<Column icon='users' heading={intl.formatMessage(messages.heading)}> <Column icon='user-plus' heading={intl.formatMessage(messages.heading)}>
<ColumnBackButtonSlim /> <ColumnBackButtonSlim />
<ScrollableList <ScrollableList
scrollKey='follow_requests' scrollKey='follow_requests'

View File

@ -9,12 +9,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { me, invitesEnabled, version, profile_directory, repository, source_url } from '../../initial_state'; import { me, invitesEnabled, version, profile_directory, repository, source_url } from '../../initial_state';
import { fetchFollowRequests } from 'mastodon/actions/accounts'; import { fetchFollowRequests } from 'mastodon/actions/accounts';
import { changeSetting } from 'mastodon/actions/settings';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import NavigationBar from '../compose/components/navigation_bar'; import NavigationBar from '../compose/components/navigation_bar';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import Toggle from 'react-toggle';
const messages = defineMessages({ const messages = defineMessages({
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
@ -41,12 +39,10 @@ const messages = defineMessages({
const mapStateToProps = state => ({ const mapStateToProps = state => ({
myAccount: state.getIn(['accounts', me]), myAccount: state.getIn(['accounts', me]),
unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size, unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
forceSingleColumn: state.getIn(['settings', 'forceSingleColumn'], false),
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
fetchFollowRequests: () => dispatch(fetchFollowRequests()), fetchFollowRequests: () => dispatch(fetchFollowRequests()),
changeForceSingleColumn: checked => dispatch(changeSetting(['forceSingleColumn'], checked)),
}); });
const badgeDisplay = (number, limit) => { const badgeDisplay = (number, limit) => {
@ -59,10 +55,16 @@ const badgeDisplay = (number, limit) => {
} }
}; };
const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
export default @connect(mapStateToProps, mapDispatchToProps) export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl @injectIntl
class GettingStarted extends ImmutablePureComponent { class GettingStarted extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,
};
static propTypes = { static propTypes = {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
myAccount: ImmutablePropTypes.map.isRequired, myAccount: ImmutablePropTypes.map.isRequired,
@ -71,24 +73,23 @@ class GettingStarted extends ImmutablePureComponent {
fetchFollowRequests: PropTypes.func.isRequired, fetchFollowRequests: PropTypes.func.isRequired,
unreadFollowRequests: PropTypes.number, unreadFollowRequests: PropTypes.number,
unreadNotifications: PropTypes.number, unreadNotifications: PropTypes.number,
forceSingleColumn: PropTypes.bool,
changeForceSingleColumn: PropTypes.func.isRequired,
}; };
componentDidMount () { componentDidMount () {
const { myAccount, fetchFollowRequests } = this.props; const { myAccount, fetchFollowRequests, multiColumn } = this.props;
if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
this.context.router.history.replace('/timelines/home');
return;
}
if (myAccount.get('locked')) { if (myAccount.get('locked')) {
fetchFollowRequests(); fetchFollowRequests();
} }
} }
handleForceSingleColumnChange = ({ target }) => {
this.props.changeForceSingleColumn(target.checked);
}
render () { render () {
const { intl, myAccount, multiColumn, unreadFollowRequests, forceSingleColumn } = this.props; const { intl, myAccount, multiColumn, unreadFollowRequests } = this.props;
const navItems = []; const navItems = [];
let i = 1; let i = 1;
@ -133,7 +134,7 @@ class GettingStarted extends ImmutablePureComponent {
height += 48*3; height += 48*3;
if (myAccount.get('locked')) { if (myAccount.get('locked')) {
navItems.push(<ColumnLink key={i++} icon='users' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />); navItems.push(<ColumnLink key={i++} icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
height += 48; height += 48;
} }
@ -187,11 +188,6 @@ class GettingStarted extends ImmutablePureComponent {
</p> </p>
</div> </div>
</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> </Column>
); );
} }

View File

@ -60,6 +60,10 @@ class KeyboardShortcuts extends ImmutablePureComponent {
<td><kbd>x</kbd></td> <td><kbd>x</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td> <td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td>
</tr> </tr>
<tr>
<td><kbd>h</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.toggle_sensitivity' defaultMessage='to show/hide media' /></td>
</tr>
<tr> <tr>
<td><kbd>up</kbd>, <kbd>k</kbd></td> <td><kbd>up</kbd>, <kbd>k</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.up' defaultMessage='to move up in the list' /></td> <td><FormattedMessage id='keyboard_shortcuts.up' defaultMessage='to move up in the list' /></td>

View 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;

View File

@ -30,6 +30,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
onHeightChange: PropTypes.func, onHeightChange: PropTypes.func,
domain: PropTypes.string.isRequired, domain: PropTypes.string.isRequired,
compact: PropTypes.bool, compact: PropTypes.bool,
showMedia: PropTypes.bool,
onToggleMediaVisibility: PropTypes.func,
}; };
state = { state = {
@ -122,6 +124,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
inline inline
onOpenVideo={this.handleOpenVideo} onOpenVideo={this.handleOpenVideo}
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
visible={this.props.showMedia}
onToggleVisibility={this.props.onToggleMediaVisibility}
/> />
); );
} else { } else {
@ -132,6 +136,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
media={status.get('media_attachments')} media={status.get('media_attachments')}
height={300} height={300}
onOpenMedia={this.props.onOpenMedia} onOpenMedia={this.props.onOpenMedia}
visible={this.props.showMedia}
onToggleVisibility={this.props.onToggleMediaVisibility}
/> />
); );
} }

View File

@ -43,7 +43,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import { boostModal, deleteModal } from '../../initial_state'; import { boostModal, deleteModal } from '../../initial_state';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
import { textForScreenReader } from '../../components/status'; import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
const messages = defineMessages({ const messages = defineMessages({
@ -131,6 +131,7 @@ class Status extends ImmutablePureComponent {
state = { state = {
fullscreen: false, fullscreen: false,
showMedia: defaultMediaVisibility(this.props.status),
}; };
componentWillMount () { componentWillMount () {
@ -146,6 +147,14 @@ class Status extends ImmutablePureComponent {
this._scrolledIntoView = false; this._scrolledIntoView = false;
this.props.dispatch(fetchStatus(nextProps.params.statusId)); this.props.dispatch(fetchStatus(nextProps.params.statusId));
} }
if (!Immutable.is(nextProps.status, this.props.status) && nextProps.status) {
this.setState({ showMedia: defaultMediaVisibility(nextProps.status) });
}
}
handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia });
} }
handleFavouriteClick = (status) => { handleFavouriteClick = (status) => {
@ -312,6 +321,10 @@ class Status extends ImmutablePureComponent {
this.handleToggleHidden(this.props.status); this.handleToggleHidden(this.props.status);
} }
handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility();
}
handleMoveUp = id => { handleMoveUp = id => {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, ancestorsIds, descendantsIds } = this.props;
@ -432,6 +445,7 @@ class Status extends ImmutablePureComponent {
mention: this.handleHotkeyMention, mention: this.handleHotkeyMention,
openProfile: this.handleHotkeyOpenProfile, openProfile: this.handleHotkeyOpenProfile,
toggleHidden: this.handleHotkeyToggleHidden, toggleHidden: this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
}; };
return ( return (
@ -455,6 +469,8 @@ class Status extends ImmutablePureComponent {
onOpenMedia={this.handleOpenMedia} onOpenMedia={this.handleOpenMedia}
onToggleHidden={this.handleToggleHidden} onToggleHidden={this.handleToggleHidden}
domain={domain} domain={domain}
showMedia={this.state.showMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility}
/> />
<ActionBar <ActionBar

View File

@ -14,6 +14,8 @@ import DrawerLoading from './drawer_loading';
import BundleColumnError from './bundle_column_error'; import BundleColumnError from './bundle_column_error';
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components'; import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import ComposePanel from './compose_panel';
import NavigationPanel from './navigation_panel';
import detectPassiveEvents from 'detect-passive-events'; import detectPassiveEvents from 'detect-passive-events';
import { scrollRight } from '../../../scroll'; import { scrollRight } from '../../../scroll';
@ -173,14 +175,22 @@ class ColumnsArea extends ImmutablePureComponent {
return ( return (
<div className='columns-area__panels'> <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'> <div className='columns-area__panels__main'>
<TabsBar key='tabs' /> <TabsBar key='tabs' />
{content} {content}
</div> </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} {floatingActionButton}
</div> </div>

View File

@ -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;

View File

@ -0,0 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import { fetchFollowRequests } from 'mastodon/actions/accounts';
import { connect } from 'react-redux';
import { NavLink, withRouter } from 'react-router-dom';
import IconWithBadge from 'mastodon/components/icon_with_badge';
import { me } from 'mastodon/initial_state';
import { List as ImmutableList } from 'immutable';
import { FormattedMessage } from 'react-intl';
const mapStateToProps = state => ({
locked: state.getIn(['accounts', me, 'locked']),
count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
});
export default @withRouter
@connect(mapStateToProps)
class FollowRequestsNavLink extends React.Component {
static propTypes = {
dispatch: PropTypes.func.isRequired,
locked: PropTypes.bool,
count: PropTypes.number.isRequired,
};
componentDidMount () {
const { dispatch, locked } = this.props;
if (locked) {
dispatch(fetchFollowRequests());
}
}
render () {
const { locked, count } = this.props;
if (!locked || count === 0) {
return null;
}
return <NavLink className='column-link column-link--transparent' to='/follow_requests'><IconWithBadge className='column-link__icon' id='user-plus' count={count} /><FormattedMessage id='navigation_bar.follow_requests' defaultMessage='Follow requests' /></NavLink>;
}
}

View 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'))).take(4);
});
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 || lists.isEmpty()) {
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>
);
}
}

View File

@ -0,0 +1,29 @@
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 FollowRequestsNavLink from './follow_requests_nav_link';
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>
<FollowRequestsNavLink />
<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);

View File

@ -1,23 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Icon from 'mastodon/components/icon'; import IconWithBadge from 'mastodon/components/icon_with_badge';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
count: state.getIn(['notifications', 'unread']), count: state.getIn(['notifications', 'unread']),
id: 'bell',
}); });
const formatNumber = num => num > 99 ? '99+' : num; export default connect(mapStateToProps)(IconWithBadge);
const NotificationsCounterIcon = ({ count }) => (
<i className='icon-with-badge'>
<Icon id='bell' fixedWidth />
{count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>}
</i>
);
NotificationsCounterIcon.propTypes = {
count: PropTypes.number.isRequired,
};
export default connect(mapStateToProps)(NotificationsCounterIcon);

View File

@ -8,14 +8,12 @@ import Icon from 'mastodon/components/icon';
import NotificationsCounterIcon from './notifications_counter_icon'; import NotificationsCounterIcon from './notifications_counter_icon';
export const links = [ 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' 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' 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 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' 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 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 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 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' 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 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>,
]; ];
export function getIndex (path) { export function getIndex (path) {

View File

@ -44,8 +44,9 @@ import {
Mutes, Mutes,
PinnedStatuses, PinnedStatuses,
Lists, Lists,
Search,
} from './util/async-components'; } 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 previewMediaState } from './components/media_modal';
import { previewState as previewVideoState } from './components/video_modal'; import { previewState as previewVideoState } from './components/video_modal';
@ -62,7 +63,6 @@ const mapStateToProps = state => ({
hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0, hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0, hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null, dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
forceSingleColumn: state.getIn(['settings', 'forceSingleColumn'], false),
}); });
const keyMap = { const keyMap = {
@ -93,6 +93,7 @@ const keyMap = {
goToMuted: 'g m', goToMuted: 'g m',
goToRequests: 'g r', goToRequests: 'g r',
toggleHidden: 'x', toggleHidden: 'x',
toggleSensitive: 'h',
}; };
class SwitchingColumnsArea extends React.PureComponent { class SwitchingColumnsArea extends React.PureComponent {
@ -101,7 +102,6 @@ class SwitchingColumnsArea extends React.PureComponent {
children: PropTypes.node, children: PropTypes.node,
location: PropTypes.object, location: PropTypes.object,
onLayoutChange: PropTypes.func.isRequired, onLayoutChange: PropTypes.func.isRequired,
forceSingleColumn: PropTypes.bool,
}; };
state = { state = {
@ -140,7 +140,7 @@ class SwitchingColumnsArea extends React.PureComponent {
} }
render () { render () {
const { children, forceSingleColumn } = this.props; const { children } = this.props;
const { mobile } = this.state; const { mobile } = this.state;
const singleColumn = forceSingleColumn || mobile; const singleColumn = forceSingleColumn || mobile;
const redirect = singleColumn ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />; const redirect = singleColumn ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />;
@ -162,7 +162,7 @@ class SwitchingColumnsArea extends React.PureComponent {
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/pinned' component={PinnedStatuses} 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/new' component={Compose} content={children} />
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
@ -207,7 +207,6 @@ class UI extends React.PureComponent {
location: PropTypes.object, location: PropTypes.object,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
dropdownMenuIsOpen: PropTypes.bool, dropdownMenuIsOpen: PropTypes.bool,
forceSingleColumn: PropTypes.bool,
}; };
state = { state = {
@ -456,7 +455,7 @@ class UI extends React.PureComponent {
render () { render () {
const { draggingOver } = this.state; const { draggingOver } = this.state;
const { children, isComposing, location, dropdownMenuIsOpen, forceSingleColumn } = this.props; const { children, isComposing, location, dropdownMenuIsOpen } = this.props;
const handlers = { const handlers = {
help: this.handleHotkeyToggleHelp, help: this.handleHotkeyToggleHelp,
@ -482,7 +481,7 @@ class UI extends React.PureComponent {
return ( return (
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused> <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 }}> <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} {children}
</SwitchingColumnsArea> </SwitchingColumnsArea>

View File

@ -129,3 +129,7 @@ export function ListEditor () {
export function ListAdder () { export function ListAdder () {
return import(/*webpackChunkName: "features/list_adder" */'../../list_adder'); return import(/*webpackChunkName: "features/list_adder" */'../../list_adder');
} }
export function Search () {
return import(/*webpackChunkName: "features/search" */'../../search');
}

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { fromJS } from 'immutable'; import { fromJS, is } from 'immutable';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
@ -102,6 +102,8 @@ class Video extends React.PureComponent {
detailed: PropTypes.bool, detailed: PropTypes.bool,
inline: PropTypes.bool, inline: PropTypes.bool,
cacheWidth: PropTypes.func, cacheWidth: PropTypes.func,
visible: PropTypes.bool,
onToggleVisibility: PropTypes.func,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
blurhash: PropTypes.string, blurhash: PropTypes.string,
link: PropTypes.node, link: PropTypes.node,
@ -117,7 +119,7 @@ class Video extends React.PureComponent {
fullscreen: false, fullscreen: false,
hovered: false, hovered: false,
muted: false, muted: false,
revealed: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all', revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
}; };
// hard coded in components.scss // hard coded in components.scss
@ -280,7 +282,16 @@ class Video extends React.PureComponent {
document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true); document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
} }
componentDidUpdate (prevProps) { componentWillReceiveProps (nextProps) {
if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
this.setState({ revealed: nextProps.visible });
}
}
componentDidUpdate (prevProps, prevState) {
if (prevState.revealed && !this.state.revealed && this.video) {
this.video.pause();
}
if (prevProps.blurhash !== this.props.blurhash && this.props.blurhash) { if (prevProps.blurhash !== this.props.blurhash && this.props.blurhash) {
this._decode(); this._decode();
} }
@ -316,11 +327,11 @@ class Video extends React.PureComponent {
} }
toggleReveal = () => { toggleReveal = () => {
if (this.state.revealed) { if (this.props.onToggleVisibility) {
this.video.pause(); this.props.onToggleVisibility();
} else {
this.setState({ revealed: !this.state.revealed });
} }
this.setState({ revealed: !this.state.revealed });
} }
handleLoadedData = () => { handleLoadedData = () => {

View File

@ -20,5 +20,6 @@ export const version = getMeta('version');
export const mascot = getMeta('mascot'); export const mascot = getMeta('mascot');
export const profile_directory = getMeta('profile_directory'); export const profile_directory = getMeta('profile_directory');
export const isStaff = getMeta('is_staff'); export const isStaff = getMeta('is_staff');
export const forceSingleColumn = !getMeta('advanced_layout');
export default initialState; export default initialState;

View File

@ -14,8 +14,6 @@ const initialState = ImmutableMap({
skinTone: 1, skinTone: 1,
forceSingleColumn: false,
home: ImmutableMap({ home: ImmutableMap({
shows: ImmutableMap({ shows: ImmutableMap({
reblog: true, reblog: true,

View File

@ -710,7 +710,7 @@
white-space: pre-wrap; white-space: pre-wrap;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 2px;
} }
} }
@ -1801,7 +1801,12 @@ a.account__display-name {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
&--start {
justify-content: flex-start;
}
&__inner { &__inner {
width: 285px;
pointer-events: auto; pointer-events: auto;
height: 100%; height: 100%;
} }
@ -1925,6 +1930,7 @@ a.account__display-name {
display: block; display: block;
flex: 1 1 auto; flex: 1 1 auto;
padding: 15px 10px; padding: 15px 10px;
padding-bottom: 13px;
color: $primary-text-color; color: $primary-text-color;
text-decoration: none; text-decoration: none;
text-align: center; text-align: center;
@ -1949,6 +1955,7 @@ a.account__display-name {
&:active { &:active {
@media screen and (min-width: 631px) { @media screen and (min-width: 631px) {
background: lighten($ui-base-color, 14%); background: lighten($ui-base-color, 14%);
border-bottom-color: lighten($ui-base-color, 14%);
} }
} }
@ -1978,11 +1985,21 @@ a.account__display-name {
padding: 0; padding: 0;
} }
.search__input,
.autosuggest-textarea__textarea { .autosuggest-textarea__textarea {
font-size: 16px; 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) { @media screen and (min-width: 360px) {
padding: 10px 0; padding: 10px 0;
} }
@ -2038,6 +2055,58 @@ a.account__display-name {
margin-top: 10px; 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 { .icon-with-badge {
position: relative; position: relative;
&__badge { &__badge {
position: absolute; position: absolute;
right: -13px; left: 9px;
top: -13px; top: -13px;
background: $ui-highlight-color; background: $ui-highlight-color;
border: 2px solid lighten($ui-base-color, 8%); 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 { .drawer__pager {
box-sizing: border-box; box-sizing: border-box;
padding: 0; 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 { .pseudo-drawer {
background: lighten($ui-base-color, 13%); background: lighten($ui-base-color, 13%);
font-size: 13px; font-size: 13px;
@ -2365,9 +2505,31 @@ a.account__display-name {
padding: 15px; padding: 15px;
text-decoration: none; text-decoration: none;
&:hover { &:hover,
&:focus,
&:active {
background: lighten($ui-base-color, 11%); 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 { .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 { .account__header__content {
color: $darker-text-color; color: $darker-text-color;
font-size: 14px; font-size: 14px;

View File

@ -36,6 +36,7 @@ class UserSettingsDecorator
user.settings['hide_network'] = hide_network_preference if change?('setting_hide_network') 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['aggregate_reblogs'] = aggregate_reblogs_preference if change?('setting_aggregate_reblogs')
user.settings['show_application'] = show_application_preference if change?('setting_show_application') user.settings['show_application'] = show_application_preference if change?('setting_show_application')
user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout')
user.settings['default_content_type']= default_content_type_preference if change?('setting_default_content_type') user.settings['default_content_type']= default_content_type_preference if change?('setting_default_content_type')
end end
@ -123,6 +124,10 @@ class UserSettingsDecorator
boolean_cast_setting 'setting_aggregate_reblogs' boolean_cast_setting 'setting_aggregate_reblogs'
end end
def advanced_layout_preference
boolean_cast_setting 'setting_advanced_layout'
end
def default_content_type_preference def default_content_type_preference
settings['setting_default_content_type'] settings['setting_default_content_type']
end end

View File

@ -104,7 +104,8 @@ class User < ApplicationRecord
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal, delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal,
:reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :hide_followers_count, :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :hide_followers_count,
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application, :default_content_type, to: :settings, prefix: :setting, allow_nil: false :expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
:advanced_layout, :default_content_type, to: :settings, prefix: :setting, allow_nil: false
attr_reader :invite_code attr_reader :invite_code
attr_writer :external attr_writer :external

View File

@ -46,6 +46,7 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:display_media] = object.current_account.user.setting_display_media store[:display_media] = object.current_account.user.setting_display_media
store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers
store[:reduce_motion] = object.current_account.user.setting_reduce_motion 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? store[:is_staff] = object.current_account.user.staff?
store[:default_content_type] = object.current_account.user.setting_default_content_type store[:default_content_type] = object.current_account.user.setting_default_content_type
end end

View File

@ -46,6 +46,9 @@
%hr#settings_web/ %hr#settings_web/
.fields-group
= f.input :setting_advanced_layout, as: :boolean, wrapper: :with_label
.fields-group .fields-group
= f.input :setting_unfollow_modal, as: :boolean, wrapper: :with_label = f.input :setting_unfollow_modal, as: :boolean, wrapper: :with_label
= f.input :setting_boost_modal, as: :boolean, wrapper: :with_label = f.input :setting_boost_modal, as: :boolean, wrapper: :with_label

View File

@ -26,6 +26,7 @@ cs:
password: Použijte alespoň 8 znaků password: Použijte alespoň 8 znaků
phrase: Shoda bude nalezena bez ohledu na velikost písmen v těle tootu či varování o obsahu phrase: Shoda bude nalezena bez ohledu na velikost písmen v těle tootu či varování o obsahu
scopes: Která API bude aplikaci povoleno používat. Pokud vyberete rozsah nejvyššího stupně, nebudete je muset vybírat jednotlivě. scopes: Která API bude aplikaci povoleno používat. Pokud vyberete rozsah nejvyššího stupně, nebudete je muset vybírat jednotlivě.
setting_advanced_layout: Pokročilé rozhraní se skládá z několika přizpůsobitelných sloupců
setting_aggregate_reblogs: Nezobrazovat nové boosty pro tooty, které byly nedávno boostnuty (ovlivňuje pouze nově přijaté boosty) setting_aggregate_reblogs: Nezobrazovat nové boosty pro tooty, které byly nedávno boostnuty (ovlivňuje pouze nově přijaté boosty)
setting_default_language: Jazyk vašich tootů může být detekován automaticky, není to však vždy přesné setting_default_language: Jazyk vašich tootů může být detekován automaticky, není to však vždy přesné
setting_display_media_default: Skrývat média označená jako citlivá setting_display_media_default: Skrývat média označená jako citlivá
@ -90,6 +91,7 @@ cs:
otp_attempt: Dvoufázový kód otp_attempt: Dvoufázový kód
password: Heslo password: Heslo
phrase: Klíčové slovo či fráze phrase: Klíčové slovo či fráze
setting_advanced_layout: Povolit pokročilé webové rozhraní
setting_aggregate_reblogs: Seskupovat boosty v časových osách setting_aggregate_reblogs: Seskupovat boosty v časových osách
setting_auto_play_gif: Automaticky přehrávat animace GIF setting_auto_play_gif: Automaticky přehrávat animace GIF
setting_boost_modal: Zobrazovat před boostnutím potvrzovací okno setting_boost_modal: Zobrazovat před boostnutím potvrzovací okno

View File

@ -26,6 +26,7 @@ en:
password: Use at least 8 characters password: Use at least 8 characters
phrase: Will be matched regardless of casing in text or content warning of a toot 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. 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_aggregate_reblogs: Do not show new boosts for toots that have been recently boosted (only affects newly-received boosts)
setting_default_content_type_html: When writing toots, assume they are written in raw HTML, unless specified otherwise setting_default_content_type_html: When writing toots, assume they are written in raw HTML, unless specified otherwise
setting_default_content_type_markdown: When writing toots, assume they are using Markdown for rich text formatting, unless specified otherwise setting_default_content_type_markdown: When writing toots, assume they are using Markdown for rich text formatting, unless specified otherwise
@ -93,6 +94,7 @@ en:
otp_attempt: Two-factor code otp_attempt: Two-factor code
password: Password password: Password
phrase: Keyword or phrase phrase: Keyword or phrase
setting_advanced_layout: Enable advanced web interface
setting_aggregate_reblogs: Group boosts in timelines setting_aggregate_reblogs: Group boosts in timelines
setting_auto_play_gif: Auto-play animated GIFs setting_auto_play_gif: Auto-play animated GIFs
setting_boost_modal: Show confirmation dialog before boosting setting_boost_modal: Show confirmation dialog before boosting

View File

@ -26,6 +26,7 @@ sk:
password: Zadaj aspoň osem znakov password: Zadaj aspoň osem znakov
phrase: Zhoda sa nájde nezávisle od toho, či je text napísaný, veľkými, alebo malými písmenami, či už v tele, alebo v hlavičke phrase: Zhoda sa nájde nezávisle od toho, či je text napísaný, veľkými, alebo malými písmenami, či už v tele, alebo v hlavičke
scopes: Ktoré API budú povolené aplikácii pre prístup. Ak vyberieš vrcholný stupeň, nemusíš už potom vyberať po jednom. scopes: Ktoré API budú povolené aplikácii pre prístup. Ak vyberieš vrcholný stupeň, nemusíš už potom vyberať po jednom.
setting_advanced_layout: Pokročilé užívateľské rozhranie sa skladá z viacero prispôsobiteľných stĺpcov
setting_aggregate_reblogs: Nezobrazuj nové vyzdvihnutia pre príspevky, ktoré už boli len nedávno povýšené (týka sa iba nanovo získaných povýšení) setting_aggregate_reblogs: Nezobrazuj nové vyzdvihnutia pre príspevky, ktoré už boli len nedávno povýšené (týka sa iba nanovo získaných povýšení)
setting_default_language: Jazyk tvojích príspevkov môže byť zistený automaticky, ale nieje to vždy presné setting_default_language: Jazyk tvojích príspevkov môže byť zistený automaticky, ale nieje to vždy presné
setting_display_media_default: Skry médiá označené ako citlivé setting_display_media_default: Skry médiá označené ako citlivé
@ -90,6 +91,7 @@ sk:
otp_attempt: Dvoj-faktorový overovací (2FA) kód otp_attempt: Dvoj-faktorový overovací (2FA) kód
password: Heslo password: Heslo
phrase: Kľúčové slovo, alebo fráza phrase: Kľúčové slovo, alebo fráza
setting_advanced_layout: Zapni pokročilé užívateľské rozhranie
setting_aggregate_reblogs: Zoskupuj vyzdvihnutia v časovej osi setting_aggregate_reblogs: Zoskupuj vyzdvihnutia v časovej osi
setting_auto_play_gif: Automaticky prehrávaj animované GIFy setting_auto_play_gif: Automaticky prehrávaj animované GIFy
setting_boost_modal: Zobrazuj potvrdzovacie okno pred povýšením setting_boost_modal: Zobrazuj potvrdzovacie okno pred povýšením
@ -99,7 +101,7 @@ sk:
setting_delete_modal: Zobrazuj potvrdzovacie okno pred vymazaním toot-u setting_delete_modal: Zobrazuj potvrdzovacie okno pred vymazaním toot-u
setting_display_media: Zobrazovanie médií setting_display_media: Zobrazovanie médií
setting_display_media_default: Štandard setting_display_media_default: Štandard
setting_display_media_hide_all: Skryť všetky setting_display_media_hide_all: Skry všetky
setting_display_media_show_all: Ukáž všetky setting_display_media_show_all: Ukáž všetky
setting_expand_spoilers: Stále rozbaľ príspevky označené varovaním o obsahu setting_expand_spoilers: Stále rozbaľ príspevky označené varovaním o obsahu
setting_hide_network: Ukri svoju sieť kontaktov setting_hide_network: Ukri svoju sieť kontaktov
@ -112,7 +114,7 @@ sk:
severity: Závažnosť severity: Závažnosť
type: Typ importu type: Typ importu
username: Prezývka username: Prezývka
username_or_email: Prezívka, alebo email username_or_email: Prezývka, alebo email
whole_word: Celé slovo whole_word: Celé slovo
featured_tag: featured_tag:
name: Haštag name: Haštag

View File

@ -35,6 +35,7 @@ defaults: &defaults
flavour: 'glitch' flavour: 'glitch'
skin: 'default' skin: 'default'
aggregate_reblogs: true aggregate_reblogs: true
advanced_layout: true
notification_emails: notification_emails:
follow: false follow: false
reblog: false reblog: false

View File

@ -13,7 +13,7 @@ module Mastodon
end end
def patch def patch
2 4
end end
def pre def pre