Merge pull request #2676 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to df6086d402
This commit is contained in:
commit
b834f41ec1
48 changed files with 430 additions and 332 deletions
|
@ -8,6 +8,7 @@ class Api::BaseController < ApplicationController
|
|||
include Api::AccessTokenTrackingConcern
|
||||
include Api::CachingConcern
|
||||
include Api::ContentSecurityPolicy
|
||||
include Api::ErrorHandling
|
||||
|
||||
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
||||
|
||||
|
@ -18,51 +19,6 @@ class Api::BaseController < ApplicationController
|
|||
|
||||
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)
|
||||
{ json: { error: error.try(:description) || 'Not authorized' } }
|
||||
end
|
||||
|
|
52
app/controllers/concerns/api/error_handling.rb
Normal file
52
app/controllers/concerns/api/error_handling.rb
Normal file
|
@ -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
|
|
@ -640,7 +640,10 @@ export const fetchNotificationsForRequest = accountId => (dispatch, getState) =>
|
|||
|
||||
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
||||
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(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
|
||||
|
||||
dispatch(fetchNotificationsForRequestSuccess(response.data, next?.uri));
|
||||
}).catch(err => {
|
||||
dispatch(fetchNotificationsForRequestFail(err));
|
||||
|
@ -673,7 +676,10 @@ export const expandNotificationsForRequest = () => (dispatch, getState) => {
|
|||
|
||||
api(getState).get(url).then(response => {
|
||||
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(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
|
||||
|
||||
dispatch(expandNotificationsForRequestSuccess(response.data, next?.uri));
|
||||
}).catch(err => {
|
||||
dispatch(expandNotificationsForRequestFail(err));
|
||||
|
|
|
@ -199,7 +199,7 @@ class ColumnHeader extends PureComponent {
|
|||
<h1 className={buttonClassName}>
|
||||
{hasTitle && (
|
||||
<>
|
||||
{backButton}
|
||||
{showBackButton && backButton}
|
||||
|
||||
<button onClick={this.handleTitleClick} className='column-header__title'>
|
||||
{!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'>
|
||||
{extraButton}
|
||||
|
|
|
@ -5,9 +5,7 @@ import { FormattedMessage, injectIntl } from 'react-intl';
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?react';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import InlineAccount from 'flavours/glitch/components/inline_account';
|
||||
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
|
||||
|
||||
|
@ -67,7 +65,7 @@ class EditedTimestamp extends PureComponent {
|
|||
return (
|
||||
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
|
||||
<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>
|
||||
</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 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 { Icon } from 'flavours/glitch/components/icon';
|
||||
import { LoadMore } from 'flavours/glitch/components/load_more';
|
||||
|
@ -76,11 +75,6 @@ class SearchResults extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className='search-results'>
|
||||
<div className='search-results__header'>
|
||||
<Icon id='search' icon={SearchIcon} />
|
||||
<FormattedMessage id='explore.search_results' defaultMessage='Search results' />
|
||||
</div>
|
||||
|
||||
{accounts}
|
||||
{hashtags}
|
||||
{statuses}
|
||||
|
|
|
@ -88,7 +88,7 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
|
|||
}
|
||||
}, [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 (
|
||||
<Column bindToDocument={!multiColumn} ref={columnRef} label={columnTitle}>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedDate } from 'react-intl';
|
||||
import { FormattedDate, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
|
@ -8,14 +8,10 @@ import { Link, withRouter } from 'react-router-dom';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
|
||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
|
||||
import { AnimatedNumber } from 'flavours/glitch/components/animated_number';
|
||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||
import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';
|
||||
import { getHashtagBarForStatus } from 'flavours/glitch/components/hashtag_bar';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
|
||||
import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon';
|
||||
import PollContainer from 'flavours/glitch/containers/poll_container';
|
||||
|
@ -133,10 +129,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
|
||||
let applicationLink = '';
|
||||
let reblogLink = '';
|
||||
const reblogIcon = 'retweet';
|
||||
const reblogIconComponent = RepeatIcon;
|
||||
let favouriteLink = '';
|
||||
let edited = '';
|
||||
|
||||
// Depending on user settings, some media are considered as parts of the
|
||||
// contents (affected by CW) while other will be displayed outside of the
|
||||
|
@ -239,68 +232,53 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
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 (!['unlisted', 'public'].includes(status.get('visibility'))) {
|
||||
reblogLink = null;
|
||||
} else if (this.props.history) {
|
||||
reblogLink = (
|
||||
<>
|
||||
{' · '}
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
||||
<Icon id={reblogIcon} icon={reblogIconComponent} />
|
||||
<span className='detailed-status__reblogs'>
|
||||
<AnimatedNumber value={status.get('reblogs_count')} />
|
||||
</span>
|
||||
</Link>
|
||||
</>
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
||||
<span className='detailed-status__reblogs'>
|
||||
<AnimatedNumber value={status.get('reblogs_count')} />
|
||||
</span>
|
||||
<FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
reblogLink = (
|
||||
<>
|
||||
{' · '}
|
||||
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||
<Icon id={reblogIcon} icon={reblogIconComponent} />
|
||||
<span className='detailed-status__reblogs'>
|
||||
<AnimatedNumber value={status.get('reblogs_count')} />
|
||||
</span>
|
||||
</a>
|
||||
</>
|
||||
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||
<span className='detailed-status__reblogs'>
|
||||
<AnimatedNumber value={status.get('reblogs_count')} />
|
||||
</span>
|
||||
<FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.history) {
|
||||
favouriteLink = (
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'>
|
||||
<Icon id='star' icon={StarIcon} />
|
||||
<span className='detailed-status__favorites'>
|
||||
<AnimatedNumber value={status.get('favourites_count')} />
|
||||
</span>
|
||||
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
favouriteLink = (
|
||||
<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'>
|
||||
<AnimatedNumber value={status.get('favourites_count')} />
|
||||
</span>
|
||||
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (status.get('edited_at')) {
|
||||
edited = (
|
||||
<>
|
||||
{' · '}
|
||||
<EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||
contentMedia.push(hashtagBar);
|
||||
|
||||
|
@ -330,9 +308,23 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
/>
|
||||
|
||||
<div className='detailed-status__meta'>
|
||||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
|
||||
<FormattedDate value={new Date(status.get('created_at'))} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
||||
</a>{edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
|
||||
<div className='detailed-status__meta__line'>
|
||||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
|
||||
<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>
|
||||
|
|
|
@ -1871,15 +1871,35 @@ body > [data-popper-placement] {
|
|||
}
|
||||
|
||||
.detailed-status__meta {
|
||||
margin-top: 16px;
|
||||
margin-top: 24px;
|
||||
color: $dark-text-color;
|
||||
font-size: 14px;
|
||||
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 {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
vertical-align: middle;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.animated-number {
|
||||
color: $secondary-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1923,19 +1943,6 @@ body > [data-popper-placement] {
|
|||
color: inherit;
|
||||
text-decoration: none;
|
||||
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 {
|
||||
|
@ -2506,6 +2513,10 @@ a.account__display-name {
|
|||
outline: 1px dotted;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
|
@ -3699,7 +3710,7 @@ $ui-header-height: 55px;
|
|||
}
|
||||
|
||||
.column-subheading {
|
||||
background: darken($ui-base-color, 4%);
|
||||
background: var(--surface-background-color);
|
||||
color: $darker-text-color;
|
||||
padding: 8px 20px;
|
||||
font-size: 12px;
|
||||
|
@ -5023,7 +5034,7 @@ a.status-card {
|
|||
}
|
||||
|
||||
.follow_requests-unlocked_explanation {
|
||||
background: darken($ui-base-color, 4%);
|
||||
background: var(--surface-background-color);
|
||||
border-bottom: 1px solid var(--background-border-color);
|
||||
contain: initial;
|
||||
flex-grow: 0;
|
||||
|
@ -5767,18 +5778,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 {
|
||||
border-bottom: 1px solid var(--background-border-color);
|
||||
|
||||
|
@ -5787,8 +5786,8 @@ a.status-card {
|
|||
}
|
||||
|
||||
&__header {
|
||||
background: darken($ui-base-color, 4%);
|
||||
border-bottom: 1px solid var(--background-border-color);
|
||||
background: var(--surface-background-color);
|
||||
padding: 15px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
|
@ -7740,7 +7739,7 @@ noscript {
|
|||
.follow-request-banner,
|
||||
.account-memorial-banner {
|
||||
padding: 20px;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
background: var(--surface-background-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
@ -8914,7 +8913,8 @@ noscript {
|
|||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: $ui-base-color;
|
||||
border: 1px solid var(--background-border-color);
|
||||
border-top: 0;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
|
|
@ -110,4 +110,5 @@ $dismiss-overlay-width: 4rem;
|
|||
--background-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
|
||||
--background-color: #{darken($ui-base-color, 8%)};
|
||||
--background-color-tint: #{rgba(darken($ui-base-color, 8%), 0.9)};
|
||||
--surface-background-color: #{darken($ui-base-color, 4%)};
|
||||
}
|
||||
|
|
|
@ -552,7 +552,10 @@ export const fetchNotificationsForRequest = accountId => (dispatch, getState) =>
|
|||
|
||||
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
||||
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(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
|
||||
|
||||
dispatch(fetchNotificationsForRequestSuccess(response.data, next?.uri));
|
||||
}).catch(err => {
|
||||
dispatch(fetchNotificationsForRequestFail(err));
|
||||
|
@ -585,7 +588,10 @@ export const expandNotificationsForRequest = () => (dispatch, getState) => {
|
|||
|
||||
api(getState).get(url).then(response => {
|
||||
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(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
|
||||
|
||||
dispatch(expandNotificationsForRequestSuccess(response.data, next?.uri));
|
||||
}).catch(err => {
|
||||
dispatch(expandNotificationsForRequestFail(err));
|
||||
|
|
|
@ -199,7 +199,7 @@ class ColumnHeader extends PureComponent {
|
|||
<h1 className={buttonClassName}>
|
||||
{hasTitle && (
|
||||
<>
|
||||
{backButton}
|
||||
{showBackButton && backButton}
|
||||
|
||||
<button onClick={this.handleTitleClick} className='column-header__title'>
|
||||
{!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'>
|
||||
{extraButton}
|
||||
|
|
|
@ -5,9 +5,7 @@ import { FormattedMessage, injectIntl } from 'react-intl';
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?react';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import InlineAccount from 'mastodon/components/inline_account';
|
||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||
|
||||
|
@ -67,7 +65,7 @@ class EditedTimestamp extends PureComponent {
|
|||
return (
|
||||
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
|
||||
<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>
|
||||
</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 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 { Icon } from 'mastodon/components/icon';
|
||||
import { LoadMore } from 'mastodon/components/load_more';
|
||||
|
@ -76,11 +75,6 @@ class SearchResults extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className='search-results'>
|
||||
<div className='search-results__header'>
|
||||
<Icon id='search' icon={SearchIcon} />
|
||||
<FormattedMessage id='explore.search_results' defaultMessage='Search results' />
|
||||
</div>
|
||||
|
||||
{accounts}
|
||||
{hashtags}
|
||||
{statuses}
|
||||
|
|
|
@ -88,7 +88,7 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
|
|||
}
|
||||
}, [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 (
|
||||
<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 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 EditedTimestamp from 'mastodon/components/edited_timestamp';
|
||||
import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
|
||||
|
@ -143,10 +141,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
let media = '';
|
||||
let applicationLink = '';
|
||||
let reblogLink = '';
|
||||
const reblogIcon = 'retweet';
|
||||
const reblogIconComponent = RepeatIcon;
|
||||
let favouriteLink = '';
|
||||
let edited = '';
|
||||
|
||||
if (this.props.measureHeight) {
|
||||
outerStyle.height = `${this.state.height}px`;
|
||||
|
@ -218,68 +213,53 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
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'))) {
|
||||
reblogLink = '';
|
||||
} else if (this.props.history) {
|
||||
reblogLink = (
|
||||
<>
|
||||
{' · '}
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
||||
<Icon id={reblogIcon} icon={reblogIconComponent} />
|
||||
<span className='detailed-status__reblogs'>
|
||||
<AnimatedNumber value={status.get('reblogs_count')} />
|
||||
</span>
|
||||
</Link>
|
||||
</>
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
||||
<span className='detailed-status__reblogs'>
|
||||
<AnimatedNumber value={status.get('reblogs_count')} />
|
||||
</span>
|
||||
<FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
reblogLink = (
|
||||
<>
|
||||
{' · '}
|
||||
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||
<Icon id={reblogIcon} icon={reblogIconComponent} />
|
||||
<span className='detailed-status__reblogs'>
|
||||
<AnimatedNumber value={status.get('reblogs_count')} />
|
||||
</span>
|
||||
</a>
|
||||
</>
|
||||
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||
<span className='detailed-status__reblogs'>
|
||||
<AnimatedNumber value={status.get('reblogs_count')} />
|
||||
</span>
|
||||
<FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.history) {
|
||||
favouriteLink = (
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'>
|
||||
<Icon id='star' icon={StarIcon} />
|
||||
<span className='detailed-status__favorites'>
|
||||
<AnimatedNumber value={status.get('favourites_count')} />
|
||||
</span>
|
||||
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
favouriteLink = (
|
||||
<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'>
|
||||
<AnimatedNumber value={status.get('favourites_count')} />
|
||||
</span>
|
||||
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (status.get('edited_at')) {
|
||||
edited = (
|
||||
<>
|
||||
{' · '}
|
||||
<EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
|
||||
|
||||
|
@ -310,9 +290,23 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
{expanded && hashtagBar}
|
||||
|
||||
<div className='detailed-status__meta'>
|
||||
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
|
||||
<FormattedDate value={new Date(status.get('created_at'))} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
||||
</a>{edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
|
||||
<div className='detailed-status__meta__line'>
|
||||
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
|
||||
<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>
|
||||
|
|
|
@ -272,6 +272,7 @@
|
|||
"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.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",
|
||||
"firehose.all": "Tots",
|
||||
"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_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.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_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_following_hint": "Fins que no ho aproveu de forma manual",
|
||||
"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.title": "Filtra les notificacions de…",
|
||||
"notifications_permission_banner.enable": "Activa les notificacions d’escriptori",
|
||||
|
|
|
@ -662,10 +662,11 @@
|
|||
"status.direct": "Privately mention @{name}",
|
||||
"status.direct_indicator": "Private mention",
|
||||
"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.embed": "Embed",
|
||||
"status.favourite": "Favorite",
|
||||
"status.favourites": "{count, plural, one {favorite} other {favorites}}",
|
||||
"status.filter": "Filter this post",
|
||||
"status.filtered": "Filtered",
|
||||
"status.hide": "Hide post",
|
||||
|
@ -686,6 +687,7 @@
|
|||
"status.reblog": "Boost",
|
||||
"status.reblog_private": "Boost with original visibility",
|
||||
"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.redraft": "Delete & re-draft",
|
||||
"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.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.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.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.",
|
||||
|
@ -271,6 +272,7 @@
|
|||
"filter_modal.select_filter.subtitle": "Käytä olemassa olevaa luokkaa tai luo uusi",
|
||||
"filter_modal.select_filter.title": "Suodata tämä 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",
|
||||
"firehose.all": "Kaikki",
|
||||
"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.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_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_following_hint": "Kunnes hyväksyt ne manuaalisesti",
|
||||
"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.title": "Suodata ilmoitukset pois kohteesta…",
|
||||
"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.title": "סינון ההודעה הזו",
|
||||
"filter_modal.title.status": "סנן הודעה",
|
||||
"filtered_notifications_banner.pending_requests": "{count, plural,=0 {אין התראות ממשתמשים ה}one {התראה אחת ממישהו/מישהי ה}two {יש התראותיים ממשתמשים }other {יש # התראות ממשתמשים }}מוכרים לך",
|
||||
"filtered_notifications_banner.title": "התראות מסוננות",
|
||||
"firehose.all": "הכל",
|
||||
"firehose.local": "שרת זה",
|
||||
|
|
|
@ -320,7 +320,7 @@
|
|||
"mute_modal.hide_notifications": "Tebɣiḍ ad teffreḍ talɣutin n umseqdac-a?",
|
||||
"mute_modal.indefinite": "Ur yettwasbadu ara",
|
||||
"navigation_bar.about": "Ɣef",
|
||||
"navigation_bar.blocks": "Imseqdacen yettusḥebsen",
|
||||
"navigation_bar.blocks": "Iseqdacen yettusḥebsen",
|
||||
"navigation_bar.bookmarks": "Ticraḍ",
|
||||
"navigation_bar.community_timeline": "Tasuddemt tadigant",
|
||||
"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.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.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.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.",
|
||||
|
@ -271,6 +272,8 @@
|
|||
"filter_modal.select_filter.subtitle": "Bruk ein eksisterande kategori eller opprett ein ny",
|
||||
"filter_modal.select_filter.title": "Filtrer dette innlegget",
|
||||
"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.local": "Denne tenaren",
|
||||
"firehose.remote": "Andre tenarar",
|
||||
|
@ -439,6 +442,10 @@
|
|||
"notification.reblog": "{name} framheva innlegget ditt",
|
||||
"notification.status": "{name} la nettopp ut",
|
||||
"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_confirmation": "Er du sikker på at du vil fjerna alle varsla dine for alltid?",
|
||||
"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_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.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.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",
|
||||
|
|
|
@ -272,6 +272,7 @@
|
|||
"filter_modal.select_filter.subtitle": "Utilize uma categoria existente ou crie uma nova",
|
||||
"filter_modal.select_filter.title": "Filtrar esta 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",
|
||||
"firehose.all": "Todas",
|
||||
"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_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.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_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_following_hint": "Até que você os aprove manualmente",
|
||||
"notifications.policy.filter_not_following_title": "Pessoas que você não segue",
|
||||
|
|
|
@ -241,6 +241,7 @@
|
|||
"empty_column.list": "ยังไม่มีสิ่งใดในรายการนี้ เมื่อสมาชิกของรายการนี้โพสต์โพสต์ใหม่ โพสต์จะปรากฏที่นี่",
|
||||
"empty_column.lists": "คุณยังไม่มีรายการใด ๆ เมื่อคุณสร้างรายการ รายการจะปรากฏที่นี่",
|
||||
"empty_column.mutes": "คุณยังไม่ได้ซ่อนผู้ใช้ใด ๆ",
|
||||
"empty_column.notification_requests": "โล่งทั้งหมด! ไม่มีสิ่งใดที่นี่ เมื่อคุณได้รับการแจ้งเตือนใหม่ การแจ้งเตือนจะปรากฏที่นี่ตามการตั้งค่าของคุณ",
|
||||
"empty_column.notifications": "คุณยังไม่มีการแจ้งเตือนใด ๆ เมื่อผู้คนอื่น ๆ โต้ตอบกับคุณ คุณจะเห็นการแจ้งเตือนที่นี่",
|
||||
"empty_column.public": "ไม่มีสิ่งใดที่นี่! เขียนบางอย่างเป็นสาธารณะ หรือติดตามผู้ใช้จากเซิร์ฟเวอร์อื่น ๆ ด้วยตนเองเพื่อเติมเส้นเวลาให้เต็ม",
|
||||
"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.title": "Lọc tút này",
|
||||
"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",
|
||||
"firehose.all": "Toàn bộ",
|
||||
"firehose.local": "Máy chủ này",
|
||||
|
@ -364,7 +364,7 @@
|
|||
"keyboard_shortcuts.my_profile": "mở hồ sơ của bạn",
|
||||
"keyboard_shortcuts.notifications": "mở thông báo",
|
||||
"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.reply": "trả lờ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 {
|
||||
margin-top: 16px;
|
||||
margin-top: 24px;
|
||||
color: $dark-text-color;
|
||||
font-size: 14px;
|
||||
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 {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
vertical-align: middle;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.animated-number {
|
||||
color: $secondary-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1711,19 +1731,6 @@ body > [data-popper-placement] {
|
|||
color: inherit;
|
||||
text-decoration: none;
|
||||
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 {
|
||||
|
@ -2292,6 +2299,10 @@ a.account__display-name {
|
|||
outline: 1px dotted;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
|
@ -3485,7 +3496,7 @@ $ui-header-height: 55px;
|
|||
}
|
||||
|
||||
.column-subheading {
|
||||
background: darken($ui-base-color, 4%);
|
||||
background: var(--surface-background-color);
|
||||
color: $darker-text-color;
|
||||
padding: 8px 20px;
|
||||
font-size: 12px;
|
||||
|
@ -4637,7 +4648,7 @@ a.status-card {
|
|||
}
|
||||
|
||||
.follow_requests-unlocked_explanation {
|
||||
background: darken($ui-base-color, 4%);
|
||||
background: var(--surface-background-color);
|
||||
border-bottom: 1px solid var(--background-border-color);
|
||||
contain: initial;
|
||||
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 {
|
||||
border-bottom: 1px solid var(--background-border-color);
|
||||
|
||||
|
@ -5289,8 +5288,8 @@ a.status-card {
|
|||
}
|
||||
|
||||
&__header {
|
||||
background: darken($ui-base-color, 4%);
|
||||
border-bottom: 1px solid var(--background-border-color);
|
||||
background: var(--surface-background-color);
|
||||
padding: 15px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
|
@ -7159,7 +7158,7 @@ noscript {
|
|||
.follow-request-banner,
|
||||
.account-memorial-banner {
|
||||
padding: 20px;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
background: var(--surface-background-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
@ -8326,7 +8325,8 @@ noscript {
|
|||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: $ui-base-color;
|
||||
border: 1px solid var(--background-border-color);
|
||||
border-top: 0;
|
||||
border-bottom-left-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-color: #{darken($ui-base-color, 8%)};
|
||||
--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
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ module Account::StatusesSearch
|
|||
def add_to_public_statuses_index!
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ module Status::SearchConcern
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
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
|
||||
|
||||
def searchable_by
|
||||
|
|
|
@ -23,7 +23,7 @@ class IpBlock < ApplicationRecord
|
|||
sign_up_requires_approval: 5000,
|
||||
sign_up_block: 5500,
|
||||
no_access: 9999,
|
||||
}
|
||||
}, prefix: true
|
||||
|
||||
validates :ip, :severity, presence: true
|
||||
validates :ip, uniqueness: true
|
||||
|
|
|
@ -71,7 +71,7 @@ class PublicFeed
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def local_only_scope
|
||||
|
|
|
@ -111,7 +111,6 @@ class Status < ApplicationRecord
|
|||
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_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 :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) }
|
||||
|
@ -495,7 +494,6 @@ class Status < ApplicationRecord
|
|||
def set_visibility
|
||||
self.visibility = reblog.visibility if reblog? && visibility.nil?
|
||||
self.visibility = (account.locked? ? :private : :public) if visibility.nil?
|
||||
self.sensitive = false if sensitive.nil?
|
||||
end
|
||||
|
||||
def set_local_only
|
||||
|
|
|
@ -446,7 +446,7 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def sign_up_email_requires_approval?
|
||||
|
|
|
@ -1846,7 +1846,10 @@ fi:
|
|||
apps_ios_action: Lataa App Storesta
|
||||
apps_step: Lataa viralliset sovelluksemme.
|
||||
apps_title: Mastodon-sovellukset
|
||||
checklist_subtitle: 'Aloitetaan, sinä aloitat uudella sosiaalisella seudulla:'
|
||||
checklist_title: Tervetuloa tarkistuslista
|
||||
edit_profile_action: Mukauta
|
||||
edit_profile_step: Täydentämällä profiilisi tietoja tehostat vaikutemaa.
|
||||
edit_profile_title: Mukauta profiiliasi
|
||||
explanation: Näillä vinkeillä pääset alkuun
|
||||
feature_action: Lue lisää
|
||||
|
|
|
@ -705,6 +705,7 @@ kab:
|
|||
moved: Igujj
|
||||
primary: Agejdan
|
||||
relationship: Assaɣ
|
||||
remove_selected_follows: Ur ṭṭafar ara iseqdacen yettwafernen
|
||||
status: Addad n umiḍan
|
||||
sessions:
|
||||
activity: Armud aneggaru
|
||||
|
@ -729,6 +730,7 @@ kab:
|
|||
current_session: Tiɣimit tamirant
|
||||
date: Azemz
|
||||
description: "%{browser} s %{platform}"
|
||||
explanation: Ha-t-en yiminigen web ikecmen akka tura ɣer umiḍan-ik·im Mastodon.
|
||||
ip: IP
|
||||
platforms:
|
||||
adobe_air: Adobe Air
|
||||
|
|
|
@ -23,6 +23,8 @@ kab:
|
|||
setting_display_media_hide_all: Ffer yal tikkelt akk taywalt
|
||||
setting_display_media_show_all: Ffer yal tikkelt teywalt yettwacreḍ d tanafrit
|
||||
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:
|
||||
data: Afaylu CSV id yusan seg uqeddac-nniḍen n Maṣṭudun
|
||||
ip_block:
|
||||
|
@ -102,7 +104,9 @@ kab:
|
|||
no_access: Sewḥel anekcum
|
||||
severity: Alugen
|
||||
notification_emails:
|
||||
favourite: Ma yella walbɛaḍ i iḥemmlen tasuffeɣt-ik·im
|
||||
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ḍ
|
||||
reblog: Yella win yesselhan adda-dik·im
|
||||
rule:
|
||||
|
|
|
@ -78,8 +78,10 @@ Rails.application.routes.draw do
|
|||
get 'remote_interaction_helper', to: 'remote_interaction_helper#index'
|
||||
|
||||
resource :instance_actor, path: 'actor', only: [:show] do
|
||||
resource :inbox, only: [:create], module: :activitypub
|
||||
resource :outbox, only: [:show], module: :activitypub
|
||||
scope module: :activitypub do
|
||||
resource :inbox, only: [:create]
|
||||
resource :outbox, only: [:show]
|
||||
end
|
||||
end
|
||||
|
||||
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 :following, only: [:index], controller: :following_accounts
|
||||
|
||||
resource :outbox, only: [:show], module: :activitypub
|
||||
resource :inbox, only: [:create], module: :activitypub
|
||||
resource :claim, only: [:create], module: :activitypub
|
||||
resources :collections, only: [:show], module: :activitypub
|
||||
resource :followers_synchronization, only: [:show], module: :activitypub
|
||||
scope module: :activitypub do
|
||||
resource :outbox, only: [:show]
|
||||
resource :inbox, only: [:create]
|
||||
resource :claim, only: [:create]
|
||||
resources :collections, only: [:show]
|
||||
resource :followers_synchronization, only: [:show]
|
||||
end
|
||||
end
|
||||
|
||||
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/ }
|
||||
|
||||
constraints(username: %r{[^@/.]+}) do
|
||||
get '/@:username', to: 'accounts#show', as: :short_account
|
||||
get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies
|
||||
get '/@:username/media', to: 'accounts#show', as: :short_account_media
|
||||
get '/@:username/tagged/:tag', to: 'accounts#show', as: :short_account_tag
|
||||
with_options to: 'accounts#show' do
|
||||
get '/@:username', as: :short_account
|
||||
get '/@:username/with_replies', as: :short_account_with_replies
|
||||
get '/@:username/media', as: :short_account_media
|
||||
get '/@:username/tagged/:tag', as: :short_account_tag
|
||||
end
|
||||
end
|
||||
|
||||
constraints(account_username: %r{[^@/.]+}) do
|
||||
|
|
|
@ -126,14 +126,16 @@ namespace :api, format: false do
|
|||
end
|
||||
|
||||
resource :instance, only: [:show] do
|
||||
resources :peers, only: [:index], controller: 'instances/peers'
|
||||
resources :rules, only: [:index], controller: 'instances/rules'
|
||||
resources :domain_blocks, only: [:index], controller: 'instances/domain_blocks'
|
||||
resource :privacy_policy, only: [:show], controller: 'instances/privacy_policies'
|
||||
resource :extended_description, only: [:show], controller: 'instances/extended_descriptions'
|
||||
resource :translation_languages, only: [:show], controller: 'instances/translation_languages'
|
||||
resource :languages, only: [:show], controller: 'instances/languages'
|
||||
resource :activity, only: [:show], controller: 'instances/activity'
|
||||
scope module: :instances do
|
||||
resources :peers, only: [:index]
|
||||
resources :rules, only: [:index]
|
||||
resources :domain_blocks, only: [:index]
|
||||
resource :privacy_policy, only: [:show]
|
||||
resource :extended_description, only: [:show]
|
||||
resource :translation_languages, only: [:show]
|
||||
resource :languages, only: [:show]
|
||||
resource :activity, only: [:show], controller: :activity
|
||||
end
|
||||
end
|
||||
|
||||
namespace :peers do
|
||||
|
@ -183,12 +185,14 @@ namespace :api, format: false do
|
|||
end
|
||||
|
||||
resources :accounts, only: [:create, :show] do
|
||||
resources :statuses, only: :index, controller: 'accounts/statuses'
|
||||
resources :followers, only: :index, controller: 'accounts/follower_accounts'
|
||||
resources :following, only: :index, controller: 'accounts/following_accounts'
|
||||
resources :lists, only: :index, controller: 'accounts/lists'
|
||||
resources :identity_proofs, only: :index, controller: 'accounts/identity_proofs'
|
||||
resources :featured_tags, only: :index, controller: 'accounts/featured_tags'
|
||||
scope module: :accounts do
|
||||
resources :statuses, only: :index
|
||||
resources :followers, only: :index, controller: :follower_accounts
|
||||
resources :following, only: :index, controller: :following_accounts
|
||||
resources :lists, only: :index
|
||||
resources :identity_proofs, only: :index
|
||||
resources :featured_tags, only: :index
|
||||
end
|
||||
|
||||
member do
|
||||
post :follow
|
||||
|
|
|
@ -295,7 +295,7 @@ module Mastodon::CLI
|
|||
skip_threshold = 7.days.ago
|
||||
skip_domains = Concurrent::Set.new
|
||||
|
||||
query = Account.remote.where(protocol: :activitypub)
|
||||
query = Account.remote.activitypub
|
||||
query = query.where(domain: domains) unless domains.empty?
|
||||
|
||||
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.
|
||||
LONG_DESC
|
||||
def export
|
||||
IpBlock.where(severity: :no_access).find_each do |ip_block|
|
||||
IpBlock.severity_no_access.find_each do |ip_block|
|
||||
case options[:format]
|
||||
when 'nginx'
|
||||
say "deny #{ip_block.ip}/#{ip_block.ip.prefix};"
|
||||
|
|
|
@ -277,7 +277,7 @@ module Mastodon::CLI
|
|||
|
||||
desc 'usage', 'Calculate disk space consumed by Mastodon'
|
||||
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("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)")
|
||||
|
@ -317,6 +317,22 @@ module Mastodon::CLI
|
|||
|
||||
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(
|
||||
Account
|
||||
Backup
|
||||
|
|
|
@ -3,10 +3,6 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe Api::BaseController do
|
||||
before do
|
||||
stub_const('FakeService', Class.new)
|
||||
end
|
||||
|
||||
controller do
|
||||
def success
|
||||
head 200
|
||||
|
@ -72,36 +68,4 @@ describe Api::BaseController do
|
|||
expect(response).to have_http_status(403)
|
||||
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
|
||||
|
|
51
spec/controllers/concerns/api/error_handling_spec.rb
Normal file
51
spec/controllers/concerns/api/error_handling_spec.rb
Normal file
|
@ -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'))
|
||||
|
||||
# Failing to log-in presents the form again
|
||||
fill_in 'user_email', with: email
|
||||
fill_in 'user_password', with: 'wrong password'
|
||||
click_on I18n.t('auth.login')
|
||||
fill_in_auth_details(email, 'wrong password')
|
||||
expect(page).to have_content(I18n.t('auth.login'))
|
||||
|
||||
# Logging in redirects to an authorization page
|
||||
fill_in 'user_email', with: email
|
||||
fill_in 'user_password', with: password
|
||||
click_on I18n.t('auth.login')
|
||||
fill_in_auth_details(email, password)
|
||||
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
|
||||
|
||||
# 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'))
|
||||
|
||||
# Failing to log-in presents the form again
|
||||
fill_in 'user_email', with: email
|
||||
fill_in 'user_password', with: 'wrong password'
|
||||
click_on I18n.t('auth.login')
|
||||
fill_in_auth_details(email, 'wrong password')
|
||||
expect(page).to have_content(I18n.t('auth.login'))
|
||||
|
||||
# Logging in redirects to an authorization page
|
||||
fill_in 'user_email', with: email
|
||||
fill_in 'user_password', with: password
|
||||
click_on I18n.t('auth.login')
|
||||
fill_in_auth_details(email, password)
|
||||
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
|
||||
|
||||
# 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'))
|
||||
|
||||
# Failing to log-in presents the form again
|
||||
fill_in 'user_email', with: email
|
||||
fill_in 'user_password', with: 'wrong password'
|
||||
click_on I18n.t('auth.login')
|
||||
fill_in_auth_details(email, 'wrong password')
|
||||
expect(page).to have_content(I18n.t('auth.login'))
|
||||
|
||||
# Logging in redirects to a two-factor authentication page
|
||||
fill_in 'user_email', with: email
|
||||
fill_in 'user_password', with: password
|
||||
click_on I18n.t('auth.login')
|
||||
fill_in_auth_details(email, password)
|
||||
expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
|
||||
|
||||
# Filling in an incorrect two-factor authentication code presents the form again
|
||||
fill_in 'user_otp_attempt', with: 'wrong'
|
||||
click_on I18n.t('auth.login')
|
||||
fill_in_otp_details('wrong')
|
||||
expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
|
||||
|
||||
# Filling in the correct TOTP code redirects to an app authorization page
|
||||
fill_in 'user_otp_attempt', with: user.current_otp
|
||||
click_on I18n.t('auth.login')
|
||||
fill_in_otp_details(user.current_otp)
|
||||
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
|
||||
|
||||
# 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'))
|
||||
|
||||
# Failing to log-in presents the form again
|
||||
fill_in 'user_email', with: email
|
||||
fill_in 'user_password', with: 'wrong password'
|
||||
click_on I18n.t('auth.login')
|
||||
fill_in_auth_details(email, 'wrong password')
|
||||
expect(page).to have_content(I18n.t('auth.login'))
|
||||
|
||||
# Logging in redirects to a two-factor authentication page
|
||||
fill_in 'user_email', with: email
|
||||
fill_in 'user_password', with: password
|
||||
click_on I18n.t('auth.login')
|
||||
fill_in_auth_details(email, password)
|
||||
expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
|
||||
|
||||
# Filling in an incorrect two-factor authentication code presents the form again
|
||||
fill_in 'user_otp_attempt', with: 'wrong'
|
||||
click_on I18n.t('auth.login')
|
||||
fill_in_otp_details('wrong')
|
||||
expect(page).to have_content(I18n.t('simple_form.hints.sessions.otp'))
|
||||
|
||||
# Filling in the correct TOTP code redirects to an app authorization page
|
||||
fill_in 'user_otp_attempt', with: user.current_otp
|
||||
click_on I18n.t('auth.login')
|
||||
fill_in_otp_details(user.current_otp)
|
||||
expect(page).to have_content(I18n.t('doorkeeper.authorizations.buttons.authorize'))
|
||||
|
||||
# Upon denying, it redirects to the apps' callback URL
|
||||
|
@ -185,6 +165,19 @@ describe 'Using OAuth from an external app' do
|
|||
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
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,6 +15,23 @@ describe Mastodon::CLI::Domains do
|
|||
describe '#purge' do
|
||||
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
|
||||
let(:domain) { 'host.example' }
|
||||
let!(:account) { Fabricate(:account, domain: domain) }
|
||||
|
|
|
@ -13,7 +13,26 @@ RSpec.describe Vacuum::ImportsVacuum do
|
|||
|
||||
describe '#perform' 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
|
||||
|
|
|
@ -21,7 +21,7 @@ describe Account::StatusesSearch, :sidekiq_inline do
|
|||
account.indexable = true
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
@ -32,7 +32,7 @@ describe Account::StatusesSearch, :sidekiq_inline do
|
|||
|
||||
context 'when picking an indexable account' 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
|
||||
|
||||
it 'has statuses in the StatusesIndex' do
|
||||
|
|
Loading…
Reference in a new issue