Merge commit 'df6086d4027910fb160d531b4fe7ffdec26b0cd7' into glitch-soc/merge-upstream
This commit is contained in:
commit
e91ede5be6
|
@ -8,6 +8,7 @@ class Api::BaseController < ApplicationController
|
||||||
include Api::AccessTokenTrackingConcern
|
include Api::AccessTokenTrackingConcern
|
||||||
include Api::CachingConcern
|
include Api::CachingConcern
|
||||||
include Api::ContentSecurityPolicy
|
include Api::ContentSecurityPolicy
|
||||||
|
include Api::ErrorHandling
|
||||||
|
|
||||||
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
||||||
|
|
||||||
|
@ -18,51 +19,6 @@ class Api::BaseController < ApplicationController
|
||||||
|
|
||||||
protect_from_forgery with: :null_session
|
protect_from_forgery with: :null_session
|
||||||
|
|
||||||
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
|
|
||||||
render json: { error: e.to_s }, status: 422
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from ActiveRecord::RecordNotUnique do
|
|
||||||
render json: { error: 'Duplicate record' }, status: 422
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from Date::Error do
|
|
||||||
render json: { error: 'Invalid date supplied' }, status: 422
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from ActiveRecord::RecordNotFound do
|
|
||||||
render json: { error: 'Record not found' }, status: 404
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do
|
|
||||||
render json: { error: 'Remote data could not be fetched' }, status: 503
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from OpenSSL::SSL::SSLError do
|
|
||||||
render json: { error: 'Remote SSL certificate could not be verified' }, status: 503
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from Mastodon::NotPermittedError do
|
|
||||||
render json: { error: 'This action is not allowed' }, status: 403
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from Seahorse::Client::NetworkingError do |e|
|
|
||||||
Rails.logger.warn "Storage server error: #{e}"
|
|
||||||
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight do
|
|
||||||
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from Mastodon::RateLimitExceededError do
|
|
||||||
render json: { error: I18n.t('errors.429') }, status: 429
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from ActionController::ParameterMissing, Mastodon::InvalidParameterError do |e|
|
|
||||||
render json: { error: e.to_s }, status: 400
|
|
||||||
end
|
|
||||||
|
|
||||||
def doorkeeper_unauthorized_render_options(error: nil)
|
def doorkeeper_unauthorized_render_options(error: nil)
|
||||||
{ json: { error: error.try(:description) || 'Not authorized' } }
|
{ json: { error: error.try(:description) || 'Not authorized' } }
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api::ErrorHandling
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
|
||||||
|
render json: { error: e.to_s }, status: 422
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from ActiveRecord::RecordNotUnique do
|
||||||
|
render json: { error: 'Duplicate record' }, status: 422
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from Date::Error do
|
||||||
|
render json: { error: 'Invalid date supplied' }, status: 422
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from ActiveRecord::RecordNotFound do
|
||||||
|
render json: { error: 'Record not found' }, status: 404
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do
|
||||||
|
render json: { error: 'Remote data could not be fetched' }, status: 503
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from OpenSSL::SSL::SSLError do
|
||||||
|
render json: { error: 'Remote SSL certificate could not be verified' }, status: 503
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from Mastodon::NotPermittedError do
|
||||||
|
render json: { error: 'This action is not allowed' }, status: 403
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from Seahorse::Client::NetworkingError do |e|
|
||||||
|
Rails.logger.warn "Storage server error: #{e}"
|
||||||
|
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight do
|
||||||
|
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from Mastodon::RateLimitExceededError do
|
||||||
|
render json: { error: I18n.t('errors.429') }, status: 429
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from ActionController::ParameterMissing, Mastodon::InvalidParameterError do |e|
|
||||||
|
render json: { error: e.to_s }, status: 400
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -552,7 +552,10 @@ export const fetchNotificationsForRequest = accountId => (dispatch, getState) =>
|
||||||
|
|
||||||
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
|
||||||
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
||||||
|
dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
|
||||||
|
|
||||||
dispatch(fetchNotificationsForRequestSuccess(response.data, next?.uri));
|
dispatch(fetchNotificationsForRequestSuccess(response.data, next?.uri));
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
dispatch(fetchNotificationsForRequestFail(err));
|
dispatch(fetchNotificationsForRequestFail(err));
|
||||||
|
@ -585,7 +588,10 @@ export const expandNotificationsForRequest = () => (dispatch, getState) => {
|
||||||
|
|
||||||
api(getState).get(url).then(response => {
|
api(getState).get(url).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
|
||||||
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
||||||
|
dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
|
||||||
|
|
||||||
dispatch(expandNotificationsForRequestSuccess(response.data, next?.uri));
|
dispatch(expandNotificationsForRequestSuccess(response.data, next?.uri));
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
dispatch(expandNotificationsForRequestFail(err));
|
dispatch(expandNotificationsForRequestFail(err));
|
||||||
|
|
|
@ -199,7 +199,7 @@ class ColumnHeader extends PureComponent {
|
||||||
<h1 className={buttonClassName}>
|
<h1 className={buttonClassName}>
|
||||||
{hasTitle && (
|
{hasTitle && (
|
||||||
<>
|
<>
|
||||||
{backButton}
|
{showBackButton && backButton}
|
||||||
|
|
||||||
<button onClick={this.handleTitleClick} className='column-header__title'>
|
<button onClick={this.handleTitleClick} className='column-header__title'>
|
||||||
{!showBackButton && <Icon id={icon} icon={iconComponent} className='column-header__icon' />}
|
{!showBackButton && <Icon id={icon} icon={iconComponent} className='column-header__icon' />}
|
||||||
|
@ -208,7 +208,7 @@ class ColumnHeader extends PureComponent {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasTitle && backButton}
|
{!hasTitle && showBackButton && backButton}
|
||||||
|
|
||||||
<div className='column-header__buttons'>
|
<div className='column-header__buttons'>
|
||||||
{extraButton}
|
{extraButton}
|
||||||
|
|
|
@ -5,9 +5,7 @@ import { FormattedMessage, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?react';
|
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
import InlineAccount from 'mastodon/components/inline_account';
|
import InlineAccount from 'mastodon/components/inline_account';
|
||||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||||
|
|
||||||
|
@ -67,7 +65,7 @@ class EditedTimestamp extends PureComponent {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
|
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
|
||||||
<button className='dropdown-menu__text-button'>
|
<button className='dropdown-menu__text-button'>
|
||||||
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' icon={ArrowDropDownIcon} />
|
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: <span className='animated-number'>{intl.formatDate(timestamp, { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</span> }} />
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,7 +7,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
|
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
|
||||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
||||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
|
||||||
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
|
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { LoadMore } from 'mastodon/components/load_more';
|
import { LoadMore } from 'mastodon/components/load_more';
|
||||||
|
@ -76,11 +75,6 @@ class SearchResults extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='search-results'>
|
<div className='search-results'>
|
||||||
<div className='search-results__header'>
|
|
||||||
<Icon id='search' icon={SearchIcon} />
|
|
||||||
<FormattedMessage id='explore.search_results' defaultMessage='Search results' />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{accounts}
|
{accounts}
|
||||||
{hashtags}
|
{hashtags}
|
||||||
{statuses}
|
{statuses}
|
||||||
|
|
|
@ -88,7 +88,7 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
|
||||||
}
|
}
|
||||||
}, [dispatch, accountId]);
|
}, [dispatch, accountId]);
|
||||||
|
|
||||||
const columnTitle = intl.formatMessage(messages.title, { name: account?.get('display_name') });
|
const columnTitle = intl.formatMessage(messages.title, { name: account?.get('display_name') || account?.get('username') });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column bindToDocument={!multiColumn} ref={columnRef} label={columnTitle}>
|
<Column bindToDocument={!multiColumn} ref={columnRef} label={columnTitle}>
|
||||||
|
|
|
@ -9,8 +9,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
|
||||||
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
|
|
||||||
import { AnimatedNumber } from 'mastodon/components/animated_number';
|
import { AnimatedNumber } from 'mastodon/components/animated_number';
|
||||||
import EditedTimestamp from 'mastodon/components/edited_timestamp';
|
import EditedTimestamp from 'mastodon/components/edited_timestamp';
|
||||||
import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
|
import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
|
||||||
|
@ -143,10 +141,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
let media = '';
|
let media = '';
|
||||||
let applicationLink = '';
|
let applicationLink = '';
|
||||||
let reblogLink = '';
|
let reblogLink = '';
|
||||||
const reblogIcon = 'retweet';
|
|
||||||
const reblogIconComponent = RepeatIcon;
|
|
||||||
let favouriteLink = '';
|
let favouriteLink = '';
|
||||||
let edited = '';
|
|
||||||
|
|
||||||
if (this.props.measureHeight) {
|
if (this.props.measureHeight) {
|
||||||
outerStyle.height = `${this.state.height}px`;
|
outerStyle.height = `${this.state.height}px`;
|
||||||
|
@ -218,68 +213,53 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('application')) {
|
if (status.get('application')) {
|
||||||
applicationLink = <> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></>;
|
applicationLink = <>·<a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibilityLink = <> · <VisibilityIcon visibility={status.get('visibility')} /></>;
|
const visibilityLink = <>·<VisibilityIcon visibility={status.get('visibility')} /></>;
|
||||||
|
|
||||||
if (['private', 'direct'].includes(status.get('visibility'))) {
|
if (['private', 'direct'].includes(status.get('visibility'))) {
|
||||||
reblogLink = '';
|
reblogLink = '';
|
||||||
} else if (this.props.history) {
|
} else if (this.props.history) {
|
||||||
reblogLink = (
|
reblogLink = (
|
||||||
<>
|
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
||||||
{' · '}
|
<span className='detailed-status__reblogs'>
|
||||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
<AnimatedNumber value={status.get('reblogs_count')} />
|
||||||
<Icon id={reblogIcon} icon={reblogIconComponent} />
|
</span>
|
||||||
<span className='detailed-status__reblogs'>
|
<FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
|
||||||
<AnimatedNumber value={status.get('reblogs_count')} />
|
</Link>
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
reblogLink = (
|
reblogLink = (
|
||||||
<>
|
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||||
{' · '}
|
<span className='detailed-status__reblogs'>
|
||||||
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
|
<AnimatedNumber value={status.get('reblogs_count')} />
|
||||||
<Icon id={reblogIcon} icon={reblogIconComponent} />
|
</span>
|
||||||
<span className='detailed-status__reblogs'>
|
<FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
|
||||||
<AnimatedNumber value={status.get('reblogs_count')} />
|
</a>
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.history) {
|
if (this.props.history) {
|
||||||
favouriteLink = (
|
favouriteLink = (
|
||||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'>
|
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'>
|
||||||
<Icon id='star' icon={StarIcon} />
|
|
||||||
<span className='detailed-status__favorites'>
|
<span className='detailed-status__favorites'>
|
||||||
<AnimatedNumber value={status.get('favourites_count')} />
|
<AnimatedNumber value={status.get('favourites_count')} />
|
||||||
</span>
|
</span>
|
||||||
|
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
favouriteLink = (
|
favouriteLink = (
|
||||||
<a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
|
<a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||||
<Icon id='star' icon={StarIcon} />
|
|
||||||
<span className='detailed-status__favorites'>
|
<span className='detailed-status__favorites'>
|
||||||
<AnimatedNumber value={status.get('favourites_count')} />
|
<AnimatedNumber value={status.get('favourites_count')} />
|
||||||
</span>
|
</span>
|
||||||
|
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('edited_at')) {
|
|
||||||
edited = (
|
|
||||||
<>
|
|
||||||
{' · '}
|
|
||||||
<EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||||
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
|
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
|
||||||
|
|
||||||
|
@ -310,9 +290,23 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
{expanded && hashtagBar}
|
{expanded && hashtagBar}
|
||||||
|
|
||||||
<div className='detailed-status__meta'>
|
<div className='detailed-status__meta'>
|
||||||
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
|
<div className='detailed-status__meta__line'>
|
||||||
<FormattedDate value={new Date(status.get('created_at'))} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
|
||||||
</a>{edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
|
<FormattedDate value={new Date(status.get('created_at'))} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{visibilityLink}
|
||||||
|
|
||||||
|
{applicationLink}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status.get('edited_at') && <div className='detailed-status__meta__line'><EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} /></div>}
|
||||||
|
|
||||||
|
<div className='detailed-status__meta__line'>
|
||||||
|
{reblogLink}
|
||||||
|
·
|
||||||
|
{favouriteLink}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -272,6 +272,7 @@
|
||||||
"filter_modal.select_filter.subtitle": "Usa una categoria existent o crea'n una de nova",
|
"filter_modal.select_filter.subtitle": "Usa una categoria existent o crea'n una de nova",
|
||||||
"filter_modal.select_filter.title": "Filtra aquest tut",
|
"filter_modal.select_filter.title": "Filtra aquest tut",
|
||||||
"filter_modal.title.status": "Filtra un tut",
|
"filter_modal.title.status": "Filtra un tut",
|
||||||
|
"filtered_notifications_banner.pending_requests": "Notificacions {count, plural, =0 {de ningú} one {d'una persona} other {de # persones}} que potser coneixes",
|
||||||
"filtered_notifications_banner.title": "Notificacions filtrades",
|
"filtered_notifications_banner.title": "Notificacions filtrades",
|
||||||
"firehose.all": "Tots",
|
"firehose.all": "Tots",
|
||||||
"firehose.local": "Aquest servidor",
|
"firehose.local": "Aquest servidor",
|
||||||
|
@ -476,13 +477,13 @@
|
||||||
"notifications.permission_denied": "Les notificacions d’escriptori no estan disponibles perquè prèviament s’ha denegat el permís al navegador",
|
"notifications.permission_denied": "Les notificacions d’escriptori no estan disponibles perquè prèviament s’ha denegat el permís al navegador",
|
||||||
"notifications.permission_denied_alert": "No es poden activar les notificacions de l'escriptori perquè abans s'ha denegat el permís del navegador",
|
"notifications.permission_denied_alert": "No es poden activar les notificacions de l'escriptori perquè abans s'ha denegat el permís del navegador",
|
||||||
"notifications.permission_required": "Les notificacions d'escriptori no estan disponibles perquè el permís requerit no ha estat concedit.",
|
"notifications.permission_required": "Les notificacions d'escriptori no estan disponibles perquè el permís requerit no ha estat concedit.",
|
||||||
"notifications.policy.filter_new_accounts.hint": "Creat durant els passats {days, plural, one {un dia} other {# dies}}",
|
"notifications.policy.filter_new_accounts.hint": "Creat {days, plural, one {ahir} other {durant els # dies passats}}",
|
||||||
"notifications.policy.filter_new_accounts_title": "Comptes nous",
|
"notifications.policy.filter_new_accounts_title": "Comptes nous",
|
||||||
"notifications.policy.filter_not_followers_hint": "Incloent les persones que us segueixen fa menys de {days, plural, one {un dia} other {# dies}}",
|
"notifications.policy.filter_not_followers_hint": "Incloent les persones que us segueixen fa menys {days, plural, one {d'un dia} other {de # dies}}",
|
||||||
"notifications.policy.filter_not_followers_title": "Persones que no us segueixen",
|
"notifications.policy.filter_not_followers_title": "Persones que no us segueixen",
|
||||||
"notifications.policy.filter_not_following_hint": "Fins que no ho aproveu de forma manual",
|
"notifications.policy.filter_not_following_hint": "Fins que no ho aproveu de forma manual",
|
||||||
"notifications.policy.filter_not_following_title": "Persones que no seguiu",
|
"notifications.policy.filter_not_following_title": "Persones que no seguiu",
|
||||||
"notifications.policy.filter_private_mentions_hint": "Filtra-ho excepte si és en resposta a una menció vostra o si seguiu el remitent",
|
"notifications.policy.filter_private_mentions_hint": "Filtrat si no és que és en resposta a una menció vostra o si seguiu el remitent",
|
||||||
"notifications.policy.filter_private_mentions_title": "Mencions privades no sol·licitades",
|
"notifications.policy.filter_private_mentions_title": "Mencions privades no sol·licitades",
|
||||||
"notifications.policy.title": "Filtra les notificacions de…",
|
"notifications.policy.title": "Filtra les notificacions de…",
|
||||||
"notifications_permission_banner.enable": "Activa les notificacions d’escriptori",
|
"notifications_permission_banner.enable": "Activa les notificacions d’escriptori",
|
||||||
|
|
|
@ -662,10 +662,11 @@
|
||||||
"status.direct": "Privately mention @{name}",
|
"status.direct": "Privately mention @{name}",
|
||||||
"status.direct_indicator": "Private mention",
|
"status.direct_indicator": "Private mention",
|
||||||
"status.edit": "Edit",
|
"status.edit": "Edit",
|
||||||
"status.edited": "Edited {date}",
|
"status.edited": "Last edited {date}",
|
||||||
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
|
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
|
||||||
"status.embed": "Embed",
|
"status.embed": "Embed",
|
||||||
"status.favourite": "Favorite",
|
"status.favourite": "Favorite",
|
||||||
|
"status.favourites": "{count, plural, one {favorite} other {favorites}}",
|
||||||
"status.filter": "Filter this post",
|
"status.filter": "Filter this post",
|
||||||
"status.filtered": "Filtered",
|
"status.filtered": "Filtered",
|
||||||
"status.hide": "Hide post",
|
"status.hide": "Hide post",
|
||||||
|
@ -686,6 +687,7 @@
|
||||||
"status.reblog": "Boost",
|
"status.reblog": "Boost",
|
||||||
"status.reblog_private": "Boost with original visibility",
|
"status.reblog_private": "Boost with original visibility",
|
||||||
"status.reblogged_by": "{name} boosted",
|
"status.reblogged_by": "{name} boosted",
|
||||||
|
"status.reblogs": "{count, plural, one {boost} other {boosts}}",
|
||||||
"status.reblogs.empty": "No one has boosted this post yet. When someone does, they will show up here.",
|
"status.reblogs.empty": "No one has boosted this post yet. When someone does, they will show up here.",
|
||||||
"status.redraft": "Delete & re-draft",
|
"status.redraft": "Delete & re-draft",
|
||||||
"status.remove_bookmark": "Remove bookmark",
|
"status.remove_bookmark": "Remove bookmark",
|
||||||
|
|
|
@ -241,6 +241,7 @@
|
||||||
"empty_column.list": "Tällä listalla ei ole vielä mitään. Kun tämän listan jäsenet lähettävät uusia julkaisuja, ne näkyvät tässä.",
|
"empty_column.list": "Tällä listalla ei ole vielä mitään. Kun tämän listan jäsenet lähettävät uusia julkaisuja, ne näkyvät tässä.",
|
||||||
"empty_column.lists": "Sinulla ei ole vielä yhtään listaa. Kun luot sellaisen, näkyy se tässä.",
|
"empty_column.lists": "Sinulla ei ole vielä yhtään listaa. Kun luot sellaisen, näkyy se tässä.",
|
||||||
"empty_column.mutes": "Et ole mykistänyt vielä yhtään käyttäjää.",
|
"empty_column.mutes": "Et ole mykistänyt vielä yhtään käyttäjää.",
|
||||||
|
"empty_column.notification_requests": "Kaikki kunnossa! Täällä ei ole mitään. Kun saat uusia ilmoituksia, ne näkyvät täällä asetustesi mukaisesti.",
|
||||||
"empty_column.notifications": "Sinulla ei ole vielä ilmoituksia. Kun keskustelet muille, näet sen täällä.",
|
"empty_column.notifications": "Sinulla ei ole vielä ilmoituksia. Kun keskustelet muille, näet sen täällä.",
|
||||||
"empty_column.public": "Täällä ei ole mitään! Kirjoita jotain julkisesti. Voit myös seurata muiden palvelimien käyttäjiä",
|
"empty_column.public": "Täällä ei ole mitään! Kirjoita jotain julkisesti. Voit myös seurata muiden palvelimien käyttäjiä",
|
||||||
"error.unexpected_crash.explanation": "Sivua ei voida näyttää oikein ohjelmointivirheen tai selaimen yhteensopivuusvajeen vuoksi.",
|
"error.unexpected_crash.explanation": "Sivua ei voida näyttää oikein ohjelmointivirheen tai selaimen yhteensopivuusvajeen vuoksi.",
|
||||||
|
@ -271,6 +272,7 @@
|
||||||
"filter_modal.select_filter.subtitle": "Käytä olemassa olevaa luokkaa tai luo uusi",
|
"filter_modal.select_filter.subtitle": "Käytä olemassa olevaa luokkaa tai luo uusi",
|
||||||
"filter_modal.select_filter.title": "Suodata tämä julkaisu",
|
"filter_modal.select_filter.title": "Suodata tämä julkaisu",
|
||||||
"filter_modal.title.status": "Suodata julkaisu",
|
"filter_modal.title.status": "Suodata julkaisu",
|
||||||
|
"filtered_notifications_banner.pending_requests": "Ilmoitukset, {count, plural, =0 {ei tänään} one {1 henkilö} other {# henkilöä}}",
|
||||||
"filtered_notifications_banner.title": "Suodatetut ilmoitukset",
|
"filtered_notifications_banner.title": "Suodatetut ilmoitukset",
|
||||||
"firehose.all": "Kaikki",
|
"firehose.all": "Kaikki",
|
||||||
"firehose.local": "Tämä palvelin",
|
"firehose.local": "Tämä palvelin",
|
||||||
|
@ -477,8 +479,11 @@
|
||||||
"notifications.permission_required": "Työpöytäilmoitukset eivät ole käytettävissä, koska siihen tarvittavaa lupaa ei ole myönnetty.",
|
"notifications.permission_required": "Työpöytäilmoitukset eivät ole käytettävissä, koska siihen tarvittavaa lupaa ei ole myönnetty.",
|
||||||
"notifications.policy.filter_new_accounts.hint": "Luotu {days, plural, one {viime päivänä} other {viimeisenä # päivänä}}",
|
"notifications.policy.filter_new_accounts.hint": "Luotu {days, plural, one {viime päivänä} other {viimeisenä # päivänä}}",
|
||||||
"notifications.policy.filter_new_accounts_title": "Uudet tilit",
|
"notifications.policy.filter_new_accounts_title": "Uudet tilit",
|
||||||
|
"notifications.policy.filter_not_followers_hint": "Mukaan lukien ne, jotka ovat seuranneet sinua vähemmän kuin {days, plural, one {päivän} other {# päivää}}",
|
||||||
"notifications.policy.filter_not_followers_title": "Henkilöt, jotka eivät seuraa sinua",
|
"notifications.policy.filter_not_followers_title": "Henkilöt, jotka eivät seuraa sinua",
|
||||||
|
"notifications.policy.filter_not_following_hint": "Kunnes hyväksyt ne manuaalisesti",
|
||||||
"notifications.policy.filter_not_following_title": "Henkilöt, joita et seuraa",
|
"notifications.policy.filter_not_following_title": "Henkilöt, joita et seuraa",
|
||||||
|
"notifications.policy.filter_private_mentions_hint": "Suodatetaan, ellei se vastaa omaan mainintaan tai jos seuraat lähettäjää",
|
||||||
"notifications.policy.filter_private_mentions_title": "Ei-toivotut yksityismaininnat",
|
"notifications.policy.filter_private_mentions_title": "Ei-toivotut yksityismaininnat",
|
||||||
"notifications.policy.title": "Suodata ilmoitukset pois kohteesta…",
|
"notifications.policy.title": "Suodata ilmoitukset pois kohteesta…",
|
||||||
"notifications_permission_banner.enable": "Ota työpöytäilmoitukset käyttöön",
|
"notifications_permission_banner.enable": "Ota työpöytäilmoitukset käyttöön",
|
||||||
|
|
|
@ -272,6 +272,7 @@
|
||||||
"filter_modal.select_filter.subtitle": "שימוש בקטגורייה קיימת או יצירת אחת חדשה",
|
"filter_modal.select_filter.subtitle": "שימוש בקטגורייה קיימת או יצירת אחת חדשה",
|
||||||
"filter_modal.select_filter.title": "סינון ההודעה הזו",
|
"filter_modal.select_filter.title": "סינון ההודעה הזו",
|
||||||
"filter_modal.title.status": "סנן הודעה",
|
"filter_modal.title.status": "סנן הודעה",
|
||||||
|
"filtered_notifications_banner.pending_requests": "{count, plural,=0 {אין התראות ממשתמשים ה}one {התראה אחת ממישהו/מישהי ה}two {יש התראותיים ממשתמשים }other {יש # התראות ממשתמשים }}מוכרים לך",
|
||||||
"filtered_notifications_banner.title": "התראות מסוננות",
|
"filtered_notifications_banner.title": "התראות מסוננות",
|
||||||
"firehose.all": "הכל",
|
"firehose.all": "הכל",
|
||||||
"firehose.local": "שרת זה",
|
"firehose.local": "שרת זה",
|
||||||
|
|
|
@ -320,7 +320,7 @@
|
||||||
"mute_modal.hide_notifications": "Tebɣiḍ ad teffreḍ talɣutin n umseqdac-a?",
|
"mute_modal.hide_notifications": "Tebɣiḍ ad teffreḍ talɣutin n umseqdac-a?",
|
||||||
"mute_modal.indefinite": "Ur yettwasbadu ara",
|
"mute_modal.indefinite": "Ur yettwasbadu ara",
|
||||||
"navigation_bar.about": "Ɣef",
|
"navigation_bar.about": "Ɣef",
|
||||||
"navigation_bar.blocks": "Imseqdacen yettusḥebsen",
|
"navigation_bar.blocks": "Iseqdacen yettusḥebsen",
|
||||||
"navigation_bar.bookmarks": "Ticraḍ",
|
"navigation_bar.bookmarks": "Ticraḍ",
|
||||||
"navigation_bar.community_timeline": "Tasuddemt tadigant",
|
"navigation_bar.community_timeline": "Tasuddemt tadigant",
|
||||||
"navigation_bar.compose": "Aru tajewwiqt tamaynut",
|
"navigation_bar.compose": "Aru tajewwiqt tamaynut",
|
||||||
|
|
|
@ -241,6 +241,7 @@
|
||||||
"empty_column.list": "Det er ingenting i denne lista enno. Når medlemer av denne lista legg ut nye statusar, så dukkar dei opp her.",
|
"empty_column.list": "Det er ingenting i denne lista enno. Når medlemer av denne lista legg ut nye statusar, så dukkar dei opp her.",
|
||||||
"empty_column.lists": "Du har ingen lister enno. Når du lagar ei, så dukkar ho opp her.",
|
"empty_column.lists": "Du har ingen lister enno. Når du lagar ei, så dukkar ho opp her.",
|
||||||
"empty_column.mutes": "Du har ikkje målbunde nokon enno.",
|
"empty_column.mutes": "Du har ikkje målbunde nokon enno.",
|
||||||
|
"empty_column.notification_requests": "Ferdig! Her er det ingenting. Når du får nye varsel, kjem dei opp her slik du har valt.",
|
||||||
"empty_column.notifications": "Du har ingen varsel enno. Kommuniser med andre for å starte samtalen.",
|
"empty_column.notifications": "Du har ingen varsel enno. Kommuniser med andre for å starte samtalen.",
|
||||||
"empty_column.public": "Det er ingenting her! Skriv noko offentleg, eller følg brukarar frå andre tenarar manuelt for å fylle det opp",
|
"empty_column.public": "Det er ingenting her! Skriv noko offentleg, eller følg brukarar frå andre tenarar manuelt for å fylle det opp",
|
||||||
"error.unexpected_crash.explanation": "På grunn av eit nettlesarkompatibilitetsproblem eller ein feil i koden vår, kunne ikkje denne sida bli vist slik den skal.",
|
"error.unexpected_crash.explanation": "På grunn av eit nettlesarkompatibilitetsproblem eller ein feil i koden vår, kunne ikkje denne sida bli vist slik den skal.",
|
||||||
|
@ -271,6 +272,8 @@
|
||||||
"filter_modal.select_filter.subtitle": "Bruk ein eksisterande kategori eller opprett ein ny",
|
"filter_modal.select_filter.subtitle": "Bruk ein eksisterande kategori eller opprett ein ny",
|
||||||
"filter_modal.select_filter.title": "Filtrer dette innlegget",
|
"filter_modal.select_filter.title": "Filtrer dette innlegget",
|
||||||
"filter_modal.title.status": "Filtrer eit innlegg",
|
"filter_modal.title.status": "Filtrer eit innlegg",
|
||||||
|
"filtered_notifications_banner.pending_requests": "Varsel frå {count, plural, =0 {ingen} one {ein person} other {# folk}} du kanskje kjenner",
|
||||||
|
"filtered_notifications_banner.title": "Filtrerte varslingar",
|
||||||
"firehose.all": "Alle",
|
"firehose.all": "Alle",
|
||||||
"firehose.local": "Denne tenaren",
|
"firehose.local": "Denne tenaren",
|
||||||
"firehose.remote": "Andre tenarar",
|
"firehose.remote": "Andre tenarar",
|
||||||
|
@ -439,6 +442,10 @@
|
||||||
"notification.reblog": "{name} framheva innlegget ditt",
|
"notification.reblog": "{name} framheva innlegget ditt",
|
||||||
"notification.status": "{name} la nettopp ut",
|
"notification.status": "{name} la nettopp ut",
|
||||||
"notification.update": "{name} redigerte eit innlegg",
|
"notification.update": "{name} redigerte eit innlegg",
|
||||||
|
"notification_requests.accept": "Godkjenn",
|
||||||
|
"notification_requests.dismiss": "Avvis",
|
||||||
|
"notification_requests.notifications_from": "Varslingar frå {name}",
|
||||||
|
"notification_requests.title": "Filtrerte varslingar",
|
||||||
"notifications.clear": "Tøm varsel",
|
"notifications.clear": "Tøm varsel",
|
||||||
"notifications.clear_confirmation": "Er du sikker på at du vil fjerna alle varsla dine for alltid?",
|
"notifications.clear_confirmation": "Er du sikker på at du vil fjerna alle varsla dine for alltid?",
|
||||||
"notifications.column_settings.admin.report": "Nye rapportar:",
|
"notifications.column_settings.admin.report": "Nye rapportar:",
|
||||||
|
@ -470,6 +477,15 @@
|
||||||
"notifications.permission_denied": "Skrivebordsvarsel er ikkje tilgjengelege på grunn av at nettlesaren tidlegare ikkje har fått naudsynte rettar til å vise dei",
|
"notifications.permission_denied": "Skrivebordsvarsel er ikkje tilgjengelege på grunn av at nettlesaren tidlegare ikkje har fått naudsynte rettar til å vise dei",
|
||||||
"notifications.permission_denied_alert": "Sidan nettlesaren tidlegare har blitt nekta naudsynte rettar, kan ikkje skrivebordsvarsel aktiverast",
|
"notifications.permission_denied_alert": "Sidan nettlesaren tidlegare har blitt nekta naudsynte rettar, kan ikkje skrivebordsvarsel aktiverast",
|
||||||
"notifications.permission_required": "Skrivebordsvarsel er utilgjengelege fordi naudsynte rettar ikkje er gitt.",
|
"notifications.permission_required": "Skrivebordsvarsel er utilgjengelege fordi naudsynte rettar ikkje er gitt.",
|
||||||
|
"notifications.policy.filter_new_accounts.hint": "Skrive siste {days, plural, one {dag} other {# dagar}}",
|
||||||
|
"notifications.policy.filter_new_accounts_title": "Nye brukarkontoar",
|
||||||
|
"notifications.policy.filter_not_followers_hint": "Inkludert folk som har fylgt deg mindre enn {days, plural, one {ein dag} other {# dagar}}",
|
||||||
|
"notifications.policy.filter_not_followers_title": "Folk som ikkje fylgjer deg",
|
||||||
|
"notifications.policy.filter_not_following_hint": "Til du godkjenner dei manuelt",
|
||||||
|
"notifications.policy.filter_not_following_title": "Folk du ikkje fylgjer",
|
||||||
|
"notifications.policy.filter_private_mentions_hint": "Filtrert viss det ikkje er eit svar på dine eigne nemningar eller viss du fylgjer avsendaren",
|
||||||
|
"notifications.policy.filter_private_mentions_title": "Masseutsende private nemningar",
|
||||||
|
"notifications.policy.title": "Filtrer ut varslingar frå…",
|
||||||
"notifications_permission_banner.enable": "Skru på skrivebordsvarsel",
|
"notifications_permission_banner.enable": "Skru på skrivebordsvarsel",
|
||||||
"notifications_permission_banner.how_to_control": "Aktiver skrivebordsvarsel for å få varsel når Mastodon ikkje er open. Du kan nøye bestemme kva samhandlingar som skal føre til skrivebordsvarsel gjennom {icon}-knappen ovanfor etter at varsel er aktivert.",
|
"notifications_permission_banner.how_to_control": "Aktiver skrivebordsvarsel for å få varsel når Mastodon ikkje er open. Du kan nøye bestemme kva samhandlingar som skal føre til skrivebordsvarsel gjennom {icon}-knappen ovanfor etter at varsel er aktivert.",
|
||||||
"notifications_permission_banner.title": "Gå aldri glipp av noko",
|
"notifications_permission_banner.title": "Gå aldri glipp av noko",
|
||||||
|
|
|
@ -272,6 +272,7 @@
|
||||||
"filter_modal.select_filter.subtitle": "Utilize uma categoria existente ou crie uma nova",
|
"filter_modal.select_filter.subtitle": "Utilize uma categoria existente ou crie uma nova",
|
||||||
"filter_modal.select_filter.title": "Filtrar esta publicação",
|
"filter_modal.select_filter.title": "Filtrar esta publicação",
|
||||||
"filter_modal.title.status": "Filtrar uma publicação",
|
"filter_modal.title.status": "Filtrar uma publicação",
|
||||||
|
"filtered_notifications_banner.pending_requests": "Notificações de {count, plural, =0 {ninguém} one {uma pessoa} other {# pessoas}} que talvez conheça",
|
||||||
"filtered_notifications_banner.title": "Notificações filtradas",
|
"filtered_notifications_banner.title": "Notificações filtradas",
|
||||||
"firehose.all": "Todas",
|
"firehose.all": "Todas",
|
||||||
"firehose.local": "Este servidor",
|
"firehose.local": "Este servidor",
|
||||||
|
@ -476,7 +477,9 @@
|
||||||
"notifications.permission_denied": "Notificações no ambiente de trabalho não estão disponíveis porque a permissão, solicitada pelo navegador, foi recusada anteriormente",
|
"notifications.permission_denied": "Notificações no ambiente de trabalho não estão disponíveis porque a permissão, solicitada pelo navegador, foi recusada anteriormente",
|
||||||
"notifications.permission_denied_alert": "Notificações no ambiente de trabalho não podem ser ativadas, pois a permissão do navegador foi recusada anteriormente",
|
"notifications.permission_denied_alert": "Notificações no ambiente de trabalho não podem ser ativadas, pois a permissão do navegador foi recusada anteriormente",
|
||||||
"notifications.permission_required": "Notificações no ambiente de trabalho não estão disponíveis porque a permissão necessária não foi concedida.",
|
"notifications.permission_required": "Notificações no ambiente de trabalho não estão disponíveis porque a permissão necessária não foi concedida.",
|
||||||
|
"notifications.policy.filter_new_accounts.hint": "Criada nos últimos {days, plural, one {um dia} other {# dias}}",
|
||||||
"notifications.policy.filter_new_accounts_title": "Novas contas",
|
"notifications.policy.filter_new_accounts_title": "Novas contas",
|
||||||
|
"notifications.policy.filter_not_followers_hint": "Incluindo pessoas que o seguem há menos de {days, plural, one {um dia} other {# dias}}",
|
||||||
"notifications.policy.filter_not_followers_title": "Pessoas não te seguem",
|
"notifications.policy.filter_not_followers_title": "Pessoas não te seguem",
|
||||||
"notifications.policy.filter_not_following_hint": "Até que você os aprove manualmente",
|
"notifications.policy.filter_not_following_hint": "Até que você os aprove manualmente",
|
||||||
"notifications.policy.filter_not_following_title": "Pessoas que você não segue",
|
"notifications.policy.filter_not_following_title": "Pessoas que você não segue",
|
||||||
|
|
|
@ -241,6 +241,7 @@
|
||||||
"empty_column.list": "ยังไม่มีสิ่งใดในรายการนี้ เมื่อสมาชิกของรายการนี้โพสต์โพสต์ใหม่ โพสต์จะปรากฏที่นี่",
|
"empty_column.list": "ยังไม่มีสิ่งใดในรายการนี้ เมื่อสมาชิกของรายการนี้โพสต์โพสต์ใหม่ โพสต์จะปรากฏที่นี่",
|
||||||
"empty_column.lists": "คุณยังไม่มีรายการใด ๆ เมื่อคุณสร้างรายการ รายการจะปรากฏที่นี่",
|
"empty_column.lists": "คุณยังไม่มีรายการใด ๆ เมื่อคุณสร้างรายการ รายการจะปรากฏที่นี่",
|
||||||
"empty_column.mutes": "คุณยังไม่ได้ซ่อนผู้ใช้ใด ๆ",
|
"empty_column.mutes": "คุณยังไม่ได้ซ่อนผู้ใช้ใด ๆ",
|
||||||
|
"empty_column.notification_requests": "โล่งทั้งหมด! ไม่มีสิ่งใดที่นี่ เมื่อคุณได้รับการแจ้งเตือนใหม่ การแจ้งเตือนจะปรากฏที่นี่ตามการตั้งค่าของคุณ",
|
||||||
"empty_column.notifications": "คุณยังไม่มีการแจ้งเตือนใด ๆ เมื่อผู้คนอื่น ๆ โต้ตอบกับคุณ คุณจะเห็นการแจ้งเตือนที่นี่",
|
"empty_column.notifications": "คุณยังไม่มีการแจ้งเตือนใด ๆ เมื่อผู้คนอื่น ๆ โต้ตอบกับคุณ คุณจะเห็นการแจ้งเตือนที่นี่",
|
||||||
"empty_column.public": "ไม่มีสิ่งใดที่นี่! เขียนบางอย่างเป็นสาธารณะ หรือติดตามผู้ใช้จากเซิร์ฟเวอร์อื่น ๆ ด้วยตนเองเพื่อเติมเส้นเวลาให้เต็ม",
|
"empty_column.public": "ไม่มีสิ่งใดที่นี่! เขียนบางอย่างเป็นสาธารณะ หรือติดตามผู้ใช้จากเซิร์ฟเวอร์อื่น ๆ ด้วยตนเองเพื่อเติมเส้นเวลาให้เต็ม",
|
||||||
"error.unexpected_crash.explanation": "เนื่องจากข้อบกพร่องในโค้ดของเราหรือปัญหาความเข้ากันได้ของเบราว์เซอร์ จึงไม่สามารถแสดงหน้านี้ได้อย่างถูกต้อง",
|
"error.unexpected_crash.explanation": "เนื่องจากข้อบกพร่องในโค้ดของเราหรือปัญหาความเข้ากันได้ของเบราว์เซอร์ จึงไม่สามารถแสดงหน้านี้ได้อย่างถูกต้อง",
|
||||||
|
|
|
@ -272,7 +272,7 @@
|
||||||
"filter_modal.select_filter.subtitle": "Sử dụng một danh mục hiện có hoặc tạo một danh mục mới",
|
"filter_modal.select_filter.subtitle": "Sử dụng một danh mục hiện có hoặc tạo một danh mục mới",
|
||||||
"filter_modal.select_filter.title": "Lọc tút này",
|
"filter_modal.select_filter.title": "Lọc tút này",
|
||||||
"filter_modal.title.status": "Lọc một tút",
|
"filter_modal.title.status": "Lọc một tút",
|
||||||
"filtered_notifications_banner.pending_requests": "{count, plural, =0 {} other {#}}",
|
"filtered_notifications_banner.pending_requests": "Thông báo từ {count, plural, =0 {không ai} other {# người}} bạn có thể biết",
|
||||||
"filtered_notifications_banner.title": "Thông báo đã lọc",
|
"filtered_notifications_banner.title": "Thông báo đã lọc",
|
||||||
"firehose.all": "Toàn bộ",
|
"firehose.all": "Toàn bộ",
|
||||||
"firehose.local": "Máy chủ này",
|
"firehose.local": "Máy chủ này",
|
||||||
|
@ -364,7 +364,7 @@
|
||||||
"keyboard_shortcuts.my_profile": "mở hồ sơ của bạn",
|
"keyboard_shortcuts.my_profile": "mở hồ sơ của bạn",
|
||||||
"keyboard_shortcuts.notifications": "mở thông báo",
|
"keyboard_shortcuts.notifications": "mở thông báo",
|
||||||
"keyboard_shortcuts.open_media": "mở ảnh hoặc video",
|
"keyboard_shortcuts.open_media": "mở ảnh hoặc video",
|
||||||
"keyboard_shortcuts.pinned": "Open pinned posts list",
|
"keyboard_shortcuts.pinned": "mở những tút đã ghim",
|
||||||
"keyboard_shortcuts.profile": "mở trang của người đăng tút",
|
"keyboard_shortcuts.profile": "mở trang của người đăng tút",
|
||||||
"keyboard_shortcuts.reply": "trả lời",
|
"keyboard_shortcuts.reply": "trả lời",
|
||||||
"keyboard_shortcuts.requests": "mở danh sách yêu cầu theo dõi",
|
"keyboard_shortcuts.requests": "mở danh sách yêu cầu theo dõi",
|
||||||
|
|
|
@ -1659,15 +1659,35 @@ body > [data-popper-placement] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.detailed-status__meta {
|
.detailed-status__meta {
|
||||||
margin-top: 16px;
|
margin-top: 24px;
|
||||||
color: $dark-text-color;
|
color: $dark-text-color;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
|
|
||||||
|
&__line {
|
||||||
|
border-bottom: 1px solid var(--background-border-color);
|
||||||
|
padding: 8px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
width: 15px;
|
width: 18px;
|
||||||
height: 15px;
|
height: 18px;
|
||||||
vertical-align: middle;
|
}
|
||||||
|
|
||||||
|
.animated-number {
|
||||||
|
color: $secondary-text-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1711,19 +1731,6 @@ body > [data-popper-placement] {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
position: relative;
|
|
||||||
top: 0.145em;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.detailed-status__favorites,
|
|
||||||
.detailed-status__reblogs {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.domain {
|
.domain {
|
||||||
|
@ -2292,6 +2299,10 @@ a.account__display-name {
|
||||||
outline: 1px dotted;
|
outline: 1px dotted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
|
@ -3485,7 +3496,7 @@ $ui-header-height: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-subheading {
|
.column-subheading {
|
||||||
background: darken($ui-base-color, 4%);
|
background: var(--surface-background-color);
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
padding: 8px 20px;
|
padding: 8px 20px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
@ -4637,7 +4648,7 @@ a.status-card {
|
||||||
}
|
}
|
||||||
|
|
||||||
.follow_requests-unlocked_explanation {
|
.follow_requests-unlocked_explanation {
|
||||||
background: darken($ui-base-color, 4%);
|
background: var(--surface-background-color);
|
||||||
border-bottom: 1px solid var(--background-border-color);
|
border-bottom: 1px solid var(--background-border-color);
|
||||||
contain: initial;
|
contain: initial;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
@ -5269,18 +5280,6 @@ a.status-card {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-results__header {
|
|
||||||
color: $dark-text-color;
|
|
||||||
background: lighten($ui-base-color, 2%);
|
|
||||||
padding: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: default;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-results__section {
|
.search-results__section {
|
||||||
border-bottom: 1px solid var(--background-border-color);
|
border-bottom: 1px solid var(--background-border-color);
|
||||||
|
|
||||||
|
@ -5289,8 +5288,8 @@ a.status-card {
|
||||||
}
|
}
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
background: darken($ui-base-color, 4%);
|
|
||||||
border-bottom: 1px solid var(--background-border-color);
|
border-bottom: 1px solid var(--background-border-color);
|
||||||
|
background: var(--surface-background-color);
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -7159,7 +7158,7 @@ noscript {
|
||||||
.follow-request-banner,
|
.follow-request-banner,
|
||||||
.account-memorial-banner {
|
.account-memorial-banner {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: lighten($ui-base-color, 4%);
|
background: var(--surface-background-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -8326,7 +8325,8 @@ noscript {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: $ui-base-color;
|
border: 1px solid var(--background-border-color);
|
||||||
|
border-top: 0;
|
||||||
border-bottom-left-radius: 4px;
|
border-bottom-left-radius: 4px;
|
||||||
border-bottom-right-radius: 4px;
|
border-bottom-right-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,4 +104,5 @@ $font-monospace: 'mastodon-font-monospace' !default;
|
||||||
--background-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
|
--background-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
|
||||||
--background-color: #{darken($ui-base-color, 8%)};
|
--background-color: #{darken($ui-base-color, 8%)};
|
||||||
--background-color-tint: #{rgba(darken($ui-base-color, 8%), 0.9)};
|
--background-color-tint: #{rgba(darken($ui-base-color, 8%), 0.9)};
|
||||||
|
--surface-background-color: #{darken($ui-base-color, 4%)};
|
||||||
}
|
}
|
||||||
|
|
|
@ -440,7 +440,7 @@ class Account < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def inboxes
|
def inboxes
|
||||||
urls = reorder(nil).where(protocol: :activitypub).group(:preferred_inbox_url).pluck(Arel.sql("coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url) AS preferred_inbox_url"))
|
urls = reorder(nil).activitypub.group(:preferred_inbox_url).pluck(Arel.sql("coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url) AS preferred_inbox_url"))
|
||||||
DeliveryFailureTracker.without_unavailable(urls)
|
DeliveryFailureTracker.without_unavailable(urls)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ module Account::StatusesSearch
|
||||||
def add_to_public_statuses_index!
|
def add_to_public_statuses_index!
|
||||||
return unless Chewy.enabled?
|
return unless Chewy.enabled?
|
||||||
|
|
||||||
statuses.without_reblogs.where(visibility: :public).reorder(nil).find_in_batches do |batch|
|
statuses.without_reblogs.public_visibility.reorder(nil).find_in_batches do |batch|
|
||||||
PublicStatusesIndex.import(batch)
|
PublicStatusesIndex.import(batch)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,7 @@ module Status::SearchConcern
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
scope :indexable, -> { without_reblogs.where(visibility: :public).joins(:account).where(account: { indexable: true }) }
|
scope :indexable, -> { without_reblogs.public_visibility.joins(:account).where(account: { indexable: true }) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def searchable_by
|
def searchable_by
|
||||||
|
|
|
@ -23,7 +23,7 @@ class IpBlock < ApplicationRecord
|
||||||
sign_up_requires_approval: 5000,
|
sign_up_requires_approval: 5000,
|
||||||
sign_up_block: 5500,
|
sign_up_block: 5500,
|
||||||
no_access: 9999,
|
no_access: 9999,
|
||||||
}
|
}, prefix: true
|
||||||
|
|
||||||
validates :ip, :severity, presence: true
|
validates :ip, :severity, presence: true
|
||||||
validates :ip, uniqueness: true
|
validates :ip, uniqueness: true
|
||||||
|
|
|
@ -71,7 +71,7 @@ class PublicFeed
|
||||||
end
|
end
|
||||||
|
|
||||||
def public_scope
|
def public_scope
|
||||||
Status.with_public_visibility.joins(:account).merge(Account.without_suspended.without_silenced)
|
Status.public_visibility.joins(:account).merge(Account.without_suspended.without_silenced)
|
||||||
end
|
end
|
||||||
|
|
||||||
def local_only_scope
|
def local_only_scope
|
||||||
|
|
|
@ -111,7 +111,6 @@ class Status < ApplicationRecord
|
||||||
scope :with_accounts, ->(ids) { where(id: ids).includes(:account) }
|
scope :with_accounts, ->(ids) { where(id: ids).includes(:account) }
|
||||||
scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') }
|
scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') }
|
||||||
scope :without_reblogs, -> { where(statuses: { reblog_of_id: nil }) }
|
scope :without_reblogs, -> { where(statuses: { reblog_of_id: nil }) }
|
||||||
scope :with_public_visibility, -> { where(visibility: :public) }
|
|
||||||
scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) }
|
scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) }
|
||||||
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
|
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
|
||||||
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
|
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
|
||||||
|
@ -495,7 +494,6 @@ class Status < ApplicationRecord
|
||||||
def set_visibility
|
def set_visibility
|
||||||
self.visibility = reblog.visibility if reblog? && visibility.nil?
|
self.visibility = reblog.visibility if reblog? && visibility.nil?
|
||||||
self.visibility = (account.locked? ? :private : :public) if visibility.nil?
|
self.visibility = (account.locked? ? :private : :public) if visibility.nil?
|
||||||
self.sensitive = false if sensitive.nil?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_local_only
|
def set_local_only
|
||||||
|
|
|
@ -446,7 +446,7 @@ class User < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def sign_up_from_ip_requires_approval?
|
def sign_up_from_ip_requires_approval?
|
||||||
sign_up_ip.present? && IpBlock.sign_up_requires_approval.exists?(['ip >>= ?', sign_up_ip.to_s])
|
sign_up_ip.present? && IpBlock.severity_sign_up_requires_approval.exists?(['ip >>= ?', sign_up_ip.to_s])
|
||||||
end
|
end
|
||||||
|
|
||||||
def sign_up_email_requires_approval?
|
def sign_up_email_requires_approval?
|
||||||
|
|
|
@ -1846,7 +1846,10 @@ fi:
|
||||||
apps_ios_action: Lataa App Storesta
|
apps_ios_action: Lataa App Storesta
|
||||||
apps_step: Lataa viralliset sovelluksemme.
|
apps_step: Lataa viralliset sovelluksemme.
|
||||||
apps_title: Mastodon-sovellukset
|
apps_title: Mastodon-sovellukset
|
||||||
|
checklist_subtitle: 'Aloitetaan, sinä aloitat uudella sosiaalisella seudulla:'
|
||||||
|
checklist_title: Tervetuloa tarkistuslista
|
||||||
edit_profile_action: Mukauta
|
edit_profile_action: Mukauta
|
||||||
|
edit_profile_step: Täydentämällä profiilisi tietoja tehostat vaikutemaa.
|
||||||
edit_profile_title: Mukauta profiiliasi
|
edit_profile_title: Mukauta profiiliasi
|
||||||
explanation: Näillä vinkeillä pääset alkuun
|
explanation: Näillä vinkeillä pääset alkuun
|
||||||
feature_action: Lue lisää
|
feature_action: Lue lisää
|
||||||
|
|
|
@ -705,6 +705,7 @@ kab:
|
||||||
moved: Igujj
|
moved: Igujj
|
||||||
primary: Agejdan
|
primary: Agejdan
|
||||||
relationship: Assaɣ
|
relationship: Assaɣ
|
||||||
|
remove_selected_follows: Ur ṭṭafar ara iseqdacen yettwafernen
|
||||||
status: Addad n umiḍan
|
status: Addad n umiḍan
|
||||||
sessions:
|
sessions:
|
||||||
activity: Armud aneggaru
|
activity: Armud aneggaru
|
||||||
|
@ -729,6 +730,7 @@ kab:
|
||||||
current_session: Tiɣimit tamirant
|
current_session: Tiɣimit tamirant
|
||||||
date: Azemz
|
date: Azemz
|
||||||
description: "%{browser} s %{platform}"
|
description: "%{browser} s %{platform}"
|
||||||
|
explanation: Ha-t-en yiminigen web ikecmen akka tura ɣer umiḍan-ik·im Mastodon.
|
||||||
ip: IP
|
ip: IP
|
||||||
platforms:
|
platforms:
|
||||||
adobe_air: Adobe Air
|
adobe_air: Adobe Air
|
||||||
|
|
|
@ -23,6 +23,8 @@ kab:
|
||||||
setting_display_media_hide_all: Ffer yal tikkelt akk taywalt
|
setting_display_media_hide_all: Ffer yal tikkelt akk taywalt
|
||||||
setting_display_media_show_all: Ffer yal tikkelt teywalt yettwacreḍ d tanafrit
|
setting_display_media_show_all: Ffer yal tikkelt teywalt yettwacreḍ d tanafrit
|
||||||
username: Tzemreḍ ad tesqedceḍ isekkilen, uṭṭunen akked yijerriden n wadda
|
username: Tzemreḍ ad tesqedceḍ isekkilen, uṭṭunen akked yijerriden n wadda
|
||||||
|
featured_tag:
|
||||||
|
name: 'Ha-t-an kra seg ihacṭagen i tesseqdaceḍ ussan-a ineggura maḍi :'
|
||||||
imports:
|
imports:
|
||||||
data: Afaylu CSV id yusan seg uqeddac-nniḍen n Maṣṭudun
|
data: Afaylu CSV id yusan seg uqeddac-nniḍen n Maṣṭudun
|
||||||
ip_block:
|
ip_block:
|
||||||
|
@ -102,7 +104,9 @@ kab:
|
||||||
no_access: Sewḥel anekcum
|
no_access: Sewḥel anekcum
|
||||||
severity: Alugen
|
severity: Alugen
|
||||||
notification_emails:
|
notification_emails:
|
||||||
|
favourite: Ma yella walbɛaḍ i iḥemmlen tasuffeɣt-ik·im
|
||||||
follow: Yeḍfer-ik·im-id walbɛaḍ
|
follow: Yeḍfer-ik·im-id walbɛaḍ
|
||||||
|
follow_request: Ma yella win i d-yessutren ad k·em-yeḍfer
|
||||||
mention: Yuder-ik·em-id walbɛaḍ
|
mention: Yuder-ik·em-id walbɛaḍ
|
||||||
reblog: Yella win yesselhan adda-dik·im
|
reblog: Yella win yesselhan adda-dik·im
|
||||||
rule:
|
rule:
|
||||||
|
|
|
@ -78,8 +78,10 @@ Rails.application.routes.draw do
|
||||||
get 'remote_interaction_helper', to: 'remote_interaction_helper#index'
|
get 'remote_interaction_helper', to: 'remote_interaction_helper#index'
|
||||||
|
|
||||||
resource :instance_actor, path: 'actor', only: [:show] do
|
resource :instance_actor, path: 'actor', only: [:show] do
|
||||||
resource :inbox, only: [:create], module: :activitypub
|
scope module: :activitypub do
|
||||||
resource :outbox, only: [:show], module: :activitypub
|
resource :inbox, only: [:create]
|
||||||
|
resource :outbox, only: [:show]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/invite/:invite_code', constraints: ->(req) { req.format == :json }, to: 'api/v1/invites#show'
|
get '/invite/:invite_code', constraints: ->(req) { req.format == :json }, to: 'api/v1/invites#show'
|
||||||
|
@ -127,11 +129,13 @@ Rails.application.routes.draw do
|
||||||
resources :followers, only: [:index], controller: :follower_accounts
|
resources :followers, only: [:index], controller: :follower_accounts
|
||||||
resources :following, only: [:index], controller: :following_accounts
|
resources :following, only: [:index], controller: :following_accounts
|
||||||
|
|
||||||
resource :outbox, only: [:show], module: :activitypub
|
scope module: :activitypub do
|
||||||
resource :inbox, only: [:create], module: :activitypub
|
resource :outbox, only: [:show]
|
||||||
resource :claim, only: [:create], module: :activitypub
|
resource :inbox, only: [:create]
|
||||||
resources :collections, only: [:show], module: :activitypub
|
resource :claim, only: [:create]
|
||||||
resource :followers_synchronization, only: [:show], module: :activitypub
|
resources :collections, only: [:show]
|
||||||
|
resource :followers_synchronization, only: [:show]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
resource :inbox, only: [:create], module: :activitypub
|
resource :inbox, only: [:create], module: :activitypub
|
||||||
|
@ -139,10 +143,12 @@ Rails.application.routes.draw do
|
||||||
get '/:encoded_at(*path)', to: redirect("/@%{path}"), constraints: { encoded_at: /%40/ }
|
get '/:encoded_at(*path)', to: redirect("/@%{path}"), constraints: { encoded_at: /%40/ }
|
||||||
|
|
||||||
constraints(username: %r{[^@/.]+}) do
|
constraints(username: %r{[^@/.]+}) do
|
||||||
get '/@:username', to: 'accounts#show', as: :short_account
|
with_options to: 'accounts#show' do
|
||||||
get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies
|
get '/@:username', as: :short_account
|
||||||
get '/@:username/media', to: 'accounts#show', as: :short_account_media
|
get '/@:username/with_replies', as: :short_account_with_replies
|
||||||
get '/@:username/tagged/:tag', to: 'accounts#show', as: :short_account_tag
|
get '/@:username/media', as: :short_account_media
|
||||||
|
get '/@:username/tagged/:tag', as: :short_account_tag
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
constraints(account_username: %r{[^@/.]+}) do
|
constraints(account_username: %r{[^@/.]+}) do
|
||||||
|
|
|
@ -126,14 +126,16 @@ namespace :api, format: false do
|
||||||
end
|
end
|
||||||
|
|
||||||
resource :instance, only: [:show] do
|
resource :instance, only: [:show] do
|
||||||
resources :peers, only: [:index], controller: 'instances/peers'
|
scope module: :instances do
|
||||||
resources :rules, only: [:index], controller: 'instances/rules'
|
resources :peers, only: [:index]
|
||||||
resources :domain_blocks, only: [:index], controller: 'instances/domain_blocks'
|
resources :rules, only: [:index]
|
||||||
resource :privacy_policy, only: [:show], controller: 'instances/privacy_policies'
|
resources :domain_blocks, only: [:index]
|
||||||
resource :extended_description, only: [:show], controller: 'instances/extended_descriptions'
|
resource :privacy_policy, only: [:show]
|
||||||
resource :translation_languages, only: [:show], controller: 'instances/translation_languages'
|
resource :extended_description, only: [:show]
|
||||||
resource :languages, only: [:show], controller: 'instances/languages'
|
resource :translation_languages, only: [:show]
|
||||||
resource :activity, only: [:show], controller: 'instances/activity'
|
resource :languages, only: [:show]
|
||||||
|
resource :activity, only: [:show], controller: :activity
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :peers do
|
namespace :peers do
|
||||||
|
@ -183,12 +185,14 @@ namespace :api, format: false do
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :accounts, only: [:create, :show] do
|
resources :accounts, only: [:create, :show] do
|
||||||
resources :statuses, only: :index, controller: 'accounts/statuses'
|
scope module: :accounts do
|
||||||
resources :followers, only: :index, controller: 'accounts/follower_accounts'
|
resources :statuses, only: :index
|
||||||
resources :following, only: :index, controller: 'accounts/following_accounts'
|
resources :followers, only: :index, controller: :follower_accounts
|
||||||
resources :lists, only: :index, controller: 'accounts/lists'
|
resources :following, only: :index, controller: :following_accounts
|
||||||
resources :identity_proofs, only: :index, controller: 'accounts/identity_proofs'
|
resources :lists, only: :index
|
||||||
resources :featured_tags, only: :index, controller: 'accounts/featured_tags'
|
resources :identity_proofs, only: :index
|
||||||
|
resources :featured_tags, only: :index
|
||||||
|
end
|
||||||
|
|
||||||
member do
|
member do
|
||||||
post :follow
|
post :follow
|
||||||
|
|
|
@ -295,7 +295,7 @@ module Mastodon::CLI
|
||||||
skip_threshold = 7.days.ago
|
skip_threshold = 7.days.ago
|
||||||
skip_domains = Concurrent::Set.new
|
skip_domains = Concurrent::Set.new
|
||||||
|
|
||||||
query = Account.remote.where(protocol: :activitypub)
|
query = Account.remote.activitypub
|
||||||
query = query.where(domain: domains) unless domains.empty?
|
query = query.where(domain: domains) unless domains.empty?
|
||||||
|
|
||||||
processed, culled = parallelize_with_progress(query.partitioned) do |account|
|
processed, culled = parallelize_with_progress(query.partitioned) do |account|
|
||||||
|
|
|
@ -105,7 +105,7 @@ module Mastodon::CLI
|
||||||
tools. Only blocks with no_access severity are returned.
|
tools. Only blocks with no_access severity are returned.
|
||||||
LONG_DESC
|
LONG_DESC
|
||||||
def export
|
def export
|
||||||
IpBlock.where(severity: :no_access).find_each do |ip_block|
|
IpBlock.severity_no_access.find_each do |ip_block|
|
||||||
case options[:format]
|
case options[:format]
|
||||||
when 'nginx'
|
when 'nginx'
|
||||||
say "deny #{ip_block.ip}/#{ip_block.ip.prefix};"
|
say "deny #{ip_block.ip}/#{ip_block.ip.prefix};"
|
||||||
|
|
|
@ -277,7 +277,7 @@ module Mastodon::CLI
|
||||||
|
|
||||||
desc 'usage', 'Calculate disk space consumed by Mastodon'
|
desc 'usage', 'Calculate disk space consumed by Mastodon'
|
||||||
def usage
|
def usage
|
||||||
say("Attachments:\t#{number_to_human_size(MediaAttachment.sum(Arel.sql('COALESCE(file_file_size, 0) + COALESCE(thumbnail_file_size, 0)')))} (#{number_to_human_size(MediaAttachment.where(account: Account.local).sum(Arel.sql('COALESCE(file_file_size, 0) + COALESCE(thumbnail_file_size, 0)')))} local)")
|
say("Attachments:\t#{number_to_human_size(media_attachment_storage_size)} (#{number_to_human_size(local_media_attachment_storage_size)} local)")
|
||||||
say("Custom emoji:\t#{number_to_human_size(CustomEmoji.sum(:image_file_size))} (#{number_to_human_size(CustomEmoji.local.sum(:image_file_size))} local)")
|
say("Custom emoji:\t#{number_to_human_size(CustomEmoji.sum(:image_file_size))} (#{number_to_human_size(CustomEmoji.local.sum(:image_file_size))} local)")
|
||||||
say("Preview cards:\t#{number_to_human_size(PreviewCard.sum(:image_file_size))}")
|
say("Preview cards:\t#{number_to_human_size(PreviewCard.sum(:image_file_size))}")
|
||||||
say("Avatars:\t#{number_to_human_size(Account.sum(:avatar_file_size))} (#{number_to_human_size(Account.local.sum(:avatar_file_size))} local)")
|
say("Avatars:\t#{number_to_human_size(Account.sum(:avatar_file_size))} (#{number_to_human_size(Account.local.sum(:avatar_file_size))} local)")
|
||||||
|
@ -317,6 +317,22 @@ module Mastodon::CLI
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def media_attachment_storage_size
|
||||||
|
MediaAttachment.sum(file_and_thumbnail_size_sql)
|
||||||
|
end
|
||||||
|
|
||||||
|
def local_media_attachment_storage_size
|
||||||
|
MediaAttachment.where(account: Account.local).sum(file_and_thumbnail_size_sql)
|
||||||
|
end
|
||||||
|
|
||||||
|
def file_and_thumbnail_size_sql
|
||||||
|
Arel.sql(
|
||||||
|
<<~SQL.squish
|
||||||
|
COALESCE(file_file_size, 0) + COALESCE(thumbnail_file_size, 0)
|
||||||
|
SQL
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
PRELOAD_MODEL_WHITELIST = %w(
|
PRELOAD_MODEL_WHITELIST = %w(
|
||||||
Account
|
Account
|
||||||
Backup
|
Backup
|
||||||
|
|
|
@ -3,10 +3,6 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe Api::BaseController do
|
describe Api::BaseController do
|
||||||
before do
|
|
||||||
stub_const('FakeService', Class.new)
|
|
||||||
end
|
|
||||||
|
|
||||||
controller do
|
controller do
|
||||||
def success
|
def success
|
||||||
head 200
|
head 200
|
||||||
|
@ -72,36 +68,4 @@ describe Api::BaseController do
|
||||||
expect(response).to have_http_status(403)
|
expect(response).to have_http_status(403)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'error handling' do
|
|
||||||
before do
|
|
||||||
routes.draw { get 'failure' => 'api/base#failure' }
|
|
||||||
end
|
|
||||||
|
|
||||||
{
|
|
||||||
ActiveRecord::RecordInvalid => 422,
|
|
||||||
ActiveRecord::RecordNotFound => 404,
|
|
||||||
ActiveRecord::RecordNotUnique => 422,
|
|
||||||
Date::Error => 422,
|
|
||||||
HTTP::Error => 503,
|
|
||||||
Mastodon::InvalidParameterError => 400,
|
|
||||||
Mastodon::NotPermittedError => 403,
|
|
||||||
Mastodon::RaceConditionError => 503,
|
|
||||||
Mastodon::RateLimitExceededError => 429,
|
|
||||||
Mastodon::UnexpectedResponseError => 503,
|
|
||||||
Mastodon::ValidationError => 422,
|
|
||||||
OpenSSL::SSL::SSLError => 503,
|
|
||||||
Seahorse::Client::NetworkingError => 503,
|
|
||||||
Stoplight::Error::RedLight => 503,
|
|
||||||
}.each do |error, code|
|
|
||||||
it "Handles error class of #{error}" do
|
|
||||||
allow(FakeService).to receive(:new).and_raise(error)
|
|
||||||
|
|
||||||
get :failure
|
|
||||||
|
|
||||||
expect(response).to have_http_status(code)
|
|
||||||
expect(FakeService).to have_received(:new)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Api::ErrorHandling do
|
||||||
|
before do
|
||||||
|
stub_const('FakeService', Class.new)
|
||||||
|
end
|
||||||
|
|
||||||
|
controller(Api::BaseController) do
|
||||||
|
def failure
|
||||||
|
FakeService.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'error handling' do
|
||||||
|
before do
|
||||||
|
routes.draw { get 'failure' => 'api/base#failure' }
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
ActiveRecord::RecordInvalid => 422,
|
||||||
|
ActiveRecord::RecordNotFound => 404,
|
||||||
|
ActiveRecord::RecordNotUnique => 422,
|
||||||
|
Date::Error => 422,
|
||||||
|
HTTP::Error => 503,
|
||||||
|
Mastodon::InvalidParameterError => 400,
|
||||||
|
Mastodon::NotPermittedError => 403,
|
||||||
|
Mastodon::RaceConditionError => 503,
|
||||||
|
Mastodon::RateLimitExceededError => 429,
|
||||||
|
Mastodon::UnexpectedResponseError => 503,
|
||||||
|
Mastodon::ValidationError => 422,
|
||||||
|
OpenSSL::SSL::SSLError => 503,
|
||||||
|
Seahorse::Client::NetworkingError => 503,
|
||||||
|
Stoplight::Error::RedLight => 503,
|
||||||
|
}.each do |error, code|
|
||||||
|
it "Handles error class of #{error}" do
|
||||||
|
allow(FakeService)
|
||||||
|
.to receive(:new)
|
||||||
|
.and_raise(error)
|
||||||
|
|
||||||
|
get :failure
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(code)
|
||||||
|
expect(FakeService)
|
||||||
|
.to have_received(:new)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -61,15 +61,11 @@ describe 'Using OAuth from an external app' do
|
||||||
expect(page).to have_content(I18n.t('auth.login'))
|
expect(page).to have_content(I18n.t('auth.login'))
|
||||||
|
|
||||||
# Failing to log-in presents the form again
|
# Failing to log-in presents the form again
|
||||||
fill_in 'user_email', with: email
|
fill_in_auth_details(email, 'wrong password')
|
||||||
fill_in 'user_password', with: 'wrong password'
|
|
||||||
click_on I18n.t('auth.login')
|
|
||||||
expect(page).to have_content(I18n.t('auth.login'))
|
expect(page).to have_content(I18n.t('auth.login'))
|
||||||
|
|
||||||
# Logging in redirects to an authorization page
|
# Logging in redirects to an authorization page
|
||||||
fill_in 'user_email', with: email
|
fill_in_auth_details(email, password)
|
||||||
fill_in 'user_password', with: password
|
|
||||||
click_on I18n.t('auth.login')
|
|
||||||
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
|
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
|
||||||
|
|
||||||
# Upon authorizing, it redirects to the apps' callback URL
|
# Upon authorizing, it redirects to the apps' callback URL
|
||||||
|
@ -88,15 +84,11 @@ describe 'Using OAuth from an external app' do
|
||||||
expect(page).to have_content(I18n.t('auth.login'))
|
expect(page).to have_content(I18n.t('auth.login'))
|
||||||
|
|
||||||
# Failing to log-in presents the form again
|
# Failing to log-in presents the form again
|
||||||
fill_in 'user_email', with: email
|
fill_in_auth_details(email, 'wrong password')
|
||||||
fill_in 'user_password', with: 'wrong password'
|
|
||||||
click_on I18n.t('auth.login')
|
|
||||||
expect(page).to have_content(I18n.t('auth.login'))
|
expect(page).to have_content(I18n.t('auth.login'))
|
||||||
|
|
||||||
# Logging in redirects to an authorization page
|
# Logging in redirects to an authorization page
|
||||||
fill_in 'user_email', with: email
|
fill_in_auth_details(email, password)
|
||||||
fill_in 'user_password', with: password
|
|
||||||
click_on I18n.t('auth.login')
|
|
||||||
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
|
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
|
||||||
|
|
||||||
# Upon denying, it redirects to the apps' callback URL
|
# Upon denying, it redirects to the apps' callback URL
|
||||||
|
@ -118,25 +110,19 @@ describe 'Using OAuth from an external app' do
|
||||||
expect(page).to have_content(I18n.t('auth.login'))
|
expect(page).to have_content(I18n.t('auth.login'))
|
||||||
|
|
||||||
# Failing to log-in presents the form again
|
# Failing to log-in presents the form again
|
||||||
fill_in 'user_email', with: email
|
fill_in_auth_details(email, 'wrong password')
|
||||||
fill_in 'user_password', with: 'wrong password'
|
|
||||||
click_on I18n.t('auth.login')
|
|
||||||
expect(page).to have_content(I18n.t('auth.login'))
|
expect(page).to have_content(I18n.t('auth.login'))
|
||||||
|
|
||||||
# Logging in redirects to a two-factor authentication page
|
# Logging in redirects to a two-factor authentication page
|
||||||
fill_in 'user_email', with: email
|
fill_in_auth_details(email, password)
|
||||||
fill_in 'user_password', with: password
|
|
||||||
click_on I18n.t('auth.login')
|
|
||||||
expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
|
expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
|
||||||
|
|
||||||
# Filling in an incorrect two-factor authentication code presents the form again
|
# Filling in an incorrect two-factor authentication code presents the form again
|
||||||
fill_in 'user_otp_attempt', with: 'wrong'
|
fill_in_otp_details('wrong')
|
||||||
click_on I18n.t('auth.login')
|
|
||||||
expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
|
expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
|
||||||
|
|
||||||
# Filling in the correct TOTP code redirects to an app authorization page
|
# Filling in the correct TOTP code redirects to an app authorization page
|
||||||
fill_in 'user_otp_attempt', with: user.current_otp
|
fill_in_otp_details(user.current_otp)
|
||||||
click_on I18n.t('auth.login')
|
|
||||||
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
|
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
|
||||||
|
|
||||||
# Upon authorizing, it redirects to the apps' callback URL
|
# Upon authorizing, it redirects to the apps' callback URL
|
||||||
|
@ -155,25 +141,19 @@ describe 'Using OAuth from an external app' do
|
||||||
expect(page).to have_content(I18n.t('auth.login'))
|
expect(page).to have_content(I18n.t('auth.login'))
|
||||||
|
|
||||||
# Failing to log-in presents the form again
|
# Failing to log-in presents the form again
|
||||||
fill_in 'user_email', with: email
|
fill_in_auth_details(email, 'wrong password')
|
||||||
fill_in 'user_password', with: 'wrong password'
|
|
||||||
click_on I18n.t('auth.login')
|
|
||||||
expect(page).to have_content(I18n.t('auth.login'))
|
expect(page).to have_content(I18n.t('auth.login'))
|
||||||
|
|
||||||
# Logging in redirects to a two-factor authentication page
|
# Logging in redirects to a two-factor authentication page
|
||||||
fill_in 'user_email', with: email
|
fill_in_auth_details(email, password)
|
||||||
fill_in 'user_password', with: password
|
|
||||||
click_on I18n.t('auth.login')
|
|
||||||
expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
|
expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
|
||||||
|
|
||||||
# Filling in an incorrect two-factor authentication code presents the form again
|
# Filling in an incorrect two-factor authentication code presents the form again
|
||||||
fill_in 'user_otp_attempt', with: 'wrong'
|
fill_in_otp_details('wrong')
|
||||||
click_on I18n.t('auth.login')
|
|
||||||
expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
|
expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
|
||||||
|
|
||||||
# Filling in the correct TOTP code redirects to an app authorization page
|
# Filling in the correct TOTP code redirects to an app authorization page
|
||||||
fill_in 'user_otp_attempt', with: user.current_otp
|
fill_in_otp_details(user.current_otp)
|
||||||
click_on I18n.t('auth.login')
|
|
||||||
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
|
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
|
||||||
|
|
||||||
# Upon denying, it redirects to the apps' callback URL
|
# Upon denying, it redirects to the apps' callback URL
|
||||||
|
@ -185,6 +165,19 @@ describe 'Using OAuth from an external app' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fill_in_auth_details(email, password)
|
||||||
|
fill_in 'user_email', with: email
|
||||||
|
fill_in 'user_password', with: password
|
||||||
|
click_on I18n.t('auth.login')
|
||||||
|
end
|
||||||
|
|
||||||
|
def fill_in_otp_details(value)
|
||||||
|
fill_in 'user_otp_attempt', with: value
|
||||||
|
click_on I18n.t('auth.login')
|
||||||
|
end
|
||||||
|
|
||||||
# TODO: external auth
|
# TODO: external auth
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,6 +15,23 @@ describe Mastodon::CLI::Domains do
|
||||||
describe '#purge' do
|
describe '#purge' do
|
||||||
let(:action) { :purge }
|
let(:action) { :purge }
|
||||||
|
|
||||||
|
context 'with invalid limited federation mode argument' do
|
||||||
|
let(:arguments) { ['example.host'] }
|
||||||
|
let(:options) { { limited_federation_mode: true } }
|
||||||
|
|
||||||
|
it 'warns about usage and exits' do
|
||||||
|
expect { subject }
|
||||||
|
.to raise_error(Thor::Error, /DOMAIN parameter not supported/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without a domains argument' do
|
||||||
|
it 'warns about usage and exits' do
|
||||||
|
expect { subject }
|
||||||
|
.to raise_error(Thor::Error, 'No domain(s) given')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with accounts from the domain' do
|
context 'with accounts from the domain' do
|
||||||
let(:domain) { 'host.example' }
|
let(:domain) { 'host.example' }
|
||||||
let!(:account) { Fabricate(:account, domain: domain) }
|
let!(:account) { Fabricate(:account, domain: domain) }
|
||||||
|
|
|
@ -13,7 +13,26 @@ RSpec.describe Vacuum::ImportsVacuum do
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
it 'cleans up the expected imports' do
|
it 'cleans up the expected imports' do
|
||||||
expect { subject.perform }.to change { BulkImport.pluck(:id) }.from([old_unconfirmed, new_unconfirmed, recent_ongoing, recent_finished, old_finished].map(&:id)).to([new_unconfirmed, recent_ongoing, recent_finished].map(&:id))
|
expect { subject.perform }
|
||||||
|
.to change { ordered_bulk_imports.pluck(:id) }
|
||||||
|
.from(original_import_ids)
|
||||||
|
.to(remaining_import_ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ordered_bulk_imports
|
||||||
|
BulkImport.order(id: :asc)
|
||||||
|
end
|
||||||
|
|
||||||
|
def original_import_ids
|
||||||
|
[old_unconfirmed, new_unconfirmed, recent_ongoing, recent_finished, old_finished].map(&:id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def vacuumed_import_ids
|
||||||
|
[old_unconfirmed, old_finished].map(&:id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def remaining_import_ids
|
||||||
|
original_import_ids - vacuumed_import_ids
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,7 +21,7 @@ describe Account::StatusesSearch, :sidekiq_inline do
|
||||||
account.indexable = true
|
account.indexable = true
|
||||||
account.save!
|
account.save!
|
||||||
|
|
||||||
expect(PublicStatusesIndex.filter(term: { account_id: account.id }).count).to eq(account.statuses.where(visibility: :public).count)
|
expect(PublicStatusesIndex.filter(term: { account_id: account.id }).count).to eq(account.statuses.public_visibility.count)
|
||||||
expect(StatusesIndex.filter(term: { account_id: account.id }).count).to eq(account.statuses.count)
|
expect(StatusesIndex.filter(term: { account_id: account.id }).count).to eq(account.statuses.count)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -32,7 +32,7 @@ describe Account::StatusesSearch, :sidekiq_inline do
|
||||||
|
|
||||||
context 'when picking an indexable account' do
|
context 'when picking an indexable account' do
|
||||||
it 'has statuses in the PublicStatusesIndex' do
|
it 'has statuses in the PublicStatusesIndex' do
|
||||||
expect(PublicStatusesIndex.filter(term: { account_id: account.id }).count).to eq(account.statuses.where(visibility: :public).count)
|
expect(PublicStatusesIndex.filter(term: { account_id: account.id }).count).to eq(account.statuses.public_visibility.count)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has statuses in the StatusesIndex' do
|
it 'has statuses in the StatusesIndex' do
|
||||||
|
|
Loading…
Reference in New Issue