Add server banner to web app, add GET /api/v2/instance to REST API (#19294)
				
					
				
			This commit is contained in:
		| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user