1
0
Fork 0
forked from gitea/nas

Merge remote-tracking branch 'parent/main' into upstream-20240906

This commit is contained in:
KMY 2024-09-09 08:47:15 +09:00
commit 218cb37fe3
176 changed files with 750 additions and 603 deletions

View file

@ -13,7 +13,7 @@ module Admin
redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.created_msg')
else
@account = @account_moderation_note.target_account
@moderation_notes = @account.targeted_moderation_notes.latest
@moderation_notes = @account.targeted_moderation_notes.chronological.includes(:account)
@warnings = @account.strikes.custom.latest
render 'admin/accounts/show'

View file

@ -33,7 +33,7 @@ module Admin
@deletion_request = @account.deletion_request
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
@moderation_notes = @account.targeted_moderation_notes.latest
@moderation_notes = @account.targeted_moderation_notes.chronological.includes(:account)
@warnings = @account.strikes.includes(:target_account, :account, :appeal).latest
@domain_block = DomainBlock.rule_for(@account.domain)
end

View file

@ -7,17 +7,12 @@ module Admin
layout 'admin'
before_action :set_body_classes
before_action :set_cache_headers
after_action :verify_authorized
private
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end

View file

@ -7,12 +7,12 @@ module Admin
def index
authorize :dashboard, :index?
@pending_appeals_count = Appeal.pending.async_count
@pending_reports_count = Report.unresolved.async_count
@pending_tags_count = Tag.pending_review.async_count
@pending_users_count = User.pending.async_count
@system_checks = Admin::SystemCheck.perform(current_user)
@time_period = (29.days.ago.to_date...Time.now.utc.to_date)
@pending_users_count = User.pending.count
@pending_reports_count = Report.unresolved.count
@pending_tags_count = Tag.pending_review.count
@pending_appeals_count = Appeal.pending.count
end
end
end

View file

@ -21,7 +21,7 @@ module Admin
redirect_to after_create_redirect_path, notice: I18n.t('admin.report_notes.created_msg')
else
@report_notes = @report.notes.includes(:account).order(id: :desc)
@report_notes = @report.notes.chronological.includes(:account)
@action_logs = @report.history.includes(:target)
@form = Admin::StatusBatchAction.new
@statuses = @report.statuses.with_includes

View file

@ -13,7 +13,7 @@ module Admin
authorize @report, :show?
@report_note = @report.notes.new
@report_notes = @report.notes.includes(:account).order(id: :desc)
@report_notes = @report.notes.chronological.includes(:account)
@action_logs = @report.history.includes(:target)
@form = Admin::StatusBatchAction.new
@statuses = @report.statuses.with_includes

View file

@ -11,7 +11,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
before_action :set_sessions, only: [:edit, :update]
before_action :set_strikes, only: [:edit, :update]
before_action :set_body_classes, only: [:new, :create, :edit, :update]
before_action :require_not_suspended!, only: [:update]
before_action :set_cache_headers, only: [:edit, :update]
before_action :set_rules, only: :new
@ -104,10 +103,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
private
def set_body_classes
@body_classes = 'admin' if %w(edit update).include?(action_name)
end
def set_invite
@invite = begin
invite = Invite.find_by(code: invite_code) if invite_code.present?

View file

@ -7,16 +7,11 @@ class Disputes::BaseController < ApplicationController
skip_before_action :require_functional!
before_action :set_body_classes
before_action :authenticate_user!
before_action :set_cache_headers
private
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end

View file

@ -6,7 +6,6 @@ class Filters::StatusesController < ApplicationController
before_action :authenticate_user!
before_action :set_filter
before_action :set_status_filters
before_action :set_body_classes
before_action :set_cache_headers
PER_PAGE = 20
@ -42,10 +41,6 @@ class Filters::StatusesController < ApplicationController
'remove' if params[:remove]
end
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end

View file

@ -5,7 +5,6 @@ class FiltersController < ApplicationController
before_action :authenticate_user!
before_action :set_filter, only: [:edit, :update, :destroy]
before_action :set_body_classes
before_action :set_cache_headers
def index
@ -52,10 +51,6 @@ class FiltersController < ApplicationController
params.require(:custom_filter).permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :exclude_quote, :exclude_profile, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy])
end
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end

View file

@ -6,7 +6,6 @@ class InvitesController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :set_body_classes
before_action :set_cache_headers
def index
@ -47,10 +46,6 @@ class InvitesController < ApplicationController
params.require(:invite).permit(:max_uses, :expires_in, :autofollow, :comment)
end
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end

View file

@ -19,9 +19,7 @@ class MediaController < ApplicationController
redirect_to @media_attachment.file.url(:original)
end
def player
@body_classes = 'player'
end
def player; end
private

View file

@ -6,7 +6,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
before_action :store_current_location
before_action :authenticate_resource_owner!
before_action :require_not_suspended!, only: :destroy
before_action :set_body_classes
before_action :set_cache_headers
before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json }
@ -23,10 +22,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
private
def set_body_classes
@body_classes = 'admin'
end
def store_current_location
store_location_for(:user, request.url)
end

View file

@ -6,7 +6,6 @@ class RelationshipsController < ApplicationController
before_action :authenticate_user!
before_action :set_accounts, only: :show
before_action :set_relationships, only: :show
before_action :set_body_classes
before_action :set_cache_headers
helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship?
@ -68,10 +67,6 @@ class RelationshipsController < ApplicationController
end
end
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end

View file

@ -4,15 +4,10 @@ class Settings::BaseController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :set_body_classes
before_action :set_cache_headers
private
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end

View file

@ -4,7 +4,6 @@ class SeveredRelationshipsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :set_body_classes
before_action :set_cache_headers
before_action :set_event, only: [:following, :followers]
@ -51,10 +50,6 @@ class SeveredRelationshipsController < ApplicationController
account.local? ? account.local_username_and_domain : account.acct
end
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end

View file

@ -4,13 +4,6 @@ class SharesController < ApplicationController
layout 'modal'
before_action :authenticate_user!
before_action :set_body_classes
def show; end
private
def set_body_classes
@body_classes = 'modal-layout compose-standalone'
end
end

View file

@ -5,7 +5,6 @@ class StatusesCleanupController < ApplicationController
before_action :authenticate_user!
before_action :set_policy
before_action :set_body_classes
before_action :set_cache_headers
def show; end
@ -34,10 +33,6 @@ class StatusesCleanupController < ApplicationController
params.require(:account_statuses_cleanup_policy).permit(:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :keep_self_emoji, :min_favs, :min_reblogs, :min_emojis)
end
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end

View file

@ -5,7 +5,7 @@ module Admin::Trends::StatusesHelper
text = if status.local?
status.text.split("\n").first
else
Nokogiri::HTML(status.text).css('html > body > *').first&.text
Nokogiri::HTML5(status.text).css('html > body > *').first&.text
end
return '' if text.blank?

View file

@ -165,6 +165,7 @@ module ApplicationHelper
def body_classes
output = body_class_string.split
output << content_for(:body_classes)
output << "theme-#{current_theme.parameterize}"
output << 'system-font' if current_account&.user&.setting_system_font_ui
output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion')

View file

@ -1,6 +1,6 @@
{
"about.blocks": "Moderoidut palvelimet",
"about.contact": "Yhteydenotto:",
"about.contact": "Yhteystiedot:",
"about.disclaimer": "Mastodon on vapaa avoimen lähdekoodin ohjelmisto ja Mastodon gGmbH:n tavaramerkki.",
"about.domain_blocks.no_reason_available": "Syy ei ole tiedossa",
"about.domain_blocks.preamble": "Mastodonin avulla voi yleensä tarkastella minkä tahansa fediversumiin kuuluvan palvelimen sisältöä ja olla yhteyksissä eri palvelinten käyttäjien kanssa. Nämä poikkeukset koskevat yksin tätä palvelinta.",
@ -304,7 +304,7 @@
"filter_modal.select_filter.title": "Suodata tämä julkaisu",
"filter_modal.title.status": "Suodata julkaisu",
"filter_warning.matches_filter": "Vastaa suodatinta ”{title}”",
"filtered_notifications_banner.pending_requests": "{count, plural, =0 {Ei keneltäkään, jonka} one {1 käyttäjältä, jonka} other {# käyttäjältä, jotka}} saatat tuntea",
"filtered_notifications_banner.pending_requests": "{count, plural, =0 {Ei keneltäkään, jonka} one {Yhdeltä käyttäjältä, jonka} other {# käyttäjältä, jotka}} saatat tuntea",
"filtered_notifications_banner.title": "Suodatetut ilmoitukset",
"firehose.all": "Kaikki",
"firehose.local": "Tämä palvelin",
@ -370,7 +370,7 @@
"home.show_announcements": "Näytä tiedotteet",
"ignore_notifications_modal.disclaimer": "Mastodon ei voi ilmoittaa käyttäjille, että olet sivuuttanut heidän ilmoituksensa. Ilmoitusten sivuuttaminen ei lopeta itse viestien lähetystä.",
"ignore_notifications_modal.filter_instead": "Suodata sen sijaan",
"ignore_notifications_modal.filter_to_act_users": "Voit silti hyväksyä, hylätä tai raportoida käyttäjiä",
"ignore_notifications_modal.filter_to_act_users": "Voit kuitenkin yhä hyväksyä, hylätä tai raportoida käyttäjiä",
"ignore_notifications_modal.filter_to_avoid_confusion": "Suodatus auttaa välttämään mahdollisia sekaannuksia",
"ignore_notifications_modal.filter_to_review_separately": "Voit käydä suodatettuja ilmoituksia läpi erikseen",
"ignore_notifications_modal.ignore": "Sivuuta ilmoitukset",

View file

@ -97,6 +97,8 @@
"block_modal.title": "An bhfuil fonn ort an t-úsáideoir a bhlocáil?",
"block_modal.you_wont_see_mentions": "Ní fheicfidh tú postálacha a luann iad.",
"boost_modal.combo": "Is féidir leat {combo} a bhrú chun é seo a scipeáil an chéad uair eile",
"boost_modal.reblog": "An post a threisiú?",
"boost_modal.undo_reblog": "An deireadh a chur le postáil?",
"bundle_column_error.copy_stacktrace": "Cóipeáil tuairisc earráide",
"bundle_column_error.error.body": "Ní féidir an leathanach a iarradh a sholáthar. Seans gurb amhlaidh mar gheall ar fhabht sa chód, nó mar gheall ar mhíréireacht leis an mbrabhsálaí.",
"bundle_column_error.error.title": "Ó, níl sé sin go maith!",
@ -467,6 +469,7 @@
"mute_modal.you_wont_see_mentions": "Ní fheicfidh tú postálacha a luann iad.",
"mute_modal.you_wont_see_posts": "Is féidir leo do phoist a fheiceáil go fóill, ach ní fheicfidh tú a gcuid postanna.",
"navigation_bar.about": "Maidir le",
"navigation_bar.administration": "Riarachán",
"navigation_bar.advanced_interface": "Oscail i gcomhéadan gréasáin chun cinn",
"navigation_bar.blocks": "Cuntais bhactha",
"navigation_bar.bookmarks": "Leabharmharcanna",
@ -483,6 +486,7 @@
"navigation_bar.follows_and_followers": "Ag leanúint agus do do leanúint",
"navigation_bar.lists": "Liostaí",
"navigation_bar.logout": "Logáil Amach",
"navigation_bar.moderation": "Measarthacht",
"navigation_bar.mutes": "Úsáideoirí balbhaithe",
"navigation_bar.opened_in_classic_interface": "Osclaítear poist, cuntais agus leathanaigh shonracha eile de réir réamhshocraithe sa chomhéadan gréasáin clasaiceach.",
"navigation_bar.personal": "Pearsanta",

View file

@ -48,7 +48,7 @@
"account.moved_to": "У {name} теперь новый аккаунт:",
"account.mute": "Игнорировать @{name}",
"account.mute_notifications_short": "Отключить уведомления",
"account.mute_short": "Немой",
"account.mute_short": "Глохни!",
"account.muted": "Игнорируется",
"account.mutual": "Взаимно",
"account.no_bio": "Описание не предоставлено.",

View file

@ -24,7 +24,7 @@ class EmojiFormatter
def to_s
return html if custom_emojis.empty? || html.blank?
tree = Nokogiri::HTML.fragment(html)
tree = Nokogiri::HTML5.fragment(html)
tree.xpath('./text()|.//text()[not(ancestor[@class="invisible"])]').to_a.each do |node|
i = -1
inside_shortname = false
@ -43,8 +43,8 @@ class EmojiFormatter
next unless (char_after.nil? || !DISALLOWED_BOUNDING_REGEX.match?(char_after)) && (emoji = emoji_map[shortcode])
result << Nokogiri::XML::Text.new(text[last_index..shortname_start_index - 1], tree.document) if shortname_start_index.positive?
result << Nokogiri::HTML.fragment(tag_for_emoji(shortcode, emoji))
result << tree.document.create_text_node(text[last_index..shortname_start_index - 1]) if shortname_start_index.positive?
result << tree.document.fragment(tag_for_emoji(shortcode, emoji))
last_index = i + 1
elsif text[i] == ':' && (i.zero? || !DISALLOWED_BOUNDING_REGEX.match?(text[i - 1]))
@ -53,7 +53,7 @@ class EmojiFormatter
end
end
result << Nokogiri::XML::Text.new(text[last_index..], tree.document)
result << tree.document.create_text_node(text[last_index..])
node.replace(result)
end

View file

@ -16,7 +16,7 @@ class PlainTextFormatter
if local?
text
else
node = Nokogiri::HTML.fragment(insert_newlines)
node = Nokogiri::HTML5.fragment(insert_newlines)
# Elements that are entirely removed with our Sanitize config
node.xpath('.//iframe|.//math|.//noembed|.//noframes|.//noscript|.//plaintext|.//script|.//style|.//svg|.//xmp').remove
node.text.chomp

View file

@ -79,6 +79,8 @@ class Account < ApplicationRecord
DISPLAY_NAME_LENGTH_LIMIT = 30
NOTE_LENGTH_LIMIT = 500
AUTOMATED_ACTOR_TYPES = %w(Application Service).freeze
include Attachmentable # Load prior to Avatar & Header concerns
include Account::Associations
@ -135,7 +137,8 @@ class Account < ApplicationRecord
scope :without_silenced, -> { where(silenced_at: nil) }
scope :without_instance_actor, -> { where.not(id: INSTANCE_ACTOR_ID) }
scope :recent, -> { reorder(id: :desc) }
scope :bots, -> { where(actor_type: %w(Application Service)) }
scope :bots, -> { where(actor_type: AUTOMATED_ACTOR_TYPES) }
scope :non_automated, -> { where.not(actor_type: AUTOMATED_ACTOR_TYPES) }
scope :groups, -> { where(actor_type: 'Group') }
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
scope :matches_uri_prefix, ->(value) { where(arel_table[:uri].matches("#{sanitize_sql_like(value)}/%", false, true)).or(where(uri: value)) }
@ -191,7 +194,7 @@ class Account < ApplicationRecord
end
def bot?
%w(Application Service).include? actor_type
AUTOMATED_ACTOR_TYPES.include?(actor_type)
end
def instance_actor?

View file

@ -73,10 +73,10 @@ class Account::Field < ActiveModelSerializers::Model
end
def extract_url_from_html
doc = Nokogiri::HTML(value).at_xpath('//body')
doc = Nokogiri::HTML5.fragment(value)
return if doc.nil?
return if doc.children.size > 1
return if doc.children.size != 1
element = doc.children.first

View file

@ -18,7 +18,7 @@ class AccountModerationNote < ApplicationRecord
belongs_to :account
belongs_to :target_account, class_name: 'Account'
scope :latest, -> { reorder('created_at DESC') }
scope :chronological, -> { reorder(id: :asc) }
validates :content, presence: true, length: { maximum: CONTENT_SIZE_LIMIT }
end

View file

@ -73,6 +73,14 @@ class Admin::AccountAction
end
end
def disabled_types_for_account(account)
if account.suspended?
%w(silence suspend)
elsif account.silenced?
%w(silence)
end
end
def i18n_scope
:activerecord
end

View file

@ -18,7 +18,7 @@ class ReportNote < ApplicationRecord
belongs_to :account
belongs_to :report, inverse_of: :notes, touch: true
scope :latest, -> { reorder(created_at: :desc) }
scope :chronological, -> { reorder(id: :asc) }
validates :content, presence: true, length: { maximum: CONTENT_SIZE_LIMIT }
end

View file

@ -25,7 +25,7 @@ class FetchOEmbedService
return if html.nil?
@format = @options[:format]
page = Nokogiri::HTML(html)
page = Nokogiri::HTML5(html)
if @format.nil? || @format == :json
@endpoint_url ||= page.at_xpath('//link[@type="application/json+oembed"]|//link[@type="text/json+oembed"]')&.attribute('href')&.value

View file

@ -73,7 +73,7 @@ class FetchResourceService < BaseService
end
def process_html(response)
page = Nokogiri::HTML(response.body_with_limit)
page = Nokogiri::HTML5(response.body_with_limit)
json_link = page.xpath('//link[@rel="alternate"]').find { |link| ACTIVITY_STREAM_LINK_TYPES.include?(link['type']) }
process(json_link['href'], terminal: true) unless json_link.nil?

View file

@ -100,7 +100,7 @@ class TranslateStatusService < BaseService
end
def unwrap_emoji_shortcodes(html)
fragment = Nokogiri::HTML.fragment(html)
fragment = Nokogiri::HTML5.fragment(html)
fragment.css('span[translate="no"]').each do |element|
element.remove_attribute('translate')
element.replace(element.children) if element.attributes.empty?

View file

@ -26,7 +26,7 @@ class VerifyLinkService < BaseService
def link_back_present?
return false if @body.blank?
links = Nokogiri::HTML5(@body).xpath('//a[contains(concat(" ", normalize-space(@rel), " "), " me ")]|//link[contains(concat(" ", normalize-space(@rel), " "), " me ")]')
links = Nokogiri::HTML5(@body).css("a[rel~='me'],link[rel~='me']")
if links.any? { |link| link['href']&.downcase == @link_back.downcase }
true

View file

@ -1,6 +1,13 @@
- content_for :page_title do
= t('admin.account_actions.title', acct: @account.pretty_acct)
- if @account.suspended?
.flash-message.alert
= t('admin.account_actions.already_suspended')
- elsif @account.silenced?
.flash-message.warn
= t('admin.account_actions.already_silenced')
= simple_form_for @account_action, url: admin_account_action_path(@account.id) do |f|
= f.input :report_id,
as: :hidden
@ -9,6 +16,7 @@
= f.input :type,
as: :radio_buttons,
collection: Admin::AccountAction.types_for_account(@account),
disabled: Admin::AccountAction.disabled_types_for_account(@account),
hint: t('simple_form.hints.admin_account_action.type_html', acct: @account.pretty_acct),
include_blank: false,
label_method: ->(type) { account_action_type_label(type) },

View file

@ -56,19 +56,19 @@
.dashboard__item
= link_to admin_reports_path, class: 'dashboard__quick-access' do
%span= t('admin.dashboard.pending_reports_html', count: @pending_reports_count)
%span= t('admin.dashboard.pending_reports_html', count: @pending_reports_count.value)
= material_symbol 'chevron_right'
= link_to admin_accounts_path(status: 'pending'), class: 'dashboard__quick-access' do
%span= t('admin.dashboard.pending_users_html', count: @pending_users_count)
%span= t('admin.dashboard.pending_users_html', count: @pending_users_count.value)
= material_symbol 'chevron_right'
= link_to admin_trends_tags_path(status: 'pending_review'), class: 'dashboard__quick-access' do
%span= t('admin.dashboard.pending_tags_html', count: @pending_tags_count)
%span= t('admin.dashboard.pending_tags_html', count: @pending_tags_count.value)
= material_symbol 'chevron_right'
= link_to admin_disputes_appeals_path(status: 'pending'), class: 'dashboard__quick-access' do
%span= t('admin.dashboard.pending_appeals_html', count: @pending_appeals_count)
%span= t('admin.dashboard.pending_appeals_html', count: @pending_appeals_count.value)
= material_symbol 'chevron_right'
.dashboard__item
= react_admin_component :dimension,

View file

@ -22,21 +22,27 @@
.report-actions__item__button
= button_tag t('admin.reports.delete_and_resolve'),
name: :delete,
class: 'button button--destructive'
class: 'button button--destructive',
disabled: statuses.empty?,
title: statuses.empty? ? t('admin.reports.actions_no_posts') : ''
.report-actions__item__description
= t('admin.reports.actions.delete_description_html')
.report-actions__item
.report-actions__item__button
= form.button t('admin.accounts.silence'),
name: :silence,
class: 'button button--destructive'
class: 'button button--destructive',
disabled: report.target_account.silenced? || report.target_account.suspended?,
title: report.target_account.silenced? ? t('admin.account_actions.already_silenced') : ''
.report-actions__item__description
= t('admin.reports.actions.silence_description_html')
.report-actions__item
.report-actions__item__button
= form.button t('admin.accounts.suspend'),
name: :suspend,
class: 'button button--destructive'
class: 'button button--destructive',
disabled: report.target_account.suspended?,
title: report.target_account.suspended? ? t('admin.account_actions.already_suspended') : ''
.report-actions__item__description
= t('admin.reports.actions.suspend_description_html')
.report-actions__item

View file

@ -7,7 +7,8 @@
collection: UserRole.assignable,
include_blank: I18n.t('admin.accounts.change_role.no_role'),
label_method: :name,
wrapper: :with_block_label
wrapper: :with_block_label,
hint: safe_join([I18n.t('simple_form.hints.user.role'), ' ', link_to(I18n.t('admin.accounts.change_role.edit_roles'), admin_roles_path)])
.actions
= f.button :button,

View file

@ -3,6 +3,8 @@
= javascript_pack_tag 'public', crossorigin: 'anonymous'
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
- content_for :body_classes, 'admin'
- content_for :content do
.admin-wrapper
.sidebar-wrapper

View file

@ -1,6 +1,8 @@
- content_for :header_tags do
= javascript_pack_tag 'public', crossorigin: 'anonymous'
- content_for :body_classes, 'modal-layout compose-standalone'
- content_for :content do
- if user_signed_in? && !@hide_header
.account-header

View file

@ -2,6 +2,8 @@
= render_initial_state
= javascript_pack_tag 'public', crossorigin: 'anonymous'
- content_for :body_classes, 'player'
:ruby
meta = @media_attachment.file.meta || {}