Merge pull request #2676 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes up to df6086d402
main
Claire 2024-03-14 21:39:48 +01:00 committed by GitHub
commit b834f41ec1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 430 additions and 332 deletions

View File

@ -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

View 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

View File

@ -640,7 +640,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));
@ -673,7 +676,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));

View File

@ -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}

View File

@ -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 'flavours/glitch/actions/modal'; import { openModal } from 'flavours/glitch/actions/modal';
import { Icon } from 'flavours/glitch/components/icon';
import InlineAccount from 'flavours/glitch/components/inline_account'; import InlineAccount from 'flavours/glitch/components/inline_account';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp'; import { RelativeTimestamp } from 'flavours/glitch/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>
); );

View File

@ -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 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import { LoadMore } from 'flavours/glitch/components/load_more'; import { LoadMore } from 'flavours/glitch/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}

View File

@ -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}>

View File

@ -1,6 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormattedDate } from 'react-intl'; import { FormattedDate, FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Link, withRouter } from 'react-router-dom'; 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 ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; 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 { AnimatedNumber } from 'flavours/glitch/components/animated_number';
import AttachmentList from 'flavours/glitch/components/attachment_list'; import AttachmentList from 'flavours/glitch/components/attachment_list';
import EditedTimestamp from 'flavours/glitch/components/edited_timestamp'; import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';
import { getHashtagBarForStatus } from 'flavours/glitch/components/hashtag_bar'; 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 PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon'; import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon';
import PollContainer from 'flavours/glitch/containers/poll_container'; import PollContainer from 'flavours/glitch/containers/poll_container';
@ -133,10 +129,7 @@ class DetailedStatus extends ImmutablePureComponent {
let applicationLink = ''; let applicationLink = '';
let reblogLink = ''; let reblogLink = '';
const reblogIcon = 'retweet';
const reblogIconComponent = RepeatIcon;
let favouriteLink = ''; let favouriteLink = '';
let edited = '';
// Depending on user settings, some media are considered as parts of the // Depending on user settings, some media are considered as parts of the
// contents (affected by CW) while other will be displayed outside of the // contents (affected by CW) while other will be displayed outside of the
@ -248,59 +241,44 @@ class DetailedStatus extends ImmutablePureComponent {
reblogLink = null; reblogLink = null;
} 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'> <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
<Icon id={reblogIcon} icon={reblogIconComponent} />
<span className='detailed-status__reblogs'> <span className='detailed-status__reblogs'>
<AnimatedNumber value={status.get('reblogs_count')} /> <AnimatedNumber value={status.get('reblogs_count')} />
</span> </span>
<FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
</Link> </Link>
</>
); );
} else { } else {
reblogLink = ( reblogLink = (
<>
{' · '}
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}> <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'> <span className='detailed-status__reblogs'>
<AnimatedNumber value={status.get('reblogs_count')} /> <AnimatedNumber value={status.get('reblogs_count')} />
</span> </span>
<FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
</a> </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);
contentMedia.push(hashtagBar); contentMedia.push(hashtagBar);
@ -330,9 +308,23 @@ class DetailedStatus extends ImmutablePureComponent {
/> />
<div className='detailed-status__meta'> <div className='detailed-status__meta'>
<div className='detailed-status__meta__line'>
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'> <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' /> <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} </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>

View File

@ -1871,15 +1871,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;
} }
} }
@ -1923,19 +1943,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 {
@ -2506,6 +2513,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;
@ -3699,7 +3710,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;
@ -5023,7 +5034,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;
@ -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 { .search-results__section {
border-bottom: 1px solid var(--background-border-color); border-bottom: 1px solid var(--background-border-color);
@ -5787,8 +5786,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;
@ -7740,7 +7739,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;
@ -8914,7 +8913,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;
} }

View File

@ -110,4 +110,5 @@ $dismiss-overlay-width: 4rem;
--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%)};
} }

View File

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

View File

@ -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}

View File

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

View File

@ -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}

View File

@ -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}>

View File

@ -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`;
@ -227,59 +222,44 @@ class DetailedStatus extends ImmutablePureComponent {
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'> <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
<Icon id={reblogIcon} icon={reblogIconComponent} />
<span className='detailed-status__reblogs'> <span className='detailed-status__reblogs'>
<AnimatedNumber value={status.get('reblogs_count')} /> <AnimatedNumber value={status.get('reblogs_count')} />
</span> </span>
<FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
</Link> </Link>
</>
); );
} else { } else {
reblogLink = ( reblogLink = (
<>
{' · '}
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}> <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'> <span className='detailed-status__reblogs'>
<AnimatedNumber value={status.get('reblogs_count')} /> <AnimatedNumber value={status.get('reblogs_count')} />
</span> </span>
<FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
</a> </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'>
<div className='detailed-status__meta__line'>
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'> <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' /> <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} </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>

View File

@ -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 descriptori no estan disponibles perquè prèviament sha denegat el permís al navegador", "notifications.permission_denied": "Les notificacions descriptori no estan disponibles perquè prèviament sha 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 descriptori", "notifications_permission_banner.enable": "Activa les notificacions descriptori",

View File

@ -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",

View File

@ -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",

View File

@ -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": "שרת זה",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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": "เนื่องจากข้อบกพร่องในโค้ดของเราหรือปัญหาความเข้ากันได้ของเบราว์เซอร์ จึงไม่สามารถแสดงหน้านี้ได้อย่างถูกต้อง",

View File

@ -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",

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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ää

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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|

View File

@ -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};"

View File

@ -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

View File

@ -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

View 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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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