Conflicts: - app/views/directories/index.html.haml Upstream has redesigned the profile directory, and we had a glitch-soc-specific change to hide follower counts. Ported that change to the new design.main
commit
9044a2b051
@ -0,0 +1,30 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::DirectoriesController < Api::BaseController
|
||||||
|
before_action :require_enabled!
|
||||||
|
before_action :set_accounts
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def require_enabled!
|
||||||
|
return not_found unless Setting.profile_directory
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_accounts
|
||||||
|
@accounts = accounts_scope.offset(params[:offset]).limit(limit_param(DEFAULT_ACCOUNTS_LIMIT))
|
||||||
|
end
|
||||||
|
|
||||||
|
def accounts_scope
|
||||||
|
Account.discoverable.tap do |scope|
|
||||||
|
scope.merge!(Account.local) if truthy_param?(:local)
|
||||||
|
scope.merge!(Account.by_recent_status) if params[:order].blank? || params[:order] == 'active'
|
||||||
|
scope.merge!(Account.order(id: :desc)) if params[:order] == 'new'
|
||||||
|
scope.merge!(Account.not_excluded_by_account(current_account)) if current_account
|
||||||
|
scope.merge!(Account.not_domain_blocked_by_account(current_account)) if current_account && !truthy_param?(:local)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,61 @@
|
|||||||
|
import api from '../api';
|
||||||
|
import { importFetchedAccounts } from './importer';
|
||||||
|
import { fetchRelationships } from './accounts';
|
||||||
|
|
||||||
|
export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST';
|
||||||
|
export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS';
|
||||||
|
export const DIRECTORY_FETCH_FAIL = 'DIRECTORY_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST';
|
||||||
|
export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS';
|
||||||
|
export const DIRECTORY_EXPAND_FAIL = 'DIRECTORY_EXPAND_FAIL';
|
||||||
|
|
||||||
|
export const fetchDirectory = params => (dispatch, getState) => {
|
||||||
|
dispatch(fetchDirectoryRequest());
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => {
|
||||||
|
dispatch(importFetchedAccounts(data));
|
||||||
|
dispatch(fetchDirectorySuccess(data));
|
||||||
|
dispatch(fetchRelationships(data.map(x => x.id)));
|
||||||
|
}).catch(error => dispatch(fetchDirectoryFail(error)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchDirectoryRequest = () => ({
|
||||||
|
type: DIRECTORY_FETCH_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchDirectorySuccess = accounts => ({
|
||||||
|
type: DIRECTORY_FETCH_SUCCESS,
|
||||||
|
accounts,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchDirectoryFail = error => ({
|
||||||
|
type: DIRECTORY_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandDirectory = params => (dispatch, getState) => {
|
||||||
|
dispatch(expandDirectoryRequest());
|
||||||
|
|
||||||
|
const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size;
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
|
||||||
|
dispatch(importFetchedAccounts(data));
|
||||||
|
dispatch(expandDirectorySuccess(data));
|
||||||
|
dispatch(fetchRelationships(data.map(x => x.id)));
|
||||||
|
}).catch(error => dispatch(expandDirectoryFail(error)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expandDirectoryRequest = () => ({
|
||||||
|
type: DIRECTORY_EXPAND_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandDirectorySuccess = accounts => ({
|
||||||
|
type: DIRECTORY_EXPAND_SUCCESS,
|
||||||
|
accounts,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandDirectoryFail = error => ({
|
||||||
|
type: DIRECTORY_EXPAND_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
export default class RadioButton extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
checked: PropTypes.bool,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
label: PropTypes.node.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { name, value, checked, onChange, label } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label className='radio-button'>
|
||||||
|
<input
|
||||||
|
name={name}
|
||||||
|
type='radio'
|
||||||
|
value={value}
|
||||||
|
checked={checked}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className={classNames('radio-button__input', { checked })} />
|
||||||
|
|
||||||
|
<span>{label}</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,149 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeGetAccount } from 'mastodon/selectors';
|
||||||
|
import Avatar from 'mastodon/components/avatar';
|
||||||
|
import DisplayName from 'mastodon/components/display_name';
|
||||||
|
import Permalink from 'mastodon/components/permalink';
|
||||||
|
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
|
||||||
|
import IconButton from 'mastodon/components/icon_button';
|
||||||
|
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||||
|
import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
|
||||||
|
import { shortNumberFormat } from 'mastodon/utils/numbers';
|
||||||
|
import { followAccount, unfollowAccount, blockAccount, unblockAccount, unmuteAccount } from 'mastodon/actions/accounts';
|
||||||
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
import { initMuteModal } from 'mastodon/actions/mutes';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
|
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||||
|
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||||
|
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { id }) => ({
|
||||||
|
account: getAccount(state, id),
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
|
||||||
|
onFollow (account) {
|
||||||
|
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
||||||
|
if (unfollowModal) {
|
||||||
|
dispatch(openModal('CONFIRM', {
|
||||||
|
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||||
|
confirm: intl.formatMessage(messages.unfollowConfirm),
|
||||||
|
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
dispatch(unfollowAccount(account.get('id')));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatch(followAccount(account.get('id')));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onBlock (account) {
|
||||||
|
if (account.getIn(['relationship', 'blocking'])) {
|
||||||
|
dispatch(unblockAccount(account.get('id')));
|
||||||
|
} else {
|
||||||
|
dispatch(blockAccount(account.get('id')));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onMute (account) {
|
||||||
|
if (account.getIn(['relationship', 'muting'])) {
|
||||||
|
dispatch(unmuteAccount(account.get('id')));
|
||||||
|
} else {
|
||||||
|
dispatch(initMuteModal(account));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
@connect(makeMapStateToProps, mapDispatchToProps)
|
||||||
|
class AccountCard extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
onFollow: PropTypes.func.isRequired,
|
||||||
|
onBlock: PropTypes.func.isRequired,
|
||||||
|
onMute: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleFollow = () => {
|
||||||
|
this.props.onFollow(this.props.account);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBlock = () => {
|
||||||
|
this.props.onBlock(this.props.account);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMute = () => {
|
||||||
|
this.props.onMute(this.props.account);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { account, intl } = this.props;
|
||||||
|
|
||||||
|
let buttons;
|
||||||
|
|
||||||
|
if (account.get('id') !== me && account.get('relationship', null) !== null) {
|
||||||
|
const following = account.getIn(['relationship', 'following']);
|
||||||
|
const requested = account.getIn(['relationship', 'requested']);
|
||||||
|
const blocking = account.getIn(['relationship', 'blocking']);
|
||||||
|
const muting = account.getIn(['relationship', 'muting']);
|
||||||
|
|
||||||
|
if (requested) {
|
||||||
|
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
|
||||||
|
} else if (blocking) {
|
||||||
|
buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
|
||||||
|
} else if (muting) {
|
||||||
|
buttons = <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />;
|
||||||
|
} else if (!account.get('moved') || following) {
|
||||||
|
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='directory__card'>
|
||||||
|
<div className='directory__card__img'>
|
||||||
|
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='directory__card__bar'>
|
||||||
|
<Permalink className='directory__card__bar__name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
|
||||||
|
<Avatar account={account} size={48} />
|
||||||
|
<DisplayName account={account} />
|
||||||
|
</Permalink>
|
||||||
|
|
||||||
|
<div className='directory__card__bar__relationship account__relationship'>
|
||||||
|
{buttons}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='directory__card__extra'>
|
||||||
|
<div className='account__header__content' dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='directory__card__extra'>
|
||||||
|
<div className='accounts-table__count'>{shortNumberFormat(account.get('statuses_count'))} <small><FormattedMessage id='account.posts' defaultMessage='Toots' /></small></div>
|
||||||
|
<div className='accounts-table__count'>{shortNumberFormat(account.get('followers_count'))} <small><FormattedMessage id='account.followers' defaultMessage='Followers' /></small></div>
|
||||||
|
<div className='accounts-table__count'>{account.get('last_status_at') === null ? <FormattedMessage id='account.never_active' defaultMessage='Never' /> : <RelativeTimestamp timestamp={account.get('last_status_at')} />} <small><FormattedMessage id='account.last_status' defaultMessage='Last active' /></small></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,171 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import Column from 'mastodon/components/column';
|
||||||
|
import ColumnHeader from 'mastodon/components/column_header';
|
||||||
|
import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'mastodon/actions/columns';
|
||||||
|
import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
import AccountCard from './components/account_card';
|
||||||
|
import RadioButton from 'mastodon/components/radio_button';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import LoadMore from 'mastodon/components/load_more';
|
||||||
|
import { ScrollContainer } from 'react-router-scroll-4';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
|
||||||
|
recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' },
|
||||||
|
newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' },
|
||||||
|
local: { id: 'directory.local', defaultMessage: 'From {domain} only' },
|
||||||
|
federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
accountIds: state.getIn(['user_lists', 'directory', 'items'], ImmutableList()),
|
||||||
|
isLoading: state.getIn(['user_lists', 'directory', 'isLoading'], true),
|
||||||
|
domain: state.getIn(['meta', 'domain']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
|
class Directory extends React.PureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
accountIds: ImmutablePropTypes.list.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
shouldUpdateScroll: PropTypes.func,
|
||||||
|
columnId: PropTypes.string,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
domain: PropTypes.string.isRequired,
|
||||||
|
params: PropTypes.shape({
|
||||||
|
order: PropTypes.string,
|
||||||
|
local: PropTypes.bool,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
order: null,
|
||||||
|
local: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
handlePin = () => {
|
||||||
|
const { columnId, dispatch } = this.props;
|
||||||
|
|
||||||
|
if (columnId) {
|
||||||
|
dispatch(removeColumn(columnId));
|
||||||
|
} else {
|
||||||
|
dispatch(addColumn('DIRECTORY', this.getParams(this.props, this.state)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getParams = (props, state) => ({
|
||||||
|
order: state.order === null ? (props.params.order || 'active') : state.order,
|
||||||
|
local: state.local === null ? (props.params.local || false) : state.local,
|
||||||
|
});
|
||||||
|
|
||||||
|
handleMove = dir => {
|
||||||
|
const { columnId, dispatch } = this.props;
|
||||||
|
dispatch(moveColumn(columnId, dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHeaderClick = () => {
|
||||||
|
this.column.scrollTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchDirectory(this.getParams(this.props, this.state)));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate (prevProps, prevState) {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
const paramsOld = this.getParams(prevProps, prevState);
|
||||||
|
const paramsNew = this.getParams(this.props, this.state);
|
||||||
|
|
||||||
|
if (paramsOld.order !== paramsNew.order || paramsOld.local !== paramsNew.local) {
|
||||||
|
dispatch(fetchDirectory(paramsNew));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.column = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChangeOrder = e => {
|
||||||
|
const { dispatch, columnId } = this.props;
|
||||||
|
|
||||||
|
if (columnId) {
|
||||||
|
dispatch(changeColumnParams(columnId, ['order'], e.target.value));
|
||||||
|
} else {
|
||||||
|
this.setState({ order: e.target.value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChangeLocal = e => {
|
||||||
|
const { dispatch, columnId } = this.props;
|
||||||
|
|
||||||
|
if (columnId) {
|
||||||
|
dispatch(changeColumnParams(columnId, ['local'], e.target.value === '1'));
|
||||||
|
} else {
|
||||||
|
this.setState({ local: e.target.value === '1' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMore = () => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(expandDirectory(this.getParams(this.props, this.state)));
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { isLoading, accountIds, intl, columnId, multiColumn, domain, shouldUpdateScroll } = this.props;
|
||||||
|
const { order, local } = this.getParams(this.props, this.state);
|
||||||
|
const pinned = !!columnId;
|
||||||
|
|
||||||
|
const scrollableArea = (
|
||||||
|
<div className='scrollable' style={{ background: 'transparent' }}>
|
||||||
|
<div className='filter-form'>
|
||||||
|
<div className='filter-form__column' role='group'>
|
||||||
|
<RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} />
|
||||||
|
<RadioButton name='order' value='new' label={intl.formatMessage(messages.newArrivals)} checked={order === 'new'} onChange={this.handleChangeOrder} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='filter-form__column' role='group'>
|
||||||
|
<RadioButton name='local' value='1' label={intl.formatMessage(messages.local, { domain })} checked={local} onChange={this.handleChangeLocal} />
|
||||||
|
<RadioButton name='local' value='0' label={intl.formatMessage(messages.federated)} checked={!local} onChange={this.handleChangeLocal} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classNames('directory__list', { loading: isLoading })}>
|
||||||
|
{accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LoadMore onClick={this.handleLoadMore} visible={!isLoading} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||||
|
<ColumnHeader
|
||||||
|
icon='address-book-o'
|
||||||
|
title={intl.formatMessage(messages.title)}
|
||||||
|
onPin={this.handlePin}
|
||||||
|
onMove={this.handleMove}
|
||||||
|
onClick={this.handleHeaderClick}
|
||||||
|
pinned={pinned}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{multiColumn && !pinned ? <ScrollContainer scrollKey='directory' shouldUpdateScroll={shouldUpdateScroll}>{scrollableArea}</ScrollContainer> : scrollableArea}
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('errors.400')
|
||||||
|
|
||||||
|
- content_for :content do
|
||||||
|
= t('errors.400')
|
@ -0,0 +1,5 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('errors.406')
|
||||||
|
|
||||||
|
- content_for :content do
|
||||||
|
= t('errors.406')
|
@ -0,0 +1,5 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('errors.503')
|
||||||
|
|
||||||
|
- content_for :content do
|
||||||
|
= t('errors.503')
|
Loading…
Reference in new issue