diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 0cf15040cd..7b1b25a19b 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -28,6 +28,9 @@ // react-router: Requires manual upgrade 'history', 'react-router-dom', + + // react-spring: Requires manual upgrade when upgrading react + '@react-spring/web', ], matchUpdateTypes: ['major'], dependencyDashboardApproval: true, diff --git a/.nvmrc b/.nvmrc index 5d621bb2fe..f2a2bc6165 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.15 +22.16 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 07ee0167b3..e9a182a8f4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -70,13 +70,6 @@ Style/OptionalBooleanParameter: - 'app/workers/domain_block_worker.rb' - 'app/workers/unfollow_follow_worker.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: short, verbose -Style/PreferredHashMethods: - Exclude: - - 'config/initializers/paperclip.rb' - # This cop supports safe autocorrection (--autocorrect). Style/RedundantConstantBase: Exclude: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ae125f64f0..89fd3c7995 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,6 +62,11 @@ Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues]. Please use the search function to make sure there are not duplicate bug reports or feature requests. +## Security Issues + +If you believe you have identified a security issue in Mastodon or our own apps, +check [SECURITY]. + ## Translations Translations are community contributed via [Crowdin]. They are periodically @@ -124,3 +129,4 @@ and API docs. Improvements are made via PRs to the [documentation repository]. [GitHub Issues]: https://github.com/mastodon/mastodon/issues [keepachangelog]: https://keepachangelog.com/en/1.0.0/ [Mastodon documentation]: https://docs.joinmastodon.org +[SECURITY]: SECURITY.md diff --git a/Gemfile b/Gemfile index 126d1bebbd..6a5f0c5343 100644 --- a/Gemfile +++ b/Gemfile @@ -62,7 +62,7 @@ gem 'inline_svg' gem 'irb', '~> 1.8' gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' -gem 'linzer', '~> 0.6.1' +gem 'linzer', '~> 0.7.2' gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'mime-types', '~> 3.7.0', require: 'mime/types/columnar' gem 'mutex_m' diff --git a/Gemfile.lock b/Gemfile.lock index d47d6ab91b..7ae8d412b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -148,6 +148,7 @@ GEM case_transform (0.2) activesupport cbor (0.5.9.8) + cgi (0.4.2) charlock_holmes (0.7.9) chewy (7.6.0) activesupport (>= 5.2) @@ -262,6 +263,7 @@ GEM fog-core (~> 2.1) fog-json (>= 1.0) formatador (1.1.0) + forwardable (1.3.3) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) @@ -396,7 +398,11 @@ GEM rexml link_header (0.0.8) lint_roller (1.1.0) - linzer (0.6.5) + linzer (0.7.2) + cgi (~> 0.4.2) + forwardable (~> 1.3, >= 1.3.3) + logger (~> 1.7, >= 1.7.0) + net-http (~> 0.6.0) openssl (~> 3.0, >= 3.0.0) rack (>= 2.2, < 4.0) starry (~> 0.2) @@ -628,7 +634,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.15) + rack (2.2.16) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-cors (2.0.2) @@ -742,7 +748,7 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 9) rspec-support (3.13.3) - rubocop (1.75.6) + rubocop (1.75.7) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -779,7 +785,8 @@ GEM lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) rubocop-rspec (~> 3.5) - ruby-prof (1.7.1) + ruby-prof (1.7.2) + base64 ruby-progressbar (1.13.0) ruby-saml (1.18.0) nokogiri (>= 1.13.10) @@ -993,7 +1000,7 @@ DEPENDENCIES letter_opener (~> 1.8) letter_opener_web (~> 3.0) link_header (~> 0.0) - linzer (~> 0.6.1) + linzer (~> 0.7.2) lograge (~> 0.12) mail (~> 2.8) mario-redis-lock (~> 1.2) diff --git a/README.md b/README.md index 008393b118..ad264822e2 100644 --- a/README.md +++ b/README.md @@ -93,12 +93,12 @@ accepted into Mastodon, you can request to be paid through our [OpenCollective]. ## License -Copyright (c) 2016-2024 Eugen Rochko (+ [`mastodon authors`](AUTHORS.md)) +Copyright (c) 2016-2025 Eugen Rochko (+ [`mastodon authors`](AUTHORS.md)) Licensed under GNU Affero General Public License as stated in the [LICENSE](LICENSE): ``` -Copyright (c) 2016-2024 Eugen Rochko & other Mastodon contributors +Copyright (c) 2016-2025 Eugen Rochko & other Mastodon contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free diff --git a/app/controllers/api/fasp/base_controller.rb b/app/controllers/api/fasp/base_controller.rb index 690f7e419a..f786ea1767 100644 --- a/app/controllers/api/fasp/base_controller.rb +++ b/app/controllers/api/fasp/base_controller.rb @@ -42,37 +42,22 @@ class Api::Fasp::BaseController < ApplicationController end def validate_signature! - signature_input = request.headers['signature-input']&.encode('UTF-8') - raise Error, 'signature-input is missing' if signature_input.blank? + raise Error, 'signature-input is missing' if request.headers['signature-input'].blank? + + provider = nil + + Linzer.verify!(request.rack_request, no_older_than: 5.minutes) do |keyid| + provider = Fasp::Provider.find(keyid) + Linzer.new_ed25519_public_key(provider.provider_public_key_pem, keyid) + end - keyid = signature_input.match(KEYID_PATTERN)[1] - provider = Fasp::Provider.find(keyid) - linzer_request = Linzer.new_request( - request.method, - request.original_url, - {}, - { - 'content-digest' => request.headers['content-digest'], - 'signature-input' => signature_input, - 'signature' => request.headers['signature'], - } - ) - message = Linzer::Message.new(linzer_request) - key = Linzer.new_ed25519_public_key(provider.provider_public_key_pem, keyid) - signature = Linzer::Signature.build(message.headers) - Linzer.verify(key, message, signature) @current_provider = provider end def sign_response response.headers['content-digest'] = "sha-256=:#{OpenSSL::Digest.base64digest('sha256', response.body || '')}:" - - linzer_response = Linzer.new_response(response.body, response.status, { 'content-digest' => response.headers['content-digest'] }) - message = Linzer::Message.new(linzer_response) key = Linzer.new_ed25519_key(current_provider.server_private_key_pem) - signature = Linzer.sign(key, message, %w(@status content-digest)) - - response.headers.merge!(signature.to_h) + Linzer.sign!(response, key:, components: %w(@status content-digest)) end def check_fasp_enabled diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index becbdb88c3..5854482dc5 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -69,6 +69,10 @@ export function importFetchedStatuses(statuses) { processStatus(status.reblog); } + if (status.quote?.quoted_status) { + processStatus(status.quote.quoted_status); + } + if (status.poll?.id) { pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls[status.poll.id])); } diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 2c583f86d4..330da74000 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -23,12 +23,20 @@ export function normalizeFilterResult(result) { export function normalizeStatus(status, normalOldStatus) { const normalStatus = { ...status }; + normalStatus.account = status.account.id; if (status.reblog && status.reblog.id) { normalStatus.reblog = status.reblog.id; } + if (status.quote?.quoted_status ?? status.quote?.quoted_status_id) { + normalStatus.quote = { + ...status.quote, + quoted_status: status.quote.quoted_status?.id ?? status.quote?.quoted_status_id, + }; + } + if (status.poll && status.poll.id) { normalStatus.poll = status.poll.id; } diff --git a/app/javascript/mastodon/components/account.tsx b/app/javascript/mastodon/components/account.tsx index 87e9a6eccf..cad7575f25 100644 --- a/app/javascript/mastodon/components/account.tsx +++ b/app/javascript/mastodon/components/account.tsx @@ -27,6 +27,7 @@ import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; import { ShortNumber } from 'mastodon/components/short_number'; import { Skeleton } from 'mastodon/components/skeleton'; import { VerifiedBadge } from 'mastodon/components/verified_badge'; +import { useIdentity } from 'mastodon/identity_context'; import { me } from 'mastodon/initial_state'; import type { MenuItem } from 'mastodon/models/dropdown_menu'; import { useAppSelector, useAppDispatch } from 'mastodon/store'; @@ -70,10 +71,12 @@ export const Account: React.FC<{ withBio?: boolean; }> = ({ id, size = 46, hidden, minimal, defaultAction, withBio }) => { const intl = useIntl(); + const { signedIn } = useIdentity(); const account = useAppSelector((state) => state.accounts.get(id)); const relationship = useAppSelector((state) => state.relationships.get(id)); const dispatch = useAppDispatch(); const accountUrl = account?.url; + const isRemote = account?.acct !== account?.username; const handleBlock = useCallback(() => { if (relationship?.blocking) { @@ -116,66 +119,74 @@ export const Account: React.FC<{ }, ]; } else if (defaultAction !== 'block') { - const handleAddToLists = () => { - const openAddToListModal = () => { - dispatch( - openModal({ - modalType: 'LIST_ADDER', - modalProps: { - accountId: id, - }, - }), - ); - }; - if (relationship?.following || relationship?.requested || id === me) { - openAddToListModal(); - } else { - dispatch( - openModal({ - modalType: 'CONFIRM_FOLLOW_TO_LIST', - modalProps: { - accountId: id, - onConfirm: () => { - apiFollowAccount(id) - .then((relationship) => { - dispatch( - followAccountSuccess({ - relationship, - alreadyFollowing: false, - }), - ); - openAddToListModal(); - }) - .catch((err: unknown) => { - dispatch(showAlertForError(err)); - }); - }, - }, - }), - ); - } - }; + arr = []; - arr = [ - { + if (isRemote && accountUrl) { + arr.push({ + text: intl.formatMessage(messages.openOriginalPage), + href: accountUrl, + }); + } + + if (signedIn) { + const handleAddToLists = () => { + const openAddToListModal = () => { + dispatch( + openModal({ + modalType: 'LIST_ADDER', + modalProps: { + accountId: id, + }, + }), + ); + }; + if (relationship?.following || relationship?.requested || id === me) { + openAddToListModal(); + } else { + dispatch( + openModal({ + modalType: 'CONFIRM_FOLLOW_TO_LIST', + modalProps: { + accountId: id, + onConfirm: () => { + apiFollowAccount(id) + .then((relationship) => { + dispatch( + followAccountSuccess({ + relationship, + alreadyFollowing: false, + }), + ); + openAddToListModal(); + }) + .catch((err: unknown) => { + dispatch(showAlertForError(err)); + }); + }, + }, + }), + ); + } + }; + + arr.push({ text: intl.formatMessage(messages.addToLists), action: handleAddToLists, - }, - ]; - - if (accountUrl) { - arr.unshift( - { - text: intl.formatMessage(messages.openOriginalPage), - href: accountUrl, - }, - null, - ); + }); } } return arr; - }, [dispatch, intl, id, accountUrl, relationship, defaultAction]); + }, [ + dispatch, + intl, + id, + accountUrl, + relationship, + defaultAction, + isRemote, + signedIn, + ]); if (hidden) { return ( diff --git a/app/javascript/mastodon/components/avatar_group.tsx b/app/javascript/mastodon/components/avatar_group.tsx index 2420728542..a629568693 100644 --- a/app/javascript/mastodon/components/avatar_group.tsx +++ b/app/javascript/mastodon/components/avatar_group.tsx @@ -7,10 +7,16 @@ import classNames from 'classnames'; export const AvatarGroup: React.FC<{ compact?: boolean; + avatarHeight?: number; children: React.ReactNode; -}> = ({ children, compact = false }) => ( +}> = ({ children, compact = false, avatarHeight }) => (
{children}
diff --git a/app/javascript/mastodon/components/counters.tsx b/app/javascript/mastodon/components/counters.tsx index 151b25a3f7..8b14d2a822 100644 --- a/app/javascript/mastodon/components/counters.tsx +++ b/app/javascript/mastodon/components/counters.tsx @@ -43,3 +43,17 @@ export const FollowersCounter = ( }} /> ); + +export const FollowersYouKnowCounter = ( + displayNumber: React.ReactNode, + pluralReady: number, +) => ( + {displayNumber}, + }} + /> +); diff --git a/app/javascript/mastodon/components/hover_card_account.tsx b/app/javascript/mastodon/components/hover_card_account.tsx index 05033e4ac7..12b74823b5 100644 --- a/app/javascript/mastodon/components/hover_card_account.tsx +++ b/app/javascript/mastodon/components/hover_card_account.tsx @@ -9,11 +9,16 @@ import { fetchAccount } from 'mastodon/actions/accounts'; import { AccountBio } from 'mastodon/components/account_bio'; import { AccountFields } from 'mastodon/components/account_fields'; import { Avatar } from 'mastodon/components/avatar'; -import { FollowersCounter } from 'mastodon/components/counters'; +import { AvatarGroup } from 'mastodon/components/avatar_group'; +import { + FollowersCounter, + FollowersYouKnowCounter, +} from 'mastodon/components/counters'; import { DisplayName } from 'mastodon/components/display_name'; import { FollowButton } from 'mastodon/components/follow_button'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { ShortNumber } from 'mastodon/components/short_number'; +import { useFetchFamiliarFollowers } from 'mastodon/features/account_timeline/hooks/familiar_followers'; import { domain } from 'mastodon/initial_state'; import { useAppSelector, useAppDispatch } from 'mastodon/store'; @@ -38,6 +43,8 @@ export const HoverCardAccount = forwardRef< } }, [dispatch, accountId, account]); + const { familiarFollowers } = useFetchFamiliarFollowers({ accountId }); + return (
-
+
+ {familiarFollowers.length > 0 && ( + <> + · +
+ + + {familiarFollowers.slice(0, 3).map((account) => ( + + ))} + +
+ + )}
diff --git a/app/javascript/mastodon/components/more_from_author.jsx b/app/javascript/mastodon/components/more_from_author.jsx deleted file mode 100644 index 719f4dda86..0000000000 --- a/app/javascript/mastodon/components/more_from_author.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import PropTypes from 'prop-types'; - -import { FormattedMessage } from 'react-intl'; - -import { IconLogo } from 'mastodon/components/logo'; -import { AuthorLink } from 'mastodon/features/explore/components/author_link'; - -export const MoreFromAuthor = ({ accountId }) => ( -
- - }} /> -
-); - -MoreFromAuthor.propTypes = { - accountId: PropTypes.string.isRequired, -}; diff --git a/app/javascript/mastodon/components/more_from_author.tsx b/app/javascript/mastodon/components/more_from_author.tsx new file mode 100644 index 0000000000..5075a29d3d --- /dev/null +++ b/app/javascript/mastodon/components/more_from_author.tsx @@ -0,0 +1,21 @@ +import { FormattedMessage } from 'react-intl'; + +import { IconLogo } from 'mastodon/components/logo'; +import { AuthorLink } from 'mastodon/features/explore/components/author_link'; + +export const MoreFromAuthor: React.FC<{ accountId: string }> = ({ + accountId, +}) => ( + }} + > + {(chunks) => ( +
+ + {chunks} +
+ )} +
+); diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 820b24cd6f..39b6e89902 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -5,14 +5,12 @@ import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import { Link } from 'react-router-dom'; - import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { HotKeys } from 'react-hotkeys'; import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; -import PushPinIcon from '@/material-icons/400-24px/push_pin.svg?react'; import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; import { ContentWarning } from 'mastodon/components/content_warning'; import { FilterWarning } from 'mastodon/components/filter_warning'; @@ -88,6 +86,7 @@ class Status extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map, account: ImmutablePropTypes.record, + children: PropTypes.node, previousId: PropTypes.string, nextInReplyToId: PropTypes.string, rootId: PropTypes.string, @@ -115,6 +114,7 @@ class Status extends ImmutablePureComponent { onMoveUp: PropTypes.func, onMoveDown: PropTypes.func, showThread: PropTypes.bool, + isQuotedPost: PropTypes.bool, getScrollPosition: PropTypes.func, updateScrollBottom: PropTypes.func, cacheMediaWidth: PropTypes.func, @@ -372,7 +372,7 @@ class Status extends ImmutablePureComponent { }; render () { - const { intl, hidden, featured, unfocusable, unread, showThread, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46 } = this.props; + const { intl, hidden, featured, unfocusable, unread, showThread, isQuotedPost = false, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46, children } = this.props; let { status, account, ...other } = this.props; @@ -519,7 +519,7 @@ class Status extends ImmutablePureComponent { ); } - } else if (status.get('card')) { + } else if (status.get('card') && !status.get('quote')) { media = ( {!skipPrepend && prepend} -
+
{(connectReply || connectUp || connectToRoot) &&
}
@@ -576,12 +588,16 @@ class Status extends ImmutablePureComponent { {...statusContentProps} /> + {children} + {media} {hashtagBar} )} - + {!isQuotedPost && + + }
diff --git a/app/javascript/mastodon/components/status_list.jsx b/app/javascript/mastodon/components/status_list.jsx index 3091e2a2a0..390659e9b6 100644 --- a/app/javascript/mastodon/components/status_list.jsx +++ b/app/javascript/mastodon/components/status_list.jsx @@ -9,7 +9,7 @@ import { TIMELINE_GAP, TIMELINE_SUGGESTIONS } from 'mastodon/actions/timelines'; import { RegenerationIndicator } from 'mastodon/components/regeneration_indicator'; import { InlineFollowSuggestions } from 'mastodon/features/home_timeline/components/inline_follow_suggestions'; -import StatusContainer from '../containers/status_container'; +import { StatusQuoteManager } from '../components/status_quoted'; import { LoadGap } from './load_gap'; import ScrollableList from './scrollable_list'; @@ -113,7 +113,7 @@ export default class StatusList extends ImmutablePureComponent { ); default: return ( - ( - = ({ isError, children }) => { + return ( +
+ + {children} +
+ ); +}; + +const QuoteLink: React.FC<{ + status: Status; +}> = ({ status }) => { + const accountId = status.get('account') as string; + const account = useAppSelector((state) => + accountId ? state.accounts.get(accountId) : undefined, + ); + + const quoteAuthorName = account?.display_name_html; + + if (!quoteAuthorName) { + return null; + } + + const quoteAuthorElement = ( + + ); + const quoteUrl = `/@${account.get('acct')}/${status.get('id') as string}`; + + return ( + + + + + + ); +}; + +type QuoteMap = ImmutableMap<'state' | 'quoted_status', string | null>; +type GetStatusSelector = ( + state: RootState, + props: { id?: string | null; contextType?: string }, +) => Status | null; + +export const QuotedStatus: React.FC<{ + quote: QuoteMap; + contextType?: string; + variant?: 'full' | 'link'; + nestingLevel?: number; +}> = ({ quote, contextType, nestingLevel = 1, variant = 'full' }) => { + const quotedStatusId = quote.get('quoted_status'); + const quoteState = quote.get('state'); + const status = useAppSelector((state) => + quotedStatusId ? state.statuses.get(quotedStatusId) : undefined, + ); + let quoteError: React.ReactNode = null; + + // 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`. + // If this returns null even though `status` exists, it's because it's filtered. + const getStatus = useMemo(() => makeGetStatus(), []) as GetStatusSelector; + const statusWithExtraData = useAppSelector((state) => + getStatus(state, { id: quotedStatusId, contextType }), + ); + const isFilteredAndHidden = status && statusWithExtraData === null; + + if (isFilteredAndHidden) { + quoteError = ( + + ); + } else if (quoteState === 'deleted') { + quoteError = ( + + ); + } else if (quoteState === 'unauthorized') { + quoteError = ( + + ); + } else if (quoteState === 'pending') { + quoteError = ( + + ); + } else if (quoteState === 'rejected' || quoteState === 'revoked') { + quoteError = ( + + ); + } else if (!status || !quotedStatusId) { + quoteError = ( + + ); + } + + if (quoteError) { + return {quoteError}; + } + + if (variant === 'link' && status) { + return ; + } + + const childQuote = status?.get('quote') as QuoteMap | undefined; + const canRenderChildQuote = + childQuote && nestingLevel <= MAX_QUOTE_POSTS_NESTING_LEVEL; + + return ( + + {/* @ts-expect-error Status is not yet typed */} + + {canRenderChildQuote && ( + + )} + + + ); +}; + +interface StatusQuoteManagerProps { + id: string; + contextType?: string; + [key: string]: unknown; +} + +/** + * This wrapper component takes a status ID and, if the associated status + * is a quote post, it renders the quote into `StatusContainer` as a child. + * It passes all other props through to `StatusContainer`. + */ + +export const StatusQuoteManager = (props: StatusQuoteManagerProps) => { + const status = useAppSelector((state) => { + const status = state.statuses.get(props.id); + const reblogId = status?.get('reblog') as string | undefined; + return reblogId ? state.statuses.get(reblogId) : status; + }); + const quote = status?.get('quote') as QuoteMap | undefined; + + if (quote) { + return ( + + + + ); + } + + return ; +}; diff --git a/app/javascript/mastodon/features/about/index.jsx b/app/javascript/mastodon/features/about/index.jsx index f2ea16a952..d2e1ea8d77 100644 --- a/app/javascript/mastodon/features/about/index.jsx +++ b/app/javascript/mastodon/features/about/index.jsx @@ -171,8 +171,8 @@ class About extends PureComponent { ) : (
    {server.get('rules').map(rule => { - const text = rule.getIn(['translations', locale, 'text']) || rule.get('text'); - const hint = rule.getIn(['translations', locale, 'hint']) || rule.get('hint'); + const text = rule.getIn(['translations', locale, 'text']) || rule.getIn(['translations', locale.split('-')[0], 'text']) || rule.get('text'); + const hint = rule.getIn(['translations', locale, 'hint']) || rule.getIn(['translations', locale.split('-')[0], 'hint']) || rule.get('hint'); return (
  1. {text}
    diff --git a/app/javascript/mastodon/features/account_featured/index.tsx b/app/javascript/mastodon/features/account_featured/index.tsx index d516bc3411..c473d311c1 100644 --- a/app/javascript/mastodon/features/account_featured/index.tsx +++ b/app/javascript/mastodon/features/account_featured/index.tsx @@ -14,7 +14,7 @@ import { Account } from 'mastodon/components/account'; import { ColumnBackButton } from 'mastodon/components/column_back_button'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { RemoteHint } from 'mastodon/components/remote_hint'; -import StatusContainer from 'mastodon/containers/status_container'; +import { StatusQuoteManager } from 'mastodon/components/status_quoted'; import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; import Column from 'mastodon/features/ui/components/column'; @@ -142,9 +142,8 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({ /> {featuredStatusIds.map((statusId) => ( - diff --git a/app/javascript/mastodon/features/account_timeline/components/familiar_followers.tsx b/app/javascript/mastodon/features/account_timeline/components/familiar_followers.tsx index fd6e1d5dfd..41bd8ab4ba 100644 --- a/app/javascript/mastodon/features/account_timeline/components/familiar_followers.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/familiar_followers.tsx @@ -1,15 +1,12 @@ -import { useEffect } from 'react'; - import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom'; -import { fetchAccountsFamiliarFollowers } from '@/mastodon/actions/accounts_familiar_followers'; import { Avatar } from '@/mastodon/components/avatar'; import { AvatarGroup } from '@/mastodon/components/avatar_group'; import type { Account } from '@/mastodon/models/account'; -import { getAccountFamiliarFollowers } from '@/mastodon/selectors/accounts'; -import { useAppDispatch, useAppSelector } from '@/mastodon/store'; + +import { useFetchFamiliarFollowers } from '../hooks/familiar_followers'; const AccountLink: React.FC<{ account?: Account }> = ({ account }) => { if (!account) { @@ -64,20 +61,11 @@ const FamiliarFollowersReadout: React.FC<{ familiarFollowers: Account[] }> = ({ export const FamiliarFollowers: React.FC<{ accountId: string }> = ({ accountId, }) => { - const dispatch = useAppDispatch(); - const familiarFollowers = useAppSelector((state) => - getAccountFamiliarFollowers(state, accountId), - ); + const { familiarFollowers, isLoading } = useFetchFamiliarFollowers({ + accountId, + }); - const hasNoData = familiarFollowers === null; - - useEffect(() => { - if (hasNoData) { - void dispatch(fetchAccountsFamiliarFollowers({ id: accountId })); - } - }, [dispatch, accountId, hasNoData]); - - if (hasNoData || familiarFollowers.length === 0) { + if (isLoading || familiarFollowers.length === 0) { return null; } diff --git a/app/javascript/mastodon/features/account_timeline/hooks/familiar_followers.ts b/app/javascript/mastodon/features/account_timeline/hooks/familiar_followers.ts new file mode 100644 index 0000000000..d8c566ad86 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/hooks/familiar_followers.ts @@ -0,0 +1,30 @@ +import { useEffect } from 'react'; + +import { fetchAccountsFamiliarFollowers } from '@/mastodon/actions/accounts_familiar_followers'; +import { getAccountFamiliarFollowers } from '@/mastodon/selectors/accounts'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import { me } from 'mastodon/initial_state'; + +export const useFetchFamiliarFollowers = ({ + accountId, +}: { + accountId?: string; +}) => { + const dispatch = useAppDispatch(); + const familiarFollowers = useAppSelector((state) => + accountId ? getAccountFamiliarFollowers(state, accountId) : null, + ); + + const hasNoData = familiarFollowers === null; + + useEffect(() => { + if (hasNoData && accountId && accountId !== me) { + void dispatch(fetchAccountsFamiliarFollowers({ id: accountId })); + } + }, [dispatch, accountId, hasNoData]); + + return { + familiarFollowers: hasNoData ? [] : familiarFollowers, + isLoading: hasNoData, + }; +}; diff --git a/app/javascript/mastodon/features/notifications/components/notification.jsx b/app/javascript/mastodon/features/notifications/components/notification.jsx index 00963b2274..86431f62fd 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.jsx +++ b/app/javascript/mastodon/features/notifications/components/notification.jsx @@ -20,7 +20,7 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; import { Account } from 'mastodon/components/account'; import { Icon } from 'mastodon/components/icon'; -import StatusContainer from 'mastodon/containers/status_container'; +import { StatusQuoteManager } from 'mastodon/components/status_quoted'; import { me } from 'mastodon/initial_state'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; @@ -175,7 +175,7 @@ class Notification extends ImmutablePureComponent { renderMention (notification) { return ( -
-
-
-
- - = ({ accountId }) => { const account = useAppSelector((state) => state.accounts.get(accountId)); if (!account) return null; - return ; + return ; }; export type LabelRenderer = ( @@ -108,7 +110,7 @@ export const NotificationGroupWithStatus: React.FC<{
- + {accountIds .slice(0, NOTIFICATIONS_GROUP_MAX_AVATARS) .map((id) => ( @@ -123,7 +125,14 @@ export const NotificationGroupWithStatus: React.FC<{
{label} - {timestamp && } + {timestamp && ( + <> + + · + + + + )}
diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_with_status.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_with_status.tsx index 3e6428287d..de484322fb 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_with_status.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_with_status.tsx @@ -12,7 +12,7 @@ import { } from 'mastodon/actions/statuses'; import type { IconProp } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon'; -import Status from 'mastodon/containers/status_container'; +import { StatusQuoteManager } from 'mastodon/components/status_quoted'; import { getStatusHidden } from 'mastodon/selectors/filters'; import { useAppSelector, useAppDispatch } from 'mastodon/store'; @@ -102,8 +102,7 @@ export const NotificationWithStatus: React.FC<{ {label}
- is not yet typed + ))} diff --git a/app/javascript/mastodon/features/search/index.tsx b/app/javascript/mastodon/features/search/index.tsx index f85fba1f78..cdb506140f 100644 --- a/app/javascript/mastodon/features/search/index.tsx +++ b/app/javascript/mastodon/features/search/index.tsx @@ -17,7 +17,7 @@ import { ColumnHeader } from 'mastodon/components/column_header'; import { CompatibilityHashtag as Hashtag } from 'mastodon/components/hashtag'; import { Icon } from 'mastodon/components/icon'; import ScrollableList from 'mastodon/components/scrollable_list'; -import Status from 'mastodon/containers/status_container'; +import { StatusQuoteManager } from 'mastodon/components/status_quoted'; import { Search } from 'mastodon/features/compose/components/search'; import { useSearchParam } from 'mastodon/hooks/useSearchParam'; import type { Hashtag as HashtagType } from 'mastodon/models/tags'; @@ -53,8 +53,7 @@ const renderHashtags = (hashtags: HashtagType[]) => const renderStatuses = (statusIds: string[]) => hidePeek(statusIds).map((id) => ( - // @ts-expect-error inferred props are wrong - + )); type SearchType = 'all' | ApiSearchType; @@ -190,8 +189,7 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({ onClickMore={handleSelectStatuses} > {results.statuses.slice(0, INITIAL_DISPLAY).map((id) => ( - // @ts-expect-error inferred props are wrong - + ))} )} diff --git a/app/javascript/mastodon/features/status/components/detailed_status.tsx b/app/javascript/mastodon/features/status/components/detailed_status.tsx index 75d995b1e0..650e439348 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.tsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.tsx @@ -26,6 +26,7 @@ import { IconLogo } from 'mastodon/components/logo'; import MediaGallery from 'mastodon/components/media_gallery'; import { PictureInPicturePlaceholder } from 'mastodon/components/picture_in_picture_placeholder'; import StatusContent from 'mastodon/components/status_content'; +import { QuotedStatus } from 'mastodon/components/status_quoted'; import { VisibilityIcon } from 'mastodon/components/visibility_icon'; import { Audio } from 'mastodon/features/audio'; import scheduleIdleTask from 'mastodon/features/ui/util/schedule_idle_task'; @@ -226,7 +227,7 @@ export const DetailedStatus: React.FC<{ /> ); } - } else if (status.get('card')) { + } else if (status.get('card') && !status.get('quote')) { media = ( -
+
{status.get('visibility') === 'direct' && (
@@ -371,6 +377,10 @@ export const DetailedStatus: React.FC<{ {...(statusContentProps as any)} /> + {status.get('quote') && ( + + )} + {media} {hashtagBar} diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 7da2df3742..0f02e7b50f 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -6,8 +6,6 @@ import classNames from 'classnames'; import { Helmet } from 'react-helmet'; import { withRouter } from 'react-router-dom'; -import { createSelector } from '@reduxjs/toolkit'; -import { List as ImmutableList } from 'immutable'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; @@ -62,7 +60,7 @@ import { } from '../../actions/statuses'; import ColumnHeader from '../../components/column_header'; import { textForScreenReader, defaultMediaVisibility } from '../../components/status'; -import StatusContainer from '../../containers/status_container'; +import { StatusQuoteManager } from '../../components/status_quoted'; import { deleteModal } from '../../initial_state'; import { makeGetStatus, makeGetPictureInPicture } from '../../selectors'; import { getAncestorsIds, getDescendantsIds } from 'mastodon/selectors/contexts'; @@ -477,7 +475,7 @@ class Status extends ImmutablePureComponent { const { params: { statusId } } = this.props; return list.map((id, i) => ( - { - if (item) { - onDismiss(item); - } + onDismiss(item); }, }); diff --git a/app/javascript/mastodon/locales/az.json b/app/javascript/mastodon/locales/az.json index c7463581cd..e988140fa0 100644 --- a/app/javascript/mastodon/locales/az.json +++ b/app/javascript/mastodon/locales/az.json @@ -23,9 +23,11 @@ "account.copy": "Profil linkini kopyala", "account.direct": "@{name} istifadəçisini fərdi olaraq etiketlə", "account.disable_notifications": "@{name} paylaşım edəndə mənə bildiriş göndərməyi dayandır", + "account.domain_blocking": "Domenin bloklanması", "account.edit_profile": "Profili redaktə et", "account.enable_notifications": "@{name} paylaşım edəndə mənə bildiriş göndər", "account.endorse": "Profildə seçilmişlərə əlavə et", + "account.featured.hashtags": "Etiketler", "account.featured_tags.last_status_at": "Son paylaşım {date} tarixində olub", "account.featured_tags.last_status_never": "Paylaşım yoxdur", "account.follow": "İzlə", diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json index 8f79d7a05d..ddd8ff3f2d 100644 --- a/app/javascript/mastodon/locales/br.json +++ b/app/javascript/mastodon/locales/br.json @@ -34,6 +34,7 @@ "account.followers": "Tud koumanantet", "account.followers.empty": "Den na heul an implijer·ez-mañ c'hoazh.", "account.followers_counter": "{count, plural, one {{counter} heulier} two {{counter} heulier} few {{counter} heulier} many {{counter} heulier} other {{counter} heulier}}", + "account.followers_you_know_counter": "{counter} a anavezit", "account.following": "Koumanantoù", "account.follows.empty": "An implijer·ez-mañ na heul den ebet.", "account.go_to_profile": "Gwelet ar profil", @@ -577,6 +578,7 @@ "status.mute": "Kuzhat @{name}", "status.mute_conversation": "Kuzhat ar gaozeadenn", "status.open": "Digeriñ ar c'hannad-mañ", + "status.quote_post_author": "Embannadenn gant {name}", "status.read_more": "Lenn muioc'h", "status.reblog": "Skignañ", "status.reblog_private": "Skignañ gant ar weledenn gentañ", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index ce71e1cf5a..b8bba92496 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -42,6 +42,7 @@ "account.followers": "Sledující", "account.followers.empty": "Tohoto uživatele zatím nikdo nesleduje.", "account.followers_counter": "{count, plural, one {{counter} sledující} few {{counter} sledující} many {{counter} sledujících} other {{counter} sledujících}}", + "account.followers_you_know_counter": "{count, one {{counter}, kterého znáte}, few {{counter}, které znáte}, many {{counter}, kterých znáte} other {{counter}, kterých znáte}}", "account.following": "Sledujete", "account.following_counter": "{count, plural, one {{counter} sledovaný} few {{counter} sledovaní} many {{counter} sledovaných} other {{counter} sledovaných}}", "account.follows.empty": "Tento uživatel zatím nikoho nesleduje.", @@ -683,7 +684,7 @@ "notifications.policy.filter_not_followers_title": "Lidé, kteří vás nesledují", "notifications.policy.filter_not_following_hint": "Dokud je ručně neschválíte", "notifications.policy.filter_not_following_title": "Lidé, které nesledujete", - "notifications.policy.filter_private_mentions_hint": "Vyfiltrováno, pokud to není odpověď na vaši zmínku nebo pokud sledujete odesílatele", + "notifications.policy.filter_private_mentions_hint": "Filtrováno, pokud to není v odpovědi na vaši vlastní zmínku nebo pokud nesledujete odesílatele", "notifications.policy.filter_private_mentions_title": "Nevyžádané soukromé zmínky", "notifications.policy.title": "Spravovat oznámení od…", "notifications_permission_banner.enable": "Povolit oznámení na ploše", @@ -863,6 +864,12 @@ "status.mute_conversation": "Skrýt konverzaci", "status.open": "Rozbalit tento příspěvek", "status.pin": "Zvýraznit na profilu", + "status.quote_error.not_found": "Tento příspěvek nelze zobrazit.", + "status.quote_error.pending_approval": "Tento příspěvek čeká na schválení od původního autora.", + "status.quote_error.rejected": "Tento příspěvek nemůže být zobrazen, protože původní autor neumožňuje, aby byl citován.", + "status.quote_error.removed": "Tento příspěvek byl odstraněn jeho autorem.", + "status.quote_error.unauthorized": "Tento příspěvek nelze zobrazit, protože nemáte oprávnění k jeho zobrazení.", + "status.quote_post_author": "Příspěvek od {name}", "status.read_more": "Číst více", "status.reblog": "Boostnout", "status.reblog_private": "Boostnout s původní viditelností", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index 6a86a4926c..7acce4ac5a 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -42,6 +42,7 @@ "account.followers": "Dilynwyr", "account.followers.empty": "Does neb yn dilyn y defnyddiwr hwn eto.", "account.followers_counter": "{count, plural, one {{counter} dilynwr} two {{counter} ddilynwr} other {{counter} dilynwyr}}", + "account.followers_you_know_counter": "{counter} rydych chi'n adnabod", "account.following": "Yn dilyn", "account.following_counter": "{count, plural, one {Yn dilyn {counter}} other {Yn dilyn {counter} arall}}", "account.follows.empty": "Dyw'r defnyddiwr hwn ddim yn dilyn unrhyw un eto.", @@ -863,6 +864,12 @@ "status.mute_conversation": "Anwybyddu sgwrs", "status.open": "Ehangu'r post hwn", "status.pin": "Dangos ar y proffil", + "status.quote_error.not_found": "Does dim modd dangos y postiad hwn.", + "status.quote_error.pending_approval": "Mae'r postiad hwn yn aros am gymeradwyaeth yr awdur gwreiddiol.", + "status.quote_error.rejected": "Does dim modd dangos y postiad hwn gan nad yw'r awdur gwreiddiol yn caniatáu iddo gael ei ddyfynnu.", + "status.quote_error.removed": "Cafodd y postiad hwn ei ddileu gan ei awdur.", + "status.quote_error.unauthorized": "Does dim modd dangos y postiad hwn gan nad oes gennych awdurdod i'w weld.", + "status.quote_post_author": "Postiad gan {name}", "status.read_more": "Darllen rhagor", "status.reblog": "Hybu", "status.reblog_private": "Hybu i'r gynulleidfa wreiddiol", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index db5e66fe94..c5514a56ac 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -863,6 +863,12 @@ "status.mute_conversation": "Skjul samtale", "status.open": "Udvid dette indlæg", "status.pin": "Fremhæv på profil", + "status.quote_error.not_found": "Dette indlæg kan ikke vises.", + "status.quote_error.pending_approval": "Dette indlæg afventer godkendelse fra den oprindelige forfatter.", + "status.quote_error.rejected": "Dette indlæg kan ikke vises, da den oprindelige forfatter ikke tillader citering heraf.", + "status.quote_error.removed": "Dette indlæg er fjernet af forfatteren.", + "status.quote_error.unauthorized": "Dette indlæg kan ikke vises, da man ikke har tilladelse til at se det.", + "status.quote_post_author": "Indlæg fra {name}", "status.read_more": "Læs mere", "status.reblog": "Fremhæv", "status.reblog_private": "Fremhæv med oprindelig synlighed", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 01b195e5d4..172645480e 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -42,6 +42,7 @@ "account.followers": "Follower", "account.followers.empty": "Diesem Profil folgt noch niemand.", "account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Follower}}", + "account.followers_you_know_counter": "{counter} bekannt", "account.following": "Folge ich", "account.following_counter": "{count, plural, one {{counter} Folge ich} other {{counter} Folge ich}}", "account.follows.empty": "Dieses Profil folgt noch niemandem.", @@ -360,7 +361,7 @@ "filter_modal.select_filter.title": "Diesen Beitrag filtern", "filter_modal.title.status": "Beitrag per Filter ausblenden", "filter_warning.matches_filter": "Übereinstimmend mit dem Filter „{title}“", - "filtered_notifications_banner.pending_requests": "Von {count, plural, =0 {keinem, den} one {einer Person, die} other {# Personen, die}} du möglicherweise kennst", + "filtered_notifications_banner.pending_requests": "Von {count, plural, =0 {keinem Profil, das dir möglicherweise bekannt ist} one {einem Profil, das dir möglicherweise bekannt ist} other {# Profilen, die dir möglicherweise bekannt sind}}", "filtered_notifications_banner.title": "Gefilterte Benachrichtigungen", "firehose.all": "Alle Server", "firehose.local": "Dieser Server", @@ -863,6 +864,12 @@ "status.mute_conversation": "Unterhaltung stummschalten", "status.open": "Beitrag öffnen", "status.pin": "Im Profil vorstellen", + "status.quote_error.not_found": "Dieser Beitrag kann nicht angezeigt werden.", + "status.quote_error.pending_approval": "Dieser Beitrag muss noch durch das ursprüngliche Profil genehmigt werden.", + "status.quote_error.rejected": "Dieser Beitrag kann nicht angezeigt werden, weil das ursprüngliche Profil das Zitieren nicht erlaubt.", + "status.quote_error.removed": "Dieser Beitrag wurde durch das Profil entfernt.", + "status.quote_error.unauthorized": "Dieser Beitrag kann nicht angezeigt werden, weil du zum Ansehen nicht berechtigt bist.", + "status.quote_post_author": "Beitrag von {name}", "status.read_more": "Gesamten Beitrag anschauen", "status.reblog": "Teilen", "status.reblog_private": "Mit der ursprünglichen Zielgruppe teilen", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index ee494c7e45..12b933004d 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -42,6 +42,7 @@ "account.followers": "Followers", "account.followers.empty": "No one follows this user yet.", "account.followers_counter": "{count, plural, one {{counter} follower} other {{counter} followers}}", + "account.followers_you_know_counter": "{counter} you know", "account.following": "Following", "account.following_counter": "{count, plural, one {{counter} following} other {{counter} following}}", "account.follows.empty": "This user doesn't follow anyone yet.", @@ -863,6 +864,13 @@ "status.mute_conversation": "Mute conversation", "status.open": "Expand this post", "status.pin": "Feature on profile", + "status.quote_error.filtered": "Hidden due to one of your filters", + "status.quote_error.not_found": "This post cannot be displayed.", + "status.quote_error.pending_approval": "This post is pending approval from the original author.", + "status.quote_error.rejected": "This post cannot be displayed as the original author does not allow it to be quoted.", + "status.quote_error.removed": "This post was removed by its author.", + "status.quote_error.unauthorized": "This post cannot be displayed as you are not authorized to view it.", + "status.quote_post_author": "Post by {name}", "status.read_more": "Read more", "status.reblog": "Boost", "status.reblog_private": "Boost with original visibility", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 73260068fe..158a937ae1 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -42,6 +42,7 @@ "account.followers": "Seguidores", "account.followers.empty": "Todavía nadie sigue a este usuario.", "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}", + "account.followers_you_know_counter": "{counter} seguidores que conocés", "account.following": "Siguiendo", "account.following_counter": "{count, plural, one {siguiendo a {counter}} other {siguiendo a {counter}}}", "account.follows.empty": "Todavía este usuario no sigue a nadie.", @@ -863,6 +864,12 @@ "status.mute_conversation": "Silenciar conversación", "status.open": "Expandir este mensaje", "status.pin": "Destacar en el perfil", + "status.quote_error.not_found": "No se puede mostrar este mensaje.", + "status.quote_error.pending_approval": "Este mensaje está pendiente de aprobación del autor original.", + "status.quote_error.rejected": "No se puede mostrar este mensaje, ya que el autor original no permite que se cite.", + "status.quote_error.removed": "Este mensaje fue eliminado por su autor.", + "status.quote_error.unauthorized": "No se puede mostrar este mensaje, ya que no tenés autorización para verlo.", + "status.quote_post_author": "Mensaje de @{name}", "status.read_more": "Leé más", "status.reblog": "Adherir", "status.reblog_private": "Adherir a la audiencia original", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index f3c00a64ef..e50a6e80a3 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -42,6 +42,7 @@ "account.followers": "Seguidores", "account.followers.empty": "Nadie sigue a este usuario todavía.", "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}", + "account.followers_you_know_counter": "{counter} que conoces", "account.following": "Siguiendo", "account.following_counter": "{count, plural, one {{counter} siguiendo} other {{counter} siguiendo}}", "account.follows.empty": "Este usuario no sigue a nadie todavía.", @@ -863,6 +864,12 @@ "status.mute_conversation": "Silenciar conversación", "status.open": "Expandir estado", "status.pin": "Destacar en el perfil", + "status.quote_error.not_found": "No se puede mostrar esta publicación.", + "status.quote_error.pending_approval": "Esta publicación está pendiente de aprobación del autor original.", + "status.quote_error.rejected": "No se puede mostrar esta publicación, puesto que el autor original no permite que sea citado.", + "status.quote_error.removed": "Esta publicación fue eliminada por su autor.", + "status.quote_error.unauthorized": "No se puede mostrar esta publicación, puesto que no estás autorizado a verla.", + "status.quote_post_author": "Publicado por {name}", "status.read_more": "Leer más", "status.reblog": "Impulsar", "status.reblog_private": "Implusar a la audiencia original", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 28d88e01fe..e0a969732e 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -42,6 +42,7 @@ "account.followers": "Seguidores", "account.followers.empty": "Todavía nadie sigue a este usuario.", "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}", + "account.followers_you_know_counter": "{counter} seguidores que conoces", "account.following": "Siguiendo", "account.following_counter": "{count, plural, one {{counter} siguiendo} other {{counter} siguiendo}}", "account.follows.empty": "Este usuario todavía no sigue a nadie.", @@ -863,6 +864,12 @@ "status.mute_conversation": "Silenciar conversación", "status.open": "Expandir publicación", "status.pin": "Destacar en el perfil", + "status.quote_error.not_found": "No se puede mostrar esta publicación.", + "status.quote_error.pending_approval": "Esta publicación está pendiente de aprobación del autor original.", + "status.quote_error.rejected": "Esta publicación no puede mostrarse porque el autor original no permite que se cite.", + "status.quote_error.removed": "Esta publicación fue eliminada por su autor.", + "status.quote_error.unauthorized": "Esta publicación no puede mostrarse, ya que no estás autorizado a verla.", + "status.quote_post_author": "Publicación de {name}", "status.read_more": "Leer más", "status.reblog": "Impulsar", "status.reblog_private": "Impulsar a la audiencia original", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index c65ed1288a..07279c107c 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -42,6 +42,7 @@ "account.followers": "Seuraajat", "account.followers.empty": "Kukaan ei seuraa tätä käyttäjää vielä.", "account.followers_counter": "{count, plural, one {{counter} seuraaja} other {{counter} seuraajaa}}", + "account.followers_you_know_counter": "{count, plural, one {{counter} tuntemasi} other {{counter} tuntemaasi}}", "account.following": "Seurattavat", "account.following_counter": "{count, plural, one {{counter} seurattava} other {{counter} seurattavaa}}", "account.follows.empty": "Tämä käyttäjä ei vielä seuraa ketään.", @@ -863,6 +864,12 @@ "status.mute_conversation": "Mykistä keskustelu", "status.open": "Laajenna julkaisu", "status.pin": "Suosittele profiilissa", + "status.quote_error.not_found": "Tätä julkaisua ei voi näyttää.", + "status.quote_error.pending_approval": "Tämä julkaisu odottaa alkuperäisen tekijänsä hyväksyntää.", + "status.quote_error.rejected": "Tätä julkaisua ei voi näyttää, sillä sen alkuperäinen tekijä ei salli lainattavan julkaisua.", + "status.quote_error.removed": "Tekijä on poistanut julkaisun.", + "status.quote_error.unauthorized": "Tätä julkaisua ei voi näyttää, koska sinulla ei ole oikeutta tarkastella sitä.", + "status.quote_post_author": "Julkaisu käyttäjältä {name}", "status.read_more": "Näytä enemmän", "status.reblog": "Tehosta", "status.reblog_private": "Tehosta alkuperäiselle yleisölle", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index 1bb9bb29b0..2004364832 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -42,6 +42,7 @@ "account.followers": "Fylgjarar", "account.followers.empty": "Ongar fylgjarar enn.", "account.followers_counter": "{count, plural, one {{counter} fylgjari} other {{counter} fylgjarar}}", + "account.followers_you_know_counter": "{counter} tú kennir", "account.following": "Fylgir", "account.following_counter": "{count, plural, one {{counter} fylgir} other {{counter} fylgja}}", "account.follows.empty": "Hesin brúkari fylgir ongum enn.", @@ -863,6 +864,12 @@ "status.mute_conversation": "Doyv samrøðu", "status.open": "Víðka henda postin", "status.pin": "Vís á vanga", + "status.quote_error.not_found": "Tað ber ikki til at vísa hendan postin.", + "status.quote_error.pending_approval": "Hesin posturin bíðar eftir góðkenning frá upprunahøvundinum.", + "status.quote_error.rejected": "Hesin posturin kann ikki vísast, tí upprunahøvundurin loyvir ikki at posturin verður siteraður.", + "status.quote_error.removed": "Hesin posturin var strikaður av høvundinum.", + "status.quote_error.unauthorized": "Hesin posturin kann ikki vísast, tí tú hevur ikki rættindi at síggja hann.", + "status.quote_post_author": "Postur hjá @{name}", "status.read_more": "Les meira", "status.reblog": "Stimbra", "status.reblog_private": "Stimbra við upprunasýni", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 9db5c4dfbd..1322551ae8 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -42,6 +42,7 @@ "account.followers": "Seguidoras", "account.followers.empty": "Aínda ninguén segue esta usuaria.", "account.followers_counter": "{count, plural, one {{counter} seguidora} other {{counter} seguidoras}}", + "account.followers_you_know_counter": "{counter} que coñeces", "account.following": "Seguindo", "account.following_counter": "{count, plural, one {{counter} seguimento} other {{counter} seguimentos}}", "account.follows.empty": "Esta usuaria aínda non segue a ninguén.", @@ -863,6 +864,12 @@ "status.mute_conversation": "Silenciar conversa", "status.open": "Estender esta publicación", "status.pin": "Destacar no perfil", + "status.quote_error.not_found": "Non se pode mostrar a publicación.", + "status.quote_error.pending_approval": "A publicación está pendente da aprobación pola autora orixinal.", + "status.quote_error.rejected": "Non se pode mostrar esta publicación xa que a autora orixinal non permite que se cite.", + "status.quote_error.removed": "Publicación eliminada pola autora.", + "status.quote_error.unauthorized": "Non se pode mostrar esta publicación porque non tes permiso para vela.", + "status.quote_post_author": "Publicación de {name}", "status.read_more": "Ler máis", "status.reblog": "Promover", "status.reblog_private": "Compartir coa audiencia orixinal", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index d3811a75c4..ccb4fdb895 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -863,6 +863,12 @@ "status.mute_conversation": "השתקת שיחה", "status.open": "הרחבת הודעה זו", "status.pin": "מובלט בפרופיל", + "status.quote_error.not_found": "לא ניתן להציג הודעה זו.", + "status.quote_error.pending_approval": "הודעה זו מחכה לאישור מידי היוצר המקורי.", + "status.quote_error.rejected": "לא ניתן להציג הודעה זו שכן המחבר.ת המקוריים לא הרשו לצטט אותה.", + "status.quote_error.removed": "הודעה זו הוסרה על ידי השולחים המקוריים.", + "status.quote_error.unauthorized": "הודעה זו לא מוצגת כיוון שאין לך רשות לראותה.", + "status.quote_post_author": "פרסום מאת {name}", "status.read_more": "לקרוא עוד", "status.reblog": "הדהוד", "status.reblog_private": "להדהד ברמת הנראות המקורית", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index b4edfd4818..4566468386 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -42,6 +42,7 @@ "account.followers": "Követő", "account.followers.empty": "Ezt a felhasználót még senki sem követi.", "account.followers_counter": "{count, plural, one {{counter} követő} other {{counter} követő}}", + "account.followers_you_know_counter": "{counter} ismerős", "account.following": "Követve", "account.following_counter": "{count, plural, one {{counter} követett} other {{counter} követett}}", "account.follows.empty": "Ez a felhasználó még senkit sem követ.", @@ -863,6 +864,12 @@ "status.mute_conversation": "Beszélgetés némítása", "status.open": "Bejegyzés kibontása", "status.pin": "Kiemelés a profilodon", + "status.quote_error.not_found": "Ez a bejegyzés nem jeleníthető meg.", + "status.quote_error.pending_approval": "Ez a bejegyzés az eredeti szerző jóváhagyására vár.", + "status.quote_error.rejected": "Ez a bejegyzés nem jeleníthető meg, mert az eredeti szerzője nem engedélyezi az idézését.", + "status.quote_error.removed": "Ezt a bejegyzés eltávolította a szerzője.", + "status.quote_error.unauthorized": "Ez a bejegyzés nem jeleníthető meg, mert nem jogosult a megtekintésére.", + "status.quote_post_author": "Szerző: {name}", "status.read_more": "Bővebben", "status.reblog": "Megtolás", "status.reblog_private": "Megtolás az eredeti közönségnek", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index aff74649fe..a2081dde92 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -42,6 +42,7 @@ "account.followers": "Fylgjendur", "account.followers.empty": "Ennþá fylgist enginn með þessum notanda.", "account.followers_counter": "{count, plural, one {Fylgjandi: {counter}} other {Fylgjendur: {counter}}}", + "account.followers_you_know_counter": "{counter} sem þú þekkir", "account.following": "Fylgist með", "account.following_counter": "{count, plural, one {Fylgist með: {counter}} other {Fylgist með: {counter}}}", "account.follows.empty": "Þessi notandi fylgist ennþá ekki með neinum.", @@ -863,6 +864,12 @@ "status.mute_conversation": "Þagga niður í samtali", "status.open": "Opna þessa færslu", "status.pin": "Birta á notandasniði", + "status.quote_error.not_found": "Þessa færslu er ekki hægt að birta.", + "status.quote_error.pending_approval": "Þessi færsla bíður eftir samþykki frá upprunalegum höfundi hennar.", + "status.quote_error.rejected": "Þessa færslu er ekki hægt að birta þar sem upphaflegur höfundur hennar leyfir ekki að vitnað sé til hennar.", + "status.quote_error.removed": "Þessi færsla var fjarlægð af höfundi hennar.", + "status.quote_error.unauthorized": "Þessa færslu er ekki hægt að birta þar sem þú hefur ekki heimild til að skoða hana.", + "status.quote_post_author": "Færsla frá {name}", "status.read_more": "Lesa meira", "status.reblog": "Endurbirting", "status.reblog_private": "Endurbirta til upphaflegra lesenda", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index ece77c01d4..7d295f23d8 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -28,6 +28,9 @@ "account.edit_profile": "프로필 편집", "account.enable_notifications": "@{name} 의 게시물 알림 켜기", "account.endorse": "프로필에 추천하기", + "account.familiar_followers_many": "{name1}, {name2} 님 외 내가 아는 {othersCount, plural, other {#}} 명이 팔로우함", + "account.familiar_followers_one": "{name1} 님이 팔로우함", + "account.familiar_followers_two": "{name1}, {name2} 님이 팔로우함", "account.featured": "추천", "account.featured.accounts": "프로필", "account.featured.hashtags": "해시태그", @@ -406,8 +409,10 @@ "hashtag.counter_by_accounts": "{count, plural, other {참여자 {counter}명}}", "hashtag.counter_by_uses": "{count, plural, other {게시물 {counter}개}}", "hashtag.counter_by_uses_today": "오늘 {count, plural, other {{counter} 개의 게시물}}", + "hashtag.feature": "프로필에 추천하기", "hashtag.follow": "해시태그 팔로우", "hashtag.mute": "#{hashtag} 뮤트", + "hashtag.unfeature": "프로필에 추천하지 않기", "hashtag.unfollow": "해시태그 팔로우 해제", "hashtags.and_other": "…및 {count, plural,other {#개}}", "hints.profiles.followers_may_be_missing": "이 프로필의 팔로워 목록은 일부 누락되었을 수 있습니다.", @@ -858,6 +863,11 @@ "status.mute_conversation": "대화 뮤트", "status.open": "상세 정보 표시", "status.pin": "고정", + "status.quote_error.not_found": "이 게시물은 표시할 수 없습니다.", + "status.quote_error.pending_approval": "이 게시물은 원작자의 승인을 기다리고 있습니다.", + "status.quote_error.rejected": "이 게시물은 원작자가 인용을 허용하지 않았기 때문에 표시할 수 없습니다.", + "status.quote_error.removed": "이 게시물은 작성자에 의해 삭제되었습니다.", + "status.quote_error.unauthorized": "이 게시물은 권한이 없기 때문에 볼 수 없습니다.", "status.read_more": "더 보기", "status.reblog": "부스트", "status.reblog_private": "원래의 수신자들에게 부스트", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index 7f5393a66c..bf6fd15de3 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -41,6 +41,7 @@ "account.followers": "Sekotāji", "account.followers.empty": "Šim lietotājam vēl nav sekotāju.", "account.followers_counter": "{count, plural, zero {{count} sekotāju} one {{count} sekotājs} other {{count} sekotāji}}", + "account.followers_you_know_counter": "{counter} jūs pazīstiet", "account.following": "Seko", "account.following_counter": "{count, plural, one {seko {counter}} other {seko {counter}}}", "account.follows.empty": "Šis lietotājs pagaidām nevienam neseko.", @@ -75,6 +76,7 @@ "account.statuses_counter": "{count, plural, zero {{counter} ierakstu} one {{counter} ieraksts} other {{counter} ieraksti}}", "account.unblock": "Atbloķēt @{name}", "account.unblock_domain": "Atbloķēt domēnu {domain}", + "account.unblock_domain_short": "Atbloķēt", "account.unblock_short": "Atbloķēt", "account.unendorse": "Neizcelt profilā", "account.unfollow": "Pārstāt sekot", @@ -367,6 +369,8 @@ "generic.saved": "Saglabāts", "getting_started.heading": "Darba sākšana", "hashtag.admin_moderation": "Atvērt #{name} satura pārraudzības saskarni", + "hashtag.browse": "Pārlūkot #{hashtag} ierakstus", + "hashtag.browse_from_account": "Pārlūkot @{name} #{hashtag} ierakstus", "hashtag.column_header.tag_mode.all": "un {additional}", "hashtag.column_header.tag_mode.any": "vai {additional}", "hashtag.column_header.tag_mode.none": "bez {additional}", @@ -381,6 +385,7 @@ "hashtag.counter_by_uses_today": "{count, plural, zero {{counter} ierakstu} one {{counter} ieraksts} other {{counter} ieraksti}} šodien", "hashtag.feature": "Attēlot profilā", "hashtag.follow": "Sekot tēmturim", + "hashtag.mute": "Apklusināt #{hashtag}", "hashtag.unfeature": "Neattēlot profilā", "hashtag.unfollow": "Pārstāt sekot tēmturim", "hashtags.and_other": "… un {count, plural, other {vēl #}}", @@ -448,6 +453,7 @@ "keyboard_shortcuts.toggle_hidden": "Rādīt/slēpt tekstu aiz satura brīdinājuma", "keyboard_shortcuts.toggle_sensitivity": "Rādīt/slēpt multividi", "keyboard_shortcuts.toot": "Uzsākt jaunu ierakstu", + "keyboard_shortcuts.translate": "tulkot ierakstu", "keyboard_shortcuts.unfocus": "Atfokusēt veidojamā teksta/meklēšanas lauku", "keyboard_shortcuts.up": "Pārvietoties augšup sarakstā", "lightbox.close": "Aizvērt", @@ -515,6 +521,7 @@ "notification.favourite": "{name} pievienoja izlasei Tavu ierakstu", "notification.follow": "{name} uzsāka Tev sekot", "notification.follow_request": "{name} nosūtīja Tev sekošanas pieprasījumu", + "notification.mentioned_you": "{name} pieminēja jūs", "notification.moderation-warning.learn_more": "Uzzināt vairāk", "notification.moderation_warning": "Ir saņemts satura pārraudzības brīdinājums", "notification.moderation_warning.action_delete_statuses": "Daži no Taviem ierakstiem tika noņemti.", @@ -741,6 +748,12 @@ "status.mute_conversation": "Apklusināt sarunu", "status.open": "Izvērst šo ierakstu", "status.pin": "Attēlot profilā", + "status.quote_error.not_found": "Šo ierakstu nevar parādīt.", + "status.quote_error.pending_approval": "Šis ieraksts gaida apstiprinājumu no tā autora.", + "status.quote_error.rejected": "Šo ierakstu nevar parādīt, jo tā autors neļauj to citēt.", + "status.quote_error.removed": "Šo ierakstu noņēma tā autors.", + "status.quote_error.unauthorized": "Šo ierakstu nevar parādīt, jo jums nav atļaujas to skatīt.", + "status.quote_post_author": "Publicēja {name}", "status.read_more": "Lasīt vairāk", "status.reblog": "Pastiprināt", "status.reblog_private": "Pastiprināt ar sākotnējo redzamību", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 404b0e0395..b9bc523fe7 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -42,6 +42,7 @@ "account.followers": "Volgers", "account.followers.empty": "Deze gebruiker heeft nog geen volgers of heeft deze verborgen.", "account.followers_counter": "{count, plural, one {{counter} volger} other {{counter} volgers}}", + "account.followers_you_know_counter": "{counter} die je kent", "account.following": "Volgend", "account.following_counter": "{count, plural, one {{counter} volgend} other {{counter} volgend}}", "account.follows.empty": "Deze gebruiker volgt nog niemand of heeft deze verborgen.", @@ -863,6 +864,12 @@ "status.mute_conversation": "Gesprek negeren", "status.open": "Volledig bericht tonen", "status.pin": "Op profiel uitlichten", + "status.quote_error.not_found": "Dit bericht kan niet worden weergegeven.", + "status.quote_error.pending_approval": "Dit bericht is in afwachting van goedkeuring door de oorspronkelijke auteur.", + "status.quote_error.rejected": "Dit bericht kan niet worden weergegeven omdat de oorspronkelijke auteur niet toestaat dat het wordt geciteerd.", + "status.quote_error.removed": "Dit bericht is verwijderd door de auteur.", + "status.quote_error.unauthorized": "Dit bericht kan niet worden weergegeven omdat je niet bevoegd bent om het te bekijken.", + "status.quote_post_author": "Bericht van {name}", "status.read_more": "Meer lezen", "status.reblog": "Boosten", "status.reblog_private": "Boost naar oorspronkelijke ontvangers", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index 88fd9bf427..4eb45a1a80 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -42,6 +42,7 @@ "account.followers": "Seguidores", "account.followers.empty": "Ainda ninguém segue este utilizador.", "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}", + "account.followers_you_know_counter": "{counter} que conhece", "account.following": "A seguir", "account.following_counter": "{count, plural, one {A seguir {counter}} other {A seguir {counter}}}", "account.follows.empty": "Este utilizador ainda não segue ninguém.", @@ -863,6 +864,12 @@ "status.mute_conversation": "Ocultar conversa", "status.open": "Expandir esta publicação", "status.pin": "Destacar no perfil", + "status.quote_error.not_found": "Esta publicação não pode ser exibida.", + "status.quote_error.pending_approval": "Esta publicação está a aguardar a aprovação do autor original.", + "status.quote_error.rejected": "Esta publicação não pode ser exibida porque o autor original não permite que seja citada.", + "status.quote_error.removed": "Esta publicação foi removida pelo seu autor.", + "status.quote_error.unauthorized": "Esta publicação não pode ser exibida porque o utilizador não está autorizado a visualizá-la.", + "status.quote_post_author": "Publicação de {name}", "status.read_more": "Ler mais", "status.reblog": "Impulsionar", "status.reblog_private": "Impulsionar com a visibilidade original", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index a82eab47d5..30415b247d 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -86,10 +86,10 @@ "admin.dashboard.retention.average": "В среднем", "admin.dashboard.retention.cohort": "Месяц регистрации", "admin.dashboard.retention.cohort_size": "Новые пользователи", - "admin.impact_report.instance_accounts": "Профили учетных записей, которые будут удалены", - "admin.impact_report.instance_followers": "Подписчики, которых потеряют наши пользователи", - "admin.impact_report.instance_follows": "Подписчики, которых потеряют их пользователи", - "admin.impact_report.title": "Резюме воздействия", + "admin.impact_report.instance_accounts": "Число профилей, которые будут удалены", + "admin.impact_report.instance_followers": "Число подписчиков, которых лишатся наши пользователи", + "admin.impact_report.instance_follows": "Число подписчиков, которых лишатся их пользователи", + "admin.impact_report.title": "Сводка последствий", "alert.rate_limited.message": "Подождите до {retry_time, time, medium}, прежде чем делать что-либо ещё.", "alert.rate_limited.title": "Слишком много запросов", "alert.unexpected.message": "Произошла непредвиденная ошибка.", @@ -305,8 +305,8 @@ "emoji_button.search_results": "Результаты поиска", "emoji_button.symbols": "Символы", "emoji_button.travel": "Путешествия и места", - "empty_column.account_featured.me": "Вы ещё ничего не закрепили в своём профиле. Знаете ли вы, что вы можете рекомендовать в этом разделе свои посты, часто используемые вами хэштеги и даже профили друзей?", - "empty_column.account_featured.other": "{acct} ещё ничего не закрепил(а) в своём профиле. Знаете ли вы, что вы можете рекомендовать в этом разделе свои посты, часто используемые вами хэштеги и даже профили друзей?", + "empty_column.account_featured.me": "Вы ещё ничего не закрепили в своём профиле. Знаете ли вы, что вы можете рекомендовать в этом разделе свои посты, часто используемые вами хештеги и даже профили друзей?", + "empty_column.account_featured.other": "{acct} ещё ничего не закрепил(а) в своём профиле. Знаете ли вы, что вы можете рекомендовать в этом разделе свои посты, часто используемые вами хештеги и даже профили друзей?", "empty_column.account_featured_other.unknown": "Этот пользователь ещё ничего не закрепил в своём профиле.", "empty_column.account_hides_collections": "Пользователь предпочёл не раскрывать эту информацию", "empty_column.account_suspended": "Учётная запись заблокирована", @@ -357,7 +357,7 @@ "filter_modal.select_filter.title": "Фильтровать этот пост", "filter_modal.title.status": "Фильтровать пост", "filter_warning.matches_filter": "Соответствует фильтру «{title}»", - "filtered_notifications_banner.pending_requests": "От {count, plural, =0 {не известных вам людей} one {# возможно вам известного человека} other {# возможно вам известных человек}}", + "filtered_notifications_banner.pending_requests": "От {count, plural, =0 {не знакомых вам людей} one {# человека, которого вы можете знать} other {# человек, которых вы можете знать}}", "filtered_notifications_banner.title": "Отфильтрованные уведомления", "firehose.all": "Всё вместе", "firehose.local": "Этот сервер", @@ -392,8 +392,8 @@ "generic.saved": "Сохранено", "getting_started.heading": "Добро пожаловать", "hashtag.admin_moderation": "Открыть интерфейс модератора для #{name}", - "hashtag.browse": "Обзор постов с хэштегом #{hashtag}", - "hashtag.browse_from_account": "Обзор постов от @{name} с хэштегом #{hashtag}", + "hashtag.browse": "Обзор постов с хештегом #{hashtag}", + "hashtag.browse_from_account": "Обзор постов от @{name} с хештегом #{hashtag}", "hashtag.column_header.tag_mode.all": "и {additional}", "hashtag.column_header.tag_mode.any": "или {additional}", "hashtag.column_header.tag_mode.none": "без {additional}", @@ -454,7 +454,7 @@ "interaction_modal.title.reblog": "Продвинуть пост {name}", "interaction_modal.title.reply": "Ответить на пост {name}", "interaction_modal.title.vote": "Голосовать в опросе {name}", - "interaction_modal.username_prompt": "Например {example}", + "interaction_modal.username_prompt": "Например, {example}", "intervals.full.days": "{number, plural, one {# день} few {# дня} other {# дней}}", "intervals.full.hours": "{number, plural, one {# час} few {# часа} other {# часов}}", "intervals.full.minutes": "{number, plural, one {# минута} few {# минуты} other {# минут}}", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index af3fd9761f..fb99b87fc3 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -42,6 +42,7 @@ "account.followers": "Ndjekës", "account.followers.empty": "Këtë përdorues ende s’e ndjek kush.", "account.followers_counter": "{count, plural, one {{counter} ndjekës} other {{counter} ndjekës}}", + "account.followers_you_know_counter": "{counter} që njihni", "account.following": "Ndjekje", "account.following_counter": "{count, plural, one {{counter} i ndjekur} other {{counter} të ndjekur}}", "account.follows.empty": "Ky përdorues ende s’ndjek kënd.", @@ -858,6 +859,12 @@ "status.mute_conversation": "Heshtoje bisedën", "status.open": "Zgjeroje këtë mesazh", "status.pin": "Pasqyrojeni në profil", + "status.quote_error.not_found": "Ky postim s’mund të shfaqet.", + "status.quote_error.pending_approval": "Ky postim është në pritje të miratimit nga autori origjinal.", + "status.quote_error.rejected": "Ky postim s’mund të shfaqet, ngaqë autori origjinal nuk lejon citim të tij.", + "status.quote_error.removed": "Ky postim u hoq nga autori i tij.", + "status.quote_error.unauthorized": "Ky postim s’mund të shfaqet, ngaqë s’jeni i autorizuar ta shihni.", + "status.quote_post_author": "Postim nga {name}", "status.read_more": "Lexoni më tepër", "status.reblog": "Përforcojeni", "status.reblog_private": "Përforcim për publikun origjinal", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 566b7ddff0..6f2e22fc6e 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -42,6 +42,7 @@ "account.followers": "Takipçi", "account.followers.empty": "Henüz kimse bu kullanıcıyı takip etmiyor.", "account.followers_counter": "{count, plural, one {{counter} takipçi} other {{counter} takipçi}}", + "account.followers_you_know_counter": "bildiğiniz {counter}", "account.following": "Takip Ediliyor", "account.following_counter": "{count, plural, one {{counter} takip edilen} other {{counter} takip edilen}}", "account.follows.empty": "Bu kullanıcı henüz kimseyi takip etmiyor.", @@ -863,6 +864,12 @@ "status.mute_conversation": "Sohbeti sessize al", "status.open": "Bu gönderiyi genişlet", "status.pin": "Profilimde öne çıkar", + "status.quote_error.not_found": "Bu gönderi görüntülenemez.", + "status.quote_error.pending_approval": "Bu gönderi özgün yazarın onayını bekliyor.", + "status.quote_error.rejected": "Bu gönderi, özgün yazar alıntılanmasına izin vermediği için görüntülenemez.", + "status.quote_error.removed": "Bu gönderi yazarı tarafından kaldırıldı.", + "status.quote_error.unauthorized": "Bu gönderiyi, yetkiniz olmadığı için görüntüleyemiyorsunuz.", + "status.quote_post_author": "{name} gönderisi", "status.read_more": "Devamını okuyun", "status.reblog": "Yeniden paylaş", "status.reblog_private": "Özgün görünürlük ile yeniden paylaş", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index f006b4015a..18b0e36068 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -843,6 +843,7 @@ "status.mute": "Приховати @{name}", "status.mute_conversation": "Ігнорувати розмову", "status.open": "Розгорнути допис", + "status.quote_post_author": "@{name} опублікував допис", "status.read_more": "Дізнатися більше", "status.reblog": "Поширити", "status.reblog_private": "Поширити для початкової аудиторії", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 14c64c92b8..248b2fcdf8 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -42,6 +42,7 @@ "account.followers": "跟隨者", "account.followers.empty": "尚未有人跟隨這位使用者。", "account.followers_counter": "被 {count, plural, other {{count} 人}}跟隨", + "account.followers_you_know_counter": "{counter} 位您知道的跟隨者", "account.following": "跟隨中", "account.following_counter": "正在跟隨 {count,plural,other {{count} 人}}", "account.follows.empty": "這位使用者尚未跟隨任何人。", @@ -863,6 +864,12 @@ "status.mute_conversation": "靜音對話", "status.open": "展開此嘟文", "status.pin": "於個人檔案推薦", + "status.quote_error.not_found": "這則嘟文無法被顯示。", + "status.quote_error.pending_approval": "此嘟文正在等待原作者審核。", + "status.quote_error.rejected": "由於原作者不允許引用,此嘟文無法被顯示。", + "status.quote_error.removed": "此嘟文已被其作者移除。", + "status.quote_error.unauthorized": "由於您未被授權檢視,此嘟文無法被顯示。", + "status.quote_post_author": "由 {name} 發嘟", "status.read_more": "閱讀更多", "status.reblog": "轉嘟", "status.reblog_private": "依照原嘟可見性轉嘟", diff --git a/app/javascript/material-icons/400-24px/article-fill.svg b/app/javascript/material-icons/400-24px/article-fill.svg new file mode 100644 index 0000000000..5ea367df92 --- /dev/null +++ b/app/javascript/material-icons/400-24px/article-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/article.svg b/app/javascript/material-icons/400-24px/article.svg new file mode 100644 index 0000000000..1265c26dad --- /dev/null +++ b/app/javascript/material-icons/400-24px/article.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/README.md b/app/javascript/material-icons/README.md index 1479cb2255..c583d5ee2c 100644 --- a/app/javascript/material-icons/README.md +++ b/app/javascript/material-icons/README.md @@ -1 +1,12 @@ -Files in this directory are Material Symbols icons fetched using the `icons:download` task. +Files in this directory are Material Symbols icons fetched using the `icons:download` rake task (see `/lib/tasks/icons.rake`). + +To add another icon, follow these steps: + +- Determine the name of the Material Symbols icon you want to download. + You can find a searchable overview of all icons on [https://fonts.google.com/icons]. + Click on the icon you want to use and find the icon name towards the bottom of the slide-out panel (it'll be something like `icon_name`) +- Import the icon in your React component using the following format: + `import IconName from '@/material-icons/400-24px/icon_name.svg?react';` +- Run `RAILS_ENV=development rails icons:download` to download any newly imported icons. + +The import should now work and the icon should appear when passed to the ` component diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index f9f70c4da2..40a68a79cf 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1421,6 +1421,10 @@ body > [data-popper-placement] { } } +.status--has-quote .quote-inline { + display: none; +} + .status { padding: 16px; min-height: 54px; @@ -1491,8 +1495,12 @@ body > [data-popper-placement] { } } + &--is-quote { + border: none; + } + &--in-thread { - $thread-margin: 46px + 10px; + --thread-margin: calc(46px + 8px); border-bottom: 0; @@ -1508,16 +1516,16 @@ body > [data-popper-placement] { .hashtag-bar, .content-warning, .filter-warning { - margin-inline-start: $thread-margin; - width: calc(100% - $thread-margin); + margin-inline-start: var(--thread-margin); + width: calc(100% - var(--thread-margin)); } .more-from-author { - width: calc(100% - $thread-margin + 2px); + width: calc(100% - var(--thread-margin) + 2px); } .status__content__read-more-button { - margin-inline-start: $thread-margin; + margin-inline-start: var(--thread-margin); } } @@ -1873,6 +1881,81 @@ body > [data-popper-placement] { } } +.status__quote { + position: relative; + margin-block-start: 16px; + margin-inline-start: 36px; + border-radius: 8px; + color: var(--nested-card-text); + background: var(--nested-card-background); + border: var(--nested-card-border); + + @media screen and (min-width: $mobile-breakpoint) { + margin-inline-start: 56px; + } +} + +.status__quote--error { + display: flex; + align-items: center; + gap: 8px; + padding: 12px; + font-size: 15px; +} + +.status__quote-author-button { + position: relative; + overflow: hidden; + display: inline-flex; + width: auto; + margin-block-start: 10px; + padding: 5px 12px; + align-items: center; + gap: 6px; + font-family: inherit; + font-size: 14px; + font-weight: 700; + line-height: normal; + letter-spacing: 0; + text-decoration: none; + color: $highlight-text-color; + background: var(--nested-card-background); + border: var(--nested-card-border); + border-radius: 4px; + + &:active, + &:focus, + &:hover { + border-color: lighten($highlight-text-color, 4%); + color: lighten($highlight-text-color, 4%); + } + + &:focus-visible { + outline: $ui-button-icon-focus-outline; + } +} + +.status__quote-icon { + position: absolute; + inset-block-start: 18px; + inset-inline-start: -40px; + display: block; + width: 26px; + height: 26px; + padding: 5px; + color: #6a49ba; + z-index: 10; + + .status__quote--error & { + inset-block-start: 50%; + transform: translateY(-50%); + } + + @media screen and (min-width: $mobile-breakpoint) { + inset-inline-start: -50px; + } +} + .detailed-status__link { display: inline-flex; align-items: center; @@ -2170,14 +2253,18 @@ a .account__avatar { .avatar-group { display: flex; - gap: 8px; - flex-wrap: wrap; + + --avatar-height: 28px; + + &:not(.avatar-group--compact) { + gap: 8px; + flex-wrap: wrap; + height: var(--avatar-height); + overflow-y: clip; + } } .avatar-group--compact { - gap: 0; - flex-wrap: nowrap; - & > :not(:first-child) { margin-inline-start: -12px; } @@ -2306,11 +2393,6 @@ a.account__display-name { } } -.status__avatar { - width: 46px; - height: 46px; -} - .muted { .status__content, .status__content p, @@ -10334,7 +10416,8 @@ noscript { padding: 15px; display: flex; align-items: center; - gap: 8px; + flex-wrap: wrap; + gap: 4px 8px; .logo { width: 16px; @@ -10428,12 +10511,6 @@ noscript { overflow: hidden; container-type: inline-size; - @container (width < 350px) { - &__header time { - display: none; - } - } - &__header { display: flex; flex-direction: column; @@ -10446,7 +10523,8 @@ noscript { &__label { display: flex; - gap: 8px; + flex-wrap: wrap; + gap: 2px 8px; font-size: 15px; line-height: 22px; color: $darker-text-color; @@ -10464,6 +10542,13 @@ noscript { time { color: $dark-text-color; } + + @container (width < 350px) { + time, + &-separator { + display: none; + } + } } } @@ -10515,6 +10600,7 @@ noscript { line-height: 22px; color: $darker-text-color; -webkit-line-clamp: 4; + line-clamp: 4; -webkit-box-orient: vertical; max-height: none; overflow: hidden; @@ -10670,7 +10756,15 @@ noscript { color: inherit; } - &__number { + &__numbers, + &__familiar-followers { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 10px; + } + + &__numbers { font-size: 15px; line-height: 22px; color: $secondary-text-color; @@ -10818,9 +10912,9 @@ noscript { .content-warning { display: block; box-sizing: border-box; - background: rgba($ui-highlight-color, 0.05); - color: $secondary-text-color; - border: 1px solid rgba($ui-highlight-color, 0.15); + background: var(--nested-card-background); + color: var(--nested-card-text); + border: var(--nested-card-border); border-radius: 8px; padding: 8px (5px + 8px); position: relative; diff --git a/app/javascript/styles/mastodon/css_variables.scss b/app/javascript/styles/mastodon/css_variables.scss index 782e08e283..413efca3f6 100644 --- a/app/javascript/styles/mastodon/css_variables.scss +++ b/app/javascript/styles/mastodon/css_variables.scss @@ -27,6 +27,10 @@ --rich-text-container-color: rgba(87, 24, 60, 100%); --rich-text-text-color: rgba(255, 175, 212, 100%); --rich-text-decorations-color: rgba(128, 58, 95, 100%); + --nested-card-background: color(from #{$ui-highlight-color} srgb r g b / 5%); + --nested-card-text: #{$secondary-text-color}; + --nested-card-border: 1px solid + color(from #{$ui-highlight-color} srgb r g b / 15%); --input-placeholder-color: #{$dark-text-color}; --input-background-color: var(--surface-variant-background-color); --on-input-color: #{$secondary-text-color}; diff --git a/app/lib/fasp/request.rb b/app/lib/fasp/request.rb index 7d8c05d406..6ea837b89c 100644 --- a/app/lib/fasp/request.rb +++ b/app/lib/fasp/request.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Fasp::Request + COVERED_COMPONENTS = %w(@method @target-uri content-digest).freeze + def initialize(provider) @provider = provider end @@ -23,55 +25,36 @@ class Fasp::Request url = @provider.url(path) body = body.present? ? body.to_json : '' headers = request_headers(verb, url, body) - response = HTTP.headers(headers).send(verb, url, body:) + key = Linzer.new_ed25519_key(@provider.server_private_key_pem, @provider.remote_identifier) + response = HTTP + .headers(headers) + .use(http_signature: { key:, covered_components: COVERED_COMPONENTS }) + .send(verb, url, body:) + validate!(response) response.parse if response.body.present? end - def request_headers(verb, url, body = '') - result = { + def request_headers(_verb, _url, body = '') + { 'accept' => 'application/json', 'content-type' => 'application/json', 'content-digest' => content_digest(body), } - result.merge(signature_headers(verb, url, result)) end def content_digest(body) "sha-256=:#{OpenSSL::Digest.base64digest('sha256', body || '')}:" end - def signature_headers(verb, url, headers) - linzer_request = Linzer.new_request(verb, url, {}, headers) - message = Linzer::Message.new(linzer_request) - key = Linzer.new_ed25519_key(@provider.server_private_key_pem, @provider.remote_identifier) - signature = Linzer.sign(key, message, %w(@method @target-uri content-digest)) - Linzer::Signer.send(:populate_parameters, key, {}) - - signature.to_h - end - def validate!(response) content_digest_header = response.headers['content-digest'] raise Mastodon::SignatureVerificationError, 'content-digest missing' if content_digest_header.blank? raise Mastodon::SignatureVerificationError, 'content-digest does not match' if content_digest_header != content_digest(response.body) + raise Mastodon::SignatureVerificationError, 'signature-input is missing' if response.headers['signature-input'].blank? - signature_input = response.headers['signature-input']&.encode('UTF-8') - raise Mastodon::SignatureVerificationError, 'signature-input is missing' if signature_input.blank? - - linzer_response = Linzer.new_response( - response.body, - response.status, - { - 'content-digest' => content_digest_header, - 'signature-input' => signature_input, - 'signature' => response.headers['signature'], - } - ) - message = Linzer::Message.new(linzer_response) key = Linzer.new_ed25519_public_key(@provider.provider_public_key_pem) - signature = Linzer::Signature.build(message.headers) - Linzer.verify(key, message, signature) + Linzer.verify!(response, key:) end end diff --git a/app/models/rule.rb b/app/models/rule.rb index 8f36f11abb..c7b532fe5d 100644 --- a/app/models/rule.rb +++ b/app/models/rule.rb @@ -42,6 +42,6 @@ class Rule < ApplicationRecord def translation_for(locale) @cached_translations ||= {} - @cached_translations[locale] ||= translations.find_by(language: locale) || RuleTranslation.new(language: locale, text: text, hint: hint) + @cached_translations[locale] ||= translations.where(language: [locale, locale.to_s.split('-').first]).order('length(language) desc').first || RuleTranslation.new(language: locale, text: text, hint: hint) end end diff --git a/app/models/user.rb b/app/models/user.rb index 7735c0d04d..0ddf1bd799 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -73,7 +73,6 @@ class User < ApplicationRecord ACTIVE_DURATION = ENV.fetch('USER_ACTIVE_DAYS', 7).to_i.days.freeze devise :two_factor_authenticatable, - otp_secret_encryption_key: Rails.configuration.x.otp_secret, otp_secret_length: 32 devise :two_factor_backupable, diff --git a/app/services/activitypub/fetch_featured_collection_service.rb b/app/services/activitypub/fetch_featured_collection_service.rb index 25c62f3be6..e1fa560a8a 100644 --- a/app/services/activitypub/fetch_featured_collection_service.rb +++ b/app/services/activitypub/fetch_featured_collection_service.rb @@ -4,13 +4,11 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService include JsonLdHelper def call(account, **options) - return if account.featured_collection_url.blank? || account.suspended? || account.local? + return if (account.featured_collection_url.blank? && options[:collection].blank?) || account.suspended? || account.local? @account = account @options = options - @json = fetch_resource(@account.featured_collection_url, true, local_follower) - - return unless supported_context?(@json) + @json = fetch_collection(options[:collection].presence || @account.featured_collection_url) process_items(collection_items(@json)) end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index e5c2319728..201f7513b9 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -57,7 +57,7 @@ class ActivityPub::ProcessAccountService < BaseService after_suspension_change! if suspension_changed? unless @options[:only_key] || @account.suspended? - check_featured_collection! if @account.featured_collection_url.present? + check_featured_collection! if @json['featured'].present? check_featured_tags_collection! if @json['featuredTags'].present? check_links! if @account.fields.any?(&:requires_verification?) end @@ -121,7 +121,7 @@ class ActivityPub::ProcessAccountService < BaseService end def set_immediate_attributes! - @account.featured_collection_url = @json['featured'] || '' + @account.featured_collection_url = valid_collection_uri(@json['featured']) @account.display_name = @json['name'] || '' @account.note = @json['summary'] || '' @account.locked = @json['manuallyApprovesFollowers'] || false @@ -186,7 +186,7 @@ class ActivityPub::ProcessAccountService < BaseService end def check_featured_collection! - ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id, { 'hashtag' => @json['featuredTags'].blank?, 'request_id' => @options[:request_id] }) + ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id, { 'hashtag' => @json['featuredTags'].blank?, 'collection' => @json['featured'], 'request_id' => @options[:request_id] }) end def check_featured_tags_collection! diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 4141fb43df..84c4ba06f1 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -19,7 +19,7 @@ class FetchLinkCardService < BaseService @status = status @original_url = parse_urls - return if @original_url.nil? || @status.with_preview_card? + return if @original_url.nil? || @status.with_preview_card? || @status.with_media? || @status.quote.present? @url = @original_url.to_s diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 57069d2dc6..d0dabc298a 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -28,6 +28,7 @@ = theme_style_tags current_theme = vite_client_tag = vite_react_refresh_tag + = vite_polyfills_tag -# Needed for the wicg-inert polyfill. It needs to be on it's own