diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index cb349f4d51..7283442d5b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -102,6 +102,16 @@ module ApplicationHelper policy(record).public_send(:"#{action}?") end + def conditional_link_to(condition, name, options = {}, html_options = {}, &block) + if condition && !current_page?(block_given? ? name : options) + link_to(name, options, html_options, &block) + elsif block_given? + content_tag(:span, options, html_options, &block) + else + content_tag(:span, name, html_options) + end + end + def material_symbol(icon, attributes = {}) safe_join( [ diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 6cde1f8882..fb1366c4e2 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -96,12 +96,17 @@ export const ensureComposeIsVisible = (getState) => { }; export function setComposeToStatus(status, text, spoiler_text) { - return{ - type: COMPOSE_SET_STATUS, - status, - text, - spoiler_text, - }; + return (dispatch, getState) => { + const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']); + + dispatch({ + type: COMPOSE_SET_STATUS, + status, + text, + spoiler_text, + maxOptions, + }); + } } export function changeCompose(text) { @@ -183,7 +188,7 @@ export function directCompose(account) { }; } -export function submitCompose() { +export function submitCompose(successCallback) { return function (dispatch, getState) { const status = getState().getIn(['compose', 'text'], ''); const media = getState().getIn(['compose', 'media_attachments']); @@ -239,6 +244,9 @@ export function submitCompose() { dispatch(insertIntoTagHistory(response.data.tags, status)); dispatch(submitComposeSuccess({ ...response.data })); + if (typeof successCallback === 'function') { + successCallback(response.data); + } // To make the app more responsive, immediately push the status // into the columns diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 42d0c1c0f1..3fddd1bcc5 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -3,7 +3,7 @@ import { browserHistory } from 'mastodon/components/router'; import api from '../api'; import { ensureComposeIsVisible, setComposeToStatus } from './compose'; -import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer'; +import { importFetchedStatus, importFetchedAccount } from './importer'; import { fetchContext } from './statuses_typed'; import { deleteFromTimelines } from './timelines'; @@ -48,7 +48,18 @@ export function fetchStatusRequest(id, skipLoading) { }; } -export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) { +/** + * @param {string} id + * @param {Object} [options] + * @param {boolean} [options.forceFetch] + * @param {boolean} [options.alsoFetchContext] + * @param {string | null | undefined} [options.parentQuotePostId] + */ +export function fetchStatus(id, { + forceFetch = false, + alsoFetchContext = true, + parentQuotePostId, +} = {}) { return (dispatch, getState) => { const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null; @@ -66,7 +77,7 @@ export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) { dispatch(importFetchedStatus(response.data)); dispatch(fetchStatusSuccess(skipLoading)); }).catch(error => { - dispatch(fetchStatusFail(id, error, skipLoading)); + dispatch(fetchStatusFail(id, error, skipLoading, parentQuotePostId)); }); }; } @@ -78,21 +89,27 @@ export function fetchStatusSuccess(skipLoading) { }; } -export function fetchStatusFail(id, error, skipLoading) { +export function fetchStatusFail(id, error, skipLoading, parentQuotePostId) { return { type: STATUS_FETCH_FAIL, id, error, + parentQuotePostId, skipLoading, skipAlert: true, }; } export function redraft(status, raw_text) { - return { - type: REDRAFT, - status, - raw_text, + return (dispatch, getState) => { + const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']); + + dispatch({ + type: REDRAFT, + status, + raw_text, + maxOptions, + }); }; } diff --git a/app/javascript/mastodon/api/interactions.ts b/app/javascript/mastodon/api/interactions.ts index 118b5f06d2..62808dcddc 100644 --- a/app/javascript/mastodon/api/interactions.ts +++ b/app/javascript/mastodon/api/interactions.ts @@ -1,10 +1,11 @@ import { apiRequestPost } from 'mastodon/api'; -import type { Status, StatusVisibility } from 'mastodon/models/status'; +import type { ApiStatusJSON } from 'mastodon/api_types/statuses'; +import type { StatusVisibility } from 'mastodon/models/status'; export const apiReblog = (statusId: string, visibility: StatusVisibility) => - apiRequestPost<{ reblog: Status }>(`v1/statuses/${statusId}/reblog`, { + apiRequestPost<{ reblog: ApiStatusJSON }>(`v1/statuses/${statusId}/reblog`, { visibility, }); export const apiUnreblog = (statusId: string) => - apiRequestPost(`v1/statuses/${statusId}/unreblog`); + apiRequestPost(`v1/statuses/${statusId}/unreblog`); diff --git a/app/javascript/mastodon/components/status_quoted.tsx b/app/javascript/mastodon/components/status_quoted.tsx index d3d8b58c33..3f7f51cf06 100644 --- a/app/javascript/mastodon/components/status_quoted.tsx +++ b/app/javascript/mastodon/components/status_quoted.tsx @@ -37,9 +37,7 @@ const QuoteWrapper: React.FC<{ ); }; -const NestedQuoteLink: React.FC<{ - status: Status; -}> = ({ status }) => { +const NestedQuoteLink: React.FC<{ status: Status }> = ({ status }) => { const accountId = status.get('account') as string; const account = useAppSelector((state) => accountId ? state.accounts.get(accountId) : undefined, @@ -78,21 +76,40 @@ type GetStatusSelector = ( export const QuotedStatus: React.FC<{ quote: QuoteMap; contextType?: string; + parentQuotePostId?: string | null; variant?: 'full' | 'link'; nestingLevel?: number; -}> = ({ quote, contextType, nestingLevel = 1, variant = 'full' }) => { +}> = ({ + quote, + contextType, + parentQuotePostId, + nestingLevel = 1, + variant = 'full', +}) => { const dispatch = useAppDispatch(); + const quoteState = useAppSelector((state) => + parentQuotePostId + ? state.statuses.getIn([parentQuotePostId, 'quote', 'state']) + : quote.get('state'), + ); + const quotedStatusId = quote.get('quoted_status'); - const quoteState = quote.get('state'); const status = useAppSelector((state) => quotedStatusId ? state.statuses.get(quotedStatusId) : undefined, ); + const shouldLoadQuote = !status?.get('isLoading') && quoteState !== 'deleted'; + useEffect(() => { - if (!status && quotedStatusId) { - dispatch(fetchStatus(quotedStatusId)); + if (shouldLoadQuote && quotedStatusId) { + dispatch( + fetchStatus(quotedStatusId, { + parentQuotePostId, + alsoFetchContext: false, + }), + ); } - }, [status, quotedStatusId, dispatch]); + }, [shouldLoadQuote, quotedStatusId, parentQuotePostId, dispatch]); // In order to find out whether the quoted post should be completely hidden // due to a matching filter, we run it through the selector used by `status_container`. @@ -173,6 +190,7 @@ export const QuotedStatus: React.FC<{ {canRenderChildQuote && ( { if (quote) { return ( - + ); } diff --git a/app/javascript/mastodon/features/compose/components/compose_form.jsx b/app/javascript/mastodon/features/compose/components/compose_form.jsx index 6dd3dbd054..f3c9d92011 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.jsx +++ b/app/javascript/mastodon/features/compose/components/compose_form.jsx @@ -73,6 +73,7 @@ class ComposeForm extends ImmutablePureComponent { singleColumn: PropTypes.bool, lang: PropTypes.string, maxChars: PropTypes.number, + redirectOnSuccess: PropTypes.bool, }; static defaultProps = { @@ -310,7 +311,7 @@ class ComposeForm extends ImmutablePureComponent { > {intl.formatMessage( this.props.isEditing ? - messages.saveChanges : + messages.saveChanges : (this.props.isInReply ? messages.reply : messages.publish) )} diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js index 15ccabf748..5f86426c4d 100644 --- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js +++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js @@ -34,7 +34,7 @@ const mapStateToProps = state => ({ maxChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters'], 500), }); -const mapDispatchToProps = (dispatch) => ({ +const mapDispatchToProps = (dispatch, props) => ({ onChange (text) { dispatch(changeCompose(text)); @@ -47,7 +47,11 @@ const mapDispatchToProps = (dispatch) => ({ modalProps: {}, })); } else { - dispatch(submitCompose()); + dispatch(submitCompose((status) => { + if (props.redirectOnSuccess) { + window.location.assign(status.url); + } + })); } }, diff --git a/app/javascript/mastodon/features/standalone/compose/index.jsx b/app/javascript/mastodon/features/standalone/compose/index.jsx index 3aff78ffee..5d336275d4 100644 --- a/app/javascript/mastodon/features/standalone/compose/index.jsx +++ b/app/javascript/mastodon/features/standalone/compose/index.jsx @@ -5,7 +5,7 @@ import ModalContainer from 'mastodon/features/ui/containers/modal_container'; const Compose = () => ( <> - + diff --git a/app/javascript/mastodon/features/standalone/status/index.tsx b/app/javascript/mastodon/features/standalone/status/index.tsx index 8d1b831467..a7850eae1c 100644 --- a/app/javascript/mastodon/features/standalone/status/index.tsx +++ b/app/javascript/mastodon/features/standalone/status/index.tsx @@ -32,7 +32,7 @@ const Embed: React.FC<{ id: string }> = ({ id }) => { const dispatchRenderSignal = useRenderSignal(); useEffect(() => { - dispatch(fetchStatus(id, false, false)); + dispatch(fetchStatus(id, { alsoFetchContext: false })); }, [dispatch, id]); const handleToggleHidden = useCallback(() => { diff --git a/app/javascript/mastodon/features/status/components/detailed_status.tsx b/app/javascript/mastodon/features/status/components/detailed_status.tsx index ec6aa003e2..3922ab5617 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.tsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.tsx @@ -381,7 +381,10 @@ export const DetailedStatus: React.FC<{ {hashtagBar} {status.get('quote') && ( - + )} )} diff --git a/app/javascript/mastodon/features/ui/components/filter_modal.jsx b/app/javascript/mastodon/features/ui/components/filter_modal.jsx index 477575bd7b..a1a39ba0ab 100644 --- a/app/javascript/mastodon/features/ui/components/filter_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/filter_modal.jsx @@ -38,7 +38,7 @@ class FilterModal extends ImmutablePureComponent { handleSuccess = () => { const { dispatch, statusId } = this.props; - dispatch(fetchStatus(statusId, true)); + dispatch(fetchStatus(statusId, {forceFetch: true})); this.setState({ isSubmitting: false, isSubmitted: true, step: 'submitted' }); }; diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 6b799a46e8..084e134d23 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -501,8 +501,13 @@ export const composeReducer = (state = initialState, action) => { } if (action.status.get('poll')) { + let options = ImmutableList(action.status.get('poll').options.map(x => x.title)); + if (options.size < action.maxOptions) { + options = options.push(''); + } + map.set('poll', ImmutableMap({ - options: ImmutableList(action.status.get('poll').options.map(x => x.title)), + options: options, multiple: action.status.get('poll').multiple, expires_in: expiresInFromExpiresAt(action.status.get('poll').expires_at), })); @@ -530,8 +535,13 @@ export const composeReducer = (state = initialState, action) => { } if (action.status.get('poll')) { + let options = ImmutableList(action.status.get('poll').options.map(x => x.title)); + if (options.size < action.maxOptions) { + options = options.push(''); + } + map.set('poll', ImmutableMap({ - options: ImmutableList(action.status.get('poll').options.map(x => x.title)), + options: options, multiple: action.status.get('poll').multiple, expires_in: expiresInFromExpiresAt(action.status.get('poll').expires_at), })); diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index 239ab13920..0f766e2507 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -73,8 +73,15 @@ export default function statuses(state = initialState, action) { switch(action.type) { case STATUS_FETCH_REQUEST: return state.setIn([action.id, 'isLoading'], true); - case STATUS_FETCH_FAIL: - return state.delete(action.id); + case STATUS_FETCH_FAIL: { + if (action.parentQuotePostId && action.error.status === 404) { + return state + .delete(action.id) + .setIn([action.parentQuotePostId, 'quote', 'state'], 'deleted') + } else { + return state.delete(action.id); + } + } case STATUS_IMPORT: return importStatus(state, action.status); case STATUSES_IMPORT: diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 54200978a8..4f9d3b6a51 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -1888,7 +1888,7 @@ a.sparkline { font-size: 15px; line-height: 22px; - li { + > li { counter-increment: step 1; padding-inline-start: 2.5rem; padding-bottom: 8px; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 40b073f68b..70b0da95be 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2333,6 +2333,7 @@ a .account__avatar { .detailed-status__display-name, .detailed-status__datetime, .detailed-status__application, +.detailed-status__link, .account__display-name { text-decoration: none; } @@ -2365,7 +2366,8 @@ a.account__display-name { } .detailed-status__application, -.detailed-status__datetime { +.detailed-status__datetime, +.detailed-status__link { color: inherit; } @@ -2551,8 +2553,9 @@ a.account__display-name { } .status__relative-time, -.detailed-status__datetime { - &:hover { +.detailed-status__datetime, +.detailed-status__link { + &:is(a):hover { text-decoration: underline; } } diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss index f49ce3c413..d938746aa0 100644 --- a/app/javascript/styles/mastodon/polls.scss +++ b/app/javascript/styles/mastodon/polls.scss @@ -101,7 +101,8 @@ cursor: pointer; } - &.editable { + &.editable, + &.disabled { align-items: center; overflow: visible; } @@ -159,7 +160,8 @@ } } - &__option.editable &__input { + &__option.editable &__input, + &__option.disabled &__input { &:active, &:focus, &:hover { diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index 69b7bd0354..ce36cfe763 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -61,6 +61,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity ActivityPub::Forwarder.new(@account, @json, @quote.status).forward! @quote.reject! + DistributionWorker.perform_async(@quote.status_id, { 'update' => true }) end def forwarder diff --git a/app/lib/antispam.rb b/app/lib/antispam.rb index 4ebf192485..9dfcda7df5 100644 --- a/app/lib/antispam.rb +++ b/app/lib/antispam.rb @@ -57,8 +57,16 @@ class Antispam end def report_if_needed!(account) - return if Report.unresolved.exists?(account: Account.representative, target_account: account) + return if system_reports.unresolved.exists?(target_account: account) - Report.create!(account: Account.representative, target_account: account, category: :spam, comment: 'Account automatically reported for posting a banned URL') + system_reports.create!( + category: :spam, + comment: 'Account automatically reported for posting a banned URL', + target_account: account + ) + end + + def system_reports + Account.representative.reports end end diff --git a/app/models/account.rb b/app/models/account.rb index e05b45a368..c9e13a4aa0 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -116,7 +116,7 @@ class Account < ApplicationRecord # Local user validations validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: USERNAME_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_username? && !actor_type_application? } - validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? && !actor_type_application? } + validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? && !actor_type_application? && !user&.bypass_registration_checks } validates :display_name, length: { maximum: DISPLAY_NAME_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_display_name? } validates :note, note_length: { maximum: NOTE_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_note? } validates :fields, length: { maximum: DEFAULT_FIELDS_SIZE }, if: -> { local? && will_save_change_to_fields? } diff --git a/app/models/status_edit.rb b/app/models/status_edit.rb index bc2ddaabe8..4a9311505a 100644 --- a/app/models/status_edit.rb +++ b/app/models/status_edit.rb @@ -44,12 +44,36 @@ class StatusEdit < ApplicationRecord scope :ordered, -> { order(id: :asc) } delegate :local?, :application, :edited?, :edited_at, - :discarded?, :visibility, :language, to: :status + :discarded?, :reply?, :visibility, :language, to: :status + + def with_media? + ordered_media_attachments.any? + end + + def with_poll? + poll_options.present? + end + + def poll + return @poll if defined?(@poll) + return @poll = nil if poll_options.blank? + + @poll = Poll.new({ + options: poll_options, + account_id: account_id, + status_id: status_id, + }) + end + + alias preloadable_poll poll def emojis return @emojis if defined?(@emojis) - @emojis = CustomEmoji.from_text([spoiler_text, text].join(' '), status.account.domain) + fields = [spoiler_text, text] + fields += preloadable_poll.options unless preloadable_poll.nil? + + @emojis = CustomEmoji.from_text(fields.join(' '), status.account.domain) end def ordered_media_attachments diff --git a/app/models/user.rb b/app/models/user.rb index 6ab37f137d..01965a67f4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -142,7 +142,9 @@ class User < ApplicationRecord delegate :can?, to: :role attr_reader :invite_code, :date_of_birth - attr_writer :external, :bypass_registration_checks, :current_account + attr_writer :external, :current_account + + attribute :bypass_registration_checks, :boolean, default: false def self.those_who_can(*any_of_privileges) matching_role_ids = UserRole.that_can(*any_of_privileges).map(&:id) @@ -505,10 +507,6 @@ class User < ApplicationRecord !!@external end - def bypass_registration_checks? - @bypass_registration_checks - end - def sanitize_role self.role = nil if role.present? && role.everyone? end diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index ed2aab2cce..1821458fbe 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -74,7 +74,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService end def update_interaction_policies! - @status.quote_approval_policy = @status_parser.quote_policy + @status.update(quote_approval_policy: @status_parser.quote_policy) end def update_media_attachments! @@ -112,6 +112,8 @@ class ActivityPub::ProcessStatusUpdateService < BaseService @status.ordered_media_attachment_ids = @next_media_attachments.map(&:id) @media_attachments_changed = true if @status.ordered_media_attachment_ids != previous_media_attachments_ids + + @status.media_attachments.reload if @media_attachments_changed end def download_media_files! diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb index 36baa6e5ac..ef22a4d798 100644 --- a/app/services/backup_service.rb +++ b/app/services/backup_service.rb @@ -55,6 +55,7 @@ class BackupService < BaseService def build_archive! tmp_file = Tempfile.new(%w(archive .zip)) + Zip.write_zip64_support = true Zip::File.open(tmp_file, create: true) do |zipfile| dump_outbox!(zipfile) dump_media_attachments!(zipfile) diff --git a/app/views/admin/reports/_media_attachments.html.haml b/app/views/admin/reports/_media_attachments.html.haml deleted file mode 100644 index aa82ec09a8..0000000000 --- a/app/views/admin/reports/_media_attachments.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -- if status.ordered_media_attachments.first.video? - = render_video_component(status, visible: false) -- elsif status.ordered_media_attachments.first.audio? - = render_audio_component(status) -- else - = render_media_gallery_component(status, visible: false) diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml deleted file mode 100644 index c1e47d5b32..0000000000 --- a/app/views/admin/reports/_status.html.haml +++ /dev/null @@ -1,53 +0,0 @@ -.batch-table__row - %label.batch-table__row__select.batch-checkbox - = f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id - .batch-table__row__content - .status__card - - if status.reblog? - .status__prepend - = material_symbol('repeat') - = t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(status.proper.account, path: admin_account_status_path(status.proper.account.id, status.proper.id))) - - elsif status.reply? && status.in_reply_to_id.present? - .status__prepend - = material_symbol('reply') - = t('admin.statuses.replied_to_html', acct_link: admin_account_inline_link_to(status.in_reply_to_account, path: status.thread.present? ? admin_account_status_path(status.thread.account_id, status.in_reply_to_id) : nil)) - .status__content>< - - if status.proper.spoiler_text.blank? - = prerender_custom_emojis(status_content_format(status.proper), status.proper.emojis) - - else - %details< - %summary>< - %strong> Content warning: #{prerender_custom_emojis(h(status.proper.spoiler_text), status.proper.emojis)} - = prerender_custom_emojis(status_content_format(status.proper), status.proper.emojis) - - - unless status.proper.ordered_media_attachments.empty? - = render partial: 'admin/reports/media_attachments', locals: { status: status.proper } - - .detailed-status__meta - - if status.application - = status.application.name - · - - = link_to admin_account_status_path(status.account.id, status), class: 'detailed-status__datetime' do - %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) - - if status.edited? - · - = link_to t('statuses.edited_at_html', date: content_tag(:time, l(status.edited_at), datetime: status.edited_at.iso8601, title: l(status.edited_at), class: 'formatted')), - admin_account_status_path(status.account_id, status), - class: 'detailed-status__datetime' - - if status.discarded? - · - %span.negative-hint= t('admin.statuses.deleted') - · - - = material_symbol visibility_icon(status) - = t("statuses.visibilities.#{status.visibility}") - · - - = link_to ActivityPub::TagManager.instance.url_for(status.proper), class: 'detailed-status__link', rel: 'noopener' do - = t('admin.statuses.view_publicly') - - - if status.proper.sensitive? - · - = material_symbol('visibility_off') - = t('stream_entries.sensitive_content') diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index 69e9c02921..516d2f5d2c 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -57,7 +57,7 @@ - if @statuses.empty? = nothing_here 'nothing-here--under-tabs' - else - = render partial: 'admin/reports/status', collection: @statuses, locals: { f: f } + = render partial: 'admin/shared/status_batch_row', collection: @statuses, as: :status, locals: { f: f } - if @report.unresolved? %hr.spacer/ diff --git a/app/views/admin/shared/_status.html.haml b/app/views/admin/shared/_status.html.haml new file mode 100644 index 0000000000..c042fd7a2c --- /dev/null +++ b/app/views/admin/shared/_status.html.haml @@ -0,0 +1,40 @@ +-# locals: (status:) + +.status__card>< + - if status.reblog? + .status__prepend + = material_symbol('repeat') + = t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(status.proper.account, path: admin_account_status_path(status.proper.account.id, status.proper.id))) + - elsif status.reply? && status.in_reply_to_id.present? + .status__prepend + = material_symbol('reply') + = t('admin.statuses.replied_to_html', acct_link: admin_account_inline_link_to(status.in_reply_to_account, path: status.thread.present? ? admin_account_status_path(status.thread.account_id, status.in_reply_to_id) : nil)) + + = render partial: 'admin/shared/status_content', locals: { status: status.proper } + + .detailed-status__meta + - if status.application + = status.application.name + · + = conditional_link_to can?(:show, status), admin_account_status_path(status.account.id, status), class: 'detailed-status__datetime' do + %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }><= l(status.created_at) + - if status.edited? +  · + = conditional_link_to can?(:show, status), admin_account_status_path(status.account.id, status, { anchor: 'history' }), class: 'detailed-status__datetime' do + %span><= t('statuses.edited_at_html', date: content_tag(:time, l(status.edited_at), datetime: status.edited_at.iso8601, title: l(status.edited_at), class: 'relative-formatted')) + - if status.discarded? +  · + %span.negative-hint= t('admin.statuses.deleted') + - unless status.reblog? +  · + %span< + = material_symbol(visibility_icon(status)) + = t("statuses.visibilities.#{status.visibility}") + - if status.proper.sensitive? +  · + = material_symbol('visibility_off') + = t('stream_entries.sensitive_content') + - unless status.direct_visibility? +  · + = link_to ActivityPub::TagManager.instance.url_for(status.proper), class: 'detailed-status__link', target: 'blank', rel: 'noopener' do + = t('admin.statuses.view_publicly') diff --git a/app/views/admin/shared/_status_attachments.html.haml b/app/views/admin/shared/_status_attachments.html.haml new file mode 100644 index 0000000000..24af2b5f7d --- /dev/null +++ b/app/views/admin/shared/_status_attachments.html.haml @@ -0,0 +1,22 @@ +- if status.with_poll? + .poll + %ul + - status.preloadable_poll.options.each_with_index do |option, _index| + %li + %label.poll__option.disabled<> + - if status.preloadable_poll.multiple? + %span.poll__input.checkbox{ role: 'checkbox', 'aria-label': option } + - else + %span.poll__input{ role: 'radio', 'aria-label': option } + %span.poll__option__text + = prerender_custom_emojis(html_aware_format(option, status.local?, multiline: false), status.emojis) + %button.button.button-secondary{ disabled: true } + = t('polls.vote') + +- if status.with_media? + - if status.ordered_media_attachments.first.video? + = render_video_component(status, visible: false) + - elsif status.ordered_media_attachments.first.audio? + = render_audio_component(status) + - else + = render_media_gallery_component(status, visible: false) diff --git a/app/views/admin/shared/_status_batch_row.html.haml b/app/views/admin/shared/_status_batch_row.html.haml new file mode 100644 index 0000000000..53d8a56e60 --- /dev/null +++ b/app/views/admin/shared/_status_batch_row.html.haml @@ -0,0 +1,5 @@ +.batch-table__row + %label.batch-table__row__select.batch-checkbox + = f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id + .batch-table__row__content + = render partial: 'admin/shared/status', object: status diff --git a/app/views/admin/shared/_status_content.html.haml b/app/views/admin/shared/_status_content.html.haml new file mode 100644 index 0000000000..aedd84bdd6 --- /dev/null +++ b/app/views/admin/shared/_status_content.html.haml @@ -0,0 +1,10 @@ +.status__content>< + - if status.spoiler_text.present? + %details< + %summary>< + %strong> Content warning: #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)} + = prerender_custom_emojis(status_content_format(status), status.emojis) + = render partial: 'admin/shared/status_attachments', locals: { status: status.proper } + - else + = prerender_custom_emojis(status_content_format(status), status.emojis) + = render partial: 'admin/shared/status_attachments', locals: { status: status.proper } diff --git a/app/views/admin/status_edits/_status_edit.html.haml b/app/views/admin/status_edits/_status_edit.html.haml index 0bec0159ee..d4cc4f5ea2 100644 --- a/app/views/admin/status_edits/_status_edit.html.haml +++ b/app/views/admin/status_edits/_status_edit.html.haml @@ -9,17 +9,7 @@ %time.formatted{ datetime: status_edit.created_at.iso8601, title: l(status_edit.created_at) }= l(status_edit.created_at) .status - .status__content>< - - if status_edit.spoiler_text.blank? - = prerender_custom_emojis(status_content_format(status_edit), status_edit.emojis) - - else - %details< - %summary>< - %strong> Content warning: #{prerender_custom_emojis(h(status_edit.spoiler_text), status_edit.emojis)} - = prerender_custom_emojis(status_content_format(status_edit), status_edit.emojis) - - - unless status_edit.ordered_media_attachments.empty? - = render partial: 'admin/reports/media_attachments', locals: { status: status_edit } + = render partial: 'admin/shared/status_content', locals: { status: status_edit } .detailed-status__meta %time.formatted{ datetime: status_edit.created_at.iso8601, title: l(status_edit.created_at) }= l(status_edit.created_at) diff --git a/app/views/admin/statuses/index.html.haml b/app/views/admin/statuses/index.html.haml index 57b9fe0e15..e914585db6 100644 --- a/app/views/admin/statuses/index.html.haml +++ b/app/views/admin/statuses/index.html.haml @@ -47,6 +47,6 @@ - if @statuses.empty? = nothing_here 'nothing-here--under-tabs' - else - = render partial: 'admin/reports/status', collection: @statuses, locals: { f: f } + = render partial: 'admin/shared/status_batch_row', collection: @statuses, as: :status, locals: { f: f } = paginate @statuses diff --git a/app/views/admin/statuses/show.html.haml b/app/views/admin/statuses/show.html.haml index 7328eeb0a7..ba5ba81987 100644 --- a/app/views/admin/statuses/show.html.haml +++ b/app/views/admin/statuses/show.html.haml @@ -53,52 +53,11 @@ %h3= t('admin.statuses.contents') -.status__card - - if @status.reblog? - .status__prepend - = material_symbol('repeat') - = t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(@status.proper.account, path: admin_account_status_path(@status.proper.account.id, @status.proper.id))) - - elsif @status.reply? && @status.in_reply_to_id.present? - .status__prepend - = material_symbol('reply') - = t('admin.statuses.replied_to_html', acct_link: admin_account_inline_link_to(@status.in_reply_to_account, path: @status.thread.present? ? admin_account_status_path(@status.thread.account_id, @status.in_reply_to_id) : nil)) - .status__content>< - - if @status.proper.spoiler_text.blank? - = prerender_custom_emojis(status_content_format(@status.proper), @status.proper.emojis) - - else - %details< - %summary>< - %strong> Content warning: #{prerender_custom_emojis(h(@status.proper.spoiler_text), @status.proper.emojis)} - = prerender_custom_emojis(status_content_format(@status.proper), @status.proper.emojis) - - - unless @status.proper.ordered_media_attachments.empty? - = render partial: 'admin/reports/media_attachments', locals: { status: @status.proper } - - .detailed-status__meta - - if @status.application - = @status.application.name - · - %span.detailed-status__datetime - %time.formatted{ datetime: @status.created_at.iso8601, title: l(@status.created_at) }= l(@status.created_at) - - if @status.edited? - · - %span.detailed-status__datetime - = t('statuses.edited_at_html', date: content_tag(:time, l(@status.edited_at), datetime: @status.edited_at.iso8601, title: l(@status.edited_at), class: 'formatted')) - - if @status.discarded? - · - %span.negative-hint= t('admin.statuses.deleted') - - unless @status.reblog? - · - = material_symbol(visibility_icon(@status)) - = t("statuses.visibilities.#{@status.visibility}") - - if @status.proper.sensitive? - · - = material_symbol('visibility_off') - = t('stream_entries.sensitive_content') += render partial: 'admin/shared/status', object: @status %hr.spacer/ -%h3= t('admin.statuses.history') +%h3#history= t('admin.statuses.history') - if @status.edits.empty? %p= t('admin.statuses.no_history') - else diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index c3fae2d3e4..a84bdbd62c 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -30,7 +30,7 @@ = vite_react_refresh_tag = vite_polyfills_tag -# Needed for the wicg-inert polyfill. It needs to be on it's own