diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index 483af2c166bec9..216fea636d1af3 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
class Settings::PreferencesController < Settings::BaseController
+ layout 'admin'
+
+ before_action :authenticate_user!
+
def show; end
def update
@@ -80,6 +84,9 @@ def user_settings_params
:setting_aggregate_reblogs,
:setting_show_application,
:setting_default_content_type,
+
+ :setting_theme,
+ :setting_advanced_layout,
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account),
interactions: %i(must_be_follower must_be_following)
)
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 94062f2be8a672..33e6313640d1b5 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -63,6 +63,14 @@ const messages = defineMessages({
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
});
+const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 3);
+
+export const ensureComposeIsVisible = (getState, routerHistory) => {
+ if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) {
+ routerHistory.push('/statuses/new');
+ }
+};
+
export function changeCompose(text) {
return {
type: COMPOSE_CHANGE,
@@ -77,9 +85,7 @@ export function replyCompose(status, routerHistory) {
status: status,
});
- if (!getState().getIn(['compose', 'mounted'])) {
- routerHistory.push('/statuses/new');
- }
+ ensureComposeIsVisible(getState, routerHistory);
};
};
@@ -102,9 +108,7 @@ export function mentionCompose(account, routerHistory) {
account: account,
});
- if (!getState().getIn(['compose', 'mounted'])) {
- routerHistory.push('/statuses/new');
- }
+ ensureComposeIsVisible(getState, routerHistory);
};
};
@@ -115,9 +119,7 @@ export function directCompose(account, routerHistory) {
account: account,
});
- if (!getState().getIn(['compose', 'mounted'])) {
- routerHistory.push('/statuses/new');
- }
+ ensureComposeIsVisible(getState, routerHistory);
};
};
diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js
index 3916b9ac14e0ec..06a19afc3ad4b8 100644
--- a/app/javascript/mastodon/actions/statuses.js
+++ b/app/javascript/mastodon/actions/statuses.js
@@ -4,6 +4,7 @@ import { evictStatus } from '../storage/modifier';
import { deleteFromTimelines } from './timelines';
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer';
+import { ensureComposeIsVisible } from './compose';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
@@ -139,7 +140,7 @@ export function redraft(status, raw_text) {
};
};
-export function deleteStatus(id, router, withRedraft = false) {
+export function deleteStatus(id, routerHistory, withRedraft = false) {
return (dispatch, getState) => {
let status = getState().getIn(['statuses', id]);
@@ -156,10 +157,7 @@ export function deleteStatus(id, router, withRedraft = false) {
if (withRedraft) {
dispatch(redraft(status, response.data.text));
-
- if (!getState().getIn(['compose', 'mounted'])) {
- router.push('/statuses/new');
- }
+ ensureComposeIsVisible(getState, routerHistory);
}
}).catch(error => {
dispatch(deleteStatusFail(id, error));
diff --git a/app/javascript/mastodon/components/autosuggest_input.js b/app/javascript/mastodon/components/autosuggest_input.js
index 4b4aa8f0e5b65e..c7d965b53ac5e2 100644
--- a/app/javascript/mastodon/components/autosuggest_input.js
+++ b/app/javascript/mastodon/components/autosuggest_input.js
@@ -49,7 +49,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
autoFocus: PropTypes.bool,
className: PropTypes.string,
id: PropTypes.string,
- searchTokens: ImmutablePropTypes.list,
+ searchTokens: PropTypes.arrayOf(PropTypes.string),
maxLength: PropTypes.number,
};
diff --git a/app/javascript/mastodon/features/compose/components/action_bar.js b/app/javascript/mastodon/features/compose/components/action_bar.js
index 95d6eeb06d03a6..077226d70ba535 100644
--- a/app/javascript/mastodon/features/compose/components/action_bar.js
+++ b/app/javascript/mastodon/features/compose/components/action_bar.js
@@ -46,7 +46,7 @@ class ActionBar extends React.PureComponent {
return (
);
diff --git a/app/javascript/mastodon/features/compose/components/navigation_bar.js b/app/javascript/mastodon/features/compose/components/navigation_bar.js
index 9910eb4f9fd07a..d8d49cb95cd539 100644
--- a/app/javascript/mastodon/features/compose/components/navigation_bar.js
+++ b/app/javascript/mastodon/features/compose/components/navigation_bar.js
@@ -20,7 +20,7 @@ export default class NavigationBar extends ImmutablePureComponent {
{this.props.account.get('acct')}
-
+
diff --git a/app/javascript/mastodon/features/compose/components/search.js b/app/javascript/mastodon/features/compose/components/search.js
index 774658b1be8fa3..6833c43ef4f478 100644
--- a/app/javascript/mastodon/features/compose/components/search.js
+++ b/app/javascript/mastodon/features/compose/components/search.js
@@ -47,6 +47,10 @@ class SearchPopout extends React.PureComponent {
export default @injectIntl
class Search extends React.PureComponent {
+ static contextTypes = {
+ router: PropTypes.object.isRequired,
+ };
+
static propTypes = {
value: PropTypes.string.isRequired,
submitted: PropTypes.bool,
@@ -54,6 +58,7 @@ class Search extends React.PureComponent {
onSubmit: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired,
onShow: PropTypes.func.isRequired,
+ openInRoute: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
@@ -76,7 +81,12 @@ class Search extends React.PureComponent {
handleKeyUp = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
+
this.props.onSubmit();
+
+ if (this.props.openInRoute) {
+ this.context.router.history.push('/search');
+ }
} else if (e.key === 'Escape') {
document.querySelector('.ui').parentElement.focus();
}
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index a671578a009e05..cb3efb57bc90dd 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -9,12 +9,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me, invitesEnabled, version, profile_directory, repository, source_url } from '../../initial_state';
import { fetchFollowRequests } from 'mastodon/actions/accounts';
-import { changeSetting } from 'mastodon/actions/settings';
import { List as ImmutableList } from 'immutable';
import { Link } from 'react-router-dom';
import NavigationBar from '../compose/components/navigation_bar';
import Icon from 'mastodon/components/icon';
-import Toggle from 'react-toggle';
const messages = defineMessages({
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
@@ -41,12 +39,10 @@ const messages = defineMessages({
const mapStateToProps = state => ({
myAccount: state.getIn(['accounts', me]),
unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
- forceSingleColumn: state.getIn(['settings', 'forceSingleColumn'], false),
});
const mapDispatchToProps = dispatch => ({
fetchFollowRequests: () => dispatch(fetchFollowRequests()),
- changeForceSingleColumn: checked => dispatch(changeSetting(['forceSingleColumn'], checked)),
});
const badgeDisplay = (number, limit) => {
@@ -71,8 +67,6 @@ class GettingStarted extends ImmutablePureComponent {
fetchFollowRequests: PropTypes.func.isRequired,
unreadFollowRequests: PropTypes.number,
unreadNotifications: PropTypes.number,
- forceSingleColumn: PropTypes.bool,
- changeForceSingleColumn: PropTypes.func.isRequired,
};
componentDidMount () {
@@ -83,12 +77,8 @@ class GettingStarted extends ImmutablePureComponent {
}
}
- handleForceSingleColumnChange = ({ target }) => {
- this.props.changeForceSingleColumn(target.checked);
- }
-
render () {
- const { intl, myAccount, multiColumn, unreadFollowRequests, forceSingleColumn } = this.props;
+ const { intl, myAccount, multiColumn, unreadFollowRequests } = this.props;
const navItems = [];
let i = 1;
@@ -187,11 +177,6 @@ class GettingStarted extends ImmutablePureComponent {
-
-
);
}
diff --git a/app/javascript/mastodon/features/search/index.js b/app/javascript/mastodon/features/search/index.js
new file mode 100644
index 00000000000000..76bf70d4bd2360
--- /dev/null
+++ b/app/javascript/mastodon/features/search/index.js
@@ -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 = () => (
+
+);
+
+export default Search;
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index ae07b89075ce53..756db3c61ad825 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -14,6 +14,8 @@ import DrawerLoading from './drawer_loading';
import BundleColumnError from './bundle_column_error';
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components';
import Icon from 'mastodon/components/icon';
+import ComposePanel from './compose_panel';
+import NavigationPanel from './navigation_panel';
import detectPassiveEvents from 'detect-passive-events';
import { scrollRight } from '../../../scroll';
@@ -173,14 +175,22 @@ class ColumnsArea extends ImmutablePureComponent {
return (
-
+
{content}
-
+
{floatingActionButton}
diff --git a/app/javascript/mastodon/features/ui/components/compose_panel.js b/app/javascript/mastodon/features/ui/components/compose_panel.js
new file mode 100644
index 00000000000000..7c1158e5d23650
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/compose_panel.js
@@ -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 = () => (
+
+
+
+
+
+
+
+
+
+ {invitesEnabled && - ·
}
+ - ·
+ - ·
+ - ·
+ - ·
+ - ·
+ - ·
+ - ·
+
+
+
+
+ {repository} (v{version}) }}
+ />
+
+
+
+);
+
+export default ComposePanel;
diff --git a/app/javascript/mastodon/features/ui/components/list_panel.js b/app/javascript/mastodon/features/ui/components/list_panel.js
new file mode 100644
index 00000000000000..9a52c1b10ca764
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/list_panel.js
@@ -0,0 +1,55 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { fetchLists } from 'mastodon/actions/lists';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { NavLink, withRouter } from 'react-router-dom';
+import Icon from 'mastodon/components/icon';
+
+const getOrderedLists = createSelector([state => state.get('lists')], lists => {
+ if (!lists) {
+ return lists;
+ }
+
+ return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
+});
+
+const mapStateToProps = state => ({
+ lists: getOrderedLists(state),
+});
+
+export default @withRouter
+@connect(mapStateToProps)
+class ListPanel extends ImmutablePureComponent {
+
+ static propTypes = {
+ dispatch: PropTypes.func.isRequired,
+ lists: ImmutablePropTypes.list,
+ };
+
+ componentDidMount () {
+ const { dispatch } = this.props;
+ dispatch(fetchLists());
+ }
+
+ render () {
+ const { lists } = this.props;
+
+ if (!lists) {
+ return null;
+ }
+
+ return (
+
+
+
+ {lists.map(list => (
+ {list.get('title')}
+ ))}
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js
new file mode 100644
index 00000000000000..e2d962c6309dec
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import { NavLink, withRouter } from 'react-router-dom';
+import { FormattedMessage } from 'react-intl';
+import Icon from 'mastodon/components/icon';
+import NotificationsCounterIcon from './notifications_counter_icon';
+import ListPanel from './list_panel';
+
+const NavigationPanel = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export default withRouter(NavigationPanel);
diff --git a/app/javascript/mastodon/features/ui/components/notifications_counter_icon.js b/app/javascript/mastodon/features/ui/components/notifications_counter_icon.js
index deb907866dafd8..9673c57feff073 100644
--- a/app/javascript/mastodon/features/ui/components/notifications_counter_icon.js
+++ b/app/javascript/mastodon/features/ui/components/notifications_counter_icon.js
@@ -9,15 +9,16 @@ const mapStateToProps = state => ({
const formatNumber = num => num > 99 ? '99+' : num;
-const NotificationsCounterIcon = ({ count }) => (
+const NotificationsCounterIcon = ({ count, className }) => (
-
+
{count > 0 && {formatNumber(count)}}
);
NotificationsCounterIcon.propTypes = {
count: PropTypes.number.isRequired,
+ className: PropTypes.string,
};
export default connect(mapStateToProps)(NotificationsCounterIcon);
diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js
index aaef88d82e5466..7c8e825ad879ef 100644
--- a/app/javascript/mastodon/features/ui/components/tabs_bar.js
+++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js
@@ -8,14 +8,12 @@ import Icon from 'mastodon/components/icon';
import NotificationsCounterIcon from './notifications_counter_icon';
export const links = [
- ,
- ,
-
- ,
- ,
- ,
-
- ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
];
export function getIndex (path) {
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 6d5279157098b6..61fd77af74362e 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -44,8 +44,9 @@ import {
Mutes,
PinnedStatuses,
Lists,
+ Search,
} from './util/async-components';
-import { me } from '../../initial_state';
+import { me, forceSingleColumn } from '../../initial_state';
import { previewState as previewMediaState } from './components/media_modal';
import { previewState as previewVideoState } from './components/video_modal';
@@ -62,7 +63,6 @@ const mapStateToProps = state => ({
hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
- forceSingleColumn: state.getIn(['settings', 'forceSingleColumn'], false),
});
const keyMap = {
@@ -101,7 +101,6 @@ class SwitchingColumnsArea extends React.PureComponent {
children: PropTypes.node,
location: PropTypes.object,
onLayoutChange: PropTypes.func.isRequired,
- forceSingleColumn: PropTypes.bool,
};
state = {
@@ -140,7 +139,7 @@ class SwitchingColumnsArea extends React.PureComponent {
}
render () {
- const { children, forceSingleColumn } = this.props;
+ const { children } = this.props;
const { mobile } = this.state;
const singleColumn = forceSingleColumn || mobile;
const redirect = singleColumn ? : ;
@@ -162,7 +161,7 @@ class SwitchingColumnsArea extends React.PureComponent {
-
+
@@ -207,7 +206,6 @@ class UI extends React.PureComponent {
location: PropTypes.object,
intl: PropTypes.object.isRequired,
dropdownMenuIsOpen: PropTypes.bool,
- forceSingleColumn: PropTypes.bool,
};
state = {
@@ -456,7 +454,7 @@ class UI extends React.PureComponent {
render () {
const { draggingOver } = this.state;
- const { children, isComposing, location, dropdownMenuIsOpen, forceSingleColumn } = this.props;
+ const { children, isComposing, location, dropdownMenuIsOpen } = this.props;
const handlers = {
help: this.handleHotkeyToggleHelp,
@@ -482,7 +480,7 @@ class UI extends React.PureComponent {
return (
-
+
{children}
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 235fd2a073c255..6e8ed163a539a5 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -129,3 +129,7 @@ export function ListEditor () {
export function ListAdder () {
return import(/*webpackChunkName: "features/list_adder" */'../../list_adder');
}
+
+export function Search () {
+ return import(/*webpackChunkName: "features/search" */'../../search');
+}
diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js
index ac8e5193803db6..aa0fa2617e5d5e 100644
--- a/app/javascript/mastodon/initial_state.js
+++ b/app/javascript/mastodon/initial_state.js
@@ -21,5 +21,6 @@ export const version = getMeta('version');
export const mascot = getMeta('mascot');
export const profile_directory = getMeta('profile_directory');
export const isStaff = getMeta('is_staff');
+export const forceSingleColumn = !getMeta('advanced_layout');
export default initialState;
diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js
index 6c0c2880236a78..cdd64ffd0f4ecc 100644
--- a/app/javascript/mastodon/reducers/settings.js
+++ b/app/javascript/mastodon/reducers/settings.js
@@ -14,8 +14,6 @@ const initialState = ImmutableMap({
skinTone: 1,
- forceSingleColumn: false,
-
home: ImmutableMap({
shows: ImmutableMap({
reblog: true,
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index fe3c55755f73dc..39931d49aa4139 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1801,7 +1801,12 @@ a.account__display-name {
display: flex;
justify-content: flex-end;
+ &--start {
+ justify-content: flex-start;
+ }
+
&__inner {
+ width: 285px;
pointer-events: auto;
height: 100%;
}
@@ -1925,6 +1930,7 @@ a.account__display-name {
display: block;
flex: 1 1 auto;
padding: 15px 10px;
+ padding-bottom: 13px;
color: $primary-text-color;
text-decoration: none;
text-align: center;
@@ -1949,6 +1955,7 @@ a.account__display-name {
&:active {
@media screen and (min-width: 631px) {
background: lighten($ui-base-color, 14%);
+ border-bottom-color: lighten($ui-base-color, 14%);
}
}
@@ -1978,11 +1985,21 @@ a.account__display-name {
padding: 0;
}
- .search__input,
.autosuggest-textarea__textarea {
font-size: 16px;
}
+ .search__input {
+ line-height: 18px;
+ font-size: 16px;
+ padding: 15px;
+ padding-right: 30px;
+ }
+
+ .search__icon .fa {
+ top: 15px;
+ }
+
@media screen and (min-width: 360px) {
padding: 10px 0;
}
@@ -2038,6 +2055,58 @@ a.account__display-name {
margin-top: 10px;
}
}
+
+ .account {
+ padding: 15px 10px;
+ }
+
+ .notification {
+ &__message {
+ margin-left: 48px + 15px * 2;
+ padding-top: 15px;
+ }
+
+ &__favourite-icon-wrapper {
+ left: -32px;
+ }
+
+ .status {
+ padding-top: 8px;
+ }
+
+ .account {
+ padding-top: 8px;
+ }
+
+ .account__avatar-wrapper {
+ margin-left: 17px;
+ margin-right: 15px;
+ }
+ }
+ }
+}
+
+.floating-action-button {
+ position: fixed;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 3.9375rem;
+ height: 3.9375rem;
+ bottom: 1.3125rem;
+ right: 1.3125rem;
+ background: darken($ui-highlight-color, 3%);
+ color: $white;
+ border-radius: 50%;
+ font-size: 21px;
+ line-height: 21px;
+ text-decoration: none;
+ box-shadow: 2px 3px 9px rgba($base-shadow-color, 0.4);
+
+ &:hover,
+ &:focus,
+ &:active {
+ background: lighten($ui-highlight-color, 7%);
}
}
@@ -2059,12 +2128,41 @@ a.account__display-name {
}
}
+@media screen and (max-width: 600px + (285px * 1) + (10px * 1)) {
+ .columns-area__panels__pane--compositional {
+ display: none;
+ }
+}
+
+@media screen and (min-width: 600px + (285px * 1) + (10px * 1)) {
+ .floating-action-button,
+ .tabs-bar__link.optional {
+ display: none;
+ }
+
+ .search-page .search {
+ display: none;
+ }
+}
+
+@media screen and (max-width: 600px + (285px * 2) + (10px * 2)) {
+ .columns-area__panels__pane--navigational {
+ display: none;
+ }
+}
+
+@media screen and (min-width: 600px + (285px * 2) + (10px * 2)) {
+ .tabs-bar {
+ display: none;
+ }
+}
+
.icon-with-badge {
position: relative;
&__badge {
position: absolute;
- right: -13px;
+ left: 9px;
top: -13px;
background: $ui-highlight-color;
border: 2px solid lighten($ui-base-color, 8%);
@@ -2077,6 +2175,57 @@ a.account__display-name {
}
}
+.column-link--transparent .icon-with-badge__badge {
+ border-color: darken($ui-base-color, 8%);
+}
+
+.compose-panel {
+ width: 285px;
+ margin-top: 10px;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ .search__input {
+ line-height: 18px;
+ font-size: 16px;
+ padding: 15px;
+ padding-right: 30px;
+ }
+
+ .search__icon .fa {
+ top: 15px;
+ }
+
+ .navigation-bar {
+ padding-top: 20px;
+ padding-bottom: 20px;
+ }
+
+ .flex-spacer {
+ background: transparent;
+ }
+
+ .autosuggest-textarea__textarea {
+ max-height: 200px;
+ }
+
+ .compose-form__upload-thumbnail {
+ height: 80px;
+ }
+}
+
+.navigation-panel {
+ margin-top: 10px;
+
+ hr {
+ border: 0;
+ background: transparent;
+ border-top: 1px solid lighten($ui-base-color, 4%);
+ margin: 10px 0;
+ }
+}
+
.drawer__pager {
box-sizing: border-box;
padding: 0;
@@ -2127,15 +2276,6 @@ a.account__display-name {
}
}
-.navigational-toggle {
- padding: 10px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- font-size: 14px;
- color: $dark-text-color;
-}
-
.pseudo-drawer {
background: lighten($ui-base-color, 13%);
font-size: 13px;
@@ -2365,9 +2505,31 @@ a.account__display-name {
padding: 15px;
text-decoration: none;
- &:hover {
+ &:hover,
+ &:focus,
+ &:active {
background: lighten($ui-base-color, 11%);
}
+
+ &:focus {
+ outline: 0;
+ }
+
+ &--transparent {
+ background: transparent;
+ color: $ui-secondary-color;
+
+ &:hover,
+ &:focus,
+ &:active {
+ background: transparent;
+ color: $primary-text-color;
+ }
+
+ &.active {
+ color: $ui-highlight-color;
+ }
+ }
}
.column-link__icon {
@@ -5436,34 +5598,6 @@ noscript {
}
}
-.floating-action-button {
- position: fixed;
- display: flex;
- justify-content: center;
- align-items: center;
- width: 3.9375rem;
- height: 3.9375rem;
- bottom: 1.3125rem;
- right: 1.3125rem;
- background: darken($ui-highlight-color, 3%);
- color: $white;
- border-radius: 50%;
- font-size: 21px;
- line-height: 21px;
- text-decoration: none;
- box-shadow: 2px 3px 9px rgba($base-shadow-color, 0.4);
-
- &:hover,
- &:focus,
- &:active {
- background: lighten($ui-highlight-color, 7%);
- }
-
- @media screen and (min-width: 630px) {
- display: none;
- }
-}
-
.account__header__content {
color: $darker-text-color;
font-size: 14px;
diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb
index b7e0d577b9e3e2..3dcdccab5bda48 100644
--- a/app/lib/user_settings_decorator.rb
+++ b/app/lib/user_settings_decorator.rb
@@ -68,6 +68,8 @@ def process_update
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['default_content_type']= default_content_type_preference if change?('setting_default_content_type')
+ user.settings['theme'] = theme_preference if change?('setting_theme')
+ user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout')
end
def larger_menus_preference
@@ -270,6 +272,10 @@ def show_application_preference
boolean_cast_setting 'setting_show_application'
end
+ def theme_preference
+ settings['setting_theme']
+ end
+
def default_language_preference
settings['setting_default_language']
end
@@ -282,6 +288,10 @@ def default_content_type_preference
settings['setting_default_content_type']
end
+ def advanced_layout_preference
+ boolean_cast_setting 'setting_advanced_layout'
+ end
+
def boolean_cast_setting(key)
ActiveModel::Type::Boolean.new.cast(settings[key])
end
diff --git a/app/models/user.rb b/app/models/user.rb
index f72acd4528b9fa..427b3206624c06 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -163,6 +163,9 @@ class User < ApplicationRecord
:aggregate_reblogs,
:show_application,
:default_content_type,
+
+ :theme,
+ :advanced_layout,
to: :settings,
prefix: :setting,
allow_nil: false
@@ -249,37 +252,6 @@ def disable_two_factor!
save!
end
- def notify_staff_about_pending_account!
- LogWorker.perform_async("\xf0\x9f\x86\x95 New account <#{self.account.username}> is awaiting admin approval.\n\nReview (moderators only): https://#{Rails.configuration.x.web_domain || Rails.configuration.x.local_domain}/admin/pending_accounts")
- User.staff.includes(:account).each do |u|
- next unless u.allows_pending_account_emails?
- AdminMailer.new_pending_account(u.account, self).deliver_later
- end
- end
-
- def detect_spam!
- janitor = janitor_account || Account.representative
-
- intro = self.invite_request&.text
- # normalize it
- intro = intro.gsub(/[\u200b-\u200d\ufeff\u200e\u200f]/, '').strip.downcase unless intro.nil?
-
- return false unless intro.blank? || intro.split.count < 5 || SPAM_TRIGGERS.match?(intro)
-
- user_friendly_action_log(janitor, :reject_registration, self.account.username, "Registration was spam filtered.")
- Form::AccountBatch.new(current_account: janitor, account_ids: account_id, action: 'reject').save
-
- true
- rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid
- false
- end
-
- def janitor_account
- account_id = ENV.fetch('JANITOR_USER', '').to_i
- return if account_id == 0
- Account.find_by(id: account_id)
- end
-
def wants_larger_menus?
@wants_larger_menus ||= (settings.larger_menus || false)
end
@@ -479,18 +451,18 @@ def password_required?
super
end
- def send_reset_password_instructions
- return false if encrypted_password.blank? && (Devise.pam_authentication || Devise.ldap_authentication)
+ def send_confirmation_instructions
+ return false if detect_spam!
super
end
- def reset_password!(new_password, new_password_confirmation)
+ def send_reset_password_instructions
return false if encrypted_password.blank? && (Devise.pam_authentication || Devise.ldap_authentication)
super
end
- def send_confirmation_instructions
- return false if detect_spam!
+ def reset_password!(new_password, new_password_confirmation)
+ return false if encrypted_password.blank? && (Devise.pam_authentication || Devise.ldap_authentication)
super
end
@@ -539,6 +511,37 @@ def prepare_returning_user!
regenerate_feed! if needs_feed_update?
end
+ def notify_staff_about_pending_account!
+ LogWorker.perform_async("\xf0\x9f\x86\x95 New account <#{self.account.username}> is awaiting admin approval.\n\nReview (moderators only): https://#{Rails.configuration.x.web_domain || Rails.configuration.x.local_domain}/admin/pending_accounts")
+ User.staff.includes(:account).each do |u|
+ next unless u.allows_pending_account_emails?
+ AdminMailer.new_pending_account(u.account, self).deliver_later
+ end
+ end
+
+ def detect_spam!
+ janitor = janitor_account || Account.representative
+
+ intro = self.invite_request&.text
+ # normalize it
+ intro = intro.gsub(/[\u200b-\u200d\ufeff\u200e\u200f]/, '').strip.downcase unless intro.nil?
+
+ return false unless intro.blank? || intro.split.count < 5 || SPAM_TRIGGERS.match?(intro)
+
+ user_friendly_action_log(janitor, :reject_registration, self.account.username, "Registration was spam filtered.")
+ Form::AccountBatch.new(current_account: janitor, account_ids: account_id, action: 'reject').save
+
+ true
+ rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid
+ false
+ end
+
+ def janitor_account
+ account_id = ENV.fetch('JANITOR_USER', '').to_i
+ return if account_id == 0
+ Account.find_by(id: account_id)
+ end
+
def regenerate_feed!
return unless Redis.current.setnx("account:#{account_id}:regeneration", true)
Redis.current.expire("account:#{account_id}:regeneration", 1.day.seconds)
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index 7afeffe25adff8..cb5b0017ee64e7 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -48,6 +48,7 @@ def meta
store[:display_media] = object.current_account.user.setting_display_media
store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers
store[:reduce_motion] = object.current_account.user.setting_reduce_motion
+ store[:advanced_layout] = object.current_account.user.setting_advanced_layout
store[:is_staff] = object.current_account.user.staff?
store[:default_content_type] = object.current_account.user.setting_default_content_type
end
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index e6198de6afcf80..80ecf6d40e9f29 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -93,6 +93,9 @@
= f.input :setting_hide_mntions_blocker, as: :boolean, wrapper: :with_label
= f.input :setting_hide_mntions_packm8, as: :boolean, wrapper: :with_label
+ .fields-group
+ = f.input :setting_advanced_layout, as: :boolean, wrapper: :with_label
+
.fields-group
= f.input :setting_unfollow_modal, as: :boolean, wrapper: :with_label
= f.input :setting_boost_modal, as: :boolean, wrapper: :with_label
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index cb387d6bf1848f..f039f619348f68 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -28,6 +28,7 @@ en:
password: Use at least 8 characters
phrase: Will be matched regardless of casing in text or content warning of a roar
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 repeats for roars that have been recently repeated (only affects newly-received repeats)
setting_default_content_type_html_html: "<strong>Bold</strong>, <u>Underline</u>, <em>Italic</em>, <code>Console</code>
, ..."
setting_default_content_type_markdown_html: "**Bold**, _Underline_, *Italic*, `Console`
, ..."
@@ -42,7 +43,8 @@ en:
setting_hide_network: Who in your pack and who joins your pack will not be shown on your profile
setting_noindex: Affects your public profile and roar pages
setting_show_application: The application you use to roar will be displayed in the detailed view of your roars
- setting_skin: Reskins the selected Monsterpit flavour
+ setting_skin: Reskins the selected Mastodon flavour
+ setting_theme: Affects how Mastodon looks when you're logged in from any device.
username: Your username will be unique on %{domain}
whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word
featured_tag:
@@ -113,6 +115,7 @@ en:
otp_attempt: Two-factor code
password: Password
phrase: Keyword or phrase
+ setting_advanced_layout: Enable advanced web interface
setting_aggregate_reblogs: Group repeats in timelines
setting_auto_play_gif: Auto-play animated GIFs
setting_boost_modal: Show confirmation dialog before repeating
@@ -175,8 +178,8 @@ en:
setting_noindex: Opt-out of search engine indexing
setting_reduce_motion: Reduce motion in animations
setting_show_application: Disclose application used to send roars
- setting_skin: Skin
setting_system_font_ui: Use system's default font
+ setting_theme: Site theme
setting_unfollow_modal: Show confirmation dialog before unfollowing someone
severity: Severity
type: Import type
diff --git a/config/settings.yml b/config/settings.yml
index 70aadd574d68a2..b0d3dfa3ae53cc 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -35,6 +35,7 @@ defaults: &defaults
flavour: 'glitch'
skin: 'default'
aggregate_reblogs: true
+ advanced_layout: true
notification_emails:
follow: false
reblog: false