From e9a90ca12a000b058a8ffa7d6876912cc36ec5cc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 25 May 2019 21:27:00 +0200 Subject: [PATCH] Add responsive panels to the single-column layout (#10820) * Add responsive panels to the single-column layout * Fixes * Fix not being able to save the preference * Fix code style issues * Set max-height on the compose textarea and add a link to relationship manager --- .../settings/preferences_controller.rb | 7 + app/javascript/mastodon/actions/compose.js | 20 +- app/javascript/mastodon/actions/statuses.js | 8 +- .../mastodon/components/autosuggest_input.js | 2 +- .../features/compose/components/action_bar.js | 2 +- .../compose/components/navigation_bar.js | 2 +- .../features/compose/components/search.js | 10 + .../features/getting_started/index.js | 17 +- .../mastodon/features/search/index.js | 17 ++ .../features/ui/components/columns_area.js | 14 +- .../features/ui/components/compose_panel.js | 41 ++++ .../features/ui/components/list_panel.js | 55 +++++ .../ui/components/navigation_panel.js | 27 +++ .../components/notifications_counter_icon.js | 5 +- .../features/ui/components/tabs_bar.js | 14 +- app/javascript/mastodon/features/ui/index.js | 14 +- .../features/ui/util/async-components.js | 4 + app/javascript/mastodon/initial_state.js | 1 + app/javascript/mastodon/reducers/settings.js | 2 - .../styles/mastodon/components.scss | 214 ++++++++++++++---- app/lib/user_settings_decorator.rb | 10 + app/models/user.rb | 75 +++--- app/serializers/initial_state_serializer.rb | 1 + app/views/settings/preferences/show.html.haml | 3 + config/locales/simple_form.en.yml | 7 +- config/settings.yml | 1 + 26 files changed, 440 insertions(+), 133 deletions(-) create mode 100644 app/javascript/mastodon/features/search/index.js create mode 100644 app/javascript/mastodon/features/ui/components/compose_panel.js create mode 100644 app/javascript/mastodon/features/ui/components/list_panel.js create mode 100644 app/javascript/mastodon/features/ui/components/navigation_panel.js 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