diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 68ed0ac6c8..0cc9c8d8fc 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.79.2. +# using RuboCop version 1.80.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index f25d0547e8..fcba923030 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -12,13 +12,14 @@ import { initialize, mswLoader } from 'msw-storybook-addon'; import { action } from 'storybook/actions'; import type { LocaleData } from '@/mastodon/locales'; -import { reducerWithInitialState, rootReducer } from '@/mastodon/reducers'; +import { reducerWithInitialState } from '@/mastodon/reducers'; import { defaultMiddleware } from '@/mastodon/store/store'; import { mockHandlers, unhandledRequestHandler } from '@/testing/api'; // If you want to run the dark theme during development, // you can change the below to `/application.scss` import '../app/javascript/styles/mastodon-light.scss'; +import './styles.css'; const localeFiles = import.meta.glob('@/mastodon/locales/*.json', { query: { as: 'json' }, @@ -49,12 +50,17 @@ const preview: Preview = { locale: 'en', }, decorators: [ - (Story, { parameters }) => { + (Story, { parameters, globals }) => { + const { locale } = globals as { locale: string }; const { state = {} } = parameters; - let reducer = rootReducer; - if (typeof state === 'object' && state) { - reducer = reducerWithInitialState(state as Record); - } + const reducer = reducerWithInitialState( + { + meta: { + locale, + }, + }, + state as Record, + ); const store = configureStore({ reducer, middleware(getDefaultMiddleware) { diff --git a/.storybook/styles.css b/.storybook/styles.css new file mode 100644 index 0000000000..ac29890895 --- /dev/null +++ b/.storybook/styles.css @@ -0,0 +1,8 @@ +a { + color: inherit; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} diff --git a/Gemfile.lock b/Gemfile.lock index 93ed9eb601..0f6f85585c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -771,7 +771,7 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 9) rspec-support (3.13.4) - rubocop (1.79.2) + rubocop (1.80.2) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -801,7 +801,7 @@ GEM rack (>= 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.44.0, < 2.0) - rubocop-rspec (3.6.0) + rubocop-rspec (3.7.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) rubocop-rspec_rails (2.31.0) @@ -903,7 +903,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.9.1) - unicode-display_width (3.1.4) + unicode-display_width (3.1.5) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) uri (1.0.3) diff --git a/app/controllers/admin/export_domain_allows_controller.rb b/app/controllers/admin/export_domain_allows_controller.rb index ca88c6525e..d1a2ea5bbf 100644 --- a/app/controllers/admin/export_domain_allows_controller.rb +++ b/app/controllers/admin/export_domain_allows_controller.rb @@ -49,8 +49,8 @@ module Admin def export_data CSV.generate(headers: export_headers, write_headers: true) do |content| - DomainAllow.allowed_domains.each do |instance| - content << [instance.domain] + DomainAllow.allowed_domains.each do |domain| + content << [domain] end end end diff --git a/app/javascript/mastodon/components/alt_text_badge.tsx b/app/javascript/mastodon/components/alt_text_badge.tsx index 07369795ac..c7fb0cd81b 100644 --- a/app/javascript/mastodon/components/alt_text_badge.tsx +++ b/app/javascript/mastodon/components/alt_text_badge.tsx @@ -13,9 +13,9 @@ import { useSelectableClick } from 'mastodon/hooks/useSelectableClick'; const offset = [0, 4] as OffsetValue; const popperConfig = { strategy: 'fixed' } as UsePopperOptions; -export const AltTextBadge: React.FC<{ - description: string; -}> = ({ description }) => { +export const AltTextBadge: React.FC<{ description: string }> = ({ + description, +}) => { const accessibilityId = useId(); const anchorRef = useRef(null); const [open, setOpen] = useState(false); @@ -56,7 +56,7 @@ export const AltTextBadge: React.FC<{ {({ props }) => (
, 'children'> { block?: boolean; secondary?: boolean; + plain?: boolean; compact?: boolean; dangerous?: boolean; loading?: boolean; @@ -35,6 +36,7 @@ export const Button: React.FC = ({ disabled, block, secondary, + plain, compact, dangerous, loading, @@ -62,6 +64,7 @@ export const Button: React.FC = ({
diff --git a/app/javascript/mastodon/components/display_name/display_name.stories.tsx b/app/javascript/mastodon/components/display_name/display_name.stories.tsx new file mode 100644 index 0000000000..ccd7dcbb91 --- /dev/null +++ b/app/javascript/mastodon/components/display_name/display_name.stories.tsx @@ -0,0 +1,81 @@ +import type { ComponentProps } from 'react'; + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { accountFactoryState } from '@/testing/factories'; + +import { DisplayName, LinkedDisplayName } from './index'; + +type PageProps = Omit, 'account'> & { + name: string; + username: string; + loading: boolean; +}; + +const meta = { + title: 'Components/DisplayName', + args: { + username: 'mastodon@mastodon.social', + name: 'Test User 🧪', + loading: false, + simple: false, + noDomain: false, + localDomain: 'mastodon.social', + }, + tags: [], + render({ name, username, loading, ...args }) { + const account = !loading + ? accountFactoryState({ + display_name: name, + acct: username, + }) + : undefined; + return ; + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Primary: Story = { + args: {}, +}; + +export const Loading: Story = { + args: { + loading: true, + }, +}; + +export const NoDomain: Story = { + args: { + noDomain: true, + }, +}; + +export const Simple: Story = { + args: { + simple: true, + }, +}; + +export const LocalUser: Story = { + args: { + username: 'localuser', + name: 'Local User', + localDomain: '', + }, +}; + +export const Linked: Story = { + render({ name, username, loading, ...args }) { + const account = !loading + ? accountFactoryState({ + display_name: name, + acct: username, + }) + : undefined; + return ; + }, +}; diff --git a/app/javascript/mastodon/components/display_name/index.tsx b/app/javascript/mastodon/components/display_name/index.tsx new file mode 100644 index 0000000000..6bd4addded --- /dev/null +++ b/app/javascript/mastodon/components/display_name/index.tsx @@ -0,0 +1,122 @@ +import type { ComponentPropsWithoutRef, FC } from 'react'; +import { useMemo } from 'react'; + +import classNames from 'classnames'; +import type { LinkProps } from 'react-router-dom'; +import { Link } from 'react-router-dom'; + +import { EmojiHTML } from '@/mastodon/features/emoji/emoji_html'; +import type { Account } from '@/mastodon/models/account'; +import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; + +import { Skeleton } from '../skeleton'; + +interface Props { + account?: Account; + localDomain?: string; + simple?: boolean; + noDomain?: boolean; +} + +export const DisplayName: FC> = ({ + account, + localDomain, + simple = false, + noDomain = false, + className, + ...props +}) => { + const username = useMemo(() => { + if (!account || noDomain) { + return null; + } + let acct = account.get('acct'); + + if (!acct.includes('@') && localDomain) { + acct = `${acct}@${localDomain}`; + } + return `@${acct}`; + }, [account, localDomain, noDomain]); + + if (!account) { + if (simple) { + return null; + } + return ( + + + + + + + {!noDomain && ( + +   + + + )} + + ); + } + const accountName = isModernEmojiEnabled() + ? account.get('display_name') + : account.get('display_name_html'); + if (simple) { + return ( + + + + ); + } + + return ( + + + + + {username && ( +  {username} + )} + + ); +}; + +export const LinkedDisplayName: FC< + Props & { asProps?: ComponentPropsWithoutRef<'span'> } & Partial +> = ({ + account, + asProps = {}, + className, + localDomain, + simple, + noDomain, + ...linkProps +}) => { + const displayProps = { + account, + className, + localDomain, + simple, + noDomain, + ...asProps, + }; + if (!account) { + return ; + } + + return ( + + + + ); +}; diff --git a/app/javascript/mastodon/components/status/reblog_button.tsx b/app/javascript/mastodon/components/status/reblog_button.tsx index afc208e032..079ca5d7c8 100644 --- a/app/javascript/mastodon/components/status/reblog_button.tsx +++ b/app/javascript/mastodon/components/status/reblog_button.tsx @@ -69,7 +69,7 @@ const messages = defineMessages({ }, reblog_private: { id: 'status.reblog_private', - defaultMessage: 'Boost with original visibility', + defaultMessage: 'Share again with your followers', }, reblog_cannot: { id: 'status.cannot_reblog', diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar/index.jsx similarity index 93% rename from app/javascript/mastodon/components/status_action_bar.jsx rename to app/javascript/mastodon/components/status_action_bar/index.jsx index 143407193b..0969240610 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar/index.jsx @@ -20,11 +20,12 @@ import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/ import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import { Dropdown } from 'mastodon/components/dropdown_menu'; -import { me } from '../initial_state'; +import { me } from '../../initial_state'; -import { IconButton } from './icon_button'; -import { isFeatureEnabled } from '../utils/environment'; -import { ReblogButton } from './status/reblog_button'; +import { IconButton } from '../icon_button'; +import { isFeatureEnabled } from '../../utils/environment'; +import { ReblogButton } from '../status/reblog_button'; +import { RemoveQuoteHint } from './remove_quote_hint'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -77,6 +78,7 @@ class StatusActionBar extends ImmutablePureComponent { status: ImmutablePropTypes.map.isRequired, relationship: ImmutablePropTypes.record, quotedAccountId: PropTypes.string, + contextType: PropTypes.string, onReply: PropTypes.func, onFavourite: PropTypes.func, onDelete: PropTypes.func, @@ -240,7 +242,7 @@ class StatusActionBar extends ImmutablePureComponent { }; render () { - const { status, relationship, quotedAccountId, intl, withDismiss, withCounters, scrollKey } = this.props; + const { status, relationship, quotedAccountId, contextType, intl, withDismiss, withCounters, scrollKey } = this.props; const { signedIn, permissions } = this.props.identity; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); @@ -249,6 +251,7 @@ class StatusActionBar extends ImmutablePureComponent { const account = status.get('account'); const writtenByMe = status.getIn(['account', 'id']) === me; const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']); + const isQuotingMe = quotedAccountId === me; let menu = []; @@ -293,7 +296,7 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick }); menu.push(null); - if (quotedAccountId === me) { + if (isQuotingMe) { menu.push({ text: intl.formatMessage(messages.revokeQuote, { name: account.get('username') }), action: this.handleRevokeQuoteClick, dangerous: true }); } @@ -360,6 +363,8 @@ class StatusActionBar extends ImmutablePureComponent { const bookmarkTitle = intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark); const favouriteTitle = intl.formatMessage(status.get('favourited') ? messages.removeFavourite : messages.favourite); const isReply = status.get('in_reply_to_account_id') === status.getIn(['account', 'id']); + + const shouldShowQuoteRemovalHint = isQuotingMe && contextType === 'notifications'; return (
@@ -375,17 +380,23 @@ class StatusActionBar extends ImmutablePureComponent {
-
- -
+ + {(dismissQuoteHint) => ( + { + dismissQuoteHint(); + return true; + }} + /> + )} +
); } diff --git a/app/javascript/mastodon/components/status_action_bar/remove_quote_hint.tsx b/app/javascript/mastodon/components/status_action_bar/remove_quote_hint.tsx new file mode 100644 index 0000000000..6046dad035 --- /dev/null +++ b/app/javascript/mastodon/components/status_action_bar/remove_quote_hint.tsx @@ -0,0 +1,90 @@ +import { useRef } from 'react'; + +import { FormattedMessage, useIntl } from 'react-intl'; + +import classNames from 'classnames'; + +import Overlay from 'react-overlays/Overlay'; + +import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; + +import { Button } from '../button'; +import { useDismissableBannerState } from '../dismissable_banner'; +import { Icon } from '../icon'; + +const DISMISSABLE_BANNER_ID = 'notifications/remove_quote_hint'; + +export const RemoveQuoteHint: React.FC<{ + canShowHint: boolean; + className?: string; + children: (dismiss: () => void) => React.ReactNode; +}> = ({ canShowHint, className, children }) => { + const anchorRef = useRef(null); + const intl = useIntl(); + + const { isVisible, dismiss } = useDismissableBannerState({ + id: DISMISSABLE_BANNER_ID, + }); + + return ( +
+ {children(dismiss)} + {isVisible && canShowHint && ( + + {({ props, placement }) => ( +
+

+ +

+ + ), + }} + > + {(text) =>

{text}

} +
+ + {(text) => ( + + )} + +
+ )} +
+ )} +
+ ); +}; diff --git a/app/javascript/mastodon/features/emoji/emoji_html.tsx b/app/javascript/mastodon/features/emoji/emoji_html.tsx index 60a27306dd..0bd1000922 100644 --- a/app/javascript/mastodon/features/emoji/emoji_html.tsx +++ b/app/javascript/mastodon/features/emoji/emoji_html.tsx @@ -1,7 +1,5 @@ import type { ComponentPropsWithoutRef, ElementType } from 'react'; -import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; - import { useEmojify } from './hooks'; import type { CustomEmojiMapArg } from './types'; @@ -12,16 +10,21 @@ type EmojiHTMLProps = Omit< htmlString: string; extraEmojis?: CustomEmojiMapArg; as?: Element; + shallow?: boolean; }; -export const ModernEmojiHTML = ({ +export const EmojiHTML = ({ extraEmojis, htmlString, - as: asElement, // Rename for syntax highlighting + as: Wrapper = 'div', // Rename for syntax highlighting + shallow, ...props -}: EmojiHTMLProps) => { - const Wrapper = asElement ?? 'div'; - const emojifiedHtml = useEmojify(htmlString, extraEmojis); +}: EmojiHTMLProps) => { + const emojifiedHtml = useEmojify({ + text: htmlString, + extraEmojis, + deep: !shallow, + }); if (emojifiedHtml === null) { return null; @@ -31,14 +34,3 @@ export const ModernEmojiHTML = ({ ); }; - -export const EmojiHTML = ( - props: EmojiHTMLProps, -) => { - if (isModernEmojiEnabled()) { - return ; - } - const { as: asElement, htmlString, extraEmojis, ...rest } = props; - const Wrapper = asElement ?? 'div'; - return ; -}; diff --git a/app/javascript/mastodon/features/emoji/hooks.ts b/app/javascript/mastodon/features/emoji/hooks.ts index 3f397f4ef0..7e91486780 100644 --- a/app/javascript/mastodon/features/emoji/hooks.ts +++ b/app/javascript/mastodon/features/emoji/hooks.ts @@ -8,6 +8,7 @@ import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; import { toSupportedLocale } from './locale'; import { determineEmojiMode } from './mode'; +import { emojifyElement, emojifyText } from './render'; import type { CustomEmojiMapArg, EmojiAppState, @@ -15,7 +16,17 @@ import type { } from './types'; import { stringHasAnyEmoji } from './utils'; -export function useEmojify(text: string, extraEmojis?: CustomEmojiMapArg) { +interface UseEmojifyOptions { + text: string; + extraEmojis?: CustomEmojiMapArg; + deep?: boolean; +} + +export function useEmojify({ + text, + extraEmojis, + deep = true, +}: UseEmojifyOptions) { const [emojifiedText, setEmojifiedText] = useState(null); const appState = useEmojiAppState(); @@ -36,17 +47,23 @@ export function useEmojify(text: string, extraEmojis?: CustomEmojiMapArg) { const emojify = useCallback( async (input: string) => { - const wrapper = document.createElement('div'); - wrapper.innerHTML = input; - const { emojifyElement } = await import('./render'); - const result = await emojifyElement(wrapper, appState, extra); + let result: string | null = null; + if (deep) { + const wrapper = document.createElement('div'); + wrapper.innerHTML = input; + if (await emojifyElement(wrapper, appState, extra)) { + result = wrapper.innerHTML; + } + } else { + result = await emojifyText(text, appState, extra); + } if (result) { - setEmojifiedText(result.innerHTML); + setEmojifiedText(result); } else { setEmojifiedText(input); } }, - [appState, extra], + [appState, deep, extra, text], ); useLayoutEffect(() => { if (isModernEmojiEnabled() && !!text.trim() && stringHasAnyEmoji(text)) { diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx b/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx index 919a41cbae..ddcc386ad8 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx @@ -33,7 +33,7 @@ const messages = defineMessages({ reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, reblog_private: { id: 'status.reblog_private', - defaultMessage: 'Boost with original visibility', + defaultMessage: 'Share again with your followers', }, cancel_reblog_private: { id: 'status.cancel_reblog_private', diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 79f7f8c3b2..0583bf99c5 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -92,8 +92,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ layout: state.getIn(['meta', 'layout']), isComposing: state.getIn(['compose', 'is_composing']), - hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0, - hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0, + hasComposingContents: state.getIn(['compose', 'text']).trim().length !== 0 || state.getIn(['compose', 'media_attachments']).size > 0 || state.getIn(['compose', 'poll']) !== null || state.getIn(['compose', 'quoted_status_id']) !== null, canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < state.getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']), firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION, newAccount: !state.getIn(['accounts', me, 'note']) && !state.getIn(['accounts', me, 'bot']) && state.getIn(['accounts', me, 'following_count'], 0) === 0 && state.getIn(['accounts', me, 'statuses_count'], 0) === 0, @@ -241,8 +240,7 @@ class UI extends PureComponent { dispatch: PropTypes.func.isRequired, children: PropTypes.node, isComposing: PropTypes.bool, - hasComposingText: PropTypes.bool, - hasMediaAttachments: PropTypes.bool, + hasComposingContents: PropTypes.bool, canUploadMore: PropTypes.bool, intl: PropTypes.object.isRequired, layout: PropTypes.string.isRequired, @@ -257,11 +255,11 @@ class UI extends PureComponent { }; handleBeforeUnload = e => { - const { intl, dispatch, isComposing, hasComposingText, hasMediaAttachments } = this.props; + const { intl, dispatch, isComposing, hasComposingContents } = this.props; dispatch(synchronouslySubmitMarkers()); - if (isComposing && (hasComposingText || hasMediaAttachments)) { + if (isComposing && hasComposingContents) { e.preventDefault(); // Setting returnValue to any string causes confirmation dialog. // Many browsers no longer display this text to users, diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json index 941712a501..c6fceba7b7 100644 --- a/app/javascript/mastodon/locales/br.json +++ b/app/javascript/mastodon/locales/br.json @@ -29,6 +29,8 @@ "account.endorse": "Lakaat war-wel war ar profil", "account.familiar_followers_one": "Heuilhet gant {name1}", "account.familiar_followers_two": "Heuilhet gant {name1} ha {name2}", + "account.featured.accounts": "Profiloù", + "account.featured.hashtags": "Gerioù-klik", "account.featured_tags.last_status_at": "Toud diwezhañ : {date}", "account.featured_tags.last_status_never": "Embannadur ebet", "account.follow": "Heuliañ", @@ -39,6 +41,7 @@ "account.followers_you_know_counter": "{counter} a anavezit", "account.following": "Koumanantoù", "account.follows.empty": "An implijer·ez-mañ na heul den ebet.", + "account.follows_you": "Ho heuilh", "account.go_to_profile": "Gwelet ar profil", "account.hide_reblogs": "Kuzh skignadennoù gant @{name}", "account.in_memoriam": "E koun.", @@ -89,7 +92,10 @@ "alt_text_modal.done": "Graet", "announcement.announcement": "Kemennad", "annual_report.summary.followers.followers": "heulier", + "annual_report.summary.followers.total": "{count} en holl", "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.summary.most_used_app.most_used_app": "arload muiañ implijet", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "ar gerioù-klik implijet ar muiañ", "annual_report.summary.most_used_hashtag.none": "Hini ebet", "annual_report.summary.new_posts.new_posts": "toudoù nevez", "attachments_list.unprocessed": "(ket meret)", @@ -149,7 +155,7 @@ "compose.saved.body": "Enrollet.", "compose_form.direct_message_warning_learn_more": "Gouzout hiroc'h", "compose_form.encryption_warning": "Toudoù war Mastodon na vezont ket sifret penn-da-benn. Na rannit ket titouroù kizidik dre Mastodon.", - "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.", + "compose_form.hashtag_warning": "Ne vo ket listennet an toud-mañ dindan gerioù-klik ebet dre m'eo anlistennet. N'eus nemet an toudoù foran a c'hall bezañ klasket dre c'her-klik.", "compose_form.lock_disclaimer": "N'eo ket {locked} ho kont. An holl a c'hal ho heuliañ evit gwelet ho toudoù prevez.", "compose_form.lock_disclaimer.lock": "prennet", "compose_form.placeholder": "Petra emaoc'h o soñjal e-barzh ?", @@ -168,6 +174,7 @@ "confirmations.block.confirm": "Stankañ", "confirmations.delete.confirm": "Dilemel", "confirmations.delete.message": "Ha sur oc'h e fell deoc'h dilemel an toud-mañ ?", + "confirmations.delete.title": "Dilemel an toud?", "confirmations.delete_list.confirm": "Dilemel", "confirmations.delete_list.message": "Ha sur eo hoc'h eus c'hoant da zilemel ar roll-mañ da vat ?", "confirmations.delete_list.title": "Dilemel al listenn?", @@ -176,10 +183,14 @@ "confirmations.follow_to_list.title": "Heuliañ an implijer·ez?", "confirmations.logout.confirm": "Digevreañ", "confirmations.logout.message": "Ha sur oc'h e fell deoc'h digevreañ ?", + "confirmations.logout.title": "Digevreañ?", "confirmations.mute.confirm": "Kuzhat", "confirmations.redraft.confirm": "Diverkañ ha skrivañ en-dro", "confirmations.unfollow.confirm": "Diheuliañ", "confirmations.unfollow.message": "Ha sur oc'h e fell deoc'h paouez da heuliañ {name} ?", + "confirmations.unfollow.title": "Paouez da heuliañ an implijer·ez?", + "content_warning.hide": "Kuzhat an embannadur", + "content_warning.show": "Diskwel memes tra", "content_warning.show_more": "Diskouez muioc'h", "conversation.delete": "Dilemel ar gaozeadenn", "conversation.mark_as_read": "Merkañ evel lennet", @@ -224,8 +235,8 @@ "empty_column.domain_blocks": "N'eus domani kuzh ebet c'hoazh.", "empty_column.explore_statuses": "N'eus tuadur ebet evit c'hoazh. Distroit diwezhatoc'h !", "empty_column.follow_requests": "N'ho peus reked heuliañ ebet c'hoazh. Pa vo resevet unan e teuio war wel amañ.", - "empty_column.followed_tags": "N'emaoc'h oc'h heuliañ hashtag ebet evit poent. Pa vioc'h e vo d'o gwelet amañ.", - "empty_column.hashtag": "N'eus netra en hashtag-mañ c'hoazh.", + "empty_column.followed_tags": "N'emaoc'h oc'h heuliañ ger-klik ebet evit poent. Pa vioc'h e vo d'o gwelet amañ.", + "empty_column.hashtag": "N'eus netra er ger-klik-mañ c'hoazh.", "empty_column.home": "Goullo eo ho red-amzer degemer! Kit da weladenniñ {public} pe implijit ar c'hlask evit kregiñ ganti ha kejañ gant implijer·ien·ezed all.", "empty_column.list": "Goullo eo al listenn-mañ evit c'hoazh. Pa vo embannet toudoù nevez gant e izili e teuint war wel amañ.", "empty_column.mutes": "N'ho peus kuzhet implijer ebet c'hoazh.", @@ -241,7 +252,7 @@ "explore.title": "Diouzh ar c'hiz", "explore.trending_links": "Keleier", "explore.trending_statuses": "Embannadurioù", - "explore.trending_tags": "Hashtagoù", + "explore.trending_tags": "Gerioù-klik", "featured_carousel.next": "War-raok", "featured_carousel.post": "Embannadenn", "featured_carousel.previous": "War-gil", @@ -269,7 +280,8 @@ "follow_suggestions.friends_of_friends_longer": "Diouzh ar c'hiz e-touez an dud heuliet ganeoc'h", "follow_suggestions.popular_suggestion_longer": "Diouzh ar c'hiz war {domain}", "follow_suggestions.view_all": "Gwelet pep tra", - "followed_tags": "Hashtagoù o heuliañ", + "follow_suggestions.who_to_follow": "Piv heuliañ", + "followed_tags": "Gerioù-klik o heuliañ", "footer.about": "Diwar-benn", "footer.directory": "Kavlec'h ar profiloù", "footer.get_app": "Pellgargañ an arload", @@ -281,19 +293,23 @@ "generic.saved": "Enrollet", "getting_started.heading": "Loc'hañ", "hashtag.admin_moderation": "Digeriñ an etrefas evezhiañ evit #{name}", + "hashtag.browse": "Furchal dre an toudoù gant #{hashtag}", + "hashtag.browse_from_account": "Furchal dre an toudoù gant @{name} gant #{hashtag}", "hashtag.column_header.tag_mode.all": "ha(g) {additional}", "hashtag.column_header.tag_mode.any": "pe {additional}", "hashtag.column_header.tag_mode.none": "hep {additional}", "hashtag.column_settings.select.no_options_message": "N'eus bet kavet ali ebet", - "hashtag.column_settings.select.placeholder": "Ouzhpennañ hashtagoù…", + "hashtag.column_settings.select.placeholder": "Ouzhpennañ gerioù-klik…", "hashtag.column_settings.tag_mode.all": "An holl anezho", "hashtag.column_settings.tag_mode.any": "Unan e mesk anezho", "hashtag.column_settings.tag_mode.none": "Hini ebet anezho", "hashtag.column_settings.tag_toggle": "Endelc'her gerioù-alc'hwez ouzhpenn evit ar bannad-mañ", "hashtag.counter_by_uses": "{count, plural, one {{counter} embannadur} other {{counter} embannadur}}", "hashtag.counter_by_uses_today": "{count, plural, one {{counter} embannadur} other {{counter} embannadur}} hiziv", + "hashtag.feature": "Lakaat war-wel war ar profil", "hashtag.follow": "Heuliañ ar ger-klik", - "hashtag.unfollow": "Paouez heuliañ an hashtag", + "hashtag.mute": "Kuzhat #{hashtag}", + "hashtag.unfollow": "Diheuliañ ar ger-klik", "hashtags.and_other": "…{count, plural, one {hag # all} other {ha # all}}", "home.column_settings.show_quotes": "Diskouez an arroudennoù", "home.column_settings.show_reblogs": "Diskouez ar skignadennoù", @@ -302,12 +318,14 @@ "home.pending_critical_update.body": "Hizivait ho servijer Mastodon kerkent ha ma c'hallit mar plij!", "home.pending_critical_update.link": "Gwelet an hizivadennoù", "home.show_announcements": "Diskouez ar c'hemennoù", + "interaction_modal.go": "Mont di", "interaction_modal.on_another_server": "War ur servijer all", "interaction_modal.on_this_server": "War ar servijer-mañ", "interaction_modal.title.favourite": "Ouzhpennañ embannadur {name} d'ar re vuiañ-karet", "interaction_modal.title.follow": "Heuliañ {name}", "interaction_modal.title.reblog": "Skignañ toud {name}", "interaction_modal.title.reply": "Respont da doud {name}", + "interaction_modal.username_prompt": "D.s. {example}", "intervals.full.days": "{number, plural, one {# devezh} other{# a zevezhioù}}", "intervals.full.hours": "{number, plural, one {# eurvezh} other{# eurvezh}}", "intervals.full.minutes": "{number, plural, one {# munut} other{# a vunutoù}}", @@ -345,12 +363,14 @@ "keyboard_shortcuts.toot": "Kregiñ gant un toud nevez", "keyboard_shortcuts.unfocus": "Difokus an dachenn testenn/klask", "keyboard_shortcuts.up": "Pignat er roll", + "learn_more_link.got_it": "Mat eo", "lightbox.close": "Serriñ", "lightbox.next": "Da-heul", "lightbox.previous": "A-raok", - "limited_account_hint.action": "Diskouez an aelad memes tra", + "limited_account_hint.action": "Diskouez ar profil memes tra", "limited_account_hint.title": "Kuzhet eo bet ar profil-mañ gant an evezhierien eus {domain}.", "link_preview.author": "Gant {name}", + "link_preview.more_from_author": "Muioc'h gant {name}", "lists.add_member": "Ouzhpennañ", "lists.add_to_list": "Ouzhpennañ d'al listenn", "lists.create": "Krouiñ", @@ -359,13 +379,19 @@ "lists.done": "Graet", "lists.edit": "Kemmañ al listenn", "lists.list_name": "Anv al listenn", + "lists.new_list_name": "Anv nevez al listenn", + "lists.no_lists_yet": "Listenn ebet c'hoazh.", "lists.replies_policy.followed": "Pep implijer.ez heuliet", "lists.replies_policy.list": "Izili ar roll", "lists.replies_policy.none": "Den ebet", + "lists.save": "Enrollañ", + "lists.search": "Klask", "load_pending": "{count, plural, one {# dra nevez} other {# dra nevez}}", "loading_indicator.label": "O kargañ…", + "media_gallery.hide": "Kuzhat", "navigation_bar.about": "Diwar-benn", "navigation_bar.account_settings": "Ger-tremen ha surentez", + "navigation_bar.administration": "Merañ", "navigation_bar.automated_deletion": "Dilemel an embannadenn ent-emgefreek", "navigation_bar.blocks": "Implijer·ezed·ien berzet", "navigation_bar.bookmarks": "Sinedoù", @@ -374,11 +400,12 @@ "navigation_bar.favourites": "Muiañ-karet", "navigation_bar.filters": "Gerioù kuzhet", "navigation_bar.follow_requests": "Pedadoù heuliañ", - "navigation_bar.followed_tags": "Hashtagoù o heuliañ", + "navigation_bar.followed_tags": "Gerioù-klik o heuliañ", "navigation_bar.follows_and_followers": "Heuliadennoù ha heulier·ezed·ien", "navigation_bar.import_export": "Enporzhiañ hag ezporzhiañ", "navigation_bar.lists": "Listennoù", "navigation_bar.logout": "Digennaskañ", + "navigation_bar.moderation": "Habaskadur", "navigation_bar.more": "Muioc'h", "navigation_bar.mutes": "Implijer·ion·ezed kuzhet", "navigation_bar.preferences": "Gwellvezioù", @@ -391,6 +418,8 @@ "notification.follow.name_and_others": "{name} {count, plural, one {hag # den all} two {ha # zen all} few {ha # den all} many {ha # den all} other {ha # den all}} zo o heuliañ ac'hanoc'h", "notification.follow_request": "Gant {name} eo bet goulennet ho heuliañ", "notification.label.reply": "Respont", + "notification.mention": "Meneg", + "notification.mentioned_you": "Gant {name} oc'h bet meneget", "notification.moderation-warning.learn_more": "Gouzout hiroc'h", "notification.own_poll": "Echu eo ho sontadeg", "notification.reblog": "Gant {name} eo bet skignet ho toud", @@ -447,7 +476,7 @@ "onboarding.profile.display_name": "Anv diskouezet", "onboarding.profile.display_name_hint": "Hoc'h anv klok pe hoc'h anv fentus…", "onboarding.profile.note": "Berr-ha-berr", - "onboarding.profile.note_hint": "Gallout a rit @menegiñ tud all pe #hashtagoù…", + "onboarding.profile.note_hint": "Gallout a rit @menegiñ tud all pe #gerioù-klik…", "onboarding.profile.save_and_continue": "Enrollañ ha kenderc'hel", "onboarding.profile.upload_avatar": "Enporzhiañ ur skeudenn profil", "password_confirmation.mismatching": "Disheñvel eo an daou c'her-termen-se", @@ -480,6 +509,7 @@ "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}eil", "relative_time.today": "hiziv", + "remove_quote_hint.button_label": "Mat eo", "reply_indicator.cancel": "Nullañ", "reply_indicator.poll": "Sontadeg", "report.block": "Stankañ", @@ -528,7 +558,7 @@ "search.placeholder": "Klask", "search.quick_action.account_search": "Profiloù a glot gant {x}", "search.quick_action.go_to_account": "Mont d'ar profil {x}", - "search.quick_action.go_to_hashtag": "Mont d'an hashtag {x}", + "search.quick_action.go_to_hashtag": "Mont d'ar ger-klik {x}", "search.quick_action.open_url": "Digeriñ an URL e-barzh Mastodon", "search.quick_action.status_search": "Embannadurioù a glot gant {x}", "search.search_or_paste": "Klask pe pegañ un URL", @@ -541,7 +571,7 @@ "search_popout.user": "implijer·ez", "search_results.accounts": "Profiloù", "search_results.all": "Pep tra", - "search_results.hashtags": "Hashtagoù", + "search_results.hashtags": "Gerioù-klik", "search_results.no_results": "Disoc'h ebet.", "search_results.see_all": "Gwelet pep tra", "search_results.statuses": "Toudoù", @@ -607,6 +637,7 @@ "subscribed_languages.save": "Enrollañ ar cheñchamantoù", "subscribed_languages.target": "Cheñch ar yezhoù koumanantet evit {target}", "tabs_bar.home": "Degemer", + "tabs_bar.menu": "Lañser", "tabs_bar.notifications": "Kemennoù", "tabs_bar.publish": "Embannadenn nevez", "tabs_bar.search": "Klask", @@ -638,6 +669,8 @@ "video.hide": "Kuzhat ar video", "video.pause": "Paouez", "video.play": "Lenn", + "visibility_modal.privacy_label": "Gwelusted", "visibility_modal.quote_followers": "Tud koumanantet hepken", - "visibility_modal.quote_public": "Pep den" + "visibility_modal.quote_public": "Pep den", + "visibility_modal.save": "Enrollañ" } diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index a59ab6702f..f8bda8f735 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -768,6 +768,9 @@ "relative_time.minutes": "{number} m", "relative_time.seconds": "{number} s", "relative_time.today": "dnes", + "remove_quote_hint.button_label": "Chápu", + "remove_quote_hint.message": "Můžete to udělat z {icon} nabídky možností.", + "remove_quote_hint.title": "Chcete odstranit citovaný příspěvek?", "reply_indicator.attachments": "{count, plural, one {{counter} příloha} few {{counter} přílohy} other {{counter} příloh}}", "reply_indicator.cancel": "Zrušit", "reply_indicator.poll": "Anketa", @@ -863,6 +866,7 @@ "status.block": "Blokovat @{name}", "status.bookmark": "Přidat do záložek", "status.cancel_reblog_private": "Zrušit boostnutí", + "status.cannot_quote": "Citování je na tomo příspěvku zakázáno", "status.cannot_reblog": "Tento příspěvek nemůže být boostnutý", "status.context.load_new_replies": "K dispozici jsou nové odpovědi", "status.context.loading": "Hledání dalších odpovědí", @@ -988,6 +992,7 @@ "visibility_modal.header": "Viditelnost a interakce", "visibility_modal.helper.direct_quoting": "Soukromé zmínky, které jsou vytvořeny na Mastodonu, nemohou být citovány ostatními.", "visibility_modal.helper.privacy_editing": "Publikované příspěvky nemohou změnit svou viditelnost.", + "visibility_modal.helper.privacy_private_self_quote": "Citace vlastních soukromých příspěvků nelze zveřejnit.", "visibility_modal.helper.private_quoting": "Příspěvky pouze pro sledující, které jsou vytvořeny na Mastodonu, nemohou být citovány ostatními.", "visibility_modal.helper.unlisted_quoting": "Když vás lidé citují, jejich příspěvek bude v časové ose populárních příspěvků také skryt.", "visibility_modal.instructions": "Nastavte, kdo bude moci interagovat s tímto příspěvkem. Tyto nastavení též můžete změnit pro všechny budoucí příspěvky v Nastavení > Výchozí nastavení příspěvků.", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 71f808a118..cf10facdbf 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -768,6 +768,7 @@ "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "relative_time.today": "i dag", + "remove_quote_hint.button_label": "Forstået", "reply_indicator.attachments": "{count, plural, one {# vedhæftning} other {# vedhæftninger}}", "reply_indicator.cancel": "Afbryd", "reply_indicator.poll": "Afstemning", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 905fc668e8..e869b49ac6 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -768,6 +768,9 @@ "relative_time.minutes": "{number} Min.", "relative_time.seconds": "{number} Sek.", "relative_time.today": "heute", + "remove_quote_hint.button_label": "Verstanden", + "remove_quote_hint.message": "Klicke dafür im Beitrag auf „{icon} Mehr“.", + "remove_quote_hint.title": "Möchtest du aus dem zitierten Beitrag entfernt werden?", "reply_indicator.attachments": "{count, plural, one {# Anhang} other {# Anhänge}}", "reply_indicator.cancel": "Abbrechen", "reply_indicator.poll": "Umfrage", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index f2f23e60fa..1acbe9089f 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -768,6 +768,9 @@ "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "relative_time.today": "today", + "remove_quote_hint.button_label": "Got it", + "remove_quote_hint.message": "You can do so from the {icon} options menu.", + "remove_quote_hint.title": "Want to remove your quoted post?", "reply_indicator.attachments": "{count, plural, one {# attachment} other {# attachments}}", "reply_indicator.cancel": "Cancel", "reply_indicator.poll": "Poll", @@ -910,7 +913,7 @@ "status.read_more": "Read more", "status.reblog": "Boost", "status.reblog_or_quote": "Boost or quote", - "status.reblog_private": "Boost with original visibility", + "status.reblog_private": "Share again with your followers", "status.reblogged_by": "{name} boosted", "status.reblogs": "{count, plural, one {boost} other {boosts}}", "status.reblogs.empty": "No one has boosted this post yet. When someone does, they will show up here.", @@ -988,7 +991,7 @@ "visibility_modal.button_title": "Set visibility", "visibility_modal.header": "Visibility and interaction", "visibility_modal.helper.direct_quoting": "Private mentions authored on Mastodon can't be quoted by others.", - "visibility_modal.helper.privacy_editing": "Published posts cannot change their visibility.", + "visibility_modal.helper.privacy_editing": "Visibility can't be changed after a post is published.", "visibility_modal.helper.privacy_private_self_quote": "Self-quotes of private posts cannot be made public.", "visibility_modal.helper.private_quoting": "Follower-only posts authored on Mastodon can't be quoted by others.", "visibility_modal.helper.unlisted_quoting": "When people quote you, their post will also be hidden from trending timelines.", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index ca0f341a80..aedf92cfa3 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -30,6 +30,8 @@ "account.edit_profile": "Redakti la profilon", "account.enable_notifications": "Sciigu min kiam @{name} afiŝos", "account.endorse": "Montri en profilo", + "account.familiar_followers_one": "Sekvita de {name1}", + "account.familiar_followers_two": "Sekvita de {name1} kaj {name2}", "account.featured": "Montrita", "account.featured.accounts": "Profiloj", "account.featured.hashtags": "Kradvortoj", @@ -305,6 +307,7 @@ "emoji_button.search_results": "Serĉaj rezultoj", "emoji_button.symbols": "Simboloj", "emoji_button.travel": "Vojaĝoj kaj lokoj", + "empty_column.account_featured.me": "Vi ankoraŭ nenion prezentis. Ĉu vi sciis, ke vi povas prezenti viajn plej ofte uzatajn kradvortojn, kaj eĉ la kontojn de viaj amikoj sur via profilo?", "empty_column.account_featured_other.unknown": "Ĉi tiu konto ankoraŭ ne montris ion ajn.", "empty_column.account_hides_collections": "Ĉi tiu uzanto elektis ne disponebligi ĉi tiu informon", "empty_column.account_suspended": "Konto suspendita", @@ -338,6 +341,7 @@ "explore.trending_links": "Novaĵoj", "explore.trending_statuses": "Afiŝoj", "explore.trending_tags": "Kradvortoj", + "featured_carousel.header": "{count, plural, one {Alpinglita afiŝo} other {Alpinglitaj afiŝoj}}", "featured_carousel.next": "Antaŭen", "featured_carousel.post": "Afiŝi", "featured_carousel.previous": "Malantaŭen", @@ -751,6 +755,9 @@ "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "relative_time.today": "hodiaŭ", + "remove_quote_hint.button_label": "Komprenite", + "remove_quote_hint.message": "Vi povas fari tion el la menuo de opcioj {icon}.", + "remove_quote_hint.title": "Ĉu vi volas forigi vian cititan afiŝon?", "reply_indicator.attachments": "{count, plural, one {# aldonaĵo} other {# aldonaĵoj}}", "reply_indicator.cancel": "Nuligi", "reply_indicator.poll": "Balotenketo", @@ -846,6 +853,7 @@ "status.block": "Bloki @{name}", "status.bookmark": "Aldoni al la legosignoj", "status.cancel_reblog_private": "Ne plu diskonigi", + "status.cannot_quote": "Citaĵoj estas malebligitaj en ĉi tiu afiŝo", "status.cannot_reblog": "Ĉi tiun afiŝon ne eblas diskonigi", "status.context.load_new_replies": "Disponeblaj novaj respondoj", "status.context.loading": "Serĉante pliajn respondojn", @@ -971,6 +979,7 @@ "visibility_modal.helper.privacy_editing": "Publikigitaj afiŝoj ne povas ŝanĝi sian videblon.", "visibility_modal.helper.private_quoting": "Afiŝoj nur por sekvantoj verkitaj ĉe Mastodon ne povas esti cititaj de aliaj.", "visibility_modal.helper.unlisted_quoting": "Kiam homoj citas vin, ilia afiŝo ankaŭ estos kaŝita de tendencaj templinioj.", + "visibility_modal.privacy_label": "Videbleco", "visibility_modal.quote_followers": "Nur sekvantoj", "visibility_modal.quote_label": "Kiu povas citi", "visibility_modal.quote_nobody": "Nur mi", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 7aa7a9b4ee..a51d8092c2 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -768,6 +768,9 @@ "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "relative_time.today": "hoy", + "remove_quote_hint.button_label": "Entendido", + "remove_quote_hint.message": "Podés hacerlo desde el menú de opciones {icon}.", + "remove_quote_hint.title": "¿Querés eliminar tu mensaje citado?", "reply_indicator.attachments": "{count, plural,one {# adjunto} other {# adjuntos}}", "reply_indicator.cancel": "Cancelar", "reply_indicator.poll": "Encuesta", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index 7f764d0cd3..e6e5ece97e 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -768,6 +768,9 @@ "relative_time.minutes": "{number} m", "relative_time.seconds": "{number} s", "relative_time.today": "hoy", + "remove_quote_hint.button_label": "Entendido", + "remove_quote_hint.message": "Puedes hacerlo desde el menú de opciones {icon}.", + "remove_quote_hint.title": "¿Quieres eliminar tu publicación citada?", "reply_indicator.attachments": "{count, plural, one {# adjunto} other {# adjuntos}}", "reply_indicator.cancel": "Cancelar", "reply_indicator.poll": "Encuesta", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 4f1464e6cc..a46592221a 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -768,6 +768,9 @@ "relative_time.minutes": "{number} m", "relative_time.seconds": "{number} s", "relative_time.today": "hoy", + "remove_quote_hint.button_label": "Entendido", + "remove_quote_hint.message": "Puedes hacerlo desde el menú de opciones {icon}.", + "remove_quote_hint.title": "¿Quieres eliminar tu publicación citada?", "reply_indicator.attachments": "{count, plural, one {# adjunto} other {# adjuntos}}", "reply_indicator.cancel": "Cancelar", "reply_indicator.poll": "Encuesta", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index 7fa3ca5fb2..700f9c4bc8 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -863,6 +863,7 @@ "status.block": "Blokeeri @{name}", "status.bookmark": "Järjehoidja", "status.cancel_reblog_private": "Lõpeta jagamine", + "status.cannot_quote": "Selle postituse tsiteerimine pole lubatud", "status.cannot_reblog": "Seda postitust ei saa jagada", "status.context.load_new_replies": "Leidub uusi vastuseid", "status.context.loading": "Kontrollin täiendavate vastuste olemasolu", @@ -988,9 +989,13 @@ "visibility_modal.header": "Nähtavus ja kasutus", "visibility_modal.helper.direct_quoting": "Ainult mainituile mõeldud Mastodoni postitusi ei saa teiste poolt tsiteerida.", "visibility_modal.helper.privacy_editing": "Avaldatud postitused ei saa muuta oma nähtavust.", + "visibility_modal.helper.privacy_private_self_quote": "Privaatsete postituste tsiteerimist oma enda poolt pole võimalik teha avalikuks.", "visibility_modal.helper.private_quoting": "Ainult jälgijatele mõeldud Mastodoni postitusi ei saa teiste poolt tsiteerida.", "visibility_modal.helper.unlisted_quoting": "Kui teised kasutajad sind tsiteerivad, siis nende postitused peidetakse ajajoonelt, mis näitavad populaarsust koguvaid postitusi.", + "visibility_modal.instructions": "Halda seda, kes võivad antud postitusega suhestuda. Lisaks võid kõikide tulevaste postituste seadistusi muuta valikust Eelistused > Postituse vaikeseadistused.", + "visibility_modal.privacy_label": "Nähtavus", "visibility_modal.quote_followers": "Ainult jälgijad", + "visibility_modal.quote_label": "Kes võivad tsiteerida", "visibility_modal.quote_nobody": "Ainult mina", "visibility_modal.quote_public": "Kõik", "visibility_modal.save": "Salvesta" diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index e5f6592ffe..19a790cbfa 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -852,6 +852,7 @@ "status.block": "انسداد ‎@{name}", "status.bookmark": "نشانک", "status.cancel_reblog_private": "ناتقویت", + "status.cannot_quote": "نقل قول برای این پست غیرفعال است", "status.cannot_reblog": "این فرسته قابل تقویت نیست", "status.context.load_new_replies": "پاسخ‌های جدیدی موجودند", "status.context.loading": "بررسی کردن برای پاسخ‌های بیش‌تر", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 22599858fb..b48520ebd3 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -768,6 +768,9 @@ "relative_time.minutes": "{number} min", "relative_time.seconds": "{number} s", "relative_time.today": "tänään", + "remove_quote_hint.button_label": "Selvä", + "remove_quote_hint.message": "Voit tehdä sen {icon}-valikosta.", + "remove_quote_hint.title": "Haluatko poistaa lainatun julkaisusi?", "reply_indicator.attachments": "{count, plural, one {# liite} other {# liitettä}}", "reply_indicator.cancel": "Peruuta", "reply_indicator.poll": "Äänestys", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index 5f7ccbbc5f..e3e9970378 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -490,6 +490,7 @@ "keyboard_shortcuts.open_media": "Ouvrir média", "keyboard_shortcuts.pinned": "Ouvrir la liste de publications épinglés", "keyboard_shortcuts.profile": "Ouvrir le profil de l’auteur·rice", + "keyboard_shortcuts.quote": "Citer la publication", "keyboard_shortcuts.reply": "Répondre au message", "keyboard_shortcuts.requests": "Ouvrir la liste de demandes d’abonnement", "keyboard_shortcuts.search": "Focuser sur le champ de recherche", @@ -599,6 +600,7 @@ "notification.label.mention": "Mention", "notification.label.private_mention": "Mention privée", "notification.label.private_reply": "Répondre en privé", + "notification.label.quote": "{name} a cité votre publication", "notification.label.reply": "Réponse", "notification.mention": "Mention", "notification.mentioned_you": "{name} vous a mentionné·e", @@ -613,6 +615,7 @@ "notification.moderation_warning.action_suspend": "Votre compte a été suspendu.", "notification.own_poll": "Votre sondage est terminé", "notification.poll": "Un sondage auquel vous avez participé vient de se terminer", + "notification.quoted_update": "{name} a modifié une publication que vous avez cité", "notification.reblog": "{name} a boosté votre message", "notification.reblog.name_and_others_with_link": "{name} et {count, plural, one {# autre} other {# autres}} ont boosté votre message", "notification.relationships_severance_event": "Connexions perdues avec {name}", @@ -656,6 +659,7 @@ "notifications.column_settings.mention": "Mentions:", "notifications.column_settings.poll": "Résultats des sondages:", "notifications.column_settings.push": "Notifications push", + "notifications.column_settings.quote": "Citations:", "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Afficher dans la colonne", "notifications.column_settings.sound": "Jouer un son", @@ -731,12 +735,18 @@ "privacy.private.short": "Abonnés", "privacy.public.long": "Tout le monde sur et en dehors de Mastodon", "privacy.public.short": "Public", + "privacy.quote.anyone": "{visibility}, n'importe qui peut citer", + "privacy.quote.disabled": "{visibility}, citations désactivées", + "privacy.quote.limited": "{visibility}, citations limitées", "privacy.unlisted.additional": "Se comporte exactement comme « public », sauf que le message n'apparaîtra pas dans les flux en direct, les hashtags, explorer ou la recherche Mastodon, même si vous les avez activé au niveau de votre compte.", "privacy.unlisted.long": "Moins de fanfares algorithmiques", "privacy.unlisted.short": "Public discret", "privacy_policy.last_updated": "Dernière mise à jour {date}", "privacy_policy.title": "Politique de confidentialité", + "quote_error.poll": "Les citations ne sont pas autorisées avec les sondages.", + "quote_error.quote": "Une seule citation à la fois est autorisée.", "quote_error.unauthorized": "Vous n'êtes pas autorisé⋅e à citer cette publication.", + "quote_error.upload": "La citation n'est pas autorisée avec un média joint.", "recommended": "Recommandé", "refresh": "Actualiser", "regeneration_indicator.please_stand_by": "Veuillez patienter.", @@ -843,14 +853,17 @@ "status.admin_account": "Ouvrir l’interface de modération pour @{name}", "status.admin_domain": "Ouvrir l’interface de modération pour {domain}", "status.admin_status": "Ouvrir ce message dans l’interface de modération", + "status.all_disabled": "Partages et citations sont désactivés", "status.block": "Bloquer @{name}", "status.bookmark": "Ajouter aux signets", "status.cancel_reblog_private": "Débooster", + "status.cannot_quote": "Les citations sont désactivées sur cette publication", "status.cannot_reblog": "Cette publication ne peut pas être boostée", "status.context.load_new_replies": "Nouvelles réponses disponibles", "status.continued_thread": "Suite du fil", "status.copy": "Copier un lien vers cette publication", "status.delete": "Supprimer", + "status.delete.success": "Publication supprimée", "status.detailed_status": "Vue détaillée de la conversation", "status.direct": "Mention privée @{name}", "status.direct_indicator": "Mention privée", @@ -875,9 +888,12 @@ "status.pin": "Épingler sur profil", "status.quote": "Citation", "status.quote.cancel": "Annuler la citation", + "status.quote_error.filtered": "Caché en raison de l'un de vos filtres", "status.quote_error.not_available": "Publication non disponible", "status.quote_error.pending_approval": "Publication en attente", "status.quote_error.pending_approval_popout.title": "Publication en attente ? Restez calme", + "status.quote_followers_only": "Seul·e·s mes abonné·e·s peuvent citer cette publication", + "status.quote_manual_review": "L'auteur va vérifier manuellement", "status.quote_policy_change": "Changer qui peut vous citer", "status.quote_private": "Les publications privées ne peuvent pas être citées", "status.read_more": "En savoir plus", @@ -955,6 +971,10 @@ "video.volume_up": "Augmenter le volume", "visibility_modal.button_title": "Définir la visibilité", "visibility_modal.header": "Visibilité et interactions", + "visibility_modal.privacy_label": "Visibilité", + "visibility_modal.quote_followers": "Abonné·e·s seulement", + "visibility_modal.quote_label": "Autoriser les citations pour", + "visibility_modal.quote_nobody": "Juste moi", "visibility_modal.quote_public": "Tout le monde", "visibility_modal.save": "Sauvegarder" } diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 062918070c..82e46bfb09 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -490,6 +490,7 @@ "keyboard_shortcuts.open_media": "Ouvrir le média", "keyboard_shortcuts.pinned": "Ouvrir la liste des messages épinglés", "keyboard_shortcuts.profile": "Ouvrir le profil de l’auteur·rice", + "keyboard_shortcuts.quote": "Citer la publication", "keyboard_shortcuts.reply": "Répondre au message", "keyboard_shortcuts.requests": "Ouvrir la liste de demandes d’abonnement", "keyboard_shortcuts.search": "Se placer dans le champ de recherche", @@ -599,6 +600,7 @@ "notification.label.mention": "Mention", "notification.label.private_mention": "Mention privée", "notification.label.private_reply": "Répondre en privé", + "notification.label.quote": "{name} a cité votre publication", "notification.label.reply": "Réponse", "notification.mention": "Mention", "notification.mentioned_you": "{name} vous a mentionné·e", @@ -613,6 +615,7 @@ "notification.moderation_warning.action_suspend": "Votre compte a été suspendu.", "notification.own_poll": "Votre sondage est terminé", "notification.poll": "Un sondage auquel vous avez participé vient de se terminer", + "notification.quoted_update": "{name} a modifié une publication que vous avez cité", "notification.reblog": "{name} a partagé votre message", "notification.reblog.name_and_others_with_link": "{name} et {count, plural, one {# autre} other {# autres}} ont boosté votre message", "notification.relationships_severance_event": "Connexions perdues avec {name}", @@ -656,6 +659,7 @@ "notifications.column_settings.mention": "Mentions :", "notifications.column_settings.poll": "Résultats des sondages :", "notifications.column_settings.push": "Notifications push", + "notifications.column_settings.quote": "Citations:", "notifications.column_settings.reblog": "Partages :", "notifications.column_settings.show": "Afficher dans la colonne", "notifications.column_settings.sound": "Jouer un son", @@ -731,12 +735,18 @@ "privacy.private.short": "Abonnés", "privacy.public.long": "Tout le monde sur et en dehors de Mastodon", "privacy.public.short": "Public", + "privacy.quote.anyone": "{visibility}, n'importe qui peut citer", + "privacy.quote.disabled": "{visibility}, citations désactivées", + "privacy.quote.limited": "{visibility}, citations limitées", "privacy.unlisted.additional": "Se comporte exactement comme « public », sauf que le message n'apparaîtra pas dans les flux en direct, les hashtags, explorer ou la recherche Mastodon, même si vous les avez activé au niveau de votre compte.", "privacy.unlisted.long": "Moins de fanfares algorithmiques", "privacy.unlisted.short": "Public discret", "privacy_policy.last_updated": "Dernière mise à jour {date}", "privacy_policy.title": "Politique de confidentialité", + "quote_error.poll": "Les citations ne sont pas autorisées avec les sondages.", + "quote_error.quote": "Une seule citation à la fois est autorisée.", "quote_error.unauthorized": "Vous n'êtes pas autorisé⋅e à citer cette publication.", + "quote_error.upload": "La citation n'est pas autorisée avec un média joint.", "recommended": "Recommandé", "refresh": "Actualiser", "regeneration_indicator.please_stand_by": "Veuillez patienter.", @@ -843,14 +853,17 @@ "status.admin_account": "Ouvrir l’interface de modération pour @{name}", "status.admin_domain": "Ouvrir l’interface de modération pour {domain}", "status.admin_status": "Ouvrir ce message dans l’interface de modération", + "status.all_disabled": "Partages et citations sont désactivés", "status.block": "Bloquer @{name}", "status.bookmark": "Ajouter aux marque-pages", "status.cancel_reblog_private": "Annuler le partage", + "status.cannot_quote": "Les citations sont désactivées sur cette publication", "status.cannot_reblog": "Ce message ne peut pas être partagé", "status.context.load_new_replies": "Nouvelles réponses disponibles", "status.continued_thread": "Suite du fil", "status.copy": "Copier le lien vers le message", "status.delete": "Supprimer", + "status.delete.success": "Publication supprimée", "status.detailed_status": "Vue détaillée de la conversation", "status.direct": "Mention privée @{name}", "status.direct_indicator": "Mention privée", @@ -875,9 +888,12 @@ "status.pin": "Épingler sur le profil", "status.quote": "Citation", "status.quote.cancel": "Annuler la citation", + "status.quote_error.filtered": "Caché en raison de l'un de vos filtres", "status.quote_error.not_available": "Publication non disponible", "status.quote_error.pending_approval": "Publication en attente", "status.quote_error.pending_approval_popout.title": "Publication en attente ? Restez calme", + "status.quote_followers_only": "Seul·e·s mes abonné·e·s peuvent citer cette publication", + "status.quote_manual_review": "L'auteur va vérifier manuellement", "status.quote_policy_change": "Changer qui peut vous citer", "status.quote_private": "Les publications privées ne peuvent pas être citées", "status.read_more": "En savoir plus", @@ -955,6 +971,10 @@ "video.volume_up": "Augmenter le volume", "visibility_modal.button_title": "Définir la visibilité", "visibility_modal.header": "Visibilité et interactions", + "visibility_modal.privacy_label": "Visibilité", + "visibility_modal.quote_followers": "Abonné·e·s seulement", + "visibility_modal.quote_label": "Autoriser les citations pour", + "visibility_modal.quote_nobody": "Juste moi", "visibility_modal.quote_public": "Tout le monde", "visibility_modal.save": "Sauvegarder" } diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 0b8ede0613..4e6bbfacd3 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -768,6 +768,9 @@ "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "relative_time.today": "hoxe", + "remove_quote_hint.button_label": "Entendido", + "remove_quote_hint.message": "Pódelo facer desde o {icon} menú de opcións.", + "remove_quote_hint.title": "Queres eliminar a publicación citada?", "reply_indicator.attachments": "{count, plural, one {# adxunto} other {# adxuntos}}", "reply_indicator.cancel": "Desbotar", "reply_indicator.poll": "Enquisa", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index ed03d4d757..7418a78784 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -768,6 +768,9 @@ "relative_time.minutes": "{number} דקות", "relative_time.seconds": "{number} שניות", "relative_time.today": "היום", + "remove_quote_hint.button_label": "הבנתי", + "remove_quote_hint.message": "ניתן לעשות זאת מתפריט האפשרויות {icon}", + "remove_quote_hint.title": "להסיר את ההודעה המצוטטת?", "reply_indicator.attachments": "{count, plural,one {# קובץ מצורף}other {# קבצים מצורפים}}", "reply_indicator.cancel": "ביטול", "reply_indicator.poll": "משאל", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index 967006e239..2f583f3334 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -768,6 +768,9 @@ "relative_time.minutes": "{number}mín", "relative_time.seconds": "{number}sek", "relative_time.today": "í dag", + "remove_quote_hint.button_label": "Náði því", + "remove_quote_hint.message": "Þú getur gert það úr {icon} valmyndinni.", + "remove_quote_hint.title": "Viltu fjarlægja tilvitnuðu færsluna þína?", "reply_indicator.attachments": "{count, plural, one {# viðhengi} other {# viðhengi}}", "reply_indicator.cancel": "Hætta við", "reply_indicator.poll": "Könnun", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index 94cb899756..445ba7cae3 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -97,8 +97,8 @@ "admin.impact_report.title": "Ietekmes kopsavilkums", "alert.rate_limited.message": "Lūdzu, mēģini vēlreiz pēc {retry_time, time, medium}.", "alert.rate_limited.title": "Biežums ierobežots", - "alert.unexpected.message": "Radās negaidīta kļūda.", - "alert.unexpected.title": "Ups!", + "alert.unexpected.message": "Atgadījās neparedzēta kļūda.", + "alert.unexpected.title": "Ak vai!", "alt_text_badge.title": "Alt teksts", "alt_text_modal.add_alt_text": "Pievienot aprakstošo tekstu", "alt_text_modal.add_text_from_image": "Pievienot tekstu no attēla", @@ -140,7 +140,7 @@ "bundle_column_error.copy_stacktrace": "Ievietot kļūdu ziņojumu starpliktuvē", "bundle_column_error.error.body": "Pieprasīto lapu nevarēja atveidot. Tas varētu būt saistīts ar kļūdu mūsu kodā, vai tā ir pārlūkprogrammas saderības problēma.", "bundle_column_error.error.title": "Ak vai!", - "bundle_column_error.network.body": "Mēģinot ielādēt šo lapu, radās kļūda. Tas varētu būt saistīts ar īslaicīgu interneta savienojuma vai šī servera problēmu.", + "bundle_column_error.network.body": "Kļūda lapas ielādēšanas laikā. Tas varētu būt īzlaicīgs sarežģījums ar interneta savienojumu vai šo serveri.", "bundle_column_error.network.title": "Tīkla kļūda", "bundle_column_error.retry": "Mēģināt vēlreiz", "bundle_column_error.return": "Atgriezties", @@ -202,7 +202,7 @@ "compose_form.poll.switch_to_multiple": "Mainīt aptaujas veidu, lai atļautu vairākas izvēles", "compose_form.poll.switch_to_single": "Mainīt aptaujas veidu, lai atļautu vienu izvēli", "compose_form.poll.type": "Stils", - "compose_form.publish": "Nosūtīt", + "compose_form.publish": "Iesūtīt", "compose_form.reply": "Atbildēt", "compose_form.save_changes": "Atjaunināt", "compose_form.spoiler.marked": "Noņemt satura brīdinājumu", @@ -509,10 +509,11 @@ "navigation_bar.filters": "Apklusinātie vārdi", "navigation_bar.follow_requests": "Sekošanas pieprasījumi", "navigation_bar.followed_tags": "Sekojamie tēmturi", - "navigation_bar.follows_and_followers": "Sekojamie un sekotāji", + "navigation_bar.follows_and_followers": "Seko un sekotāji", "navigation_bar.lists": "Saraksti", "navigation_bar.logout": "Iziet", "navigation_bar.moderation": "Satura pārraudzība", + "navigation_bar.more": "Vairāk", "navigation_bar.mutes": "Apklusinātie lietotāji", "navigation_bar.opened_in_classic_interface": "Ieraksti, konti un citas noteiktas lapas pēc noklusējuma tiek atvērtas klasiskajā tīmekļa saskarnē.", "navigation_bar.preferences": "Iestatījumi", @@ -734,6 +735,7 @@ "status.bookmark": "Grāmatzīme", "status.cancel_reblog_private": "Nepastiprināt", "status.cannot_reblog": "Šo ierakstu nevar pastiprināt", + "status.context.loading": "Pārbauda, vai ir vairāk atbilžu", "status.continued_thread": "Turpināts pavediens", "status.copy": "Ievietot ieraksta saiti starpliktuvē", "status.delete": "Dzēst", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index d762191b71..55c2b2423d 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -768,6 +768,9 @@ "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "relative_time.today": "vandaag", + "remove_quote_hint.button_label": "Begrepen", + "remove_quote_hint.message": "Je kunt dit doen vanuit het {icon} optiesmenu.", + "remove_quote_hint.title": "Wil je jouw geciteerd bericht verwijderen?", "reply_indicator.attachments": "{count, plural, one {# bijlage} other {# bijlagen}}", "reply_indicator.cancel": "Annuleren", "reply_indicator.poll": "Peiling", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 2c5fe289b7..35f2545cdb 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -768,6 +768,9 @@ "relative_time.minutes": "{number} phút", "relative_time.seconds": "{number}s", "relative_time.today": "hôm nay", + "remove_quote_hint.button_label": "Đã hiểu", + "remove_quote_hint.message": "Bạn cũng có thể làm trong menu tùy chọn {icon}", + "remove_quote_hint.title": "Gỡ tút mà bạn đã trích dẫn?", "reply_indicator.attachments": "{count, plural, other {# tập tin đính kèm}}", "reply_indicator.cancel": "Hủy bỏ", "reply_indicator.poll": "Vốt", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index bfb436e049..713147428f 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -768,6 +768,9 @@ "relative_time.minutes": "{number} 分钟前", "relative_time.seconds": "{number} 秒前", "relative_time.today": "今天", + "remove_quote_hint.button_label": "明白了", + "remove_quote_hint.message": "你可以通过 {icon} 选项菜单进行此操作。", + "remove_quote_hint.title": "是否需要删除你的引用嘟文?", "reply_indicator.attachments": "{count, plural, other {# 个附件}}", "reply_indicator.cancel": "取消", "reply_indicator.poll": "投票", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 10481b5e86..ee360d63bf 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -768,6 +768,9 @@ "relative_time.minutes": "{number} 分鐘前", "relative_time.seconds": "{number} 秒", "relative_time.today": "今天", + "remove_quote_hint.button_label": "了解", + "remove_quote_hint.message": "您能自 {icon} 選單完成此操作。", + "remove_quote_hint.title": "是否想要移除您的引用嘟文?", "reply_indicator.attachments": "{count, plural, other {# 個附加檔案}}", "reply_indicator.cancel": "取消", "reply_indicator.poll": "投票", diff --git a/app/javascript/mastodon/reducers/index.ts b/app/javascript/mastodon/reducers/index.ts index cbf22b3118..19ecbbfff4 100644 --- a/app/javascript/mastodon/reducers/index.ts +++ b/app/javascript/mastodon/reducers/index.ts @@ -101,9 +101,9 @@ const RootStateRecord = ImmutableRecord(initialRootState, 'RootState'); export const rootReducer = combineReducers(reducers, RootStateRecord); export function reducerWithInitialState( - stateOverrides: Record = {}, + ...stateOverrides: Record[] ) { - const initialStateRecord = mergeDeep(initialRootState, stateOverrides); + const initialStateRecord = mergeDeep(initialRootState, ...stateOverrides); const PatchedRootStateRecord = ImmutableRecord( initialStateRecord, 'RootState', diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index cea8949f23..43cf4e5342 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -117,6 +117,7 @@ const initialState = ImmutableMap({ 'explore/links': false, 'explore/statuses': false, 'explore/tags': false, + 'notifications/remove_quote_hint': false, }), }); diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index ef08d6b751..1b5867e8b9 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -201,6 +201,41 @@ } } + &.button--plain { + color: $highlight-text-color; + background: transparent; + padding: 6px; + + // The button has no outline, so we use negative margin to + // visually align its label with its surroundings while maintaining + // a generous click target + margin-inline: -6px; + border: 1px solid transparent; + + &:active, + &:focus, + &:hover { + border-color: transparent; + color: lighten($highlight-text-color, 4%); + background-color: transparent; + text-decoration: none; + } + + &:disabled, + &.disabled { + opacity: 0.7; + border-color: transparent; + color: $ui-button-disabled-color; + + &:active, + &:focus, + &:hover { + border-color: transparent; + color: $ui-button-disabled-color; + } + } + } + &.button-tertiary { background: transparent; padding: 6px 17px; @@ -436,8 +471,8 @@ } } -body > [data-popper-placement] { - z-index: 3; +[data-popper-placement] { + z-index: 9999; } .invisible { @@ -7092,7 +7127,8 @@ a.status-card { cursor: default; } -.media-gallery__alt__popover { +.info-tooltip { + color: $white; background: color.change($black, $alpha: 0.65); backdrop-filter: $backdrop-blur-filter; border-radius: 4px; @@ -7104,20 +7140,36 @@ a.status-card { max-height: 30em; overflow-y: auto; + &--solid { + color: var(--nested-card-text); + background: + /* This is a bit of a silly hack for layering two background colours + * since --nested-card-background is too transparent for a tooltip */ + linear-gradient( + var(--nested-card-background), + var(--nested-card-background) + ), + linear-gradient(var(--background-color), var(--background-color)); + border: var(--nested-card-border); + } + h4 { font-size: 15px; line-height: 20px; font-weight: 500; - color: $white; margin-bottom: 8px; } p { font-size: 15px; line-height: 20px; - color: color.change($white, $alpha: 0.85); + opacity: 0.85; white-space: pre-line; } + + .button { + margin-block-start: 8px; + } } .attachment-list { diff --git a/app/models/domain_allow.rb b/app/models/domain_allow.rb index 47ada7ac23..8eab3164e5 100644 --- a/app/models/domain_allow.rb +++ b/app/models/domain_allow.rb @@ -27,7 +27,7 @@ class DomainAllow < ApplicationRecord end def allowed_domains - select(:domain) + pluck(:domain) end def rule_for(domain) diff --git a/app/models/relay.rb b/app/models/relay.rb index 813a861c68..8a9524e9b3 100644 --- a/app/models/relay.rb +++ b/app/models/relay.rb @@ -34,7 +34,7 @@ class Relay < ApplicationRecord payload = Oj.dump(follow_activity(activity_id)) update!(state: :pending, follow_activity_id: activity_id) - DeliveryFailureTracker.reset!(inbox_url) + reset_delivery_tracker ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url) end @@ -43,12 +43,16 @@ class Relay < ApplicationRecord payload = Oj.dump(unfollow_activity(activity_id)) update!(state: :idle, follow_activity_id: nil) - DeliveryFailureTracker.reset!(inbox_url) + reset_delivery_tracker ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url) end private + def reset_delivery_tracker + DeliveryFailureTracker.reset!(inbox_url) + end + def follow_activity(activity_id) { '@context': ActivityPub::TagManager::CONTEXT, diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 332875e1ea..426a32770a 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -244,6 +244,15 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer canQuote: { automaticApproval: approved_uris, }, + canReply: { + always: 'https://www.w3.org/ns/activitystreams#Public', + }, + canLike: { + always: 'https://www.w3.org/ns/activitystreams#Public', + }, + canAnnounce: { + always: 'https://www.w3.org/ns/activitystreams#Public', + }, } end diff --git a/app/views/admin/shared/_status_attachments.html.haml b/app/views/admin/shared/_status_attachments.html.haml index 24af2b5f7d..d34a4221db 100644 --- a/app/views/admin/shared/_status_attachments.html.haml +++ b/app/views/admin/shared/_status_attachments.html.haml @@ -1,7 +1,7 @@ - if status.with_poll? .poll %ul - - status.preloadable_poll.options.each_with_index do |option, _index| + - status.preloadable_poll.options.each do |option| %li %label.poll__option.disabled<> - if status.preloadable_poll.multiple? diff --git a/app/workers/web/push_notification_worker.rb b/app/workers/web/push_notification_worker.rb index 32279a9e74..f9b1dc4f19 100644 --- a/app/workers/web/push_notification_worker.rb +++ b/app/workers/web/push_notification_worker.rb @@ -15,6 +15,15 @@ class Web::PushNotificationWorker return if @notification.updated_at < TTL.ago + # Clean up old Web::PushSubscriptions that were added before validation of + # the endpoint and keys: #30542, #30540 + unless @subscription.valid? + Rails.logger.debug { "Web::PushSubscription is invalid, removing: #{subscription_id}" } + @subscription.destroy! + + return + end + # Polymorphically associated activity could have been deleted # in the meantime, so we have to double-check before proceeding return unless @notification.activity.present? && @subscription.pushable?(@notification) diff --git a/config/locales/br.yml b/config/locales/br.yml index 38794dd75d..75e22a9530 100644 --- a/config/locales/br.yml +++ b/config/locales/br.yml @@ -63,6 +63,7 @@ br: all: Pep tra local: Lec'hel remote: A-bell + title: Lec'hiadur media_attachments: Restroù media stag moderation: active: Oberiant @@ -180,6 +181,7 @@ br: name: Anv registrations: confirm: Kadarnaat + save: Enrollañ status: Statud follow_recommendations: status: Statud @@ -260,6 +262,7 @@ br: title: Disklêriadennoù unresolved: Andiskoulmet updated_at: Nevesaet + view_profile: Gwelet ar profil roles: categories: devops: DevOps @@ -287,6 +290,7 @@ br: danger_zone: Takad dañjer discovery: privacy: Buhez prevez + profile_directory: Kavlec'h ar profiloù title: Dizoloadur trends: Luskadoù domain_blocks: @@ -310,6 +314,10 @@ br: strikes: actions: delete_statuses: Dilamet eo bet toudoù %{target} gant %{name} + tags: + title: Gerioù-klik + terms_of_service: + save_draft: Enrollañ ar brouilhed trends: allow: Aotren approved: Aprouet @@ -324,7 +332,7 @@ br: dashboard: tag_uses_measure: implijoù hollek not_usable: N'haller ket en implijout - title: Hashtagoù diouzh ar c'hiz + title: Gerioù-klik diouzh ar c'hiz warning_presets: add_new: Ouzhpenniñ unan nevez delete: Dilemel @@ -342,7 +350,7 @@ br: none: ur c'hemenn diwall new_trends: new_trending_tags: - title: Hashtagoù diouzh ar c'hiz + title: Gerioù-klik diouzh ar c'hiz appearance: discovery: Dizoloadur localization: @@ -350,6 +358,7 @@ br: guide_link: https://crowdin.com/project/mastodon application_mailer: view: 'Sellet :' + view_profile: Gwelet ar profil view_status: Gwelet ar c'hannad auth: delete_account: Dilemel ar gont @@ -396,6 +405,8 @@ br: created_at: Deiziad title_actions: none: Diwall + edit_profile: + other: All emoji_styles: auto: Emgefreek twemoji: Twemoji @@ -422,6 +433,8 @@ br: other: "%{count} a gannadoù" two: "%{count} gannad" title: Siloù + new: + save: Enrollañ ar sil nevez statuses: index: title: Toudoù silet @@ -547,7 +560,7 @@ br: back: Distreiñ da vMastodon development: Diorren edit_profile: Kemmañ ar profil - featured_tags: Hashtagoù pennañ + featured_tags: Gerioù-klik pennañ import: Enporzhiañ import_and_export: Enporzhiañ hag ezporzhiañ preferences: Gwellvezioù @@ -609,6 +622,9 @@ br: edit_profile_title: Personelaat ho profil feature_action: Gouzout hiroc'h follow_action: Heuliañ + hashtags_title: Gerioù-klik diouzh ar c'hiz + hashtags_view_more: Gwelet muioc'h a c'herioù-klik diouzh ar c'hiz + share_title: Rannit ho kont Mastodon sign_in_action: Kevreañ subject: Donemat e Mastodon title: Degemer mat e bourzh, %{name}! diff --git a/config/locales/cy.yml b/config/locales/cy.yml index 3d590069af..917b401d2e 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -1894,6 +1894,9 @@ cy: self_vote: Nid oes modd i chi bleidleisio yn eich polau eich hun too_few_options: rhaid cael mwy nag un eitem too_many_options: ni all gynnwys mwy na %{max} eitem + vote: Pleidleisio + posting_defaults: + explanation: Bydd y gosodiadau hyn yn cael eu defnyddio fel rhagosodiadau pan fyddwch chi'n creu postiadau newydd, ond gallwch chi eu golygu fesul postiad o fewn y cyfansoddwr. preferences: other: Arall posting_defaults: Rhagosodiadau postio diff --git a/config/locales/en.yml b/config/locales/en.yml index ebbb72fb07..7a9cb9f506 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -367,7 +367,7 @@ en: title: Custom emojis uncategorized: Uncategorized unlist: Unlist - unlisted: Quiet public + unlisted: Unlisted update_failed_msg: Could not update that emoji updated_msg: Emoji successfully updated! upload: Upload @@ -1923,7 +1923,7 @@ en: private_long: Only show to followers public: Public public_long: Anyone on and off Mastodon - unlisted: Unlisted + unlisted: Quiet public unlisted_long: Hidden from Mastodon search results, trending, and public timelines statuses_cleanup: enabled: Automatically delete old posts diff --git a/config/locales/eo.yml b/config/locales/eo.yml index 9c5060315e..c5b91c89af 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -581,6 +581,8 @@ eo: all: Ĉiuj limited: Limigita title: Moderigado + moderation_notes: + title: Notoj de moderigado private_comment: Privata komento public_comment: Publika komento purge: Purigu @@ -1081,12 +1083,18 @@ eo: trending: Popularaĵoj username_blocks: add_new: Aldoni novan + comparison: + contains: Enhavas + equals: Egalas + contains_html: Enhavas %{string} delete: Forigi edit: title: Redakti uzantnoman regulon + matches_exactly_html: Egalas %{string} new: create: Krei regulon title: Krei novan uzantnoman regulon + no_username_block_selected: Neniuj uzantnomaj reguloj estis ŝanĝitaj ĉar neniuj estis elektitaj not_permitted: Ne permesita title: Uzantnomaj reguloj updated_msg: Sukcese ĝisdatigita uzantnoma regulo @@ -1356,6 +1364,8 @@ eo: other: Alia emoji_styles: auto: Aŭtomata + native: Denaska + twemoji: Twemoji errors: '400': La peto kiun vi sendis estas nevalida au malformas. '403': Vi ne havas la rajton por vidi ĉi tiun paĝon. @@ -1720,6 +1730,8 @@ eo: too_few_options: devas enhavi pli da unu propono too_many_options: ne povas enhavi pli da %{max} proponoj vote: Voĉdoni + posting_defaults: + explanation: Ĉi tiuj agordoj estos uzataj kiel defaŭltaj kiam vi kreas novajn afiŝojn, sed vi povas redakti ilin per afiŝo en la redaktilo. preferences: other: Aliaj aferoj posting_defaults: Afiŝaj defaŭltoj @@ -1944,6 +1956,9 @@ eo: does_not_match_previous_name: ne kongruas kun la antaŭa nomo terms_of_service: title: Kondiĉoj de uzado + terms_of_service_interstitial: + past_preamble_html: Ni ŝanĝis niajn servkondiĉojn ekde via lasta vizito. Ni instigas vin revizii la ĝisdatigitajn kondiĉojn. + title: La servkondiĉoj de %{domain} ŝanĝiĝas themes: contrast: Mastodon (Forta kontrasto) default: Mastodon (Malhela) diff --git a/config/locales/lv.yml b/config/locales/lv.yml index 7c296022f5..269c31aeda 100644 --- a/config/locales/lv.yml +++ b/config/locales/lv.yml @@ -1549,8 +1549,8 @@ lv: muting: Importē noklusinātos kontus type: Importa veids type_groups: - constructive: Seko un Grāmatzīmes - destructive: Bloķētie un noklusinātie + constructive: Seko un grāmatzīmes + destructive: Liegtie un noklusinātie types: blocking: Bloķēšanas saraksts bookmarks: Grāmatzīmes diff --git a/config/locales/simple_form.br.yml b/config/locales/simple_form.br.yml index b415640eb0..c0a2fb38a4 100644 --- a/config/locales/simple_form.br.yml +++ b/config/locales/simple_form.br.yml @@ -2,6 +2,8 @@ br: simple_form: hints: + account: + note: 'Gallout a rit @menegiñ tud all pe #gerioù-klik.' defaults: avatar: WEBP, PNG, GIF pe JPG. Bihanoc'h eget %{size}. A vo izelaet betek %{dimensions}px header: WEBP, PNG, GIF pe JPG. Bihanoc'h eget %{size}. A vo izelaet betek %{dimensions}px @@ -56,7 +58,7 @@ br: username: Anv whole_word: Ger a-bezh featured_tag: - name: Hashtag + name: Ger-klik invite: comment: Evezhiadenn invite_request: @@ -71,8 +73,8 @@ br: hint: Titouroù ouzhpenn text: Reolenn tag: - name: Hashtag - trendable: Aotren an hashtag-mañ da zont war wel dindan tuadurioù + name: Ger-klik + trendable: Aotren ar ger-klik-mañ da zont war wel dindan tuadurioù user: role: Roll time_zone: Gwerzhid eur diff --git a/config/locales/simple_form.eo.yml b/config/locales/simple_form.eo.yml index d2fb53b6f1..a10fdbdcd0 100644 --- a/config/locales/simple_form.eo.yml +++ b/config/locales/simple_form.eo.yml @@ -56,10 +56,12 @@ eo: scopes: Kiujn API-ojn la aplikaĵo permesiĝos atingi. Se vi elektas supran amplekson, vi ne bezonas elekti la individuajn. setting_aggregate_reblogs: Ne montri novajn plusendojn de mesaĝoj lastatempe plusenditaj (nur efikas al nove ricevitaj plusendoj) setting_always_send_emails: Normale, la sciigoj per retpoŝto ne estos senditaj kiam vi uzas Mastodon aktive + setting_default_quote_policy: Ĉi tiu agordo nur validos por afiŝoj kreitaj per la sekva versio de Mastodon, sed vi povas elekti vian preferon prepare. setting_default_sensitive: La tiklaj vidaŭdaĵoj estas implicite kaŝitaj kaj povas esti malkaŝitaj per alklako setting_display_media_default: Kaŝi plurmediojn markitajn kiel tiklaj setting_display_media_hide_all: Ĉiam kaŝi la vidaŭdaĵojn setting_display_media_show_all: Ĉiam montri la vidaŭdaĵojn + setting_emoji_style: Kiel montri emoĝiojn. "Aŭtomata" provos uzi denaskajn emoĝiojn, sed uzas Twemoji por malnovaj retumiloj. setting_system_scrollbars_ui: Aplikas nur por surtablaj retumiloj baziĝas de Safari kaj Chrome setting_use_blurhash: Transirojn estas bazita sur la koloroj de la kaŝitaj aŭdovidaĵoj sed ne montri iun ajn detalon setting_use_pending_items: Kaŝi tempoliniajn ĝisdatigojn malantaŭ klako anstataŭ aŭtomate rulumi la fluon @@ -148,6 +150,9 @@ eo: min_age: Ne devus esti malsupre la minimuma aĝo postulita de la leĝoj de via jurisdikcio. user: chosen_languages: Kun tio markita nur mesaĝoj en elektitaj lingvoj aperos en publikaj tempolinioj + date_of_birth: + one: Ni devas certigi, ke vi estas almenaŭ %{count}-jaraĝa por uzi %{domain}. Ni ne konservos ĉi tion. + other: Ni devas certigi, ke vi estas almenaŭ %{count}-jaraĝaj por uzi %{domain}. Ni ne konservos ĉi tion. role: La rolo kontrolas kiujn permesojn la uzanto havas. user_role: color: Koloro uzita por la rolo sur la UI, kun RGB-formato @@ -227,6 +232,8 @@ eo: setting_auto_play_gif: Aŭtomate ekigi GIF-ojn setting_boost_modal: Montri konfirman fenestron antaŭ ol diskonigi mesaĝon setting_default_language: Publikada lingvo + setting_default_privacy: Afiŝa videblo + setting_default_quote_policy: Kiu povas citi setting_default_sensitive: Ĉiam marki la vidaŭdaĵojn kiel tiklaj setting_delete_modal: Montri konfirman fenestron antaŭ ol forigi mesaĝon setting_disable_hover_cards: Malebligi profilan antaŭmontron kiam oni musumas @@ -235,6 +242,7 @@ eo: setting_display_media_default: Implicita setting_display_media_hide_all: Kaŝi ĉiujn setting_display_media_show_all: Montri ĉiujn + setting_emoji_style: Emoĝio-stilo setting_expand_spoilers: Ĉiam malfoldas mesaĝojn markitajn per averto pri enhavo setting_hide_network: Kaŝi viajn sekvantojn kaj sekvatojn setting_missing_alt_text_modal: Montru konfirman dialogon antaŭ afiŝado de aŭdvidaĵoj sen altteksto @@ -317,6 +325,7 @@ eo: follow_request: Sendi retmesaĝon kiam iu petas sekvi vin mention: Sendi retmesaĝon kiam iu mencias vin pending_account: Sendi retmesaĝon kiam nova konto bezonas kontrolon + quote: Iu citis vin reblog: Sendi retmesaĝon kiam iu diskonigas vian mesaĝon report: Nova raporto estas proponita software_updates: @@ -363,6 +372,8 @@ eo: name: Nomo permissions_as_keys: Permesoj position: Prioritato + username_block: + allow_with_approval: Permesi aliĝojn kun aprobo webhook: events: Ŝaltitaj eventoj template: Utilŝarĝa ŝablono diff --git a/lib/paperclip/color_extractor.rb b/lib/paperclip/color_extractor.rb index e0cdfdb523..62daa07795 100644 --- a/lib/paperclip/color_extractor.rb +++ b/lib/paperclip/color_extractor.rb @@ -182,9 +182,9 @@ module Paperclip t += 1 if t.negative? t -= 1 if t > 1 - return (p + ((q - p) * 6 * t)) if t < 1 / 6.0 + return p + ((q - p) * 6 * t) if t < 1 / 6.0 return q if t < 1 / 2.0 - return (p + ((q - p) * ((2 / 3.0) - t) * 6)) if t < 2 / 3.0 + return p + ((q - p) * ((2 / 3.0) - t) * 6) if t < 2 / 3.0 p end diff --git a/spec/controllers/admin/export_domain_allows_controller_spec.rb b/spec/controllers/admin/export_domain_allows_controller_spec.rb index dcb1f55a99..7879a5e181 100644 --- a/spec/controllers/admin/export_domain_allows_controller_spec.rb +++ b/spec/controllers/admin/export_domain_allows_controller_spec.rb @@ -17,17 +17,6 @@ RSpec.describe Admin::ExportDomainAllowsController do end end - describe 'GET #export' do - it 'renders instances' do - Fabricate(:domain_allow, domain: 'good.domain') - Fabricate(:domain_allow, domain: 'better.domain') - - get :export, params: { format: :csv } - expect(response).to have_http_status(200) - expect(response.body).to eq(domain_allows_csv_file) - end - end - describe 'POST #import' do it 'allows imported domains' do post :import, params: { admin_import: { data: fixture_file_upload('domain_allows.csv') } } @@ -50,10 +39,4 @@ RSpec.describe Admin::ExportDomainAllowsController do expect(flash[:error]).to eq(I18n.t('admin.export_domain_allows.no_file')) end end - - private - - def domain_allows_csv_file - File.read(File.join(file_fixture_path, 'domain_allows.csv')) - end end diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb index a110717166..04c2d5dbbb 100644 --- a/spec/controllers/auth/registrations_controller_spec.rb +++ b/spec/controllers/auth/registrations_controller_spec.rb @@ -342,42 +342,6 @@ RSpec.describe Auth::RegistrationsController do end end - context 'when age verification is enabled' do - subject { post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', agreement: 'true' }.merge(date_of_birth) } } - - before do - Setting.min_age = 16 - end - - let(:date_of_birth) { {} } - - context 'when date of birth is below age limit' do - let(:date_of_birth) { 13.years.ago.then { |date| { 'date_of_birth(1i)': date.day.to_s, 'date_of_birth(2i)': date.month.to_s, 'date_of_birth(3i)': date.year.to_s } } } - - it 'does not create user' do - subject - user = User.find_by(email: 'test@example.com') - expect(user).to be_nil - end - end - - context 'when date of birth is above age limit' do - let(:date_of_birth) { 17.years.ago.then { |date| { 'date_of_birth(1i)': date.day.to_s, 'date_of_birth(2i)': date.month.to_s, 'date_of_birth(3i)': date.year.to_s } } } - - it 'redirects to setup and creates user' do - subject - - expect(response).to redirect_to auth_setup_path - - expect(User.find_by(email: 'test@example.com')) - .to be_present - .and have_attributes( - age_verified_at: not_eq(nil) - ) - end - end - end - it_behaves_like 'registration mode based responses', :create end diff --git a/spec/models/domain_allow_spec.rb b/spec/models/domain_allow_spec.rb index fbb324657e..0c69aaff8d 100644 --- a/spec/models/domain_allow_spec.rb +++ b/spec/models/domain_allow_spec.rb @@ -12,4 +12,19 @@ RSpec.describe DomainAllow do it { is_expected.to_not allow_value('xn--r9j5b5b').for(:domain) } end end + + describe '.allowed_domains' do + subject { described_class.allowed_domains } + + context 'without domain allows' do + it { is_expected.to be_an(Array).and(be_empty) } + end + + context 'with domain allows' do + let!(:allowed_domain) { Fabricate :domain_allow } + let!(:other_allowed_domain) { Fabricate :domain_allow } + + it { is_expected.to contain_exactly(allowed_domain.domain, other_allowed_domain.domain) } + end + end end diff --git a/spec/models/relay_spec.rb b/spec/models/relay_spec.rb index 9c917e2d2e..03758ca6a8 100644 --- a/spec/models/relay_spec.rb +++ b/spec/models/relay_spec.rb @@ -33,7 +33,7 @@ RSpec.describe Relay do describe 'Callbacks' do describe 'Ensure disabled on destroy' do - before { stub_delivery_worker } + before { stub_services } context 'when relay is enabled' do let(:relay) { Fabricate :relay, state: :accepted } @@ -71,7 +71,7 @@ RSpec.describe Relay do describe '#disable' do let(:relay) { Fabricate :relay, state: :accepted, follow_activity_id: 'https://host.example/123' } - before { stub_delivery_worker } + before { stub_services } it 'changes state to idle and removes the activity id' do expect { relay.disable! } @@ -79,13 +79,15 @@ RSpec.describe Relay do .and change { relay.reload.follow_activity_id }.to(be_nil) expect(ActivityPub::DeliveryWorker) .to have_received(:perform_async).with(match('Undo'), Account.representative.id, relay.inbox_url) + expect(DeliveryFailureTracker) + .to have_received(:reset!).with(relay.inbox_url) end end describe '#enable' do let(:relay) { Fabricate :relay, state: :idle, follow_activity_id: '' } - before { stub_delivery_worker } + before { stub_services } it 'changes state to pending and populates the activity id' do expect { relay.enable! } @@ -93,10 +95,13 @@ RSpec.describe Relay do .and change { relay.reload.follow_activity_id }.to(be_present) expect(ActivityPub::DeliveryWorker) .to have_received(:perform_async).with(match('Follow'), Account.representative.id, relay.inbox_url) + expect(DeliveryFailureTracker) + .to have_received(:reset!).with(relay.inbox_url) end end - def stub_delivery_worker + def stub_services allow(ActivityPub::DeliveryWorker).to receive(:perform_async) + allow(DeliveryFailureTracker).to receive(:reset!) end end diff --git a/spec/requests/admin/export_domain_allows_spec.rb b/spec/requests/admin/export_domain_allows_spec.rb index 761c39984e..dd1a55a022 100644 --- a/spec/requests/admin/export_domain_allows_spec.rb +++ b/spec/requests/admin/export_domain_allows_spec.rb @@ -3,9 +3,9 @@ require 'rails_helper' RSpec.describe 'Admin Export Domain Allows' do - describe 'POST /admin/export_domain_allows/import' do - before { sign_in Fabricate(:admin_user) } + before { sign_in Fabricate(:admin_user) } + describe 'POST /admin/export_domain_allows/import' do it 'gracefully handles invalid nested params' do post import_admin_export_domain_allows_path(admin_import: 'invalid') @@ -13,4 +13,28 @@ RSpec.describe 'Admin Export Domain Allows' do .to redirect_to(admin_instances_path) end end + + describe 'GET /admin/export_domain_allows/export.csv' do + before do + Fabricate(:domain_allow, domain: 'good.domain') + Fabricate(:domain_allow, domain: 'better.domain') + end + + it 'returns CSV response with instance domain values' do + get export_admin_export_domain_allows_path(format: :csv) + + expect(response) + .to have_http_status(200) + expect(response.body) + .to eq(domain_allows_csv_file) + expect(response.media_type) + .to eq('text/csv') + end + end + + private + + def domain_allows_csv_file + File.read(File.join(file_fixture_path, 'domain_allows.csv')) + end end diff --git a/spec/requests/api/v1/accounts_spec.rb b/spec/requests/api/v1/accounts_spec.rb index a040174d38..0e64915baf 100644 --- a/spec/requests/api/v1/accounts_spec.rb +++ b/spec/requests/api/v1/accounts_spec.rb @@ -110,9 +110,12 @@ RSpec.describe '/api/v1/accounts' do let(:date_of_birth) { 13.years.ago.strftime('%d.%m.%Y') } it 'returns http unprocessable entity' do - subject + expect { subject } + .to not_change(User, :count) + .and not_change(Account, :count) - expect(response).to have_http_status(422) + expect(response) + .to have_http_status(422) expect(response.content_type) .to start_with('application/json') end @@ -122,9 +125,27 @@ RSpec.describe '/api/v1/accounts' do let(:date_of_birth) { 17.years.ago.strftime('%d.%m.%Y') } it 'creates a user', :aggregate_failures do - subject + expect { subject } + .to change(User, :count).by(1) + .and change(Account, :count).by(1) - expect(response).to have_http_status(200) + expect(response) + .to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + end + end + + context 'when date of birth is over age limit in ISO-8601 format' do + let(:date_of_birth) { 17.years.ago.to_date.iso8601 } + + it 'creates a user', :aggregate_failures do + expect { subject } + .to change(User, :count).by(1) + .and change(Account, :count).by(1) + + expect(response) + .to have_http_status(200) expect(response.content_type) .to start_with('application/json') end diff --git a/spec/system/auth/registrations_spec.rb b/spec/system/auth/registrations_spec.rb new file mode 100644 index 0000000000..4c08bf47ee --- /dev/null +++ b/spec/system/auth/registrations_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Auth Registration' do + context 'when age verification is enabled' do + before { Setting.min_age = 16 } + + context 'when date of birth is below age limit' do + let(:date_of_birth) { 13.years.ago } + + it 'does not create user record and displays errors' do + visit new_user_registration_path + expect(page) + .to have_title(I18n.t('auth.register')) + + expect { fill_in_and_submit_form } + .to not_change(User, :count) + expect(page) + .to have_content(/error below/) + end + end + + context 'when date of birth is above age limit' do + let(:date_of_birth) { 17.years.ago } + + it 'creates user and marks as verified' do + visit new_user_registration_path + expect(page) + .to have_title(I18n.t('auth.register')) + + expect { fill_in_and_submit_form } + .to change(User, :count).by(1) + expect(User.last) + .to have_attributes(email: 'test@example.com', age_verified_at: be_present) + expect(page) + .to have_content(I18n.t('auth.setup.title')) + end + end + + def fill_in_and_submit_form + # Avoid the registration spam check + travel_to 10.seconds.from_now + + fill_in 'user_account_attributes_username', with: 'test' + fill_in 'user_email', with: 'test@example.com' + fill_in 'user_password', with: 'Test.123.Pass' + fill_in 'user_password_confirmation', with: 'Test.123.Pass' + check 'user_agreement' + + find('input[aria-label="Day"]').fill_in with: date_of_birth.day + find('input[autocomplete="bday-month"]').fill_in with: date_of_birth.month + find('input[autocomplete="bday-year"]').fill_in with: date_of_birth.year + + click_on I18n.t('auth.register') + end + end +end diff --git a/spec/workers/web/push_notification_worker_spec.rb b/spec/workers/web/push_notification_worker_spec.rb index d18d6c4d68..7ef4cd4fe5 100644 --- a/spec/workers/web/push_notification_worker_spec.rb +++ b/spec/workers/web/push_notification_worker_spec.rb @@ -36,6 +36,9 @@ RSpec.describe Web::PushNotificationWorker do let(:std_input) { 'When I grow up, I want to be a watermelon' } let(:std_ciphertext) { 'DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_yl95bQpu6cVPTpK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_Qulcy4a-fN' } + # Invalid subscription: + let(:invalid_subscription) { Fabricate.build(:web_push_subscription, user_id: user.id, key_p256dh: 'invalid', key_auth: 'invalid', endpoint: endpoint, standard: true, data: { alerts: { notification.type => true } }) } + describe 'perform' do around do |example| original_private = Rails.configuration.x.vapid.private_key @@ -83,6 +86,25 @@ RSpec.describe Web::PushNotificationWorker do end # rubocop:enable RSpec/SubjectStub + context 'with invalid record that will fail' do + before do + # Fabricator always runs validation, here we deliberately want to bypass + # the validation, simulating an invalid Web::PushSubscription that was + # created before PRs #30542, #30540 added validation. + invalid_subscription.save(validate: false) + end + + it 'removes the record and does not process the request' do + expect { subject.perform(invalid_subscription.id, notification.id) } + .to_not raise_error + + expect { invalid_subscription.reload } + .to raise_error ActiveRecord::RecordNotFound + + expect(a_request(:post, endpoint)).to_not have_been_made + end + end + def legacy_web_push_endpoint_request a_request( :post, diff --git a/yarn.lock b/yarn.lock index e3a712e7e7..d96694452f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3180,8 +3180,8 @@ __metadata: linkType: hard "@reduxjs/toolkit@npm:^2.0.1": - version: 2.8.2 - resolution: "@reduxjs/toolkit@npm:2.8.2" + version: 2.9.0 + resolution: "@reduxjs/toolkit@npm:2.9.0" dependencies: "@standard-schema/spec": "npm:^1.0.0" "@standard-schema/utils": "npm:^0.3.0" @@ -3197,7 +3197,7 @@ __metadata: optional: true react-redux: optional: true - checksum: 10c0/6a7a33bad5f1100340757151ff86ca0c4c248f030ae56ce0e6f1d98b39fa87c8f193e9faa2ebd6d5a4c0416921e9f9f7a2bbdd81156c39f08f6bf5ce70c2b927 + checksum: 10c0/eef65436b3cd96a264de09e94b8a9d585773578442ef3c1c5f2b3bb261a727405e89b004965198f95c5391645b7dbc6576dc07b46de1bede1d6c62c13c17c7d0 languageName: node linkType: hard @@ -11973,8 +11973,8 @@ __metadata: linkType: hard "sass@npm:^1.62.1": - version: 1.91.0 - resolution: "sass@npm:1.91.0" + version: 1.92.0 + resolution: "sass@npm:1.92.0" dependencies: "@parcel/watcher": "npm:^2.4.1" chokidar: "npm:^4.0.0" @@ -11985,7 +11985,7 @@ __metadata: optional: true bin: sass: sass.js - checksum: 10c0/5be1c98f7a618cb5f90b62f63d2aa0f78f9bf369c93ec7cd9880752a26b0ead19aa63cc341e8a26ce6c74d080baa5705f1685dff52fe6a3f28a7828ae50182b6 + checksum: 10c0/bdff9fa6988620e2a81962efdd016e3894d19934cfadc105cf41db767f59dd47afd8ff32840e612ef700cb67e19d9e83c108f1724eb8f0bef56c4877dbe6f14d languageName: node linkType: hard