Merge remote-tracking branch 'parent/main' into upstream-20231021

This commit is contained in:
KMY 2023-10-21 10:32:06 +09:00
commit f9eaaec4e7
95 changed files with 1002 additions and 657 deletions

View file

@ -236,7 +236,7 @@ module.exports = {
}, },
// Common React utilities // Common React utilities
{ {
pattern: '{classnames,react-helmet,react-router-dom}', pattern: '{classnames,react-helmet,react-router,react-router-dom}',
group: 'external', group: 'external',
position: 'before', position: 'before',
}, },

View file

@ -114,6 +114,7 @@ jobs:
BUNDLE_WITH: 'pam_authentication test' BUNDLE_WITH: 'pam_authentication test'
CI_JOBS: ${{ matrix.ci_job }}/4 CI_JOBS: ${{ matrix.ci_job }}/4
ES_ENABLED: false ES_ENABLED: false
GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }}
strategy: strategy:
fail-fast: false fail-fast: false

View file

@ -48,27 +48,6 @@ Lint/UnusedBlockArgument:
- 'config/initializers/paperclip.rb' - 'config/initializers/paperclip.rb'
- 'config/initializers/simple_form.rb' - 'config/initializers/simple_form.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Lint/UselessAssignment:
Exclude:
- 'app/services/activitypub/process_status_update_service.rb'
- 'config/initializers/3_omniauth.rb'
- 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb'
- 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb'
- 'spec/controllers/api/v1/favourites_controller_spec.rb'
- 'spec/controllers/concerns/account_controller_concern_spec.rb'
- 'spec/helpers/jsonld_helper_spec.rb'
- 'spec/models/account_spec.rb'
- 'spec/models/domain_block_spec.rb'
- 'spec/models/status_spec.rb'
- 'spec/models/user_spec.rb'
- 'spec/models/webauthn_credentials_spec.rb'
- 'spec/services/account_search_service_spec.rb'
- 'spec/services/post_status_service_spec.rb'
- 'spec/services/precompute_feed_service_spec.rb'
- 'spec/services/resolve_url_service_spec.rb'
- 'spec/views/statuses/show.html.haml_spec.rb'
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize: Metrics/AbcSize:
Max: 144 Max: 144
@ -86,26 +65,6 @@ Metrics/CyclomaticComplexity:
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Max: 27 Max: 27
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
# SupportedStyles: snake_case, normalcase, non_integer
# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
Naming/VariableNumber:
Exclude:
- 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb'
- 'db/migrate/20180514140000_revert_index_change_on_statuses_for_api_v1_accounts_account_id_statuses.rb'
- 'db/migrate/20190820003045_update_statuses_index.rb'
- 'db/migrate/20190823221802_add_local_index_to_statuses.rb'
- 'db/migrate/20200119112504_add_public_index_to_statuses.rb'
- 'spec/models/account_spec.rb'
- 'spec/models/domain_block_spec.rb'
- 'spec/models/user_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: SafeMultiline.
Performance/DeletePrefix:
Exclude:
- 'app/models/featured_tag.rb'
Performance/MapMethodChain: Performance/MapMethodChain:
Exclude: Exclude:
- 'app/models/feed.rb' - 'app/models/feed.rb'

View file

@ -1,9 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Apps::CredentialsController < Api::BaseController class Api::V1::Apps::CredentialsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }
def show def show
render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key) return doorkeeper_render_error unless valid_doorkeeper_token?
render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key client_id scopes)
end end
end end

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Admin
module DisputesHelper
def strike_action_label(appeal)
t(key_for_action(appeal),
scope: 'admin.strikes.actions',
name: content_tag(:span, appeal.strike.account.username, class: 'username'),
target: content_tag(:span, appeal.account.username, class: 'target'))
.html_safe
end
private
def key_for_action(appeal)
AccountWarning.actions.slice(appeal.strike.action).keys.first
end
end
end

View file

@ -9,6 +9,10 @@ module FormattingHelper
TextFormatter.new(text, options).to_s TextFormatter.new(text, options).to_s
end end
def url_for_preview_card(preview_card)
preview_card.url
end
def extract_status_plain_text(status) def extract_status_plain_text(status)
PlainTextFormatter.new(status.text, status.local?).to_s PlainTextFormatter.new(status.text, status.local?).to_s
end end

View file

@ -4,29 +4,28 @@ import { createPortal } from 'react-dom';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { withRouter } from 'react-router-dom';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
export default class ColumnBackButton extends PureComponent { class ColumnBackButton extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
onClick: PropTypes.func, onClick: PropTypes.func,
...WithRouterPropTypes,
}; };
handleClick = () => { handleClick = () => {
const { router } = this.context; const { onClick, history } = this.props;
const { onClick } = this.props;
if (onClick) { if (onClick) {
onClick(); onClick();
} else if (router.history.location?.state?.fromMastodon) { } else if (history.location?.state?.fromMastodon) {
router.history.goBack(); history.goBack();
} else { } else {
router.history.push('/'); history.push('/');
} }
}; };
@ -60,3 +59,5 @@ export default class ColumnBackButton extends PureComponent {
} }
} }
export default withRouter(ColumnBackButton);

View file

@ -5,8 +5,10 @@ import { createPortal } from 'react-dom';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
const messages = defineMessages({ const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
@ -18,7 +20,6 @@ const messages = defineMessages({
class ColumnHeader extends PureComponent { class ColumnHeader extends PureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object, identity: PropTypes.object,
}; };
@ -38,6 +39,7 @@ class ColumnHeader extends PureComponent {
onClick: PropTypes.func, onClick: PropTypes.func,
appendContent: PropTypes.node, appendContent: PropTypes.node,
collapseIssues: PropTypes.bool, collapseIssues: PropTypes.bool,
...WithRouterPropTypes,
}; };
state = { state = {
@ -63,12 +65,12 @@ class ColumnHeader extends PureComponent {
}; };
handleBackClick = () => { handleBackClick = () => {
const { router } = this.context; const { history } = this.props;
if (router.history.location?.state?.fromMastodon) { if (history.location?.state?.fromMastodon) {
router.history.goBack(); history.goBack();
} else { } else {
router.history.push('/'); history.push('/');
} }
}; };
@ -78,15 +80,14 @@ class ColumnHeader extends PureComponent {
handlePin = () => { handlePin = () => {
if (!this.props.pinned) { if (!this.props.pinned) {
this.context.router.history.replace('/'); this.props.history.replace('/');
} }
this.props.onPin(); this.props.onPin();
}; };
render () { render () {
const { router } = this.context; const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues, history } = this.props;
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues } = this.props;
const { collapsed, animating } = this.state; const { collapsed, animating } = this.state;
const wrapperClassName = classNames('column-header__wrapper', { const wrapperClassName = classNames('column-header__wrapper', {
@ -129,7 +130,7 @@ class ColumnHeader extends PureComponent {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>; pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
} }
if (!pinned && ((multiColumn && router.history.location?.state?.fromMastodon) || showBackButton)) { if (!pinned && ((multiColumn && history.location?.state?.fromMastodon) || showBackButton)) {
backButton = ( backButton = (
<button onClick={this.handleBackClick} className='column-header__back-button'> <button onClick={this.handleBackClick} className='column-header__back-button'>
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth /> <Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
@ -215,4 +216,4 @@ class ColumnHeader extends PureComponent {
} }
export default injectIntl(ColumnHeader); export default injectIntl(withRouter(ColumnHeader));

View file

@ -2,13 +2,16 @@ import PropTypes from 'prop-types';
import { PureComponent, cloneElement, Children } from 'react'; import { PureComponent, cloneElement, Children } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { supportsPassiveEvents } from 'detect-passive-events'; import { supportsPassiveEvents } from 'detect-passive-events';
import Overlay from 'react-overlays/Overlay'; import Overlay from 'react-overlays/Overlay';
import { CircularProgress } from "./circular_progress"; import { CircularProgress } from 'mastodon/components/circular_progress';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { IconButton } from './icon_button'; import { IconButton } from './icon_button';
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
@ -16,10 +19,6 @@ let id = 0;
class DropdownMenu extends PureComponent { class DropdownMenu extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired, items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
loading: PropTypes.bool, loading: PropTypes.bool,
@ -159,11 +158,7 @@ class DropdownMenu extends PureComponent {
} }
export default class Dropdown extends PureComponent { class Dropdown extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
children: PropTypes.node, children: PropTypes.node,
@ -183,6 +178,7 @@ export default class Dropdown extends PureComponent {
renderItem: PropTypes.func, renderItem: PropTypes.func,
renderHeader: PropTypes.func, renderHeader: PropTypes.func,
onItemClick: PropTypes.func, onItemClick: PropTypes.func,
...WithRouterPropTypes
}; };
static defaultProps = { static defaultProps = {
@ -250,7 +246,7 @@ export default class Dropdown extends PureComponent {
item.action(); item.action();
} else if (item && item.to) { } else if (item && item.to) {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(item.to); this.props.history.push(item.to);
} }
}; };
@ -338,3 +334,5 @@ export default class Dropdown extends PureComponent {
} }
} }
export default withRouter(Dropdown);

View file

@ -2,14 +2,13 @@ import PropTypes from 'prop-types';
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import 'wicg-inert'; import 'wicg-inert';
import { multiply } from 'color-blend'; import { multiply } from 'color-blend';
import { createBrowserHistory } from 'history'; import { createBrowserHistory } from 'history';
export default class ModalRoot extends PureComponent { import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router';
static contextTypes = { class ModalRoot extends PureComponent {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
children: PropTypes.node, children: PropTypes.node,
@ -20,6 +19,7 @@ export default class ModalRoot extends PureComponent {
b: PropTypes.number, b: PropTypes.number,
}), }),
ignoreFocus: PropTypes.bool, ignoreFocus: PropTypes.bool,
...WithOptionalRouterPropTypes,
}; };
activeElement = this.props.children ? document.activeElement : null; activeElement = this.props.children ? document.activeElement : null;
@ -55,7 +55,7 @@ export default class ModalRoot extends PureComponent {
componentDidMount () { componentDidMount () {
window.addEventListener('keyup', this.handleKeyUp, false); window.addEventListener('keyup', this.handleKeyUp, false);
window.addEventListener('keydown', this.handleKeyDown, false); window.addEventListener('keydown', this.handleKeyDown, false);
this.history = this.context.router ? this.context.router.history : createBrowserHistory(); this.history = this.props.history || createBrowserHistory();
} }
UNSAFE_componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps (nextProps) {
@ -156,3 +156,5 @@ export default class ModalRoot extends PureComponent {
} }
} }
export default withOptionalRouter(ModalRoot);

View file

@ -1,35 +0,0 @@
import { PureComponent } from 'react';
import { Switch, Route, withRouter } from 'react-router-dom';
import AccountNavigation from 'mastodon/features/account/navigation';
import Trends from 'mastodon/features/getting_started/containers/trends_container';
import { showTrends } from 'mastodon/initial_state';
const DefaultNavigation = () => (
showTrends ? (
<>
<div className='flex-spacer' />
<Trends />
</>
) : null
);
class NavigationPortal extends PureComponent {
render () {
return (
<Switch>
<Route path='/@:acct' exact component={AccountNavigation} />
<Route path='/@:acct/tagged/:tagged?' exact component={AccountNavigation} />
<Route path='/@:acct/with_replies' exact component={AccountNavigation} />
<Route path='/@:acct/followers' exact component={AccountNavigation} />
<Route path='/@:acct/following' exact component={AccountNavigation} />
<Route path='/@:acct/media' exact component={AccountNavigation} />
<Route component={DefaultNavigation} />
</Switch>
);
}
}
export default withRouter(NavigationPortal);

View file

@ -0,0 +1,25 @@
import { Switch, Route } from 'react-router-dom';
import AccountNavigation from 'mastodon/features/account/navigation';
import Trends from 'mastodon/features/getting_started/containers/trends_container';
import { showTrends } from 'mastodon/initial_state';
const DefaultNavigation: React.FC = () =>
showTrends ? (
<>
<div className='flex-spacer' />
<Trends />
</>
) : null;
export const NavigationPortal: React.FC = () => (
<Switch>
<Route path='/@:acct' exact component={AccountNavigation} />
<Route path='/@:acct/tagged/:tagged?' exact component={AccountNavigation} />
<Route path='/@:acct/with_replies' exact component={AccountNavigation} />
<Route path='/@:acct/followers' exact component={AccountNavigation} />
<Route path='/@:acct/following' exact component={AccountNavigation} />
<Route path='/@:acct/media' exact component={AccountNavigation} />
<Route component={DefaultNavigation} />
</Switch>
);

View file

@ -1,15 +1,18 @@
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import React from 'react'; import React from 'react';
import { createBrowserHistory } from 'history';
import { Router as OriginalRouter } from 'react-router'; import { Router as OriginalRouter } from 'react-router';
import type { LocationDescriptor, Path } from 'history';
import { createBrowserHistory } from 'history';
import { layoutFromWindow } from 'mastodon/is_mobile'; import { layoutFromWindow } from 'mastodon/is_mobile';
interface MastodonLocationState { interface MastodonLocationState {
fromMastodon?: boolean; fromMastodon?: boolean;
mastodonModalKey?: string; mastodonModalKey?: string;
} }
type HistoryPath = Path | LocationDescriptor<MastodonLocationState>;
const browserHistory = createBrowserHistory< const browserHistory = createBrowserHistory<
MastodonLocationState | undefined MastodonLocationState | undefined
@ -17,25 +20,36 @@ const browserHistory = createBrowserHistory<
const originalPush = browserHistory.push.bind(browserHistory); const originalPush = browserHistory.push.bind(browserHistory);
const originalReplace = browserHistory.replace.bind(browserHistory); const originalReplace = browserHistory.replace.bind(browserHistory);
browserHistory.push = (path: string, state?: MastodonLocationState) => { function extractRealPath(path: HistoryPath) {
if (typeof path === 'string') return path;
else return path.pathname;
}
browserHistory.push = (path: HistoryPath, state?: MastodonLocationState) => {
state = state ?? {}; state = state ?? {};
state.fromMastodon = true; state.fromMastodon = true;
if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) { const realPath = extractRealPath(path);
originalPush(`/deck${path}`, state); if (!realPath) return;
if (layoutFromWindow() === 'multi-column' && !realPath.startsWith('/deck')) {
originalPush(`/deck${realPath}`, state);
} else { } else {
originalPush(path, state); originalPush(path, state);
} }
}; };
browserHistory.replace = (path: string, state?: MastodonLocationState) => { browserHistory.replace = (path: HistoryPath, state?: MastodonLocationState) => {
if (browserHistory.location.state?.fromMastodon) { if (browserHistory.location.state?.fromMastodon) {
state = state ?? {}; state = state ?? {};
state.fromMastodon = true; state.fromMastodon = true;
} }
if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) { const realPath = extractRealPath(path);
originalReplace(`/deck${path}`, state); if (!realPath) return;
if (layoutFromWindow() === 'multi-column' && !realPath.startsWith('/deck')) {
originalReplace(`/deck${realPath}`, state);
} else { } else {
originalReplace(path, state); originalReplace(path, state);
} }

View file

@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import { Children, cloneElement, PureComponent } from 'react'; import { Children, cloneElement, PureComponent } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useLocation } from 'react-router-dom';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -34,12 +35,33 @@ const mapStateToProps = (state, { scrollKey }) => {
}; };
}; };
class ScrollableList extends PureComponent { // This component only exists to be able to call useLocation()
const IOArticleContainerWrapper = ({id, index, listLength, intersectionObserverWrapper, trackScroll, scrollKey, children}) => {
const location = useLocation();
static contextTypes = { return (<IntersectionObserverArticleContainer
router: PropTypes.object, id={id}
index={index}
listLength={listLength}
intersectionObserverWrapper={intersectionObserverWrapper}
saveHeightKey={trackScroll ? `${location.key}:${scrollKey}` : null}
>
{children}
</IntersectionObserverArticleContainer>);
}; };
IOArticleContainerWrapper.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
scrollKey: PropTypes.string.isRequired,
intersectionObserverWrapper: PropTypes.object.isRequired,
trackScroll: PropTypes.bool.isRequired,
children: PropTypes.node,
};
class ScrollableList extends PureComponent {
static propTypes = { static propTypes = {
scrollKey: PropTypes.string.isRequired, scrollKey: PropTypes.string.isRequired,
onLoadMore: PropTypes.func, onLoadMore: PropTypes.func,
@ -331,13 +353,14 @@ class ScrollableList extends PureComponent {
{loadPending} {loadPending}
{Children.map(this.props.children, (child, index) => ( {Children.map(this.props.children, (child, index) => (
<IntersectionObserverArticleContainer <IOArticleContainerWrapper
key={child.key} key={child.key}
id={child.key} id={child.key}
index={index} index={index}
listLength={childrenCount} listLength={childrenCount}
intersectionObserverWrapper={this.intersectionObserverWrapper} intersectionObserverWrapper={this.intersectionObserverWrapper}
saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null} trackScroll={trackScroll}
scrollKey={scrollKey}
> >
{cloneElement(child, child.type.name === 'ColumnLink' ? {} : { {cloneElement(child, child.type.name === 'ColumnLink' ? {} : {
getScrollPosition: this.getScrollPosition, getScrollPosition: this.getScrollPosition,
@ -345,7 +368,7 @@ class ScrollableList extends PureComponent {
cachedMediaWidth: this.state.cachedMediaWidth, cachedMediaWidth: this.state.cachedMediaWidth,
cacheMediaWidth: this.cacheMediaWidth, cacheMediaWidth: this.cacheMediaWidth,
})} })}
</IntersectionObserverArticleContainer> </IOArticleContainerWrapper>
))} ))}
{loadMore} {loadMore}

View file

@ -12,6 +12,7 @@ import { HotKeys } from 'react-hotkeys';
import AttachmentList from 'mastodon/components/attachment_list'; import AttachmentList from 'mastodon/components/attachment_list';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder'; import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
import { withOptionalRouter, WithRouterPropTypes } from 'mastodon/utils/react_router';
import CompactedStatusContainer from '../containers/compacted_status_container'; import CompactedStatusContainer from '../containers/compacted_status_container';
import Card from '../features/status/components/card'; import Card from '../features/status/components/card';
@ -81,10 +82,6 @@ const messages = defineMessages({
class Status extends ImmutablePureComponent { class Status extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
@ -130,6 +127,7 @@ class Status extends ImmutablePureComponent {
available: PropTypes.bool, available: PropTypes.bool,
}), }),
withoutEmojiReactions: PropTypes.bool, withoutEmojiReactions: PropTypes.bool,
...WithRouterPropTypes,
}; };
// Avoid checking props that are functions (and whose equality will always // Avoid checking props that are functions (and whose equality will always
@ -272,7 +270,7 @@ class Status extends ImmutablePureComponent {
handleHotkeyReply = e => { handleHotkeyReply = e => {
e.preventDefault(); e.preventDefault();
this.props.onReply(this._properStatus(), this.context.router.history); this.props.onReply(this._properStatus(), this.props.history);
}; };
handleHotkeyFavourite = () => { handleHotkeyFavourite = () => {
@ -285,7 +283,7 @@ class Status extends ImmutablePureComponent {
handleHotkeyMention = e => { handleHotkeyMention = e => {
e.preventDefault(); e.preventDefault();
this.props.onMention(this._properStatus().get('account'), this.context.router.history); this.props.onMention(this._properStatus().get('account'), this.props.history);
}; };
handleHotkeyOpen = () => { handleHotkeyOpen = () => {
@ -294,14 +292,14 @@ class Status extends ImmutablePureComponent {
return; return;
} }
const { router } = this.context; const { history } = this.props;
const status = this._properStatus(); const status = this._properStatus();
if (!router) { if (!history) {
return; return;
} }
router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`); history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
}; };
handleHotkeyOpenProfile = () => { handleHotkeyOpenProfile = () => {
@ -309,14 +307,14 @@ class Status extends ImmutablePureComponent {
}; };
_openProfile = (proper = true) => { _openProfile = (proper = true) => {
const { router } = this.context; const { history } = this.props;
const status = proper ? this._properStatus() : this.props.status; const status = proper ? this._properStatus() : this.props.status;
if (!router) { if (!history) {
return; return;
} }
router.history.push(`/@${status.getIn(['account', 'acct'])}`); history.push(`/@${status.getIn(['account', 'acct'])}`);
}; };
handleHotkeyMoveUp = e => { handleHotkeyMoveUp = e => {
@ -686,4 +684,4 @@ class Status extends ImmutablePureComponent {
} }
export default injectIntl(Status); export default withOptionalRouter(injectIntl(Status));

View file

@ -3,12 +3,14 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import DropdownMenuContainer from '../containers/dropdown_menu_container'; import DropdownMenuContainer from '../containers/dropdown_menu_container';
@ -70,7 +72,6 @@ const mapStateToProps = (state, { status }) => ({
class StatusActionBar extends ImmutablePureComponent { class StatusActionBar extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object, identity: PropTypes.object,
}; };
@ -106,6 +107,7 @@ class StatusActionBar extends ImmutablePureComponent {
withCounters: PropTypes.bool, withCounters: PropTypes.bool,
scrollKey: PropTypes.string, scrollKey: PropTypes.string,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
...WithRouterPropTypes,
}; };
// Avoid checking props that are functions (and whose equality will always // Avoid checking props that are functions (and whose equality will always
@ -120,7 +122,7 @@ class StatusActionBar extends ImmutablePureComponent {
const { signedIn } = this.context.identity; const { signedIn } = this.context.identity;
if (signedIn) { if (signedIn) {
this.props.onReply(this.props.status, this.context.router.history); this.props.onReply(this.props.status, this.props.history);
} else { } else {
this.props.onInteractionModal('reply', this.props.status); this.props.onInteractionModal('reply', this.props.status);
} }
@ -187,15 +189,15 @@ class StatusActionBar extends ImmutablePureComponent {
}; };
handleDeleteClick = () => { handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.context.router.history); this.props.onDelete(this.props.status, this.props.history);
}; };
handleRedraftClick = () => { handleRedraftClick = () => {
this.props.onDelete(this.props.status, this.context.router.history, true); this.props.onDelete(this.props.status, this.props.history, true);
}; };
handleEditClick = () => { handleEditClick = () => {
this.props.onEdit(this.props.status, this.context.router.history); this.props.onEdit(this.props.status, this.props.history);
}; };
handlePinClick = () => { handlePinClick = () => {
@ -203,11 +205,11 @@ class StatusActionBar extends ImmutablePureComponent {
}; };
handleMentionClick = () => { handleMentionClick = () => {
this.props.onMention(this.props.status.get('account'), this.context.router.history); this.props.onMention(this.props.status.get('account'), this.props.history);
}; };
handleDirectClick = () => { handleDirectClick = () => {
this.props.onDirect(this.props.status.get('account'), this.context.router.history); this.props.onDirect(this.props.status.get('account'), this.props.history);
}; };
handleMuteClick = () => { handleMuteClick = () => {
@ -247,7 +249,7 @@ class StatusActionBar extends ImmutablePureComponent {
}; };
handleOpen = () => { handleOpen = () => {
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`); this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`);
}; };
handleOpenMentions = () => { handleOpenMentions = () => {
@ -477,4 +479,4 @@ class StatusActionBar extends ImmutablePureComponent {
} }
export default connect(mapStateToProps)(injectIntl(StatusActionBar)); export default withRouter(connect(mapStateToProps)(injectIntl(StatusActionBar)));

View file

@ -4,7 +4,7 @@ import { PureComponent } from 'react';
import { FormattedMessage, injectIntl } from 'react-intl'; import { FormattedMessage, injectIntl } from 'react-intl';
import classnames from 'classnames'; import classnames from 'classnames';
import { Link } from 'react-router-dom'; import { Link, withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -68,7 +68,6 @@ const mapStateToProps = state => ({
class StatusContent extends PureComponent { class StatusContent extends PureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object, identity: PropTypes.object,
}; };
@ -83,6 +82,10 @@ class StatusContent extends PureComponent {
onCollapsedToggle: PropTypes.func, onCollapsedToggle: PropTypes.func,
languages: ImmutablePropTypes.map, languages: ImmutablePropTypes.map,
intl: PropTypes.object, intl: PropTypes.object,
// from react-router
match: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
}; };
state = { state = {
@ -173,18 +176,18 @@ class StatusContent extends PureComponent {
} }
onMentionClick = (mention, e) => { onMentionClick = (mention, e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(`/@${mention.get('acct')}`); this.props.history.push(`/@${mention.get('acct')}`);
} }
}; };
onHashtagClick = (hashtag, e) => { onHashtagClick = (hashtag, e) => {
hashtag = hashtag.replace(/^#/, ''); hashtag = hashtag.replace(/^#/, '');
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(`/tags/${hashtag}`); this.props.history.push(`/tags/${hashtag}`);
} }
}; };
@ -252,7 +255,7 @@ class StatusContent extends PureComponent {
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') }; const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
const language = status.getIn(['translation', 'language']) || status.get('language'); const language = status.getIn(['translation', 'language']) || status.get('language');
const classNames = classnames('status__content', { const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.context.router, 'status__content--with-action': this.props.onClick && this.props.history,
'status__content--with-spoiler': status.get('spoiler_text').length > 0, 'status__content--with-spoiler': status.get('spoiler_text').length > 0,
'status__content--collapsed': renderReadMore, 'status__content--collapsed': renderReadMore,
}); });
@ -329,4 +332,4 @@ class StatusContent extends PureComponent {
} }
export default connect(mapStateToProps)(injectIntl(StatusContent)); export default withRouter(connect(mapStateToProps)(injectIntl(StatusContent)));

View file

@ -14,10 +14,6 @@ const messages = defineMessages({
class FeaturedTags extends ImmutablePureComponent { class FeaturedTags extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
featuredTags: ImmutablePropTypes.list, featuredTags: ImmutablePropTypes.list,

View file

@ -4,7 +4,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { NavLink } from 'react-router-dom'; import { NavLink, withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
@ -19,6 +19,7 @@ import { ShortNumber } from 'mastodon/components/short_number';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { autoPlayGif, me, domain } from 'mastodon/initial_state'; import { autoPlayGif, me, domain } from 'mastodon/initial_state';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import AccountNoteContainer from '../containers/account_note_container'; import AccountNoteContainer from '../containers/account_note_container';
import FollowRequestNoteContainer from '../containers/follow_request_note_container'; import FollowRequestNoteContainer from '../containers/follow_request_note_container';
@ -86,11 +87,6 @@ const dateFormatOptions = {
class Header extends ImmutablePureComponent { class Header extends ImmutablePureComponent {
static contextTypes = {
identity: PropTypes.object,
router: PropTypes.object,
};
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
identity_props: ImmutablePropTypes.list, identity_props: ImmutablePropTypes.list,
@ -117,6 +113,11 @@ class Header extends ImmutablePureComponent {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
domain: PropTypes.string.isRequired, domain: PropTypes.string.isRequired,
hidden: PropTypes.bool, hidden: PropTypes.bool,
...WithRouterPropTypes,
};
static contextTypes = {
identity: PropTypes.object,
}; };
setRef = c => { setRef = c => {
@ -179,25 +180,24 @@ class Header extends ImmutablePureComponent {
}; };
handleHashtagClick = e => { handleHashtagClick = e => {
const { router } = this.context; const { history } = this.props;
const value = e.currentTarget.textContent.replace(/^#/, ''); const value = e.currentTarget.textContent.replace(/^#/, '');
if (router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
router.history.push(`/tags/${value}`); history.push(`/tags/${value}`);
} }
}; };
handleMentionClick = e => { handleMentionClick = e => {
const { router } = this.context; const { history, onOpenURL } = this.props;
const { onOpenURL } = this.props;
if (router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
const link = e.currentTarget; const link = e.currentTarget;
onOpenURL(link.href, router.history, () => { onOpenURL(link.href, history, () => {
window.location = link.href; window.location = link.href;
}); });
} }
@ -506,4 +506,4 @@ class Header extends ImmutablePureComponent {
} }
export default injectIntl(Header); export default withRouter(injectIntl(Header));

View file

@ -2,17 +2,19 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { NavLink } from 'react-router-dom'; import { NavLink, withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import InnerHeader from '../../account/components/header'; import InnerHeader from '../../account/components/header';
import MemorialNote from './memorial_note'; import MemorialNote from './memorial_note';
import MovedNote from './moved_note'; import MovedNote from './moved_note';
export default class Header extends ImmutablePureComponent { class Header extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
@ -37,10 +39,7 @@ export default class Header extends ImmutablePureComponent {
hideTabs: PropTypes.bool, hideTabs: PropTypes.bool,
domain: PropTypes.string.isRequired, domain: PropTypes.string.isRequired,
hidden: PropTypes.bool, hidden: PropTypes.bool,
}; ...WithRouterPropTypes,
static contextTypes = {
router: PropTypes.object,
}; };
handleFollow = () => { handleFollow = () => {
@ -52,11 +51,11 @@ export default class Header extends ImmutablePureComponent {
}; };
handleMention = () => { handleMention = () => {
this.props.onMention(this.props.account, this.context.router.history); this.props.onMention(this.props.account, this.props.history);
}; };
handleDirect = () => { handleDirect = () => {
this.props.onDirect(this.props.account, this.context.router.history); this.props.onDirect(this.props.account, this.props.history);
}; };
handleReport = () => { handleReport = () => {
@ -177,3 +176,5 @@ export default class Header extends ImmutablePureComponent {
} }
} }
export default withRouter(Header);

View file

@ -39,7 +39,6 @@ const mapStateToProps = (state, { columnId }) => {
class CommunityTimeline extends PureComponent { class CommunityTimeline extends PureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object, identity: PropTypes.object,
}; };

View file

@ -10,6 +10,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz'; import { length } from 'stringz';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router';
import AutosuggestInput from '../../../components/autosuggest_input'; import AutosuggestInput from '../../../components/autosuggest_input';
import AutosuggestTextarea from '../../../components/autosuggest_textarea'; import AutosuggestTextarea from '../../../components/autosuggest_textarea';
@ -43,11 +44,6 @@ const messages = defineMessages({
}); });
class ComposeForm extends ImmutablePureComponent { class ComposeForm extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
text: PropTypes.string.isRequired, text: PropTypes.string.isRequired,
@ -78,6 +74,7 @@ class ComposeForm extends ImmutablePureComponent {
singleColumn: PropTypes.bool, singleColumn: PropTypes.bool,
lang: PropTypes.string, lang: PropTypes.string,
circleId: PropTypes.string, circleId: PropTypes.string,
...WithOptionalRouterPropTypes
}; };
static defaultProps = { static defaultProps = {
@ -121,7 +118,7 @@ class ComposeForm extends ImmutablePureComponent {
return; return;
} }
this.props.onSubmit(this.context.router ? this.context.router.history : null); this.props.onSubmit(this.props.history || null);
if (e) { if (e) {
e.preventDefault(); e.preventDefault();
@ -337,4 +334,4 @@ class ComposeForm extends ImmutablePureComponent {
} }
export default injectIntl(ComposeForm); export default withOptionalRouter(injectIntl(ComposeForm));

View file

@ -6,6 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import AttachmentList from 'mastodon/components/attachment_list'; import AttachmentList from 'mastodon/components/attachment_list';
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router';
import { Avatar } from '../../../components/avatar'; import { Avatar } from '../../../components/avatar';
import { DisplayName } from '../../../components/display_name'; import { DisplayName } from '../../../components/display_name';
@ -17,14 +18,11 @@ const messages = defineMessages({
class ReplyIndicator extends ImmutablePureComponent { class ReplyIndicator extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
onCancel: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
...WithOptionalRouterPropTypes,
}; };
handleClick = () => { handleClick = () => {
@ -34,7 +32,7 @@ class ReplyIndicator extends ImmutablePureComponent {
handleAccountClick = (e) => { handleAccountClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`); this.props.history?.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
} }
}; };
@ -72,4 +70,4 @@ class ReplyIndicator extends ImmutablePureComponent {
} }
export default injectIntl(ReplyIndicator); export default withOptionalRouter(injectIntl(ReplyIndicator));

View file

@ -4,12 +4,14 @@ import { PureComponent } from 'react';
import { defineMessages, injectIntl, FormattedMessage, FormattedList } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage, FormattedList } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { domain, searchEnabled } from 'mastodon/initial_state'; import { domain, searchEnabled } from 'mastodon/initial_state';
import { HASHTAG_REGEX } from 'mastodon/utils/hashtags'; import { HASHTAG_REGEX } from 'mastodon/utils/hashtags';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
const messages = defineMessages({ const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
@ -30,7 +32,6 @@ const labelForRecentSearch = search => {
class Search extends PureComponent { class Search extends PureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object.isRequired,
identity: PropTypes.object.isRequired, identity: PropTypes.object.isRequired,
}; };
@ -48,6 +49,7 @@ class Search extends PureComponent {
openInRoute: PropTypes.bool, openInRoute: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
singleColumn: PropTypes.bool, singleColumn: PropTypes.bool,
...WithRouterPropTypes,
}; };
state = { state = {
@ -163,32 +165,29 @@ class Search extends PureComponent {
}; };
handleHashtagClick = () => { handleHashtagClick = () => {
const { router } = this.context; const { value, onClickSearchResult, history } = this.props;
const { value, onClickSearchResult } = this.props;
const query = value.trim().replace(/^#/, ''); const query = value.trim().replace(/^#/, '');
router.history.push(`/tags/${query}`); history.push(`/tags/${query}`);
onClickSearchResult(query, 'hashtag'); onClickSearchResult(query, 'hashtag');
this._unfocus(); this._unfocus();
}; };
handleAccountClick = () => { handleAccountClick = () => {
const { router } = this.context; const { value, onClickSearchResult, history } = this.props;
const { value, onClickSearchResult } = this.props;
const query = value.trim().replace(/^@/, ''); const query = value.trim().replace(/^@/, '');
router.history.push(`/@${query}`); history.push(`/@${query}`);
onClickSearchResult(query, 'account'); onClickSearchResult(query, 'account');
this._unfocus(); this._unfocus();
}; };
handleURLClick = () => { handleURLClick = () => {
const { router } = this.context; const { value, onOpenURL, history } = this.props;
const { value, onOpenURL } = this.props;
onOpenURL(value, router.history); onOpenURL(value, history);
this._unfocus(); this._unfocus();
}; };
@ -201,13 +200,12 @@ class Search extends PureComponent {
}; };
handleRecentSearchClick = search => { handleRecentSearchClick = search => {
const { onChange } = this.props; const { onChange, history } = this.props;
const { router } = this.context;
if (search.get('type') === 'account') { if (search.get('type') === 'account') {
router.history.push(`/@${search.get('q')}`); history.push(`/@${search.get('q')}`);
} else if (search.get('type') === 'hashtag') { } else if (search.get('type') === 'hashtag') {
router.history.push(`/tags/${search.get('q')}`); history.push(`/tags/${search.get('q')}`);
} else { } else {
onChange(search.get('q')); onChange(search.get('q'));
this._submit(search.get('type')); this._submit(search.get('type'));
@ -239,8 +237,7 @@ class Search extends PureComponent {
} }
_submit (type) { _submit (type) {
const { onSubmit, openInRoute, value, onClickSearchResult } = this.props; const { onSubmit, openInRoute, value, onClickSearchResult, history } = this.props;
const { router } = this.context;
onSubmit(type); onSubmit(type);
@ -249,7 +246,7 @@ class Search extends PureComponent {
} }
if (openInRoute) { if (openInRoute) {
router.history.push('/search'); history.push('/search');
} }
this._unfocus(); this._unfocus();
@ -398,4 +395,4 @@ class Search extends PureComponent {
} }
export default injectIntl(Search); export default withRouter(injectIntl(Search));

View file

@ -13,10 +13,6 @@ import Motion from '../../ui/util/optional_motion';
export default class Upload extends ImmutablePureComponent { export default class Upload extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, media: ImmutablePropTypes.map.isRequired,
onUndo: PropTypes.func.isRequired, onUndo: PropTypes.func.isRequired,

View file

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Link } from 'react-router-dom'; import { Link, withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
@ -17,6 +17,7 @@ import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import StatusContent from 'mastodon/components/status_content'; import StatusContent from 'mastodon/components/status_content';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { autoPlayGif } from 'mastodon/initial_state'; import { autoPlayGif } from 'mastodon/initial_state';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
const messages = defineMessages({ const messages = defineMessages({
more: { id: 'status.more', defaultMessage: 'More' }, more: { id: 'status.more', defaultMessage: 'More' },
@ -30,10 +31,6 @@ const messages = defineMessages({
class Conversation extends ImmutablePureComponent { class Conversation extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
conversationId: PropTypes.string.isRequired, conversationId: PropTypes.string.isRequired,
accounts: ImmutablePropTypes.list.isRequired, accounts: ImmutablePropTypes.list.isRequired,
@ -45,6 +42,7 @@ class Conversation extends ImmutablePureComponent {
markRead: PropTypes.func.isRequired, markRead: PropTypes.func.isRequired,
delete: PropTypes.func.isRequired, delete: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
...WithRouterPropTypes,
}; };
handleMouseEnter = ({ currentTarget }) => { handleMouseEnter = ({ currentTarget }) => {
@ -74,7 +72,7 @@ class Conversation extends ImmutablePureComponent {
}; };
handleClick = () => { handleClick = () => {
if (!this.context.router) { if (!this.props.history) {
return; return;
} }
@ -84,7 +82,7 @@ class Conversation extends ImmutablePureComponent {
markRead(); markRead();
} }
this.context.router.history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`); this.props.history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`);
}; };
handleMarkAsRead = () => { handleMarkAsRead = () => {
@ -92,7 +90,7 @@ class Conversation extends ImmutablePureComponent {
}; };
handleReply = () => { handleReply = () => {
this.props.reply(this.props.lastStatus, this.context.router.history); this.props.reply(this.props.lastStatus, this.props.history);
}; };
handleDelete = () => { handleDelete = () => {
@ -202,4 +200,4 @@ class Conversation extends ImmutablePureComponent {
} }
export default injectIntl(Conversation); export default withRouter(injectIntl(Conversation));

View file

@ -36,10 +36,6 @@ const mapStateToProps = state => ({
class Directory extends PureComponent { class Directory extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
accountIds: ImmutablePropTypes.list.isRequired, accountIds: ImmutablePropTypes.list.isRequired,

View file

@ -32,7 +32,6 @@ const mapStateToProps = state => ({
class Explore extends PureComponent { class Explore extends PureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object, identity: PropTypes.object,
}; };

View file

@ -45,13 +45,10 @@ class Statuses extends PureComponent {
const emptyMessage = <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' />; const emptyMessage = <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' />;
return ( return (
<>
<DismissableBanner id='explore/statuses'>
<FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favorites are ranked higher.' />
</DismissableBanner>
<StatusList <StatusList
trackScroll trackScroll
prepend={<DismissableBanner id='explore/statuses'><FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favorites are ranked higher.' /></DismissableBanner>}
alwaysPrepend
timelineId='explore' timelineId='explore'
statusIds={statusIds} statusIds={statusIds}
scrollKey='explore-statuses' scrollKey='explore-statuses'
@ -62,7 +59,6 @@ class Statuses extends PureComponent {
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
withCounters withCounters
/> />
</>
); );
} }

View file

@ -4,6 +4,7 @@ import { PureComponent } from 'react';
import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
@ -20,6 +21,7 @@ import EmojiPickerDropdown from 'mastodon/features/compose/containers/emoji_pick
import unicodeMapping from 'mastodon/features/emoji/emoji_unicode_mapping_light'; import unicodeMapping from 'mastodon/features/emoji/emoji_unicode_mapping_light';
import { autoPlayGif, reduceMotion, disableSwiping, mascot } from 'mastodon/initial_state'; import { autoPlayGif, reduceMotion, disableSwiping, mascot } from 'mastodon/initial_state';
import { assetHost } from 'mastodon/utils/config'; import { assetHost } from 'mastodon/utils/config';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
const messages = defineMessages({ const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' }, close: { id: 'lightbox.close', defaultMessage: 'Close' },
@ -27,14 +29,10 @@ const messages = defineMessages({
next: { id: 'lightbox.next', defaultMessage: 'Next' }, next: { id: 'lightbox.next', defaultMessage: 'Next' },
}); });
class Content extends ImmutablePureComponent { class ContentWithRouter extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
announcement: ImmutablePropTypes.map.isRequired, announcement: ImmutablePropTypes.map.isRequired,
...WithRouterPropTypes,
}; };
setRef = c => { setRef = c => {
@ -89,25 +87,25 @@ class Content extends ImmutablePureComponent {
} }
onMentionClick = (mention, e) => { onMentionClick = (mention, e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(`/@${mention.get('acct')}`); this.props.history.push(`/@${mention.get('acct')}`);
} }
}; };
onHashtagClick = (hashtag, e) => { onHashtagClick = (hashtag, e) => {
hashtag = hashtag.replace(/^#/, ''); hashtag = hashtag.replace(/^#/, '');
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (this.props.history&& e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(`/tags/${hashtag}`); this.props.history.push(`/tags/${hashtag}`);
} }
}; };
onStatusClick = (status, e) => { onStatusClick = (status, e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`); this.props.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
} }
}; };
@ -153,6 +151,8 @@ class Content extends ImmutablePureComponent {
} }
const Content = withRouter(ContentWithRouter);
class Emoji extends PureComponent { class Emoji extends PureComponent {
static propTypes = { static propTypes = {

View file

@ -68,7 +68,6 @@ const badgeDisplay = (number, limit) => {
class GettingStarted extends ImmutablePureComponent { class GettingStarted extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object.isRequired,
identity: PropTypes.object, identity: PropTypes.object,
}; };

View file

@ -37,7 +37,7 @@ const getHomeFeedSpeed = createSelector([
state => state.getIn(['timelines', 'home', 'pendingItems'], ImmutableList()), state => state.getIn(['timelines', 'home', 'pendingItems'], ImmutableList()),
state => state.get('statuses'), state => state.get('statuses'),
], (statusIds, pendingStatusIds, statusMap) => { ], (statusIds, pendingStatusIds, statusMap) => {
const recentStatusIds = pendingStatusIds.size > 0 ? pendingStatusIds : statusIds; const recentStatusIds = pendingStatusIds.concat(statusIds);
const statuses = recentStatusIds.filter(id => id !== null).map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20); const statuses = recentStatusIds.filter(id => id !== null).map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20);
if (statuses.isEmpty()) { if (statuses.isEmpty()) {

View file

@ -4,6 +4,7 @@ import { PureComponent } from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -22,6 +23,7 @@ import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { RadioButton } from 'mastodon/components/radio_button'; import { RadioButton } from 'mastodon/components/radio_button';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import StatusListContainer from 'mastodon/features/ui/containers/status_list_container'; import StatusListContainer from 'mastodon/features/ui/containers/status_list_container';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
const messages = defineMessages({ const messages = defineMessages({
deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' }, deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
@ -38,10 +40,6 @@ const mapStateToProps = (state, props) => ({
class ListTimeline extends PureComponent { class ListTimeline extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
@ -50,6 +48,7 @@ class ListTimeline extends PureComponent {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
list: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]), list: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
...WithRouterPropTypes,
}; };
handlePin = () => { handlePin = () => {
@ -59,7 +58,7 @@ class ListTimeline extends PureComponent {
dispatch(removeColumn(columnId)); dispatch(removeColumn(columnId));
} else { } else {
dispatch(addColumn('LIST', { id: this.props.params.id })); dispatch(addColumn('LIST', { id: this.props.params.id }));
this.context.router.history.push('/'); this.props.history.push('/');
} }
}; };
@ -137,7 +136,7 @@ class ListTimeline extends PureComponent {
if (columnId) { if (columnId) {
dispatch(removeColumn(columnId)); dispatch(removeColumn(columnId));
} else { } else {
this.context.router.history.push('/lists'); this.props.history.push('/lists');
} }
}, },
}, },
@ -263,4 +262,4 @@ class ListTimeline extends PureComponent {
} }
export default connect(mapStateToProps)(injectIntl(ListTimeline)); export default withRouter(connect(mapStateToProps)(injectIntl(ListTimeline)));

View file

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Link } from 'react-router-dom'; import { Link, withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
@ -15,6 +15,7 @@ import { Icon } from 'mastodon/components/icon';
import AccountContainer from 'mastodon/containers/account_container'; import AccountContainer from 'mastodon/containers/account_container';
import StatusContainer from 'mastodon/containers/status_container'; import StatusContainer from 'mastodon/containers/status_container';
import { me } from 'mastodon/initial_state'; import { me } from 'mastodon/initial_state';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import FollowRequestContainer from '../containers/follow_request_container'; import FollowRequestContainer from '../containers/follow_request_container';
@ -44,11 +45,6 @@ const notificationForScreenReader = (intl, message, timestamp) => {
}; };
class Notification extends ImmutablePureComponent { class Notification extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
notification: ImmutablePropTypes.map.isRequired, notification: ImmutablePropTypes.map.isRequired,
hidden: PropTypes.bool, hidden: PropTypes.bool,
@ -65,6 +61,7 @@ class Notification extends ImmutablePureComponent {
cacheMediaWidth: PropTypes.func, cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number, cachedMediaWidth: PropTypes.number,
unread: PropTypes.bool, unread: PropTypes.bool,
...WithRouterPropTypes,
}; };
handleMoveUp = () => { handleMoveUp = () => {
@ -81,7 +78,7 @@ class Notification extends ImmutablePureComponent {
const { notification } = this.props; const { notification } = this.props;
if (notification.get('status')) { if (notification.get('status')) {
this.context.router.history.push(`/@${notification.getIn(['status', 'account', 'acct'])}/${notification.get('status')}`); this.props.history.push(`/@${notification.getIn(['status', 'account', 'acct'])}/${notification.get('status')}`);
} else { } else {
this.handleOpenProfile(); this.handleOpenProfile();
} }
@ -89,14 +86,14 @@ class Notification extends ImmutablePureComponent {
handleOpenProfile = () => { handleOpenProfile = () => {
const { notification } = this.props; const { notification } = this.props;
this.context.router.history.push(`/@${notification.getIn(['account', 'acct'])}`); this.props.history.push(`/@${notification.getIn(['account', 'acct'])}`);
}; };
handleMention = e => { handleMention = e => {
e.preventDefault(); e.preventDefault();
const { notification, onMention } = this.props; const { notification, onMention } = this.props;
onMention(notification.get('account'), this.context.router.history); onMention(notification.get('account'), this.props.history);
}; };
handleHotkeyFavourite = () => { handleHotkeyFavourite = () => {
@ -560,4 +557,4 @@ class Notification extends ImmutablePureComponent {
} }
export default injectIntl(Notification); export default withRouter(injectIntl(Notification));

View file

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom'; import { Link, withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
@ -19,6 +19,7 @@ import Column from 'mastodon/features/ui/components/column';
import { me } from 'mastodon/initial_state'; import { me } from 'mastodon/initial_state';
import { makeGetAccount } from 'mastodon/selectors'; import { makeGetAccount } from 'mastodon/selectors';
import { assetHost } from 'mastodon/utils/config'; import { assetHost } from 'mastodon/utils/config';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import ArrowSmallRight from './components/arrow_small_right'; import ArrowSmallRight from './components/arrow_small_right';
import Step from './components/step'; import Step from './components/step';
@ -38,15 +39,11 @@ const mapStateToProps = () => {
}; };
class Onboarding extends ImmutablePureComponent { class Onboarding extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,
};
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
...WithRouterPropTypes,
}; };
state = { state = {
@ -56,11 +53,10 @@ class Onboarding extends ImmutablePureComponent {
}; };
handleClose = () => { handleClose = () => {
const { dispatch } = this.props; const { dispatch, history } = this.props;
const { router } = this.context;
dispatch(closeOnboarding()); dispatch(closeOnboarding());
router.history.push('/home'); history.push('/home');
}; };
handleProfileClick = () => { handleProfileClick = () => {
@ -72,10 +68,9 @@ class Onboarding extends ImmutablePureComponent {
}; };
handleComposeClick = () => { handleComposeClick = () => {
const { dispatch, intl } = this.props; const { dispatch, intl, history } = this.props;
const { router } = this.context;
dispatch(focusCompose(router.history, intl.formatMessage(messages.template))); dispatch(focusCompose(history, intl.formatMessage(messages.template)));
}; };
handleShareClick = () => { handleShareClick = () => {
@ -153,4 +148,4 @@ class Onboarding extends ImmutablePureComponent {
} }
export default connect(mapStateToProps)(injectIntl(Onboarding)); export default withRouter(connect(mapStateToProps)(injectIntl(Onboarding)));

View file

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
@ -15,6 +16,7 @@ import { openModal } from 'mastodon/actions/modal';
import { IconButton } from 'mastodon/components/icon_button'; import { IconButton } from 'mastodon/components/icon_button';
import { me, boostModal } from 'mastodon/initial_state'; import { me, boostModal } from 'mastodon/initial_state';
import { makeGetStatus } from 'mastodon/selectors'; import { makeGetStatus } from 'mastodon/selectors';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
const messages = defineMessages({ const messages = defineMessages({
reply: { id: 'status.reply', defaultMessage: 'Reply' }, reply: { id: 'status.reply', defaultMessage: 'Reply' },
@ -43,7 +45,6 @@ const makeMapStateToProps = () => {
class Footer extends ImmutablePureComponent { class Footer extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object, identity: PropTypes.object,
}; };
@ -55,17 +56,17 @@ class Footer extends ImmutablePureComponent {
askReplyConfirmation: PropTypes.bool, askReplyConfirmation: PropTypes.bool,
withOpenButton: PropTypes.bool, withOpenButton: PropTypes.bool,
onClose: PropTypes.func, onClose: PropTypes.func,
...WithRouterPropTypes,
}; };
_performReply = () => { _performReply = () => {
const { dispatch, status, onClose } = this.props; const { dispatch, status, onClose, history } = this.props;
const { router } = this.context;
if (onClose) { if (onClose) {
onClose(true); onClose(true);
} }
dispatch(replyCompose(status, router.history)); dispatch(replyCompose(status, history));
}; };
handleReplyClick = () => { handleReplyClick = () => {
@ -149,9 +150,7 @@ class Footer extends ImmutablePureComponent {
}; };
handleOpenClick = e => { handleOpenClick = e => {
const { router } = this.context; if (e.button !== 0 || !history) {
if (e.button !== 0 || !router) {
return; return;
} }
@ -161,7 +160,7 @@ class Footer extends ImmutablePureComponent {
onClose(); onClose();
} }
router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`); history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
}; };
render () { render () {
@ -204,4 +203,4 @@ class Footer extends ImmutablePureComponent {
} }
export default connect(makeMapStateToProps)(injectIntl(Footer)); export default withRouter(connect(makeMapStateToProps)(injectIntl(Footer)));

View file

@ -41,7 +41,6 @@ const mapStateToProps = (state, { columnId }) => {
class PublicTimeline extends PureComponent { class PublicTimeline extends PureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object, identity: PropTypes.object,
}; };

View file

@ -4,11 +4,13 @@ import { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { IconButton } from '../../../components/icon_button'; import { IconButton } from '../../../components/icon_button';
@ -63,7 +65,6 @@ const mapStateToProps = (state, { status }) => ({
class ActionBar extends PureComponent { class ActionBar extends PureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object, identity: PropTypes.object,
}; };
@ -94,6 +95,7 @@ class ActionBar extends PureComponent {
onPin: PropTypes.func, onPin: PropTypes.func,
onEmbed: PropTypes.func, onEmbed: PropTypes.func,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
...WithRouterPropTypes,
}; };
handleOpenMentions = () => { handleOpenMentions = () => {
@ -129,23 +131,23 @@ class ActionBar extends PureComponent {
}; };
handleDeleteClick = () => { handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.context.router.history); this.props.onDelete(this.props.status, this.props.history);
}; };
handleRedraftClick = () => { handleRedraftClick = () => {
this.props.onDelete(this.props.status, this.context.router.history, true); this.props.onDelete(this.props.status, this.props.history, true);
}; };
handleEditClick = () => { handleEditClick = () => {
this.props.onEdit(this.props.status, this.context.router.history); this.props.onEdit(this.props.status, this.props.history);
}; };
handleDirectClick = () => { handleDirectClick = () => {
this.props.onDirect(this.props.status.get('account'), this.context.router.history); this.props.onDirect(this.props.status.get('account'), this.props.history);
}; };
handleMentionClick = () => { handleMentionClick = () => {
this.props.onMention(this.props.status.get('account'), this.context.router.history); this.props.onMention(this.props.status.get('account'), this.props.history);
}; };
handleMuteClick = () => { handleMuteClick = () => {
@ -373,4 +375,4 @@ class ActionBar extends PureComponent {
} }
export default connect(mapStateToProps)(injectIntl(ActionBar)); export default withRouter(connect(mapStateToProps)(injectIntl(ActionBar)));

View file

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { injectIntl, defineMessages, FormattedDate, FormattedMessage } from 'react-intl'; import { injectIntl, defineMessages, FormattedDate, FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Link } from 'react-router-dom'; import { Link, withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
@ -14,6 +14,7 @@ import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder'; import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
import { enableEmojiReaction } from 'mastodon/initial_state'; import { enableEmojiReaction } from 'mastodon/initial_state';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { Avatar } from '../../../components/avatar'; import { Avatar } from '../../../components/avatar';
import { DisplayName } from '../../../components/display_name'; import { DisplayName } from '../../../components/display_name';
@ -46,10 +47,6 @@ const messages = defineMessages({
class DetailedStatus extends ImmutablePureComponent { class DetailedStatus extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
onOpenMedia: PropTypes.func.isRequired, onOpenMedia: PropTypes.func.isRequired,
@ -68,6 +65,7 @@ class DetailedStatus extends ImmutablePureComponent {
onToggleMediaVisibility: PropTypes.func, onToggleMediaVisibility: PropTypes.func,
onEmojiReact: PropTypes.func, onEmojiReact: PropTypes.func,
onUnEmojiReact: PropTypes.func, onUnEmojiReact: PropTypes.func,
...WithRouterPropTypes,
}; };
state = { state = {
@ -75,9 +73,9 @@ class DetailedStatus extends ImmutablePureComponent {
}; };
handleAccountClick = (e) => { handleAccountClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) { if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.props.history) {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`); this.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
} }
e.stopPropagation(); e.stopPropagation();
@ -282,7 +280,7 @@ class DetailedStatus extends ImmutablePureComponent {
if (['private', 'direct'].includes(status.get('visibility_ex'))) { if (['private', 'direct'].includes(status.get('visibility_ex'))) {
reblogLink = ''; reblogLink = '';
} else if (this.context.router) { } else if (this.props.history) {
reblogLink = ( reblogLink = (
<> <>
{' · '} {' · '}
@ -308,7 +306,7 @@ class DetailedStatus extends ImmutablePureComponent {
); );
} }
if (this.context.router) { if (this.props.history) {
favouriteLink = ( favouriteLink = (
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'> <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'>
<Icon id='star' /> <Icon id='star' />
@ -420,4 +418,4 @@ class DetailedStatus extends ImmutablePureComponent {
} }
export default injectIntl(DetailedStatus); export default withRouter(injectIntl(DetailedStatus));

View file

@ -4,6 +4,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { withRouter } from 'react-router-dom';
import Immutable from 'immutable'; import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
@ -17,6 +18,7 @@ import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import ScrollContainer from 'mastodon/containers/scroll_container'; import ScrollContainer from 'mastodon/containers/scroll_container';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { import {
unblockAccount, unblockAccount,
@ -71,6 +73,7 @@ import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from
import ActionBar from './components/action_bar'; import ActionBar from './components/action_bar';
import DetailedStatus from './components/detailed_status'; import DetailedStatus from './components/detailed_status';
const messages = defineMessages({ const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
@ -199,7 +202,6 @@ const titleFromStatus = (intl, status) => {
class Status extends ImmutablePureComponent { class Status extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object, identity: PropTypes.object,
}; };
@ -219,6 +221,7 @@ class Status extends ImmutablePureComponent {
inUse: PropTypes.bool, inUse: PropTypes.bool,
available: PropTypes.bool, available: PropTypes.bool,
}), }),
...WithRouterPropTypes
}; };
state = { state = {
@ -312,11 +315,11 @@ class Status extends ImmutablePureComponent {
modalProps: { modalProps: {
message: intl.formatMessage(messages.replyMessage), message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm), confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status, this.context.router.history)), onConfirm: () => dispatch(replyCompose(status, this.props.history)),
}, },
})); }));
} else { } else {
dispatch(replyCompose(status, this.context.router.history)); dispatch(replyCompose(status, this.props.history));
} }
} else { } else {
dispatch(openModal({ dispatch(openModal({
@ -560,7 +563,7 @@ class Status extends ImmutablePureComponent {
}; };
handleHotkeyOpenProfile = () => { handleHotkeyOpenProfile = () => {
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`); this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
}; };
handleHotkeyToggleHidden = () => { handleHotkeyToggleHidden = () => {
@ -816,4 +819,4 @@ class Status extends ImmutablePureComponent {
} }
export default injectIntl(connect(makeMapStateToProps)(Status)); export default withRouter(injectIntl(connect(makeMapStateToProps)(Status)));

View file

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
@ -12,6 +13,7 @@ import { changeBoostPrivacy } from 'mastodon/actions/boosts';
import AttachmentList from 'mastodon/components/attachment_list'; import AttachmentList from 'mastodon/components/attachment_list';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown'; import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { Avatar } from '../../../components/avatar'; import { Avatar } from '../../../components/avatar';
import Button from '../../../components/button'; import Button from '../../../components/button';
@ -49,11 +51,6 @@ const mapDispatchToProps = dispatch => {
}; };
class BoostModal extends ImmutablePureComponent { class BoostModal extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
onReblog: PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired,
@ -61,6 +58,7 @@ class BoostModal extends ImmutablePureComponent {
onChangeBoostPrivacy: PropTypes.func.isRequired, onChangeBoostPrivacy: PropTypes.func.isRequired,
privacy: PropTypes.string.isRequired, privacy: PropTypes.string.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
...WithRouterPropTypes,
}; };
componentDidMount() { componentDidMount() {
@ -76,7 +74,7 @@ class BoostModal extends ImmutablePureComponent {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
this.props.onClose(); this.props.onClose();
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`); this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
} }
}; };
@ -155,4 +153,4 @@ class BoostModal extends ImmutablePureComponent {
} }
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(BoostModal)); export default withRouter(connect(mapStateToProps, mapDispatchToProps)(injectIntl(BoostModal)));

View file

@ -54,11 +54,6 @@ const componentMap = {
}; };
export default class ColumnsArea extends ImmutablePureComponent { export default class ColumnsArea extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,
};
static propTypes = { static propTypes = {
columns: ImmutablePropTypes.list.isRequired, columns: ImmutablePropTypes.list.isRequired,
isModalOpen: PropTypes.bool.isRequired, isModalOpen: PropTypes.bool.isRequired,

View file

@ -100,7 +100,7 @@ class LinkFooter extends PureComponent {
{DividingCircle} {DividingCircle}
<a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a> <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a>
{DividingCircle} {DividingCircle}
<span class='version'>v{version}</span> <span className='version'>v{version}</span>
</p> </p>
</div> </div>
); );

View file

@ -1,7 +1,5 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -71,4 +69,4 @@ class ListPanel extends ImmutablePureComponent {
} }
export default withRouter(connect(mapStateToProps)(ListPanel)); export default connect(mapStateToProps)(ListPanel);

View file

@ -6,7 +6,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { WordmarkLogo } from 'mastodon/components/logo'; import { WordmarkLogo } from 'mastodon/components/logo';
import NavigationPortal from 'mastodon/components/navigation_portal'; import { NavigationPortal } from 'mastodon/components/navigation_portal';
import { enableDtlMenu, timelinePreview, trendsEnabled, dtlTag } from 'mastodon/initial_state'; import { enableDtlMenu, timelinePreview, trendsEnabled, dtlTag } from 'mastodon/initial_state';
import { transientSingleColumn } from 'mastodon/is_mobile'; import { transientSingleColumn } from 'mastodon/is_mobile';
@ -41,7 +41,6 @@ const messages = defineMessages({
class NavigationPanel extends Component { class NavigationPanel extends Component {
static contextTypes = { static contextTypes = {
router: PropTypes.object.isRequired,
identity: PropTypes.object.isRequired, identity: PropTypes.object.isRequired,
}; };
@ -67,23 +66,29 @@ class NavigationPanel extends Component {
<ColumnLink transparent to='/search' icon='search' text={intl.formatMessage(messages.search)} /> <ColumnLink transparent to='/search' icon='search' text={intl.formatMessage(messages.search)} />
)); ));
let banner = undefined;
if(transientSingleColumn)
banner = (<div className='switch-to-advanced'>
{intl.formatMessage(messages.openedInClassicInterface)}
{" "}
<a href={`/deck${location.pathname}`} className='switch-to-advanced__toggle'>
{intl.formatMessage(messages.advancedInterface)}
</a>
</div>);
return ( return (
<div className='navigation-panel'> <div className='navigation-panel'>
<div className='navigation-panel__logo'> <div className='navigation-panel__logo'>
<Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link> <Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
{!banner && <hr />}
</div>
{transientSingleColumn ? ( {banner &&
<div class='switch-to-advanced'> <div class='navigation-panel__banner'>
{intl.formatMessage(messages.openedInClassicInterface)} {banner}
{" "}
<a href={`/deck${location.pathname}`} class='switch-to-advanced__toggle'>
{intl.formatMessage(messages.advancedInterface)}
</a>
</div>
) : (
<hr />
)}
</div> </div>
}
{signedIn && ( {signedIn && (
<> <>

View file

@ -16,6 +16,7 @@ import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodo
import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding'; import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
import PictureInPicture from 'mastodon/features/picture_in_picture'; import PictureInPicture from 'mastodon/features/picture_in_picture';
import { layoutFromWindow } from 'mastodon/is_mobile'; import { layoutFromWindow } from 'mastodon/is_mobile';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
import { clearHeight } from '../../actions/height_cache'; import { clearHeight } from '../../actions/height_cache';
@ -278,7 +279,6 @@ class SwitchingColumnsArea extends PureComponent {
class UI extends PureComponent { class UI extends PureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object.isRequired,
identity: PropTypes.object.isRequired, identity: PropTypes.object.isRequired,
}; };
@ -289,12 +289,12 @@ class UI extends PureComponent {
hasComposingText: PropTypes.bool, hasComposingText: PropTypes.bool,
hasMediaAttachments: PropTypes.bool, hasMediaAttachments: PropTypes.bool,
canUploadMore: PropTypes.bool, canUploadMore: PropTypes.bool,
location: PropTypes.object,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
dropdownMenuIsOpen: PropTypes.bool, dropdownMenuIsOpen: PropTypes.bool,
layout: PropTypes.string.isRequired, layout: PropTypes.string.isRequired,
firstLaunch: PropTypes.bool, firstLaunch: PropTypes.bool,
username: PropTypes.string, username: PropTypes.string,
...WithRouterPropTypes,
}; };
state = { state = {
@ -391,7 +391,7 @@ class UI extends PureComponent {
handleServiceWorkerPostMessage = ({ data }) => { handleServiceWorkerPostMessage = ({ data }) => {
if (data.type === 'navigate') { if (data.type === 'navigate') {
this.context.router.history.push(data.path); this.props.history.push(data.path);
} else { } else {
console.warn('Unknown message type:', data.type); console.warn('Unknown message type:', data.type);
} }
@ -512,12 +512,12 @@ class UI extends PureComponent {
}; };
handleHotkeyBack = () => { handleHotkeyBack = () => {
const { router } = this.context; const { history } = this.props;
if (router.history.location?.state?.fromMastodon) { if (history.location?.state?.fromMastodon) {
router.history.goBack(); history.goBack();
} else { } else {
router.history.push('/'); history.push('/');
} }
}; };
@ -527,38 +527,38 @@ class UI extends PureComponent {
handleHotkeyToggleHelp = () => { handleHotkeyToggleHelp = () => {
if (this.props.location.pathname === '/keyboard-shortcuts') { if (this.props.location.pathname === '/keyboard-shortcuts') {
this.context.router.history.goBack(); this.props.history.goBack();
} else { } else {
this.context.router.history.push('/keyboard-shortcuts'); this.props.history.push('/keyboard-shortcuts');
} }
}; };
handleHotkeyGoToHome = () => { handleHotkeyGoToHome = () => {
this.context.router.history.push('/home'); this.props.history.push('/home');
}; };
handleHotkeyGoToNotifications = () => { handleHotkeyGoToNotifications = () => {
this.context.router.history.push('/notifications'); this.props.history.push('/notifications');
}; };
handleHotkeyGoToLocal = () => { handleHotkeyGoToLocal = () => {
this.context.router.history.push('/public/local'); this.props.history.push('/public/local');
}; };
handleHotkeyGoToFederated = () => { handleHotkeyGoToFederated = () => {
this.context.router.history.push('/public'); this.props.history.push('/public');
}; };
handleHotkeyGoToDirect = () => { handleHotkeyGoToDirect = () => {
this.context.router.history.push('/conversations'); this.props.history.push('/conversations');
}; };
handleHotkeyGoToStart = () => { handleHotkeyGoToStart = () => {
this.context.router.history.push('/getting-started'); this.props.history.push('/getting-started');
}; };
handleHotkeyGoToFavourites = () => { handleHotkeyGoToFavourites = () => {
this.context.router.history.push('/favourites'); this.props.history.push('/favourites');
}; };
handleHotkeyGoToEmojiReactions = () => { handleHotkeyGoToEmojiReactions = () => {
@ -566,23 +566,23 @@ class UI extends PureComponent {
}; };
handleHotkeyGoToPinned = () => { handleHotkeyGoToPinned = () => {
this.context.router.history.push('/pinned'); this.props.history.push('/pinned');
}; };
handleHotkeyGoToProfile = () => { handleHotkeyGoToProfile = () => {
this.context.router.history.push(`/@${this.props.username}`); this.props.history.push(`/@${this.props.username}`);
}; };
handleHotkeyGoToBlocked = () => { handleHotkeyGoToBlocked = () => {
this.context.router.history.push('/blocks'); this.props.history.push('/blocks');
}; };
handleHotkeyGoToMuted = () => { handleHotkeyGoToMuted = () => {
this.context.router.history.push('/mutes'); this.props.history.push('/mutes');
}; };
handleHotkeyGoToRequests = () => { handleHotkeyGoToRequests = () => {
this.context.router.history.push('/follow_requests'); this.props.history.push('/follow_requests');
}; };
render () { render () {

View file

@ -1,7 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Component, PureComponent, cloneElement, Children } from 'react'; import { Component, cloneElement, Children } from 'react';
import { Switch, Route } from 'react-router-dom'; import { Switch, Route, useLocation } from 'react-router-dom';
import StackTrace from 'stacktrace-js'; import StackTrace from 'stacktrace-js';
@ -10,14 +10,8 @@ import ColumnLoading from '../components/column_loading';
import BundleContainer from '../containers/bundle_container'; import BundleContainer from '../containers/bundle_container';
// Small wrapper to pass multiColumn to the route components // Small wrapper to pass multiColumn to the route components
export class WrappedSwitch extends PureComponent { export const WrappedSwitch = ({ multiColumn, children }) => {
static contextTypes = { const location = useLocation();
router: PropTypes.object,
};
render () {
const { multiColumn, children } = this.props;
const { location } = this.context.router.route;
const decklessLocation = multiColumn && location.pathname.startsWith('/deck') const decklessLocation = multiColumn && location.pathname.startsWith('/deck')
? {...location, pathname: location.pathname.slice(5)} ? {...location, pathname: location.pathname.slice(5)}
@ -28,9 +22,8 @@ export class WrappedSwitch extends PureComponent {
{Children.map(children, child => child ? cloneElement(child, { multiColumn }) : null)} {Children.map(children, child => child ? cloneElement(child, { multiColumn }) : null)}
</Switch> </Switch>
); );
} };
}
WrappedSwitch.propTypes = { WrappedSwitch.propTypes = {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,

View file

@ -71,11 +71,11 @@
"account.unmute_notifications_short": "Poista ilmoitusten mykistys", "account.unmute_notifications_short": "Poista ilmoitusten mykistys",
"account.unmute_short": "Poista mykistys", "account.unmute_short": "Poista mykistys",
"account_note.placeholder": "Lisää muistiinpano napsauttamalla", "account_note.placeholder": "Lisää muistiinpano napsauttamalla",
"admin.dashboard.daily_retention": "Käyttäjän pysyminen rekisteröitymisen jälkeiseen päivään mennessä", "admin.dashboard.daily_retention": "Käyttäjien pysyvyys rekisteröitymisen jälkeen päivittäin",
"admin.dashboard.monthly_retention": "Käyttäjän pysyminen rekisteröitymisen jälkeiseen kuukauteen mennessä", "admin.dashboard.monthly_retention": "Käyttäjien pysyvyys rekisteröitymisen jälkeen kuukausittain",
"admin.dashboard.retention.average": "Keskimäärin", "admin.dashboard.retention.average": "Keskimäärin",
"admin.dashboard.retention.cohort": "Kirjautumiset", "admin.dashboard.retention.cohort": "Rekisteröitymis-kk.",
"admin.dashboard.retention.cohort_size": "Uudet käyttäjät", "admin.dashboard.retention.cohort_size": "Uusia käyttäjiä",
"admin.impact_report.instance_accounts": "Tilien profiilit, jotka tämä poistaisi", "admin.impact_report.instance_accounts": "Tilien profiilit, jotka tämä poistaisi",
"admin.impact_report.instance_followers": "Seuraajat, jotka käyttäjämme menettäisivät", "admin.impact_report.instance_followers": "Seuraajat, jotka käyttäjämme menettäisivät",
"admin.impact_report.instance_follows": "Seuraajat, jotka heidän käyttäjänsä menettäisivät", "admin.impact_report.instance_follows": "Seuraajat, jotka heidän käyttäjänsä menettäisivät",
@ -114,7 +114,7 @@
"column.directory": "Selaa profiileja", "column.directory": "Selaa profiileja",
"column.domain_blocks": "Estetyt verkkotunnukset", "column.domain_blocks": "Estetyt verkkotunnukset",
"column.favourites": "Suosikit", "column.favourites": "Suosikit",
"column.firehose": "Live-syötteet", "column.firehose": "Livesyötteet",
"column.follow_requests": "Seuraamispyynnöt", "column.follow_requests": "Seuraamispyynnöt",
"column.home": "Koti", "column.home": "Koti",
"column.lists": "Listat", "column.lists": "Listat",
@ -135,7 +135,7 @@
"community.column_settings.remote_only": "Vain etätilit", "community.column_settings.remote_only": "Vain etätilit",
"compose.language.change": "Vaihda kieli", "compose.language.change": "Vaihda kieli",
"compose.language.search": "Hae kieliä...", "compose.language.search": "Hae kieliä...",
"compose.published.body": "Julkaisusi julkaistiin.", "compose.published.body": "Julkaisu lähetetty.",
"compose.published.open": "Avaa", "compose.published.open": "Avaa",
"compose.saved.body": "Julkaisu tallennettu.", "compose.saved.body": "Julkaisu tallennettu.",
"compose_form.direct_message_warning_learn_more": "Lisätietoja", "compose_form.direct_message_warning_learn_more": "Lisätietoja",
@ -436,10 +436,10 @@
"notifications.clear": "Tyhjennä ilmoitukset", "notifications.clear": "Tyhjennä ilmoitukset",
"notifications.clear_confirmation": "Haluatko varmasti poistaa kaikki ilmoitukset pysyvästi?", "notifications.clear_confirmation": "Haluatko varmasti poistaa kaikki ilmoitukset pysyvästi?",
"notifications.column_settings.admin.report": "Uudet ilmoitukset:", "notifications.column_settings.admin.report": "Uudet ilmoitukset:",
"notifications.column_settings.admin.sign_up": "Uudet kirjautumiset:", "notifications.column_settings.admin.sign_up": "Uudet rekisteröitymiset:",
"notifications.column_settings.alert": "Työpöytäilmoitukset", "notifications.column_settings.alert": "Työpöytäilmoitukset",
"notifications.column_settings.favourite": "Suosikit:", "notifications.column_settings.favourite": "Suosikit:",
"notifications.column_settings.filter_bar.advanced": "Näytä kaikki kategoriat", "notifications.column_settings.filter_bar.advanced": "Näytä kaikki luokat",
"notifications.column_settings.filter_bar.category": "Pikasuodatuspalkki", "notifications.column_settings.filter_bar.category": "Pikasuodatuspalkki",
"notifications.column_settings.filter_bar.show_bar": "Näytä suodatinpalkki", "notifications.column_settings.filter_bar.show_bar": "Näytä suodatinpalkki",
"notifications.column_settings.follow": "Uudet seuraajat:", "notifications.column_settings.follow": "Uudet seuraajat:",
@ -517,7 +517,7 @@
"privacy.private.short": "Vain seuraajat", "privacy.private.short": "Vain seuraajat",
"privacy.public.long": "Näkyy kaikille", "privacy.public.long": "Näkyy kaikille",
"privacy.public.short": "Julkinen", "privacy.public.short": "Julkinen",
"privacy.unlisted.long": "Näkyy kaikille, mutta jää pois löytämisominaisuuksista", "privacy.unlisted.long": "Näkyy kaikille mutta jää pois löytämisominaisuuksista",
"privacy.unlisted.short": "Listaamaton", "privacy.unlisted.short": "Listaamaton",
"privacy_policy.last_updated": "Viimeksi päivitetty {date}", "privacy_policy.last_updated": "Viimeksi päivitetty {date}",
"privacy_policy.title": "Tietosuojakäytäntö", "privacy_policy.title": "Tietosuojakäytäntö",
@ -589,13 +589,13 @@
"search.quick_action.go_to_hashtag": "Siirry aihetunnisteeseen {x}", "search.quick_action.go_to_hashtag": "Siirry aihetunnisteeseen {x}",
"search.quick_action.open_url": "Avaa URL-osoite Mastodonissa", "search.quick_action.open_url": "Avaa URL-osoite Mastodonissa",
"search.quick_action.status_search": "Julkaisut haulla {x}", "search.quick_action.status_search": "Julkaisut haulla {x}",
"search.search_or_paste": "Hae tai kirjoita URL-osoite", "search.search_or_paste": "Hae tai liitä URL-osoite",
"search_popout.full_text_search_disabled_message": "Ei saatavilla palvelimella {domain}.", "search_popout.full_text_search_disabled_message": "Ei saatavilla palvelimella {domain}.",
"search_popout.language_code": "ISO-kielikoodi", "search_popout.language_code": "ISO-kielikoodi",
"search_popout.options": "Hakuvalinnat", "search_popout.options": "Hakuvalinnat",
"search_popout.quick_actions": "Pikatoiminnot", "search_popout.quick_actions": "Pikatoiminnot",
"search_popout.recent": "Viimeaikaiset haut", "search_popout.recent": "Viimeaikaiset haut",
"search_popout.specific_date": "tietty päivämäärä", "search_popout.specific_date": "tarkka päiväys",
"search_popout.user": "käyttäjä", "search_popout.user": "käyttäjä",
"search_results.accounts": "Profiilit", "search_results.accounts": "Profiilit",
"search_results.all": "Kaikki", "search_results.all": "Kaikki",

View file

@ -209,7 +209,7 @@
"compose.language.search": "言語を検索...", "compose.language.search": "言語を検索...",
"compose.published.body": "投稿されました!", "compose.published.body": "投稿されました!",
"compose.published.open": "開く", "compose.published.open": "開く",
"compose.saved.body": "投稿が保存されました", "compose.saved.body": "変更を保存しました。",
"compose_form.direct_message_warning_learn_more": "もっと詳しく", "compose_form.direct_message_warning_learn_more": "もっと詳しく",
"compose_form.encryption_warning": "Mastodonの投稿はエンドツーエンド暗号化に対応していません。安全に送受信されるべき情報をMastodonで共有しないでください。", "compose_form.encryption_warning": "Mastodonの投稿はエンドツーエンド暗号化に対応していません。安全に送受信されるべき情報をMastodonで共有しないでください。",
"compose_form.hashtag_warning": "この投稿は公開設定ではないのでハッシュタグの一覧に表示されません。公開投稿だけがハッシュタグで検索できます。", "compose_form.hashtag_warning": "この投稿は公開設定ではないのでハッシュタグの一覧に表示されません。公開投稿だけがハッシュタグで検索できます。",

View file

@ -204,7 +204,7 @@
"dismissable_banner.explore_links": "이 소식들은 오늘 소셜 웹에서 가장 많이 공유된 내용들입니다. 새 소식을 더 많은 사람들이 공유할수록 높은 순위가 됩니다.", "dismissable_banner.explore_links": "이 소식들은 오늘 소셜 웹에서 가장 많이 공유된 내용들입니다. 새 소식을 더 많은 사람들이 공유할수록 높은 순위가 됩니다.",
"dismissable_banner.explore_statuses": "이 게시물들은 오늘 소셜 웹에서 호응을 얻고 있는 게시물들입니다. 부스트와 관심을 받는 새로운 글들이 높은 순위가 됩니다.", "dismissable_banner.explore_statuses": "이 게시물들은 오늘 소셜 웹에서 호응을 얻고 있는 게시물들입니다. 부스트와 관심을 받는 새로운 글들이 높은 순위가 됩니다.",
"dismissable_banner.explore_tags": "이 해시태그들은 이 서버와 분산화된 네트워크의 다른 서버에서 사람들의 인기를 끌고 있는 것들입니다.", "dismissable_banner.explore_tags": "이 해시태그들은 이 서버와 분산화된 네트워크의 다른 서버에서 사람들의 인기를 끌고 있는 것들입니다.",
"dismissable_banner.public_timeline": "이것들은 {domain}에 있는 사람들이 팔로우한 사람들의 최신 게시물들입니다.", "dismissable_banner.public_timeline": "{domain} 사람들이 팔로우하는 소셜 웹 사람들의 최신 공개 게시물입니다.",
"embed.instructions": "아래의 코드를 복사하여 대화를 원하는 곳으로 공유하세요.", "embed.instructions": "아래의 코드를 복사하여 대화를 원하는 곳으로 공유하세요.",
"embed.preview": "이렇게 표시됩니다:", "embed.preview": "이렇게 표시됩니다:",
"emoji_button.activity": "활동", "emoji_button.activity": "활동",

View file

@ -153,7 +153,7 @@
"compose_form.publish": "Legg ut", "compose_form.publish": "Legg ut",
"compose_form.publish_form": "Legg ut", "compose_form.publish_form": "Legg ut",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Gøym", "compose_form.save_changes": "Lagre endringar",
"compose_form.sensitive.hide": "{count, plural, one {Marker mediet som ømtolig} other {Marker media som ømtolige}}", "compose_form.sensitive.hide": "{count, plural, one {Marker mediet som ømtolig} other {Marker media som ømtolige}}",
"compose_form.sensitive.marked": "{count, plural, one {Mediet er markert som ømtolig} other {Media er markerte som ømtolige}}", "compose_form.sensitive.marked": "{count, plural, one {Mediet er markert som ømtolig} other {Media er markerte som ømtolige}}",
"compose_form.sensitive.unmarked": "{count, plural, one {Mediet er ikkje markert som ømtolig} other {Media er ikkje markerte som ømtolige}}", "compose_form.sensitive.unmarked": "{count, plural, one {Mediet er ikkje markert som ømtolig} other {Media er ikkje markerte som ømtolige}}",
@ -171,7 +171,7 @@
"confirmations.delete_list.confirm": "Slett", "confirmations.delete_list.confirm": "Slett",
"confirmations.delete_list.message": "Er du sikker på at du vil sletta denne lista for alltid?", "confirmations.delete_list.message": "Er du sikker på at du vil sletta denne lista for alltid?",
"confirmations.discard_edit_media.confirm": "Forkast", "confirmations.discard_edit_media.confirm": "Forkast",
"confirmations.discard_edit_media.message": "Du har ulagra endringar i mediaskildringa eller førehandsvisinga. Vil du forkaste dei likevel?", "confirmations.discard_edit_media.message": "Du har ulagra endringar i mediaskildringa eller førehandsvisinga. Vil du forkasta dei likevel?",
"confirmations.domain_block.confirm": "Skjul alt frå domenet", "confirmations.domain_block.confirm": "Skjul alt frå domenet",
"confirmations.domain_block.message": "Er du heilt, heilt sikker på at du vil skjula heile {domain}? I dei fleste tilfelle er det godt nok og føretrekt med nokre få målretta blokkeringar eller målbindingar. Du kjem ikkje til å sjå innhald frå domenet i fødererte tidsliner eller i varsla dine. Fylgjarane dine frå domenet vert fjerna.", "confirmations.domain_block.message": "Er du heilt, heilt sikker på at du vil skjula heile {domain}? I dei fleste tilfelle er det godt nok og føretrekt med nokre få målretta blokkeringar eller målbindingar. Du kjem ikkje til å sjå innhald frå domenet i fødererte tidsliner eller i varsla dine. Fylgjarane dine frå domenet vert fjerna.",
"confirmations.edit.confirm": "Rediger", "confirmations.edit.confirm": "Rediger",
@ -285,7 +285,7 @@
"footer.privacy_policy": "Personvernsreglar", "footer.privacy_policy": "Personvernsreglar",
"footer.source_code": "Vis kjeldekode", "footer.source_code": "Vis kjeldekode",
"footer.status": "Status", "footer.status": "Status",
"generic.saved": "Gøymt", "generic.saved": "Lagra",
"getting_started.heading": "Kom i gang", "getting_started.heading": "Kom i gang",
"hashtag.column_header.tag_mode.all": "og {additional}", "hashtag.column_header.tag_mode.all": "og {additional}",
"hashtag.column_header.tag_mode.any": "eller {additional}", "hashtag.column_header.tag_mode.any": "eller {additional}",
@ -314,7 +314,7 @@
"home.pending_critical_update.link": "Sjå oppdateringar", "home.pending_critical_update.link": "Sjå oppdateringar",
"home.pending_critical_update.title": "Kritisk sikkerheitsoppdatering er tilgjengeleg!", "home.pending_critical_update.title": "Kritisk sikkerheitsoppdatering er tilgjengeleg!",
"home.show_announcements": "Vis kunngjeringar", "home.show_announcements": "Vis kunngjeringar",
"interaction_modal.description.favourite": "Med ein konto på Mastodon kan du favorittmerkja dette innlegget for å visa forfattaren at du set pris på det, og for å lagra det til seinare.", "interaction_modal.description.favourite": "Med ein konto på Mastodon kan du favorittmerka dette innlegget for å visa forfattaren at du set pris på det, og for å lagra det til seinare.",
"interaction_modal.description.follow": "Med ein konto på Mastodon kan du fylgja {name} for å sjå innlegga deira i din heimestraum.", "interaction_modal.description.follow": "Med ein konto på Mastodon kan du fylgja {name} for å sjå innlegga deira i din heimestraum.",
"interaction_modal.description.reblog": "Med ein konto på Mastodon kan du framheva dette innlegget for å dela det med dine eigne fylgjarar.", "interaction_modal.description.reblog": "Med ein konto på Mastodon kan du framheva dette innlegget for å dela det med dine eigne fylgjarar.",
"interaction_modal.description.reply": "Med ein konto på Mastodon kan du svara på dette innlegget.", "interaction_modal.description.reply": "Med ein konto på Mastodon kan du svara på dette innlegget.",
@ -673,7 +673,7 @@
"status.unmute_conversation": "Opphev målbinding av samtalen", "status.unmute_conversation": "Opphev målbinding av samtalen",
"status.unpin": "Løys frå profil", "status.unpin": "Løys frå profil",
"subscribed_languages.lead": "Kun innlegg på valde språk vil bli dukke opp i heimestraumen din og i listene dine etter denne endringa. For å motta innlegg på alle språk, la vere å velje nokon.", "subscribed_languages.lead": "Kun innlegg på valde språk vil bli dukke opp i heimestraumen din og i listene dine etter denne endringa. For å motta innlegg på alle språk, la vere å velje nokon.",
"subscribed_languages.save": "Gøym", "subscribed_languages.save": "Lagre endringar",
"subscribed_languages.target": "Endre abonnerte språk for {target}", "subscribed_languages.target": "Endre abonnerte språk for {target}",
"tabs_bar.home": "Heim", "tabs_bar.home": "Heim",
"tabs_bar.notifications": "Varsel", "tabs_bar.notifications": "Varsel",

View file

@ -303,7 +303,7 @@
"hashtag.unfollow": "Отпрати хеш ознаку", "hashtag.unfollow": "Отпрати хеш ознаку",
"hashtags.and_other": "…и {count, plural, one {још #} few {још #}other {још #}}", "hashtags.and_other": "…и {count, plural, one {још #} few {још #}other {још #}}",
"home.actions.go_to_explore": "Погледате шта је у тренду", "home.actions.go_to_explore": "Погледате шта је у тренду",
"home.actions.go_to_suggestions": "Пронађeте људе које бисте пратили", "home.actions.go_to_suggestions": "Пронађете људе које бисте пратили",
"home.column_settings.basic": "Основна", "home.column_settings.basic": "Основна",
"home.column_settings.show_reblogs": "Прикажи подржавања", "home.column_settings.show_reblogs": "Прикажи подржавања",
"home.column_settings.show_replies": "Прикажи одговоре", "home.column_settings.show_replies": "Прикажи одговоре",

View file

@ -114,7 +114,7 @@
"column.directory": "瀏覽個人檔案", "column.directory": "瀏覽個人檔案",
"column.domain_blocks": "已封鎖網域", "column.domain_blocks": "已封鎖網域",
"column.favourites": "最愛", "column.favourites": "最愛",
"column.firehose": "即時內容", "column.firehose": "即時河道",
"column.follow_requests": "跟隨請求", "column.follow_requests": "跟隨請求",
"column.home": "首頁", "column.home": "首頁",
"column.lists": "列表", "column.lists": "列表",

View file

@ -0,0 +1,60 @@
import PropTypes from "prop-types";
import { __RouterContext } from "react-router";
import hoistStatics from "hoist-non-react-statics";
export const WithRouterPropTypes = {
match: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
};
export const WithOptionalRouterPropTypes = {
match: PropTypes.object,
location: PropTypes.object,
history: PropTypes.object,
};
// This is copied from https://github.com/remix-run/react-router/blob/v5.3.4/packages/react-router/modules/withRouter.js
// but does not fail if called outside of a React Router context
export function withOptionalRouter(Component) {
const displayName = `withRouter(${Component.displayName || Component.name})`;
const C = props => {
const { wrappedComponentRef, ...remainingProps } = props;
return (
<__RouterContext.Consumer>
{context => {
if(context)
return (
<Component
{...remainingProps}
{...context}
ref={wrappedComponentRef}
/>
);
else
return (
<Component
{...remainingProps}
ref={wrappedComponentRef}
/>
);
}}
</__RouterContext.Consumer>
);
};
C.displayName = displayName;
C.WrappedComponent = Component;
C.propTypes = {
wrappedComponentRef: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
PropTypes.object
])
};
return hoistStatics(C, Component);
}

View file

@ -2536,6 +2536,7 @@ $ui-header-height: 55px;
.navigation-panel__sign-in-banner, .navigation-panel__sign-in-banner,
.navigation-panel__logo, .navigation-panel__logo,
.navigation-panel__banner,
.getting-started__trends { .getting-started__trends {
display: none; display: none;
} }

View file

@ -19,7 +19,7 @@ class ActivityPub::LinkedDataSignature
return unless type == 'RsaSignature2017' return unless type == 'RsaSignature2017'
creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri) creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
creator ||= ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) if creator&.public_key.blank?
return if creator.nil? return if creator.nil?
@ -28,6 +28,8 @@ class ActivityPub::LinkedDataSignature
to_be_verified = options_hash + document_hash to_be_verified = options_hash + document_hash
creator if creator.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), Base64.decode64(signature), to_be_verified) creator if creator.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), Base64.decode64(signature), to_be_verified)
rescue OpenSSL::PKey::RSAError
false
end end
def sign!(creator, sign_with: nil) def sign!(creator, sign_with: nil)

View file

@ -51,7 +51,7 @@ class FeaturedTag < ApplicationRecord
private private
def strip_name def strip_name
self.name = name&.strip&.gsub(/\A#/, '') self.name = name&.strip&.delete_prefix('#')
end end
def set_tag def set_tag

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class REST::ApplicationSerializer < ActiveModel::Serializer class REST::ApplicationSerializer < ActiveModel::Serializer
attributes :id, :name, :website, :redirect_uri, attributes :id, :name, :website, :scopes, :redirect_uri,
:client_id, :client_secret, :vapid_key :client_id, :client_secret, :vapid_key
def id def id

View file

@ -101,8 +101,6 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
end end
end end
added_media_attachments = @next_media_attachments - previous_media_attachments
@status.ordered_media_attachment_ids = @next_media_attachments.map(&:id) @status.ordered_media_attachment_ids = @next_media_attachments.map(&:id)
@media_attachments_changed = true if @status.ordered_media_attachment_ids != previous_media_attachments_ids @media_attachments_changed = true if @status.ordered_media_attachment_ids != previous_media_attachments_ids

View file

@ -4,7 +4,7 @@
= image_tag appeal.account.avatar.url(:original), alt: '', width: 40, height: 40, class: 'avatar' = image_tag appeal.account.avatar.url(:original), alt: '', width: 40, height: 40, class: 'avatar'
.log-entry__content .log-entry__content
.log-entry__title .log-entry__title
= t(appeal.strike.action, scope: 'admin.strikes.actions', name: content_tag(:span, appeal.strike.account.username, class: 'username'), target: content_tag(:span, appeal.account.username, class: 'target')).html_safe = strike_action_label(appeal)
.log-entry__timestamp .log-entry__timestamp
%time.formatted{ datetime: appeal.strike.created_at.iso8601 } %time.formatted{ datetime: appeal.strike.created_at.iso8601 }
= l(appeal.strike.created_at) = l(appeal.strike.created_at)

View file

@ -4,7 +4,7 @@
.batch-table__row__content.pending-account .batch-table__row__content.pending-account
.pending-account__header .pending-account__header
= link_to preview_card.title, preview_card.url = link_to preview_card.title, url_for_preview_card(preview_card)
%br/ %br/

View file

@ -1,118 +0,0 @@
{
"ignored_warnings": [
{
"warning_type": "Cross-Site Scripting",
"warning_code": 2,
"fingerprint": "71cf98c8235b5cfa9946b5db8fdc1a2f3a862566abb34e4542be6f3acae78233",
"check_name": "CrossSiteScripting",
"message": "Unescaped model attribute",
"file": "app/views/admin/disputes/appeals/_appeal.html.haml",
"line": 7,
"link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting",
"code": "t((Unresolved Model).new.strike.action, :scope => \"admin.strikes.actions\", :name => content_tag(:span, (Unresolved Model).new.strike.account.username, :class => \"username\"), :target => content_tag(:span, (Unresolved Model).new.account.username, :class => \"target\"))",
"render_path": [
{
"type": "template",
"name": "admin/disputes/appeals/index",
"line": 20,
"file": "app/views/admin/disputes/appeals/index.html.haml",
"rendered": {
"name": "admin/disputes/appeals/_appeal",
"file": "app/views/admin/disputes/appeals/_appeal.html.haml"
}
}
],
"location": {
"type": "template",
"template": "admin/disputes/appeals/_appeal"
},
"user_input": "(Unresolved Model).new.strike",
"confidence": "Weak",
"cwe_id": [
79
],
"note": ""
},
{
"warning_type": "Cross-Site Scripting",
"warning_code": 4,
"fingerprint": "cd5cfd7f40037fbfa753e494d7129df16e358bfc43ef0da3febafbf4ee1ed3ac",
"check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in `link_to` href",
"file": "app/views/admin/trends/links/_preview_card.html.haml",
"line": 7,
"link": "https://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to((Unresolved Model).new.title, (Unresolved Model).new.url)",
"render_path": [
{
"type": "template",
"name": "admin/trends/links/index",
"line": 49,
"file": "app/views/admin/trends/links/index.html.haml",
"rendered": {
"name": "admin/trends/links/_preview_card",
"file": "app/views/admin/trends/links/_preview_card.html.haml"
}
}
],
"location": {
"type": "template",
"template": "admin/trends/links/_preview_card"
},
"user_input": "(Unresolved Model).new.url",
"confidence": "Weak",
"cwe_id": [
79
],
"note": ""
},
{
"warning_type": "Mass Assignment",
"warning_code": 105,
"fingerprint": "d0511f0287aea4ed9511f5a744f880cb15af77a8ec88f81b7365b00b642cf427",
"check_name": "PermitAttributes",
"message": "Potentially dangerous key allowed for mass assignment",
"file": "app/controllers/api/v1/reports_controller.rb",
"line": 26,
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
"code": "params.permit(:account_id, :comment, :category, :forward, :forward_to_domains => ([]), :status_ids => ([]), :rule_ids => ([]))",
"render_path": null,
"location": {
"type": "method",
"class": "Api::V1::ReportsController",
"method": "report_params"
},
"user_input": ":account_id",
"confidence": "High",
"cwe_id": [
915
],
"note": ""
},
{
"warning_type": "Mass Assignment",
"warning_code": 105,
"fingerprint": "dd59382eb5fda8da4d29f5d52dc8261ed9070d3c61cecc12b8332e18448b1c11",
"check_name": "PermitAttributes",
"message": "Potentially dangerous key allowed for mass assignment",
"file": "app/controllers/api/v2/search_controller.rb",
"line": 42,
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
"code": "params.permit(:type, :offset, :min_id, :max_id, :account_id, :following, :searchability)",
"render_path": null,
"location": {
"type": "method",
"class": "Api::V2::SearchController",
"method": "search_params"
},
"user_input": ":account_id",
"confidence": "High",
"cwe_id": [
915
],
"note": ""
}
],
"updated": "2023-07-30 22:53:30 +0900",
"brakeman_version": "6.0.1"
}

View file

@ -1,3 +1,5 @@
--- ---
:skip_checks: :skip_checks:
- CheckPermitAttributes - CheckPermitAttributes
:url_safe_methods:
- url_for_preview_card

View file

@ -9,9 +9,6 @@ Rails.application.config.middleware.use OmniAuth::Builder do
end end
Devise.setup do |config| Devise.setup do |config|
# Devise omniauth strategies
options = {}
# CAS strategy # CAS strategy
if ENV['CAS_ENABLED'] == 'true' if ENV['CAS_ENABLED'] == 'true'
cas_options = {} cas_options = {}

View file

@ -53,3 +53,7 @@ sk:
position: position:
elevated: nemôže byť vyššia ako vaša súčasná rola elevated: nemôže byť vyššia ako vaša súčasná rola
own_role: nie je možné zmeniť s vašou aktuálnou rolou own_role: nie je možné zmeniť s vašou aktuálnou rolou
webhook:
attributes:
events:
invalid_permissions: nemožno zahrnúť udalosti, ku ktorým nemáte práva

View file

@ -129,6 +129,7 @@ sk:
crypto: Šifrovanie End-to-end crypto: Šifrovanie End-to-end
favourites: Obľúbené favourites: Obľúbené
filters: Filtre filters: Filtre
follow: Sledovanie, stlmenie a blokovanie
follows: Sledovania follows: Sledovania
lists: Zoznamy lists: Zoznamy
media: Mediálne prílohy media: Mediálne prílohy
@ -148,9 +149,19 @@ sk:
scopes: scopes:
admin:read: prezeraj všetky dáta na serveri admin:read: prezeraj všetky dáta na serveri
admin:read:accounts: prezeraj chúlostivé informácie na všetkých účtoch admin:read:accounts: prezeraj chúlostivé informácie na všetkých účtoch
admin:read:canonical_email_blocks: čítať citlivé informácie všetkých kanonických e-mailových blokov
admin:read:domain_allows: čítať citlivé informácie zo všetkých povolených domén
admin:read:domain_blocks: čítať citlivé informácie zo všetkých blokov domén
admin:read:email_domain_blocks: čítať citlivé informácie zo všetkých blokov emailových domén
admin:read:ip_blocks: čítať citlivé informácie zo všetkých blokov IP
admin:read:reports: čítaj chulostivé informácie o všetkých hláseniach a nahlásených účtoch admin:read:reports: čítaj chulostivé informácie o všetkých hláseniach a nahlásených účtoch
admin:write: uprav všetky dáta na serveri admin:write: uprav všetky dáta na serveri
admin:write:accounts: urob moderovacie úkony na účtoch admin:write:accounts: urob moderovacie úkony na účtoch
admin:write:canonical_email_blocks: vykonať akcie moderácie na kanonických emailových blokoch
admin:write:domain_allows: vykonať akcie moderácie na povolených doménach
admin:write:domain_blocks: vykonať akcie moderácie na doménových blokoch
admin:write:email_domain_blocks: vykonať akcie moderácie na blokoch emailových domén
admin:write:ip_blocks: vykonať akcie moderácie na blokoch IP
admin:write:reports: urob moderovacie úkony voči hláseniam admin:write:reports: urob moderovacie úkony voči hláseniam
crypto: používať end-to-end šifrovanie crypto: používať end-to-end šifrovanie
follow: uprav vzťahy svojho účtu follow: uprav vzťahy svojho účtu
@ -159,6 +170,7 @@ sk:
read:accounts: prezri si informácie o účte read:accounts: prezri si informácie o účte
read:blocks: prezri svoje bloky read:blocks: prezri svoje bloky
read:bookmarks: pozri svoje záložky read:bookmarks: pozri svoje záložky
read:favourites: zobraziť vaše obľúbené
read:filters: prezri svoje filtrovanie read:filters: prezri svoje filtrovanie
read:follows: prezri si svoje sledovania read:follows: prezri si svoje sledovania
read:lists: prezri si svoje zoznamy read:lists: prezri si svoje zoznamy

View file

@ -33,7 +33,7 @@ fi:
accounts: accounts:
add_email_domain_block: Estä sähköpostiverkkotunnus add_email_domain_block: Estä sähköpostiverkkotunnus
approve: Hyväksy approve: Hyväksy
approved_msg: Käyttäjän %{username} liittymishakemus hyväksyttiin approved_msg: Käyttäjän %{username} rekisteröitymishakemus hyväksyttiin
are_you_sure: Oletko varma? are_you_sure: Oletko varma?
avatar: Profiilikuva avatar: Profiilikuva
by_domain: Verkkotunnus by_domain: Verkkotunnus
@ -364,7 +364,7 @@ fi:
other: "<strong>%{count}</strong> odottavaa käyttäjää" other: "<strong>%{count}</strong> odottavaa käyttäjää"
resolved_reports: ratkaistut raportit resolved_reports: ratkaistut raportit
software: Ohjelmisto software: Ohjelmisto
sources: Rekisteröitymisen lähteet sources: Rekisteröitymislähteet
space: Tilankäyttö space: Tilankäyttö
title: Hallintapaneeli title: Hallintapaneeli
top_languages: Aktiivisimmat kielet top_languages: Aktiivisimmat kielet
@ -440,7 +440,7 @@ fi:
title: Estä uusi sähköpostiverkkotunnus title: Estä uusi sähköpostiverkkotunnus
no_email_domain_block_selected: Sähköpostin verkkotunnuksia ei muutettu, koska yhtään ei ollut valittuna no_email_domain_block_selected: Sähköpostin verkkotunnuksia ei muutettu, koska yhtään ei ollut valittuna
not_permitted: Ei sallittu not_permitted: Ei sallittu
resolved_dns_records_hint_html: Verkkotunnuksen nimi määräytyy seuraaviin MX-verkkotunnuksiin, jotka ovat viime kädessä vastuussa sähköpostin vastaanottamisesta. MX-verkkotunnuksen estäminen estää kirjautumisen mistä tahansa sähköpostiosoitteesta, joka käyttää samaa MX-verkkotunnusta, vaikka näkyvä verkkotunnuksen nimi olisikin erilainen. <strong>Varo estämästä suuria sähköpostin palveluntarjoajia.</strong> resolved_dns_records_hint_html: Verkkotunnuksen nimi määräytyy seuraaviin MX-verkkotunnuksiin, jotka ovat viime kädessä vastuussa sähköpostin vastaanottamisesta. MX-verkkotunnuksen estäminen estää rekisteröitymisen mistä tahansa sähköpostiosoitteesta, joka käyttää samaa MX-verkkotunnusta, vaikka näkyvä verkkotunnuksen nimi olisikin erilainen. <strong>Varo estämästä suuria sähköpostin palveluntarjoajia.</strong>
resolved_through_html: Ratkaistu %{domain} kautta resolved_through_html: Ratkaistu %{domain} kautta
title: Estetyt sähköpostiverkkotunnukset title: Estetyt sähköpostiverkkotunnukset
export_domain_allows: export_domain_allows:
@ -1405,7 +1405,7 @@ fi:
migrations: migrations:
acct: Muuttanut tunnukselle acct: Muuttanut tunnukselle
cancel: Peruuta uudelleenohjaus cancel: Peruuta uudelleenohjaus
cancel_explanation: Uudelleenohjauksen peruuttaminen aktivoi uudelleen nykyisen tilisi, mutta ei palauta seuraajia, jotka on siirretty kyseiselle tilille. cancel_explanation: Uudelleenohjauksen peruuttaminen aktivoi nykyisen tilisi uudelleen mutta ei palauta seuraajia, jotka on siirretty kyseiselle tilille.
cancelled_msg: Uudelleenohjaus peruttu onnistuneesti. cancelled_msg: Uudelleenohjaus peruttu onnistuneesti.
errors: errors:
already_moved: on sama tili, jonka olet jo siirtänyt already_moved: on sama tili, jonka olet jo siirtänyt

View file

@ -570,7 +570,7 @@ nn:
enabled: Skrudd på enabled: Skrudd på
inbox_url: Overførings-URL inbox_url: Overførings-URL
pending: Avventer overgangens godkjenning pending: Avventer overgangens godkjenning
save_and_enable: Lagr og slå på save_and_enable: Lagre og slå på
setup: Sett opp en overgangsforbindelse setup: Sett opp en overgangsforbindelse
signatures_not_enabled: Overgangar fungerer ikkje så lenge sikker- eller kvitlistingsmodus er aktivert signatures_not_enabled: Overgangar fungerer ikkje så lenge sikker- eller kvitlistingsmodus er aktivert
status: Status status: Status
@ -1280,7 +1280,7 @@ nn:
deselect: Vel ingen deselect: Vel ingen
none: Ingen none: Ingen
order_by: Sorter etter order_by: Sorter etter
save_changes: Lagr endringar save_changes: Lagre endringar
select_all_matching_items: select_all_matching_items:
one: Vel %{count} element som passar til søket ditt. one: Vel %{count} element som passar til søket ditt.
other: Vel %{count} element som passar til søket ditt. other: Vel %{count} element som passar til søket ditt.

View file

@ -59,14 +59,14 @@ fi:
setting_display_media_default: Piilota arkaluonteiseksi merkitty media setting_display_media_default: Piilota arkaluonteiseksi merkitty media
setting_display_media_hide_all: Piilota media aina setting_display_media_hide_all: Piilota media aina
setting_display_media_show_all: Näytä media aina setting_display_media_show_all: Näytä media aina
setting_use_blurhash: Liukuvärit perustuvat piilotettujen kuvien väreihin, mutta sumentavat yksityiskohdat setting_use_blurhash: Liukuvärit perustuvat piilotettujen kuvien väreihin mutta sumentavat yksityiskohdat
setting_use_pending_items: Piilota aikajanan päivitykset napsautuksen taakse syötteen automaattisen vierityksen sijaan setting_use_pending_items: Piilota aikajanan päivitykset napsautuksen taakse syötteen automaattisen vierityksen sijaan
username: Voit käyttää kirjaimia, numeroita ja alaviivoja username: Voit käyttää kirjaimia, numeroita ja alaviivoja
whole_word: Kun avainsana tai -fraasi on kokonaan aakkosnumeerinen, se on voimassa vain, jos se vastaa koko sanaa whole_word: Kun avainsana tai -fraasi on kokonaan aakkosnumeerinen, se on voimassa vain, jos se vastaa koko sanaa
domain_allow: domain_allow:
domain: Tämä verkkotunnus voi noutaa tietoja tältä palvelimelta ja sieltä saapuvat tiedot käsitellään ja tallennetaan domain: Tämä verkkotunnus voi noutaa tietoja tältä palvelimelta ja sieltä saapuvat tiedot käsitellään ja tallennetaan
email_domain_block: email_domain_block:
domain: Tämä voi olla se verkkotunnus, joka näkyy sähköpostiosoitteessa tai MX tietueessa jota se käyttää. Ne tarkistetaan rekisteröitymisen yhteydessä. domain: Tämä voi olla verkkotunnus, joka näkyy sähköpostiosoitteessa tai sen käyttämässä MX-tietueessa. Ne tarkistetaan rekisteröitymisen yhteydessä.
with_dns_records: Annetun verkkotunnuksen DNS-tietueet yritetään ratkaista ja tulokset myös estetään with_dns_records: Annetun verkkotunnuksen DNS-tietueet yritetään ratkaista ja tulokset myös estetään
featured_tag: featured_tag:
name: 'Tässä muutamia hiljattain käyttämiäsi aihetunnisteita:' name: 'Tässä muutamia hiljattain käyttämiäsi aihetunnisteita:'
@ -112,7 +112,7 @@ fi:
ip: Kirjoita IPv4- tai IPv6-osoite. Voit estää kokonaisia alueita käyttämällä CIDR-syntaksia. Varo, että et lukitse itseäsi ulos! ip: Kirjoita IPv4- tai IPv6-osoite. Voit estää kokonaisia alueita käyttämällä CIDR-syntaksia. Varo, että et lukitse itseäsi ulos!
severities: severities:
no_access: Estä pääsy kaikkiin resursseihin no_access: Estä pääsy kaikkiin resursseihin
sign_up_block: Uudet kirjautumiset eivät ole mahdollisia sign_up_block: Uudet rekisteröitymiset eivät ole mahdollisia
sign_up_requires_approval: Uudet rekisteröitymiset edellyttävät hyväksyntääsi sign_up_requires_approval: Uudet rekisteröitymiset edellyttävät hyväksyntääsi
severity: Valitse, mitä tapahtuu tämän IP-osoitteen pyynnöille severity: Valitse, mitä tapahtuu tämän IP-osoitteen pyynnöille
rule: rule:

View file

@ -30,7 +30,7 @@ zh-TW:
suspend: 禁止所有對該帳號任何互動,並且刪除其內容。三十天內可以撤銷此動作。關閉所有對此帳號之檢舉報告。 suspend: 禁止所有對該帳號任何互動,並且刪除其內容。三十天內可以撤銷此動作。關閉所有對此帳號之檢舉報告。
warning_preset_id: 選用。您仍可在預設的結尾新增自訂文字 warning_preset_id: 選用。您仍可在預設的結尾新增自訂文字
announcement: announcement:
all_day: 核取後,只會顯示出時間範圍中的日期部分 all_day: 當選取時,僅顯示出時間範圍中的日期部分
ends_at: 可選的,公告會於該時間點自動取消發布 ends_at: 可選的,公告會於該時間點自動取消發布
scheduled_at: 空白則立即發布公告 scheduled_at: 空白則立即發布公告
starts_at: 可選的,讓公告在特定時間範圍內顯示 starts_at: 可選的,讓公告在特定時間範圍內顯示
@ -60,7 +60,7 @@ zh-TW:
setting_display_media_hide_all: 總是隱藏所有媒體 setting_display_media_hide_all: 總是隱藏所有媒體
setting_display_media_show_all: 總是顯示標為敏感內容的媒體 setting_display_media_show_all: 總是顯示標為敏感內容的媒體
setting_use_blurhash: 彩色漸層圖樣是基於隱藏媒體內容顏色產生,所有細節將變得模糊 setting_use_blurhash: 彩色漸層圖樣是基於隱藏媒體內容顏色產生,所有細節將變得模糊
setting_use_pending_items: 關閉自動捲動更新,時間軸只會於點擊後更新 setting_use_pending_items: 關閉自動捲動更新,時間軸於點擊後更新
username: 您可以使用字幕、數字與底線 username: 您可以使用字幕、數字與底線
whole_word: 如果關鍵字或詞組僅有字母與數字,則其將只在符合整個單字的時候才會套用 whole_word: 如果關鍵字或詞組僅有字母與數字,則其將只在符合整個單字的時候才會套用
domain_allow: domain_allow:

View file

@ -578,7 +578,7 @@ zh-TW:
mark_as_sensitive_description_html: 被檢舉的嘟文中的媒體將會被標記為敏感內容,並將會記錄一次警告,以協助您升級同一帳號未來的違規行為。 mark_as_sensitive_description_html: 被檢舉的嘟文中的媒體將會被標記為敏感內容,並將會記錄一次警告,以協助您升級同一帳號未來的違規行為。
other_description_html: 檢視更多控制帳號行為以及自訂檢舉帳號通知之選項。 other_description_html: 檢視更多控制帳號行為以及自訂檢舉帳號通知之選項。
resolve_description_html: 被檢舉的帳號將不被採取任何行動,不會加以刪除線標記,並且此份報告將被關閉。 resolve_description_html: 被檢舉的帳號將不被採取任何行動,不會加以刪除線標記,並且此份報告將被關閉。
silence_description_html: 此帳號僅對已跟隨帳號之使用者或手動查詢可見,將大幅度限制觸及範圍。此設定可隨時被還原。關閉所有對此帳號之檢舉報告。 silence_description_html: 此帳號僅對已跟隨帳號之使用者或手動查詢可見,將大幅度限制觸及範圍。此設定可隨時被還原。關閉所有對此帳號之檢舉報告。
suspend_description_html: 此帳號及其所有內容將不可被存取並且最終被移除,並且無法與之進行互動。三十天內可以撤銷此動作。關閉所有對此帳號之檢舉報告。 suspend_description_html: 此帳號及其所有內容將不可被存取並且最終被移除,並且無法與之進行互動。三十天內可以撤銷此動作。關閉所有對此帳號之檢舉報告。
actions_description_html: 決定應對此報告採取何種行動。若您對檢舉之帳號採取懲罰措施,則將對他們發送 e-mail 通知,如非選擇了 <strong>垃圾郵件</strong> 類別。 actions_description_html: 決定應對此報告採取何種行動。若您對檢舉之帳號採取懲罰措施,則將對他們發送 e-mail 通知,如非選擇了 <strong>垃圾郵件</strong> 類別。
actions_description_remote_html: 決定將對此檢舉報告採取何種動作。這將僅作用於<strong>您的伺服器</strong>與此遠端帳號及其內容之通訊行為。 actions_description_remote_html: 決定將對此檢舉報告採取何種動作。這將僅作用於<strong>您的伺服器</strong>與此遠端帳號及其內容之通訊行為。

4
db/migrate/.rubocop.yml Normal file
View file

@ -0,0 +1,4 @@
inherit_from: ../../.rubocop.yml
Naming/VariableNumber:
CheckSymbols: false

View file

@ -19,7 +19,6 @@ class AddSilencedAtSuspendedAtToAccounts < ActiveRecord::Migration[5.2]
# Record suspend date of blocks and silences for users whose limitations match # Record suspend date of blocks and silences for users whose limitations match
# a domain block # a domain block
DomainBlock.where(severity: [:silence, :suspend]).find_each do |block| DomainBlock.where(severity: [:silence, :suspend]).find_each do |block|
scope = block.accounts
if block.suspend? if block.suspend?
block.accounts.where(suspended: true).in_batches.update_all(suspended_at: block.created_at) block.accounts.where(suspended: true).in_batches.update_all(suspended_at: block.created_at)
else else

View file

@ -18,7 +18,6 @@ class RemoveSuspendedSilencedAccountFields < ActiveRecord::Migration[5.2]
# Record suspend date of blocks and silences for users whose limitations match # Record suspend date of blocks and silences for users whose limitations match
# a domain block # a domain block
DomainBlock.where(severity: [:silence, :suspend]).find_each do |block| DomainBlock.where(severity: [:silence, :suspend]).find_each do |block|
scope = block.accounts
if block.suspend? if block.suspend?
block.accounts.where(suspended: true).in_batches.update_all(suspended_at: block.created_at) block.accounts.where(suspended: true).in_batches.update_all(suspended_at: block.created_at)
else else

View file

@ -79,6 +79,7 @@
"fuzzysort": "^2.0.4", "fuzzysort": "^2.0.4",
"glob": "^10.2.6", "glob": "^10.2.6",
"history": "^4.10.1", "history": "^4.10.1",
"hoist-non-react-statics": "^3.3.2",
"http-link-header": "^1.1.1", "http-link-header": "^1.1.1",
"immutable": "^4.3.0", "immutable": "^4.3.0",
"imports-loader": "^1.2.0", "imports-loader": "^1.2.0",
@ -112,8 +113,8 @@
"react-overlays": "^5.2.1", "react-overlays": "^5.2.1",
"react-redux": "^8.0.4", "react-redux": "^8.0.4",
"react-redux-loading-bar": "^5.0.4", "react-redux-loading-bar": "^5.0.4",
"react-router": "^4.3.1", "react-router": "^5.3.4",
"react-router-dom": "^4.1.1", "react-router-dom": "^5.3.4",
"react-router-scroll-4": "^1.0.0-beta.1", "react-router-scroll-4": "^1.0.0-beta.1",
"react-select": "^5.7.3", "react-select": "^5.7.3",
"react-sparklines": "^1.7.0", "react-sparklines": "^1.7.0",
@ -159,6 +160,7 @@
"@types/emoji-mart": "^3.0.9", "@types/emoji-mart": "^3.0.9",
"@types/escape-html": "^1.0.2", "@types/escape-html": "^1.0.2",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/http-link-header": "^1.0.3", "@types/http-link-header": "^1.0.3",
"@types/intl": "^1.2.0", "@types/intl": "^1.2.0",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
@ -175,6 +177,7 @@
"@types/react-immutable-proptypes": "^2.1.0", "@types/react-immutable-proptypes": "^2.1.0",
"@types/react-motion": "^0.0.35", "@types/react-motion": "^0.0.35",
"@types/react-overlays": "^3.1.0", "@types/react-overlays": "^3.1.0",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/react-select": "^5.0.1", "@types/react-select": "^5.0.1",
"@types/react-sparklines": "^1.7.2", "@types/react-sparklines": "^1.7.2",

View file

@ -18,10 +18,14 @@ RSpec.describe Admin::Disputes::AppealsController do
describe 'GET #index' do describe 'GET #index' do
let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
it 'lists appeals' do before { appeal }
it 'returns a page that lists details of appeals' do
get :index get :index
expect(response).to have_http_status(200) expect(response).to have_http_status(:success)
expect(response.body).to include("<span class=\"username\">#{strike.account.username}</span>")
expect(response.body).to include("<span class=\"target\">#{appeal.account.username}</span>")
end end
end end

View file

@ -62,7 +62,7 @@ describe AccountControllerConcern do
end end
it 'sets link headers' do it 'sets link headers' do
account = Fabricate(:account, username: 'username') Fabricate(:account, username: 'username')
get 'success', params: { account_username: 'username' } get 'success', params: { account_username: 'username' }
expect(response.headers['Link'].to_s).to eq '<http://test.host/.well-known/webfinger?resource=acct%3Ausername%40cb6e6126.ngrok.io>; rel="lrdd"; type="application/jrd+json", <https://cb6e6126.ngrok.io/users/username>; rel="alternate"; type="application/activity+json"' expect(response.headers['Link'].to_s).to eq '<http://test.host/.well-known/webfinger?resource=acct%3Ausername%40cb6e6126.ngrok.io>; rel="lrdd"; type="application/jrd+json", <https://cb6e6126.ngrok.io/users/username>; rel="alternate"; type="application/activity+json"'
end end

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::DisputesHelper do
describe 'strike_action_label' do
it 'returns html describing the appeal' do
adam = Account.new(username: 'Adam')
becky = Account.new(username: 'Becky')
strike = AccountWarning.new(account: adam, action: :suspend)
appeal = Appeal.new(strike: strike, account: becky)
expected = <<~OUTPUT.strip
<span class="username">Adam</span> suspended <span class="target">Becky</span>'s account
OUTPUT
result = helper.strike_action_label(appeal)
expect(result).to eq(expected)
end
end
end

View file

@ -179,14 +179,14 @@ describe JsonLdHelper do
it 'deems a safe compacting as such' do it 'deems a safe compacting as such' do
json['object'].delete('convo') json['object'].delete('convo')
compacted = compact(json) compacted = compact(json)
deemed_compatible = patch_for_forwarding!(json, compacted) patch_for_forwarding!(json, compacted)
expect(compacted['to']).to eq ['https://www.w3.org/ns/activitystreams#Public'] expect(compacted['to']).to eq ['https://www.w3.org/ns/activitystreams#Public']
expect(safe_for_forwarding?(json, compacted)).to be true expect(safe_for_forwarding?(json, compacted)).to be true
end end
it 'deems an unsafe compacting as such' do it 'deems an unsafe compacting as such' do
compacted = compact(json) compacted = compact(json)
deemed_compatible = patch_for_forwarding!(json, compacted) patch_for_forwarding!(json, compacted)
expect(compacted['to']).to eq ['https://www.w3.org/ns/activitystreams#Public'] expect(compacted['to']).to eq ['https://www.w3.org/ns/activitystreams#Public']
expect(safe_for_forwarding?(json, compacted)).to be false expect(safe_for_forwarding?(json, compacted)).to be false
end end

View file

@ -34,6 +34,40 @@ RSpec.describe ActivityPub::LinkedDataSignature do
end end
end end
context 'when local account record is missing a public key' do
let(:raw_signature) do
{
'creator' => 'http://example.com/alice',
'created' => '2017-09-23T20:21:34Z',
}
end
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
let(:service_stub) { instance_double(ActivityPub::FetchRemoteKeyService) }
before do
# Ensure signature is computed with the old key
signature
# Unset key
old_key = sender.public_key
sender.update!(private_key: '', public_key: '')
allow(ActivityPub::FetchRemoteKeyService).to receive(:new).and_return(service_stub)
allow(service_stub).to receive(:call).with('http://example.com/alice', id: false) do
sender.update!(public_key: old_key)
sender
end
end
it 'fetches key and returns creator' do
expect(subject.verify_actor!).to eq sender
expect(service_stub).to have_received(:call).with('http://example.com/alice', id: false).once
end
end
context 'when signature is missing' do context 'when signature is missing' do
let(:signature) { nil } let(:signature) { nil }

View file

@ -1356,4 +1356,254 @@ describe Mastodon::CLI::Accounts do
end end
end end
end end
describe '#prune' do
let!(:local_account) { Fabricate(:account) }
let!(:bot_account) { Fabricate(:account, bot: true, domain: 'example.com') }
let!(:group_account) { Fabricate(:account, actor_type: 'Group', domain: 'example.com') }
let!(:mentioned_account) { Fabricate(:account, domain: 'example.com') }
let!(:prunable_accounts) do
Fabricate.times(3, :account, domain: 'example.com', bot: false, suspended_at: nil, silenced_at: nil)
end
before do
Fabricate(:mention, account: mentioned_account, status: Fabricate(:status, account: Fabricate(:account)))
stub_parallelize_with_progress!
end
it 'prunes all remote accounts with no interactions with local users' do
cli.prune
prunable_account_ids = prunable_accounts.pluck(:id)
expect(Account.where(id: prunable_account_ids).count).to eq(0)
end
it 'displays a successful message' do
expect { cli.prune }.to output(
a_string_including("OK, pruned #{prunable_accounts.size} accounts")
).to_stdout
end
it 'does not prune local accounts' do
cli.prune
expect(Account.exists?(id: local_account.id)).to be(true)
end
it 'does not prune bot accounts' do
cli.prune
expect(Account.exists?(id: bot_account.id)).to be(true)
end
it 'does not prune group accounts' do
cli.prune
expect(Account.exists?(id: group_account.id)).to be(true)
end
it 'does not prune accounts that have been mentioned' do
cli.prune
expect(Account.exists?(id: mentioned_account.id)).to be true
end
context 'with --dry-run option' do
before do
cli.options = { dry_run: true }
end
it 'does not prune any account' do
cli.prune
prunable_account_ids = prunable_accounts.pluck(:id)
expect(Account.where(id: prunable_account_ids).count).to eq(prunable_accounts.size)
end
it 'displays a successful message with (DRY RUN)' do
expect { cli.prune }.to output(
a_string_including("OK, pruned #{prunable_accounts.size} accounts (DRY RUN)")
).to_stdout
end
end
end
describe '#migrate' do
let!(:source_account) { Fabricate(:account) }
let!(:target_account) { Fabricate(:account, domain: 'example.com') }
let(:arguments) { [source_account.username] }
let(:resolve_account_service) { instance_double(ResolveAccountService, call: nil) }
let(:move_service) { instance_double(MoveService, call: nil) }
before do
allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service)
allow(MoveService).to receive(:new).and_return(move_service)
end
shared_examples 'a successful migration' do
it 'calls the MoveService for the last migration' do
cli.invoke(:migrate, arguments, options)
last_migration = source_account.migrations.last
expect(move_service).to have_received(:call).with(last_migration).once
end
it 'displays a successful message' do
expect { cli.invoke(:migrate, arguments, options) }.to output(
a_string_including("OK, migrated #{source_account.acct} to #{target_account.acct}")
).to_stdout
end
end
context 'when both --replay and --target options are given' do
let(:options) { { replay: true, target: "#{target_account.username}@example.com" } }
it 'exits with an error message indicating that using both options is not possible' do
expect { cli.invoke(:migrate, arguments, options) }.to output(
a_string_including('Use --replay or --target, not both')
).to_stdout
.and raise_error(SystemExit)
end
end
context 'when no option is given' do
it 'exits with an error message indicating that at least one option must be used' do
expect { cli.invoke(:migrate, arguments, {}) }.to output(
a_string_including('Use either --replay or --target')
).to_stdout
.and raise_error(SystemExit)
end
end
context 'when the given username is not found' do
let(:arguments) { ['non_existent_username'] }
it 'exits with an error message indicating that there is no such account' do
expect { cli.invoke(:migrate, arguments, replay: true) }.to output(
a_string_including("No such account: #{arguments.first}")
).to_stdout
.and raise_error(SystemExit)
end
end
context 'with --replay option' do
let(:options) { { replay: true } }
context 'when the specified account has no previous migrations' do
it 'exits with an error message indicating that the given account has no previous migrations' do
expect { cli.invoke(:migrate, arguments, options) }.to output(
a_string_including('The specified account has not performed any migration')
).to_stdout
.and raise_error(SystemExit)
end
end
context 'when the specified account has a previous migration' do
before do
allow(resolve_account_service).to receive(:call).with(source_account.acct, any_args).and_return(source_account)
allow(resolve_account_service).to receive(:call).with(target_account.acct, any_args).and_return(target_account)
target_account.aliases.create!(acct: source_account.acct)
source_account.migrations.create!(acct: target_account.acct)
source_account.update!(moved_to_account: target_account)
end
it_behaves_like 'a successful migration'
context 'when the specified account is redirecting to a different target account' do
before do
source_account.update!(moved_to_account: nil)
end
it 'exits with an error message' do
expect { cli.invoke(:migrate, arguments, options) }.to output(
a_string_including('The specified account is not redirecting to its last migration target. Use --force if you want to replay the migration anyway')
).to_stdout
.and raise_error(SystemExit)
end
end
context 'with --force option' do
let(:options) { { replay: true, force: true } }
it_behaves_like 'a successful migration'
end
end
end
context 'with --target option' do
let(:options) { { target: target_account.acct } }
before do
allow(resolve_account_service).to receive(:call).with(source_account.acct, any_args).and_return(source_account)
allow(resolve_account_service).to receive(:call).with(target_account.acct, any_args).and_return(target_account)
end
context 'when the specified target account is not found' do
before do
allow(resolve_account_service).to receive(:call).with(target_account.acct).and_return(nil)
end
it 'exits with an error message indicating that there is no such account' do
expect { cli.invoke(:migrate, arguments, options) }.to output(
a_string_including("The specified target account could not be found: #{options[:target]}")
).to_stdout
.and raise_error(SystemExit)
end
end
context 'when the specified target account exists' do
before do
target_account.aliases.create!(acct: source_account.acct)
end
it 'creates a migration for the specified account with the target account' do
cli.invoke(:migrate, arguments, options)
last_migration = source_account.migrations.last
expect(last_migration.acct).to eq(target_account.acct)
end
it_behaves_like 'a successful migration'
end
context 'when the migration record is invalid' do
it 'exits with an error indicating that the validation failed' do
expect { cli.invoke(:migrate, arguments, options) }.to output(
a_string_including('Error: Validation failed')
).to_stdout
.and raise_error(SystemExit)
end
end
context 'when the specified account is redirecting to a different target account' do
before do
allow(Account).to receive(:find_local).with(source_account.username).and_return(source_account)
allow(source_account).to receive(:moved_to_account_id).and_return(-1)
end
it 'exits with an error message' do
expect { cli.invoke(:migrate, arguments, options) }.to output(
a_string_including('The specified account is redirecting to a different target account. Use --force if you want to change the migration target')
).to_stdout
.and raise_error(SystemExit)
end
end
context 'with --target and --force options' do
let(:options) { { target: target_account.acct, force: true } }
before do
target_account.aliases.create!(acct: source_account.acct)
allow(Account).to receive(:find_local).with(source_account.username).and_return(source_account)
allow(source_account).to receive(:moved_to_account_id).and_return(-1)
end
it_behaves_like 'a successful migration'
end
end
end
end end

View file

@ -356,7 +356,7 @@ RSpec.describe Account do
end end
it 'does not return suspended users' do it 'does not return suspended users' do
match = Fabricate( Fabricate(
:account, :account,
display_name: 'Display Name', display_name: 'Display Name',
username: 'username', username: 'username',
@ -483,7 +483,7 @@ RSpec.describe Account do
end end
it 'does not return non-followed accounts' do it 'does not return non-followed accounts' do
match = Fabricate( Fabricate(
:account, :account,
display_name: 'A & l & i & c & e', display_name: 'A & l & i & c & e',
username: 'username', username: 'username',
@ -495,7 +495,7 @@ RSpec.describe Account do
end end
it 'does not return suspended users' do it 'does not return suspended users' do
match = Fabricate( Fabricate(
:account, :account,
display_name: 'Display Name', display_name: 'Display Name',
username: 'username', username: 'username',
@ -535,7 +535,7 @@ RSpec.describe Account do
end end
it 'does not return suspended users' do it 'does not return suspended users' do
match = Fabricate( Fabricate(
:account, :account,
display_name: 'Display Name', display_name: 'Display Name',
username: 'username', username: 'username',
@ -719,10 +719,10 @@ RSpec.describe Account do
context 'when is local' do context 'when is local' do
it 'is invalid if the username is not unique in case-insensitive comparison among local accounts' do it 'is invalid if the username is not unique in case-insensitive comparison among local accounts' do
account_1 = Fabricate(:account, username: 'the_doctor') _account = Fabricate(:account, username: 'the_doctor')
account_2 = Fabricate.build(:account, username: 'the_Doctor') non_unique_account = Fabricate.build(:account, username: 'the_Doctor')
account_2.valid? non_unique_account.valid?
expect(account_2).to model_have_error_on_field(:username) expect(non_unique_account).to model_have_error_on_field(:username)
end end
it 'is invalid if the username is reserved' do it 'is invalid if the username is reserved' do
@ -743,9 +743,9 @@ RSpec.describe Account do
end end
it 'is valid if we are creating a possibly-conflicting instance actor account' do it 'is valid if we are creating a possibly-conflicting instance actor account' do
account_1 = Fabricate(:account, username: 'examplecom') _account = Fabricate(:account, username: 'examplecom')
account_2 = Fabricate.build(:account, id: -99, actor_type: 'Application', locked: true, username: 'example.com') instance_account = Fabricate.build(:account, id: -99, actor_type: 'Application', locked: true, username: 'example.com')
expect(account_2.valid?).to be true expect(instance_account.valid?).to be true
end end
it 'is invalid if the username doesn\'t only contains letters, numbers and underscores' do it 'is invalid if the username doesn\'t only contains letters, numbers and underscores' do
@ -877,17 +877,17 @@ RSpec.describe Account do
describe 'remote' do describe 'remote' do
it 'returns an array of accounts who have a domain' do it 'returns an array of accounts who have a domain' do
account_1 = Fabricate(:account, domain: nil) _account = Fabricate(:account, domain: nil)
account_2 = Fabricate(:account, domain: 'example.com') account_with_domain = Fabricate(:account, domain: 'example.com')
expect(described_class.remote).to contain_exactly(account_2) expect(described_class.remote).to contain_exactly(account_with_domain)
end end
end end
describe 'local' do describe 'local' do
it 'returns an array of accounts who do not have a domain' do it 'returns an array of accounts who do not have a domain' do
account_1 = Fabricate(:account, domain: nil) local_account = Fabricate(:account, domain: nil)
account_2 = Fabricate(:account, domain: 'example.com') _account_with_domain = Fabricate(:account, domain: 'example.com')
expect(described_class.where('id > 0').local).to contain_exactly(account_1) expect(described_class.where('id > 0').local).to contain_exactly(local_account)
end end
end end
@ -911,17 +911,17 @@ RSpec.describe Account do
describe 'silenced' do describe 'silenced' do
it 'returns an array of accounts who are silenced' do it 'returns an array of accounts who are silenced' do
account_1 = Fabricate(:account, silenced: true) silenced_account = Fabricate(:account, silenced: true)
account_2 = Fabricate(:account, silenced: false) _account = Fabricate(:account, silenced: false)
expect(described_class.silenced).to contain_exactly(account_1) expect(described_class.silenced).to contain_exactly(silenced_account)
end end
end end
describe 'suspended' do describe 'suspended' do
it 'returns an array of accounts who are suspended' do it 'returns an array of accounts who are suspended' do
account_1 = Fabricate(:account, suspended: true) suspended_account = Fabricate(:account, suspended: true)
account_2 = Fabricate(:account, suspended: false) _account = Fabricate(:account, suspended: false)
expect(described_class.suspended).to contain_exactly(account_1) expect(described_class.suspended).to contain_exactly(suspended_account)
end end
end end

View file

@ -11,10 +11,10 @@ RSpec.describe DomainBlock do
end end
it 'is invalid if the same normalized domain already exists' do it 'is invalid if the same normalized domain already exists' do
domain_block_1 = Fabricate(:domain_block, domain: 'にゃん') _domain_block = Fabricate(:domain_block, domain: 'にゃん')
domain_block_2 = Fabricate.build(:domain_block, domain: 'xn--r9j5b5b') domain_block_with_normalized_value = Fabricate.build(:domain_block, domain: 'xn--r9j5b5b')
domain_block_2.valid? domain_block_with_normalized_value.valid?
expect(domain_block_2).to model_have_error_on_field(:domain) expect(domain_block_with_normalized_value).to model_have_error_on_field(:domain)
end end
end end

View file

@ -341,7 +341,7 @@ RSpec.describe Status do
describe '#replies_count' do describe '#replies_count' do
it 'is the number of replies' do it 'is the number of replies' do
reply = Fabricate(:status, account: bob, thread: subject) Fabricate(:status, account: bob, thread: subject)
expect(subject.replies_count).to eq 1 expect(subject.replies_count).to eq 1
end end

View file

@ -55,17 +55,17 @@ RSpec.describe User do
describe 'scopes' do describe 'scopes' do
describe 'recent' do describe 'recent' do
it 'returns an array of recent users ordered by id' do it 'returns an array of recent users ordered by id' do
user_1 = Fabricate(:user) first_user = Fabricate(:user)
user_2 = Fabricate(:user) second_user = Fabricate(:user)
expect(described_class.recent).to eq [user_2, user_1] expect(described_class.recent).to eq [second_user, first_user]
end end
end end
describe 'confirmed' do describe 'confirmed' do
it 'returns an array of users who are confirmed' do it 'returns an array of users who are confirmed' do
user_1 = Fabricate(:user, confirmed_at: nil) Fabricate(:user, confirmed_at: nil)
user_2 = Fabricate(:user, confirmed_at: Time.zone.now) confirmed_user = Fabricate(:user, confirmed_at: Time.zone.now)
expect(described_class.confirmed).to contain_exactly(user_2) expect(described_class.confirmed).to contain_exactly(confirmed_user)
end end
end end

View file

@ -37,7 +37,7 @@ RSpec.describe WebauthnCredential do
end end
it 'is invalid if already exist a webauthn credential with the same external id' do it 'is invalid if already exist a webauthn credential with the same external id' do
existing_webauthn_credential = Fabricate(:webauthn_credential, external_id: '_Typ0ygudDnk9YUVWLQayw') Fabricate(:webauthn_credential, external_id: '_Typ0ygudDnk9YUVWLQayw')
new_webauthn_credential = Fabricate.build(:webauthn_credential, external_id: '_Typ0ygudDnk9YUVWLQayw') new_webauthn_credential = Fabricate.build(:webauthn_credential, external_id: '_Typ0ygudDnk9YUVWLQayw')
new_webauthn_credential.valid? new_webauthn_credential.valid?
@ -47,7 +47,7 @@ RSpec.describe WebauthnCredential do
it 'is invalid if user already registered a webauthn credential with the same nickname' do it 'is invalid if user already registered a webauthn credential with the same nickname' do
user = Fabricate(:user) user = Fabricate(:user)
existing_webauthn_credential = Fabricate(:webauthn_credential, user_id: user.id, nickname: 'USB Key') Fabricate(:webauthn_credential, user_id: user.id, nickname: 'USB Key')
new_webauthn_credential = Fabricate.build(:webauthn_credential, user_id: user.id, nickname: 'USB Key') new_webauthn_credential = Fabricate.build(:webauthn_credential, user_id: user.id, nickname: 'USB Key')
new_webauthn_credential.valid? new_webauthn_credential.valid?

View file

@ -9,7 +9,8 @@ describe 'Credentials' do
end end
context 'with an oauth token' do context 'with an oauth token' do
let(:token) { Fabricate(:accessible_access_token, scopes: 'read', application: Fabricate(:application)) } let(:application) { Fabricate(:application, scopes: 'read') }
let(:token) { Fabricate(:accessible_access_token, application: application) }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
it 'returns the app information correctly', :aggregate_failures do it 'returns the app information correctly', :aggregate_failures do
@ -21,7 +22,35 @@ describe 'Credentials' do
a_hash_including( a_hash_including(
name: token.application.name, name: token.application.name,
website: token.application.website, website: token.application.website,
vapid_key: Rails.configuration.x.vapid_public_key vapid_key: Rails.configuration.x.vapid_public_key,
scopes: token.application.scopes.map(&:to_s),
client_id: token.application.uid
)
)
end
end
context 'with a non-read scoped oauth token' do
let(:application) { Fabricate(:application, scopes: 'admin:write') }
let(:token) { Fabricate(:accessible_access_token, application: application) }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
it 'returns http success' do
subject
expect(response).to have_http_status(200)
end
it 'returns the app information correctly' do
subject
expect(body_as_json).to match(
a_hash_including(
name: token.application.name,
website: token.application.website,
vapid_key: Rails.configuration.x.vapid_public_key,
scopes: token.application.scopes.map(&:to_s),
client_id: token.application.uid
) )
) )
end end
@ -36,5 +65,49 @@ describe 'Credentials' do
expect(response).to have_http_status(401) expect(response).to have_http_status(401)
end end
end end
context 'with a revoked oauth token' do
let(:application) { Fabricate(:application, scopes: 'read') }
let(:token) { Fabricate(:accessible_access_token, application: application, revoked_at: DateTime.now.utc) }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
it 'returns http authorization error' do
subject
expect(response).to have_http_status(401)
end
it 'returns the error in the json response' do
subject
expect(body_as_json).to match(
a_hash_including(
error: 'The access token was revoked'
)
)
end
end
context 'with an invalid oauth token' do
let(:application) { Fabricate(:application, scopes: 'read') }
let(:token) { Fabricate(:accessible_access_token, application: application) }
let(:headers) { { 'Authorization' => "Bearer #{token.token}-invalid" } }
it 'returns http authorization error' do
subject
expect(response).to have_http_status(401)
end
it 'returns the error in the json response' do
subject
expect(body_as_json).to match(
a_hash_including(
error: 'The access token is invalid'
)
)
end
end
end end
end end

View file

@ -56,7 +56,7 @@ describe AccountSearchService, type: :service do
service = instance_double(ResolveAccountService, call: nil) service = instance_double(ResolveAccountService, call: nil)
allow(ResolveAccountService).to receive(:new).and_return(service) allow(ResolveAccountService).to receive(:new).and_return(service)
results = subject.call('newuser@remote.com', nil, limit: 10, resolve: true) subject.call('newuser@remote.com', nil, limit: 10, resolve: true)
expect(service).to have_received(:call).with('newuser@remote.com') expect(service).to have_received(:call).with('newuser@remote.com')
end end
@ -64,14 +64,14 @@ describe AccountSearchService, type: :service do
service = instance_double(ResolveAccountService, call: nil) service = instance_double(ResolveAccountService, call: nil)
allow(ResolveAccountService).to receive(:new).and_return(service) allow(ResolveAccountService).to receive(:new).and_return(service)
results = subject.call('newuser@remote.com', nil, limit: 10, resolve: false) subject.call('newuser@remote.com', nil, limit: 10, resolve: false)
expect(service).to_not have_received(:call) expect(service).to_not have_received(:call)
end end
end end
it 'returns the fuzzy match first, and does not return suspended exacts' do it 'returns the fuzzy match first, and does not return suspended exacts' do
partial = Fabricate(:account, username: 'exactness') partial = Fabricate(:account, username: 'exactness')
exact = Fabricate(:account, username: 'exact', suspended: true) Fabricate(:account, username: 'exact', suspended: true)
results = subject.call('exact', nil, limit: 10) results = subject.call('exact', nil, limit: 10)
expect(results.size).to eq 1 expect(results.size).to eq 1
@ -79,7 +79,7 @@ describe AccountSearchService, type: :service do
end end
it 'does not return suspended remote accounts' do it 'does not return suspended remote accounts' do
remote = Fabricate(:account, username: 'a', domain: 'remote', display_name: 'e', suspended: true) Fabricate(:account, username: 'a', domain: 'remote', display_name: 'e', suspended: true)
results = subject.call('a@example.com', nil, limit: 2) results = subject.call('a@example.com', nil, limit: 2)
expect(results.size).to eq 0 expect(results.size).to eq 0

View file

@ -307,7 +307,7 @@ RSpec.describe PostStatusService, type: :service do
it 'processes duplicate mentions correctly' do it 'processes duplicate mentions correctly' do
account = Fabricate(:account) account = Fabricate(:account)
mentioned_account = Fabricate(:account, username: 'alice') Fabricate(:account, username: 'alice')
expect do expect do
subject.call(account, text: '@alice @alice @alice hey @alice') subject.call(account, text: '@alice @alice @alice hey @alice')
@ -377,7 +377,7 @@ RSpec.describe PostStatusService, type: :service do
account = Fabricate(:account) account = Fabricate(:account)
media = Fabricate(:media_attachment, account: Fabricate(:account)) media = Fabricate(:media_attachment, account: Fabricate(:account))
status = subject.call( subject.call(
account, account,
text: 'test status update', text: 'test status update',
media_ids: [media.id] media_ids: [media.id]
@ -458,7 +458,7 @@ RSpec.describe PostStatusService, type: :service do
it 'hit ng words for mention' do it 'hit ng words for mention' do
account = Fabricate(:account) account = Fabricate(:account)
mentioned = Fabricate(:account, username: 'ohagi', domain: nil) Fabricate(:account, username: 'ohagi', domain: nil)
text = 'ng word test @ohagi' text = 'ng word test @ohagi'
Form::AdminSettings.new(ng_words_for_stranger_mention: 'test', stranger_mention_from_local_ng: '1').save Form::AdminSettings.new(ng_words_for_stranger_mention: 'test', stranger_mention_from_local_ng: '1').save
@ -467,7 +467,7 @@ RSpec.describe PostStatusService, type: :service do
it 'hit ng words for mention but local posts are not checked' do it 'hit ng words for mention but local posts are not checked' do
account = Fabricate(:account) account = Fabricate(:account)
mentioned = Fabricate(:account, username: 'ohagi', domain: nil) Fabricate(:account, username: 'ohagi', domain: nil)
text = 'ng word test @ohagi' text = 'ng word test @ohagi'
Form::AdminSettings.new(ng_words_for_stranger_mention: 'test', stranger_mention_from_local_ng: '0').save Form::AdminSettings.new(ng_words_for_stranger_mention: 'test', stranger_mention_from_local_ng: '0').save

View file

@ -27,7 +27,7 @@ RSpec.describe PrecomputeFeedService, type: :service do
muted_account = Fabricate(:account) muted_account = Fabricate(:account)
Fabricate(:mute, account: account, target_account: muted_account) Fabricate(:mute, account: account, target_account: muted_account)
reblog = Fabricate(:status, account: muted_account) reblog = Fabricate(:status, account: muted_account)
status = Fabricate(:status, account: account, reblog: reblog) Fabricate(:status, account: account, reblog: reblog)
subject.call(account) subject.call(account)

View file

@ -8,7 +8,7 @@ describe ResolveURLService, type: :service do
describe '#call' do describe '#call' do
it 'returns nil when there is no resource url' do it 'returns nil when there is no resource url' do
url = 'http://example.com/missing-resource' url = 'http://example.com/missing-resource'
known_account = Fabricate(:account, uri: url, domain: 'example.com') Fabricate(:account, uri: url, domain: 'example.com')
service = instance_double(FetchResourceService) service = instance_double(FetchResourceService)
allow(FetchResourceService).to receive(:new).and_return service allow(FetchResourceService).to receive(:new).and_return service

View file

@ -44,7 +44,7 @@ RSpec.configure do |config|
config.display_try_failure_messages = true config.display_try_failure_messages = true
# Use the GitHub Annotations formatter for CI # Use the GitHub Annotations formatter for CI
if ENV['GITHUB_ACTIONS'] == 'true' if ENV['GITHUB_ACTIONS'] == 'true' && ENV['GITHUB_RSPEC'] == 'true'
require 'rspec/github' require 'rspec/github'
config.add_formatter RSpec::Github::Formatter config.add_formatter RSpec::Github::Formatter
end end

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'rails_helper'
describe 'admin/trends/links/_preview_card.html.haml' do
it 'correctly escapes user supplied url values' do
form = instance_double(ActionView::Helpers::FormHelper, check_box: nil)
trend = PreviewCardTrend.new(allowed: false)
preview_card = Fabricate.build(
:preview_card,
url: 'https://host.example/path?query=<script>',
trend: trend,
title: 'Fun'
)
render partial: 'admin/trends/links/preview_card', locals: { preview_card: preview_card, f: form }
expect(rendered).to include('<a href="https://host.example/path?query=&lt;script&gt;">Fun</a>')
end
end

View file

@ -13,7 +13,7 @@ describe 'statuses/show.html.haml', without_verify_partial_doubles: true do
it 'has valid opengraph tags' do it 'has valid opengraph tags' do
alice = Fabricate(:account, username: 'alice', display_name: 'Alice') alice = Fabricate(:account, username: 'alice', display_name: 'Alice')
status = Fabricate(:status, account: alice, text: 'Hello World') status = Fabricate(:status, account: alice, text: 'Hello World')
media = Fabricate(:media_attachment, account: alice, status: status, type: :video) Fabricate(:media_attachment, account: alice, status: status, type: :video)
assign(:status, status) assign(:status, status)
assign(:account, alice) assign(:account, alice)
@ -32,7 +32,7 @@ describe 'statuses/show.html.haml', without_verify_partial_doubles: true do
it 'has twitter player tag' do it 'has twitter player tag' do
alice = Fabricate(:account, username: 'alice', display_name: 'Alice') alice = Fabricate(:account, username: 'alice', display_name: 'Alice')
status = Fabricate(:status, account: alice, text: 'Hello World') status = Fabricate(:status, account: alice, text: 'Hello World')
media = Fabricate(:media_attachment, account: alice, status: status, type: :video) Fabricate(:media_attachment, account: alice, status: status, type: :video)
assign(:status, status) assign(:status, status)
assign(:account, alice) assign(:account, alice)

View file

@ -1096,12 +1096,12 @@
dependencies: dependencies:
regenerator-runtime "^0.12.0" regenerator-runtime "^0.12.0"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.22.3", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.8", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.22.15" version "7.21.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.15.tgz#38f46494ccf6cf020bd4eed7124b425e83e523b8" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200"
integrity sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA== integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==
dependencies: dependencies:
regenerator-runtime "^0.14.0" regenerator-runtime "^0.13.11"
"@babel/runtime@^7.12.1", "@babel/runtime@^7.22.5", "@babel/runtime@^7.9.2": "@babel/runtime@^7.12.1", "@babel/runtime@^7.22.5", "@babel/runtime@^7.9.2":
version "7.23.1" version "7.23.1"
@ -1110,6 +1110,20 @@
dependencies: dependencies:
regenerator-runtime "^0.14.0" regenerator-runtime "^0.14.0"
"@babel/runtime@^7.18.3":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec"
integrity sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==
dependencies:
regenerator-runtime "^0.13.11"
"@babel/runtime@^7.22.3":
version "7.22.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.3.tgz#0a7fce51d43adbf0f7b517a71f4c3aaca92ebcbb"
integrity sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==
dependencies:
regenerator-runtime "^0.13.11"
"@babel/template@^7.22.15", "@babel/template@^7.22.5": "@babel/template@^7.22.15", "@babel/template@^7.22.5":
version "7.22.15" version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
@ -2438,7 +2452,7 @@
"@types/react" "*" "@types/react" "*"
"@types/react-router" "*" "@types/react-router" "*"
"@types/react-router@*": "@types/react-router@*", "@types/react-router@^5.1.20":
version "5.1.20" version "5.1.20"
resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c" resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c"
integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q== integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==
@ -6497,7 +6511,7 @@ hash.js@^1.0.0, hash.js@^1.0.3:
inherits "^2.0.3" inherits "^2.0.3"
minimalistic-assert "^1.0.1" minimalistic-assert "^1.0.1"
history@^4.10.1, history@^4.7.2: history@^4.10.1, history@^4.9.0:
version "4.10.1" version "4.10.1"
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
@ -6518,12 +6532,7 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0" minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1" minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^2.5.0: hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
version "2.5.5"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
version "3.3.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@ -9886,7 +9895,7 @@ prompts@^2.0.1:
kleur "^3.0.3" kleur "^3.0.3"
sisteransi "^1.0.5" sisteransi "^1.0.5"
prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1" version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -10110,7 +10119,7 @@ react-intl@^6.4.2:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
react-is@^16.13.1, react-is@^16.7.0: react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@ -10175,17 +10184,18 @@ react-redux@^8.0.4, react-redux@^8.1.1:
react-is "^18.0.0" react-is "^18.0.0"
use-sync-external-store "^1.0.0" use-sync-external-store "^1.0.0"
react-router-dom@^4.1.1: react-router-dom@^5.3.4:
version "4.3.1" version "5.3.4"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.4.tgz#2ed62ffd88cae6db134445f4a0c0ae8b91d2e5e6"
integrity sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA== integrity sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==
dependencies: dependencies:
history "^4.7.2" "@babel/runtime" "^7.12.13"
invariant "^2.2.4" history "^4.9.0"
loose-envify "^1.3.1" loose-envify "^1.3.1"
prop-types "^15.6.1" prop-types "^15.6.2"
react-router "^4.3.1" react-router "5.3.4"
warning "^4.0.1" tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router-scroll-4@^1.0.0-beta.1: react-router-scroll-4@^1.0.0-beta.1:
version "1.0.0-beta.2" version "1.0.0-beta.2"
@ -10195,18 +10205,20 @@ react-router-scroll-4@^1.0.0-beta.1:
scroll-behavior "^0.9.1" scroll-behavior "^0.9.1"
warning "^3.0.0" warning "^3.0.0"
react-router@^4.3.1: react-router@5.3.4, react-router@^5.3.4:
version "4.3.1" version "5.3.4"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e" resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.4.tgz#8ca252d70fcc37841e31473c7a151cf777887bb5"
integrity sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg== integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==
dependencies: dependencies:
history "^4.7.2" "@babel/runtime" "^7.12.13"
hoist-non-react-statics "^2.5.0" history "^4.9.0"
invariant "^2.2.4" hoist-non-react-statics "^3.1.0"
loose-envify "^1.3.1" loose-envify "^1.3.1"
path-to-regexp "^1.7.0" path-to-regexp "^1.7.0"
prop-types "^15.6.1" prop-types "^15.6.2"
warning "^4.0.1" react-is "^16.6.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-select@*, react-select@^5.7.3: react-select@*, react-select@^5.7.3:
version "5.7.4" version "5.7.4"
@ -10457,7 +10469,7 @@ regenerator-runtime@^0.12.0:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
regenerator-runtime@^0.13.3: regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.3:
version "0.13.11" version "0.13.11"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==