Add server banner to web app, add GET /api/v2/instance to REST API (#19294)
				
					
				
			This commit is contained in:
		
							parent
							
								
									cedcece0cc
								
							
						
					
					
						commit
						d2528b26b6
					
				@ -18,7 +18,7 @@ class AboutController < ApplicationController
 | 
			
		||||
  def more
 | 
			
		||||
    flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor]
 | 
			
		||||
 | 
			
		||||
    toc_generator = TOCGenerator.new(@instance_presenter.site_extended_description)
 | 
			
		||||
    toc_generator = TOCGenerator.new(@instance_presenter.extended_description)
 | 
			
		||||
 | 
			
		||||
    @rules             = Rule.ordered
 | 
			
		||||
    @contents          = toc_generator.html
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,6 @@ class Api::V1::InstancesController < Api::BaseController
 | 
			
		||||
 | 
			
		||||
  def show
 | 
			
		||||
    expires_in 3.minutes, public: true
 | 
			
		||||
    render_with_cache json: {}, serializer: REST::InstanceSerializer, root: 'instance'
 | 
			
		||||
    render_with_cache json: InstancePresenter.new, serializer: REST::V1::InstanceSerializer, root: 'instance'
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								app/controllers/api/v2/instances_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/controllers/api/v2/instances_controller.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class Api::V2::InstancesController < Api::V1::InstancesController
 | 
			
		||||
  def show
 | 
			
		||||
    expires_in 3.minutes, public: true
 | 
			
		||||
    render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance'
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -1,27 +0,0 @@
 | 
			
		||||
import api from '../api';
 | 
			
		||||
 | 
			
		||||
export const RULES_FETCH_REQUEST = 'RULES_FETCH_REQUEST';
 | 
			
		||||
export const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS';
 | 
			
		||||
export const RULES_FETCH_FAIL    = 'RULES_FETCH_FAIL';
 | 
			
		||||
 | 
			
		||||
export const fetchRules = () => (dispatch, getState) => {
 | 
			
		||||
  dispatch(fetchRulesRequest());
 | 
			
		||||
 | 
			
		||||
  api(getState)
 | 
			
		||||
    .get('/api/v1/instance').then(({ data }) => dispatch(fetchRulesSuccess(data.rules)))
 | 
			
		||||
    .catch(err => dispatch(fetchRulesFail(err)));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const fetchRulesRequest = () => ({
 | 
			
		||||
  type: RULES_FETCH_REQUEST,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const fetchRulesSuccess = rules => ({
 | 
			
		||||
  type: RULES_FETCH_SUCCESS,
 | 
			
		||||
  rules,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const fetchRulesFail = error => ({
 | 
			
		||||
  type: RULES_FETCH_FAIL,
 | 
			
		||||
  error,
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										30
									
								
								app/javascript/mastodon/actions/server.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/javascript/mastodon/actions/server.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
import api from '../api';
 | 
			
		||||
import { importFetchedAccount } from './importer';
 | 
			
		||||
 | 
			
		||||
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 fetchServer = () => (dispatch, getState) => {
 | 
			
		||||
  dispatch(fetchServerRequest());
 | 
			
		||||
 | 
			
		||||
  api(getState)
 | 
			
		||||
    .get('/api/v2/instance').then(({ data }) => {
 | 
			
		||||
      if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
 | 
			
		||||
      dispatch(fetchServerSuccess(data));
 | 
			
		||||
    }).catch(err => dispatch(fetchServerFail(err)));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const fetchServerRequest = () => ({
 | 
			
		||||
  type: SERVER_FETCH_REQUEST,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const fetchServerSuccess = server => ({
 | 
			
		||||
  type: SERVER_FETCH_SUCCESS,
 | 
			
		||||
  server,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const fetchServerFail = error => ({
 | 
			
		||||
  type: SERVER_FETCH_FAIL,
 | 
			
		||||
  error,
 | 
			
		||||
});
 | 
			
		||||
@ -9,6 +9,7 @@ import { defineMessages, injectIntl } from 'react-intl';
 | 
			
		||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
import { me } from '../initial_state';
 | 
			
		||||
import RelativeTimestamp from './relative_timestamp';
 | 
			
		||||
import Skeleton from 'mastodon/components/skeleton';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  follow: { id: 'account.follow', defaultMessage: 'Follow' },
 | 
			
		||||
@ -26,7 +27,7 @@ export default @injectIntl
 | 
			
		||||
class Account extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    account: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
    account: ImmutablePropTypes.map,
 | 
			
		||||
    onFollow: PropTypes.func.isRequired,
 | 
			
		||||
    onBlock: PropTypes.func.isRequired,
 | 
			
		||||
    onMute: PropTypes.func.isRequired,
 | 
			
		||||
@ -67,7 +68,16 @@ class Account extends ImmutablePureComponent {
 | 
			
		||||
    const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (!account) {
 | 
			
		||||
      return <div />;
 | 
			
		||||
      return (
 | 
			
		||||
        <div className='account'>
 | 
			
		||||
          <div className='account__wrapper'>
 | 
			
		||||
            <div className='account__display-name'>
 | 
			
		||||
              <div className='account__avatar-wrapper'><Skeleton width={36} height={36} /></div>
 | 
			
		||||
              <DisplayName />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (hidden) {
 | 
			
		||||
 | 
			
		||||
@ -2,11 +2,12 @@ import React from 'react';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { autoPlayGif } from 'mastodon/initial_state';
 | 
			
		||||
import Skeleton from 'mastodon/components/skeleton';
 | 
			
		||||
 | 
			
		||||
export default class DisplayName extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    account: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
    account: ImmutablePropTypes.map,
 | 
			
		||||
    others: ImmutablePropTypes.list,
 | 
			
		||||
    localDomain: PropTypes.string,
 | 
			
		||||
  };
 | 
			
		||||
@ -48,7 +49,7 @@ export default class DisplayName extends React.PureComponent {
 | 
			
		||||
      if (others.size - 2 > 0) {
 | 
			
		||||
        suffix = `+${others.size - 2}`;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
    } else if ((others && others.size > 0) || this.props.account) {
 | 
			
		||||
      if (others && others.size > 0) {
 | 
			
		||||
        account = others.first();
 | 
			
		||||
      } else {
 | 
			
		||||
@ -63,6 +64,9 @@ export default class DisplayName extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
      displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
 | 
			
		||||
      suffix      = <span className='display-name__account'>@{acct}</span>;
 | 
			
		||||
    } else {
 | 
			
		||||
      displayName = <bdi><strong className='display-name__html'><Skeleton width='10ch' /></strong></bdi>;
 | 
			
		||||
      suffix = <span className='display-name__account'><Skeleton width='7ch' /></span>;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										91
									
								
								app/javascript/mastodon/components/server_banner.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								app/javascript/mastodon/components/server_banner.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,91 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { domain } from 'mastodon/initial_state';
 | 
			
		||||
import { fetchServer } from 'mastodon/actions/server';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import Account from 'mastodon/containers/account_container';
 | 
			
		||||
import ShortNumber from 'mastodon/components/short_number';
 | 
			
		||||
import Skeleton from 'mastodon/components/skeleton';
 | 
			
		||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
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'),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default @connect(mapStateToProps)
 | 
			
		||||
@injectIntl
 | 
			
		||||
class ServerBanner extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    server: PropTypes.object,
 | 
			
		||||
    dispatch: PropTypes.func,
 | 
			
		||||
    intl: PropTypes.object,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    const { dispatch } = this.props;
 | 
			
		||||
    dispatch(fetchServer());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { server, intl } = this.props;
 | 
			
		||||
    const isLoading = server.get('isLoading');
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='server-banner'>
 | 
			
		||||
        <div className='server-banner__introduction'>
 | 
			
		||||
          <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' />
 | 
			
		||||
 | 
			
		||||
        <div className='server-banner__description'>
 | 
			
		||||
          {isLoading ? (
 | 
			
		||||
            <>
 | 
			
		||||
              <Skeleton width='100%' />
 | 
			
		||||
              <br />
 | 
			
		||||
              <Skeleton width='100%' />
 | 
			
		||||
              <br />
 | 
			
		||||
              <Skeleton width='70%' />
 | 
			
		||||
            </>
 | 
			
		||||
          ) : server.get('description')}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div className='server-banner__meta'>
 | 
			
		||||
          <div className='server-banner__meta__column'>
 | 
			
		||||
            <h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
 | 
			
		||||
 | 
			
		||||
            <Account id={server.getIn(['contact', 'account', 'id'])} />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div className='server-banner__meta__column'>
 | 
			
		||||
            <h4><FormattedMessage id='server_banner.server_stats' defaultMessage='Server stats:' /></h4>
 | 
			
		||||
 | 
			
		||||
            {isLoading ? (
 | 
			
		||||
              <>
 | 
			
		||||
                <strong className='server-banner__number'><Skeleton width='10ch' /></strong>
 | 
			
		||||
                <br />
 | 
			
		||||
                <span className='server-banner__number-label'><Skeleton width='5ch' /></span>
 | 
			
		||||
              </>
 | 
			
		||||
            ) : (
 | 
			
		||||
              <>
 | 
			
		||||
                <strong className='server-banner__number'><ShortNumber value={server.getIn(['usage', 'users', 'active_month'])} /></strong>
 | 
			
		||||
                <br />
 | 
			
		||||
                <span className='server-banner__number-label' title={intl.formatMessage(messages.aboutActiveUsers)}><FormattedMessage id='server_banner.active_users' defaultMessage='active users' /></span>
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <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>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -7,7 +7,7 @@ import Button from 'mastodon/components/button';
 | 
			
		||||
import Option from './components/option';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = state => ({
 | 
			
		||||
  rules: state.get('rules'),
 | 
			
		||||
  rules: state.getIn(['server', 'rules']),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default @connect(mapStateToProps)
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ import SearchContainer from 'mastodon/features/compose/containers/search_contain
 | 
			
		||||
import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container';
 | 
			
		||||
import NavigationContainer from 'mastodon/features/compose/containers/navigation_container';
 | 
			
		||||
import LinkFooter from './link_footer';
 | 
			
		||||
import ServerBanner from 'mastodon/components/server_banner';
 | 
			
		||||
import { changeComposing } from 'mastodon/actions/compose';
 | 
			
		||||
 | 
			
		||||
export default @connect()
 | 
			
		||||
@ -35,6 +36,7 @@ class ComposePanel extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
        {!signedIn && (
 | 
			
		||||
          <React.Fragment>
 | 
			
		||||
            <ServerBanner />
 | 
			
		||||
            <div className='flex-spacer' />
 | 
			
		||||
          </React.Fragment>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import React from 'react';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import { submitReport } from 'mastodon/actions/reports';
 | 
			
		||||
import { expandAccountTimeline } from 'mastodon/actions/timelines';
 | 
			
		||||
import { fetchRules } from 'mastodon/actions/rules';
 | 
			
		||||
import { fetchServer } from 'mastodon/actions/server';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import { makeGetAccount } from 'mastodon/selectors';
 | 
			
		||||
@ -117,7 +117,7 @@ class ReportModal extends ImmutablePureComponent {
 | 
			
		||||
    const { dispatch, accountId } = this.props;
 | 
			
		||||
 | 
			
		||||
    dispatch(expandAccountTimeline(accountId, { withReplies: true }));
 | 
			
		||||
    dispatch(fetchRules());
 | 
			
		||||
    dispatch(fetchServer());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@ import { debounce } from 'lodash';
 | 
			
		||||
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
 | 
			
		||||
import { expandHomeTimeline } from '../../actions/timelines';
 | 
			
		||||
import { expandNotifications } from '../../actions/notifications';
 | 
			
		||||
import { fetchRules } from '../../actions/rules';
 | 
			
		||||
import { fetchServer } from '../../actions/server';
 | 
			
		||||
import { clearHeight } from '../../actions/height_cache';
 | 
			
		||||
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
 | 
			
		||||
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
 | 
			
		||||
@ -392,7 +392,7 @@ class UI extends React.PureComponent {
 | 
			
		||||
      this.props.dispatch(expandHomeTimeline());
 | 
			
		||||
      this.props.dispatch(expandNotifications());
 | 
			
		||||
 | 
			
		||||
      setTimeout(() => this.props.dispatch(fetchRules()), 3000);
 | 
			
		||||
      setTimeout(() => this.props.dispatch(fetchServer()), 3000);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
 | 
			
		||||
 | 
			
		||||
@ -28,6 +28,5 @@ export const title = getMeta('title');
 | 
			
		||||
export const cropImages = getMeta('crop_images');
 | 
			
		||||
export const disableSwiping = getMeta('disable_swiping');
 | 
			
		||||
export const languages = initialState && initialState.languages;
 | 
			
		||||
export const server = initialState && initialState.server;
 | 
			
		||||
 | 
			
		||||
export default initialState;
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ import status_lists from './status_lists';
 | 
			
		||||
import mutes from './mutes';
 | 
			
		||||
import blocks from './blocks';
 | 
			
		||||
import boosts from './boosts';
 | 
			
		||||
import rules from './rules';
 | 
			
		||||
import server from './server';
 | 
			
		||||
import contexts from './contexts';
 | 
			
		||||
import compose from './compose';
 | 
			
		||||
import search from './search';
 | 
			
		||||
@ -62,7 +62,7 @@ const reducers = {
 | 
			
		||||
  mutes,
 | 
			
		||||
  blocks,
 | 
			
		||||
  boosts,
 | 
			
		||||
  rules,
 | 
			
		||||
  server,
 | 
			
		||||
  contexts,
 | 
			
		||||
  compose,
 | 
			
		||||
  search,
 | 
			
		||||
 | 
			
		||||
@ -1,13 +0,0 @@
 | 
			
		||||
import { RULES_FETCH_SUCCESS } from 'mastodon/actions/rules';
 | 
			
		||||
import { List as ImmutableList, fromJS } from 'immutable';
 | 
			
		||||
 | 
			
		||||
const initialState = ImmutableList();
 | 
			
		||||
 | 
			
		||||
export default function rules(state = initialState, action) {
 | 
			
		||||
  switch (action.type) {
 | 
			
		||||
  case RULES_FETCH_SUCCESS:
 | 
			
		||||
    return fromJS(action.rules);
 | 
			
		||||
  default:
 | 
			
		||||
    return state;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								app/javascript/mastodon/reducers/server.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/javascript/mastodon/reducers/server.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
import { SERVER_FETCH_REQUEST, SERVER_FETCH_SUCCESS, SERVER_FETCH_FAIL } from 'mastodon/actions/server';
 | 
			
		||||
import { Map as ImmutableMap, fromJS } from 'immutable';
 | 
			
		||||
 | 
			
		||||
const initialState = ImmutableMap({
 | 
			
		||||
  isLoading: true,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default function server(state = initialState, action) {
 | 
			
		||||
  switch (action.type) {
 | 
			
		||||
  case SERVER_FETCH_REQUEST:
 | 
			
		||||
    return state.set('isLoading', true);
 | 
			
		||||
  case SERVER_FETCH_SUCCESS:
 | 
			
		||||
    return fromJS(action.server).set('isLoading', false);
 | 
			
		||||
  case SERVER_FETCH_FAIL:
 | 
			
		||||
    return state.set('isLoading', false);
 | 
			
		||||
  default:
 | 
			
		||||
    return state;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -8021,3 +8021,85 @@ noscript {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.server-banner {
 | 
			
		||||
  padding: 20px 0;
 | 
			
		||||
 | 
			
		||||
  &__introduction {
 | 
			
		||||
    color: $darker-text-color;
 | 
			
		||||
    margin-bottom: 20px;
 | 
			
		||||
 | 
			
		||||
    strong {
 | 
			
		||||
      font-weight: 600;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    a {
 | 
			
		||||
      color: inherit;
 | 
			
		||||
      text-decoration: underline;
 | 
			
		||||
 | 
			
		||||
      &:hover,
 | 
			
		||||
      &:active,
 | 
			
		||||
      &:focus {
 | 
			
		||||
        text-decoration: none;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__hero {
 | 
			
		||||
    display: block;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: auto;
 | 
			
		||||
    margin-bottom: 20px;
 | 
			
		||||
    aspect-ratio: 1.9;
 | 
			
		||||
    border: 0;
 | 
			
		||||
    background: $ui-base-color;
 | 
			
		||||
    object-fit: cover;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__description {
 | 
			
		||||
    margin-bottom: 20px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__meta {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: 10px;
 | 
			
		||||
    max-width: 100%;
 | 
			
		||||
 | 
			
		||||
    &__column {
 | 
			
		||||
      flex: 0 0 auto;
 | 
			
		||||
      width: calc(50% - 5px);
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__number {
 | 
			
		||||
    font-weight: 600;
 | 
			
		||||
    color: $primary-text-color;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__number-label {
 | 
			
		||||
    color: $darker-text-color;
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  h4 {
 | 
			
		||||
    text-transform: uppercase;
 | 
			
		||||
    color: $darker-text-color;
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
    font-weight: 600;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .account {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    border: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .account__avatar-wrapper {
 | 
			
		||||
    margin-left: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .spacer {
 | 
			
		||||
    margin: 10px 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,19 +1,51 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class InstancePresenter
 | 
			
		||||
  delegate(
 | 
			
		||||
    :site_contact_email,
 | 
			
		||||
    :site_title,
 | 
			
		||||
    :site_short_description,
 | 
			
		||||
    :site_description,
 | 
			
		||||
    :site_extended_description,
 | 
			
		||||
    :site_terms,
 | 
			
		||||
    :closed_registrations_message,
 | 
			
		||||
    to: Setting
 | 
			
		||||
  )
 | 
			
		||||
class InstancePresenter < ActiveModelSerializers::Model
 | 
			
		||||
  attributes :domain, :title, :version, :source_url,
 | 
			
		||||
             :description, :languages, :rules, :contact
 | 
			
		||||
 | 
			
		||||
  def contact_account
 | 
			
		||||
    Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, ''))
 | 
			
		||||
  class ContactPresenter < ActiveModelSerializers::Model
 | 
			
		||||
    attributes :email, :account
 | 
			
		||||
 | 
			
		||||
    def email
 | 
			
		||||
      Setting.site_contact_email
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def account
 | 
			
		||||
      Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, ''))
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def contact
 | 
			
		||||
    ContactPresenter.new
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def closed_registrations_message
 | 
			
		||||
    Setting.closed_registrations_message
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def description
 | 
			
		||||
    Setting.site_short_description
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def extended_description
 | 
			
		||||
    Setting.site_extended_description
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def privacy_policy
 | 
			
		||||
    Setting.site_terms
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def domain
 | 
			
		||||
    Rails.configuration.x.local_domain
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def title
 | 
			
		||||
    Setting.site_title
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def languages
 | 
			
		||||
    [I18n.default_locale]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def rules
 | 
			
		||||
@ -40,8 +72,8 @@ class InstancePresenter
 | 
			
		||||
    Rails.cache.fetch('sample_accounts', expires_in: 12.hours) { Account.local.discoverable.popular.limit(3) }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def version_number
 | 
			
		||||
    Mastodon::Version
 | 
			
		||||
  def version
 | 
			
		||||
    Mastodon::Version.to_s
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def source_url
 | 
			
		||||
 | 
			
		||||
@ -5,23 +5,24 @@ class InitialStateSerializer < ActiveModel::Serializer
 | 
			
		||||
 | 
			
		||||
  attributes :meta, :compose, :accounts,
 | 
			
		||||
             :media_attachments, :settings,
 | 
			
		||||
             :languages, :server
 | 
			
		||||
             :languages
 | 
			
		||||
 | 
			
		||||
  has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
 | 
			
		||||
  has_one :role, serializer: REST::RoleSerializer
 | 
			
		||||
 | 
			
		||||
  # rubocop:disable Metrics/AbcSize
 | 
			
		||||
  def meta
 | 
			
		||||
    store = {
 | 
			
		||||
      streaming_api_base_url: Rails.configuration.x.streaming_api_base_url,
 | 
			
		||||
      access_token: object.token,
 | 
			
		||||
      locale: I18n.locale,
 | 
			
		||||
      domain: Rails.configuration.x.local_domain,
 | 
			
		||||
      title: instance_presenter.site_title,
 | 
			
		||||
      domain: instance_presenter.domain,
 | 
			
		||||
      title: instance_presenter.title,
 | 
			
		||||
      admin: object.admin&.id&.to_s,
 | 
			
		||||
      search_enabled: Chewy.enabled?,
 | 
			
		||||
      repository: Mastodon::Version.repository,
 | 
			
		||||
      source_url: Mastodon::Version.source_url,
 | 
			
		||||
      version: Mastodon::Version.to_s,
 | 
			
		||||
      source_url: instance_presenter.source_url,
 | 
			
		||||
      version: instance_presenter.version,
 | 
			
		||||
      limited_federation_mode: Rails.configuration.x.whitelist_mode,
 | 
			
		||||
      mascot: instance_presenter.mascot&.file&.url,
 | 
			
		||||
      profile_directory: Setting.profile_directory,
 | 
			
		||||
@ -54,6 +55,7 @@ class InitialStateSerializer < ActiveModel::Serializer
 | 
			
		||||
 | 
			
		||||
    store
 | 
			
		||||
  end
 | 
			
		||||
  # rubocop:enable Metrics/AbcSize
 | 
			
		||||
 | 
			
		||||
  def compose
 | 
			
		||||
    store = {}
 | 
			
		||||
@ -85,13 +87,6 @@ class InitialStateSerializer < ActiveModel::Serializer
 | 
			
		||||
    LanguagesHelper::SUPPORTED_LOCALES.map { |(key, value)| [key, value[0], value[1]] }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def server
 | 
			
		||||
    {
 | 
			
		||||
      hero: instance_presenter.hero&.file&.url || instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'),
 | 
			
		||||
      description: instance_presenter.site_short_description.presence || I18n.t('about.about_mastodon_html'),
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def instance_presenter
 | 
			
		||||
 | 
			
		||||
@ -22,11 +22,11 @@ class ManifestSerializer < ActiveModel::Serializer
 | 
			
		||||
             :share_target, :shortcuts
 | 
			
		||||
 | 
			
		||||
  def name
 | 
			
		||||
    object.site_title
 | 
			
		||||
    object.title
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def short_name
 | 
			
		||||
    object.site_title
 | 
			
		||||
    object.title
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def icons
 | 
			
		||||
 | 
			
		||||
@ -1,61 +1,39 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class REST::InstanceSerializer < ActiveModel::Serializer
 | 
			
		||||
  class ContactSerializer < ActiveModel::Serializer
 | 
			
		||||
    attributes :email
 | 
			
		||||
 | 
			
		||||
    has_one :account, serializer: REST::AccountSerializer
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  include RoutingHelper
 | 
			
		||||
 | 
			
		||||
  attributes :uri, :title, :short_description, :description, :email,
 | 
			
		||||
             :version, :urls, :stats, :thumbnail,
 | 
			
		||||
             :languages, :registrations, :approval_required, :invites_enabled,
 | 
			
		||||
             :configuration
 | 
			
		||||
 | 
			
		||||
  has_one :contact_account, serializer: REST::AccountSerializer
 | 
			
		||||
  attributes :domain, :title, :version, :source_url, :description,
 | 
			
		||||
             :usage, :thumbnail, :languages, :configuration,
 | 
			
		||||
             :registrations
 | 
			
		||||
 | 
			
		||||
  has_one :contact, serializer: ContactSerializer
 | 
			
		||||
  has_many :rules, serializer: REST::RuleSerializer
 | 
			
		||||
 | 
			
		||||
  delegate :contact_account, :rules, to: :instance_presenter
 | 
			
		||||
 | 
			
		||||
  def uri
 | 
			
		||||
    Rails.configuration.x.local_domain
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def title
 | 
			
		||||
    Setting.site_title
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def short_description
 | 
			
		||||
    Setting.site_short_description
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def description
 | 
			
		||||
    Setting.site_description
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def email
 | 
			
		||||
    Setting.site_contact_email
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def version
 | 
			
		||||
    Mastodon::Version.to_s
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def thumbnail
 | 
			
		||||
    instance_presenter.thumbnail ? full_asset_url(instance_presenter.thumbnail.file.url) : full_pack_url('media/images/preview.png')
 | 
			
		||||
    object.thumbnail ? full_asset_url(object.thumbnail.file.url) : full_pack_url('media/images/preview.png')
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def stats
 | 
			
		||||
  def usage
 | 
			
		||||
    {
 | 
			
		||||
      user_count: instance_presenter.user_count,
 | 
			
		||||
      status_count: instance_presenter.status_count,
 | 
			
		||||
      domain_count: instance_presenter.domain_count,
 | 
			
		||||
      users: {
 | 
			
		||||
        active_month: object.active_user_count(4),
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def urls
 | 
			
		||||
    { streaming_api: Rails.configuration.x.streaming_api_base_url }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def configuration
 | 
			
		||||
    {
 | 
			
		||||
      urls: {
 | 
			
		||||
        streaming: Rails.configuration.x.streaming_api_base_url,
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      statuses: {
 | 
			
		||||
        max_characters: StatusLengthValidator::MAX_CHARS,
 | 
			
		||||
        max_media_attachments: 4,
 | 
			
		||||
@ -80,25 +58,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def languages
 | 
			
		||||
    [I18n.default_locale]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def registrations
 | 
			
		||||
    Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def approval_required
 | 
			
		||||
    Setting.registrations_mode == 'approved'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def invites_enabled
 | 
			
		||||
    UserRole.everyone.can?(:invite_users)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def instance_presenter
 | 
			
		||||
    @instance_presenter ||= InstancePresenter.new
 | 
			
		||||
    {
 | 
			
		||||
      enabled: Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode,
 | 
			
		||||
      approval_required: Setting.registrations_mode == 'approved',
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										102
									
								
								app/serializers/rest/v1/instance_serializer.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								app/serializers/rest/v1/instance_serializer.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,102 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class REST::V1::InstanceSerializer < ActiveModel::Serializer
 | 
			
		||||
  include RoutingHelper
 | 
			
		||||
 | 
			
		||||
  attributes :uri, :title, :short_description, :description, :email,
 | 
			
		||||
             :version, :urls, :stats, :thumbnail,
 | 
			
		||||
             :languages, :registrations, :approval_required, :invites_enabled,
 | 
			
		||||
             :configuration
 | 
			
		||||
 | 
			
		||||
  has_one :contact_account, serializer: REST::AccountSerializer
 | 
			
		||||
 | 
			
		||||
  has_many :rules, serializer: REST::RuleSerializer
 | 
			
		||||
 | 
			
		||||
  def uri
 | 
			
		||||
    object.domain
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def short_description
 | 
			
		||||
    object.description
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def description
 | 
			
		||||
    Setting.site_description # Legacy
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def email
 | 
			
		||||
    object.contact.email
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def contact_account
 | 
			
		||||
    object.contact.account
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def thumbnail
 | 
			
		||||
    instance_presenter.thumbnail ? full_asset_url(instance_presenter.thumbnail.file.url) : full_pack_url('media/images/preview.png')
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def stats
 | 
			
		||||
    {
 | 
			
		||||
      user_count: instance_presenter.user_count,
 | 
			
		||||
      status_count: instance_presenter.status_count,
 | 
			
		||||
      domain_count: instance_presenter.domain_count,
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def urls
 | 
			
		||||
    { streaming_api: Rails.configuration.x.streaming_api_base_url }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def usage
 | 
			
		||||
    {
 | 
			
		||||
      users: {
 | 
			
		||||
        active_month: instance_presenter.active_user_count(4),
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def configuration
 | 
			
		||||
    {
 | 
			
		||||
      statuses: {
 | 
			
		||||
        max_characters: StatusLengthValidator::MAX_CHARS,
 | 
			
		||||
        max_media_attachments: 4,
 | 
			
		||||
        characters_reserved_per_url: StatusLengthValidator::URL_PLACEHOLDER_CHARS,
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      media_attachments: {
 | 
			
		||||
        supported_mime_types: MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES + MediaAttachment::AUDIO_MIME_TYPES,
 | 
			
		||||
        image_size_limit: MediaAttachment::IMAGE_LIMIT,
 | 
			
		||||
        image_matrix_limit: Attachmentable::MAX_MATRIX_LIMIT,
 | 
			
		||||
        video_size_limit: MediaAttachment::VIDEO_LIMIT,
 | 
			
		||||
        video_frame_rate_limit: MediaAttachment::MAX_VIDEO_FRAME_RATE,
 | 
			
		||||
        video_matrix_limit: MediaAttachment::MAX_VIDEO_MATRIX_LIMIT,
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      polls: {
 | 
			
		||||
        max_options: PollValidator::MAX_OPTIONS,
 | 
			
		||||
        max_characters_per_option: PollValidator::MAX_OPTION_CHARS,
 | 
			
		||||
        min_expiration: PollValidator::MIN_EXPIRATION,
 | 
			
		||||
        max_expiration: PollValidator::MAX_EXPIRATION,
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def registrations
 | 
			
		||||
    Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def approval_required
 | 
			
		||||
    Setting.registrations_mode == 'approved'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def invites_enabled
 | 
			
		||||
    UserRole.everyone.can?(:invite_users)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def instance_presenter
 | 
			
		||||
    @instance_presenter ||= InstancePresenter.new
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -9,7 +9,7 @@
 | 
			
		||||
  .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.site_title, class: 'parallax'
 | 
			
		||||
        = 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' }
 | 
			
		||||
@ -31,14 +31,14 @@
 | 
			
		||||
    .contact-widget
 | 
			
		||||
      %h4= t 'about.administered_by'
 | 
			
		||||
 | 
			
		||||
      = account_link_to(@instance_presenter.contact_account)
 | 
			
		||||
      = account_link_to(@instance_presenter.contact.account)
 | 
			
		||||
 | 
			
		||||
      - if @instance_presenter.site_contact_email.present?
 | 
			
		||||
      - if @instance_presenter.contact.email.present?
 | 
			
		||||
        %h4
 | 
			
		||||
          = succeed ':' do
 | 
			
		||||
            = t 'about.contact'
 | 
			
		||||
 | 
			
		||||
        = mail_to @instance_presenter.site_contact_email, nil, title: @instance_presenter.site_contact_email
 | 
			
		||||
        = mail_to @instance_presenter.contact.email, nil, title: @instance_presenter.contact.email
 | 
			
		||||
 | 
			
		||||
  .column-3
 | 
			
		||||
    = render 'application/flashes'
 | 
			
		||||
 | 
			
		||||
@ -40,11 +40,11 @@
 | 
			
		||||
 | 
			
		||||
      .hero-widget
 | 
			
		||||
        .hero-widget__img
 | 
			
		||||
          = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.site_title
 | 
			
		||||
          = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.title
 | 
			
		||||
 | 
			
		||||
        .hero-widget__text
 | 
			
		||||
          %p
 | 
			
		||||
            = @instance_presenter.site_short_description.html_safe.presence || t('about.about_mastodon_html')
 | 
			
		||||
            = @instance_presenter.description.html_safe.presence || t('about.about_mastodon_html')
 | 
			
		||||
            = link_to about_more_path do
 | 
			
		||||
              = t('about.learn_more')
 | 
			
		||||
              = fa_icon 'angle-double-right'
 | 
			
		||||
@ -53,7 +53,7 @@
 | 
			
		||||
          .hero-widget__footer__column
 | 
			
		||||
            %h4= t 'about.administered_by'
 | 
			
		||||
 | 
			
		||||
            = account_link_to @instance_presenter.contact_account
 | 
			
		||||
            = account_link_to @instance_presenter.contact.account
 | 
			
		||||
 | 
			
		||||
          .hero-widget__footer__column
 | 
			
		||||
            %h4= t 'about.server_stats'
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
.hero-widget
 | 
			
		||||
  .hero-widget__img
 | 
			
		||||
    = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.site_title
 | 
			
		||||
    = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.title
 | 
			
		||||
 | 
			
		||||
  .hero-widget__text
 | 
			
		||||
    %p= @instance_presenter.site_short_description.html_safe.presence || t('about.about_mastodon_html')
 | 
			
		||||
    %p= @instance_presenter.description.html_safe.presence || t('about.about_mastodon_html')
 | 
			
		||||
 | 
			
		||||
- if Setting.trends && !(user_signed_in? && !current_user.setting_trends)
 | 
			
		||||
  - trends = Trends.tags.query.allowed.limit(3)
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,6 @@
 | 
			
		||||
.grid
 | 
			
		||||
  .column-0
 | 
			
		||||
    .box-widget
 | 
			
		||||
      .rich-formatting= @instance_presenter.site_terms.html_safe.presence || t('terms.body_html')
 | 
			
		||||
      .rich-formatting= @instance_presenter.privacy_policy.html_safe.presence || t('terms.body_html')
 | 
			
		||||
  .column-1
 | 
			
		||||
    = render 'application/sidebar'
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,12 @@
 | 
			
		||||
- thumbnail     = @instance_presenter.thumbnail
 | 
			
		||||
- description ||= strip_tags(@instance_presenter.site_short_description.presence || t('about.about_mastodon_html'))
 | 
			
		||||
- description ||= strip_tags(@instance_presenter.description.presence || t('about.about_mastodon_html'))
 | 
			
		||||
 | 
			
		||||
%meta{ name: 'description', content: description }/
 | 
			
		||||
 | 
			
		||||
= opengraph 'og:site_name', t('about.hosted_on', domain: site_hostname)
 | 
			
		||||
= opengraph 'og:url', url_for(only_path: false)
 | 
			
		||||
= opengraph 'og:type', 'website'
 | 
			
		||||
= opengraph 'og:title', @instance_presenter.site_title
 | 
			
		||||
= opengraph 'og:title', @instance_presenter.title
 | 
			
		||||
= opengraph 'og:description', description
 | 
			
		||||
= opengraph 'og:image', full_asset_url(thumbnail&.file&.url || asset_pack_path('media/images/preview.png', protocol: :request))
 | 
			
		||||
= opengraph 'og:image:width', thumbnail ? thumbnail.meta['width'] : '1200'
 | 
			
		||||
 | 
			
		||||
@ -616,10 +616,12 @@ Rails.application.routes.draw do
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    namespace :v2 do
 | 
			
		||||
      resources :media, only: [:create]
 | 
			
		||||
      get '/search', to: 'search#index', as: :search
 | 
			
		||||
 | 
			
		||||
      resources :media,       only: [:create]
 | 
			
		||||
      resources :suggestions, only: [:index]
 | 
			
		||||
      resources :filters,     only: [:index, :create, :show, :update, :destroy]
 | 
			
		||||
      resource  :instance,    only: [:show]
 | 
			
		||||
 | 
			
		||||
      namespace :admin do
 | 
			
		||||
        resources :accounts, only: [:index]
 | 
			
		||||
 | 
			
		||||
@ -3,21 +3,20 @@ require 'rails_helper'
 | 
			
		||||
describe InstancePresenter do
 | 
			
		||||
  let(:instance_presenter) { InstancePresenter.new }
 | 
			
		||||
 | 
			
		||||
  context do
 | 
			
		||||
  describe '#description' do
 | 
			
		||||
    around do |example|
 | 
			
		||||
      site_description = Setting.site_description
 | 
			
		||||
      site_description = Setting.site_short_description
 | 
			
		||||
      example.run
 | 
			
		||||
      Setting.site_description = site_description
 | 
			
		||||
      Setting.site_short_description = site_description
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "delegates site_description to Setting" do
 | 
			
		||||
      Setting.site_description = "Site desc"
 | 
			
		||||
 | 
			
		||||
      expect(instance_presenter.site_description).to eq "Site desc"
 | 
			
		||||
      Setting.site_short_description = "Site desc"
 | 
			
		||||
      expect(instance_presenter.description).to eq "Site desc"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context do
 | 
			
		||||
  describe '#extended_description' do
 | 
			
		||||
    around do |example|
 | 
			
		||||
      site_extended_description = Setting.site_extended_description
 | 
			
		||||
      example.run
 | 
			
		||||
@ -26,12 +25,11 @@ describe InstancePresenter do
 | 
			
		||||
 | 
			
		||||
    it "delegates site_extended_description to Setting" do
 | 
			
		||||
      Setting.site_extended_description = "Extended desc"
 | 
			
		||||
 | 
			
		||||
      expect(instance_presenter.site_extended_description).to eq "Extended desc"
 | 
			
		||||
      expect(instance_presenter.extended_description).to eq "Extended desc"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context do
 | 
			
		||||
  describe '#email' do
 | 
			
		||||
    around do |example|
 | 
			
		||||
      site_contact_email = Setting.site_contact_email
 | 
			
		||||
      example.run
 | 
			
		||||
@ -40,12 +38,11 @@ describe InstancePresenter do
 | 
			
		||||
 | 
			
		||||
    it "delegates contact_email to Setting" do
 | 
			
		||||
      Setting.site_contact_email = "admin@example.com"
 | 
			
		||||
 | 
			
		||||
      expect(instance_presenter.site_contact_email).to eq "admin@example.com"
 | 
			
		||||
      expect(instance_presenter.contact.email).to eq "admin@example.com"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "contact_account" do
 | 
			
		||||
  describe '#account' do
 | 
			
		||||
    around do |example|
 | 
			
		||||
      site_contact_username = Setting.site_contact_username
 | 
			
		||||
      example.run
 | 
			
		||||
@ -55,12 +52,11 @@ describe InstancePresenter do
 | 
			
		||||
    it "returns the account for the site contact username" do
 | 
			
		||||
      Setting.site_contact_username = "aaa"
 | 
			
		||||
      account = Fabricate(:account, username: "aaa")
 | 
			
		||||
 | 
			
		||||
      expect(instance_presenter.contact_account).to eq(account)
 | 
			
		||||
      expect(instance_presenter.contact.account).to eq(account)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "user_count" do
 | 
			
		||||
  describe '#user_count' do
 | 
			
		||||
    it "returns the number of site users" do
 | 
			
		||||
      Rails.cache.write 'user_count', 123
 | 
			
		||||
 | 
			
		||||
@ -68,7 +64,7 @@ describe InstancePresenter do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "status_count" do
 | 
			
		||||
  describe '#status_count' do
 | 
			
		||||
    it "returns the number of local statuses" do
 | 
			
		||||
      Rails.cache.write 'local_status_count', 234
 | 
			
		||||
 | 
			
		||||
@ -76,7 +72,7 @@ describe InstancePresenter do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "domain_count" do
 | 
			
		||||
  describe '#domain_count' do
 | 
			
		||||
    it "returns the number of known domains" do
 | 
			
		||||
      Rails.cache.write 'distinct_domain_count', 345
 | 
			
		||||
 | 
			
		||||
@ -84,9 +80,9 @@ describe InstancePresenter do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#version_number' do
 | 
			
		||||
    it 'returns Mastodon::Version' do
 | 
			
		||||
      expect(instance_presenter.version_number).to be(Mastodon::Version)
 | 
			
		||||
  describe '#version' do
 | 
			
		||||
    it 'returns string' do
 | 
			
		||||
      expect(instance_presenter.version).to be_a String
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,25 +12,7 @@ describe 'about/show.html.haml', without_verify_partial_doubles: true do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'has valid open graph tags' do
 | 
			
		||||
    instance_presenter = double(
 | 
			
		||||
      :instance_presenter,
 | 
			
		||||
      site_title: 'something',
 | 
			
		||||
      site_short_description: 'something',
 | 
			
		||||
      site_description: 'something',
 | 
			
		||||
      version_number: '1.0',
 | 
			
		||||
      source_url: 'https://github.com/mastodon/mastodon',
 | 
			
		||||
      open_registrations: false,
 | 
			
		||||
      thumbnail: nil,
 | 
			
		||||
      hero: nil,
 | 
			
		||||
      mascot: nil,
 | 
			
		||||
      user_count: 420,
 | 
			
		||||
      status_count: 69,
 | 
			
		||||
      active_user_count: 420,
 | 
			
		||||
      contact_account: nil,
 | 
			
		||||
      sample_accounts: []
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    assign(:instance_presenter, instance_presenter)
 | 
			
		||||
    assign(:instance_presenter, InstancePresenter.new)
 | 
			
		||||
    render
 | 
			
		||||
 | 
			
		||||
    header_tags = view.content_for(:header_tags)
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user