mirror of
https://framagit.org/tykayn/mastodon.git
synced 2023-08-25 08:33:12 +02:00
Merge pull request #181 from glitch-soc/upstream-merge-again
Merge upstream, pull in fixes for tootsuite/mastodon#{5409,5417}
This commit is contained in:
commit
7c44ad6355
@ -5,12 +5,14 @@ env:
|
||||
browser: true
|
||||
node: true
|
||||
es6: true
|
||||
jest: true
|
||||
|
||||
parser: babel-eslint
|
||||
|
||||
plugins:
|
||||
- react
|
||||
- jsx-a11y
|
||||
- import
|
||||
|
||||
parserOptions:
|
||||
sourceType: module
|
||||
@ -21,8 +23,14 @@ parserOptions:
|
||||
modules: true
|
||||
spread: true
|
||||
|
||||
rules:
|
||||
settings:
|
||||
import/extensions:
|
||||
- .js
|
||||
import/ignore:
|
||||
- node_modules
|
||||
- \\.(css|scss|json)$
|
||||
|
||||
rules:
|
||||
brace-style: warn
|
||||
comma-dangle:
|
||||
- error
|
||||
@ -125,3 +133,17 @@ rules:
|
||||
jsx-a11y/role-supports-aria-props: off
|
||||
jsx-a11y/scope: warn
|
||||
jsx-a11y/tabindex-no-positive: warn
|
||||
|
||||
import/extensions:
|
||||
- error
|
||||
- always
|
||||
- js: never
|
||||
import/newline-after-import: error
|
||||
import/no-extraneous-dependencies:
|
||||
- error
|
||||
- devDependencies:
|
||||
- "config/webpack/**"
|
||||
- "app/javascript/mastodon/test_setup.js"
|
||||
- "app/javascript/**/__tests__/**"
|
||||
import/no-unresolved: error
|
||||
import/no-webpack-loader-syntax: error
|
||||
|
@ -53,5 +53,5 @@ before_script:
|
||||
|
||||
script:
|
||||
- travis_retry bundle exec parallel_test spec/ --group-by filesize --type rspec
|
||||
- npm test
|
||||
- yarn test
|
||||
- bundle exec i18n-tasks unused
|
||||
|
@ -39,6 +39,7 @@ class Settings::PreferencesController < ApplicationController
|
||||
:setting_boost_modal,
|
||||
:setting_delete_modal,
|
||||
:setting_auto_play_gif,
|
||||
:setting_reduce_motion,
|
||||
:setting_system_font_ui,
|
||||
:setting_noindex,
|
||||
:setting_theme,
|
||||
|
@ -48,7 +48,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
// Mastodon imports //
|
||||
import emojify from 'mastodon/features/emoji/emoji';
|
||||
import emojify from '../../../mastodon/features/emoji/emoji';
|
||||
import IconButton from '../../../mastodon/components/icon_button';
|
||||
import Avatar from '../../../mastodon/components/avatar';
|
||||
|
||||
|
@ -2,10 +2,10 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
// Mastodon imports //
|
||||
import { closeModal } from 'mastodon/actions/modal';
|
||||
import { closeModal } from '../../../mastodon/actions/modal';
|
||||
|
||||
// Our imports //
|
||||
import { changeLocalSetting } from 'glitch/actions/local_settings';
|
||||
import { changeLocalSetting } from '../../../glitch/actions/local_settings';
|
||||
import LocalSettings from '.';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
@ -8,7 +8,7 @@ import LocalSettingsPage from './page';
|
||||
import LocalSettingsNavigation from './navigation';
|
||||
|
||||
// Stylesheet imports
|
||||
import './style';
|
||||
import './style.scss';
|
||||
|
||||
export default class LocalSettings extends React.PureComponent {
|
||||
|
||||
|
@ -7,7 +7,7 @@ import { injectIntl, defineMessages } from 'react-intl';
|
||||
import LocalSettingsNavigationItem from './item';
|
||||
|
||||
// Stylesheet imports
|
||||
import './style';
|
||||
import './style.scss';
|
||||
|
||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
|
||||
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
// Stylesheet imports
|
||||
import './style';
|
||||
import './style.scss';
|
||||
|
||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
|
||||
|
@ -8,7 +8,7 @@ import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||
import LocalSettingsPageItem from './item';
|
||||
|
||||
// Stylesheet imports
|
||||
import './style';
|
||||
import './style.scss';
|
||||
|
||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
|
||||
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
// Stylesheet imports
|
||||
import './style';
|
||||
import './style.scss';
|
||||
|
||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'intl';
|
||||
import 'intl/locale-data/jsonp/en.js';
|
||||
import 'intl/locale-data/jsonp/en';
|
||||
import 'es6-symbol/implement';
|
||||
import includes from 'array-includes';
|
||||
import assign from 'object-assign';
|
||||
|
@ -0,0 +1,35 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
|
||||
<div
|
||||
className="account__avatar"
|
||||
data-avatar-of="@alice"
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"backgroundImage": "url(/animated/alice.gif)",
|
||||
"backgroundSize": "100px 100px",
|
||||
"height": "100px",
|
||||
"width": "100px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`<Avatar /> Still renders a still avatar 1`] = `
|
||||
<div
|
||||
className="account__avatar"
|
||||
data-avatar-of="@alice"
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"backgroundImage": "url(/static/alice.jpg)",
|
||||
"backgroundSize": "100px 100px",
|
||||
"height": "100px",
|
||||
"width": "100px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
@ -0,0 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<AvatarOverlay renders a overlay avatar 1`] = `
|
||||
<div
|
||||
className="account__avatar-overlay"
|
||||
>
|
||||
<div
|
||||
className="account__avatar-overlay-base"
|
||||
data-avatar-of="@alice"
|
||||
style={
|
||||
Object {
|
||||
"backgroundImage": "url(/static/alice.jpg)",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="account__avatar-overlay-overlay"
|
||||
data-avatar-of="@eve@blackhat.lair"
|
||||
style={
|
||||
Object {
|
||||
"backgroundImage": "url(/static/eve.jpg)",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,114 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] = `
|
||||
<button
|
||||
className="button button-secondary"
|
||||
disabled={undefined}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "36px",
|
||||
"lineHeight": "36px",
|
||||
"padding": "0 16px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`<Button /> renders a button element 1`] = `
|
||||
<button
|
||||
className="button"
|
||||
disabled={undefined}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "36px",
|
||||
"lineHeight": "36px",
|
||||
"padding": "0 16px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`<Button /> renders a disabled attribute if props.disabled given 1`] = `
|
||||
<button
|
||||
className="button"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "36px",
|
||||
"lineHeight": "36px",
|
||||
"padding": "0 16px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`<Button /> renders class="button--block" if props.block given 1`] = `
|
||||
<button
|
||||
className="button button--block"
|
||||
disabled={undefined}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "36px",
|
||||
"lineHeight": "36px",
|
||||
"padding": "0 16px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`<Button /> renders the children 1`] = `
|
||||
<button
|
||||
className="button"
|
||||
disabled={undefined}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "36px",
|
||||
"lineHeight": "36px",
|
||||
"padding": "0 16px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<p>
|
||||
children
|
||||
</p>
|
||||
</button>
|
||||
`;
|
||||
|
||||
exports[`<Button /> renders the given text 1`] = `
|
||||
<button
|
||||
className="button"
|
||||
disabled={undefined}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "36px",
|
||||
"lineHeight": "36px",
|
||||
"padding": "0 16px",
|
||||
}
|
||||
}
|
||||
>
|
||||
foo
|
||||
</button>
|
||||
`;
|
||||
|
||||
exports[`<Button /> renders the props.text instead of children 1`] = `
|
||||
<button
|
||||
className="button"
|
||||
disabled={undefined}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "36px",
|
||||
"lineHeight": "36px",
|
||||
"padding": "0 16px",
|
||||
}
|
||||
}
|
||||
>
|
||||
foo
|
||||
</button>
|
||||
`;
|
@ -0,0 +1,23 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<DisplayName /> renders display name + account name 1`] = `
|
||||
<span
|
||||
className="display-name"
|
||||
>
|
||||
<strong
|
||||
className="display-name__html"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<p>Foo</p>",
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
<span
|
||||
className="display-name__account"
|
||||
>
|
||||
@
|
||||
bar@baz
|
||||
</span>
|
||||
</span>
|
||||
`;
|
36
app/javascript/mastodon/components/__tests__/avatar-test.js
Normal file
36
app/javascript/mastodon/components/__tests__/avatar-test.js
Normal file
@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { fromJS } from 'immutable';
|
||||
import Avatar from '../avatar';
|
||||
|
||||
describe('<Avatar />', () => {
|
||||
const account = fromJS({
|
||||
username: 'alice',
|
||||
acct: 'alice',
|
||||
display_name: 'Alice',
|
||||
avatar: '/animated/alice.gif',
|
||||
avatar_static: '/static/alice.jpg',
|
||||
});
|
||||
|
||||
const size = 100;
|
||||
|
||||
describe('Autoplay', () => {
|
||||
it('renders a animated avatar', () => {
|
||||
const component = renderer.create(<Avatar account={account} animate size={size} />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Still', () => {
|
||||
it('renders a still avatar', () => {
|
||||
const component = renderer.create(<Avatar account={account} size={size} />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
// TODO add autoplay test if possible
|
||||
});
|
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { fromJS } from 'immutable';
|
||||
import AvatarOverlay from '../avatar_overlay';
|
||||
|
||||
describe('<AvatarOverlay', () => {
|
||||
const account = fromJS({
|
||||
username: 'alice',
|
||||
acct: 'alice',
|
||||
display_name: 'Alice',
|
||||
avatar: '/animated/alice.gif',
|
||||
avatar_static: '/static/alice.jpg',
|
||||
});
|
||||
|
||||
const friend = fromJS({
|
||||
username: 'eve',
|
||||
acct: 'eve@blackhat.lair',
|
||||
display_name: 'Evelyn',
|
||||
avatar: '/animated/eve.gif',
|
||||
avatar_static: '/static/eve.jpg',
|
||||
});
|
||||
|
||||
it('renders a overlay avatar', () => {
|
||||
const component = renderer.create(<AvatarOverlay account={account} friend={friend} />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
75
app/javascript/mastodon/components/__tests__/button-test.js
Normal file
75
app/javascript/mastodon/components/__tests__/button-test.js
Normal file
@ -0,0 +1,75 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import Button from '../button';
|
||||
|
||||
describe('<Button />', () => {
|
||||
it('renders a button element', () => {
|
||||
const component = renderer.create(<Button />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders the given text', () => {
|
||||
const text = 'foo';
|
||||
const component = renderer.create(<Button text={text} />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('handles click events using the given handler', () => {
|
||||
const handler = jest.fn();
|
||||
const button = shallow(<Button onClick={handler} />);
|
||||
button.find('button').simulate('click');
|
||||
|
||||
expect(handler.mock.calls.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('does not handle click events if props.disabled given', () => {
|
||||
const handler = jest.fn();
|
||||
const button = shallow(<Button onClick={handler} disabled />);
|
||||
button.find('button').simulate('click');
|
||||
|
||||
expect(handler.mock.calls.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('renders a disabled attribute if props.disabled given', () => {
|
||||
const component = renderer.create(<Button disabled />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders the children', () => {
|
||||
const children = <p>children</p>;
|
||||
const component = renderer.create(<Button>{children}</Button>);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders the props.text instead of children', () => {
|
||||
const text = 'foo';
|
||||
const children = <p>children</p>;
|
||||
const component = renderer.create(<Button text={text}>{children}</Button>);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders class="button--block" if props.block given', () => {
|
||||
const component = renderer.create(<Button block />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('adds class "button-secondary" if props.secondary given', () => {
|
||||
const component = renderer.create(<Button secondary />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { fromJS } from 'immutable';
|
||||
import DisplayName from '../display_name';
|
||||
|
||||
describe('<DisplayName />', () => {
|
||||
it('renders display name + account name', () => {
|
||||
const account = fromJS({
|
||||
username: 'bar',
|
||||
acct: 'bar@baz',
|
||||
display_name_html: '<p>Foo</p>',
|
||||
});
|
||||
const component = renderer.create(<DisplayName account={account} />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import Motion from 'react-motion/lib/Motion';
|
||||
import Motion from '../features/ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import IconButton from './icon_button';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Motion from 'react-motion/lib/Motion';
|
||||
import Motion from '../features/ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import detectPassiveEvents from 'detect-passive-events';
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import Motion from 'react-motion/lib/Motion';
|
||||
import Motion from '../features/ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class IconButton extends React.PureComponent {
|
||||
|
||||
@ -56,27 +57,26 @@ export default class IconButton extends React.PureComponent {
|
||||
style.textAlign = 'left';
|
||||
}
|
||||
|
||||
const classes = ['icon-button'];
|
||||
const {
|
||||
active,
|
||||
animate,
|
||||
className,
|
||||
disabled,
|
||||
expanded,
|
||||
icon,
|
||||
inverted,
|
||||
overlay,
|
||||
pressed,
|
||||
tabIndex,
|
||||
title,
|
||||
} = this.props;
|
||||
|
||||
if (this.props.active) {
|
||||
classes.push('active');
|
||||
}
|
||||
|
||||
if (this.props.disabled) {
|
||||
classes.push('disabled');
|
||||
}
|
||||
|
||||
if (this.props.inverted) {
|
||||
classes.push('inverted');
|
||||
}
|
||||
|
||||
if (this.props.overlay) {
|
||||
classes.push('overlayed');
|
||||
}
|
||||
|
||||
if (this.props.className) {
|
||||
classes.push(this.props.className);
|
||||
}
|
||||
const classes = classNames(className, 'icon-button', {
|
||||
active,
|
||||
disabled,
|
||||
inverted,
|
||||
overlayed: overlay,
|
||||
});
|
||||
|
||||
const flipDeg = this.props.flip ? -180 : -360;
|
||||
const rotateDeg = this.props.active ? flipDeg : 0;
|
||||
@ -90,23 +90,23 @@ export default class IconButton extends React.PureComponent {
|
||||
damping: 7,
|
||||
};
|
||||
const motionStyle = {
|
||||
rotate: this.props.animate ? spring(rotateDeg, springOpts) : 0,
|
||||
rotate: animate ? spring(rotateDeg, springOpts) : 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<Motion defaultStyle={motionDefaultStyle} style={motionStyle}>
|
||||
{({ rotate }) =>
|
||||
<button
|
||||
aria-label={this.props.title}
|
||||
aria-pressed={this.props.pressed}
|
||||
aria-expanded={this.props.expanded}
|
||||
title={this.props.title}
|
||||
className={classes.join(' ')}
|
||||
aria-label={title}
|
||||
aria-pressed={pressed}
|
||||
aria-expanded={expanded}
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={this.handleClick}
|
||||
style={style}
|
||||
tabIndex={this.props.tabIndex}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />
|
||||
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
|
||||
{this.props.label}
|
||||
</button>
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ const messages = defineMessages({
|
||||
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
share: { id: 'status.share', defaultMessage: 'Share' },
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
|
||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||
@ -182,7 +183,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||
{shareButton}
|
||||
|
||||
<div className='status__action-bar-dropdown'>
|
||||
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel='More' />
|
||||
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -10,6 +10,7 @@ import { hydrateStore } from '../actions/store';
|
||||
import { connectUserStream } from '../actions/streaming';
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
import { getLocale } from '../locales';
|
||||
|
||||
const { localeData, messages } = getLocale();
|
||||
addLocaleData(localeData);
|
||||
|
||||
|
@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import Motion from 'react-motion/lib/Motion';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import { connect } from 'react-redux';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Motion from 'react-motion/lib/Motion';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import detectPassiveEvents from 'detect-passive-events';
|
||||
import classNames from 'classnames';
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Motion from 'react-motion/lib/Motion';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import Motion from 'react-motion/lib/Motion';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Motion from 'react-motion/lib/Motion';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Motion from 'react-motion/lib/Motion';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
|
||||
export default class Warning extends React.PureComponent {
|
||||
|
@ -1,15 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import AutosuggestStatus from '../components/autosuggest_status';
|
||||
import { makeGetStatus } from '../../../selectors';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
status: getStatus(state, id),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export default connect(makeMapStateToProps)(AutosuggestStatus);
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import { changeComposeSensitivity } from '../../../actions/compose';
|
||||
import Motion from 'react-motion/lib/Motion';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
|
@ -10,7 +10,7 @@ import { changeLocalSetting } from '../../../glitch/actions/local_settings';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import SearchContainer from './containers/search_container';
|
||||
import Motion from 'react-motion/lib/Motion';
|
||||
import Motion from '../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import SearchResultsContainer from './containers/search_results_container';
|
||||
import { changeComposing } from '../../actions/compose';
|
||||
|
@ -0,0 +1,61 @@
|
||||
import emojify from '../emoji';
|
||||
|
||||
describe('emoji', () => {
|
||||
describe('.emojify', () => {
|
||||
it('ignores unknown shortcodes', () => {
|
||||
expect(emojify(':foobarbazfake:')).toEqual(':foobarbazfake:');
|
||||
});
|
||||
|
||||
it('ignores shortcodes inside of tags', () => {
|
||||
expect(emojify('<p data-foo=":smile:"></p>')).toEqual('<p data-foo=":smile:"></p>');
|
||||
});
|
||||
|
||||
it('works with unclosed tags', () => {
|
||||
expect(emojify('hello>')).toEqual('hello>');
|
||||
expect(emojify('<hello')).toEqual('<hello');
|
||||
});
|
||||
|
||||
it('works with unclosed shortcodes', () => {
|
||||
expect(emojify('smile:')).toEqual('smile:');
|
||||
expect(emojify(':smile')).toEqual(':smile');
|
||||
});
|
||||
|
||||
it('does unicode', () => {
|
||||
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
|
||||
'<img draggable="false" class="emojione" alt="👩👩👦👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />');
|
||||
expect(emojify('👨👩👧👧')).toEqual(
|
||||
'<img draggable="false" class="emojione" alt="👨👩👧👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />');
|
||||
expect(emojify('👩👩👦')).toEqual('<img draggable="false" class="emojione" alt="👩👩👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg" />');
|
||||
expect(emojify('\u2757')).toEqual(
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
|
||||
});
|
||||
|
||||
it('does multiple unicode', () => {
|
||||
expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
|
||||
expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
|
||||
expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
|
||||
expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
|
||||
'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> bar');
|
||||
});
|
||||
|
||||
it('ignores unicode inside of tags', () => {
|
||||
expect(emojify('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>')).toEqual('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>');
|
||||
});
|
||||
|
||||
it('does multiple emoji properly (issue 5188)', () => {
|
||||
expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />');
|
||||
expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />');
|
||||
});
|
||||
|
||||
it('does an emoji that has no shortcode', () => {
|
||||
expect(emojify('🕉️')).toEqual('<img draggable="false" class="emojione" alt="🕉️" title="" src="/emoji/1f549.svg" />');
|
||||
});
|
||||
|
||||
it('does an emoji whose filename is irregular', () => {
|
||||
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg" />');
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,130 @@
|
||||
import { pick } from 'lodash';
|
||||
import { emojiIndex } from 'emoji-mart';
|
||||
import { search } from '../emoji_mart_search_light';
|
||||
|
||||
const trimEmojis = emoji => pick(emoji, ['id', 'unified', 'native', 'custom']);
|
||||
|
||||
describe('emoji_index', () => {
|
||||
it('should give same result for emoji_index_light and emoji-mart', () => {
|
||||
const expected = [
|
||||
{
|
||||
id: 'pineapple',
|
||||
unified: '1f34d',
|
||||
native: '🍍',
|
||||
},
|
||||
];
|
||||
expect(search('pineapple').map(trimEmojis)).toEqual(expected);
|
||||
expect(emojiIndex.search('pineapple').map(trimEmojis)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('orders search results correctly', () => {
|
||||
const expected = [
|
||||
{
|
||||
id: 'apple',
|
||||
unified: '1f34e',
|
||||
native: '🍎',
|
||||
},
|
||||
{
|
||||
id: 'pineapple',
|
||||
unified: '1f34d',
|
||||
native: '🍍',
|
||||
},
|
||||
{
|
||||
id: 'green_apple',
|
||||
unified: '1f34f',
|
||||
native: '🍏',
|
||||
},
|
||||
{
|
||||
id: 'iphone',
|
||||
unified: '1f4f1',
|
||||
native: '📱',
|
||||
},
|
||||
];
|
||||
expect(search('apple').map(trimEmojis)).toEqual(expected);
|
||||
expect(emojiIndex.search('apple').map(trimEmojis)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('handles custom emoji', () => {
|
||||
const custom = [
|
||||
{
|
||||
id: 'mastodon',
|
||||
name: 'mastodon',
|
||||
short_names: ['mastodon'],
|
||||
text: '',
|
||||
emoticons: [],
|
||||
keywords: ['mastodon'],
|
||||
imageUrl: 'http://example.com',
|
||||
custom: true,
|
||||
},
|
||||
];
|
||||
search('', { custom });
|
||||
emojiIndex.search('', { custom });
|
||||
const expected = [
|
||||
{
|
||||
id: 'mastodon',
|
||||
custom: true,
|
||||
},
|
||||
];
|
||||
expect(search('masto').map(trimEmojis)).toEqual(expected);
|
||||
expect(emojiIndex.search('masto').map(trimEmojis)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should filter only emojis we care about, exclude pineapple', () => {
|
||||
const emojisToShowFilter = unified => unified !== '1F34D';
|
||||
expect(search('apple', { emojisToShowFilter }).map((obj) => obj.id))
|
||||
.not.toContain('pineapple');
|
||||
expect(emojiIndex.search('apple', { emojisToShowFilter }).map((obj) => obj.id))
|
||||
.not.toContain('pineapple');
|
||||
});
|
||||
|
||||
it('can include/exclude categories', () => {
|
||||
expect(search('flag', { include: ['people'] })).toEqual([]);
|
||||
expect(emojiIndex.search('flag', { include: ['people'] })).toEqual([]);
|
||||
});
|
||||
|
||||
it('does an emoji whose unified name is irregular', () => {
|
||||
const expected = [
|
||||
{
|
||||
'id': 'water_polo',
|
||||
'unified': '1f93d',
|
||||
'native': '🤽',
|
||||
},
|
||||
{
|
||||
'id': 'man-playing-water-polo',
|
||||
'unified': '1f93d-200d-2642-fe0f',
|
||||
'native': '🤽♂️',
|
||||
},
|
||||
{
|
||||
'id': 'woman-playing-water-polo',
|
||||
'unified': '1f93d-200d-2640-fe0f',
|
||||
'native': '🤽♀️',
|
||||
},
|
||||
];
|
||||
expect(search('polo').map(trimEmojis)).toEqual(expected);
|
||||
expect(emojiIndex.search('polo').map(trimEmojis)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('can search for thinking_face', () => {
|
||||
const expected = [
|
||||
{
|
||||
id: 'thinking_face',
|
||||
unified: '1f914',
|
||||
native: '🤔',
|
||||
},
|
||||
];
|
||||
expect(search('thinking_fac').map(trimEmojis)).toEqual(expected);
|
||||
expect(emojiIndex.search('thinking_fac').map(trimEmojis)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('can search for woman-facepalming', () => {
|
||||
const expected = [
|
||||
{
|
||||
id: 'woman-facepalming',
|
||||
unified: '1f926-200d-2640-fe0f',
|
||||
native: '🤦♀️',
|
||||
},
|
||||
];
|
||||
expect(search('woman-facep').map(trimEmojis)).toEqual(expected);
|
||||
expect(emojiIndex.search('woman-facep').map(trimEmojis)).toEqual(expected);
|
||||
});
|
||||
});
|
@ -9,7 +9,8 @@ const { unicodeToFilename } = require('./unicode_to_filename');
|
||||
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
|
||||
const emojiMap = require('./emoji_map.json');
|
||||
const { emojiIndex } = require('emoji-mart');
|
||||
const emojiMartData = require('emoji-mart/dist/data').default;
|
||||
const { default: emojiMartData } = require('emoji-mart/dist/data');
|
||||
|
||||
const excluded = ['®', '©', '™'];
|
||||
const skins = ['🏻', '🏼', '🏽', '🏾', '🏿'];
|
||||
const shortcodeMap = {};
|
||||
|
@ -48,6 +48,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||
let media = '';
|
||||
let mediaIcon = null;
|
||||
let applicationLink = '';
|
||||
let reblogLink = '';
|
||||
let reblogIcon = 'retweet';
|
||||
|
||||
if (status.get('media_attachments').size > 0) {
|
||||
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
|
||||
@ -85,6 +87,23 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||
applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>;
|
||||
}
|
||||
|
||||
if (status.get('visibility') === 'direct') {
|
||||
reblogIcon = 'envelope';
|
||||
} else if (status.get('visibility') === 'private') {
|
||||
reblogIcon = 'lock';
|
||||
}
|
||||
|
||||
if (status.get('visibility') === 'private') {
|
||||
reblogLink = <i className={`fa fa-${reblogIcon}`} />;
|
||||
} else {
|
||||
reblogLink = (<Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
||||
<i className={`fa fa-${reblogIcon}`} />
|
||||
<span className='detailed-status__reblogs'>
|
||||
<FormattedNumber value={status.get('reblogs_count')} />
|
||||
</span>
|
||||
</Link>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='detailed-status'>
|
||||
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
|
||||
@ -101,12 +120,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||
<div className='detailed-status__meta'>
|
||||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
|
||||
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
||||
</a>{applicationLink} · <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
||||
<i className='fa fa-retweet' />
|
||||
<span className='detailed-status__reblogs'>
|
||||
<FormattedNumber value={status.get('reblogs_count')} />
|
||||
</span>
|
||||
</Link> · <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
|
||||
</a>{applicationLink} · {reblogLink} · <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
|
||||
<i className='fa fa-star' />
|
||||
<span className='detailed-status__favorites'>
|
||||
<FormattedNumber value={status.get('favourites_count')} />
|
||||
|
@ -1,14 +1,18 @@
|
||||
import { expect } from 'chai';
|
||||
import { mount } from 'enzyme';
|
||||
import sinon from 'sinon';
|
||||
import React from 'react';
|
||||
import Column from '../../../../../../app/javascript/mastodon/features/ui/components/column';
|
||||
import ColumnHeader from '../../../../../../app/javascript/mastodon/features/ui/components/column_header';
|
||||
import { mount } from 'enzyme';
|
||||
import Column from '../column';
|
||||
import ColumnHeader from '../column_header';
|
||||
|
||||
describe('<Column />', () => {
|
||||
describe('<ColumnHeader /> click handler', () => {
|
||||
const originalRaf = global.requestAnimationFrame;
|
||||
|
||||
beforeEach(() => {
|
||||
global.requestAnimationFrame = sinon.spy();
|
||||
global.requestAnimationFrame = jest.fn();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
global.requestAnimationFrame = originalRaf;
|
||||
});
|
||||
|
||||
it('runs the scroll animation if the column contains scrollable content', () => {
|
||||
@ -18,13 +22,13 @@ describe('<Column />', () => {
|
||||
</Column>
|
||||
);
|
||||
wrapper.find(ColumnHeader).simulate('click');
|
||||
expect(global.requestAnimationFrame.called).to.equal(true);
|
||||
expect(global.requestAnimationFrame.mock.calls.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('does not try to scroll if there is no scrollable content', () => {
|
||||
const wrapper = mount(<Column heading='notifications' />);
|
||||
wrapper.find(ColumnHeader).simulate('click');
|
||||
expect(global.requestAnimationFrame.called).to.equal(false);
|
||||
expect(global.requestAnimationFrame.mock.calls.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Motion from 'react-motion/lib/Motion';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
|
34
app/javascript/mastodon/features/ui/util/optional_motion.js
Normal file
34
app/javascript/mastodon/features/ui/util/optional_motion.js
Normal file
@ -0,0 +1,34 @@
|
||||
// Like react-motion's Motion, but checks to see if the user prefers
|
||||
// reduced motion and uses a cross-fade in those cases.
|
||||
|
||||
import Motion from 'react-motion/lib/Motion';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const stylesToKeep = ['opacity', 'backgroundOpacity'];
|
||||
|
||||
const extractValue = (value) => {
|
||||
// This is either an object with a "val" property or it's a number
|
||||
return (typeof value === 'object' && value && 'val' in value) ? value.val : value;
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const reduceMotion = state.getIn(['meta', 'reduce_motion']);
|
||||
|
||||
if (reduceMotion) {
|
||||
const { style, defaultStyle } = ownProps;
|
||||
|
||||
Object.keys(style).forEach(key => {
|
||||
if (stylesToKeep.includes(key)) {
|
||||
return;
|
||||
}
|
||||
// If it's setting an x or height or scale or some other value, we need
|
||||
// to preserve the end-state value without actually animating it
|
||||
style[key] = defaultStyle[key] = extractValue(style[key]);
|
||||
});
|
||||
|
||||
return { style, defaultStyle };
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(Motion);
|
@ -184,6 +184,7 @@
|
||||
"status.load_more": "Carrega més",
|
||||
"status.media_hidden": "Multimèdia amagat",
|
||||
"status.mention": "Esmentar @{name}",
|
||||
"status.more": "Més",
|
||||
"status.mute_conversation": "Silenciar conversació",
|
||||
"status.open": "Ampliar aquest estat",
|
||||
"status.pin": "Fixat en el perfil",
|
||||
|
@ -179,6 +179,7 @@
|
||||
"status.load_more": "Load more",
|
||||
"status.media_hidden": "Media hidden",
|
||||
"status.mention": "Mention @{name}",
|
||||
"status.more": "More",
|
||||
"status.mute_conversation": "Mute conversation",
|
||||
"status.open": "Expand this status",
|
||||
"status.pin": "Pin on profile",
|
||||
|
@ -179,6 +179,7 @@
|
||||
"status.load_more": "Cargar más",
|
||||
"status.media_hidden": "Contenido multimedia oculto",
|
||||
"status.mention": "Mencionar",
|
||||
"status.more": "Más",
|
||||
"status.mute_conversation": "Silenciar conversación",
|
||||
"status.open": "Expandir estado",
|
||||
"status.pin": "Fijar",
|
||||
|
@ -179,6 +179,7 @@
|
||||
"status.load_more": "Charger plus",
|
||||
"status.media_hidden": "Média caché",
|
||||
"status.mention": "Mentionner",
|
||||
"status.more": "Plus",
|
||||
"status.mute_conversation": "Masquer la conversation",
|
||||
"status.open": "Déplier ce statut",
|
||||
"status.pin": "Épingler sur le profil",
|
||||
|
@ -18,7 +18,7 @@
|
||||
"account.unblock_domain": "{domain} 숨김 해제",
|
||||
"account.unfollow": "팔로우 해제",
|
||||
"account.unmute": "뮤트 해제",
|
||||
"account.view_full_profile": "View full profile",
|
||||
"account.view_full_profile": "전체 프로필 보기",
|
||||
"boost_modal.combo": "다음부터 {combo}를 누르면 이 과정을 건너뛸 수 있습니다.",
|
||||
"bundle_column_error.body": "Something went wrong while loading this component.",
|
||||
"bundle_column_error.retry": "Try again",
|
||||
@ -33,7 +33,7 @@
|
||||
"column.home": "홈",
|
||||
"column.mutes": "뮤트 중인 사용자",
|
||||
"column.notifications": "알림",
|
||||
"column.pins": "고정된 Toot",
|
||||
"column.pins": "고정된 툿",
|
||||
"column.public": "연합 타임라인",
|
||||
"column_back_button.label": "돌아가기",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
@ -47,7 +47,7 @@
|
||||
"compose_form.lock_disclaimer": "이 계정은 {locked}로 설정 되어 있지 않습니다. 누구나 이 계정을 팔로우 할 수 있으며, 팔로워 공개의 포스팅을 볼 수 있습니다.",
|
||||
"compose_form.lock_disclaimer.lock": "비공개",
|
||||
"compose_form.placeholder": "지금 무엇을 하고 있나요?",
|
||||
"compose_form.publish": "Toot",
|
||||
"compose_form.publish": "툿",
|
||||
"compose_form.publish_loud": "{publish}!",
|
||||
"compose_form.sensitive": "이 미디어를 민감한 미디어로 취급",
|
||||
"compose_form.spoiler": "텍스트 숨기기",
|
||||
@ -63,8 +63,8 @@
|
||||
"confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?",
|
||||
"confirmations.unfollow.confirm": "Unfollow",
|
||||
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||
"embed.instructions": "Embed this status on your website by copying the code below.",
|
||||
"embed.preview": "Here is what it will look like:",
|
||||
"embed.instructions": "아래의 코드를 복사하여 대화를 원하는 곳으로 퍼가세요.",
|
||||
"embed.preview": "다음과 같이 표시됩니다:",
|
||||
"emoji_button.activity": "활동",
|
||||
"emoji_button.custom": "Custom",
|
||||
"emoji_button.flags": "국기",
|
||||
@ -82,7 +82,6 @@
|
||||
"empty_column.community": "로컬 타임라인에 아무 것도 없습니다. 아무거나 적어 보세요!",
|
||||
"empty_column.hashtag": "이 해시태그는 아직 사용되지 않았습니다.",
|
||||
"empty_column.home": "아직 아무도 팔로우 하고 있지 않습니다. {public}를 보러 가거나, 검색하여 다른 사용자를 찾아 보세요.",
|
||||
"empty_column.home.inactivity": "홈 피드에 아무 것도 없습니다. 한동안 활동하지 않은 경우 곧 원래대로 돌아올 것입니다.",
|
||||
"empty_column.home.public_timeline": "연합 타임라인",
|
||||
"empty_column.notifications": "아직 알림이 없습니다. 다른 사람과 대화를 시작해 보세요!",
|
||||
"empty_column.public": "여기엔 아직 아무 것도 없습니다! 공개적으로 무언가 포스팅하거나, 다른 인스턴스 유저를 팔로우 해서 가득 채워보세요!",
|
||||
@ -113,7 +112,7 @@
|
||||
"navigation_bar.info": "이 인스턴스에 대해서",
|
||||
"navigation_bar.logout": "로그아웃",
|
||||
"navigation_bar.mutes": "뮤트 중인 사용자",
|
||||
"navigation_bar.pins": "고정된 Toot",
|
||||
"navigation_bar.pins": "고정된 툿",
|
||||
"navigation_bar.preferences": "사용자 설정",
|
||||
"navigation_bar.public_timeline": "연합 타임라인",
|
||||
"notification.favourite": "{name}님이 즐겨찾기 했습니다",
|
||||
@ -159,29 +158,34 @@
|
||||
"privacy.public.long": "공개 타임라인에 표시",
|
||||
"privacy.public.short": "공개",
|
||||
"privacy.unlisted.long": "공개 타임라인에 표시하지 않음",
|
||||
"privacy.unlisted.short": "Unlisted",
|
||||
"privacy.unlisted.short": "타임라인에 비표시",
|
||||
"relative_time.days": "{number}일 전",
|
||||
"relative_time.hours": "{number}시간 전",
|
||||
"relative_time.just_now": "방금",
|
||||
"relative_time.minutes": "{number}분 전",
|
||||
"relative_time.seconds": "{number}초 전",
|
||||
"reply_indicator.cancel": "취소",
|
||||
"report.placeholder": "코멘트",
|
||||
"report.submit": "신고하기",
|
||||
"report.target": "문제가 된 사용자",
|
||||
"search.placeholder": "검색",
|
||||
"search_popout.search_format": "Advanced search format",
|
||||
"search_popout.tips.hashtag": "hashtag",
|
||||
"search_popout.tips.status": "status",
|
||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
||||
"search_popout.tips.user": "user",
|
||||
"search_popout.search_format": "고급 검색 방법",
|
||||
"search_popout.tips.hashtag": "해시태그",
|
||||
"search_popout.tips.status": "툿",
|
||||
"search_popout.tips.text": "단순한 텍스트 검색은 관계된 프로필 이름, 유저 이름 그리고 해시태그를 표시합니다",
|
||||
"search_popout.tips.user": "유저",
|
||||
"search_results.total": "{count, number}건의 결과",
|
||||
"standalone.public_title": "A look inside...",
|
||||
"status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다",
|
||||
"status.delete": "삭제",
|
||||
"status.embed": "Embed",
|
||||
"status.embed": "공유하기",
|
||||
"status.favourite": "즐겨찾기",
|
||||
"status.load_more": "더 보기",
|
||||
"status.media_hidden": "미디어 숨겨짐",
|
||||
"status.mention": "답장",
|
||||
"status.mute_conversation": "이 대화를 뮤트",
|
||||
"status.open": "상세 정보 표시",
|
||||
"status.pin": "Pin on profile",
|
||||
"status.pin": "고정",
|
||||
"status.reblog": "부스트",
|
||||
"status.reblogged_by": "{name}님이 부스트 했습니다",
|
||||
"status.reply": "답장",
|
||||
@ -193,7 +197,7 @@
|
||||
"status.show_less": "숨기기",
|
||||
"status.show_more": "더 보기",
|
||||
"status.unmute_conversation": "이 대화의 뮤트 해제하기",
|
||||
"status.unpin": "Unpin from profile",
|
||||
"status.unpin": "고정 해제",
|
||||
"tabs_bar.compose": "포스트",
|
||||
"tabs_bar.federated_timeline": "연합",
|
||||
"tabs_bar.home": "홈",
|
||||
@ -212,5 +216,9 @@
|
||||
"video.mute": "Mute sound",
|
||||
"video.pause": "Pause",
|
||||
"video.play": "Play",
|
||||
"video.unmute": "Unmute sound"
|
||||
"video.unmute": "Unmute sound",
|
||||
"video_player.expand": "Expand video",
|
||||
"video_player.toggle_sound": "Toggle sound",
|
||||
"video_player.toggle_visible": "Toggle visibility",
|
||||
"video_player.video_error": "Video could not be played"
|
||||
}
|
||||
|
@ -179,6 +179,7 @@
|
||||
"status.load_more": "Cargar mai",
|
||||
"status.media_hidden": "Mèdia rescondut",
|
||||
"status.mention": "Mencionar",
|
||||
"status.more": "Mai",
|
||||
"status.mute_conversation": "Rescondre la conversacion",
|
||||
"status.open": "Desplegar aqueste estatut",
|
||||
"status.pin": "Penjar al perfil",
|
||||
|
@ -179,6 +179,7 @@
|
||||
"status.load_more": "Załaduj więcej",
|
||||
"status.media_hidden": "Zawartość multimedialna ukryta",
|
||||
"status.mention": "Wspomnij o @{name}",
|
||||
"status.more": "Więcej",
|
||||
"status.mute_conversation": "Wycisz konwersację",
|
||||
"status.open": "Rozszerz ten wpis",
|
||||
"status.pin": "Przypnij do profilu",
|
||||
|
@ -1,6 +1,5 @@
|
||||
import * as OfflinePluginRuntime from 'offline-plugin/runtime';
|
||||
import * as WebPushSubscription from './web_push_subscription';
|
||||
import Mastodon from 'mastodon/containers/mastodon';
|
||||
import Mastodon from './containers/mastodon';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import ready from './ready';
|
||||
@ -25,7 +24,7 @@ function main() {
|
||||
ReactDOM.render(<Mastodon {...props} />, mountNode);
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// avoid offline in dev mode because it's harder to debug
|
||||
OfflinePluginRuntime.install();
|
||||
require('offline-plugin/runtime').install();
|
||||
WebPushSubscription.register();
|
||||
}
|
||||
perf.stop('main()');
|
||||
|
@ -31,10 +31,10 @@ const initialTimeline = ImmutableMap({
|
||||
});
|
||||
|
||||
const normalizeTimeline = (state, timeline, statuses, next) => {
|
||||
const ids = ImmutableList(statuses.map(status => status.get('id')));
|
||||
const oldIds = state.getIn([timeline, 'items'], ImmutableList());
|
||||
const ids = ImmutableList(statuses.map(status => status.get('id'))).filter(newId => !oldIds.includes(newId));
|
||||
const wasLoaded = state.getIn([timeline, 'loaded']);
|
||||
const hadNext = state.getIn([timeline, 'next']);
|
||||
const oldIds = state.getIn([timeline, 'items'], ImmutableList());
|
||||
|
||||
return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
|
||||
mMap.set('loaded', true);
|
||||
@ -45,8 +45,8 @@ const normalizeTimeline = (state, timeline, statuses, next) => {
|
||||
};
|
||||
|
||||
const appendNormalizedTimeline = (state, timeline, statuses, next) => {
|
||||
const ids = ImmutableList(statuses.map(status => status.get('id')));
|
||||
const oldIds = state.getIn([timeline, 'items'], ImmutableList());
|
||||
const ids = ImmutableList(statuses.map(status => status.get('id'))).filter(newId => !oldIds.includes(newId));
|
||||
|
||||
return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
|
||||
mMap.set('isLoading', false);
|
||||
|
5
app/javascript/mastodon/test_setup.js
Normal file
5
app/javascript/mastodon/test_setup.js
Normal file
@ -0,0 +1,5 @@
|
||||
import { configure } from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
|
||||
const adapter = new Adapter();
|
||||
configure({ adapter });
|
@ -2,7 +2,8 @@ import loadPolyfills from '../mastodon/load_polyfills';
|
||||
|
||||
// import default stylesheet with variables
|
||||
require('font-awesome/css/font-awesome.css');
|
||||
import 'styles/application';
|
||||
|
||||
import '../styles/application.scss';
|
||||
|
||||
require.context('../images/', true);
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { start } from 'rails-ujs';
|
||||
import 'font-awesome/css/font-awesome.css';
|
||||
|
||||
// import common styling
|
||||
require('../styles/common.scss');
|
||||
|
||||
require.context('../images/', true);
|
||||
|
||||
start();
|
||||
|
@ -1,2 +1,2 @@
|
||||
import 'packs/application';
|
||||
import 'themes/spin/style';
|
||||
import '../../packs/application';
|
||||
import './style.scss';
|
||||
|
@ -100,11 +100,24 @@ class FeedManager
|
||||
end
|
||||
|
||||
def populate_feed(account)
|
||||
prepopulate_limit = FeedManager::MAX_ITEMS / 4
|
||||
statuses = Status.as_home_timeline(account).order(account_id: :desc).limit(prepopulate_limit)
|
||||
statuses.reverse_each do |status|
|
||||
next if filter_from_home?(status, account)
|
||||
add_to_feed(:home, account, status)
|
||||
added = 0
|
||||
limit = FeedManager::MAX_ITEMS / 2
|
||||
max_id = nil
|
||||
|
||||
loop do
|
||||
statuses = Status.as_home_timeline(account)
|
||||
.paginate_by_max_id(limit, max_id)
|
||||
|
||||
break if statuses.empty?
|
||||
|
||||
statuses.each do |status|
|
||||
next if filter_from_home?(status, account)
|
||||
added += 1 if add_to_feed(:home, account, status)
|
||||
end
|
||||
|
||||
break unless added.zero?
|
||||
|
||||
max_id = statuses.last.id
|
||||
end
|
||||
end
|
||||
|
||||
@ -167,13 +180,19 @@ class FeedManager
|
||||
# either action is appropriate.
|
||||
def add_to_feed(timeline_type, account, status)
|
||||
timeline_key = key(timeline_type, account.id)
|
||||
reblog_key = key(timeline_type, account.id, 'reblogs')
|
||||
reblog_key = key(timeline_type, account.id, 'reblogs')
|
||||
|
||||
if status.reblog?
|
||||
reblog_set_key = key(timeline_type, account.id, "reblogs:#{status.reblog_of_id}")
|
||||
|
||||
# If the original status or a reblog of it is within
|
||||
# REBLOG_FALLOFF statuses from the top, do not re-insert it into
|
||||
# the feed
|
||||
rank = redis.zrevrank(timeline_key, status.reblog_of_id)
|
||||
|
||||
redis.sadd(reblog_set_key, status.reblog_of_id) unless rank.nil?
|
||||
redis.sadd(reblog_set_key, status.id)
|
||||
|
||||
return false if !rank.nil? && rank < FeedManager::REBLOG_FALLOFF
|
||||
|
||||
reblog_rank = redis.zrevrank(reblog_key, status.reblog_of_id)
|
||||
@ -194,7 +213,7 @@ class FeedManager
|
||||
# do so if appropriate.
|
||||
def remove_from_feed(timeline_type, account, status)
|
||||
timeline_key = key(timeline_type, account.id)
|
||||
reblog_key = key(timeline_type, account.id, 'reblogs')
|
||||
reblog_key = key(timeline_type, account.id, 'reblogs')
|
||||
|
||||
if status.reblog?
|
||||
# 1. If the reblogging status is not in the feed, stop.
|
||||
@ -204,12 +223,21 @@ class FeedManager
|
||||
# 2. Remove the reblogged status from the `:reblogs` zset.
|
||||
redis.zrem(reblog_key, status.reblog_of_id)
|
||||
|
||||
# 3. Add the reblogged status to the feed using the reblogging
|
||||
# status' ID as its score, and the reblogged status' ID as its
|
||||
# value.
|
||||
redis.zadd(timeline_key, status.id, status.reblog_of_id)
|
||||
# 3. Remove reblog from set of this status's reblogs, and
|
||||
# re-insert another reblog or original into the feed if
|
||||
# one remains in the set
|
||||
reblog_set_key = key(timeline_type, account.id, "reblogs:#{status.reblog_of_id}")
|
||||
|
||||
redis.srem(reblog_set_key, status.id)
|
||||
other_reblog = redis.srandmember(reblog_set_key)
|
||||
|
||||
redis.zadd(timeline_key, other_reblog, other_reblog) if other_reblog
|
||||
|
||||
# 4. Remove the reblogging status from the feed (as normal)
|
||||
# (outside conditional)
|
||||
else
|
||||
# If the original is getting deleted, no use for reblog references
|
||||
redis.del(key(timeline_type, account.id, "reblogs:#{status.id}"))
|
||||
end
|
||||
|
||||
redis.zrem(timeline_key, status.id)
|
||||
|
@ -23,6 +23,7 @@ class UserSettingsDecorator
|
||||
user.settings['boost_modal'] = boost_modal_preference if change?('setting_boost_modal')
|
||||
user.settings['delete_modal'] = delete_modal_preference if change?('setting_delete_modal')
|
||||
user.settings['auto_play_gif'] = auto_play_gif_preference if change?('setting_auto_play_gif')
|
||||
user.settings['reduce_motion'] = reduce_motion_preference if change?('setting_reduce_motion')
|
||||
user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui')
|
||||
user.settings['noindex'] = noindex_preference if change?('setting_noindex')
|
||||
user.settings['theme'] = theme_preference if change?('setting_theme')
|
||||
@ -64,6 +65,10 @@ class UserSettingsDecorator
|
||||
boolean_cast_setting 'setting_auto_play_gif'
|
||||
end
|
||||
|
||||
def reduce_motion_preference
|
||||
boolean_cast_setting 'setting_reduce_motion'
|
||||
end
|
||||
|
||||
def noindex_preference
|
||||
boolean_cast_setting 'setting_noindex'
|
||||
end
|
||||
|
@ -102,6 +102,10 @@ class User < ApplicationRecord
|
||||
settings.auto_play_gif
|
||||
end
|
||||
|
||||
def setting_reduce_motion
|
||||
settings.reduce_motion
|
||||
end
|
||||
|
||||
def setting_system_font_ui
|
||||
settings.system_font_ui
|
||||
end
|
||||
|
@ -25,6 +25,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||
store[:boost_modal] = object.current_account.user.setting_boost_modal
|
||||
store[:delete_modal] = object.current_account.user.setting_delete_modal
|
||||
store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif
|
||||
store[:reduce_motion] = object.current_account.user.setting_reduce_motion
|
||||
end
|
||||
|
||||
store
|
||||
|
@ -35,6 +35,7 @@
|
||||
|
||||
.fields-group
|
||||
= f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label
|
||||
= f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label
|
||||
= f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label
|
||||
|
||||
.actions
|
||||
|
@ -19,15 +19,14 @@
|
||||
%a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
|
||||
.e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }<
|
||||
= Formatter.instance.format(status, custom_emojify: true)
|
||||
|
||||
- if !status.media_attachments.empty?
|
||||
- if status.media_attachments.first.video?
|
||||
- video = status.media_attachments.first
|
||||
%div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive?, width: 670, height: 380) }}><
|
||||
%div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive?, width: 670, height: 380) }}<
|
||||
- else
|
||||
%div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}><
|
||||
%div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}<
|
||||
- elsif status.preview_cards.first
|
||||
%div{ data: { component: 'Card', props: Oj.dump('maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_cards.first, serializer: REST::PreviewCardSerializer).as_json) }}><
|
||||
%div{ data: { component: 'Card', props: Oj.dump('maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_cards.first, serializer: REST::PreviewCardSerializer).as_json) }}<
|
||||
|
||||
.detailed-status__meta
|
||||
%data.dt-published{ value: status.created_at.to_time.iso8601 }
|
||||
@ -40,9 +39,16 @@
|
||||
- else
|
||||
= link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener'
|
||||
·
|
||||
%span<
|
||||
= fa_icon('retweet')
|
||||
%span= status.reblogs_count
|
||||
- if status.direct_visibility?
|
||||
%span<
|
||||
= fa_icon('envelope')
|
||||
- elsif status.private_visibility?
|
||||
%span<
|
||||
= fa_icon('lock')
|
||||
- else
|
||||
%span<
|
||||
= fa_icon('retweet')
|
||||
%span= status.reblogs_count
|
||||
·
|
||||
%span<
|
||||
= fa_icon('star')
|
||||
|
@ -6,8 +6,9 @@ class Scheduler::FeedCleanupScheduler
|
||||
|
||||
def perform
|
||||
redis.pipelined do
|
||||
inactive_users.pluck(:account_id).each do |account_id|
|
||||
inactive_users.each do |account_id|
|
||||
redis.del(FeedManager.instance.key(:home, account_id))
|
||||
redis.del(FeedManager.instance.key(:home, account_id, 'reblogs'))
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -15,7 +16,7 @@ class Scheduler::FeedCleanupScheduler
|
||||
private
|
||||
|
||||
def inactive_users
|
||||
User.confirmed.inactive
|
||||
@inactive_users ||= User.confirmed.inactive.pluck(:account_id)
|
||||
end
|
||||
|
||||
def redis
|
||||
|
@ -120,9 +120,9 @@ oc:
|
||||
destroyed_msg: Nòta de moderacion ben suprimida !
|
||||
|
||||
custom_emojis:
|
||||
copied_msg: Còpia locale de l’emoji ben creada
|
||||
copied_msg: Còpia locala de l’emoji ben creada
|
||||
copy: Copiar
|
||||
copy_failed_msg: Fracàs de la còpia locale de l’emoji
|
||||
copy_failed_msg: Fracàs de la còpia locala de l’emoji
|
||||
created_msg: Emoji ben creat !
|
||||
delete: Suprimir
|
||||
destroyed_msg: Emojo ben suprimit !
|
||||
|
@ -44,6 +44,7 @@ en:
|
||||
setting_default_sensitive: Always mark media as sensitive
|
||||
setting_delete_modal: Show confirmation dialog before deleting a toot
|
||||
setting_noindex: Opt-out of search engine indexing
|
||||
setting_reduce_motion: Reduce motion in animations
|
||||
setting_system_font_ui: Use system's default font
|
||||
setting_theme: Site theme
|
||||
setting_unfollow_modal: Show confirmation dialog before unfollowing someone
|
||||
|
@ -38,6 +38,7 @@ oc:
|
||||
otp_attempt: Còdi Two-factor
|
||||
password: Senhal
|
||||
setting_auto_play_gif: Lectura automatica dels GIFS animats
|
||||
setting_reduce_motion: Reduire la velocitat de las animacions
|
||||
setting_boost_modal: Afichar una fenèstra de confirmacion abans de partejar un estatut
|
||||
setting_default_privacy: Confidencialitat de las publicacions
|
||||
setting_default_sensitive: Totjorn marcar los mèdias coma sensibles
|
||||
|
@ -48,6 +48,7 @@ pl:
|
||||
setting_default_sensitive: Zawsze oznaczaj zawartość multimedialną jako wrażliwą
|
||||
setting_delete_modal: Pytaj o potwierdzenie przed usunięciem wpisu
|
||||
setting_noindex: Nie indeksuj mojego profilu w wyszukiwarkach internetowych
|
||||
setting_reduce_motion: Ogranicz ruch w animacjach
|
||||
setting_system_font_ui: Używaj domyślnej czcionki systemu
|
||||
setting_theme: Motyw strony
|
||||
setting_unfollow_modal: Pytaj o potwierdzenie przed cofnięciem śledzenia
|
||||
|
@ -22,6 +22,7 @@ defaults: &defaults
|
||||
boost_modal: false
|
||||
delete_modal: true
|
||||
auto_play_gif: false
|
||||
reduce_motion: false
|
||||
system_font_ui: false
|
||||
noindex: false
|
||||
theme: 'default'
|
||||
|
@ -26,7 +26,7 @@ class StatusIdsToTimestampIds < ActiveRecord::Migration[5.1]
|
||||
SELECT setval('statuses_id_seq', (SELECT MAX(id) FROM statuses));
|
||||
ALTER TABLE statuses
|
||||
ALTER COLUMN id
|
||||
SET DEFAULT nextval('statuses_id_seq');"
|
||||
SET DEFAULT nextval('statuses_id_seq');
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
17
jest.config.js
Normal file
17
jest.config.js
Normal file
@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
projects: [
|
||||
'<rootDir>/app/javascript/mastodon',
|
||||
],
|
||||
testPathIgnorePatterns: [
|
||||
'<rootDir>/node_modules/',
|
||||
'<rootDir>/vendor/',
|
||||
'<rootDir>/config/',
|
||||
'<rootDir>/log/',
|
||||
'<rootDir>/public/',
|
||||
'<rootDir>/tmp/',
|
||||
],
|
||||
setupFiles: [
|
||||
'raf/polyfill',
|
||||
],
|
||||
setupTestFrameworkScriptFile: '<rootDir>/app/javascript/mastodon/test_setup.js',
|
||||
};
|
@ -21,7 +21,7 @@ module Mastodon
|
||||
end
|
||||
|
||||
def flags
|
||||
'rc2'
|
||||
'rc3'
|
||||
end
|
||||
|
||||
def to_a
|
||||
|
30
package.json
30
package.json
@ -7,9 +7,9 @@
|
||||
"build:production": "cross-env RAILS_ENV=production ./bin/webpack",
|
||||
"manage:translations": "node ./config/webpack/translationRunner.js",
|
||||
"start": "node ./streaming/index.js",
|
||||
"test": "npm run test:lint && npm run test:mocha",
|
||||
"test": "npm run test:lint && npm run test:jest",
|
||||
"test:lint": "eslint -c .eslintrc.yml --ext=js app/javascript/ config/webpack/ spec/javascript/ streaming/",
|
||||
"test:mocha": "cross-env NODE_ENV=test mocha --require ./spec/javascript/setup.js --compilers js:babel-register ./spec/javascript/components/**/*.test.js",
|
||||
"test:jest": "cross-env NODE_ENV=test jest",
|
||||
"postinstall": "npm rebuild node-sass"
|
||||
},
|
||||
"repository": {
|
||||
@ -58,6 +58,7 @@
|
||||
"immutable": "^3.8.1",
|
||||
"intersection-observer": "^0.4.0",
|
||||
"intl": "^1.2.5",
|
||||
"intl-messageformat": "^2.1.0",
|
||||
"intl-relativeformat": "^2.0.0",
|
||||
"is-nan": "^1.2.1",
|
||||
"js-yaml": "^3.9.0",
|
||||
@ -119,22 +120,37 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^7.2.3",
|
||||
"chai": "^4.1.0",
|
||||
"chai-enzyme": "^0.8.0",
|
||||
"enzyme": "^3.0.0",
|
||||
"enzyme-adapter-react-16": "^1.0.0",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-plugin-import": "^2.7.0",
|
||||
"eslint-plugin-jsx-a11y": "^4.0.0",
|
||||
"eslint-plugin-react": "^6.10.3",
|
||||
"jsdom": "^11.1.0",
|
||||
"mocha": "^3.4.1",
|
||||
"jest": "^21.2.1",
|
||||
"raf": "^3.4.0",
|
||||
"react-intl-translations-manager": "^5.0.0",
|
||||
"react-test-renderer": "^16.0.0",
|
||||
"sinon": "^2.3.7",
|
||||
"webpack-dev-server": "^2.6.1",
|
||||
"yargs": "^8.0.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "*"
|
||||
},
|
||||
"jest": {
|
||||
"projects": [
|
||||
"<rootDir>/app/javascript/mastodon"
|
||||
],
|
||||
"testPathIgnorePatterns": [
|
||||
"<rootDir>/node_modules/",
|
||||
"<rootDir>/vendor/",
|
||||
"<rootDir>/config/",
|
||||
"<rootDir>/log/",
|
||||
"<rootDir>/public/",
|
||||
"<rootDir>/tmp/"
|
||||
],
|
||||
"setupFiles": [
|
||||
"raf/polyfill"
|
||||
],
|
||||
"setupTestFrameworkScriptFile": "<rootDir>/app/javascript/mastodon/test_setup.js"
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
---
|
||||
env:
|
||||
mocha: true
|
@ -1,44 +0,0 @@
|
||||
import React from 'react';
|
||||
import Avatar from '../../../app/javascript/mastodon/components/avatar';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { render } from 'enzyme';
|
||||
import { fromJS } from 'immutable';
|
||||
|
||||
describe('<Avatar />', () => {
|
||||
const account = fromJS({
|
||||
username: 'alice',
|
||||
acct: 'alice',
|
||||
display_name: 'Alice',
|
||||
avatar: '/animated/alice.gif',
|
||||
avatar_static: '/static/alice.jpg',
|
||||
});
|
||||
|
||||
const size = 100;
|
||||
const animated = render(<Avatar account={account} animate size={size} />);
|
||||
const still = render(<Avatar account={account} size={size} />);
|
||||
|
||||
// Autoplay
|
||||
xit('renders a div element with the given src as background', () => {
|
||||
expect(animated.find('div')).to.have.style('background-image', `url(${account.get('avatar')})`);
|
||||
});
|
||||
|
||||
xit('renders a div element of the given size', () => {
|
||||
['width', 'height'].map((attr) => {
|
||||
expect(animated.find('div')).to.have.style(attr, `${size}px`);
|
||||
});
|
||||
});
|
||||
|
||||
// Still
|
||||
xit('renders a div element with the given static src as background if not autoplay', () => {
|
||||
expect(still.find('div')).to.have.style('background-image', `url(${account.get('avatar_static')})`);
|
||||
});
|
||||
|
||||
xit('renders a div element of the given size if not autoplay', () => {
|
||||
['width', 'height'].map((attr) => {
|
||||
expect(still.find('div')).to.have.style(attr, `${size}px`);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO add autoplay test if possible
|
||||
});
|
@ -1,36 +0,0 @@
|
||||
import React from 'react';
|
||||
import AvatarOverlay from '../../../app/javascript/mastodon/components/avatar_overlay';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { render } from 'enzyme';
|
||||
import { fromJS } from 'immutable';
|
||||
|
||||
describe('<Avatar />', () => {
|
||||
const account = fromJS({
|
||||
username: 'alice',
|
||||
acct: 'alice',
|
||||
display_name: 'Alice',
|
||||
avatar: '/animated/alice.gif',
|
||||
avatar_static: '/static/alice.jpg',
|
||||
});
|
||||
|
||||
const friend = fromJS({
|
||||
username: 'eve',
|
||||
acct: 'eve@blackhat.lair',
|
||||
display_name: 'Evelyn',
|
||||
avatar: '/animated/eve.gif',
|
||||
avatar_static: '/static/eve.jpg',
|
||||
});
|
||||
|
||||
const overlay = render(<AvatarOverlay account={account} friend={friend} />);
|
||||
|
||||
xit('renders account static src as base of overlay avatar', () => {
|
||||
expect(overlay.find('.account__avatar-overlay-base'))
|
||||
.to.have.style('background-image', `url(${account.get('avatar_static')})`);
|
||||
});
|
||||
|
||||
xit('renders friend static src as overlay of overlay avatar', () => {
|
||||
expect(overlay.find('.account__avatar-overlay-overlay'))
|
||||
.to.have.style('background-image', `url(${friend.get('avatar_static')})`);
|
||||
});
|
||||
});
|
@ -1,72 +0,0 @@
|
||||
import React from 'react';
|
||||
import Button from '../../../app/javascript/mastodon/components/button';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { shallow } from 'enzyme';
|
||||
import sinon from 'sinon';
|
||||
|
||||
describe('<Button />', () => {
|
||||
xit('renders a button element', () => {
|
||||
const wrapper = shallow(<Button />);
|
||||
expect(wrapper).to.match('button');
|
||||
});
|
||||
|
||||
xit('renders the given text', () => {
|
||||
const text = 'foo';
|
||||
const wrapper = shallow(<Button text={text} />);
|
||||
expect(wrapper.find('button')).to.have.text(text);
|
||||
});
|
||||
|
||||
it('handles click events using the given handler', () => {
|
||||
const handler = sinon.spy();
|
||||
const wrapper = shallow(<Button onClick={handler} />);
|
||||
wrapper.find('button').simulate('click');
|
||||
expect(handler.calledOnce).to.equal(true);
|
||||
});
|
||||
|
||||
it('does not handle click events if props.disabled given', () => {
|
||||
const handler = sinon.spy();
|
||||
const wrapper = shallow(<Button onClick={handler} disabled />);
|
||||
wrapper.find('button').simulate('click');
|
||||
expect(handler.called).to.equal(false);
|
||||
});
|
||||
|
||||
xit('renders a disabled attribute if props.disabled given', () => {
|
||||
const wrapper = shallow(<Button disabled />);
|
||||
expect(wrapper.find('button')).to.be.disabled();
|
||||
});
|
||||
|
||||
xit('renders the children', () => {
|
||||
const children = <p>children</p>;
|
||||
const wrapper = shallow(<Button>{children}</Button>);
|
||||
expect(wrapper.find('button')).to.contain(children);
|
||||
});
|
||||
|
||||
xit('renders the props.text instead of children', () => {
|
||||
const text = 'foo';
|
||||
const children = <p>children</p>;
|
||||
const wrapper = shallow(<Button text={text}>{children}</Button>);
|
||||
expect(wrapper.find('button')).to.have.text(text);
|
||||
expect(wrapper.find('button')).to.not.contain(children);
|
||||
});
|
||||
|
||||
xit('renders style="display: block; width: 100%;" if props.block given', () => {
|
||||
const wrapper = shallow(<Button block />);
|
||||
expect(wrapper.find('button')).to.have.className('button--block');
|
||||
});
|
||||
|
||||
xit('renders style="display: inline-block; width: auto;" by default', () => {
|
||||
const wrapper = shallow(<Button />);
|
||||
expect(wrapper.find('button')).to.not.have.className('button--block');
|
||||
});
|
||||
|
||||
xit('adds class "button-secondary" if props.secondary given', () => {
|
||||
const wrapper = shallow(<Button secondary />);
|
||||
expect(wrapper.find('button')).to.have.className('button-secondary');
|
||||
});
|
||||
|
||||
xit('does not add class "button-secondary" by default', () => {
|
||||
const wrapper = shallow(<Button />);
|
||||
expect(wrapper.find('button')).to.not.have.className('button-secondary');
|
||||
});
|
||||
});
|
@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
import DisplayName from '../../../app/javascript/mastodon/components/display_name';
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { render } from 'enzyme';
|
||||
import { fromJS } from 'immutable';
|
||||
|
||||
describe('<DisplayName />', () => {
|
||||
xit('renders display name + account name', () => {
|
||||
const account = fromJS({
|
||||
username: 'bar',
|
||||
acct: 'bar@baz',
|
||||
display_name_html: '<p>Foo</p>',
|
||||
});
|
||||
const wrapper = render(<DisplayName account={account} />);
|
||||
expect(wrapper).to.have.text('Foo @bar@baz');
|
||||
});
|
||||
});
|
@ -1,111 +0,0 @@
|
||||
import { expect } from 'chai';
|
||||
import { search } from '../../../app/javascript/mastodon/features/emoji/emoji_mart_search_light';
|
||||
import { emojiIndex } from 'emoji-mart';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
const trimEmojis = emoji => pick(emoji, ['id', 'unified', 'native', 'custom']);
|
||||
|
||||
// hack to fix https://github.com/chaijs/type-detect/issues/98
|
||||
// see: https://github.com/chaijs/type-detect/issues/98#issuecomment-325010785
|
||||
import jsdom from 'jsdom';
|
||||
global.window = new jsdom.JSDOM().window;
|
||||
global.document = window.document;
|
||||
global.HTMLElement = window.HTMLElement;
|
||||
|
||||
describe('emoji_index', () => {
|
||||
|
||||
it('should give same result for emoji_index_light and emoji-mart', () => {
|
||||
let expected = [{
|
||||
id: 'pineapple',
|
||||
unified: '1f34d',
|
||||
native: '🍍',
|
||||
}];
|
||||
expect(search('pineapple').map(trimEmojis)).to.deep.equal(expected);
|
||||
expect(emojiIndex.search('pineapple').map(trimEmojis)).to.deep.equal(expected);
|
||||
});
|
||||
|
||||
it('orders search results correctly', () => {
|
||||
let expected = [{
|
||||
id: 'apple',
|
||||
unified: '1f34e',
|
||||
native: '🍎',
|
||||
}, {
|
||||
id: 'pineapple',
|
||||
unified: '1f34d',
|
||||
native: '🍍',
|
||||
}, {
|
||||
id: 'green_apple',
|
||||
unified: '1f34f',
|
||||
native: '🍏',
|
||||
}, {
|
||||
id: 'iphone',
|
||||
unified: '1f4f1',
|
||||
native: '📱',
|
||||
}];
|
||||
expect(search('apple').map(trimEmojis)).to.deep.equal(expected);
|
||||
expect(emojiIndex.search('apple').map(trimEmojis)).to.deep.equal(expected);
|
||||
});
|
||||
|
||||
it('handles custom emoji', () => {
|
||||
let custom = [{
|
||||
id: 'mastodon',
|
||||
name: 'mastodon',
|
||||
short_names: ['mastodon'],
|
||||
text: '',
|
||||
emoticons: [],
|
||||
keywords: ['mastodon'],
|
||||
imageUrl: 'http://example.com',
|
||||
custom: true,
|
||||
}];
|
||||
search('', { custom });
|
||||
emojiIndex.search('', { custom });
|
||||
let expected = [ { id: 'mastodon', custom: true } ];
|
||||
expect(search('masto').map(trimEmojis)).to.deep.equal(expected);
|
||||
expect(emojiIndex.search('masto').map(trimEmojis)).to.deep.equal(expected);
|
||||
});
|
||||
|
||||
it('should filter only emojis we care about, exclude pineapple', () => {
|
||||
let emojisToShowFilter = (unified) => unified !== '1F34D';
|
||||
expect(search('apple', { emojisToShowFilter }).map((obj) => obj.id))
|
||||
.not.to.contain('pineapple');
|
||||
expect(emojiIndex.search('apple', { emojisToShowFilter }).map((obj) => obj.id))
|
||||
.not.to.contain('pineapple');
|
||||
});
|
||||
|
||||
it('can include/exclude categories', () => {
|
||||
expect(search('flag', { include: ['people'] }))
|
||||
.to.deep.equal([]);
|
||||
expect(emojiIndex.search('flag', { include: ['people'] }))
|
||||
.to.deep.equal([]);
|
||||
});
|
||||
|
||||
it('does an emoji whose unified name is irregular', () => {
|
||||
let expected = [{
|
||||
'id': 'water_polo',
|
||||
'unified': '1f93d',
|
||||
'native': '🤽',
|
||||
}, {
|
||||
'id': 'man-playing-water-polo',
|
||||
'unified': '1f93d-200d-2642-fe0f',
|
||||
'native': '🤽♂️',
|
||||
}, {
|
||||
'id': 'woman-playing-water-polo',
|
||||
'unified': '1f93d-200d-2640-fe0f',
|
||||
'native': '🤽♀️',
|
||||
}];
|
||||
expect(search('polo').map(trimEmojis)).to.deep.equal(expected);
|
||||
expect(emojiIndex.search('polo').map(trimEmojis)).to.deep.equal(expected);
|
||||
});
|
||||
|
||||
it('can search for thinking_face', () => {
|
||||
let expected = [ { id: 'thinking_face', unified: '1f914', native: '🤔' } ];
|
||||
expect(search('thinking_fac').map(trimEmojis)).to.deep.equal(expected);
|
||||
expect(emojiIndex.search('thinking_fac').map(trimEmojis)).to.deep.equal(expected);
|
||||
});
|
||||
|
||||
it('can search for woman-facepalming', () => {
|
||||
let expected = [ { id: 'woman-facepalming', unified: '1f926-200d-2640-fe0f', native: '🤦♀️' } ];
|
||||
expect(search('woman-facep').map(trimEmojis)).to.deep.equal(expected);
|
||||
expect(emojiIndex.search('woman-facep').map(trimEmojis)).deep.equal(expected);
|
||||
});
|
||||
});
|
@ -1,61 +0,0 @@
|
||||
import { expect } from 'chai';
|
||||
import emojify from '../../../app/javascript/mastodon/features/emoji/emoji';
|
||||
|
||||
describe('emojify', () => {
|
||||
it('ignores unknown shortcodes', () => {
|
||||
expect(emojify(':foobarbazfake:')).to.equal(':foobarbazfake:');
|
||||
});
|
||||
|
||||
it('ignores shortcodes inside of tags', () => {
|
||||
expect(emojify('<p data-foo=":smile:"></p>')).to.equal('<p data-foo=":smile:"></p>');
|
||||
});
|
||||
|
||||
it('works with unclosed tags', () => {
|
||||
expect(emojify('hello>')).to.equal('hello>');
|
||||
expect(emojify('<hello')).to.equal('<hello');
|
||||
});
|
||||
|
||||
it('works with unclosed shortcodes', () => {
|
||||
expect(emojify('smile:')).to.equal('smile:');
|
||||
expect(emojify(':smile')).to.equal(':smile');
|
||||
});
|
||||
|
||||
it('does unicode', () => {
|
||||
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).to.equal(
|
||||
'<img draggable="false" class="emojione" alt="👩👩👦👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />');
|
||||
expect(emojify('👨👩👧👧')).to.equal(
|
||||
'<img draggable="false" class="emojione" alt="👨👩👧👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />');
|
||||
expect(emojify('👩👩👦')).to.equal('<img draggable="false" class="emojione" alt="👩👩👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg" />');
|
||||
expect(emojify('\u2757')).to.equal(
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
|
||||
});
|
||||
|
||||
it('does multiple unicode', () => {
|
||||
expect(emojify('\u2757 #\uFE0F\u20E3')).to.equal(
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
|
||||
expect(emojify('\u2757#\uFE0F\u20E3')).to.equal(
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
|
||||
expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).to.equal(
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
|
||||
expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).to.equal(
|
||||
'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> bar');
|
||||
});
|
||||
|
||||
it('ignores unicode inside of tags', () => {
|
||||
expect(emojify('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>')).to.equal('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>');
|
||||
});
|
||||
|
||||
it('does multiple emoji properly (issue 5188)', () => {
|
||||
expect(emojify('👌🌈💕')).to.equal('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />');
|
||||
expect(emojify('👌 🌈 💕')).to.equal('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />');
|
||||
});
|
||||
|
||||
it('does an emoji that has no shortcode', () => {
|
||||
expect(emojify('🕉️')).to.equal('<img draggable="false" class="emojione" alt="🕉️" title="" src="/emoji/1f549.svg" />');
|
||||
});
|
||||
|
||||
it('does an emoji whose filename is irregular', () => {
|
||||
expect(emojify('↙️')).to.equal('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg" />');
|
||||
});
|
||||
|
||||
});
|
@ -1,15 +0,0 @@
|
||||
import { JSDOM } from 'jsdom';
|
||||
import Enzyme from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
|
||||
const { window } = new JSDOM('', {
|
||||
userAgent: 'node.js',
|
||||
});
|
||||
|
||||
Object.keys(window).forEach(property => {
|
||||
if (typeof global[property] === 'undefined') {
|
||||
global[property] = window[property];
|
||||
}
|
||||
});
|
@ -231,33 +231,66 @@ RSpec.describe FeedManager do
|
||||
end
|
||||
|
||||
describe '#unpush' do
|
||||
it 'leaves a reblogged status when deleting the reblog' do
|
||||
account = Fabricate(:account)
|
||||
reblogged = Fabricate(:status)
|
||||
status = Fabricate(:status, reblog: reblogged)
|
||||
let(:receiver) { Fabricate(:account) }
|
||||
|
||||
FeedManager.instance.push('type', account, status)
|
||||
it 'leaves a reblogged status if original was on feed' do
|
||||
reblogged = Fabricate(:status)
|
||||
status = Fabricate(:status, reblog: reblogged)
|
||||
|
||||
FeedManager.instance.push('type', receiver, reblogged)
|
||||
FeedManager::REBLOG_FALLOFF.times { FeedManager.instance.push('type', receiver, Fabricate(:status)) }
|
||||
FeedManager.instance.push('type', receiver, status)
|
||||
|
||||
# The reblogging status should show up under normal conditions.
|
||||
expect(Redis.current.zrange("feed:type:#{account.id}", 0, -1)).to eq [status.id.to_s]
|
||||
expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to include(status.id.to_s)
|
||||
|
||||
FeedManager.instance.unpush('type', account, status)
|
||||
FeedManager.instance.unpush('type', receiver, status)
|
||||
|
||||
# Because we couldn't tell if the status showed up any other way,
|
||||
# we had to stick the reblogged status in by itself.
|
||||
expect(Redis.current.zrange("feed:type:#{account.id}", 0, -1)).to eq [reblogged.id.to_s]
|
||||
# Restore original status
|
||||
expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to_not include(status.id.to_s)
|
||||
expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to include(reblogged.id.to_s)
|
||||
end
|
||||
|
||||
it 'removes a reblogged status if it was only reblogged once' do
|
||||
reblogged = Fabricate(:status)
|
||||
status = Fabricate(:status, reblog: reblogged)
|
||||
|
||||
FeedManager.instance.push('type', receiver, status)
|
||||
|
||||
# The reblogging status should show up under normal conditions.
|
||||
expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to eq [status.id.to_s]
|
||||
|
||||
FeedManager.instance.unpush('type', receiver, status)
|
||||
|
||||
expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to be_empty
|
||||
end
|
||||
|
||||
it 'leaves a reblogged status if another reblog was in feed' do
|
||||
reblogged = Fabricate(:status)
|
||||
status = Fabricate(:status, reblog: reblogged)
|
||||
another_status = Fabricate(:status, reblog: reblogged)
|
||||
|
||||
FeedManager.instance.push('type', receiver, status)
|
||||
FeedManager.instance.push('type', receiver, another_status)
|
||||
|
||||
# The reblogging status should show up under normal conditions.
|
||||
expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to eq [status.id.to_s]
|
||||
|
||||
FeedManager.instance.unpush('type', receiver, status)
|
||||
|
||||
expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to eq [another_status.id.to_s]
|
||||
end
|
||||
|
||||
it 'sends push updates' do
|
||||
account = Fabricate(:account)
|
||||
status = Fabricate(:status)
|
||||
FeedManager.instance.push('type', account, status)
|
||||
status = Fabricate(:status)
|
||||
|
||||
FeedManager.instance.push('type', receiver, status)
|
||||
|
||||
allow(Redis.current).to receive_messages(publish: nil)
|
||||
FeedManager.instance.unpush('type', account, status)
|
||||
FeedManager.instance.unpush('type', receiver, status)
|
||||
|
||||
deletion = Oj.dump(event: :delete, payload: status.id.to_s)
|
||||
expect(Redis.current).to have_received(:publish).with("timeline:#{account.id}", deletion)
|
||||
expect(Redis.current).to have_received(:publish).with("timeline:#{receiver.id}", deletion)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user