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: 'substring', message: 'Use .slice instead of .substring.' },
{ property: 'substr', message: 'Use .slice instead of .substr.' }, { 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-self-assign': 'off',
'no-unused-expressions': 'error', 'no-unused-expressions': 'error',
'no-unused-vars': 'off', 'no-unused-vars': 'off',

View file

@ -4,6 +4,11 @@ exclude:
- 'vendor/**/*' - 'vendor/**/*'
- lib/templates/haml/scaffold/_form.html.haml - lib/templates/haml/scaffold/_form.html.haml
require:
- ./lib/linter/haml_middle_dot.rb
linters: linters:
AltText: AltText:
enabled: true enabled: true
MiddleDot:
enabled: true

View file

@ -61,7 +61,7 @@ docker-compose.override.yml
/app/javascript/mastodon/features/emoji/emoji_map.json /app/javascript/mastodon/features/emoji/emoji_map.json
# Ignore locale files # Ignore locale files
/app/javascript/mastodon/locales /app/javascript/mastodon/locales/*.json
/config/locales /config/locales
# Ignore vendored CSS reset # Ignore vendored CSS reset

View file

@ -11,6 +11,7 @@ require:
- rubocop-rspec - rubocop-rspec
- rubocop-performance - rubocop-performance
- rubocop-capybara - rubocop-capybara
- ./lib/linter/rubocop_middle_dot
AllCops: AllCops:
TargetRubyVersion: 3.0 # Set to minimum supported version of CI 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 # https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainhashliteral
Style/TrailingCommaInHashLiteral: Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: 'comma' EnforcedStyleForMultiline: 'comma'
Style/MiddleDot:
Enabled: true

View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@ module ReactComponentHelper
end end
def react_admin_component(name, props = {}) 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) div_tag_with_data(data)
end end

View file

@ -24,8 +24,6 @@ import {
fillListTimelineGaps, fillListTimelineGaps,
} from './timelines'; } from './timelines';
const { messages } = getLocale();
/** /**
* @param {number} max * @param {number} max
* @returns {number} * @returns {number}
@ -43,8 +41,10 @@ const randomUpTo = max =>
* @param {function(object): boolean} [options.accept] * @param {function(object): boolean} [options.accept]
* @returns {function(): void} * @returns {function(): void}
*/ */
export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => {
connectStream(channelName, params, (dispatch, getState) => { const { messages } = getLocale();
return connectStream(channelName, params, (dispatch, getState) => {
const locale = getState().getIn(['meta', 'locale']); const locale = getState().getIn(['meta', 'locale']);
// @ts-expect-error // @ts-expect-error
@ -125,6 +125,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
}, },
}; };
}); });
};
/** /**
* @param {Function} dispatch * @param {Function} dispatch

View file

@ -144,7 +144,7 @@ class Account extends ImmutablePureComponent {
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at')); const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
if (firstVerifiedField) { if (firstVerifiedField) {
verification = <>· <VerifiedBadge link={firstVerifiedField.get('value')} /></>; verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
} }
return ( return (
@ -155,9 +155,13 @@ class Account extends ImmutablePureComponent {
<Avatar account={account} size={size} /> <Avatar account={account} size={size} />
</div> </div>
<div> <div className='account__contents'>
<DisplayName account={account} /> <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> </div>
</Link> </Link>

View file

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

View file

@ -1,24 +1,19 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import { IntlProvider } from 'react-intl'; import { IntlProvider } from 'mastodon/locales';
import { getLocale, onProviderError } from '../locales';
const { messages } = getLocale();
export default class AdminComponent extends PureComponent { export default class AdminComponent extends PureComponent {
static propTypes = { static propTypes = {
locale: PropTypes.string.isRequired,
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
}; };
render () { render () {
const { locale, children } = this.props; const { children } = this.props;
return ( return (
<IntlProvider locale={locale} messages={messages} onError={onProviderError}> <IntlProvider>
{children} {children}
</IntlProvider> </IntlProvider>
); );

View file

@ -1,18 +1,14 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import { IntlProvider } from 'react-intl';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { fetchCustomEmojis } from '../actions/custom_emojis'; import { fetchCustomEmojis } from '../actions/custom_emojis';
import { hydrateStore } from '../actions/store'; import { hydrateStore } from '../actions/store';
import Compose from '../features/standalone/compose'; import Compose from '../features/standalone/compose';
import initialState from '../initial_state'; import initialState from '../initial_state';
import { getLocale, onProviderError } from '../locales'; import { IntlProvider } from '../locales';
import { store } from '../store'; import { store } from '../store';
const { messages } = getLocale();
if (initialState) { if (initialState) {
store.dispatch(hydrateStore(initialState)); store.dispatch(hydrateStore(initialState));
@ -20,17 +16,11 @@ if (initialState) {
store.dispatch(fetchCustomEmojis()); store.dispatch(fetchCustomEmojis());
export default class TimelineContainer extends PureComponent { export default class ComposeContainer extends PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
};
render () { render () {
const { locale } = this.props;
return ( return (
<IntlProvider locale={locale} messages={messages} onError={onProviderError}> <IntlProvider>
<Provider store={store}> <Provider store={store}>
<Compose /> <Compose />
</Provider> </Provider>

View file

@ -1,8 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import { IntlProvider } from 'react-intl';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { BrowserRouter, Route } from 'react-router-dom'; 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 ErrorBoundary from 'mastodon/components/error_boundary';
import UI from 'mastodon/features/ui'; import UI from 'mastodon/features/ui';
import initialState, { title as siteTitle } from 'mastodon/initial_state'; 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'; import { store } from 'mastodon/store';
const { messages } = getLocale();
const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`; const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;
const hydrateAction = hydrateStore(initialState); const hydrateAction = hydrateStore(initialState);
@ -42,10 +38,6 @@ const createIdentityContext = state => ({
export default class Mastodon extends PureComponent { export default class Mastodon extends PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
};
static childContextTypes = { static childContextTypes = {
identity: PropTypes.shape({ identity: PropTypes.shape({
signedIn: PropTypes.bool.isRequired, signedIn: PropTypes.bool.isRequired,
@ -81,10 +73,8 @@ export default class Mastodon extends PureComponent {
} }
render () { render () {
const { locale } = this.props;
return ( return (
<IntlProvider locale={locale} messages={messages} onError={onProviderError}> <IntlProvider>
<ReduxProvider store={store}> <ReduxProvider store={store}>
<ErrorBoundary> <ErrorBoundary>
<BrowserRouter> <BrowserRouter>

View file

@ -2,8 +2,6 @@ import PropTypes from 'prop-types';
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { IntlProvider } from 'react-intl';
import { fromJS } from 'immutable'; import { fromJS } from 'immutable';
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag'; 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 Card from 'mastodon/features/status/components/card';
import MediaModal from 'mastodon/features/ui/components/media_modal'; import MediaModal from 'mastodon/features/ui/components/media_modal';
import Video from 'mastodon/features/video'; import Video from 'mastodon/features/video';
import { getLocale, onProviderError } from 'mastodon/locales'; import { IntlProvider } from 'mastodon/locales';
import { getScrollbarWidth } from 'mastodon/utils/scrollbar'; import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
const { messages } = getLocale();
const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio }; const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio };
export default class MediaContainer extends PureComponent { export default class MediaContainer extends PureComponent {
static propTypes = { static propTypes = {
locale: PropTypes.string.isRequired,
components: PropTypes.object.isRequired, components: PropTypes.object.isRequired,
}; };
@ -73,7 +68,7 @@ export default class MediaContainer extends PureComponent {
}; };
render () { render () {
const { locale, components } = this.props; const { components } = this.props;
let handleOpenVideo; let handleOpenVideo;
@ -83,7 +78,7 @@ export default class MediaContainer extends PureComponent {
} }
return ( return (
<IntlProvider locale={locale} messages={messages} onError={onProviderError}> <IntlProvider>
<> <>
{[].map.call(components, (component, i) => { {[].map.call(components, (component, i) => {
const componentName = component.getAttribute('data-component'); const componentName = component.getAttribute('data-component');

View file

@ -247,7 +247,7 @@ class DetailedStatus extends ImmutablePureComponent {
} else if (this.context.router) { } else if (this.context.router) {
reblogLink = ( reblogLink = (
<> <>
· {' · '}
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'> <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
<Icon id={reblogIcon} /> <Icon id={reblogIcon} />
<span className='detailed-status__reblogs'> <span className='detailed-status__reblogs'>
@ -259,7 +259,7 @@ class DetailedStatus extends ImmutablePureComponent {
} else { } else {
reblogLink = ( reblogLink = (
<> <>
· {' · '}
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}> <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
<Icon id={reblogIcon} /> <Icon id={reblogIcon} />
<span className='detailed-status__reblogs'> <span className='detailed-status__reblogs'>
@ -313,7 +313,7 @@ class DetailedStatus extends ImmutablePureComponent {
if (status.get('edited_at')) { if (status.get('edited_at')) {
edited = ( edited = (
<> <>
· {' · '}
<EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} /> <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 => { [].forEach.call(document.querySelectorAll('[data-admin-component]'), element => {
const componentName = element.getAttribute('data-admin-component'); 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 }) => { import('../mastodon/containers/admin_component').then(({ default: AdminComponent }) => {
return import('../mastodon/components/admin/' + componentName).then(({ default: Component }) => { return import('../mastodon/components/admin/' + componentName).then(({ default: Component }) => {
const root = createRoot(element); const root = createRoot(element);
root.render ( root.render (
<AdminComponent locale={locale}> <AdminComponent>
<Component {...componentProps} /> <Component {...componentProps} />
</AdminComponent>, </AdminComponent>,
); );

View file

@ -1,14 +1,15 @@
import './public-path'; import './public-path';
import main from "mastodon/main"
import { start } from '../mastodon/common'; import { start } from '../mastodon/common';
import { loadLocale } from '../mastodon/load_locale'; import { loadLocale } from '../mastodon/locales';
import { loadPolyfills } from '../mastodon/polyfills'; import { loadPolyfills } from '../mastodon/polyfills';
start(); start();
loadPolyfills().then(loadLocale).then(async () => { loadPolyfills()
const { default: main } = await import('mastodon/main'); .then(loadLocale)
.then(main)
return main(); .catch(e => {
}).catch(e => {
console.error(e); console.error(e);
}); });

View file

@ -15,8 +15,7 @@ import { start } from '../mastodon/common';
import { timeAgoString } from '../mastodon/components/relative_timestamp'; import { timeAgoString } from '../mastodon/components/relative_timestamp';
import emojify from '../mastodon/features/emoji/emoji'; import emojify from '../mastodon/features/emoji/emoji';
import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions'; import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions';
import { loadLocale } from '../mastodon/load_locale'; import { loadLocale, getLocale } from '../mastodon/locales';
import { getLocale } from '../mastodon/locales';
import { loadPolyfills } from '../mastodon/polyfills'; import { loadPolyfills } from '../mastodon/polyfills';
import ready from '../mastodon/ready'; import ready from '../mastodon/ready';

View file

@ -3,7 +3,6 @@ import { createRoot } from 'react-dom/client';
import { start } from '../mastodon/common'; import { start } from '../mastodon/common';
import ComposeContainer from '../mastodon/containers/compose_container'; import ComposeContainer from '../mastodon/containers/compose_container';
import { loadLocale } from '../mastodon/load_locale';
import { loadPolyfills } from '../mastodon/polyfills'; import { loadPolyfills } from '../mastodon/polyfills';
import ready from '../mastodon/ready'; import ready from '../mastodon/ready';
@ -26,6 +25,6 @@ function main() {
ready(loaded); ready(loaded);
} }
loadPolyfills().then(loadLocale).then(main).catch(error => { loadPolyfills().then(main).catch(error => {
console.error(error); console.error(error);
}); });

View file

@ -3,11 +3,8 @@
display: block; display: block;
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); overflow: hidden;
border-radius: 4px;
@media screen and (max-width: $no-gap-breakpoint) {
box-shadow: none;
}
&:hover, &:hover,
&:active, &:active,
@ -22,7 +19,6 @@
height: 130px; height: 130px;
position: relative; position: relative;
background: darken($ui-base-color, 12%); background: darken($ui-base-color, 12%);
border-radius: 4px 4px 0 0;
img { img {
display: block; display: block;
@ -30,7 +26,6 @@
height: 100%; height: 100%;
margin: 0; margin: 0;
object-fit: cover; object-fit: cover;
border-radius: 4px 4px 0 0;
} }
@media screen and (width <= 600px) { @media screen and (width <= 600px) {
@ -45,11 +40,6 @@
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
background: lighten($ui-base-color, 4%); background: lighten($ui-base-color, 4%);
border-radius: 0 0 4px 4px;
@media screen and (max-width: $no-gap-breakpoint) {
border-radius: 0;
}
.avatar { .avatar {
flex: 0 0 auto; 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 { .verified-badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
color: $valid-value-color; color: $valid-value-color;
gap: 4px; gap: 4px;
overflow: hidden;
white-space: nowrap;
> span {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
}
a { a {
color: inherit; color: inherit;

View file

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

View file

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

View file

@ -18,6 +18,7 @@ class Form::Import
muting: ['Account address', 'Hide notifications'], muting: ['Account address', 'Hide notifications'],
domain_blocking: ['#domain'], domain_blocking: ['#domain'],
bookmarks: ['#uri'], bookmarks: ['#uri'],
lists: ['List name', 'Account address'],
}.freeze }.freeze
KNOWN_FIRST_HEADERS = EXPECTED_HEADERS_BY_TYPE.values.map(&:first).uniq.freeze KNOWN_FIRST_HEADERS = EXPECTED_HEADERS_BY_TYPE.values.map(&:first).uniq.freeze
@ -30,6 +31,7 @@ class Form::Import
'Hide notifications' => 'hide_notifications', 'Hide notifications' => 'hide_notifications',
'#domain' => 'domain', '#domain' => 'domain',
'#uri' => 'uri', '#uri' => 'uri',
'List name' => 'list_name',
}.freeze }.freeze
class EmptyFileError < StandardError; end 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 :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 :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 :bookmarks if data.original_filename&.start_with?('bookmarks')
return :lists if data.original_filename&.start_with?('lists')
end end
# Whether the uploaded CSV file seems to correspond to a different import type than the one selected # 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 private
def default_csv_header def default_csv_headers
case type.to_sym case type.to_sym
when :following, :blocking, :muting when :following, :blocking, :muting
'Account address' ['Account address']
when :domain_blocking when :domain_blocking
'#domain' ['#domain']
when :bookmarks when :bookmarks
'#uri' ['#uri']
when :lists
['List name', 'Account address']
end end
end end
@ -98,7 +103,7 @@ class Form::Import
field&.split(',')&.map(&:strip)&.presence field&.split(',')&.map(&:strip)&.presence
when 'Account address' when 'Account address'
field.strip.gsub(/\A@/, '') field.strip.gsub(/\A@/, '')
when '#domain', '#uri' when '#domain', '#uri', 'List name'
field.strip field.strip
else else
field field
@ -109,7 +114,7 @@ class Form::Import
@csv_data.take(1) # Ensure the headers are read @csv_data.take(1) # Ensure the headers are read
raise EmptyFileError if @csv_data.headers == true 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 @csv_data
end end
@ -133,7 +138,7 @@ class Form::Import
def validate_data def validate_data
return if data.nil? 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.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 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 @type = row.bulk_import.type.to_sym
case @type case @type
when :following, :blocking, :muting when :following, :blocking, :muting, :lists
target_acct = @data['acct'] target_acct = @data['acct']
target_domain = domain(target_acct) target_domain = domain(target_acct)
@target_account = stoplight_wrap_request(target_domain) { ResolveAccountService.new.call(target_acct, { check_delivery_availability: true }) } @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? return false unless StatusPolicy.new(@account, @target_status).show?
@account.bookmarks.find_or_create_by!(status: @target_status) @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 end
true true

View file

@ -16,6 +16,8 @@ class BulkImportService < BaseService
import_domain_blocks! import_domain_blocks!
when :bookmarks when :bookmarks
import_bookmarks! import_bookmarks!
when :lists
import_lists!
end end
@import.update!(state: :finished, finished_at: Time.now.utc) if @import.processed_items == @import.total_items @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] [row.id]
end end
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 end

View file

@ -9,6 +9,6 @@
- if email_domain_block.parent.present? - 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.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 }) = 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/ %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? - if f.object.public_comment.present?
·
= f.object.public_comment = f.object.public_comment
- if existing_relationships - if existing_relationships
·
= fa_icon 'warning fw' = fa_icon 'warning fw'
= t('admin.export_domain_blocks.import.existing_relationships_warning') = t('admin.export_domain_blocks.import.existing_relationships_warning')

View file

@ -6,7 +6,7 @@
%small %small
- if instance.domain_block - 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 - elsif instance.domain_allow
= t('admin.accounts.whitelisted') = t('admin.accounts.whitelisted')
- else - else

View file

@ -58,7 +58,7 @@
%td= @instance.domain_block.public_comment %td= @instance.domain_block.public_comment
%tr %tr
%th= t('admin.instances.content_policies.policy') %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.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 } = 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 .pending-account__header
%samp= link_to "#{ip_block.ip}/#{ip_block.ip.prefix}", admin_accounts_path(ip: "#{ip_block.ip}/#{ip_block.ip.prefix}") %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? - if ip_block.comment.present?
·
= ip_block.comment = ip_block.comment
%br/ %br/
= t("simple_form.labels.ip_block.severities.#{ip_block.severity}") = t("simple_form.labels.ip_block.severities.#{ip_block.severity}")

View file

@ -24,7 +24,7 @@
= t('admin.roles.everyone_full_description_html') = t('admin.roles.everyone_full_description_html')
- else - else
= link_to t('admin.roles.assigned_users', count: role.users.count), admin_accounts_path(role_ids: role.id) = 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) %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 %div
= table_link_to 'pencil', t('admin.accounts.edit'), edit_admin_role_path(role) if can?(:update, role) = 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 .fields-group
= f.input :media_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' } = 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]+' } = f.input :backups_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
.actions .actions

View file

@ -10,21 +10,21 @@
- if preview_card.provider_name.present? - if preview_card.provider_name.present?
= preview_card.provider_name = preview_card.provider_name
·
- if preview_card.language.present? - if preview_card.language.present?
= standard_locale_name(preview_card.language) = 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 }) = t('admin.trends.links.shared_by_over_week', count: preview_card.history.reduce(0) { |sum, day| sum + day.accounts })
- if preview_card.trend.allowed? - 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) %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? - if preview_card.decaying?
·
= t('admin.trends.tags.peaked_on_and_decaying', date: l(preview_card.max_score_at.to_date, format: :short)) = t('admin.trends.tags.peaked_on_and_decaying', date: l(preview_card.max_score_at.to_date, format: :short))
- elsif preview_card.requires_review? - elsif preview_card.requires_review?
·
= t('admin.trends.pending_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)) = 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? - if status.account.domain.present?
·
= status.account.domain = status.account.domain
- if status.language.present? - if status.language.present?
·
= standard_locale_name(status.language) = standard_locale_name(status.language)
- if status.trendable? && !status.account.discoverable? - if status.trendable? && !status.account.discoverable?
·
= t('admin.trends.statuses.not_discoverable') = t('admin.trends.statuses.not_discoverable')
- if status.trend.allowed? - 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) %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? - elsif status.requires_review?
·
= t('admin.trends.pending_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 }) = 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)) - 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) %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? - if tag.decaying?
·
= t('admin.trends.tags.peaked_on_and_decaying', date: l(tag.max_score_at.to_date, format: :short)) = t('admin.trends.tags.peaked_on_and_decaying', date: l(tag.max_score_at.to_date, format: :short))
- elsif tag.requires_review? - elsif tag.requires_review?
·
= t('admin.trends.pending_review') = t('admin.trends.pending_review')

View file

@ -10,7 +10,7 @@
- else - else
%span.negative-hint= t('admin.webhooks.disabled') %span.negative-hint= t('admin.webhooks.disabled')
·
%abbr{ title: webhook.events.join(', ') }= t('admin.webhooks.enabled_events', count: webhook.events.size) %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') %> <%= raw t('admin_mailer.new_trends.new_trending_links.title') %>
<% @links.each do |link| %> <% @links.each do |link| %>
- <%= link.title %> <%= link.url %> - <%= 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)) %> <%= 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 %> <% end %>
<%= raw t('application_mailer.view')%> <%= admin_trends_links_url %> <%= raw t('application_mailer.view')%> <%= admin_trends_links_url %>

View file

@ -2,7 +2,7 @@
<% @statuses.each do |status| %> <% @statuses.each do |status| %>
- <%= ActivityPub::TagManager.instance.url_for(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 %> <% end %>
<%= raw t('application_mailer.view')%> <%= admin_trends_statuses_url %> <%= raw t('application_mailer.view')%> <%= admin_trends_statuses_url %>

View file

@ -2,7 +2,7 @@
<% @tags.each do |tag| %> <% @tags.each do |tag| %>
- #<%= tag.display_name %> - #<%= 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 %> <% end %>
<% if @lowest_trending_tag %> <% if @lowest_trending_tag %>

View file

@ -1,7 +1,9 @@
- account_url = local_assigns[:admin] ? admin_account_path(account.id) : ActivityPub::TagManager.instance.url_for(account) - account_url = local_assigns[:admin] ? admin_account_path(account.id) : ActivityPub::TagManager.instance.url_for(account)
- compact ||= false
.card.h-card .card.h-card
= link_to account_url, target: '_blank', rel: 'noopener noreferrer' do = link_to account_url, target: '_blank', rel: 'noopener noreferrer' do
- unless compact
.card__img .card__img
= image_tag account.header.url, alt: '' = image_tag account.header.url, alt: ''
.card__bar .card__bar

View file

@ -7,6 +7,12 @@
.simple_form .simple_form
= render 'auth/shared/progress', stage: 'rules' = render 'auth/shared/progress', stage: 'rules'
- 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') %h1.title= t('auth.rules.title')
%p.lead= t('auth.rules.preamble', domain: site_hostname) %p.lead= t('auth.rules.preamble', domain: site_hostname)

View file

@ -23,7 +23,7 @@
- else - else
= t('doorkeeper.authorized_applications.index.never_used') = t('doorkeeper.authorized_applications.index.never_used')
·
= t('doorkeeper.authorized_applications.index.authorized_at', date: l(application.created_at.to_date)) = 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| = simple_form_for @import, url: settings_imports_path do |f|
.field-group .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-row
.fields-group.fields-row__column.fields-row__column-6 .fields-group.fields-row__column.fields-row__column-6

View file

@ -30,9 +30,18 @@ module KmyblueComponent
end end
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(AppendComponent)
SimpleForm.include_component(RecommendedComponent) SimpleForm.include_component(RecommendedComponent)
SimpleForm.include_component(KmyblueComponent) SimpleForm.include_component(KmyblueComponent)
SimpleForm.include_component(WarningHintComponent)
SimpleForm.setup do |config| SimpleForm.setup do |config|
# Wrappers are used by the form builder to generate a # Wrappers are used by the form builder to generate a
@ -114,6 +123,7 @@ SimpleForm.setup do |config|
b.use :html5 b.use :html5
b.use :label b.use :label
b.use :hint, wrap_with: { tag: :span, class: :hint } 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 :input, wrap_with: { tag: :div, class: :label_input }
b.use :error, wrap_with: { tag: :span, class: :error } b.use :error, wrap_with: { tag: :span, class: :error }
end end

View file

@ -25,7 +25,7 @@ module Twitter::TwitterText
\) \)
/iox /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}' 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_query_ending_chars] = /[a-z0-9_&=#\/\-#{UCHARS}]/iou
REGEXEN[:valid_url_path] = /(?: REGEXEN[:valid_url_path] = /(?:
(?: (?:

View file

@ -1126,8 +1126,11 @@ en:
rules: rules:
accept: Accept accept: Accept
back: Back 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: 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: Some ground rules.
title_invited: You've been invited.
security: Security security: Security
set_new_password: Set new password set_new_password: Set new password
setup: setup:

View file

@ -82,7 +82,7 @@ en:
backups_retention_period: Keep generated user archives for the specified number of days. 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. 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 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. custom_css: You can apply custom styles on the web version of Mastodon.
mascot: Overrides the illustration in the advanced web interface. 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. 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: [ collectCoverageFrom: [
'app/javascript/mastodon/**/*.{js,jsx,ts,tsx}', 'app/javascript/mastodon/**/*.{js,jsx,ts,tsx}',
'!app/javascript/mastodon/features/emoji/emoji_compressed.js', '!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/service_worker/entry.js',
'!app/javascript/mastodon/test_setup.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_relative '../../../config/environment'
require 'thor' require 'thor'
require_relative 'helper' require_relative 'progress_helper'
module Mastodon module Mastodon
module CLI module CLI
class Base < Thor class Base < Thor
include CLI::Helper include ProgressHelper
def self.exit_on_failure? def self.exit_on_failure?
true true
end 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 end
end end

View file

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

View file

@ -29,15 +29,7 @@ module Mastodon::CLI
database will be imported into the indices, unless overridden with --no-import. database will be imported into the indices, unless overridden with --no-import.
LONG_DESC LONG_DESC
def deploy def deploy
if options[:concurrency] < 1 verify_deploy_options!
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
indices = if options[:only] indices = if options[:only]
options[:only].map { |str| "#{str.camelize}Index".constantize } options[:only].map { |str| "#{str.camelize}Index".constantize }
@ -98,5 +90,26 @@ module Mastodon::CLI
say("Indexed #{added} records, de-indexed #{removed}", :green, true) say("Indexed #{added} records, de-indexed #{removed}", :green, true)
end 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
end end

View file

@ -21,7 +21,6 @@
"lint:sass": "stylelint \"**/*.{css,scss}\" && prettier --check \"**/*.{css,scss}\"", "lint:sass": "stylelint \"**/*.{css,scss}\" && prettier --check \"**/*.{css,scss}\"",
"lint:yml": "prettier --check \"**/*.{yaml,yml}\"", "lint:yml": "prettier --check \"**/*.{yaml,yml}\"",
"lint": "yarn lint:js && yarn lint:json && yarn lint:sass && yarn lint: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", "postversion": "git push --tags",
"prepare": "husky install", "prepare": "husky install",
"start": "node ./streaming/index.js", "start": "node ./streaming/index.js",
@ -49,6 +48,7 @@
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"abortcontroller-polyfill": "^1.7.5", "abortcontroller-polyfill": "^1.7.5",
"arrow-key-navigation": "^1.2.0", "arrow-key-navigation": "^1.2.0",
"async-mutex": "^0.4.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"axios": "^1.4.0", "axios": "^1.4.0",
"babel-loader": "^8.3.0", "babel-loader": "^8.3.0",
@ -138,12 +138,12 @@
"webpack-cli": "^3.3.12", "webpack-cli": "^3.3.12",
"webpack-merge": "^5.9.0", "webpack-merge": "^5.9.0",
"wicg-inert": "^3.1.2", "wicg-inert": "^3.1.2",
"workbox-expiration": "^6.6.0", "workbox-expiration": "^7.0.0",
"workbox-precaching": "^6.6.0", "workbox-precaching": "^7.0.0",
"workbox-routing": "^6.6.0", "workbox-routing": "^7.0.0",
"workbox-strategies": "^6.6.0", "workbox-strategies": "^7.0.0",
"workbox-webpack-plugin": "^6.6.0", "workbox-webpack-plugin": "^7.0.0",
"workbox-window": "^6.6.0", "workbox-window": "^7.0.0",
"ws": "^8.12.1" "ws": "^8.12.1"
}, },
"devDependencies": { "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 it 'returns a tag with data attributes' do
expect(parsed_html.div['data-admin-component']).to eq('Name') 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
end end

View file

@ -924,4 +924,78 @@ describe Mastodon::CLI::Accounts do
end end
end 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 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', 'muting', 'imports.txt', 1
it_behaves_like 'too many CSV rows', 'domain_blocking', 'domain_blocks.csv', 2 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', '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 # Importing list of addresses with no headers into various types
it_behaves_like 'valid import', 'following', 'imports.txt' 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 # Importing bookmarks list with no headers into expected type
it_behaves_like 'valid import', 'bookmarks', 'bookmark-imports.txt' 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 # Importing followed accounts with headers into various compatible types
it_behaves_like 'valid import', 'following', 'following_accounts.csv' it_behaves_like 'valid import', 'following', 'following_accounts.csv'
it_behaves_like 'valid import', 'blocking', '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 }, { '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 # Based on the bug report 20571 where UTF-8 encoded domains were rejecting import of their users
# #
# https://github.com/mastodon/mastodon/issues/20571 # https://github.com/mastodon/mastodon/issues/20571

View file

@ -91,5 +91,77 @@ RSpec.describe BulkImportRowService do
end end
end 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
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/koi8-r').to_return(request_fixture('koi8-r.txt'))
stub_request(:get, 'http://example.com/日本語').to_return(request_fixture('sjis.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, '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/test-').to_return(request_fixture('idn.txt'))
stub_request(:get, 'http://example.com/windows-1251').to_return(request_fixture('windows-1251.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 expect(a_request(:get, 'http://example.com/sjis')).to_not have_been_made
end end
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 end
context 'with a remote status' do context 'with a remote status' do

274
yarn.lock
View file

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