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