From 76960f128a764f5105c076813d8ffaa0df985dda 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 bfade8976f..24961cdd9d 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 39ae6bc080..5e6556cb53 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 e282e2ab72..b67c503e95 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 c040edb58b..6e8ddb2279 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 db18635be1..f4a3b9d4b6 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 e6d3060eb3..7e2cd447b9 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 aaa424dca6..e4a8437d0e 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 f5fa53f08e..562151fe24 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 b93399aa91..751015d18d 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 5be163f5a4..c4d4611a2d 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 7ed8f1719d..84eab1cae1 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 3807ce9227..8b4ad0b087 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 1895af912b..6871793026 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 0000000000..cb14acd622 --- /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 d446989ab6..0000000000 --- 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 421cb7fab0..6e57e3ddc4 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 c311ad0489..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 0d4f101c7a..0000000000 --- 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 e008d8f025..3cde1b1ede 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 6d2e5426e4..b2dbfdac0a 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 0000000000..4d5ee3ccf9 --- /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 3912f75c7d..89ae20007b 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 54247e6f66..77187a59ed 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 29fd5cde9b..01ab8f8f4b 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 72c7acbb4e..22e6b01a1f 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 3bec37d1e4..f9fc785618 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 3fa5fef09b..4fe2f18bfb 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 e74bff9cc1..d8aa522d80 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 986d605495..0eb877cc32 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 0000000000..adb5e82ef7 --- /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 b8d5d82c6d..0000000000 --- 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 f2f182c566..bb6ae74c33 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 9c684c277f..77534c9de3 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 b8265a549a..c299bd509b 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 2611f068b9..d89d488546 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 971eafc709d7e6f457362112323bf0585f1d8d29 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 61062fd2c8..3232e12a2b 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 84a1271b8b..3aed807358 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 d2f1712437..7b945a0ea2 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 3026dde0a8..6167b404f0 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 187e04ad17..83a566710d 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 45e7f7e7b0..6c76ddd4dd 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 29851c83bd518f7fe8ba44cf0c550c57047ddfa4 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 2ea4075333..91029f665d 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 fc08de13dd..ce616e8306 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 562e72655c..9daeb3c60f 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 562151fe24..7400111293 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 751015d18d..f76550678e 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 c4d4611a2d..4538db050d 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 84eab1cae1..fba3c5df78 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 cb14acd622..0000000000 --- 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 0000000000..01133ca239 --- /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 6e57e3ddc4..0000000000 --- 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 0000000000..63f45c3047 --- /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 0000000000..1ea77c798e --- /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 0000000000..8a69123174 --- /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 83368fae7d..0000000000 --- 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 dff8a54dac..0000000000 --- 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 6ab306b8cf..0000000000 --- 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 65e09e97f2..0000000000 --- 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 9bb4d4dbf3..ebcc6903f8 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 01ab8f8f4b..f26321c41a 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 22e6b01a1f..da43bba7d6 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 f9fc785618..0f3b84549d 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 42c2b41522..f611812ef6 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 31f2454fee..a08b485fd7 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 3f133bff9a..28208b619b 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 3ae6343635..fd751c0419 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 2d2750c6a902c989fc5bec5ec5f9052c488771df 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 83a566710d..ddda6eaac6 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 1c298d97c5fe42239885d756d3d6183e4f739dc6 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 36856127f2..de34b5ee3c 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 92cffc5a2a..74034f36fd 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 b646a15e26..9c747e595d 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 c7a8838bd710cf175799c15ed2d9cad907be1ee1 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 8b7b634071..b50306deda 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 57f077c4e8..d63a42557f 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 719856d495..1b3dd889c1 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 ab3fa864ab..234f4a601d 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 6a8da6e60d..2c292c42d4 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 e24a587f84bfc117f7fab5c54f320a2a73db1a7d 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 24961cdd9d..91dcd8e60c 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 12ca463422..d1ed30b260 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 bd561df1d2..eff89bdaee 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 c5a55bc27c..7cb973c4b4 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 5d4b6c4d0d..cdce4fd28a 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 93f9bd4181..65cf789ce3 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 ab290912e2..6d67d389d2 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 b8d3ac0e86..3dc6f8f8e5 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 798d8d8b4f..d6c6b62c81 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 8812feb316..1ca3483715 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 f35e13d128..98f2e77090 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 a30666a08b..3bbdd08db8 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 d94a41eb3d..6b3e49eba0 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 602e12793e..85f3f8039d 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 1ed3ae8573..eedbfff9d9 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 363df369d5..d528ab8eb7 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 55d8524dbe..689f051029 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 0000000000..3b27711521 --- /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 0000000000..3a1d97c0c9 --- /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 749c9434d1a84a4b2e8c7a713e38a15d7c6167c7 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 77534c9de3..0000000000 --- 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 a08b485fd7..49e9c7f743 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 c4426198c3d956c9a2a0829c86ddc08153594d18 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 ecebdb6965..79291b3d08 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 5ed793cdba..f6d6daa3e5 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 c671e23d287580981c06474e0e03d47e4c8f6645 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 62e45a5f30..cff8cb1f88 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 7d04d875c5..bb209b3841 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 93ea1d1e4a..0000000000 --- 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 427661c9ebfb2a808324a14475c2a36600df06e0 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 ba49e480ad..50066572c6 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 9c86b994db2bf16a73439851eecd0f8069072aa3 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 fb481e8a82..eb57a3cd15 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 a04157e69f4d51f9d6664dd18dea07857daf12e8 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 5891da6f6d..205df48d44 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 6debddcf89eea05ab576f5d9b09f72f7130edb13 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 843ca2ec2b..4bbbed2673 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 2faa54b955..b0789cd426 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 f41e8e6f23..f9f3a7c315 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 f6d6daa3e5..09282de7c8 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 ceceb27c7a..d3fd62adec 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 643e6828d2..7423d2d092 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 bd1bdbd24d..7dc96f01b3 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 3e87f71196..6a1b6ea3eb 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 0000000000..cc21a3e315 --- /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 98fa5d6004..35fbb8d2ef 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 79d1f5249e..31b53fd879 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 4c2503d36ccedf2adffba2f28e5f12bcc68fe885 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 1ea77c798e..4fa8b2247c 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 5846344977834675e70952af91ed7598946486e8 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 cff8cb1f88..ad164af1e4 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 bb209b3841..a9919bd3a2 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 94f9888d571cae313e8bbb1c450bf132bdede7aa 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 3232e12a2b..9ed6b583b5 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 a274d79f586482d5c349d9687681dfe40fb1f456 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 50066572c6..cf1d612f3d 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 c74040abfa94b323a6ae4947e08e5f19123544d4 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 4eac8e611e..f8eb9d7bfb 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 1ede1a56e4..b478213808 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 f1cf82cf27..100692a17b 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 0000000000..9fc953cb3e --- /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 91bcce6551..42aba8e213 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 122807cdcd..a14c3e7c16 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 e1349c2294..cd077ff863 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 7ddf3378cd..fc5e49a966 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 0000000000..106717f97b --- /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 0000000000..f9f6430ca0 --- /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 0000000000..1722c4c616 --- /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 0000000000..7e2bb9ac0b --- /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 0000000000..ee14917330 --- /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 0000000000..d6b581a9bb --- /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 0000000000..65d04cfedd --- /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 0000000000..721d24fa18 --- /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 0000000000..3054716816 --- /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 21f904b344e57f68dd86b91d7228bdae37e75624 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 dbc0400e8e..185db0732a 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 f2dbbcdec5b1056b66b3c15e22bafd8b3c90784a 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 f4f9177996..a05b67440c 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 14dc8a6a5fa0f4b9af365641ed6dfab7e9b2768d 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 14a61de88c..3d081fdd90 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 dc0f5492c9..378c6754d9 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 f2088ffb30..e213348fbc 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 779883e031..2d4b5f56b0 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 c1f7189bfe..9da3d53e34 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 1b38b40c55..8c71c66145 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 71191f1a22..6837c14c82 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 4b80a0c8c3..c395c46341 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 0000000000..969065f73f --- /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 4ab746c8fa..780db75a10 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 11f229602e..e6378b8021 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 0000000000..55164ed88a --- /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 29a157491e..8e414963f3 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 ebf789c1b3..c627e6cede 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 335e3c7321..42f33dfc35 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 711a2aff05..c103307f97 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 f0ffd39cfb..62fcf84ac1 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 c1425ecdb9..df4cfe207b 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 0000000000..e98c830598 --- /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 0000000000..fe82f8219b --- /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 0000000000..deed64ae88 --- /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 0000000000..cb98df2dc2 --- /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 0000000000..938b67afa3 --- /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 0000000000..e09a2b04e5 --- /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 0000000000..869e937445 --- /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 8f34072fc0ea76ae51a506b0c0ae88a1efacd061 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 ea863f5d18..0f3b85388c 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 0000000000..8b5746ad30 --- /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 9a0c4c8a7e..53a84ecb53 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 27de4740ca..653a258667 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 b329cae791..b11ac478a4 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 d4854f1869..635b6f4113 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 6b053a9dc1..dc1f720220 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 0aebcd4761cb45fbcc8b38f6d464998d0a818955 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 f10f470279..e123c03d66 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 a66827421b0422b780f1bcdb9900d39de86ec3d5 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 cf1d612f3d..a263d673de 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 09b05d7e8ba4f072cfaad921bddf37a5de35ab02 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 4eae51c7b5..b8dae79939 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 7d001c4cbc..782e460a42 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 a8905160f5..c2d3135d92 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 261f302c05..ca351c39b2 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 872aed9998..fc27f71473 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 6b48fc28bb..b32a58e990 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 8e745a234c125cfd56a558c9989440dbfd8435a4 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 c8b287f90d..1a16472bdb 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 0000000000..525479be81 --- /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 6d9afa4740..5818de7119 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 560617ed7d..b6c910171d 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 31b53fd879..5bfe11f6aa 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 fb9740ce3f..0e20f26c7c 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 25fe9ed379..e88631e475 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 8de2905414..38203a55f7 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 8ec6dd205e..2cad04fccd 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 73c751def1..bf364b6253 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 a4f6c145ab..702aa1c354 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 a16aa500cf..4e8727ca39 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 f3246d04c5..10bd579363 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 519b9a97a5..d76edddd51 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 6e9c608ab0..d966bffa9b 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 de3410fd58..8249503c59 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 e123c03d66..67f53fa785 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 a7232eb6b4..9d9c748877 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 a863678a33..75468898d2 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 9e69570a01..ef7fbdefcd 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 79c0048f9f..c7743183cc 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 59155781c7..2fca1e1c14 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 1dae40a739..3605f0b9bf 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 becc748244..4d4bc748f7 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 795491546c..0dd9264e00 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 7c0f37a06f..b3e855c122 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 8269c45797..4b0852f081 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 51c6aa5cb0..052a06e5ca 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 accce10f86..bba585cec6 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 d4a9293115..9689bce9ee 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 52ce0847c4..660b2e92ac 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 84ff82c789..3141c52c05 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 d3e0ac63a4..554e7efb68 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 03d18250b2..90abdfea76 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 d59060bd5c..5c2ba54e00 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 a62fa004a1..7746c8cd78 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 7ea47baef2..68f5378918 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 4360808b50..14c24c70cc 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 528639943c..e399e88f37 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 868bc2a582..ac7484d96d 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 a72c6941e9..93d31b69d5 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 0ec0c27362..cd8f29dddd 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 b75f923729..9bfaa35807 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 63d9e2a0f4..d07645ab6b 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 9bedf37444..c0cd01315f 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 0ab97b8ce9..93722a15bc 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 75f07f5adf..5f7c2e8da0 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 670ac652fb..5a15ba7418 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 9875075796..2a00409a41 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 613ae203ed..782c235c41 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 c9521e3c87..c2ad0d7173 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 7f8e5855fa..32ba4409c3 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 f577122710..76ef5391f0 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 18ba00244d..54e0d94ee0 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 399800b2a6..a28b6db409 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 310affa5e3..89ab4d8d9f 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 69500848d9..7b85e37ed8 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 be9363d846..d28104b2c7 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 21cea2e4f8..1b29cdcbea 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 a836109a0d..77b01d3072 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 fbc1d59592..4db718d074 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 8098d7e6d0..86632c3938 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 a12f01fa5d..3e65e610ba 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 2edb6cfc28..236f837e2d 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 a711a8ae73..6318cc95fb 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 390ac8d904..e45d221d76 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 26db476771fe38e9c868169b631380e3b4290f83 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 8839ed68d7..76414138af 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 ef344388c5baee2a8eb8f383216782b0211a2867 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 1a16472bdb..0d0fbfac91 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 dfed69285f..f0aeec0b3e 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