1
0
Fork 0
forked from gitea/nas

Merge commit 'aea67d448b' into kb_migration

This commit is contained in:
KMY 2023-06-04 10:15:09 +09:00
commit bb2e964dca
74 changed files with 747 additions and 870 deletions

View file

@ -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',

View file

@ -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

View file

@ -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

View file

@ -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
@ -213,3 +214,6 @@ Style/TrailingCommaInArrayLiteral:
# https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainhashliteral
Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: 'comma'
Style/MiddleDot:
Enabled: true

View file

@ -5,7 +5,7 @@ ruby '>= 3.0.0'
gem 'pkg-config', '~> 1.5'
gem 'puma', '~> 6.2'
gem 'puma', '~> 6.3'
gem 'rails', '~> 6.1.7'
gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.2'

View file

@ -501,7 +501,7 @@ GEM
premailer (~> 1.7, >= 1.7.9)
private_address_check (0.5.0)
public_suffix (5.0.1)
puma (6.2.2)
puma (6.3.0)
nio4r (~> 2.0)
pundit (2.3.0)
activesupport (>= 3.0.0)
@ -649,7 +649,7 @@ GEM
redis (>= 4.5.0, < 5)
sidekiq-bulk (0.2.0)
sidekiq
sidekiq-scheduler (5.0.2)
sidekiq-scheduler (5.0.3)
rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8)
tilt (>= 1.4.0)
@ -847,7 +847,7 @@ DEPENDENCIES
premailer-rails
private_address_check (~> 0.5)
public_suffix (~> 5.0)
puma (~> 6.2)
puma (~> 6.3)
pundit (~> 2.3)
rack (~> 2.2.7)
rack-attack (~> 6.6)

View file

@ -12,6 +12,7 @@ class Settings::ImportsController < Settings::BaseController
muting: 'muted_accounts_failures.csv',
domain_blocking: 'blocked_domains_failures.csv',
bookmarks: 'bookmarks_failures.csv',
lists: 'lists_failures.csv',
}.freeze
TYPE_TO_HEADERS_MAP = {
@ -20,6 +21,7 @@ class Settings::ImportsController < Settings::BaseController
muting: ['Account address', 'Hide notifications'],
domain_blocking: false,
bookmarks: false,
lists: false,
}.freeze
def index
@ -49,6 +51,8 @@ class Settings::ImportsController < Settings::BaseController
csv << [row.data['domain']]
when :bookmarks
csv << [row.data['uri']]
when :lists
csv << [row.data['list_name'], row.data['acct']]
end
end
end

View file

@ -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

View file

@ -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
@ -125,6 +125,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
},
};
});
};
/**
* @param {Function} dispatch

View file

@ -144,7 +144,7 @@ class Account extends ImmutablePureComponent {
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
if (firstVerifiedField) {
verification = <>· <VerifiedBadge link={firstVerifiedField.get('value')} /></>;
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
}
return (
@ -155,9 +155,13 @@ class Account extends ImmutablePureComponent {
<Avatar account={account} size={size} />
</div>
<div>
<div className='account__contents'>
<DisplayName account={account} />
{!minimal && <><ShortNumber value={account.get('followers_count')} isHide={account.getIn(['other_settings', 'hide_followers_count']) || false} renderer={counterRenderer('followers')} /> {verification} {muteTimeRemaining}</>}
{!minimal && (
<div className='account__details'>
<ShortNumber value={account.get('followers_count')} isHide={account.getIn(['other_settings', 'hide_followers_count']) || false} renderer={counterRenderer('followers')} /> {verification} {muteTimeRemaining}
</div>
)}
</div>
</Link>

View file

@ -57,9 +57,9 @@ class Poll extends ImmutablePureComponent {
};
static getDerivedStateFromProps (props, state) {
const { poll, intl } = props;
const { poll } = props;
const expires_at = poll.get('expires_at');
const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < intl.now();
const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < Date.now();
return (expired === state.expired) ? null : { expired };
}
@ -76,10 +76,10 @@ class Poll extends ImmutablePureComponent {
}
_setupTimer () {
const { poll, intl } = this.props;
const { poll } = this.props;
clearTimeout(this._timer);
if (!this.state.expired) {
const delay = (new Date(poll.get('expires_at'))).getTime() - intl.now();
const delay = (new Date(poll.get('expires_at'))).getTime() - Date.now();
this._timer = setTimeout(() => {
this.setState({ expired: true });
}, delay);

View file

@ -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 (
<IntlProvider locale={locale} messages={messages} onError={onProviderError}>
<IntlProvider>
{children}
</IntlProvider>
);

View file

@ -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 (
<IntlProvider locale={locale} messages={messages} onError={onProviderError}>
<IntlProvider>
<Provider store={store}>
<Compose />
</Provider>

View file

@ -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';
@ -17,11 +15,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);
@ -42,10 +38,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,
@ -81,10 +73,8 @@ export default class Mastodon extends PureComponent {
}
render () {
const { locale } = this.props;
return (
<IntlProvider locale={locale} messages={messages} onError={onProviderError}>
<IntlProvider>
<ReduxProvider store={store}>
<ErrorBoundary>
<BrowserRouter>

View file

@ -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 (
<IntlProvider locale={locale} messages={messages} onError={onProviderError}>
<IntlProvider>
<>
{[].map.call(components, (component, i) => {
const componentName = component.getAttribute('data-component');

View file

@ -247,7 +247,7 @@ class DetailedStatus extends ImmutablePureComponent {
} else if (this.context.router) {
reblogLink = (
<>
·
{' · '}
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
<Icon id={reblogIcon} />
<span className='detailed-status__reblogs'>
@ -259,7 +259,7 @@ class DetailedStatus extends ImmutablePureComponent {
} else {
reblogLink = (
<>
·
{' · '}
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
<Icon id={reblogIcon} />
<span className='detailed-status__reblogs'>
@ -313,7 +313,7 @@ class DetailedStatus extends ImmutablePureComponent {
if (status.get('edited_at')) {
edited = (
<>
·
{' · '}
<EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} />
</>
);

View file

@ -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 });
}

View file

@ -0,0 +1,22 @@
export interface LocaleData {
locale: string;
messages: Record<string, string>;
}
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;
}

View file

@ -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);
}

View file

@ -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';

View file

@ -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<React.ComponentProps<typeof BaseIntlProvider>, '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 (
<BaseIntlProvider
locale={locale}
messages={messages}
onError={onProviderError}
{...props}
>
{children}
</BaseIntlProvider>
);
};

View file

@ -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<HTMLElement>('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 });
});
}

View file

@ -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";
}
```

View file

@ -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;

View file

@ -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: "daquí {0} an",
other: "daquí {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: "daquí {0} mes",
other: "daquí {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: "daquí {0} jorn",
other: "daquí {0} jorns",
},
past: {
one: "fa {0} jorn",
other: "fa {0} jorns",
},
},
},
hour: {
displayName: "ora",
relativeTime: {
future: {
one: "daquí {0} ora",
other: "daquí {0} oras",
},
past: {
one: "fa {0} ora",
other: "fa {0} oras",
},
},
},
minute: {
displayName: "minuta",
relativeTime: {
future: {
one: "daquí {0} minuta",
other: "daquí {0} minutas",
},
past: {
one: "fa {0} minuta",
other: "fa {0} minutas",
},
},
},
second: {
displayName: "segonda",
relative: {
0: "ara",
},
relativeTime: {
future: {
one: "daquí {0} segonda",
other: "daquí {0} segondas",
},
past: {
one: "fa {0} segonda",
other: "fa {0} segondas",
},
},
},
},
}];
export default rules;

View file

@ -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;

View file

@ -239,14 +239,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 (
<AdminComponent locale={locale}>
<AdminComponent>
<Component {...componentProps} />
</AdminComponent>,
);

View file

@ -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);
});

View file

@ -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';

View file

@ -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);
});

View file

@ -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;

View file

@ -7949,13 +7949,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;

View file

@ -137,6 +137,10 @@ code {
color: $secondary-text-color;
margin-bottom: 30px;
&.invited-by {
margin-bottom: 15px;
}
a {
color: $highlight-text-color;
}

View file

@ -30,6 +30,7 @@ class BulkImport < ApplicationRecord
muting: 2,
domain_blocking: 3,
bookmarks: 4,
lists: 5,
}
enum state: {

View file

@ -18,6 +18,7 @@ class Form::Import
muting: ['Account address', 'Hide notifications'],
domain_blocking: ['#domain'],
bookmarks: ['#uri'],
lists: ['List name', 'Account address'],
}.freeze
KNOWN_FIRST_HEADERS = EXPECTED_HEADERS_BY_TYPE.values.map(&:first).uniq.freeze
@ -30,6 +31,7 @@ class Form::Import
'Hide notifications' => 'hide_notifications',
'#domain' => 'domain',
'#uri' => 'uri',
'List name' => 'list_name',
}.freeze
class EmptyFileError < StandardError; end
@ -48,6 +50,7 @@ class Form::Import
return :muting if data.original_filename&.start_with?('mutes') || data.original_filename&.start_with?('muted_accounts')
return :domain_blocking if data.original_filename&.start_with?('domain_blocks') || data.original_filename&.start_with?('blocked_domains')
return :bookmarks if data.original_filename&.start_with?('bookmarks')
return :lists if data.original_filename&.start_with?('lists')
end
# Whether the uploaded CSV file seems to correspond to a different import type than the one selected
@ -76,14 +79,16 @@ class Form::Import
private
def default_csv_header
def default_csv_headers
case type.to_sym
when :following, :blocking, :muting
'Account address'
['Account address']
when :domain_blocking
'#domain'
['#domain']
when :bookmarks
'#uri'
['#uri']
when :lists
['List name', 'Account address']
end
end
@ -98,7 +103,7 @@ class Form::Import
field&.split(',')&.map(&:strip)&.presence
when 'Account address'
field.strip.gsub(/\A@/, '')
when '#domain', '#uri'
when '#domain', '#uri', 'List name'
field.strip
else
field
@ -109,7 +114,7 @@ class Form::Import
@csv_data.take(1) # Ensure the headers are read
raise EmptyFileError if @csv_data.headers == true
@csv_data = CSV.open(data.path, encoding: 'UTF-8', skip_blanks: true, headers: [default_csv_header], converters: csv_converter) unless KNOWN_FIRST_HEADERS.include?(@csv_data.headers&.first)
@csv_data = CSV.open(data.path, encoding: 'UTF-8', skip_blanks: true, headers: default_csv_headers, converters: csv_converter) unless KNOWN_FIRST_HEADERS.include?(@csv_data.headers&.first)
@csv_data
end
@ -133,7 +138,7 @@ class Form::Import
def validate_data
return if data.nil?
return errors.add(:data, I18n.t('imports.errors.too_large')) if data.size > FILE_SIZE_LIMIT
return errors.add(:data, I18n.t('imports.errors.incompatible_type')) unless csv_data.headers.include?(default_csv_header)
return errors.add(:data, I18n.t('imports.errors.incompatible_type')) unless default_csv_headers.all? { |header| csv_data.headers.include?(header) }
errors.add(:data, I18n.t('imports.errors.over_rows_processing_limit', count: ROWS_PROCESSING_LIMIT)) if csv_row_count > ROWS_PROCESSING_LIMIT

View file

@ -7,7 +7,7 @@ class BulkImportRowService
@type = row.bulk_import.type.to_sym
case @type
when :following, :blocking, :muting
when :following, :blocking, :muting, :lists
target_acct = @data['acct']
target_domain = domain(target_acct)
@target_account = stoplight_wrap_request(target_domain) { ResolveAccountService.new.call(target_acct, { check_delivery_availability: true }) }
@ -33,6 +33,12 @@ class BulkImportRowService
return false unless StatusPolicy.new(@account, @target_status).show?
@account.bookmarks.find_or_create_by!(status: @target_status)
when :lists
list = @account.owned_lists.find_or_create_by!(title: @data['list_name'])
FollowService.new.call(@account, @target_account) unless @account.id == @target_account.id
list.accounts << @target_account
end
true

View file

@ -16,6 +16,8 @@ class BulkImportService < BaseService
import_domain_blocks!
when :bookmarks
import_bookmarks!
when :lists
import_lists!
end
@import.update!(state: :finished, finished_at: Time.now.utc) if @import.processed_items == @import.total_items
@ -157,4 +159,24 @@ class BulkImportService < BaseService
[row.id]
end
end
def import_lists!
rows = @import.rows.to_a
if @import.overwrite?
included_lists = rows.map { |row| row.data['list_name'] }.uniq
@account.owned_lists.where.not(title: included_lists).destroy_all
# As list membership changes do not retroactively change timeline
# contents, simplify things by just clearing everything
@account.owned_lists.find_each do |list|
list.list_accounts.destroy_all
end
end
Import::RowWorker.push_bulk(rows) do |row|
[row.id]
end
end
end

View file

@ -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 })

View file

@ -29,11 +29,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')

View file

@ -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

View file

@ -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 }

View file

@ -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}")

View file

@ -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)

View file

@ -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

View file

@ -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')

View file

@ -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')

View file

@ -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')

View file

@ -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)

View file

@ -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 %>

View file

@ -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 %>

View file

@ -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 %>

View file

@ -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'

View file

@ -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|

View file

@ -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))

View file

@ -3,7 +3,7 @@
= simple_form_for @import, url: settings_imports_path do |f|
.field-group
= f.input :type, as: :grouped_select, collection: { constructive: %i(following bookmarks), destructive: %i(muting blocking domain_blocking) }, wrapper: :with_block_label, include_blank: false, label_method: ->(type) { I18n.t("imports.types.#{type}") }, group_label_method: ->(group) { I18n.t("imports.type_groups.#{group.first}") }, group_method: :last, hint: t('imports.preface')
= f.input :type, as: :grouped_select, collection: { constructive: %i(following bookmarks lists), destructive: %i(muting blocking domain_blocking) }, wrapper: :with_block_label, include_blank: false, label_method: ->(type) { I18n.t("imports.types.#{type}") }, group_label_method: ->(group) { I18n.t("imports.type_groups.#{group.first}") }, group_method: :last, hint: t('imports.preface')
.fields-row
.fields-group.fields-row__column.fields-row__column-6

View file

@ -30,9 +30,18 @@ module KmyblueComponent
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(KmyblueComponent)
SimpleForm.include_component(WarningHintComponent)
SimpleForm.setup do |config|
# Wrappers are used by the form builder to generate a
@ -114,6 +123,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

View file

@ -25,7 +25,7 @@ module Twitter::TwitterText
\)
/iox
UCHARS = '\u{A0}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFEF}\u{10000}-\u{1FFFD}\u{20000}-\u{2FFFD}\u{30000}-\u{3FFFD}\u{40000}-\u{4FFFD}\u{50000}-\u{5FFFD}\u{60000}-\u{6FFFD}\u{70000}-\u{7FFFD}\u{80000}-\u{8FFFD}\u{90000}-\u{9FFFD}\u{A0000}-\u{AFFFD}\u{B0000}-\u{BFFFD}\u{C0000}-\u{CFFFD}\u{D0000}-\u{DFFFD}\u{E1000}-\u{EFFFD}\u{E000}-\u{F8FF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}'
REGEXEN[:valid_url_query_chars] = /[a-z0-9!?\*'\(\);:&=\+\$\/%#\[\]\-_\.,~|@#{UCHARS}]/iou
REGEXEN[:valid_url_query_chars] = /[a-z0-9!?\*'\(\);:&=\+\$\/%#\[\]\-_\.,~|@\^#{UCHARS}]/iou
REGEXEN[:valid_url_query_ending_chars] = /[a-z0-9_&=#\/\-#{UCHARS}]/iou
REGEXEN[:valid_url_path] = /(?:
(?:

View file

@ -1126,8 +1126,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:

View file

@ -82,7 +82,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.

View file

@ -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);

View file

@ -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',
],

View file

@ -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

View file

@ -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

View file

@ -4,16 +4,39 @@ require_relative '../../../config/boot'
require_relative '../../../config/environment'
require 'thor'
require_relative 'helper'
require_relative 'progress_helper'
module Mastodon
module CLI
class Base < Thor
include CLI::Helper
include ProgressHelper
def self.exit_on_failure?
true
end
private
def pastel
@pastel ||= Pastel.new
end
def dry_run?
options[:dry_run]
end
def dry_run_mode_suffix
dry_run? ? ' (DRY RUN)' : ''
end
def reset_connection_pools!
ActiveRecord::Base.establish_connection(
ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).first.configuration_hash
.dup
.tap { |config| config['pool'] = options[:concurrency] + 1 }
)
RedisConfiguration.establish_pool(options[:concurrency])
end
end
end
end

View file

@ -9,23 +9,19 @@ HttpLog.configuration.logger = dev_null
Paperclip.options[:log] = false
Chewy.logger = dev_null
module Mastodon::CLI
module Helper
def dry_run?
options[:dry_run]
end
require 'ruby-progressbar/outputs/null'
def dry_run_mode_suffix
dry_run? ? ' (DRY RUN)' : ''
end
module Mastodon::CLI
module ProgressHelper
PROGRESS_FORMAT = '%c/%u |%b%i| %e'
def create_progress_bar(total = nil)
ProgressBar.create(total: total, format: '%c/%u |%b%i| %e')
end
def reset_connection_pools!
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env].dup.tap { |config| config['pool'] = options[:concurrency] + 1 })
RedisConfiguration.establish_pool(options[:concurrency])
ProgressBar.create(
{
total: total,
format: PROGRESS_FORMAT,
}.merge(progress_output_options)
)
end
def parallelize_with_progress(scope)
@ -82,8 +78,10 @@ module Mastodon::CLI
[total.value, aggregate.value]
end
def pastel
@pastel ||= Pastel.new
private
def progress_output_options
Rails.env.test? ? { output: ProgressBar::Outputs::Null } : {}
end
end
end

View file

@ -29,15 +29,7 @@ module Mastodon::CLI
database will be imported into the indices, unless overridden with --no-import.
LONG_DESC
def deploy
if options[:concurrency] < 1
say('Cannot run with this concurrency setting, must be at least 1', :red)
exit(1)
end
if options[:batch_size] < 1
say('Cannot run with this batch_size setting, must be at least 1', :red)
exit(1)
end
verify_deploy_options!
indices = if options[:only]
options[:only].map { |str| "#{str.camelize}Index".constantize }
@ -98,5 +90,26 @@ module Mastodon::CLI
say("Indexed #{added} records, de-indexed #{removed}", :green, true)
end
private
def verify_deploy_options!
verify_deploy_concurrency!
verify_deploy_batch_size!
end
def verify_deploy_concurrency!
return unless options[:concurrency] < 1
say('Cannot run with this concurrency setting, must be at least 1', :red)
exit(1)
end
def verify_deploy_batch_size!
return unless options[:batch_size] < 1
say('Cannot run with this batch_size setting, must be at least 1', :red)
exit(1)
end
end
end

View file

@ -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",
@ -49,6 +48,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",
@ -138,12 +138,12 @@
"webpack-cli": "^3.3.12",
"webpack-merge": "^5.9.0",
"wicg-inert": "^3.1.2",
"workbox-expiration": "^6.6.0",
"workbox-precaching": "^6.6.0",
"workbox-routing": "^6.6.0",
"workbox-strategies": "^6.6.0",
"workbox-webpack-plugin": "^6.6.0",
"workbox-window": "^6.6.0",
"workbox-expiration": "^7.0.0",
"workbox-precaching": "^7.0.0",
"workbox-routing": "^7.0.0",
"workbox-strategies": "^7.0.0",
"workbox-webpack-plugin": "^7.0.0",
"workbox-window": "^7.0.0",
"ws": "^8.12.1"
},
"devDependencies": {

3
spec/fixtures/files/lists.csv vendored Normal file
View file

@ -0,0 +1,3 @@
Mastodon project,gargron@example.com
Mastodon project,mastodon@example.com
test,foo@example.com
1 Mastodon project gargron@example.com
2 Mastodon project mastodon@example.com
3 test foo@example.com

View file

@ -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

View file

@ -924,4 +924,78 @@ describe Mastodon::CLI::Accounts do
end
end
end
describe '#rotate' do
context 'when neither username nor --all option are given' do
it 'exits with an error message' do
expect { cli.rotate }.to output(
a_string_including('No account(s) given')
).to_stdout
.and raise_error(SystemExit)
end
end
context 'when a username is given' do
let(:account) { Fabricate(:account) }
it 'correctly rotates keys for the specified account' do
old_private_key = account.private_key
old_public_key = account.public_key
cli.rotate(account.username)
account.reload
expect(account.private_key).to_not eq(old_private_key)
expect(account.public_key).to_not eq(old_public_key)
end
it 'broadcasts the new keys for the specified account' do
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in)
cli.rotate(account.username)
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_in).with(anything, account.id, anything).once
end
context 'when the given username is not found' do
it 'exits with an error message when the specified username is not found' do
expect { cli.rotate('non_existent_username') }.to output(
a_string_including('No such account')
).to_stdout
.and raise_error(SystemExit)
end
end
end
context 'when --all option is provided' do
let(:accounts) { Fabricate.times(3, :account) }
let(:options) { { all: true } }
before do
allow(Account).to receive(:local).and_return(Account.where(id: accounts.map(&:id)))
cli.options = { all: true }
end
it 'correctly rotates keys for all local accounts' do
old_private_keys = accounts.map(&:private_key)
old_public_keys = accounts.map(&:public_key)
cli.rotate
accounts.each(&:reload)
expect(accounts.map(&:private_key)).to_not eq(old_private_keys)
expect(accounts.map(&:public_key)).to_not eq(old_public_keys)
end
it 'broadcasts the new keys for each account' do
allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in)
cli.rotate
accounts.each do |account|
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_in).with(anything, account.id, anything).once
end
end
end
end
end

View file

@ -86,6 +86,7 @@ RSpec.describe Form::Import do
it_behaves_like 'too many CSV rows', 'muting', 'imports.txt', 1
it_behaves_like 'too many CSV rows', 'domain_blocking', 'domain_blocks.csv', 2
it_behaves_like 'too many CSV rows', 'bookmarks', 'bookmark-imports.txt', 3
it_behaves_like 'too many CSV rows', 'lists', 'lists.csv', 2
# Importing list of addresses with no headers into various types
it_behaves_like 'valid import', 'following', 'imports.txt'
@ -98,6 +99,9 @@ RSpec.describe Form::Import do
# Importing bookmarks list with no headers into expected type
it_behaves_like 'valid import', 'bookmarks', 'bookmark-imports.txt'
# Importing lists with no headers into expected type
it_behaves_like 'valid import', 'lists', 'lists.csv'
# Importing followed accounts with headers into various compatible types
it_behaves_like 'valid import', 'following', 'following_accounts.csv'
it_behaves_like 'valid import', 'blocking', 'following_accounts.csv'
@ -273,6 +277,12 @@ RSpec.describe Form::Import do
{ 'acct' => 'user@test.com', 'hide_notifications' => false },
]
it_behaves_like 'on successful import', 'lists', 'merge', 'lists.csv', [
{ 'acct' => 'gargron@example.com', 'list_name' => 'Mastodon project' },
{ 'acct' => 'mastodon@example.com', 'list_name' => 'Mastodon project' },
{ 'acct' => 'foo@example.com', 'list_name' => 'test' },
]
# Based on the bug report 20571 where UTF-8 encoded domains were rejecting import of their users
#
# https://github.com/mastodon/mastodon/issues/20571

View file

@ -91,5 +91,77 @@ RSpec.describe BulkImportRowService do
end
end
end
context 'when importing a list row' do
let(:import_type) { 'lists' }
let(:target_account) { Fabricate(:account) }
let(:data) do
{ 'acct' => target_account.acct, 'list_name' => 'my list' }
end
shared_examples 'common behavior' do
context 'when the target account is already followed' do
before do
account.follow!(target_account)
end
it 'returns true' do
expect(subject.call(import_row)).to be true
end
it 'adds the target account to the list' do
expect { subject.call(import_row) }.to change { ListAccount.joins(:list).exists?(account_id: target_account.id, list: { title: 'my list' }) }.from(false).to(true)
end
end
context 'when the user already requested to follow the target account' do
before do
account.request_follow!(target_account)
end
it 'returns true' do
expect(subject.call(import_row)).to be true
end
it 'adds the target account to the list' do
expect { subject.call(import_row) }.to change { ListAccount.joins(:list).exists?(account_id: target_account.id, list: { title: 'my list' }) }.from(false).to(true)
end
end
context 'when the target account is neither followed nor requested' do
it 'returns true' do
expect(subject.call(import_row)).to be true
end
it 'adds the target account to the list' do
expect { subject.call(import_row) }.to change { ListAccount.joins(:list).exists?(account_id: target_account.id, list: { title: 'my list' }) }.from(false).to(true)
end
end
context 'when the target account is the user themself' do
let(:target_account) { account }
it 'returns true' do
expect(subject.call(import_row)).to be true
end
it 'adds the target account to the list' do
expect { subject.call(import_row) }.to change { ListAccount.joins(:list).exists?(account_id: target_account.id, list: { title: 'my list' }) }.from(false).to(true)
end
end
end
context 'when the list does not exist yet' do
include_examples 'common behavior'
end
context 'when the list exists' do
before do
Fabricate(:list, account: account, title: 'my list')
end
include_examples 'common behavior'
end
end
end
end

View file

@ -12,6 +12,7 @@ RSpec.describe FetchLinkCardService, type: :service do
stub_request(:get, 'http://example.com/koi8-r').to_return(request_fixture('koi8-r.txt'))
stub_request(:get, 'http://example.com/日本語').to_return(request_fixture('sjis.txt'))
stub_request(:get, 'https://github.com/qbi/WannaCry').to_return(status: 404)
stub_request(:get, 'http://example.com/test?data=file.gpx%5E1').to_return(status: 200)
stub_request(:get, 'http://example.com/test-').to_return(request_fixture('idn.txt'))
stub_request(:get, 'http://example.com/windows-1251').to_return(request_fixture('windows-1251.txt'))
@ -87,6 +88,15 @@ RSpec.describe FetchLinkCardService, type: :service do
expect(a_request(:get, 'http://example.com/sjis')).to_not have_been_made
end
end
context do
let(:status) { Fabricate(:status, text: 'test http://example.com/test?data=file.gpx^1') }
it 'does fetch URLs with a caret in search params' do
expect(a_request(:get, 'http://example.com/test?data=file.gpx')).to_not have_been_made
expect(a_request(:get, 'http://example.com/test?data=file.gpx%5E1')).to have_been_made.once
end
end
end
context 'with a remote status' do

274
yarn.lock
View file

@ -1743,6 +1743,15 @@
"@jridgewell/set-array" "^1.0.0"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/gen-mapping@^0.3.0":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==
dependencies:
"@jridgewell/set-array" "^1.0.1"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping" "^0.3.9"
"@jridgewell/gen-mapping@^0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
@ -1767,6 +1776,14 @@
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
"@jridgewell/source-map@^0.3.2":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.3.tgz#8108265659d4c33e72ffe14e33d6cc5eb59f2fda"
integrity sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==
dependencies:
"@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9"
"@jridgewell/sourcemap-codec@1.4.14":
version "1.4.14"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
@ -3017,9 +3034,9 @@ ansi-regex@^2.0.0:
integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
ansi-regex@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
version "4.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed"
integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==
ansi-regex@^5.0.0, ansi-regex@^5.0.1:
version "5.0.1"
@ -3266,6 +3283,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"
@ -7862,9 +7886,9 @@ loader-utils@^1.2.3, loader-utils@^1.4.0:
json5 "^1.0.1"
loader-utils@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
version "2.0.4"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c"
integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==
dependencies:
big.js "^5.2.2"
emojis-list "^3.0.0"
@ -10819,7 +10843,7 @@ source-map@^0.7.3:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==
source-map@^0.8.0-beta.0, source-map@~0.8.0-beta.0:
source-map@^0.8.0-beta.0:
version "0.8.0-beta.0"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11"
integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==
@ -10900,9 +10924,9 @@ sprintf-js@~1.0.2:
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
ssri@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.0.tgz#79ca74e21f8ceaeddfcb4b90143c458b8d988808"
integrity sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==
version "8.0.1"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af"
integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==
dependencies:
minipass "^3.1.1"
@ -11396,13 +11420,13 @@ terser-webpack-plugin@^1.4.3, terser-webpack-plugin@^4.2.3:
webpack-sources "^1.4.3"
terser@^5.0.0, terser@^5.3.4:
version "5.13.1"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.13.1.tgz#66332cdc5a01b04a224c9fad449fc1a18eaa1799"
integrity sha512-hn4WKOfwnwbYfe48NgrQjqNOH9jzLqRcIfbYytOXCOv46LBfWr9bDS17MQqOi+BWGD0sJK3Sj5NC/gJjiojaoA==
version "5.17.6"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.6.tgz#d810e75e1bb3350c799cd90ebefe19c9412c12de"
integrity sha512-V8QHcs8YuyLkLHsJO5ucyff1ykrLVsR4dNnS//L5Y3NiSXpbK1J+WMVUs67eI0KTxs9JtHhgEQpXQVHlHI92DQ==
dependencies:
"@jridgewell/source-map" "^0.3.2"
acorn "^8.5.0"
commander "^2.20.0"
source-map "~0.8.0-beta.0"
source-map-support "~0.5.20"
tesseract.js-core@^2.2.0:
@ -12314,25 +12338,25 @@ word-wrap@^1.2.3, word-wrap@~1.2.3:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
workbox-background-sync@6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.6.1.tgz#08d603a33717ce663e718c30cc336f74909aff2f"
integrity sha512-trJd3ovpWCvzu4sW0E8rV3FUyIcC0W8G+AZ+VcqzzA890AsWZlUGOTSxIMmIHVusUw/FDq1HFWfy/kC/WTRqSg==
workbox-background-sync@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-7.0.0.tgz#2b84b96ca35fec976e3bd2794b70e4acec46b3a5"
integrity sha512-S+m1+84gjdueM+jIKZ+I0Lx0BDHkk5Nu6a3kTVxP4fdj3gKouRNmhO8H290ybnJTOPfBDtTMXSQA/QLTvr7PeA==
dependencies:
idb "^7.0.1"
workbox-core "6.6.1"
workbox-core "7.0.0"
workbox-broadcast-update@6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-6.6.1.tgz#0fad9454cf8e4ace0c293e5617c64c75d8a8c61e"
integrity sha512-fBhffRdaANdeQ1V8s692R9l/gzvjjRtydBOvR6WCSB0BNE2BacA29Z4r9/RHd9KaXCPl6JTdI9q0bR25YKP8TQ==
workbox-broadcast-update@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-7.0.0.tgz#7f611ca1a94ba8ac0aa40fa171c9713e0f937d22"
integrity sha512-oUuh4jzZrLySOo0tC0WoKiSg90bVAcnE98uW7F8GFiSOXnhogfNDGZelPJa+6KpGBO5+Qelv04Hqx2UD+BJqNQ==
dependencies:
workbox-core "6.6.1"
workbox-core "7.0.0"
workbox-build@6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-6.6.1.tgz#6010e9ce550910156761448f2dbea8cfcf759cb0"
integrity sha512-INPgDx6aRycAugUixbKgiEQBWD0MPZqU5r0jyr24CehvNuLPSXp/wGOpdRJmts656lNiXwqV7dC2nzyrzWEDnw==
workbox-build@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-7.0.0.tgz#02ab5ef2991b3369b8b9395703f08912212769b4"
integrity sha512-CttE7WCYW9sZC+nUYhQg3WzzGPr4IHmrPnjKiu3AMXsiNQKx+l4hHl63WTrnicLmKEKHScWDH8xsGBdrYgtBzg==
dependencies:
"@apideck/better-ajv-errors" "^0.3.1"
"@babel/core" "^7.11.1"
@ -12356,132 +12380,132 @@ workbox-build@6.6.1:
strip-comments "^2.0.1"
tempy "^0.6.0"
upath "^1.2.0"
workbox-background-sync "6.6.1"
workbox-broadcast-update "6.6.1"
workbox-cacheable-response "6.6.1"
workbox-core "6.6.1"
workbox-expiration "6.6.1"
workbox-google-analytics "6.6.1"
workbox-navigation-preload "6.6.1"
workbox-precaching "6.6.1"
workbox-range-requests "6.6.1"
workbox-recipes "6.6.1"
workbox-routing "6.6.1"
workbox-strategies "6.6.1"
workbox-streams "6.6.1"
workbox-sw "6.6.1"
workbox-window "6.6.1"
workbox-background-sync "7.0.0"
workbox-broadcast-update "7.0.0"
workbox-cacheable-response "7.0.0"
workbox-core "7.0.0"
workbox-expiration "7.0.0"
workbox-google-analytics "7.0.0"
workbox-navigation-preload "7.0.0"
workbox-precaching "7.0.0"
workbox-range-requests "7.0.0"
workbox-recipes "7.0.0"
workbox-routing "7.0.0"
workbox-strategies "7.0.0"
workbox-streams "7.0.0"
workbox-sw "7.0.0"
workbox-window "7.0.0"
workbox-cacheable-response@6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-6.6.1.tgz#284c2b86be3f4fd191970ace8c8e99797bcf58e9"
integrity sha512-85LY4veT2CnTCDxaVG7ft3NKaFbH6i4urZXgLiU4AiwvKqS2ChL6/eILiGRYXfZ6gAwDnh5RkuDbr/GMS4KSag==
workbox-cacheable-response@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-7.0.0.tgz#ee27c036728189eed69d25a135013053277482d2"
integrity sha512-0lrtyGHn/LH8kKAJVOQfSu3/80WDc9Ma8ng0p2i/5HuUndGttH+mGMSvOskjOdFImLs2XZIimErp7tSOPmu/6g==
dependencies:
workbox-core "6.6.1"
workbox-core "7.0.0"
workbox-core@6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.6.1.tgz#7184776d4134c5ed2f086878c882728fc9084265"
integrity sha512-ZrGBXjjaJLqzVothoE12qTbVnOAjFrHDXpZe7coCb6q65qI/59rDLwuFMO4PcZ7jcbxY+0+NhUVztzR/CbjEFw==
workbox-core@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-7.0.0.tgz#dec114ec923cc2adc967dd9be1b8a0bed50a3545"
integrity sha512-81JkAAZtfVP8darBpfRTovHg8DGAVrKFgHpOArZbdFd78VqHr5Iw65f2guwjE2NlCFbPFDoez3D3/6ZvhI/rwQ==
workbox-expiration@6.6.1, workbox-expiration@^6.6.0:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.6.1.tgz#a841fa36676104426dbfb9da1ef6a630b4f93739"
integrity sha512-qFiNeeINndiOxaCrd2DeL1Xh1RFug3JonzjxUHc5WkvkD2u5abY3gZL1xSUNt3vZKsFFGGORItSjVTVnWAZO4A==
workbox-expiration@7.0.0, workbox-expiration@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-7.0.0.tgz#3d90bcf2a7577241de950f89784f6546b66c2baa"
integrity sha512-MLK+fogW+pC3IWU9SFE+FRStvDVutwJMR5if1g7oBJx3qwmO69BNoJQVaMXq41R0gg3MzxVfwOGKx3i9P6sOLQ==
dependencies:
idb "^7.0.1"
workbox-core "6.6.1"
workbox-core "7.0.0"
workbox-google-analytics@6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-6.6.1.tgz#a07a6655ab33d89d1b0b0a935ffa5dea88618c5d"
integrity sha512-1TjSvbFSLmkpqLcBsF7FuGqqeDsf+uAXO/pjiINQKg3b1GN0nBngnxLcXDYo1n/XxK4N7RaRrpRlkwjY/3ocuA==
workbox-google-analytics@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-7.0.0.tgz#603b2c4244af1e85de0fb26287d4e17d3293452a"
integrity sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==
dependencies:
workbox-background-sync "6.6.1"
workbox-core "6.6.1"
workbox-routing "6.6.1"
workbox-strategies "6.6.1"
workbox-background-sync "7.0.0"
workbox-core "7.0.0"
workbox-routing "7.0.0"
workbox-strategies "7.0.0"
workbox-navigation-preload@6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-6.6.1.tgz#61a34fe125558dd88cf09237f11bd966504ea059"
integrity sha512-DQCZowCecO+wRoIxJI2V6bXWK6/53ff+hEXLGlQL4Rp9ZaPDLrgV/32nxwWIP7QpWDkVEtllTAK5h6cnhxNxDA==
workbox-navigation-preload@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-7.0.0.tgz#4913878dbbd97057181d57baa18d2bbdde085c6c"
integrity sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==
dependencies:
workbox-core "6.6.1"
workbox-core "7.0.0"
workbox-precaching@6.6.1, workbox-precaching@^6.6.0:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.6.1.tgz#dedeeba10a2d163d990bf99f1c2066ac0d1a19e2"
integrity sha512-K4znSJ7IKxCnCYEdhNkMr7X1kNh8cz+mFgx9v5jFdz1MfI84pq8C2zG+oAoeE5kFrUf7YkT5x4uLWBNg0DVZ5A==
workbox-precaching@7.0.0, workbox-precaching@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-7.0.0.tgz#3979ba8033aadf3144b70e9fe631d870d5fbaa03"
integrity sha512-EC0vol623LJqTJo1mkhD9DZmMP604vHqni3EohhQVwhJlTgyKyOkMrZNy5/QHfOby+39xqC01gv4LjOm4HSfnA==
dependencies:
workbox-core "6.6.1"
workbox-routing "6.6.1"
workbox-strategies "6.6.1"
workbox-core "7.0.0"
workbox-routing "7.0.0"
workbox-strategies "7.0.0"
workbox-range-requests@6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-6.6.1.tgz#ddaf7e73af11d362fbb2f136a9063a4c7f507a39"
integrity sha512-4BDzk28govqzg2ZpX0IFkthdRmCKgAKreontYRC5YsAPB2jDtPNxqx3WtTXgHw1NZalXpcH/E4LqUa9+2xbv1g==
workbox-range-requests@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-7.0.0.tgz#97511901e043df27c1aa422adcc999a7751f52ed"
integrity sha512-SxAzoVl9j/zRU9OT5+IQs7pbJBOUOlriB8Gn9YMvi38BNZRbM+RvkujHMo8FOe9IWrqqwYgDFBfv6sk76I1yaQ==
dependencies:
workbox-core "6.6.1"
workbox-core "7.0.0"
workbox-recipes@6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-6.6.1.tgz#ea70d2b2b0b0bce8de0a9d94f274d4a688e69fae"
integrity sha512-/oy8vCSzromXokDA+X+VgpeZJvtuf8SkQ8KL0xmRivMgJZrjwM3c2tpKTJn6PZA6TsbxGs3Sc7KwMoZVamcV2g==
workbox-recipes@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-7.0.0.tgz#1a6a01c8c2dfe5a41eef0fed3fe517e8a45c6514"
integrity sha512-DntcK9wuG3rYQOONWC0PejxYYIDHyWWZB/ueTbOUDQgefaeIj1kJ7pdP3LZV2lfrj8XXXBWt+JDRSw1lLLOnww==
dependencies:
workbox-cacheable-response "6.6.1"
workbox-core "6.6.1"
workbox-expiration "6.6.1"
workbox-precaching "6.6.1"
workbox-routing "6.6.1"
workbox-strategies "6.6.1"
workbox-cacheable-response "7.0.0"
workbox-core "7.0.0"
workbox-expiration "7.0.0"
workbox-precaching "7.0.0"
workbox-routing "7.0.0"
workbox-strategies "7.0.0"
workbox-routing@6.6.1, workbox-routing@^6.6.0:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.6.1.tgz#cba9a1c7e0d1ea11e24b6f8c518840efdc94f581"
integrity sha512-j4ohlQvfpVdoR8vDYxTY9rA9VvxTHogkIDwGdJ+rb2VRZQ5vt1CWwUUZBeD/WGFAni12jD1HlMXvJ8JS7aBWTg==
workbox-routing@7.0.0, workbox-routing@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-7.0.0.tgz#6668438a06554f60645aedc77244a4fe3a91e302"
integrity sha512-8YxLr3xvqidnbVeGyRGkaV4YdlKkn5qZ1LfEePW3dq+ydE73hUUJJuLmGEykW3fMX8x8mNdL0XrWgotcuZjIvA==
dependencies:
workbox-core "6.6.1"
workbox-core "7.0.0"
workbox-strategies@6.6.1, workbox-strategies@^6.6.0:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-6.6.1.tgz#38d0f0fbdddba97bd92e0c6418d0b1a2ccd5b8bf"
integrity sha512-WQLXkRnsk4L81fVPkkgon1rZNxnpdO5LsO+ws7tYBC6QQQFJVI6v98klrJEjFtZwzw/mB/HT5yVp7CcX0O+mrw==
workbox-strategies@7.0.0, workbox-strategies@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-7.0.0.tgz#dcba32b3f3074476019049cc490fe1a60ea73382"
integrity sha512-dg3qJU7tR/Gcd/XXOOo7x9QoCI9nk74JopaJaYAQ+ugLi57gPsXycVdBnYbayVj34m6Y8ppPwIuecrzkpBVwbA==
dependencies:
workbox-core "6.6.1"
workbox-core "7.0.0"
workbox-streams@6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-6.6.1.tgz#b2f7ba7b315c27a6e3a96a476593f99c5d227d26"
integrity sha512-maKG65FUq9e4BLotSKWSTzeF0sgctQdYyTMq529piEN24Dlu9b6WhrAfRpHdCncRS89Zi2QVpW5V33NX8PgH3Q==
workbox-streams@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-7.0.0.tgz#36722aecd04785f88b6f709e541c094fc658c0f9"
integrity sha512-moVsh+5to//l6IERWceYKGiftc+prNnqOp2sgALJJFbnNVpTXzKISlTIsrWY+ogMqt+x1oMazIdHj25kBSq/HQ==
dependencies:
workbox-core "6.6.1"
workbox-routing "6.6.1"
workbox-core "7.0.0"
workbox-routing "7.0.0"
workbox-sw@6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-6.6.1.tgz#d4c4ca3125088e8b9fd7a748ed537fa0247bd72c"
integrity sha512-R7whwjvU2abHH/lR6kQTTXLHDFU2izht9kJOvBRYK65FbwutT4VvnUAJIgHvfWZ/fokrOPhfoWYoPCMpSgUKHQ==
workbox-sw@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-7.0.0.tgz#7350126411e3de1409f7ec243df8d06bb5b08b86"
integrity sha512-SWfEouQfjRiZ7GNABzHUKUyj8pCoe+RwjfOIajcx6J5mtgKkN+t8UToHnpaJL5UVVOf5YhJh+OHhbVNIHe+LVA==
workbox-webpack-plugin@^6.6.0:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.1.tgz#4f81cc1ad4e5d2cd7477a86ba83c84ee2d187531"
integrity sha512-zpZ+ExFj9NmiI66cFEApyjk7hGsfJ1YMOaLXGXBoZf0v7Iu6hL0ZBe+83mnDq3YYWAfA3fnyFejritjOHkFcrA==
workbox-webpack-plugin@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-7.0.0.tgz#6c61661a2cacde1239192a5877a041a2943d1a55"
integrity sha512-R1ZzCHPfzeJjLK2/TpKUhxSQ3fFDCxlWxgRhhSjMQLz3G2MlBnyw/XeYb34e7SGgSv0qG22zEhMIzjMNqNeKbw==
dependencies:
fast-json-stable-stringify "^2.1.0"
pretty-bytes "^5.4.1"
upath "^1.2.0"
webpack-sources "^1.4.3"
workbox-build "6.6.1"
workbox-build "7.0.0"
workbox-window@6.6.1, workbox-window@^6.6.0:
version "6.6.1"
resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-6.6.1.tgz#f22a394cbac36240d0dadcbdebc35f711bb7b89e"
integrity sha512-wil4nwOY58nTdCvif/KEZjQ2NP8uk3gGeRNy2jPBbzypU4BT4D9L8xiwbmDBpZlSgJd2xsT9FvSNU0gsxV51JQ==
workbox-window@7.0.0, workbox-window@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-7.0.0.tgz#a683ab33c896e4f16786794eac7978fc98a25d08"
integrity sha512-j7P/bsAWE/a7sxqTzXo3P2ALb1reTfZdvVp6OJ/uLr/C2kZAMvjeWGm8V4htQhor7DOvYg0sSbFN2+flT5U0qA==
dependencies:
"@types/trusted-types" "^2.0.2"
workbox-core "6.6.1"
workbox-core "7.0.0"
wrap-ansi@^5.1.0:
version "5.1.0"
@ -12532,9 +12556,9 @@ write-file-atomic@^5.0.1:
signal-exit "^4.0.1"
ws@^6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
version "6.2.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e"
integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==
dependencies:
async-limiter "~1.0.0"