Change about page to be mounted in the web UI (#19345)

local
Eugen Rochko 2 years ago committed by GitHub
parent b04633a961
commit 1bd00036c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 60
      app/controllers/about_controller.rb
  2. 23
      app/controllers/api/v1/instances/domain_blocks_controller.rb
  3. 18
      app/controllers/api/v1/instances/extended_descriptions_controller.rb
  4. BIN
      app/javascript/fonts/montserrat/Montserrat-Medium.ttf
  5. BIN
      app/javascript/fonts/montserrat/Montserrat-Regular.ttf
  6. BIN
      app/javascript/fonts/montserrat/Montserrat-Regular.woff
  7. BIN
      app/javascript/fonts/montserrat/Montserrat-Regular.woff2
  8. 61
      app/javascript/mastodon/actions/server.js
  9. 33
      app/javascript/mastodon/components/image.js
  10. 8
      app/javascript/mastodon/components/server_banner.js
  11. 4
      app/javascript/mastodon/components/skeleton.js
  12. 195
      app/javascript/mastodon/features/about/index.js
  13. 2
      app/javascript/mastodon/features/privacy_policy/index.js
  14. 2
      app/javascript/mastodon/features/report/rules.js
  15. 2
      app/javascript/mastodon/features/ui/components/link_footer.js
  16. 46
      app/javascript/mastodon/reducers/server.js
  17. 2
      app/javascript/styles/application.scss
  18. 4
      app/javascript/styles/contrast/diff.scss
  19. 21
      app/javascript/styles/fonts/montserrat.scss
  20. 86
      app/javascript/styles/mastodon-light/diff.scss
  21. 902
      app/javascript/styles/mastodon/about.scss
  22. 34
      app/javascript/styles/mastodon/compact_header.scss
  23. 474
      app/javascript/styles/mastodon/components.scss
  24. 1
      app/javascript/styles/mastodon/containers.scss
  25. 1
      app/javascript/styles/mastodon/dashboard.scss
  26. 63
      app/javascript/styles/mastodon/forms.scss
  27. 229
      app/javascript/styles/mastodon/widgets.scss
  28. 4
      app/models/domain_block.rb
  29. 15
      app/models/extended_description.rb
  30. 17
      app/serializers/rest/domain_block_serializer.rb
  31. 13
      app/serializers/rest/extended_description_serializer.rb
  32. 12
      app/views/about/_domain_blocks.html.haml
  33. 96
      app/views/about/more.html.haml
  34. 4
      app/views/about/show.html.haml
  35. 28
      config/locales/en.yml
  36. 6
      config/routes.rb
  37. 8
      spec/controllers/about_controller_spec.rb

@ -1,63 +1,11 @@
# frozen_string_literal: true
class AboutController < ApplicationController
include RegistrationSpamConcern
include WebAppControllerConcern
layout 'public'
skip_before_action :require_functional!
before_action :require_open_federation!, only: [:more]
before_action :set_body_classes, only: :show
before_action :set_instance_presenter
before_action :set_expires_in, only: [:more]
skip_before_action :require_functional!, only: [:more]
def more
flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor]
toc_generator = TOCGenerator.new(@instance_presenter.extended_description)
@rules = Rule.ordered
@contents = toc_generator.html
@table_of_contents = toc_generator.toc
@blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks?
end
helper_method :display_blocks?
helper_method :display_blocks_rationale?
helper_method :public_fetch_mode?
helper_method :new_user
private
def require_open_federation!
not_found if whitelist_mode?
end
def display_blocks?
Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?)
end
def display_blocks_rationale?
Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)
end
def new_user
User.new.tap do |user|
user.build_account
user.build_invite_request
end
end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def set_body_classes
@hide_navbar = true
end
def set_expires_in
expires_in 0, public: true
def show
expires_in 0, public: true unless user_signed_in?
end
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
class Api::V1::Instances::DomainBlocksController < Api::BaseController
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
before_action :require_enabled_api!
before_action :set_domain_blocks
def index
expires_in 3.minutes, public: true
render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: (Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?))
end
private
def require_enabled_api!
head 404 unless Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?)
end
def set_domain_blocks
@domain_blocks = DomainBlock.with_user_facing_limitations.by_severity
end
end

@ -0,0 +1,18 @@
# frozen_string_literal: true
class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
before_action :set_extended_description
def show
expires_in 3.minutes, public: true
render json: @extended_description, serializer: REST::ExtendedDescriptionSerializer
end
private
def set_extended_description
@extended_description = ExtendedDescription.current
end
end

@ -5,6 +5,14 @@ export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST';
export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS';
export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL';
export const EXTENDED_DESCRIPTION_REQUEST = 'EXTENDED_DESCRIPTION_REQUEST';
export const EXTENDED_DESCRIPTION_SUCCESS = 'EXTENDED_DESCRIPTION_SUCCESS';
export const EXTENDED_DESCRIPTION_FAIL = 'EXTENDED_DESCRIPTION_FAIL';
export const SERVER_DOMAIN_BLOCKS_FETCH_REQUEST = 'SERVER_DOMAIN_BLOCKS_FETCH_REQUEST';
export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS';
export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL';
export const fetchServer = () => (dispatch, getState) => {
dispatch(fetchServerRequest());
@ -28,3 +36,56 @@ const fetchServerFail = error => ({
type: SERVER_FETCH_FAIL,
error,
});
export const fetchExtendedDescription = () => (dispatch, getState) => {
dispatch(fetchExtendedDescriptionRequest());
api(getState)
.get('/api/v1/instance/extended_description')
.then(({ data }) => dispatch(fetchExtendedDescriptionSuccess(data)))
.catch(err => dispatch(fetchExtendedDescriptionFail(err)));
};
const fetchExtendedDescriptionRequest = () => ({
type: EXTENDED_DESCRIPTION_REQUEST,
});
const fetchExtendedDescriptionSuccess = description => ({
type: EXTENDED_DESCRIPTION_SUCCESS,
description,
});
const fetchExtendedDescriptionFail = error => ({
type: EXTENDED_DESCRIPTION_FAIL,
error,
});
export const fetchDomainBlocks = () => (dispatch, getState) => {
dispatch(fetchDomainBlocksRequest());
api(getState)
.get('/api/v1/instance/domain_blocks')
.then(({ data }) => dispatch(fetchDomainBlocksSuccess(true, data)))
.catch(err => {
if (err.response.status === 404) {
dispatch(fetchDomainBlocksSuccess(false, []));
} else {
dispatch(fetchDomainBlocksFail(err));
}
});
};
const fetchDomainBlocksRequest = () => ({
type: SERVER_DOMAIN_BLOCKS_FETCH_REQUEST,
});
const fetchDomainBlocksSuccess = (isAvailable, blocks) => ({
type: SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS,
isAvailable,
blocks,
});
const fetchDomainBlocksFail = error => ({
type: SERVER_DOMAIN_BLOCKS_FETCH_FAIL,
error,
});

@ -0,0 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import Blurhash from './blurhash';
import classNames from 'classnames';
export default class Image extends React.PureComponent {
static propTypes = {
src: PropTypes.string,
srcSet: PropTypes.string,
blurhash: PropTypes.string,
className: PropTypes.string,
};
state = {
loaded: false,
};
handleLoad = () => this.setState({ loaded: true });
render () {
const { src, srcSet, blurhash, className } = this.props;
const { loaded } = this.state;
return (
<div className={classNames('image', { loaded }, className)} role='presentation'>
{blurhash && <Blurhash hash={blurhash} className='image__preview' />}
<img src={src} srcSet={srcSet} alt='' onLoad={this.handleLoad} />
</div>
);
}
}

@ -7,13 +7,15 @@ import ShortNumber from 'mastodon/components/short_number';
import Skeleton from 'mastodon/components/skeleton';
import Account from 'mastodon/containers/account_container';
import { domain } from 'mastodon/initial_state';
import Image from 'mastodon/components/image';
import { Link } from 'react-router-dom';
const messages = defineMessages({
aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' },
});
const mapStateToProps = state => ({
server: state.get('server'),
server: state.getIn(['server', 'server']),
});
export default @connect(mapStateToProps)
@ -41,7 +43,7 @@ class ServerBanner extends React.PureComponent {
<FormattedMessage id='server_banner.introduction' defaultMessage='{domain} is part of the decentralized social network powered by {mastodon}.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} />
</div>
<img src={server.get('thumbnail')} alt={server.get('title')} className='server-banner__hero' />
<Image blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' />
<div className='server-banner__description'>
{isLoading ? (
@ -83,7 +85,7 @@ class ServerBanner extends React.PureComponent {
<hr className='spacer' />
<a className='button button--block button-secondary' href='/about/more' target='_blank'><FormattedMessage id='server_banner.learn_more' defaultMessage='Learn more' /></a>
<Link className='button button--block button-secondary' to='/about'><FormattedMessage id='server_banner.learn_more' defaultMessage='Learn more' /></Link>
</div>
);
}

@ -4,8 +4,8 @@ import PropTypes from 'prop-types';
const Skeleton = ({ width, height }) => <span className='skeleton' style={{ width, height }}>&zwnj;</span>;
Skeleton.propTypes = {
width: PropTypes.number,
height: PropTypes.number,
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
};
export default Skeleton;

@ -1,27 +1,214 @@
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import Column from 'mastodon/components/column';
import LinkFooter from 'mastodon/features/ui/components/link_footer';
import { Helmet } from 'react-helmet';
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'mastodon/actions/server';
import Account from 'mastodon/containers/account_container';
import Skeleton from 'mastodon/components/skeleton';
import Icon from 'mastodon/components/icon';
import classNames from 'classnames';
import Image from 'mastodon/components/image';
const messages = defineMessages({
title: { id: 'column.about', defaultMessage: 'About' },
rules: { id: 'about.rules', defaultMessage: 'Server rules' },
blocks: { id: 'about.blocks', defaultMessage: 'Moderated servers' },
silenced: { id: 'about.domain_blocks.silenced.title', defaultMessage: 'Limited' },
silencedExplanation: { id: 'about.domain_blocks.silenced.explanation', defaultMessage: 'You will generally not see profiles and content from this server, unless you explicitly look it up or opt into it by following.' },
suspended: { id: 'about.domain_blocks.suspended.title', defaultMessage: 'Suspended' },
suspendedExplanation: { id: 'about.domain_blocks.suspended.explanation', defaultMessage: 'No data from this server will be processed, stored or exchanged, making any interaction or communication with users from this server impossible.' },
});
export default @injectIntl
const severityMessages = {
silence: {
title: messages.silenced,
explanation: messages.silencedExplanation,
},
suspend: {
title: messages.suspended,
explanation: messages.suspendedExplanation,
},
};
const mapStateToProps = state => ({
server: state.getIn(['server', 'server']),
extendedDescription: state.getIn(['server', 'extendedDescription']),
domainBlocks: state.getIn(['server', 'domainBlocks']),
});
class Section extends React.PureComponent {
static propTypes = {
title: PropTypes.string,
children: PropTypes.node,
open: PropTypes.bool,
onOpen: PropTypes.func,
};
state = {
collapsed: !this.props.open,
};
handleClick = () => {
const { onOpen } = this.props;
const { collapsed } = this.state;
this.setState({ collapsed: !collapsed }, () => onOpen && onOpen());
}
render () {
const { title, children } = this.props;
const { collapsed } = this.state;
return (
<div className={classNames('about__section', { active: !collapsed })}>
<div className='about__section__title' role='button' tabIndex='0' onClick={this.handleClick}>
<Icon id={collapsed ? 'chevron-right' : 'chevron-down'} fixedWidth /> {title}
</div>
{!collapsed && (
<div className='about__section__body'>{children}</div>
)}
</div>
);
}
}
export default @connect(mapStateToProps)
@injectIntl
class About extends React.PureComponent {
static propTypes = {
server: ImmutablePropTypes.map,
extendedDescription: ImmutablePropTypes.map,
domainBlocks: ImmutablePropTypes.contains({
isLoading: PropTypes.bool,
isAvailable: PropTypes.bool,
items: ImmutablePropTypes.list,
}),
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
componentDidMount () {
const { dispatch } = this.props;
dispatch(fetchServer());
dispatch(fetchExtendedDescription());
}
handleDomainBlocksOpen = () => {
const { dispatch } = this.props;
dispatch(fetchDomainBlocks());
}
render () {
const { intl } = this.props;
const { intl, server, extendedDescription, domainBlocks } = this.props;
const isLoading = server.get('isLoading');
return (
<Column>
<LinkFooter />
<div className='scrollable about'>
<div className='about__header'>
<Image blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />
<h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1>
<p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank'>Mastodon</a> }} /></p>
</div>
<div className='about__meta'>
<div className='about__meta__column'>
<h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
<Account id={server.getIn(['contact', 'account', 'id'])} />
</div>
<hr className='about__meta__divider' />
<div className='about__meta__column'>
<h4><FormattedMessage id='about.contact' defaultMessage='Contact:' /></h4>
{isLoading ? <Skeleton width='10ch' /> : <a className='about__mail' href={`mailto:${server.getIn(['contact', 'email'])}`}>{server.getIn(['contact', 'email'])}</a>}
</div>
</div>
<Section open title={intl.formatMessage(messages.title)}>
{extendedDescription.get('isLoading') ? (
<>
<Skeleton width='100%' />
<br />
<Skeleton width='100%' />
<br />
<Skeleton width='100%' />
<br />
<Skeleton width='70%' />
</>
) : (extendedDescription.get('content')?.length > 0 ? (
<div
className='prose'
dangerouslySetInnerHTML={{ __html: extendedDescription.get('content') }}
/>
) : (
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
))}
</Section>
<Section title={intl.formatMessage(messages.rules)}>
{!isLoading && (server.get('rules').isEmpty() ? (
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
) : (
<ol className='rules-list'>
{server.get('rules').map(rule => (
<li key={rule.get('id')}>
<span className='rules-list__text'>{rule.get('text')}</span>
</li>
))}
</ol>
))}
</Section>
<Section title={intl.formatMessage(messages.blocks)} onOpen={this.handleDomainBlocksOpen}>
{domainBlocks.get('isLoading') ? (
<>
<Skeleton width='100%' />
<br />
<Skeleton width='70%' />
</>
) : (domainBlocks.get('isAvailable') ? (
<>
<p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p>
<table className='about__domain-blocks'>
<thead>
<tr>
<th><FormattedMessage id='about.domain_blocks.domain' defaultMessage='Domain' /></th>
<th><FormattedMessage id='about.domain_blocks.severity' defaultMessage='Severity' /></th>
<th><FormattedMessage id='about.domain_blocks.comment' defaultMessage='Reason' /></th>
</tr>
</thead>
<tbody>
{domainBlocks.get('items').map(block => (
<tr key={block.get('domain')}>
<td><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></td>
<td><span title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span></td>
<td>{block.get('comment')}</td>
</tr>
))}
</tbody>
</table>
</>
) : (
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
))}
</Section>
<LinkFooter />
</div>
<Helmet>
<title>{intl.formatMessage(messages.title)}</title>

@ -44,7 +44,7 @@ class PrivacyPolicy extends React.PureComponent {
</div>
<div
className='privacy-policy__body'
className='privacy-policy__body prose'
dangerouslySetInnerHTML={{ __html: content }}
/>
</div>

@ -7,7 +7,7 @@ import Button from 'mastodon/components/button';
import Option from './components/option';
const mapStateToProps = state => ({
rules: state.getIn(['server', 'rules']),
rules: state.getIn(['server', 'server', 'rules']),
});
export default @connect(mapStateToProps)

@ -51,7 +51,7 @@ class LinkFooter extends React.PureComponent {
const items = [];
items.push(<a key='apps' href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Get the app' /></a>);
items.push(<a key='about' href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About' /></a>);
items.push(<Link key='about' to='/about'><FormattedMessage id='navigation_bar.info' defaultMessage='About' /></Link>);
items.push(<a key='mastodon' href='https://joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.what_is_mastodon' defaultMessage='About Mastodon' /></a>);
items.push(<a key='docs' href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a>);
items.push(<Link key='privacy-policy' to='/privacy-policy'><FormattedMessage id='getting_started.privacy_policy' defaultMessage='Privacy Policy' /></Link>);

@ -1,18 +1,52 @@
import { SERVER_FETCH_REQUEST, SERVER_FETCH_SUCCESS, SERVER_FETCH_FAIL } from 'mastodon/actions/server';
import { Map as ImmutableMap, fromJS } from 'immutable';
import {
SERVER_FETCH_REQUEST,
SERVER_FETCH_SUCCESS,
SERVER_FETCH_FAIL,
EXTENDED_DESCRIPTION_REQUEST,
EXTENDED_DESCRIPTION_SUCCESS,
EXTENDED_DESCRIPTION_FAIL,
SERVER_DOMAIN_BLOCKS_FETCH_REQUEST,
SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS,
SERVER_DOMAIN_BLOCKS_FETCH_FAIL,
} from 'mastodon/actions/server';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
const initialState = ImmutableMap({
isLoading: true,
server: ImmutableMap({
isLoading: true,
}),
extendedDescription: ImmutableMap({
isLoading: true,
}),
domainBlocks: ImmutableMap({
isLoading: true,
isAvailable: true,
items: ImmutableList(),
}),
});
export default function server(state = initialState, action) {
switch (action.type) {
case SERVER_FETCH_REQUEST:
return state.set('isLoading', true);
return state.setIn(['server', 'isLoading'], true);
case SERVER_FETCH_SUCCESS:
return fromJS(action.server).set('isLoading', false);
return state.set('server', fromJS(action.server)).setIn(['server', 'isLoading'], false);
case SERVER_FETCH_FAIL:
return state.set('isLoading', false);
return state.setIn(['server', 'isLoading'], false);
case EXTENDED_DESCRIPTION_REQUEST:
return state.setIn(['extendedDescription', 'isLoading'], true);
case EXTENDED_DESCRIPTION_SUCCESS:
return state.set('extendedDescription', fromJS(action.description)).setIn(['extendedDescription', 'isLoading'], false);
case EXTENDED_DESCRIPTION_FAIL:
return state.setIn(['extendedDescription', 'isLoading'], false);
case SERVER_DOMAIN_BLOCKS_FETCH_REQUEST:
return state.setIn(['domainBlocks', 'isLoading'], true);
case SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS:
return state.setIn(['domainBlocks', 'items'], fromJS(action.blocks)).setIn(['domainBlocks', 'isLoading'], false).setIn(['domainBlocks', 'isAvailable'], action.isAvailable);
case SERVER_DOMAIN_BLOCKS_FETCH_FAIL:
return state.setIn(['domainBlocks', 'isLoading'], false);
default:
return state;
}

@ -2,7 +2,6 @@
@import 'mastodon/variables';
@import 'fonts/roboto';
@import 'fonts/roboto-mono';
@import 'fonts/montserrat';
@import 'mastodon/reset';
@import 'mastodon/basics';
@ -10,7 +9,6 @@
@import 'mastodon/containers';
@import 'mastodon/lists';
@import 'mastodon/footer';
@import 'mastodon/compact_header';
@import 'mastodon/widgets';
@import 'mastodon/forms';
@import 'mastodon/accounts';

@ -13,10 +13,6 @@
}
}
.rich-formatting a,
.rich-formatting p a,
.rich-formatting li a,
.landing-page__short-description p a,
.status__content a,
.reply-indicator__content a {
color: lighten($ui-highlight-color, 12%);

@ -1,21 +0,0 @@
@font-face {
font-family: mastodon-font-display;
src:
local('Montserrat'),
url('../fonts/montserrat/Montserrat-Regular.woff2') format('woff2'),
url('../fonts/montserrat/Montserrat-Regular.woff') format('woff'),
url('../fonts/montserrat/Montserrat-Regular.ttf') format('truetype');
font-weight: 400;
font-display: swap;
font-style: normal;
}
@font-face {
font-family: mastodon-font-display;
src:
local('Montserrat Medium'),
url('../fonts/montserrat/Montserrat-Medium.ttf') format('truetype');
font-weight: 500;
font-display: swap;
font-style: normal;
}

@ -36,6 +36,20 @@ html {
border-top: 0;
}
.column > .scrollable.about {
border-top: 1px solid lighten($ui-base-color, 8%);
}
.about__meta,
.about__section__title {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
}
.rules-list li::before {
background: $ui-highlight-color;
}
.directory__card__img {
background: lighten($ui-base-color, 12%);
}
@ -45,10 +59,6 @@ html {
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
.table-of-contents {
border: 1px solid lighten($ui-base-color, 8%);
}
.column-back-button,
.column-header {
background: $white;
@ -138,11 +148,6 @@ html {
.compose-form__poll-wrapper select,
.search__input,
.setting-text,
.box-widget input[type="text"],
.box-widget input[type="email"],
.box-widget input[type="password"],
.box-widget textarea,
.statuses-grid .detailed-status,
.report-dialog-modal__textarea,
.audio-player {
border: 1px solid lighten($ui-base-color, 8%);
@ -480,52 +485,16 @@ html {
background: $white;
}
.tabs-bar {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
border-bottom: 0;
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 0;
}
&__link {
padding-bottom: 14px;
border-bottom-width: 1px;
border-bottom-color: lighten($ui-base-color, 8%);
&:hover,
&:active,
&:focus {
background: $ui-base-color;
}
&.active {
&:hover,
&:active,
&:focus {
background: transparent;
border-bottom-color: $ui-highlight-color;
}
}
}
}
// Change the default colors used on some parts of the profile pages
.activity-stream-tabs {
background: $account-background-color;
border-bottom-color: lighten($ui-base-color, 8%);
}
.box-widget,
.nothing-here,
.page-header,
.directory__tag > a,
.directory__tag > div,
.landing-page__call-to-action,
.contact-widget,
.landing .hero-widget__text,
.landing-page__information.contact-widget {
.directory__tag > div {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
@ -536,11 +505,6 @@ html {
}
}
.landing .hero-widget__text {
border-top: 0;
border-bottom: 0;
}
.simple_form {
input[type="text"],
input[type="number"],
@ -553,26 +517,12 @@ html {
}
}
.landing .hero-widget__footer {
background: $white;
border: 1px solid lighten($ui-base-color, 8%);
border-top: 0;
@media screen and (max-width: $no-gap-breakpoint) {
border: 0;
}
}
.picture-in-picture-placeholder {
background: $white;
border-color: lighten($ui-base-color, 8%);
color: lighten($ui-base-color, 8%);
}
.brand__tagline {
color: $ui-secondary-color;
}
.directory__tag > a {
&:hover,
&:active,
@ -666,8 +616,7 @@ html {
}
}
.simple_form,
.table-form {
.simple_form {
.warning {
box-shadow: none;
background: rgba($error-red, 0.5);
@ -801,9 +750,6 @@ html {
}
.hero-widget,
.box-widget,
.contact-widget,
.landing-page__information.contact-widget,
.moved-account-widget,
.memoriam-widget,
.activity-stream,

@ -1,7 +1,5 @@
$maximum-width: 1235px;
$fluid-breakpoint: $maximum-width + 20px;
$column-breakpoint: 700px;
$small-breakpoint: 960px;
.container {
box-sizing: border-box;
@ -15,892 +13,44 @@ $small-breakpoint: 960px;
}
}
.rich-formatting {
font-family: $font-sans-serif, sans-serif;
font-size: 14px;
font-weight: 400;
line-height: 1.7;
word-wrap: break-word;
color: $darker-text-color;
a {
color: $highlight-text-color;
text-decoration: underline;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
p,
li {
color: $darker-text-color;
}
p {
margin-top: 0;
margin-bottom: 0.85em;
&:last-child {
margin-bottom: 0;
}
}
strong {
font-weight: 700;
color: $secondary-text-color;
}
em {
font-style: italic;
color: $secondary-text-color;
}
code {
font-size: 0.85em;
background: darken($ui-base-color, 8%);
border-radius: 4px;
padding: 0.2em 0.3em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: $font-display, sans-serif;
margin-top: 1.275em;
margin-bottom: 0.85em;
font-weight: 500;
color: $secondary-text-color;
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.75em;
}
h3 {
font-size: 1.5em;
}
h4 {
font-size: 1.25em;
}
h5,
h6 {
font-size: 1em;
}
ul {
list-style: disc;
}
ol {
list-style: decimal;
}
ul,
ol {
margin: 0;
padding: 0;
padding-left: 2em;
margin-bottom: 0.85em;
&[type='a'] {
list-style-type: lower-alpha;
}
&[type='i'] {
list-style-type: lower-roman;
}
}
hr {
width: 100%;
height: 0;
border: 0;
border-bottom: 1px solid lighten($ui-base-color, 4%);
margin: 1.7em 0;
&.spacer {
height: 1px;
border: 0;
}
}
table {
width: 100%;
border-collapse: collapse;
break-inside: auto;
margin-top: 24px;
margin-bottom: 32px;
thead tr,
tbody tr {
border-bottom: 1px solid lighten($ui-base-color, 4%);
font-size: 1em;
line-height: 1.625;
font-weight: 400;
text-align: left;
color: $darker-text-color;
}
thead tr {
border-bottom-width: 2px;
line-height: 1.5;
font-weight: 500;
color: $dark-text-color;
}
th,
td {
padding: 8px;
align-self: flex-start;
align-items: flex-start;
word-break: break-all;
&.nowrap {
width: 25%;
position: relative;
&::before {
content: '&nbsp;';
visibility: hidden;
}
span {
position: absolute;
left: 8px;
right: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
& > :first-child {
margin-top: 0;
}
.brand {
position: relative;
text-decoration: none;
}
.information-board {
background: darken($ui-base-color, 4%);
padding: 20px 0;
.container-alt {
position: relative;
padding-right: 280px + 15px;
}
&__sections {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
&__section {
flex: 1 0 0;
font-family: $font-sans-serif, sans-serif;
font-size: 16px;
line-height: 28px;
color: $primary-text-color;
text-align: right;
padding: 10px 15px;
span,
strong {
display: block;
}
span {
&:last-child {
color: $secondary-text-color;
}
}
strong {
font-family: $font-display, sans-serif;
font-weight: 500;
font-size: 32px;
line-height: 48px;
}
@media screen and (max-width: $column-breakpoint) {
text-align: center;
}
}
.panel {
position: absolute;
width: 280px;
box-sizing: border-box;
background: darken($ui-base-color, 8%);
padding: 20px;
padding-top: 10px;
border-radius: 4px 4px 0 0;
right: 0;
bottom: -40px;
.panel-header {
font-family: $font-display, sans-serif;
font-size: 14px;
line-height: 24px;
font-weight: 500;
color: $darker-text-color;
padding-bottom: 5px;
margin-bottom: 15px;
border-bottom: 1px solid lighten($ui-base-color, 4%);
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
a,
span {
font-weight: 400;
color: darken($darker-text-color, 10%);
}
a {
text-decoration: none;
}
}
}
.owner {
text-align: center;
.avatar {
width: 80px;
height: 80px;
margin: 0 auto;
margin-bottom: 15px;
img {
display: block;
width: 80px;
height: 80px;
border-radius: 48px;
}
}
.name {
font-size: 14px;
a {
display: block;
color: $primary-text-color;
text-decoration: none;
&:hover {
.display_name {
text-decoration: underline;
}
}
}
.username {
display: block;
color: $darker-text-color;
}
}
}
}
.rules-list {
font-size: 15px;
line-height: 22px;
color: $primary-text-color;
counter-reset: list-counter;
.landing-page {
p,
li {
font-family: $font-sans-serif, sans-serif;
font-size: 16px;
font-weight: 400;
line-height: 30px;
margin-bottom: 12px;
color: $darker-text-color;
a {
color: $highlight-text-color;
text-decoration: underline;
}
}
em {
display: inline;
margin: 0;
padding: 0;
font-weight: 700;
background: transparent;
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: lighten($darker-text-color, 10%);
}
h1 {
font-family: $font-display, sans-serif;
font-size: 26px;
line-height: 30px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
small {
font-family: $font-sans-serif, sans-serif;
display: block;
font-size: 18px;
font-weight: 400;
color: lighten($darker-text-color, 10%);
}
}
h2 {
font-family: $font-display, sans-serif;
font-size: 22px;
line-height: 26px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
}
h3 {
font-family: $font-display, sans-serif;
font-size: 18px;
line-height: 24px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
}
h4 {
font-family: $font-display, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
}
h5 {
font-family: $font-display, sans-serif;
font-size: 14px;
line-height: 24px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
}
h6 {
font-family: $font-display, sans-serif;
font-size: 12px;
line-height: 24px;
position: relative;
border-bottom: 1px solid lighten($ui-base-color, 8%);
padding: 1em 1.75em;
padding-left: 3em;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
}
ul,
ol {
margin-left: 20px;
&[type='a'] {
list-style-type: lower-alpha;
}
&[type='i'] {
list-style-type: lower-roman;
}
}
ul {
list-style: disc;
}
ol {
list-style: decimal;
}
li > ol,
li > ul {
margin-top: 6px;
}
hr {
width: 100%;
height: 0;
border: 0;
border-bottom: 1px solid rgba($ui-base-lighter-color, 0.6);
margin: 20px 0;
&.spacer {
height: 1px;
border: 0;
}
}
&__information,
&__forms {
padding: 20px;
}
&__call-to-action {
background: $ui-base-color;
border-radius: 4px;
padding: 25px 40px;
overflow: hidden;
box-sizing: border-box;
.row {
width: 100%;
display: flex;
flex-direction: row-reverse;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
}
.row__information-board {
display: flex;
justify-content: flex-end;
align-items: flex-end;
.information-board__section {
flex: 1 0 auto;
padding: 0 10px;
}
@media screen and (max-width: $no-gap-breakpoint) {
width: 100%;
justify-content: space-between;
}
}
.row__mascot {
flex: 1;
margin: 10px -50px 0 0;
@media screen and (max-width: $no-gap-breakpoint) {
display: none;
}
}
}
&__logo {
margin-right: 20px;
img {
height: 50px;
width: auto;
mix-blend-mode: lighten;
}
}
&__information {
padding: 45px 40px;
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
strong {
counter-increment: list-counter;
&::before {
content: counter(list-counter);
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
background: $highlight-text-color;
color: $ui-base-color;
border-radius: 50%;
width: 4ch;
height: 4ch;
font-weight: 500;
color: lighten($darker-text-color, 10%);
}
.account {
border-bottom: 0;
padding: 0;
&__display-name {
align-items: center;
display: flex;
margin-right: 5px;
}
div.account__display-name {
&:hover {
.display-name strong {
text-decoration: none;
}
}
.account__avatar {
cursor: default;
}
}
&__avatar-wrapper {
margin-left: 0;
flex: 0 0 auto;
}
.display-name {
font-size: 15px;
&__account {
font-size: 14px;
}
}
}
@media screen and (max-width: $small-breakpoint) {
.contact {
margin-top: 30px;
}
}
@media screen and (max-width: $column-breakpoint) {
padding: 25px 20px;
}
}
&__information,
&__forms,
#mastodon-timeline {
box-sizing: border-box;
background: $ui-base-color;
border-radius: 4px;
box-shadow: 0 0 6px rgba($black, 0.1);
}
&__mascot {
height: 104px;
position: relative;
left: -40px;
bottom: 25px;
img {
height: 190px;
width: auto;
}
}
&__short-description {
.row {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
margin-bottom: 40px;
}
@media screen and (max-width: $column-breakpoint) {
.row {
margin-bottom: 20px;
}
}
p a {
color: $secondary-text-color;
}
h1 {
font-weight: 500;
color: $primary-text-color;
margin-bottom: 0;
small {
color: $darker-text-color;
span {
color: $secondary-text-color;
}
}
}
p:last-child {
margin-bottom: 0;
}
}
&__hero {
margin-bottom: 10px;
img {
display: block;
margin: 0;
max-width: 100%;
height: auto;
border-radius: 4px;
}
}
@media screen and (max-width: 840px) {
.information-board {
.container-alt {
padding-right: 20px;
}
.panel {
position: static;
margin-top: 20px;
width: 100%;
border-radius: 4px;
.panel-header {
text-align: center;
}
}
}
}
@media screen and (max-width: 675px) {
.header-wrapper {
padding-top: 0;
&.compact {
padding-bottom: 0;
}
&.compact .hero .heading {
text-align: initial;
}
}
.header .container-alt,
.features .container-alt {
display: block;
}
}
.cta {
margin: 20px;
}
}
.landing {
margin-bottom: 100px;
@media screen and (max-width: 738px) {
margin-bottom: 0;
}
&__brand {
display: flex;
justify-content: center;
align-items: center;
padding: 50px;
.logo {
fill: $primary-text-color;
height: 52px;
}
@media screen and (max-width: $no-gap-breakpoint) {
padding: 0;
margin-bottom: 30px;
}
}
.directory {
margin-top: 30px;
background: transparent;
box-shadow: none;
border-radius: 0;
}
.hero-widget {
margin-top: 30px;
margin-bottom: 0;
h4 {
padding: 10px;
text-transform: uppercase;
font-weight: 700;
font-size: 13px;
color: $darker-text-color;
}
&__text {
border-radius: 0;
padding-bottom: 0;
}
&__footer {
background: $ui-base-color;
padding: 10px;
border-radius: 0 0 4px 4px;
display: flex;
&__column {
flex: 1 1 50%;
overflow-x: hidden;
}
}
.account {
padding: 10px 0;
border-bottom: 0;
.account__display-name {
display: flex;
align-items: center;
}
}
&__counters__wrapper {
display: flex;
}
&__counter {
padding: 10px;
width: 50%;
strong {
font-family: $font-display, sans-serif;
font-size: 15px;
font-weight: 700;
display: block;
}
span {
font-size: 14px;
color: $darker-text-color;
}
}
}
.simple_form .user_agreement .label_input > label {
font-weight: 400;
color: $darker-text-color;
}
.simple_form p.lead {
color: $darker-text-color;
font-size: 15px;
line-height: 20px;
font-weight: 400;
margin-bottom: 25px;
}
&__grid {
max-width: 960px;
margin: 0 auto;
display: grid;
grid-template-columns: minmax(0, 50%) minmax(0, 50%);
grid-gap: 30px;
@media screen and (max-width: 738px) {
grid-template-columns: minmax(0, 100%);
grid-gap: 10px;
&__column-login {
grid-row: 1;
display: flex;
flex-direction: column;
.box-widget {
order: 2;
flex: 0 0 auto;
}
.hero-widget {
margin-top: 0;
margin-bottom: 10px;
order: 1;
flex: 0 0 auto;
}
}
&__column-registration {
grid-row: 2;
}
.directory {
margin-top: 10px;
}
}
@media screen and (max-width: $no-gap-breakpoint) {
grid-gap: 0;
.hero-widget {
display: block;
margin-bottom: 0;
box-shadow: none;
&__img,
&__img img,
&__footer {
border-radius: 0;
}
}
.hero-widget,
.box-widget,
.directory__tag {
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
.directory {
margin-top: 0;
&__tag {
margin-bottom: 0;
& > a,
& > div {
border-radius: 0;
box-shadow: none;
}
&:last-child {
border-bottom: 0;
}
}
}
}
}
}
.brand {
position: relative;
text-decoration: none;
}
.brand__tagline {
display: block;
position: absolute;
bottom: -10px;
left: 50px;
width: 300px;
color: $ui-primary-color;
text-decoration: none;
font-size: 14px;
@media screen and (max-width: $no-gap-breakpoint) {
position: static;
width: auto;
margin-top: 20px;
color: $dark-text-color;
}
}
.rules-list {
background: darken($ui-base-color, 2%);
border: 1px solid darken($ui-base-color, 8%);
border-radius: 4px;
padding: 0.5em 2.5em !important;
margin-top: 1.85em !important;
li {
border-bottom: 1px solid lighten($ui-base-color, 4%);
color: $dark-text-color;
padding: 1em;
&:last-child {
border-bottom: 0;
}
}
&__text {
color: $primary-text-color;
}
}

@ -1,34 +0,0 @@
.compact-header {
h1 {
font-size: 24px;
line-height: 28px;
color: $darker-text-color;
font-weight: 500;
margin-bottom: 20px;
padding: 0 10px;
word-wrap: break-word;
@media screen and (max-width: 740px) {
text-align: center;
padding: 20px 10px 0;
}
a {
color: inherit;
text-decoration: none;
}
small {
font-weight: 400;
color: $secondary-text-color;
}
img {
display: inline-block;
margin-bottom: -5px;
margin-right: 15px;
width: 36px;
height: 36px;
}
}
}

@ -3261,6 +3261,7 @@ $ui-header-height: 55px;
padding: 10px;
padding-top: 20px;
z-index: 1;
font-size: 13px;
ul {
margin-bottom: 10px;
@ -3272,7 +3273,6 @@ $ui-header-height: 55px;
p {
color: $dark-text-color;
font-size: 13px;
margin-bottom: 20px;
a {
@ -8266,79 +8266,202 @@ noscript {
&__body {
margin-top: 20px;
color: $secondary-text-color;
font-size: 15px;
line-height: 22px;
}
}
h1,
p,
ul,
ol {
margin-bottom: 20px;
}
.prose {
color: $secondary-text-color;
font-size: 15px;
line-height: 22px;
ul {
list-style: disc;
}
p,
ul,
ol {
margin-top: 1.25em;
margin-bottom: 1.25em;
}
ol {
list-style: decimal;
}
img {
margin-top: 2em;
margin-bottom: 2em;
}
ul,
ol {
padding-left: 1em;
video {
margin-top: 2em;
margin-bottom: 2em;
}
figure {
margin-top: 2em;
margin-bottom: 2em;
figcaption {
font-size: 0.875em;
line-height: 1.4285714;
margin-top: 0.8571429em;
}
}
li {
margin-bottom: 10px;
figure > * {
margin-top: 0;
margin-bottom: 0;
}
&::marker {
color: $darker-text-color;
}
h1 {
font-size: 1.5em;
margin-top: 0;
margin-bottom: 1em;
line-height: 1.33;
}
&:last-child {
margin-bottom: 0;
}
}
h2 {
font-size: 1.25em;
margin-top: 1.6em;
margin-bottom: 0.6em;
line-height: 1.6;
}
h3,
h4,
h5,
h6 {
margin-top: 1.5em;
margin-bottom: 0.5em;
line-height: 1.5;
}
h1 {
color: $primary-text-color;
font-size: 19px;
line-height: 24px;
font-weight: 700;
margin-top: 30px;
ol {
counter-reset: list-counter;
}
&:first-child {
margin-top: 0;
}
}
li {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
strong {
font-weight: 700;
color: $primary-text-color;
}
ol > li {
counter-increment: list-counter;
em {
font-style: italic;
&::before {
content: counter(list-counter) ".";
position: absolute;
left: 0;
}
}
a {
color: $highlight-text-color;
text-decoration: underline;
ul > li::before {
content: "";
position: absolute;
background-color: $darker-text-color;
border-radius: 50%;
width: 0.375em;
height: 0.375em;
top: 0.5em;
left: 0.25em;
}
&:focus,
&:hover,
&:active {
text-decoration: none;
}
}
ul > li,
ol > li {
position: relative;
padding-left: 1.75em;
}
hr {
border: 1px solid lighten($ui-base-color, 4%);
margin: 30px 0;
& > ul > li p {
margin-top: 0.75em;
margin-bottom: 0.75em;
}
& > ul > li > *:first-child {
margin-top: 1.25em;
}
& > ul > li > *:last-child {
margin-bottom: 1.25em;
}
& > ol > li > *:first-child {
margin-top: 1.25em;
}
& > ol > li > *:last-child {
margin-bottom: 1.25em;
}
ul ul,
ul ol,
ol ul,
ol ol {
margin-top: 0.75em;
margin-bottom: 0.75em;
}
h1,
h2,
h3,
h4,
h5,
h6,
strong,
b {
color: $primary-text-color;
font-weight: 700;
}
em,
i {
font-style: italic;
}
a {
color: $highlight-text-color;
text-decoration: underline;
&:focus,
&:hover,
&:active {
text-decoration: none;
}
}
code {
font-size: 0.875em;
background: darken($ui-base-color, 8%);
border-radius: 4px;
padding: 0.2em 0.3em;
}
hr {
border: 0;
border-top: 1px solid lighten($ui-base-color, 4%);
margin-top: 3em;
margin-bottom: 3em;
}
hr + * {
margin-top: 0;
}
h2 + * {
margin-top: 0;
}
h3 + * {
margin-top: 0;
}
h4 + *,
h5 + *,
h6 + * {
margin-top: 0;
}
& > :first-child {
margin-top: 0;
}
& > :last-child {
margin-bottom: 0;
}
}
.dismissable-banner {
@ -8365,3 +8488,242 @@ noscript {
justify-content: center;
}
}
.image {
position: relative;
overflow: hidden;
&__preview {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
&.loaded &__preview {
display: none;
}
img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
border: 0;
background: transparent;
opacity: 0;
}
&.loaded img {
opacity: 1;
}
}
.about {
padding: 20px;
@media screen and (min-width: $no-gap-breakpoint) {
border-radius: 4px;
}
&__header {
margin-bottom: 30px;
&__hero {
width: 100%;
height: auto;
aspect-ratio: 1.9;
background: lighten($ui-base-color, 4%);
border-radius: 8px;
margin-bottom: 30px;
}
h1,
p {
text-align: center;
}
h1 {
font-size: 24px;
line-height: 1.5;
font-weight: 700;
margin-bottom: 10px;
}
p {
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: $darker-text-color;
}
}
&__meta {
background: lighten($ui-base-color, 4%);
border-radius: 4px;
display: flex;
margin-bottom: 30px;
font-size: 15px;
&__column {
box-sizing: border-box;
width: 50%;
padding: 20px;
}
&__divider {
width: 0;
border: 0;
border-style: solid;
border-color: lighten($ui-base-color, 8%);
border-left-width: 1px;
min-height: calc(100% - 60px);
flex: 0 0 auto;
}
h4 {
font-size: 15px;
text-transform: uppercase;
color: $darker-text-color;
font-weight: 500;
margin-bottom: 20px;
}
@media screen and (max-width: 600px) {
display: block;
h4 {
text-align: center;
}
&__column {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
&__divider {
min-height: 0;
width: 100%;
border-left-width: 0;
border-top-width: 1px;
}
}
.layout-multiple-columns & {
display: block;
h4 {
text-align: center;
}
&__column {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
&__divider {
min-height: 0;
width: 100%;
border-left-width: 0;
border-top-width: 1px;
}
}
}
&__mail {
color: $primary-text-color;
text-decoration: none;
font-weight: 500;
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
.getting-started__footer {
padding: 0;
margin-top: 60px;
text-align: center;
font-size: 15px;
line-height: 22px;
@media screen and (min-width: $no-gap-breakpoint) {
display: none;
}
}
.account {
padding: 0;
border: 0;
}
.account__avatar-wrapper {
margin-left: 0;
}
.account__relationship {
display: none;
}
&__section {
margin-bottom: 10px;
&__title {
font-size: 17px;
font-weight: 600;
line-height: 22px;
padding: 20px;
border-radius: 4px;
background: lighten($ui-base-color, 4%);
color: $highlight-text-color;
cursor: pointer;
}
&.active &__title {
border-radius: 4px 4px 0 0;
}
&__body {
border: 1px solid lighten($ui-base-color, 4%);
border-top: 0;
padding: 20px;
font-size: 15px;
line-height: 22px;
}
}
&__domain-blocks {
margin-top: 30px;
width: 100%;
border-collapse: collapse;
break-inside: auto;
th {
text-align: left;
font-weight: 500;
color: $darker-text-color;
}
thead tr,
tbody tr {
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
tbody tr:last-child {
border-bottom: 0;
}
th,
td {
padding: 8px;
}
}
}

@ -30,7 +30,6 @@
outline: 0;
padding: 12px 16px;
line-height: 32px;
font-family: $font-display, sans-serif;
font-weight: 500;
font-size: 14px;
}

@ -38,7 +38,6 @@
font-weight: 500;
font-size: 24px;
color: $primary-text-color;
font-family: $font-display, sans-serif;
margin-bottom: 20px;
line-height: 30px;
}

@ -139,18 +139,9 @@ code {
}
.rules-list {
list-style: decimal;
font-size: 17px;
line-height: 22px;
font-weight: 500;
background: transparent;
border: 0;
padding: 0.5em 1em !important;
margin-bottom: 30px;
li {
border-color: lighten($ui-base-color, 8%);
}
}
.hint {
@ -603,41 +594,6 @@ code {
}
}
}
&__overlay-area {
position: relative;
&__blurred form {
filter: blur(2px);
}
&__overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: rgba($ui-base-color, 0.65);
border-radius: 4px;
margin-left: -4px;
margin-top: -4px;
padding: 4px;
&__content {
text-align: center;
&.rich-formatting {
&,
p {
color: $primary-text-color;
}
}
}
}
}
}
.block-icon {
@ -908,24 +864,7 @@ code {
}
}
.table-form {
p {
margin-bottom: 15px;
strong {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
}
}
.simple_form,
.table-form {
.simple_form {
.warning {
box-sizing: border-box;
background: rgba($error-value-color, 0.5);

@ -112,13 +112,6 @@
}
}
.box-widget {
padding: 20px;
border-radius: 4px;
background: $ui-base-color;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
}
.placeholder-widget {
padding: 16px;
border-radius: 4px;
@ -128,47 +121,6 @@
margin-bottom: 10px;
}
.contact-widget {
min-height: 100%;
font-size: 15px;
color: $darker-text-color;
line-height: 20px;
word-wrap: break-word;
font-weight: 400;
padding: 0;
h4 {
padding: 10px;
text-transform: uppercase;
font-weight: 700;
font-size: 13px;
color: $darker-text-color;
}
.account {
border-bottom: 0;
padding: 10px 0;
padding-top: 5px;
}
& > a {
display: inline-block;
padding: 10px;
padding-top: 0;
color: $darker-text-color;
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
}
.moved-account-widget {
padding: 15px;
padding-bottom: 20px;
@ -249,37 +201,6 @@
margin-bottom: 10px;
}
.page-header {
background: lighten($ui-base-color, 8%);
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
border-radius: 4px;
padding: 60px 15px;
text-align: center;
margin: 10px 0;
h1 {
color: $primary-text-color;
font-size: 36px;
line-height: 1.1;
font-weight: 700;
margin-bottom: 10px;
}
p {
font-size: 15px;
color: $darker-text-color;
}
@media screen and (max-width: $no-gap-breakpoint) {
margin-top: 0;
background: lighten($ui-base-color, 4%);
h1 {
font-size: 24px;
}
}
}
.directory {
background: $ui-base-color;
border-radius: 4px;
@ -366,34 +287,6 @@
}
}
.avatar-stack {
display: flex;
justify-content: flex-end;
.account__avatar {
flex: 0 0 auto;
width: 36px;
height: 36px;
border-radius: 50%;
position: relative;
margin-left: -10px;
background: darken($ui-base-color, 8%);
border: 2px solid $ui-base-color;
&:nth-child(1) {
z-index: 1;
}
&:nth-child(2) {
z-index: 2;
}
&:nth-child(3) {
z-index: 3;
}
}
}
.accounts-table {
width: 100%;
@ -495,11 +388,7 @@
.moved-account-widget,
.memoriam-widget,
.box-widget,
.contact-widget,
.landing-page__information.contact-widget,
.directory,
.page-header {
.directory {
@media screen and (max-width: $no-gap-breakpoint) {
margin-bottom: 0;
box-shadow: none;
@ -507,88 +396,6 @@
}
}
$maximum-width: 1235px;
$fluid-breakpoint: $maximum-width + 20px;
.statuses-grid {
min-height: 600px;
@media screen and (max-width: 640px) {
width: 100% !important; // Masonry layout is unnecessary at this width
}
&__item {
width: math.div(960px - 20px, 3);
@media screen and (max-width: $fluid-breakpoint) {
width: math.div(940px - 20px, 3);
}
@media screen and (max-width: 640px) {
width: 100%;
}
@media screen and (max-width: $no-gap-breakpoint) {
width: 100vw;
}
}
.detailed-status {
border-radius: 4px;
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 1px solid lighten($ui-base-color, 16%);
}
&.compact {
.detailed-status__meta {
margin-top: 15px;
}
.status__content {
font-size: 15px;
line-height: 20px;
.emojione {
width: 20px;
height: 20px;
margin: -3px 0 0;
}
.status__content__spoiler-link {
line-height: 20px;
margin: 0;
}
}
.media-gallery,
.status-card,
.video-player {
margin-top: 15px;
}
}
}
}
.notice-widget {
margin-bottom: 10px;
color: $darker-text-color;
p {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
a {
font-size: 14px;
line-height: 20px;
}
}
.notice-widget,
.placeholder-widget {
a {
text-decoration: none;
@ -602,37 +409,3 @@ $fluid-breakpoint: $maximum-width + 20px;
}
}
}
.table-of-contents {
background: darken($ui-base-color, 4%);
min-height: 100%;
font-size: 14px;
border-radius: 4px;
li a {
display: block;
font-weight: 500;
padding: 15px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-decoration: none;
color: $primary-text-color;
border-bottom: 1px solid lighten($ui-base-color, 4%);
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
li:last-child a {
border-bottom: 0;
}
li ul {
padding-left: 20px;
border-bottom: 1px solid lighten($ui-base-color, 4%);
}
}

@ -28,8 +28,8 @@ class DomainBlock < ApplicationRecord
delegate :count, to: :accounts, prefix: true
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)) }
scope :by_severity, -> { order(Arel.sql('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), reject_media, domain')) }
scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]) }
scope :by_severity, -> { order(Arel.sql('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), domain')) }
def to_log_human_identifier
domain

@ -0,0 +1,15 @@
# frozen_string_literal: true
class ExtendedDescription < ActiveModelSerializers::Model
attributes :updated_at, :text
def self.current
custom = Setting.find_by(var: 'site_extended_description')
if custom&.value.present?
new(text: custom.value, updated_at: custom.updated_at)
else
new
end
end
end

@ -0,0 +1,17 @@
# frozen_string_literal: true
class REST::DomainBlockSerializer < ActiveModel::Serializer
attributes :domain, :digest, :severity, :comment
def domain
object.public_domain
end
def digest
object.domain_digest
end
def comment
object.public_comment if instance_options[:with_comment]
end
end

@ -0,0 +1,13 @@
# frozen_string_literal: true
class REST::ExtendedDescriptionSerializer < ActiveModel::Serializer
attributes :updated_at, :content
def updated_at
object.updated_at&.iso8601
end
def content
object.text
end
end

@ -1,12 +0,0 @@
%table
%thead
%tr
%th= t('about.unavailable_content_description.domain')
%th= t('about.unavailable_content_description.reason')
%tbody
- domain_blocks.each do |domain_block|
%tr
%td.nowrap
%span{ title: "SHA-256: #{domain_block.domain_digest}" }= domain_block.public_domain
%td
= domain_block.public_comment if display_blocks_rationale?

@ -1,96 +0,0 @@
- content_for :page_title do
= site_hostname
- content_for :header_tags do
= javascript_pack_tag 'public', crossorigin: 'anonymous'
= render partial: 'shared/og'
.grid-4
.column-0
.public-account-header.public-account-header--no-bar
.public-account-header__image
= image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.title, class: 'parallax'
.column-1
.landing-page__call-to-action{ dir: 'ltr' }
.row
.row__information-board
.information-board__section
%span= t 'about.user_count_before'
%strong= friendly_number_to_human @instance_presenter.user_count
%span= t 'about.user_count_after', count: @instance_presenter.user_count
.information-board__section
%span= t 'about.status_count_before'
%strong= friendly_number_to_human @instance_presenter.status_count
%span= t 'about.status_count_after', count: @instance_presenter.status_count
.row__mascot
.landing-page__mascot
= image_tag @instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg'), alt: ''
.column-2
.contact-widget
%h4= t 'about.administered_by'
= account_link_to(@instance_presenter.contact.account)
- if @instance_presenter.contact.email.present?
%h4
= succeed ':' do
= t 'about.contact'
= mail_to @instance_presenter.contact.email, nil, title: @instance_presenter.contact.email
.column-3
= render 'application/flashes'
- if @contents.blank? && @rules.empty? && (!display_blocks? || @blocks&.empty?)
= nothing_here
- else
.box-widget
.rich-formatting
- unless @rules.empty?
%h2#rules= t('about.rules')
%p= t('about.rules_html')
%ol.rules-list
- @rules.each do |rule|
%li
.rules-list__text= rule.text
= @contents.html_safe
- if display_blocks? && !@blocks.empty?
%h2#unavailable-content= t('about.unavailable_content')
%p= t('about.unavailable_content_html')
- if (blocks = @blocks.select(&:reject_media?)) && !blocks.empty?
%h3= t('about.unavailable_content_description.rejecting_media_title')
%p= t('about.unavailable_content_description.rejecting_media')
= render partial: 'domain_blocks', locals: { domain_blocks: blocks }
- if (blocks = @blocks.select(&:silence?)) && !blocks.empty?
%h3= t('about.unavailable_content_description.silenced_title')
%p= t('about.unavailable_content_description.silenced')
= render partial: 'domain_blocks', locals: { domain_blocks: blocks }
- if (blocks = @blocks.select(&:suspend?)) && !blocks.empty?
%h3= t('about.unavailable_content_description.suspended_title')
%p= t('about.unavailable_content_description.suspended')
= render partial: 'domain_blocks', locals: { domain_blocks: blocks }
.column-4
%ul.table-of-contents
- unless @rules.empty?
%li= link_to t('about.rules'), '#rules'
- @table_of_contents.each do |item|
%li
= link_to item.title, "##{item.anchor}"
- unless item.children.empty?
%ul
- item.children.each do |sub_item|
%li= link_to sub_item.title, "##{sub_item.anchor}"
- if display_blocks? && !@blocks.empty?
%li= link_to t('about.unavailable_content'), '#unavailable-content'

@ -0,0 +1,4 @@
- content_for :page_title do
= t('about.title')
= render partial: 'shared/web_app'

@ -2,41 +2,15 @@
en:
about:
about_mastodon_html: 'The social network of the future: No ads, no corporate surveillance, ethical design, and decentralization! Own your data with Mastodon!'
about_this: About
administered_by: 'Administered by:'
api: API
apps: Mobile apps
contact: Contact
contact_missing: Not set
contact_unavailable: N/A
documentation: Documentation
hosted_on: Mastodon hosted on %{domain}
instance_actor_flash: |
This account is a virtual actor used to represent the server itself and not any individual user.
It is used for federation purposes and should not be blocked unless you want to block the whole instance, in which case you should use a domain block.
privacy_policy: Privacy Policy
rules: Server rules
rules_html: 'Below is a summary of rules you need to follow if you want to have an account on this server of Mastodon:'
source_code: Source code
status_count_after:
one: post
other: posts
status_count_before: Who published
unavailable_content: Moderated servers
unavailable_content_description:
domain: Server
reason: Reason
rejecting_media: 'Media files from these servers will not be processed or stored, and no thumbnails will be displayed, requiring manual click-through to the original file:'
rejecting_media_title: Filtered media
silenced: 'Posts from these servers will be hidden in public timelines and conversations, and no notifications will be generated from their users interactions, unless you are following them:'
silenced_title: Limited servers
suspended: 'No data from these servers will be processed, stored or exchanged, making any interaction or communication with users from these servers impossible:'
suspended_title: Suspended servers
unavailable_content_html: Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.
user_count_after:
one: user
other: users
user_count_before: Home to
title: About
what_is_mastodon: What is Mastodon?
accounts:
choices_html: "%{name}'s choices:"

@ -487,7 +487,9 @@ Rails.application.routes.draw do
resource :instance, only: [:show] do
resources :peers, only: [:index], controller: 'instances/peers'
resources :rules, only: [:index], controller: 'instances/rules'
resources :domain_blocks, only: [:index], controller: 'instances/domain_blocks'
resource :privacy_policy, only: [:show], controller: 'instances/privacy_policies'
resource :extended_description, only: [:show], controller: 'instances/extended_descriptions'
resource :activity, only: [:show], controller: 'instances/activity'
end
@ -642,8 +644,8 @@ Rails.application.routes.draw do
get '/web/(*any)', to: 'home#index', as: :web
get '/about', to: redirect('/')
get '/about/more', to: 'about#more'
get '/about', to: 'about#show'
get '/about/more', to: redirect('/about')
get '/privacy-policy', to: 'privacy#show', as: :privacy_policy
get '/terms', to: redirect('/privacy-policy')

@ -3,13 +3,9 @@ require 'rails_helper'
RSpec.describe AboutController, type: :controller do
render_views
describe 'GET #more' do
describe 'GET #show' do
before do
get :more
end
it 'assigns @instance_presenter' do
expect(assigns(:instance_presenter)).to be_kind_of InstancePresenter
get :show
end
it 'returns http success' do

Loading…
Cancel
Save