From 44cd88adc4e2f4028dcc2b08b98368f0dc90cee4 Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Wed, 31 May 2023 23:43:39 +0200 Subject: [PATCH 01/92] Upgrade react-intl (#24906) --- .eslintrc.js | 1 + .github/dependabot.yml | 12 - .github/workflows/check-i18n.yml | 3 +- .../mastodon/actions/notifications.js | 2 +- app/javascript/mastodon/components/domain.tsx | 11 +- .../mastodon/components/load_gap.tsx | 10 +- .../components/relative_timestamp.tsx | 14 +- .../mastodon/containers/admin_component.jsx | 9 +- .../mastodon/containers/compose_container.jsx | 9 +- .../mastodon/containers/mastodon.jsx | 9 +- .../mastodon/containers/media_container.jsx | 9 +- .../mastodon/features/onboarding/follows.jsx | 4 +- .../mastodon/features/onboarding/share.jsx | 8 +- app/javascript/mastodon/load_locale.js | 14 + .../mastodon/locales/defaultMessages.json | 4484 ----------------- app/javascript/mastodon/locales/index.js | 13 + .../mastodon/locales/whitelist_af.json | 2 - .../mastodon/locales/whitelist_an.json | 2 - .../mastodon/locales/whitelist_ar.json | 2 - .../mastodon/locales/whitelist_ast.json | 2 - .../mastodon/locales/whitelist_be.json | 2 - .../mastodon/locales/whitelist_bg.json | 2 - .../mastodon/locales/whitelist_bn.json | 2 - .../mastodon/locales/whitelist_br.json | 2 - .../mastodon/locales/whitelist_bs.json | 2 - .../mastodon/locales/whitelist_ca.json | 2 - .../mastodon/locales/whitelist_ckb.json | 2 - .../mastodon/locales/whitelist_co.json | 2 - .../mastodon/locales/whitelist_cs.json | 2 - .../mastodon/locales/whitelist_csb.json | 2 - .../mastodon/locales/whitelist_cy.json | 2 - .../mastodon/locales/whitelist_da.json | 2 - .../mastodon/locales/whitelist_de.json | 5 - .../mastodon/locales/whitelist_el.json | 2 - .../mastodon/locales/whitelist_en-GB.json | 2 - .../mastodon/locales/whitelist_en.json | 2 - .../mastodon/locales/whitelist_eo.json | 2 - .../mastodon/locales/whitelist_es-AR.json | 2 - .../mastodon/locales/whitelist_es-MX.json | 2 - .../mastodon/locales/whitelist_es.json | 2 - .../mastodon/locales/whitelist_et.json | 2 - .../mastodon/locales/whitelist_eu.json | 2 - .../mastodon/locales/whitelist_fa.json | 2 - .../mastodon/locales/whitelist_fi.json | 2 - .../mastodon/locales/whitelist_fo.json | 2 - .../mastodon/locales/whitelist_fr-QC.json | 2 - .../mastodon/locales/whitelist_fr.json | 2 - .../mastodon/locales/whitelist_fy.json | 2 - .../mastodon/locales/whitelist_ga.json | 2 - .../mastodon/locales/whitelist_gd.json | 2 - .../mastodon/locales/whitelist_gl.json | 2 - .../mastodon/locales/whitelist_he.json | 2 - .../mastodon/locales/whitelist_hi.json | 2 - .../mastodon/locales/whitelist_hr.json | 2 - .../mastodon/locales/whitelist_hu.json | 2 - .../mastodon/locales/whitelist_hy.json | 2 - .../mastodon/locales/whitelist_id.json | 2 - .../mastodon/locales/whitelist_ig.json | 2 - .../mastodon/locales/whitelist_io.json | 2 - .../mastodon/locales/whitelist_is.json | 2 - .../mastodon/locales/whitelist_it.json | 2 - .../mastodon/locales/whitelist_ja.json | 2 - .../mastodon/locales/whitelist_ka.json | 2 - .../mastodon/locales/whitelist_kab.json | 2 - .../mastodon/locales/whitelist_kk.json | 2 - .../mastodon/locales/whitelist_kn.json | 2 - .../mastodon/locales/whitelist_ko.json | 2 - .../mastodon/locales/whitelist_ku.json | 2 - .../mastodon/locales/whitelist_kw.json | 2 - .../mastodon/locales/whitelist_la.json | 2 - .../mastodon/locales/whitelist_lt.json | 2 - .../mastodon/locales/whitelist_lv.json | 2 - .../mastodon/locales/whitelist_mk.json | 2 - .../mastodon/locales/whitelist_ml.json | 2 - .../mastodon/locales/whitelist_mr.json | 2 - .../mastodon/locales/whitelist_ms.json | 2 - .../mastodon/locales/whitelist_my.json | 2 - .../mastodon/locales/whitelist_nl.json | 2 - .../mastodon/locales/whitelist_nn.json | 2 - .../mastodon/locales/whitelist_no.json | 2 - .../mastodon/locales/whitelist_oc.json | 2 - .../mastodon/locales/whitelist_pa.json | 2 - .../mastodon/locales/whitelist_pl.json | 2 - .../mastodon/locales/whitelist_pt-BR.json | 2 - .../mastodon/locales/whitelist_pt-PT.json | 2 - .../mastodon/locales/whitelist_ro.json | 2 - .../mastodon/locales/whitelist_ru.json | 2 - .../mastodon/locales/whitelist_sa.json | 2 - .../mastodon/locales/whitelist_sc.json | 2 - .../mastodon/locales/whitelist_sco.json | 2 - .../mastodon/locales/whitelist_si.json | 2 - .../mastodon/locales/whitelist_sk.json | 2 - .../mastodon/locales/whitelist_sl.json | 2 - .../mastodon/locales/whitelist_sq.json | 2 - .../mastodon/locales/whitelist_sr-Latn.json | 2 - .../mastodon/locales/whitelist_sr.json | 2 - .../mastodon/locales/whitelist_sv.json | 2 - .../mastodon/locales/whitelist_szl.json | 2 - .../mastodon/locales/whitelist_ta.json | 2 - .../mastodon/locales/whitelist_tai.json | 2 - .../mastodon/locales/whitelist_te.json | 2 - .../mastodon/locales/whitelist_th.json | 2 - .../mastodon/locales/whitelist_tr.json | 2 - .../mastodon/locales/whitelist_tt.json | 2 - .../mastodon/locales/whitelist_ug.json | 2 - .../mastodon/locales/whitelist_uk.json | 2 - .../mastodon/locales/whitelist_ur.json | 2 - .../mastodon/locales/whitelist_uz.json | 2 - .../mastodon/locales/whitelist_vi.json | 2 - .../mastodon/locales/whitelist_zgh.json | 2 - .../mastodon/locales/whitelist_zh-CN.json | 2 - .../mastodon/locales/whitelist_zh-HK.json | 2 - .../mastodon/locales/whitelist_zh-TW.json | 2 - .../mastodon/polyfills/base_polyfills.ts | 2 - app/javascript/mastodon/polyfills/index.ts | 4 +- app/javascript/mastodon/polyfills/intl.ts | 105 + .../service_worker/web_push_locales.js | 2 +- .../service_worker/web_push_notifications.js | 2 +- app/javascript/packs/application.js | 3 +- app/javascript/packs/public.jsx | 6 +- app/javascript/packs/share.jsx | 4 +- app/views/layouts/application.html.haml | 2 +- app/views/layouts/embedded.html.haml | 2 +- babel.config.js | 2 +- config/formatjs-formatter.js | 11 + config/webpack/generateLocalePacks.js | 51 - config/webpack/shared.js | 6 - config/webpack/translationRunner.js | 102 +- package.json | 13 +- yarn.lock | 333 +- 130 files changed, 413 insertions(+), 5046 deletions(-) create mode 100644 app/javascript/mastodon/load_locale.js delete mode 100644 app/javascript/mastodon/locales/defaultMessages.json delete mode 100644 app/javascript/mastodon/locales/whitelist_af.json delete mode 100644 app/javascript/mastodon/locales/whitelist_an.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ar.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ast.json delete mode 100644 app/javascript/mastodon/locales/whitelist_be.json delete mode 100644 app/javascript/mastodon/locales/whitelist_bg.json delete mode 100644 app/javascript/mastodon/locales/whitelist_bn.json delete mode 100644 app/javascript/mastodon/locales/whitelist_br.json delete mode 100644 app/javascript/mastodon/locales/whitelist_bs.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ca.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ckb.json delete mode 100644 app/javascript/mastodon/locales/whitelist_co.json delete mode 100644 app/javascript/mastodon/locales/whitelist_cs.json delete mode 100644 app/javascript/mastodon/locales/whitelist_csb.json delete mode 100644 app/javascript/mastodon/locales/whitelist_cy.json delete mode 100644 app/javascript/mastodon/locales/whitelist_da.json delete mode 100644 app/javascript/mastodon/locales/whitelist_de.json delete mode 100644 app/javascript/mastodon/locales/whitelist_el.json delete mode 100644 app/javascript/mastodon/locales/whitelist_en-GB.json delete mode 100644 app/javascript/mastodon/locales/whitelist_en.json delete mode 100644 app/javascript/mastodon/locales/whitelist_eo.json delete mode 100644 app/javascript/mastodon/locales/whitelist_es-AR.json delete mode 100644 app/javascript/mastodon/locales/whitelist_es-MX.json delete mode 100644 app/javascript/mastodon/locales/whitelist_es.json delete mode 100644 app/javascript/mastodon/locales/whitelist_et.json delete mode 100644 app/javascript/mastodon/locales/whitelist_eu.json delete mode 100644 app/javascript/mastodon/locales/whitelist_fa.json delete mode 100644 app/javascript/mastodon/locales/whitelist_fi.json delete mode 100644 app/javascript/mastodon/locales/whitelist_fo.json delete mode 100644 app/javascript/mastodon/locales/whitelist_fr-QC.json delete mode 100644 app/javascript/mastodon/locales/whitelist_fr.json delete mode 100644 app/javascript/mastodon/locales/whitelist_fy.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ga.json delete mode 100644 app/javascript/mastodon/locales/whitelist_gd.json delete mode 100644 app/javascript/mastodon/locales/whitelist_gl.json delete mode 100644 app/javascript/mastodon/locales/whitelist_he.json delete mode 100644 app/javascript/mastodon/locales/whitelist_hi.json delete mode 100644 app/javascript/mastodon/locales/whitelist_hr.json delete mode 100644 app/javascript/mastodon/locales/whitelist_hu.json delete mode 100644 app/javascript/mastodon/locales/whitelist_hy.json delete mode 100644 app/javascript/mastodon/locales/whitelist_id.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ig.json delete mode 100644 app/javascript/mastodon/locales/whitelist_io.json delete mode 100644 app/javascript/mastodon/locales/whitelist_is.json delete mode 100644 app/javascript/mastodon/locales/whitelist_it.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ja.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ka.json delete mode 100644 app/javascript/mastodon/locales/whitelist_kab.json delete mode 100644 app/javascript/mastodon/locales/whitelist_kk.json delete mode 100644 app/javascript/mastodon/locales/whitelist_kn.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ko.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ku.json delete mode 100644 app/javascript/mastodon/locales/whitelist_kw.json delete mode 100644 app/javascript/mastodon/locales/whitelist_la.json delete mode 100644 app/javascript/mastodon/locales/whitelist_lt.json delete mode 100644 app/javascript/mastodon/locales/whitelist_lv.json delete mode 100644 app/javascript/mastodon/locales/whitelist_mk.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ml.json delete mode 100644 app/javascript/mastodon/locales/whitelist_mr.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ms.json delete mode 100644 app/javascript/mastodon/locales/whitelist_my.json delete mode 100644 app/javascript/mastodon/locales/whitelist_nl.json delete mode 100644 app/javascript/mastodon/locales/whitelist_nn.json delete mode 100644 app/javascript/mastodon/locales/whitelist_no.json delete mode 100644 app/javascript/mastodon/locales/whitelist_oc.json delete mode 100644 app/javascript/mastodon/locales/whitelist_pa.json delete mode 100644 app/javascript/mastodon/locales/whitelist_pl.json delete mode 100644 app/javascript/mastodon/locales/whitelist_pt-BR.json delete mode 100644 app/javascript/mastodon/locales/whitelist_pt-PT.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ro.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ru.json delete mode 100644 app/javascript/mastodon/locales/whitelist_sa.json delete mode 100644 app/javascript/mastodon/locales/whitelist_sc.json delete mode 100644 app/javascript/mastodon/locales/whitelist_sco.json delete mode 100644 app/javascript/mastodon/locales/whitelist_si.json delete mode 100644 app/javascript/mastodon/locales/whitelist_sk.json delete mode 100644 app/javascript/mastodon/locales/whitelist_sl.json delete mode 100644 app/javascript/mastodon/locales/whitelist_sq.json delete mode 100644 app/javascript/mastodon/locales/whitelist_sr-Latn.json delete mode 100644 app/javascript/mastodon/locales/whitelist_sr.json delete mode 100644 app/javascript/mastodon/locales/whitelist_sv.json delete mode 100644 app/javascript/mastodon/locales/whitelist_szl.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ta.json delete mode 100644 app/javascript/mastodon/locales/whitelist_tai.json delete mode 100644 app/javascript/mastodon/locales/whitelist_te.json delete mode 100644 app/javascript/mastodon/locales/whitelist_th.json delete mode 100644 app/javascript/mastodon/locales/whitelist_tr.json delete mode 100644 app/javascript/mastodon/locales/whitelist_tt.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ug.json delete mode 100644 app/javascript/mastodon/locales/whitelist_uk.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ur.json delete mode 100644 app/javascript/mastodon/locales/whitelist_uz.json delete mode 100644 app/javascript/mastodon/locales/whitelist_vi.json delete mode 100644 app/javascript/mastodon/locales/whitelist_zgh.json delete mode 100644 app/javascript/mastodon/locales/whitelist_zh-CN.json delete mode 100644 app/javascript/mastodon/locales/whitelist_zh-HK.json delete mode 100644 app/javascript/mastodon/locales/whitelist_zh-TW.json create mode 100644 app/javascript/mastodon/polyfills/intl.ts create mode 100644 config/formatjs-formatter.js delete mode 100644 config/webpack/generateLocalePacks.js diff --git a/.eslintrc.js b/.eslintrc.js index bfade8976..24961cdd9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -293,6 +293,7 @@ module.exports = { '.*rc.js', 'ide-helper.js', 'config/webpack/**/*', + 'config/formatjs-formatter.js', ], env: { diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 39ae6bc08..5e6556cb5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -25,18 +25,6 @@ updates: - dependency-name: 'react-hotkeys' versions: - '>= 2' - # TODO: This version has breaking changes - - dependency-name: 'intl-messageformat' - versions: - - '>= 3' - # TODO: This version has breaking changes - - dependency-name: 'react-intl' - versions: - - '>= 3' - # TODO: This version has breaking changes - - dependency-name: 'babel-plugin-react-intl' - versions: - - '>= 7' # TODO: This version requires code changes - dependency-name: 'webpack-dev-server' versions: diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml index e282e2ab7..b67c503e9 100644 --- a/.github/workflows/check-i18n.yml +++ b/.github/workflows/check-i18n.yml @@ -41,8 +41,7 @@ jobs: - name: Check for missing strings in English JSON run: | - yarn build:development - yarn manage:translations en + yarn i18n:extract --throws git diff --exit-code - name: Check locale file normalization diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index c040edb58..6e8ddb227 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -1,4 +1,4 @@ -import IntlMessageFormat from 'intl-messageformat'; +import { IntlMessageFormat } from 'intl-messageformat'; import { defineMessages } from 'react-intl'; import { List as ImmutableList } from 'immutable'; diff --git a/app/javascript/mastodon/components/domain.tsx b/app/javascript/mastodon/components/domain.tsx index db18635be..f4a3b9d4b 100644 --- a/app/javascript/mastodon/components/domain.tsx +++ b/app/javascript/mastodon/components/domain.tsx @@ -1,7 +1,6 @@ import { useCallback } from 'react'; -import type { InjectedIntl } from 'react-intl'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, useIntl } from 'react-intl'; import { IconButton } from './icon_button'; @@ -15,9 +14,11 @@ const messages = defineMessages({ interface Props { domain: string; onUnblockDomain: (domain: string) => void; - intl: InjectedIntl; } -const _Domain: React.FC = ({ domain, onUnblockDomain, intl }) => { + +export const Domain: React.FC = ({ domain, onUnblockDomain }) => { + const intl = useIntl(); + const handleDomainUnblock = useCallback(() => { onUnblockDomain(domain); }, [domain, onUnblockDomain]); @@ -41,5 +42,3 @@ const _Domain: React.FC = ({ domain, onUnblockDomain, intl }) => { ); }; - -export const Domain = injectIntl(_Domain); diff --git a/app/javascript/mastodon/components/load_gap.tsx b/app/javascript/mastodon/components/load_gap.tsx index e6d3060eb..7e2cd447b 100644 --- a/app/javascript/mastodon/components/load_gap.tsx +++ b/app/javascript/mastodon/components/load_gap.tsx @@ -1,7 +1,6 @@ import { useCallback } from 'react'; -import type { InjectedIntl } from 'react-intl'; -import { injectIntl, defineMessages } from 'react-intl'; +import { useIntl, defineMessages } from 'react-intl'; import { Icon } from 'mastodon/components/icon'; @@ -13,10 +12,11 @@ interface Props { disabled: boolean; maxId: string; onClick: (maxId: string) => void; - intl: InjectedIntl; } -const _LoadGap: React.FC = ({ disabled, maxId, onClick, intl }) => { +export const LoadGap: React.FC = ({ disabled, maxId, onClick }) => { + const intl = useIntl(); + const handleClick = useCallback(() => { onClick(maxId); }, [maxId, onClick]); @@ -32,5 +32,3 @@ const _LoadGap: React.FC = ({ disabled, maxId, onClick, intl }) => { ); }; - -export const LoadGap = injectIntl(_LoadGap); diff --git a/app/javascript/mastodon/components/relative_timestamp.tsx b/app/javascript/mastodon/components/relative_timestamp.tsx index aaa424dca..e4a8437d0 100644 --- a/app/javascript/mastodon/components/relative_timestamp.tsx +++ b/app/javascript/mastodon/components/relative_timestamp.tsx @@ -1,6 +1,6 @@ import { Component } from 'react'; -import type { InjectedIntl } from 'react-intl'; +import type { IntlShape } from 'react-intl'; import { injectIntl, defineMessages } from 'react-intl'; const messages = defineMessages({ @@ -103,7 +103,7 @@ const getUnitDelay = (units: string) => { }; export const timeAgoString = ( - intl: InjectedIntl, + intl: IntlShape, date: Date, now: number, year: number, @@ -155,7 +155,7 @@ export const timeAgoString = ( }; const timeRemainingString = ( - intl: InjectedIntl, + intl: IntlShape, date: Date, now: number, timeGiven = true @@ -190,7 +190,7 @@ const timeRemainingString = ( }; interface Props { - intl: InjectedIntl; + intl: IntlShape; timestamp: string; year: number; futureDate?: boolean; @@ -201,7 +201,7 @@ interface States { } class RelativeTimestamp extends Component { state = { - now: this.props.intl.now(), + now: Date.now(), }; static defaultProps = { @@ -223,7 +223,7 @@ class RelativeTimestamp extends Component { UNSAFE_componentWillReceiveProps(nextProps: Props) { if (this.props.timestamp !== nextProps.timestamp) { - this.setState({ now: this.props.intl.now() }); + this.setState({ now: Date.now() }); } } @@ -253,7 +253,7 @@ class RelativeTimestamp extends Component { : Math.max(updateInterval, unitRemainder); this._timer = window.setTimeout(() => { - this.setState({ now: this.props.intl.now() }); + this.setState({ now: Date.now() }); }, delay); } diff --git a/app/javascript/mastodon/containers/admin_component.jsx b/app/javascript/mastodon/containers/admin_component.jsx index f5fa53f08..562151fe2 100644 --- a/app/javascript/mastodon/containers/admin_component.jsx +++ b/app/javascript/mastodon/containers/admin_component.jsx @@ -1,12 +1,11 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { IntlProvider, addLocaleData } from 'react-intl'; +import { IntlProvider } from 'react-intl'; -import { getLocale } from '../locales'; +import { getLocale, onProviderError } from '../locales'; -const { localeData, messages } = getLocale(); -addLocaleData(localeData); +const { messages } = getLocale(); export default class AdminComponent extends PureComponent { @@ -19,7 +18,7 @@ export default class AdminComponent extends PureComponent { const { locale, children } = this.props; return ( - + {children} ); diff --git a/app/javascript/mastodon/containers/compose_container.jsx b/app/javascript/mastodon/containers/compose_container.jsx index b93399aa9..751015d18 100644 --- a/app/javascript/mastodon/containers/compose_container.jsx +++ b/app/javascript/mastodon/containers/compose_container.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { IntlProvider, addLocaleData } from 'react-intl'; +import { IntlProvider } from 'react-intl'; import { Provider } from 'react-redux'; @@ -9,11 +9,10 @@ import { fetchCustomEmojis } from '../actions/custom_emojis'; import { hydrateStore } from '../actions/store'; import Compose from '../features/standalone/compose'; import initialState from '../initial_state'; -import { getLocale } from '../locales'; +import { getLocale, onProviderError } from '../locales'; import { store } from '../store'; -const { localeData, messages } = getLocale(); -addLocaleData(localeData); +const { messages } = getLocale(); if (initialState) { store.dispatch(hydrateStore(initialState)); @@ -31,7 +30,7 @@ export default class TimelineContainer extends PureComponent { const { locale } = this.props; return ( - + diff --git a/app/javascript/mastodon/containers/mastodon.jsx b/app/javascript/mastodon/containers/mastodon.jsx index 5be163f5a..c4d4611a2 100644 --- a/app/javascript/mastodon/containers/mastodon.jsx +++ b/app/javascript/mastodon/containers/mastodon.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { IntlProvider, addLocaleData } from 'react-intl'; +import { IntlProvider } from 'react-intl'; import { Helmet } from 'react-helmet'; import { BrowserRouter, Route } from 'react-router-dom'; @@ -16,11 +16,10 @@ import { connectUserStream } from 'mastodon/actions/streaming'; import ErrorBoundary from 'mastodon/components/error_boundary'; import UI from 'mastodon/features/ui'; import initialState, { title as siteTitle } from 'mastodon/initial_state'; -import { getLocale } from 'mastodon/locales'; +import { getLocale, onProviderError } from 'mastodon/locales'; import { store } from 'mastodon/store'; -const { localeData, messages } = getLocale(); -addLocaleData(localeData); +const { messages } = getLocale(); const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`; @@ -83,7 +82,7 @@ export default class Mastodon extends PureComponent { const { locale } = this.props; return ( - + diff --git a/app/javascript/mastodon/containers/media_container.jsx b/app/javascript/mastodon/containers/media_container.jsx index 7ed8f1719..84eab1cae 100644 --- a/app/javascript/mastodon/containers/media_container.jsx +++ b/app/javascript/mastodon/containers/media_container.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; import { createPortal } from 'react-dom'; -import { IntlProvider, addLocaleData } from 'react-intl'; +import { IntlProvider } from 'react-intl'; import { fromJS } from 'immutable'; @@ -14,11 +14,10 @@ import Audio from 'mastodon/features/audio'; import Card from 'mastodon/features/status/components/card'; import MediaModal from 'mastodon/features/ui/components/media_modal'; import Video from 'mastodon/features/video'; -import { getLocale } from 'mastodon/locales'; +import { getLocale, onProviderError } from 'mastodon/locales'; import { getScrollbarWidth } from 'mastodon/utils/scrollbar'; -const { localeData, messages } = getLocale(); -addLocaleData(localeData); +const { messages } = getLocale(); const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio }; @@ -84,7 +83,7 @@ export default class MediaContainer extends PureComponent { } return ( - + <> {[].map.call(components, (component, i) => { const componentName = component.getAttribute('data-component'); diff --git a/app/javascript/mastodon/features/onboarding/follows.jsx b/app/javascript/mastodon/features/onboarding/follows.jsx index 3807ce922..8b4ad0b08 100644 --- a/app/javascript/mastodon/features/onboarding/follows.jsx +++ b/app/javascript/mastodon/features/onboarding/follows.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { FormattedMessage, FormattedHTMLMessage } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; @@ -77,7 +77,7 @@ class Follows extends PureComponent { {loadedContent} -

+

{chunks} }} />

diff --git a/app/javascript/mastodon/features/onboarding/share.jsx b/app/javascript/mastodon/features/onboarding/share.jsx index 1895af912..687179302 100644 --- a/app/javascript/mastodon/features/onboarding/share.jsx +++ b/app/javascript/mastodon/features/onboarding/share.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { defineMessages, injectIntl, FormattedMessage, FormattedHTMLMessage } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import { Link } from 'react-router-dom'; @@ -168,9 +168,9 @@ class Share extends PureComponent { -

-

-

+

{chunks} }} />

+

{chunks} }} />

+

{chunks} }} />

diff --git a/app/javascript/mastodon/load_locale.js b/app/javascript/mastodon/load_locale.js new file mode 100644 index 000000000..cb14acd62 --- /dev/null +++ b/app/javascript/mastodon/load_locale.js @@ -0,0 +1,14 @@ +import { setLocale } from "./locales"; + +export async function loadLocale() { + const locale = document.querySelector('html').lang || 'en'; + + const localeData = await import( + /* webpackMode: "lazy" */ + /* webpackChunkName: "locale/[request]" */ + /* webpackInclude: /\.json$/ */ + /* webpackPreload: true */ + `mastodon/locales/${locale}.json`); + + setLocale({ messages: localeData }); +} diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json deleted file mode 100644 index d446989ab..000000000 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ /dev/null @@ -1,4484 +0,0 @@ -[ - { - "descriptors": [ - { - "defaultMessage": "Oops!", - "id": "alert.unexpected.title" - }, - { - "defaultMessage": "An unexpected error occurred.", - "id": "alert.unexpected.message" - }, - { - "defaultMessage": "Rate limited", - "id": "alert.rate_limited.title" - }, - { - "defaultMessage": "Please retry after {retry_time, time, medium}.", - "id": "alert.rate_limited.message" - } - ], - "path": "app/javascript/mastodon/actions/alerts.json" - }, - { - "descriptors": [ - { - "defaultMessage": "File upload limit exceeded.", - "id": "upload_error.limit" - }, - { - "defaultMessage": "File upload not allowed with polls.", - "id": "upload_error.poll" - } - ], - "path": "app/javascript/mastodon/actions/compose.json" - }, - { - "descriptors": [ - { - "defaultMessage": "{name} mentioned you", - "id": "notification.mention" - }, - { - "defaultMessage": "{count} notifications", - "id": "notifications.group" - } - ], - "path": "app/javascript/mastodon/actions/notifications.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Follow", - "id": "account.follow" - }, - { - "defaultMessage": "Unfollow", - "id": "account.unfollow" - }, - { - "defaultMessage": "Awaiting approval. Click to cancel follow request", - "id": "account.requested" - }, - { - "defaultMessage": "Unblock @{name}", - "id": "account.unblock" - }, - { - "defaultMessage": "Unmute @{name}", - "id": "account.unmute" - }, - { - "defaultMessage": "Mute notifications from @{name}", - "id": "account.mute_notifications" - }, - { - "defaultMessage": "Unmute notifications from @{name}", - "id": "account.unmute_notifications" - }, - { - "defaultMessage": "Mute @{name}", - "id": "account.mute" - }, - { - "defaultMessage": "Block @{name}", - "id": "account.block" - } - ], - "path": "app/javascript/mastodon/components/account.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Other", - "id": "report.categories.other" - }, - { - "defaultMessage": "Spam", - "id": "report.categories.spam" - }, - { - "defaultMessage": "Content violates one or more server rules", - "id": "report.categories.violation" - } - ], - "path": "app/javascript/mastodon/components/admin/ReportReasonSelector.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Loading...", - "id": "loading_indicator.label" - }, - { - "defaultMessage": "Sign-up month", - "id": "admin.dashboard.retention.cohort" - }, - { - "defaultMessage": "New users", - "id": "admin.dashboard.retention.cohort_size" - }, - { - "defaultMessage": "Average", - "id": "admin.dashboard.retention.average" - }, - { - "defaultMessage": "User retention rate by day after sign-up", - "id": "admin.dashboard.daily_retention" - }, - { - "defaultMessage": "User retention rate by month after sign-up", - "id": "admin.dashboard.monthly_retention" - } - ], - "path": "app/javascript/mastodon/components/admin/Retention.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Trending now", - "id": "trends.trending_now" - } - ], - "path": "app/javascript/mastodon/components/admin/Trends.json" - }, - { - "descriptors": [ - { - "defaultMessage": "(unprocessed)", - "id": "attachments_list.unprocessed" - } - ], - "path": "app/javascript/mastodon/components/attachment_list.json" - }, - { - "descriptors": [ - { - "defaultMessage": "{count} per week", - "id": "autosuggest_hashtag.per_week" - } - ], - "path": "app/javascript/mastodon/components/autosuggest_hashtag.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Back", - "id": "column_back_button.label" - } - ], - "path": "app/javascript/mastodon/components/column_back_button_slim.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Back", - "id": "column_back_button.label" - } - ], - "path": "app/javascript/mastodon/components/column_back_button.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Show settings", - "id": "column_header.show_settings" - }, - { - "defaultMessage": "Hide settings", - "id": "column_header.hide_settings" - }, - { - "defaultMessage": "Move column to the left", - "id": "column_header.moveLeft_settings" - }, - { - "defaultMessage": "Move column to the right", - "id": "column_header.moveRight_settings" - }, - { - "defaultMessage": "Unpin", - "id": "column_header.unpin" - }, - { - "defaultMessage": "Pin", - "id": "column_header.pin" - }, - { - "defaultMessage": "Back", - "id": "column_back_button.label" - } - ], - "path": "app/javascript/mastodon/components/column_header.json" - }, - { - "descriptors": [ - { - "defaultMessage": "{count, plural, one {{counter} Post} other {{counter} Posts}}", - "id": "account.statuses_counter" - }, - { - "defaultMessage": "{count, plural, one {{counter} Following} other {{counter} Following}}", - "id": "account.following_counter" - }, - { - "defaultMessage": "{count, plural, one {{counter} Follower} other {{counter} Followers}}", - "id": "account.followers_counter" - } - ], - "path": "app/javascript/mastodon/components/common_counter.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Dismiss", - "id": "dismissable_banner.dismiss" - } - ], - "path": "app/javascript/mastodon/components/dismissable_banner.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Unblock domain {domain}", - "id": "account.unblock_domain" - } - ], - "path": "app/javascript/mastodon/components/domain.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Edited {count, plural, one {# time} other {# times}}", - "id": "status.edited_x_times" - }, - { - "defaultMessage": "{name} created {date}", - "id": "status.history.created" - }, - { - "defaultMessage": "{name} edited {date}", - "id": "status.history.edited" - }, - { - "defaultMessage": "Edited {date}", - "id": "status.edited" - } - ], - "path": "app/javascript/mastodon/components/edited_timestamp/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.", - "id": "error.unexpected_crash.explanation_addons" - }, - { - "defaultMessage": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.", - "id": "error.unexpected_crash.explanation" - }, - { - "defaultMessage": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.", - "id": "error.unexpected_crash.next_steps_addons" - }, - { - "defaultMessage": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.", - "id": "error.unexpected_crash.next_steps" - }, - { - "defaultMessage": "Report issue", - "id": "errors.unexpected_crash.report_issue" - }, - { - "defaultMessage": "Copy stacktrace to clipboard", - "id": "errors.unexpected_crash.copy_stacktrace" - } - ], - "path": "app/javascript/mastodon/components/error_boundary.json" - }, - { - "descriptors": [ - { - "defaultMessage": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}", - "id": "trends.counter_by_accounts" - } - ], - "path": "app/javascript/mastodon/components/hashtag.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Load more", - "id": "status.load_more" - } - ], - "path": "app/javascript/mastodon/components/load_gap.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Load more", - "id": "status.load_more" - } - ], - "path": "app/javascript/mastodon/components/load_more.json" - }, - { - "descriptors": [ - { - "defaultMessage": "{count, plural, one {# new item} other {# new items}}", - "id": "load_pending" - } - ], - "path": "app/javascript/mastodon/components/load_pending.json" - }, - { - "descriptors": [ - { - "defaultMessage": "{number, plural, one {Hide image} other {Hide images}}", - "id": "media_gallery.toggle_visible" - }, - { - "defaultMessage": "Not available", - "id": "status.uncached_media_warning" - }, - { - "defaultMessage": "Sensitive content", - "id": "status.sensitive_warning" - }, - { - "defaultMessage": "Media hidden", - "id": "status.media_hidden" - } - ], - "path": "app/javascript/mastodon/components/media_gallery.json" - }, - { - "descriptors": [ - { - "defaultMessage": "You need to login to access this resource.", - "id": "not_signed_in_indicator.not_signed_in" - } - ], - "path": "app/javascript/mastodon/components/not_signed_in_indicator.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Put it back", - "id": "picture_in_picture.restore" - } - ], - "path": "app/javascript/mastodon/components/picture_in_picture_placeholder.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Closed", - "id": "poll.closed" - }, - { - "defaultMessage": "You voted for this answer", - "id": "poll.voted" - }, - { - "defaultMessage": "{votes, plural, one {# vote} other {# votes}}", - "id": "poll.votes" - }, - { - "defaultMessage": "{count, plural, one {# person} other {# people}}", - "id": "poll.total_people" - }, - { - "defaultMessage": "{count, plural, one {# vote} other {# votes}}", - "id": "poll.total_votes" - }, - { - "defaultMessage": "Vote", - "id": "poll.vote" - }, - { - "defaultMessage": "Refresh", - "id": "poll.refresh" - } - ], - "path": "app/javascript/mastodon/components/poll.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Loading…", - "id": "regeneration_indicator.label" - }, - { - "defaultMessage": "Your home feed is being prepared!", - "id": "regeneration_indicator.sublabel" - } - ], - "path": "app/javascript/mastodon/components/regeneration_indicator.json" - }, - { - "descriptors": [ - { - "defaultMessage": "today", - "id": "relative_time.today" - }, - { - "defaultMessage": "now", - "id": "relative_time.just_now" - }, - { - "defaultMessage": "just now", - "id": "relative_time.full.just_now" - }, - { - "defaultMessage": "{number}s", - "id": "relative_time.seconds" - }, - { - "defaultMessage": "{number, plural, one {# second} other {# seconds}} ago", - "id": "relative_time.full.seconds" - }, - { - "defaultMessage": "{number}m", - "id": "relative_time.minutes" - }, - { - "defaultMessage": "{number, plural, one {# minute} other {# minutes}} ago", - "id": "relative_time.full.minutes" - }, - { - "defaultMessage": "{number}h", - "id": "relative_time.hours" - }, - { - "defaultMessage": "{number, plural, one {# hour} other {# hours}} ago", - "id": "relative_time.full.hours" - }, - { - "defaultMessage": "{number}d", - "id": "relative_time.days" - }, - { - "defaultMessage": "{number, plural, one {# day} other {# days}} ago", - "id": "relative_time.full.days" - }, - { - "defaultMessage": "Moments remaining", - "id": "time_remaining.moments" - }, - { - "defaultMessage": "{number, plural, one {# second} other {# seconds}} left", - "id": "time_remaining.seconds" - }, - { - "defaultMessage": "{number, plural, one {# minute} other {# minutes}} left", - "id": "time_remaining.minutes" - }, - { - "defaultMessage": "{number, plural, one {# hour} other {# hours}} left", - "id": "time_remaining.hours" - }, - { - "defaultMessage": "{number, plural, one {# day} other {# days}} left", - "id": "time_remaining.days" - } - ], - "path": "app/javascript/mastodon/components/relative_timestamp.json" - }, - { - "descriptors": [ - { - "defaultMessage": "People using this server during the last 30 days (Monthly Active Users)", - "id": "server_banner.about_active_users" - }, - { - "defaultMessage": "{domain} is part of the decentralized social network powered by {mastodon}.", - "id": "server_banner.introduction" - }, - { - "defaultMessage": "Administered by:", - "id": "server_banner.administered_by" - }, - { - "defaultMessage": "Server stats:", - "id": "server_banner.server_stats" - }, - { - "defaultMessage": "active users", - "id": "server_banner.active_users" - }, - { - "defaultMessage": "Learn more", - "id": "server_banner.learn_more" - } - ], - "path": "app/javascript/mastodon/components/server_banner.json" - }, - { - "descriptors": [ - { - "defaultMessage": "{count}K", - "id": "units.short.thousand" - }, - { - "defaultMessage": "{count}M", - "id": "units.short.million" - }, - { - "defaultMessage": "{count}B", - "id": "units.short.billion" - } - ], - "path": "app/javascript/mastodon/components/short_number.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Delete", - "id": "status.delete" - }, - { - "defaultMessage": "Delete & re-draft", - "id": "status.redraft" - }, - { - "defaultMessage": "Edit", - "id": "status.edit" - }, - { - "defaultMessage": "Privately mention @{name}", - "id": "status.direct" - }, - { - "defaultMessage": "Mention @{name}", - "id": "status.mention" - }, - { - "defaultMessage": "Mute @{name}", - "id": "account.mute" - }, - { - "defaultMessage": "Block @{name}", - "id": "account.block" - }, - { - "defaultMessage": "Reply", - "id": "status.reply" - }, - { - "defaultMessage": "Share", - "id": "status.share" - }, - { - "defaultMessage": "More", - "id": "status.more" - }, - { - "defaultMessage": "Reply to thread", - "id": "status.replyAll" - }, - { - "defaultMessage": "Boost", - "id": "status.reblog" - }, - { - "defaultMessage": "Boost with original visibility", - "id": "status.reblog_private" - }, - { - "defaultMessage": "Unboost", - "id": "status.cancel_reblog_private" - }, - { - "defaultMessage": "This post cannot be boosted", - "id": "status.cannot_reblog" - }, - { - "defaultMessage": "Favourite", - "id": "status.favourite" - }, - { - "defaultMessage": "Bookmark", - "id": "status.bookmark" - }, - { - "defaultMessage": "Remove bookmark", - "id": "status.remove_bookmark" - }, - { - "defaultMessage": "Expand this status", - "id": "status.open" - }, - { - "defaultMessage": "Report @{name}", - "id": "status.report" - }, - { - "defaultMessage": "Mute conversation", - "id": "status.mute_conversation" - }, - { - "defaultMessage": "Unmute conversation", - "id": "status.unmute_conversation" - }, - { - "defaultMessage": "Pin on profile", - "id": "status.pin" - }, - { - "defaultMessage": "Unpin from profile", - "id": "status.unpin" - }, - { - "defaultMessage": "Embed", - "id": "status.embed" - }, - { - "defaultMessage": "Open moderation interface for @{name}", - "id": "status.admin_account" - }, - { - "defaultMessage": "Open this post in the moderation interface", - "id": "status.admin_status" - }, - { - "defaultMessage": "Open moderation interface for {domain}", - "id": "status.admin_domain" - }, - { - "defaultMessage": "Copy link to post", - "id": "status.copy" - }, - { - "defaultMessage": "Hide post", - "id": "status.hide" - }, - { - "defaultMessage": "Block domain {domain}", - "id": "account.block_domain" - }, - { - "defaultMessage": "Unblock domain {domain}", - "id": "account.unblock_domain" - }, - { - "defaultMessage": "Unmute @{name}", - "id": "account.unmute" - }, - { - "defaultMessage": "Unblock @{name}", - "id": "account.unblock" - }, - { - "defaultMessage": "Filter this post", - "id": "status.filter" - }, - { - "defaultMessage": "Open original page", - "id": "account.open_original_page" - } - ], - "path": "app/javascript/mastodon/components/status_action_bar.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Translated from {lang} using {provider}", - "id": "status.translated_from_with" - }, - { - "defaultMessage": "Show original", - "id": "status.show_original" - }, - { - "defaultMessage": "Translate", - "id": "status.translate" - }, - { - "defaultMessage": "Read more", - "id": "status.read_more" - }, - { - "defaultMessage": "Show more", - "id": "status.show_more" - }, - { - "defaultMessage": "Show less", - "id": "status.show_less" - } - ], - "path": "app/javascript/mastodon/components/status_content.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Public", - "id": "privacy.public.short" - }, - { - "defaultMessage": "Unlisted", - "id": "privacy.unlisted.short" - }, - { - "defaultMessage": "Followers only", - "id": "privacy.private.short" - }, - { - "defaultMessage": "Mentioned people only", - "id": "privacy.direct.short" - }, - { - "defaultMessage": "Edited {date}", - "id": "status.edited" - }, - { - "defaultMessage": "Filtered", - "id": "status.filtered" - }, - { - "defaultMessage": "Show anyway", - "id": "status.show_filter_reason" - }, - { - "defaultMessage": "Pinned post", - "id": "status.pinned" - }, - { - "defaultMessage": "{name} boosted", - "id": "status.reblogged_by" - }, - { - "defaultMessage": "Private mention", - "id": "status.direct_indicator" - }, - { - "defaultMessage": "Replied to {name}", - "id": "status.replied_to" - } - ], - "path": "app/javascript/mastodon/components/status.json" - }, - { - "descriptors": [ - { - "defaultMessage": "{resource} from other servers are not displayed.", - "id": "timeline_hint.remote_resource_not_displayed" - }, - { - "defaultMessage": "Browse more on the original profile", - "id": "account.browse_more_on_origin_server" - } - ], - "path": "app/javascript/mastodon/components/timeline_hint.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Unfollow", - "id": "confirmations.unfollow.confirm" - }, - { - "defaultMessage": "Are you sure you want to unfollow {name}?", - "id": "confirmations.unfollow.message" - } - ], - "path": "app/javascript/mastodon/containers/account_container.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Block entire domain", - "id": "confirmations.domain_block.confirm" - }, - { - "defaultMessage": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", - "id": "confirmations.domain_block.message" - } - ], - "path": "app/javascript/mastodon/containers/domain_container.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Delete", - "id": "confirmations.delete.confirm" - }, - { - "defaultMessage": "Are you sure you want to delete this status?", - "id": "confirmations.delete.message" - }, - { - "defaultMessage": "Delete & redraft", - "id": "confirmations.redraft.confirm" - }, - { - "defaultMessage": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", - "id": "confirmations.redraft.message" - }, - { - "defaultMessage": "Reply", - "id": "confirmations.reply.confirm" - }, - { - "defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", - "id": "confirmations.reply.message" - }, - { - "defaultMessage": "Edit", - "id": "confirmations.edit.confirm" - }, - { - "defaultMessage": "Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?", - "id": "confirmations.edit.message" - }, - { - "defaultMessage": "Block entire domain", - "id": "confirmations.domain_block.confirm" - }, - { - "defaultMessage": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", - "id": "confirmations.domain_block.message" - } - ], - "path": "app/javascript/mastodon/containers/status_container.json" - }, - { - "descriptors": [ - { - "defaultMessage": "About", - "id": "column.about" - }, - { - "defaultMessage": "Server rules", - "id": "about.rules" - }, - { - "defaultMessage": "Moderated servers", - "id": "about.blocks" - }, - { - "defaultMessage": "Limited", - "id": "about.domain_blocks.silenced.title" - }, - { - "defaultMessage": "You will generally not see profiles and content from this server, unless you explicitly look it up or opt into it by following.", - "id": "about.domain_blocks.silenced.explanation" - }, - { - "defaultMessage": "Suspended", - "id": "about.domain_blocks.suspended.title" - }, - { - "defaultMessage": "No data from this server will be processed, stored or exchanged, making any interaction or communication with users from this server impossible.", - "id": "about.domain_blocks.suspended.explanation" - }, - { - "defaultMessage": "Decentralized social media powered by {mastodon}", - "id": "about.powered_by" - }, - { - "defaultMessage": "Administered by:", - "id": "server_banner.administered_by" - }, - { - "defaultMessage": "Contact:", - "id": "about.contact" - }, - { - "defaultMessage": "This information has not been made available on this server.", - "id": "about.not_available" - }, - { - "defaultMessage": "Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.", - "id": "about.domain_blocks.preamble" - }, - { - "defaultMessage": "Reason not available", - "id": "about.domain_blocks.no_reason_available" - }, - { - "defaultMessage": "Mastodon is free, open-source software, and a trademark of Mastodon gGmbH.", - "id": "about.disclaimer" - } - ], - "path": "app/javascript/mastodon/features/about/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Account suspended", - "id": "empty_column.account_suspended" - }, - { - "defaultMessage": "Profile unavailable", - "id": "empty_column.account_unavailable" - } - ], - "path": "app/javascript/mastodon/features/account_gallery/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Posts", - "id": "account.posts" - }, - { - "defaultMessage": "Posts and replies", - "id": "account.posts_with_replies" - }, - { - "defaultMessage": "Media", - "id": "account.media" - } - ], - "path": "app/javascript/mastodon/features/account_timeline/components/header.json" - }, - { - "descriptors": [ - { - "defaultMessage": "This profile has been hidden by the moderators of {domain}.", - "id": "limited_account_hint.title" - }, - { - "defaultMessage": "Show profile anyway", - "id": "limited_account_hint.action" - } - ], - "path": "app/javascript/mastodon/features/account_timeline/components/limited_account_hint.json" - }, - { - "descriptors": [ - { - "defaultMessage": "In Memoriam.", - "id": "account.in_memoriam" - } - ], - "path": "app/javascript/mastodon/features/account_timeline/components/memorial_note.json" - }, - { - "descriptors": [ - { - "defaultMessage": "{name} has indicated that their new account is now:", - "id": "account.moved_to" - }, - { - "defaultMessage": "Go to profile", - "id": "account.go_to_profile" - } - ], - "path": "app/javascript/mastodon/features/account_timeline/components/moved_note.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Withdraw request", - "id": "confirmations.cancel_follow_request.confirm" - }, - { - "defaultMessage": "Unfollow", - "id": "confirmations.unfollow.confirm" - }, - { - "defaultMessage": "Block entire domain", - "id": "confirmations.domain_block.confirm" - }, - { - "defaultMessage": "Are you sure you want to unfollow {name}?", - "id": "confirmations.unfollow.message" - }, - { - "defaultMessage": "Are you sure you want to withdraw your request to follow {name}?", - "id": "confirmations.cancel_follow_request.message" - }, - { - "defaultMessage": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", - "id": "confirmations.domain_block.message" - } - ], - "path": "app/javascript/mastodon/features/account_timeline/containers/header_container.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Older posts", - "id": "timeline_hint.resources.statuses" - }, - { - "defaultMessage": "Account suspended", - "id": "empty_column.account_suspended" - }, - { - "defaultMessage": "Profile unavailable", - "id": "empty_column.account_unavailable" - }, - { - "defaultMessage": "No posts found", - "id": "empty_column.account_timeline" - } - ], - "path": "app/javascript/mastodon/features/account_timeline/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Click to add a note", - "id": "account_note.placeholder" - }, - { - "defaultMessage": "Saved", - "id": "generic.saved" - }, - { - "defaultMessage": "Note", - "id": "account.account_note_header" - } - ], - "path": "app/javascript/mastodon/features/account/components/account_note.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Last post on {date}", - "id": "account.featured_tags.last_status_at" - }, - { - "defaultMessage": "No posts", - "id": "account.featured_tags.last_status_never" - }, - { - "defaultMessage": "{name}'s featured hashtags", - "id": "account.featured_tags.title" - } - ], - "path": "app/javascript/mastodon/features/account/components/featured_tags.json" - }, - { - "descriptors": [ - { - "defaultMessage": "{name} has requested to follow you", - "id": "account.requested_follow" - }, - { - "defaultMessage": "Authorize", - "id": "follow_request.authorize" - }, - { - "defaultMessage": "Reject", - "id": "follow_request.reject" - } - ], - "path": "app/javascript/mastodon/features/account/components/follow_request_note.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Unfollow", - "id": "account.unfollow" - }, - { - "defaultMessage": "Follow", - "id": "account.follow" - }, - { - "defaultMessage": "Withdraw follow request", - "id": "account.cancel_follow_request" - }, - { - "defaultMessage": "Awaiting approval. Click to cancel follow request", - "id": "account.requested" - }, - { - "defaultMessage": "Unblock @{name}", - "id": "account.unblock" - }, - { - "defaultMessage": "Edit profile", - "id": "account.edit_profile" - }, - { - "defaultMessage": "Ownership of this link was checked on {date}", - "id": "account.link_verified_on" - }, - { - "defaultMessage": "This account privacy status is set to locked. The owner manually reviews who can follow them.", - "id": "account.locked_info" - }, - { - "defaultMessage": "Mention @{name}", - "id": "account.mention" - }, - { - "defaultMessage": "Privately mention @{name}", - "id": "account.direct" - }, - { - "defaultMessage": "Unmute @{name}", - "id": "account.unmute" - }, - { - "defaultMessage": "Block @{name}", - "id": "account.block" - }, - { - "defaultMessage": "Mute @{name}", - "id": "account.mute" - }, - { - "defaultMessage": "Report @{name}", - "id": "account.report" - }, - { - "defaultMessage": "Share @{name}'s profile", - "id": "account.share" - }, - { - "defaultMessage": "Media", - "id": "account.media" - }, - { - "defaultMessage": "Block domain {domain}", - "id": "account.block_domain" - }, - { - "defaultMessage": "Unblock domain {domain}", - "id": "account.unblock_domain" - }, - { - "defaultMessage": "Hide boosts from @{name}", - "id": "account.hide_reblogs" - }, - { - "defaultMessage": "Show boosts from @{name}", - "id": "account.show_reblogs" - }, - { - "defaultMessage": "Notify me when @{name} posts", - "id": "account.enable_notifications" - }, - { - "defaultMessage": "Stop notifying me when @{name} posts", - "id": "account.disable_notifications" - }, - { - "defaultMessage": "Pinned posts", - "id": "navigation_bar.pins" - }, - { - "defaultMessage": "Preferences", - "id": "navigation_bar.preferences" - }, - { - "defaultMessage": "Follow requests", - "id": "navigation_bar.follow_requests" - }, - { - "defaultMessage": "Favourites", - "id": "navigation_bar.favourites" - }, - { - "defaultMessage": "Lists", - "id": "navigation_bar.lists" - }, - { - "defaultMessage": "Followed hashtags", - "id": "navigation_bar.followed_tags" - }, - { - "defaultMessage": "Blocked users", - "id": "navigation_bar.blocks" - }, - { - "defaultMessage": "Blocked domains", - "id": "navigation_bar.domain_blocks" - }, - { - "defaultMessage": "Muted users", - "id": "navigation_bar.mutes" - }, - { - "defaultMessage": "Feature on profile", - "id": "account.endorse" - }, - { - "defaultMessage": "Don't feature on profile", - "id": "account.unendorse" - }, - { - "defaultMessage": "Add or Remove from lists", - "id": "account.add_or_remove_from_list" - }, - { - "defaultMessage": "Open moderation interface for @{name}", - "id": "status.admin_account" - }, - { - "defaultMessage": "Open moderation interface for {domain}", - "id": "status.admin_domain" - }, - { - "defaultMessage": "Change subscribed languages", - "id": "account.languages" - }, - { - "defaultMessage": "Open original page", - "id": "account.open_original_page" - }, - { - "defaultMessage": "Follows you", - "id": "account.follows_you" - }, - { - "defaultMessage": "Blocked", - "id": "account.blocked" - }, - { - "defaultMessage": "Muted", - "id": "account.muted" - }, - { - "defaultMessage": "Domain blocked", - "id": "account.domain_blocked" - }, - { - "defaultMessage": "Bot", - "id": "account.badges.bot" - }, - { - "defaultMessage": "Group", - "id": "account.badges.group" - }, - { - "defaultMessage": "Joined", - "id": "account.joined_short" - } - ], - "path": "app/javascript/mastodon/features/account/components/header.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Play", - "id": "video.play" - }, - { - "defaultMessage": "Pause", - "id": "video.pause" - }, - { - "defaultMessage": "Mute sound", - "id": "video.mute" - }, - { - "defaultMessage": "Unmute sound", - "id": "video.unmute" - }, - { - "defaultMessage": "Download file", - "id": "video.download" - }, - { - "defaultMessage": "Hide audio", - "id": "audio.hide" - }, - { - "defaultMessage": "Sensitive content", - "id": "status.sensitive_warning" - }, - { - "defaultMessage": "Media hidden", - "id": "status.media_hidden" - } - ], - "path": "app/javascript/mastodon/features/audio/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Blocked users", - "id": "column.blocks" - }, - { - "defaultMessage": "You haven't blocked any users yet.", - "id": "empty_column.blocks" - } - ], - "path": "app/javascript/mastodon/features/blocks/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Bookmarks", - "id": "column.bookmarks" - }, - { - "defaultMessage": "You don't have any bookmarked posts yet. When you bookmark one, it will show up here.", - "id": "empty_column.bookmarked_statuses" - } - ], - "path": "app/javascript/mastodon/features/bookmarked_statuses/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Creating an account on {domain} is currently not possible, but please keep in mind that you do not need an account specifically on {domain} to use Mastodon.", - "id": "closed_registrations_modal.description" - }, - { - "defaultMessage": "Signing up on Mastodon", - "id": "closed_registrations_modal.title" - }, - { - "defaultMessage": "Mastodon is decentralized, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!", - "id": "closed_registrations_modal.preamble" - }, - { - "defaultMessage": "On this server", - "id": "interaction_modal.on_this_server" - }, - { - "defaultMessage": "On a different server", - "id": "interaction_modal.on_another_server" - }, - { - "defaultMessage": "Since Mastodon is decentralized, you can create an account on another server and still interact with this one.", - "id": "closed_registrations.other_server_instructions" - }, - { - "defaultMessage": "Find another server", - "id": "closed_registrations_modal.find_another_server" - } - ], - "path": "app/javascript/mastodon/features/closed_registrations_modal/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Media only", - "id": "community.column_settings.media_only" - } - ], - "path": "app/javascript/mastodon/features/community_timeline/components/column_settings.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Local timeline", - "id": "column.community" - }, - { - "defaultMessage": "These are the most recent public posts from people whose accounts are hosted by {domain}.", - "id": "dismissable_banner.community_timeline" - }, - { - "defaultMessage": "The local timeline is empty. Write something publicly to get the ball rolling!", - "id": "empty_column.community" - } - ], - "path": "app/javascript/mastodon/features/community_timeline/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Edit profile", - "id": "account.edit_profile" - }, - { - "defaultMessage": "Pinned posts", - "id": "navigation_bar.pins" - }, - { - "defaultMessage": "Preferences", - "id": "navigation_bar.preferences" - }, - { - "defaultMessage": "Follow requests", - "id": "navigation_bar.follow_requests" - }, - { - "defaultMessage": "Favourites", - "id": "navigation_bar.favourites" - }, - { - "defaultMessage": "Lists", - "id": "navigation_bar.lists" - }, - { - "defaultMessage": "Followed hashtags", - "id": "navigation_bar.followed_tags" - }, - { - "defaultMessage": "Blocked users", - "id": "navigation_bar.blocks" - }, - { - "defaultMessage": "Blocked domains", - "id": "navigation_bar.domain_blocks" - }, - { - "defaultMessage": "Muted users", - "id": "navigation_bar.mutes" - }, - { - "defaultMessage": "Muted words", - "id": "navigation_bar.filters" - }, - { - "defaultMessage": "Logout", - "id": "navigation_bar.logout" - }, - { - "defaultMessage": "Bookmarks", - "id": "navigation_bar.bookmarks" - } - ], - "path": "app/javascript/mastodon/features/compose/components/action_bar.json" - }, - { - "descriptors": [ - { - "defaultMessage": "What is on your mind?", - "id": "compose_form.placeholder" - }, - { - "defaultMessage": "Write your warning here", - "id": "compose_form.spoiler_placeholder" - }, - { - "defaultMessage": "Publish", - "id": "compose_form.publish" - }, - { - "defaultMessage": "{publish}!", - "id": "compose_form.publish_loud" - }, - { - "defaultMessage": "Save changes", - "id": "compose_form.save_changes" - } - ], - "path": "app/javascript/mastodon/features/compose/components/compose_form.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Insert emoji", - "id": "emoji_button.label" - }, - { - "defaultMessage": "Search...", - "id": "emoji_button.search" - }, - { - "defaultMessage": "Custom", - "id": "emoji_button.custom" - }, - { - "defaultMessage": "Frequently used", - "id": "emoji_button.recent" - }, - { - "defaultMessage": "Search results", - "id": "emoji_button.search_results" - }, - { - "defaultMessage": "People", - "id": "emoji_button.people" - }, - { - "defaultMessage": "Nature", - "id": "emoji_button.nature" - }, - { - "defaultMessage": "Food & Drink", - "id": "emoji_button.food" - }, - { - "defaultMessage": "Activity", - "id": "emoji_button.activity" - }, - { - "defaultMessage": "Travel & Places", - "id": "emoji_button.travel" - }, - { - "defaultMessage": "Objects", - "id": "emoji_button.objects" - }, - { - "defaultMessage": "Symbols", - "id": "emoji_button.symbols" - }, - { - "defaultMessage": "Flags", - "id": "emoji_button.flags" - }, - { - "defaultMessage": "No matching emojis found", - "id": "emoji_button.not_found" - } - ], - "path": "app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Change language", - "id": "compose.language.change" - }, - { - "defaultMessage": "Search languages...", - "id": "compose.language.search" - }, - { - "defaultMessage": "Clear", - "id": "emoji_button.clear" - } - ], - "path": "app/javascript/mastodon/features/compose/components/language_dropdown.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Edit profile", - "id": "navigation_bar.edit_profile" - } - ], - "path": "app/javascript/mastodon/features/compose/components/navigation_bar.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Add a poll", - "id": "poll_button.add_poll" - }, - { - "defaultMessage": "Remove poll", - "id": "poll_button.remove_poll" - } - ], - "path": "app/javascript/mastodon/features/compose/components/poll_button.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Choice {number}", - "id": "compose_form.poll.option_placeholder" - }, - { - "defaultMessage": "Add a choice", - "id": "compose_form.poll.add_option" - }, - { - "defaultMessage": "Remove this choice", - "id": "compose_form.poll.remove_option" - }, - { - "defaultMessage": "Poll duration", - "id": "compose_form.poll.duration" - }, - { - "defaultMessage": "Change poll to allow multiple choices", - "id": "compose_form.poll.switch_to_multiple" - }, - { - "defaultMessage": "Change poll to allow for a single choice", - "id": "compose_form.poll.switch_to_single" - }, - { - "defaultMessage": "{number, plural, one {# minute} other {# minutes}}", - "id": "intervals.full.minutes" - }, - { - "defaultMessage": "{number, plural, one {# hour} other {# hours}}", - "id": "intervals.full.hours" - }, - { - "defaultMessage": "{number, plural, one {# day} other {# days}}", - "id": "intervals.full.days" - } - ], - "path": "app/javascript/mastodon/features/compose/components/poll_form.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Public", - "id": "privacy.public.short" - }, - { - "defaultMessage": "Visible for all", - "id": "privacy.public.long" - }, - { - "defaultMessage": "Unlisted", - "id": "privacy.unlisted.short" - }, - { - "defaultMessage": "Visible for all, but opted-out of discovery features", - "id": "privacy.unlisted.long" - }, - { - "defaultMessage": "Followers only", - "id": "privacy.private.short" - }, - { - "defaultMessage": "Visible for followers only", - "id": "privacy.private.long" - }, - { - "defaultMessage": "Mentioned people only", - "id": "privacy.direct.short" - }, - { - "defaultMessage": "Visible for mentioned users only", - "id": "privacy.direct.long" - }, - { - "defaultMessage": "Adjust status privacy", - "id": "privacy.change" - } - ], - "path": "app/javascript/mastodon/features/compose/components/privacy_dropdown.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Cancel", - "id": "reply_indicator.cancel" - } - ], - "path": "app/javascript/mastodon/features/compose/components/reply_indicator.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Dismiss suggestion", - "id": "suggestions.dismiss" - }, - { - "defaultMessage": "You might be interested in…", - "id": "suggestions.header" - }, - { - "defaultMessage": "Profiles", - "id": "search_results.accounts" - }, - { - "defaultMessage": "Posts", - "id": "search_results.statuses" - }, - { - "defaultMessage": "Searching posts by their content is not enabled on this Mastodon server.", - "id": "search_results.statuses_fts_disabled" - }, - { - "defaultMessage": "Hashtags", - "id": "search_results.hashtags" - }, - { - "defaultMessage": "{count, plural, one {# result} other {# results}}", - "id": "search_results.total" - } - ], - "path": "app/javascript/mastodon/features/compose/components/search_results.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Search", - "id": "search.placeholder" - }, - { - "defaultMessage": "Search or paste URL", - "id": "search.search_or_paste" - }, - { - "defaultMessage": "Open URL in Mastodon", - "id": "search.quick_action.open_url" - }, - { - "defaultMessage": "Go to hashtag {x}", - "id": "search.quick_action.go_to_hashtag" - }, - { - "defaultMessage": "Go to profile {x}", - "id": "search.quick_action.go_to_account" - }, - { - "defaultMessage": "Posts matching {x}", - "id": "search.quick_action.status_search" - }, - { - "defaultMessage": "Profiles matching {x}", - "id": "search.quick_action.account_search" - }, - { - "defaultMessage": "Recent searches", - "id": "search_popout.recent" - }, - { - "defaultMessage": "No recent searches", - "id": "search.no_recent_searches" - }, - { - "defaultMessage": "Quick actions", - "id": "search_popout.quick_actions" - } - ], - "path": "app/javascript/mastodon/features/compose/components/search.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Add images, a video or an audio file", - "id": "upload_button.label" - } - ], - "path": "app/javascript/mastodon/features/compose/components/upload_button.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Processing…", - "id": "upload_progress.processing" - }, - { - "defaultMessage": "Uploading…", - "id": "upload_progress.label" - } - ], - "path": "app/javascript/mastodon/features/compose/components/upload_progress.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Delete", - "id": "upload_form.undo" - }, - { - "defaultMessage": "Edit", - "id": "upload_form.edit" - }, - { - "defaultMessage": "No description added", - "id": "upload_form.description_missing" - } - ], - "path": "app/javascript/mastodon/features/compose/components/upload.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Are you sure you want to log out?", - "id": "confirmations.logout.message" - }, - { - "defaultMessage": "Log out", - "id": "confirmations.logout.confirm" - } - ], - "path": "app/javascript/mastodon/features/compose/containers/navigation_container.json" - }, - { - "descriptors": [ - { - "defaultMessage": "{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}", - "id": "compose_form.sensitive.marked" - }, - { - "defaultMessage": "{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}", - "id": "compose_form.sensitive.unmarked" - }, - { - "defaultMessage": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}", - "id": "compose_form.sensitive.hide" - } - ], - "path": "app/javascript/mastodon/features/compose/containers/sensitive_button_container.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Text is hidden behind warning", - "id": "compose_form.spoiler.marked" - }, - { - "defaultMessage": "Text is not hidden", - "id": "compose_form.spoiler.unmarked" - } - ], - "path": "app/javascript/mastodon/features/compose/containers/spoiler_button_container.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", - "id": "compose_form.lock_disclaimer" - }, - { - "defaultMessage": "locked", - "id": "compose_form.lock_disclaimer.lock" - }, - { - "defaultMessage": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.", - "id": "compose_form.hashtag_warning" - }, - { - "defaultMessage": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.", - "id": "compose_form.encryption_warning" - }, - { - "defaultMessage": "Learn more", - "id": "compose_form.direct_message_warning_learn_more" - } - ], - "path": "app/javascript/mastodon/features/compose/containers/warning_container.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Getting started", - "id": "getting_started.heading" - }, - { - "defaultMessage": "Home", - "id": "tabs_bar.home" - }, - { - "defaultMessage": "Notifications", - "id": "tabs_bar.notifications" - }, - { - "defaultMessage": "Federated timeline", - "id": "navigation_bar.public_timeline" - }, - { - "defaultMessage": "Local timeline", - "id": "navigation_bar.community_timeline" - }, - { - "defaultMessage": "Preferences", - "id": "navigation_bar.preferences" - }, - { - "defaultMessage": "Logout", - "id": "navigation_bar.logout" - }, - { - "defaultMessage": "Compose new post", - "id": "navigation_bar.compose" - }, - { - "defaultMessage": "Are you sure you want to log out?", - "id": "confirmations.logout.message" - }, - { - "defaultMessage": "Log out", - "id": "confirmations.logout.confirm" - } - ], - "path": "app/javascript/mastodon/features/compose/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "More", - "id": "status.more" - }, - { - "defaultMessage": "View conversation", - "id": "conversation.open" - }, - { - "defaultMessage": "Reply", - "id": "status.reply" - }, - { - "defaultMessage": "Mark as read", - "id": "conversation.mark_as_read" - }, - { - "defaultMessage": "Delete conversation", - "id": "conversation.delete" - }, - { - "defaultMessage": "Mute conversation", - "id": "status.mute_conversation" - }, - { - "defaultMessage": "Unmute conversation", - "id": "status.unmute_conversation" - }, - { - "defaultMessage": "With {names}", - "id": "conversation.with" - } - ], - "path": "app/javascript/mastodon/features/direct_timeline/components/conversation.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Reply", - "id": "confirmations.reply.confirm" - }, - { - "defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", - "id": "confirmations.reply.message" - } - ], - "path": "app/javascript/mastodon/features/direct_timeline/containers/conversation_container.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Private mentions", - "id": "column.direct" - }, - { - "defaultMessage": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.", - "id": "compose_form.encryption_warning" - }, - { - "defaultMessage": "Learn more", - "id": "compose_form.direct_message_warning_learn_more" - }, - { - "defaultMessage": "You don't have any private mentions yet. When you send or receive one, it will show up here.", - "id": "empty_column.direct" - } - ], - "path": "app/javascript/mastodon/features/direct_timeline/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Unfollow", - "id": "account.unfollow" - }, - { - "defaultMessage": "Follow", - "id": "account.follow" - }, - { - "defaultMessage": "Withdraw follow request", - "id": "account.cancel_follow_request" - }, - { - "defaultMessage": "Withdraw request", - "id": "confirmations.cancel_follow_request.confirm" - }, - { - "defaultMessage": "Awaiting approval. Click to cancel follow request", - "id": "account.requested" - }, - { - "defaultMessage": "Unblock", - "id": "account.unblock_short" - }, - { - "defaultMessage": "Unmute", - "id": "account.unmute_short" - }, - { - "defaultMessage": "Unfollow", - "id": "confirmations.unfollow.confirm" - }, - { - "defaultMessage": "Edit profile", - "id": "account.edit_profile" - }, - { - "defaultMessage": "Are you sure you want to unfollow {name}?", - "id": "confirmations.unfollow.message" - }, - { - "defaultMessage": "Are you sure you want to withdraw your request to follow {name}?", - "id": "confirmations.cancel_follow_request.message" - }, - { - "defaultMessage": "Posts", - "id": "account.posts" - }, - { - "defaultMessage": "Followers", - "id": "account.followers" - }, - { - "defaultMessage": "Following", - "id": "account.following" - } - ], - "path": "app/javascript/mastodon/features/directory/components/account_card.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Browse profiles", - "id": "column.directory" - }, - { - "defaultMessage": "Recently active", - "id": "directory.recently_active" - }, - { - "defaultMessage": "New arrivals", - "id": "directory.new_arrivals" - }, - { - "defaultMessage": "From {domain} only", - "id": "directory.local" - }, - { - "defaultMessage": "From known fediverse", - "id": "directory.federated" - } - ], - "path": "app/javascript/mastodon/features/directory/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Blocked domains", - "id": "column.domain_blocks" - }, - { - "defaultMessage": "Unblock domain {domain}", - "id": "account.unblock_domain" - }, - { - "defaultMessage": "There are no blocked domains yet.", - "id": "empty_column.domain_blocks" - } - ], - "path": "app/javascript/mastodon/features/domain_blocks/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Explore", - "id": "explore.title" - }, - { - "defaultMessage": "Search results", - "id": "explore.search_results" - }, - { - "defaultMessage": "Posts", - "id": "explore.trending_statuses" - }, - { - "defaultMessage": "Hashtags", - "id": "explore.trending_tags" - }, - { - "defaultMessage": "People", - "id": "explore.suggested_follows" - }, - { - "defaultMessage": "News", - "id": "explore.trending_links" - } - ], - "path": "app/javascript/mastodon/features/explore/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "These news stories are being talked about by people on this and other servers of the decentralized network right now.", - "id": "dismissable_banner.explore_links" - }, - { - "defaultMessage": "Nothing is trending right now. Check back later!", - "id": "empty_column.explore_statuses" - } - ], - "path": "app/javascript/mastodon/features/explore/links.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Search for {q}", - "id": "search_results.title" - }, - { - "defaultMessage": "Could not find anything for these search terms", - "id": "search_results.nothing_found" - }, - { - "defaultMessage": "All", - "id": "search_results.all" - }, - { - "defaultMessage": "Profiles", - "id": "search_results.accounts" - }, - { - "defaultMessage": "Hashtags", - "id": "search_results.hashtags" - }, - { - "defaultMessage": "Posts", - "id": "search_results.statuses" - } - ], - "path": "app/javascript/mastodon/features/explore/results.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Nothing is trending right now. Check back later!", - "id": "empty_column.explore_statuses" - }, - { - "defaultMessage": "These posts from this and other servers in the decentralized network are gaining traction on this server right now.", - "id": "dismissable_banner.explore_statuses" - } - ], - "path": "app/javascript/mastodon/features/explore/statuses.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Nothing is trending right now. Check back later!", - "id": "empty_column.explore_statuses" - } - ], - "path": "app/javascript/mastodon/features/explore/suggestions.json" - }, - { - "descriptors": [ - { - "defaultMessage": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.", - "id": "dismissable_banner.explore_tags" - }, - { - "defaultMessage": "Nothing is trending right now. Check back later!", - "id": "empty_column.explore_statuses" - } - ], - "path": "app/javascript/mastodon/features/explore/tags.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Favourites", - "id": "column.favourites" - }, - { - "defaultMessage": "You don't have any favourite posts yet. When you favourite one, it will show up here.", - "id": "empty_column.favourited_statuses" - } - ], - "path": "app/javascript/mastodon/features/favourited_statuses/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Refresh", - "id": "refresh" - }, - { - "defaultMessage": "No one has favourited this post yet. When someone does, they will show up here.", - "id": "empty_column.favourites" - } - ], - "path": "app/javascript/mastodon/features/favourites/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Expired filter!", - "id": "filter_modal.added.expired_title" - }, - { - "defaultMessage": "This filter category has expired, you will need to change the expiration date for it to apply.", - "id": "filter_modal.added.expired_explanation" - }, - { - "defaultMessage": "Context mismatch!", - "id": "filter_modal.added.context_mismatch_title" - }, - { - "defaultMessage": "This filter category does not apply to the context in which you have accessed this post. If you want the post to be filtered in this context too, you will have to edit the filter.", - "id": "filter_modal.added.context_mismatch_explanation" - }, - { - "defaultMessage": "settings page", - "id": "filter_modal.added.settings_link" - }, - { - "defaultMessage": "Filter added!", - "id": "filter_modal.added.title" - }, - { - "defaultMessage": "This post has been added to the following filter category: {title}.", - "id": "filter_modal.added.short_explanation" - }, - { - "defaultMessage": "Filter settings", - "id": "filter_modal.added.review_and_configure_title" - }, - { - "defaultMessage": "To review and further configure this filter category, go to the {settings_link}.", - "id": "filter_modal.added.review_and_configure" - }, - { - "defaultMessage": "Done", - "id": "report.close" - } - ], - "path": "app/javascript/mastodon/features/filters/added_to_filter.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Search or create", - "id": "filter_modal.select_filter.search" - }, - { - "defaultMessage": "Clear", - "id": "emoji_button.clear" - }, - { - "defaultMessage": "expired", - "id": "filter_modal.select_filter.expired" - }, - { - "defaultMessage": "does not apply to this context", - "id": "filter_modal.select_filter.context_mismatch" - }, - { - "defaultMessage": "New category: {name}", - "id": "filter_modal.select_filter.prompt_new" - }, - { - "defaultMessage": "Filter this post", - "id": "filter_modal.select_filter.title" - }, - { - "defaultMessage": "Use an existing category or create a new one", - "id": "filter_modal.select_filter.subtitle" - } - ], - "path": "app/javascript/mastodon/features/filters/select_filter.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Authorize", - "id": "follow_request.authorize" - }, - { - "defaultMessage": "Reject", - "id": "follow_request.reject" - } - ], - "path": "app/javascript/mastodon/features/follow_requests/components/account_authorize.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Follow requests", - "id": "column.follow_requests" - }, - { - "defaultMessage": "You don't have any follow requests yet. When you receive one, it will show up here.", - "id": "empty_column.follow_requests" - }, - { - "defaultMessage": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.", - "id": "follow_requests.unlocked_explanation" - } - ], - "path": "app/javascript/mastodon/features/follow_requests/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Followed hashtags", - "id": "followed_tags" - }, - { - "defaultMessage": "You have not followed any hashtags yet. When you do, they will show up here.", - "id": "empty_column.followed_tags" - } - ], - "path": "app/javascript/mastodon/features/followed_tags/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Followers", - "id": "timeline_hint.resources.followers" - }, - { - "defaultMessage": "Account suspended", - "id": "empty_column.account_suspended" - }, - { - "defaultMessage": "Profile unavailable", - "id": "empty_column.account_unavailable" - }, - { - "defaultMessage": "No one follows this user yet.", - "id": "account.followers.empty" - } - ], - "path": "app/javascript/mastodon/features/followers/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Follows", - "id": "timeline_hint.resources.follows" - }, - { - "defaultMessage": "Account suspended", - "id": "empty_column.account_suspended" - }, - { - "defaultMessage": "Profile unavailable", - "id": "empty_column.account_unavailable" - }, - { - "defaultMessage": "This user doesn't follow anyone yet.", - "id": "account.follows.empty" - } - ], - "path": "app/javascript/mastodon/features/following/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Close", - "id": "lightbox.close" - }, - { - "defaultMessage": "Previous", - "id": "lightbox.previous" - }, - { - "defaultMessage": "Next", - "id": "lightbox.next" - }, - { - "defaultMessage": "Announcement", - "id": "announcement.announcement" - } - ], - "path": "app/javascript/mastodon/features/getting_started/components/announcements.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Trending now", - "id": "trends.trending_now" - } - ], - "path": "app/javascript/mastodon/features/getting_started/components/trends.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Home", - "id": "tabs_bar.home" - }, - { - "defaultMessage": "Notifications", - "id": "tabs_bar.notifications" - }, - { - "defaultMessage": "Federated timeline", - "id": "navigation_bar.public_timeline" - }, - { - "defaultMessage": "Settings", - "id": "column_subheading.settings" - }, - { - "defaultMessage": "Local timeline", - "id": "navigation_bar.community_timeline" - }, - { - "defaultMessage": "Explore", - "id": "navigation_bar.explore" - }, - { - "defaultMessage": "Private mentions", - "id": "navigation_bar.direct" - }, - { - "defaultMessage": "Bookmarks", - "id": "navigation_bar.bookmarks" - }, - { - "defaultMessage": "Preferences", - "id": "navigation_bar.preferences" - }, - { - "defaultMessage": "Follow requests", - "id": "navigation_bar.follow_requests" - }, - { - "defaultMessage": "Favourites", - "id": "navigation_bar.favourites" - }, - { - "defaultMessage": "Blocked users", - "id": "navigation_bar.blocks" - }, - { - "defaultMessage": "Blocked domains", - "id": "navigation_bar.domain_blocks" - }, - { - "defaultMessage": "Muted users", - "id": "navigation_bar.mutes" - }, - { - "defaultMessage": "Pinned posts", - "id": "navigation_bar.pins" - }, - { - "defaultMessage": "Lists", - "id": "navigation_bar.lists" - }, - { - "defaultMessage": "Discover", - "id": "navigation_bar.discover" - }, - { - "defaultMessage": "Personal", - "id": "navigation_bar.personal" - }, - { - "defaultMessage": "Security", - "id": "navigation_bar.security" - }, - { - "defaultMessage": "Getting started", - "id": "getting_started.heading" - } - ], - "path": "app/javascript/mastodon/features/getting_started/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Enter hashtags…", - "id": "hashtag.column_settings.select.placeholder" - }, - { - "defaultMessage": "No suggestions found", - "id": "hashtag.column_settings.select.no_options_message" - }, - { - "defaultMessage": "Any of these", - "id": "hashtag.column_settings.tag_mode.any" - }, - { - "defaultMessage": "All of these", - "id": "hashtag.column_settings.tag_mode.all" - }, - { - "defaultMessage": "None of these", - "id": "hashtag.column_settings.tag_mode.none" - }, - { - "defaultMessage": "Include additional tags in this column", - "id": "hashtag.column_settings.tag_toggle" - }, - { - "defaultMessage": "Local only", - "id": "community.column_settings.local_only" - } - ], - "path": "app/javascript/mastodon/features/hashtag_timeline/components/column_settings.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Follow hashtag", - "id": "hashtag.follow" - }, - { - "defaultMessage": "Unfollow hashtag", - "id": "hashtag.unfollow" - }, - { - "defaultMessage": "or {additional}", - "id": "hashtag.column_header.tag_mode.any" - }, - { - "defaultMessage": "and {additional}", - "id": "hashtag.column_header.tag_mode.all" - }, - { - "defaultMessage": "without {additional}", - "id": "hashtag.column_header.tag_mode.none" - }, - { - "defaultMessage": "There is nothing in this hashtag yet.", - "id": "empty_column.hashtag" - } - ], - "path": "app/javascript/mastodon/features/hashtag_timeline/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Basic", - "id": "home.column_settings.basic" - }, - { - "defaultMessage": "Show boosts", - "id": "home.column_settings.show_reblogs" - }, - { - "defaultMessage": "Show replies", - "id": "home.column_settings.show_replies" - } - ], - "path": "app/javascript/mastodon/features/home_timeline/components/column_settings.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Home", - "id": "column.home" - }, - { - "defaultMessage": "Show announcements", - "id": "home.show_announcements" - }, - { - "defaultMessage": "Hide announcements", - "id": "home.hide_announcements" - }, - { - "defaultMessage": "Your home timeline is empty! Follow more people to fill it up. {suggestions}", - "id": "empty_column.home" - }, - { - "defaultMessage": "See some suggestions", - "id": "empty_column.home.suggestions" - } - ], - "path": "app/javascript/mastodon/features/home_timeline/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Copied", - "id": "copypaste.copied" - }, - { - "defaultMessage": "Copy", - "id": "copypaste.copy" - }, - { - "defaultMessage": "Reply to {name}'s post", - "id": "interaction_modal.title.reply" - }, - { - "defaultMessage": "With an account on Mastodon, you can respond to this post.", - "id": "interaction_modal.description.reply" - }, - { - "defaultMessage": "Boost {name}'s post", - "id": "interaction_modal.title.reblog" - }, - { - "defaultMessage": "With an account on Mastodon, you can boost this post to share it with your own followers.", - "id": "interaction_modal.description.reblog" - }, - { - "defaultMessage": "Favourite {name}'s post", - "id": "interaction_modal.title.favourite" - }, - { - "defaultMessage": "With an account on Mastodon, you can favourite this post to let the author know you appreciate it and save it for later.", - "id": "interaction_modal.description.favourite" - }, - { - "defaultMessage": "Follow {name}", - "id": "interaction_modal.title.follow" - }, - { - "defaultMessage": "With an account on Mastodon, you can follow {name} to receive their posts in your home feed.", - "id": "interaction_modal.description.follow" - }, - { - "defaultMessage": "Create account", - "id": "sign_in_banner.create_account" - }, - { - "defaultMessage": "Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one.", - "id": "interaction_modal.preamble" - }, - { - "defaultMessage": "On this server", - "id": "interaction_modal.on_this_server" - }, - { - "defaultMessage": "Login", - "id": "sign_in_banner.sign_in" - }, - { - "defaultMessage": "On a different server", - "id": "interaction_modal.on_another_server" - }, - { - "defaultMessage": "Copy and paste this URL into the search field of your favourite Mastodon app or the web interface of your Mastodon server.", - "id": "interaction_modal.other_server_instructions" - } - ], - "path": "app/javascript/mastodon/features/interaction_modal/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Keyboard Shortcuts", - "id": "keyboard_shortcuts.heading" - }, - { - "defaultMessage": "Hotkey", - "id": "keyboard_shortcuts.hotkey" - }, - { - "defaultMessage": "Description", - "id": "keyboard_shortcuts.description" - }, - { - "defaultMessage": "to reply", - "id": "keyboard_shortcuts.reply" - }, - { - "defaultMessage": "to mention author", - "id": "keyboard_shortcuts.mention" - }, - { - "defaultMessage": "to open author's profile", - "id": "keyboard_shortcuts.profile" - }, - { - "defaultMessage": "to favourite", - "id": "keyboard_shortcuts.favourite" - }, - { - "defaultMessage": "to boost", - "id": "keyboard_shortcuts.boost" - }, - { - "defaultMessage": "to open status", - "id": "keyboard_shortcuts.enter" - }, - { - "defaultMessage": "to open media", - "id": "keyboard_shortcuts.open_media" - }, - { - "defaultMessage": "to show/hide text behind CW", - "id": "keyboard_shortcuts.toggle_hidden" - }, - { - "defaultMessage": "to show/hide media", - "id": "keyboard_shortcuts.toggle_sensitivity" - }, - { - "defaultMessage": "to move up in the list", - "id": "keyboard_shortcuts.up" - }, - { - "defaultMessage": "to move down in the list", - "id": "keyboard_shortcuts.down" - }, - { - "defaultMessage": "to focus a status in one of the columns", - "id": "keyboard_shortcuts.column" - }, - { - "defaultMessage": "to focus the compose textarea", - "id": "keyboard_shortcuts.compose" - }, - { - "defaultMessage": "to start a brand new post", - "id": "keyboard_shortcuts.toot" - }, - { - "defaultMessage": "to show/hide CW field", - "id": "keyboard_shortcuts.spoilers" - }, - { - "defaultMessage": "to navigate back", - "id": "keyboard_shortcuts.back" - }, - { - "defaultMessage": "to focus search", - "id": "keyboard_shortcuts.search" - }, - { - "defaultMessage": "to un-focus compose textarea/search", - "id": "keyboard_shortcuts.unfocus" - }, - { - "defaultMessage": "to open home timeline", - "id": "keyboard_shortcuts.home" - }, - { - "defaultMessage": "to open notifications column", - "id": "keyboard_shortcuts.notifications" - }, - { - "defaultMessage": "to open local timeline", - "id": "keyboard_shortcuts.local" - }, - { - "defaultMessage": "to open federated timeline", - "id": "keyboard_shortcuts.federated" - }, - { - "defaultMessage": "to open direct messages column", - "id": "keyboard_shortcuts.direct" - }, - { - "defaultMessage": "to open \"get started\" column", - "id": "keyboard_shortcuts.start" - }, - { - "defaultMessage": "to open favourites list", - "id": "keyboard_shortcuts.favourites" - }, - { - "defaultMessage": "to open pinned posts list", - "id": "keyboard_shortcuts.pinned" - }, - { - "defaultMessage": "to open your profile", - "id": "keyboard_shortcuts.my_profile" - }, - { - "defaultMessage": "to open blocked users list", - "id": "keyboard_shortcuts.blocked" - }, - { - "defaultMessage": "to open muted users list", - "id": "keyboard_shortcuts.muted" - }, - { - "defaultMessage": "to open follow requests list", - "id": "keyboard_shortcuts.requests" - }, - { - "defaultMessage": "to display this legend", - "id": "keyboard_shortcuts.legend" - } - ], - "path": "app/javascript/mastodon/features/keyboard_shortcuts/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Remove from list", - "id": "lists.account.remove" - }, - { - "defaultMessage": "Add to list", - "id": "lists.account.add" - } - ], - "path": "app/javascript/mastodon/features/list_adder/components/list.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Remove from list", - "id": "lists.account.remove" - }, - { - "defaultMessage": "Add to list", - "id": "lists.account.add" - } - ], - "path": "app/javascript/mastodon/features/list_editor/components/account.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Change title", - "id": "lists.edit.submit" - } - ], - "path": "app/javascript/mastodon/features/list_editor/components/edit_list_form.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Search among people you follow", - "id": "lists.search" - } - ], - "path": "app/javascript/mastodon/features/list_editor/components/search.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Are you sure you want to permanently delete this list?", - "id": "confirmations.delete_list.message" - }, - { - "defaultMessage": "Delete", - "id": "confirmations.delete_list.confirm" - }, - { - "defaultMessage": "Any followed user", - "id": "lists.replies_policy.followed" - }, - { - "defaultMessage": "No one", - "id": "lists.replies_policy.none" - }, - { - "defaultMessage": "Members of the list", - "id": "lists.replies_policy.list" - }, - { - "defaultMessage": "Edit list", - "id": "lists.edit" - }, - { - "defaultMessage": "Delete list", - "id": "lists.delete" - }, - { - "defaultMessage": "Show replies to:", - "id": "lists.replies_policy.title" - }, - { - "defaultMessage": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", - "id": "empty_column.list" - } - ], - "path": "app/javascript/mastodon/features/list_timeline/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "New list title", - "id": "lists.new.title_placeholder" - }, - { - "defaultMessage": "Add list", - "id": "lists.new.create" - } - ], - "path": "app/javascript/mastodon/features/lists/components/new_list_form.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Lists", - "id": "column.lists" - }, - { - "defaultMessage": "Your lists", - "id": "lists.subheading" - }, - { - "defaultMessage": "You don't have any lists yet. When you create one, it will show up here.", - "id": "empty_column.lists" - } - ], - "path": "app/javascript/mastodon/features/lists/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Muted users", - "id": "column.mutes" - }, - { - "defaultMessage": "You haven't muted any users yet.", - "id": "empty_column.mutes" - } - ], - "path": "app/javascript/mastodon/features/mutes/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Clear notifications", - "id": "notifications.clear" - } - ], - "path": "app/javascript/mastodon/features/notifications/components/clear_column_button.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Highlight unread notifications", - "id": "notifications.column_settings.unread_notifications.highlight" - }, - { - "defaultMessage": "Show filter bar", - "id": "notifications.column_settings.filter_bar.show_bar" - }, - { - "defaultMessage": "Display all categories", - "id": "notifications.column_settings.filter_bar.advanced" - }, - { - "defaultMessage": "Desktop notifications", - "id": "notifications.column_settings.alert" - }, - { - "defaultMessage": "Show in column", - "id": "notifications.column_settings.show" - }, - { - "defaultMessage": "Play sound", - "id": "notifications.column_settings.sound" - }, - { - "defaultMessage": "Push notifications", - "id": "notifications.column_settings.push" - }, - { - "defaultMessage": "Desktop notifications are unavailable due to previously denied browser permissions request", - "id": "notifications.permission_denied" - }, - { - "defaultMessage": "Desktop notifications are unavailable because the required permission has not been granted.", - "id": "notifications.permission_required" - }, - { - "defaultMessage": "Unread notifications", - "id": "notifications.column_settings.unread_notifications.category" - }, - { - "defaultMessage": "Quick filter bar", - "id": "notifications.column_settings.filter_bar.category" - }, - { - "defaultMessage": "New followers:", - "id": "notifications.column_settings.follow" - }, - { - "defaultMessage": "New follow requests:", - "id": "notifications.column_settings.follow_request" - }, - { - "defaultMessage": "Favourites:", - "id": "notifications.column_settings.favourite" - }, - { - "defaultMessage": "Mentions:", - "id": "notifications.column_settings.mention" - }, - { - "defaultMessage": "Boosts:", - "id": "notifications.column_settings.reblog" - }, - { - "defaultMessage": "Poll results:", - "id": "notifications.column_settings.poll" - }, - { - "defaultMessage": "New posts:", - "id": "notifications.column_settings.status" - }, - { - "defaultMessage": "Edits:", - "id": "notifications.column_settings.update" - }, - { - "defaultMessage": "New sign-ups:", - "id": "notifications.column_settings.admin.sign_up" - }, - { - "defaultMessage": "New reports:", - "id": "notifications.column_settings.admin.report" - } - ], - "path": "app/javascript/mastodon/features/notifications/components/column_settings.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Mentions", - "id": "notifications.filter.mentions" - }, - { - "defaultMessage": "Favourites", - "id": "notifications.filter.favourites" - }, - { - "defaultMessage": "Boosts", - "id": "notifications.filter.boosts" - }, - { - "defaultMessage": "Poll results", - "id": "notifications.filter.polls" - }, - { - "defaultMessage": "Follows", - "id": "notifications.filter.follows" - }, - { - "defaultMessage": "Updates from people you follow", - "id": "notifications.filter.statuses" - }, - { - "defaultMessage": "All", - "id": "notifications.filter.all" - } - ], - "path": "app/javascript/mastodon/features/notifications/components/filter_bar.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Authorize", - "id": "follow_request.authorize" - }, - { - "defaultMessage": "Reject", - "id": "follow_request.reject" - } - ], - "path": "app/javascript/mastodon/features/notifications/components/follow_request.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Grant permission.", - "id": "notifications.grant_permission" - } - ], - "path": "app/javascript/mastodon/features/notifications/components/grant_permission_button.json" - }, - { - "descriptors": [ - { - "defaultMessage": "{name} favourited your status", - "id": "notification.favourite" - }, - { - "defaultMessage": "{name} followed you", - "id": "notification.follow" - }, - { - "defaultMessage": "Your poll has ended", - "id": "notification.own_poll" - }, - { - "defaultMessage": "A poll you have voted in has ended", - "id": "notification.poll" - }, - { - "defaultMessage": "{name} boosted your status", - "id": "notification.reblog" - }, - { - "defaultMessage": "{name} just posted", - "id": "notification.status" - }, - { - "defaultMessage": "{name} edited a post", - "id": "notification.update" - }, - { - "defaultMessage": "{name} signed up", - "id": "notification.admin.sign_up" - }, - { - "defaultMessage": "{name} reported {target}", - "id": "notification.admin.report" - }, - { - "defaultMessage": "{name} has requested to follow you", - "id": "notification.follow_request" - } - ], - "path": "app/javascript/mastodon/features/notifications/components/notification.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Close", - "id": "lightbox.close" - }, - { - "defaultMessage": "Never miss a thing", - "id": "notifications_permission_banner.title" - }, - { - "defaultMessage": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.", - "id": "notifications_permission_banner.how_to_control" - }, - { - "defaultMessage": "Enable desktop notifications", - "id": "notifications_permission_banner.enable" - } - ], - "path": "app/javascript/mastodon/features/notifications/components/notifications_permission_banner.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Open report", - "id": "report_notification.open" - }, - { - "defaultMessage": "Other", - "id": "report_notification.categories.other" - }, - { - "defaultMessage": "Spam", - "id": "report_notification.categories.spam" - }, - { - "defaultMessage": "Rule violation", - "id": "report_notification.categories.violation" - }, - { - "defaultMessage": "{count, plural, one {# post} other {# posts}} attached", - "id": "report_notification.attached_statuses" - } - ], - "path": "app/javascript/mastodon/features/notifications/components/report.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Are you sure you want to permanently clear all your notifications?", - "id": "notifications.clear_confirmation" - }, - { - "defaultMessage": "Clear notifications", - "id": "notifications.clear" - }, - { - "defaultMessage": "Desktop notifications can't be enabled, as browser permission has been denied before", - "id": "notifications.permission_denied_alert" - } - ], - "path": "app/javascript/mastodon/features/notifications/containers/column_settings_container.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Notifications", - "id": "column.notifications" - }, - { - "defaultMessage": "Mark every notification as read", - "id": "notifications.mark_as_read" - }, - { - "defaultMessage": "You don't have any notifications yet. When other people interact with you, you will see it here.", - "id": "empty_column.notifications" - } - ], - "path": "app/javascript/mastodon/features/notifications/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.", - "id": "onboarding.follows.empty" - }, - { - "defaultMessage": "Popular on Mastodon", - "id": "onboarding.follows.title" - }, - { - "defaultMessage": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!", - "id": "onboarding.follows.lead" - }, - { - "defaultMessage": "Did you know? Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!", - "id": "onboarding.tips.accounts_from_other_servers" - }, - { - "defaultMessage": "Take me back", - "id": "onboarding.actions.back" - } - ], - "path": "app/javascript/mastodon/features/onboarding/follows.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Hello #Mastodon!", - "id": "onboarding.compose.template" - }, - { - "defaultMessage": "You've made it!", - "id": "onboarding.start.title" - }, - { - "defaultMessage": "Your new Mastodon account is ready to go. Here's how you can make the most of it:", - "id": "onboarding.start.lead" - }, - { - "defaultMessage": "Customize your profile", - "id": "onboarding.steps.setup_profile.title" - }, - { - "defaultMessage": "Others are more likely to interact with you with a filled out profile.", - "id": "onboarding.steps.setup_profile.body" - }, - { - "defaultMessage": "Follow {count, plural, one {one person} other {# people}}", - "id": "onboarding.steps.follow_people.title" - }, - { - "defaultMessage": "You curate your own feed. Let's fill it with interesting people.", - "id": "onboarding.steps.follow_people.body" - }, - { - "defaultMessage": "Make your first post", - "id": "onboarding.steps.publish_status.title" - }, - { - "defaultMessage": "Say hello to the world.", - "id": "onboarding.steps.publish_status.body" - }, - { - "defaultMessage": "Share your profile", - "id": "onboarding.steps.share_profile.title" - }, - { - "defaultMessage": "Let your friends know how to find you on Mastodon!", - "id": "onboarding.steps.share_profile.body" - }, - { - "defaultMessage": "Want to skip right ahead?", - "id": "onboarding.start.skip" - }, - { - "defaultMessage": "See what's trending", - "id": "onboarding.actions.go_to_explore" - }, - { - "defaultMessage": "Don't show this screen again", - "id": "onboarding.actions.close" - } - ], - "path": "app/javascript/mastodon/features/onboarding/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "I'm {username} on #Mastodon! Come follow me at {url}", - "id": "onboarding.share.message" - }, - { - "defaultMessage": "Copied", - "id": "copypaste.copied" - }, - { - "defaultMessage": "Copy to clipboard", - "id": "copypaste.copy_to_clipboard" - }, - { - "defaultMessage": "Share your profile", - "id": "onboarding.share.title" - }, - { - "defaultMessage": "Let people know how they can find you on Mastodon!", - "id": "onboarding.share.lead" - }, - { - "defaultMessage": "Did you know? You can verify your account by putting a link to your Mastodon profile on your own website and adding the website to your profile. No fees or documents necessary!", - "id": "onboarding.tips.verification" - }, - { - "defaultMessage": "Did you know? If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!", - "id": "onboarding.tips.migration" - }, - { - "defaultMessage": "Did you know? You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!", - "id": "onboarding.tips.2fa" - }, - { - "defaultMessage": "Possible next steps:", - "id": "onboarding.share.next_steps" - }, - { - "defaultMessage": "Go to your home feed", - "id": "onboarding.actions.go_to_home" - }, - { - "defaultMessage": "See what's trending", - "id": "onboarding.actions.go_to_explore" - }, - { - "defaultMessage": "Take me back", - "id": "onboarding.action.back" - } - ], - "path": "app/javascript/mastodon/features/onboarding/share.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Reply", - "id": "status.reply" - }, - { - "defaultMessage": "Reply to thread", - "id": "status.replyAll" - }, - { - "defaultMessage": "Boost", - "id": "status.reblog" - }, - { - "defaultMessage": "Boost with original visibility", - "id": "status.reblog_private" - }, - { - "defaultMessage": "Unboost", - "id": "status.cancel_reblog_private" - }, - { - "defaultMessage": "This post cannot be boosted", - "id": "status.cannot_reblog" - }, - { - "defaultMessage": "Favourite", - "id": "status.favourite" - }, - { - "defaultMessage": "Reply", - "id": "confirmations.reply.confirm" - }, - { - "defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", - "id": "confirmations.reply.message" - }, - { - "defaultMessage": "Expand this status", - "id": "status.open" - } - ], - "path": "app/javascript/mastodon/features/picture_in_picture/components/footer.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Close", - "id": "lightbox.close" - } - ], - "path": "app/javascript/mastodon/features/picture_in_picture/components/header.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Pinned post", - "id": "column.pins" - } - ], - "path": "app/javascript/mastodon/features/pinned_statuses/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Privacy Policy", - "id": "privacy_policy.title" - }, - { - "defaultMessage": "Last updated {date}", - "id": "privacy_policy.last_updated" - } - ], - "path": "app/javascript/mastodon/features/privacy_policy/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Media only", - "id": "community.column_settings.media_only" - }, - { - "defaultMessage": "Remote only", - "id": "community.column_settings.remote_only" - } - ], - "path": "app/javascript/mastodon/features/public_timeline/components/column_settings.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Federated timeline", - "id": "column.public" - }, - { - "defaultMessage": "These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.", - "id": "dismissable_banner.public_timeline" - }, - { - "defaultMessage": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", - "id": "empty_column.public" - } - ], - "path": "app/javascript/mastodon/features/public_timeline/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Refresh", - "id": "refresh" - }, - { - "defaultMessage": "No one has boosted this post yet. When someone does, they will show up here.", - "id": "status.reblogs.empty" - } - ], - "path": "app/javascript/mastodon/features/reblogs/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "I don't like it", - "id": "report.reasons.dislike" - }, - { - "defaultMessage": "It is not something you want to see", - "id": "report.reasons.dislike_description" - }, - { - "defaultMessage": "It's spam", - "id": "report.reasons.spam" - }, - { - "defaultMessage": "Malicious links, fake engagement, or repetitive replies", - "id": "report.reasons.spam_description" - }, - { - "defaultMessage": "It violates server rules", - "id": "report.reasons.violation" - }, - { - "defaultMessage": "You are aware that it breaks specific rules", - "id": "report.reasons.violation_description" - }, - { - "defaultMessage": "It's something else", - "id": "report.reasons.other" - }, - { - "defaultMessage": "The issue does not fit into other categories", - "id": "report.reasons.other_description" - }, - { - "defaultMessage": "post", - "id": "report.category.title_status" - }, - { - "defaultMessage": "profile", - "id": "report.category.title_account" - }, - { - "defaultMessage": "Tell us what's going on with this {type}", - "id": "report.category.title" - }, - { - "defaultMessage": "Choose the best match", - "id": "report.category.subtitle" - }, - { - "defaultMessage": "Next", - "id": "report.next" - } - ], - "path": "app/javascript/mastodon/features/report/category.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Type or paste additional comments", - "id": "report.placeholder" - }, - { - "defaultMessage": "Is there anything else you think we should know?", - "id": "report.comment.title" - }, - { - "defaultMessage": "The account is from another server. Send an anonymized copy of the report there as well?", - "id": "report.forward_hint" - }, - { - "defaultMessage": "Forward to {target}", - "id": "report.forward" - }, - { - "defaultMessage": "Submit report", - "id": "report.submit" - } - ], - "path": "app/javascript/mastodon/features/report/comment.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Public", - "id": "privacy.public.short" - }, - { - "defaultMessage": "Unlisted", - "id": "privacy.unlisted.short" - }, - { - "defaultMessage": "Followers only", - "id": "privacy.private.short" - }, - { - "defaultMessage": "Mentioned people only", - "id": "privacy.direct.short" - } - ], - "path": "app/javascript/mastodon/features/report/components/status_check_box.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Which rules are being violated?", - "id": "report.rules.title" - }, - { - "defaultMessage": "Select all that apply", - "id": "report.rules.subtitle" - }, - { - "defaultMessage": "Next", - "id": "report.next" - } - ], - "path": "app/javascript/mastodon/features/report/rules.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Are there any posts that back up this report?", - "id": "report.statuses.title" - }, - { - "defaultMessage": "Select all that apply", - "id": "report.statuses.subtitle" - }, - { - "defaultMessage": "Next", - "id": "report.next" - } - ], - "path": "app/javascript/mastodon/features/report/statuses.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Thanks for reporting, we'll look into this.", - "id": "report.thanks.title_actionable" - }, - { - "defaultMessage": "Don't want to see this?", - "id": "report.thanks.title" - }, - { - "defaultMessage": "While we review this, you can take action against @{name}:", - "id": "report.thanks.take_action_actionable" - }, - { - "defaultMessage": "Here are your options for controlling what you see on Mastodon:", - "id": "report.thanks.take_action" - }, - { - "defaultMessage": "Unfollow @{name}", - "id": "report.unfollow" - }, - { - "defaultMessage": "You are following this account. To not see their posts in your home feed anymore, unfollow them.", - "id": "report.unfollow_explanation" - }, - { - "defaultMessage": "Unfollow", - "id": "account.unfollow" - }, - { - "defaultMessage": "Mute @{name}", - "id": "account.mute" - }, - { - "defaultMessage": "You will not see their posts. They can still follow you and see your posts and will not know that they are muted.", - "id": "report.mute_explanation" - }, - { - "defaultMessage": "Mute", - "id": "report.mute" - }, - { - "defaultMessage": "Muted", - "id": "account.muted" - }, - { - "defaultMessage": "Block @{name}", - "id": "account.block" - }, - { - "defaultMessage": "You will not see their posts. They will not be able to see your posts or follow you. They will be able to tell that they are blocked.", - "id": "report.block_explanation" - }, - { - "defaultMessage": "Block", - "id": "report.block" - }, - { - "defaultMessage": "Blocked", - "id": "account.blocked" - }, - { - "defaultMessage": "Done", - "id": "report.close" - } - ], - "path": "app/javascript/mastodon/features/report/thanks.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Delete", - "id": "status.delete" - }, - { - "defaultMessage": "Delete & re-draft", - "id": "status.redraft" - }, - { - "defaultMessage": "Edit", - "id": "status.edit" - }, - { - "defaultMessage": "Privately mention @{name}", - "id": "status.direct" - }, - { - "defaultMessage": "Mention @{name}", - "id": "status.mention" - }, - { - "defaultMessage": "Reply", - "id": "status.reply" - }, - { - "defaultMessage": "Boost", - "id": "status.reblog" - }, - { - "defaultMessage": "Boost with original visibility", - "id": "status.reblog_private" - }, - { - "defaultMessage": "Unboost", - "id": "status.cancel_reblog_private" - }, - { - "defaultMessage": "This post cannot be boosted", - "id": "status.cannot_reblog" - }, - { - "defaultMessage": "Favourite", - "id": "status.favourite" - }, - { - "defaultMessage": "Bookmark", - "id": "status.bookmark" - }, - { - "defaultMessage": "More", - "id": "status.more" - }, - { - "defaultMessage": "Mute @{name}", - "id": "status.mute" - }, - { - "defaultMessage": "Mute conversation", - "id": "status.mute_conversation" - }, - { - "defaultMessage": "Unmute conversation", - "id": "status.unmute_conversation" - }, - { - "defaultMessage": "Block @{name}", - "id": "status.block" - }, - { - "defaultMessage": "Report @{name}", - "id": "status.report" - }, - { - "defaultMessage": "Share", - "id": "status.share" - }, - { - "defaultMessage": "Pin on profile", - "id": "status.pin" - }, - { - "defaultMessage": "Unpin from profile", - "id": "status.unpin" - }, - { - "defaultMessage": "Embed", - "id": "status.embed" - }, - { - "defaultMessage": "Open moderation interface for @{name}", - "id": "status.admin_account" - }, - { - "defaultMessage": "Open this post in the moderation interface", - "id": "status.admin_status" - }, - { - "defaultMessage": "Open moderation interface for {domain}", - "id": "status.admin_domain" - }, - { - "defaultMessage": "Copy link to post", - "id": "status.copy" - }, - { - "defaultMessage": "Block domain {domain}", - "id": "account.block_domain" - }, - { - "defaultMessage": "Unblock domain {domain}", - "id": "account.unblock_domain" - }, - { - "defaultMessage": "Unmute @{name}", - "id": "account.unmute" - }, - { - "defaultMessage": "Unblock @{name}", - "id": "account.unblock" - }, - { - "defaultMessage": "Open original page", - "id": "account.open_original_page" - } - ], - "path": "app/javascript/mastodon/features/status/components/action_bar.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Sensitive content", - "id": "status.sensitive_warning" - } - ], - "path": "app/javascript/mastodon/features/status/components/card.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Public", - "id": "privacy.public.short" - }, - { - "defaultMessage": "Unlisted", - "id": "privacy.unlisted.short" - }, - { - "defaultMessage": "Followers only", - "id": "privacy.private.short" - }, - { - "defaultMessage": "Mentioned people only", - "id": "privacy.direct.short" - }, - { - "defaultMessage": "Private mention", - "id": "status.direct_indicator" - } - ], - "path": "app/javascript/mastodon/features/status/components/detailed_status.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Delete", - "id": "confirmations.delete.confirm" - }, - { - "defaultMessage": "Are you sure you want to delete this status?", - "id": "confirmations.delete.message" - }, - { - "defaultMessage": "Delete & redraft", - "id": "confirmations.redraft.confirm" - }, - { - "defaultMessage": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", - "id": "confirmations.redraft.message" - }, - { - "defaultMessage": "Show more for all", - "id": "status.show_more_all" - }, - { - "defaultMessage": "Show less for all", - "id": "status.show_less_all" - }, - { - "defaultMessage": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}", - "id": "status.title.with_attachments" - }, - { - "defaultMessage": "Detailed conversation view", - "id": "status.detailed_status" - }, - { - "defaultMessage": "Reply", - "id": "confirmations.reply.confirm" - }, - { - "defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", - "id": "confirmations.reply.message" - }, - { - "defaultMessage": "Block entire domain", - "id": "confirmations.domain_block.confirm" - }, - { - "defaultMessage": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", - "id": "confirmations.domain_block.message" - } - ], - "path": "app/javascript/mastodon/features/status/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Close", - "id": "lightbox.close" - }, - { - "defaultMessage": "Change subscribed languages for {target}", - "id": "subscribed_languages.target" - }, - { - "defaultMessage": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.", - "id": "subscribed_languages.lead" - }, - { - "defaultMessage": "Save changes", - "id": "subscribed_languages.save" - } - ], - "path": "app/javascript/mastodon/features/subscribed_languages_modal/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Are you sure you want to block {name}?", - "id": "confirmations.block.message" - }, - { - "defaultMessage": "Cancel", - "id": "confirmation_modal.cancel" - }, - { - "defaultMessage": "Block & Report", - "id": "confirmations.block.block_and_report" - }, - { - "defaultMessage": "Block", - "id": "confirmations.block.confirm" - } - ], - "path": "app/javascript/mastodon/features/ui/components/block_modal.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Unboost", - "id": "status.cancel_reblog_private" - }, - { - "defaultMessage": "Boost", - "id": "status.reblog" - }, - { - "defaultMessage": "Public", - "id": "privacy.public.short" - }, - { - "defaultMessage": "Unlisted", - "id": "privacy.unlisted.short" - }, - { - "defaultMessage": "Followers only", - "id": "privacy.private.short" - }, - { - "defaultMessage": "Mentioned people only", - "id": "privacy.direct.short" - }, - { - "defaultMessage": "You can press {combo} to skip this next time", - "id": "boost_modal.combo" - } - ], - "path": "app/javascript/mastodon/features/ui/components/boost_modal.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Copied", - "id": "copypaste.copied" - }, - { - "defaultMessage": "404", - "id": "bundle_column_error.routing.title" - }, - { - "defaultMessage": "The requested page could not be found. Are you sure the URL in the address bar is correct?", - "id": "bundle_column_error.routing.body" - }, - { - "defaultMessage": "Network error", - "id": "bundle_column_error.network.title" - }, - { - "defaultMessage": "There was an error when trying to load this page. This could be due to a temporary problem with your internet connection or this server.", - "id": "bundle_column_error.network.body" - }, - { - "defaultMessage": "Oh, no!", - "id": "bundle_column_error.error.title" - }, - { - "defaultMessage": "The requested page could not be rendered. It could be due to a bug in our code, or a browser compatibility issue.", - "id": "bundle_column_error.error.body" - }, - { - "defaultMessage": "Try again", - "id": "bundle_column_error.retry" - }, - { - "defaultMessage": "Copy error report", - "id": "bundle_column_error.copy_stacktrace" - }, - { - "defaultMessage": "Go back home", - "id": "bundle_column_error.return" - } - ], - "path": "app/javascript/mastodon/features/ui/components/bundle_column_error.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Something went wrong while loading this component.", - "id": "bundle_modal_error.message" - }, - { - "defaultMessage": "Try again", - "id": "bundle_modal_error.retry" - }, - { - "defaultMessage": "Close", - "id": "bundle_modal_error.close" - } - ], - "path": "app/javascript/mastodon/features/ui/components/bundle_modal_error.json" - }, - { - "descriptors": [ - { - "defaultMessage": "{name} created {date}", - "id": "status.history.created" - }, - { - "defaultMessage": "{name} edited {date}", - "id": "status.history.edited" - } - ], - "path": "app/javascript/mastodon/features/ui/components/compare_history_modal.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Cancel", - "id": "confirmation_modal.cancel" - } - ], - "path": "app/javascript/mastodon/features/ui/components/confirmation_modal.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Are you sure you want to log out?", - "id": "confirmations.logout.message" - }, - { - "defaultMessage": "Log out", - "id": "confirmations.logout.confirm" - }, - { - "defaultMessage": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.", - "id": "moved_to_account_banner.text" - }, - { - "defaultMessage": "Your account {disabledAccount} is currently disabled.", - "id": "disabled_account_banner.text" - }, - { - "defaultMessage": "Account settings", - "id": "disabled_account_banner.account_settings" - } - ], - "path": "app/javascript/mastodon/features/ui/components/disabled_account_banner.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Close", - "id": "lightbox.close" - }, - { - "defaultMessage": "Embed", - "id": "status.embed" - }, - { - "defaultMessage": "Embed this status on your website by copying the code below.", - "id": "embed.instructions" - }, - { - "defaultMessage": "Here is what it will look like:", - "id": "embed.preview" - } - ], - "path": "app/javascript/mastodon/features/ui/components/embed_modal.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Close", - "id": "lightbox.close" - }, - { - "defaultMessage": "Filter a post", - "id": "filter_modal.title.status" - } - ], - "path": "app/javascript/mastodon/features/ui/components/filter_modal.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Close", - "id": "lightbox.close" - }, - { - "defaultMessage": "Apply", - "id": "upload_modal.apply" - }, - { - "defaultMessage": "Applying…", - "id": "upload_modal.applying" - }, - { - "defaultMessage": "A quick brown fox jumps over the lazy dog", - "id": "upload_modal.description_placeholder" - }, - { - "defaultMessage": "Choose image", - "id": "upload_modal.choose_image" - }, - { - "defaultMessage": "You have unsaved changes to the media description or preview, discard them anyway?", - "id": "confirmations.discard_edit_media.message" - }, - { - "defaultMessage": "Discard", - "id": "confirmations.discard_edit_media.confirm" - }, - { - "defaultMessage": "Describe for people who are hard of hearing", - "id": "upload_form.audio_description" - }, - { - "defaultMessage": "Describe for people who are deaf, hard of hearing, blind or have low vision", - "id": "upload_form.video_description" - }, - { - "defaultMessage": "Describe for people who are blind or have low vision", - "id": "upload_form.description" - }, - { - "defaultMessage": "Analyzing picture…", - "id": "upload_modal.analyzing_picture" - }, - { - "defaultMessage": "Preparing OCR…", - "id": "upload_modal.preparing_ocr" - }, - { - "defaultMessage": "Edit media", - "id": "upload_modal.edit_media" - }, - { - "defaultMessage": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.", - "id": "upload_modal.hint" - }, - { - "defaultMessage": "Change thumbnail", - "id": "upload_form.thumbnail" - }, - { - "defaultMessage": "Detect text from picture", - "id": "upload_modal.detect_text" - }, - { - "defaultMessage": "Preview ({ratio})", - "id": "upload_modal.preview_label" - } - ], - "path": "app/javascript/mastodon/features/ui/components/focal_point_modal.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Follow requests", - "id": "navigation_bar.follow_requests" - } - ], - "path": "app/javascript/mastodon/features/ui/components/follow_requests_column_link.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Publish", - "id": "compose_form.publish_form" - }, - { - "defaultMessage": "Create account", - "id": "sign_in_banner.create_account" - }, - { - "defaultMessage": "Login", - "id": "sign_in_banner.sign_in" - } - ], - "path": "app/javascript/mastodon/features/ui/components/header.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Close", - "id": "lightbox.close" - } - ], - "path": "app/javascript/mastodon/features/ui/components/image_modal.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Are you sure you want to log out?", - "id": "confirmations.logout.message" - }, - { - "defaultMessage": "Log out", - "id": "confirmations.logout.confirm" - }, - { - "defaultMessage": "About", - "id": "footer.about" - }, - { - "defaultMessage": "Status", - "id": "footer.status" - }, - { - "defaultMessage": "Invite people", - "id": "footer.invite" - }, - { - "defaultMessage": "Profiles directory", - "id": "footer.directory" - }, - { - "defaultMessage": "Privacy policy", - "id": "footer.privacy_policy" - }, - { - "defaultMessage": "Get the app", - "id": "footer.get_app" - }, - { - "defaultMessage": "Keyboard shortcuts", - "id": "footer.keyboard_shortcuts" - }, - { - "defaultMessage": "View source code", - "id": "footer.source_code" - } - ], - "path": "app/javascript/mastodon/features/ui/components/link_footer.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Close", - "id": "lightbox.close" - }, - { - "defaultMessage": "Previous", - "id": "lightbox.previous" - }, - { - "defaultMessage": "Next", - "id": "lightbox.next" - } - ], - "path": "app/javascript/mastodon/features/ui/components/media_modal.json" - }, - { - "descriptors": [ - { - "defaultMessage": "{number, plural, one {# minute} other {# minutes}}", - "id": "intervals.full.minutes" - }, - { - "defaultMessage": "{number, plural, one {# hour} other {# hours}}", - "id": "intervals.full.hours" - }, - { - "defaultMessage": "{number, plural, one {# day} other {# days}}", - "id": "intervals.full.days" - }, - { - "defaultMessage": "Indefinite", - "id": "mute_modal.indefinite" - }, - { - "defaultMessage": "Are you sure you want to mute {name}?", - "id": "confirmations.mute.message" - }, - { - "defaultMessage": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", - "id": "confirmations.mute.explanation" - }, - { - "defaultMessage": "Hide notifications from this user?", - "id": "mute_modal.hide_notifications" - }, - { - "defaultMessage": "Duration", - "id": "mute_modal.duration" - }, - { - "defaultMessage": "Cancel", - "id": "confirmation_modal.cancel" - }, - { - "defaultMessage": "Mute", - "id": "confirmations.mute.confirm" - } - ], - "path": "app/javascript/mastodon/features/ui/components/mute_modal.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Home", - "id": "tabs_bar.home" - }, - { - "defaultMessage": "Notifications", - "id": "tabs_bar.notifications" - }, - { - "defaultMessage": "Explore", - "id": "explore.title" - }, - { - "defaultMessage": "Local", - "id": "tabs_bar.local_timeline" - }, - { - "defaultMessage": "Federated", - "id": "tabs_bar.federated_timeline" - }, - { - "defaultMessage": "Private mentions", - "id": "navigation_bar.direct" - }, - { - "defaultMessage": "Favourites", - "id": "navigation_bar.favourites" - }, - { - "defaultMessage": "Bookmarks", - "id": "navigation_bar.bookmarks" - }, - { - "defaultMessage": "Lists", - "id": "navigation_bar.lists" - }, - { - "defaultMessage": "Preferences", - "id": "navigation_bar.preferences" - }, - { - "defaultMessage": "Follows and followers", - "id": "navigation_bar.follows_and_followers" - }, - { - "defaultMessage": "About", - "id": "navigation_bar.about" - }, - { - "defaultMessage": "Search", - "id": "navigation_bar.search" - } - ], - "path": "app/javascript/mastodon/features/ui/components/navigation_panel.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Close", - "id": "lightbox.close" - }, - { - "defaultMessage": "Report {target}", - "id": "report.target" - } - ], - "path": "app/javascript/mastodon/features/ui/components/report_modal.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Create account", - "id": "sign_in_banner.create_account" - }, - { - "defaultMessage": "Login to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.", - "id": "sign_in_banner.text" - }, - { - "defaultMessage": "Login", - "id": "sign_in_banner.sign_in" - } - ], - "path": "app/javascript/mastodon/features/ui/components/sign_in_banner.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Drag & drop to upload", - "id": "upload_area.title" - } - ], - "path": "app/javascript/mastodon/features/ui/components/upload_area.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Compress image view box", - "id": "lightbox.compress" - }, - { - "defaultMessage": "Expand image view box", - "id": "lightbox.expand" - } - ], - "path": "app/javascript/mastodon/features/ui/components/zoomable_image.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Your draft will be lost if you leave Mastodon.", - "id": "ui.beforeunload" - } - ], - "path": "app/javascript/mastodon/features/ui/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Play", - "id": "video.play" - }, - { - "defaultMessage": "Pause", - "id": "video.pause" - }, - { - "defaultMessage": "Mute sound", - "id": "video.mute" - }, - { - "defaultMessage": "Unmute sound", - "id": "video.unmute" - }, - { - "defaultMessage": "Hide video", - "id": "video.hide" - }, - { - "defaultMessage": "Expand video", - "id": "video.expand" - }, - { - "defaultMessage": "Close video", - "id": "video.close" - }, - { - "defaultMessage": "Full screen", - "id": "video.fullscreen" - }, - { - "defaultMessage": "Exit full screen", - "id": "video.exit_fullscreen" - }, - { - "defaultMessage": "Sensitive content", - "id": "status.sensitive_warning" - }, - { - "defaultMessage": "Media hidden", - "id": "status.media_hidden" - } - ], - "path": "app/javascript/mastodon/features/video/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "That username is taken. Try another", - "id": "username.taken" - }, - { - "defaultMessage": "Password confirmation exceeds the maximum password length", - "id": "password_confirmation.exceeds_maxlength" - }, - { - "defaultMessage": "Password confirmation does not match", - "id": "password_confirmation.mismatching" - } - ], - "path": "app/javascript/packs/public.json" - } -] \ No newline at end of file diff --git a/app/javascript/mastodon/locales/index.js b/app/javascript/mastodon/locales/index.js index 421cb7fab..6e57e3ddc 100644 --- a/app/javascript/mastodon/locales/index.js +++ b/app/javascript/mastodon/locales/index.js @@ -7,3 +7,16 @@ export function setLocale(locale) { export function getLocale() { return theLocale; } + +export function onProviderError(error) { + // Silent the error, like upstream does + if(process.env.NODE_ENV === 'production') return; + + // This browser does not advertise Intl support for this locale, we only print a warning + // As-per the spec, the browser should select the best matching locale + if(typeof error === "object" && error.message.match("MISSING_DATA")) { + console.warn(error.message); + } + + console.error(error); +} diff --git a/app/javascript/mastodon/locales/whitelist_af.json b/app/javascript/mastodon/locales/whitelist_af.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_af.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_an.json b/app/javascript/mastodon/locales/whitelist_an.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_an.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_ar.json b/app/javascript/mastodon/locales/whitelist_ar.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_ar.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_ast.json b/app/javascript/mastodon/locales/whitelist_ast.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_ast.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_be.json b/app/javascript/mastodon/locales/whitelist_be.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_be.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_bg.json b/app/javascript/mastodon/locales/whitelist_bg.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_bg.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_bn.json b/app/javascript/mastodon/locales/whitelist_bn.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_bn.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_br.json b/app/javascript/mastodon/locales/whitelist_br.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_br.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_bs.json b/app/javascript/mastodon/locales/whitelist_bs.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_bs.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_ca.json b/app/javascript/mastodon/locales/whitelist_ca.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_ca.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_ckb.json b/app/javascript/mastodon/locales/whitelist_ckb.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_ckb.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_co.json b/app/javascript/mastodon/locales/whitelist_co.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_co.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_cs.json b/app/javascript/mastodon/locales/whitelist_cs.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_cs.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_csb.json b/app/javascript/mastodon/locales/whitelist_csb.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_csb.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_cy.json b/app/javascript/mastodon/locales/whitelist_cy.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_cy.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_da.json b/app/javascript/mastodon/locales/whitelist_da.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_da.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_de.json b/app/javascript/mastodon/locales/whitelist_de.json deleted file mode 100644 index c311ad048..000000000 --- a/app/javascript/mastodon/locales/whitelist_de.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "account.badges.bot", - "compose_form.publish_loud", - "search_results.hashtags" -] diff --git a/app/javascript/mastodon/locales/whitelist_el.json b/app/javascript/mastodon/locales/whitelist_el.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_el.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_en-GB.json b/app/javascript/mastodon/locales/whitelist_en-GB.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_en-GB.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_en.json b/app/javascript/mastodon/locales/whitelist_en.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_en.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_eo.json b/app/javascript/mastodon/locales/whitelist_eo.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_eo.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_es-AR.json b/app/javascript/mastodon/locales/whitelist_es-AR.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_es-AR.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_es-MX.json b/app/javascript/mastodon/locales/whitelist_es-MX.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_es-MX.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_es.json b/app/javascript/mastodon/locales/whitelist_es.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_es.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_et.json b/app/javascript/mastodon/locales/whitelist_et.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_et.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_eu.json b/app/javascript/mastodon/locales/whitelist_eu.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_eu.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_fa.json b/app/javascript/mastodon/locales/whitelist_fa.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_fa.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_fi.json b/app/javascript/mastodon/locales/whitelist_fi.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_fi.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_fo.json b/app/javascript/mastodon/locales/whitelist_fo.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_fo.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_fr-QC.json b/app/javascript/mastodon/locales/whitelist_fr-QC.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_fr-QC.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_fr.json b/app/javascript/mastodon/locales/whitelist_fr.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_fr.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_fy.json b/app/javascript/mastodon/locales/whitelist_fy.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_fy.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_ga.json b/app/javascript/mastodon/locales/whitelist_ga.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_ga.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_gd.json b/app/javascript/mastodon/locales/whitelist_gd.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_gd.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_gl.json b/app/javascript/mastodon/locales/whitelist_gl.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_gl.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_he.json b/app/javascript/mastodon/locales/whitelist_he.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_he.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_hi.json b/app/javascript/mastodon/locales/whitelist_hi.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_hi.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_hr.json b/app/javascript/mastodon/locales/whitelist_hr.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_hr.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_hu.json b/app/javascript/mastodon/locales/whitelist_hu.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_hu.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_hy.json b/app/javascript/mastodon/locales/whitelist_hy.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_hy.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_id.json b/app/javascript/mastodon/locales/whitelist_id.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_id.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_ig.json b/app/javascript/mastodon/locales/whitelist_ig.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_ig.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_io.json b/app/javascript/mastodon/locales/whitelist_io.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_io.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_is.json b/app/javascript/mastodon/locales/whitelist_is.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_is.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_it.json b/app/javascript/mastodon/locales/whitelist_it.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_it.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_ja.json b/app/javascript/mastodon/locales/whitelist_ja.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_ja.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_ka.json b/app/javascript/mastodon/locales/whitelist_ka.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_ka.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_kab.json b/app/javascript/mastodon/locales/whitelist_kab.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_kab.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_kk.json b/app/javascript/mastodon/locales/whitelist_kk.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_kk.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_kn.json b/app/javascript/mastodon/locales/whitelist_kn.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_kn.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_ko.json b/app/javascript/mastodon/locales/whitelist_ko.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_ko.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_ku.json b/app/javascript/mastodon/locales/whitelist_ku.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_ku.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_kw.json b/app/javascript/mastodon/locales/whitelist_kw.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_kw.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_la.json b/app/javascript/mastodon/locales/whitelist_la.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_la.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_lt.json b/app/javascript/mastodon/locales/whitelist_lt.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_lt.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_lv.json b/app/javascript/mastodon/locales/whitelist_lv.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_lv.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_mk.json b/app/javascript/mastodon/locales/whitelist_mk.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_mk.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_ml.json b/app/javascript/mastodon/locales/whitelist_ml.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_ml.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_mr.json b/app/javascript/mastodon/locales/whitelist_mr.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_mr.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_ms.json b/app/javascript/mastodon/locales/whitelist_ms.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_ms.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_my.json b/app/javascript/mastodon/locales/whitelist_my.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_my.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_nl.json b/app/javascript/mastodon/locales/whitelist_nl.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_nl.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_nn.json b/app/javascript/mastodon/locales/whitelist_nn.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_nn.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_no.json b/app/javascript/mastodon/locales/whitelist_no.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_no.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_oc.json b/app/javascript/mastodon/locales/whitelist_oc.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_oc.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_pa.json b/app/javascript/mastodon/locales/whitelist_pa.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_pa.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_pl.json b/app/javascript/mastodon/locales/whitelist_pl.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_pl.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_pt-BR.json b/app/javascript/mastodon/locales/whitelist_pt-BR.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_pt-BR.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_pt-PT.json b/app/javascript/mastodon/locales/whitelist_pt-PT.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_pt-PT.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_ro.json b/app/javascript/mastodon/locales/whitelist_ro.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_ro.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_ru.json b/app/javascript/mastodon/locales/whitelist_ru.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_ru.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_sa.json b/app/javascript/mastodon/locales/whitelist_sa.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_sa.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_sc.json b/app/javascript/mastodon/locales/whitelist_sc.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_sc.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_sco.json b/app/javascript/mastodon/locales/whitelist_sco.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_sco.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_si.json b/app/javascript/mastodon/locales/whitelist_si.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_si.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_sk.json b/app/javascript/mastodon/locales/whitelist_sk.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_sk.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_sl.json b/app/javascript/mastodon/locales/whitelist_sl.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_sl.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_sq.json b/app/javascript/mastodon/locales/whitelist_sq.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_sq.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_sr-Latn.json b/app/javascript/mastodon/locales/whitelist_sr-Latn.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_sr-Latn.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_sr.json b/app/javascript/mastodon/locales/whitelist_sr.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_sr.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_sv.json b/app/javascript/mastodon/locales/whitelist_sv.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_sv.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_szl.json b/app/javascript/mastodon/locales/whitelist_szl.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_szl.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_ta.json b/app/javascript/mastodon/locales/whitelist_ta.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_ta.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_tai.json b/app/javascript/mastodon/locales/whitelist_tai.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_tai.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_te.json b/app/javascript/mastodon/locales/whitelist_te.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_te.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_th.json b/app/javascript/mastodon/locales/whitelist_th.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_th.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_tr.json b/app/javascript/mastodon/locales/whitelist_tr.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_tr.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_tt.json b/app/javascript/mastodon/locales/whitelist_tt.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_tt.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_ug.json b/app/javascript/mastodon/locales/whitelist_ug.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_ug.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_uk.json b/app/javascript/mastodon/locales/whitelist_uk.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_uk.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_ur.json b/app/javascript/mastodon/locales/whitelist_ur.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_ur.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_uz.json b/app/javascript/mastodon/locales/whitelist_uz.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_uz.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_vi.json b/app/javascript/mastodon/locales/whitelist_vi.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_vi.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_zgh.json b/app/javascript/mastodon/locales/whitelist_zgh.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_zgh.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_zh-CN.json b/app/javascript/mastodon/locales/whitelist_zh-CN.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_zh-CN.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_zh-HK.json b/app/javascript/mastodon/locales/whitelist_zh-HK.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_zh-HK.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/whitelist_zh-TW.json b/app/javascript/mastodon/locales/whitelist_zh-TW.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_zh-TW.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/polyfills/base_polyfills.ts b/app/javascript/mastodon/polyfills/base_polyfills.ts index e008d8f02..3cde1b1ed 100644 --- a/app/javascript/mastodon/polyfills/base_polyfills.ts +++ b/app/javascript/mastodon/polyfills/base_polyfills.ts @@ -1,5 +1,3 @@ -import 'intl'; -import 'intl/locale-data/jsonp/en'; import 'core-js/features/object/assign'; import 'core-js/features/object/values'; import 'core-js/features/symbol'; diff --git a/app/javascript/mastodon/polyfills/index.ts b/app/javascript/mastodon/polyfills/index.ts index 6d2e5426e..b2dbfdac0 100644 --- a/app/javascript/mastodon/polyfills/index.ts +++ b/app/javascript/mastodon/polyfills/index.ts @@ -2,6 +2,8 @@ // If there are no polyfills, then this is just Promise.resolve() which means // it will execute in the same tick of the event loop (i.e. near-instant). +import { loadIntlPolyfills } from './intl'; + function importBasePolyfills() { return import(/* webpackChunkName: "base_polyfills" */ './base_polyfills'); } @@ -13,7 +15,6 @@ function importExtraPolyfills() { export function loadPolyfills() { const needsBasePolyfills = !( 'toBlob' in HTMLCanvasElement.prototype && - 'Intl' in window && 'assign' in Object && 'values' in Object && 'Symbol' in window && @@ -32,6 +33,7 @@ export function loadPolyfills() { ); return Promise.all([ + loadIntlPolyfills(), needsBasePolyfills && importBasePolyfills(), needsExtraPolyfills && importExtraPolyfills(), ]); diff --git a/app/javascript/mastodon/polyfills/intl.ts b/app/javascript/mastodon/polyfills/intl.ts new file mode 100644 index 000000000..4d5ee3ccf --- /dev/null +++ b/app/javascript/mastodon/polyfills/intl.ts @@ -0,0 +1,105 @@ +// import { shouldPolyfill as shouldPolyfillCanonicalLocales } from '@formatjs/intl-getcanonicallocales/should-polyfill'; +// import { shouldPolyfill as shouldPolyfillLocale } from '@formatjs/intl-locale/should-polyfill'; +import { shouldPolyfill as shoudPolyfillPluralRules } from '@formatjs/intl-pluralrules/should-polyfill'; +// import { shouldPolyfill as shouldPolyfillNumberFormat } from '@formatjs/intl-numberformat/should-polyfill'; +// import { shouldPolyfill as shouldPolyfillIntlDateTimeFormat } from '@formatjs/intl-datetimeformat/should-polyfill'; +// import { shouldPolyfill as shouldPolyfillIntlRelativeTimeFormat } from '@formatjs/intl-relativetimeformat/should-polyfill'; + +// async function loadGetCanonicalLocalesPolyfill() { +// // This platform already supports Intl.getCanonicalLocales +// if (shouldPolyfillCanonicalLocales()) { +// await import('@formatjs/intl-getcanonicallocales/polyfill'); +// } +// } + +// async function loadLocalePolyfill() { +// // This platform already supports Intl.Locale +// if (shouldPolyfillLocale()) { +// await import('@formatjs/intl-locale/polyfill'); +// } +// } + +// async function loadIntlNumberFormatPolyfill(locale: string) { +// const unsupportedLocale = shouldPolyfillNumberFormat(locale); +// // This locale is supported +// if (!unsupportedLocale) { +// return; +// } +// // Load the polyfill 1st BEFORE loading data +// await import('@formatjs/intl-numberformat/polyfill-force'); +// await import(`@formatjs/intl-numberformat/locale-data/${unsupportedLocale}`); +// } + +// async function loadIntlDateTimeFormatPolyfill(locale: string) { +// const unsupportedLocale = shouldPolyfillIntlDateTimeFormat(locale); +// // This locale is supported +// if (!unsupportedLocale) { +// return; +// } +// // Load the polyfill 1st BEFORE loading data +// await import('@formatjs/intl-datetimeformat/polyfill-force'); + +// // Parallelize CLDR data loading +// const dataPolyfills = [ +// import('@formatjs/intl-datetimeformat/add-all-tz'), +// import(`@formatjs/intl-datetimeformat/locale-data/${unsupportedLocale}`), +// ]; +// await Promise.all(dataPolyfills); +// } + +async function loadIntlPluralRulesPolyfills(locale: string) { + const unsupportedLocale = shoudPolyfillPluralRules(locale); + // This locale is supported + if (!unsupportedLocale) { + return; + } + // Load the polyfill 1st BEFORE loading data + await import( + /* webpackChunkName: "i18n-pluralrules-polyfill" */ '@formatjs/intl-pluralrules/polyfill-force' + ); + await import( + /* webpackChunkName: "i18n-pluralrules-polyfill-[request]" */ `@formatjs/intl-pluralrules/locale-data/${unsupportedLocale}` + ); +} + +// async function loadIntlRelativeTimeFormatPolyfill(locale: string) { +// const unsupportedLocale = shouldPolyfillIntlRelativeTimeFormat(locale); +// // This locale is supported +// if (!unsupportedLocale) { +// return; +// } +// // Load the polyfill 1st BEFORE loading data +// await import( +// /* webpackChunkName: "i18n-relativetimeformat-polyfill" */ +// '@formatjs/intl-relativetimeformat/polyfill-force' +// ); +// await import( +// /* webpackChunkName: "i18n-relativetimeformat-polyfill-[request]" */ +// `@formatjs/intl-relativetimeformat/locale-data/${unsupportedLocale}` +// ); +// } + +export async function loadIntlPolyfills() { + const locale = document.querySelector('html')?.lang || 'en'; + + // order is important here + + // Supported in IE11 and most other browsers, not useful + // await loadGetCanonicalLocalesPolyfill() + + // Supported in IE11 and most other browsers, not useful + // await loadLocalePolyfill() + + // Supported in IE11 and most other browsers, not useful + // await loadIntlNumberFormatPolyfill(locale) + + // Supported in IE11 and most other browsers, not useful + // await loadIntlDateTimeFormatPolyfill(locale) + + // Supported from Safari 13+, may still be useful + await loadIntlPluralRulesPolyfills(locale); + + // This is not used yet in the codebase yet + // Supported from Safari 14+ + // await loadIntlRelativeTimeFormatPolyfill(locale); +} diff --git a/app/javascript/mastodon/service_worker/web_push_locales.js b/app/javascript/mastodon/service_worker/web_push_locales.js index 3912f75c7..89ae20007 100644 --- a/app/javascript/mastodon/service_worker/web_push_locales.js +++ b/app/javascript/mastodon/service_worker/web_push_locales.js @@ -10,7 +10,7 @@ const filtered = {}; const filenames = fs.readdirSync(path.resolve(__dirname, '../locales')); filenames.forEach(filename => { - if (!filename.match(/\.json$/) || filename.match(/defaultMessages|whitelist/)) return; + if (!filename.match(/\.json$/)) return; const content = fs.readFileSync(path.resolve(__dirname, `../locales/${filename}`), 'utf-8'); const full = JSON.parse(content); diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js index 54247e6f6..77187a59e 100644 --- a/app/javascript/mastodon/service_worker/web_push_notifications.js +++ b/app/javascript/mastodon/service_worker/web_push_notifications.js @@ -1,4 +1,4 @@ -import IntlMessageFormat from 'intl-messageformat'; +import { IntlMessageFormat } from 'intl-messageformat'; import { unescape } from 'lodash'; diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 29fd5cde9..01ab8f8f4 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -1,10 +1,11 @@ import './public-path'; import { start } from '../mastodon/common'; +import { loadLocale } from '../mastodon/load_locale'; import { loadPolyfills } from '../mastodon/polyfills'; start(); -loadPolyfills().then(async () => { +loadPolyfills().then(loadLocale).then(async () => { const { default: main } = await import('mastodon/main'); return main(); diff --git a/app/javascript/packs/public.jsx b/app/javascript/packs/public.jsx index 72c7acbb4..22e6b01a1 100644 --- a/app/javascript/packs/public.jsx +++ b/app/javascript/packs/public.jsx @@ -2,7 +2,7 @@ import { createRoot } from 'react-dom/client'; import './public-path'; -import * as IntlMessageFormat from 'intl-messageformat'; +import { IntlMessageFormat } from 'intl-messageformat'; import { defineMessages } from 'react-intl'; import { delegate } from '@rails/ujs'; @@ -15,6 +15,7 @@ import { start } from '../mastodon/common'; import { timeAgoString } from '../mastodon/components/relative_timestamp'; import emojify from '../mastodon/features/emoji/emoji'; import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions'; +import { loadLocale } from '../mastodon/load_locale'; import { getLocale } from '../mastodon/locales'; import { loadPolyfills } from '../mastodon/polyfills'; import ready from '../mastodon/ready'; @@ -46,7 +47,7 @@ window.addEventListener('message', e => { }); function loaded() { - const { localeData } = getLocale(); + const { messages: localeData } = getLocale(); const scrollToDetailedStatus = () => { const history = createBrowserHistory(); @@ -352,6 +353,7 @@ function main() { } loadPolyfills() + .then(loadLocale) .then(main) .then(loadKeyboardExtensions) .catch(error => { diff --git a/app/javascript/packs/share.jsx b/app/javascript/packs/share.jsx index 3bec37d1e..f9fc78561 100644 --- a/app/javascript/packs/share.jsx +++ b/app/javascript/packs/share.jsx @@ -1,9 +1,9 @@ import './public-path'; -import React from 'react'; import { createRoot } from 'react-dom/client'; import { start } from '../mastodon/common'; import ComposeContainer from '../mastodon/containers/compose_container'; +import { loadLocale } from '../mastodon/load_locale'; import { loadPolyfills } from '../mastodon/polyfills'; import ready from '../mastodon/ready'; @@ -26,6 +26,6 @@ function main() { ready(loaded); } -loadPolyfills().then(main).catch(error => { +loadPolyfills().then(loadLocale).then(main).catch(error => { console.error(error); }); diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 3fa5fef09..4fe2f18bf 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -29,7 +29,7 @@ = stylesheet_pack_tag 'common', media: 'all', crossorigin: 'anonymous' = stylesheet_pack_tag current_theme, media: 'all', crossorigin: 'anonymous' = javascript_pack_tag 'common', crossorigin: 'anonymous' - = javascript_pack_tag "locale_#{I18n.locale}", crossorigin: 'anonymous' + = preload_pack_asset "locale/#{I18n.locale}-json.js" = csrf_meta_tags unless skip_csrf_meta_tags? %meta{ name: 'style-nonce', content: request.content_security_policy_nonce } diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml index e74bff9cc..d8aa522d8 100644 --- a/app/views/layouts/embedded.html.haml +++ b/app/views/layouts/embedded.html.haml @@ -14,7 +14,7 @@ = stylesheet_pack_tag 'common', media: 'all', crossorigin: 'anonymous' = stylesheet_pack_tag Setting.default_settings['theme'], media: 'all', crossorigin: 'anonymous' = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' - = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' + = preload_pack_asset "locale/#{I18n.locale}-json.js" = render_initial_state = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' %body.embed diff --git a/babel.config.js b/babel.config.js index 986d60549..0eb877cc3 100644 --- a/babel.config.js +++ b/babel.config.js @@ -22,7 +22,7 @@ module.exports = (api) => { ['@babel/env', envOptions], ], plugins: [ - ['react-intl', { messagesDir: './build/messages' }], + ['formatjs'], 'preval', '@babel/plugin-transform-optional-chaining', '@babel/plugin-transform-nullish-coalescing-operator', diff --git a/config/formatjs-formatter.js b/config/formatjs-formatter.js new file mode 100644 index 000000000..adb5e82ef --- /dev/null +++ b/config/formatjs-formatter.js @@ -0,0 +1,11 @@ +const path = require('path'); + +const currentTranslations = require(path.join(__dirname, "../app/javascript/mastodon/locales/en.json")); + +exports.format = (msgs) => { + const results = {}; + for (const [id, msg] of Object.entries(msgs)) { + results[id] = currentTranslations[id] || msg.defaultMessage; + } + return results; +}; diff --git a/config/webpack/generateLocalePacks.js b/config/webpack/generateLocalePacks.js deleted file mode 100644 index b8d5d82c6..000000000 --- a/config/webpack/generateLocalePacks.js +++ /dev/null @@ -1,51 +0,0 @@ -// To avoid adding a lot of boilerplate, locale packs are -// automatically generated here. These are written into the tmp/ -// directory and then used to generate locale_en.js, locale_fr.js, etc. - -const fs = require('fs'); -const path = require('path'); - -const { mkdirp } = require('mkdirp'); -const rimraf = require('rimraf'); - -const localesJsonPath = path.join(__dirname, '../../app/javascript/mastodon/locales'); -const locales = fs.readdirSync(localesJsonPath).filter(filename => { - return /\.json$/.test(filename) && - !/defaultMessages/.test(filename) && - !/whitelist/.test(filename); -}).map(filename => filename.replace(/\.json$/, '')); - -const outPath = path.join(__dirname, '../../tmp/packs'); - -rimraf.sync(outPath); -mkdirp.sync(outPath); - -const outPaths = []; - -locales.forEach(locale => { - const localePath = path.join(outPath, `locale_${locale}.js`); - const baseLocale = locale.split('-')[0]; // e.g. 'zh-TW' -> 'zh' - const localeDataPath = [ - // first try react-intl - `../../node_modules/react-intl/locale-data/${baseLocale}.js`, - // then check locales/locale-data - `../../app/javascript/mastodon/locales/locale-data/${baseLocale}.js`, - // fall back to English (this is what react-intl does anyway) - '../../node_modules/react-intl/locale-data/en.js', - ].filter(filename => fs.existsSync(path.join(outPath, filename))) - .map(filename => filename.replace(/..\/..\/node_modules\//, ''))[0]; - - const localeContent = `// -// locale_${locale}.js -// automatically generated by generateLocalePacks.js -// -import messages from '../../app/javascript/mastodon/locales/${locale}.json'; -import localeData from ${JSON.stringify(localeDataPath)}; -import { setLocale } from '../../app/javascript/mastodon/locales'; -setLocale({messages, localeData}); -`; - fs.writeFileSync(localePath, localeContent, 'utf8'); - outPaths.push(localePath); -}); - -module.exports = outPaths; diff --git a/config/webpack/shared.js b/config/webpack/shared.js index f2f182c56..bb6ae74c3 100644 --- a/config/webpack/shared.js +++ b/config/webpack/shared.js @@ -9,7 +9,6 @@ const webpack = require('webpack'); const AssetsManifestPlugin = require('webpack-assets-manifest'); const { env, settings, themes, output } = require('./configuration'); -const localePackPaths = require('./generateLocalePacks'); const rules = require('./rules'); const extensionGlob = `**/*{${settings.extensions.join(',')}}*`; @@ -24,11 +23,6 @@ module.exports = { localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry); return localMap; }, {}), - localePackPaths.reduce((map, entry) => { - const localMap = map; - localMap[basename(entry, extname(entry, extname(entry)))] = resolve(entry); - return localMap; - }, {}), Object.keys(themes).reduce((themePaths, name) => { themePaths[name] = resolve(join(settings.source_path, themes[name])); return themePaths; diff --git a/config/webpack/translationRunner.js b/config/webpack/translationRunner.js index 9c684c277..77534c9de 100644 --- a/config/webpack/translationRunner.js +++ b/config/webpack/translationRunner.js @@ -1,101 +1,3 @@ -const fs = require('fs'); -const path = require('path'); +console.error("The localisation functionality has been refactored, please see the Localisation section in the development documentation (https://docs.joinmastodon.org/dev/code/#localizations)"); -// eslint-disable-next-line import/order -const { default: manageTranslations } = require('react-intl-translations-manager'); - -const RFC5646_REGEXP = /^[a-z]{2,3}(?:-(?:x|[A-Za-z]{2,4}))*$/; - -const rootDirectory = path.resolve(__dirname, '..', '..'); -const translationsDirectory = path.resolve(rootDirectory, 'app', 'javascript', 'mastodon', 'locales'); -const messagesDirectory = path.resolve(rootDirectory, 'build', 'messages'); -const availableLanguages = fs.readdirSync(translationsDirectory).reduce((languages, filename) => { - const basename = path.basename(filename, '.json'); - if (RFC5646_REGEXP.test(basename)) { - languages.push(basename); - } - return languages; -}, []); - -const testRFC5646 = language => { - if (!RFC5646_REGEXP.test(language)) { - throw new Error('Not RFC5646 name'); - } -}; - -const testAvailability = language => { - if (!availableLanguages.includes(language)) { - throw new Error('Not an available language'); - } -}; - -const validateLanguages = (languages, validators) => { - const invalidLanguages = languages.reduce((acc, language) => { - try { - validators.forEach(validator => validator(language)); - } catch (error) { - acc.push({ language, error }); - } - return acc; - }, []); - - if (invalidLanguages.length > 0) { - console.error(` -Error: Specified invalid LANGUAGES: -${invalidLanguages.map(({ language, error }) => `* ${language}: ${error.message}`).join('\n')} - -Use yarn "manage:translations -- --help" for usage information -`); - process.exit(1); - } -}; - -const usage = `Usage: yarn manage:translations [OPTIONS] [LANGUAGES] - -Manage JavaScript translation files in Mastodon. Generates and update translations in translationsDirectory: ${translationsDirectory} - -LANGUAGES -The RFC5646 language tag for the language you want to test or fix. If you want to input multiple languages, separate them with space. - -Available languages: -${availableLanguages.join(', ')} -`; - -const { argv } = require('yargs') - .usage(usage) - .option('f', { - alias: 'force', - default: false, - describe: 'force using the provided languages. create files if not exists.', - type: 'boolean', - }); - -// check if message directory exists -if (!fs.existsSync(messagesDirectory)) { - console.error(` -Error: messagesDirectory not exists -(${messagesDirectory}) -Try to run "yarn build:development" first`); - process.exit(1); -} - -// determine the languages list -const languages = (argv._.length > 0) ? argv._ : availableLanguages; - -// validate languages -validateLanguages(languages, [ - testRFC5646, - !argv.force && testAvailability, -].filter(Boolean)); - -// manage translations -manageTranslations({ - messagesDirectory, - translationsDirectory, - detectDuplicateIds: false, - singleMessagesFile: true, - languages, - jsonOptions: { - trailingNewline: true, - }, -}); +process.exit(1); diff --git a/package.json b/package.json index b8265a549..c299bd509 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "build:development": "cross-env RAILS_ENV=development NODE_ENV=development ./bin/webpack", "build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./bin/webpack", "manage:translations": "node ./config/webpack/translationRunner.js", + "i18n:extract": "formatjs extract 'app/javascript/**/*.{js,jsx,ts,tsx}' '--ignore=**/*.d.ts' --out-file app/javascript/mastodon/locales/en.json --format config/formatjs-formatter.js", "start": "node ./streaming/index.js", "test": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:typecheck && ${npm_execpath} run test:jest", "test:lint": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:lint:sass", @@ -34,6 +35,7 @@ "@babel/preset-react": "^7.22.3", "@babel/preset-typescript": "^7.21.5", "@babel/runtime": "^7.22.3", + "@formatjs/intl-pluralrules": "^5.2.2", "@gamestdio/websocket": "^0.3.2", "@github/webauthn-json": "^2.1.1", "@rails/ujs": "^6.1.7", @@ -43,9 +45,9 @@ "autoprefixer": "^10.4.14", "axios": "^1.4.0", "babel-loader": "^8.3.0", + "babel-plugin-formatjs": "^10.5.1", "babel-plugin-lodash": "^3.3.4", "babel-plugin-preval": "^5.1.0", - "babel-plugin-react-intl": "^6.2.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "blurhash": "^2.0.5", "classnames": "^2.3.2", @@ -69,9 +71,7 @@ "http-link-header": "^1.1.1", "immutable": "^4.3.0", "imports-loader": "^1.2.0", - "intl": "^1.2.5", - "intl-messageformat": "^2.2.0", - "intl-relativeformat": "^6.4.3", + "intl-messageformat": "^10.3.5", "js-yaml": "^4.1.0", "jsdom": "^22.1.0", "lodash": "^4.17.21", @@ -93,7 +93,7 @@ "react-hotkeys": "^1.1.4", "react-immutable-proptypes": "^2.2.0", "react-immutable-pure-component": "^2.2.2", - "react-intl": "^2.9.0", + "react-intl": "^6.4.2", "react-motion": "^0.5.2", "react-notification": "^6.8.5", "react-overlays": "^5.2.1", @@ -139,6 +139,7 @@ "ws": "^8.12.1" }, "devDependencies": { + "@formatjs/cli": "^6.1.1", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@types/babel__core": "^7.20.1", @@ -159,7 +160,6 @@ "@types/react-dom": "^18.2.4", "@types/react-helmet": "^6.1.6", "@types/react-immutable-proptypes": "^2.1.0", - "@types/react-intl": "2.3.18", "@types/react-motion": "^0.0.34", "@types/react-overlays": "^3.1.0", "@types/react-router-dom": "^5.3.3", @@ -193,7 +193,6 @@ "jest-environment-jsdom": "^29.5.0", "lint-staged": "^13.2.2", "prettier": "^2.8.8", - "react-intl-translations-manager": "^5.0.3", "react-test-renderer": "^18.2.0", "stylelint": "^15.6.2", "stylelint-config-standard-scss": "^9.0.0", diff --git a/yarn.lock b/yarn.lock index 2611f068b..d89d48854 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24,7 +24,7 @@ jsonpointer "^5.0.0" leven "^3.1.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.21.4": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.21.4": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.21.4.tgz#d0fa9e4413aca81f2b23b9442797bda1826edb39" integrity sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g== @@ -36,7 +36,28 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.3.tgz#cd502a6a0b6e37d7ad72ce7e71a7160a3ae36f7e" integrity sha512-aNtko9OPOwVESUFp3MZfD8Uzxl7JzSeJpd7npIoxCasU37PFbAQRpKglkaKwlHOyeJdrREpo8TW8ldrkYWwvIQ== -"@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.22.1", "@babel/core@^7.7.2": +"@babel/core@^7.10.4", "@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3": + version "7.21.8" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.8.tgz#2a8c7f0f53d60100ba4c32470ba0281c92aa9aa4" + integrity sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.21.4" + "@babel/generator" "^7.21.5" + "@babel/helper-compilation-targets" "^7.21.5" + "@babel/helper-module-transforms" "^7.21.5" + "@babel/helpers" "^7.21.5" + "@babel/parser" "^7.21.8" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.21.5" + "@babel/types" "^7.21.5" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.2" + semver "^6.3.0" + +"@babel/core@^7.22.1": version "7.22.1" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.1.tgz#5de51c5206f4c6f5533562838337a603c1033cfd" integrity sha512-Hkqu7J4ynysSXxmAahpN1jjRwVJ+NdpraFLIWflgjpVob3KNyK3/tIUc7Q7szed8WMp0JNa7Qtd1E9Oo22F9gA== @@ -57,7 +78,7 @@ json5 "^2.2.2" semver "^6.3.0" -"@babel/generator@^7.22.0", "@babel/generator@^7.22.3", "@babel/generator@^7.7.2": +"@babel/generator@^7.21.5", "@babel/generator@^7.22.0", "@babel/generator@^7.22.3", "@babel/generator@^7.7.2": version "7.22.3" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.3.tgz#0ff675d2edb93d7596c5f6728b52615cfc0df01e" integrity sha512-C17MW4wlk//ES/CJDL51kPNwl+qiBQyN7b9SKyVp11BLGFeSPoVaHrv+MNt8jwQFhQWowW88z1eeBx3pFz9v8A== @@ -90,7 +111,7 @@ "@babel/helper-annotate-as-pure" "^7.18.6" "@babel/types" "^7.19.0" -"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.22.1": +"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.21.5", "@babel/helper-compilation-targets@^7.22.1": version "7.22.1" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.1.tgz#bfcd6b7321ffebe33290d68550e2c9d7eb7c7a58" integrity sha512-Rqx13UM3yVB5q0D/KwQ8+SPfX/+Rnsy1Lw1k/UwOC4KC6qrzIQoY3lYnBu5EHKBlEHHcj0M0W8ltPSkD8rqfsQ== @@ -164,7 +185,7 @@ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== -"@babel/helper-environment-visitor@^7.22.1": +"@babel/helper-environment-visitor@^7.21.5", "@babel/helper-environment-visitor@^7.22.1": version "7.22.1" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.1.tgz#ac3a56dbada59ed969d712cf527bd8271fe3eba8" integrity sha512-Z2tgopurB/kTbidvzeBrc2To3PUP/9i5MUe+fU6QJCQDyPwSH2oRapkLw3KGECDYSjhQZCNxEvNvZlLw8JjGwA== @@ -333,7 +354,7 @@ "@babel/traverse" "^7.18.10" "@babel/types" "^7.18.10" -"@babel/helpers@^7.22.0": +"@babel/helpers@^7.21.5", "@babel/helpers@^7.22.0": version "7.22.3" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.3.tgz#53b74351da9684ea2f694bf0877998da26dd830e" integrity sha512-jBJ7jWblbgr7r6wYZHMdIqKc73ycaTcCaWRq4/2LpuPHcx7xMlZvpGQkOYc9HeSjn6rcx15CPlgVcBtZ4WZJ2w== @@ -351,7 +372,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.9", "@babel/parser@^7.22.0", "@babel/parser@^7.22.4": +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.5", "@babel/parser@^7.21.8", "@babel/parser@^7.21.9", "@babel/parser@^7.22.0", "@babel/parser@^7.22.4": version "7.22.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.4.tgz#a770e98fd785c231af9d93f6459d36770993fb32" integrity sha512-VLLsx06XkEYqBtE5YGPwfSGwfrjnyPP5oiGty3S8pQLFDFLaS8VwWSIxkTXpcvr5zeYLE6+MBNl2npl/YnfofA== @@ -460,7 +481,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.12.13", "@babel/plugin-syntax-jsx@^7.21.4", "@babel/plugin-syntax-jsx@^7.7.2": +"@babel/plugin-syntax-jsx@7", "@babel/plugin-syntax-jsx@^7.12.13", "@babel/plugin-syntax-jsx@^7.21.4", "@babel/plugin-syntax-jsx@^7.7.2": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz#f264ed7bf40ffc9ec239edabc17a50c4f5b6fea2" integrity sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ== @@ -1135,7 +1156,16 @@ dependencies: regenerator-runtime "^0.13.11" -"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.21.9", "@babel/template@^7.3.3": +"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" + integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + +"@babel/template@^7.21.9": version "7.21.9" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.21.9.tgz#bf8dad2859130ae46088a99c1f265394877446fb" integrity sha512-MK0X5k8NKOuWRamiEfc3KEJiHMTkGZNUjzMipqCGDDc6ijRl/B7RGSKVGncu4Ro/HdyzzY6cmoXuKI2Gffk7vQ== @@ -1144,7 +1174,23 @@ "@babel/parser" "^7.21.9" "@babel/types" "^7.21.5" -"@babel/traverse@^7.18.10", "@babel/traverse@^7.20.7", "@babel/traverse@^7.22.1", "@babel/traverse@^7.7.2": +"@babel/traverse@7": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.5.tgz#ad22361d352a5154b498299d523cf72998a4b133" + integrity sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw== + dependencies: + "@babel/code-frame" "^7.21.4" + "@babel/generator" "^7.21.5" + "@babel/helper-environment-visitor" "^7.21.5" + "@babel/helper-function-name" "^7.21.0" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.21.5" + "@babel/types" "^7.21.5" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/traverse@^7.18.10", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.5", "@babel/traverse@^7.22.1", "@babel/traverse@^7.7.2": version "7.22.4" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.4.tgz#c3cf96c5c290bd13b55e29d025274057727664c0" integrity sha512-Tn1pDsjIcI+JcLKq1AVlZEr4226gpuAQTsLMorsYg9tuS/kG7nuwwJ4AB8jfQuEgb/COBwR/DqJxmoiYFu5/rQ== @@ -1160,7 +1206,16 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.22.0", "@babel/types@^7.22.3", "@babel/types@^7.22.4", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": +"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.12.11", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.5.tgz#18dfbd47c39d3904d5db3d3dc2cc80bedb60e5b6" + integrity sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q== + dependencies: + "@babel/helper-string-parser" "^7.21.5" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + +"@babel/types@^7.22.0", "@babel/types@^7.22.3", "@babel/types@^7.22.4": version "7.22.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.4.tgz#56a2653ae7e7591365dabf20b76295410684c071" integrity sha512-Tx9x3UBHTTsMSW85WB2kphxYQVvrZ/t1FxD88IpSgIjiUJlCm9z+xWIDwyo1vffTwSqteqyznB8ZE9vYYk16zA== @@ -1335,6 +1390,11 @@ dependencies: "@floating-ui/core" "^1.0.1" +"@formatjs/cli@^6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@formatjs/cli/-/cli-6.1.1.tgz#089d6d25fe96490f8d1401a53705b3cdfefd7afb" + integrity sha512-prUblUQRJwFQqfmBtRWXZFKX+QmhXQkBKRl54hWTCwenskorK6+LTlm9TFbUDhfib2Xt3iDsjk7o9LpeU/AQCw== + "@formatjs/ecma402-abstract@1.15.0": version "1.15.0" resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.15.0.tgz#0a285a5dc69889e15d53803bd5036272e23e5a18" @@ -1343,6 +1403,13 @@ "@formatjs/intl-localematcher" "0.2.32" tslib "^2.4.0" +"@formatjs/fast-memoize@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.0.1.tgz#f15aaa73caad5562899c69bdcad8db82adcd3b0b" + integrity sha512-M2GgV+qJn5WJQAYewz7q2Cdl6fobQa69S1AzSM2y0P68ZDbK5cWrJIcPCO395Of1ksftGZoOt4LYCO/j9BKBSA== + dependencies: + tslib "^2.4.0" + "@formatjs/icu-messageformat-parser@2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.4.0.tgz#e165f3594c68416ce15f63793768251de2a85f88" @@ -1360,6 +1427,24 @@ "@formatjs/ecma402-abstract" "1.15.0" tslib "^2.4.0" +"@formatjs/intl-displaynames@6.3.2": + version "6.3.2" + resolved "https://registry.yarnpkg.com/@formatjs/intl-displaynames/-/intl-displaynames-6.3.2.tgz#be169393a132eed9ca9c10ccb9d22ab150e24c90" + integrity sha512-kBOh0O7QYKLUqaZujLSEF2+au017plPp63R6Hrokl+oDtLyTt9y9pEuCTbOKh/P8CC9THnDLKRKgeVWZw5Ek8A== + dependencies: + "@formatjs/ecma402-abstract" "1.15.0" + "@formatjs/intl-localematcher" "0.2.32" + tslib "^2.4.0" + +"@formatjs/intl-listformat@7.2.2": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@formatjs/intl-listformat/-/intl-listformat-7.2.2.tgz#d787932b5d6f1f936c73c5fec531692ab7069c7a" + integrity sha512-YIruRGwUrmgVOXjWi6VbwPcRNBkEfgK2DFjyyqopCmpfJ+39vnl46oLpVchErnuXs6kkARy5GcGaGV7xRsH4lw== + dependencies: + "@formatjs/ecma402-abstract" "1.15.0" + "@formatjs/intl-localematcher" "0.2.32" + tslib "^2.4.0" + "@formatjs/intl-localematcher@0.2.32": version "0.2.32" resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz#00d4d307cd7d514b298e15a11a369b86c8933ec1" @@ -1367,17 +1452,27 @@ dependencies: tslib "^2.4.0" -"@formatjs/intl-unified-numberformat@^3.3.3": - version "3.3.6" - resolved "https://registry.yarnpkg.com/@formatjs/intl-unified-numberformat/-/intl-unified-numberformat-3.3.6.tgz#ab69818f7568894023cb31fdb5b5c7eed62c6537" - integrity sha512-VQYswh9Pxf4kN6FQvKprAQwSJrF93eJstCDPM1HIt3c3O6NqPFWNWhZ91PLTppOV11rLYsFK11ZxiGbnLNiPTg== +"@formatjs/intl-pluralrules@^5.2.2": + version "5.2.2" + resolved "https://registry.yarnpkg.com/@formatjs/intl-pluralrules/-/intl-pluralrules-5.2.2.tgz#6322d20a6d0172459e4faf4b0f06603c931673aa" + integrity sha512-mEbnbRzsSCIYqaBmrmUlOsPu5MG6KfMcnzekPzUrUucX2dNiI1KWBGHK6IoXl5c8zx60L1NXJ6cSQ7akoc15SQ== dependencies: - "@formatjs/intl-utils" "^2.2.5" + "@formatjs/ecma402-abstract" "1.15.0" + "@formatjs/intl-localematcher" "0.2.32" + tslib "^2.4.0" -"@formatjs/intl-utils@^2.2.5": - version "2.2.5" - resolved "https://registry.yarnpkg.com/@formatjs/intl-utils/-/intl-utils-2.2.5.tgz#eaafd94df3d102ee13e54e80f992a33868a6b1e8" - integrity sha512-p7gcmazKROteL4IECCp03Qrs790fZ8tbemUAjQu0+K0AaAlK49rI1SIFFq3LzDUAqXIshV95JJhRe/yXxkal5g== +"@formatjs/intl@2.7.2": + version "2.7.2" + resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-2.7.2.tgz#83dc77080a984d4883195bed39eedd947ebfd3d7" + integrity sha512-ziiQfnXwY0/rXhtohSAmYMqDjRsihoMKdl8H2aA+FvxG9638E0XrvfBFCb+1HhimNiuqRz5fTY7F/bZtsJxsjA== + dependencies: + "@formatjs/ecma402-abstract" "1.15.0" + "@formatjs/fast-memoize" "2.0.1" + "@formatjs/icu-messageformat-parser" "2.4.0" + "@formatjs/intl-displaynames" "6.3.2" + "@formatjs/intl-listformat" "7.2.2" + intl-messageformat "10.3.5" + tslib "^2.4.0" "@formatjs/ts-transformer@3.13.1": version "3.13.1" @@ -1933,7 +2028,29 @@ resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc" integrity sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q== -"@types/babel__core@^7.1.12", "@types/babel__core@^7.1.14", "@types/babel__core@^7.1.3", "@types/babel__core@^7.20.1": +"@types/babel__core@*", "@types/babel__core@^7.1.7": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891" + integrity sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__core@^7.1.12", "@types/babel__core@^7.1.14": + version "7.1.18" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.18.tgz#1a29abcc411a9c05e2094c98f9a1b7da6cdf49f8" + integrity sha512-S7unDjm/C7z2A2R9NzfKCK1I+BAALDtxEmsJBwlB3EzNfb929ykjL++1CK9LO++EIp2fQrC8O+BwjKvz6UeDyQ== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__core@^7.20.1": version "7.20.1" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.1.tgz#916ecea274b0c776fec721e333e55762d3a9614b" integrity sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw== @@ -1951,6 +2068,13 @@ dependencies: "@babel/types" "^7.0.0" +"@types/babel__helper-plugin-utils@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@types/babel__helper-plugin-utils/-/babel__helper-plugin-utils-7.10.0.tgz#dcd2416f9c189d5837ab2a276368cf67134efe78" + integrity sha512-60YtHzhQ9HAkToHVV+TB4VLzBn9lrfgrsOjiJMtbv/c1jPdekBxaByd6DMsGBzROXWoIL6U3lEFvvbu69RkUoA== + dependencies: + "@types/babel__core" "*" + "@types/babel__template@*": version "7.0.2" resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.2.tgz#4ff63d6b52eddac1de7b975a5223ed32ecea9307" @@ -1966,6 +2090,13 @@ dependencies: "@babel/types" "^7.3.0" +"@types/babel__traverse@^7.1.7": + version "7.18.5" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.5.tgz#c107216842905afafd3b6e774f6f935da6f5db80" + integrity sha512-enCvTL8m/EHS/zIvJno9nE+ndYPh1/oNFzRYRmtUqJICG2VnCSBzMLW5VN2KCQU91f23tsNKR8v7VJJQMatl7Q== + dependencies: + "@babel/types" "^7.3.0" + "@types/body-parser@*": version "1.19.2" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" @@ -2253,11 +2384,6 @@ "@types/prop-types" "*" immutable "^3.8.2" -"@types/react-intl@2.3.18": - version "2.3.18" - resolved "https://registry.yarnpkg.com/@types/react-intl/-/react-intl-2.3.18.tgz#fd2d8b7f4d0a1dd05b5f1784ab0d7fe1786a690d" - integrity sha512-DVNJs49zUxKRZng8VuILE886Yihdsf3yLr5vHk9zJrmF8SyRSK3sxNSvikAKxNkv9hX55XBTJShz6CkJnbNjgg== - "@types/react-motion@^0.0.34": version "0.0.34" resolved "https://registry.yarnpkg.com/@types/react-motion/-/react-motion-0.0.34.tgz#789ff2063e2f7fbb6085d291135c442e8b35291a" @@ -2338,7 +2464,16 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@>=16.9.11", "@types/react@^18.0.26", "@types/react@^18.2.7": +"@types/react@*", "@types/react@16 || 17 || 18", "@types/react@>=16.9.11", "@types/react@^18.0.26": + version "18.2.6" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.6.tgz#5cd53ee0d30ffc193b159d3516c8c8ad2f19d571" + integrity sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/react@^18.2.7": version "18.2.7" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.7.tgz#dfb4518042a3117a045b8c222316f83414a783b3" integrity sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw== @@ -2372,11 +2507,6 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== -"@types/schema-utils@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/schema-utils/-/schema-utils-1.0.0.tgz#295d36f01e2cb8bc3207ca1d9a68e210db6b40cb" - integrity sha512-YesPanU1+WCigC/Aj1Mga8UCOjHIfMNHZ3zzDsUY7lI8GlKnh/Kv2QwJOQ+jNQ36Ru7IfzSedlG14hppYaN13A== - "@types/semver@^7.3.12": version "7.3.13" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" @@ -3214,6 +3344,23 @@ babel-loader@^8.3.0: make-dir "^3.1.0" schema-utils "^2.6.5" +babel-plugin-formatjs@^10.5.1: + version "10.5.1" + resolved "https://registry.yarnpkg.com/babel-plugin-formatjs/-/babel-plugin-formatjs-10.5.1.tgz#9baeccb590538fb1915ef85fb7dfd13aedd8b1fa" + integrity sha512-IkwrKjl2Zg6br2wuayPIsaPF92RzGgh5WdQj+A/9zokpYeIF7sscZGwwHmeTSoPnIAAENvjRMm/escMQkp+eKg== + dependencies: + "@babel/core" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-jsx" "7" + "@babel/traverse" "7" + "@babel/types" "^7.12.11" + "@formatjs/icu-messageformat-parser" "2.4.0" + "@formatjs/ts-transformer" "3.13.1" + "@types/babel__core" "^7.1.7" + "@types/babel__helper-plugin-utils" "^7.10.0" + "@types/babel__traverse" "^7.1.7" + tslib "^2.4.0" + babel-plugin-istanbul@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" @@ -3298,19 +3445,6 @@ babel-plugin-preval@^5.1.0: babel-plugin-macros "^3.0.1" require-from-string "^2.0.2" -babel-plugin-react-intl@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-react-intl/-/babel-plugin-react-intl-6.2.0.tgz#ac51ca757f318938792fc91e1747515e9225386a" - integrity sha512-ajGpa14mLzyDgdOS75DRlQ0aEL+q7iSCB77613YUPOZbxnAvfB0wg+gLngbd/43eKRw7a4y+IzO3P8kDHl40nA== - dependencies: - "@babel/core" "^7.7.2" - "@babel/helper-plugin-utils" "^7.0.0" - "@types/babel__core" "^7.1.3" - "@types/schema-utils" "^1.0.0" - fs-extra "^8.1.0" - intl-messageformat-parser "^4.1.1" - schema-utils "^2.2.0" - babel-plugin-transform-react-remove-prop-types@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" @@ -3751,7 +3885,7 @@ chalk@5.2.0: resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.2.0.tgz#249623b7d66869c673699fb66d65723e54dfcfb3" integrity sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA== -chalk@^2.0.0, chalk@^2.3.2, chalk@^2.4.2: +chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -5741,15 +5875,6 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= -fs-extra@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - fs-extra@^9.0.1: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" @@ -5921,7 +6046,7 @@ glob@^10.2.5, glob@^10.2.6: minipass "^5.0.0 || ^6.0.2" path-scurry "^1.7.0" -glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@^7.0.3, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -6506,48 +6631,17 @@ intersection-observer@^0.12.0: resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.12.2.tgz#4a45349cc0cd91916682b1f44c28d7ec737dc375" integrity sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg== -intl-format-cache@^2.0.5: - version "2.2.9" - resolved "https://registry.yarnpkg.com/intl-format-cache/-/intl-format-cache-2.2.9.tgz#fb560de20c549cda20b569cf1ffb6dc62b5b93b4" - integrity sha512-Zv/u8wRpekckv0cLkwpVdABYST4hZNTDaX7reFetrYTJwxExR2VyTqQm+l0WmL0Qo8Mjb9Tf33qnfj0T7pjxdQ== - -intl-messageformat-parser@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-1.4.0.tgz#b43d45a97468cadbe44331d74bb1e8dea44fc075" - integrity sha1-tD1FqXRoytvkQzHXS7Ho3qRPwHU= - -intl-messageformat-parser@^4.1.1: - version "4.1.4" - resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-4.1.4.tgz#98f3415e6990d44bebf2e0ad8e4cfbacf3ef5ed3" - integrity sha512-zV4kBUD1yhxSyaXm6bGhmP4HFH9Gh4pRQwNn+xq5P+B1dT8mpaAfU75nfUn4HgddIB6pyFnzM5MQjO55UpJwkQ== +intl-messageformat@10.3.5, intl-messageformat@^10.3.5: + version "10.3.5" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.3.5.tgz#f55684fc663e62616ad59d3a504ea0cac3f267b7" + integrity sha512-6kPkftF8Jg3XJCkGKa5OD+nYQ+qcSxF4ZkuDdXZ6KGG0VXn+iblJqRFyDdm9VvKcMyC0Km2+JlVQffFM52D0YA== dependencies: - "@formatjs/intl-unified-numberformat" "^3.3.3" + "@formatjs/ecma402-abstract" "1.15.0" + "@formatjs/fast-memoize" "2.0.1" + "@formatjs/icu-messageformat-parser" "2.4.0" + tslib "^2.4.0" -intl-messageformat@^2.0.0, intl-messageformat@^2.1.0, intl-messageformat@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-2.2.0.tgz#345bcd46de630b7683330c2e52177ff5eab484fc" - integrity sha1-NFvNRt5jC3aDMwwuUhd/9eq0hPw= - dependencies: - intl-messageformat-parser "1.4.0" - -intl-relativeformat@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/intl-relativeformat/-/intl-relativeformat-2.2.0.tgz#6aca95d019ec8d30b6c5653b6629f9983ea5b6c5" - integrity sha512-4bV/7kSKaPEmu6ArxXf9xjv1ny74Zkwuey8Pm01NH4zggPP7JHwg2STk8Y3JdspCKRDriwIyLRfEXnj2ZLr4Bw== - dependencies: - intl-messageformat "^2.0.0" - -intl-relativeformat@^6.4.3: - version "6.4.3" - resolved "https://registry.yarnpkg.com/intl-relativeformat/-/intl-relativeformat-6.4.3.tgz#cb5559e1e257cc2e763583502012a354bb777efe" - integrity sha512-VxZXZfhuX/zBVfxzE/J6kPUpsyWKYjqtZ3jVGZwr6wzK5BOLVpe1vSlwCQX56w5UjlpL63fS8Nxq0kgTyf1gJA== - -intl@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/intl/-/intl-1.2.5.tgz#82244a2190c4e419f8371f5aa34daa3420e2abde" - integrity sha1-giRKIZDE5Bn4Nx9ao02qNCDiq94= - -invariant@^2.1.1, invariant@^2.2.2, invariant@^2.2.4: +invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -7603,13 +7697,6 @@ json5@^2.1.2, json5@^2.2.0, json5@^2.2.2: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= - optionalDependencies: - graceful-fs "^4.1.6" - jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" @@ -9587,26 +9674,21 @@ react-immutable-pure-component@^2.2.2: resolved "https://registry.yarnpkg.com/react-immutable-pure-component/-/react-immutable-pure-component-2.2.2.tgz#3014d3e20cd5a7a4db73b81f1f1464f4d351684b" integrity sha512-vkgoMJUDqHZfXXnjVlG3keCxSO/U6WeDQ5/Sl0GK2cH8TOxEzQ5jXqDXHEL/jqk6fsNxV05oH5kD7VNMUE2k+A== -react-intl-translations-manager@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/react-intl-translations-manager/-/react-intl-translations-manager-5.0.3.tgz#aee010ecf35975673e033ca5d7d3f4147894324d" - integrity sha512-EfBeugnOGFcdUbQyY9TqBMbuauQ8wm73ZqFr0UqCljhbXl7YDHQcVzclWFRkVmlUffzxitLQFhAZEVVeRNQSwA== +react-intl@^6.4.2: + version "6.4.2" + resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-6.4.2.tgz#cf4f49f5f89e66e0975927783d0d270e708314fd" + integrity sha512-q8QyLZfbyqV3Ifa7vtjRrgfSQPGTR6Fi+u9tP/CuzhUPl9DJEPIrvUFhlBryKtRW2qNASqchaP/79Obip+h6oA== dependencies: - chalk "^2.3.2" - glob "^7.1.2" - json-stable-stringify "^1.0.1" - mkdirp "^0.5.1" - -react-intl@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-2.9.0.tgz#c97c5d17d4718f1575fdbd5a769f96018a3b1843" - integrity sha512-27jnDlb/d2A7mSJwrbOBnUgD+rPep+abmoJE511Tf8BnoONIAUehy/U1zZCHGO17mnOwMWxqN4qC0nW11cD6rA== - dependencies: - hoist-non-react-statics "^3.3.0" - intl-format-cache "^2.0.5" - intl-messageformat "^2.1.0" - intl-relativeformat "^2.1.0" - invariant "^2.1.1" + "@formatjs/ecma402-abstract" "1.15.0" + "@formatjs/icu-messageformat-parser" "2.4.0" + "@formatjs/intl" "2.7.2" + "@formatjs/intl-displaynames" "6.3.2" + "@formatjs/intl-listformat" "7.2.2" + "@types/hoist-non-react-statics" "^3.3.1" + "@types/react" "16 || 17 || 18" + hoist-non-react-statics "^3.3.2" + intl-messageformat "10.3.5" + tslib "^2.4.0" "react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.2.0: version "18.2.0" @@ -10318,7 +10400,7 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.2.0, schema-utils@^2.6.5: +schema-utils@^2.6.5: version "2.7.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== @@ -11644,11 +11726,6 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" -universalify@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== - universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" From 69057467cba138d2c9e459f565e88ea5979f61b0 Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Thu, 1 Jun 2023 00:10:21 +0200 Subject: [PATCH 02/92] Translate CW, poll options and media descriptions (#24175) Co-authored-by: Claire --- .../mastodon/actions/importer/normalizer.js | 38 ++- app/javascript/mastodon/actions/statuses.js | 3 +- .../mastodon/components/media_attachments.jsx | 15 +- .../mastodon/components/media_gallery.jsx | 12 +- app/javascript/mastodon/components/poll.jsx | 12 +- app/javascript/mastodon/components/status.jsx | 30 ++- .../mastodon/components/status_content.jsx | 20 +- .../mastodon/containers/status_container.jsx | 2 +- .../status/components/detailed_status.jsx | 14 +- .../mastodon/features/status/index.jsx | 2 +- .../features/ui/components/audio_modal.jsx | 12 +- .../features/ui/components/media_modal.jsx | 7 +- .../features/ui/components/video_modal.jsx | 12 +- app/javascript/mastodon/reducers/polls.js | 29 +++ app/javascript/mastodon/reducers/statuses.js | 26 +- app/lib/emoji_formatter.rb | 11 +- app/lib/translation_service/deepl.rb | 19 +- .../translation_service/libre_translate.rb | 19 +- app/models/translation.rb | 14 ++ .../rest/translation_serializer.rb | 35 ++- app/services/translate_status_service.rb | 83 ++++++- .../statuses/translations_controller_spec.rb | 2 +- spec/lib/translation_service/deepl_spec.rb | 26 +- .../libre_translate_spec.rb | 34 ++- .../services/translate_status_service_spec.rb | 226 ++++++++++++++++++ 25 files changed, 603 insertions(+), 100 deletions(-) create mode 100644 app/models/translation.rb create mode 100644 spec/services/translate_status_service_spec.rb diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 61062fd2c..3232e12a2 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -6,7 +6,7 @@ import { unescapeHTML } from '../../utils/html'; const domParser = new DOMParser(); -const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { +const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => { obj[`:${emoji.shortcode}:`] = emoji; return obj; }, {}); @@ -20,7 +20,7 @@ export function searchTextFromRawStatus (status) { export function normalizeAccount(account) { account = { ...account }; - const emojiMap = makeEmojiMap(account); + const emojiMap = makeEmojiMap(account.emojis); const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name; account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); @@ -86,7 +86,7 @@ export function normalizeStatus(status, normalOldStatus) { const spoilerText = normalStatus.spoiler_text || ''; const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); - const emojiMap = makeEmojiMap(normalStatus); + const emojiMap = makeEmojiMap(normalStatus.emojis); normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); @@ -97,22 +97,48 @@ export function normalizeStatus(status, normalOldStatus) { return normalStatus; } +export function normalizeStatusTranslation(translation, status) { + const emojiMap = makeEmojiMap(status.get('emojis').toJS()); + + const normalTranslation = { + detected_source_language: translation.detected_source_language, + language: translation.language, + provider: translation.provider, + contentHtml: emojify(translation.content, emojiMap), + spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap), + spoiler_text: translation.spoiler_text, + }; + + return normalTranslation; +} + export function normalizePoll(poll) { const normalPoll = { ...poll }; - const emojiMap = makeEmojiMap(normalPoll); + const emojiMap = makeEmojiMap(poll.emojis); normalPoll.options = poll.options.map((option, index) => ({ ...option, voted: poll.own_votes && poll.own_votes.includes(index), - title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap), + titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap), })); return normalPoll; } +export function normalizePollOptionTranslation(translation, poll) { + const emojiMap = makeEmojiMap(poll.get('emojis').toJS()); + + const normalTranslation = { + ...translation, + titleHtml: emojify(escapeTextContentForBrowser(translation.title), emojiMap), + }; + + return normalTranslation; +} + export function normalizeAnnouncement(announcement) { const normalAnnouncement = { ...announcement }; - const emojiMap = makeEmojiMap(normalAnnouncement); + const emojiMap = makeEmojiMap.emojis(normalAnnouncement); normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap); diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 84a1271b8..3aed80735 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -343,7 +343,8 @@ export const translateStatusFail = (id, error) => ({ error, }); -export const undoStatusTranslation = id => ({ +export const undoStatusTranslation = (id, pollId) => ({ type: STATUS_TRANSLATE_UNDO, id, + pollId, }); diff --git a/app/javascript/mastodon/components/media_attachments.jsx b/app/javascript/mastodon/components/media_attachments.jsx index d2f171243..7b945a0ea 100644 --- a/app/javascript/mastodon/components/media_attachments.jsx +++ b/app/javascript/mastodon/components/media_attachments.jsx @@ -51,8 +51,9 @@ export default class MediaAttachments extends ImmutablePureComponent { }; render () { - const { status, lang, width, height } = this.props; + const { status, width, height } = this.props; const mediaAttachments = status.get('media_attachments'); + const language = status.getIn(['language', 'translation']) || status.get('language') || this.props.lang; if (mediaAttachments.size === 0) { return null; @@ -60,14 +61,15 @@ export default class MediaAttachments extends ImmutablePureComponent { if (mediaAttachments.getIn([0, 'type']) === 'audio') { const audio = mediaAttachments.get(0); + const description = audio.getIn(['translation', 'description']) || audio.get('description'); return ( {Component => ( @@ -90,8 +93,8 @@ export default class MediaAttachments extends ImmutablePureComponent { frameRate={video.getIn(['meta', 'original', 'frame_rate'])} blurhash={video.get('blurhash')} src={video.get('url')} - alt={video.get('description')} - lang={lang || status.get('language')} + alt={description} + lang={language} width={width} height={height} inline @@ -107,7 +110,7 @@ export default class MediaAttachments extends ImmutablePureComponent { {Component => ( ALT); } + const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); + if (attachment.get('type') === 'unknown') { return (

- +
{mentionsPlaceholder} -
+
{!hidden && poll} - {!hidden && translateButton} + {translateButton}
); } else if (this.props.onClick) { return ( <>
-
+
{poll} {translateButton} @@ -303,7 +303,7 @@ class StatusContent extends PureComponent { } else { return (
-
+
{poll} {translateButton} diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index 3026dde0a..6167b404f 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -180,7 +180,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ onTranslate (status) { if (status.get('translation')) { - dispatch(undoStatusTranslation(status.get('id'))); + dispatch(undoStatusTranslation(status.get('id'), status.get('poll'))); } else { dispatch(translateStatus(status.get('id'))); } diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx index 187e04ad1..83a566710 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -133,17 +133,20 @@ class DetailedStatus extends ImmutablePureComponent { outerStyle.height = `${this.state.height}px`; } + const language = status.getIn(['translation', 'language']) || status.get('language'); + if (pictureInPicture.get('inUse')) { media = ; } else if (status.get('media_attachments').size > 0) { if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { const attachment = status.getIn(['media_attachments', 0]); + const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); media = (
-
+
- {!minimal && <> {verification} {muteTimeRemaining}} + {!minimal && ( +
+ {verification} {muteTimeRemaining} +
+ )}
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 45e7f7e7b..6c76ddd4d 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -7814,13 +7814,28 @@ noscript { } } +.account__contents { + overflow: hidden; +} + +.account__details { + display: flex; + flex-wrap: wrap; + column-gap: 1em; +} + .verified-badge { display: inline-flex; align-items: center; color: $valid-value-color; gap: 4px; overflow: hidden; - text-overflow: ellipsis; + white-space: nowrap; + + > span { + overflow: hidden; + text-overflow: ellipsis; + } a { color: inherit; From b0780cfeeda641645ea65da257a72ec507e71647 Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Fri, 2 Jun 2023 15:00:27 +0200 Subject: [PATCH 26/92] Fix `/share` and cleanup and reorganize frontend locale loading (#25240) --- .prettierignore | 2 +- app/helpers/react_component_helper.rb | 2 +- app/javascript/mastodon/actions/streaming.js | 9 +- .../mastodon/containers/admin_component.jsx | 11 +- .../mastodon/containers/compose_container.jsx | 16 +- .../mastodon/containers/mastodon.jsx | 14 +- .../mastodon/containers/media_container.jsx | 11 +- app/javascript/mastodon/load_locale.js | 14 -- .../mastodon/locales/global_locale.ts | 22 ++ app/javascript/mastodon/locales/index.js | 22 -- app/javascript/mastodon/locales/index.ts | 5 + .../mastodon/locales/intl_provider.tsx | 56 +++++ .../mastodon/locales/load_locale.ts | 29 +++ .../mastodon/locales/locale-data/README.md | 221 ------------------ .../mastodon/locales/locale-data/co.js | 110 --------- .../mastodon/locales/locale-data/oc.js | 110 --------- .../mastodon/locales/locale-data/sa.js | 98 -------- app/javascript/packs/admin.jsx | 4 +- app/javascript/packs/application.js | 17 +- app/javascript/packs/public.jsx | 3 +- app/javascript/packs/share.jsx | 3 +- jest.config.js | 1 - package.json | 1 + spec/helpers/react_component_helper_spec.rb | 2 +- yarn.lock | 7 + 25 files changed, 152 insertions(+), 638 deletions(-) delete mode 100644 app/javascript/mastodon/load_locale.js create mode 100644 app/javascript/mastodon/locales/global_locale.ts delete mode 100644 app/javascript/mastodon/locales/index.js create mode 100644 app/javascript/mastodon/locales/index.ts create mode 100644 app/javascript/mastodon/locales/intl_provider.tsx create mode 100644 app/javascript/mastodon/locales/load_locale.ts delete mode 100644 app/javascript/mastodon/locales/locale-data/README.md delete mode 100644 app/javascript/mastodon/locales/locale-data/co.js delete mode 100644 app/javascript/mastodon/locales/locale-data/oc.js delete mode 100644 app/javascript/mastodon/locales/locale-data/sa.js diff --git a/.prettierignore b/.prettierignore index 2ea407533..91029f665 100644 --- a/.prettierignore +++ b/.prettierignore @@ -61,7 +61,7 @@ docker-compose.override.yml /app/javascript/mastodon/features/emoji/emoji_map.json # Ignore locale files -/app/javascript/mastodon/locales +/app/javascript/mastodon/locales/*.json /config/locales # Ignore vendored CSS reset diff --git a/app/helpers/react_component_helper.rb b/app/helpers/react_component_helper.rb index fc08de13d..ce616e830 100644 --- a/app/helpers/react_component_helper.rb +++ b/app/helpers/react_component_helper.rb @@ -11,7 +11,7 @@ module ReactComponentHelper end def react_admin_component(name, props = {}) - data = { 'admin-component': name.to_s.camelcase, props: Oj.dump({ locale: I18n.locale }.merge(props)) } + data = { 'admin-component': name.to_s.camelcase, props: Oj.dump(props) } div_tag_with_data(data) end diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 562e72655..9daeb3c60 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -24,8 +24,6 @@ import { fillListTimelineGaps, } from './timelines'; -const { messages } = getLocale(); - /** * @param {number} max * @returns {number} @@ -43,8 +41,10 @@ const randomUpTo = max => * @param {function(object): boolean} [options.accept] * @returns {function(): void} */ -export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => - connectStream(channelName, params, (dispatch, getState) => { +export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => { + const { messages } = getLocale(); + + return connectStream(channelName, params, (dispatch, getState) => { const locale = getState().getIn(['meta', 'locale']); // @ts-expect-error @@ -121,6 +121,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti }, }; }); +}; /** * @param {Function} dispatch diff --git a/app/javascript/mastodon/containers/admin_component.jsx b/app/javascript/mastodon/containers/admin_component.jsx index 562151fe2..740011129 100644 --- a/app/javascript/mastodon/containers/admin_component.jsx +++ b/app/javascript/mastodon/containers/admin_component.jsx @@ -1,24 +1,19 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { IntlProvider } from 'react-intl'; - -import { getLocale, onProviderError } from '../locales'; - -const { messages } = getLocale(); +import { IntlProvider } from 'mastodon/locales'; export default class AdminComponent extends PureComponent { static propTypes = { - locale: PropTypes.string.isRequired, children: PropTypes.node.isRequired, }; render () { - const { locale, children } = this.props; + const { children } = this.props; return ( - + {children} ); diff --git a/app/javascript/mastodon/containers/compose_container.jsx b/app/javascript/mastodon/containers/compose_container.jsx index 751015d18..f76550678 100644 --- a/app/javascript/mastodon/containers/compose_container.jsx +++ b/app/javascript/mastodon/containers/compose_container.jsx @@ -1,18 +1,14 @@ -import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { IntlProvider } from 'react-intl'; - import { Provider } from 'react-redux'; import { fetchCustomEmojis } from '../actions/custom_emojis'; import { hydrateStore } from '../actions/store'; import Compose from '../features/standalone/compose'; import initialState from '../initial_state'; -import { getLocale, onProviderError } from '../locales'; +import { IntlProvider } from '../locales'; import { store } from '../store'; -const { messages } = getLocale(); if (initialState) { store.dispatch(hydrateStore(initialState)); @@ -20,17 +16,11 @@ if (initialState) { store.dispatch(fetchCustomEmojis()); -export default class TimelineContainer extends PureComponent { - - static propTypes = { - locale: PropTypes.string.isRequired, - }; +export default class ComposeContainer extends PureComponent { render () { - const { locale } = this.props; - return ( - + diff --git a/app/javascript/mastodon/containers/mastodon.jsx b/app/javascript/mastodon/containers/mastodon.jsx index c4d4611a2..4538db050 100644 --- a/app/javascript/mastodon/containers/mastodon.jsx +++ b/app/javascript/mastodon/containers/mastodon.jsx @@ -1,8 +1,6 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { IntlProvider } from 'react-intl'; - import { Helmet } from 'react-helmet'; import { BrowserRouter, Route } from 'react-router-dom'; @@ -16,11 +14,9 @@ import { connectUserStream } from 'mastodon/actions/streaming'; import ErrorBoundary from 'mastodon/components/error_boundary'; import UI from 'mastodon/features/ui'; import initialState, { title as siteTitle } from 'mastodon/initial_state'; -import { getLocale, onProviderError } from 'mastodon/locales'; +import { IntlProvider } from 'mastodon/locales'; import { store } from 'mastodon/store'; -const { messages } = getLocale(); - const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`; const hydrateAction = hydrateStore(initialState); @@ -40,10 +36,6 @@ const createIdentityContext = state => ({ export default class Mastodon extends PureComponent { - static propTypes = { - locale: PropTypes.string.isRequired, - }; - static childContextTypes = { identity: PropTypes.shape({ signedIn: PropTypes.bool.isRequired, @@ -79,10 +71,8 @@ export default class Mastodon extends PureComponent { } render () { - const { locale } = this.props; - return ( - + diff --git a/app/javascript/mastodon/containers/media_container.jsx b/app/javascript/mastodon/containers/media_container.jsx index 84eab1cae..fba3c5df7 100644 --- a/app/javascript/mastodon/containers/media_container.jsx +++ b/app/javascript/mastodon/containers/media_container.jsx @@ -2,8 +2,6 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; import { createPortal } from 'react-dom'; -import { IntlProvider } from 'react-intl'; - import { fromJS } from 'immutable'; import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag'; @@ -14,17 +12,14 @@ import Audio from 'mastodon/features/audio'; import Card from 'mastodon/features/status/components/card'; import MediaModal from 'mastodon/features/ui/components/media_modal'; import Video from 'mastodon/features/video'; -import { getLocale, onProviderError } from 'mastodon/locales'; +import { IntlProvider } from 'mastodon/locales'; import { getScrollbarWidth } from 'mastodon/utils/scrollbar'; -const { messages } = getLocale(); - const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio }; export default class MediaContainer extends PureComponent { static propTypes = { - locale: PropTypes.string.isRequired, components: PropTypes.object.isRequired, }; @@ -73,7 +68,7 @@ export default class MediaContainer extends PureComponent { }; render () { - const { locale, components } = this.props; + const { components } = this.props; let handleOpenVideo; @@ -83,7 +78,7 @@ export default class MediaContainer extends PureComponent { } return ( - + <> {[].map.call(components, (component, i) => { const componentName = component.getAttribute('data-component'); diff --git a/app/javascript/mastodon/load_locale.js b/app/javascript/mastodon/load_locale.js deleted file mode 100644 index cb14acd62..000000000 --- a/app/javascript/mastodon/load_locale.js +++ /dev/null @@ -1,14 +0,0 @@ -import { setLocale } from "./locales"; - -export async function loadLocale() { - const locale = document.querySelector('html').lang || 'en'; - - const localeData = await import( - /* webpackMode: "lazy" */ - /* webpackChunkName: "locale/[request]" */ - /* webpackInclude: /\.json$/ */ - /* webpackPreload: true */ - `mastodon/locales/${locale}.json`); - - setLocale({ messages: localeData }); -} diff --git a/app/javascript/mastodon/locales/global_locale.ts b/app/javascript/mastodon/locales/global_locale.ts new file mode 100644 index 000000000..01133ca23 --- /dev/null +++ b/app/javascript/mastodon/locales/global_locale.ts @@ -0,0 +1,22 @@ +export interface LocaleData { + locale: string; + messages: Record; +} + +let loadedLocale: LocaleData; + +export function setLocale(locale: LocaleData) { + loadedLocale = locale; +} + +export function getLocale() { + if (!loadedLocale && process.env.NODE_ENV === 'development') { + throw new Error('getLocale() called before any locale has been set'); + } + + return loadedLocale; +} + +export function isLocaleLoaded() { + return !!loadedLocale; +} diff --git a/app/javascript/mastodon/locales/index.js b/app/javascript/mastodon/locales/index.js deleted file mode 100644 index 6e57e3ddc..000000000 --- a/app/javascript/mastodon/locales/index.js +++ /dev/null @@ -1,22 +0,0 @@ -let theLocale; - -export function setLocale(locale) { - theLocale = locale; -} - -export function getLocale() { - return theLocale; -} - -export function onProviderError(error) { - // Silent the error, like upstream does - if(process.env.NODE_ENV === 'production') return; - - // This browser does not advertise Intl support for this locale, we only print a warning - // As-per the spec, the browser should select the best matching locale - if(typeof error === "object" && error.message.match("MISSING_DATA")) { - console.warn(error.message); - } - - console.error(error); -} diff --git a/app/javascript/mastodon/locales/index.ts b/app/javascript/mastodon/locales/index.ts new file mode 100644 index 000000000..63f45c304 --- /dev/null +++ b/app/javascript/mastodon/locales/index.ts @@ -0,0 +1,5 @@ +export type { LocaleData } from './global_locale'; +export { setLocale, getLocale, isLocaleLoaded } from './global_locale'; +export { loadLocale } from './load_locale'; + +export { IntlProvider } from './intl_provider'; diff --git a/app/javascript/mastodon/locales/intl_provider.tsx b/app/javascript/mastodon/locales/intl_provider.tsx new file mode 100644 index 000000000..1ea77c798 --- /dev/null +++ b/app/javascript/mastodon/locales/intl_provider.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'react'; + +import { IntlProvider as BaseIntlProvider } from 'react-intl'; + +import { getLocale, isLocaleLoaded } from './global_locale'; +import { loadLocale } from './load_locale'; + +function onProviderError(error: unknown) { + // Silent the error, like upstream does + if (process.env.NODE_ENV === 'production') return; + + // This browser does not advertise Intl support for this locale, we only print a warning + // As-per the spec, the browser should select the best matching locale + if ( + error && + typeof error === 'object' && + error instanceof Error && + error.message.match('MISSING_DATA') + ) { + console.warn(error.message); + } + + console.error(error); +} + +export const IntlProvider: React.FC< + Omit, 'locale' | 'messages'> +> = ({ children, ...props }) => { + const [localeLoaded, setLocaleLoaded] = useState(false); + + useEffect(() => { + async function loadLocaleData() { + if (!isLocaleLoaded()) { + await loadLocale(); + } + + setLocaleLoaded(true); + } + void loadLocaleData(); + }, []); + + if (!localeLoaded) return null; + + const { locale, messages } = getLocale(); + + return ( + + {children} + + ); +}; diff --git a/app/javascript/mastodon/locales/load_locale.ts b/app/javascript/mastodon/locales/load_locale.ts new file mode 100644 index 000000000..8a6912317 --- /dev/null +++ b/app/javascript/mastodon/locales/load_locale.ts @@ -0,0 +1,29 @@ +import { Semaphore } from 'async-mutex'; + +import type { LocaleData } from './global_locale'; +import { isLocaleLoaded, setLocale } from './global_locale'; + +const localeLoadingSemaphore = new Semaphore(1); + +export async function loadLocale() { + const locale = document.querySelector('html')?.lang || 'en'; + + // We use a Semaphore here so only one thing can try to load the locales at + // the same time. If one tries to do it while its in progress, it will wait + // for the initial load to finish before it is resumed (and will see that locale + // data is already loaded) + await localeLoadingSemaphore.runExclusive(async () => { + // if the locale is already set, then do nothing + if (isLocaleLoaded()) return; + + const localeData = (await import( + /* webpackMode: "lazy" */ + /* webpackChunkName: "locale/[request]" */ + /* webpackInclude: /\.json$/ */ + /* webpackPreload: true */ + `mastodon/locales/${locale}.json` + )) as LocaleData['messages']; + + setLocale({ messages: localeData, locale }); + }); +} diff --git a/app/javascript/mastodon/locales/locale-data/README.md b/app/javascript/mastodon/locales/locale-data/README.md deleted file mode 100644 index 83368fae7..000000000 --- a/app/javascript/mastodon/locales/locale-data/README.md +++ /dev/null @@ -1,221 +0,0 @@ -# Custom Locale Data - -This folder is used to store custom locale data. These custom locale data are -not yet provided by [Unicode Common Locale Data Repository](http://cldr.unicode.org/development/new-cldr-developers) -and hence not provided in [react-intl/locale-data/*](https://github.com/yahoo/react-intl). - -The locale data should support [Locale Data APIs](https://github.com/yahoo/react-intl/wiki/API#locale-data-apis) -of the react-intl library. - -It is recommended to start your custom locale data from this sample English -locale data ([*](#plural-rules)): - -```javascript -/*eslint eqeqeq: "off"*/ -/*eslint no-nested-ternary: "off"*/ - -export default [ - { - locale: "en", - pluralRuleFunction: function(e, a) { - var n = String(e).split("."), - l = !n[1], - o = Number(n[0]) == e, - t = o && n[0].slice(-1), - r = o && n[0].slice(-2); - return a ? 1 == t && 11 != r ? "one" : 2 == t && 12 != r ? "two" : 3 == t && 13 != r ? "few" : "other" : 1 == e && l ? "one" : "other" - }, - fields: { - year: { - displayName: "year", - relative: { - 0: "this year", - 1: "next year", - "-1": "last year" - }, - relativeTime: { - future: { - one: "in {0} year", - other: "in {0} years" - }, - past: { - one: "{0} year ago", - other: "{0} years ago" - } - } - }, - month: { - displayName: "month", - relative: { - 0: "this month", - 1: "next month", - "-1": "last month" - }, - relativeTime: { - future: { - one: "in {0} month", - other: "in {0} months" - }, - past: { - one: "{0} month ago", - other: "{0} months ago" - } - } - }, - day: { - displayName: "day", - relative: { - 0: "today", - 1: "tomorrow", - "-1": "yesterday" - }, - relativeTime: { - future: { - one: "in {0} day", - other: "in {0} days" - }, - past: { - one: "{0} day ago", - other: "{0} days ago" - } - } - }, - hour: { - displayName: "hour", - relativeTime: { - future: { - one: "in {0} hour", - other: "in {0} hours" - }, - past: { - one: "{0} hour ago", - other: "{0} hours ago" - } - } - }, - minute: { - displayName: "minute", - relativeTime: { - future: { - one: "in {0} minute", - other: "in {0} minutes" - }, - past: { - one: "{0} minute ago", - other: "{0} minutes ago" - } - } - }, - second: { - displayName: "second", - relative: { - 0: "now" - }, - relativeTime: { - future: { - one: "in {0} second", - other: "in {0} seconds" - }, - past: { - one: "{0} second ago", - other: "{0} seconds ago" - } - } - } - } - } -] - -``` - -## Notes - -### Plural Rules - -The function `pluralRuleFunction()` should return the key to proper string of -a plural form(s). The purpose of the function is to provide key of translate -strings of correct plural form according. The different forms are described in -[CLDR's Plural Rules][cldr-plural-rules], - -[cldr-plural-rules]: http://cldr.unicode.org/index/cldr-spec/plural-rules - -#### Quick Overview on CLDR Rules - -Let's take English as an example. - -When you describe a number, you can be either describe it as: -* Cardinals: 1st, 2nd, 3rd ... 11th, 12th ... 21st, 22nd, 23nd .... -* Ordinals: 1, 2, 3 ... - -In any of these cases, the nouns will reflect the number with singular or plural -form. For example: -* in 0 days -* in 1 day -* in 2 days - -The `pluralRuleFunction` receives 2 parameters: -* `e`: a string representation of the number. Such as, "`1`", "`2`", "`2.1`". -* `a`: `true` if this is "cardinal" type of description. `false` for ordinal and other case. - -#### How you should write `pluralRuleFunction` - -The first rule to write pluralRuleFunction is never translate the output string -into your language. [Plural Rules][cldr-plural-rules] specified you should use -these as the return values: - - * "`zero`" - * "`one`" (singular) - * "`two`" (dual) - * "`few`" (paucal) - * "`many`" (also used for fractions if they have a separate class) - * "`other`" (required—general plural form—also used if the language only has a single form) - -Again, we'll use English as the example here. - -Let's read the `return` statement in the pluralRuleFunction above: -```javascript - return a ? 1 == t && 11 != r ? "one" : 2 == t && 12 != r ? "two" : 3 == t && 13 != r ? "few" : "other" : 1 == e && l ? "one" : "other" -``` - -This nested ternary is hard to read. It basically means: -```javascript -// e: the number variable to examine -// a: "true" if cardinals -// l: "true" if the variable e has nothin after decimal mark (e.g. "1.0" would be false) -// o: "true" if the variable e is an integer -// t: the "ones" of the number. e.g. "3" for number "9123" -// r: the "ones" and "tens" of the number. e.g. "23" for number "9123" -if (a == true) { - if (t == 1 && r != 11) { - return "one"; // i.e. 1st, 21st, 101st, 121st ... - } else if (t == 2 && r != 12) { - return "two"; // i.e. 2nd, 22nd, 102nd, 122nd ... - } else if (t == 3 && r != 13) { - return "few"; // i.e. 3rd, 23rd, 103rd, 123rd ... - } else { - return "other"; // i.e. 4th, 11th, 12th, 24th ... - } -} else { - if (e == 1 && l) { - return "one"; // i.e. 1 day - } else { - return "other"; // i.e. 0 days, 2 days, 3 days - } -} -``` - -If your language, like French, do not have complicated cardinal rules, you may -use the French's version of it: -```javascript -function (e, a) { - return a ? 1 == e ? "one" : "other" : e >= 0 && e < 2 ? "one" : "other"; -} -``` - -If your language, like Chinese, do not have any pluralization rule at all you -may use the Chinese's version of it: -```javascript -function (e, a) { - return "other"; -} -``` diff --git a/app/javascript/mastodon/locales/locale-data/co.js b/app/javascript/mastodon/locales/locale-data/co.js deleted file mode 100644 index dff8a54da..000000000 --- a/app/javascript/mastodon/locales/locale-data/co.js +++ /dev/null @@ -1,110 +0,0 @@ -/*eslint eqeqeq: "off"*/ -/*eslint no-nested-ternary: "off"*/ -/*eslint quotes: "off"*/ - -const rules = [{ - locale: "co", - pluralRuleFunction: function (e, a) { - return a ? 1 == e ? "one" : "other" : e >= 0 && e < 2 ? "one" : "other"; - }, - fields: { - year: { - displayName: "annu", - relative: { - 0: "quist'annu", - 1: "l'annu chì vene", - "-1": "l'annu passatu", - }, - relativeTime: { - future: { - one: "in {0} annu", - other: "in {0} anni", - }, - past: { - one: "{0} annu fà", - other: "{0} anni fà", - }, - }, - }, - month: { - displayName: "mese", - relative: { - 0: "Questu mese", - 1: "u mese chì vene", - "-1": "u mese passatu", - }, - relativeTime: { - future: { - one: "in {0} mese", - other: "in {0} mesi", - }, - past: { - one: "{0} mese fà", - other: "{0} mesi fà", - }, - }, - }, - day: { - displayName: "ghjornu", - relative: { - 0: "oghje", - 1: "dumane", - "-1": "eri", - }, - relativeTime: { - future: { - one: "in {0} ghjornu", - other: "in {0} ghjornu", - }, - past: { - one: "{0} ghjornu fà", - other: "{0} ghjorni fà", - }, - }, - }, - hour: { - displayName: "ora", - relativeTime: { - future: { - one: "in {0} ora", - other: "in {0} ore", - }, - past: { - one: "{0} ora fà", - other: "{0} ore fà", - }, - }, - }, - minute: { - displayName: "minuta", - relativeTime: { - future: { - one: "in {0} minuta", - other: "in {0} minute", - }, - past: { - one: "{0} minuta fà", - other: "{0} minute fà", - }, - }, - }, - second: { - displayName: "siconda", - relative: { - 0: "avà", - }, - relativeTime: { - future: { - one: "in {0} siconda", - other: "in {0} siconde", - }, - past: { - one: "{0} siconda fà", - other: "{0} siconde fà", - }, - }, - }, - }, -}]; - -export default rules; diff --git a/app/javascript/mastodon/locales/locale-data/oc.js b/app/javascript/mastodon/locales/locale-data/oc.js deleted file mode 100644 index 6ab306b8c..000000000 --- a/app/javascript/mastodon/locales/locale-data/oc.js +++ /dev/null @@ -1,110 +0,0 @@ -/*eslint eqeqeq: "off"*/ -/*eslint no-nested-ternary: "off"*/ -/*eslint quotes: "off"*/ - -const rules = [{ - locale: "oc", - pluralRuleFunction: function (e, a) { - return a ? 1 == e ? "one" : "other" : e >= 0 && e < 2 ? "one" : "other"; - }, - fields: { - year: { - displayName: "an", - relative: { - 0: "ongan", - 1: "l'an que ven", - "-1": "l'an passat", - }, - relativeTime: { - future: { - one: "d’aquí {0} an", - other: "d’aquí {0} ans", - }, - past: { - one: "fa {0} an", - other: "fa {0} ans", - }, - }, - }, - month: { - displayName: "mes", - relative: { - 0: "aqueste mes", - 1: "lo mes que ven", - "-1": "lo mes passat", - }, - relativeTime: { - future: { - one: "d’aquí {0} mes", - other: "d’aquí {0} meses", - }, - past: { - one: "fa {0} mes", - other: "fa {0} meses", - }, - }, - }, - day: { - displayName: "jorn", - relative: { - 0: "uèi", - 1: "deman", - "-1": "ièr", - }, - relativeTime: { - future: { - one: "d’aquí {0} jorn", - other: "d’aquí {0} jorns", - }, - past: { - one: "fa {0} jorn", - other: "fa {0} jorns", - }, - }, - }, - hour: { - displayName: "ora", - relativeTime: { - future: { - one: "d’aquí {0} ora", - other: "d’aquí {0} oras", - }, - past: { - one: "fa {0} ora", - other: "fa {0} oras", - }, - }, - }, - minute: { - displayName: "minuta", - relativeTime: { - future: { - one: "d’aquí {0} minuta", - other: "d’aquí {0} minutas", - }, - past: { - one: "fa {0} minuta", - other: "fa {0} minutas", - }, - }, - }, - second: { - displayName: "segonda", - relative: { - 0: "ara", - }, - relativeTime: { - future: { - one: "d’aquí {0} segonda", - other: "d’aquí {0} segondas", - }, - past: { - one: "fa {0} segonda", - other: "fa {0} segondas", - }, - }, - }, - }, -}]; - -export default rules; diff --git a/app/javascript/mastodon/locales/locale-data/sa.js b/app/javascript/mastodon/locales/locale-data/sa.js deleted file mode 100644 index 65e09e97f..000000000 --- a/app/javascript/mastodon/locales/locale-data/sa.js +++ /dev/null @@ -1,98 +0,0 @@ -/*eslint eqeqeq: "off"*/ -/*eslint no-nested-ternary: "off"*/ -/*eslint quotes: "off"*/ -/*eslint comma-dangle: "off"*/ - -const rules = [ - { - locale: "sa", - fields: { - year: { - displayName: "year", - relative: { - 0: "this year", - 1: "next year", - "-1": "last year" - }, - relativeTime: { - future: { - other: "+{0} y" - }, - past: { - other: "-{0} y" - } - } - }, - month: { - displayName: "month", - relative: { - 0: "this month", - 1: "next month", - "-1": "last month" - }, - relativeTime: { - future: { - other: "+{0} m" - }, - past: { - other: "-{0} m" - } - } - }, - day: { - displayName: "day", - relative: { - 0: "अद्य", - 1: "श्वः", - "-1": "गतदिनम्" - }, - relativeTime: { - future: { - other: "+{0} d" - }, - past: { - other: "-{0} d" - } - } - }, - hour: { - displayName: "hour", - relativeTime: { - future: { - other: "+{0} h" - }, - past: { - other: "-{0} h" - } - } - }, - minute: { - displayName: "minute", - relativeTime: { - future: { - other: "+{0} min" - }, - past: { - other: "-{0} min" - } - } - }, - second: { - displayName: "second", - relative: { - 0: "now" - }, - relativeTime: { - future: { - other: "+{0} s" - }, - past: { - other: "-{0} s" - } - } - } - } - } -]; - -export default rules; diff --git a/app/javascript/packs/admin.jsx b/app/javascript/packs/admin.jsx index 9bb4d4dbf..ebcc6903f 100644 --- a/app/javascript/packs/admin.jsx +++ b/app/javascript/packs/admin.jsx @@ -229,14 +229,14 @@ ready(() => { [].forEach.call(document.querySelectorAll('[data-admin-component]'), element => { const componentName = element.getAttribute('data-admin-component'); - const { locale, ...componentProps } = JSON.parse(element.getAttribute('data-props')); + const componentProps = JSON.parse(element.getAttribute('data-props')); import('../mastodon/containers/admin_component').then(({ default: AdminComponent }) => { return import('../mastodon/components/admin/' + componentName).then(({ default: Component }) => { const root = createRoot(element); root.render ( - + , ); diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 01ab8f8f4..f26321c41 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -1,14 +1,15 @@ import './public-path'; +import main from "mastodon/main" + import { start } from '../mastodon/common'; -import { loadLocale } from '../mastodon/load_locale'; +import { loadLocale } from '../mastodon/locales'; import { loadPolyfills } from '../mastodon/polyfills'; start(); -loadPolyfills().then(loadLocale).then(async () => { - const { default: main } = await import('mastodon/main'); - - return main(); -}).catch(e => { - console.error(e); -}); +loadPolyfills() + .then(loadLocale) + .then(main) + .catch(e => { + console.error(e); + }); diff --git a/app/javascript/packs/public.jsx b/app/javascript/packs/public.jsx index 22e6b01a1..da43bba7d 100644 --- a/app/javascript/packs/public.jsx +++ b/app/javascript/packs/public.jsx @@ -15,8 +15,7 @@ import { start } from '../mastodon/common'; import { timeAgoString } from '../mastodon/components/relative_timestamp'; import emojify from '../mastodon/features/emoji/emoji'; import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions'; -import { loadLocale } from '../mastodon/load_locale'; -import { getLocale } from '../mastodon/locales'; +import { loadLocale, getLocale } from '../mastodon/locales'; import { loadPolyfills } from '../mastodon/polyfills'; import ready from '../mastodon/ready'; diff --git a/app/javascript/packs/share.jsx b/app/javascript/packs/share.jsx index f9fc78561..0f3b84549 100644 --- a/app/javascript/packs/share.jsx +++ b/app/javascript/packs/share.jsx @@ -3,7 +3,6 @@ import { createRoot } from 'react-dom/client'; import { start } from '../mastodon/common'; import ComposeContainer from '../mastodon/containers/compose_container'; -import { loadLocale } from '../mastodon/load_locale'; import { loadPolyfills } from '../mastodon/polyfills'; import ready from '../mastodon/ready'; @@ -26,6 +25,6 @@ function main() { ready(loaded); } -loadPolyfills().then(loadLocale).then(main).catch(error => { +loadPolyfills().then(main).catch(error => { console.error(error); }); diff --git a/jest.config.js b/jest.config.js index 42c2b4152..f611812ef 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,7 +13,6 @@ const config = { collectCoverageFrom: [ 'app/javascript/mastodon/**/*.{js,jsx,ts,tsx}', '!app/javascript/mastodon/features/emoji/emoji_compressed.js', - '!app/javascript/mastodon/locales/locale-data/*.js', '!app/javascript/mastodon/service_worker/entry.js', '!app/javascript/mastodon/test_setup.js', ], diff --git a/package.json b/package.json index 31f2454fe..a08b485fd 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@reduxjs/toolkit": "^1.9.5", "abortcontroller-polyfill": "^1.7.5", "arrow-key-navigation": "^1.2.0", + "async-mutex": "^0.4.0", "autoprefixer": "^10.4.14", "axios": "^1.4.0", "babel-loader": "^8.3.0", diff --git a/spec/helpers/react_component_helper_spec.rb b/spec/helpers/react_component_helper_spec.rb index 3f133bff9..28208b619 100644 --- a/spec/helpers/react_component_helper_spec.rb +++ b/spec/helpers/react_component_helper_spec.rb @@ -33,7 +33,7 @@ describe ReactComponentHelper do it 'returns a tag with data attributes' do expect(parsed_html.div['data-admin-component']).to eq('Name') - expect(parsed_html.div['data-props']).to eq('{"locale":"en","one":"two"}') + expect(parsed_html.div['data-props']).to eq('{"one":"two"}') end end diff --git a/yarn.lock b/yarn.lock index 3ae634363..fd751c041 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3273,6 +3273,13 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== +async-mutex@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.4.0.tgz#ae8048cd4d04ace94347507504b3cf15e631c25f" + integrity sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA== + dependencies: + tslib "^2.4.0" + async@^2.6.2: version "2.6.4" resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" From acc419b81be647336059ed8048dd88c1c1c1e95a Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 2 Jun 2023 09:40:23 -0400 Subject: [PATCH 27/92] Fix spacing of middle dots in the detailed status meta section (#25247) --- .../mastodon/features/status/components/detailed_status.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx index 83a566710..ddda6eaac 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -217,7 +217,7 @@ class DetailedStatus extends ImmutablePureComponent { } else if (this.context.router) { reblogLink = ( <> - · + {' · '} @@ -229,7 +229,7 @@ class DetailedStatus extends ImmutablePureComponent { } else { reblogLink = ( <> - · + {' · '} @@ -263,7 +263,7 @@ class DetailedStatus extends ImmutablePureComponent { if (status.get('edited_at')) { edited = ( <> - · + {' · '} ); From 94329f28e1d6edace2667daeaf0097f895e4940c Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 2 Jun 2023 18:09:08 +0200 Subject: [PATCH 28/92] =?UTF-8?q?Change=20wording=20of=20=E2=80=9CContent?= =?UTF-8?q?=20cache=20retention=20period=E2=80=9D=20setting=20to=20highlig?= =?UTF-8?q?ht=20destructive=20implications=20(#23261)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/settings/content_retention/show.html.haml | 2 +- config/initializers/simple_form.rb | 10 ++++++++++ config/locales/simple_form.en.yml | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/views/admin/settings/content_retention/show.html.haml b/app/views/admin/settings/content_retention/show.html.haml index 36856127f..de34b5ee3 100644 --- a/app/views/admin/settings/content_retention/show.html.haml +++ b/app/views/admin/settings/content_retention/show.html.haml @@ -15,7 +15,7 @@ .fields-group = f.input :media_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' } - = f.input :content_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' } + = f.input :content_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }, hint: false, warning_hint: t('simple_form.hints.form_admin_settings.content_cache_retention_period') = f.input :backups_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' } .actions diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb index 92cffc5a2..74034f36f 100644 --- a/config/initializers/simple_form.rb +++ b/config/initializers/simple_form.rb @@ -19,8 +19,17 @@ module RecommendedComponent end end +module WarningHintComponent + def warning_hint(_wrapper_options = nil) + @warning_hint ||= begin + options[:warning_hint].to_s.html_safe if options[:warning_hint].present? + end + end +end + SimpleForm.include_component(AppendComponent) SimpleForm.include_component(RecommendedComponent) +SimpleForm.include_component(WarningHintComponent) SimpleForm.setup do |config| # Wrappers are used by the form builder to generate a @@ -101,6 +110,7 @@ SimpleForm.setup do |config| b.use :html5 b.use :label b.use :hint, wrap_with: { tag: :span, class: :hint } + b.use :warning_hint, wrap_with: { tag: :span, class: [:hint, 'warning-hint'] } b.use :input, wrap_with: { tag: :div, class: :label_input } b.use :error, wrap_with: { tag: :span, class: :error } end diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index b646a15e2..9c747e595 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -78,7 +78,7 @@ en: backups_retention_period: Keep generated user archives for the specified number of days. bootstrap_timeline_accounts: These accounts will be pinned to the top of new users' follow recommendations. closed_registrations_message: Displayed when sign-ups are closed - content_cache_retention_period: Posts from other servers will be deleted after the specified number of days when set to a positive value. This may be irreversible. + content_cache_retention_period: All posts and boosts from other servers will be deleted after the specified number of days. Some posts may not be recoverable. All related bookmarks, favourites and boosts will also be lost and impossible to undo. custom_css: You can apply custom styles on the web version of Mastodon. mascot: Overrides the illustration in the advanced web interface. media_cache_retention_period: Downloaded media files will be deleted after the specified number of days when set to a positive value, and re-downloaded on demand. From 0766c9a631e45ff66603ff10fa69808b8452d0b3 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 2 Jun 2023 18:35:37 +0200 Subject: [PATCH 29/92] Add card with who invited you to join when displaying rules on sign-up (#23475) --- app/javascript/styles/mastodon/accounts.scss | 14 ++------------ app/javascript/styles/mastodon/forms.scss | 4 ++++ app/views/application/_card.html.haml | 6 ++++-- app/views/auth/registrations/rules.html.haml | 10 ++++++++-- config/locales/en.yml | 3 +++ 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index 8b7b63407..b50306ded 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -3,11 +3,8 @@ display: block; text-decoration: none; color: inherit; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - - @media screen and (max-width: $no-gap-breakpoint) { - box-shadow: none; - } + overflow: hidden; + border-radius: 4px; &:hover, &:active, @@ -22,7 +19,6 @@ height: 130px; position: relative; background: darken($ui-base-color, 12%); - border-radius: 4px 4px 0 0; img { display: block; @@ -30,7 +26,6 @@ height: 100%; margin: 0; object-fit: cover; - border-radius: 4px 4px 0 0; } @media screen and (width <= 600px) { @@ -45,11 +40,6 @@ justify-content: flex-start; align-items: center; background: lighten($ui-base-color, 4%); - border-radius: 0 0 4px 4px; - - @media screen and (max-width: $no-gap-breakpoint) { - border-radius: 0; - } .avatar { flex: 0 0 auto; diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 57f077c4e..d63a42557 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -137,6 +137,10 @@ code { color: $secondary-text-color; margin-bottom: 30px; + &.invited-by { + margin-bottom: 15px; + } + a { color: $highlight-text-color; } diff --git a/app/views/application/_card.html.haml b/app/views/application/_card.html.haml index 719856d49..1b3dd889c 100644 --- a/app/views/application/_card.html.haml +++ b/app/views/application/_card.html.haml @@ -1,9 +1,11 @@ - account_url = local_assigns[:admin] ? admin_account_path(account.id) : ActivityPub::TagManager.instance.url_for(account) +- compact ||= false .card.h-card = link_to account_url, target: '_blank', rel: 'noopener noreferrer' do - .card__img - = image_tag account.header.url, alt: '' + - unless compact + .card__img + = image_tag account.header.url, alt: '' .card__bar .avatar = image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo' diff --git a/app/views/auth/registrations/rules.html.haml b/app/views/auth/registrations/rules.html.haml index ab3fa864a..234f4a601 100644 --- a/app/views/auth/registrations/rules.html.haml +++ b/app/views/auth/registrations/rules.html.haml @@ -7,8 +7,14 @@ .simple_form = render 'auth/shared/progress', stage: 'rules' - %h1.title= t('auth.rules.title') - %p.lead= t('auth.rules.preamble', domain: site_hostname) + - if @invite.present? && @invite.autofollow? + %h1.title= t('auth.rules.title_invited') + %p.lead.invited-by= t('auth.rules.invited_by', domain: site_hostname) + = render 'application/card', account: @invite.user.account, compact: true + %p.lead= t('auth.rules.preamble_invited', domain: site_hostname) + - else + %h1.title= t('auth.rules.title') + %p.lead= t('auth.rules.preamble', domain: site_hostname) %ol.rules-list - @rules.each do |rule| diff --git a/config/locales/en.yml b/config/locales/en.yml index 6a8da6e60..2c292c42d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1031,8 +1031,11 @@ en: rules: accept: Accept back: Back + invited_by: 'You can join %{domain} thanks to the invitation you have received from:' preamble: These are set and enforced by the %{domain} moderators. + preamble_invited: Before you proceed, please consider the ground rules set by the moderators of %{domain}. title: Some ground rules. + title_invited: You've been invited. security: Security set_new_password: Set new password setup: From 768b00c4d0c05c35c2c6c9bc8b4a821f1bde119d Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 2 Jun 2023 13:58:18 -0400 Subject: [PATCH 30/92] =?UTF-8?q?Consistently=20use=20middle=20dot=20(?= =?UTF-8?q?=C2=B7)=20instead=20of=20bullet=20(=E2=80=A2)=20to=20separate?= =?UTF-8?q?=20items=20(#25248)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 9 ++++++ .haml-lint.yml | 5 +++ .rubocop.yml | 4 +++ .../_email_domain_block.html.haml | 2 +- .../_domain_block.html.haml | 6 ++-- app/views/admin/instances/_instance.html.haml | 2 +- app/views/admin/instances/show.html.haml | 2 +- app/views/admin/ip_blocks/_ip_block.html.haml | 2 +- app/views/admin/roles/_role.html.haml | 2 +- .../trends/links/_preview_card.html.haml | 10 +++--- .../admin/trends/statuses/_status.html.haml | 10 +++--- app/views/admin/trends/tags/_tag.html.haml | 6 ++-- app/views/admin/webhooks/_webhook.html.haml | 2 +- .../admin_mailer/_new_trending_links.text.erb | 4 +-- .../_new_trending_statuses.text.erb | 2 +- .../admin_mailer/_new_trending_tags.text.erb | 2 +- .../authorized_applications/index.html.haml | 2 +- lib/linter/haml_middle_dot.rb | 26 ++++++++++++++++ lib/linter/rubocop_middle_dot.rb | 31 +++++++++++++++++++ 19 files changed, 102 insertions(+), 27 deletions(-) create mode 100644 lib/linter/haml_middle_dot.rb create mode 100644 lib/linter/rubocop_middle_dot.rb diff --git a/.eslintrc.js b/.eslintrc.js index 24961cdd9..91dcd8e60 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -81,6 +81,15 @@ module.exports = { { property: 'substring', message: 'Use .slice instead of .substring.' }, { property: 'substr', message: 'Use .slice instead of .substr.' }, ], + 'no-restricted-syntax': [ + 'error', + { + // eslint-disable-next-line no-restricted-syntax + selector: 'Literal[value=/•/], JSXText[value=/•/]', + // eslint-disable-next-line no-restricted-syntax + message: "Use '·' (middle dot) instead of '•' (bullet)", + }, + ], 'no-self-assign': 'off', 'no-unused-expressions': 'error', 'no-unused-vars': 'off', diff --git a/.haml-lint.yml b/.haml-lint.yml index 12ca46342..d1ed30b26 100644 --- a/.haml-lint.yml +++ b/.haml-lint.yml @@ -4,6 +4,11 @@ exclude: - 'vendor/**/*' - lib/templates/haml/scaffold/_form.html.haml +require: + - ./lib/linter/haml_middle_dot.rb + linters: AltText: enabled: true + MiddleDot: + enabled: true diff --git a/.rubocop.yml b/.rubocop.yml index bd561df1d..eff89bdae 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -11,6 +11,7 @@ require: - rubocop-rspec - rubocop-performance - rubocop-capybara + - ./lib/linter/rubocop_middle_dot AllCops: TargetRubyVersion: 3.0 # Set to minimum supported version of CI @@ -205,3 +206,6 @@ Style/TrailingCommaInArrayLiteral: # https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainhashliteral Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: 'comma' + +Style/MiddleDot: + Enabled: true diff --git a/app/views/admin/email_domain_blocks/_email_domain_block.html.haml b/app/views/admin/email_domain_blocks/_email_domain_block.html.haml index c5a55bc27..7cb973c4b 100644 --- a/app/views/admin/email_domain_blocks/_email_domain_block.html.haml +++ b/app/views/admin/email_domain_blocks/_email_domain_block.html.haml @@ -9,6 +9,6 @@ - if email_domain_block.parent.present? = t('admin.email_domain_blocks.resolved_through_html', domain: content_tag(:samp, email_domain_block.parent.domain)) - • + · = t('admin.email_domain_blocks.attempts_over_week', count: email_domain_block.history.reduce(0) { |sum, day| sum + day.accounts }) diff --git a/app/views/admin/export_domain_blocks/_domain_block.html.haml b/app/views/admin/export_domain_blocks/_domain_block.html.haml index 5d4b6c4d0..cdce4fd28 100644 --- a/app/views/admin/export_domain_blocks/_domain_block.html.haml +++ b/app/views/admin/export_domain_blocks/_domain_block.html.haml @@ -17,11 +17,11 @@ %br/ - = f.object.policies.map { |policy| t(policy, scope: 'admin.instances.content_policies.policies') }.join(' • ') + = f.object.policies.map { |policy| t(policy, scope: 'admin.instances.content_policies.policies') }.join(' · ') - if f.object.public_comment.present? - • + · = f.object.public_comment - if existing_relationships - • + · = fa_icon 'warning fw' = t('admin.export_domain_blocks.import.existing_relationships_warning') diff --git a/app/views/admin/instances/_instance.html.haml b/app/views/admin/instances/_instance.html.haml index 93f9bd418..65cf789ce 100644 --- a/app/views/admin/instances/_instance.html.haml +++ b/app/views/admin/instances/_instance.html.haml @@ -6,7 +6,7 @@ %small - if instance.domain_block - = instance.domain_block.policies.map { |policy| t(policy, scope: 'admin.instances.content_policies.policies') }.join(' • ') + = instance.domain_block.policies.map { |policy| t(policy, scope: 'admin.instances.content_policies.policies') }.join(' · ') - elsif instance.domain_allow = t('admin.accounts.whitelisted') - else diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml index ab290912e..6d67d389d 100644 --- a/app/views/admin/instances/show.html.haml +++ b/app/views/admin/instances/show.html.haml @@ -58,7 +58,7 @@ %td= @instance.domain_block.public_comment %tr %th= t('admin.instances.content_policies.policy') - %td= @instance.domain_block.policies.map { |policy| t(policy, scope: 'admin.instances.content_policies.policies') }.join(' • ') + %td= @instance.domain_block.policies.map { |policy| t(policy, scope: 'admin.instances.content_policies.policies') }.join(' · ') = link_to t('admin.domain_blocks.edit'), edit_admin_domain_block_path(@instance.domain_block), class: 'button' = link_to t('admin.domain_blocks.undo'), admin_domain_block_path(@instance.domain_block), class: 'button', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete } diff --git a/app/views/admin/ip_blocks/_ip_block.html.haml b/app/views/admin/ip_blocks/_ip_block.html.haml index b8d3ac0e8..3dc6f8f8e 100644 --- a/app/views/admin/ip_blocks/_ip_block.html.haml +++ b/app/views/admin/ip_blocks/_ip_block.html.haml @@ -5,7 +5,7 @@ .pending-account__header %samp= link_to "#{ip_block.ip}/#{ip_block.ip.prefix}", admin_accounts_path(ip: "#{ip_block.ip}/#{ip_block.ip.prefix}") - if ip_block.comment.present? - • + · = ip_block.comment %br/ = t("simple_form.labels.ip_block.severities.#{ip_block.severity}") diff --git a/app/views/admin/roles/_role.html.haml b/app/views/admin/roles/_role.html.haml index 798d8d8b4..d6c6b62c8 100644 --- a/app/views/admin/roles/_role.html.haml +++ b/app/views/admin/roles/_role.html.haml @@ -24,7 +24,7 @@ = t('admin.roles.everyone_full_description_html') - else = link_to t('admin.roles.assigned_users', count: role.users.count), admin_accounts_path(role_ids: role.id) - • + · %abbr{ title: role.permissions_as_keys.map { |privilege| I18n.t("admin.roles.privileges.#{privilege}") }.join(', ') }= t('admin.roles.permissions_count', count: role.permissions_as_keys.size) %div = table_link_to 'pencil', t('admin.accounts.edit'), edit_admin_role_path(role) if can?(:update, role) diff --git a/app/views/admin/trends/links/_preview_card.html.haml b/app/views/admin/trends/links/_preview_card.html.haml index 8812feb31..1ca348371 100644 --- a/app/views/admin/trends/links/_preview_card.html.haml +++ b/app/views/admin/trends/links/_preview_card.html.haml @@ -10,21 +10,21 @@ - if preview_card.provider_name.present? = preview_card.provider_name - • + · - if preview_card.language.present? = standard_locale_name(preview_card.language) - • + · = t('admin.trends.links.shared_by_over_week', count: preview_card.history.reduce(0) { |sum, day| sum + day.accounts }) - if preview_card.trend.allowed? - • + · %abbr{ title: t('admin.trends.tags.current_score', score: preview_card.trend.score) }= t('admin.trends.tags.trending_rank', rank: preview_card.trend.rank) - if preview_card.decaying? - • + · = t('admin.trends.tags.peaked_on_and_decaying', date: l(preview_card.max_score_at.to_date, format: :short)) - elsif preview_card.requires_review? - • + · = t('admin.trends.pending_review') diff --git a/app/views/admin/trends/statuses/_status.html.haml b/app/views/admin/trends/statuses/_status.html.haml index f35e13d12..98f2e7709 100644 --- a/app/views/admin/trends/statuses/_status.html.haml +++ b/app/views/admin/trends/statuses/_status.html.haml @@ -17,17 +17,17 @@ = t('admin.trends.statuses.shared_by', count: status.reblogs_count + status.favourites_count, friendly_count: friendly_number_to_human(status.reblogs_count + status.favourites_count)) - if status.account.domain.present? - • + · = status.account.domain - if status.language.present? - • + · = standard_locale_name(status.language) - if status.trendable? && !status.account.discoverable? - • + · = t('admin.trends.statuses.not_discoverable') - if status.trend.allowed? - • + · %abbr{ title: t('admin.trends.tags.current_score', score: status.trend.score) }= t('admin.trends.tags.trending_rank', rank: status.trend.rank) - elsif status.requires_review? - • + · = t('admin.trends.pending_review') diff --git a/app/views/admin/trends/tags/_tag.html.haml b/app/views/admin/trends/tags/_tag.html.haml index a30666a08..3bbdd08db 100644 --- a/app/views/admin/trends/tags/_tag.html.haml +++ b/app/views/admin/trends/tags/_tag.html.haml @@ -13,12 +13,12 @@ = t('admin.trends.tags.used_by_over_week', count: tag.history.reduce(0) { |sum, day| sum + day.accounts }) - if tag.trendable? && (rank = Trends.tags.rank(tag.id)) - • + · %abbr{ title: t('admin.trends.tags.current_score', score: Trends.tags.score(tag.id)) }= t('admin.trends.tags.trending_rank', rank: rank + 1) - if tag.decaying? - • + · = t('admin.trends.tags.peaked_on_and_decaying', date: l(tag.max_score_at.to_date, format: :short)) - elsif tag.requires_review? - • + · = t('admin.trends.pending_review') diff --git a/app/views/admin/webhooks/_webhook.html.haml b/app/views/admin/webhooks/_webhook.html.haml index d94a41eb3..6b3e49eba 100644 --- a/app/views/admin/webhooks/_webhook.html.haml +++ b/app/views/admin/webhooks/_webhook.html.haml @@ -10,7 +10,7 @@ - else %span.negative-hint= t('admin.webhooks.disabled') - • + · %abbr{ title: webhook.events.join(', ') }= t('admin.webhooks.enabled_events', count: webhook.events.size) diff --git a/app/views/admin_mailer/_new_trending_links.text.erb b/app/views/admin_mailer/_new_trending_links.text.erb index 602e12793..85f3f8039 100644 --- a/app/views/admin_mailer/_new_trending_links.text.erb +++ b/app/views/admin_mailer/_new_trending_links.text.erb @@ -1,8 +1,8 @@ <%= raw t('admin_mailer.new_trends.new_trending_links.title') %> <% @links.each do |link| %> -- <%= link.title %> • <%= link.url %> - <%= standard_locale_name(link.language) %> • <%= raw t('admin.trends.links.usage_comparison', today: link.history.get(Time.now.utc).accounts, yesterday: link.history.get(Time.now.utc - 1.day).accounts) %> • <%= t('admin.trends.tags.current_score', score: link.trend.score.round(2)) %> +- <%= link.title %> · <%= link.url %> + <%= standard_locale_name(link.language) %> · <%= raw t('admin.trends.links.usage_comparison', today: link.history.get(Time.now.utc).accounts, yesterday: link.history.get(Time.now.utc - 1.day).accounts) %> · <%= t('admin.trends.tags.current_score', score: link.trend.score.round(2)) %> <% end %> <%= raw t('application_mailer.view')%> <%= admin_trends_links_url %> diff --git a/app/views/admin_mailer/_new_trending_statuses.text.erb b/app/views/admin_mailer/_new_trending_statuses.text.erb index 1ed3ae857..eedbfff9d 100644 --- a/app/views/admin_mailer/_new_trending_statuses.text.erb +++ b/app/views/admin_mailer/_new_trending_statuses.text.erb @@ -2,7 +2,7 @@ <% @statuses.each do |status| %> - <%= ActivityPub::TagManager.instance.url_for(status) %> - <%= standard_locale_name(status.language) %> • <%= raw t('admin.trends.tags.current_score', score: status.trend.score.round(2)) %> + <%= standard_locale_name(status.language) %> · <%= raw t('admin.trends.tags.current_score', score: status.trend.score.round(2)) %> <% end %> <%= raw t('application_mailer.view')%> <%= admin_trends_statuses_url %> diff --git a/app/views/admin_mailer/_new_trending_tags.text.erb b/app/views/admin_mailer/_new_trending_tags.text.erb index 363df369d..d528ab8eb 100644 --- a/app/views/admin_mailer/_new_trending_tags.text.erb +++ b/app/views/admin_mailer/_new_trending_tags.text.erb @@ -2,7 +2,7 @@ <% @tags.each do |tag| %> - #<%= tag.display_name %> - <%= raw t('admin.trends.tags.usage_comparison', today: tag.history.get(Time.now.utc).accounts, yesterday: tag.history.get(Time.now.utc - 1.day).accounts) %> • <%= t('admin.trends.tags.current_score', score: Trends.tags.score(tag.id).round(2)) %> + <%= raw t('admin.trends.tags.usage_comparison', today: tag.history.get(Time.now.utc).accounts, yesterday: tag.history.get(Time.now.utc - 1.day).accounts) %> · <%= t('admin.trends.tags.current_score', score: Trends.tags.score(tag.id).round(2)) %> <% end %> <% if @lowest_trending_tag %> diff --git a/app/views/oauth/authorized_applications/index.html.haml b/app/views/oauth/authorized_applications/index.html.haml index 55d8524db..689f05102 100644 --- a/app/views/oauth/authorized_applications/index.html.haml +++ b/app/views/oauth/authorized_applications/index.html.haml @@ -23,7 +23,7 @@ - else = t('doorkeeper.authorized_applications.index.never_used') - • + · = t('doorkeeper.authorized_applications.index.authorized_at', date: l(application.created_at.to_date)) diff --git a/lib/linter/haml_middle_dot.rb b/lib/linter/haml_middle_dot.rb new file mode 100644 index 000000000..3b2771152 --- /dev/null +++ b/lib/linter/haml_middle_dot.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module HamlLint + # Bans the usage of “•” (bullet) in HTML/HAML in favor of “·” (middle dot) in anything that will end up as a text node. (including string literals in Ruby code) + class Linter::MiddleDot < Linter + include LinterRegistry + + # rubocop:disable Style/MiddleDot + BULLET = '•' + # rubocop:enable Style/MiddleDot + MIDDLE_DOT = '·' + MESSAGE = "Use '#{MIDDLE_DOT}' (middle dot) instead of '#{BULLET}' (bullet)".freeze + + def visit_plain(node) + return unless node.text.include?(BULLET) + + record_lint(node, MESSAGE) + end + + def visit_script(node) + return unless node.script.include?(BULLET) + + record_lint(node, MESSAGE) + end + end +end diff --git a/lib/linter/rubocop_middle_dot.rb b/lib/linter/rubocop_middle_dot.rb new file mode 100644 index 000000000..3a1d97c0c --- /dev/null +++ b/lib/linter/rubocop_middle_dot.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Style + # Bans the usage of “•” (bullet) in HTML/HAML in favor of “·” (middle dot) in string literals + class MiddleDot < Base + extend AutoCorrector + extend Util + + # rubocop:disable Style/MiddleDot + BULLET = '•' + # rubocop:enable Style/MiddleDot + MIDDLE_DOT = '·' + MESSAGE = "Use '#{MIDDLE_DOT}' (middle dot) instead of '#{BULLET}' (bullet)".freeze + + def on_str(node) + # Constants like __FILE__ are handled as strings, + # but don't respond to begin. + return unless node.loc.respond_to?(:begin) && node.loc.begin + + return unless node.value.include?(BULLET) + + add_offense(node, message: MESSAGE) do |corrector| + corrector.replace(node, node.source.gsub(BULLET, MIDDLE_DOT)) + end + end + end + end + end +end From aea67d448bc7974aa4c342f5c1987ec4ee2681ea Mon Sep 17 00:00:00 2001 From: Nick Schonning Date: Fri, 2 Jun 2023 14:01:36 -0400 Subject: [PATCH 31/92] Cleanup old translationRunner (#25241) --- config/webpack/translationRunner.js | 3 --- package.json | 1 - 2 files changed, 4 deletions(-) delete mode 100644 config/webpack/translationRunner.js diff --git a/config/webpack/translationRunner.js b/config/webpack/translationRunner.js deleted file mode 100644 index 77534c9de..000000000 --- a/config/webpack/translationRunner.js +++ /dev/null @@ -1,3 +0,0 @@ -console.error("The localisation functionality has been refactored, please see the Localisation section in the development documentation (https://docs.joinmastodon.org/dev/code/#localizations)"); - -process.exit(1); diff --git a/package.json b/package.json index a08b485fd..49e9c7f74 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "lint:sass": "stylelint \"**/*.{css,scss}\" && prettier --check \"**/*.{css,scss}\"", "lint:yml": "prettier --check \"**/*.{yaml,yml}\"", "lint": "yarn lint:js && yarn lint:json && yarn lint:sass && yarn lint:yml", - "manage:translations": "node ./config/webpack/translationRunner.js", "postversion": "git push --tags", "prepare": "husky install", "start": "node ./streaming/index.js", From 4a5464f36051ea9cbc0c7afa8c885f4f2bdd9245 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 5 Jun 2023 01:42:17 +0200 Subject: [PATCH 32/92] Change "Follow 7 people" to "Find at least 7 people to follow" in web UI (#24954) --- app/javascript/mastodon/features/onboarding/index.jsx | 2 +- app/javascript/mastodon/locales/en.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/features/onboarding/index.jsx b/app/javascript/mastodon/features/onboarding/index.jsx index ecebdb696..79291b3d0 100644 --- a/app/javascript/mastodon/features/onboarding/index.jsx +++ b/app/javascript/mastodon/features/onboarding/index.jsx @@ -120,7 +120,7 @@ class Onboarding extends ImmutablePureComponent {
0 && account.get('note').length > 0)} icon='address-book-o' label={} description={} /> - = 7} icon='user-plus' label={} description={} /> + = 7} icon='user-plus' label={} description={} /> = 1} icon='pencil-square-o' label={} description={} /> } description={} />
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 5ed793cdb..f6d6daa3e 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -460,8 +460,8 @@ "onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:", "onboarding.start.skip": "Want to skip right ahead?", "onboarding.start.title": "You've made it!", - "onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.", - "onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}", + "onboarding.steps.follow_people.body": "You curate your own home feed. Let's fill it with interesting people.", + "onboarding.steps.follow_people.title": "Find at least {count, plural, one {one person} other {# people}} to follow", "onboarding.steps.publish_status.body": "Say hello to the world.", "onboarding.steps.publish_status.title": "Make your first post", "onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.", From e49819142f98a94ba44e24aa093815bf74afab05 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Sun, 4 Jun 2023 19:57:05 -0400 Subject: [PATCH 33/92] Remove unmaintained `nsa` gem (#25265) --- Gemfile | 1 - Gemfile.lock | 7 ------- config/initializers/statsd.rb | 15 --------------- 3 files changed, 23 deletions(-) delete mode 100644 config/initializers/statsd.rb diff --git a/Gemfile b/Gemfile index 62e45a5f3..cff8cb1f8 100644 --- a/Gemfile +++ b/Gemfile @@ -60,7 +60,6 @@ gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar' gem 'nokogiri', '~> 1.15' -gem 'nsa', '~> 0.2' gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' gem 'parslet' diff --git a/Gemfile.lock b/Gemfile.lock index 7d04d875c..bb209b384 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -442,11 +442,6 @@ GEM nokogiri (1.15.2) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nsa (0.2.8) - activesupport (>= 4.2, < 7) - concurrent-ruby (~> 1.0, >= 1.0.2) - sidekiq (>= 3.5) - statsd-ruby (~> 1.4, >= 1.4.0) oj (3.14.3) omniauth (1.9.2) hashie (>= 3.4.6) @@ -682,7 +677,6 @@ GEM net-scp (>= 1.1.2) net-ssh (>= 2.8.0) stackprof (0.2.25) - statsd-ruby (1.5.0) stoplight (3.0.1) redlock (~> 1.0) strong_migrations (0.8.0) @@ -831,7 +825,6 @@ DEPENDENCIES net-http (~> 0.3.2) net-ldap (~> 0.18) nokogiri (~> 1.15) - nsa (~> 0.2) oj (~> 3.14) omniauth (~> 1.9) omniauth-cas (~> 2.0) diff --git a/config/initializers/statsd.rb b/config/initializers/statsd.rb deleted file mode 100644 index 93ea1d1e4..000000000 --- a/config/initializers/statsd.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -if ENV['STATSD_ADDR'].present? - host, port = ENV['STATSD_ADDR'].split(':') - - $statsd = ::Statsd.new(host, port) - $statsd.namespace = ENV.fetch('STATSD_NAMESPACE') { ['Mastodon', Rails.env].join('.') } - - ::NSA.inform_statsd($statsd) do |informant| - informant.collect(:action_controller, :web) - informant.collect(:active_record, :db) - informant.collect(:active_support_cache, :cache) - informant.collect(:sidekiq, :sidekiq) - end -end From 03a707f6a6c8bbefec630c04d183fc9f22b23d9d Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Mon, 5 Jun 2023 03:16:12 -0300 Subject: [PATCH 34/92] Add test coverage for `Mastodon::CLI::Accounts#merge` (#25199) --- spec/lib/mastodon/cli/accounts_spec.rb | 111 +++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/spec/lib/mastodon/cli/accounts_spec.rb b/spec/lib/mastodon/cli/accounts_spec.rb index ba49e480a..50066572c 100644 --- a/spec/lib/mastodon/cli/accounts_spec.rb +++ b/spec/lib/mastodon/cli/accounts_spec.rb @@ -998,4 +998,115 @@ describe Mastodon::CLI::Accounts do end end end + + describe '#merge' do + shared_examples 'an account not found' do |acct| + it 'exits with an error message indicating that there is no such account' do + expect { cli.invoke(:merge, arguments) }.to output( + a_string_including("No such account (#{acct})") + ).to_stdout + .and raise_error(SystemExit) + end + end + + context 'when "from_account" is not found' do + let(:to_account) { Fabricate(:account, domain: 'example.com') } + let(:arguments) { ['non_existent_username@domain.com', "#{to_account.username}@#{to_account.domain}"] } + + it_behaves_like 'an account not found', 'non_existent_username@domain.com' + end + + context 'when "from_account" is a local account' do + let(:from_account) { Fabricate(:account, domain: nil, username: 'bob') } + let(:to_account) { Fabricate(:account, domain: 'example.com') } + let(:arguments) { [from_account.username, "#{to_account.username}@#{to_account.domain}"] } + + it_behaves_like 'an account not found', 'bob' + end + + context 'when "to_account" is not found' do + let(:from_account) { Fabricate(:account, domain: 'example.com') } + let(:arguments) { ["#{from_account.username}@#{from_account.domain}", 'non_existent_username'] } + + it_behaves_like 'an account not found', 'non_existent_username' + end + + context 'when "to_account" is local' do + let(:from_account) { Fabricate(:account, domain: 'example.com') } + let(:to_account) { Fabricate(:account, domain: nil, username: 'bob') } + let(:arguments) do + ["#{from_account.username}@#{from_account.domain}", "#{to_account.username}@#{to_account.domain}"] + end + + it_behaves_like 'an account not found', 'bob@' + end + + context 'when "from_account" and "to_account" public keys do not match' do + let(:from_account) { instance_double(Account, username: 'bob', domain: 'example1.com', local?: false, public_key: 'from_account') } + let(:to_account) { instance_double(Account, username: 'bob', domain: 'example2.com', local?: false, public_key: 'to_account') } + let(:arguments) do + ["#{from_account.username}@#{from_account.domain}", "#{to_account.username}@#{to_account.domain}"] + end + + before do + allow(Account).to receive(:find_remote).with(from_account.username, from_account.domain).and_return(from_account) + allow(Account).to receive(:find_remote).with(to_account.username, to_account.domain).and_return(to_account) + end + + it 'exits with an error message indicating that the accounts do not have the same pub key' do + expect { cli.invoke(:merge, arguments) }.to output( + a_string_including("Accounts don't have the same public key, might not be duplicates!\nOverride with --force") + ).to_stdout + .and raise_error(SystemExit) + end + + context 'with --force option' do + let(:options) { { force: true } } + + before do + allow(to_account).to receive(:merge_with!) + allow(from_account).to receive(:destroy) + end + + it 'merges "from_account" into "to_account"' do + cli.invoke(:merge, arguments, options) + + expect(to_account).to have_received(:merge_with!).with(from_account).once + end + + it 'deletes "from_account"' do + cli.invoke(:merge, arguments, options) + + expect(from_account).to have_received(:destroy).once + end + end + end + + context 'when "from_account" and "to_account" public keys match' do + let(:from_account) { instance_double(Account, username: 'bob', domain: 'example1.com', local?: false, public_key: 'pub_key') } + let(:to_account) { instance_double(Account, username: 'bob', domain: 'example2.com', local?: false, public_key: 'pub_key') } + let(:arguments) do + ["#{from_account.username}@#{from_account.domain}", "#{to_account.username}@#{to_account.domain}"] + end + + before do + allow(Account).to receive(:find_remote).with(from_account.username, from_account.domain).and_return(from_account) + allow(Account).to receive(:find_remote).with(to_account.username, to_account.domain).and_return(to_account) + allow(to_account).to receive(:merge_with!) + allow(from_account).to receive(:destroy) + end + + it 'merges "from_account" into "to_account"' do + cli.invoke(:merge, arguments) + + expect(to_account).to have_received(:merge_with!).with(from_account).once + end + + it 'deletes "from_account"' do + cli.invoke(:merge, arguments) + + expect(from_account).to have_received(:destroy) + end + end + end end From 5c87c6650424c0ec759257b0c8e71fa2d51eedc9 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 5 Jun 2023 02:20:18 -0400 Subject: [PATCH 35/92] Add coverage for CLI::CanonicalEmailBlocks command (#25239) --- .../cli/canonical_email_blocks_spec.rb | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/spec/lib/mastodon/cli/canonical_email_blocks_spec.rb b/spec/lib/mastodon/cli/canonical_email_blocks_spec.rb index fb481e8a8..eb57a3cd1 100644 --- a/spec/lib/mastodon/cli/canonical_email_blocks_spec.rb +++ b/spec/lib/mastodon/cli/canonical_email_blocks_spec.rb @@ -4,9 +4,57 @@ require 'rails_helper' require 'mastodon/cli/canonical_email_blocks' describe Mastodon::CLI::CanonicalEmailBlocks do + let(:cli) { described_class.new } + describe '.exit_on_failure?' do it 'returns true' do expect(described_class.exit_on_failure?).to be true end end + + describe '#find' do + let(:arguments) { ['user@example.com'] } + + context 'when a block is present' do + before { Fabricate(:canonical_email_block, email: 'user@example.com') } + + it 'announces the presence of the block' do + expect { cli.invoke(:find, arguments) }.to output( + a_string_including('user@example.com is blocked') + ).to_stdout + end + end + + context 'when a block is not present' do + it 'announces the absence of the block' do + expect { cli.invoke(:find, arguments) }.to output( + a_string_including('user@example.com is not blocked') + ).to_stdout + end + end + end + + describe '#remove' do + let(:arguments) { ['user@example.com'] } + + context 'when a block is present' do + before { Fabricate(:canonical_email_block, email: 'user@example.com') } + + it 'removes the block' do + expect { cli.invoke(:remove, arguments) }.to output( + a_string_including('Unblocked user@example.com') + ).to_stdout + + expect(CanonicalEmailBlock.matching_email('user@example.com')).to be_empty + end + end + + context 'when a block is not present' do + it 'announces the absence of the block' do + expect { cli.invoke(:remove, arguments) }.to output( + a_string_including('user@example.com is not blocked') + ).to_stdout + end + end + end end From 0daf78f903ba81a2fd1566fd9845cea623e8324c Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 5 Jun 2023 02:22:03 -0400 Subject: [PATCH 36/92] Add `allow_other_host: true` to backups controller (#25266) --- app/controllers/backups_controller.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/backups_controller.rb b/app/controllers/backups_controller.rb index 5891da6f6..205df48d4 100644 --- a/app/controllers/backups_controller.rb +++ b/app/controllers/backups_controller.rb @@ -11,15 +11,15 @@ class BackupsController < ApplicationController def download case Paperclip::Attachment.default_options[:storage] when :s3 - redirect_to @backup.dump.expiring_url(10) + redirect_to @backup.dump.expiring_url(10), allow_other_host: true when :fog if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present? - redirect_to @backup.dump.expiring_url(Time.now.utc + 10) + redirect_to @backup.dump.expiring_url(Time.now.utc + 10), allow_other_host: true else - redirect_to full_asset_url(@backup.dump.url) + redirect_to full_asset_url(@backup.dump.url), allow_other_host: true end when :filesystem - redirect_to full_asset_url(@backup.dump.url) + redirect_to full_asset_url(@backup.dump.url), allow_other_host: true end end From bacb6749217767de83120ad1dea6a59f69ee66d9 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Mon, 5 Jun 2023 00:37:02 -0700 Subject: [PATCH 37/92] Add exclusive lists (#22048) Co-authored-by: Liam Cooke Co-authored-by: John Holdun Co-authored-by: Effy Elden Co-authored-by: Lina Reyne Co-authored-by: Lina <20880695+necropolina@users.noreply.github.com> Co-authored-by: Claire --- app/controllers/api/v1/lists_controller.rb | 2 +- app/javascript/mastodon/actions/lists.js | 4 +- .../mastodon/features/list_timeline/index.jsx | 18 ++++++++- app/javascript/mastodon/locales/en.json | 1 + .../mastodon/reducers/list_editor.js | 2 + app/lib/feed_manager.rb | 26 +++++++------ app/models/list.rb | 1 + app/serializers/rest/list_serializer.rb | 2 +- .../20230605085710_add_exclusive_to_lists.rb | 7 ++++ db/schema.rb | 3 +- spec/lib/feed_manager_spec.rb | 37 +++++++++++++++++++ 11 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 db/migrate/20230605085710_add_exclusive_to_lists.rb diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb index 843ca2ec2..4bbbed267 100644 --- a/app/controllers/api/v1/lists_controller.rb +++ b/app/controllers/api/v1/lists_controller.rb @@ -42,6 +42,6 @@ class Api::V1::ListsController < Api::BaseController end def list_params - params.permit(:title, :replies_policy) + params.permit(:title, :replies_policy, :exclusive) end end diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js index 2faa54b95..b0789cd42 100644 --- a/app/javascript/mastodon/actions/lists.js +++ b/app/javascript/mastodon/actions/lists.js @@ -151,10 +151,10 @@ export const createListFail = error => ({ error, }); -export const updateList = (id, title, shouldReset, replies_policy) => (dispatch, getState) => { +export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => { dispatch(updateListRequest(id)); - api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy }).then(({ data }) => { + api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => { dispatch(updateListSuccess(data)); if (shouldReset) { diff --git a/app/javascript/mastodon/features/list_timeline/index.jsx b/app/javascript/mastodon/features/list_timeline/index.jsx index f41e8e6f2..f9f3a7c31 100644 --- a/app/javascript/mastodon/features/list_timeline/index.jsx +++ b/app/javascript/mastodon/features/list_timeline/index.jsx @@ -8,6 +8,8 @@ import { Helmet } from 'react-helmet'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; +import Toggle from 'react-toggle'; + import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns'; import { fetchList, deleteList, updateList } from 'mastodon/actions/lists'; import { openModal } from 'mastodon/actions/modal'; @@ -145,7 +147,13 @@ class ListTimeline extends PureComponent { handleRepliesPolicyChange = ({ target }) => { const { dispatch } = this.props; const { id } = this.props.params; - dispatch(updateList(id, undefined, false, target.value)); + dispatch(updateList(id, undefined, false, undefined, target.value)); + }; + + onExclusiveToggle = ({ target }) => { + const { dispatch } = this.props; + const { id } = this.props.params; + dispatch(updateList(id, undefined, false, target.checked, undefined)); }; render () { @@ -154,6 +162,7 @@ class ListTimeline extends PureComponent { const pinned = !!columnId; const title = list ? list.get('title') : id; const replies_policy = list ? list.get('replies_policy') : undefined; + const isExclusive = list ? list.get('exclusive') : undefined; if (typeof list === 'undefined') { return ( @@ -191,6 +200,13 @@ class ListTimeline extends PureComponent {
+
+ + +
+ { replies_policy !== undefined && (
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index f6d6daa3e..09282de7c 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -356,6 +356,7 @@ "lists.delete": "Delete list", "lists.edit": "Edit list", "lists.edit.submit": "Change title", + "lists.exclusive": "Hide these posts from home", "lists.new.create": "Add list", "lists.new.title_placeholder": "New list title", "lists.replies_policy.followed": "Any followed user", diff --git a/app/javascript/mastodon/reducers/list_editor.js b/app/javascript/mastodon/reducers/list_editor.js index ceceb27c7..d3fd62ade 100644 --- a/app/javascript/mastodon/reducers/list_editor.js +++ b/app/javascript/mastodon/reducers/list_editor.js @@ -25,6 +25,7 @@ const initialState = ImmutableMap({ isSubmitting: false, isChanged: false, title: '', + isExclusive: false, accounts: ImmutableMap({ items: ImmutableList(), @@ -46,6 +47,7 @@ export default function listEditorReducer(state = initialState, action) { return state.withMutations(map => { map.set('listId', action.list.get('id')); map.set('title', action.list.get('title')); + map.set('isExclusive', action.list.get('is_exclusive')); map.set('isSubmitting', false); }); case LIST_EDITOR_TITLE_CHANGE: diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 643e6828d..7423d2d09 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -40,9 +40,9 @@ class FeedManager def filter?(timeline_type, status, receiver) case timeline_type when :home - filter_from_home?(status, receiver.id, build_crutches(receiver.id, [status])) + filter_from_home?(status, receiver.id, build_crutches(receiver.id, [status]), :home) when :list - filter_from_list?(status, receiver) || filter_from_home?(status, receiver.account_id, build_crutches(receiver.account_id, [status])) + filter_from_list?(status, receiver) || filter_from_home?(status, receiver.account_id, build_crutches(receiver.account_id, [status]), :list) when :mentions filter_from_mentions?(status, receiver.id) when :tags @@ -351,10 +351,11 @@ class FeedManager # @param [Integer] receiver_id # @param [Hash] crutches # @return [Boolean] - def filter_from_home?(status, receiver_id, crutches) + def filter_from_home?(status, receiver_id, crutches, timeline_type = :home) return false if receiver_id == status.account_id return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?) - return true if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language) + return true if timeline_type != :list && crutches[:exclusive_list_users][status.account_id].present? + return true if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language) check_for_blocks = crutches[:active_mentions][status.id] || [] check_for_blocks.push(status.account_id) @@ -543,13 +544,16 @@ class FeedManager arr end - crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map(&:in_reply_to_account_id)).pluck(:target_account_id).index_with(true) - crutches[:languages] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id)).pluck(:target_account_id, :languages).to_h - crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map { |s| s.account_id if s.reblog? }, show_reblogs: false).pluck(:target_account_id).index_with(true) - crutches[:blocking] = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true) - crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true) - crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.flat_map { |s| [s.account.domain, s.reblog&.account&.domain] }.compact).pluck(:domain).index_with(true) - crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| [s.account_id, s.reblog&.account_id] }.flatten.compact).pluck(:account_id).index_with(true) + lists = List.where(account_id: receiver_id, exclusive: true) + + crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map(&:in_reply_to_account_id)).pluck(:target_account_id).index_with(true) + crutches[:languages] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id)).pluck(:target_account_id, :languages).to_h + crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map { |s| s.account_id if s.reblog? }, show_reblogs: false).pluck(:target_account_id).index_with(true) + crutches[:blocking] = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true) + crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true) + crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.flat_map { |s| [s.account.domain, s.reblog&.account&.domain] }.compact).pluck(:domain).index_with(true) + crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| [s.account_id, s.reblog&.account_id] }.flatten.compact).pluck(:account_id).index_with(true) + crutches[:exclusive_list_users] = ListAccount.where(list: lists, account_id: statuses.map(&:account_id)).pluck(:account_id).index_with(true) crutches end diff --git a/app/models/list.rb b/app/models/list.rb index bd1bdbd24..7dc96f01b 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -10,6 +10,7 @@ # created_at :datetime not null # updated_at :datetime not null # replies_policy :integer default("list"), not null +# exclusive :boolean default(FALSE) # class List < ApplicationRecord diff --git a/app/serializers/rest/list_serializer.rb b/app/serializers/rest/list_serializer.rb index 3e87f7119..6a1b6ea3e 100644 --- a/app/serializers/rest/list_serializer.rb +++ b/app/serializers/rest/list_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class REST::ListSerializer < ActiveModel::Serializer - attributes :id, :title, :replies_policy + attributes :id, :title, :replies_policy, :exclusive def id object.id.to_s diff --git a/db/migrate/20230605085710_add_exclusive_to_lists.rb b/db/migrate/20230605085710_add_exclusive_to_lists.rb new file mode 100644 index 000000000..cc21a3e31 --- /dev/null +++ b/db/migrate/20230605085710_add_exclusive_to_lists.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddExclusiveToLists < ActiveRecord::Migration[6.1] + def change + add_column :lists, :exclusive, :boolean, null: false, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 98fa5d600..35fbb8d2e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_05_31_154811) do +ActiveRecord::Schema.define(version: 2023_06_05_085710) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -567,6 +567,7 @@ ActiveRecord::Schema.define(version: 2023_05_31_154811) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "replies_policy", default: 0, null: false + t.boolean "exclusive", default: false t.index ["account_id"], name: "index_lists_on_account_id" end diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index 79d1f5249..31b53fd87 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -26,6 +26,7 @@ RSpec.describe FeedManager do let(:alice) { Fabricate(:account, username: 'alice') } let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') } let(:jeff) { Fabricate(:account, username: 'jeff') } + let(:list) { Fabricate(:list, account: alice) } context 'with home feed' do it 'returns false for followee\'s status' do @@ -153,6 +154,42 @@ RSpec.describe FeedManager do status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de') expect(FeedManager.instance.filter?(:home, status, alice)).to be false end + + it 'returns true for post from followee on exclusive list' do + list.exclusive = true + alice.follow!(bob) + list.accounts << bob + allow(List).to receive(:where).and_return(list) + status = Fabricate(:status, text: 'I post a lot', account: bob) + expect(FeedManager.instance.filter?(:home, status, alice)).to be true + end + + it 'returns true for reblog from followee on exclusive list' do + list.exclusive = true + alice.follow!(jeff) + list.accounts << jeff + allow(List).to receive(:where).and_return(list) + status = Fabricate(:status, text: 'I post a lot', account: bob) + reblog = Fabricate(:status, reblog: status, account: jeff) + expect(FeedManager.instance.filter?(:home, reblog, alice)).to be true + end + + it 'returns false for post from followee on non-exclusive list' do + list.exclusive = false + alice.follow!(bob) + list.accounts << bob + status = Fabricate(:status, text: 'I post a lot', account: bob) + expect(FeedManager.instance.filter?(:home, status, alice)).to be false + end + + it 'returns false for reblog from followee on non-exclusive list' do + list.exclusive = false + alice.follow!(jeff) + list.accounts << jeff + status = Fabricate(:status, text: 'I post a lot', account: bob) + reblog = Fabricate(:status, reblog: status, account: jeff) + expect(FeedManager.instance.filter?(:home, reblog, alice)).to be false + end end context 'with mentions feed' do From c2c396157fa76160b2bb28fd224b1eac40c9c220 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 5 Jun 2023 09:52:36 +0200 Subject: [PATCH 38/92] Fix design issues with recent react-intl upgrade (#25272) --- app/javascript/mastodon/locales/intl_provider.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/mastodon/locales/intl_provider.tsx b/app/javascript/mastodon/locales/intl_provider.tsx index 1ea77c798..4fa8b2247 100644 --- a/app/javascript/mastodon/locales/intl_provider.tsx +++ b/app/javascript/mastodon/locales/intl_provider.tsx @@ -48,6 +48,7 @@ export const IntlProvider: React.FC< locale={locale} messages={messages} onError={onProviderError} + textComponent='span' {...props} > {children} From a8310b15ed95dc262084940d79edb2f456f96742 Mon Sep 17 00:00:00 2001 From: Nick Schonning Date: Mon, 5 Jun 2023 08:40:35 -0400 Subject: [PATCH 39/92] Update kt-paperclip 7.2 from sha (#25274) --- Gemfile | 2 +- Gemfile.lock | 20 +++++++------------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/Gemfile b/Gemfile index cff8cb1f8..ad164af1e 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,7 @@ gem 'dotenv-rails', '~> 2.8' gem 'aws-sdk-s3', '~> 1.123', require: false gem 'fog-core', '<= 2.4.0' gem 'fog-openstack', '~> 0.3', require: false -gem 'kt-paperclip', '~> 7.1', github: 'kreeti/kt-paperclip', ref: '11abf222dc31bff71160a1d138b445214f434b2b' +gem 'kt-paperclip', '~> 7.2' gem 'blurhash', '~> 0.1' gem 'active_model_serializers', '~> 0.10' diff --git a/Gemfile.lock b/Gemfile.lock index bb209b384..a9919bd3a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,18 +7,6 @@ GIT hkdf (~> 0.2) jwt (~> 2.0) -GIT - remote: https://github.com/kreeti/kt-paperclip.git - revision: 11abf222dc31bff71160a1d138b445214f434b2b - ref: 11abf222dc31bff71160a1d138b445214f434b2b - specs: - kt-paperclip (7.1.1) - activemodel (>= 4.2.0) - activesupport (>= 4.2.0) - marcel (~> 1.0.1) - mime-types - terrapin (~> 0.6.0) - GIT remote: https://github.com/mastodon/rails-settings-cached.git revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab @@ -380,6 +368,12 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) + kt-paperclip (7.2.0) + activemodel (>= 4.2.0) + activesupport (>= 4.2.0) + marcel (~> 1.0.1) + mime-types + terrapin (~> 0.6.0) launchy (2.5.2) addressable (~> 2.8) letter_opener (1.8.1) @@ -813,7 +807,7 @@ DEPENDENCIES json-ld-preloaded (~> 3.2) json-schema (~> 4.0) kaminari (~> 1.2) - kt-paperclip (~> 7.1)! + kt-paperclip (~> 7.2) letter_opener (~> 1.8) letter_opener_web (~> 2.0) link_header (~> 0.0) From a6c898f50160c322eb53252a9a2fff8b7b93a0fb Mon Sep 17 00:00:00 2001 From: "S.H" Date: Mon, 5 Jun 2023 21:49:51 +0900 Subject: [PATCH 40/92] Fix not shown announcements in hometimeline. (#25251) --- app/javascript/mastodon/actions/importer/normalizer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 3232e12a2..9ed6b583b 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -138,7 +138,7 @@ export function normalizePollOptionTranslation(translation, poll) { export function normalizeAnnouncement(announcement) { const normalAnnouncement = { ...announcement }; - const emojiMap = makeEmojiMap.emojis(normalAnnouncement); + const emojiMap = makeEmojiMap(normalAnnouncement.emojis); normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap); From 70cd2d600029bb43e98994886b4e528c565fafd6 Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Mon, 5 Jun 2023 09:51:25 -0300 Subject: [PATCH 41/92] Add test coverage for `Mastodon::CLI::Accounts#cull` (#25250) --- spec/lib/mastodon/cli/accounts_spec.rb | 139 +++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/spec/lib/mastodon/cli/accounts_spec.rb b/spec/lib/mastodon/cli/accounts_spec.rb index 50066572c..cf1d612f3 100644 --- a/spec/lib/mastodon/cli/accounts_spec.rb +++ b/spec/lib/mastodon/cli/accounts_spec.rb @@ -1109,4 +1109,143 @@ describe Mastodon::CLI::Accounts do end end end + + describe '#cull' do + let(:delete_account_service) { instance_double(DeleteAccountService, call: nil) } + let!(:tom) { Fabricate(:account, updated_at: 30.days.ago, username: 'tom', uri: 'https://example.com/users/tom', domain: 'example.com') } + let!(:bob) { Fabricate(:account, updated_at: 30.days.ago, last_webfingered_at: nil, username: 'bob', uri: 'https://example.org/users/bob', domain: 'example.org') } + let!(:gon) { Fabricate(:account, updated_at: 15.days.ago, last_webfingered_at: 15.days.ago, username: 'gon', uri: 'https://example.net/users/gon', domain: 'example.net') } + let!(:ana) { Fabricate(:account, username: 'ana', uri: 'https://example.com/users/ana', domain: 'example.com') } + let!(:tales) { Fabricate(:account, updated_at: 10.days.ago, last_webfingered_at: nil, username: 'tales', uri: 'https://example.net/users/tales', domain: 'example.net') } + + before do + allow(DeleteAccountService).to receive(:new).and_return(delete_account_service) + end + + context 'when no domain is specified' do + let(:scope) { Account.remote.where(protocol: :activitypub).partitioned } + + before do + allow(cli).to receive(:parallelize_with_progress).and_yield(tom) + .and_yield(bob) + .and_yield(gon) + .and_yield(ana) + .and_yield(tales) + .and_return([5, 3]) + stub_request(:head, 'https://example.org/users/bob').to_return(status: 404) + stub_request(:head, 'https://example.net/users/gon').to_return(status: 410) + stub_request(:head, 'https://example.net/users/tales').to_return(status: 200) + end + + it 'deletes all inactive remote accounts that longer exist in the origin server' do + cli.cull + + expect(cli).to have_received(:parallelize_with_progress).with(scope).once + expect(delete_account_service).to have_received(:call).with(bob, reserve_username: false).once + expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once + end + + it 'does not delete any active remote account that still exists in the origin server' do + cli.cull + + expect(cli).to have_received(:parallelize_with_progress).with(scope).once + expect(delete_account_service).to_not have_received(:call).with(tom, reserve_username: false) + expect(delete_account_service).to_not have_received(:call).with(ana, reserve_username: false) + expect(delete_account_service).to_not have_received(:call).with(tales, reserve_username: false) + end + + it 'touches inactive remote accounts that have not been deleted' do + allow(tales).to receive(:touch) + + cli.cull + + expect(tales).to have_received(:touch).once + end + + it 'displays the summary correctly' do + expect { cli.cull }.to output( + a_string_including('Visited 5 accounts, removed 3') + ).to_stdout + end + end + + context 'when a domain is specified' do + let(:domain) { 'example.net' } + let(:scope) { Account.remote.where(protocol: :activitypub, domain: domain).partitioned } + + before do + allow(cli).to receive(:parallelize_with_progress).and_yield(gon) + .and_yield(tales) + .and_return([2, 2]) + stub_request(:head, 'https://example.net/users/gon').to_return(status: 410) + stub_request(:head, 'https://example.net/users/tales').to_return(status: 404) + end + + it 'deletes inactive remote accounts that longer exist in the specified domain' do + cli.cull(domain) + + expect(cli).to have_received(:parallelize_with_progress).with(scope).once + expect(delete_account_service).to have_received(:call).with(gon, reserve_username: false).once + expect(delete_account_service).to have_received(:call).with(tales, reserve_username: false).once + end + + it 'displays the summary correctly' do + expect { cli.cull }.to output( + a_string_including('Visited 2 accounts, removed 2') + ).to_stdout + end + end + + context 'when a domain is unavailable' do + shared_examples 'an unavailable domain' do + before do + allow(cli).to receive(:parallelize_with_progress).and_yield(tales).and_return([1, 0]) + end + + it 'skips accounts from the unavailable domain' do + cli.cull + + expect(delete_account_service).to_not have_received(:call).with(tales, reserve_username: false) + end + + it 'displays the summary correctly' do + expect { cli.cull }.to output( + a_string_including("Visited 1 accounts, removed 0\nThe following domains were not available during the check:\n example.net") + ).to_stdout + end + end + + context 'when a connection timeout occurs' do + before do + stub_request(:head, 'https://example.net/users/tales').to_timeout + end + + it_behaves_like 'an unavailable domain' + end + + context 'when a connection error occurs' do + before do + stub_request(:head, 'https://example.net/users/tales').to_raise(HTTP::ConnectionError) + end + + it_behaves_like 'an unavailable domain' + end + + context 'when an ssl error occurs' do + before do + stub_request(:head, 'https://example.net/users/tales').to_raise(OpenSSL::SSL::SSLError) + end + + it_behaves_like 'an unavailable domain' + end + + context 'when a private network address error occurs' do + before do + stub_request(:head, 'https://example.net/users/tales').to_raise(Mastodon::PrivateNetworkAddressError) + end + + it_behaves_like 'an unavailable domain' + end + end + end end From 3b21c13dcc80bad4d8d1ec7c7c52470c5d3942aa Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 5 Jun 2023 10:52:33 -0400 Subject: [PATCH 42/92] Rails 7 compatibility fix for `Admin::Metrics::Dimension` classes (#25277) --- .../dimension/instance_accounts_dimension.rb | 19 +++++++----- .../dimension/instance_languages_dimension.rb | 25 ++++++++++++---- .../metrics/dimension/languages_dimension.rb | 19 +++++++----- .../admin/metrics/dimension/query_helper.rb | 13 ++++++++ .../metrics/dimension/servers_dimension.rb | 24 +++++++++++---- .../metrics/dimension/sources_dimension.rb | 20 ++++++++----- .../dimension/tag_languages_dimension.rb | 29 ++++++++++++++---- .../dimension/tag_servers_dimension.rb | 30 +++++++++++++++---- .../instance_accounts_dimension_spec.rb | 18 +++++++++++ .../instance_languages_dimension_spec.rb | 18 +++++++++++ .../dimension/languages_dimension_spec.rb | 18 +++++++++++ .../dimension/servers_dimension_spec.rb | 18 +++++++++++ .../software_versions_dimension_spec.rb | 18 +++++++++++ .../dimension/sources_dimension_spec.rb | 18 +++++++++++ .../dimension/space_usage_dimension_spec.rb | 18 +++++++++++ .../dimension/tag_languages_dimension_spec.rb | 18 +++++++++++ .../dimension/tag_servers_dimension_spec.rb | 18 +++++++++++ 17 files changed, 297 insertions(+), 44 deletions(-) create mode 100644 app/lib/admin/metrics/dimension/query_helper.rb create mode 100644 spec/lib/admin/metrics/dimension/instance_accounts_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/instance_languages_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/languages_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/servers_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/software_versions_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/sources_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/space_usage_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/tag_languages_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/tag_servers_dimension_spec.rb diff --git a/app/lib/admin/metrics/dimension/instance_accounts_dimension.rb b/app/lib/admin/metrics/dimension/instance_accounts_dimension.rb index 4eac8e611..f8eb9d7bf 100644 --- a/app/lib/admin/metrics/dimension/instance_accounts_dimension.rb +++ b/app/lib/admin/metrics/dimension/instance_accounts_dimension.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Admin::Metrics::Dimension::InstanceAccountsDimension < Admin::Metrics::Dimension::BaseDimension + include Admin::Metrics::Dimension::QueryHelper include LanguagesHelper def self.with_params? @@ -14,19 +15,23 @@ class Admin::Metrics::Dimension::InstanceAccountsDimension < Admin::Metrics::Dim protected def perform_query - sql = <<-SQL.squish + dimension_data_rows.map { |row| { key: row['username'], human_key: row['username'], value: row['value'].to_s } } + end + + def sql_array + [sql_query_string, { domain: params[:domain], limit: @limit }] + end + + def sql_query_string + <<~SQL.squish SELECT accounts.username, count(follows.*) AS value FROM accounts LEFT JOIN follows ON follows.target_account_id = accounts.id - WHERE accounts.domain = $1 + WHERE accounts.domain = :domain GROUP BY accounts.id, follows.target_account_id ORDER BY value DESC - LIMIT $2 + LIMIT :limit SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, params[:domain]], [nil, @limit]]) - - rows.map { |row| { key: row['username'], human_key: row['username'], value: row['value'].to_s } } end def params diff --git a/app/lib/admin/metrics/dimension/instance_languages_dimension.rb b/app/lib/admin/metrics/dimension/instance_languages_dimension.rb index 1ede1a56e..b47821380 100644 --- a/app/lib/admin/metrics/dimension/instance_languages_dimension.rb +++ b/app/lib/admin/metrics/dimension/instance_languages_dimension.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Admin::Metrics::Dimension::InstanceLanguagesDimension < Admin::Metrics::Dimension::BaseDimension + include Admin::Metrics::Dimension::QueryHelper include LanguagesHelper def self.with_params? @@ -14,21 +15,33 @@ class Admin::Metrics::Dimension::InstanceLanguagesDimension < Admin::Metrics::Di protected def perform_query - sql = <<-SQL.squish + dimension_data_rows.map { |row| { key: row['language'], human_key: standard_locale_name(row['language']), value: row['value'].to_s } } + end + + def sql_array + [sql_query_string, { domain: params[:domain], earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }] + end + + def sql_query_string + <<~SQL.squish SELECT COALESCE(statuses.language, 'und') AS language, count(*) AS value FROM statuses INNER JOIN accounts ON accounts.id = statuses.account_id - WHERE accounts.domain = $1 - AND statuses.id BETWEEN $2 AND $3 + WHERE accounts.domain = :domain + AND statuses.id BETWEEN :earliest_status_id AND :latest_status_id AND statuses.reblog_of_id IS NULL GROUP BY COALESCE(statuses.language, 'und') ORDER BY count(*) DESC - LIMIT $4 + LIMIT :limit SQL + end - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, params[:domain]], [nil, Mastodon::Snowflake.id_at(@start_at, with_random: false)], [nil, Mastodon::Snowflake.id_at(@end_at, with_random: false)], [nil, @limit]]) + def earliest_status_id + Mastodon::Snowflake.id_at(@start_at, with_random: false) + end - rows.map { |row| { key: row['language'], human_key: standard_locale_name(row['language']), value: row['value'].to_s } } + def latest_status_id + Mastodon::Snowflake.id_at(@end_at, with_random: false) end def params diff --git a/app/lib/admin/metrics/dimension/languages_dimension.rb b/app/lib/admin/metrics/dimension/languages_dimension.rb index f1cf82cf2..100692a17 100644 --- a/app/lib/admin/metrics/dimension/languages_dimension.rb +++ b/app/lib/admin/metrics/dimension/languages_dimension.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Admin::Metrics::Dimension::LanguagesDimension < Admin::Metrics::Dimension::BaseDimension + include Admin::Metrics::Dimension::QueryHelper include LanguagesHelper def key @@ -10,18 +11,22 @@ class Admin::Metrics::Dimension::LanguagesDimension < Admin::Metrics::Dimension: protected def perform_query - sql = <<-SQL.squish + dimension_data_rows.map { |row| { key: row['locale'], human_key: standard_locale_name(row['locale']), value: row['value'].to_s } } + end + + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at, limit: @limit }] + end + + def sql_query_string + <<~SQL.squish SELECT locale, count(*) AS value FROM users - WHERE current_sign_in_at BETWEEN $1 AND $2 + WHERE current_sign_in_at BETWEEN :start_at AND :end_at AND locale IS NOT NULL GROUP BY locale ORDER BY count(*) DESC - LIMIT $3 + LIMIT :limit SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, @limit]]) - - rows.map { |row| { key: row['locale'], human_key: standard_locale_name(row['locale']), value: row['value'].to_s } } end end diff --git a/app/lib/admin/metrics/dimension/query_helper.rb b/app/lib/admin/metrics/dimension/query_helper.rb new file mode 100644 index 000000000..9fc953cb3 --- /dev/null +++ b/app/lib/admin/metrics/dimension/query_helper.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Admin::Metrics::Dimension::QueryHelper + protected + + def dimension_data_rows + ActiveRecord::Base.connection.select_all(sanitized_sql_string) + end + + def sanitized_sql_string + ActiveRecord::Base.sanitize_sql_array(sql_array) + end +end diff --git a/app/lib/admin/metrics/dimension/servers_dimension.rb b/app/lib/admin/metrics/dimension/servers_dimension.rb index 91bcce655..42aba8e21 100644 --- a/app/lib/admin/metrics/dimension/servers_dimension.rb +++ b/app/lib/admin/metrics/dimension/servers_dimension.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Dimension::ServersDimension < Admin::Metrics::Dimension::BaseDimension + include Admin::Metrics::Dimension::QueryHelper + def key 'servers' end @@ -8,18 +10,30 @@ class Admin::Metrics::Dimension::ServersDimension < Admin::Metrics::Dimension::B protected def perform_query - sql = <<-SQL.squish + dimension_data_rows.map { |row| { key: row['domain'] || Rails.configuration.x.local_domain, human_key: row['domain'] || Rails.configuration.x.local_domain, value: row['value'].to_s } } + end + + def sql_array + [sql_query_string, { earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }] + end + + def sql_query_string + <<~SQL.squish SELECT accounts.domain, count(*) AS value FROM statuses INNER JOIN accounts ON accounts.id = statuses.account_id - WHERE statuses.id BETWEEN $1 AND $2 + WHERE statuses.id BETWEEN :earliest_status_id AND :latest_status_id GROUP BY accounts.domain ORDER BY count(*) DESC - LIMIT $3 + LIMIT :limit SQL + end - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, Mastodon::Snowflake.id_at(@start_at)], [nil, Mastodon::Snowflake.id_at(@end_at)], [nil, @limit]]) + def earliest_status_id + Mastodon::Snowflake.id_at(@start_at) + end - rows.map { |row| { key: row['domain'] || Rails.configuration.x.local_domain, human_key: row['domain'] || Rails.configuration.x.local_domain, value: row['value'].to_s } } + def latest_status_id + Mastodon::Snowflake.id_at(@end_at) end end diff --git a/app/lib/admin/metrics/dimension/sources_dimension.rb b/app/lib/admin/metrics/dimension/sources_dimension.rb index 122807cdc..a14c3e7c1 100644 --- a/app/lib/admin/metrics/dimension/sources_dimension.rb +++ b/app/lib/admin/metrics/dimension/sources_dimension.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Dimension::SourcesDimension < Admin::Metrics::Dimension::BaseDimension + include Admin::Metrics::Dimension::QueryHelper + def key 'sources' end @@ -8,18 +10,22 @@ class Admin::Metrics::Dimension::SourcesDimension < Admin::Metrics::Dimension::B protected def perform_query - sql = <<-SQL.squish + dimension_data_rows.map { |row| { key: row['name'] || 'web', human_key: row['name'] || I18n.t('admin.dashboard.website'), value: row['value'].to_s } } + end + + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at, limit: @limit }] + end + + def sql_query_string + <<~SQL.squish SELECT oauth_applications.name, count(*) AS value FROM users LEFT JOIN oauth_applications ON oauth_applications.id = users.created_by_application_id - WHERE users.created_at BETWEEN $1 AND $2 + WHERE users.created_at BETWEEN :start_at AND :end_at GROUP BY oauth_applications.name ORDER BY count(*) DESC - LIMIT $3 + LIMIT :limit SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, @limit]]) - - rows.map { |row| { key: row['name'] || 'web', human_key: row['name'] || I18n.t('admin.dashboard.website'), value: row['value'].to_s } } end end diff --git a/app/lib/admin/metrics/dimension/tag_languages_dimension.rb b/app/lib/admin/metrics/dimension/tag_languages_dimension.rb index e1349c229..cd077ff86 100644 --- a/app/lib/admin/metrics/dimension/tag_languages_dimension.rb +++ b/app/lib/admin/metrics/dimension/tag_languages_dimension.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Admin::Metrics::Dimension::TagLanguagesDimension < Admin::Metrics::Dimension::BaseDimension + include Admin::Metrics::Dimension::QueryHelper include LanguagesHelper def self.with_params? @@ -14,20 +15,36 @@ class Admin::Metrics::Dimension::TagLanguagesDimension < Admin::Metrics::Dimensi protected def perform_query - sql = <<-SQL.squish + dimension_data_rows.map { |row| { key: row['language'], human_key: standard_locale_name(row['language']), value: row['value'].to_s } } + end + + def sql_array + [sql_query_string, { tag_id: tag_id, earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }] + end + + def sql_query_string + <<~SQL.squish SELECT COALESCE(statuses.language, 'und') AS language, count(*) AS value FROM statuses INNER JOIN statuses_tags ON statuses_tags.status_id = statuses.id - WHERE statuses_tags.tag_id = $1 - AND statuses.id BETWEEN $2 AND $3 + WHERE statuses_tags.tag_id = :tag_id + AND statuses.id BETWEEN :earliest_status_id AND :latest_status_id GROUP BY COALESCE(statuses.language, 'und') ORDER BY count(*) DESC - LIMIT $4 + LIMIT :limit SQL + end - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, params[:id]], [nil, Mastodon::Snowflake.id_at(@start_at, with_random: false)], [nil, Mastodon::Snowflake.id_at(@end_at, with_random: false)], [nil, @limit]]) + def tag_id + params[:id] + end - rows.map { |row| { key: row['language'], human_key: standard_locale_name(row['language']), value: row['value'].to_s } } + def earliest_status_id + Mastodon::Snowflake.id_at(@start_at, with_random: false) + end + + def latest_status_id + Mastodon::Snowflake.id_at(@end_at, with_random: false) end def params diff --git a/app/lib/admin/metrics/dimension/tag_servers_dimension.rb b/app/lib/admin/metrics/dimension/tag_servers_dimension.rb index 7ddf3378c..fc5e49a96 100644 --- a/app/lib/admin/metrics/dimension/tag_servers_dimension.rb +++ b/app/lib/admin/metrics/dimension/tag_servers_dimension.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Dimension::TagServersDimension < Admin::Metrics::Dimension::BaseDimension + include Admin::Metrics::Dimension::QueryHelper + def self.with_params? true end @@ -12,21 +14,37 @@ class Admin::Metrics::Dimension::TagServersDimension < Admin::Metrics::Dimension protected def perform_query - sql = <<-SQL.squish + dimension_data_rows.map { |row| { key: row['domain'] || Rails.configuration.x.local_domain, human_key: row['domain'] || Rails.configuration.x.local_domain, value: row['value'].to_s } } + end + + def sql_array + [sql_query_string, { tag_id: tag_id, earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }] + end + + def sql_query_string + <<-SQL.squish SELECT accounts.domain, count(*) AS value FROM statuses INNER JOIN accounts ON accounts.id = statuses.account_id INNER JOIN statuses_tags ON statuses_tags.status_id = statuses.id - WHERE statuses_tags.tag_id = $1 - AND statuses.id BETWEEN $2 AND $3 + WHERE statuses_tags.tag_id = :tag_id + AND statuses.id BETWEEN :earliest_status_id AND :latest_status_id GROUP BY accounts.domain ORDER BY count(*) DESC - LIMIT $4 + LIMIT :limit SQL + end - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, params[:id]], [nil, Mastodon::Snowflake.id_at(@start_at, with_random: false)], [nil, Mastodon::Snowflake.id_at(@end_at, with_random: false)], [nil, @limit]]) + def tag_id + params[:id] + end - rows.map { |row| { key: row['domain'] || Rails.configuration.x.local_domain, human_key: row['domain'] || Rails.configuration.x.local_domain, value: row['value'].to_s } } + def earliest_status_id + Mastodon::Snowflake.id_at(@start_at, with_random: false) + end + + def latest_status_id + Mastodon::Snowflake.id_at(@end_at, with_random: false) end def params diff --git a/spec/lib/admin/metrics/dimension/instance_accounts_dimension_spec.rb b/spec/lib/admin/metrics/dimension/instance_accounts_dimension_spec.rb new file mode 100644 index 000000000..106717f97 --- /dev/null +++ b/spec/lib/admin/metrics/dimension/instance_accounts_dimension_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Dimension::InstanceAccountsDimension do + subject(:dimension) { described_class.new(start_at, end_at, limit, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:limit) { 10 } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { dimension.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/dimension/instance_languages_dimension_spec.rb b/spec/lib/admin/metrics/dimension/instance_languages_dimension_spec.rb new file mode 100644 index 000000000..f9f6430ca --- /dev/null +++ b/spec/lib/admin/metrics/dimension/instance_languages_dimension_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Dimension::InstanceLanguagesDimension do + subject(:dimension) { described_class.new(start_at, end_at, limit, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:limit) { 10 } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { dimension.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/dimension/languages_dimension_spec.rb b/spec/lib/admin/metrics/dimension/languages_dimension_spec.rb new file mode 100644 index 000000000..1722c4c61 --- /dev/null +++ b/spec/lib/admin/metrics/dimension/languages_dimension_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Dimension::LanguagesDimension do + subject(:dimension) { described_class.new(start_at, end_at, limit, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:limit) { 10 } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { dimension.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/dimension/servers_dimension_spec.rb b/spec/lib/admin/metrics/dimension/servers_dimension_spec.rb new file mode 100644 index 000000000..7e2bb9ac0 --- /dev/null +++ b/spec/lib/admin/metrics/dimension/servers_dimension_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Dimension::ServersDimension do + subject(:dimension) { described_class.new(start_at, end_at, limit, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:limit) { 10 } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { dimension.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/dimension/software_versions_dimension_spec.rb b/spec/lib/admin/metrics/dimension/software_versions_dimension_spec.rb new file mode 100644 index 000000000..ee1491733 --- /dev/null +++ b/spec/lib/admin/metrics/dimension/software_versions_dimension_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Dimension::SoftwareVersionsDimension do + subject(:dimension) { described_class.new(start_at, end_at, limit, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:limit) { 10 } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { dimension.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/dimension/sources_dimension_spec.rb b/spec/lib/admin/metrics/dimension/sources_dimension_spec.rb new file mode 100644 index 000000000..d6b581a9b --- /dev/null +++ b/spec/lib/admin/metrics/dimension/sources_dimension_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Dimension::SourcesDimension do + subject(:dimension) { described_class.new(start_at, end_at, limit, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:limit) { 10 } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { dimension.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/dimension/space_usage_dimension_spec.rb b/spec/lib/admin/metrics/dimension/space_usage_dimension_spec.rb new file mode 100644 index 000000000..65d04cfed --- /dev/null +++ b/spec/lib/admin/metrics/dimension/space_usage_dimension_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Dimension::SpaceUsageDimension do + subject(:dimension) { described_class.new(start_at, end_at, limit, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:limit) { 10 } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { dimension.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/dimension/tag_languages_dimension_spec.rb b/spec/lib/admin/metrics/dimension/tag_languages_dimension_spec.rb new file mode 100644 index 000000000..721d24fa1 --- /dev/null +++ b/spec/lib/admin/metrics/dimension/tag_languages_dimension_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Dimension::TagLanguagesDimension do + subject(:dimension) { described_class.new(start_at, end_at, limit, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:limit) { 10 } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { dimension.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/dimension/tag_servers_dimension_spec.rb b/spec/lib/admin/metrics/dimension/tag_servers_dimension_spec.rb new file mode 100644 index 000000000..305471681 --- /dev/null +++ b/spec/lib/admin/metrics/dimension/tag_servers_dimension_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Dimension::TagServersDimension do + subject(:dimension) { described_class.new(start_at, end_at, limit, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:limit) { 10 } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { dimension.data }.to_not raise_error + end + end +end From 1483a3ddfe74e4fb81d87447a1781943eab86c60 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 5 Jun 2023 17:32:24 +0200 Subject: [PATCH 43/92] Add data-nosnippet so Google doesn't use trending posts in snippets for / (#25279) --- app/javascript/mastodon/features/explore/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/explore/index.jsx b/app/javascript/mastodon/features/explore/index.jsx index dbc0400e8..185db0732 100644 --- a/app/javascript/mastodon/features/explore/index.jsx +++ b/app/javascript/mastodon/features/explore/index.jsx @@ -67,7 +67,7 @@ class Explore extends PureComponent {
-
+
{isSearching ? ( ) : ( From e428670e614b59048431c79e849d18315da98d72 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 5 Jun 2023 17:35:05 +0200 Subject: [PATCH 44/92] Fix CSP headers when S3_ALIAS_HOST includes a path component (#25273) --- config/initializers/content_security_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index f4f917799..a05b67440 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -3,7 +3,7 @@ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy def host_to_url(str) - "http#{Rails.configuration.x.use_https ? 's' : ''}://#{str}" if str.present? + "http#{Rails.configuration.x.use_https ? 's' : ''}://#{str}".split('/').first if str.present? end base_host = Rails.configuration.x.web_domain From af135bddd0a17be9a1345216db7986b593d51151 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 5 Jun 2023 14:46:04 -0400 Subject: [PATCH 45/92] Update `Admin::Metrics::Measure` classes for Rails 7 (#25236) --- .../measure/instance_accounts_measure.rb | 24 ++++++--------- .../measure/instance_followers_measure.rb | 24 ++++++--------- .../measure/instance_follows_measure.rb | 24 ++++++--------- .../instance_media_attachments_measure.rb | 23 +++++--------- .../measure/instance_reports_measure.rb | 24 ++++++--------- .../measure/instance_statuses_measure.rb | 30 ++++++++++--------- .../metrics/measure/new_users_measure.rb | 16 +++++----- .../metrics/measure/opened_reports_measure.rb | 16 +++++----- app/lib/admin/metrics/measure/query_helper.rb | 25 ++++++++++++++++ .../measure/resolved_reports_measure.rb | 16 +++++----- .../metrics/measure/tag_servers_measure.rb | 24 ++++++++++----- .../measure/active_users_measure_spec.rb | 17 +++++++++++ .../measure/instance_accounts_measure_spec.rb | 6 ++++ .../instance_followers_measure_spec.rb | 6 ++++ .../measure/instance_follows_measure_spec.rb | 6 ++++ ...instance_media_attachments_measure_spec.rb | 6 ++++ .../measure/instance_reports_measure_spec.rb | 6 ++++ .../measure/instance_statuses_measure_spec.rb | 6 ++++ .../measure/interactions_measure_spec.rb | 17 +++++++++++ .../metrics/measure/new_users_measure_spec.rb | 17 +++++++++++ .../measure/opened_reports_measure_spec.rb | 17 +++++++++++ .../measure/resolved_reports_measure_spec.rb | 17 +++++++++++ .../measure/tag_accounts_measure_spec.rb | 19 ++++++++++++ .../measure/tag_servers_measure_spec.rb | 19 ++++++++++++ .../metrics/measure/tag_uses_measure_spec.rb | 19 ++++++++++++ 25 files changed, 307 insertions(+), 117 deletions(-) create mode 100644 app/lib/admin/metrics/measure/query_helper.rb create mode 100644 spec/lib/admin/metrics/measure/active_users_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/interactions_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/new_users_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/opened_reports_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/resolved_reports_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/tag_accounts_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/tag_servers_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/tag_uses_measure_spec.rb diff --git a/app/lib/admin/metrics/measure/instance_accounts_measure.rb b/app/lib/admin/metrics/measure/instance_accounts_measure.rb index 14a61de88..3d081fdd9 100644 --- a/app/lib/admin/metrics/measure/instance_accounts_measure.rb +++ b/app/lib/admin/metrics/measure/instance_accounts_measure.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Measure::InstanceAccountsMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper + def self.with_params? true end @@ -25,33 +27,25 @@ class Admin::Metrics::Measure::InstanceAccountsMeasure < Admin::Metrics::Measure nil end - def perform_data_query - account_matching_sql = begin - if params[:include_subdomains] - "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))" - else - 'accounts.domain = $3::text' - end - end + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain] }] + end - sql = <<-SQL.squish + def sql_query_string + <<~SQL.squish SELECT axis.*, ( WITH new_accounts AS ( SELECT accounts.id FROM accounts WHERE date_trunc('day', accounts.created_at)::date = axis.period - AND #{account_matching_sql} + AND #{account_domain_sql(params[:include_subdomains])} ) SELECT count(*) FROM new_accounts ) AS value FROM ( - SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period ) AS axis SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, params[:domain]]]) - - rows.map { |row| { date: row['period'], value: row['value'].to_s } } end def time_period diff --git a/app/lib/admin/metrics/measure/instance_followers_measure.rb b/app/lib/admin/metrics/measure/instance_followers_measure.rb index dc0f5492c..378c6754d 100644 --- a/app/lib/admin/metrics/measure/instance_followers_measure.rb +++ b/app/lib/admin/metrics/measure/instance_followers_measure.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Measure::InstanceFollowersMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper + def self.with_params? true end @@ -25,34 +27,26 @@ class Admin::Metrics::Measure::InstanceFollowersMeasure < Admin::Metrics::Measur nil end - def perform_data_query - account_matching_sql = begin - if params[:include_subdomains] - "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))" - else - 'accounts.domain = $3::text' - end - end + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain] }] + end - sql = <<-SQL.squish + def sql_query_string + <<~SQL.squish SELECT axis.*, ( WITH new_followers AS ( SELECT follows.id FROM follows INNER JOIN accounts ON follows.account_id = accounts.id WHERE date_trunc('day', follows.created_at)::date = axis.period - AND #{account_matching_sql} + AND #{account_domain_sql(params[:include_subdomains])} ) SELECT count(*) FROM new_followers ) AS value FROM ( - SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period ) AS axis SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, params[:domain]]]) - - rows.map { |row| { date: row['period'], value: row['value'].to_s } } end def time_period diff --git a/app/lib/admin/metrics/measure/instance_follows_measure.rb b/app/lib/admin/metrics/measure/instance_follows_measure.rb index f2088ffb3..e213348fb 100644 --- a/app/lib/admin/metrics/measure/instance_follows_measure.rb +++ b/app/lib/admin/metrics/measure/instance_follows_measure.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Measure::InstanceFollowsMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper + def self.with_params? true end @@ -25,34 +27,26 @@ class Admin::Metrics::Measure::InstanceFollowsMeasure < Admin::Metrics::Measure: nil end - def perform_data_query - account_matching_sql = begin - if params[:include_subdomains] - "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))" - else - 'accounts.domain = $3::text' - end - end + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain] }] + end - sql = <<-SQL.squish + def sql_query_string + <<~SQL.squish SELECT axis.*, ( WITH new_follows AS ( SELECT follows.id FROM follows INNER JOIN accounts ON follows.target_account_id = accounts.id WHERE date_trunc('day', follows.created_at)::date = axis.period - AND #{account_matching_sql} + AND #{account_domain_sql(params[:include_subdomains])} ) SELECT count(*) FROM new_follows ) AS value FROM ( - SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period ) AS axis SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, params[:domain]]]) - - rows.map { |row| { date: row['period'], value: row['value'].to_s } } end def time_period diff --git a/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb b/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb index 779883e03..2d4b5f56b 100644 --- a/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb +++ b/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper include ActionView::Helpers::NumberHelper def self.with_params? @@ -35,34 +36,26 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics: nil end - def perform_data_query - account_matching_sql = begin - if params[:include_subdomains] - "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))" - else - 'accounts.domain = $3::text' - end - end + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain] }] + end - sql = <<-SQL.squish + def sql_query_string + <<~SQL.squish SELECT axis.*, ( WITH new_media_attachments AS ( SELECT COALESCE(media_attachments.file_file_size, 0) + COALESCE(media_attachments.thumbnail_file_size, 0) AS size FROM media_attachments INNER JOIN accounts ON accounts.id = media_attachments.account_id WHERE date_trunc('day', media_attachments.created_at)::date = axis.period - AND #{account_matching_sql} + AND #{account_domain_sql(params[:include_subdomains])} ) SELECT SUM(size) FROM new_media_attachments ) AS value FROM ( - SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period ) AS axis SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, params[:domain]]]) - - rows.map { |row| { date: row['period'], value: row['value'].to_s } } end def time_period diff --git a/app/lib/admin/metrics/measure/instance_reports_measure.rb b/app/lib/admin/metrics/measure/instance_reports_measure.rb index c1f7189bf..9da3d53e3 100644 --- a/app/lib/admin/metrics/measure/instance_reports_measure.rb +++ b/app/lib/admin/metrics/measure/instance_reports_measure.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Measure::InstanceReportsMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper + def self.with_params? true end @@ -25,34 +27,26 @@ class Admin::Metrics::Measure::InstanceReportsMeasure < Admin::Metrics::Measure: nil end - def perform_data_query - account_matching_sql = begin - if params[:include_subdomains] - "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $3::text))" - else - 'accounts.domain = $3::text' - end - end + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain] }] + end - sql = <<-SQL.squish + def sql_query_string + <<~SQL.squish SELECT axis.*, ( WITH new_reports AS ( SELECT reports.id FROM reports INNER JOIN accounts ON accounts.id = reports.target_account_id WHERE date_trunc('day', reports.created_at)::date = axis.period - AND #{account_matching_sql} + AND #{account_domain_sql(params[:include_subdomains])} ) SELECT count(*) FROM new_reports ) AS value FROM ( - SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period ) AS axis SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, params[:domain]]]) - - rows.map { |row| { date: row['period'], value: row['value'].to_s } } end def time_period diff --git a/app/lib/admin/metrics/measure/instance_statuses_measure.rb b/app/lib/admin/metrics/measure/instance_statuses_measure.rb index 1b38b40c5..8c71c6614 100644 --- a/app/lib/admin/metrics/measure/instance_statuses_measure.rb +++ b/app/lib/admin/metrics/measure/instance_statuses_measure.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper + def self.with_params? true end @@ -25,35 +27,35 @@ class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure nil end - def perform_data_query - account_matching_sql = begin - if params[:include_subdomains] - "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || $5::text))" - else - 'accounts.domain = $5::text' - end - end + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain], earliest_status_id: earliest_status_id, latest_status_id: latest_status_id }] + end - sql = <<-SQL.squish + def sql_query_string + <<~SQL.squish SELECT axis.*, ( WITH new_statuses AS ( SELECT statuses.id FROM statuses INNER JOIN accounts ON accounts.id = statuses.account_id - WHERE statuses.id BETWEEN $3 AND $4 - AND #{account_matching_sql} + WHERE statuses.id BETWEEN :earliest_status_id AND :latest_status_id + AND #{account_domain_sql(params[:include_subdomains])} AND date_trunc('day', statuses.created_at)::date = axis.period ) SELECT count(*) FROM new_statuses ) AS value FROM ( - SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period ) AS axis SQL + end - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, Mastodon::Snowflake.id_at(@start_at, with_random: false)], [nil, Mastodon::Snowflake.id_at(@end_at, with_random: false)], [nil, params[:domain]]]) + def earliest_status_id + Mastodon::Snowflake.id_at(@start_at, with_random: false) + end - rows.map { |row| { date: row['period'], value: row['value'].to_s } } + def latest_status_id + Mastodon::Snowflake.id_at(@end_at, with_random: false) end def time_period diff --git a/app/lib/admin/metrics/measure/new_users_measure.rb b/app/lib/admin/metrics/measure/new_users_measure.rb index 71191f1a2..6837c14c8 100644 --- a/app/lib/admin/metrics/measure/new_users_measure.rb +++ b/app/lib/admin/metrics/measure/new_users_measure.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Measure::NewUsersMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper + def key 'new_users' end @@ -15,8 +17,12 @@ class Admin::Metrics::Measure::NewUsersMeasure < Admin::Metrics::Measure::BaseMe User.where(created_at: previous_time_period).count end - def perform_data_query - sql = <<-SQL.squish + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at }] + end + + def sql_query_string + <<~SQL.squish SELECT axis.*, ( WITH new_users AS ( SELECT users.id @@ -26,12 +32,8 @@ class Admin::Metrics::Measure::NewUsersMeasure < Admin::Metrics::Measure::BaseMe SELECT count(*) FROM new_users ) AS value FROM ( - SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period ) AS axis SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at]]) - - rows.map { |row| { date: row['period'], value: row['value'].to_s } } end end diff --git a/app/lib/admin/metrics/measure/opened_reports_measure.rb b/app/lib/admin/metrics/measure/opened_reports_measure.rb index 4b80a0c8c..c395c4634 100644 --- a/app/lib/admin/metrics/measure/opened_reports_measure.rb +++ b/app/lib/admin/metrics/measure/opened_reports_measure.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Measure::OpenedReportsMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper + def key 'opened_reports' end @@ -15,8 +17,12 @@ class Admin::Metrics::Measure::OpenedReportsMeasure < Admin::Metrics::Measure::B Report.where(created_at: previous_time_period).count end - def perform_data_query - sql = <<-SQL.squish + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at }] + end + + def sql_query_string + <<~SQL.squish SELECT axis.*, ( WITH new_reports AS ( SELECT reports.id @@ -26,12 +32,8 @@ class Admin::Metrics::Measure::OpenedReportsMeasure < Admin::Metrics::Measure::B SELECT count(*) FROM new_reports ) AS value FROM ( - SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period ) AS axis SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at]]) - - rows.map { |row| { date: row['period'], value: row['value'].to_s } } end end diff --git a/app/lib/admin/metrics/measure/query_helper.rb b/app/lib/admin/metrics/measure/query_helper.rb new file mode 100644 index 000000000..969065f73 --- /dev/null +++ b/app/lib/admin/metrics/measure/query_helper.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Admin::Metrics::Measure::QueryHelper + protected + + def perform_data_query + measurement_data_rows.map { |row| { date: row['period'], value: row['value'].to_s } } + end + + def measurement_data_rows + ActiveRecord::Base.connection.select_all(sanitized_sql_string) + end + + def sanitized_sql_string + ActiveRecord::Base.sanitize_sql_array(sql_array) + end + + def account_domain_sql(include_subdomains) + if include_subdomains + "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || :domain::text))" + else + 'accounts.domain = :domain::text' + end + end +end diff --git a/app/lib/admin/metrics/measure/resolved_reports_measure.rb b/app/lib/admin/metrics/measure/resolved_reports_measure.rb index 4ab746c8f..780db75a1 100644 --- a/app/lib/admin/metrics/measure/resolved_reports_measure.rb +++ b/app/lib/admin/metrics/measure/resolved_reports_measure.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Measure::ResolvedReportsMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper + def key 'resolved_reports' end @@ -15,8 +17,12 @@ class Admin::Metrics::Measure::ResolvedReportsMeasure < Admin::Metrics::Measure: Report.resolved.where(action_taken_at: previous_time_period).count end - def perform_data_query - sql = <<-SQL.squish + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at }] + end + + def sql_query_string + <<~SQL.squish SELECT axis.*, ( WITH resolved_reports AS ( SELECT reports.id @@ -26,12 +32,8 @@ class Admin::Metrics::Measure::ResolvedReportsMeasure < Admin::Metrics::Measure: SELECT count(*) FROM resolved_reports ) AS value FROM ( - SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period ) AS axis SQL - - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at]]) - - rows.map { |row| { date: row['period'], value: row['value'].to_s } } end end diff --git a/app/lib/admin/metrics/measure/tag_servers_measure.rb b/app/lib/admin/metrics/measure/tag_servers_measure.rb index 11f229602..e6378b802 100644 --- a/app/lib/admin/metrics/measure/tag_servers_measure.rb +++ b/app/lib/admin/metrics/measure/tag_servers_measure.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::Metrics::Measure::TagServersMeasure < Admin::Metrics::Measure::BaseMeasure + include Admin::Metrics::Measure::QueryHelper + def self.with_params? true end @@ -19,25 +21,33 @@ class Admin::Metrics::Measure::TagServersMeasure < Admin::Metrics::Measure::Base tag.statuses.where('statuses.id BETWEEN ? AND ?', Mastodon::Snowflake.id_at(@start_at - length_of_period, with_random: false), Mastodon::Snowflake.id_at(@end_at - length_of_period, with_random: false)).joins(:account).count('distinct accounts.domain') end - def perform_data_query - sql = <<-SQL.squish + def sql_array + [sql_query_string, { start_at: @start_at, end_at: @end_at, tag_id: tag.id, earliest_status_id: earliest_status_id, latest_status_id: latest_status_id }] + end + + def sql_query_string + <<~SQL.squish SELECT axis.*, ( SELECT count(distinct accounts.domain) AS value FROM statuses INNER JOIN statuses_tags ON statuses.id = statuses_tags.status_id INNER JOIN accounts ON statuses.account_id = accounts.id - WHERE statuses_tags.tag_id = $1 - AND statuses.id BETWEEN $2 AND $3 + WHERE statuses_tags.tag_id = :tag_id + AND statuses.id BETWEEN :earliest_status_id AND :latest_status_id AND date_trunc('day', statuses.created_at)::date = axis.day ) FROM ( - SELECT generate_series(date_trunc('day', $4::timestamp)::date, date_trunc('day', $5::timestamp)::date, ('1 day')::interval) AS day + SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, ('1 day')::interval) AS day ) as axis SQL + end - rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, params[:id].to_i], [nil, Mastodon::Snowflake.id_at(@start_at, with_random: false)], [nil, Mastodon::Snowflake.id_at(@end_at, with_random: false)], [nil, @start_at], [nil, @end_at]]) + def earliest_status_id + Mastodon::Snowflake.id_at(@start_at, with_random: false) + end - rows.map { |row| { date: row['day'], value: row['value'].to_s } } + def latest_status_id + Mastodon::Snowflake.id_at(@end_at, with_random: false) end def tag diff --git a/spec/lib/admin/metrics/measure/active_users_measure_spec.rb b/spec/lib/admin/metrics/measure/active_users_measure_spec.rb new file mode 100644 index 000000000..55164ed88 --- /dev/null +++ b/spec/lib/admin/metrics/measure/active_users_measure_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::ActiveUsersMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/measure/instance_accounts_measure_spec.rb b/spec/lib/admin/metrics/measure/instance_accounts_measure_spec.rb index 29a157491..8e414963f 100644 --- a/spec/lib/admin/metrics/measure/instance_accounts_measure_spec.rb +++ b/spec/lib/admin/metrics/measure/instance_accounts_measure_spec.rb @@ -37,4 +37,10 @@ describe Admin::Metrics::Measure::InstanceAccountsMeasure do end end end + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end end diff --git a/spec/lib/admin/metrics/measure/instance_followers_measure_spec.rb b/spec/lib/admin/metrics/measure/instance_followers_measure_spec.rb index ebf789c1b..c627e6ced 100644 --- a/spec/lib/admin/metrics/measure/instance_followers_measure_spec.rb +++ b/spec/lib/admin/metrics/measure/instance_followers_measure_spec.rb @@ -39,4 +39,10 @@ describe Admin::Metrics::Measure::InstanceFollowersMeasure do end end end + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end end diff --git a/spec/lib/admin/metrics/measure/instance_follows_measure_spec.rb b/spec/lib/admin/metrics/measure/instance_follows_measure_spec.rb index 335e3c732..42f33dfc3 100644 --- a/spec/lib/admin/metrics/measure/instance_follows_measure_spec.rb +++ b/spec/lib/admin/metrics/measure/instance_follows_measure_spec.rb @@ -39,4 +39,10 @@ describe Admin::Metrics::Measure::InstanceFollowsMeasure do end end end + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end end diff --git a/spec/lib/admin/metrics/measure/instance_media_attachments_measure_spec.rb b/spec/lib/admin/metrics/measure/instance_media_attachments_measure_spec.rb index 711a2aff0..c103307f9 100644 --- a/spec/lib/admin/metrics/measure/instance_media_attachments_measure_spec.rb +++ b/spec/lib/admin/metrics/measure/instance_media_attachments_measure_spec.rb @@ -40,4 +40,10 @@ describe Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure do end end end + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end end diff --git a/spec/lib/admin/metrics/measure/instance_reports_measure_spec.rb b/spec/lib/admin/metrics/measure/instance_reports_measure_spec.rb index f0ffd39cf..62fcf84ac 100644 --- a/spec/lib/admin/metrics/measure/instance_reports_measure_spec.rb +++ b/spec/lib/admin/metrics/measure/instance_reports_measure_spec.rb @@ -36,4 +36,10 @@ describe Admin::Metrics::Measure::InstanceReportsMeasure do end end end + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end end diff --git a/spec/lib/admin/metrics/measure/instance_statuses_measure_spec.rb b/spec/lib/admin/metrics/measure/instance_statuses_measure_spec.rb index c1425ecdb..df4cfe207 100644 --- a/spec/lib/admin/metrics/measure/instance_statuses_measure_spec.rb +++ b/spec/lib/admin/metrics/measure/instance_statuses_measure_spec.rb @@ -36,4 +36,10 @@ describe Admin::Metrics::Measure::InstanceStatusesMeasure do end end end + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end end diff --git a/spec/lib/admin/metrics/measure/interactions_measure_spec.rb b/spec/lib/admin/metrics/measure/interactions_measure_spec.rb new file mode 100644 index 000000000..e98c83059 --- /dev/null +++ b/spec/lib/admin/metrics/measure/interactions_measure_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::InteractionsMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/measure/new_users_measure_spec.rb b/spec/lib/admin/metrics/measure/new_users_measure_spec.rb new file mode 100644 index 000000000..fe82f8219 --- /dev/null +++ b/spec/lib/admin/metrics/measure/new_users_measure_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::NewUsersMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/measure/opened_reports_measure_spec.rb b/spec/lib/admin/metrics/measure/opened_reports_measure_spec.rb new file mode 100644 index 000000000..deed64ae8 --- /dev/null +++ b/spec/lib/admin/metrics/measure/opened_reports_measure_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::OpenedReportsMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/measure/resolved_reports_measure_spec.rb b/spec/lib/admin/metrics/measure/resolved_reports_measure_spec.rb new file mode 100644 index 000000000..cb98df2dc --- /dev/null +++ b/spec/lib/admin/metrics/measure/resolved_reports_measure_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::ResolvedReportsMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:params) { ActionController::Parameters.new } + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/measure/tag_accounts_measure_spec.rb b/spec/lib/admin/metrics/measure/tag_accounts_measure_spec.rb new file mode 100644 index 000000000..938b67afa --- /dev/null +++ b/spec/lib/admin/metrics/measure/tag_accounts_measure_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::TagAccountsMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let!(:tag) { Fabricate(:tag) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:params) { ActionController::Parameters.new(id: tag.id) } + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/measure/tag_servers_measure_spec.rb b/spec/lib/admin/metrics/measure/tag_servers_measure_spec.rb new file mode 100644 index 000000000..e09a2b04e --- /dev/null +++ b/spec/lib/admin/metrics/measure/tag_servers_measure_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::TagServersMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let!(:tag) { Fabricate(:tag) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:params) { ActionController::Parameters.new(id: tag.id) } + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end +end diff --git a/spec/lib/admin/metrics/measure/tag_uses_measure_spec.rb b/spec/lib/admin/metrics/measure/tag_uses_measure_spec.rb new file mode 100644 index 000000000..869e93744 --- /dev/null +++ b/spec/lib/admin/metrics/measure/tag_uses_measure_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Metrics::Measure::TagUsesMeasure do + subject(:measure) { described_class.new(start_at, end_at, params) } + + let!(:tag) { Fabricate(:tag) } + + let(:start_at) { 2.days.ago } + let(:end_at) { Time.now.utc } + let(:params) { ActionController::Parameters.new(id: tag.id) } + + describe '#data' do + it 'runs data query without error' do + expect { measure.data }.to_not raise_error + end + end +end From 0ddc89528213859bf4caeefd475a93ed973ab0e0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 6 Jun 2023 04:14:28 +0200 Subject: [PATCH 46/92] Change follow button in account row to be more obvious in web UI (#24956) --- .../mastodon/components/account.jsx | 41 ++++++++++--------- app/javascript/mastodon/locales/en.json | 8 ++-- .../styles/mastodon/components.scss | 6 ++- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx index ea863f5d1..0f3b85388 100644 --- a/app/javascript/mastodon/components/account.jsx +++ b/app/javascript/mastodon/components/account.jsx @@ -16,6 +16,7 @@ import { VerifiedBadge } from 'mastodon/components/verified_badge'; import { me } from '../initial_state'; import { Avatar } from './avatar'; +import Button from './button'; import { DisplayName } from './display_name'; import { IconButton } from './icon_button'; import { RelativeTimestamp } from './relative_timestamp'; @@ -23,13 +24,13 @@ import { RelativeTimestamp } from './relative_timestamp'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' }, - unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' }, - mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, - block: { id: 'account.block', defaultMessage: 'Block @{name}' }, + cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' }, + unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' }, + unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' }, + mute_notifications: { id: 'account.mute_notifications_short', defaultMessage: 'Mute notifications' }, + unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' }, + mute: { id: 'account.mute_short', defaultMessage: 'Mute' }, + block: { id: 'account.block_short', defaultMessage: 'Block' }, }); class Account extends ImmutablePureComponent { @@ -96,39 +97,39 @@ class Account extends ImmutablePureComponent { let buttons; - if (actionIcon) { - if (onActionClick) { - buttons = ; - } - } else if (account.get('id') !== me && account.get('relationship', null) !== null) { + if (actionIcon && onActionClick) { + buttons = ; + } else if (!actionIcon && account.get('id') !== me && account.get('relationship', null) !== null) { const following = account.getIn(['relationship', 'following']); const requested = account.getIn(['relationship', 'requested']); const blocking = account.getIn(['relationship', 'blocking']); const muting = account.getIn(['relationship', 'muting']); if (requested) { - buttons = ; + buttons = - ); - } - -} diff --git a/app/javascript/mastodon/components/load_more.tsx b/app/javascript/mastodon/components/load_more.tsx new file mode 100644 index 000000000..8b5746ad3 --- /dev/null +++ b/app/javascript/mastodon/components/load_more.tsx @@ -0,0 +1,24 @@ +import { FormattedMessage } from 'react-intl'; + +interface Props { + onClick: (event: React.MouseEvent) => void; + disabled?: boolean; + visible?: boolean; +} +export const LoadMore: React.FC = ({ + onClick, + disabled, + visible = true, +}) => { + return ( + + ); +}; diff --git a/app/javascript/mastodon/components/scrollable_list.jsx b/app/javascript/mastodon/components/scrollable_list.jsx index 9a0c4c8a7..53a84ecb5 100644 --- a/app/javascript/mastodon/components/scrollable_list.jsx +++ b/app/javascript/mastodon/components/scrollable_list.jsx @@ -15,7 +15,7 @@ import IntersectionObserverArticleContainer from '../containers/intersection_obs import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen'; import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'; -import LoadMore from './load_more'; +import { LoadMore } from './load_more'; import LoadPending from './load_pending'; import LoadingIndicator from './loading_indicator'; diff --git a/app/javascript/mastodon/features/account_gallery/index.jsx b/app/javascript/mastodon/features/account_gallery/index.jsx index 27de4740c..653a25866 100644 --- a/app/javascript/mastodon/features/account_gallery/index.jsx +++ b/app/javascript/mastodon/features/account_gallery/index.jsx @@ -9,7 +9,7 @@ import { connect } from 'react-redux'; import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts'; import { openModal } from 'mastodon/actions/modal'; import ColumnBackButton from 'mastodon/components/column_back_button'; -import LoadMore from 'mastodon/components/load_more'; +import { LoadMore } from 'mastodon/components/load_more'; import LoadingIndicator from 'mastodon/components/loading_indicator'; import ScrollContainer from 'mastodon/containers/scroll_container'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; diff --git a/app/javascript/mastodon/features/compose/components/search_results.jsx b/app/javascript/mastodon/features/compose/components/search_results.jsx index b329cae79..b11ac478a 100644 --- a/app/javascript/mastodon/features/compose/components/search_results.jsx +++ b/app/javascript/mastodon/features/compose/components/search_results.jsx @@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { Icon } from 'mastodon/components/icon'; -import LoadMore from 'mastodon/components/load_more'; +import { LoadMore } from 'mastodon/components/load_more'; import { ImmutableHashtag as Hashtag } from '../../../components/hashtag'; import AccountContainer from '../../../containers/account_container'; diff --git a/app/javascript/mastodon/features/directory/index.jsx b/app/javascript/mastodon/features/directory/index.jsx index d4854f186..635b6f411 100644 --- a/app/javascript/mastodon/features/directory/index.jsx +++ b/app/javascript/mastodon/features/directory/index.jsx @@ -13,7 +13,7 @@ import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'mastodo import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory'; import Column from 'mastodon/components/column'; import ColumnHeader from 'mastodon/components/column_header'; -import LoadMore from 'mastodon/components/load_more'; +import { LoadMore } from 'mastodon/components/load_more'; import LoadingIndicator from 'mastodon/components/loading_indicator'; import { RadioButton } from 'mastodon/components/radio_button'; import ScrollContainer from 'mastodon/containers/scroll_container'; diff --git a/app/javascript/mastodon/features/explore/results.jsx b/app/javascript/mastodon/features/explore/results.jsx index 6b053a9dc..dc1f72022 100644 --- a/app/javascript/mastodon/features/explore/results.jsx +++ b/app/javascript/mastodon/features/explore/results.jsx @@ -11,7 +11,7 @@ import { connect } from 'react-redux'; import { expandSearch } from 'mastodon/actions/search'; import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag'; -import LoadMore from 'mastodon/components/load_more'; +import { LoadMore } from 'mastodon/components/load_more'; import LoadingIndicator from 'mastodon/components/loading_indicator'; import Account from 'mastodon/containers/account_container'; import Status from 'mastodon/containers/status_container'; From b22bfae4f93d635dc9416d45ad11b29d9f5104e5 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 6 Jun 2023 07:34:04 -0400 Subject: [PATCH 52/92] Add coverage for `DomainBlock#public_domain` method (#25283) --- spec/models/domain_block_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/models/domain_block_spec.rb b/spec/models/domain_block_spec.rb index f10f47027..e123c03d6 100644 --- a/spec/models/domain_block_spec.rb +++ b/spec/models/domain_block_spec.rb @@ -91,4 +91,22 @@ RSpec.describe DomainBlock do expect(newer.stricter_than?(older)).to be false end end + + describe '#public_domain' do + context 'with a domain block that is obfuscated' do + let(:domain_block) { Fabricate(:domain_block, domain: 'hostname.example.com', obfuscate: true) } + + it 'garbles the domain' do + expect(domain_block.public_domain).to eq 'hostna**.******e.com' + end + end + + context 'with a domain block that is not obfuscated' do + let(:domain_block) { Fabricate(:domain_block, domain: 'example.com', obfuscate: false) } + + it 'returns the domain value' do + expect(domain_block.public_domain).to eq 'example.com' + end + end + end end From eb6f8181e12ad0d7c47ae7c71ca6b737c844e35c Mon Sep 17 00:00:00 2001 From: Daniel M Brasil Date: Tue, 6 Jun 2023 08:37:09 -0300 Subject: [PATCH 53/92] Add test coverage for `Mastodon::CLI::Accounts#reset_relationships` (#25194) --- spec/lib/mastodon/cli/accounts_spec.rb | 113 +++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/spec/lib/mastodon/cli/accounts_spec.rb b/spec/lib/mastodon/cli/accounts_spec.rb index cf1d612f3..a263d673d 100644 --- a/spec/lib/mastodon/cli/accounts_spec.rb +++ b/spec/lib/mastodon/cli/accounts_spec.rb @@ -1248,4 +1248,117 @@ describe Mastodon::CLI::Accounts do end end end + + describe '#reset_relationships' do + let(:target_account) { Fabricate(:account) } + let(:arguments) { [target_account.username] } + + context 'when no option is given' do + it 'exits with an error message indicating that at least one option is required' do + expect { cli.invoke(:reset_relationships, arguments) }.to output( + a_string_including('Please specify either --follows or --followers, or both') + ).to_stdout + .and raise_error(SystemExit) + end + end + + context 'when the given username is not found' do + let(:arguments) { ['non_existent_username'] } + + it 'exits with an error message indicating that there is no such account' do + expect { cli.invoke(:reset_relationships, arguments, follows: true) }.to output( + a_string_including('No such account') + ).to_stdout + .and raise_error(SystemExit) + end + end + + context 'when the given username is found' do + let(:total_relationships) { 10 } + let!(:accounts) { Fabricate.times(total_relationships, :account) } + + context 'with --follows option' do + let(:options) { { follows: true } } + + before do + accounts.each { |account| target_account.follow!(account) } + end + + it 'resets all "following" relationships from the target account' do + cli.invoke(:reset_relationships, arguments, options) + + expect(target_account.reload.following).to be_empty + end + + it 'calls BootstrapTimelineWorker once to rebuild the timeline' do + allow(BootstrapTimelineWorker).to receive(:perform_async) + + cli.invoke(:reset_relationships, arguments, options) + + expect(BootstrapTimelineWorker).to have_received(:perform_async).with(target_account.id).once + end + + it 'displays a successful message' do + expect { cli.invoke(:reset_relationships, arguments, options) }.to output( + a_string_including("Processed #{total_relationships} relationships") + ).to_stdout + end + end + + context 'with --followers option' do + let(:options) { { followers: true } } + + before do + accounts.each { |account| account.follow!(target_account) } + end + + it 'resets all "followers" relationships from the target account' do + cli.invoke(:reset_relationships, arguments, options) + + expect(target_account.reload.followers).to be_empty + end + + it 'displays a successful message' do + expect { cli.invoke(:reset_relationships, arguments, options) }.to output( + a_string_including("Processed #{total_relationships} relationships") + ).to_stdout + end + end + + context 'with --follows and --followers options' do + let(:options) { { followers: true, follows: true } } + + before do + accounts.first(6).each { |account| account.follow!(target_account) } + accounts.last(4).each { |account| target_account.follow!(account) } + end + + it 'resets all "followers" relationships from the target account' do + cli.invoke(:reset_relationships, arguments, options) + + expect(target_account.reload.followers).to be_empty + end + + it 'resets all "following" relationships from the target account' do + cli.invoke(:reset_relationships, arguments, options) + + expect(target_account.reload.following).to be_empty + end + + it 'calls BootstrapTimelineWorker once to rebuild the timeline' do + allow(BootstrapTimelineWorker).to receive(:perform_async) + + cli.invoke(:reset_relationships, arguments, options) + + expect(BootstrapTimelineWorker).to have_received(:perform_async).with(target_account.id).once + end + + it 'displays a successful message' do + expect { cli.invoke(:reset_relationships, arguments, options) }.to output( + a_string_including("Processed #{total_relationships} relationships") + ).to_stdout + end + end + end + end end From 1e243e2df7ff9dfaf753e86e8f93760a96ffda18 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 6 Jun 2023 07:57:00 -0400 Subject: [PATCH 54/92] Misc spec coverage for `Admin::` area controllers (#25282) --- .../admin/account_actions_controller_spec.rb | 12 ++ .../admin/accounts_controller_spec.rb | 124 ++++++++++++++++++ .../admin/announcements_controller_spec.rb | 26 ++++ .../admin/relays_controller_spec.rb | 41 ++++++ .../admin/statuses_controller_spec.rb | 10 ++ .../admin/warning_presets_controller_spec.rb | 64 +++++++++ 6 files changed, 277 insertions(+) diff --git a/spec/controllers/admin/account_actions_controller_spec.rb b/spec/controllers/admin/account_actions_controller_spec.rb index 4eae51c7b..b8dae7993 100644 --- a/spec/controllers/admin/account_actions_controller_spec.rb +++ b/spec/controllers/admin/account_actions_controller_spec.rb @@ -20,4 +20,16 @@ describe Admin::AccountActionsController do expect(response).to have_http_status(:success) end end + + describe 'POST #create' do + let(:account) { Fabricate(:account) } + + it 'records the account action' do + expect do + post :create, params: { account_id: account.id, admin_account_action: { type: 'silence' } } + end.to change { account.strikes.count }.by(1) + + expect(response).to redirect_to(admin_account_path(account.id)) + end + end end diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb index 7d001c4cb..782e460a4 100644 --- a/spec/controllers/admin/accounts_controller_spec.rb +++ b/spec/controllers/admin/accounts_controller_spec.rb @@ -309,4 +309,128 @@ RSpec.describe Admin::AccountsController do end end end + + describe 'POST #unsensitive' do + subject { post :unsensitive, params: { id: account.id } } + + let(:current_user) { Fabricate(:user, role: role) } + let(:account) { Fabricate(:account, sensitized_at: 1.year.ago) } + + context 'when user is admin' do + let(:role) { UserRole.find_by(name: 'Admin') } + + it 'marks accounts not sensitized' do + subject + + expect(account.reload).to_not be_sensitized + expect(response).to redirect_to admin_account_path(account.id) + end + end + + context 'when user is not admin' do + let(:role) { UserRole.everyone } + + it 'fails to change account' do + subject + + expect(response).to have_http_status 403 + end + end + end + + describe 'POST #unsilence' do + subject { post :unsilence, params: { id: account.id } } + + let(:current_user) { Fabricate(:user, role: role) } + let(:account) { Fabricate(:account, silenced_at: 1.year.ago) } + + context 'when user is admin' do + let(:role) { UserRole.find_by(name: 'Admin') } + + it 'marks accounts not silenced' do + subject + + expect(account.reload).to_not be_silenced + expect(response).to redirect_to admin_account_path(account.id) + end + end + + context 'when user is not admin' do + let(:role) { UserRole.everyone } + + it 'fails to change account' do + subject + + expect(response).to have_http_status 403 + end + end + end + + describe 'POST #unsuspend' do + subject { post :unsuspend, params: { id: account.id } } + + let(:current_user) { Fabricate(:user, role: role) } + let(:account) { Fabricate(:account) } + + before do + account.suspend! + end + + context 'when user is admin' do + let(:role) { UserRole.find_by(name: 'Admin') } + + it 'marks accounts not suspended' do + subject + + expect(account.reload).to_not be_suspended + expect(response).to redirect_to admin_account_path(account.id) + end + end + + context 'when user is not admin' do + let(:role) { UserRole.everyone } + + it 'fails to change account' do + subject + + expect(response).to have_http_status 403 + end + end + end + + describe 'POST #destroy' do + subject { post :destroy, params: { id: account.id } } + + let(:current_user) { Fabricate(:user, role: role) } + let(:account) { Fabricate(:account) } + + before do + account.suspend! + end + + context 'when user is admin' do + let(:role) { UserRole.find_by(name: 'Admin') } + + before do + allow(Admin::AccountDeletionWorker).to receive(:perform_async).with(account.id) + end + + it 'destroys the account' do + subject + + expect(Admin::AccountDeletionWorker).to have_received(:perform_async).with(account.id) + expect(response).to redirect_to admin_account_path(account.id) + end + end + + context 'when user is not admin' do + let(:role) { UserRole.everyone } + + it 'fails to change account' do + subject + + expect(response).to have_http_status 403 + end + end + end end diff --git a/spec/controllers/admin/announcements_controller_spec.rb b/spec/controllers/admin/announcements_controller_spec.rb index a8905160f..c2d3135d9 100644 --- a/spec/controllers/admin/announcements_controller_spec.rb +++ b/spec/controllers/admin/announcements_controller_spec.rb @@ -73,4 +73,30 @@ describe Admin::AnnouncementsController do expect(flash.notice).to match(I18n.t('admin.announcements.destroyed_msg')) end end + + describe 'POST #publish' do + subject { post :publish, params: { id: announcement.id } } + + let(:announcement) { Fabricate(:announcement, published_at: nil) } + + it 'marks announcement published' do + subject + + expect(announcement.reload).to be_published + expect(response).to redirect_to admin_announcements_path + end + end + + describe 'POST #unpublish' do + subject { post :unpublish, params: { id: announcement.id } } + + let(:announcement) { Fabricate(:announcement, published_at: 4.days.ago) } + + it 'marks announcement as not published' do + subject + + expect(announcement.reload).to_not be_published + expect(response).to redirect_to admin_announcements_path + end + end end diff --git a/spec/controllers/admin/relays_controller_spec.rb b/spec/controllers/admin/relays_controller_spec.rb index 261f302c0..ca351c39b 100644 --- a/spec/controllers/admin/relays_controller_spec.rb +++ b/spec/controllers/admin/relays_controller_spec.rb @@ -56,4 +56,45 @@ describe Admin::RelaysController do end end end + + describe 'DELETE #destroy' do + let(:relay) { Fabricate(:relay) } + + it 'deletes an existing relay' do + delete :destroy, params: { id: relay.id } + + expect { relay.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect(response).to redirect_to(admin_relays_path) + end + end + + describe 'POST #enable' do + let(:relay) { Fabricate(:relay, state: :idle) } + + before do + stub_request(:post, /example.com/).to_return(status: 200) + end + + it 'updates a relay from idle to pending' do + post :enable, params: { id: relay.id } + + expect(relay.reload).to be_pending + expect(response).to redirect_to(admin_relays_path) + end + end + + describe 'POST #disable' do + let(:relay) { Fabricate(:relay, state: :pending) } + + before do + stub_request(:post, /example.com/).to_return(status: 200) + end + + it 'updates a relay from pending to idle' do + post :disable, params: { id: relay.id } + + expect(relay.reload).to be_idle + expect(response).to redirect_to(admin_relays_path) + end + end end diff --git a/spec/controllers/admin/statuses_controller_spec.rb b/spec/controllers/admin/statuses_controller_spec.rb index 872aed999..fc27f7147 100644 --- a/spec/controllers/admin/statuses_controller_spec.rb +++ b/spec/controllers/admin/statuses_controller_spec.rb @@ -41,6 +41,16 @@ describe Admin::StatusesController do end end + describe 'GET #show' do + before do + get :show, params: { account_id: account.id, id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + describe 'POST #batch' do before do post :batch, params: { :account_id => account.id, action => '', :admin_status_batch_action => { status_ids: status_ids } } diff --git a/spec/controllers/admin/warning_presets_controller_spec.rb b/spec/controllers/admin/warning_presets_controller_spec.rb index 6b48fc28b..b32a58e99 100644 --- a/spec/controllers/admin/warning_presets_controller_spec.rb +++ b/spec/controllers/admin/warning_presets_controller_spec.rb @@ -18,4 +18,68 @@ describe Admin::WarningPresetsController do expect(response).to have_http_status(:success) end end + + describe 'GET #edit' do + let(:account_warning_preset) { Fabricate(:account_warning_preset) } + + it 'returns http success and renders edit' do + get :edit, params: { id: account_warning_preset.id } + + expect(response).to have_http_status(:success) + expect(response).to render_template(:edit) + end + end + + describe 'POST #create' do + context 'with valid data' do + it 'creates a new account_warning_preset and redirects' do + expect do + post :create, params: { account_warning_preset: { text: 'The account_warning_preset text.' } } + end.to change(AccountWarningPreset, :count).by(1) + + expect(response).to redirect_to(admin_warning_presets_path) + end + end + + context 'with invalid data' do + it 'does creates a new account_warning_preset and renders index' do + expect do + post :create, params: { account_warning_preset: { text: '' } } + end.to_not change(AccountWarningPreset, :count) + + expect(response).to render_template(:index) + end + end + end + + describe 'PUT #update' do + let(:account_warning_preset) { Fabricate(:account_warning_preset, text: 'Original text') } + + context 'with valid data' do + it 'updates the account_warning_preset and redirects' do + put :update, params: { id: account_warning_preset.id, account_warning_preset: { text: 'Updated text.' } } + + expect(response).to redirect_to(admin_warning_presets_path) + end + end + + context 'with invalid data' do + it 'does not update the account_warning_preset and renders index' do + put :update, params: { id: account_warning_preset.id, account_warning_preset: { text: '' } } + + expect(response).to render_template(:edit) + end + end + end + + describe 'DELETE #destroy' do + let!(:account_warning_preset) { Fabricate(:account_warning_preset) } + + it 'destroys the account_warning_preset and redirects' do + delete :destroy, params: { id: account_warning_preset.id } + + expect { account_warning_preset.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect(response).to redirect_to(admin_warning_presets_path) + end + end end From c42591356da45c52f215ea00cf5c0ecd753afe06 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 6 Jun 2023 07:58:33 -0400 Subject: [PATCH 55/92] Fix `RSpec/DescribedClass` cop (#25104) --- .rubocop_todo.yml | 73 --------- spec/controllers/.rubocop.yml | 6 + spec/lib/entity_cache_spec.rb | 2 +- spec/lib/extractor_spec.rb | 20 +-- spec/lib/feed_manager_spec.rb | 140 +++++++++--------- spec/lib/ostatus/tag_manager_spec.rb | 16 +- spec/lib/request_spec.rb | 2 +- spec/lib/tag_manager_spec.rb | 22 +-- spec/lib/webfinger_resource_spec.rb | 28 ++-- spec/mailers/notification_mailer_spec.rb | 10 +- spec/mailers/user_mailer_spec.rb | 20 +-- spec/models/account_conversation_spec.rb | 16 +- spec/models/account_domain_block_spec.rb | 4 +- spec/models/account_migration_spec.rb | 2 +- spec/models/account_spec.rb | 90 +++++------ spec/models/block_spec.rb | 4 +- spec/models/domain_block_spec.rb | 36 ++--- spec/models/email_domain_block_spec.rb | 6 +- spec/models/export_spec.rb | 14 +- spec/models/favourite_spec.rb | 8 +- spec/models/follow_spec.rb | 8 +- spec/models/identity_spec.rb | 2 +- spec/models/import_spec.rb | 6 +- spec/models/media_attachment_spec.rb | 20 +-- spec/models/notification_spec.rb | 8 +- spec/models/relationship_filter_spec.rb | 2 +- spec/models/report_filter_spec.rb | 6 +- spec/models/session_activation_spec.rb | 2 +- spec/models/setting_spec.rb | 2 +- spec/models/site_upload_spec.rb | 2 +- spec/models/status_pin_spec.rb | 18 +-- spec/models/status_spec.rb | 54 +++---- spec/models/user_spec.rb | 32 ++-- .../account_moderation_note_policy_spec.rb | 4 +- .../account_relationships_presenter_spec.rb | 2 +- .../status_relationships_presenter_spec.rb | 2 +- .../activitypub/note_serializer_spec.rb | 2 +- .../update_poll_serializer_spec.rb | 2 +- .../rest/account_serializer_spec.rb | 2 +- .../fetch_remote_account_service_spec.rb | 2 +- .../fetch_remote_actor_service_spec.rb | 2 +- .../fetch_remote_key_service_spec.rb | 2 +- ..._block_domain_from_account_service_spec.rb | 2 +- .../services/authorize_follow_service_spec.rb | 2 +- .../batched_remove_status_service_spec.rb | 2 +- spec/services/block_domain_service_spec.rb | 2 +- spec/services/block_service_spec.rb | 2 +- .../bootstrap_timeline_service_spec.rb | 2 +- .../clear_domain_media_service_spec.rb | 2 +- spec/services/favourite_service_spec.rb | 2 +- spec/services/follow_service_spec.rb | 2 +- spec/services/import_service_spec.rb | 12 +- spec/services/post_status_service_spec.rb | 2 +- spec/services/precompute_feed_service_spec.rb | 2 +- .../services/process_mentions_service_spec.rb | 2 +- spec/services/purge_domain_service_spec.rb | 2 +- spec/services/reblog_service_spec.rb | 4 +- spec/services/reject_follow_service_spec.rb | 2 +- .../remove_from_followers_service_spec.rb | 2 +- spec/services/remove_status_service_spec.rb | 2 +- spec/services/unallow_domain_service_spec.rb | 2 +- spec/services/unblock_service_spec.rb | 2 +- spec/services/unfollow_service_spec.rb | 2 +- spec/services/unmute_service_spec.rb | 2 +- spec/services/update_account_service_spec.rb | 2 +- spec/validators/note_length_validator_spec.rb | 2 +- 66 files changed, 347 insertions(+), 414 deletions(-) create mode 100644 spec/controllers/.rubocop.yml diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index c8b287f90..1a16472bd 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -237,79 +237,6 @@ RSpec/AnyInstance: - 'spec/workers/activitypub/delivery_worker_spec.rb' - 'spec/workers/web/push_notification_worker_spec.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: SkipBlocks, EnforcedStyle. -# SupportedStyles: described_class, explicit -RSpec/DescribedClass: - Exclude: - - 'spec/controllers/concerns/cache_concern_spec.rb' - - 'spec/controllers/concerns/challengable_concern_spec.rb' - - 'spec/lib/entity_cache_spec.rb' - - 'spec/lib/extractor_spec.rb' - - 'spec/lib/feed_manager_spec.rb' - - 'spec/lib/hash_object_spec.rb' - - 'spec/lib/ostatus/tag_manager_spec.rb' - - 'spec/lib/request_spec.rb' - - 'spec/lib/tag_manager_spec.rb' - - 'spec/lib/webfinger_resource_spec.rb' - - 'spec/mailers/notification_mailer_spec.rb' - - 'spec/mailers/user_mailer_spec.rb' - - 'spec/models/account_conversation_spec.rb' - - 'spec/models/account_domain_block_spec.rb' - - 'spec/models/account_migration_spec.rb' - - 'spec/models/account_spec.rb' - - 'spec/models/block_spec.rb' - - 'spec/models/domain_block_spec.rb' - - 'spec/models/email_domain_block_spec.rb' - - 'spec/models/export_spec.rb' - - 'spec/models/favourite_spec.rb' - - 'spec/models/follow_spec.rb' - - 'spec/models/identity_spec.rb' - - 'spec/models/import_spec.rb' - - 'spec/models/media_attachment_spec.rb' - - 'spec/models/notification_spec.rb' - - 'spec/models/relationship_filter_spec.rb' - - 'spec/models/report_filter_spec.rb' - - 'spec/models/session_activation_spec.rb' - - 'spec/models/setting_spec.rb' - - 'spec/models/site_upload_spec.rb' - - 'spec/models/status_pin_spec.rb' - - 'spec/models/status_spec.rb' - - 'spec/models/user_spec.rb' - - 'spec/policies/account_moderation_note_policy_spec.rb' - - 'spec/presenters/account_relationships_presenter_spec.rb' - - 'spec/presenters/status_relationships_presenter_spec.rb' - - 'spec/serializers/activitypub/note_serializer_spec.rb' - - 'spec/serializers/activitypub/update_poll_serializer_spec.rb' - - 'spec/serializers/rest/account_serializer_spec.rb' - - 'spec/services/activitypub/fetch_remote_account_service_spec.rb' - - 'spec/services/activitypub/fetch_remote_actor_service_spec.rb' - - 'spec/services/activitypub/fetch_remote_key_service_spec.rb' - - 'spec/services/after_block_domain_from_account_service_spec.rb' - - 'spec/services/authorize_follow_service_spec.rb' - - 'spec/services/batched_remove_status_service_spec.rb' - - 'spec/services/block_domain_service_spec.rb' - - 'spec/services/block_service_spec.rb' - - 'spec/services/bootstrap_timeline_service_spec.rb' - - 'spec/services/clear_domain_media_service_spec.rb' - - 'spec/services/favourite_service_spec.rb' - - 'spec/services/follow_service_spec.rb' - - 'spec/services/import_service_spec.rb' - - 'spec/services/post_status_service_spec.rb' - - 'spec/services/precompute_feed_service_spec.rb' - - 'spec/services/process_mentions_service_spec.rb' - - 'spec/services/purge_domain_service_spec.rb' - - 'spec/services/reblog_service_spec.rb' - - 'spec/services/reject_follow_service_spec.rb' - - 'spec/services/remove_from_followers_service_spec.rb' - - 'spec/services/remove_status_service_spec.rb' - - 'spec/services/unallow_domain_service_spec.rb' - - 'spec/services/unblock_service_spec.rb' - - 'spec/services/unfollow_service_spec.rb' - - 'spec/services/unmute_service_spec.rb' - - 'spec/services/update_account_service_spec.rb' - - 'spec/validators/note_length_validator_spec.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). RSpec/EmptyExampleGroup: Exclude: diff --git a/spec/controllers/.rubocop.yml b/spec/controllers/.rubocop.yml new file mode 100644 index 000000000..525479be8 --- /dev/null +++ b/spec/controllers/.rubocop.yml @@ -0,0 +1,6 @@ +inherit_from: ../../.rubocop.yml + +# Anonymous controllers in specs cannot access described_class +# https://github.com/rubocop/rubocop-rspec/blob/master/lib/rubocop/cop/rspec/described_class.rb#L36-L39 +RSpec/DescribedClass: + SkipBlocks: true diff --git a/spec/lib/entity_cache_spec.rb b/spec/lib/entity_cache_spec.rb index 6d9afa474..5818de711 100644 --- a/spec/lib/entity_cache_spec.rb +++ b/spec/lib/entity_cache_spec.rb @@ -7,7 +7,7 @@ RSpec.describe EntityCache do let(:remote_account) { Fabricate(:account, domain: 'remote.test', username: 'bob', url: 'https://remote.test/') } describe '#emoji' do - subject { EntityCache.instance.emoji(shortcodes, domain) } + subject { described_class.instance.emoji(shortcodes, domain) } context 'when called with an empty list of shortcodes' do let(:shortcodes) { [] } diff --git a/spec/lib/extractor_spec.rb b/spec/lib/extractor_spec.rb index 560617ed7..b6c910171 100644 --- a/spec/lib/extractor_spec.rb +++ b/spec/lib/extractor_spec.rb @@ -6,19 +6,19 @@ describe Extractor do describe 'extract_mentions_or_lists_with_indices' do it 'returns an empty array if the given string does not have at signs' do text = 'a string without at signs' - extracted = Extractor.extract_mentions_or_lists_with_indices(text) + extracted = described_class.extract_mentions_or_lists_with_indices(text) expect(extracted).to eq [] end it 'does not extract mentions which ends with particular characters' do text = '@screen_name@' - extracted = Extractor.extract_mentions_or_lists_with_indices(text) + extracted = described_class.extract_mentions_or_lists_with_indices(text) expect(extracted).to eq [] end it 'returns mentions as an array' do text = '@screen_name' - extracted = Extractor.extract_mentions_or_lists_with_indices(text) + extracted = described_class.extract_mentions_or_lists_with_indices(text) expect(extracted).to eq [ { screen_name: 'screen_name', indices: [0, 12] }, ] @@ -26,7 +26,7 @@ describe Extractor do it 'yields mentions if a block is given' do text = '@screen_name' - Extractor.extract_mentions_or_lists_with_indices(text) do |screen_name, start_position, end_position| + described_class.extract_mentions_or_lists_with_indices(text) do |screen_name, start_position, end_position| expect(screen_name).to eq 'screen_name' expect(start_position).to eq 0 expect(end_position).to eq 12 @@ -37,31 +37,31 @@ describe Extractor do describe 'extract_hashtags_with_indices' do it 'returns an empty array if it does not have #' do text = 'a string without hash sign' - extracted = Extractor.extract_hashtags_with_indices(text) + extracted = described_class.extract_hashtags_with_indices(text) expect(extracted).to eq [] end it 'does not exclude normal hash text before ://' do text = '#hashtag://' - extracted = Extractor.extract_hashtags_with_indices(text) + extracted = described_class.extract_hashtags_with_indices(text) expect(extracted).to eq [{ hashtag: 'hashtag', indices: [0, 8] }] end it 'excludes http://' do text = '#hashtaghttp://' - extracted = Extractor.extract_hashtags_with_indices(text) + extracted = described_class.extract_hashtags_with_indices(text) expect(extracted).to eq [{ hashtag: 'hashtag', indices: [0, 8] }] end it 'excludes https://' do text = '#hashtaghttps://' - extracted = Extractor.extract_hashtags_with_indices(text) + extracted = described_class.extract_hashtags_with_indices(text) expect(extracted).to eq [{ hashtag: 'hashtag', indices: [0, 8] }] end it 'yields hashtags if a block is given' do text = '#hashtag' - Extractor.extract_hashtags_with_indices(text) do |hashtag, start_position, end_position| + described_class.extract_hashtags_with_indices(text) do |hashtag, start_position, end_position| expect(hashtag).to eq 'hashtag' expect(start_position).to eq 0 expect(end_position).to eq 8 @@ -72,7 +72,7 @@ describe Extractor do describe 'extract_cashtags_with_indices' do it 'returns []' do text = '$cashtag' - extracted = Extractor.extract_cashtags_with_indices(text) + extracted = described_class.extract_cashtags_with_indices(text) expect(extracted).to eq [] end end diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index 31b53fd87..5bfe11f6a 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -15,7 +15,7 @@ RSpec.describe FeedManager do end describe '#key' do - subject { FeedManager.instance.key(:home, 1) } + subject { described_class.instance.key(:home, 1) } it 'returns a string' do expect(subject).to be_a String @@ -32,26 +32,26 @@ RSpec.describe FeedManager do it 'returns false for followee\'s status' do status = Fabricate(:status, text: 'Hello world', account: alice) bob.follow!(alice) - expect(FeedManager.instance.filter?(:home, status, bob)).to be false + expect(described_class.instance.filter?(:home, status, bob)).to be false end it 'returns false for reblog by followee' do status = Fabricate(:status, text: 'Hello world', account: jeff) reblog = Fabricate(:status, reblog: status, account: alice) bob.follow!(alice) - expect(FeedManager.instance.filter?(:home, reblog, bob)).to be false + expect(described_class.instance.filter?(:home, reblog, bob)).to be false end it 'returns true for post from account who blocked me' do status = Fabricate(:status, text: 'Hello, World', account: alice) alice.block!(bob) - expect(FeedManager.instance.filter?(:home, status, bob)).to be true + expect(described_class.instance.filter?(:home, status, bob)).to be true end it 'returns true for post from blocked account' do status = Fabricate(:status, text: 'Hello, World', account: alice) bob.block!(alice) - expect(FeedManager.instance.filter?(:home, status, bob)).to be true + expect(described_class.instance.filter?(:home, status, bob)).to be true end it 'returns true for reblog by followee of blocked account' do @@ -59,7 +59,7 @@ RSpec.describe FeedManager do reblog = Fabricate(:status, reblog: status, account: alice) bob.follow!(alice) bob.block!(jeff) - expect(FeedManager.instance.filter?(:home, reblog, bob)).to be true + expect(described_class.instance.filter?(:home, reblog, bob)).to be true end it 'returns true for reblog by followee of muted account' do @@ -67,7 +67,7 @@ RSpec.describe FeedManager do reblog = Fabricate(:status, reblog: status, account: alice) bob.follow!(alice) bob.mute!(jeff) - expect(FeedManager.instance.filter?(:home, reblog, bob)).to be true + expect(described_class.instance.filter?(:home, reblog, bob)).to be true end it 'returns true for reblog by followee of someone who is blocking recipient' do @@ -75,14 +75,14 @@ RSpec.describe FeedManager do reblog = Fabricate(:status, reblog: status, account: alice) bob.follow!(alice) jeff.block!(bob) - expect(FeedManager.instance.filter?(:home, reblog, bob)).to be true + expect(described_class.instance.filter?(:home, reblog, bob)).to be true end it 'returns true for reblog from account with reblogs disabled' do status = Fabricate(:status, text: 'Hello world', account: jeff) reblog = Fabricate(:status, reblog: status, account: alice) bob.follow!(alice, reblogs: false) - expect(FeedManager.instance.filter?(:home, reblog, bob)).to be true + expect(described_class.instance.filter?(:home, reblog, bob)).to be true end it 'returns false for reply by followee to another followee' do @@ -90,49 +90,49 @@ RSpec.describe FeedManager do reply = Fabricate(:status, text: 'Nay', thread: status, account: alice) bob.follow!(alice) bob.follow!(jeff) - expect(FeedManager.instance.filter?(:home, reply, bob)).to be false + expect(described_class.instance.filter?(:home, reply, bob)).to be false end it 'returns false for reply by followee to recipient' do status = Fabricate(:status, text: 'Hello world', account: bob) reply = Fabricate(:status, text: 'Nay', thread: status, account: alice) bob.follow!(alice) - expect(FeedManager.instance.filter?(:home, reply, bob)).to be false + expect(described_class.instance.filter?(:home, reply, bob)).to be false end it 'returns false for reply by followee to self' do status = Fabricate(:status, text: 'Hello world', account: alice) reply = Fabricate(:status, text: 'Nay', thread: status, account: alice) bob.follow!(alice) - expect(FeedManager.instance.filter?(:home, reply, bob)).to be false + expect(described_class.instance.filter?(:home, reply, bob)).to be false end it 'returns true for reply by followee to non-followed account' do status = Fabricate(:status, text: 'Hello world', account: jeff) reply = Fabricate(:status, text: 'Nay', thread: status, account: alice) bob.follow!(alice) - expect(FeedManager.instance.filter?(:home, reply, bob)).to be true + expect(described_class.instance.filter?(:home, reply, bob)).to be true end it 'returns true for the second reply by followee to a non-federated status' do reply = Fabricate(:status, text: 'Reply 1', reply: true, account: alice) second_reply = Fabricate(:status, text: 'Reply 2', thread: reply, account: alice) bob.follow!(alice) - expect(FeedManager.instance.filter?(:home, second_reply, bob)).to be true + expect(described_class.instance.filter?(:home, second_reply, bob)).to be true end it 'returns false for status by followee mentioning another account' do bob.follow!(alice) jeff.follow!(alice) status = PostStatusService.new.call(alice, text: 'Hey @jeff') - expect(FeedManager.instance.filter?(:home, status, bob)).to be false + expect(described_class.instance.filter?(:home, status, bob)).to be false end it 'returns true for status by followee mentioning blocked account' do bob.block!(jeff) bob.follow!(alice) status = PostStatusService.new.call(alice, text: 'Hey @jeff') - expect(FeedManager.instance.filter?(:home, status, bob)).to be true + expect(described_class.instance.filter?(:home, status, bob)).to be true end it 'returns true for reblog of a personally blocked domain' do @@ -140,19 +140,19 @@ RSpec.describe FeedManager do alice.follow!(jeff) status = Fabricate(:status, text: 'Hello world', account: bob) reblog = Fabricate(:status, reblog: status, account: jeff) - expect(FeedManager.instance.filter?(:home, reblog, alice)).to be true + expect(described_class.instance.filter?(:home, reblog, alice)).to be true end it 'returns true for German post when follow is set to English only' do alice.follow!(bob, languages: %w(en)) status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de') - expect(FeedManager.instance.filter?(:home, status, alice)).to be true + expect(described_class.instance.filter?(:home, status, alice)).to be true end it 'returns false for German post when follow is set to German' do alice.follow!(bob, languages: %w(de)) status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de') - expect(FeedManager.instance.filter?(:home, status, alice)).to be false + expect(described_class.instance.filter?(:home, status, alice)).to be false end it 'returns true for post from followee on exclusive list' do @@ -196,27 +196,27 @@ RSpec.describe FeedManager do it 'returns true for status that mentions blocked account' do bob.block!(jeff) status = PostStatusService.new.call(alice, text: 'Hey @jeff') - expect(FeedManager.instance.filter?(:mentions, status, bob)).to be true + expect(described_class.instance.filter?(:mentions, status, bob)).to be true end it 'returns true for status that replies to a blocked account' do status = Fabricate(:status, text: 'Hello world', account: jeff) reply = Fabricate(:status, text: 'Nay', thread: status, account: alice) bob.block!(jeff) - expect(FeedManager.instance.filter?(:mentions, reply, bob)).to be true + expect(described_class.instance.filter?(:mentions, reply, bob)).to be true end it 'returns true for status by silenced account who recipient is not following' do status = Fabricate(:status, text: 'Hello world', account: alice) alice.silence! - expect(FeedManager.instance.filter?(:mentions, status, bob)).to be true + expect(described_class.instance.filter?(:mentions, status, bob)).to be true end it 'returns false for status by followed silenced account' do status = Fabricate(:status, text: 'Hello world', account: alice) alice.silence! bob.follow!(alice) - expect(FeedManager.instance.filter?(:mentions, status, bob)).to be false + expect(described_class.instance.filter?(:mentions, status, bob)).to be false end end end @@ -228,7 +228,7 @@ RSpec.describe FeedManager do members = Array.new(FeedManager::MAX_ITEMS) { |count| [count, count] } redis.zadd("feed:home:#{account.id}", members) - FeedManager.instance.push_to_home(account, status) + described_class.instance.push_to_home(account, status) expect(redis.zcard("feed:home:#{account.id}")).to eq FeedManager::MAX_ITEMS end @@ -239,7 +239,7 @@ RSpec.describe FeedManager do reblogged = Fabricate(:status) reblog = Fabricate(:status, reblog: reblogged) - expect(FeedManager.instance.push_to_home(account, reblog)).to be true + expect(described_class.instance.push_to_home(account, reblog)).to be true end it 'does not save a new reblog of a recent status' do @@ -247,9 +247,9 @@ RSpec.describe FeedManager do reblogged = Fabricate(:status) reblog = Fabricate(:status, reblog: reblogged) - FeedManager.instance.push_to_home(account, reblogged) + described_class.instance.push_to_home(account, reblogged) - expect(FeedManager.instance.push_to_home(account, reblog)).to be false + expect(described_class.instance.push_to_home(account, reblog)).to be false end it 'saves a new reblog of an old status' do @@ -257,14 +257,14 @@ RSpec.describe FeedManager do reblogged = Fabricate(:status) reblog = Fabricate(:status, reblog: reblogged) - FeedManager.instance.push_to_home(account, reblogged) + described_class.instance.push_to_home(account, reblogged) # Fill the feed with intervening statuses FeedManager::REBLOG_FALLOFF.times do - FeedManager.instance.push_to_home(account, Fabricate(:status)) + described_class.instance.push_to_home(account, Fabricate(:status)) end - expect(FeedManager.instance.push_to_home(account, reblog)).to be true + expect(described_class.instance.push_to_home(account, reblog)).to be true end it 'does not save a new reblog of a recently-reblogged status' do @@ -273,10 +273,10 @@ RSpec.describe FeedManager do reblogs = Array.new(2) { Fabricate(:status, reblog: reblogged) } # The first reblog will be accepted - FeedManager.instance.push_to_home(account, reblogs.first) + described_class.instance.push_to_home(account, reblogs.first) # The second reblog should be ignored - expect(FeedManager.instance.push_to_home(account, reblogs.last)).to be false + expect(described_class.instance.push_to_home(account, reblogs.last)).to be false end it 'saves a new reblog of a recently-reblogged status when previous reblog has been deleted' do @@ -285,15 +285,15 @@ RSpec.describe FeedManager do old_reblog = Fabricate(:status, reblog: reblogged) # The first reblog should be accepted - expect(FeedManager.instance.push_to_home(account, old_reblog)).to be true + expect(described_class.instance.push_to_home(account, old_reblog)).to be true # The first reblog should be successfully removed - expect(FeedManager.instance.unpush_from_home(account, old_reblog)).to be true + expect(described_class.instance.unpush_from_home(account, old_reblog)).to be true reblog = Fabricate(:status, reblog: reblogged) # The second reblog should be accepted - expect(FeedManager.instance.push_to_home(account, reblog)).to be true + expect(described_class.instance.push_to_home(account, reblog)).to be true end it 'does not save a new reblog of a multiply-reblogged-then-unreblogged status' do @@ -302,14 +302,14 @@ RSpec.describe FeedManager do reblogs = Array.new(3) { Fabricate(:status, reblog: reblogged) } # Accept the reblogs - FeedManager.instance.push_to_home(account, reblogs[0]) - FeedManager.instance.push_to_home(account, reblogs[1]) + described_class.instance.push_to_home(account, reblogs[0]) + described_class.instance.push_to_home(account, reblogs[1]) # Unreblog the first one - FeedManager.instance.unpush_from_home(account, reblogs[0]) + described_class.instance.unpush_from_home(account, reblogs[0]) # The last reblog should still be ignored - expect(FeedManager.instance.push_to_home(account, reblogs.last)).to be false + expect(described_class.instance.push_to_home(account, reblogs.last)).to be false end it 'saves a new reblog of a long-ago-reblogged status' do @@ -318,15 +318,15 @@ RSpec.describe FeedManager do reblogs = Array.new(2) { Fabricate(:status, reblog: reblogged) } # The first reblog will be accepted - FeedManager.instance.push_to_home(account, reblogs.first) + described_class.instance.push_to_home(account, reblogs.first) # Fill the feed with intervening statuses FeedManager::REBLOG_FALLOFF.times do - FeedManager.instance.push_to_home(account, Fabricate(:status)) + described_class.instance.push_to_home(account, Fabricate(:status)) end # The second reblog should also be accepted - expect(FeedManager.instance.push_to_home(account, reblogs.last)).to be true + expect(described_class.instance.push_to_home(account, reblogs.last)).to be true end end @@ -334,9 +334,9 @@ RSpec.describe FeedManager do account = Fabricate(:account) reblog = Fabricate(:status) status = Fabricate(:status, reblog: reblog) - FeedManager.instance.push_to_home(account, status) + described_class.instance.push_to_home(account, status) - expect(FeedManager.instance.push_to_home(account, reblog)).to be false + expect(described_class.instance.push_to_home(account, reblog)).to be false end end @@ -359,9 +359,9 @@ RSpec.describe FeedManager do it "does not push when the given status's reblog is already inserted" do reblog = Fabricate(:status) status = Fabricate(:status, reblog: reblog) - FeedManager.instance.push_to_list(list, status) + described_class.instance.push_to_list(list, status) - expect(FeedManager.instance.push_to_list(list, reblog)).to be false + expect(described_class.instance.push_to_list(list, reblog)).to be false end context 'when replies policy is set to no replies' do @@ -371,19 +371,19 @@ RSpec.describe FeedManager do it 'pushes statuses that are not replies' do status = Fabricate(:status, text: 'Hello world', account: bob) - expect(FeedManager.instance.push_to_list(list, status)).to be true + expect(described_class.instance.push_to_list(list, status)).to be true end it 'pushes statuses that are replies to list owner' do status = Fabricate(:status, text: 'Hello world', account: owner) reply = Fabricate(:status, text: 'Nay', thread: status, account: bob) - expect(FeedManager.instance.push_to_list(list, reply)).to be true + expect(described_class.instance.push_to_list(list, reply)).to be true end it 'does not push replies to another member of the list' do status = Fabricate(:status, text: 'Hello world', account: alice) reply = Fabricate(:status, text: 'Nay', thread: status, account: bob) - expect(FeedManager.instance.push_to_list(list, reply)).to be false + expect(described_class.instance.push_to_list(list, reply)).to be false end end @@ -394,25 +394,25 @@ RSpec.describe FeedManager do it 'pushes statuses that are not replies' do status = Fabricate(:status, text: 'Hello world', account: bob) - expect(FeedManager.instance.push_to_list(list, status)).to be true + expect(described_class.instance.push_to_list(list, status)).to be true end it 'pushes statuses that are replies to list owner' do status = Fabricate(:status, text: 'Hello world', account: owner) reply = Fabricate(:status, text: 'Nay', thread: status, account: bob) - expect(FeedManager.instance.push_to_list(list, reply)).to be true + expect(described_class.instance.push_to_list(list, reply)).to be true end it 'pushes replies to another member of the list' do status = Fabricate(:status, text: 'Hello world', account: alice) reply = Fabricate(:status, text: 'Nay', thread: status, account: bob) - expect(FeedManager.instance.push_to_list(list, reply)).to be true + expect(described_class.instance.push_to_list(list, reply)).to be true end it 'does not push replies to someone not a member of the list' do status = Fabricate(:status, text: 'Hello world', account: eve) reply = Fabricate(:status, text: 'Nay', thread: status, account: bob) - expect(FeedManager.instance.push_to_list(list, reply)).to be false + expect(described_class.instance.push_to_list(list, reply)).to be false end end @@ -423,25 +423,25 @@ RSpec.describe FeedManager do it 'pushes statuses that are not replies' do status = Fabricate(:status, text: 'Hello world', account: bob) - expect(FeedManager.instance.push_to_list(list, status)).to be true + expect(described_class.instance.push_to_list(list, status)).to be true end it 'pushes statuses that are replies to list owner' do status = Fabricate(:status, text: 'Hello world', account: owner) reply = Fabricate(:status, text: 'Nay', thread: status, account: bob) - expect(FeedManager.instance.push_to_list(list, reply)).to be true + expect(described_class.instance.push_to_list(list, reply)).to be true end it 'pushes replies to another member of the list' do status = Fabricate(:status, text: 'Hello world', account: alice) reply = Fabricate(:status, text: 'Nay', thread: status, account: bob) - expect(FeedManager.instance.push_to_list(list, reply)).to be true + expect(described_class.instance.push_to_list(list, reply)).to be true end it 'pushes replies to someone not a member of the list' do status = Fabricate(:status, text: 'Hello world', account: eve) reply = Fabricate(:status, text: 'Nay', thread: status, account: bob) - expect(FeedManager.instance.push_to_list(list, reply)).to be true + expect(described_class.instance.push_to_list(list, reply)).to be true end end end @@ -451,9 +451,9 @@ RSpec.describe FeedManager do account = Fabricate(:account, id: 0) reblog = Fabricate(:status) status = Fabricate(:status, reblog: reblog) - FeedManager.instance.push_to_home(account, status) + described_class.instance.push_to_home(account, status) - FeedManager.instance.merge_into_home(account, reblog.account) + described_class.instance.merge_into_home(account, reblog.account) expect(redis.zscore('feed:home:0', reblog.id)).to be_nil end @@ -466,14 +466,14 @@ RSpec.describe FeedManager do reblogged = Fabricate(:status) status = Fabricate(:status, reblog: reblogged) - FeedManager.instance.push_to_home(receiver, reblogged) - FeedManager::REBLOG_FALLOFF.times { FeedManager.instance.push_to_home(receiver, Fabricate(:status)) } - FeedManager.instance.push_to_home(receiver, status) + described_class.instance.push_to_home(receiver, reblogged) + FeedManager::REBLOG_FALLOFF.times { described_class.instance.push_to_home(receiver, Fabricate(:status)) } + described_class.instance.push_to_home(receiver, status) # The reblogging status should show up under normal conditions. expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s) - FeedManager.instance.unpush_from_home(receiver, status) + described_class.instance.unpush_from_home(receiver, status) # Restore original status expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s) @@ -484,12 +484,12 @@ RSpec.describe FeedManager do reblogged = Fabricate(:status) status = Fabricate(:status, reblog: reblogged) - FeedManager.instance.push_to_home(receiver, status) + described_class.instance.push_to_home(receiver, status) # The reblogging status should show up under normal conditions. expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [status.id.to_s] - FeedManager.instance.unpush_from_home(receiver, status) + described_class.instance.unpush_from_home(receiver, status) expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to be_empty end @@ -499,14 +499,14 @@ RSpec.describe FeedManager do reblogs = Array.new(3) { Fabricate(:status, reblog: reblogged) } reblogs.each do |reblog| - FeedManager.instance.push_to_home(receiver, reblog) + described_class.instance.push_to_home(receiver, reblog) end # The reblogging status should show up under normal conditions. expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.first.id.to_s] reblogs[0...-1].each do |reblog| - FeedManager.instance.unpush_from_home(receiver, reblog) + described_class.instance.unpush_from_home(receiver, reblog) end expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.last.id.to_s] @@ -515,10 +515,10 @@ RSpec.describe FeedManager do it 'sends push updates' do status = Fabricate(:status) - FeedManager.instance.push_to_home(receiver, status) + described_class.instance.push_to_home(receiver, status) allow(redis).to receive_messages(publish: nil) - FeedManager.instance.unpush_from_home(receiver, status) + described_class.instance.unpush_from_home(receiver, status) deletion = Oj.dump(event: :delete, payload: status.id.to_s) expect(redis).to have_received(:publish).with("timeline:#{receiver.id}", deletion) @@ -544,7 +544,7 @@ RSpec.describe FeedManager do end it 'correctly cleans the home timeline' do - FeedManager.instance.clear_from_home(account, target_account) + described_class.instance.clear_from_home(account, target_account) expect(redis.zrange("feed:home:#{account.id}", 0, -1)).to eq [status_1.id.to_s, status_7.id.to_s] end diff --git a/spec/lib/ostatus/tag_manager_spec.rb b/spec/lib/ostatus/tag_manager_spec.rb index fb9740ce3..0e20f26c7 100644 --- a/spec/lib/ostatus/tag_manager_spec.rb +++ b/spec/lib/ostatus/tag_manager_spec.rb @@ -5,40 +5,40 @@ require 'rails_helper' describe OStatus::TagManager do describe '#unique_tag' do it 'returns a unique tag' do - expect(OStatus::TagManager.instance.unique_tag(Time.utc(2000), 12, 'Status')).to eq 'tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status' + expect(described_class.instance.unique_tag(Time.utc(2000), 12, 'Status')).to eq 'tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status' end end describe '#unique_tag_to_local_id' do it 'returns the ID part' do - expect(OStatus::TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status', 'Status')).to eql '12' + expect(described_class.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Status', 'Status')).to eql '12' end it 'returns nil if it is not local id' do - expect(OStatus::TagManager.instance.unique_tag_to_local_id('tag:remote,2000-01-01:objectId=12:objectType=Status', 'Status')).to be_nil + expect(described_class.instance.unique_tag_to_local_id('tag:remote,2000-01-01:objectId=12:objectType=Status', 'Status')).to be_nil end it 'returns nil if it is not expected type' do - expect(OStatus::TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Block', 'Status')).to be_nil + expect(described_class.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectId=12:objectType=Block', 'Status')).to be_nil end it 'returns nil if it does not have object ID' do - expect(OStatus::TagManager.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectType=Status', 'Status')).to be_nil + expect(described_class.instance.unique_tag_to_local_id('tag:cb6e6126.ngrok.io,2000-01-01:objectType=Status', 'Status')).to be_nil end end describe '#local_id?' do it 'returns true for a local ID' do - expect(OStatus::TagManager.instance.local_id?('tag:cb6e6126.ngrok.io;objectId=12:objectType=Status')).to be true + expect(described_class.instance.local_id?('tag:cb6e6126.ngrok.io;objectId=12:objectType=Status')).to be true end it 'returns false for a foreign ID' do - expect(OStatus::TagManager.instance.local_id?('tag:foreign.tld;objectId=12:objectType=Status')).to be false + expect(described_class.instance.local_id?('tag:foreign.tld;objectId=12:objectType=Status')).to be false end end describe '#uri_for' do - subject { OStatus::TagManager.instance.uri_for(target) } + subject { described_class.instance.uri_for(target) } context 'with comment object' do let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: true) } diff --git a/spec/lib/request_spec.rb b/spec/lib/request_spec.rb index 25fe9ed37..e88631e47 100644 --- a/spec/lib/request_spec.rb +++ b/spec/lib/request_spec.rb @@ -4,7 +4,7 @@ require 'rails_helper' require 'securerandom' describe Request do - subject { Request.new(:get, 'http://example.com') } + subject { described_class.new(:get, 'http://example.com') } describe '#headers' do it 'returns user agent' do diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb index 8de290541..38203a55f 100644 --- a/spec/lib/tag_manager_spec.rb +++ b/spec/lib/tag_manager_spec.rb @@ -16,15 +16,15 @@ RSpec.describe TagManager do end it 'returns true for nil' do - expect(TagManager.instance.local_domain?(nil)).to be true + expect(described_class.instance.local_domain?(nil)).to be true end it 'returns true if the slash-stripped string equals to local domain' do - expect(TagManager.instance.local_domain?('DoMaIn.Example.com/')).to be true + expect(described_class.instance.local_domain?('DoMaIn.Example.com/')).to be true end it 'returns false for irrelevant string' do - expect(TagManager.instance.local_domain?('DoMaIn.Example.com!')).to be false + expect(described_class.instance.local_domain?('DoMaIn.Example.com!')).to be false end end @@ -41,25 +41,25 @@ RSpec.describe TagManager do end it 'returns true for nil' do - expect(TagManager.instance.web_domain?(nil)).to be true + expect(described_class.instance.web_domain?(nil)).to be true end it 'returns true if the slash-stripped string equals to web domain' do - expect(TagManager.instance.web_domain?('DoMaIn.Example.com/')).to be true + expect(described_class.instance.web_domain?('DoMaIn.Example.com/')).to be true end it 'returns false for string with irrelevant characters' do - expect(TagManager.instance.web_domain?('DoMaIn.Example.com!')).to be false + expect(described_class.instance.web_domain?('DoMaIn.Example.com!')).to be false end end describe '#normalize_domain' do it 'returns nil if the given parameter is nil' do - expect(TagManager.instance.normalize_domain(nil)).to be_nil + expect(described_class.instance.normalize_domain(nil)).to be_nil end it 'returns normalized domain' do - expect(TagManager.instance.normalize_domain('DoMaIn.Example.com/')).to eq 'domain.example.com' + expect(described_class.instance.normalize_domain('DoMaIn.Example.com/')).to eq 'domain.example.com' end end @@ -72,17 +72,17 @@ RSpec.describe TagManager do it 'returns true if the normalized string with port is local URL' do Rails.configuration.x.web_domain = 'domain.example.com:42' - expect(TagManager.instance.local_url?('https://DoMaIn.Example.com:42/')).to be true + expect(described_class.instance.local_url?('https://DoMaIn.Example.com:42/')).to be true end it 'returns true if the normalized string without port is local URL' do Rails.configuration.x.web_domain = 'domain.example.com' - expect(TagManager.instance.local_url?('https://DoMaIn.Example.com/')).to be true + expect(described_class.instance.local_url?('https://DoMaIn.Example.com/')).to be true end it 'returns false for string with irrelevant characters' do Rails.configuration.x.web_domain = 'domain.example.com' - expect(TagManager.instance.local_url?('https://domain.example.net/')).to be false + expect(described_class.instance.local_url?('https://domain.example.net/')).to be false end end end diff --git a/spec/lib/webfinger_resource_spec.rb b/spec/lib/webfinger_resource_spec.rb index 8ec6dd205..2cad04fcc 100644 --- a/spec/lib/webfinger_resource_spec.rb +++ b/spec/lib/webfinger_resource_spec.rb @@ -17,7 +17,7 @@ describe WebfingerResource do resource = 'https://example.com/users/alice/other' expect do - WebfingerResource.new(resource).username + described_class.new(resource).username end.to raise_error(ActiveRecord::RecordNotFound) end @@ -32,7 +32,7 @@ describe WebfingerResource do expect(Rails.application.routes).to receive(:recognize_path).with(resource).and_return(recognized).at_least(:once) expect do - WebfingerResource.new(resource).username + described_class.new(resource).username end.to raise_error(ActiveRecord::RecordNotFound) end @@ -40,28 +40,28 @@ describe WebfingerResource do resource = 'website for http://example.com/users/alice/other' expect do - WebfingerResource.new(resource).username + described_class.new(resource).username end.to raise_error(WebfingerResource::InvalidRequest) end it 'finds the username in a valid https route' do resource = 'https://example.com/users/alice' - result = WebfingerResource.new(resource).username + result = described_class.new(resource).username expect(result).to eq 'alice' end it 'finds the username in a mixed case http route' do resource = 'HTTp://exAMPLe.com/users/alice' - result = WebfingerResource.new(resource).username + result = described_class.new(resource).username expect(result).to eq 'alice' end it 'finds the username in a valid http route' do resource = 'http://example.com/users/alice' - result = WebfingerResource.new(resource).username + result = described_class.new(resource).username expect(result).to eq 'alice' end end @@ -71,7 +71,7 @@ describe WebfingerResource do resource = 'user@remote-host.com' expect do - WebfingerResource.new(resource).username + described_class.new(resource).username end.to raise_error(ActiveRecord::RecordNotFound) end @@ -79,7 +79,7 @@ describe WebfingerResource do Rails.configuration.x.local_domain = 'example.com' resource = 'alice@example.com' - result = WebfingerResource.new(resource).username + result = described_class.new(resource).username expect(result).to eq 'alice' end @@ -87,7 +87,7 @@ describe WebfingerResource do Rails.configuration.x.web_domain = 'example.com' resource = 'alice@example.com' - result = WebfingerResource.new(resource).username + result = described_class.new(resource).username expect(result).to eq 'alice' end end @@ -97,7 +97,7 @@ describe WebfingerResource do resource = 'acct:user@remote-host.com' expect do - WebfingerResource.new(resource).username + described_class.new(resource).username end.to raise_error(ActiveRecord::RecordNotFound) end @@ -105,7 +105,7 @@ describe WebfingerResource do resource = 'acct:user@remote-host@remote-hostess.remote.local@remote' expect do - WebfingerResource.new(resource).username + described_class.new(resource).username end.to raise_error(ActiveRecord::RecordNotFound) end @@ -113,7 +113,7 @@ describe WebfingerResource do Rails.configuration.x.local_domain = 'example.com' resource = 'acct:alice@example.com' - result = WebfingerResource.new(resource).username + result = described_class.new(resource).username expect(result).to eq 'alice' end @@ -121,7 +121,7 @@ describe WebfingerResource do Rails.configuration.x.web_domain = 'example.com' resource = 'acct:alice@example.com' - result = WebfingerResource.new(resource).username + result = described_class.new(resource).username expect(result).to eq 'alice' end end @@ -131,7 +131,7 @@ describe WebfingerResource do resource = 'df/:dfkj' expect do - WebfingerResource.new(resource).username + described_class.new(resource).username end.to raise_error(WebfingerResource::InvalidRequest) end end diff --git a/spec/mailers/notification_mailer_spec.rb b/spec/mailers/notification_mailer_spec.rb index 73c751def..bf364b625 100644 --- a/spec/mailers/notification_mailer_spec.rb +++ b/spec/mailers/notification_mailer_spec.rb @@ -23,7 +23,7 @@ RSpec.describe NotificationMailer do describe 'mention' do let(:mention) { Mention.create!(account: receiver.account, status: foreign_status) } - let(:mail) { NotificationMailer.mention(receiver.account, Notification.create!(account: receiver.account, activity: mention)) } + let(:mail) { described_class.mention(receiver.account, Notification.create!(account: receiver.account, activity: mention)) } include_examples 'localized subject', 'notification_mailer.mention.subject', name: 'bob' @@ -40,7 +40,7 @@ RSpec.describe NotificationMailer do describe 'follow' do let(:follow) { sender.follow!(receiver.account) } - let(:mail) { NotificationMailer.follow(receiver.account, Notification.create!(account: receiver.account, activity: follow)) } + let(:mail) { described_class.follow(receiver.account, Notification.create!(account: receiver.account, activity: follow)) } include_examples 'localized subject', 'notification_mailer.follow.subject', name: 'bob' @@ -56,7 +56,7 @@ RSpec.describe NotificationMailer do describe 'favourite' do let(:favourite) { Favourite.create!(account: sender, status: own_status) } - let(:mail) { NotificationMailer.favourite(own_status.account, Notification.create!(account: receiver.account, activity: favourite)) } + let(:mail) { described_class.favourite(own_status.account, Notification.create!(account: receiver.account, activity: favourite)) } include_examples 'localized subject', 'notification_mailer.favourite.subject', name: 'bob' @@ -73,7 +73,7 @@ RSpec.describe NotificationMailer do describe 'reblog' do let(:reblog) { Status.create!(account: sender, reblog: own_status) } - let(:mail) { NotificationMailer.reblog(own_status.account, Notification.create!(account: receiver.account, activity: reblog)) } + let(:mail) { described_class.reblog(own_status.account, Notification.create!(account: receiver.account, activity: reblog)) } include_examples 'localized subject', 'notification_mailer.reblog.subject', name: 'bob' @@ -90,7 +90,7 @@ RSpec.describe NotificationMailer do describe 'follow_request' do let(:follow_request) { Fabricate(:follow_request, account: sender, target_account: receiver.account) } - let(:mail) { NotificationMailer.follow_request(receiver.account, Notification.create!(account: receiver.account, activity: follow_request)) } + let(:mail) { described_class.follow_request(receiver.account, Notification.create!(account: receiver.account, activity: follow_request)) } include_examples 'localized subject', 'notification_mailer.follow_request.subject', name: 'bob' diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index a4f6c145a..702aa1c35 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -19,7 +19,7 @@ describe UserMailer do end describe 'confirmation_instructions' do - let(:mail) { UserMailer.confirmation_instructions(receiver, 'spec') } + let(:mail) { described_class.confirmation_instructions(receiver, 'spec') } it 'renders confirmation instructions' do receiver.update!(locale: nil) @@ -34,7 +34,7 @@ describe UserMailer do end describe 'reconfirmation_instructions' do - let(:mail) { UserMailer.confirmation_instructions(receiver, 'spec') } + let(:mail) { described_class.confirmation_instructions(receiver, 'spec') } it 'renders reconfirmation instructions' do receiver.update!(email: 'new-email@example.com', locale: nil) @@ -48,7 +48,7 @@ describe UserMailer do end describe 'reset_password_instructions' do - let(:mail) { UserMailer.reset_password_instructions(receiver, 'spec') } + let(:mail) { described_class.reset_password_instructions(receiver, 'spec') } it 'renders reset password instructions' do receiver.update!(locale: nil) @@ -61,7 +61,7 @@ describe UserMailer do end describe 'password_change' do - let(:mail) { UserMailer.password_change(receiver) } + let(:mail) { described_class.password_change(receiver) } it 'renders password change notification' do receiver.update!(locale: nil) @@ -73,7 +73,7 @@ describe UserMailer do end describe 'email_changed' do - let(:mail) { UserMailer.email_changed(receiver) } + let(:mail) { described_class.email_changed(receiver) } it 'renders email change notification' do receiver.update!(locale: nil) @@ -86,7 +86,7 @@ describe UserMailer do describe 'warning' do let(:strike) { Fabricate(:account_warning, target_account: receiver.account, text: 'dont worry its just the testsuite', action: 'suspend') } - let(:mail) { UserMailer.warning(receiver, strike) } + let(:mail) { described_class.warning(receiver, strike) } it 'renders warning notification' do receiver.update!(locale: nil) @@ -97,7 +97,7 @@ describe UserMailer do describe 'webauthn_credential_deleted' do let(:credential) { Fabricate(:webauthn_credential, user_id: receiver.id) } - let(:mail) { UserMailer.webauthn_credential_deleted(receiver, credential) } + let(:mail) { described_class.webauthn_credential_deleted(receiver, credential) } it 'renders webauthn credential deleted notification' do receiver.update!(locale: nil) @@ -112,7 +112,7 @@ describe UserMailer do let(:ip) { '192.168.0.1' } let(:agent) { 'NCSA_Mosaic/2.0 (Windows 3.1)' } let(:timestamp) { Time.now.utc } - let(:mail) { UserMailer.suspicious_sign_in(receiver, ip, agent, timestamp) } + let(:mail) { described_class.suspicious_sign_in(receiver, ip, agent, timestamp) } it 'renders suspicious sign in notification' do receiver.update!(locale: nil) @@ -125,7 +125,7 @@ describe UserMailer do describe 'appeal_approved' do let(:appeal) { Fabricate(:appeal, account: receiver.account, approved_at: Time.now.utc) } - let(:mail) { UserMailer.appeal_approved(receiver, appeal) } + let(:mail) { described_class.appeal_approved(receiver, appeal) } it 'renders appeal_approved notification' do expect(mail.subject).to eq I18n.t('user_mailer.appeal_approved.subject', date: I18n.l(appeal.created_at)) @@ -135,7 +135,7 @@ describe UserMailer do describe 'appeal_rejected' do let(:appeal) { Fabricate(:appeal, account: receiver.account, rejected_at: Time.now.utc) } - let(:mail) { UserMailer.appeal_rejected(receiver, appeal) } + let(:mail) { described_class.appeal_rejected(receiver, appeal) } it 'renders appeal_rejected notification' do expect(mail.subject).to eq I18n.t('user_mailer.appeal_rejected.subject', date: I18n.l(appeal.created_at)) diff --git a/spec/models/account_conversation_spec.rb b/spec/models/account_conversation_spec.rb index a16aa500c..4e8727ca3 100644 --- a/spec/models/account_conversation_spec.rb +++ b/spec/models/account_conversation_spec.rb @@ -12,7 +12,7 @@ RSpec.describe AccountConversation do status = Fabricate(:status, account: alice, visibility: :direct) status.mentions.create(account: bob) - conversation = AccountConversation.add_status(alice, status) + conversation = described_class.add_status(alice, status) expect(conversation.participant_accounts).to include(bob) expect(conversation.last_status).to eq status @@ -21,12 +21,12 @@ RSpec.describe AccountConversation do it 'appends to old record when there is a match' do last_status = Fabricate(:status, account: alice, visibility: :direct) - conversation = AccountConversation.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [last_status.id]) + conversation = described_class.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [last_status.id]) status = Fabricate(:status, account: bob, visibility: :direct, thread: last_status) status.mentions.create(account: alice) - new_conversation = AccountConversation.add_status(alice, status) + new_conversation = described_class.add_status(alice, status) expect(new_conversation.id).to eq conversation.id expect(new_conversation.participant_accounts).to include(bob) @@ -36,13 +36,13 @@ RSpec.describe AccountConversation do it 'creates new record when new participants are added' do last_status = Fabricate(:status, account: alice, visibility: :direct) - conversation = AccountConversation.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [last_status.id]) + conversation = described_class.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [last_status.id]) status = Fabricate(:status, account: bob, visibility: :direct, thread: last_status) status.mentions.create(account: alice) status.mentions.create(account: mark) - new_conversation = AccountConversation.add_status(alice, status) + new_conversation = described_class.add_status(alice, status) expect(new_conversation.id).to_not eq conversation.id expect(new_conversation.participant_accounts).to include(bob, mark) @@ -55,7 +55,7 @@ RSpec.describe AccountConversation do it 'updates last status to a previous value' do last_status = Fabricate(:status, account: alice, visibility: :direct) status = Fabricate(:status, account: alice, visibility: :direct) - conversation = AccountConversation.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [status.id, last_status.id]) + conversation = described_class.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [status.id, last_status.id]) last_status.mentions.create(account: bob) last_status.destroy! conversation.reload @@ -65,10 +65,10 @@ RSpec.describe AccountConversation do it 'removes the record if no other statuses are referenced' do last_status = Fabricate(:status, account: alice, visibility: :direct) - conversation = AccountConversation.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [last_status.id]) + conversation = described_class.create!(account: alice, conversation: last_status.conversation, participant_account_ids: [bob.id], status_ids: [last_status.id]) last_status.mentions.create(account: bob) last_status.destroy! - expect(AccountConversation.where(id: conversation.id).count).to eq 0 + expect(described_class.where(id: conversation.id).count).to eq 0 end end end diff --git a/spec/models/account_domain_block_spec.rb b/spec/models/account_domain_block_spec.rb index f3246d04c..10bd57936 100644 --- a/spec/models/account_domain_block_spec.rb +++ b/spec/models/account_domain_block_spec.rb @@ -7,14 +7,14 @@ RSpec.describe AccountDomainBlock do account = Fabricate(:account) Rails.cache.write("exclude_domains_for:#{account.id}", 'a.domain.already.blocked') - AccountDomainBlock.create!(account: account, domain: 'a.domain.blocked.later') + described_class.create!(account: account, domain: 'a.domain.blocked.later') expect(Rails.cache.exist?("exclude_domains_for:#{account.id}")).to be false end it 'removes blocking cache after destruction' do account = Fabricate(:account) - block = AccountDomainBlock.create!(account: account, domain: 'domain') + block = described_class.create!(account: account, domain: 'domain') Rails.cache.write("exclude_domains_for:#{account.id}", 'domain') block.destroy! diff --git a/spec/models/account_migration_spec.rb b/spec/models/account_migration_spec.rb index 519b9a97a..d76edddd5 100644 --- a/spec/models/account_migration_spec.rb +++ b/spec/models/account_migration_spec.rb @@ -7,7 +7,7 @@ RSpec.describe AccountMigration do let(:source_account) { Fabricate(:account) } let(:target_acct) { target_account.acct } - let(:subject) { AccountMigration.new(account: source_account, acct: target_acct) } + let(:subject) { described_class.new(account: source_account, acct: target_acct) } context 'with valid properties' do let(:target_account) { Fabricate(:account, username: 'target', domain: 'remote.org') } diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 6e9c608ab..d966bffa9 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -362,7 +362,7 @@ RSpec.describe Account do suspended: true ) - results = Account.search_for('username') + results = described_class.search_for('username') expect(results).to eq [] end @@ -375,7 +375,7 @@ RSpec.describe Account do match.user.update(approved: false) - results = Account.search_for('username') + results = described_class.search_for('username') expect(results).to eq [] end @@ -388,7 +388,7 @@ RSpec.describe Account do match.user.update(confirmed_at: nil) - results = Account.search_for('username') + results = described_class.search_for('username') expect(results).to eq [] end @@ -400,7 +400,7 @@ RSpec.describe Account do domain: 'example.com' ) - results = Account.search_for('A?l\i:c e') + results = described_class.search_for('A?l\i:c e') expect(results).to eq [match] end @@ -412,7 +412,7 @@ RSpec.describe Account do domain: 'example.com' ) - results = Account.search_for('display') + results = described_class.search_for('display') expect(results).to eq [match] end @@ -424,7 +424,7 @@ RSpec.describe Account do domain: 'example.com' ) - results = Account.search_for('username') + results = described_class.search_for('username') expect(results).to eq [match] end @@ -436,19 +436,19 @@ RSpec.describe Account do domain: 'example.com' ) - results = Account.search_for('example') + results = described_class.search_for('example') expect(results).to eq [match] end it 'limits by 10 by default' do 11.times.each { Fabricate(:account, display_name: 'Display Name') } - results = Account.search_for('display') + results = described_class.search_for('display') expect(results.size).to eq 10 end it 'accepts arbitrary limits' do 2.times.each { Fabricate(:account, display_name: 'Display Name') } - results = Account.search_for('display', limit: 1) + results = described_class.search_for('display', limit: 1) expect(results.size).to eq 1 end @@ -458,7 +458,7 @@ RSpec.describe Account do { display_name: 'Display Name', username: 'username', domain: 'example.com' }, ].map(&method(:Fabricate).curry(2).call(:account)) - results = Account.search_for('username') + results = described_class.search_for('username') expect(results).to eq matches end end @@ -476,7 +476,7 @@ RSpec.describe Account do ) account.follow!(match) - results = Account.advanced_search_for('A?l\i:c e', account, limit: 10, following: true) + results = described_class.advanced_search_for('A?l\i:c e', account, limit: 10, following: true) expect(results).to eq [match] end @@ -488,7 +488,7 @@ RSpec.describe Account do domain: 'example.com' ) - results = Account.advanced_search_for('A?l\i:c e', account, limit: 10, following: true) + results = described_class.advanced_search_for('A?l\i:c e', account, limit: 10, following: true) expect(results).to eq [] end @@ -501,7 +501,7 @@ RSpec.describe Account do suspended: true ) - results = Account.advanced_search_for('username', account, limit: 10, following: true) + results = described_class.advanced_search_for('username', account, limit: 10, following: true) expect(results).to eq [] end @@ -514,7 +514,7 @@ RSpec.describe Account do match.user.update(approved: false) - results = Account.advanced_search_for('username', account, limit: 10, following: true) + results = described_class.advanced_search_for('username', account, limit: 10, following: true) expect(results).to eq [] end @@ -527,7 +527,7 @@ RSpec.describe Account do match.user.update(confirmed_at: nil) - results = Account.advanced_search_for('username', account, limit: 10, following: true) + results = described_class.advanced_search_for('username', account, limit: 10, following: true) expect(results).to eq [] end end @@ -541,7 +541,7 @@ RSpec.describe Account do suspended: true ) - results = Account.advanced_search_for('username', account) + results = described_class.advanced_search_for('username', account) expect(results).to eq [] end @@ -554,7 +554,7 @@ RSpec.describe Account do match.user.update(approved: false) - results = Account.advanced_search_for('username', account) + results = described_class.advanced_search_for('username', account) expect(results).to eq [] end @@ -567,7 +567,7 @@ RSpec.describe Account do match.user.update(confirmed_at: nil) - results = Account.advanced_search_for('username', account) + results = described_class.advanced_search_for('username', account) expect(results).to eq [] end @@ -579,19 +579,19 @@ RSpec.describe Account do domain: 'example.com' ) - results = Account.advanced_search_for('A?l\i:c e', account) + results = described_class.advanced_search_for('A?l\i:c e', account) expect(results).to eq [match] end it 'limits by 10 by default' do 11.times { Fabricate(:account, display_name: 'Display Name') } - results = Account.advanced_search_for('display', account) + results = described_class.advanced_search_for('display', account) expect(results.size).to eq 10 end it 'accepts arbitrary limits' do 2.times { Fabricate(:account, display_name: 'Display Name') } - results = Account.advanced_search_for('display', account, limit: 1) + results = described_class.advanced_search_for('display', account, limit: 1) expect(results.size).to eq 1 end @@ -600,7 +600,7 @@ RSpec.describe Account do followed_match = Fabricate(:account, username: 'Matcher') Fabricate(:follow, account: account, target_account: followed_match) - results = Account.advanced_search_for('match', account) + results = described_class.advanced_search_for('match', account) expect(results).to eq [followed_match, match] expect(results.first.rank).to be > results.last.rank end @@ -639,31 +639,31 @@ RSpec.describe Account do describe '.following_map' do it 'returns an hash' do - expect(Account.following_map([], 1)).to be_a Hash + expect(described_class.following_map([], 1)).to be_a Hash end end describe '.followed_by_map' do it 'returns an hash' do - expect(Account.followed_by_map([], 1)).to be_a Hash + expect(described_class.followed_by_map([], 1)).to be_a Hash end end describe '.blocking_map' do it 'returns an hash' do - expect(Account.blocking_map([], 1)).to be_a Hash + expect(described_class.blocking_map([], 1)).to be_a Hash end end describe '.requested_map' do it 'returns an hash' do - expect(Account.requested_map([], 1)).to be_a Hash + expect(described_class.requested_map([], 1)).to be_a Hash end end describe '.requested_by_map' do it 'returns an hash' do - expect(Account.requested_by_map([], 1)).to be_a Hash + expect(described_class.requested_by_map([], 1)).to be_a Hash end end @@ -834,7 +834,7 @@ RSpec.describe Account do { username: 'b', domain: 'b' }, ].map(&method(:Fabricate).curry(2).call(:account)) - expect(Account.where('id > 0').alphabetic).to eq matches + expect(described_class.where('id > 0').alphabetic).to eq matches end end @@ -843,7 +843,7 @@ RSpec.describe Account do match = Fabricate(:account, display_name: 'pattern and suffix') Fabricate(:account, display_name: 'prefix and pattern') - expect(Account.matches_display_name('pattern')).to eq [match] + expect(described_class.matches_display_name('pattern')).to eq [match] end end @@ -852,24 +852,24 @@ RSpec.describe Account do match = Fabricate(:account, username: 'pattern_and_suffix') Fabricate(:account, username: 'prefix_and_pattern') - expect(Account.matches_username('pattern')).to eq [match] + expect(described_class.matches_username('pattern')).to eq [match] end end describe 'by_domain_and_subdomains' do it 'returns exact domain matches' do account = Fabricate(:account, domain: 'example.com') - expect(Account.by_domain_and_subdomains('example.com')).to eq [account] + expect(described_class.by_domain_and_subdomains('example.com')).to eq [account] end it 'returns subdomains' do account = Fabricate(:account, domain: 'foo.example.com') - expect(Account.by_domain_and_subdomains('example.com')).to eq [account] + expect(described_class.by_domain_and_subdomains('example.com')).to eq [account] end it 'does not return partially matching domains' do account = Fabricate(:account, domain: 'grexample.com') - expect(Account.by_domain_and_subdomains('example.com')).to_not eq [account] + expect(described_class.by_domain_and_subdomains('example.com')).to_not eq [account] end end @@ -877,7 +877,7 @@ RSpec.describe Account do it 'returns an array of accounts who have a domain' do account_1 = Fabricate(:account, domain: nil) account_2 = Fabricate(:account, domain: 'example.com') - expect(Account.remote).to contain_exactly(account_2) + expect(described_class.remote).to contain_exactly(account_2) end end @@ -885,7 +885,7 @@ RSpec.describe Account do it 'returns an array of accounts who do not have a domain' do account_1 = Fabricate(:account, domain: nil) account_2 = Fabricate(:account, domain: 'example.com') - expect(Account.where('id > 0').local).to contain_exactly(account_1) + expect(described_class.where('id > 0').local).to contain_exactly(account_1) end end @@ -896,14 +896,14 @@ RSpec.describe Account do matches[index] = Fabricate(:account, domain: matches[index]) end - expect(Account.where('id > 0').partitioned).to match_array(matches) + expect(described_class.where('id > 0').partitioned).to match_array(matches) end end describe 'recent' do it 'returns a relation of accounts sorted by recent creation' do matches = Array.new(2) { Fabricate(:account) } - expect(Account.where('id > 0').recent).to match_array(matches) + expect(described_class.where('id > 0').recent).to match_array(matches) end end @@ -911,7 +911,7 @@ RSpec.describe Account do it 'returns an array of accounts who are silenced' do account_1 = Fabricate(:account, silenced: true) account_2 = Fabricate(:account, silenced: false) - expect(Account.silenced).to contain_exactly(account_1) + expect(described_class.silenced).to contain_exactly(account_1) end end @@ -919,7 +919,7 @@ RSpec.describe Account do it 'returns an array of accounts who are suspended' do account_1 = Fabricate(:account, suspended: true) account_2 = Fabricate(:account, suspended: false) - expect(Account.suspended).to contain_exactly(account_1) + expect(described_class.suspended).to contain_exactly(account_1) end end @@ -941,18 +941,18 @@ RSpec.describe Account do end it 'returns every usable non-suspended account' do - expect(Account.searchable).to contain_exactly(silenced_local, silenced_remote, local_account, remote_account) + expect(described_class.searchable).to contain_exactly(silenced_local, silenced_remote, local_account, remote_account) end it 'does not mess with previously-applied scopes' do - expect(Account.where.not(id: remote_account.id).searchable).to contain_exactly(silenced_local, silenced_remote, local_account) + expect(described_class.where.not(id: remote_account.id).searchable).to contain_exactly(silenced_local, silenced_remote, local_account) end end end context 'when is local' do it 'generates keys' do - account = Account.create!(domain: nil, username: Faker::Internet.user_name(separators: ['_'])) + account = described_class.create!(domain: nil, username: Faker::Internet.user_name(separators: ['_'])) expect(account.keypair).to be_private expect(account.keypair).to be_public end @@ -961,12 +961,12 @@ RSpec.describe Account do context 'when is remote' do it 'does not generate keys' do key = OpenSSL::PKey::RSA.new(1024).public_key - account = Account.create!(domain: 'remote', username: Faker::Internet.user_name(separators: ['_']), public_key: key.to_pem) + account = described_class.create!(domain: 'remote', username: Faker::Internet.user_name(separators: ['_']), public_key: key.to_pem) expect(account.keypair.params).to eq key.params end it 'normalizes domain' do - account = Account.create!(domain: 'にゃん', username: Faker::Internet.user_name(separators: ['_'])) + account = described_class.create!(domain: 'にゃん', username: Faker::Internet.user_name(separators: ['_'])) expect(account.domain).to eq 'xn--r9j5b5b' end end @@ -986,7 +986,7 @@ RSpec.describe Account do threads = Array.new(increment_by) do Thread.new do true while wait_for_start - Account.find(subject.id).increment_count!(:followers_count) + described_class.find(subject.id).increment_count!(:followers_count) end end diff --git a/spec/models/block_spec.rb b/spec/models/block_spec.rb index de3410fd5..8249503c5 100644 --- a/spec/models/block_spec.rb +++ b/spec/models/block_spec.rb @@ -23,7 +23,7 @@ RSpec.describe Block do Rails.cache.write("exclude_account_ids_for:#{account.id}", []) Rails.cache.write("exclude_account_ids_for:#{target_account.id}", []) - Block.create!(account: account, target_account: target_account) + described_class.create!(account: account, target_account: target_account) expect(Rails.cache.exist?("exclude_account_ids_for:#{account.id}")).to be false expect(Rails.cache.exist?("exclude_account_ids_for:#{target_account.id}")).to be false @@ -32,7 +32,7 @@ RSpec.describe Block do it 'removes blocking cache after destruction' do account = Fabricate(:account) target_account = Fabricate(:account) - block = Block.create!(account: account, target_account: target_account) + block = described_class.create!(account: account, target_account: target_account) Rails.cache.write("exclude_account_ids_for:#{account.id}", [target_account.id]) Rails.cache.write("exclude_account_ids_for:#{target_account.id}", [account.id]) diff --git a/spec/models/domain_block_spec.rb b/spec/models/domain_block_spec.rb index e123c03d6..67f53fa78 100644 --- a/spec/models/domain_block_spec.rb +++ b/spec/models/domain_block_spec.rb @@ -21,73 +21,73 @@ RSpec.describe DomainBlock do describe '.blocked?' do it 'returns true if the domain is suspended' do Fabricate(:domain_block, domain: 'example.com', severity: :suspend) - expect(DomainBlock.blocked?('example.com')).to be true + expect(described_class.blocked?('example.com')).to be true end it 'returns false even if the domain is silenced' do Fabricate(:domain_block, domain: 'example.com', severity: :silence) - expect(DomainBlock.blocked?('example.com')).to be false + expect(described_class.blocked?('example.com')).to be false end it 'returns false if the domain is not suspended nor silenced' do - expect(DomainBlock.blocked?('example.com')).to be false + expect(described_class.blocked?('example.com')).to be false end end describe '.rule_for' do it 'returns rule matching a blocked domain' do block = Fabricate(:domain_block, domain: 'example.com') - expect(DomainBlock.rule_for('example.com')).to eq block + expect(described_class.rule_for('example.com')).to eq block end it 'returns a rule matching a subdomain of a blocked domain' do block = Fabricate(:domain_block, domain: 'example.com') - expect(DomainBlock.rule_for('sub.example.com')).to eq block + expect(described_class.rule_for('sub.example.com')).to eq block end it 'returns a rule matching a blocked subdomain' do block = Fabricate(:domain_block, domain: 'sub.example.com') - expect(DomainBlock.rule_for('sub.example.com')).to eq block + expect(described_class.rule_for('sub.example.com')).to eq block end it 'returns a rule matching a blocked TLD' do block = Fabricate(:domain_block, domain: 'google') - expect(DomainBlock.rule_for('google')).to eq block + expect(described_class.rule_for('google')).to eq block end it 'returns a rule matching a subdomain of a blocked TLD' do block = Fabricate(:domain_block, domain: 'google') - expect(DomainBlock.rule_for('maps.google')).to eq block + expect(described_class.rule_for('maps.google')).to eq block end end describe '#stricter_than?' do it 'returns true if the new block has suspend severity while the old has lower severity' do - suspend = DomainBlock.new(domain: 'domain', severity: :suspend) - silence = DomainBlock.new(domain: 'domain', severity: :silence) - noop = DomainBlock.new(domain: 'domain', severity: :noop) + suspend = described_class.new(domain: 'domain', severity: :suspend) + silence = described_class.new(domain: 'domain', severity: :silence) + noop = described_class.new(domain: 'domain', severity: :noop) expect(suspend.stricter_than?(silence)).to be true expect(suspend.stricter_than?(noop)).to be true end it 'returns false if the new block has lower severity than the old one' do - suspend = DomainBlock.new(domain: 'domain', severity: :suspend) - silence = DomainBlock.new(domain: 'domain', severity: :silence) - noop = DomainBlock.new(domain: 'domain', severity: :noop) + suspend = described_class.new(domain: 'domain', severity: :suspend) + silence = described_class.new(domain: 'domain', severity: :silence) + noop = described_class.new(domain: 'domain', severity: :noop) expect(silence.stricter_than?(suspend)).to be false expect(noop.stricter_than?(suspend)).to be false expect(noop.stricter_than?(silence)).to be false end it 'returns false if the new block does is less strict regarding reports' do - older = DomainBlock.new(domain: 'domain', severity: :silence, reject_reports: true) - newer = DomainBlock.new(domain: 'domain', severity: :silence, reject_reports: false) + older = described_class.new(domain: 'domain', severity: :silence, reject_reports: true) + newer = described_class.new(domain: 'domain', severity: :silence, reject_reports: false) expect(newer.stricter_than?(older)).to be false end it 'returns false if the new block does is less strict regarding media' do - older = DomainBlock.new(domain: 'domain', severity: :silence, reject_media: true) - newer = DomainBlock.new(domain: 'domain', severity: :silence, reject_media: false) + older = described_class.new(domain: 'domain', severity: :silence, reject_media: true) + newer = described_class.new(domain: 'domain', severity: :silence, reject_media: false) expect(newer.stricter_than?(older)).to be false end end diff --git a/spec/models/email_domain_block_spec.rb b/spec/models/email_domain_block_spec.rb index a7232eb6b..9d9c74887 100644 --- a/spec/models/email_domain_block_spec.rb +++ b/spec/models/email_domain_block_spec.rb @@ -14,12 +14,12 @@ RSpec.describe EmailDomainBlock do it 'returns true if the domain is blocked' do Fabricate(:email_domain_block, domain: 'example.com') - expect(EmailDomainBlock.block?(input)).to be true + expect(described_class.block?(input)).to be true end it 'returns false if the domain is not blocked' do Fabricate(:email_domain_block, domain: 'other-example.com') - expect(EmailDomainBlock.block?(input)).to be false + expect(described_class.block?(input)).to be false end end @@ -38,7 +38,7 @@ RSpec.describe EmailDomainBlock do it 'returns true if the domain is blocked' do Fabricate(:email_domain_block, domain: 'mail.foo.com') - expect(EmailDomainBlock.block?(input)).to be true + expect(described_class.block?(input)).to be true end end end diff --git a/spec/models/export_spec.rb b/spec/models/export_spec.rb index a863678a3..75468898d 100644 --- a/spec/models/export_spec.rb +++ b/spec/models/export_spec.rb @@ -12,7 +12,7 @@ describe Export do it 'returns a csv of the blocked accounts' do target_accounts.each { |target_account| account.block!(target_account) } - export = Export.new(account).to_blocked_accounts_csv + export = described_class.new(account).to_blocked_accounts_csv results = export.strip.split expect(results.size).to eq 2 @@ -22,7 +22,7 @@ describe Export do it 'returns a csv of the muted accounts' do target_accounts.each { |target_account| account.mute!(target_account) } - export = Export.new(account).to_muted_accounts_csv + export = described_class.new(account).to_muted_accounts_csv results = export.strip.split("\n") expect(results.size).to eq 3 @@ -33,7 +33,7 @@ describe Export do it 'returns a csv of the following accounts' do target_accounts.each { |target_account| account.follow!(target_account) } - export = Export.new(account).to_following_accounts_csv + export = described_class.new(account).to_following_accounts_csv results = export.strip.split("\n") expect(results.size).to eq 3 @@ -45,24 +45,24 @@ describe Export do describe 'total_storage' do it 'returns the total size of the media attachments' do media_attachment = Fabricate(:media_attachment, account: account) - expect(Export.new(account).total_storage).to eq media_attachment.file_file_size || 0 + expect(described_class.new(account).total_storage).to eq media_attachment.file_file_size || 0 end end describe 'total_follows' do it 'returns the total number of the followed accounts' do target_accounts.each { |target_account| account.follow!(target_account) } - expect(Export.new(account.reload).total_follows).to eq 2 + expect(described_class.new(account.reload).total_follows).to eq 2 end it 'returns the total number of the blocked accounts' do target_accounts.each { |target_account| account.block!(target_account) } - expect(Export.new(account.reload).total_blocks).to eq 2 + expect(described_class.new(account.reload).total_blocks).to eq 2 end it 'returns the total number of the muted accounts' do target_accounts.each { |target_account| account.mute!(target_account) } - expect(Export.new(account.reload).total_mutes).to eq 2 + expect(described_class.new(account.reload).total_mutes).to eq 2 end end end diff --git a/spec/models/favourite_spec.rb b/spec/models/favourite_spec.rb index 9e69570a0..ef7fbdefc 100644 --- a/spec/models/favourite_spec.rb +++ b/spec/models/favourite_spec.rb @@ -10,12 +10,12 @@ RSpec.describe Favourite do let(:status) { Fabricate(:status, reblog: reblog) } it 'invalidates if the reblogged status is already a favourite' do - Favourite.create!(account: account, status: reblog) - expect(Favourite.new(account: account, status: status).valid?).to be false + described_class.create!(account: account, status: reblog) + expect(described_class.new(account: account, status: status).valid?).to be false end it 'replaces status with the reblogged one if it is a reblog' do - favourite = Favourite.create!(account: account, status: status) + favourite = described_class.create!(account: account, status: status) expect(favourite.status).to eq reblog end end @@ -24,7 +24,7 @@ RSpec.describe Favourite do let(:status) { Fabricate(:status, reblog: nil) } it 'saves with the specified status' do - favourite = Favourite.create!(account: account, status: status) + favourite = described_class.create!(account: account, status: status) expect(favourite.status).to eq status end end diff --git a/spec/models/follow_spec.rb b/spec/models/follow_spec.rb index 79c0048f9..c7743183c 100644 --- a/spec/models/follow_spec.rb +++ b/spec/models/follow_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Follow do let(:bob) { Fabricate(:account, username: 'bob') } describe 'validations' do - subject { Follow.new(account: alice, target_account: bob, rate_limit: true) } + subject { described_class.new(account: alice, target_account: bob, rate_limit: true) } it 'is invalid without an account' do follow = Fabricate.build(:follow, account: nil) @@ -38,10 +38,10 @@ RSpec.describe Follow do describe 'recent' do it 'sorts so that more recent follows comes earlier' do - follow0 = Follow.create!(account: alice, target_account: bob) - follow1 = Follow.create!(account: bob, target_account: alice) + follow0 = described_class.create!(account: alice, target_account: bob) + follow1 = described_class.create!(account: bob, target_account: alice) - a = Follow.recent.to_a + a = described_class.recent.to_a expect(a.size).to eq 2 expect(a[0]).to eq follow1 diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb index 59155781c..2fca1e1c1 100644 --- a/spec/models/identity_spec.rb +++ b/spec/models/identity_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Identity do end it 'returns an instance of Identity' do - expect(described_class.find_for_oauth(auth)).to be_instance_of Identity + expect(described_class.find_for_oauth(auth)).to be_instance_of described_class end end end diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb index 1dae40a73..3605f0b9b 100644 --- a/spec/models/import_spec.rb +++ b/spec/models/import_spec.rb @@ -9,17 +9,17 @@ RSpec.describe Import do describe 'validations' do it 'has a valid parameters' do - import = Import.create(account: account, type: type, data: data) + import = described_class.create(account: account, type: type, data: data) expect(import).to be_valid end it 'is invalid without an type' do - import = Import.create(account: account, data: data) + import = described_class.create(account: account, data: data) expect(import).to model_have_error_on_field(:type) end it 'is invalid without a data' do - import = Import.create(account: account, type: type) + import = described_class.create(account: account, type: type) expect(import).to model_have_error_on_field(:data) end end diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb index becc74824..4d4bc748f 100644 --- a/spec/models/media_attachment_spec.rb +++ b/spec/models/media_attachment_spec.rb @@ -85,7 +85,7 @@ RSpec.describe MediaAttachment do end describe 'animated gif conversion' do - let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture('avatar.gif')) } + let(:media) { described_class.create(account: Fabricate(:account), file: attachment_fixture('avatar.gif')) } it 'sets type to gifv' do expect(media.type).to eq 'gifv' @@ -109,7 +109,7 @@ RSpec.describe MediaAttachment do fixtures.each do |fixture| context fixture[:filename] do - let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture(fixture[:filename])) } + let(:media) { described_class.create(account: Fabricate(:account), file: attachment_fixture(fixture[:filename])) } it 'sets type to image' do expect(media.type).to eq 'image' @@ -129,7 +129,7 @@ RSpec.describe MediaAttachment do end describe 'ogg with cover art' do - let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture('boop.ogg')) } + let(:media) { described_class.create(account: Fabricate(:account), file: attachment_fixture('boop.ogg')) } it 'detects it as an audio file' do expect(media.type).to eq 'audio' @@ -153,7 +153,7 @@ RSpec.describe MediaAttachment do end describe 'jpeg' do - let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture('attachment.jpg')) } + let(:media) { described_class.create(account: Fabricate(:account), file: attachment_fixture('attachment.jpg')) } it 'sets meta for different style' do expect(media.file.meta['original']['width']).to eq 600 @@ -171,7 +171,7 @@ RSpec.describe MediaAttachment do describe 'base64-encoded jpeg' do let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('attachment.jpg').read)}" } - let(:media) { MediaAttachment.create(account: Fabricate(:account), file: base64_attachment) } + let(:media) { described_class.create(account: Fabricate(:account), file: base64_attachment) } it 'saves media attachment' do expect(media.persisted?).to be true @@ -184,7 +184,7 @@ RSpec.describe MediaAttachment do end it 'is invalid without file' do - media = MediaAttachment.new(account: Fabricate(:account)) + media = described_class.new(account: Fabricate(:account)) expect(media.valid?).to be false end @@ -192,26 +192,26 @@ RSpec.describe MediaAttachment do it 'rejects video files that are too large' do stub_const 'MediaAttachment::IMAGE_LIMIT', 100.megabytes stub_const 'MediaAttachment::VIDEO_LIMIT', 1.kilobyte - expect { MediaAttachment.create!(account: Fabricate(:account), file: attachment_fixture('attachment.webm')) }.to raise_error(ActiveRecord::RecordInvalid) + expect { described_class.create!(account: Fabricate(:account), file: attachment_fixture('attachment.webm')) }.to raise_error(ActiveRecord::RecordInvalid) end it 'accepts video files that are small enough' do stub_const 'MediaAttachment::IMAGE_LIMIT', 1.kilobyte stub_const 'MediaAttachment::VIDEO_LIMIT', 100.megabytes - media = MediaAttachment.create!(account: Fabricate(:account), file: attachment_fixture('attachment.webm')) + media = described_class.create!(account: Fabricate(:account), file: attachment_fixture('attachment.webm')) expect(media.valid?).to be true end it 'rejects image files that are too large' do stub_const 'MediaAttachment::IMAGE_LIMIT', 1.kilobyte stub_const 'MediaAttachment::VIDEO_LIMIT', 100.megabytes - expect { MediaAttachment.create!(account: Fabricate(:account), file: attachment_fixture('attachment.jpg')) }.to raise_error(ActiveRecord::RecordInvalid) + expect { described_class.create!(account: Fabricate(:account), file: attachment_fixture('attachment.jpg')) }.to raise_error(ActiveRecord::RecordInvalid) end it 'accepts image files that are small enough' do stub_const 'MediaAttachment::IMAGE_LIMIT', 100.megabytes stub_const 'MediaAttachment::VIDEO_LIMIT', 1.kilobyte - media = MediaAttachment.create!(account: Fabricate(:account), file: attachment_fixture('attachment.jpg')) + media = described_class.create!(account: Fabricate(:account), file: attachment_fixture('attachment.jpg')) expect(media.valid?).to be true end end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index 795491546..0dd9264e0 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -38,22 +38,22 @@ RSpec.describe Notification do describe '#type' do it 'returns :reblog for a Status' do - notification = Notification.new(activity: Status.new) + notification = described_class.new(activity: Status.new) expect(notification.type).to eq :reblog end it 'returns :mention for a Mention' do - notification = Notification.new(activity: Mention.new) + notification = described_class.new(activity: Mention.new) expect(notification.type).to eq :mention end it 'returns :favourite for a Favourite' do - notification = Notification.new(activity: Favourite.new) + notification = described_class.new(activity: Favourite.new) expect(notification.type).to eq :favourite end it 'returns :follow for a Follow' do - notification = Notification.new(activity: Follow.new) + notification = described_class.new(activity: Follow.new) expect(notification.type).to eq :follow end end diff --git a/spec/models/relationship_filter_spec.rb b/spec/models/relationship_filter_spec.rb index 7c0f37a06..b3e855c12 100644 --- a/spec/models/relationship_filter_spec.rb +++ b/spec/models/relationship_filter_spec.rb @@ -8,7 +8,7 @@ describe RelationshipFilter do describe '#results' do context 'when default params are used' do let(:subject) do - RelationshipFilter.new(account, 'order' => 'active').results + described_class.new(account, 'order' => 'active').results end before do diff --git a/spec/models/report_filter_spec.rb b/spec/models/report_filter_spec.rb index 8269c4579..4b0852f08 100644 --- a/spec/models/report_filter_spec.rb +++ b/spec/models/report_filter_spec.rb @@ -5,7 +5,7 @@ require 'rails_helper' describe ReportFilter do describe 'with empty params' do it 'defaults to unresolved reports list' do - filter = ReportFilter.new({}) + filter = described_class.new({}) expect(filter.results).to eq Report.unresolved end @@ -13,7 +13,7 @@ describe ReportFilter do describe 'with invalid params' do it 'raises with key error' do - filter = ReportFilter.new(wrong: true) + filter = described_class.new(wrong: true) expect { filter.results }.to raise_error(/wrong/) end @@ -21,7 +21,7 @@ describe ReportFilter do describe 'with valid params' do it 'combines filters on Report' do - filter = ReportFilter.new(account_id: '123', resolved: true, target_account_id: '456') + filter = described_class.new(account_id: '123', resolved: true, target_account_id: '456') allow(Report).to receive(:where).and_return(Report.none) allow(Report).to receive(:resolved).and_return(Report.none) diff --git a/spec/models/session_activation_spec.rb b/spec/models/session_activation_spec.rb index 51c6aa5cb..052a06e5c 100644 --- a/spec/models/session_activation_spec.rb +++ b/spec/models/session_activation_spec.rb @@ -80,7 +80,7 @@ RSpec.describe SessionActivation do end it 'returns an instance of SessionActivation' do - expect(described_class.activate(**options)).to be_a SessionActivation + expect(described_class.activate(**options)).to be_a described_class end end diff --git a/spec/models/setting_spec.rb b/spec/models/setting_spec.rb index accce10f8..bba585cec 100644 --- a/spec/models/setting_spec.rb +++ b/spec/models/setting_spec.rb @@ -146,7 +146,7 @@ RSpec.describe Setting do it 'includes Setting with value of default_value' do setting = described_class.all_as_records[key] - expect(setting).to be_a Setting + expect(setting).to be_a described_class expect(setting).to have_attributes(var: key) expect(setting).to have_attributes(value: 'default_value') end diff --git a/spec/models/site_upload_spec.rb b/spec/models/site_upload_spec.rb index d4a929311..9689bce9e 100644 --- a/spec/models/site_upload_spec.rb +++ b/spec/models/site_upload_spec.rb @@ -4,7 +4,7 @@ require 'rails_helper' RSpec.describe SiteUpload do describe '#cache_key' do - let(:site_upload) { SiteUpload.new(var: 'var') } + let(:site_upload) { described_class.new(var: 'var') } it 'returns cache_key' do expect(site_upload.cache_key).to eq 'site_uploads/var' diff --git a/spec/models/status_pin_spec.rb b/spec/models/status_pin_spec.rb index 52ce0847c..660b2e92a 100644 --- a/spec/models/status_pin_spec.rb +++ b/spec/models/status_pin_spec.rb @@ -8,14 +8,14 @@ RSpec.describe StatusPin do account = Fabricate(:account) status = Fabricate(:status, account: account) - expect(StatusPin.new(account: account, status: status).save).to be true + expect(described_class.new(account: account, status: status).save).to be true end it 'does not allow pins of statuses by someone else' do account = Fabricate(:account) status = Fabricate(:status) - expect(StatusPin.new(account: account, status: status).save).to be false + expect(described_class.new(account: account, status: status).save).to be false end it 'does not allow pins of reblogs' do @@ -23,21 +23,21 @@ RSpec.describe StatusPin do status = Fabricate(:status, account: account) reblog = Fabricate(:status, reblog: status) - expect(StatusPin.new(account: account, status: reblog).save).to be false + expect(described_class.new(account: account, status: reblog).save).to be false end it 'does allow pins of direct statuses' do account = Fabricate(:account) status = Fabricate(:status, account: account, visibility: :private) - expect(StatusPin.new(account: account, status: status).save).to be true + expect(described_class.new(account: account, status: status).save).to be true end it 'does not allow pins of direct statuses' do account = Fabricate(:account) status = Fabricate(:status, account: account, visibility: :direct) - expect(StatusPin.new(account: account, status: status).save).to be false + expect(described_class.new(account: account, status: status).save).to be false end max_pins = 5 @@ -50,10 +50,10 @@ RSpec.describe StatusPin do end max_pins.times do |i| - expect(StatusPin.new(account: account, status: status[i]).save).to be true + expect(described_class.new(account: account, status: status[i]).save).to be true end - expect(StatusPin.new(account: account, status: status[max_pins]).save).to be false + expect(described_class.new(account: account, status: status[max_pins]).save).to be false end it 'allows pins above the max for remote accounts' do @@ -65,10 +65,10 @@ RSpec.describe StatusPin do end max_pins.times do |i| - expect(StatusPin.new(account: account, status: status[i]).save).to be true + expect(described_class.new(account: account, status: status[i]).save).to be true end - expect(StatusPin.new(account: account, status: status[max_pins]).save).to be true + expect(described_class.new(account: account, status: status[max_pins]).save).to be true end end end diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index 84ff82c78..3141c52c0 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -160,7 +160,7 @@ RSpec.describe Status do reblog = Fabricate(:status, account: bob, reblog: subject) expect(subject.reblogs_count).to eq 1 expect { subject.destroy }.to_not raise_error - expect(Status.find_by(id: reblog.id)).to be_nil + expect(described_class.find_by(id: reblog.id)).to be_nil end end @@ -206,7 +206,7 @@ RSpec.describe Status do end describe '.mutes_map' do - subject { Status.mutes_map([status.conversation.id], account) } + subject { described_class.mutes_map([status.conversation.id], account) } let(:status) { Fabricate(:status) } let(:account) { Fabricate(:account) } @@ -222,7 +222,7 @@ RSpec.describe Status do end describe '.favourites_map' do - subject { Status.favourites_map([status], account) } + subject { described_class.favourites_map([status], account) } let(:status) { Fabricate(:status) } let(:account) { Fabricate(:account) } @@ -238,7 +238,7 @@ RSpec.describe Status do end describe '.reblogs_map' do - subject { Status.reblogs_map([status], account) } + subject { described_class.reblogs_map([status], account) } let(:status) { Fabricate(:status) } let(:account) { Fabricate(:account) } @@ -265,17 +265,17 @@ RSpec.describe Status do context 'when given one tag' do it 'returns the expected statuses' do - expect(Status.tagged_with([tag1.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status1.id, status5.id) - expect(Status.tagged_with([tag2.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status2.id, status5.id) - expect(Status.tagged_with([tag3.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status3.id, status5.id) + expect(described_class.tagged_with([tag1.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status1.id, status5.id) + expect(described_class.tagged_with([tag2.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status2.id, status5.id) + expect(described_class.tagged_with([tag3.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status3.id, status5.id) end end context 'when given multiple tags' do it 'returns the expected statuses' do - expect(Status.tagged_with([tag1.id, tag2.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status1.id, status2.id, status5.id) - expect(Status.tagged_with([tag1.id, tag3.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status1.id, status3.id, status5.id) - expect(Status.tagged_with([tag2.id, tag3.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status2.id, status3.id, status5.id) + expect(described_class.tagged_with([tag1.id, tag2.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status1.id, status2.id, status5.id) + expect(described_class.tagged_with([tag1.id, tag3.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status1.id, status3.id, status5.id) + expect(described_class.tagged_with([tag2.id, tag3.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status2.id, status3.id, status5.id) end end end @@ -292,17 +292,17 @@ RSpec.describe Status do context 'when given one tag' do it 'returns the expected statuses' do - expect(Status.tagged_with_all([tag1.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status1.id, status5.id) - expect(Status.tagged_with_all([tag2.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status2.id, status5.id) - expect(Status.tagged_with_all([tag3.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status3.id) + expect(described_class.tagged_with_all([tag1.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status1.id, status5.id) + expect(described_class.tagged_with_all([tag2.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status2.id, status5.id) + expect(described_class.tagged_with_all([tag3.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status3.id) end end context 'when given multiple tags' do it 'returns the expected statuses' do - expect(Status.tagged_with_all([tag1.id, tag2.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status5.id) - expect(Status.tagged_with_all([tag1.id, tag3.id]).reorder(:id).pluck(:id).uniq).to eq [] - expect(Status.tagged_with_all([tag2.id, tag3.id]).reorder(:id).pluck(:id).uniq).to eq [] + expect(described_class.tagged_with_all([tag1.id, tag2.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status5.id) + expect(described_class.tagged_with_all([tag1.id, tag3.id]).reorder(:id).pluck(:id).uniq).to eq [] + expect(described_class.tagged_with_all([tag2.id, tag3.id]).reorder(:id).pluck(:id).uniq).to eq [] end end end @@ -319,17 +319,17 @@ RSpec.describe Status do context 'when given one tag' do it 'returns the expected statuses' do - expect(Status.tagged_with_none([tag1.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status2.id, status3.id, status4.id) - expect(Status.tagged_with_none([tag2.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status1.id, status3.id, status4.id) - expect(Status.tagged_with_none([tag3.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status1.id, status2.id, status4.id) + expect(described_class.tagged_with_none([tag1.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status2.id, status3.id, status4.id) + expect(described_class.tagged_with_none([tag2.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status1.id, status3.id, status4.id) + expect(described_class.tagged_with_none([tag3.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status1.id, status2.id, status4.id) end end context 'when given multiple tags' do it 'returns the expected statuses' do - expect(Status.tagged_with_none([tag1.id, tag2.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status3.id, status4.id) - expect(Status.tagged_with_none([tag1.id, tag3.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status2.id, status4.id) - expect(Status.tagged_with_none([tag2.id, tag3.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status1.id, status4.id) + expect(described_class.tagged_with_none([tag1.id, tag2.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status3.id, status4.id) + expect(described_class.tagged_with_none([tag1.id, tag3.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status2.id, status4.id) + expect(described_class.tagged_with_none([tag2.id, tag3.id]).reorder(:id).pluck(:id).uniq).to contain_exactly(status1.id, status4.id) end end end @@ -344,21 +344,21 @@ RSpec.describe Status do end it 'creates new conversation for stand-alone status' do - expect(Status.create(account: alice, text: 'First').conversation_id).to_not be_nil + expect(described_class.create(account: alice, text: 'First').conversation_id).to_not be_nil end it 'keeps conversation of parent node' do parent = Fabricate(:status, text: 'First') - expect(Status.create(account: alice, thread: parent, text: 'Response').conversation_id).to eq parent.conversation_id + expect(described_class.create(account: alice, thread: parent, text: 'Response').conversation_id).to eq parent.conversation_id end it 'sets `local` to true for status by local account' do - expect(Status.create(account: alice, text: 'foo').local).to be true + expect(described_class.create(account: alice, text: 'foo').local).to be true end it 'sets `local` to false for status by remote account' do alice.update(domain: 'example.com') - expect(Status.create(account: alice, text: 'foo').local).to be false + expect(described_class.create(account: alice, text: 'foo').local).to be false end end @@ -372,7 +372,7 @@ RSpec.describe Status do describe 'after_create' do it 'saves ActivityPub uri as uri for local status' do - status = Status.create(account: alice, text: 'foo') + status = described_class.create(account: alice, text: 'foo') status.reload expect(status.uri).to start_with('https://') end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index d3e0ac63a..554e7efb6 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -57,7 +57,7 @@ RSpec.describe User do it 'returns an array of recent users ordered by id' do user_1 = Fabricate(:user) user_2 = Fabricate(:user) - expect(User.recent).to eq [user_2, user_1] + expect(described_class.recent).to eq [user_2, user_1] end end @@ -65,7 +65,7 @@ RSpec.describe User do it 'returns an array of users who are confirmed' do user_1 = Fabricate(:user, confirmed_at: nil) user_2 = Fabricate(:user, confirmed_at: Time.zone.now) - expect(User.confirmed).to contain_exactly(user_2) + expect(described_class.confirmed).to contain_exactly(user_2) end end @@ -74,7 +74,7 @@ RSpec.describe User do specified = Fabricate(:user, current_sign_in_at: 15.days.ago) Fabricate(:user, current_sign_in_at: 6.days.ago) - expect(User.inactive).to contain_exactly(specified) + expect(described_class.inactive).to contain_exactly(specified) end end @@ -83,7 +83,7 @@ RSpec.describe User do specified = Fabricate(:user, email: 'specified@spec') Fabricate(:user, email: 'unspecified@spec') - expect(User.matches_email('specified')).to contain_exactly(specified) + expect(described_class.matches_email('specified')).to contain_exactly(specified) end end @@ -96,7 +96,7 @@ RSpec.describe User do Fabricate(:session_activation, user: user2, ip: '2160:8888::24', session_id: '3') Fabricate(:session_activation, user: user2, ip: '2160:8888::25', session_id: '4') - expect(User.matches_ip('2160:2160::/32')).to contain_exactly(user1) + expect(described_class.matches_ip('2160:2160::/32')).to contain_exactly(user1) end end end @@ -113,19 +113,19 @@ RSpec.describe User do end it 'allows a non-blacklisted user to be created' do - user = User.new(email: 'foo@example.com', account: account, password: password, agreement: true) + user = described_class.new(email: 'foo@example.com', account: account, password: password, agreement: true) expect(user).to be_valid end it 'does not allow a blacklisted user to be created' do - user = User.new(email: 'foo@mvrht.com', account: account, password: password, agreement: true) + user = described_class.new(email: 'foo@mvrht.com', account: account, password: password, agreement: true) expect(user).to_not be_valid end it 'does not allow a subdomain blacklisted user to be created' do - user = User.new(email: 'foo@mvrht.com.topdomain.tld', account: account, password: password, agreement: true) + user = described_class.new(email: 'foo@mvrht.com.topdomain.tld', account: account, password: password, agreement: true) expect(user).to_not be_valid end @@ -349,17 +349,17 @@ RSpec.describe User do end it 'does not allow a user to be created unless they are whitelisted' do - user = User.new(email: 'foo@example.com', account: account, password: password, agreement: true) + user = described_class.new(email: 'foo@example.com', account: account, password: password, agreement: true) expect(user).to_not be_valid end it 'allows a user to be created if they are whitelisted' do - user = User.new(email: 'foo@mastodon.space', account: account, password: password, agreement: true) + user = described_class.new(email: 'foo@mastodon.space', account: account, password: password, agreement: true) expect(user).to be_valid end it 'does not allow a user with a whitelisted top domain as subdomain in their email address to be created' do - user = User.new(email: 'foo@mastodon.space.userdomain.com', account: account, password: password, agreement: true) + user = described_class.new(email: 'foo@mastodon.space.userdomain.com', account: account, password: password, agreement: true) expect(user).to_not be_valid end @@ -373,7 +373,7 @@ RSpec.describe User do it 'does not allow a user to be created with a specific blacklisted subdomain even if the top domain is whitelisted' do Rails.configuration.x.email_domains_blacklist = 'blacklisted.mastodon.space' - user = User.new(email: 'foo@blacklisted.mastodon.space', account: account, password: password) + user = described_class.new(email: 'foo@blacklisted.mastodon.space', account: account, password: password) expect(user).to_not be_valid end end @@ -527,19 +527,19 @@ RSpec.describe User do end describe '.those_who_can' do - let!(:moderator_user) { Fabricate(:user, role: UserRole.find_by(name: 'Moderator')) } + before { Fabricate(:user, role: UserRole.find_by(name: 'Moderator')) } context 'when there are not any user roles' do before { UserRole.destroy_all } it 'returns an empty list' do - expect(User.those_who_can(:manage_blocks)).to eq([]) + expect(described_class.those_who_can(:manage_blocks)).to eq([]) end end context 'when there are not users with the needed role' do it 'returns an empty list' do - expect(User.those_who_can(:manage_blocks)).to eq([]) + expect(described_class.those_who_can(:manage_blocks)).to eq([]) end end @@ -547,7 +547,7 @@ RSpec.describe User do let!(:admin_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } it 'returns the users with the role' do - expect(User.those_who_can(:manage_blocks)).to eq([admin_user]) + expect(described_class.those_who_can(:manage_blocks)).to eq([admin_user]) end end end diff --git a/spec/policies/account_moderation_note_policy_spec.rb b/spec/policies/account_moderation_note_policy_spec.rb index 03d18250b..90abdfea7 100644 --- a/spec/policies/account_moderation_note_policy_spec.rb +++ b/spec/policies/account_moderation_note_policy_spec.rb @@ -11,13 +11,13 @@ RSpec.describe AccountModerationNotePolicy do permissions :create? do context 'when staff' do it 'grants to create' do - expect(subject).to permit(admin, AccountModerationNotePolicy) + expect(subject).to permit(admin, described_class) end end context 'when not staff' do it 'denies to create' do - expect(subject).to_not permit(john, AccountModerationNotePolicy) + expect(subject).to_not permit(john, described_class) end end end diff --git a/spec/presenters/account_relationships_presenter_spec.rb b/spec/presenters/account_relationships_presenter_spec.rb index d59060bd5..5c2ba54e0 100644 --- a/spec/presenters/account_relationships_presenter_spec.rb +++ b/spec/presenters/account_relationships_presenter_spec.rb @@ -14,7 +14,7 @@ RSpec.describe AccountRelationshipsPresenter do allow(Account).to receive(:domain_blocking_map).with(account_ids, current_account_id).and_return(default_map) end - let(:presenter) { AccountRelationshipsPresenter.new(account_ids, current_account_id, **options) } + let(:presenter) { described_class.new(account_ids, current_account_id, **options) } let(:current_account_id) { Fabricate(:account).id } let(:account_ids) { [Fabricate(:account).id] } let(:default_map) { { 1 => true } } diff --git a/spec/presenters/status_relationships_presenter_spec.rb b/spec/presenters/status_relationships_presenter_spec.rb index a62fa004a..7746c8cd7 100644 --- a/spec/presenters/status_relationships_presenter_spec.rb +++ b/spec/presenters/status_relationships_presenter_spec.rb @@ -12,7 +12,7 @@ RSpec.describe StatusRelationshipsPresenter do allow(Status).to receive(:pins_map).with(anything, current_account_id).and_return(default_map) end - let(:presenter) { StatusRelationshipsPresenter.new(statuses, current_account_id, **options) } + let(:presenter) { described_class.new(statuses, current_account_id, **options) } let(:current_account_id) { Fabricate(:account).id } let(:statuses) { [Fabricate(:status)] } let(:status_ids) { statuses.map(&:id) + statuses.filter_map(&:reblog_of_id) } diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb index 7ea47baef..68f537891 100644 --- a/spec/serializers/activitypub/note_serializer_spec.rb +++ b/spec/serializers/activitypub/note_serializer_spec.rb @@ -15,7 +15,7 @@ describe ActivityPub::NoteSerializer do let!(:reply5) { Fabricate(:status, account: account, thread: parent, visibility: :direct) } before(:each) do - @serialization = ActiveModelSerializers::SerializableResource.new(parent, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) + @serialization = ActiveModelSerializers::SerializableResource.new(parent, serializer: described_class, adapter: ActivityPub::Adapter) end it 'has a Note type' do diff --git a/spec/serializers/activitypub/update_poll_serializer_spec.rb b/spec/serializers/activitypub/update_poll_serializer_spec.rb index 4360808b5..14c24c70c 100644 --- a/spec/serializers/activitypub/update_poll_serializer_spec.rb +++ b/spec/serializers/activitypub/update_poll_serializer_spec.rb @@ -10,7 +10,7 @@ describe ActivityPub::UpdatePollSerializer do let!(:status) { Fabricate(:status, account: account, poll: poll) } before(:each) do - @serialization = ActiveModelSerializers::SerializableResource.new(status, serializer: ActivityPub::UpdatePollSerializer, adapter: ActivityPub::Adapter) + @serialization = ActiveModelSerializers::SerializableResource.new(status, serializer: described_class, adapter: ActivityPub::Adapter) end it 'has a Update type' do diff --git a/spec/serializers/rest/account_serializer_spec.rb b/spec/serializers/rest/account_serializer_spec.rb index 528639943..e399e88f3 100644 --- a/spec/serializers/rest/account_serializer_spec.rb +++ b/spec/serializers/rest/account_serializer_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' describe REST::AccountSerializer do - subject { JSON.parse(ActiveModelSerializers::SerializableResource.new(account, serializer: REST::AccountSerializer).to_json) } + subject { JSON.parse(ActiveModelSerializers::SerializableResource.new(account, serializer: described_class).to_json) } let(:role) { Fabricate(:user_role, name: 'Role', highlighted: true) } let(:user) { Fabricate(:user, role: role) } diff --git a/spec/services/activitypub/fetch_remote_account_service_spec.rb b/spec/services/activitypub/fetch_remote_account_service_spec.rb index 868bc2a58..ac7484d96 100644 --- a/spec/services/activitypub/fetch_remote_account_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_account_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do - subject { ActivityPub::FetchRemoteAccountService.new } + subject { described_class.new } let!(:actor) do { diff --git a/spec/services/activitypub/fetch_remote_actor_service_spec.rb b/spec/services/activitypub/fetch_remote_actor_service_spec.rb index a72c6941e..93d31b69d 100644 --- a/spec/services/activitypub/fetch_remote_actor_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_actor_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do - subject { ActivityPub::FetchRemoteActorService.new } + subject { described_class.new } let!(:actor) do { diff --git a/spec/services/activitypub/fetch_remote_key_service_spec.rb b/spec/services/activitypub/fetch_remote_key_service_spec.rb index 0ec0c2736..cd8f29ddd 100644 --- a/spec/services/activitypub/fetch_remote_key_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_key_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe ActivityPub::FetchRemoteKeyService, type: :service do - subject { ActivityPub::FetchRemoteKeyService.new } + subject { described_class.new } let(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } diff --git a/spec/services/after_block_domain_from_account_service_spec.rb b/spec/services/after_block_domain_from_account_service_spec.rb index b75f92372..9bfaa3580 100644 --- a/spec/services/after_block_domain_from_account_service_spec.rb +++ b/spec/services/after_block_domain_from_account_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe AfterBlockDomainFromAccountService, type: :service do - subject { AfterBlockDomainFromAccountService.new } + subject { described_class.new } let!(:wolf) { Fabricate(:account, username: 'wolf', domain: 'evil.org', inbox_url: 'https://evil.org/inbox', protocol: :activitypub) } let!(:alice) { Fabricate(:account, username: 'alice') } diff --git a/spec/services/authorize_follow_service_spec.rb b/spec/services/authorize_follow_service_spec.rb index 63d9e2a0f..d07645ab6 100644 --- a/spec/services/authorize_follow_service_spec.rb +++ b/spec/services/authorize_follow_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe AuthorizeFollowService, type: :service do - subject { AuthorizeFollowService.new } + subject { described_class.new } let(:sender) { Fabricate(:account, username: 'alice') } diff --git a/spec/services/batched_remove_status_service_spec.rb b/spec/services/batched_remove_status_service_spec.rb index 9bedf3744..c0cd01315 100644 --- a/spec/services/batched_remove_status_service_spec.rb +++ b/spec/services/batched_remove_status_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe BatchedRemoveStatusService, type: :service do - subject { BatchedRemoveStatusService.new } + subject { described_class.new } let!(:alice) { Fabricate(:account) } let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') } diff --git a/spec/services/block_domain_service_spec.rb b/spec/services/block_domain_service_spec.rb index 0ab97b8ce..93722a15b 100644 --- a/spec/services/block_domain_service_spec.rb +++ b/spec/services/block_domain_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe BlockDomainService, type: :service do - subject { BlockDomainService.new } + subject { described_class.new } let!(:bad_account) { Fabricate(:account, username: 'badguy666', domain: 'evil.org') } let!(:bad_status1) { Fabricate(:status, account: bad_account, text: 'You suck') } diff --git a/spec/services/block_service_spec.rb b/spec/services/block_service_spec.rb index 75f07f5ad..5f7c2e8da 100644 --- a/spec/services/block_service_spec.rb +++ b/spec/services/block_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe BlockService, type: :service do - subject { BlockService.new } + subject { described_class.new } let(:sender) { Fabricate(:account, username: 'alice') } diff --git a/spec/services/bootstrap_timeline_service_spec.rb b/spec/services/bootstrap_timeline_service_spec.rb index 670ac652f..5a15ba741 100644 --- a/spec/services/bootstrap_timeline_service_spec.rb +++ b/spec/services/bootstrap_timeline_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe BootstrapTimelineService, type: :service do - subject { BootstrapTimelineService.new } + subject { described_class.new } context 'when the new user has registered from an invite' do let(:service) { double } diff --git a/spec/services/clear_domain_media_service_spec.rb b/spec/services/clear_domain_media_service_spec.rb index 987507579..2a00409a4 100644 --- a/spec/services/clear_domain_media_service_spec.rb +++ b/spec/services/clear_domain_media_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe ClearDomainMediaService, type: :service do - subject { ClearDomainMediaService.new } + subject { described_class.new } let!(:bad_account) { Fabricate(:account, username: 'badguy666', domain: 'evil.org') } let!(:bad_status1) { Fabricate(:status, account: bad_account, text: 'You suck') } diff --git a/spec/services/favourite_service_spec.rb b/spec/services/favourite_service_spec.rb index 613ae203e..782c235c4 100644 --- a/spec/services/favourite_service_spec.rb +++ b/spec/services/favourite_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe FavouriteService, type: :service do - subject { FavouriteService.new } + subject { described_class.new } let(:sender) { Fabricate(:account, username: 'alice') } diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb index c9521e3c8..c2ad0d717 100644 --- a/spec/services/follow_service_spec.rb +++ b/spec/services/follow_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe FollowService, type: :service do - subject { FollowService.new } + subject { described_class.new } let(:sender) { Fabricate(:account, username: 'alice') } diff --git a/spec/services/import_service_spec.rb b/spec/services/import_service_spec.rb index 7f8e5855f..32ba4409c 100644 --- a/spec/services/import_service_spec.rb +++ b/spec/services/import_service_spec.rb @@ -14,7 +14,7 @@ RSpec.describe ImportService, type: :service do end context 'when importing old-style list of muted users' do - subject { ImportService.new } + subject { described_class.new } let(:csv) { attachment_fixture('mute-imports.txt') } @@ -52,7 +52,7 @@ RSpec.describe ImportService, type: :service do end context 'when importing new-style list of muted users' do - subject { ImportService.new } + subject { described_class.new } let(:csv) { attachment_fixture('new-mute-imports.txt') } @@ -93,7 +93,7 @@ RSpec.describe ImportService, type: :service do end context 'when importing old-style list of followed users' do - subject { ImportService.new } + subject { described_class.new } let(:csv) { attachment_fixture('mute-imports.txt') } @@ -135,7 +135,7 @@ RSpec.describe ImportService, type: :service do end context 'when importing new-style list of followed users' do - subject { ImportService.new } + subject { described_class.new } let(:csv) { attachment_fixture('new-following-imports.txt') } @@ -182,7 +182,7 @@ RSpec.describe ImportService, type: :service do # # https://github.com/mastodon/mastodon/issues/20571 context 'with a utf-8 encoded domains' do - subject { ImportService.new } + subject { described_class.new } let!(:nare) { Fabricate(:account, username: 'nare', domain: 'թութ.հայ', locked: false, protocol: :activitypub, inbox_url: 'https://թութ.հայ/inbox') } let(:csv) { attachment_fixture('utf8-followers.txt') } @@ -201,7 +201,7 @@ RSpec.describe ImportService, type: :service do end context 'when importing bookmarks' do - subject { ImportService.new } + subject { described_class.new } let(:csv) { attachment_fixture('bookmark-imports.txt') } let(:local_account) { Fabricate(:account, username: 'foo', domain: '') } diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index f57712271..76ef5391f 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe PostStatusService, type: :service do - subject { PostStatusService.new } + subject { described_class.new } it 'creates a new status' do account = Fabricate(:account) diff --git a/spec/services/precompute_feed_service_spec.rb b/spec/services/precompute_feed_service_spec.rb index 18ba00244..54e0d94ee 100644 --- a/spec/services/precompute_feed_service_spec.rb +++ b/spec/services/precompute_feed_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe PrecomputeFeedService, type: :service do - subject { PrecomputeFeedService.new } + subject { described_class.new } describe 'call' do let(:account) { Fabricate(:account) } diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb index 399800b2a..a28b6db40 100644 --- a/spec/services/process_mentions_service_spec.rb +++ b/spec/services/process_mentions_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe ProcessMentionsService, type: :service do - subject { ProcessMentionsService.new } + subject { described_class.new } let(:account) { Fabricate(:account, username: 'alice') } diff --git a/spec/services/purge_domain_service_spec.rb b/spec/services/purge_domain_service_spec.rb index 310affa5e..89ab4d8d9 100644 --- a/spec/services/purge_domain_service_spec.rb +++ b/spec/services/purge_domain_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe PurgeDomainService, type: :service do - subject { PurgeDomainService.new } + subject { described_class.new } let!(:old_account) { Fabricate(:account, domain: 'obsolete.org') } let!(:old_status1) { Fabricate(:status, account: old_account) } diff --git a/spec/services/reblog_service_spec.rb b/spec/services/reblog_service_spec.rb index 69500848d..7b85e37ed 100644 --- a/spec/services/reblog_service_spec.rb +++ b/spec/services/reblog_service_spec.rb @@ -6,7 +6,7 @@ RSpec.describe ReblogService, type: :service do let(:alice) { Fabricate(:account, username: 'alice') } context 'when creates a reblog with appropriate visibility' do - subject { ReblogService.new } + subject { described_class.new } let(:visibility) { :public } let(:reblog_visibility) { :public } @@ -62,7 +62,7 @@ RSpec.describe ReblogService, type: :service do end context 'with ActivityPub' do - subject { ReblogService.new } + subject { described_class.new } let(:bob) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } let(:status) { Fabricate(:status, account: bob) } diff --git a/spec/services/reject_follow_service_spec.rb b/spec/services/reject_follow_service_spec.rb index be9363d84..d28104b2c 100644 --- a/spec/services/reject_follow_service_spec.rb +++ b/spec/services/reject_follow_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe RejectFollowService, type: :service do - subject { RejectFollowService.new } + subject { described_class.new } let(:sender) { Fabricate(:account, username: 'alice') } diff --git a/spec/services/remove_from_followers_service_spec.rb b/spec/services/remove_from_followers_service_spec.rb index 21cea2e4f..1b29cdcbe 100644 --- a/spec/services/remove_from_followers_service_spec.rb +++ b/spec/services/remove_from_followers_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe RemoveFromFollowersService, type: :service do - subject { RemoveFromFollowersService.new } + subject { described_class.new } let(:bob) { Fabricate(:account, username: 'bob') } diff --git a/spec/services/remove_status_service_spec.rb b/spec/services/remove_status_service_spec.rb index a836109a0..77b01d307 100644 --- a/spec/services/remove_status_service_spec.rb +++ b/spec/services/remove_status_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe RemoveStatusService, type: :service do - subject { RemoveStatusService.new } + subject { described_class.new } let!(:alice) { Fabricate(:account) } let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com') } diff --git a/spec/services/unallow_domain_service_spec.rb b/spec/services/unallow_domain_service_spec.rb index fbc1d5959..4db718d07 100644 --- a/spec/services/unallow_domain_service_spec.rb +++ b/spec/services/unallow_domain_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe UnallowDomainService, type: :service do - subject { UnallowDomainService.new } + subject { described_class.new } let!(:bad_account) { Fabricate(:account, username: 'badguy666', domain: 'evil.org') } let!(:bad_status1) { Fabricate(:status, account: bad_account, text: 'You suck') } diff --git a/spec/services/unblock_service_spec.rb b/spec/services/unblock_service_spec.rb index 8098d7e6d..86632c393 100644 --- a/spec/services/unblock_service_spec.rb +++ b/spec/services/unblock_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe UnblockService, type: :service do - subject { UnblockService.new } + subject { described_class.new } let(:sender) { Fabricate(:account, username: 'alice') } diff --git a/spec/services/unfollow_service_spec.rb b/spec/services/unfollow_service_spec.rb index a12f01fa5..3e65e610b 100644 --- a/spec/services/unfollow_service_spec.rb +++ b/spec/services/unfollow_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe UnfollowService, type: :service do - subject { UnfollowService.new } + subject { described_class.new } let(:sender) { Fabricate(:account, username: 'alice') } diff --git a/spec/services/unmute_service_spec.rb b/spec/services/unmute_service_spec.rb index 2edb6cfc2..236f837e2 100644 --- a/spec/services/unmute_service_spec.rb +++ b/spec/services/unmute_service_spec.rb @@ -3,5 +3,5 @@ require 'rails_helper' RSpec.describe UnmuteService, type: :service do - subject { UnmuteService.new } + subject { described_class.new } end diff --git a/spec/services/update_account_service_spec.rb b/spec/services/update_account_service_spec.rb index a711a8ae7..6318cc95f 100644 --- a/spec/services/update_account_service_spec.rb +++ b/spec/services/update_account_service_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe UpdateAccountService, type: :service do - subject { UpdateAccountService.new } + subject { described_class.new } describe 'switching form locked to unlocked accounts' do let(:account) { Fabricate(:account, locked: true) } diff --git a/spec/validators/note_length_validator_spec.rb b/spec/validators/note_length_validator_spec.rb index 390ac8d90..e45d221d7 100644 --- a/spec/validators/note_length_validator_spec.rb +++ b/spec/validators/note_length_validator_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' describe NoteLengthValidator do - subject { NoteLengthValidator.new(attributes: { note: true }, maximum: 500) } + subject { described_class.new(attributes: { note: true }, maximum: 500) } describe '#validate' do it 'adds an error when text is over 500 characters' do From 9a653899e907ff7c1dcec3a6faf868075976974e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Jun 2023 14:16:41 +0200 Subject: [PATCH 56/92] Update dependency redis to v4.6.7 (#25300) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8839ed68d..76414138a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1880,10 +1880,10 @@ resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71" integrity sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg== -"@redis/client@1.5.6": - version "1.5.6" - resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.5.6.tgz#869cc65718d7d5493ef655a71dc40f3bc64a1b28" - integrity sha512-dFD1S6je+A47Lj22jN/upVU2fj4huR7S9APd7/ziUXsIXDL+11GPYti4Suv5y8FuXaN+0ZG4JF+y1houEJ7ToA== +"@redis/client@1.5.8": + version "1.5.8" + resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.5.8.tgz#a375ba7861825bd0d2dc512282b8bff7b98dbcb1" + integrity sha512-xzElwHIO6rBAqzPeVnCzgvrnBEcFL1P0w8P65VNLRkdVW8rOE58f52hdj0BDgmsdOm4f1EoXPZtH4Fh7M/qUpw== dependencies: cluster-key-slot "1.1.2" generic-pool "3.9.0" @@ -1899,10 +1899,10 @@ resolved "https://registry.yarnpkg.com/@redis/json/-/json-1.0.4.tgz#f372b5f93324e6ffb7f16aadcbcb4e5c3d39bda1" integrity sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw== -"@redis/search@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.1.2.tgz#6a8f66ba90812d39c2457420f859ce8fbd8f3838" - integrity sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA== +"@redis/search@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.1.3.tgz#b5a6837522ce9028267fe6f50762a8bcfd2e998b" + integrity sha512-4Dg1JjvCevdiCBTZqjhKkGoC5/BcB7k9j99kdMnaXFXg8x4eyOIVg9487CMv7/BUVkFLZCaIh8ead9mU15DNng== "@redis/time-series@1.0.4": version "1.0.4" @@ -10001,15 +10001,15 @@ redent@^3.0.0: strip-indent "^3.0.0" redis@^4.6.5: - version "4.6.5" - resolved "https://registry.yarnpkg.com/redis/-/redis-4.6.5.tgz#f32fbde44429e96f562bb0c9b1db0143ab8cfa4f" - integrity sha512-O0OWA36gDQbswOdUuAhRL6mTZpHFN525HlgZgDaVNgCJIAZR3ya06NTESb0R+TUZ+BFaDpz6NnnVvoMx9meUFg== + version "4.6.7" + resolved "https://registry.yarnpkg.com/redis/-/redis-4.6.7.tgz#c73123ad0b572776223f172ec78185adb72a6b57" + integrity sha512-KrkuNJNpCwRm5vFJh0tteMxW8SaUzkm5fBH7eL5hd/D0fAkzvapxbfGPP/r+4JAXdQuX7nebsBkBqA2RHB7Usw== dependencies: "@redis/bloom" "1.2.0" - "@redis/client" "1.5.6" + "@redis/client" "1.5.8" "@redis/graph" "1.1.0" "@redis/json" "1.0.4" - "@redis/search" "1.1.2" + "@redis/search" "1.1.3" "@redis/time-series" "1.0.4" redux-immutable@^4.0.0: From c66250abf17b4aad1abc1500c14bf9862c078226 Mon Sep 17 00:00:00 2001 From: Nick Schonning Date: Tue, 6 Jun 2023 08:50:51 -0400 Subject: [PATCH 57/92] Autofix Rubocop Regex Style rules (#23690) Co-authored-by: Claire --- .rubocop_todo.yml | 46 ---------------------- app/lib/link_details_extractor.rb | 14 +++---- app/lib/plain_text_formatter.rb | 2 +- app/lib/tag_manager.rb | 6 +-- app/lib/text_formatter.rb | 2 +- app/lib/webfinger_resource.rb | 2 +- app/models/account.rb | 6 +-- app/models/domain_allow.rb | 2 +- app/models/domain_block.rb | 2 +- app/models/site_upload.rb | 2 +- app/models/tag.rb | 2 +- app/services/backup_service.rb | 4 +- app/services/fetch_link_card_service.rb | 2 +- app/services/fetch_oembed_service.rb | 2 +- app/services/search_service.rb | 2 +- config/initializers/rack_attack.rb | 6 +-- config/initializers/twitter_regex.rb | 16 ++++---- config/routes.rb | 6 +-- lib/mastodon/premailer_webpack_strategy.rb | 2 +- lib/paperclip/color_extractor.rb | 2 +- lib/tasks/emojis.rake | 2 +- lib/tasks/mastodon.rake | 8 ++-- 22 files changed, 46 insertions(+), 92 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 1a16472bd..0d0fbfac9 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1412,52 +1412,6 @@ Style/RedundantFetchBlock: - 'config/initializers/paperclip.rb' - 'config/puma.rb' -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantRegexpCharacterClass: - Exclude: - - 'app/lib/link_details_extractor.rb' - - 'app/lib/tag_manager.rb' - - 'app/models/domain_allow.rb' - - 'app/models/domain_block.rb' - - 'app/services/fetch_oembed_service.rb' - - 'config/initializers/rack_attack.rb' - - 'lib/tasks/emojis.rake' - - 'lib/tasks/mastodon.rake' - -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantRegexpEscape: - Exclude: - - 'app/lib/webfinger_resource.rb' - - 'app/models/account.rb' - - 'app/models/tag.rb' - - 'app/services/fetch_link_card_service.rb' - - 'config/initializers/twitter_regex.rb' - - 'lib/paperclip/color_extractor.rb' - - 'lib/tasks/mastodon.rake' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, AllowInnerSlashes. -# SupportedStyles: slashes, percent_r, mixed -Style/RegexpLiteral: - Exclude: - - 'app/lib/link_details_extractor.rb' - - 'app/lib/plain_text_formatter.rb' - - 'app/lib/tag_manager.rb' - - 'app/lib/text_formatter.rb' - - 'app/models/account.rb' - - 'app/models/domain_allow.rb' - - 'app/models/domain_block.rb' - - 'app/models/site_upload.rb' - - 'app/models/tag.rb' - - 'app/services/backup_service.rb' - - 'app/services/fetch_oembed_service.rb' - - 'app/services/search_service.rb' - - 'config/initializers/rack_attack.rb' - - 'config/initializers/twitter_regex.rb' - - 'config/routes.rb' - - 'lib/mastodon/premailer_webpack_strategy.rb' - - 'lib/tasks/mastodon.rake' - # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. # AllowedMethods: present?, blank?, presence, try, try! diff --git a/app/lib/link_details_extractor.rb b/app/lib/link_details_extractor.rb index dfed69285..f0aeec0b3 100644 --- a/app/lib/link_details_extractor.rb +++ b/app/lib/link_details_extractor.rb @@ -7,15 +7,15 @@ class LinkDetailsExtractor # Some publications wrap their JSON-LD data in their