diff options
93 files changed, 1605 insertions, 416 deletions
diff --git a/.env.production.sample b/.env.production.sample index a64959c77..3388d380a 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -169,15 +169,12 @@ STREAMING_CLUSTER_NUM=1 # Maximum allowed display name characters # MAX_DISPLAY_NAME_CHARS=30 -# Maximum image and video upload sizes +# Maximum image and video/audio upload sizes # Units are in bytes # 1048576 bytes equals 1 megabyte # MAX_IMAGE_SIZE=8388608 # MAX_VIDEO_SIZE=41943040 -# Maximum length of audio uploads in seconds -# MAX_AUDIO_LENGTH=60 - # LDAP authentication (optional) # LDAP_ENABLED=true # LDAP_HOST=localhost diff --git a/CHANGELOG.md b/CHANGELOG.md index c89f35cdf..539fec531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,45 @@ Changelog All notable changes to this project will be documented in this file. +## [2.9.2] - 2019-06-22 +### Added + +- Add `short_description` and `approval_required` to `GET /api/v1/instance` ([Gargron](https://github.com/tootsuite/mastodon/pull/11146)) + +### Changed + +- Change camera icon to paperclip icon in upload form ([koyuawsmbrtn](https://github.com/tootsuite/mastodon/pull/11149)) + +### Fixed + +- Fix audio-only OGG and WebM files not being processed as such ([Gargron](https://github.com/tootsuite/mastodon/pull/11151)) +- Fix audio not being downloaded from remote servers ([Gargron](https://github.com/tootsuite/mastodon/pull/11145)) + +## [2.9.1] - 2019-06-22 +### Added + +- Add moderation API ([Gargron](https://github.com/tootsuite/mastodon/pull/9387)) +- Add audio uploads ([Gargron](https://github.com/tootsuite/mastodon/pull/11123), [Gargron](https://github.com/tootsuite/mastodon/pull/11141)) + +### Changed + +- Change domain blocks to automatically support subdomains ([Gargron](https://github.com/tootsuite/mastodon/pull/11138)) +- Change Nanobox configuration to bring it up to date ([danhunsaker](https://github.com/tootsuite/mastodon/pull/11083)) + +### Removed + +- Remove expensive counters from federation page in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11139)) + +### Fixed + +- Fix converted media being saved with original extension and mime type ([Gargron](https://github.com/tootsuite/mastodon/pull/11130)) +- Fix layout of identity proofs settings ([acid-chicken](https://github.com/tootsuite/mastodon/pull/11126)) +- Fix active scope only returning suspended users ([ThibG](https://github.com/tootsuite/mastodon/pull/11111)) +- Fix sanitizer making block level elements unreadable ([Gargron](https://github.com/tootsuite/mastodon/pull/10836)) +- Fix label for site theme not being translated in admin UI ([palindromordnilap](https://github.com/tootsuite/mastodon/pull/11121)) +- Fix statuses not being filtered irreversibly in web UI under some circumstances ([ThibG](https://github.com/tootsuite/mastodon/pull/11113)) +- Fix scrolling behaviour in compose form ([ThibG](https://github.com/tootsuite/mastodon/pull/11093)) + ## [2.9.0] - 2019-06-13 ### Added diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index b0d45ce47..0c7760d77 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -127,6 +127,7 @@ module Admin :by_domain, :active, :pending, + :disabled, :silenced, :suspended, :username, diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 71597763b..377cac8ad 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -13,7 +13,7 @@ module Admin authorize :domain_block, :create? @domain_block = DomainBlock.new(resource_params) - existing_domain_block = resource_params[:domain].present? ? DomainBlock.find_by(domain: resource_params[:domain]) : nil + existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) @domain_block.save diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index 6dd659a30..7888e844f 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -18,7 +18,7 @@ module Admin @blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count @available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url) @media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size) - @domain_block = DomainBlock.find_by(domain: params[:id]) + @domain_block = DomainBlock.rule_for(params[:id]) end private diff --git a/app/controllers/api/v1/admin/account_actions_controller.rb b/app/controllers/api/v1/admin/account_actions_controller.rb new file mode 100644 index 000000000..29c9b7107 --- /dev/null +++ b/app/controllers/api/v1/admin/account_actions_controller.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class Api::V1::Admin::AccountActionsController < Api::BaseController + before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' } + before_action :require_staff! + before_action :set_account + + def create + account_action = Admin::AccountAction.new(resource_params) + account_action.target_account = @account + account_action.current_account = current_account + account_action.save! + + render_empty + end + + private + + def set_account + @account = Account.find(params[:account_id]) + end + + def resource_params + params.permit( + :type, + :report_id, + :warning_preset_id, + :text, + :send_email_notification + ) + end +end diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb new file mode 100644 index 000000000..c306180ca --- /dev/null +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +class Api::V1::Admin::AccountsController < Api::BaseController + include Authorization + include AccountableConcern + + LIMIT = 100 + + before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:accounts' }, only: [:index, :show] + before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }, except: [:index, :show] + before_action :require_staff! + before_action :set_accounts, only: :index + before_action :set_account, except: :index + before_action :require_local_account!, only: [:enable, :approve, :reject] + + after_action :insert_pagination_headers, only: :index + + FILTER_PARAMS = %i( + local + remote + by_domain + active + pending + disabled + silenced + suspended + username + display_name + email + ip + staff + ).freeze + + PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze + + def index + authorize :account, :index? + render json: @accounts, each_serializer: REST::Admin::AccountSerializer + end + + def show + authorize @account, :show? + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def enable + authorize @account.user, :enable? + @account.user.enable! + log_action :enable, @account.user + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def approve + authorize @account.user, :approve? + @account.user.approve! + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def reject + authorize @account.user, :reject? + SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true) + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def unsilence + authorize @account, :unsilence? + @account.unsilence! + log_action :unsilence, @account + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def unsuspend + authorize @account, :unsuspend? + @account.unsuspend! + log_action :unsuspend, @account + render json: @account, serializer: REST::Admin::AccountSerializer + end + + private + + def set_accounts + @accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def set_account + @account = Account.find(params[:id]) + end + + def filtered_accounts + AccountFilter.new(filter_params).results + end + + def filter_params + params.permit(*FILTER_PARAMS) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty? + end + + def pagination_max_id + @accounts.last.id + end + + def pagination_since_id + @accounts.first.id + end + + def records_continue? + @accounts.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end + + def require_local_account! + forbidden unless @account.local? && @account.user.present? + end +end diff --git a/app/controllers/api/v1/admin/reports_controller.rb b/app/controllers/api/v1/admin/reports_controller.rb new file mode 100644 index 000000000..1d48d3160 --- /dev/null +++ b/app/controllers/api/v1/admin/reports_controller.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +class Api::V1::Admin::ReportsController < Api::BaseController + include Authorization + include AccountableConcern + + LIMIT = 100 + + before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:reports' }, only: [:index, :show] + before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:reports' }, except: [:index, :show] + before_action :require_staff! + before_action :set_reports, only: :index + before_action :set_report, except: :index + + after_action :insert_pagination_headers, only: :index + + FILTER_PARAMS = %i( + resolved + account_id + target_account_id + ).freeze + + PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze + + def index + authorize :report, :index? + render json: @reports, each_serializer: REST::Admin::ReportSerializer + end + + def show + authorize @report, :show? + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def assign_to_self + authorize @report, :update? + @report.update!(assigned_account_id: current_account.id) + log_action :assigned_to_self, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def unassign + authorize @report, :update? + @report.update!(assigned_account_id: nil) + log_action :unassigned, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def reopen + authorize @report, :update? + @report.unresolve! + log_action :reopen, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def resolve + authorize @report, :update? + @report.resolve!(current_account) + log_action :resolve, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + private + + def set_reports + @reports = filtered_reports.order(id: :desc).with_accounts.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def set_report + @report = Report.find(params[:id]) + end + + def filtered_reports + ReportFilter.new(filter_params).results + end + + def filter_params + params.permit(*FILTER_PARAMS) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_reports_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_reports_url(pagination_params(min_id: pagination_since_id)) unless @reports.empty? + end + + def pagination_max_id + @reports.last.id + end + + def pagination_since_id + @reports.first.id + end + + def records_continue? + @reports.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end +end diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index a245db2d1..d44b52d26 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -7,6 +7,8 @@ class MediaController < ApplicationController before_action :set_media_attachment before_action :verify_permitted_status! + before_action :check_playable, only: :player + before_action :allow_iframing, only: :player content_security_policy only: :player do |p| p.frame_ancestors(false) @@ -18,8 +20,6 @@ class MediaController < ApplicationController def player @body_classes = 'player' - response.headers['X-Frame-Options'] = 'ALLOWALL' - raise ActiveRecord::RecordNotFound unless @media_attachment.video? || @media_attachment.gifv? end private @@ -34,4 +34,12 @@ class MediaController < ApplicationController # Reraise in order to get a 404 instead of a 403 error code raise ActiveRecord::RecordNotFound end + + def check_playable + not_found unless @media_attachment.larger_media_format? + end + + def allow_iframing + response.headers['X-Frame-Options'] = 'ALLOWALL' + end end diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 950cf6d09..8fc18dd06 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -39,6 +39,6 @@ class MediaProxyController < ApplicationController end def reject_media? - DomainBlock.find_by(domain: @media_attachment.account.domain)&.reject_media? + DomainBlock.reject_media?(@media_attachment.account.domain) end end diff --git a/app/controllers/settings/identity_proofs_controller.rb b/app/controllers/settings/identity_proofs_controller.rb index 4d0938545..e84c1aca6 100644 --- a/app/controllers/settings/identity_proofs_controller.rb +++ b/app/controllers/settings/identity_proofs_controller.rb @@ -61,8 +61,4 @@ class Settings::IdentityProofsController < Settings::BaseController def post_params params.require(:account_identity_proof).permit(:post_status, :status_text) end - - def set_body_classes - @body_classes = '' - end end diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js index 6ef101f11..291caff45 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.js +++ b/app/javascript/flavours/glitch/components/media_gallery.js @@ -177,7 +177,7 @@ class Item extends React.PureComponent { if (attachment.get('type') === 'unknown') { return ( <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> - <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}> + <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}> <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' /> </a> </div> diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index f6d73475a..ed2623ebb 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -521,16 +521,16 @@ export default class Status extends ImmutablePureComponent { media={status.get('media_attachments')} /> ); - } else if (attachments.getIn([0, 'type']) === 'video') { // Media type is 'video' - const video = status.getIn(['media_attachments', 0]); + } else if (['video', 'audio'].includes(attachments.getIn([0, 'type']))) { + const attachment = status.getIn(['media_attachments', 0]); media = ( <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} > {Component => (<Component - preview={video.get('preview_url')} - blurhash={video.get('blurhash')} - src={video.get('url')} - alt={video.get('description')} + preview={attachment.get('preview_url')} + blurhash={attachment.get('blurhash')} + src={attachment.get('url')} + alt={attachment.get('description')} inline sensitive={status.get('sensitive')} letterbox={settings.getIn(['media', 'letterbox'])} @@ -544,7 +544,7 @@ export default class Status extends ImmutablePureComponent { />)} </Bundle> ); - mediaIcon = 'video-camera'; + mediaIcon = attachment.get('type') === 'video' ? 'video-camera' : 'music'; } else { // Media type is 'image' or 'gifv' media = ( <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}> diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js index cbce675d5..822cfa95d 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js @@ -296,12 +296,12 @@ class ComposeForm extends ImmutablePureComponent { let disabledButton = isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia); return ( - <div className='composer' ref={this.setRef}> + <div className='composer'> <WarningContainer /> <ReplyIndicatorContainer /> - <div className={`composer--spoiler ${spoiler ? 'composer--spoiler--visible' : ''}`}> + <div className={`composer--spoiler ${spoiler ? 'composer--spoiler--visible' : ''}`} ref={this.setRef}> <AutosuggestInput placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={spoilerText} diff --git a/app/javascript/flavours/glitch/features/compose/containers/options_container.js b/app/javascript/flavours/glitch/features/compose/containers/options_container.js index c8c7ecd43..df842f3bf 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/options_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/options_container.js @@ -16,7 +16,7 @@ function mapStateToProps (state) { acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','), resetFileKey: state.getIn(['compose', 'resetFileKey']), hasPoll: !!poll, - allowMedia: !poll && (media ? media.size < 4 && !media.some(item => item.get('type') === 'video') : true), + allowMedia: !poll && (media ? media.size < 4 && !media.some(item => ['video', 'audio'].includes(item.get('type'))) : true), hasMedia: media && !!media.size, allowPoll: !(media && !!media.size), showContentTypeChoice: state.getIn(['local_settings', 'show_content_type_choice']), diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index ddedac4d4..1c2258256 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -131,14 +131,14 @@ export default class DetailedStatus extends ImmutablePureComponent { } else if (status.get('media_attachments').size > 0) { if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { media = <AttachmentList media={status.get('media_attachments')} />; - } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - const video = status.getIn(['media_attachments', 0]); + } else if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) { + const attachment = status.getIn(['media_attachments', 0]); media = ( <Video - preview={video.get('preview_url')} - blurhash={video.get('blurhash')} - src={video.get('url')} - alt={video.get('description')} + preview={attachment.get('preview_url')} + blurhash={attachment.get('blurhash')} + src={attachment.get('url')} + alt={attachment.get('description')} inline sensitive={status.get('sensitive')} letterbox={settings.getIn(['media', 'letterbox'])} @@ -150,7 +150,7 @@ export default class DetailedStatus extends ImmutablePureComponent { onToggleVisibility={this.props.onToggleMediaVisibility} /> ); - mediaIcon = 'video-camera'; + mediaIcon = attachment.get('type') === 'video' ? 'video-camera' : 'music'; } else { media = ( <MediaGallery diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss index c06d79ffc..3eb5551c6 100644 --- a/app/javascript/flavours/glitch/styles/components/composer.scss +++ b/app/javascript/flavours/glitch/styles/components/composer.scss @@ -370,6 +370,7 @@ border-radius: 4px; height: 140px; width: 100%; + background-color: $base-shadow-color; background-position: center; background-size: cover; background-repeat: no-repeat; diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 56618462b..77bac61ee 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -157,7 +157,7 @@ class Item extends React.PureComponent { if (attachment.get('type') === 'unknown') { return ( <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> - <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}> + <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}> <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' /> </a> </div> diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index aa5e870dc..9b1035649 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -333,17 +333,17 @@ class Status extends ImmutablePureComponent { media={status.get('media_attachments')} /> ); - } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - const video = status.getIn(['media_attachments', 0]); + } else if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) { + const attachment = status.getIn(['media_attachments', 0]); media = ( <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} > {Component => ( <Component - preview={video.get('preview_url')} - blurhash={video.get('blurhash')} - src={video.get('url')} - alt={video.get('description')} + preview={attachment.get('preview_url')} + blurhash={attachment.get('blurhash')} + src={attachment.get('url')} + alt={attachment.get('description')} width={this.props.cachedMediaWidth} height={110} inline diff --git a/app/javascript/mastodon/features/compose/components/upload_button.js b/app/javascript/mastodon/features/compose/components/upload_button.js index 90e2769f3..d550019f4 100644 --- a/app/javascript/mastodon/features/compose/components/upload_button.js +++ b/app/javascript/mastodon/features/compose/components/upload_button.js @@ -7,9 +7,11 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePropTypes from 'react-immutable-proptypes'; const messages = defineMessages({ - upload: { id: 'upload_button.label', defaultMessage: 'Add media (JPEG, PNG, GIF, WebM, MP4, MOV)' }, + upload: { id: 'upload_button.label', defaultMessage: 'Add media ({formats})' }, }); +const SUPPORTED_FORMATS = 'JPEG, PNG, GIF, WebM, MP4, MOV, OGG, WAV, MP3, FLAC'; + const makeMapStateToProps = () => { const mapStateToProps = state => ({ acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']), @@ -60,9 +62,9 @@ class UploadButton extends ImmutablePureComponent { return ( <div className='compose-form__upload-button'> - <IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} /> + <IconButton icon='paperclip' title={intl.formatMessage(messages.upload, { formats: SUPPORTED_FORMATS })} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} /> <label> - <span style={{ display: 'none' }}>{intl.formatMessage(messages.upload)}</span> + <span style={{ display: 'none' }}>{intl.formatMessage(messages.upload, { formats: SUPPORTED_FORMATS })}</span> <input key={resetFileKey} ref={this.setRef} diff --git a/app/javascript/mastodon/features/compose/containers/upload_button_container.js b/app/javascript/mastodon/features/compose/containers/upload_button_container.js index d8b8c4b6e..1471e628b 100644 --- a/app/javascript/mastodon/features/compose/containers/upload_button_container.js +++ b/app/javascript/mastodon/features/compose/containers/upload_button_container.js @@ -3,7 +3,7 @@ import UploadButton from '../components/upload_button'; import { uploadCompose } from '../../../actions/compose'; const mapStateToProps = state => ({ - disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')), + disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))), unavailable: state.getIn(['compose', 'poll']) !== null, resetFileKey: state.getIn(['compose', 'resetFileKey']), }); diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index c7aa4d033..4af157af1 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -107,15 +107,15 @@ export default class DetailedStatus extends ImmutablePureComponent { } if (status.get('media_attachments').size > 0) { - if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - const video = status.getIn(['media_attachments', 0]); + if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) { + const attachment = status.getIn(['media_attachments', 0]); media = ( <Video - preview={video.get('preview_url')} - blurhash={video.get('blurhash')} - src={video.get('url')} - alt={video.get('description')} + preview={attachment.get('preview_url')} + blurhash={attachment.get('blurhash')} + src={attachment.get('url')} + alt={attachment.get('description')} width={300} height={150} inline diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 1586580a8..d05c61f98 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -369,7 +369,7 @@ "trends.count_by_accounts": "{count} {rawCount, plural, one {person} آخرون {people}} يتحدثون", "ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.", "upload_area.title": "اسحب ثم أفلت للرفع", - "upload_button.label": "إضافة وسائط (JPEG، PNG، GIF، WebM، MP4، MOV)", + "upload_button.label": "إضافة وسائط ({formats})", "upload_error.limit": "لقد تم بلوغ الحد الأقصى المسموح به لإرسال الملفات.", "upload_error.poll": "لا يمكن إدراج ملفات في استطلاعات الرأي.", "upload_form.description": "وصف للمعاقين بصريا", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index acb9709d0..bb73b2a41 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -314,7 +314,7 @@ "search_results.accounts": "Gent", "search_results.hashtags": "Etiquetes", "search_results.statuses": "Toots", - "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}", "status.admin_account": "Obre l'interfície de moderació per a @{name}", "status.admin_status": "Obre aquest toot a la interfície de moderació", "status.block": "Bloqueja @{name}", @@ -366,7 +366,7 @@ "time_remaining.minutes": "{number, plural, one {# minut} other {# minuts}} restants", "time_remaining.moments": "Moments restants", "time_remaining.seconds": "{number, plural, one {# segon} other {# segons}} restants", - "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {gent}} talking", "ui.beforeunload": "El teu esborrany es perdrà si surts de Mastodon.", "upload_area.title": "Arrossega i deixa anar per a carregar", "upload_button.label": "Afegir multimèdia (JPEG, PNG, GIF, WebM, MP4, MOV)", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 5c63af3b2..ac8bc9b9f 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -369,7 +369,7 @@ "trends.count_by_accounts": "{count} {rawCount, plural, eine {Person} other {Personen}} reden darüber", "ui.beforeunload": "Dein Entwurf geht verloren, wenn du Mastodon verlässt.", "upload_area.title": "Zum Hochladen hereinziehen", - "upload_button.label": "Mediendatei hinzufügen (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Mediendatei hinzufügen ({formats})", "upload_error.limit": "Dateiupload-Limit erreicht.", "upload_error.poll": "Dateiuploads sind in Kombination mit Umfragen nicht erlaubt.", "upload_form.description": "Für Menschen mit Sehbehinderung beschreiben", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index faca8241d..076aca2b1 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -1051,7 +1051,7 @@ { "descriptors": [ { - "defaultMessage": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "defaultMessage": "Add media ({formats})", "id": "upload_button.label" } ], diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 49e4ddbda..a75c41799 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -374,7 +374,7 @@ "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Add media ({formats})", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 4eca05ca5..342a15bfb 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -71,20 +71,20 @@ "compose_form.lock_disclaimer": "Tilisi ei ole {locked}. Kuka tahansa voi seurata tiliäsi ja nähdä vain seuraajille rajaamasi julkaisut.", "compose_form.lock_disclaimer.lock": "lukittu", "compose_form.placeholder": "Mitä mietit?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", + "compose_form.poll.add_option": "Lisää valinta", + "compose_form.poll.duration": "Äänestyksen kesto", + "compose_form.poll.option_placeholder": "Valinta numero", + "compose_form.poll.remove_option": "Poista tämä valinta", "compose_form.publish": "Tuuttaa", - "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.publish_loud": "Julkista!", + "compose_form.sensitive.hide": "Valitse tämä arkaluontoisena", "compose_form.sensitive.marked": "Media on merkitty arkaluontoiseksi", "compose_form.sensitive.unmarked": "Mediaa ei ole merkitty arkaluontoiseksi", "compose_form.spoiler.marked": "Teksti on piilotettu varoituksen taakse", "compose_form.spoiler.unmarked": "Teksti ei ole piilotettu", "compose_form.spoiler_placeholder": "Sisältövaroitus", "confirmation_modal.cancel": "Peruuta", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "Estä ja raportoi", "confirmations.block.confirm": "Estä", "confirmations.block.message": "Haluatko varmasti estää käyttäjän {name}?", "confirmations.delete.confirm": "Poista", @@ -118,7 +118,7 @@ "emoji_button.symbols": "Symbolit", "emoji_button.travel": "Matkailu", "empty_column.account_timeline": "Ei ole 'toots' täällä!", - "empty_column.account_unavailable": "Profile unavailable", + "empty_column.account_unavailable": "Profiilia ei löydy", "empty_column.blocks": "Et ole vielä estänyt yhtään käyttäjää.", "empty_column.community": "Paikallinen aikajana on tyhjä. Homma lähtee käyntiin, kun kirjoitat jotain julkista!", "empty_column.direct": "Sinulla ei ole vielä yhtään viestiä yksittäiselle käyttäjälle. Kun lähetät tai vastaanotat sellaisen, se näkyy täällä.", @@ -138,7 +138,7 @@ "follow_request.reject": "Hylkää", "getting_started.developers": "Kehittäjille", "getting_started.directory": "Profiili hakemisto", - "getting_started.documentation": "Documentation", + "getting_started.documentation": "Documentaatio", "getting_started.heading": "Aloitus", "getting_started.invite": "Kutsu ihmisiä", "getting_started.open_source_notice": "Mastodon on avoimen lähdekoodin ohjelma. Voit avustaa tai raportoida ongelmia GitHubissa: {github}.", @@ -147,8 +147,8 @@ "hashtag.column_header.tag_mode.all": "ja {additional}", "hashtag.column_header.tag_mode.any": "tai {additional}", "hashtag.column_header.tag_mode.none": "ilman {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.select.no_options_message": "Ehdostuta ei löydetty", + "hashtag.column_settings.select.placeholder": "Laita häshtägejä…", "hashtag.column_settings.tag_mode.all": "Kaikki", "hashtag.column_settings.tag_mode.any": "Kaikki", "hashtag.column_settings.tag_mode.none": "Ei mikään", @@ -156,25 +156,25 @@ "home.column_settings.basic": "Perusasetukset", "home.column_settings.show_reblogs": "Näytä buustaukset", "home.column_settings.show_replies": "Näytä vastaukset", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "intervals.full.days": "Päivä päiviä", + "intervals.full.hours": "Tunti tunteja", + "intervals.full.minutes": "Minuuti minuuteja", "introduction.federation.action": "Seuraava", - "introduction.federation.federated.headline": "Federated", - "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", - "introduction.federation.home.headline": "Home", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", - "introduction.federation.local.headline": "Local", - "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", - "introduction.interactions.action": "Finish toot-orial!", - "introduction.interactions.favourite.headline": "Favourite", - "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", - "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", - "introduction.interactions.reply.headline": "Reply", - "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", - "introduction.welcome.action": "Let's go!", - "introduction.welcome.headline": "First steps", + "introduction.federation.federated.headline": "Federaatioitettu", + "introduction.federation.federated.text": "Julkisia viestejä muiden serverien that is not a word aikoo tulla federoituun aikajanaan.", + "introduction.federation.home.headline": "Koti", + "introduction.federation.home.text": "Viestit muilta pelaajilta jota seuraat aikovat tulla koti sivuusi. Voit seurata ketä vain missä vain serverillä!", + "introduction.federation.local.headline": "Paikallinen", + "introduction.federation.local.text": "Julkiset viestit muilta pelaajilta samalla serverillä tulevat sinun paikalliseen aikajanaan.", + "introduction.interactions.action": "Suorita harjoitus!", + "introduction.interactions.favourite.headline": "Lempi", + "introduction.interactions.favourite.text": "Toot is not a word.", + "introduction.interactions.reblog.headline": "Nopeutus", + "introduction.interactions.reblog.text": "Toot is not a word", + "introduction.interactions.reply.headline": "Vastaa", + "introduction.interactions.reply.text": "TOOT IS NOT A WORD", + "introduction.welcome.action": "Mennään!", + "introduction.welcome.headline": "Ensimmäiset askeleet", "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", "keyboard_shortcuts.back": "liiku taaksepäin", "keyboard_shortcuts.blocked": "avaa lista estetyistä käyttäjistä", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index d5e88c4f3..f7e2e4353 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -40,7 +40,7 @@ "boost_modal.combo": "Puoi premere {combo} per saltare questo passaggio la prossima volta", "bundle_column_error.body": "E' avvenuto un errore durante il caricamento di questo componente.", "bundle_column_error.retry": "Riprova", - "bundle_column_error.title": "Network error", + "bundle_column_error.title": "Errore di rete", "bundle_modal_error.close": "Chiudi", "bundle_modal_error.message": "C'è stato un errore mentre questo componente veniva caricato.", "bundle_modal_error.retry": "Riprova", @@ -71,20 +71,20 @@ "compose_form.lock_disclaimer": "Il tuo account non è {bloccato}. Chiunque può decidere di seguirti per vedere i tuoi post per soli seguaci.", "compose_form.lock_disclaimer.lock": "bloccato", "compose_form.placeholder": "A cosa stai pensando?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", + "compose_form.poll.add_option": "Aggiungi una scelta", + "compose_form.poll.duration": "Durata del sondaggio", + "compose_form.poll.option_placeholder": "Scelta {number}", + "compose_form.poll.remove_option": "Rimuovi questa scelta", "compose_form.publish": "Toot", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Segna media come sensibile", "compose_form.sensitive.marked": "Questo media è contrassegnato come sensibile", "compose_form.sensitive.unmarked": "Questo media non è contrassegnato come sensibile", "compose_form.spoiler.marked": "Il testo è nascosto dall'avviso", "compose_form.spoiler.unmarked": "Il testo non è nascosto", "compose_form.spoiler_placeholder": "Content warning", "confirmation_modal.cancel": "Annulla", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "Blocca & Segnala", "confirmations.block.confirm": "Blocca", "confirmations.block.message": "Sei sicuro di voler bloccare {name}?", "confirmations.delete.confirm": "Cancella", @@ -118,7 +118,7 @@ "emoji_button.symbols": "Simboli", "emoji_button.travel": "Viaggi e luoghi", "empty_column.account_timeline": "Non ci sono toot qui!", - "empty_column.account_unavailable": "Profile unavailable", + "empty_column.account_unavailable": "Profilo non disponibile", "empty_column.blocks": "Non hai ancora bloccato nessun utente.", "empty_column.community": "La timeline locale è vuota. Condividi qualcosa pubblicamente per dare inizio alla festa!", "empty_column.direct": "Non hai ancora nessun messaggio diretto. Quando ne manderai o riceverai qualcuno, apparirà qui.", @@ -156,15 +156,15 @@ "home.column_settings.basic": "Semplice", "home.column_settings.show_reblogs": "Mostra post condivisi", "home.column_settings.show_replies": "Mostra risposte", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "intervals.full.days": "{number, plural, one {# giorno} other {# giorni}}", + "intervals.full.hours": "{number, plural, one {# ora} other {# ore}}", + "intervals.full.minutes": "{number, plural, one {# minuto} other {# minuti}}", "introduction.federation.action": "Avanti", - "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.headline": "Federato", "introduction.federation.federated.text": "I post pubblici provenienti da altri server del fediverse saranno mostrati nella timeline federata.", "introduction.federation.home.headline": "Home", "introduction.federation.home.text": "I post scritti da persone che segui saranno mostrati nella timeline home. Puoi seguire chiunque su qualunque server!", - "introduction.federation.local.headline": "Local", + "introduction.federation.local.headline": "Locale", "introduction.federation.local.text": "I post pubblici scritti da persone sul tuo stesso server saranno mostrati nella timeline locale.", "introduction.interactions.action": "Finisci il tutorial!", "introduction.interactions.favourite.headline": "Apprezza", @@ -204,17 +204,17 @@ "keyboard_shortcuts.search": "per spostare il focus sulla ricerca", "keyboard_shortcuts.start": "per aprire la colonna \"Come iniziare\"", "keyboard_shortcuts.toggle_hidden": "per mostrare/nascondere il testo dei CW", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "mostrare/nascondere media", "keyboard_shortcuts.toot": "per iniziare a scrivere un toot completamente nuovo", "keyboard_shortcuts.unfocus": "per uscire dall'area di composizione o dalla ricerca", "keyboard_shortcuts.up": "per spostarsi in alto nella lista", "lightbox.close": "Chiudi", "lightbox.next": "Successivo", "lightbox.previous": "Precedente", - "lightbox.view_context": "View context", + "lightbox.view_context": "Mostra contesto", "lists.account.add": "Aggiungi alla lista", "lists.account.remove": "Togli dalla lista", - "lists.delete": "Delete list", + "lists.delete": "Elimina lista", "lists.edit": "Modifica lista", "lists.edit.submit": "Cambia titolo", "lists.new.create": "Aggiungi lista", @@ -243,16 +243,16 @@ "navigation_bar.lists": "Liste", "navigation_bar.logout": "Esci", "navigation_bar.mutes": "Utenti silenziati", - "navigation_bar.personal": "Personal", + "navigation_bar.personal": "Personale", "navigation_bar.pins": "Toot fissati in cima", "navigation_bar.preferences": "Impostazioni", - "navigation_bar.profile_directory": "Profile directory", + "navigation_bar.profile_directory": "Directory dei profili", "navigation_bar.public_timeline": "Timeline federata", "navigation_bar.security": "Sicurezza", "notification.favourite": "{name} ha apprezzato il tuo post", "notification.follow": "{name} ha iniziato a seguirti", "notification.mention": "{name} ti ha menzionato", - "notification.poll": "A poll you have voted in has ended", + "notification.poll": "Un sondaggio in cui hai votato è terminato", "notification.reblog": "{name} ha condiviso il tuo post", "notifications.clear": "Cancella notifiche", "notifications.clear_confirmation": "Vuoi davvero cancellare tutte le notifiche?", @@ -263,7 +263,7 @@ "notifications.column_settings.filter_bar.show": "Mostra", "notifications.column_settings.follow": "Nuovi seguaci:", "notifications.column_settings.mention": "Menzioni:", - "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.poll": "Risultati del sondaggio:", "notifications.column_settings.push": "Notifiche push", "notifications.column_settings.reblog": "Post condivisi:", "notifications.column_settings.show": "Mostra in colonna", @@ -273,14 +273,14 @@ "notifications.filter.favourites": "Apprezzati", "notifications.filter.follows": "Seguaci", "notifications.filter.mentions": "Menzioni", - "notifications.filter.polls": "Poll results", + "notifications.filter.polls": "Risultati del sondaggio", "notifications.group": "{count} notifiche", "poll.closed": "Chiuso", "poll.refresh": "Aggiorna", "poll.total_votes": "{count, plural, one {# voto} other {# voti}}", "poll.vote": "Vota", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", + "poll_button.add_poll": "Aggiungi un sondaggio", + "poll_button.remove_poll": "Rimuovi sondaggio", "privacy.change": "Modifica privacy del post", "privacy.direct.long": "Invia solo a utenti menzionati", "privacy.direct.short": "Diretto", @@ -292,8 +292,8 @@ "privacy.unlisted.short": "Non elencato", "regeneration_indicator.label": "Caricamento in corso…", "regeneration_indicator.sublabel": "Stiamo preparando il tuo home feed!", - "relative_time.days": "{number}d", - "relative_time.hours": "{number}h", + "relative_time.days": "{number}g", + "relative_time.hours": "{number}o", "relative_time.just_now": "ora", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", @@ -307,8 +307,8 @@ "search.placeholder": "Cerca", "search_popout.search_format": "Formato di ricerca avanzato", "search_popout.tips.full_text": "Testo semplice per trovare gli status che hai scritto, segnato come apprezzati, condiviso o in cui sei stato citato, e inoltre i nomi utente, nomi visualizzati e hashtag che lo contengono.", - "search_popout.tips.hashtag": "hashtag", - "search_popout.tips.status": "status", + "search_popout.tips.hashtag": "etichetta", + "search_popout.tips.status": "stato", "search_popout.tips.text": "Testo semplice per trovare nomi visualizzati, nomi utente e hashtag che lo contengono", "search_popout.tips.user": "utente", "search_results.accounts": "Gente", @@ -371,7 +371,7 @@ "upload_area.title": "Trascina per caricare", "upload_button.label": "Aggiungi file multimediale", "upload_error.limit": "Limite al caricamento di file superato.", - "upload_error.poll": "File upload not allowed with polls.", + "upload_error.poll": "Caricamento file non consentito nei sondaggi.", "upload_form.description": "Descrizione per utenti con disabilità visive", "upload_form.focus": "Modifica anteprima", "upload_form.undo": "Cancella", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index bdc1f98fb..6dadf7c60 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -374,7 +374,7 @@ "trends.count_by_accounts": "{count}人がトゥート", "ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。", "upload_area.title": "ドラッグ&ドロップでアップロード", - "upload_button.label": "メディアを追加 (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "メディアを追加 ({formats})", "upload_error.limit": "アップロードできる上限を超えています。", "upload_error.poll": "アンケートではファイルをアップロードできません。", "upload_form.description": "視覚障害者のための説明", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 4bcab5ba0..f6504f4bb 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -361,15 +361,15 @@ "tabs_bar.local_timeline": "Lokaal", "tabs_bar.notifications": "Meldingen", "tabs_bar.search": "Zoeken", - "time_remaining.days": "{number, plural, one {# dag} other {# dagen}} left", - "time_remaining.hours": "{number, plural, one {# uur} other {# uur}} left", - "time_remaining.minutes": "{number, plural, one {# minuut} other {# minuten}} left", + "time_remaining.days": "{number, plural, one {# dag} other {# dagen}} te gaan", + "time_remaining.hours": "{number, plural, one {# uur} other {# uur}} te gaan", + "time_remaining.minutes": "{number, plural, one {# minuut} other {# minuten}} te gaan", "time_remaining.moments": "Nog enkele ogenblikken resterend", - "time_remaining.seconds": "{number, plural, one {# seconde} other {# seconden}} left", + "time_remaining.seconds": "{number, plural, one {# seconde} other {# seconden}} te gaan", "trends.count_by_accounts": "{count} {rawCount, plural, one {persoon praat} other {mensen praten}} hierover", "ui.beforeunload": "Je concept zal verloren gaan als je Mastodon verlaat.", - "upload_area.title": "Hierin slepen om te uploaden", - "upload_button.label": "Media toevoegen (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_area.title": "Hiernaar toe slepen om te uploaden", + "upload_button.label": "Media toevoegen ({formats})", "upload_error.limit": "Uploadlimiet van bestand overschreden.", "upload_error.poll": "Het uploaden van bestanden is in polls niet toegestaan.", "upload_form.description": "Omschrijf dit voor mensen met een visuele beperking", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index d624aa25c..18993af97 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -1,5 +1,5 @@ { - "account.add_or_remove_from_list": "Pridaj, alebo odstráň zo zoznamov", + "account.add_or_remove_from_list": "Pridaj do, alebo odober zo zoznamov", "account.badges.bot": "Bot", "account.block": "Blokuj @{name}", "account.block_domain": "Ukry všetko z {domain}", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index f0e813e90..51794a862 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -149,10 +149,10 @@ "hashtag.column_header.tag_mode.none": "brez {additional}", "hashtag.column_settings.select.no_options_message": "Ni najdenih predlogov", "hashtag.column_settings.select.placeholder": "Vpiši ključnik…", - "hashtag.column_settings.tag_mode.all": "Vse našteto", + "hashtag.column_settings.tag_mode.all": "Vse od naštetega", "hashtag.column_settings.tag_mode.any": "Karkoli od naštetega", "hashtag.column_settings.tag_mode.none": "Nič od naštetega", - "hashtag.column_settings.tag_toggle": "V ta stolpec vključite dodatne oznake", + "hashtag.column_settings.tag_toggle": "Za ta stolpec vključi dodatne oznake", "home.column_settings.basic": "Osnovno", "home.column_settings.show_reblogs": "Pokaži spodbude", "home.column_settings.show_replies": "Pokaži odgovore", @@ -161,187 +161,187 @@ "intervals.full.minutes": "{number, plural, one {# minuta} two {# minuti} few {# minute} other {# minut}}", "introduction.federation.action": "Naprej", "introduction.federation.federated.headline": "Združeno", - "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", - "introduction.federation.home.headline": "Home", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", - "introduction.federation.local.headline": "Local", - "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", - "introduction.interactions.action": "Finish toot-orial!", - "introduction.interactions.favourite.headline": "Favourite", - "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", - "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", - "introduction.interactions.reply.headline": "Reply", - "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", - "introduction.welcome.action": "Let's go!", - "introduction.welcome.headline": "First steps", - "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", - "keyboard_shortcuts.back": "za krmarjenje nazaj", - "keyboard_shortcuts.blocked": "to open blocked users list", - "keyboard_shortcuts.boost": "suniti", - "keyboard_shortcuts.column": "osredotočiti status v enega od stolpcev", - "keyboard_shortcuts.compose": "osredotočiti na sestavljanje besedila", + "introduction.federation.federated.text": "Javne objave iz drugih strežnikov fediverse-a bodo prikazane v združeni časovnici.", + "introduction.federation.home.headline": "Domov", + "introduction.federation.home.text": "Objave oseb, ki jim sledite, bodo prikazane v vaši domači časovnici. Lahko sledite vsakomur na katerem koli strežniku!", + "introduction.federation.local.headline": "Lokalno", + "introduction.federation.local.text": "Javne objave ljudi na istem strežniku, se bodo prikazale na lokalni časovnici.", + "introduction.interactions.action": "Zaključi vadnico!", + "introduction.interactions.favourite.headline": "Priljubljeni", + "introduction.interactions.favourite.text": "Tut lahko shranite za pozneje in ga vzljubite ter s tem pokažete avtorju, da vam je ta tut priljubljen.", + "introduction.interactions.reblog.headline": "Spodbudi", + "introduction.interactions.reblog.text": "Tute drugih ljudi lahko delite z vašimi sledilci, tako da spodbudite tute.", + "introduction.interactions.reply.headline": "Odgovori", + "introduction.interactions.reply.text": "Lahko odgovarjate na tuje in vaše tute, kar bo odgovore povezalo v pogovor.", + "introduction.welcome.action": "Gremo!", + "introduction.welcome.headline": "Prvi koraki", + "introduction.welcome.text": "Dobrodošli v fediverse-u! Čez nekaj trenutkov boste lahko oddajali sporočila in se pogovarjali s prijatelji prek različnih strežnikov. Vendar je ta strežnik {domain} poseben - gosti vaš profil, zato si zapomnite njegovo ime.", + "keyboard_shortcuts.back": "pojdi nazaj", + "keyboard_shortcuts.blocked": "odpri seznam blokiranih uporabnikov", + "keyboard_shortcuts.boost": "spodbudi", + "keyboard_shortcuts.column": "fokusiraj na status v enemu od stolpcev", + "keyboard_shortcuts.compose": "fokusiraj na območje za sestavljanje besedila", "keyboard_shortcuts.description": "Opis", - "keyboard_shortcuts.direct": "to open direct messages column", - "keyboard_shortcuts.down": "premakniti navzdol po seznamu", - "keyboard_shortcuts.enter": "odpreti status", - "keyboard_shortcuts.favourite": "to favourite", - "keyboard_shortcuts.favourites": "to open favourites list", - "keyboard_shortcuts.federated": "to open federated timeline", + "keyboard_shortcuts.direct": "odpri stolpec za neposredna sporočila", + "keyboard_shortcuts.down": "premakni se navzdol po seznamu", + "keyboard_shortcuts.enter": "odpri status", + "keyboard_shortcuts.favourite": "vzljubi", + "keyboard_shortcuts.favourites": "odpri seznam priljubljenih", + "keyboard_shortcuts.federated": "odpri združeno časovnico", "keyboard_shortcuts.heading": "Tipkovne bližnjice", - "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.home": "odpri domačo časovnico", "keyboard_shortcuts.hotkey": "Hitra tipka", - "keyboard_shortcuts.legend": "to display this legend", - "keyboard_shortcuts.local": "to open local timeline", - "keyboard_shortcuts.mention": "to mention author", - "keyboard_shortcuts.muted": "to open muted users list", - "keyboard_shortcuts.my_profile": "to open your profile", - "keyboard_shortcuts.notifications": "to open notifications column", - "keyboard_shortcuts.pinned": "to open pinned toots list", - "keyboard_shortcuts.profile": "to open author's profile", - "keyboard_shortcuts.reply": "to reply", - "keyboard_shortcuts.requests": "to open follow requests list", - "keyboard_shortcuts.search": "to focus search", - "keyboard_shortcuts.start": "to open \"get started\" column", - "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", - "keyboard_shortcuts.toot": "da začnete povsem nov tut", - "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", - "keyboard_shortcuts.up": "to move up in the list", - "lightbox.close": "Close", - "lightbox.next": "Next", - "lightbox.previous": "Previous", - "lightbox.view_context": "View context", - "lists.account.add": "Add to list", - "lists.account.remove": "Remove from list", - "lists.delete": "Delete list", - "lists.edit": "Edit list", - "lists.edit.submit": "Change title", - "lists.new.create": "Add list", - "lists.new.title_placeholder": "New list title", - "lists.search": "Search among people you follow", - "lists.subheading": "Your lists", - "loading_indicator.label": "Loading...", - "media_gallery.toggle_visible": "Toggle visibility", - "missing_indicator.label": "Not found", - "missing_indicator.sublabel": "This resource could not be found", - "mute_modal.hide_notifications": "Hide notifications from this user?", - "navigation_bar.apps": "Mobile apps", - "navigation_bar.blocks": "Blocked users", - "navigation_bar.community_timeline": "Local timeline", - "navigation_bar.compose": "Compose new toot", - "navigation_bar.direct": "Direct messages", - "navigation_bar.discover": "Discover", - "navigation_bar.domain_blocks": "Hidden domains", - "navigation_bar.edit_profile": "Edit profile", - "navigation_bar.favourites": "Favourites", - "navigation_bar.filters": "Muted words", - "navigation_bar.follow_requests": "Follow requests", - "navigation_bar.follows_and_followers": "Follows and followers", - "navigation_bar.info": "O tem vozlišču", - "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", - "navigation_bar.lists": "Lists", - "navigation_bar.logout": "Logout", - "navigation_bar.mutes": "Muted users", - "navigation_bar.personal": "Personal", + "keyboard_shortcuts.legend": "pokaži to legendo", + "keyboard_shortcuts.local": "odpri lokalno časovnico", + "keyboard_shortcuts.mention": "omeni avtorja", + "keyboard_shortcuts.muted": "odpri seznam utišanih uporabnikov", + "keyboard_shortcuts.my_profile": "odpri svoj profil", + "keyboard_shortcuts.notifications": "odpri stolpec z obvestili", + "keyboard_shortcuts.pinned": "odpri seznam pripetih tutov", + "keyboard_shortcuts.profile": "odpri avtorjev profil", + "keyboard_shortcuts.reply": "odgovori", + "keyboard_shortcuts.requests": "odpri seznam s prošnjami za sledenje", + "keyboard_shortcuts.search": "fokusiraj na iskanje", + "keyboard_shortcuts.start": "odpri stolpec \"začni\"", + "keyboard_shortcuts.toggle_hidden": "prikaži/skrij besedilo za CW", + "keyboard_shortcuts.toggle_sensitivity": "prikaži/skrij medije", + "keyboard_shortcuts.toot": "začni povsem nov tut", + "keyboard_shortcuts.unfocus": "odfokusiraj območje za sestavljanje besedila/iskanje", + "keyboard_shortcuts.up": "premakni se navzgor po seznamu", + "lightbox.close": "Zapri", + "lightbox.next": "Naslednji", + "lightbox.previous": "Prejšnji", + "lightbox.view_context": "Poglej kontekst", + "lists.account.add": "Dodaj na seznam", + "lists.account.remove": "Odstrani s seznama", + "lists.delete": "Izbriši seznam", + "lists.edit": "Uredi seznam", + "lists.edit.submit": "Spremeni naslov", + "lists.new.create": "Dodaj seznam", + "lists.new.title_placeholder": "Nov naslov seznama", + "lists.search": "Išči med ljudmi, katerim sledite", + "lists.subheading": "Vaši seznami", + "loading_indicator.label": "Nalaganje...", + "media_gallery.toggle_visible": "Preklopi vidljivost", + "missing_indicator.label": "Ni najdeno", + "missing_indicator.sublabel": "Tega vira ni bilo mogoče najti", + "mute_modal.hide_notifications": "Skrij obvestila tega uporabnika?", + "navigation_bar.apps": "Mobilne aplikacije", + "navigation_bar.blocks": "Blokirani uporabniki", + "navigation_bar.community_timeline": "Lokalna časovnica", + "navigation_bar.compose": "Sestavi nov tut", + "navigation_bar.direct": "Neposredna sporočila", + "navigation_bar.discover": "Odkrijte", + "navigation_bar.domain_blocks": "Skrite domene", + "navigation_bar.edit_profile": "Uredi profil", + "navigation_bar.favourites": "Priljubljeni", + "navigation_bar.filters": "Utišane besede", + "navigation_bar.follow_requests": "Prošnje za sledenje", + "navigation_bar.follows_and_followers": "Sledenja in sledilci", + "navigation_bar.info": "O tem strežniku", + "navigation_bar.keyboard_shortcuts": "Hitre tipke", + "navigation_bar.lists": "Seznami", + "navigation_bar.logout": "Odjava", + "navigation_bar.mutes": "Utišani uporabniki", + "navigation_bar.personal": "Osebno", "navigation_bar.pins": "Pripeti tuti", - "navigation_bar.preferences": "Preferences", - "navigation_bar.profile_directory": "Profile directory", - "navigation_bar.public_timeline": "Federated timeline", - "navigation_bar.security": "Security", - "notification.favourite": "{name} favourited your status", - "notification.follow": "{name} followed you", - "notification.mention": "{name} mentioned you", - "notification.poll": "A poll you have voted in has ended", - "notification.reblog": "{name} boosted your status", - "notifications.clear": "Clear notifications", - "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", - "notifications.column_settings.alert": "Desktop notifications", - "notifications.column_settings.favourite": "Favourites:", - "notifications.column_settings.filter_bar.advanced": "Display all categories", - "notifications.column_settings.filter_bar.category": "Quick filter bar", - "notifications.column_settings.filter_bar.show": "Show", - "notifications.column_settings.follow": "New followers:", - "notifications.column_settings.mention": "Mentions:", - "notifications.column_settings.poll": "Poll results:", - "notifications.column_settings.push": "Push notifications", - "notifications.column_settings.reblog": "Boosts:", - "notifications.column_settings.show": "Show in column", - "notifications.column_settings.sound": "Play sound", - "notifications.filter.all": "All", - "notifications.filter.boosts": "Boosts", - "notifications.filter.favourites": "Favourites", - "notifications.filter.follows": "Follows", - "notifications.filter.mentions": "Mentions", - "notifications.filter.polls": "Poll results", - "notifications.group": "{count} notifications", - "poll.closed": "Closed", - "poll.refresh": "Refresh", - "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", - "poll.vote": "Vote", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", - "privacy.change": "Adjust status privacy", - "privacy.direct.long": "Post to mentioned users only", - "privacy.direct.short": "Direct", - "privacy.private.long": "Post to followers only", - "privacy.private.short": "Followers-only", - "privacy.public.long": "Post to public timelines", - "privacy.public.short": "Public", - "privacy.unlisted.long": "Do not show in public timelines", - "privacy.unlisted.short": "Unlisted", - "regeneration_indicator.label": "Loading…", - "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "navigation_bar.preferences": "Nastavitve", + "navigation_bar.profile_directory": "Imenik profilov", + "navigation_bar.public_timeline": "Združena časovnica", + "navigation_bar.security": "Varnost", + "notification.favourite": "{name} je vzljubil/a vaš status", + "notification.follow": "{name} vam sledi", + "notification.mention": "{name} vas je omenil/a", + "notification.poll": "Glasovanje, v katerem ste sodelovali, se je končalo", + "notification.reblog": "{name} je spodbudil/a vaš status", + "notifications.clear": "Počisti obvestila", + "notifications.clear_confirmation": "Ali ste prepričani, da želite trajno izbrisati vsa vaša obvestila?", + "notifications.column_settings.alert": "Namizna obvestila", + "notifications.column_settings.favourite": "Priljubljeni:", + "notifications.column_settings.filter_bar.advanced": "Prikaži vse kategorije", + "notifications.column_settings.filter_bar.category": "Vrstica za hitro filtriranje", + "notifications.column_settings.filter_bar.show": "Pokaži", + "notifications.column_settings.follow": "Novi sledilci:", + "notifications.column_settings.mention": "Omembe:", + "notifications.column_settings.poll": "Rezultati glasovanja:", + "notifications.column_settings.push": "Potisna obvestila", + "notifications.column_settings.reblog": "Spodbude:", + "notifications.column_settings.show": "Prikaži v stolpcu", + "notifications.column_settings.sound": "Predvajaj zvok", + "notifications.filter.all": "Vse", + "notifications.filter.boosts": "Spodbude", + "notifications.filter.favourites": "Priljubljeni", + "notifications.filter.follows": "Sledi", + "notifications.filter.mentions": "Omembe", + "notifications.filter.polls": "Rezultati glasovanj", + "notifications.group": "{count} obvestil", + "poll.closed": "Zaprto", + "poll.refresh": "Osveži", + "poll.total_votes": "{count, plural,one {# glas} other {# glasov}}", + "poll.vote": "Glasuj", + "poll_button.add_poll": "Dodaj anketo", + "poll_button.remove_poll": "Odstrani anketo", + "privacy.change": "Prilagodi zasebnost statusa", + "privacy.direct.long": "Objavi samo omenjenim uporabnikom", + "privacy.direct.short": "Neposredno", + "privacy.private.long": "Objavi samo sledilcem", + "privacy.private.short": "Samo sledilci", + "privacy.public.long": "Objavi na javne časovnice", + "privacy.public.short": "Javno", + "privacy.unlisted.long": "Ne objavi na javne časovnice", + "privacy.unlisted.short": "Ni prikazano", + "regeneration_indicator.label": "Nalaganje…", + "regeneration_indicator.sublabel": "Vaš domači vir se pripravlja!", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", - "relative_time.just_now": "now", + "relative_time.just_now": "zdaj", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", - "reply_indicator.cancel": "Cancel", - "report.forward": "Forward to {target}", - "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", - "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", - "report.placeholder": "Additional comments", - "report.submit": "Submit", - "report.target": "Report {target}", - "search.placeholder": "Search", - "search_popout.search_format": "Advanced search format", - "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", - "search_popout.tips.hashtag": "hashtag", - "search_popout.tips.status": "status", - "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", - "search_popout.tips.user": "user", - "search_results.accounts": "People", - "search_results.hashtags": "Hashtags", + "reply_indicator.cancel": "Prekliči", + "report.forward": "Posreduj do {target}", + "report.forward_hint": "Račun je iz drugega strežnika. Pošljem anonimno kopijo poročila tudi na drugi strežnik?", + "report.hint": "Poročilo bo poslano moderatorjem vašega vozlišča. Spodaj lahko navedete, zakaj prijavljate ta račun:", + "report.placeholder": "Dodatni komentarji", + "report.submit": "Pošlji", + "report.target": "Prijavi {target}", + "search.placeholder": "Iskanje", + "search_popout.search_format": "Napredna oblika iskanja", + "search_popout.tips.full_text": "Enostavno besedilo vrne statuse, ki ste jih napisali, vzljubili, spodbudili ali ste bili v njih omenjeni, kot tudi ujemajoča se uporabniška imena, prikazna imena in ključnike.", + "search_popout.tips.hashtag": "ključnik", + "search_popout.tips.status": "stanje", + "search_popout.tips.text": "Enostavno besedilo vrne ujemajoča se prikazna imena, uporabniška imena in ključnike", + "search_popout.tips.user": "uporabnik", + "search_results.accounts": "Ljudje", + "search_results.hashtags": "Ključniki", "search_results.statuses": "Tuti", - "search_results.total": "{count, number} {count, plural, one {result} other {results}}", - "status.admin_account": "Open moderation interface for @{name}", - "status.admin_status": "Open this status in the moderation interface", - "status.block": "Block @{name}", - "status.cancel_reblog_private": "Unboost", - "status.cannot_reblog": "This post cannot be boosted", - "status.copy": "Copy link to status", - "status.delete": "Delete", - "status.detailed_status": "Detailed conversation view", - "status.direct": "Direct message @{name}", - "status.embed": "Embed", - "status.favourite": "Favourite", - "status.filtered": "Filtered", - "status.load_more": "Load more", - "status.media_hidden": "Media hidden", - "status.mention": "Mention @{name}", - "status.more": "More", - "status.mute": "Mute @{name}", - "status.mute_conversation": "Mute conversation", - "status.open": "Expand this status", - "status.pin": "Pin on profile", + "search_results.total": "{count, number} {count, plural, one {rezultat} other {rezultatov}}", + "status.admin_account": "Odpri vmesnik za moderiranje za @{name}", + "status.admin_status": "Odpri status v vmesniku za moderiranje", + "status.block": "Blokiraj @{name}", + "status.cancel_reblog_private": "Prekini spodbudo", + "status.cannot_reblog": "Te objave ni mogoče spodbuditi", + "status.copy": "Kopiraj povezavo do statusa", + "status.delete": "Izbriši", + "status.detailed_status": "Podroben pogled pogovora", + "status.direct": "Neposredno sporočilo @{name}", + "status.embed": "Vgradi", + "status.favourite": "Priljubljen", + "status.filtered": "Filtrirano", + "status.load_more": "Naloži več", + "status.media_hidden": "Mediji so skriti", + "status.mention": "Omeni @{name}", + "status.more": "Več", + "status.mute": "Utišaj @{name}", + "status.mute_conversation": "Utišaj pogovor", + "status.open": "Razširi ta status", + "status.pin": "Pripni na profil", "status.pinned": "Pripeti tut", - "status.read_more": "Read more", - "status.reblog": "Suni", - "status.reblog_private": "Suni v prvotno občinstvo", - "status.reblogged_by": "{name} sunjen", - "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.", - "status.redraft": "Delete & re-draft", + "status.read_more": "Preberi več", + "status.reblog": "Spodbudi", + "status.reblog_private": "Spodbudi izvirnemu občinstvu", + "status.reblogged_by": "{name} spodbujen", + "status.reblogs.empty": "Nihče še ni spodbudil tega tuta. Ko se bo to zgodilo, se bodo pojavili tukaj.", + "status.redraft": "Izbriši in preoblikuj", "status.reply": "Odgovori", "status.replyAll": "Odgovori na objavo", "status.report": "Prijavi @{name}", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 05c29f16e..865d3a514 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -320,7 +320,7 @@ "status.block": "屏蔽 @{name}", "status.cancel_reblog_private": "取消转嘟", "status.cannot_reblog": "无法转嘟这条嘟文", - "status.copy": "复制链接到嘟文中", + "status.copy": "复制嘟文链接", "status.delete": "删除", "status.detailed_status": "对话详情", "status.direct": "发送私信给 @{name}", diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 597a8d1dc..dc750dbfe 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -557,6 +557,7 @@ .compose-form__upload-thumbnail { border-radius: 4px; + background-color: $base-shadow-color; background-position: center; background-size: cover; background-repeat: no-repeat; diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index f55dd35b2..00f0dd42d 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -370,7 +370,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def unsupported_media_type?(mime_type) - mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type) + mime_type.present? && !MediaAttachment.supported_mime_types.include?(mime_type) end def supported_blurhash?(blurhash) @@ -380,7 +380,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def skip_download? return @skip_download if defined?(@skip_download) - @skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media? + @skip_download ||= DomainBlock.reject_media?(@account.domain) end def reply_to_local? diff --git a/app/lib/activitypub/activity/flag.rb b/app/lib/activitypub/activity/flag.rb index f73b93058..1659bc61f 100644 --- a/app/lib/activitypub/activity/flag.rb +++ b/app/lib/activitypub/activity/flag.rb @@ -23,7 +23,7 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity private def skip_reports? - DomainBlock.find_by(domain: @account.domain)&.reject_reports? + DomainBlock.reject_reports?(@account.domain) end def object_uris diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb index 3840c8fbf..60de712db 100644 --- a/app/lib/ostatus/activity/creation.rb +++ b/app/lib/ostatus/activity/creation.rb @@ -148,7 +148,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base end def save_media - do_not_download = DomainBlock.find_by(domain: @account.domain)&.reject_media? + do_not_download = DomainBlock.reject_media?(@account.domain) media_attachments = [] @xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: OStatus::TagManager::XMLNS).each do |link| @@ -176,7 +176,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base end def save_emojis(parent) - do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media? + do_not_download = DomainBlock.reject_media?(parent.account.domain) return if do_not_download diff --git a/app/models/account.rb b/app/models/account.rb index 520b183e8..3d7b0dda3 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -102,6 +102,7 @@ class Account < ApplicationRecord scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) } scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc')) } scope :popular, -> { order('account_stats.followers_count desc') } + scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) } delegate :email, :unconfirmed_email, @@ -110,6 +111,8 @@ class Account < ApplicationRecord :confirmed?, :approved?, :pending?, + :disabled?, + :role, :admin?, :moderator?, :staff?, diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb index d2503100c..c3b1fe08d 100644 --- a/app/models/account_filter.rb +++ b/app/models/account_filter.rb @@ -37,6 +37,8 @@ class AccountFilter Account.without_suspended when 'pending' accounts_with_users.merge User.pending + when 'disabled' + accounts_with_users.merge User.disabled when 'silenced' Account.silenced when 'suspended' diff --git a/app/models/concerns/attachmentable.rb b/app/models/concerns/attachmentable.rb index de4cf8775..24f5968de 100644 --- a/app/models/concerns/attachmentable.rb +++ b/app/models/concerns/attachmentable.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'mime/types' +require 'mime/types/columnar' module Attachmentable extend ActiveSupport::Concern @@ -10,10 +10,21 @@ module Attachmentable included do before_post_process :set_file_extensions before_post_process :check_image_dimensions + before_post_process :set_file_content_type end private + def set_file_content_type + self.class.attachment_definitions.each_key do |attachment_name| + attachment = send(attachment_name) + + next if attachment.blank? || attachment.queued_for_write[:original].blank? + + attachment.instance_write :content_type, calculated_content_type(attachment) + end + end + def set_file_extensions self.class.attachment_definitions.each_key do |attachment_name| attachment = send(attachment_name) @@ -47,4 +58,10 @@ module Attachmentable extension end + + def calculated_content_type(attachment) + Paperclip.run('file', '-b --mime :file', file: attachment.queued_for_write[:original].path).split(/[:;\s]+/).first.chomp + rescue Terrapin::CommandLineError + '' + end end diff --git a/app/models/concerns/user_roles.rb b/app/models/concerns/user_roles.rb index 58dffdc46..a42b4a172 100644 --- a/app/models/concerns/user_roles.rb +++ b/app/models/concerns/user_roles.rb @@ -13,6 +13,20 @@ module UserRoles admin? || moderator? end + def role=(value) + case value + when 'admin' + self.admin = true + self.moderator = false + when 'moderator' + self.admin = false + self.moderator = true + else + self.admin = false + self.moderator = false + end + end + def role if admin? 'admin' diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index d3cc70504..e73cd9bd2 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -39,6 +39,7 @@ class CustomEmoji < ApplicationRecord scope :local, -> { where(domain: nil) } scope :remote, -> { where.not(domain: nil) } scope :alphabetic, -> { order(domain: :asc, shortcode: :asc) } + scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) } remotable_attachment :image, LIMIT diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index 84c08c158..25d3b87ef 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -24,14 +24,41 @@ class DomainBlock < ApplicationRecord scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } - def self.blocked?(domain) - where(domain: domain, severity: :suspend).exists? + class << self + def suspend?(domain) + !!rule_for(domain)&.suspend? + end + + def silence?(domain) + !!rule_for(domain)&.silence? + end + + def reject_media?(domain) + !!rule_for(domain)&.reject_media? + end + + def reject_reports?(domain) + !!rule_for(domain)&.reject_reports? + end + + alias blocked? suspend? + + def rule_for(domain) + return if domain.blank? + + uri = Addressable::URI.new.tap { |u| u.host = domain.gsub(/[\/]/, '') } + segments = uri.normalized_host.split('.') + variants = segments.map.with_index { |_, i| segments[i..-1].join('.') } + + where(domain: variants[0..-2]).order(Arel.sql('char_length(domain) desc')).first + end end def stricter_than?(other_block) - return true if suspend? + return true if suspend? return false if other_block.suspend? && (silence? || noop?) return false if other_block.silence? && noop? + (reject_media || !other_block.reject_media) && (reject_reports || !other_block.reject_reports) end diff --git a/app/models/instance.rb b/app/models/instance.rb index 7bf000d40..797a191e0 100644 --- a/app/models/instance.rb +++ b/app/models/instance.rb @@ -8,15 +8,11 @@ class Instance def initialize(resource) @domain = resource.domain @accounts_count = resource.is_a?(DomainBlock) ? nil : resource.accounts_count - @domain_block = resource.is_a?(DomainBlock) ? resource : DomainBlock.find_by(domain: domain) + @domain_block = resource.is_a?(DomainBlock) ? resource : DomainBlock.rule_for(domain) end - def cached_sample_accounts - Rails.cache.fetch("#{cache_key}/sample_accounts", expires_in: 12.hours) { Account.where(domain: domain).searchable.joins(:account_stat).popular.limit(3) } - end - - def cached_accounts_count - @accounts_count || Rails.cache.fetch("#{cache_key}/count", expires_in: 12.hours) { Account.where(domain: domain).count } + def countable? + @accounts_count.present? end def to_param diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 70a671b4a..815ac0258 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -24,16 +24,16 @@ class MediaAttachment < ApplicationRecord self.inheritance_column = nil - enum type: [:image, :gifv, :video, :audio, :unknown] + enum type: [:image, :gifv, :video, :unknown, :audio] IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].freeze VIDEO_FILE_EXTENSIONS = ['.webm', '.mp4', '.m4v', '.mov'].freeze - AUDIO_FILE_EXTENSIONS = ['.mp3', '.m4a', '.wav', '.ogg'].freeze + AUDIO_FILE_EXTENSIONS = ['.ogg', '.oga', '.mp3', '.m4a', '.wav', '.flac', '.opus'].freeze IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze - VIDEO_MIME_TYPES = ['video/webm', 'video/mp4', 'video/quicktime'].freeze + VIDEO_MIME_TYPES = ['video/webm', 'video/mp4', 'video/quicktime', 'video/ogg'].freeze VIDEO_CONVERTIBLE_MIME_TYPES = ['video/webm', 'video/quicktime'].freeze - AUDIO_MIME_TYPES = ['audio/mpeg', 'audio/mp4', 'audio/vnd.wav', 'audio/wav', 'audio/x-wav', 'audio/x-wave', 'audio/ogg',].freeze + AUDIO_MIME_TYPES = ['audio/wave', 'audio/wav', 'audio/x-wav', 'audio/x-wave', 'audio/vdn.wav', 'audio/x-pn-wave', 'audio/ogg', 'audio/mpeg', 'audio/mp3', 'audio/mp4', 'audio/webm', 'audio/flac'].freeze BLURHASH_OPTIONS = { x_comp: 4, @@ -53,22 +53,6 @@ class MediaAttachment < ApplicationRecord }, }.freeze - AUDIO_STYLES = { - original: { - format: 'mp4', - convert_options: { - output: { - filter_complex: '"[0:a]compand,showwaves=s=640x360:mode=line,format=yuv420p[v]"', - map: '"[v]" -map 0:a', - threads: 2, - vcodec: 'libx264', - acodec: 'aac', - movflags: '+faststart', - }, - }, - }, - }.freeze - VIDEO_STYLES = { small: { convert_options: { @@ -83,8 +67,21 @@ class MediaAttachment < ApplicationRecord }, }.freeze + AUDIO_STYLES = { + original: { + format: 'mp3', + content_type: 'audio/mpeg', + convert_options: { + output: { + 'q:a' => 2, + }, + }, + }, + }.freeze + VIDEO_FORMAT = { format: 'mp4', + content_type: 'video/mp4', convert_options: { output: { 'loglevel' => 'fatal', @@ -101,6 +98,11 @@ class MediaAttachment < ApplicationRecord }, }.freeze + VIDEO_CONVERTED_STYLES = { + small: VIDEO_STYLES[:small], + original: VIDEO_FORMAT, + }.freeze + IMAGE_LIMIT = (ENV['MAX_IMAGE_SIZE'] || 8.megabytes).to_i VIDEO_LIMIT = (ENV['MAX_VIDEO_SIZE'] || 40.megabytes).to_i @@ -114,8 +116,8 @@ class MediaAttachment < ApplicationRecord convert_options: { all: '-quality 90 -strip' } validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES - validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :video_or_gifv? - validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :video_or_gifv? + validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :larger_media_format? + validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :larger_media_format? remotable_attachment :file, VIDEO_LIMIT include Attachmentable @@ -138,8 +140,12 @@ class MediaAttachment < ApplicationRecord file.blank? && remote_url.present? end - def video_or_gifv? - video? || gifv? + def larger_media_format? + video? || gifv? || audio? + end + + def audio_or_video? + audio? || video? end def to_param @@ -171,37 +177,37 @@ class MediaAttachment < ApplicationRecord before_save :set_meta class << self + def supported_mime_types + IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES + end + + def supported_file_extensions + IMAGE_FILE_EXTENSIONS + VIDEO_FILE_EXTENSIONS + AUDIO_FILE_EXTENSIONS + end + private def file_styles(f) - if f.instance.file_content_type == 'image/gif' - { - small: IMAGE_STYLES[:small], - original: VIDEO_FORMAT, - } - elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type + if f.instance.file_content_type == 'image/gif' || VIDEO_CONVERTIBLE_MIME_TYPES.include?(f.instance.file_content_type) + VIDEO_CONVERTED_STYLES + elsif IMAGE_MIME_TYPES.include?(f.instance.file_content_type) IMAGE_STYLES - elsif AUDIO_MIME_TYPES.include? f.instance.file_content_type - AUDIO_STYLES - elsif VIDEO_CONVERTIBLE_MIME_TYPES.include?(f.instance.file_content_type) - { - small: VIDEO_STYLES[:small], - original: VIDEO_FORMAT, - } - else + elsif VIDEO_MIME_TYPES.include?(f.instance.file_content_type) VIDEO_STYLES + else + AUDIO_STYLES end end def file_processors(f) if f.file_content_type == 'image/gif' [:gif_transcoder, :blurhash_transcoder] - elsif VIDEO_MIME_TYPES.include? f.file_content_type - [:video_transcoder, :blurhash_transcoder] - elsif AUDIO_MIME_TYPES.include? f.file_content_type - [:audio_transcoder] + elsif VIDEO_MIME_TYPES.include?(f.file_content_type) + [:video_transcoder, :blurhash_transcoder, :type_corrector] + elsif AUDIO_MIME_TYPES.include?(f.file_content_type) + [:transcoder, :type_corrector] else - [:lazy_thumbnail, :blurhash_transcoder] + [:lazy_thumbnail, :blurhash_transcoder, :type_corrector] end end end @@ -224,7 +230,15 @@ class MediaAttachment < ApplicationRecord end def set_type_and_extension - self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : AUDIO_MIME_TYPES.include?(file_content_type) ? :audio : :image + self.type = begin + if VIDEO_MIME_TYPES.include?(file_content_type) + :video + elsif AUDIO_MIME_TYPES.include?(file_content_type) + :audio + else + :image + end + end end def set_meta @@ -267,7 +281,7 @@ class MediaAttachment < ApplicationRecord frame_rate: movie.frame_rate, duration: movie.duration, bitrate: movie.bitrate, - } + }.compact end def reset_parent_cache diff --git a/app/models/report.rb b/app/models/report.rb index 86c303798..5192ceef7 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -17,6 +17,8 @@ # class Report < ApplicationRecord + include Paginable + belongs_to :account belongs_to :target_account, class_name: 'Account' belongs_to :action_taken_by_account, class_name: 'Account', optional: true @@ -26,6 +28,7 @@ class Report < ApplicationRecord scope :unresolved, -> { where(action_taken: false) } scope :resolved, -> { where(action_taken: true) } + scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].each_with_object({}) { |k, h| h[k] = { user: [:invite_request, :invite] } }) } validates :comment, length: { maximum: 1000 } diff --git a/app/models/report_filter.rb b/app/models/report_filter.rb index 56ab28df7..a392d60c3 100644 --- a/app/models/report_filter.rb +++ b/app/models/report_filter.rb @@ -9,9 +9,11 @@ class ReportFilter def results scope = Report.unresolved + params.each do |key, value| scope = scope.merge scope_for(key, value) end + scope end diff --git a/app/models/user.rb b/app/models/user.rb index fcfb79612..f6936cb9d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -87,6 +87,7 @@ class User < ApplicationRecord scope :approved, -> { where(approved: true) } scope :confirmed, -> { where.not(confirmed_at: nil) } scope :enabled, -> { where(disabled: false) } + scope :disabled, -> { where(disabled: true) } scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) } scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended_at: nil }) } scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) } diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index efed199f3..c46caa28e 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -76,7 +76,7 @@ class InitialStateSerializer < ActiveModel::Serializer end def media_attachments - { accept_content_types: MediaAttachment::IMAGE_FILE_EXTENSIONS + MediaAttachment::VIDEO_FILE_EXTENSIONS + MediaAttachment::AUDIO_FILE_EXTENSIONS + MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES + MediaAttachment::AUDIO_MIME_TYPES } + { accept_content_types: MediaAttachment.supported_file_extensions + MediaAttachment.supported_mime_types } end private diff --git a/app/serializers/rest/admin/account_serializer.rb b/app/serializers/rest/admin/account_serializer.rb new file mode 100644 index 000000000..f579d3302 --- /dev/null +++ b/app/serializers/rest/admin/account_serializer.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +class REST::Admin::AccountSerializer < ActiveModel::Serializer + attributes :id, :username, :domain, :created_at, + :email, :ip, :role, :confirmed, :suspended, + :silenced, :disabled, :approved, :locale, + :invite_request + + attribute :created_by_application_id, if: :created_by_application? + attribute :invited_by_account_id, if: :invited? + + has_one :account, serializer: REST::AccountSerializer + + def id + object.id.to_s + end + + def email + object.user_email + end + + def ip + object.user_current_sign_in_ip.to_s.presence + end + + def role + object.user_role + end + + def suspended + object.suspended? + end + + def silenced + object.silenced? + end + + def confirmed + object.user_confirmed? + end + + def disabled + object.user_disabled? + end + + def approved + object.user_approved? + end + + def account + object + end + + def locale + object.user_locale + end + + def created_by_application_id + object.user&.created_by_application_id&.to_s&.presence + end + + def invite_request + object.user&.invite_request&.text + end + + def invited_by_account_id + object.user&.invite&.user&.account_id&.to_s&.presence + end + + def invited? + object.user&.invited? + end + + def created_by_application? + object.user&.created_by_application_id&.present? + end +end diff --git a/app/serializers/rest/admin/report_serializer.rb b/app/serializers/rest/admin/report_serializer.rb new file mode 100644 index 000000000..7a77132c0 --- /dev/null +++ b/app/serializers/rest/admin/report_serializer.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class REST::Admin::ReportSerializer < ActiveModel::Serializer + attributes :id, :action_taken, :comment, :created_at, :updated_at + + has_one :account, serializer: REST::Admin::AccountSerializer + has_one :target_account, serializer: REST::Admin::AccountSerializer + has_one :assigned_account, serializer: REST::Admin::AccountSerializer + has_one :action_taken_by_account, serializer: REST::Admin::AccountSerializer + + has_many :statuses, serializer: REST::StatusSerializer + + def id + object.id.to_s + end +end diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index 98c53c84a..e913f0c64 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -3,9 +3,9 @@ class REST::InstanceSerializer < ActiveModel::Serializer include RoutingHelper - attributes :uri, :title, :description, :email, + attributes :uri, :title, :short_description, :description, :email, :version, :urls, :stats, :thumbnail, :max_toot_chars, :poll_limits, - :languages, :registrations + :languages, :registrations, :approval_required has_one :contact_account, serializer: REST::AccountSerializer @@ -19,6 +19,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer Setting.site_title end + def short_description + Setting.site_short_description + end + def description Setting.site_description end @@ -68,6 +72,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode end + def approval_required + Setting.registrations_mode == 'approved' + end + private def instance_presenter diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index ad22d37fe..05c017bdf 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -205,7 +205,7 @@ class ActivityPub::ProcessAccountService < BaseService def domain_block return @domain_block if defined?(@domain_block) - @domain_block = DomainBlock.find_by(domain: @domain) + @domain_block = DomainBlock.rule_for(@domain) end def key_changed? diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index 497f0394b..c6eef04d4 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -76,7 +76,7 @@ class BlockDomainService < BaseService end def blocked_domain_accounts - Account.where(domain: blocked_domain) + Account.by_domain_and_subdomains(blocked_domain) end def media_from_blocked_domain @@ -84,6 +84,6 @@ class BlockDomainService < BaseService end def emojis_from_blocked_domains - CustomEmoji.where(domain: blocked_domain) + CustomEmoji.by_domain_and_subdomains(blocked_domain) end end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index c2584e090..6d7c44913 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -107,7 +107,7 @@ class PostStatusService < BaseService @media = @account.media_attachments.where(status_id: nil).where(id: @options[:media_ids].take(4).map(&:to_i)) - raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:video?) + raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:audio_or_video?) end def language_from_option(str) diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb index 11e33a83a..57c9ccfe1 100644 --- a/app/services/resolve_account_service.rb +++ b/app/services/resolve_account_service.rb @@ -146,7 +146,7 @@ class ResolveAccountService < BaseService def domain_block return @domain_block if defined?(@domain_block) - @domain_block = DomainBlock.find_by(domain: @domain) + @domain_block = DomainBlock.rule_for(@domain) end def atom_url diff --git a/app/services/unblock_domain_service.rb b/app/services/unblock_domain_service.rb index 9b8526fbe..fc262a50a 100644 --- a/app/services/unblock_domain_service.rb +++ b/app/services/unblock_domain_service.rb @@ -14,7 +14,8 @@ class UnblockDomainService < BaseService end def blocked_accounts - scope = Account.where(domain: domain_block.domain) + scope = Account.by_domain_and_subdomains(domain_block.domain) + if domain_block.silence? scope.where(silenced_at: @domain_block.created_at) else diff --git a/app/services/update_remote_profile_service.rb b/app/services/update_remote_profile_service.rb index 68d36addf..403395a0d 100644 --- a/app/services/update_remote_profile_service.rb +++ b/app/services/update_remote_profile_service.rb @@ -26,7 +26,7 @@ class UpdateRemoteProfileService < BaseService account.note = remote_profile.note || '' account.locked = remote_profile.locked? - if !account.suspended? && !DomainBlock.find_by(domain: account.domain)&.reject_media? + if !account.suspended? && !DomainBlock.reject_media?(account.domain) if remote_profile.avatar.present? account.avatar_remote_url = remote_profile.avatar else @@ -46,7 +46,7 @@ class UpdateRemoteProfileService < BaseService end def save_emojis - do_not_download = DomainBlock.find_by(domain: account.domain)&.reject_media? + do_not_download = DomainBlock.reject_media?(account.domain) return if do_not_download diff --git a/app/views/admin/instances/index.html.haml b/app/views/admin/instances/index.html.haml index 9574c3147..61e578409 100644 --- a/app/views/admin/instances/index.html.haml +++ b/app/views/admin/instances/index.html.haml @@ -33,21 +33,22 @@ %h4 = instance.domain %small - = t('admin.instances.known_accounts', count: instance.cached_accounts_count) - - if instance.domain_block + - first_item = true - if !instance.domain_block.noop? - • = t("admin.domain_blocks.severity.#{instance.domain_block.severity}") + - first_item = false - if instance.domain_block.reject_media? - • + - unless first_item + • = t('admin.domain_blocks.rejecting_media') + - first_item = false - if instance.domain_block.reject_reports? - • + - unless first_item + • = t('admin.domain_blocks.rejecting_reports') - - .avatar-stack - - instance.cached_sample_accounts.each do |account| - = image_tag current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar' - + - else + = t('admin.accounts.no_limits_imposed') + - if instance.countable? + .trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= number_to_human instance.accounts_count, strip_insignificant_zeros: true = paginate paginated_instances diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index 95b96feef..069d0053f 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -27,7 +27,7 @@ = render partial: 'stream_entries/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay } - if !status.media_attachments.empty? - - if status.media_attachments.first.video? + - if status.media_attachments.first.audio_or_video? - video = status.media_attachments.first = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do = render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments } diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index d383d3443..dcb4ce0b9 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -31,7 +31,7 @@ = render partial: 'stream_entries/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay } - if !status.media_attachments.empty? - - if status.media_attachments.first.video? + - if status.media_attachments.first.audio_or_video? - video = status.media_attachments.first = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description do = render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments } diff --git a/config/application.rb b/config/application.rb index 150fdce6c..4534ede49 100644 --- a/config/application.rb +++ b/config/application.rb @@ -10,7 +10,7 @@ require_relative '../app/lib/exceptions' require_relative '../lib/paperclip/lazy_thumbnail' require_relative '../lib/paperclip/gif_transcoder' require_relative '../lib/paperclip/video_transcoder' -require_relative '../lib/paperclip/audio_transcoder' +require_relative '../lib/paperclip/type_corrector' require_relative '../lib/mastodon/snowflake' require_relative '../lib/mastodon/version' require_relative '../lib/devise/ldap_authenticatable' diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 2a963b32b..a5c9caa4a 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -82,7 +82,13 @@ Doorkeeper.configure do :'read:search', :'read:statuses', :follow, - :push + :push, + :'admin:read', + :'admin:read:accounts', + :'admin:read:reports', + :'admin:write', + :'admin:write:accounts', + :'admin:write:reports' # Change the way client credentials are retrieved from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then diff --git a/config/locales/activerecord.fi.yml b/config/locales/activerecord.fi.yml index 23c538b19..2b2ffd121 100644 --- a/config/locales/activerecord.fi.yml +++ b/config/locales/activerecord.fi.yml @@ -1 +1,13 @@ +--- fi: + activerecord: + attributes: + poll: + expires_at: Määräaika + options: Vaihtoehdot + errors: + models: + account: + attributes: + username: + invalid: Vain kirjaimia, numeroita ja alleviivoja diff --git a/config/locales/activerecord.it.yml b/config/locales/activerecord.it.yml index 4cec9fb63..70afdaef1 100644 --- a/config/locales/activerecord.it.yml +++ b/config/locales/activerecord.it.yml @@ -1,12 +1,16 @@ --- it: activerecord: + attributes: + poll: + expires_at: Scadenza + options: Scelte errors: models: account: attributes: username: - invalid: solo lettere, numeri e trattino basso + invalid: solo lettere, numeri e trattini bassi status: attributes: reblog: diff --git a/config/locales/devise.it.yml b/config/locales/devise.it.yml index fc36fdbff..b603e12c6 100644 --- a/config/locales/devise.it.yml +++ b/config/locales/devise.it.yml @@ -12,6 +12,7 @@ it: last_attempt: Hai un altro tentativo prima che il tuo account venga bloccato. locked: Il tuo account è stato bloccato. not_found_in_database: "%{authentication_keys} o password invalida." + pending: Il tuo account è ancora in fase di approvazione. timeout: La tua sessione è terminata. Per favore, effettua l'accesso o registrati per continuare. unauthenticated: Devi effettuare l'accesso o registrarti per continuare. unconfirmed: Devi confermare il tuo indirizzo email per continuare. @@ -20,6 +21,7 @@ it: action: Verifica indirizzo email action_with_app: Conferma e torna a %{app} explanation: Hai creato un account su %{host} con questo indirizzo email. Sei lonatno solo un clic dall'attivarlo. Se non sei stato tu, per favore ignora questa email. + explanation_when_pending: Hai richiesto un invito a %{host} con questo indirizzo email. Una volta confermato il tuo indirizzo e-mail, analizzeremo la tua richiesta. Non potrai eseguire l'accesso fino a quel momento. Se la tua richiesta sarà rifiutata, i tuoi dati saranno rimossi, quindi nessun'altra azione ti sarà richiesta. Se non fossi stato tu, per favore ignora questa email. extra_html: Per favore controlla<a href="%{terms_path}">le regole del server</a> e <a href="%{policy_path}">i nostri termini di servizio</a>. subject: 'Mastodon: Istruzioni di conferma per %{instance}' title: Verifica indirizzo email @@ -60,6 +62,7 @@ it: signed_up: Benvenuto! Ti sei registrato con successo. signed_up_but_inactive: Ti sei registrato con successo. Purtroppo però non possiamo farti accedere perché non hai ancora attivato il tuo account. signed_up_but_locked: Ti sei registrato con successo. Purtroppo però non possiamo farti accedere perché il tuo account è bloccato. + signed_up_but_pending: Un messaggio con un collegamento per la conferma è stato inviato al tuo indirizzo email. Dopo aver cliccato il collegamento, esamineremo la tua richiesta. Ti sarà notificato se verrà approvata. signed_up_but_unconfirmed: Un messaggio con un link di conferma è stato inviato al tuo indirizzo email. Per favore, visita il link per attivare il tuo account. update_needs_confirmation: Hai aggiornato correttamente il tuo account, ma abbiamo bisogno di verificare il tuo nuovo indirizzo email. Per favore, controlla la posta in arrivo e visita il link di conferma per verificare il tuo indirizzo email. updated: Il tuo account è stato aggiornato con successo. diff --git a/config/locales/doorkeeper.ca.yml b/config/locales/doorkeeper.ca.yml index dfa46551f..dde70f47a 100644 --- a/config/locales/doorkeeper.ca.yml +++ b/config/locales/doorkeeper.ca.yml @@ -114,6 +114,12 @@ ca: application: title: OAuth autorització requerida scopes: + admin:read: llegir totes les dades en el servidor + admin:read:accounts: llegir l'informació sensible de tots els comptes + admin:read:reports: llegir l'informació sensible de tots els informes i comptes reportats + admin:write: modificar totes les dades en el servidor + admin:write:accounts: fer l'acció de moderació en els comptes + admin:write:reports: fer l'acció de moderació en els informes follow: seguir, blocar, desblocar i deixar de seguir comptes push: rebre notificacions push del teu compte read: llegir les dades del teu compte diff --git a/config/locales/doorkeeper.co.yml b/config/locales/doorkeeper.co.yml index 542ad7c57..d45041a4e 100644 --- a/config/locales/doorkeeper.co.yml +++ b/config/locales/doorkeeper.co.yml @@ -114,6 +114,12 @@ co: application: title: Auturizazione OAuth riquestata scopes: + admin:read: leghje tutti i dati nant'à u servore + admin:read:accounts: leghje i cuntinuti sensibili di tutti i conti + admin:read:reports: leghje i cuntinuti sensibili di tutti i rapporti è conti signalati + admin:write: mudificà tutti i dati nant'à u servore + admin:write:accounts: realizà azzione di muderazione nant'à i conti + admin:write:reports: realizà azzione di muderazione nant'à i rapporti follow: Mudificà rilazione trà i conti push: Riceve e vostre nutificazione push read: leghje tutte l’infurmazioni di u vostru contu diff --git a/config/locales/doorkeeper.cs.yml b/config/locales/doorkeeper.cs.yml index f523e125d..cb5cd147c 100644 --- a/config/locales/doorkeeper.cs.yml +++ b/config/locales/doorkeeper.cs.yml @@ -114,6 +114,12 @@ cs: application: title: Je požadována autorizace OAuth scopes: + admin:read: číst všechna data na serveru + admin:read:accounts: číst citlivé informace všech účtů + admin:read:reports: číst citlivé informace všech nahlášení a nahlášených účtů + admin:write: měnit všechna data na serveru + admin:write:accounts: provádět moderátorské akce s účty + admin:write:reports: provádět moderátorské akce s nahlášeními follow: upravovat vztahy mezi profily push: přijímat vaše push oznámení read: vidět všechna data vašeho účtu diff --git a/config/locales/doorkeeper.de.yml b/config/locales/doorkeeper.de.yml index edf9c660c..c41a847b2 100644 --- a/config/locales/doorkeeper.de.yml +++ b/config/locales/doorkeeper.de.yml @@ -114,6 +114,12 @@ de: application: title: OAuth-Autorisierung nötig scopes: + admin:read: alle Daten auf dem Server lesen + admin:read:accounts: sensible Daten aller Konten lesen + admin:read:reports: sensible Daten aller Meldungen und gemeldeten Konten lesen + admin:write: alle Daten auf dem Server ändern + admin:write:accounts: Moderationsaktionen auf Konten ausführen + admin:write:reports: Moderationsaktionen auf Meldungen ausführen follow: Kontenbeziehungen verändern push: deine Push-Benachrichtigungen erhalten read: all deine Daten lesen diff --git a/config/locales/doorkeeper.el.yml b/config/locales/doorkeeper.el.yml index e820ff8a6..c63688ade 100644 --- a/config/locales/doorkeeper.el.yml +++ b/config/locales/doorkeeper.el.yml @@ -114,6 +114,12 @@ el: application: title: Απαιτείται έγκριση OAuth scopes: + admin:read: ανάγνωση δεδομένων στον διακομιστή + admin:read:accounts: ανάγνωση ευαίσθητων πληροφοριών όλων των λογαριασμών + admin:read:reports: ανάγνωση ευαίσθητων πληροφοριών όλων των καταγγελιών και των καταγγελλομένων λογαριασμών + admin:write: αλλαγή δεδομένων στον διακομιστή + admin:write:accounts: εκτέλεση διαχειριστικών ενεργειών σε λογαριασμούς + admin:write:reports: εκτέλεση διαχειριστικών ενεργειών σε καταγγελίες follow: να αλλάζει τις σχέσεις με λογαριασμούς push: να λαμβάνει τις ειδοποιήσεις σου read: να διαβάζει όλα τα στοιχεία του λογαριασμού σου diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 211b210d7..4e9c83a8f 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -114,6 +114,12 @@ en: application: title: OAuth authorization required scopes: + admin:read: read all data on the server + admin:read:accounts: read sensitive information of all accounts + admin:read:reports: read sensitive information of all reports and reported accounts + admin:write: modify all data on the server + admin:write:accounts: perform moderation actions on accounts + admin:write:reports: perform moderation actions on reports follow: modify account relationships push: receive your push notifications read: read all your account's data diff --git a/config/locales/doorkeeper.gl.yml b/config/locales/doorkeeper.gl.yml index 0dc45d5a3..90cbd9b38 100644 --- a/config/locales/doorkeeper.gl.yml +++ b/config/locales/doorkeeper.gl.yml @@ -114,6 +114,12 @@ gl: application: title: Precisa autorización OAuth scopes: + admin:read: ler todos os datos no servidor + admin:read:accounts: ler información sensible de todas as contas + admin:read:reports: ler información sensible de todos os informes e contas reportadas + admin:write: modificar todos os datos no servidor + admin:write:accounts: executar accións de moderación nas contas + admin:write:reports: executar accións de moderación nos informes follow: modificar as relacións da conta push: recibir notificacións push read: ler todos os datos da súa conta diff --git a/config/locales/doorkeeper.it.yml b/config/locales/doorkeeper.it.yml index f6bd8b4bc..361d0bd75 100644 --- a/config/locales/doorkeeper.it.yml +++ b/config/locales/doorkeeper.it.yml @@ -36,9 +36,11 @@ it: scopes: Dividi gli scopes con spazi. Lascia vuoto per utilizzare gli scopes di default. index: application: Applicazione + callback_url: URL di callback delete: Elimina name: Nome new: Nuova applicazione + scopes: Visibilità show: Mostra title: Le tue applicazioni new: @@ -112,6 +114,12 @@ it: application: title: Autorizzazione OAuth richiesta scopes: + admin:read: leggere tutti i dati dal server + admin:read:accounts: leggere dati sensibili di tutti gli account + admin:read:reports: leggere dati sensibili di tutte le segnalazioni e gli account segnalati + admin:write: modificare tutti i dati sul server + admin:write:accounts: eseguire azioni di moderazione sugli account + admin:write:reports: eseguire azioni di moderazione sulle segnalazioni follow: modificare relazioni tra account push: ricevere le tue notifiche push read: leggere tutte le informazioni del tuo account diff --git a/config/locales/doorkeeper.ja.yml b/config/locales/doorkeeper.ja.yml index 9bc2d9a80..d80212f82 100644 --- a/config/locales/doorkeeper.ja.yml +++ b/config/locales/doorkeeper.ja.yml @@ -114,6 +114,12 @@ ja: application: title: OAuth認証 scopes: + admin:read: サーバーのすべてのデータの読み取り + admin:read:accounts: すべてのアカウントの機密情報の読み取り + admin:read:reports: すべての通報と通報されたアカウントの機密情報の読み取り + admin:write: サーバーのすべてのデータの変更 + admin:write:accounts: アカウントに対するアクションの実行 + admin:write:reports: 通報に対するアクションの実行 follow: アカウントのつながりを変更 push: プッシュ通知の受信 read: アカウントのすべてのデータの読み取り diff --git a/config/locales/doorkeeper.pl.yml b/config/locales/doorkeeper.pl.yml index de724f6c9..2068eeef4 100644 --- a/config/locales/doorkeeper.pl.yml +++ b/config/locales/doorkeeper.pl.yml @@ -114,6 +114,12 @@ pl: application: title: Uwierzytelnienie OAuth jest wymagane scopes: + admin:read: odczytaj wszystkie dane na serwerze + admin:read:accounts: odczytaj wrażliwe informacje na wszystkich kontach + admin:read:reports: odczytaj wrażliwe informacje ze wszystkich zgłoszeń oraz zgłoszonych kont + admin:write: zmodyfikuj wszystkie dane na serwerze + admin:write:accounts: wykonaj działania moderacyjne na kontach + admin:write:reports: wykonaj działania moderacyjne na zgłoszeniach follow: możliwość śledzenia kont push: otrzymywanie powiadomień push dla Twojego konta read: możliwość odczytu wszystkich danych konta diff --git a/config/locales/doorkeeper.sl.yml b/config/locales/doorkeeper.sl.yml index 341ed2830..26d92ddb5 100644 --- a/config/locales/doorkeeper.sl.yml +++ b/config/locales/doorkeeper.sl.yml @@ -114,6 +114,12 @@ sl: application: title: Potrebna je OAuth pooblastitev scopes: + admin:read: preberi vse podatke na strežniku + admin:read:accounts: preberi občutljive informacije vseh računov + admin:read:reports: preberi občutljive informacije vseh prijav in prijavljenih računov + admin:write: spremeni vse podatke na strežniku + admin:write:accounts: izvedi moderirana dejanja na računih + admin:write:reports: izvedi moderirana dejanja na prijavah follow: spremeni razmerja med računi push: prejmi potisna obvestila read: preberi vse podatke svojega računa diff --git a/config/locales/doorkeeper.zh-CN.yml b/config/locales/doorkeeper.zh-CN.yml index 1cce6adc2..dd9337904 100644 --- a/config/locales/doorkeeper.zh-CN.yml +++ b/config/locales/doorkeeper.zh-CN.yml @@ -113,6 +113,12 @@ zh-CN: application: title: 需要 OAuth 认证 scopes: + admin:read: 读取服务器上的所有数据 + admin:read:accounts: 读取所有账户的敏感信息 + admin:read:reports: 读取所有举报和被举报账户的敏感信息 + admin:write: 修改服务器上的所有数据 + admin:write:accounts: 对账户执行管理操作 + admin:write:reports: 对举报执行管理操作 follow: 关注或屏蔽用户 push: 接收你的帐户的推送通知 read: 读取你的帐户数据 diff --git a/config/locales/it.yml b/config/locales/it.yml index 1d1238056..6dfe212d1 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -7,23 +7,33 @@ it: active_count_after: attivo active_footnote: Utenti Attivi Mensili (MAU) administered_by: 'Amministrato da:' + api: API apps: Applicazioni Mobile apps_platforms: Usa Mastodon da iOS, Android e altre piattaforme + browse_directory: Sfoglia la directory dei profili e filtra per interessi + browse_public_posts: Sfoglia il flusso in tempo reale di post pubblici su Mastodon contact: Contatti contact_missing: Non impostato contact_unavailable: N/D + discover_users: Scopri utenti documentation: Documentazione extended_description_html: | <h3>Un buon posto per le regole</h3> <p>La descrizione estesa non è ancora stata preparata.</p> + federation_hint_html: Con un account su %{instance} sarai in grado di seguire persone su qualsiasi server Mastodon e oltre. generic_description: "%{domain} è un server nella rete" - get_apps: Prova l'app per smartphone + get_apps: Prova un'app per smartphone hosted_on: Mastodon ospitato su %{domain} learn_more: Scopri altro privacy_policy: Politica della privacy + see_whats_happening: Guarda cosa succede + server_stats: 'Statistiche del server:' source_code: Codice sorgente + status_count_after: + one: stato + other: stati status_count_before: Che hanno pubblicato - tagline: Segui vecchi amici e trovane nuovi + tagline: Segui amici e trovane di nuovi terms: Termini di Servizio user_count_after: one: utente @@ -40,6 +50,7 @@ it: joined: Dal %{date} last_active: ultima attività link_verified_on: La proprietà di questo link è stata controllata il %{date} + media: Media moved_html: "%{name} è stato spostato su %{new_profile_link}:" network_hidden: Questa informazione non e' disponibile nothing_here: Qui non c'è nulla! @@ -47,12 +58,17 @@ it: people_who_follow: Persone che seguono %{name} pin_errors: following: Devi gia seguire la persona che vuoi promuovere + posts: + one: Toot + other: Toot posts_tab_heading: Toot posts_with_replies: Toot e risposte reserved_username: Il nome utente è gia stato preso roles: admin: Amministratore + bot: Bot moderator: Moderatore + unavailable: Profilo non disponibile unfollow: Non seguire più admin: account_actions: @@ -64,7 +80,10 @@ it: delete: Elimina destroyed_msg: Nota di moderazione distrutta con successo! accounts: + approve: Approva + approve_all: Approva tutto are_you_sure: Sei sicuro? + avatar: Immagine di profilo by_domain: Dominio change_email: changed_msg: Account email cambiato con successo! @@ -84,6 +103,7 @@ it: display_name: Nome visualizzato domain: Dominio edit: Modifica + email: Email email_status: Stato email enable: Abilita enabled: Abilitato @@ -94,6 +114,7 @@ it: header: Intestazione inbox_url: URL inbox invited_by: Invitato da + ip: IP joined: Unito location: all: Tutto @@ -106,15 +127,18 @@ it: moderation: active: Attivo all: Tutto + pending: In attesa silenced: Silenziati suspended: Sospesi title: Moderazione moderation_notes: Note di moderazione most_recent_activity: Attività più recenti most_recent_ip: IP più recenti + no_account_selected: Nessun account è stato modificato visto che non ne è stato selezionato nessuno no_limits_imposed: Nessun limite imposto not_subscribed: Non sottoscritto outbox_url: URL outbox + pending: Revisioni in attesa perform_full_suspension: Sospendi profile_url: URL profilo promote: Promuovi @@ -122,6 +146,8 @@ it: public: Pubblico push_subscription_expires: Sottoscrizione PuSH scaduta redownload: Aggiorna avatar + reject: Rifiuta + reject_all: Rifiuta tutto remove_avatar: Rimuovi avatar remove_header: Rimuovi intestazione resend_confirmation: @@ -148,6 +174,7 @@ it: statuses: Stati subscribe: Sottoscrivi suspended: Sospeso + time_in_queue: Attesa in coda %{time} title: Account unconfirmed_email: Email non confermata undo_silenced: Rimuovi silenzia @@ -155,6 +182,7 @@ it: unsubscribe: Annulla l'iscrizione username: Nome utente warn: Avverti + web: Web action_logs: actions: assigned_to_self_report: "%{name} ha assegnato il rapporto %{target} a se stesso" @@ -188,6 +216,7 @@ it: update_custom_emoji: "%{name} ha aggiornato l'emoji %{target}" update_status: "%{name} stato aggiornato da %{target}" deleted_status: "(stato cancellato)" + title: Registro di controllo custom_emojis: by_domain: Dominio copied_msg: Creata con successo una copia locale dell'emoji @@ -198,6 +227,7 @@ it: destroyed_msg: Emoji distrutto con successo! disable: Disabilita disabled_msg: Questa emoji è stata disabilitata con successo + emoji: Emoji enable: Abilita enabled_msg: Questa emoji è stata abilitata con successo image_hint: PNG fino a 50 KB @@ -205,6 +235,7 @@ it: new: title: Aggiungi nuovo emoji personalizzato overwrite: Sovrascrivi + shortcode: Scorciatoia shortcode_hint: Almeno due caratteri, solo caratteri alfanumerici e trattino basso title: Emoji personalizzate unlisted: Non elencato @@ -212,19 +243,23 @@ it: updated_msg: Emoji aggiornata con successo! upload: Carica dashboard: + backlog: lavori arretrati config: Configurazione feature_deletions: Cancellazioni di account feature_invites: Link di invito feature_profile_directory: Directory dei profili feature_registrations: Registrazioni feature_relay: Ripetitore di federazione + feature_timeline_preview: Anteprima timeline features: Funzionalità hidden_service: Federazione con servizi nascosti open_reports: apri report recent_users: Utenti Recenti search: Ricerca testo intero single_user_mode: Modalita utente singolo + software: Software space: Utilizzo dello spazio + title: Cruscotto total_users: utenti totali trends: Tendenze week_interactions: interazioni per questa settimana @@ -235,6 +270,7 @@ it: created_msg: Il blocco del dominio sta venendo processato destroyed_msg: Il blocco del dominio è stato rimosso domain: Dominio + existing_domain_block_html: Hai già impostato limitazioni più stringenti su %{name}, dovresti <a href="%{unblock_url}">sbloccare</a> prima. new: create: Crea blocco hint: Il blocco dominio non previene la creazione di utenti nel database, ma applicherà automaticamente e retroattivamente metodi di moderazione specifici su quegli account. @@ -248,6 +284,8 @@ it: reject_media_hint: Rimuovi i file media salvati in locale e blocca i download futuri. Irrilevante per le sospensioni reject_reports: Respingi rapporti reject_reports_hint: Ignora tutti i rapporti provenienti da questo dominio. Irrilevante per sospensioni + rejecting_media: rigetta file media + rejecting_reports: rigetta segnalazioni severity: silence: silenziato suspend: sospeso @@ -276,16 +314,19 @@ it: title: Seguaci di %{acct} instances: by_domain: Dominio + delivery_available: Distribuzione disponibile known_accounts: one: "%{count} account noto" other: "%{count} account noti" moderation: + all: Tutto limited: Limitato title: Moderazione title: Istanze conosciute total_blocked_by_us: Bloccato da noi total_followed_by_them: Seguito da loro total_followed_by_us: Seguito da noi + total_reported: Segnalazioni su di loro total_storage: Media allegati invites: deactivate_all: Disattiva tutto @@ -295,6 +336,8 @@ it: expired: Scaduto title: Filtro title: Inviti + pending_accounts: + title: Account in attesa (%{count}) relays: add_new: Aggiungi ripetitore delete: Cancella @@ -308,12 +351,14 @@ it: pending: In attesa dell'approvazione del ripetitore save_and_enable: Salva e attiva setup: Crea una connessione con un ripetitore + status: Stato title: Ripetitori report_notes: created_msg: Nota rapporto creata! destroyed_msg: Nota rapporto cancellata! reports: account: + note: note report: rapporto action_taken_by: Azione intrapresa da are_you_sure: Sei sicuro? @@ -349,6 +394,7 @@ it: desc_html: Separa i nomi utente con virgola. Funziona solo con account locali e non bloccati. Quando vuoto, valido per tutti gli amministratori locali. title: Seguiti predefiniti per i nuovi utenti contact_information: + email: E-mail di lavoro username: Nome utente del contatto custom_css: desc_html: Modifica l'aspetto con il CSS caricato in ogni pagina @@ -378,10 +424,17 @@ it: min_invite_role: disabled: Nessuno title: Permetti inviti da + registrations_mode: + modes: + approved: Approvazione richiesta per le iscrizioni + none: Nessuno può iscriversi + open: Chiunque può iscriversi + title: Modalità di registrazione show_known_fediverse_at_about_page: desc_html: Quando attivato, mostra nell'anteprima i toot da tutte le istanze conosciute. Altrimenti mostra solo i toot locali. title: Mostra la fediverse conosciuta nell'anteprima della timeline show_staff_badge: + desc_html: Mostra un distintivo dello staff sulla pagina dell'utente title: Mostra badge staff site_description: desc_html: Paragrafo introduttivo nella pagina iniziale. Descrive ciò che rende speciale questo server Mastodon e qualunque altra cosa sia importante dire. Potete usare marcatori HTML, in particolare <code><a></code> e <code><em></code>. @@ -410,6 +463,8 @@ it: nsfw_off: Segna come non sensibile nsfw_on: Segna come sensibile failed_to_execute: Impossibile eseguire + media: + title: Media no_media: Nessun media no_status_selected: Nessun status è stato modificato perché nessuno era stato selezionato title: Gli status dell'account @@ -418,11 +473,14 @@ it: callback_url: URL Callback confirmed: Confermato expires_in: Scade in + last_delivery: Ultima distribuzione + title: WebSub topic: Argomento tags: accounts: Account hidden: Nascosto - hide: Non mostrare nella directory + hide: Nascondi dalla directory + name: Etichetta title: Hashtag unhide: Mostra nella directory visible: Visibile @@ -433,8 +491,25 @@ it: edit: Modifica edit_preset: Modifica avviso predefinito title: Gestisci avvisi predefiniti + admin_mailer: + new_pending_account: + body: I dettagli del nuovo account sono qui sotto. Puoi approvare o rifiutare questa richiesta. + subject: Nuovo account pronto per la revisione su %{instance} (%{username}) + new_report: + body: "%{reporter} ha segnalato %{target}" + body_remote: Qualcuno da %{domain} ha segnalato %{target} + subject: Nuova segnalazione per %{instance} (#%{id}) + appearance: + advanced_web_interface: Interfaccia web avanzata + advanced_web_interface_hint: |- + Se vuoi utilizzare l'intera larghezza dello schermo, l'interfaccia web avanzata ti consente di configurare varie colonne per mostrare più informazioni allo stesso tempo, secondo le tue preferenze: + Home, notifiche, timeline federata, qualsiasi numero di liste e etichette. + animations_and_accessibility: Animazioni e accessibiiltà + confirmation_dialogs: Dialoghi di conferma + sensitive_content: Contenuto sensibile application_mailer: notification_preferences: Cambia preferenze email + salutation: "%{name}," settings: 'Cambia le impostazioni per le email: %{link}' view: 'Guarda:' view_profile: Mostra profilo @@ -446,22 +521,32 @@ it: regenerate_token: Rigenera il token di accesso token_regenerated: Token di accesso rigenerato warning: Fa' molta attenzione con questi dati. Non fornirli mai a nessun altro! + your_token: Il tuo token di accesso auth: + apply_for_account: Richiedi un invito + change_password: Password + checkbox_agreement_html: Sono d'accordo con le <a href="%{rules_path}" target="_blank">regole del server</a> ed i <a href="%{terms_path}" target="_blank">termini di servizio</a> confirm_email: Conferma email delete_account: Elimina account delete_account_html: Se desideri cancellare il tuo account, puoi <a href="%{path}">farlo qui</a>. Ti sarà chiesta conferma. didnt_get_confirmation: Non hai ricevuto le istruzioni di conferma? forgot_password: Hai dimenticato la tua password? + invalid_reset_password_token: Il token di reimpostazione della password non è valido o è scaduto. Per favore richiedine uno nuovo. login: Entra logout: Esci da Mastodon migrate_account: Sposta ad un account differente migrate_account_html: Se vuoi che questo account sia reindirizzato a uno diverso, puoi <a href="%{path}">configurarlo qui</a>. or_log_in_with: Oppure accedi con + providers: + cas: CAS + saml: SAML register: Iscriviti + registration_closed: "%{instance} non accetta nuovi membri" resend_confirmation: Invia di nuovo le istruzioni di conferma reset_password: Resetta la password security: Credenziali set_new_password: Imposta una nuova password + trouble_logging_in: Problemi di accesso? authorize_follow: already_following: Stai già seguendo questo account error: Sfortunatamente c'è stato un errore nel consultare l'account remoto @@ -494,6 +579,7 @@ it: proceed: Cancella l'account success_msg: Il tuo account è stato cancellato warning_html: È garantita la cancellazione del contenuto solo da questo server. I contenuti che sono stati ampiamente condivisi probabilmente lasceranno delle tracce. I server offline e quelli che non ricevono più i tuoi aggiornamenti non aggiorneranno i loro database. + warning_title: Disponibilità di contenuto diffuso directories: directory: Directory dei profili enabled: Attualmente sei elencato nella directory. @@ -511,11 +597,14 @@ it: '422': content: Verifica di sicurezza non riuscita. Stai bloccando i cookies? title: Verifica di sicurezza non riuscita - '429': Throttled + '429': Limitato '500': content: Siamo spiacenti, ma qualcosa non ha funzionato dal nostro lato. title: Questa pagina non è corretta noscript_html: Per usare l'interfaccia web di Mastodon dovi abilitare JavaScript. In alternativa puoi provare una delle <a href="%{apps_path}">app native</a> per Mastodon per la tua piattaforma. + existing_username_validator: + not_found: impossibile trovare un utente locale con quel nome utente + not_found_multiple: impossibile trovare %{usernames} exports: archive_takeout: date: Data @@ -525,6 +614,7 @@ it: request: Richiedi il tuo archivio size: Dimensioni blocks: Stai bloccando + csv: CSV domain_blocks: Blocchi di dominio follows: Stai seguendo lists: Liste @@ -544,6 +634,7 @@ it: title: Modifica filtro errors: invalid_context: Contesto mancante o non valido + invalid_irreversible: Il filtraggio irreversibile funziona solo nei contesti di home o notifiche index: delete: Cancella title: Filtri @@ -552,13 +643,36 @@ it: footer: developers: Sviluppatori more: Altro… + resources: Risorse generic: + all: Tutto changes_saved_msg: Modifiche effettuate con successo! copy: Copia + order_by: Ordina per save_changes: Salva modifiche validation_errors: one: Qualcosa ancora non va bene! Per favore, controlla l'errore qui sotto other: Qualcosa ancora non va bene! Per favore, controlla i %{count} errori qui sotto + html_validator: + invalid_markup: 'contiene markup HTML non valido: %{error}' + identity_proofs: + active: Attive + authorize: Si, autorizza + authorize_connection_prompt: Autorizzare questa connessione crittografata? + errors: + failed: La connessione crittografata non è riuscita. Per favore riprova da %{provider}. + keybase: + invalid_token: I toked di Keybase sono hash di firme e devono essere lunghi 66 caratteri esadecimali + verification_failed: Keybase non riconosce questo token come firma dell'utente Keybase %{kb_username}. Per favore riprova da Keybase. + wrong_user: Impossibile creare una prova per %{proving} mentre si è effettuato l'accesso come %{current}. Accedi come %{proving} e riprova. + explanation_html: Qui puoi connettere crittograficamente le tue altre identità, come il profilo Keybase. Questo consente ad altre persone di inviarti messaggi criptati e fidarsi dei contenuto che tu invii a loro. + i_am_html: Io sono %{username} su %{service}. + identity: Identità + inactive: Inattiva + publicize_checkbox: 'E posta questo:' + publicize_toot: 'É provato! Io sono %{username} su %{service}: %{url}' + status: Stato della verifica + view_proof: Vedi prova imports: modes: merge: Fondi @@ -573,6 +687,7 @@ it: following: Lista dei seguaci muting: Lista dei silenziati upload: Carica + in_memoriam_html: In Memoriam. invites: delete: Disattiva expired: Scaduto @@ -643,14 +758,26 @@ it: body: 'Il tuo status è stato condiviso da %{name}:' subject: "%{name} ha condiviso il tuo status" title: Nuova condivisione + number: + human: + decimal_units: + format: "%n%u" + units: + billion: G + million: M + quadrillion: P + thousand: k + trillion: T pagination: newer: Più recente next: Avanti older: Più vecchio prev: Indietro + truncate: "…" polls: errors: already_voted: Hai già votato in questo sondaggio + duplicate_options: contiene oggetti duplicati duration_too_long: è troppo lontano nel futuro duration_too_short: è troppo presto expired: Il sondaggio si è già concluso @@ -659,12 +786,28 @@ it: too_many_options: non può contenere più di %{max} elementi preferences: other: Altro + posting_defaults: Predefinite di pubblicazione + public_timelines: Timeline pubbliche + relationships: + activity: Attività dell'account + dormant: Dormiente + last_active: Ultima volta attivo + most_recent: Più recente + moved: Trasferito + mutual: Reciproco + primary: Principale + relationship: Relazione + remove_selected_domains: Rimuovi tutti i seguaci dai domini selezionati + remove_selected_followers: Rimuovi i seguaci selezionati + remove_selected_follows: Smetti di seguire gli utenti selezionati + status: Stato dell'account remote_follow: acct: Inserisci il tuo username@dominio da cui vuoi seguire questo utente missing_resource: Impossibile trovare l'URL di reindirizzamento richiesto per il tuo account no_account_html: Non hai un account? Puoi <a href='%{sign_up_path}' target='_blank'>iscriverti qui</a> proceed: Conferma prompt: 'Stai per seguire:' + reason_html: "<strong>Perchè questo passo è necessario?</strong> <code>%{instance}</code> potrebbe non essere il server nel quale tu sei registrato, quindi dobbiamo reindirizzarti prima al tuo server." remote_interaction: favourite: proceed: Continua per segnare come apprezzato @@ -685,15 +828,49 @@ it: too_soon: La data di pubblicazione deve essere nel futuro sessions: activity: Ultima attività + browser: Browser browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + electron: Electron + firefox: Firefox generic: Browser sconosciuto + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Nokia S40 Ovi Browser + opera: Opera + otter: Otter + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo current_session: Sessione corrente description: "%{browser} su %{platform}" explanation: Questi sono i browser da cui attualmente è avvenuto l'accesso al tuo account Mastodon. + ip: IP platforms: + adobe_air: Adobe Air + android: Android + blackberry: Blackberry + chrome_os: ChromeOS + firefox_os: Firefox OS + ios: iOS + linux: Linux + mac: Mac other: piattaforma sconosciuta + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone + revoke: Revoca + revoke_success: Sessione revocata con successo title: Sessioni settings: + account: Account + account_settings: Impostazioni dell'account + appearance: Interfaccia authorized_apps: Applicazioni autorizzate back: Torna a Mastodon delete: Cancellazione account @@ -701,10 +878,14 @@ it: edit_profile: Modifica profilo export: Esporta impostazioni featured_tags: Hashtag in evidenza + identity_proofs: Prove di identità import: Importa + import_and_export: Importa ed esporta migrate: Migrazione dell'account notifications: Notifiche preferences: Preferenze + profile: Profilo + relationships: Follows and followers two_factor_authentication: Autenticazione a due fattori statuses: attached: @@ -712,7 +893,11 @@ it: image: one: "%{count} immagine" other: "%{count} immagini" + video: + one: "%{count} video" + other: "%{count} video" boosted_from_html: Condiviso da %{acct_link} + content_warning: 'Avviso di contenuto: %{warning}' disallowed_hashtags: one: 'contiene un hashtag non permesso: %{tags}' other: 'contiene gli hashtags non permessi: %{tags}' @@ -731,6 +916,7 @@ it: vote: Vota show_more: Mostra di più sign_in_to_participate: Accedi per partecipare alla conversazione + title: '%{name}: "%{quote}"' visibilities: private: Mostra solo ai tuoi seguaci private_long: Mostra solo ai seguaci @@ -748,6 +934,10 @@ it: contrast: Mastodon (contrasto elevato) default: Mastodon (scuro) mastodon-light: Mastodon (chiaro) + time: + formats: + default: "%b %d, %Y, %H:%M" + month: "%b %Y" two_factor_authentication: code_hint: Inserisci il codice generato dalla tua app di autenticazione description_html: Se abiliti <strong>l'autorizzazione a due fattori</strong>, entrare nel tuo account ti richiederà di avere vicino il tuo telefono, il quale ti genererà un codice per eseguire l'accesso. @@ -769,7 +959,24 @@ it: explanation: Hai richiesto un backup completo del tuo account Mastodon. È pronto per essere scaricato! subject: Il tuo archivio è pronto per essere scaricato title: Esportazione archivio + warning: + explanation: + disable: Mentre il tuo account è congelato, i tuoi dati dell'account rimangono intatti, ma non potrai eseguire nessuna azione fintanto che non viene sbloccato. + silence: Mentre il tuo account è limitato, solo le persone che già ti seguono possono vedere i tuoi toot su questo server, e potresti essere escluso da vari elenchi pubblici. Comunque, altri possono manualmente seguirti. + suspend: Il tuo account è stato sospeso, e tutti i tuoi toot ed i tuoi file media caricati sono stati irreversibilmente rimossi da questo server, e dai server dove avevi dei seguaci. + review_server_policies: Rivedi regole del server + subject: + disable: Il tuo account %{acct} è stato congelato + none: Avviso per %{acct} + silence: Il tuo account %{acct} è stato limitato + suspend: Il tuo account %{acct} è stato sospeso + title: + disable: Account congelato + none: Avviso + silence: Account limitato + suspend: Account sospeso welcome: + edit_profile_action: Imposta profilo edit_profile_step: Puoi personalizzare il tuo profilo caricando un avatar, un'intestazione, modificando il tuo nome visualizzato e così via. Se vuoi controllare i tuoi nuovi seguaci prima di autorizzarli a seguirti, puoi bloccare il tuo account. explanation: Ecco alcuni suggerimenti per iniziare final_action: Inizia a postare @@ -789,6 +996,7 @@ it: follow_limit_reached: Non puoi seguire più di %{limit} persone invalid_email: L'indirizzo email inserito non è valido invalid_otp_token: Codice d'accesso non valido + otp_lost_help_html: Se perdessi l'accesso ad entrambi, puoi entrare in contatto con %{email} seamless_external_login: Ti sei collegato per mezzo di un servizio esterno, quindi le impostazioni di email e password non sono disponibili. signed_in_as: 'Hai effettuato l''accesso come:' verification: diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 538e6d554..3285a26b6 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -35,7 +35,7 @@ pl: one: wpisu other: wpisów status_count_before: Są autorami - tagline: Śledź znajomych i poznawal nowych + tagline: Śledź znajomych i poznawaj nowych terms: Zasady użytkowania user_count_after: few: użytkowników diff --git a/config/locales/simple_form.it.yml b/config/locales/simple_form.it.yml index b832a8035..377a55293 100644 --- a/config/locales/simple_form.it.yml +++ b/config/locales/simple_form.it.yml @@ -27,6 +27,7 @@ it: phrase: Il confronto sarà eseguito ignorando minuscole/maiuscole e i content warning scopes: A quali API l'applicazione potrà avere accesso. Se selezionate un ambito di alto livello, non c'è bisogno di selezionare quelle singole. setting_aggregate_reblogs: Non mostrare nuove condivisioni per toot che sono stati condivisi di recente (ha effetto solo sulle nuove condivisioni) + setting_default_sensitive: Media con contenuti sensibili sono nascosti in modo predefinito e possono essere rivelati con un click setting_display_media_default: Nascondi media segnati come sensibili setting_display_media_hide_all: Nascondi sempre tutti i media setting_display_media_show_all: Nascondi sempre i media segnati come sensibili @@ -39,6 +40,8 @@ it: name: 'Eccone alcuni che potresti usare:' imports: data: File CSV esportato da un altro server Mastodon + invite_request: + text: Questo ci aiuterà ad esaminare la tua richiesta sessions: otp: 'Inserisci il codice a due fattori generato dall''app del tuo telefono o usa uno dei codici di recupero:' user: @@ -62,12 +65,14 @@ it: warning_preset_id: Usa un avviso preimpostato defaults: autofollow: Invita a seguire il tuo account + avatar: Immagine di profilo bot: Questo account è un bot chosen_languages: Filtra lingue confirm_new_password: Conferma nuova password confirm_password: Conferma password context: Contesti del filtro current_password: Password corrente + data: Data discoverable: Inserisci questo account nella directory display_name: Nome visualizzato email: Indirizzo email @@ -82,7 +87,9 @@ it: new_password: Nuova password note: Biografia otp_attempt: Codice due-fattori + password: Password phrase: Parola chiave o frase + setting_advanced_layout: Abilita interfaccia web avanzata setting_aggregate_reblogs: Raggruppa condivisioni in timeline setting_auto_play_gif: Play automatico GIF animate setting_boost_modal: Mostra dialogo di conferma prima del boost @@ -107,18 +114,26 @@ it: username: Nome utente username_or_email: Nome utente o email whole_word: Parola intera + featured_tag: + name: Etichetta interactions: must_be_follower: Blocca notifiche da chi non ti segue must_be_following: Blocca notifiche dalle persone che non segui must_be_following_dm: Blocca i messaggi diretti dalle persone che non segui + invite_request: + text: Perchè vuoi unirti? notification_emails: digest: Invia email riassuntive favourite: Invia email quando segna come preferito al tuo stato follow: Invia email quando qualcuno ti segue follow_request: Invia email quando qualcuno richiede di seguirti mention: Invia email quando qualcuno ti menziona + pending_account: Invia e-mail quando un nuovo account richiede l'approvazione reblog: Invia email quando qualcuno da un boost al tuo stato report: Manda una mail quando viene inviato un nuovo rapporto + 'no': 'No' + recommended: Consigliato required: + mark: "*" text: richiesto 'yes': Si diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 89f2e7a8d..dd7d9304d 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -135,5 +135,6 @@ ja: 'no': いいえ recommended: おすすめ required: + mark: "*" text: 必須 'yes': はい diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 3e1255276..75a43e322 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -546,6 +546,7 @@ sk: migrate_account_html: Ak si želáš presmerovať tento účet na nejaký iný, môžeš si to <a href="%{path}">nastaviť tu</a>. or_log_in_with: Alebo prihlás s register: Zaregistruj sa + registration_closed: "%{instance} neprijíma nových členov" resend_confirmation: Zašli potvrdzujúce pokyny znovu reset_password: Obnov heslo security: Zabezpečenie @@ -668,6 +669,7 @@ sk: publicize_checkbox: 'A poslať toto:' publicize_toot: 'Je to dokázané! Na %{service} som %{username}: %{url}' status: Stav overenia + view_proof: Ukáž overenie imports: modes: merge: Spoj dohromady @@ -766,6 +768,7 @@ sk: too_many_options: nemôže zahŕňať viac ako %{max} položiek preferences: other: Ostatné + public_timelines: Verejné časové osi relationships: activity: Aktivita účtu dormant: Spiace @@ -773,6 +776,7 @@ sk: most_recent: Najnovšie moved: Presunuli sa mutual: Spoločné + primary: Hlavné relationship: Vzťah remove_selected_followers: Odstráň vybraných následovatrľov remove_selected_follows: Prestaň sledovať vybraných užívateľov diff --git a/config/routes.rb b/config/routes.rb index 0d49a07d6..05bcaf9e2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -409,6 +409,29 @@ Rails.application.routes.draw do namespace :push do resource :subscription, only: [:create, :show, :update, :destroy] end + + namespace :admin do + resources :accounts, only: [:index, :show] do + member do + post :enable + post :unsilence + post :unsuspend + post :approve + post :reject + end + + resource :action, only: [:create], controller: 'account_actions' + end + + resources :reports, only: [:index, :show] do + member do + post :assign_to_self + post :unassign + post :reopen + post :resolve + end + end + end end namespace :v2 do diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 56b846a36..cd216b92d 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 0 + 2 end def pre diff --git a/lib/paperclip/audio_transcoder.rb b/lib/paperclip/audio_transcoder.rb deleted file mode 100644 index 323ec7bfe..000000000 --- a/lib/paperclip/audio_transcoder.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module Paperclip - class AudioTranscoder < Paperclip::Processor - def make - max_aud_len = (ENV['MAX_AUDIO_LENGTH'] || 60.0).to_f - - meta = ::Av.cli.identify(@file.path) - # {:length=>"0:00:02.14", :duration=>2.14, :audio_encode=>"mp3", :audio_bitrate=>"44100 Hz", :audio_channels=>"mono"} - if meta[:duration] > max_aud_len - raise Mastodon::ValidationError, "Audio uploads must be less than #{max_aud_len} seconds in length." - end - - final_file = Paperclip::Transcoder.make(file, options, attachment) - - attachment.instance.file_file_name = 'media.mp4' - attachment.instance.file_content_type = 'video/mp4' - attachment.instance.type = MediaAttachment.types[:video] - - final_file - end - end -end diff --git a/lib/paperclip/type_corrector.rb b/lib/paperclip/type_corrector.rb new file mode 100644 index 000000000..0b0c10a56 --- /dev/null +++ b/lib/paperclip/type_corrector.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'mime/types/columnar' + +module Paperclip + class TypeCorrector < Paperclip::Processor + def make + target_extension = options[:format] + extension = File.extname(attachment.instance.file_file_name) + + return @file unless options[:style] == :original && target_extension && extension != target_extension + + attachment.instance.file_content_type = options[:content_type] || attachment.instance.file_content_type + attachment.instance.file_file_name = File.basename(attachment.instance.file_file_name, '.*') + '.' + target_extension + + @file + end + end +end diff --git a/spec/controllers/api/v1/admin/account_actions_controller_spec.rb b/spec/controllers/api/v1/admin/account_actions_controller_spec.rb new file mode 100644 index 000000000..a5a8f4bb0 --- /dev/null +++ b/spec/controllers/api/v1/admin/account_actions_controller_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +RSpec.describe Api::V1::Admin::AccountActionsController, type: :controller do + render_views + + let(:role) { 'moderator' } + let(:user) { Fabricate(:user, role: role, account: Fabricate(:account, username: 'alice')) } + let(:scopes) { 'admin:read admin:write' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:account) { Fabricate(:user).account } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + shared_examples 'forbidden for wrong scope' do |wrong_scope| + let(:scopes) { wrong_scope } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + shared_examples 'forbidden for wrong role' do |wrong_role| + let(:role) { wrong_role } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + describe 'POST #create' do + before do + post :create, params: { account_id: account.id, type: 'disable' } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'performs action against account' do + expect(account.reload.user_disabled?).to be true + end + + it 'logs action' do + log_item = Admin::ActionLog.last + + expect(log_item).to_not be_nil + expect(log_item.action).to eq :disable + expect(log_item.account_id).to eq user.account_id + expect(log_item.target_id).to eq account.user.id + end + end +end diff --git a/spec/controllers/api/v1/admin/accounts_controller_spec.rb b/spec/controllers/api/v1/admin/accounts_controller_spec.rb new file mode 100644 index 000000000..f3f9946ba --- /dev/null +++ b/spec/controllers/api/v1/admin/accounts_controller_spec.rb @@ -0,0 +1,147 @@ +require 'rails_helper' + +RSpec.describe Api::V1::Admin::AccountsController, type: :controller do + render_views + + let(:role) { 'moderator' } + let(:user) { Fabricate(:user, role: role, account: Fabricate(:account, username: 'alice')) } + let(:scopes) { 'admin:read admin:write' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:account) { Fabricate(:user).account } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + shared_examples 'forbidden for wrong scope' do |wrong_scope| + let(:scopes) { wrong_scope } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + shared_examples 'forbidden for wrong role' do |wrong_role| + let(:role) { wrong_role } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + describe 'GET #index' do + before do + get :index + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'GET #show' do + before do + get :show, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #approve' do + before do + account.user.update(approved: false) + post :approve, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'approves user' do + expect(account.reload.user_approved?).to be true + end + end + + describe 'POST #reject' do + before do + account.user.update(approved: false) + post :reject, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'removes user' do + expect(User.where(id: account.user.id).count).to eq 0 + end + end + + describe 'POST #enable' do + before do + account.user.update(disabled: true) + post :enable, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'enables user' do + expect(account.reload.user_disabled?).to be false + end + end + + describe 'POST #unsuspend' do + before do + account.touch(:suspended_at) + post :unsuspend, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'unsuspends account' do + expect(account.reload.suspended?).to be false + end + end + + describe 'POST #unsilence' do + before do + account.touch(:silenced_at) + post :unsilence, params: { id: account.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'unsilences account' do + expect(account.reload.silenced?).to be false + end + end +end diff --git a/spec/controllers/api/v1/admin/reports_controller_spec.rb b/spec/controllers/api/v1/admin/reports_controller_spec.rb new file mode 100644 index 000000000..4ed3c5dc4 --- /dev/null +++ b/spec/controllers/api/v1/admin/reports_controller_spec.rb @@ -0,0 +1,109 @@ +require 'rails_helper' + +RSpec.describe Api::V1::Admin::ReportsController, type: :controller do + render_views + + let(:role) { 'moderator' } + let(:user) { Fabricate(:user, role: role, account: Fabricate(:account, username: 'alice')) } + let(:scopes) { 'admin:read admin:write' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:report) { Fabricate(:report) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + shared_examples 'forbidden for wrong scope' do |wrong_scope| + let(:scopes) { wrong_scope } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + shared_examples 'forbidden for wrong role' do |wrong_role| + let(:role) { wrong_role } + + it 'returns http forbidden' do + expect(response).to have_http_status(403) + end + end + + describe 'GET #index' do + before do + get :index + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'GET #show' do + before do + get :show, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #resolve' do + before do + post :resolve, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #reopen' do + before do + post :reopen, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #assign_to_self' do + before do + post :assign_to_self, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + describe 'POST #unassign' do + before do + post :unassign, params: { id: report.id } + end + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', 'user' + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 379872316..ce9ea250d 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -687,6 +687,23 @@ RSpec.describe Account, type: :model do end end + describe 'by_domain_and_subdomains' do + it 'returns exact domain matches' do + account = Fabricate(:account, domain: 'example.com') + expect(Account.by_domain_and_subdomains('example.com')).to eq [account] + end + + it 'returns subdomains' do + account = Fabricate(:account, domain: 'foo.example.com') + expect(Account.by_domain_and_subdomains('example.com')).to eq [account] + end + + it 'does not return partially matching domains' do + account = Fabricate(:account, domain: 'grexample.com') + expect(Account.by_domain_and_subdomains('example.com')).to_not eq [account] + end + end + describe 'expiring' do it 'returns remote accounts with followers whose subscription expiration date is past or not given' do local = Fabricate(:account, domain: nil) diff --git a/spec/models/domain_block_spec.rb b/spec/models/domain_block_spec.rb index 0035fd0ff..d98c5e118 100644 --- a/spec/models/domain_block_spec.rb +++ b/spec/models/domain_block_spec.rb @@ -21,23 +21,40 @@ RSpec.describe DomainBlock, type: :model do end end - describe 'blocked?' do + describe '.blocked?' do it 'returns true if the domain is suspended' do - Fabricate(:domain_block, domain: 'domain', severity: :suspend) - expect(DomainBlock.blocked?('domain')).to eq true + Fabricate(:domain_block, domain: 'example.com', severity: :suspend) + expect(DomainBlock.blocked?('example.com')).to eq true end it 'returns false even if the domain is silenced' do - Fabricate(:domain_block, domain: 'domain', severity: :silence) - expect(DomainBlock.blocked?('domain')).to eq false + Fabricate(:domain_block, domain: 'example.com', severity: :silence) + expect(DomainBlock.blocked?('example.com')).to eq false end it 'returns false if the domain is not suspended nor silenced' do - expect(DomainBlock.blocked?('domain')).to eq false + expect(DomainBlock.blocked?('example.com')).to eq false end end - describe 'stricter_than?' do + describe '.rule_for' do + it 'returns rule matching a blocked domain' do + block = Fabricate(:domain_block, domain: 'example.com') + expect(DomainBlock.rule_for('example.com')).to eq block + end + + it 'returns a rule matching a subdomain of a blocked domain' do + block = Fabricate(:domain_block, domain: 'example.com') + expect(DomainBlock.rule_for('sub.example.com')).to eq block + end + + it 'returns a rule matching a blocked subdomain' do + block = Fabricate(:domain_block, domain: 'sub.example.com') + expect(DomainBlock.rule_for('sub.example.com')).to eq block + end + end + + describe '#stricter_than?' do it 'returns true if the new block has suspend severity while the old has lower severity' do suspend = DomainBlock.new(domain: 'domain', severity: :suspend) silence = DomainBlock.new(domain: 'domain', severity: :silence) |