Merge commit '2ce0b666a139726dc406e6c1887728553b947e59' into glitch-soc/merge-upstream

Conflicts:
- `config/webpack/generateLocalePacks.js`:
  A dependency update changed how functions are imported.
  Also, some linting fixes not applicable to glitch-soc.
pull/2236/head
Claire 2023-05-25 20:43:25 +02:00
commit b735954971
128 changed files with 1393 additions and 560 deletions

View File

@ -55,10 +55,7 @@ module.exports = {
'\\.(css|scss|json)$', '\\.(css|scss|json)$',
], ],
'import/resolver': { 'import/resolver': {
node: { typescript: {},
paths: ['app/javascript'],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
}, },
}, },
@ -104,7 +101,6 @@ module.exports = {
'react/jsx-equals-spacing': 'error', 'react/jsx-equals-spacing': 'error',
'react/jsx-no-bind': 'error', 'react/jsx-no-bind': 'error',
'react/jsx-no-target-blank': 'off', 'react/jsx-no-target-blank': 'off',
'react/no-deprecated': 'off',
'react/no-unknown-property': 'off', 'react/no-unknown-property': 'off',
'react/self-closing-comp': 'error', 'react/self-closing-comp': 'error',
@ -168,11 +164,14 @@ module.exports = {
{ {
js: 'never', js: 'never',
jsx: 'never', jsx: 'never',
mjs: 'never',
ts: 'never', ts: 'never',
tsx: 'never', tsx: 'never',
}, },
], ],
'import/first': 'error',
'import/newline-after-import': 'error', 'import/newline-after-import': 'error',
'import/no-anonymous-default-export': 'error',
'import/no-extraneous-dependencies': [ 'import/no-extraneous-dependencies': [
'error', 'error',
{ {
@ -187,6 +186,9 @@ module.exports = {
'import/no-amd': 'error', 'import/no-amd': 'error',
'import/no-commonjs': 'error', 'import/no-commonjs': 'error',
'import/no-import-module-exports': 'error', 'import/no-import-module-exports': 'error',
'import/no-relative-packages': 'error',
'import/no-self-import': 'error',
'import/no-useless-path-segments': 'error',
'import/no-webpack-loader-syntax': 'error', 'import/no-webpack-loader-syntax': 'error',
'promise/always-return': 'off', 'promise/always-return': 'off',
@ -258,6 +260,7 @@ module.exports = {
extends: [ extends: [
'eslint:recommended', 'eslint:recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:react/recommended', 'plugin:react/recommended',
'plugin:react-hooks/recommended', 'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended', 'plugin:jsx-a11y/recommended',
@ -268,8 +271,66 @@ module.exports = {
'plugin:prettier/recommended', 'plugin:prettier/recommended',
], ],
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
rules: { rules: {
'@typescript-eslint/no-explicit-any': 'off', 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
'import/order': [
'error',
{
alphabetize: { order: 'asc' },
'newlines-between': 'always',
groups: [
'builtin',
'external',
'internal',
'parent',
['index', 'sibling'],
'object',
],
pathGroups: [
// React core packages
{
pattern: '{react,react-dom,prop-types}',
group: 'builtin',
position: 'after',
},
// I18n
{
pattern: 'react-intl',
group: 'builtin',
position: 'after',
},
// Common React utilities
{
pattern: '{classnames,react-helmet}',
group: 'external',
position: 'before',
},
// Immutable / Redux / data store
{
pattern: '{immutable,react-redux,react-immutable-proptypes,react-immutable-pure-component,reselect}',
group: 'external',
position: 'before',
},
// Internal packages
{
pattern: '{mastodon/**}',
group: 'internal',
position: 'after',
},
],
pathGroupsExcludedImportTypes: [],
},
],
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
'@typescript-eslint/consistent-type-exports': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
'jsdoc/require-jsdoc': 'off', 'jsdoc/require-jsdoc': 'off',

View File

@ -94,11 +94,6 @@ Lint/AmbiguousBlockAssociation:
- 'spec/services/unsuspend_account_service_spec.rb' - 'spec/services/unsuspend_account_service_spec.rb'
- 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb' - 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb'
# This cop supports safe autocorrection (--autocorrect).
Lint/AmbiguousOperatorPrecedence:
Exclude:
- 'config/initializers/rack_attack.rb'
# Configuration parameters: AllowComments, AllowEmptyLambdas. # Configuration parameters: AllowComments, AllowEmptyLambdas.
Lint/EmptyBlock: Lint/EmptyBlock:
Exclude: Exclude:
@ -646,24 +641,6 @@ RSpec/RepeatedExampleGroupBody:
Exclude: Exclude:
- 'spec/controllers/statuses_controller_spec.rb' - 'spec/controllers/statuses_controller_spec.rb'
RSpec/RepeatedExampleGroupDescription:
Exclude:
- 'spec/controllers/admin/reports/actions_controller_spec.rb'
- 'spec/policies/report_note_policy_spec.rb'
RSpec/ScatteredSetup:
Exclude:
- 'spec/controllers/activitypub/followers_synchronizations_controller_spec.rb'
- 'spec/controllers/activitypub/outboxes_controller_spec.rb'
- 'spec/controllers/admin/disputes/appeals_controller_spec.rb'
- 'spec/controllers/auth/registrations_controller_spec.rb'
- 'spec/services/activitypub/process_account_service_spec.rb'
# This cop supports safe autocorrection (--autocorrect).
RSpec/SharedContext:
Exclude:
- 'spec/services/unsuspend_account_service_spec.rb'
RSpec/StubbedMock: RSpec/StubbedMock:
Exclude: Exclude:
- 'spec/controllers/api/base_controller_spec.rb' - 'spec/controllers/api/base_controller_spec.rb'

View File

@ -166,7 +166,7 @@ GEM
sshkit (~> 1.3) sshkit (~> 1.3)
capistrano-yarn (2.0.2) capistrano-yarn (2.0.2)
capistrano (~> 3.0) capistrano (~> 3.0)
capybara (3.39.0) capybara (3.39.1)
addressable addressable
matrix matrix
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
@ -331,7 +331,7 @@ GEM
httplog (1.6.2) httplog (1.6.2)
rack (>= 2.0) rack (>= 2.0)
rainbow (>= 2.0.0) rainbow (>= 2.0.0)
i18n (1.12.0) i18n (1.13.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
i18n-tasks (1.0.12) i18n-tasks (1.0.12)
activesupport (>= 4.0.2) activesupport (>= 4.0.2)
@ -418,7 +418,7 @@ GEM
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2023.0218.1) mime-types-data (3.2023.0218.1)
mini_mime (1.1.2) mini_mime (1.1.2)
mini_portile2 (2.8.1) mini_portile2 (2.8.2)
minitest (5.18.0) minitest (5.18.0)
msgpack (1.7.0) msgpack (1.7.0)
multi_json (1.15.0) multi_json (1.15.0)
@ -698,7 +698,7 @@ GEM
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)
terrapin (0.6.0) terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
thor (1.2.1) thor (1.2.2)
tilt (2.1.0) tilt (2.1.0)
timeout (0.3.2) timeout (0.3.2)
tpm-key_attestation (0.12.0) tpm-key_attestation (0.12.0)

View File

@ -58,7 +58,7 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController
end end
def set_canonical_email_blocks_from_test def set_canonical_email_blocks_from_test
@canonical_email_blocks = CanonicalEmailBlock.matching_email(params[:email]) @canonical_email_blocks = CanonicalEmailBlock.matching_email(params.require(:email))
end end
def set_canonical_email_block def set_canonical_email_block

View File

@ -1,11 +1,12 @@
import { createAction } from '@reduxjs/toolkit'; import { createAction } from '@reduxjs/toolkit';
import type { LayoutType } from '../is_mobile'; import type { LayoutType } from '../is_mobile';
export const focusApp = createAction('APP_FOCUS'); export const focusApp = createAction('APP_FOCUS');
export const unfocusApp = createAction('APP_UNFOCUS'); export const unfocusApp = createAction('APP_UNFOCUS');
type ChangeLayoutPayload = { interface ChangeLayoutPayload {
layout: LayoutType; layout: LayoutType;
}; }
export const changeLayout = export const changeLayout =
createAction<ChangeLayoutPayload>('APP_LAYOUT_CHANGE'); createAction<ChangeLayoutPayload>('APP_LAYOUT_CHANGE');

View File

@ -1,12 +1,12 @@
import api from '../api'; import api from '../api';
import { importFetchedStatuses } from './importer'; import { importFetchedStatuses } from './importer';
import { me } from '../initial_state';
export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST'; export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS'; export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL'; export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
import { me } from '../initial_state';
export function fetchPinnedStatuses() { export function fetchPinnedStatuses() {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(fetchPinnedStatusesRequest()); dispatch(fetchPinnedStatusesRequest());

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
import { fromJS } from 'immutable'; import { fromJS } from 'immutable';
import DisplayName from '../display_name'; import { DisplayName } from '../display_name';
describe('<DisplayName />', () => { describe('<DisplayName />', () => {
it('renders display name + account name', () => { it('renders display name + account name', () => {

View File

@ -2,18 +2,18 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Avatar } from './avatar'; import { Avatar } from './avatar';
import DisplayName from './display_name'; import { DisplayName } from './display_name';
import { IconButton } from './icon_button'; import { IconButton } from './icon_button';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from '../initial_state'; import { me } from '../initial_state';
import { RelativeTimestamp } from './relative_timestamp'; import { RelativeTimestamp } from './relative_timestamp';
import Skeleton from 'mastodon/components/skeleton';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { counterRenderer } from 'mastodon/components/common_counter'; import { counterRenderer } from 'mastodon/components/common_counter';
import ShortNumber from 'mastodon/components/short_number'; import ShortNumber from 'mastodon/components/short_number';
import classNames from 'classnames'; import classNames from 'classnames';
import { VerifiedBadge } from 'mastodon/components/verified_badge'; import { VerifiedBadge } from 'mastodon/components/verified_badge';
import { EmptyAccount } from 'mastodon/components/empty_account';
const messages = defineMessages({ const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' },
@ -77,20 +77,7 @@ class Account extends ImmutablePureComponent {
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal } = this.props; const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal } = this.props;
if (!account) { if (!account) {
return ( return <EmptyAccount size={size} minimal={minimal} />;
<div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'>
<div className='account__display-name'>
<div className='account__avatar-wrapper'><Skeleton width={size} height={size} /></div>
<div>
<DisplayName />
<Skeleton width='7ch' />
</div>
</div>
</div>
</div>
);
} }
if (hidden) { if (hidden) {

View File

@ -1,8 +1,11 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import ShortNumber from './short_number';
import { TransitionMotion, spring } from 'react-motion'; import { TransitionMotion, spring } from 'react-motion';
import { reduceMotion } from '../initial_state'; import { reduceMotion } from '../initial_state';
import ShortNumber from './short_number';
const obfuscatedCount = (count: number) => { const obfuscatedCount = (count: number) => {
if (count < 0) { if (count < 0) {
return 0; return 0;
@ -13,10 +16,10 @@ const obfuscatedCount = (count: number) => {
} }
}; };
type Props = { interface Props {
value: number; value: number;
obfuscate?: boolean; obfuscate?: boolean;
}; }
export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => { export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
const [previousValue, setPreviousValue] = useState(value); const [previousValue, setPreviousValue] = useState(value);
const [direction, setDirection] = useState<1 | -1>(1); const [direction, setDirection] = useState<1 | -1>(1);
@ -64,7 +67,11 @@ export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
transform: `translateY(${style.y * 100}%)`, transform: `translateY(${style.y * 100}%)`,
}} }}
> >
{obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />} {obfuscate ? (
obfuscatedCount(data as number)
) : (
<ShortNumber value={data as number} />
)}
</span> </span>
))} ))}
</span> </span>

View File

@ -154,7 +154,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
this.input.focus(); this.input.focus();
}; };
componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps (nextProps) {
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
this.setState({ suggestionsHidden: false }); this.setState({ suggestionsHidden: false });
} }

View File

@ -153,7 +153,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
this.textarea.focus(); this.textarea.focus();
}; };
componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps (nextProps) {
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
this.setState({ suggestionsHidden: false }); this.setState({ suggestionsHidden: false });
} }

View File

@ -1,16 +1,18 @@
import * as React from 'react'; import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { autoPlayGif } from '../initial_state';
import { useHovering } from '../../hooks/useHovering'; import { useHovering } from '../../hooks/useHovering';
import type { Account } from '../../types/resources'; import type { Account } from '../../types/resources';
import { autoPlayGif } from '../initial_state';
type Props = { interface Props {
account: Account; account: Account;
size: number; size: number;
style?: React.CSSProperties; style?: React.CSSProperties;
inline?: boolean; inline?: boolean;
animate?: boolean; animate?: boolean;
}; }
export const Avatar: React.FC<Props> = ({ export const Avatar: React.FC<Props> = ({
account, account,

View File

@ -1,15 +1,16 @@
import React from 'react'; import React from 'react';
import type { Account } from '../../types/resources';
import { useHovering } from '../../hooks/useHovering'; import { useHovering } from '../../hooks/useHovering';
import type { Account } from '../../types/resources';
import { autoPlayGif } from '../initial_state'; import { autoPlayGif } from '../initial_state';
type Props = { interface Props {
account: Account; account: Account;
friend: Account; friend: Account;
size?: number; size?: number;
baseSize?: number; baseSize?: number;
overlaySize?: number; overlaySize?: number;
}; }
export const AvatarOverlay: React.FC<Props> = ({ export const AvatarOverlay: React.FC<Props> = ({
account, account,

View File

@ -1,14 +1,14 @@
import { decode } from 'blurhash';
import React, { useRef, useEffect } from 'react'; import React, { useRef, useEffect } from 'react';
type Props = { import { decode } from 'blurhash';
interface Props extends React.HTMLAttributes<HTMLCanvasElement> {
hash: string; hash: string;
width?: number; width?: number;
height?: number; height?: number;
dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched
children?: never; children?: never;
[key: string]: any; }
};
const Blurhash: React.FC<Props> = ({ const Blurhash: React.FC<Props> = ({
hash, hash,
width = 32, width = 32,
@ -21,6 +21,7 @@ const Blurhash: React.FC<Props> = ({
useEffect(() => { useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const canvas = canvasRef.current!; const canvas = canvasRef.current!;
// eslint-disable-next-line no-self-assign // eslint-disable-next-line no-self-assign
canvas.width = canvas.width; // resets canvas canvas.width = canvas.width; // resets canvas

View File

@ -1,79 +0,0 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { autoPlayGif } from 'mastodon/initial_state';
import Skeleton from 'mastodon/components/skeleton';
export default class DisplayName extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
others: ImmutablePropTypes.list,
localDomain: PropTypes.string,
};
handleMouseEnter = ({ currentTarget }) => {
if (autoPlayGif) {
return;
}
const emojis = currentTarget.querySelectorAll('.custom-emoji');
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-original');
}
};
handleMouseLeave = ({ currentTarget }) => {
if (autoPlayGif) {
return;
}
const emojis = currentTarget.querySelectorAll('.custom-emoji');
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-static');
}
};
render () {
const { others, localDomain } = this.props;
let displayName, suffix, account;
if (others && others.size > 1) {
displayName = others.take(2).map(a => <bdi key={a.get('id')}><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi>).reduce((prev, cur) => [prev, ', ', cur]);
if (others.size - 2 > 0) {
suffix = `+${others.size - 2}`;
}
} else if ((others && others.size > 0) || this.props.account) {
if (others && others.size > 0) {
account = others.first();
} else {
account = this.props.account;
}
let acct = account.get('acct');
if (acct.indexOf('@') === -1 && localDomain) {
acct = `${acct}@${localDomain}`;
}
displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
suffix = <span className='display-name__account'>@{acct}</span>;
} else {
displayName = <bdi><strong className='display-name__html'><Skeleton width='10ch' /></strong></bdi>;
suffix = <span className='display-name__account'><Skeleton width='7ch' /></span>;
}
return (
<span className='display-name' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
{displayName} {suffix}
</span>
);
}
}

View File

@ -0,0 +1,121 @@
import React from 'react';
import type { List } from 'immutable';
import type { Account } from '../../types/resources';
import { autoPlayGif } from '../initial_state';
import Skeleton from './skeleton';
interface Props {
account?: Account;
others?: List<Account>;
localDomain?: string;
}
export class DisplayName extends React.PureComponent<Props> {
handleMouseEnter: React.ReactEventHandler<HTMLSpanElement> = ({
currentTarget,
}) => {
if (autoPlayGif) {
return;
}
const emojis =
currentTarget.querySelectorAll<HTMLImageElement>('img.custom-emoji');
emojis.forEach((emoji) => {
const originalSrc = emoji.getAttribute('data-original');
if (originalSrc != null) emoji.src = originalSrc;
});
};
handleMouseLeave: React.ReactEventHandler<HTMLSpanElement> = ({
currentTarget,
}) => {
if (autoPlayGif) {
return;
}
const emojis =
currentTarget.querySelectorAll<HTMLImageElement>('img.custom-emoji');
emojis.forEach((emoji) => {
const staticSrc = emoji.getAttribute('data-static');
if (staticSrc != null) emoji.src = staticSrc;
});
};
render() {
const { others, localDomain } = this.props;
let displayName: React.ReactNode,
suffix: React.ReactNode,
account: Account | undefined;
if (others && others.size > 0) {
account = others.first();
} else if (this.props.account) {
account = this.props.account;
}
if (others && others.size > 1) {
displayName = others
.take(2)
.map((a) => (
<bdi key={a.get('id')}>
<strong
className='display-name__html'
dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }}
/>
</bdi>
))
.reduce((prev, cur) => [prev, ', ', cur]);
if (others.size - 2 > 0) {
suffix = `+${others.size - 2}`;
}
} else if (account) {
let acct = account.get('acct');
if (acct.indexOf('@') === -1 && localDomain) {
acct = `${acct}@${localDomain}`;
}
displayName = (
<bdi>
<strong
className='display-name__html'
dangerouslySetInnerHTML={{
__html: account.get('display_name_html'),
}}
/>
</bdi>
);
suffix = <span className='display-name__account'>@{acct}</span>;
} else {
displayName = (
<bdi>
<strong className='display-name__html'>
<Skeleton width='10ch' />
</strong>
</bdi>
);
suffix = (
<span className='display-name__account'>
<Skeleton width='7ch' />
</span>
);
}
return (
<span
className='display-name'
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
{displayName} {suffix}
</span>
);
}
}

View File

@ -1,6 +1,9 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { InjectedIntl } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
import { IconButton } from './icon_button'; import { IconButton } from './icon_button';
import { InjectedIntl, defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages({
unblockDomain: { unblockDomain: {
@ -9,11 +12,11 @@ const messages = defineMessages({
}, },
}); });
type Props = { interface Props {
domain: string; domain: string;
onUnblockDomain: (domain: string) => void; onUnblockDomain: (domain: string) => void;
intl: InjectedIntl; intl: InjectedIntl;
}; }
const _Domain: React.FC<Props> = ({ domain, onUnblockDomain, intl }) => { const _Domain: React.FC<Props> = ({ domain, onUnblockDomain, intl }) => {
const handleDomainUnblock = useCallback(() => { const handleDomainUnblock = useCallback(() => {
onUnblockDomain(domain); onUnblockDomain(domain);

View File

@ -0,0 +1,33 @@
import React from 'react';
import classNames from 'classnames';
import { DisplayName } from 'mastodon/components/display_name';
import Skeleton from 'mastodon/components/skeleton';
interface Props {
size?: number;
minimal?: boolean;
}
export const EmptyAccount: React.FC<Props> = ({
size = 46,
minimal = false,
}) => {
return (
<div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'>
<div className='account__display-name'>
<div className='account__avatar-wrapper'>
<Skeleton width={size} height={size} />
</div>
<div>
<DisplayName />
<Skeleton width='7ch' />
</div>
</div>
</div>
</div>
);
};

View File

@ -1,6 +1,6 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
type Props = { interface Props {
src: string; src: string;
key: string; key: string;
alt?: string; alt?: string;
@ -8,7 +8,7 @@ type Props = {
width: number; width: number;
height: number; height: number;
onClick?: () => void; onClick?: () => void;
}; }
export const GIFV: React.FC<Props> = ({ export const GIFV: React.FC<Props> = ({
src, src,

View File

@ -1,13 +1,14 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
type Props = { interface Props extends React.HTMLAttributes<HTMLImageElement> {
id: string; id: string;
className?: string; className?: string;
fixedWidth?: boolean; fixedWidth?: boolean;
children?: never; children?: never;
[key: string]: any; }
};
export const Icon: React.FC<Props> = ({ export const Icon: React.FC<Props> = ({
id, id,
className, className,

View File

@ -1,9 +1,11 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import { Icon } from './icon';
import { AnimatedNumber } from './animated_number';
type Props = { import classNames from 'classnames';
import { AnimatedNumber } from './animated_number';
import { Icon } from './icon';
interface Props {
className?: string; className?: string;
title: string; title: string;
icon: string; icon: string;
@ -25,11 +27,11 @@ type Props = {
obfuscateCount?: boolean; obfuscateCount?: boolean;
href?: string; href?: string;
ariaHidden: boolean; ariaHidden: boolean;
}; }
type States = { interface States {
activate: boolean; activate: boolean;
deactivate: boolean; deactivate: boolean;
}; }
export class IconButton extends React.PureComponent<Props, States> { export class IconButton extends React.PureComponent<Props, States> {
static defaultProps = { static defaultProps = {
size: 18, size: 18,

View File

@ -1,14 +1,15 @@
import React from 'react'; import React from 'react';
import { Icon } from './icon'; import { Icon } from './icon';
const formatNumber = (num: number): number | string => (num > 40 ? '40+' : num); const formatNumber = (num: number): number | string => (num > 40 ? '40+' : num);
type Props = { interface Props {
id: string; id: string;
count: number; count: number;
issueBadge: boolean; issueBadge: boolean;
className: string; className: string;
}; }
export const IconWithBadge: React.FC<Props> = ({ export const IconWithBadge: React.FC<Props> = ({
id, id,
count, count,

View File

@ -1,15 +1,14 @@
import React from 'react'; import React from 'react';
import logo from 'mastodon/../images/logo.svg'; import logo from 'mastodon/../images/logo.svg';
export const WordmarkLogo = () => ( export const WordmarkLogo: React.FC = () => (
<svg viewBox='0 0 261 66' className='logo logo--wordmark' role='img'> <svg viewBox='0 0 261 66' className='logo logo--wordmark' role='img'>
<title>Mastodon</title> <title>Mastodon</title>
<use xlinkHref='#logo-symbol-wordmark' /> <use xlinkHref='#logo-symbol-wordmark' />
</svg> </svg>
); );
export const SymbolLogo = () => ( export const SymbolLogo: React.FC = () => (
<img src={logo} alt='Mastodon' className='logo logo--icon' /> <img src={logo} alt='Mastodon' className='logo logo--icon' />
); );
export default WordmarkLogo;

View File

@ -231,7 +231,7 @@ class MediaGallery extends React.PureComponent {
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
} }
componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps (nextProps) {
if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) { if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) {
this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' }); this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' });
} else if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) { } else if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
@ -256,7 +256,7 @@ class MediaGallery extends React.PureComponent {
}; };
handleClick = (index) => { handleClick = (index) => {
this.props.onOpenMedia(this.props.media, index); this.props.onOpenMedia(this.props.media, index, this.props.lang);
}; };
handleRef = c => { handleRef = c => {

View File

@ -57,7 +57,7 @@ export default class ModalRoot extends React.PureComponent {
this.history = this.context.router ? this.context.router.history : createBrowserHistory(); this.history = this.context.router ? this.context.router.history : createBrowserHistory();
} }
componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps (nextProps) {
if (!!nextProps.children && !this.props.children) { if (!!nextProps.children && !this.props.children) {
this.activeElement = document.activeElement; this.activeElement = document.activeElement;

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
export const NotSignedInIndicator: React.FC = () => ( export const NotSignedInIndicator: React.FC = () => (
@ -6,7 +7,7 @@ export const NotSignedInIndicator: React.FC = () => (
<div className='empty-column-indicator'> <div className='empty-column-indicator'>
<FormattedMessage <FormattedMessage
id='not_signed_in_indicator.not_signed_in' id='not_signed_in_indicator.not_signed_in'
defaultMessage='You need to sign in to access this resource.' defaultMessage='You need to login to access this resource.'
/> />
</div> </div>
</div> </div>

View File

@ -1,13 +1,14 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
type Props = { interface Props {
value: string; value: string;
checked: boolean; checked: boolean;
name: string; name: string;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void; onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
label: React.ReactNode; label: React.ReactNode;
}; }
export const RadioButton: React.FC<Props> = ({ export const RadioButton: React.FC<Props> = ({
name, name,

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { injectIntl, defineMessages, InjectedIntl } from 'react-intl';
import type { InjectedIntl } from 'react-intl';
import { injectIntl, defineMessages } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages({
today: { id: 'relative_time.today', defaultMessage: 'today' }, today: { id: 'relative_time.today', defaultMessage: 'today' },
@ -187,16 +189,16 @@ const timeRemainingString = (
return relativeTime; return relativeTime;
}; };
type Props = { interface Props {
intl: InjectedIntl; intl: InjectedIntl;
timestamp: string; timestamp: string;
year: number; year: number;
futureDate?: boolean; futureDate?: boolean;
short?: boolean; short?: boolean;
}; }
type States = { interface States {
now: number; now: number;
}; }
class RelativeTimestamp extends React.Component<Props, States> { class RelativeTimestamp extends React.Component<Props, States> {
state = { state = {
now: this.props.intl.now(), now: this.props.intl.now(),

View File

@ -7,7 +7,7 @@ import ShortNumber from 'mastodon/components/short_number';
import Skeleton from 'mastodon/components/skeleton'; import Skeleton from 'mastodon/components/skeleton';
import Account from 'mastodon/containers/account_container'; import Account from 'mastodon/containers/account_container';
import { domain } from 'mastodon/initial_state'; import { domain } from 'mastodon/initial_state';
import { Image } from 'mastodon/components/image'; import { ServerHeroImage } from 'mastodon/components/server_hero_image';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
const messages = defineMessages({ const messages = defineMessages({
@ -41,7 +41,7 @@ class ServerBanner extends React.PureComponent {
<FormattedMessage id='server_banner.introduction' defaultMessage='{domain} is part of the decentralized social network powered by {mastodon}.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} /> <FormattedMessage id='server_banner.introduction' defaultMessage='{domain} is part of the decentralized social network powered by {mastodon}.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} />
</div> </div>
<Image blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' /> <ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' />
<div className='server-banner__description'> <div className='server-banner__description'>
{isLoading ? ( {isLoading ? (

View File

@ -1,15 +1,17 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { Blurhash } from './blurhash';
import classNames from 'classnames'; import classNames from 'classnames';
type Props = { import { Blurhash } from './blurhash';
interface Props {
src: string; src: string;
srcSet?: string; srcSet?: string;
blurhash?: string; blurhash?: string;
className?: string; className?: string;
}; }
export const Image: React.FC<Props> = ({ export const ServerHeroImage: React.FC<Props> = ({
src, src,
srcSet, srcSet,
blurhash, blurhash,

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import { Avatar } from './avatar'; import { Avatar } from './avatar';
import { AvatarOverlay } from './avatar_overlay'; import { AvatarOverlay } from './avatar_overlay';
import { RelativeTimestamp } from './relative_timestamp'; import { RelativeTimestamp } from './relative_timestamp';
import DisplayName from './display_name'; import { DisplayName } from './display_name';
import StatusContent from './status_content'; import StatusContent from './status_content';
import StatusActionBar from './status_action_bar'; import StatusActionBar from './status_action_bar';
import AttachmentList from './attachment_list'; import AttachmentList from './attachment_list';
@ -194,11 +194,12 @@ class Status extends ImmutablePureComponent {
handleOpenVideo = (options) => { handleOpenVideo = (options) => {
const status = this._properStatus(); const status = this._properStatus();
this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), options); this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), status.get('language'), options);
}; };
handleOpenMedia = (media, index) => { handleOpenMedia = (media, index) => {
this.props.onOpenMedia(this._properStatus().get('id'), media, index); const status = this._properStatus();
this.props.onOpenMedia(status.get('id'), media, index, status.get('language'));
}; };
handleHotkeyOpenMedia = e => { handleHotkeyOpenMedia = e => {
@ -208,10 +209,11 @@ class Status extends ImmutablePureComponent {
e.preventDefault(); e.preventDefault();
if (status.get('media_attachments').size > 0) { if (status.get('media_attachments').size > 0) {
const lang = status.get('language');
if (status.getIn(['media_attachments', 0, 'type']) === 'video') { if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), { startTime: 0 }); onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, { startTime: 0 });
} else { } else {
onOpenMedia(status.get('id'), status.get('media_attachments'), 0); onOpenMedia(status.get('id'), status.get('media_attachments'), 0, lang);
} }
} }
}; };

View File

@ -26,6 +26,7 @@ export default class StatusList extends ImmutablePureComponent {
alwaysPrepend: PropTypes.bool, alwaysPrepend: PropTypes.bool,
withCounters: PropTypes.bool, withCounters: PropTypes.bool,
timelineId: PropTypes.string, timelineId: PropTypes.string,
lastId: PropTypes.string,
}; };
static defaultProps = { static defaultProps = {
@ -55,7 +56,8 @@ export default class StatusList extends ImmutablePureComponent {
}; };
handleLoadOlder = debounce(() => { handleLoadOlder = debounce(() => {
this.props.onLoadMore(this.props.statusIds.size > 0 ? this.props.statusIds.last() : undefined); const { statusIds, lastId, onLoadMore } = this.props;
onLoadMore(lastId || (statusIds.size > 0 ? statusIds.last() : undefined));
}, 300, { leading: true }); }, 300, { leading: true });
_selectChild (index, align_top) { _selectChild (index, align_top) {

View File

@ -1,9 +1,10 @@
import React from 'react'; import React from 'react';
import { Icon } from './icon'; import { Icon } from './icon';
type Props = { interface Props {
link: string; link: string;
}; }
export const VerifiedBadge: React.FC<Props> = ({ link }) => ( export const VerifiedBadge: React.FC<Props> = ({ link }) => (
<span className='verified-badge'> <span className='verified-badge'>
<Icon id='check' className='verified-badge__mark' /> <Icon id='check' className='verified-badge__mark' />

View File

@ -29,19 +29,20 @@ export default class MediaContainer extends PureComponent {
state = { state = {
media: null, media: null,
index: null, index: null,
lang: null,
time: null, time: null,
backgroundColor: null, backgroundColor: null,
options: null, options: null,
}; };
handleOpenMedia = (media, index) => { handleOpenMedia = (media, index, lang) => {
document.body.classList.add('with-modals--active'); document.body.classList.add('with-modals--active');
document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
this.setState({ media, index }); this.setState({ media, index, lang });
}; };
handleOpenVideo = (options) => { handleOpenVideo = (lang, options) => {
const { components } = this.props; const { components } = this.props;
const { media } = JSON.parse(components[options.componentIndex].getAttribute('data-props')); const { media } = JSON.parse(components[options.componentIndex].getAttribute('data-props'));
const mediaList = fromJS(media); const mediaList = fromJS(media);
@ -49,7 +50,7 @@ export default class MediaContainer extends PureComponent {
document.body.classList.add('with-modals--active'); document.body.classList.add('with-modals--active');
document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
this.setState({ media: mediaList, options }); this.setState({ media: mediaList, lang, options });
}; };
handleCloseMedia = () => { handleCloseMedia = () => {
@ -105,6 +106,7 @@ export default class MediaContainer extends PureComponent {
<MediaModal <MediaModal
media={this.state.media} media={this.state.media}
index={this.state.index || 0} index={this.state.index || 0}
lang={this.state.lang}
currentTime={this.state.options?.startTime} currentTime={this.state.options?.startTime}
autoPlay={this.state.options?.autoPlay} autoPlay={this.state.options?.autoPlay}
volume={this.state.options?.defaultVolume} volume={this.state.options?.defaultVolume}

View File

@ -182,12 +182,12 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
dispatch(mentionCompose(account, router)); dispatch(mentionCompose(account, router));
}, },
onOpenMedia (statusId, media, index) { onOpenMedia (statusId, media, index, lang) {
dispatch(openModal('MEDIA', { statusId, media, index })); dispatch(openModal('MEDIA', { statusId, media, index, lang }));
}, },
onOpenVideo (statusId, media, options) { onOpenVideo (statusId, media, lang, options) {
dispatch(openModal('VIDEO', { statusId, media, options })); dispatch(openModal('VIDEO', { statusId, media, lang, options }));
}, },
onBlock (status) { onBlock (status) {

View File

@ -11,7 +11,7 @@ import Account from 'mastodon/containers/account_container';
import Skeleton from 'mastodon/components/skeleton'; import Skeleton from 'mastodon/components/skeleton';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import classNames from 'classnames'; import classNames from 'classnames';
import { Image } from 'mastodon/components/image'; import { ServerHeroImage } from 'mastodon/components/server_hero_image';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'column.about', defaultMessage: 'About' }, title: { id: 'column.about', defaultMessage: 'About' },
@ -114,7 +114,7 @@ class About extends React.PureComponent {
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}> <Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
<div className='scrollable about'> <div className='scrollable about'>
<div className='about__header'> <div className='about__header'>
<Image blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' /> <ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />
<h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1> <h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1>
<p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank'>Mastodon</a> }} /></p> <p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank'>Mastodon</a> }} /></p>
</div> </div>

View File

@ -22,7 +22,7 @@ class InlineAlert extends React.PureComponent {
static TRANSITION_DELAY = 200; static TRANSITION_DELAY = 200;
componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps (nextProps) {
if (!this.props.show && nextProps.show) { if (!this.props.show && nextProps.show) {
this.setState({ mountMessage: true }); this.setState({ mountMessage: true });
} else if (this.props.show && !nextProps.show) { } else if (this.props.show && !nextProps.show) {
@ -58,11 +58,11 @@ class AccountNote extends ImmutablePureComponent {
saved: false, saved: false,
}; };
componentWillMount () { UNSAFE_componentWillMount () {
this._reset(); this._reset();
} }
componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps (nextProps) {
const accountWillChange = !is(this.props.account, nextProps.account); const accountWillChange = !is(this.props.account, nextProps.account);
const newState = {}; const newState = {};

View File

@ -136,16 +136,17 @@ class AccountGallery extends ImmutablePureComponent {
handleOpenMedia = attachment => { handleOpenMedia = attachment => {
const { dispatch } = this.props; const { dispatch } = this.props;
const statusId = attachment.getIn(['status', 'id']); const statusId = attachment.getIn(['status', 'id']);
const lang = attachment.getIn(['status', 'language']);
if (attachment.get('type') === 'video') { if (attachment.get('type') === 'video') {
dispatch(openModal('VIDEO', { media: attachment, statusId, options: { autoPlay: true } })); dispatch(openModal('VIDEO', { media: attachment, statusId, lang, options: { autoPlay: true } }));
} else if (attachment.get('type') === 'audio') { } else if (attachment.get('type') === 'audio') {
dispatch(openModal('AUDIO', { media: attachment, statusId, options: { autoPlay: true } })); dispatch(openModal('AUDIO', { media: attachment, statusId, lang, options: { autoPlay: true } }));
} else { } else {
const media = attachment.getIn(['status', 'media_attachments']); const media = attachment.getIn(['status', 'media_attachments']);
const index = media.findIndex(x => x.get('id') === attachment.get('id')); const index = media.findIndex(x => x.get('id') === attachment.get('id'));
dispatch(openModal('MEDIA', { media, index, statusId })); dispatch(openModal('MEDIA', { media, index, statusId, lang }));
} }
}; };

View File

@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { AvatarOverlay } from '../../../components/avatar_overlay'; import { AvatarOverlay } from '../../../components/avatar_overlay';
import DisplayName from '../../../components/display_name'; import { DisplayName } from '../../../components/display_name';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
export default class MovedNote extends ImmutablePureComponent { export default class MovedNote extends ImmutablePureComponent {

View File

@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { lookupAccount, fetchAccount } from '../../actions/accounts'; import { lookupAccount, fetchAccount } from '../../actions/accounts';
import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines'; import { expandAccountFeaturedTimeline, expandAccountTimeline, connectTimeline, disconnectTimeline } from '../../actions/timelines';
import StatusList from '../../components/status_list'; import StatusList from '../../components/status_list';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
@ -14,7 +14,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import TimelineHint from 'mastodon/components/timeline_hint'; import TimelineHint from 'mastodon/components/timeline_hint';
import { me } from 'mastodon/initial_state'; import { me } from 'mastodon/initial_state';
import { connectTimeline, disconnectTimeline } from 'mastodon/actions/timelines';
import LimitedAccountHint from './components/limited_account_hint'; import LimitedAccountHint from './components/limited_account_hint';
import { getAccountHidden } from 'mastodon/selectors'; import { getAccountHidden } from 'mastodon/selectors';
import { fetchFeaturedTags } from '../../actions/featured_tags'; import { fetchFeaturedTags } from '../../actions/featured_tags';

View File

@ -136,7 +136,7 @@ class Audio extends React.PureComponent {
} }
} }
componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps (nextProps) {
if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) { if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
this.setState({ revealed: nextProps.visible }); this.setState({ revealed: nextProps.visible });
} }

View File

@ -34,7 +34,7 @@ class Blocks extends ImmutablePureComponent {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
componentWillMount () { UNSAFE_componentWillMount () {
this.props.dispatch(fetchBlocks()); this.props.dispatch(fetchBlocks());
} }

View File

@ -34,7 +34,7 @@ class Bookmarks extends ImmutablePureComponent {
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
}; };
componentWillMount () { UNSAFE_componentWillMount () {
this.props.dispatch(fetchBookmarkedStatuses()); this.props.dispatch(fetchBookmarkedStatuses());
} }

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Avatar } from '../../../components/avatar'; import { Avatar } from '../../../components/avatar';
import DisplayName from '../../../components/display_name'; import { DisplayName } from '../../../components/display_name';
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';

View File

@ -59,7 +59,7 @@ class ModifierPickerMenu extends React.PureComponent {
this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1); this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
}; };
componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps (nextProps) {
if (nextProps.active) { if (nextProps.active) {
this.attachListeners(); this.attachListeners();
} else { } else {

View File

@ -212,7 +212,7 @@ class PrivacyDropdown extends React.PureComponent {
this.props.onChange(value); this.props.onChange(value);
}; };
componentWillMount () { UNSAFE_componentWillMount () {
const { intl: { formatMessage } } = this.props; const { intl: { formatMessage } } = this.props;
this.options = [ this.options = [

View File

@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Avatar } from '../../../components/avatar'; import { Avatar } from '../../../components/avatar';
import { IconButton } from '../../../components/icon_button'; import { IconButton } from '../../../components/icon_button';
import DisplayName from '../../../components/display_name'; import { DisplayName } from '../../../components/display_name';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
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';

View File

@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeGetAccount } from 'mastodon/selectors'; import { makeGetAccount } from 'mastodon/selectors';
import { Avatar } from 'mastodon/components/avatar'; import { Avatar } from 'mastodon/components/avatar';
import DisplayName from 'mastodon/components/display_name'; import { DisplayName } from 'mastodon/components/display_name';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Button from 'mastodon/components/button'; import Button from 'mastodon/components/button';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';

View File

@ -34,7 +34,7 @@ class Blocks extends ImmutablePureComponent {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
componentWillMount () { UNSAFE_componentWillMount () {
this.props.dispatch(fetchDomainBlocks()); this.props.dispatch(fetchDomainBlocks());
} }

View File

@ -34,7 +34,7 @@ class Favourites extends ImmutablePureComponent {
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
}; };
componentWillMount () { UNSAFE_componentWillMount () {
this.props.dispatch(fetchFavouritedStatuses()); this.props.dispatch(fetchFavouritedStatuses());
} }

View File

@ -31,13 +31,13 @@ class Favourites extends ImmutablePureComponent {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
componentWillMount () { UNSAFE_componentWillMount () {
if (!this.props.accountIds) { if (!this.props.accountIds) {
this.props.dispatch(fetchFavourites(this.props.params.statusId)); this.props.dispatch(fetchFavourites(this.props.params.statusId));
} }
} }
componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps (nextProps) {
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
this.props.dispatch(fetchFavourites(nextProps.params.statusId)); this.props.dispatch(fetchFavourites(nextProps.params.statusId));
} }

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Avatar } from '../../../components/avatar'; import { Avatar } from '../../../components/avatar';
import DisplayName from '../../../components/display_name'; import { DisplayName } from '../../../components/display_name';
import { IconButton } from '../../../components/icon_button'; import { IconButton } from '../../../components/icon_button';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';

View File

@ -39,7 +39,7 @@ class FollowRequests extends ImmutablePureComponent {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
componentWillMount () { UNSAFE_componentWillMount () {
this.props.dispatch(fetchFollowRequests()); this.props.dispatch(fetchFollowRequests());
} }

View File

@ -143,7 +143,7 @@ class InteractionModal extends React.PureComponent {
<div className='interaction-modal__choices'> <div className='interaction-modal__choices'>
<div className='interaction-modal__choices__choice'> <div className='interaction-modal__choices__choice'>
<h3><FormattedMessage id='interaction_modal.on_this_server' defaultMessage='On this server' /></h3> <h3><FormattedMessage id='interaction_modal.on_this_server' defaultMessage='On this server' /></h3>
<a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a> <a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
{signupButton} {signupButton}
</div> </div>

View File

@ -4,7 +4,7 @@ import { makeGetAccount } from '../../../selectors';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { Avatar } from '../../../components/avatar'; import { Avatar } from '../../../components/avatar';
import DisplayName from '../../../components/display_name'; import { DisplayName } from '../../../components/display_name';
import { injectIntl } from 'react-intl'; import { injectIntl } from 'react-intl';
const makeMapStateToProps = () => { const makeMapStateToProps = () => {

View File

@ -5,7 +5,7 @@ import { makeGetAccount } from '../../../selectors';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { Avatar } from '../../../components/avatar'; import { Avatar } from '../../../components/avatar';
import DisplayName from '../../../components/display_name'; import { DisplayName } from '../../../components/display_name';
import { IconButton } from '../../../components/icon_button'; import { IconButton } from '../../../components/icon_button';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { removeFromListEditor, addToListEditor } from '../../../actions/lists'; import { removeFromListEditor, addToListEditor } from '../../../actions/lists';

View File

@ -76,7 +76,7 @@ class ListTimeline extends React.PureComponent {
this.disconnect = dispatch(connectListStream(id)); this.disconnect = dispatch(connectListStream(id));
} }
componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps (nextProps) {
const { dispatch } = this.props; const { dispatch } = this.props;
const { id } = nextProps.params; const { id } = nextProps.params;

View File

@ -42,7 +42,7 @@ class Lists extends ImmutablePureComponent {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
componentWillMount () { UNSAFE_componentWillMount () {
this.props.dispatch(fetchLists()); this.props.dispatch(fetchLists());
} }

View File

@ -35,7 +35,7 @@ class Mutes extends ImmutablePureComponent {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
componentWillMount () { UNSAFE_componentWillMount () {
this.props.dispatch(fetchMutes()); this.props.dispatch(fetchMutes());
} }

View File

@ -2,7 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Avatar } from 'mastodon/components/avatar'; import { Avatar } from 'mastodon/components/avatar';
import DisplayName from 'mastodon/components/display_name'; import { DisplayName } from 'mastodon/components/display_name';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { IconButton } from 'mastodon/components/icon_button'; import { IconButton } from 'mastodon/components/icon_button';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';

View File

@ -93,7 +93,7 @@ class Notifications extends React.PureComponent {
trackScroll: true, trackScroll: true,
}; };
componentWillMount() { UNSAFE_componentWillMount() {
this.props.dispatch(mountNotifications()); this.props.dispatch(mountNotifications());
} }

View File

@ -7,7 +7,7 @@ import { fetchSuggestions } from 'mastodon/actions/suggestions';
import { markAsPartial } from 'mastodon/actions/timelines'; import { markAsPartial } from 'mastodon/actions/timelines';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import Account from 'mastodon/containers/account_container'; import Account from 'mastodon/containers/account_container';
import EmptyAccount from 'mastodon/components/account'; import { EmptyAccount } from 'mastodon/components/empty_account';
import { FormattedMessage, FormattedHTMLMessage } from 'react-intl'; import { FormattedMessage, FormattedHTMLMessage } from 'react-intl';
import { makeGetAccount } from 'mastodon/selectors'; import { makeGetAccount } from 'mastodon/selectors';
import { me } from 'mastodon/initial_state'; import { me } from 'mastodon/initial_state';
@ -31,6 +31,7 @@ class Follows extends React.PureComponent {
suggestions: ImmutablePropTypes.list, suggestions: ImmutablePropTypes.list,
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
multiColumn: PropTypes.bool,
}; };
componentDidMount () { componentDidMount () {
@ -44,7 +45,7 @@ class Follows extends React.PureComponent {
} }
render () { render () {
const { onBack, isLoading, suggestions, account } = this.props; const { onBack, isLoading, suggestions, account, multiColumn } = this.props;
let loadedContent; let loadedContent;
@ -58,7 +59,7 @@ class Follows extends React.PureComponent {
return ( return (
<Column> <Column>
<ColumnBackButton onClick={onBack} /> <ColumnBackButton multiColumn={multiColumn} onClick={onBack} />
<div className='scrollable privacy-policy'> <div className='scrollable privacy-policy'>
<div className='column-title'> <div className='column-title'>
@ -84,4 +85,4 @@ class Follows extends React.PureComponent {
} }
export default connect(mapStateToProps)(Follows); export default connect(mapStateToProps)(Follows);

View File

@ -40,6 +40,7 @@ class Onboarding extends ImmutablePureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
multiColumn: PropTypes.bool,
}; };
state = { state = {
@ -93,14 +94,14 @@ class Onboarding extends ImmutablePureComponent {
} }
render () { render () {
const { account } = this.props; const { account, multiColumn } = this.props;
const { step, shareClicked } = this.state; const { step, shareClicked } = this.state;
switch(step) { switch(step) {
case 'follows': case 'follows':
return <Follows onBack={this.handleBackClick} />; return <Follows onBack={this.handleBackClick} multiColumn={multiColumn} />;
case 'share': case 'share':
return <Share onBack={this.handleBackClick} />; return <Share onBack={this.handleBackClick} multiColumn={multiColumn} />;
} }
return ( return (
@ -114,7 +115,7 @@ class Onboarding extends ImmutablePureComponent {
<div className='onboarding__steps'> <div className='onboarding__steps'>
<Step onClick={this.handleProfileClick} href='/settings/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} /> <Step onClick={this.handleProfileClick} href='/settings/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
<Step onClick={this.handleFollowClick} completed={(account.get('following_count') * 1) >= 7} icon='user-plus' label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Follow {count, plural, one {one person} other {# people}}' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage='You curate your own feed. Lets fill it with interesting people.' />} /> <Step onClick={this.handleFollowClick} completed={(account.get('following_count') * 1) >= 7} icon='user-plus' label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Follow {count, plural, one {one person} other {# people}}' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own feed. Let's fill it with interesting people." />} />
<Step onClick={this.handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' />} /> <Step onClick={this.handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' />} />
<Step onClick={this.handleShareClick} completed={shareClicked} icon='copy' label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} /> <Step onClick={this.handleShareClick} completed={shareClicked} icon='copy' label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
</div> </div>

View File

@ -140,17 +140,18 @@ class Share extends React.PureComponent {
static propTypes = { static propTypes = {
onBack: PropTypes.func, onBack: PropTypes.func,
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
multiColumn: PropTypes.bool,
intl: PropTypes.object, intl: PropTypes.object,
}; };
render () { render () {
const { onBack, account, intl } = this.props; const { onBack, account, multiColumn, intl } = this.props;
const url = (new URL(`/@${account.get('username')}`, document.baseURI)).href; const url = (new URL(`/@${account.get('username')}`, document.baseURI)).href;
return ( return (
<Column> <Column>
<ColumnBackButton onClick={onBack} /> <ColumnBackButton multiColumn={multiColumn} onClick={onBack} />
<div className='scrollable privacy-policy'> <div className='scrollable privacy-policy'>
<div className='column-title'> <div className='column-title'>

View File

@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
import { IconButton } from 'mastodon/components/icon_button'; import { IconButton } from 'mastodon/components/icon_button';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Avatar } from 'mastodon/components/avatar'; import { Avatar } from 'mastodon/components/avatar';
import DisplayName from 'mastodon/components/display_name'; import { DisplayName } from 'mastodon/components/display_name';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages({

View File

@ -29,7 +29,7 @@ class PinnedStatuses extends ImmutablePureComponent {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
componentWillMount () { UNSAFE_componentWillMount () {
this.props.dispatch(fetchPinnedStatuses()); this.props.dispatch(fetchPinnedStatuses());
} }

View File

@ -31,13 +31,13 @@ class Reblogs extends ImmutablePureComponent {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
componentWillMount () { UNSAFE_componentWillMount () {
if (!this.props.accountIds) { if (!this.props.accountIds) {
this.props.dispatch(fetchReblogs(this.props.params.statusId)); this.props.dispatch(fetchReblogs(this.props.params.statusId));
} }
} }
componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
this.props.dispatch(fetchReblogs(nextProps.params.statusId)); this.props.dispatch(fetchReblogs(nextProps.params.statusId));
} }

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import StatusContent from 'mastodon/components/status_content'; import StatusContent from 'mastodon/components/status_content';
import { Avatar } from 'mastodon/components/avatar'; import { Avatar } from 'mastodon/components/avatar';
import DisplayName from 'mastodon/components/display_name'; import { DisplayName } from 'mastodon/components/display_name';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import Option from './option'; import Option from './option';
import MediaAttachments from 'mastodon/components/media_attachments'; import MediaAttachments from 'mastodon/components/media_attachments';

View File

@ -66,7 +66,7 @@ export default class Card extends React.PureComponent {
revealed: !this.props.sensitive, revealed: !this.props.sensitive,
}; };
componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps (nextProps) {
if (!Immutable.is(this.props.card, nextProps.card)) { if (!Immutable.is(this.props.card, nextProps.card)) {
this.setState({ embedded: false, previewLoaded: false }); this.setState({ embedded: false, previewLoaded: false });
} }

View File

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { Avatar } from '../../../components/avatar'; import { Avatar } from '../../../components/avatar';
import DisplayName from '../../../components/display_name'; import { DisplayName } from '../../../components/display_name';
import StatusContent from '../../../components/status_content'; import StatusContent from '../../../components/status_content';
import MediaGallery from '../../../components/media_gallery'; import MediaGallery from '../../../components/media_gallery';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';

View File

@ -128,12 +128,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(mentionCompose(account, router)); dispatch(mentionCompose(account, router));
}, },
onOpenMedia (media, index) { onOpenMedia (media, index, lang) {
dispatch(openModal('MEDIA', { media, index })); dispatch(openModal('MEDIA', { media, index, lang }));
}, },
onOpenVideo (media, options) { onOpenVideo (media, lang, options) {
dispatch(openModal('VIDEO', { media, options })); dispatch(openModal('VIDEO', { media, lang, options }));
}, },
onBlock (status) { onBlock (status) {

View File

@ -207,7 +207,7 @@ class Status extends ImmutablePureComponent {
loadedStatusId: undefined, loadedStatusId: undefined,
}; };
componentWillMount () { UNSAFE_componentWillMount () {
this.props.dispatch(fetchStatus(this.props.params.statusId)); this.props.dispatch(fetchStatus(this.props.params.statusId));
} }
@ -215,7 +215,7 @@ class Status extends ImmutablePureComponent {
attachFullscreenListener(this.onFullScreenChange); attachFullscreenListener(this.onFullScreenChange);
} }
componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps (nextProps) {
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
this._scrolledIntoView = false; this._scrolledIntoView = false;
this.props.dispatch(fetchStatus(nextProps.params.statusId)); this.props.dispatch(fetchStatus(nextProps.params.statusId));
@ -345,12 +345,12 @@ class Status extends ImmutablePureComponent {
this.props.dispatch(mentionCompose(account, router)); this.props.dispatch(mentionCompose(account, router));
}; };
handleOpenMedia = (media, index) => { handleOpenMedia = (media, index, lang) => {
this.props.dispatch(openModal('MEDIA', { statusId: this.props.status.get('id'), media, index })); this.props.dispatch(openModal('MEDIA', { statusId: this.props.status.get('id'), media, index, lang }));
}; };
handleOpenVideo = (media, options) => { handleOpenVideo = (media, lang, options) => {
this.props.dispatch(openModal('VIDEO', { statusId: this.props.status.get('id'), media, options })); this.props.dispatch(openModal('VIDEO', { statusId: this.props.status.get('id'), media, lang, options }));
}; };
handleHotkeyOpenMedia = e => { handleHotkeyOpenMedia = e => {

View File

@ -7,7 +7,7 @@ import Button from '../../../components/button';
import StatusContent from '../../../components/status_content'; import StatusContent from '../../../components/status_content';
import { Avatar } from '../../../components/avatar'; import { Avatar } from '../../../components/avatar';
import { RelativeTimestamp } from '../../../components/relative_timestamp'; import { RelativeTimestamp } from '../../../components/relative_timestamp';
import DisplayName from '../../../components/display_name'; import { DisplayName } from '../../../components/display_name';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import AttachmentList from 'mastodon/components/attachment_list'; import AttachmentList from 'mastodon/components/attachment_list';

View File

@ -33,11 +33,11 @@ class Bundle extends React.PureComponent {
forceRender: false, forceRender: false,
}; };
componentWillMount() { UNSAFE_componentWillMount() {
this.load(this.props); this.load(this.props);
} }
componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.fetchComponent !== this.props.fetchComponent) { if (nextProps.fetchComponent !== this.props.fetchComponent) {
this.load(nextProps); this.load(nextProps);
} }

View File

@ -18,7 +18,7 @@ import {
BookmarkedStatuses, BookmarkedStatuses,
ListTimeline, ListTimeline,
Directory, Directory,
} from '../../ui/util/async-components'; } from '../util/async-components';
import ComposePanel from './compose_panel'; import ComposePanel from './compose_panel';
import NavigationPanel from './navigation_panel'; import NavigationPanel from './navigation_panel';
import { supportsPassiveEvents } from 'detect-passive-events'; import { supportsPassiveEvents } from 'detect-passive-events';
@ -76,7 +76,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl'); this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
} }
componentWillUpdate(nextProps) { UNSAFE_componentWillUpdate(nextProps) {
if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) { if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel); this.node.removeEventListener('wheel', this.handleWheel);
} }

View File

@ -5,11 +5,10 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import { changeUploadCompose, uploadThumbnail, onChangeMediaDescription, onChangeMediaFocus } from '../../../actions/compose'; import { changeUploadCompose, uploadThumbnail, onChangeMediaDescription, onChangeMediaFocus } from '../../../actions/compose';
import { getPointerPosition } from '../../video'; import Video, { getPointerPosition } from '../../video';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { IconButton } from 'mastodon/components/icon_button'; import { IconButton } from 'mastodon/components/icon_button';
import Button from 'mastodon/components/button'; import Button from 'mastodon/components/button';
import Video from 'mastodon/features/video';
import Audio from 'mastodon/features/audio'; import Audio from 'mastodon/features/audio';
import Textarea from 'react-textarea-autosize'; import Textarea from 'react-textarea-autosize';
import UploadProgress from 'mastodon/features/compose/components/upload_progress'; import UploadProgress from 'mastodon/features/compose/components/upload_progress';

View File

@ -51,13 +51,13 @@ class Header extends React.PureComponent {
if (registrationsOpen) { if (registrationsOpen) {
signupButton = ( signupButton = (
<a href='/auth/sign_up' className='button button-tertiary'> <a href='/auth/sign_up' className='button'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /> <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a> </a>
); );
} else { } else {
signupButton = ( signupButton = (
<button className='button button-tertiary' onClick={openClosedRegistrationsModal}> <button className='button' onClick={openClosedRegistrationsModal}>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /> <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</button> </button>
); );
@ -65,8 +65,8 @@ class Header extends React.PureComponent {
content = ( content = (
<> <>
<a href='/auth/sign_in' className='button'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
{signupButton} {signupButton}
<a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
</> </>
); );
} }

View File

@ -3,7 +3,6 @@ import ReactSwipeableViews from 'react-swipeable-views';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Video from 'mastodon/features/video'; import Video from 'mastodon/features/video';
import { connect } from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { IconButton } from 'mastodon/components/icon_button'; import { IconButton } from 'mastodon/components/icon_button';
@ -21,15 +20,12 @@ const messages = defineMessages({
next: { id: 'lightbox.next', defaultMessage: 'Next' }, next: { id: 'lightbox.next', defaultMessage: 'Next' },
}); });
const mapStateToProps = (state, { statusId }) => ({
language: state.getIn(['statuses', statusId, 'language']),
});
class MediaModal extends ImmutablePureComponent { class MediaModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.list.isRequired, media: ImmutablePropTypes.list.isRequired,
statusId: PropTypes.string, statusId: PropTypes.string,
lang: PropTypes.string,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
@ -133,7 +129,7 @@ class MediaModal extends ImmutablePureComponent {
}; };
render () { render () {
const { media, language, statusId, intl, onClose } = this.props; const { media, statusId, lang, intl, onClose } = this.props;
const { navigationHidden } = this.state; const { navigationHidden } = this.state;
const index = this.getIndex(); const index = this.getIndex();
@ -153,7 +149,7 @@ class MediaModal extends ImmutablePureComponent {
width={width} width={width}
height={height} height={height}
alt={image.get('description')} alt={image.get('description')}
lang={language} lang={lang}
key={image.get('url')} key={image.get('url')}
onClick={this.toggleNavigation} onClick={this.toggleNavigation}
zoomButtonHidden={this.state.zoomButtonHidden} zoomButtonHidden={this.state.zoomButtonHidden}
@ -176,7 +172,7 @@ class MediaModal extends ImmutablePureComponent {
onCloseVideo={onClose} onCloseVideo={onClose}
detailed detailed
alt={image.get('description')} alt={image.get('description')}
lang={language} lang={lang}
key={image.get('url')} key={image.get('url')}
/> />
); );
@ -188,7 +184,7 @@ class MediaModal extends ImmutablePureComponent {
height={height} height={height}
key={image.get('url')} key={image.get('url')}
alt={image.get('description')} alt={image.get('description')}
lang={language} lang={lang}
onClick={this.toggleNavigation} onClick={this.toggleNavigation}
/> />
); );
@ -256,4 +252,4 @@ class MediaModal extends ImmutablePureComponent {
} }
export default connect(mapStateToProps, null, null, { forwardRef: true })(injectIntl(MediaModal)); export default injectIntl(MediaModal);

View File

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Logo from 'mastodon/components/logo'; import { WordmarkLogo } from 'mastodon/components/logo';
import { timelinePreview, showTrends } from 'mastodon/initial_state'; import { timelinePreview, showTrends } from 'mastodon/initial_state';
import ColumnLink from './column_link'; import ColumnLink from './column_link';
import DisabledAccountBanner from './disabled_account_banner'; import DisabledAccountBanner from './disabled_account_banner';
@ -46,7 +46,7 @@ class NavigationPanel extends React.Component {
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'><Logo /></Link> <Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
<hr /> <hr />
</div> </div>

View File

@ -16,13 +16,13 @@ const SignInBanner = () => {
if (registrationsOpen) { if (registrationsOpen) {
signupButton = ( signupButton = (
<a href='/auth/sign_up' className='button button--block button-tertiary'> <a href='/auth/sign_up' className='button button--block'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /> <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a> </a>
); );
} else { } else {
signupButton = ( signupButton = (
<button className='button button--block button-tertiary' onClick={openClosedRegistrationsModal}> <button className='button button--block' onClick={openClosedRegistrationsModal}>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /> <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</button> </button>
); );
@ -30,9 +30,9 @@ const SignInBanner = () => {
return ( return (
<div className='sign-in-banner'> <div className='sign-in-banner'>
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.' /></p> <p><FormattedMessage id='sign_in_banner.text' defaultMessage='Login to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.' /></p>
<a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
{signupButton} {signupButton}
<a href='/auth/sign_in' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
</div> </div>
); );
}; };

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Motion from '../../ui/util/optional_motion'; import Motion from '../util/optional_motion';
import spring from 'react-motion/lib/spring'; import spring from 'react-motion/lib/spring';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';

View File

@ -37,6 +37,7 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, { timelineId }) => ({ const mapStateToProps = (state, { timelineId }) => ({
statusIds: getStatusIds(state, { type: timelineId }), statusIds: getStatusIds(state, { type: timelineId }),
lastId: state.getIn(['timelines', timelineId, 'items'])?.last(),
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true), isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false), isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
hasMore: state.getIn(['timelines', timelineId, 'hasMore']), hasMore: state.getIn(['timelines', timelineId, 'hasMore']),

View File

@ -123,7 +123,7 @@ class SwitchingColumnsArea extends React.PureComponent {
mobile: PropTypes.bool, mobile: PropTypes.bool,
}; };
componentWillMount () { UNSAFE_componentWillMount () {
if (this.props.mobile) { if (this.props.mobile) {
document.body.classList.toggle('layout-single-column', true); document.body.classList.toggle('layout-single-column', true);
document.body.classList.toggle('layout-multiple-columns', false); document.body.classList.toggle('layout-multiple-columns', false);

View File

@ -370,7 +370,7 @@ class Video extends React.PureComponent {
} }
} }
componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps (nextProps) {
if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) { if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
this.setState({ revealed: nextProps.visible }); this.setState({ revealed: nextProps.visible });
} }
@ -469,7 +469,7 @@ class Video extends React.PureComponent {
handleOpenVideo = () => { handleOpenVideo = () => {
this.video.pause(); this.video.pause();
this.props.onOpenVideo({ this.props.onOpenVideo(this.props.lang, {
startTime: this.video.currentTime, startTime: this.video.currentTime,
autoPlay: !this.state.paused, autoPlay: !this.state.paused,
defaultVolume: this.state.volume, defaultVolume: this.state.volume,

View File

@ -1,4 +1,5 @@
import { supportsPassiveEvents } from 'detect-passive-events'; import { supportsPassiveEvents } from 'detect-passive-events';
import { forceSingleColumn } from './initial_state'; import { forceSingleColumn } from './initial_state';
const LAYOUT_BREAKPOINT = 630; const LAYOUT_BREAKPOINT = 630;

View File

@ -356,7 +356,7 @@
{ {
"descriptors": [ "descriptors": [
{ {
"defaultMessage": "You need to sign in to access this resource.", "defaultMessage": "You need to login to access this resource.",
"id": "not_signed_in_indicator.not_signed_in" "id": "not_signed_in_indicator.not_signed_in"
} }
], ],
@ -2623,7 +2623,7 @@
"id": "interaction_modal.on_this_server" "id": "interaction_modal.on_this_server"
}, },
{ {
"defaultMessage": "Sign in", "defaultMessage": "Login",
"id": "sign_in_banner.sign_in" "id": "sign_in_banner.sign_in"
}, },
{ {
@ -3236,7 +3236,7 @@
"id": "onboarding.steps.follow_people.title" "id": "onboarding.steps.follow_people.title"
}, },
{ {
"defaultMessage": "You curate your own feed. Lets fill it with interesting people.", "defaultMessage": "You curate your own feed. Let's fill it with interesting people.",
"id": "onboarding.steps.follow_people.body" "id": "onboarding.steps.follow_people.body"
}, },
{ {
@ -4175,7 +4175,7 @@
"id": "sign_in_banner.create_account" "id": "sign_in_banner.create_account"
}, },
{ {
"defaultMessage": "Sign in", "defaultMessage": "Login",
"id": "sign_in_banner.sign_in" "id": "sign_in_banner.sign_in"
} }
], ],
@ -4374,11 +4374,11 @@
"id": "sign_in_banner.create_account" "id": "sign_in_banner.create_account"
}, },
{ {
"defaultMessage": "Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.", "defaultMessage": "Login to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.",
"id": "sign_in_banner.text" "id": "sign_in_banner.text"
}, },
{ {
"defaultMessage": "Sign in", "defaultMessage": "Login",
"id": "sign_in_banner.sign_in" "id": "sign_in_banner.sign_in"
} }
], ],

View File

@ -391,7 +391,7 @@
"navigation_bar.public_timeline": "Federated timeline", "navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.search": "Search", "navigation_bar.search": "Search",
"navigation_bar.security": "Security", "navigation_bar.security": "Security",
"not_signed_in_indicator.not_signed_in": "You need to sign in to access this resource.", "not_signed_in_indicator.not_signed_in": "You need to login to access this resource.",
"notification.admin.report": "{name} reported {target}", "notification.admin.report": "{name} reported {target}",
"notification.admin.sign_up": "{name} signed up", "notification.admin.sign_up": "{name} signed up",
"notification.favourite": "{name} favourited your post", "notification.favourite": "{name} favourited your post",
@ -573,8 +573,8 @@
"server_banner.learn_more": "Learn more", "server_banner.learn_more": "Learn more",
"server_banner.server_stats": "Server stats:", "server_banner.server_stats": "Server stats:",
"sign_in_banner.create_account": "Create account", "sign_in_banner.create_account": "Create account",
"sign_in_banner.sign_in": "Sign in", "sign_in_banner.sign_in": "Login",
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.", "sign_in_banner.text": "Login to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.",
"status.admin_account": "Open moderation interface for @{name}", "status.admin_account": "Open moderation interface for @{name}",
"status.admin_domain": "Open moderation interface for {domain}", "status.admin_domain": "Open moderation interface for {domain}",
"status.admin_status": "Open this post in the moderation interface", "status.admin_status": "Open this post in the moderation interface",

View File

@ -2,7 +2,7 @@
/*eslint no-nested-ternary: "off"*/ /*eslint no-nested-ternary: "off"*/
/*eslint quotes: "off"*/ /*eslint quotes: "off"*/
export default [{ const rules = [{
locale: "co", locale: "co",
pluralRuleFunction: function (e, a) { pluralRuleFunction: function (e, a) {
return a ? 1 == e ? "one" : "other" : e >= 0 && e < 2 ? "one" : "other"; return a ? 1 == e ? "one" : "other" : e >= 0 && e < 2 ? "one" : "other";
@ -106,3 +106,5 @@ export default [{
}, },
}, },
}]; }];
export default rules;

View File

@ -2,7 +2,7 @@
/*eslint no-nested-ternary: "off"*/ /*eslint no-nested-ternary: "off"*/
/*eslint quotes: "off"*/ /*eslint quotes: "off"*/
export default [{ const rules = [{
locale: "oc", locale: "oc",
pluralRuleFunction: function (e, a) { pluralRuleFunction: function (e, a) {
return a ? 1 == e ? "one" : "other" : e >= 0 && e < 2 ? "one" : "other"; return a ? 1 == e ? "one" : "other" : e >= 0 && e < 2 ? "one" : "other";
@ -106,3 +106,5 @@ export default [{
}, },
}, },
}]; }];
export default rules;

View File

@ -2,9 +2,8 @@
/*eslint no-nested-ternary: "off"*/ /*eslint no-nested-ternary: "off"*/
/*eslint quotes: "off"*/ /*eslint quotes: "off"*/
/*eslint comma-dangle: "off"*/ /*eslint comma-dangle: "off"*/
/*eslint semi: "off"*/
export default [ const rules = [
{ {
locale: "sa", locale: "sa",
fields: { fields: {
@ -94,4 +93,6 @@ export default [
} }
} }
} }
] ];
export default rules;

View File

@ -10,8 +10,13 @@ if (!HTMLCanvasElement.prototype.toBlob) {
const BASE64_MARKER = ';base64,'; const BASE64_MARKER = ';base64,';
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
value(callback: BlobCallback, type = 'image/png', quality: any) { value: function (
const dataURL = this.toDataURL(type, quality); this: HTMLCanvasElement,
callback: BlobCallback,
type = 'image/png',
quality: unknown
) {
const dataURL: string = this.toDataURL(type, quality);
let data; let data;
if (dataURL.indexOf(BASE64_MARKER) >= 0) { if (dataURL.indexOf(BASE64_MARKER) >= 0) {

View File

@ -1,46 +1,47 @@
import { combineReducers } from 'redux-immutable';
import dropdown_menu from './dropdown_menu';
import timelines from './timelines';
import meta from './meta';
import alerts from './alerts';
import { loadingBarReducer } from 'react-redux-loading-bar'; import { loadingBarReducer } from 'react-redux-loading-bar';
import modal from './modal'; import { combineReducers } from 'redux-immutable';
import user_lists from './user_lists';
import domain_lists from './domain_lists';
import accounts from './accounts'; import accounts from './accounts';
import accounts_counters from './accounts_counters'; import accounts_counters from './accounts_counters';
import statuses from './statuses'; import accounts_map from './accounts_map';
import relationships from './relationships'; import alerts from './alerts';
import settings from './settings'; import announcements from './announcements';
import push_notifications from './push_notifications';
import status_lists from './status_lists';
import mutes from './mutes';
import blocks from './blocks'; import blocks from './blocks';
import boosts from './boosts'; import boosts from './boosts';
import server from './server';
import contexts from './contexts';
import compose from './compose'; import compose from './compose';
import search from './search'; import contexts from './contexts';
import media_attachments from './media_attachments';
import notifications from './notifications';
import height_cache from './height_cache';
import custom_emojis from './custom_emojis';
import lists from './lists';
import listEditor from './list_editor';
import listAdder from './list_adder';
import filters from './filters';
import conversations from './conversations'; import conversations from './conversations';
import suggestions from './suggestions'; import custom_emojis from './custom_emojis';
import polls from './polls'; import domain_lists from './domain_lists';
import trends from './trends'; import dropdown_menu from './dropdown_menu';
import { missedUpdatesReducer } from './missed_updates'; import filters from './filters';
import announcements from './announcements';
import markers from './markers';
import picture_in_picture from './picture_in_picture';
import accounts_map from './accounts_map';
import history from './history';
import tags from './tags';
import followed_tags from './followed_tags'; import followed_tags from './followed_tags';
import height_cache from './height_cache';
import history from './history';
import listAdder from './list_adder';
import listEditor from './list_editor';
import lists from './lists';
import markers from './markers';
import media_attachments from './media_attachments';
import meta from './meta';
import { missedUpdatesReducer } from './missed_updates';
import modal from './modal';
import mutes from './mutes';
import notifications from './notifications';
import picture_in_picture from './picture_in_picture';
import polls from './polls';
import push_notifications from './push_notifications';
import relationships from './relationships';
import search from './search';
import server from './server';
import settings from './settings';
import status_lists from './status_lists';
import statuses from './statuses';
import suggestions from './suggestions';
import tags from './tags';
import timelines from './timelines';
import trends from './trends';
import user_lists from './user_lists';
const reducers = { const reducers = {
announcements, announcements,

View File

@ -2,13 +2,13 @@ import {
MARKERS_SUBMIT_SUCCESS, MARKERS_SUBMIT_SUCCESS,
} from '../actions/markers'; } from '../actions/markers';
import { Map as ImmutableMap } from 'immutable';
const initialState = ImmutableMap({ const initialState = ImmutableMap({
home: '0', home: '0',
notifications: '0', notifications: '0',
}); });
import { Map as ImmutableMap } from 'immutable';
export default function markers(state = initialState, action) { export default function markers(state = initialState, action) {
switch(action.type) { switch(action.type) {
case MARKERS_SUBMIT_SUCCESS: case MARKERS_SUBMIT_SUCCESS:

View File

@ -1,12 +1,14 @@
import { Record } from 'immutable'; import { Record } from 'immutable';
import type { Action } from 'redux';
import { NOTIFICATIONS_UPDATE } from '../actions/notifications';
import { focusApp, unfocusApp } from '../actions/app';
type MissedUpdatesState = { import type { Action } from 'redux';
import { focusApp, unfocusApp } from '../actions/app';
import { NOTIFICATIONS_UPDATE } from '../actions/notifications';
interface MissedUpdatesState {
focused: boolean; focused: boolean;
unread: number; unread: number;
}; }
const initialState = Record<MissedUpdatesState>({ const initialState = Record<MissedUpdatesState>({
focused: true, focused: true,
unread: 0, unread: 0,

View File

@ -1,14 +1,32 @@
import type { TypedUseSelectorHook } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit';
import { rootReducer } from '../reducers'; import { rootReducer } from '../reducers';
import { loadingBarMiddleware } from './middlewares/loading_bar';
import { errorsMiddleware } from './middlewares/errors'; import { errorsMiddleware } from './middlewares/errors';
import { loadingBarMiddleware } from './middlewares/loading_bar';
import { soundsMiddleware } from './middlewares/sounds'; import { soundsMiddleware } from './middlewares/sounds';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
export const store = configureStore({ export const store = configureStore({
reducer: rootReducer, reducer: rootReducer,
middleware: (getDefaultMiddleware) => middleware: (getDefaultMiddleware) =>
getDefaultMiddleware() getDefaultMiddleware({
// In development, Redux Toolkit enables 2 default middlewares to detect
// common issues with states. Unfortunately, our use of ImmutableJS for state
// triggers both, so lets disable them until our state is fully refactored
// https://redux-toolkit.js.org/api/serializabilityMiddleware
// This checks recursively that every values in the state are serializable in JSON
// Which is not the case, as we use ImmutableJS structures, but also File objects
serializableCheck: false,
// https://redux-toolkit.js.org/api/immutabilityMiddleware
// This checks recursively if every value in the state is immutable (ie, a JS primitive type)
// But this is not the case, as our Root State is an ImmutableJS map, which is an object
immutableCheck: false,
})
.concat( .concat(
loadingBarMiddleware({ loadingBarMiddleware({
promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'], promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'],

View File

@ -1,17 +1,18 @@
import { Middleware } from 'redux'; import type { AnyAction, Middleware } from 'redux';
import type { RootState } from '..';
import { showAlertForError } from '../../actions/alerts'; import { showAlertForError } from '../../actions/alerts';
import { RootState } from '..';
const defaultFailSuffix = 'FAIL'; const defaultFailSuffix = 'FAIL';
export const errorsMiddleware: Middleware<Record<string, never>, RootState> = export const errorsMiddleware: Middleware<Record<string, never>, RootState> =
({ dispatch }) => ({ dispatch }) =>
(next) => (next) =>
(action) => { (action: AnyAction & { skipAlert?: boolean; skipNotFound?: boolean }) => {
if (action.type && !action.skipAlert) { if (action.type && !action.skipAlert) {
const isFail = new RegExp(`${defaultFailSuffix}$`, 'g'); const isFail = new RegExp(`${defaultFailSuffix}$`, 'g');
if (action.type.match(isFail)) { if (typeof action.type === 'string' && action.type.match(isFail)) {
dispatch(showAlertForError(action.error, action.skipNotFound)); dispatch(showAlertForError(action.error, action.skipNotFound));
} }
} }

View File

@ -1,6 +1,7 @@
import { showLoading, hideLoading } from 'react-redux-loading-bar'; import { showLoading, hideLoading } from 'react-redux-loading-bar';
import { Middleware } from 'redux'; import type { AnyAction, Middleware } from 'redux';
import { RootState } from '..';
import type { RootState } from '..';
interface Config { interface Config {
promiseTypeSuffixes?: string[]; promiseTypeSuffixes?: string[];
@ -19,7 +20,7 @@ export const loadingBarMiddleware = (
return ({ dispatch }) => return ({ dispatch }) =>
(next) => (next) =>
(action) => { (action: AnyAction) => {
if (action.type && !action.skipLoading) { if (action.type && !action.skipLoading) {
const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes; const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes;
@ -27,13 +28,15 @@ export const loadingBarMiddleware = (
const isFulfilled = new RegExp(`${FULFILLED}$`, 'g'); const isFulfilled = new RegExp(`${FULFILLED}$`, 'g');
const isRejected = new RegExp(`${REJECTED}$`, 'g'); const isRejected = new RegExp(`${REJECTED}$`, 'g');
if (action.type.match(isPending)) { if (typeof action.type === 'string') {
dispatch(showLoading()); if (action.type.match(isPending)) {
} else if ( dispatch(showLoading());
action.type.match(isFulfilled) || } else if (
action.type.match(isRejected) action.type.match(isFulfilled) ||
) { action.type.match(isRejected)
dispatch(hideLoading()); ) {
dispatch(hideLoading());
}
} }
} }

View File

@ -1,5 +1,6 @@
import { Middleware, AnyAction } from 'redux'; import type { Middleware, AnyAction } from 'redux';
import { RootState } from '..';
import type { RootState } from '..';
interface AudioSource { interface AudioSource {
src: string; src: string;
@ -27,7 +28,7 @@ const play = (audio: HTMLAudioElement) => {
} }
} }
audio.play(); void audio.play();
}; };
export const soundsMiddleware = (): Middleware< export const soundsMiddleware = (): Middleware<
@ -47,13 +48,15 @@ export const soundsMiddleware = (): Middleware<
]), ]),
}; };
return () => (next) => (action: AnyAction) => { return () =>
const sound = action?.meta?.sound; (next) =>
(action: AnyAction & { meta?: { sound?: string } }) => {
const sound = action?.meta?.sound;
if (sound && soundCache[sound]) { if (sound && soundCache[sound]) {
play(soundCache[sound]); play(soundCache[sound]);
} }
return next(action); return next(action);
}; };
}; };

View File

@ -1,8 +1,9 @@
export function uuid(a?: string): string { export function uuid(a?: string): string {
return a return a
? ( ? (
(a as any as number) ^ (a as unknown as number) ^
((Math.random() * 16) >> ((a as any as number) / 4)) ((Math.random() * 16) >> ((a as unknown as number) / 4))
).toString(16) ).toString(16)
: ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid); : // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid);
} }

View File

@ -3118,7 +3118,7 @@ $ui-header-height: 55px;
&.active { &.active {
transition: none; transition: none;
box-shadow: 0 0 0 2px rgba(lighten($highlight-text-color, 8%), 0.7); box-shadow: 0 0 0 6px rgba(lighten($highlight-text-color, 8%), 0.7);
} }
} }
@ -6447,13 +6447,6 @@ a.status-card.compact:hover {
&--wide { &--wide {
grid-column: span 2; grid-column: span 2;
} }
&.standalone {
.media-gallery__item-gifv-thumbnail {
transform: none;
top: 0;
}
}
} }
.media-gallery__item-thumbnail { .media-gallery__item-thumbnail {
@ -6501,11 +6494,7 @@ a.status-card.compact:hover {
cursor: zoom-in; cursor: zoom-in;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
position: relative;
top: 50%;
transform: translateY(-50%);
width: 100%; width: 100%;
z-index: 1;
} }
.media-gallery__item-thumbnail-label { .media-gallery__item-thumbnail-label {
@ -6604,6 +6593,8 @@ a.status-card.compact:hover {
border-radius: 4px; border-radius: 4px;
box-sizing: border-box; box-sizing: border-box;
color: $white; color: $white;
display: flex;
align-items: center;
&.editable { &.editable {
border-radius: 0; border-radius: 0;
@ -6638,9 +6629,6 @@ a.status-card.compact:hover {
&.inline { &.inline {
video { video {
object-fit: contain; object-fit: contain;
position: relative;
top: 50%;
transform: translateY(-50%);
} }
} }

Some files were not shown because too many files have changed in this diff Show More