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

rebase/4.0.0rc1
Eugen Rochko 2022-10-13 14:42:37 +02:00 committed by GitHub
parent b04633a961
commit 1bd00036c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 904 additions and 1578 deletions

View File

@ -1,63 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
class AboutController < ApplicationController class AboutController < ApplicationController
include RegistrationSpamConcern include WebAppControllerConcern
layout 'public' skip_before_action :require_functional!
before_action :require_open_federation!, only: [:more] def show
before_action :set_body_classes, only: :show expires_in 0, public: true unless user_signed_in?
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
end end
end end

View File

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

View File

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

View File

@ -5,6 +5,14 @@ export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST';
export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS'; export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS';
export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL'; 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) => { export const fetchServer = () => (dispatch, getState) => {
dispatch(fetchServerRequest()); dispatch(fetchServerRequest());
@ -28,3 +36,56 @@ const fetchServerFail = error => ({
type: SERVER_FETCH_FAIL, type: SERVER_FETCH_FAIL,
error, 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,
});

View File

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

View File

@ -7,13 +7,15 @@ import ShortNumber from 'mastodon/components/short_number';
import Skeleton from 'mastodon/components/skeleton'; import Skeleton from 'mastodon/components/skeleton';
import Account from 'mastodon/containers/account_container'; import Account from 'mastodon/containers/account_container';
import { domain } from 'mastodon/initial_state'; import { domain } from 'mastodon/initial_state';
import Image from 'mastodon/components/image';
import { Link } from 'react-router-dom';
const messages = defineMessages({ const messages = defineMessages({
aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' }, aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' },
}); });
const mapStateToProps = state => ({ const mapStateToProps = state => ({
server: state.get('server'), server: state.getIn(['server', 'server']),
}); });
export default @connect(mapStateToProps) 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> }} /> <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> </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'> <div className='server-banner__description'>
{isLoading ? ( {isLoading ? (
@ -83,7 +85,7 @@ class ServerBanner extends React.PureComponent {
<hr className='spacer' /> <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> </div>
); );
} }

View File

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

View File

@ -1,27 +1,214 @@
import React from 'react'; 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 PropTypes from 'prop-types';
import Column from 'mastodon/components/column'; import Column from 'mastodon/components/column';
import LinkFooter from 'mastodon/features/ui/components/link_footer'; import LinkFooter from 'mastodon/features/ui/components/link_footer';
import { Helmet } from 'react-helmet'; 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({ const messages = defineMessages({
title: { id: 'column.about', defaultMessage: 'About' }, 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 { class About extends React.PureComponent {
static propTypes = { 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, intl: PropTypes.object.isRequired,
}; };
componentDidMount () {
const { dispatch } = this.props;
dispatch(fetchServer());
dispatch(fetchExtendedDescription());
}
handleDomainBlocksOpen = () => {
const { dispatch } = this.props;
dispatch(fetchDomainBlocks());
}
render () { render () {
const { intl } = this.props; const { intl, server, extendedDescription, domainBlocks } = this.props;
const isLoading = server.get('isLoading');
return ( return (
<Column> <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> <Helmet>
<title>{intl.formatMessage(messages.title)}</title> <title>{intl.formatMessage(messages.title)}</title>

View File

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

View File

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

View File

@ -51,7 +51,7 @@ class LinkFooter extends React.PureComponent {
const items = []; 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='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='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(<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>); items.push(<Link key='privacy-policy' to='/privacy-policy'><FormattedMessage id='getting_started.privacy_policy' defaultMessage='Privacy Policy' /></Link>);

View File

@ -1,18 +1,52 @@
import { SERVER_FETCH_REQUEST, SERVER_FETCH_SUCCESS, SERVER_FETCH_FAIL } from 'mastodon/actions/server'; import {
import { Map as ImmutableMap, fromJS } from 'immutable'; 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({ 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) { export default function server(state = initialState, action) {
switch (action.type) { switch (action.type) {
case SERVER_FETCH_REQUEST: case SERVER_FETCH_REQUEST:
return state.set('isLoading', true); return state.setIn(['server', 'isLoading'], true);
case SERVER_FETCH_SUCCESS: 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: 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: default:
return state; return state;
} }

View File

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

View File

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

View File

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

View File

@ -36,6 +36,20 @@ html {
border-top: 0; 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 { .directory__card__img {
background: lighten($ui-base-color, 12%); background: lighten($ui-base-color, 12%);
} }
@ -45,10 +59,6 @@ html {
border-bottom: 1px solid lighten($ui-base-color, 8%); border-bottom: 1px solid lighten($ui-base-color, 8%);
} }
.table-of-contents {
border: 1px solid lighten($ui-base-color, 8%);
}
.column-back-button, .column-back-button,
.column-header { .column-header {
background: $white; background: $white;
@ -138,11 +148,6 @@ html {
.compose-form__poll-wrapper select, .compose-form__poll-wrapper select,
.search__input, .search__input,
.setting-text, .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, .report-dialog-modal__textarea,
.audio-player { .audio-player {
border: 1px solid lighten($ui-base-color, 8%); border: 1px solid lighten($ui-base-color, 8%);
@ -480,52 +485,16 @@ html {
background: $white; 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 // Change the default colors used on some parts of the profile pages
.activity-stream-tabs { .activity-stream-tabs {
background: $account-background-color; background: $account-background-color;
border-bottom-color: lighten($ui-base-color, 8%); border-bottom-color: lighten($ui-base-color, 8%);
} }
.box-widget,
.nothing-here, .nothing-here,
.page-header, .page-header,
.directory__tag > a, .directory__tag > a,
.directory__tag > div, .directory__tag > div {
.landing-page__call-to-action,
.contact-widget,
.landing .hero-widget__text,
.landing-page__information.contact-widget {
background: $white; background: $white;
border: 1px solid lighten($ui-base-color, 8%); 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 { .simple_form {
input[type="text"], input[type="text"],
input[type="number"], 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 { .picture-in-picture-placeholder {
background: $white; background: $white;
border-color: lighten($ui-base-color, 8%); border-color: lighten($ui-base-color, 8%);
color: lighten($ui-base-color, 8%); color: lighten($ui-base-color, 8%);
} }
.brand__tagline {
color: $ui-secondary-color;
}
.directory__tag > a { .directory__tag > a {
&:hover, &:hover,
&:active, &:active,
@ -666,8 +616,7 @@ html {
} }
} }
.simple_form, .simple_form {
.table-form {
.warning { .warning {
box-shadow: none; box-shadow: none;
background: rgba($error-red, 0.5); background: rgba($error-red, 0.5);
@ -801,9 +750,6 @@ html {
} }
.hero-widget, .hero-widget,
.box-widget,
.contact-widget,
.landing-page__information.contact-widget,
.moved-account-widget, .moved-account-widget,
.memoriam-widget, .memoriam-widget,
.activity-stream, .activity-stream,

View File

@ -1,7 +1,5 @@
$maximum-width: 1235px; $maximum-width: 1235px;
$fluid-breakpoint: $maximum-width + 20px; $fluid-breakpoint: $maximum-width + 20px;
$column-breakpoint: 700px;
$small-breakpoint: 960px;
.container { .container {
box-sizing: border-box; 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;
}
}
.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;
}
}
}
}
.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;
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 {
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;
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 { .brand {
position: relative; position: relative;
text-decoration: none; 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 { .rules-list {
background: darken($ui-base-color, 2%); font-size: 15px;
border: 1px solid darken($ui-base-color, 8%); line-height: 22px;
border-radius: 4px; color: $primary-text-color;
padding: 0.5em 2.5em !important; counter-reset: list-counter;
margin-top: 1.85em !important;
li { li {
border-bottom: 1px solid lighten($ui-base-color, 4%); position: relative;
color: $dark-text-color; border-bottom: 1px solid lighten($ui-base-color, 8%);
padding: 1em; padding: 1em 1.75em;
padding-left: 3em;
font-weight: 500;
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;
display: flex;
justify-content: center;
align-items: center;
}
&:last-child { &:last-child {
border-bottom: 0; border-bottom: 0;
} }
} }
&__text {
color: $primary-text-color;
}
} }

View File

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

View File

@ -3261,6 +3261,7 @@ $ui-header-height: 55px;
padding: 10px; padding: 10px;
padding-top: 20px; padding-top: 20px;
z-index: 1; z-index: 1;
font-size: 13px;
ul { ul {
margin-bottom: 10px; margin-bottom: 10px;
@ -3272,7 +3273,6 @@ $ui-header-height: 55px;
p { p {
color: $dark-text-color; color: $dark-text-color;
font-size: 13px;
margin-bottom: 20px; margin-bottom: 20px;
a { a {
@ -8266,78 +8266,201 @@ noscript {
&__body { &__body {
margin-top: 20px; margin-top: 20px;
color: $secondary-text-color; }
font-size: 15px; }
line-height: 22px;
h1, .prose {
p, color: $secondary-text-color;
ul, font-size: 15px;
ol { line-height: 22px;
margin-bottom: 20px;
p,
ul,
ol {
margin-top: 1.25em;
margin-bottom: 1.25em;
}
img {
margin-top: 2em;
margin-bottom: 2em;
}
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;
} }
}
ul { figure > * {
list-style: disc; margin-top: 0;
margin-bottom: 0;
}
h1 {
font-size: 1.5em;
margin-top: 0;
margin-bottom: 1em;
line-height: 1.33;
}
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;
}
ol {
counter-reset: list-counter;
}
li {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
ol > li {
counter-increment: list-counter;
&::before {
content: counter(list-counter) ".";
position: absolute;
left: 0;
} }
}
ol { ul > li::before {
list-style: decimal; content: "";
position: absolute;
background-color: $darker-text-color;
border-radius: 50%;
width: 0.375em;
height: 0.375em;
top: 0.5em;
left: 0.25em;
}
ul > li,
ol > li {
position: relative;
padding-left: 1.75em;
}
& > 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;
} }
}
ul, code {
ol { font-size: 0.875em;
padding-left: 1em; background: darken($ui-base-color, 8%);
} border-radius: 4px;
padding: 0.2em 0.3em;
}
li { hr {
margin-bottom: 10px; border: 0;
border-top: 1px solid lighten($ui-base-color, 4%);
margin-top: 3em;
margin-bottom: 3em;
}
&::marker { hr + * {
color: $darker-text-color; margin-top: 0;
} }
&:last-child { h2 + * {
margin-bottom: 0; margin-top: 0;
} }
}
h1 { h3 + * {
color: $primary-text-color; margin-top: 0;
font-size: 19px; }
line-height: 24px;
font-weight: 700;
margin-top: 30px;
&:first-child { h4 + *,
margin-top: 0; h5 + *,
} h6 + * {
} margin-top: 0;
}
strong { & > :first-child {
font-weight: 700; margin-top: 0;
color: $primary-text-color; }
}
em { & > :last-child {
font-style: italic; margin-bottom: 0;
}
a {
color: $highlight-text-color;
text-decoration: underline;
&:focus,
&:hover,
&:active {
text-decoration: none;
}
}
hr {
border: 1px solid lighten($ui-base-color, 4%);
margin: 30px 0;
}
} }
} }
@ -8365,3 +8488,242 @@ noscript {
justify-content: center; 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;
}
}
}

View File

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

View File

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

View File

@ -139,18 +139,9 @@ code {
} }
.rules-list { .rules-list {
list-style: decimal;
font-size: 17px; font-size: 17px;
line-height: 22px; line-height: 22px;
font-weight: 500;
background: transparent;
border: 0;
padding: 0.5em 1em !important;
margin-bottom: 30px; margin-bottom: 30px;
li {
border-color: lighten($ui-base-color, 8%);
}
} }
.hint { .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 { .block-icon {
@ -908,24 +864,7 @@ code {
} }
} }
.table-form { .simple_form {
p {
margin-bottom: 15px;
strong {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
}
}
.simple_form,
.table-form {
.warning { .warning {
box-sizing: border-box; box-sizing: border-box;
background: rgba($error-value-color, 0.5); background: rgba($error-value-color, 0.5);

View File

@ -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 { .placeholder-widget {
padding: 16px; padding: 16px;
border-radius: 4px; border-radius: 4px;
@ -128,47 +121,6 @@
margin-bottom: 10px; 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 { .moved-account-widget {
padding: 15px; padding: 15px;
padding-bottom: 20px; padding-bottom: 20px;
@ -249,37 +201,6 @@
margin-bottom: 10px; 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 { .directory {
background: $ui-base-color; background: $ui-base-color;
border-radius: 4px; 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 { .accounts-table {
width: 100%; width: 100%;
@ -495,11 +388,7 @@
.moved-account-widget, .moved-account-widget,
.memoriam-widget, .memoriam-widget,
.box-widget, .directory {
.contact-widget,
.landing-page__information.contact-widget,
.directory,
.page-header {
@media screen and (max-width: $no-gap-breakpoint) { @media screen and (max-width: $no-gap-breakpoint) {
margin-bottom: 0; margin-bottom: 0;
box-shadow: none; 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 { .placeholder-widget {
a { a {
text-decoration: none; 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%);
}
}

View File

@ -28,8 +28,8 @@ class DomainBlock < ApplicationRecord
delegate :count, to: :accounts, prefix: true delegate :count, to: :accounts, prefix: true
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } 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 :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), reject_media, domain')) } 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 def to_log_human_identifier
domain domain

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,41 +2,15 @@
en: en:
about: 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_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 api: API
apps: Mobile apps apps: Mobile apps
contact: Contact
contact_missing: Not set contact_missing: Not set
contact_unavailable: N/A contact_unavailable: N/A
documentation: Documentation documentation: Documentation
hosted_on: Mastodon hosted on %{domain} 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 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 source_code: Source code
status_count_after: title: About
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
what_is_mastodon: What is Mastodon? what_is_mastodon: What is Mastodon?
accounts: accounts:
choices_html: "%{name}'s choices:" choices_html: "%{name}'s choices:"

View File

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

View File

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