[build] upgrade eslint to 9.37.0 (#88)

Co-authored-by: tobi <tobi.smethurst@protonmail.com>
Reviewed-on: https://codeberg.org/superseriousbusiness/masto-fe-standalone/pulls/88
Co-authored-by: Zoë Bijl <moiety@noreply.codeberg.org>
Co-committed-by: Zoë Bijl <moiety@noreply.codeberg.org>
This commit is contained in:
Zoë Bijl
2025-10-12 13:42:02 +02:00
committed by tobi
parent 75d7a62693
commit 1ff70886a1
975 changed files with 22196 additions and 21964 deletions

View File

@@ -1,12 +1,12 @@
import renderer from 'react-test-renderer';
import renderer from "react-test-renderer";
import AutosuggestEmoji from '../autosuggest_emoji';
import AutosuggestEmoji from "../autosuggest_emoji";
describe('<AutosuggestEmoji />', () => {
it('renders native emoji', () => {
describe("<AutosuggestEmoji />", () => {
it("renders native emoji", () => {
const emoji = {
native: '💙',
colons: ':foobar:',
native: "💙",
colons: ":foobar:",
};
const component = renderer.create(<AutosuggestEmoji emoji={emoji} />);
const tree = component.toJSON();
@@ -14,12 +14,12 @@ describe('<AutosuggestEmoji />', () => {
expect(tree).toMatchSnapshot();
});
it('renders emoji with custom url', () => {
it("renders emoji with custom url", () => {
const emoji = {
custom: true,
imageUrl: 'http://example.com/emoji.png',
native: 'foobar',
colons: ':foobar:',
imageUrl: "http://example.com/emoji.png",
native: "foobar",
colons: ":foobar:",
};
const component = renderer.create(<AutosuggestEmoji emoji={emoji} />);
const tree = component.toJSON();

View File

@@ -1,22 +1,22 @@
import { fromJS } from 'immutable';
import { fromJS } from "immutable";
import renderer from 'react-test-renderer';
import renderer from "react-test-renderer";
import { Avatar } from '../avatar';
import { Avatar } from "../avatar";
describe('<Avatar />', () => {
describe("<Avatar />", () => {
const account = fromJS({
username: 'alice',
acct: 'alice',
display_name: 'Alice',
avatar: '/animated/alice.gif',
avatar_static: '/static/alice.jpg',
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', () => {
describe("Autoplay", () => {
it("renders a animated avatar", () => {
const component = renderer.create(<Avatar account={account} animate size={size} />);
const tree = component.toJSON();
@@ -24,8 +24,8 @@ describe('<Avatar />', () => {
});
});
describe('Still', () => {
it('renders a still avatar', () => {
describe("Still", () => {
it("renders a still avatar", () => {
const component = renderer.create(<Avatar account={account} size={size} />);
const tree = component.toJSON();

View File

@@ -1,27 +1,27 @@
import { fromJS } from 'immutable';
import { fromJS } from "immutable";
import renderer from 'react-test-renderer';
import renderer from "react-test-renderer";
import { AvatarOverlay } from '../avatar_overlay';
import { AvatarOverlay } from "../avatar_overlay";
describe('<AvatarOverlay', () => {
describe("<AvatarOverlay", () => {
const account = fromJS({
username: 'alice',
acct: 'alice',
display_name: 'Alice',
avatar: '/animated/alice.gif',
avatar_static: '/static/alice.jpg',
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',
username: "eve",
acct: "eve@blackhat.lair",
display_name: "Evelyn",
avatar: "/animated/eve.gif",
avatar_static: "/static/eve.jpg",
});
it('renders a overlay avatar', () => {
it("renders a overlay avatar", () => {
const component = renderer.create(<AvatarOverlay account={account} friend={friend} />);
const tree = component.toJSON();

View File

@@ -1,48 +1,48 @@
import { render, fireEvent, screen } from '@testing-library/react';
import renderer from 'react-test-renderer';
import { render, fireEvent, screen } from "@testing-library/react";
import renderer from "react-test-renderer";
import Button from '../button';
import Button from "../button";
describe('<Button />', () => {
it('renders a button element', () => {
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';
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', () => {
it("handles click events using the given handler", () => {
const handler = jest.fn();
render(<Button onClick={handler}>button</Button>);
fireEvent.click(screen.getByText('button'));
fireEvent.click(screen.getByText("button"));
expect(handler.mock.calls.length).toEqual(1);
expect(handler.mock.calls).toHaveLength(1);
});
it('does not handle click events if props.disabled given', () => {
it("does not handle click events if props.disabled given", () => {
const handler = jest.fn();
render(<Button onClick={handler} disabled>button</Button>);
fireEvent.click(screen.getByText('button'));
fireEvent.click(screen.getByText("button"));
expect(handler.mock.calls.length).toEqual(0);
expect(handler.mock.calls).toHaveLength(0);
});
it('renders a disabled attribute if props.disabled given', () => {
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', () => {
it("renders the children", () => {
const children = <p>children</p>;
const component = renderer.create(<Button>{children}</Button>);
const tree = component.toJSON();
@@ -50,8 +50,8 @@ describe('<Button />', () => {
expect(tree).toMatchSnapshot();
});
it('renders the props.text instead of children', () => {
const text = 'foo';
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();
@@ -59,14 +59,14 @@ describe('<Button />', () => {
expect(tree).toMatchSnapshot();
});
it('renders class="button--block" if props.block given', () => {
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', () => {
it("adds class \"button-secondary\" if props.secondary given", () => {
const component = renderer.create(<Button secondary />);
const tree = component.toJSON();

View File

@@ -1,15 +1,15 @@
import { fromJS } from 'immutable';
import { fromJS } from "immutable";
import renderer from 'react-test-renderer';
import renderer from "react-test-renderer";
import { DisplayName } from '../display_name';
import { DisplayName } from "../display_name";
describe('<DisplayName />', () => {
it('renders display name + account name', () => {
describe("<DisplayName />", () => {
it("renders display name + account name", () => {
const account = fromJS({
username: 'bar',
acct: 'bar@baz',
display_name_html: '<p>Foo</p>',
username: "bar",
acct: "bar@baz",
display_name_html: "<p>Foo</p>",
});
const component = renderer.create(<DisplayName account={account} />);
const tree = component.toJSON();

View File

@@ -1,7 +1,7 @@
import { fromJS } from 'immutable';
import { fromJS } from "immutable";
import type { StatusLike } from '../hashtag_bar';
import { computeHashtagBarForStatus } from '../hashtag_bar';
import { type StatusLike } from "../hashtag_bar";
import { computeHashtagBarForStatus } from "../hashtag_bar";
function createStatus(
content: string,
@@ -12,43 +12,43 @@ function createStatus(
return fromJS({
tags: hashtags.map((name) => ({ name })),
contentHtml: content,
media_attachments: hasMedia ? ['fakeMedia'] : [],
media_attachments: hasMedia ? ["fakeMedia"] : [],
spoiler_text: spoilerText,
}) as unknown as StatusLike; // need to force the type here, as it is not properly defined
}
describe('computeHashtagBarForStatus', () => {
it('does nothing when there are no tags', () => {
const status = createStatus('<p>Simple text</p>', []);
describe("computeHashtagBarForStatus", () => {
it("does nothing when there are no tags", () => {
const status = createStatus("<p>Simple text</p>", []);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Simple text</p>"`,
"\"<p>Simple text</p>\"",
);
});
it('displays out of band hashtags in the bar', () => {
it("displays out of band hashtags in the bar", () => {
const status = createStatus(
'<p>Simple text <a href="test">#hashtag</a></p>',
['hashtag', 'test'],
"<p>Simple text <a href=\"test\">#hashtag</a></p>",
["hashtag", "test"],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual(['test']);
expect(hashtagsInBar).toEqual(["test"]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Simple text <a href="test">#hashtag</a></p>"`,
"\"<p>Simple text <a href=\"test\">#hashtag</a></p>\"",
);
});
it('does not truncate the contents when the last child is a text node', () => {
it("does not truncate the contents when the last child is a text node", () => {
const status = createStatus(
'this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text',
['test'],
"this is a #<a class=\"zrl\" href=\"https://example.com/search?tag=test\">test</a>. Some more text",
["test"],
);
const { hashtagsInBar, statusContentProps } =
@@ -56,29 +56,29 @@ describe('computeHashtagBarForStatus', () => {
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text"`,
"\"this is a #<a class=\"zrl\" href=\"https://example.com/search?tag=test\">test</a>. Some more text\"",
);
});
it('extract tags from the last line', () => {
it("extract tags from the last line", () => {
const status = createStatus(
'<p>Simple text</p><p><a href="test">#hashtag</a></p>',
['hashtag'],
"<p>Simple text</p><p><a href=\"test\">#hashtag</a></p>",
["hashtag"],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual(['hashtag']);
expect(hashtagsInBar).toEqual(["hashtag"]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Simple text</p>"`,
"\"<p>Simple text</p>\"",
);
});
it('does not include tags from content', () => {
it("does not include tags from content", () => {
const status = createStatus(
'<p>Simple text with a <a href="test">#hashtag</a></p><p><a href="test">#hashtag</a></p>',
['hashtag'],
"<p>Simple text with a <a href=\"test\">#hashtag</a></p><p><a href=\"test\">#hashtag</a></p>",
["hashtag"],
);
const { hashtagsInBar, statusContentProps } =
@@ -86,14 +86,14 @@ describe('computeHashtagBarForStatus', () => {
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Simple text with a <a href="test">#hashtag</a></p>"`,
"\"<p>Simple text with a <a href=\"test\">#hashtag</a></p>\"",
);
});
it('works with one line status and hashtags', () => {
it("works with one line status and hashtags", () => {
const status = createStatus(
'<p><a href="test">#test</a>. And another <a href="test">#hashtag</a></p>',
['hashtag', 'test'],
"<p><a href=\"test\">#test</a>. And another <a href=\"test\">#hashtag</a></p>",
["hashtag", "test"],
);
const { hashtagsInBar, statusContentProps } =
@@ -101,44 +101,44 @@ describe('computeHashtagBarForStatus', () => {
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p><a href="test">#test</a>. And another <a href="test">#hashtag</a></p>"`,
"\"<p><a href=\"test\">#test</a>. And another <a href=\"test\">#hashtag</a></p>\"",
);
});
it('de-duplicate accentuated characters with case differences', () => {
it("de-duplicate accentuated characters with case differences", () => {
const status = createStatus(
'<p>Text</p><p><a href="test">#éaa</a> <a href="test">#Éaa</a></p>',
['éaa'],
"<p>Text</p><p><a href=\"test\">#éaa</a> <a href=\"test\">#Éaa</a></p>",
["éaa"],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual(['Éaa']);
expect(hashtagsInBar).toEqual(["Éaa"]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Text</p>"`,
"\"<p>Text</p>\"",
);
});
it('handles server-side normalized tags with accentuated characters', () => {
it("handles server-side normalized tags with accentuated characters", () => {
const status = createStatus(
'<p>Text</p><p><a href="test">#éaa</a> <a href="test">#Éaa</a></p>',
['eaa'], // The server may normalize the hashtags in the `tags` attribute
"<p>Text</p><p><a href=\"test\">#éaa</a> <a href=\"test\">#Éaa</a></p>",
["eaa"], // The server may normalize the hashtags in the `tags` attribute
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual(['Éaa']);
expect(hashtagsInBar).toEqual(["Éaa"]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Text</p>"`,
"\"<p>Text</p>\"",
);
});
it('does not display in bar a hashtag in content with a case difference', () => {
it("does not display in bar a hashtag in content with a case difference", () => {
const status = createStatus(
'<p>Text <a href="test">#Éaa</a></p><p><a href="test">#éaa</a></p>',
['éaa'],
"<p>Text <a href=\"test\">#Éaa</a></p><p><a href=\"test\">#éaa</a></p>",
["éaa"],
);
const { hashtagsInBar, statusContentProps } =
@@ -146,14 +146,14 @@ describe('computeHashtagBarForStatus', () => {
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Text <a href="test">#Éaa</a></p>"`,
"\"<p>Text <a href=\"test\">#Éaa</a></p>\"",
);
});
it('does not modify a status with a line of hashtags only', () => {
it("does not modify a status with a line of hashtags only", () => {
const status = createStatus(
'<p><a href="test">#test</a> <a href="test">#hashtag</a></p>',
['test', 'hashtag'],
"<p><a href=\"test\">#test</a> <a href=\"test\">#hashtag</a></p>",
["test", "hashtag"],
);
const { hashtagsInBar, statusContentProps } =
@@ -161,14 +161,14 @@ describe('computeHashtagBarForStatus', () => {
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p><a href="test">#test</a> <a href="test">#hashtag</a></p>"`,
"\"<p><a href=\"test\">#test</a> <a href=\"test\">#hashtag</a></p>\"",
);
});
it('puts the hashtags in the bar if a status content has hashtags in the only line and has a media', () => {
it("puts the hashtags in the bar if a status content has hashtags in the only line and has a media", () => {
const status = createStatus(
'<p>This is my content! <a href="test">#hashtag</a></p>',
['hashtag'],
"<p>This is my content! <a href=\"test\">#hashtag</a></p>",
["hashtag"],
true,
);
@@ -177,30 +177,30 @@ describe('computeHashtagBarForStatus', () => {
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>This is my content! <a href="test">#hashtag</a></p>"`,
"\"<p>This is my content! <a href=\"test\">#hashtag</a></p>\"",
);
});
it('puts the hashtags in the bar if a status content is only hashtags and has a media', () => {
it("puts the hashtags in the bar if a status content is only hashtags and has a media", () => {
const status = createStatus(
'<p><a href="test">#test</a> <a href="test">#hashtag</a></p>',
['test', 'hashtag'],
"<p><a href=\"test\">#test</a> <a href=\"test\">#hashtag</a></p>",
["test", "hashtag"],
true,
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual(['test', 'hashtag']);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(`""`);
expect(hashtagsInBar).toEqual(["test", "hashtag"]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot("\"\"");
});
it('does not use the hashtag bar if the status content is only hashtags, has a CW and a media', () => {
it("does not use the hashtag bar if the status content is only hashtags, has a CW and a media", () => {
const status = createStatus(
'<p><a href="test">#test</a> <a href="test">#hashtag</a></p>',
['test', 'hashtag'],
"<p><a href=\"test\">#test</a> <a href=\"test\">#hashtag</a></p>",
["test", "hashtag"],
true,
'My CW text',
"My CW text",
);
const { hashtagsInBar, statusContentProps } =
@@ -208,7 +208,7 @@ describe('computeHashtagBarForStatus', () => {
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p><a href="test">#test</a> <a href="test">#hashtag</a></p>"`,
"\"<p><a href=\"test\">#test</a> <a href=\"test\">#hashtag</a></p>\"",
);
});
});

View File

@@ -1,36 +1,36 @@
import PropTypes from 'prop-types';
import PropTypes from "prop-types";
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from "react-intl";
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import classNames from "classnames";
import { Link } from "react-router-dom";
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from "react-immutable-proptypes";
import ImmutablePureComponent from "react-immutable-pure-component";
import { EmptyAccount } from 'mastodon/components/empty_account';
import { ShortNumber } from 'mastodon/components/short_number';
import { VerifiedBadge } from 'mastodon/components/verified_badge';
import { EmptyAccount } from "mastodon/components/empty_account";
import { ShortNumber } from "mastodon/components/short_number";
import { VerifiedBadge } from "mastodon/components/verified_badge";
import { me } from '../initial_state';
import { me } from "../initial_state";
import { Avatar } from './avatar';
import Button from './button';
import { FollowersCounter } from './counters';
import { DisplayName } from './display_name';
import { IconButton } from './icon_button';
import { RelativeTimestamp } from './relative_timestamp';
import { Avatar } from "./avatar";
import Button from "./button";
import { FollowersCounter } from "./counters";
import { DisplayName } from "./display_name";
import { IconButton } from "./icon_button";
import { RelativeTimestamp } from "./relative_timestamp";
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
mute_notifications: { id: 'account.mute_notifications_short', defaultMessage: 'Mute notifications' },
unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' },
mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
block: { id: 'account.block_short', defaultMessage: 'Block' },
follow: { id: "account.follow", defaultMessage: "Follow" },
unfollow: { id: "account.unfollow", defaultMessage: "Unfollow" },
cancel_follow_request: { id: "account.cancel_follow_request", defaultMessage: "Withdraw follow request" },
unblock: { id: "account.unblock_short", defaultMessage: "Unblock" },
unmute: { id: "account.unmute_short", defaultMessage: "Unmute" },
mute_notifications: { id: "account.mute_notifications_short", defaultMessage: "Mute notifications" },
unmute_notifications: { id: "account.unmute_notifications_short", defaultMessage: "Unmute notifications" },
mute: { id: "account.mute_short", defaultMessage: "Mute" },
block: { id: "account.block_short", defaultMessage: "Block" },
});
class Account extends ImmutablePureComponent {
@@ -90,8 +90,8 @@ class Account extends ImmutablePureComponent {
if (hidden) {
return (
<>
{account.get('display_name')}
{account.get('username')}
{account.get("display_name")}
{account.get("username")}
</>
);
}
@@ -100,11 +100,11 @@ class Account extends ImmutablePureComponent {
if (actionIcon && onActionClick) {
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
} else if (!actionIcon && account.get('id') !== me && account.get('relationship', null) !== null) {
const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']);
const muting = account.getIn(['relationship', 'muting']);
} else if (!actionIcon && account.get("id") !== me && account.get("relationship", null) !== null) {
const following = account.getIn(["relationship", "following"]);
const requested = account.getIn(["relationship", "requested"]);
const blocking = account.getIn(["relationship", "blocking"]);
const muting = account.getIn(["relationship", "muting"]);
if (requested) {
buttons = <Button text={intl.formatMessage(messages.cancel_follow_request)} onClick={this.handleFollow} />;
@@ -113,7 +113,7 @@ class Account extends ImmutablePureComponent {
} else if (muting) {
let hidingNotificationsButton;
if (account.getIn(['relationship', 'muting_notifications'])) {
if (account.getIn(["relationship", "muting_notifications"])) {
hidingNotificationsButton = <Button text={intl.formatMessage(messages.unmute_notifications)} onClick={this.handleUnmuteNotifications} />;
} else {
hidingNotificationsButton = <Button text={intl.formatMessage(messages.mute_notifications)} onClick={this.handleMuteNotifications} />;
@@ -125,33 +125,33 @@ class Account extends ImmutablePureComponent {
{hidingNotificationsButton}
</>
);
} else if (defaultAction === 'mute') {
} else if (defaultAction === "mute") {
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
} else if (defaultAction === 'block') {
} else if (defaultAction === "block") {
buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />;
} else if (!account.get('moved') || following) {
} else if (!account.get("moved") || following) {
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
}
}
let muteTimeRemaining;
if (account.get('mute_expires_at')) {
muteTimeRemaining = <>· <RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></>;
if (account.get("mute_expires_at")) {
muteTimeRemaining = <>· <RelativeTimestamp timestamp={account.get("mute_expires_at")} futureDate /></>;
}
let verification;
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
const firstVerifiedField = account.get("fields").find(item => !!item.get("verified_at"));
if (firstVerifiedField) {
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
verification = <VerifiedBadge link={firstVerifiedField.get("value")} />;
}
return (
<div className={classNames('account', { 'account--minimal': minimal })}>
<div className={classNames("account", { "account--minimal": minimal })}>
<div className='account__wrapper'>
<Link key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`}>
<Link key={account.get("id")} className='account__display-name' title={account.get("acct")} to={`/@${account.get("acct")}`}>
<div className='account__avatar-wrapper'>
<Avatar account={account} size={size} />
</div>
@@ -160,7 +160,7 @@ class Account extends ImmutablePureComponent {
<DisplayName account={account} />
{!minimal && (
<div className='account__details'>
<ShortNumber value={account.get('followers_count')} renderer={FollowersCounter} /> {verification} {muteTimeRemaining}
<ShortNumber value={account.get("followers_count")} renderer={FollowersCounter} /> {verification} {muteTimeRemaining}
</div>
)}
</div>
@@ -173,10 +173,10 @@ class Account extends ImmutablePureComponent {
)}
</div>
{withBio && (account.get('note').length > 0 ? (
{withBio && (account.get("note").length > 0 ? (
<div
className='account__note translate'
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
dangerouslySetInnerHTML={{ __html: account.get("note_emojified") }}
/>
) : (
<div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>

View File

@@ -1,14 +1,14 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { FormattedNumber } from 'react-intl';
import { FormattedNumber } from "react-intl";
import classNames from 'classnames';
import classNames from "classnames";
import { Sparklines, SparklinesCurve } from 'react-sparklines';
import { Sparklines, SparklinesCurve } from "react-sparklines";
import api from 'mastodon/api';
import { Skeleton } from 'mastodon/components/skeleton';
import api from "mastodon/api";
import { Skeleton } from "mastodon/components/skeleton";
const percIncrease = (a, b) => {
let percent;
@@ -48,7 +48,7 @@ export default class Counter extends PureComponent {
componentDidMount () {
const { measure, start_at, end_at, params } = this.props;
api().post('/api/v1/admin/measures', { keys: [measure], start_at, end_at, [measure]: params }).then(res => {
api().post("/api/v1/admin/measures", { keys: [measure], start_at, end_at, [measure]: params }).then(res => {
this.setState({
loading: false,
data: res.data,
@@ -78,7 +78,7 @@ export default class Counter extends PureComponent {
content = (
<>
<span className='sparkline__value__total'>{measure.human_value || <FormattedNumber value={measure.total} />}</span>
{measure.previous_total && (<span className={classNames('sparkline__value__change', { positive: percentChange > 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'}<FormattedNumber value={percentChange} style='percent' /></span>)}
{measure.previous_total && (<span className={classNames("sparkline__value__change", { positive: percentChange > 0, negative: percentChange < 0 })}>{percentChange > 0 && "+"}<FormattedNumber value={percentChange} style='percent' /></span>)}
</>
);
}

View File

@@ -1,11 +1,11 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { FormattedNumber } from 'react-intl';
import { FormattedNumber } from "react-intl";
import api from 'mastodon/api';
import { Skeleton } from 'mastodon/components/skeleton';
import { roundTo10 } from 'mastodon/utils/numbers';
import api from "mastodon/api";
import { Skeleton } from "mastodon/components/skeleton";
import { roundTo10 } from "mastodon/utils/numbers";
export default class Dimension extends PureComponent {
@@ -26,7 +26,7 @@ export default class Dimension extends PureComponent {
componentDidMount () {
const { start_at, end_at, dimension, limit, params } = this.props;
api().post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit, [dimension]: params }).then(res => {
api().post("/api/v1/admin/dimensions", { keys: [dimension], start_at, end_at, limit, [dimension]: params }).then(res => {
this.setState({
loading: false,
data: res.data,
@@ -74,7 +74,7 @@ export default class Dimension extends PureComponent {
</td>
<td className='dimension__item__value'>
{typeof item.human_value !== 'undefined' ? item.human_value : <FormattedNumber value={item.value} />}
{typeof item.human_value !== "undefined" ? item.human_value : <FormattedNumber value={item.value} />}
</td>
</tr>
))}

View File

@@ -1,12 +1,12 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { FormattedNumber, FormattedMessage } from 'react-intl';
import { FormattedNumber, FormattedMessage } from "react-intl";
import classNames from 'classnames';
import classNames from "classnames";
import api from 'mastodon/api';
import { Skeleton } from 'mastodon/components/skeleton';
import api from "mastodon/api";
import { Skeleton } from "mastodon/components/skeleton";
export default class ImpactReport extends PureComponent {
@@ -27,8 +27,8 @@ export default class ImpactReport extends PureComponent {
include_subdomains: true,
};
api().post('/api/v1/admin/measures', {
keys: ['instance_accounts', 'instance_follows', 'instance_followers'],
api().post("/api/v1/admin/measures", {
keys: ["instance_accounts", "instance_follows", "instance_followers"],
start_at: null,
end_at: null,
instance_accounts: params,
@@ -63,7 +63,7 @@ export default class ImpactReport extends PureComponent {
</td>
</tr>
<tr className={classNames('dimension__item', { negative: !loading && data[1].total > 0 })}>
<tr className={classNames("dimension__item", { negative: !loading && data[1].total > 0 })}>
<td className='dimension__item__key'>
<FormattedMessage id='admin.impact_report.instance_follows' defaultMessage='Followers their users would lose' />
</td>
@@ -73,7 +73,7 @@ export default class ImpactReport extends PureComponent {
</td>
</tr>
<tr className={classNames('dimension__item', { negative: !loading && data[2].total > 0 })}>
<tr className={classNames("dimension__item", { negative: !loading && data[2].total > 0 })}>
<td className='dimension__item__key'>
<FormattedMessage id='admin.impact_report.instance_followers' defaultMessage='Followers our users would lose' />
</td>

View File

@@ -1,17 +1,17 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { injectIntl, defineMessages } from 'react-intl';
import { injectIntl, defineMessages } from "react-intl";
import classNames from 'classnames';
import classNames from "classnames";
import api from 'mastodon/api';
import api from "mastodon/api";
const messages = defineMessages({
legal: { id: 'report.categories.legal', defaultMessage: 'Legal' },
other: { id: 'report.categories.other', defaultMessage: 'Other' },
spam: { id: 'report.categories.spam', defaultMessage: 'Spam' },
violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' },
legal: { id: "report.categories.legal", defaultMessage: "Legal" },
other: { id: "report.categories.other", defaultMessage: "Other" },
spam: { id: "report.categories.spam", defaultMessage: "Spam" },
violation: { id: "report.categories.violation", defaultMessage: "Content violates one or more server rules" },
});
class Category extends PureComponent {
@@ -37,11 +37,11 @@ class Category extends PureComponent {
const { id, text, disabled, selected, children } = this.props;
return (
<div tabIndex={0} role='button' className={classNames('report-reason-selector__category', { selected, disabled })} onClick={this.handleClick}>
<div tabIndex={0} role='button' className={classNames("report-reason-selector__category", { selected, disabled })} onClick={this.handleClick}>
{selected && <input type='hidden' name='report[category]' value={id} />}
<div className='report-reason-selector__category__label'>
<span className={classNames('poll__input', { active: selected, disabled })} />
<span className={classNames("poll__input", { active: selected, disabled })} />
{text}
</div>
@@ -78,8 +78,8 @@ class Rule extends PureComponent {
const { id, text, disabled, selected } = this.props;
return (
<div tabIndex={0} role='button' className={classNames('report-reason-selector__rule', { selected, disabled })} onClick={this.handleClick}>
<span className={classNames('poll__input', { checkbox: true, active: selected, disabled })} />
<div tabIndex={0} role='button' className={classNames("report-reason-selector__rule", { selected, disabled })} onClick={this.handleClick}>
<span className={classNames("poll__input", { checkbox: true, active: selected, disabled })} />
{selected && <input type='hidden' name='report[rule_ids][]' value={id} />}
{text}
</div>
@@ -105,7 +105,7 @@ class ReportReasonSelector extends PureComponent {
};
componentDidMount() {
api().get('/api/v1/instance').then(res => {
api().get("/api/v1/instance").then(res => {
this.setState({
rules: res.data.rules,
});
@@ -150,10 +150,10 @@ class ReportReasonSelector extends PureComponent {
return (
<div className='report-reason-selector'>
<Category id='other' text={intl.formatMessage(messages.other)} selected={category === 'other'} onSelect={this.handleSelect} disabled={disabled} />
<Category id='legal' text={intl.formatMessage(messages.legal)} selected={category === 'legal'} onSelect={this.handleSelect} disabled={disabled} />
<Category id='spam' text={intl.formatMessage(messages.spam)} selected={category === 'spam'} onSelect={this.handleSelect} disabled={disabled} />
<Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === 'violation'} onSelect={this.handleSelect} disabled={disabled}>
<Category id='other' text={intl.formatMessage(messages.other)} selected={category === "other"} onSelect={this.handleSelect} disabled={disabled} />
<Category id='legal' text={intl.formatMessage(messages.legal)} selected={category === "legal"} onSelect={this.handleSelect} disabled={disabled} />
<Category id='spam' text={intl.formatMessage(messages.spam)} selected={category === "spam"} onSelect={this.handleSelect} disabled={disabled} />
<Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === "violation"} onSelect={this.handleSelect} disabled={disabled}>
{rules.map(rule => <Rule key={rule.id} id={rule.id} text={rule.text} selected={rule_ids.includes(rule.id)} onToggle={this.handleToggle} disabled={disabled} />)}
</Category>
</div>

View File

@@ -1,20 +1,20 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl';
import { FormattedMessage, FormattedNumber, FormattedDate } from "react-intl";
import classNames from 'classnames';
import classNames from "classnames";
import api from 'mastodon/api';
import { roundTo10 } from 'mastodon/utils/numbers';
import api from "mastodon/api";
import { roundTo10 } from "mastodon/utils/numbers";
const dateForCohort = cohort => {
const timeZone = 'UTC';
const timeZone = "UTC";
switch(cohort.frequency) {
case 'day':
return <FormattedDate value={cohort.period} month='long' day='2-digit' timeZone={timeZone} />;
default:
return <FormattedDate value={cohort.period} month='long' year='numeric' timeZone={timeZone} />;
case "day":
return <FormattedDate value={cohort.period} month='long' day='2-digit' timeZone={timeZone} />;
default:
return <FormattedDate value={cohort.period} month='long' year='numeric' timeZone={timeZone} />;
}
};
@@ -34,7 +34,7 @@ export default class Retention extends PureComponent {
componentDidMount () {
const { start_at, end_at, frequency } = this.props;
api().post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => {
api().post("/api/v1/admin/retention", { start_at, end_at, frequency }).then(res => {
this.setState({
loading: false,
data: res.data,
@@ -96,7 +96,7 @@ export default class Retention extends PureComponent {
return (
<td key={retention.date}>
<div className={classNames('retention__table__box', 'retention__table__average', `retention__table__box--${roundTo10(average * 100)}`)}>
<div className={classNames("retention__table__box", "retention__table__average", `retention__table__box--${roundTo10(average * 100)}`)}>
<FormattedNumber value={average} style='percent' />
</div>
</td>
@@ -122,7 +122,7 @@ export default class Retention extends PureComponent {
{cohort.data.slice(1).map(retention => (
<td key={retention.date}>
<div className={classNames('retention__table__box', `retention__table__box--${roundTo10(retention.rate * 100)}`)}>
<div className={classNames("retention__table__box", `retention__table__box--${roundTo10(retention.rate * 100)}`)}>
<FormattedNumber value={retention.rate} style='percent' />
</div>
</td>
@@ -136,11 +136,11 @@ export default class Retention extends PureComponent {
let title = null;
switch(frequency) {
case 'day':
title = <FormattedMessage id='admin.dashboard.daily_retention' defaultMessage='User retention rate by day after sign-up' />;
break;
default:
title = <FormattedMessage id='admin.dashboard.monthly_retention' defaultMessage='User retention rate by month after sign-up' />;
case "day":
title = <FormattedMessage id='admin.dashboard.daily_retention' defaultMessage='User retention rate by day after sign-up' />;
break;
default:
title = <FormattedMessage id='admin.dashboard.monthly_retention' defaultMessage='User retention rate by month after sign-up' />;
}
return (

View File

@@ -1,12 +1,12 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from "react-intl";
import classNames from 'classnames';
import classNames from "classnames";
import api from 'mastodon/api';
import Hashtag from 'mastodon/components/hashtag';
import api from "mastodon/api";
import Hashtag from "mastodon/components/hashtag";
export default class Trends extends PureComponent {
@@ -22,7 +22,7 @@ export default class Trends extends PureComponent {
componentDidMount () {
const { limit } = this.props;
api().get('/api/v1/admin/trends/tags', { params: { limit } }).then(res => {
api().get("/api/v1/admin/trends/tags", { params: { limit } }).then(res => {
this.setState({
loading: false,
data: res.data,
@@ -57,7 +57,7 @@ export default class Trends extends PureComponent {
people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1}
uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1}
history={hashtag.history.reverse().map(day => day.uses)}
className={classNames(hashtag.requires_review && 'trends__item--requires-review', !hashtag.trendable && !hashtag.requires_review && 'trends__item--disabled')}
className={classNames(hashtag.requires_review && "trends__item--requires-review", !hashtag.trendable && !hashtag.requires_review && "trends__item--disabled")}
/>
))}
</div>

View File

@@ -1,13 +1,13 @@
import { useCallback, useState } from 'react';
import { useCallback, useState } from "react";
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';
import { ShortNumber } from "./short_number";
interface Props {
value: number;
value: number,
}
export const AnimatedNumber: React.FC<Props> = ({ value }) => {
const [previousValue, setPreviousValue] = useState(value);
@@ -48,7 +48,7 @@ export const AnimatedNumber: React.FC<Props> = ({ value }) => {
<span
key={key}
style={{
position: direction * style.y > 0 ? 'absolute' : 'static',
position: direction * style.y > 0 ? "absolute" : "static",
transform: `translateY(${style.y * 100}%)`,
}}
>

View File

@@ -1,15 +1,15 @@
import PropTypes from 'prop-types';
import PropTypes from "prop-types";
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from "react-intl";
import classNames from 'classnames';
import classNames from "classnames";
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from "react-immutable-proptypes";
import ImmutablePureComponent from "react-immutable-pure-component";
import { Icon } from 'mastodon/components/icon';
import { Icon } from "mastodon/components/icon";
const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
const filename = url => url.split("/").pop().split("#")[0].split("?")[0];
export default class AttachmentList extends ImmutablePureComponent {
@@ -22,7 +22,7 @@ export default class AttachmentList extends ImmutablePureComponent {
const { media, compact } = this.props;
return (
<div className={classNames('attachment-list', { compact })}>
<div className={classNames("attachment-list", { compact })}>
{!compact && (
<div className='attachment-list__icon'>
<Icon id='link' />
@@ -31,13 +31,13 @@ export default class AttachmentList extends ImmutablePureComponent {
<ul className='attachment-list__list'>
{media.map(attachment => {
const displayUrl = attachment.get('remote_url') || attachment.get('url');
const displayUrl = attachment.get("remote_url") || attachment.get("url");
return (
<li key={attachment.get('id')}>
<li key={attachment.get("id")}>
<a href={displayUrl} target='_blank' rel='noopener noreferrer'>
{compact && <Icon id='link' />}
{compact && ' ' }
{compact && " " }
{displayUrl ? filename(displayUrl) : <FormattedMessage id='attachments_list.unprocessed' defaultMessage='(unprocessed)' />}
</a>
</li>

View File

@@ -1,9 +1,9 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { assetHost } from 'mastodon/utils/config';
import { assetHost } from "mastodon/utils/config";
import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light';
import unicodeMapping from "../features/emoji/emoji_unicode_mapping_light";
export default class AutosuggestEmoji extends PureComponent {
@@ -18,7 +18,7 @@ export default class AutosuggestEmoji extends PureComponent {
if (emoji.custom) {
url = emoji.imageUrl;
} else {
const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, "")];
if (!mapping) {
return null;

View File

@@ -1,19 +1,19 @@
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from "react-intl";
import { ShortNumber } from 'mastodon/components/short_number';
import { ShortNumber } from "mastodon/components/short_number";
interface Props {
tag: {
name: string;
url?: string;
name: string,
url?: string,
history?: {
uses: number;
accounts: string;
day: string;
}[];
following?: boolean;
type: 'hashtag';
};
uses: number,
accounts: string,
day: string,
}[],
following?: boolean,
type: "hashtag",
},
}
export const AutosuggestHashtag: React.FC<Props> = ({ tag }) => {

View File

@@ -1,14 +1,14 @@
import PropTypes from 'prop-types';
import PropTypes from "prop-types";
import classNames from 'classnames';
import classNames from "classnames";
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from "react-immutable-proptypes";
import ImmutablePureComponent from "react-immutable-pure-component";
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import AutosuggestAccountContainer from "../features/compose/containers/autosuggest_account_container";
import AutosuggestEmoji from './autosuggest_emoji';
import { AutosuggestHashtag } from './autosuggest_hashtag';
import AutosuggestEmoji from "./autosuggest_emoji";
import { AutosuggestHashtag } from "./autosuggest_hashtag";
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
let word;
@@ -59,7 +59,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
static defaultProps = {
autoFocus: true,
searchTokens: ['@', ':', '#'],
searchTokens: ["@", ":", "#"],
};
state = {
@@ -100,39 +100,39 @@ export default class AutosuggestInput extends ImmutablePureComponent {
}
switch(e.key) {
case 'Escape':
if (suggestions.size === 0 || suggestionsHidden) {
document.querySelector('.ui').parentElement.focus();
} else {
e.preventDefault();
this.setState({ suggestionsHidden: true });
}
case "Escape":
if (suggestions.size === 0 || suggestionsHidden) {
document.querySelector(".ui").parentElement.focus();
} else {
e.preventDefault();
this.setState({ suggestionsHidden: true });
}
break;
case 'ArrowDown':
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
}
break;
case "ArrowDown":
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
}
break;
case 'ArrowUp':
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
}
break;
case "ArrowUp":
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
}
break;
case 'Enter':
case 'Tab':
break;
case "Enter":
case "Tab":
// Select suggestion
if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
e.stopPropagation();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
}
if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
e.stopPropagation();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
}
break;
break;
}
if (e.defaultPrevented || !this.props.onKeyDown) {
@@ -151,7 +151,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
};
onSuggestionClick = (e) => {
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute("data-index"));
e.preventDefault();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
this.input.focus();
@@ -171,19 +171,19 @@ export default class AutosuggestInput extends ImmutablePureComponent {
const { selectedSuggestion } = this.state;
let inner, key;
if (suggestion.type === 'emoji') {
if (suggestion.type === "emoji") {
inner = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id;
} else if (suggestion.type ==='hashtag') {
} else if (suggestion.type ==="hashtag") {
inner = <AutosuggestHashtag tag={suggestion} />;
key = suggestion.name;
} else if (suggestion.type === 'account') {
} else if (suggestion.type === "account") {
inner = <AutosuggestAccountContainer id={suggestion.id} />;
key = suggestion.id;
}
return (
<div role='button' tabIndex={0} key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
<div role='button' tabIndex={0} key={key} data-index={i} className={classNames("autosuggest-textarea__suggestions__item", { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
{inner}
</div>
);
@@ -196,7 +196,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
return (
<div className='autosuggest-input'>
<label>
<span style={{ display: 'none' }}>{placeholder}</span>
<span style={{ display: "none" }}>{placeholder}</span>
<input
type='text'
@@ -220,7 +220,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
/>
</label>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? "" : "autosuggest-textarea__suggestions--visible"}`}>
{suggestions.map(this.renderSuggestion)}
</div>
</div>

View File

@@ -1,16 +1,16 @@
import PropTypes from 'prop-types';
import PropTypes from "prop-types";
import classNames from 'classnames';
import classNames from "classnames";
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from "react-immutable-proptypes";
import ImmutablePureComponent from "react-immutable-pure-component";
import Textarea from 'react-textarea-autosize';
import Textarea from "react-textarea-autosize";
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import AutosuggestAccountContainer from "../features/compose/containers/autosuggest_account_container";
import AutosuggestEmoji from './autosuggest_emoji';
import { AutosuggestHashtag } from './autosuggest_hashtag';
import AutosuggestEmoji from "./autosuggest_emoji";
import { AutosuggestHashtag } from "./autosuggest_hashtag";
const textAtCursorMatchesToken = (str, caretPosition) => {
let word;
@@ -24,7 +24,7 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
word = str.slice(left, right + caretPosition);
}
if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) {
if (!word || word.trim().length < 3 || ["@", ":", "#"].indexOf(word[0]) === -1) {
return [null, null];
}
@@ -97,39 +97,39 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
}
switch(e.key) {
case 'Escape':
if (suggestions.size === 0 || suggestionsHidden) {
document.querySelector('.ui').parentElement.focus();
} else {
e.preventDefault();
this.setState({ suggestionsHidden: true });
}
case "Escape":
if (suggestions.size === 0 || suggestionsHidden) {
document.querySelector(".ui").parentElement.focus();
} else {
e.preventDefault();
this.setState({ suggestionsHidden: true });
}
break;
case 'ArrowDown':
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
}
break;
case "ArrowDown":
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
}
break;
case 'ArrowUp':
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
}
break;
case "ArrowUp":
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
}
break;
case 'Enter':
case 'Tab':
break;
case "Enter":
case "Tab":
// Select suggestion
if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
e.stopPropagation();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
}
if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
e.stopPropagation();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
}
break;
break;
}
if (e.defaultPrevented || !this.props.onKeyDown) {
@@ -151,7 +151,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
};
onSuggestionClick = (e) => {
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute("data-index"));
e.preventDefault();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
this.textarea.focus();
@@ -178,19 +178,19 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
const { selectedSuggestion } = this.state;
let inner, key;
if (suggestion.type === 'emoji') {
if (suggestion.type === "emoji") {
inner = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id;
} else if (suggestion.type === 'hashtag') {
} else if (suggestion.type === "hashtag") {
inner = <AutosuggestHashtag tag={suggestion} />;
key = suggestion.name;
} else if (suggestion.type === 'account') {
} else if (suggestion.type === "account") {
inner = <AutosuggestAccountContainer id={suggestion.id} />;
key = suggestion.id;
}
return (
<div role='button' tabIndex={0} key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
<div role='button' tabIndex={0} key={key} data-index={i} className={classNames("autosuggest-textarea__suggestions__item", { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
{inner}
</div>
);
@@ -204,7 +204,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
<div className='autosuggest-textarea'>
<label>
<span style={{ display: 'none' }}>{placeholder}</span>
<span style={{ display: "none" }}>{placeholder}</span>
<Textarea
ref={this.setTextarea}
@@ -229,7 +229,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
</div>,
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? "" : "autosuggest-textarea__suggestions--visible"}`}>
{suggestions.map(this.renderSuggestion)}
</div>
</div>,

View File

@@ -1,15 +1,15 @@
import classNames from 'classnames';
import classNames from "classnames";
import { useHovering } from '../../hooks/useHovering';
import type { Account } from '../../types/resources';
import { autoPlayGif } from '../initial_state';
import { useHovering } from "../../hooks/useHovering";
import { type Account } from "../../types/resources";
import { autoPlayGif } from "../initial_state";
interface Props {
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
size: number;
style?: React.CSSProperties;
inline?: boolean;
animate?: boolean;
account: Account | undefined, // FIXME: remove `undefined` once we know for sure its always there
size: number,
style?: React.CSSProperties,
inline?: boolean,
animate?: boolean,
}
export const Avatar: React.FC<Props> = ({
@@ -29,19 +29,19 @@ export const Avatar: React.FC<Props> = ({
const src =
hovering || animate
? account?.get('avatar')
: account?.get('avatar_static');
? account?.get("avatar")
: account?.get("avatar_static");
return (
<div
className={classNames('account__avatar', {
'account__avatar-inline': inline,
className={classNames("account__avatar", {
"account__avatar-inline": inline,
})}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={style}
>
{src && <img src={src} alt={account?.get('acct')} />}
{src && <img src={src} alt={account?.get("acct")} />}
</div>
);
};

View File

@@ -1,11 +1,11 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePropTypes from "react-immutable-proptypes";
import { autoPlayGif } from '../initial_state';
import { autoPlayGif } from "../initial_state";
import { Avatar } from './avatar';
import { Avatar } from "./avatar";
export default class AvatarComposite extends PureComponent {
@@ -24,10 +24,10 @@ export default class AvatarComposite extends PureComponent {
let width = 50;
let height = 100;
let top = 'auto';
let left = 'auto';
let bottom = 'auto';
let right = 'auto';
let top = "auto";
let left = "auto";
let bottom = "auto";
let right = "auto";
if (size === 1) {
width = 100;
@@ -39,35 +39,35 @@ export default class AvatarComposite extends PureComponent {
if (size === 2) {
if (index === 0) {
right = '1px';
right = "1px";
} else {
left = '1px';
left = "1px";
}
} else if (size === 3) {
if (index === 0) {
right = '1px';
right = "1px";
} else if (index > 0) {
left = '1px';
left = "1px";
}
if (index === 1) {
bottom = '1px';
bottom = "1px";
} else if (index > 1) {
top = '1px';
top = "1px";
}
} else if (size === 4) {
if (index === 0 || index === 2) {
right = '1px';
right = "1px";
}
if (index === 1 || index === 3) {
left = '1px';
left = "1px";
}
if (index < 2) {
bottom = '1px';
bottom = "1px";
} else {
top = '1px';
top = "1px";
}
}
@@ -81,7 +81,7 @@ export default class AvatarComposite extends PureComponent {
};
return (
<div key={account.get('id')} style={style}>
<div key={account.get("id")} style={style}>
<Avatar account={account} animate={animate} />
</div>
);

View File

@@ -1,13 +1,13 @@
import { useHovering } from '../../hooks/useHovering';
import type { Account } from '../../types/resources';
import { autoPlayGif } from '../initial_state';
import { useHovering } from "../../hooks/useHovering";
import { type Account } from "../../types/resources";
import { autoPlayGif } from "../initial_state";
interface Props {
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
size?: number;
baseSize?: number;
overlaySize?: number;
account: Account | undefined, // FIXME: remove `undefined` once we know for sure its always there
friend: Account | undefined, // FIXME: remove `undefined` once we know for sure its always there
size?: number,
baseSize?: number,
overlaySize?: number,
}
export const AvatarOverlay: React.FC<Props> = ({
@@ -20,11 +20,11 @@ export const AvatarOverlay: React.FC<Props> = ({
const { hovering, handleMouseEnter, handleMouseLeave } =
useHovering(autoPlayGif);
const accountSrc = hovering
? account?.get('avatar')
: account?.get('avatar_static');
? account?.get("avatar")
: account?.get("avatar_static");
const friendSrc = hovering
? friend?.get('avatar')
: friend?.get('avatar_static');
? friend?.get("avatar")
: friend?.get("avatar_static");
return (
<div
@@ -38,7 +38,7 @@ export const AvatarOverlay: React.FC<Props> = ({
className='account__avatar'
style={{ width: `${baseSize}px`, height: `${baseSize}px` }}
>
{accountSrc && <img src={accountSrc} alt={account?.get('acct')} />}
{accountSrc && <img src={accountSrc} alt={account?.get("acct")} />}
</div>
</div>
<div className='account__avatar-overlay-overlay'>
@@ -46,7 +46,7 @@ export const AvatarOverlay: React.FC<Props> = ({
className='account__avatar'
style={{ width: `${overlaySize}px`, height: `${overlaySize}px` }}
>
{friendSrc && <img src={friendSrc} alt={friend?.get('acct')} />}
{friendSrc && <img src={friendSrc} alt={friend?.get("acct")} />}
</div>
</div>
</div>

View File

@@ -1,10 +1,10 @@
import PropTypes from 'prop-types';
import PropTypes from "prop-types";
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from "react-intl";
import { ReactComponent as GroupsIcon } from '@material-design-icons/svg/outlined/group.svg';
import { ReactComponent as PersonIcon } from '@material-design-icons/svg/outlined/person.svg';
import { ReactComponent as SmartToyIcon } from '@material-design-icons/svg/outlined/smart_toy.svg';
import { ReactComponent as GroupsIcon } from "@material-design-icons/svg/outlined/group.svg";
import { ReactComponent as PersonIcon } from "@material-design-icons/svg/outlined/person.svg";
import { ReactComponent as SmartToyIcon } from "@material-design-icons/svg/outlined/smart_toy.svg";
export const Badge = ({ icon, label, domain }) => (

View File

@@ -1,13 +1,13 @@
import { memo, useRef, useEffect } from 'react';
import { memo, useRef, useEffect } from "react";
import { decode } from 'blurhash';
import { decode } from "blurhash";
interface Props extends React.HTMLAttributes<HTMLCanvasElement> {
hash: string;
width?: number;
height?: number;
dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched
children?: never;
hash: string,
width?: number,
height?: number,
dummy?: boolean, // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched
children?: never,
}
const Blurhash: React.FC<Props> = ({
hash,
@@ -25,16 +25,18 @@ const Blurhash: React.FC<Props> = ({
// eslint-disable-next-line no-self-assign
canvas.width = canvas.width; // resets canvas
if (dummy || !hash) return;
if (dummy || !hash) {
return;
}
try {
const pixels = decode(hash, width, height);
const ctx = canvas.getContext('2d');
const ctx = canvas.getContext("2d");
const imageData = new ImageData(pixels, width, height);
ctx?.putImageData(imageData, 0, 0);
} catch (err) {
console.error('Blurhash decoding failure', { err, hash });
console.error("Blurhash decoding failure", { err, hash });
}
}, [dummy, hash, width, height]);

View File

@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import classNames from 'classnames';
import classNames from "classnames";
export default class Button extends PureComponent {
@@ -18,7 +18,7 @@ export default class Button extends PureComponent {
};
static defaultProps = {
type: 'button',
type: "button",
};
handleClick = (e) => {
@@ -36,9 +36,9 @@ export default class Button extends PureComponent {
}
render () {
const className = classNames('button', this.props.className, {
'button-secondary': this.props.secondary,
'button--block': this.props.block,
const className = classNames("button", this.props.className, {
"button-secondary": this.props.secondary,
"button--block": this.props.block,
});
return (

View File

@@ -1,6 +1,6 @@
interface Props {
size: number;
strokeWidth: number;
size: number,
strokeWidth: number,
}
export const CircularProgress: React.FC<Props> = ({ size, strokeWidth }) => {

View File

@@ -1,9 +1,9 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { supportsPassiveEvents } from 'detect-passive-events';
import { supportsPassiveEvents } from "detect-passive-events";
import { scrollTop } from '../scroll';
import { scrollTop } from "../scroll";
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
@@ -21,7 +21,7 @@ export default class Column extends PureComponent {
if (this.props.bindToDocument) {
scrollable = document.scrollingElement;
} else {
scrollable = this.node.querySelector('.scrollable');
scrollable = this.node.querySelector(".scrollable");
}
if (!scrollable) {
@@ -32,7 +32,7 @@ export default class Column extends PureComponent {
}
handleWheel = () => {
if (typeof this._interruptScrollAnimation !== 'function') {
if (typeof this._interruptScrollAnimation !== "function") {
return;
}
@@ -45,17 +45,17 @@ export default class Column extends PureComponent {
componentDidMount () {
if (this.props.bindToDocument) {
document.addEventListener('wheel', this.handleWheel, listenerOptions);
document.addEventListener("wheel", this.handleWheel, listenerOptions);
} else {
this.node.addEventListener('wheel', this.handleWheel, listenerOptions);
this.node.addEventListener("wheel", this.handleWheel, listenerOptions);
}
}
componentWillUnmount () {
if (this.props.bindToDocument) {
document.removeEventListener('wheel', this.handleWheel, listenerOptions);
document.removeEventListener("wheel", this.handleWheel, listenerOptions);
} else {
this.node.removeEventListener('wheel', this.handleWheel, listenerOptions);
this.node.removeEventListener("wheel", this.handleWheel, listenerOptions);
}
}

View File

@@ -1,10 +1,10 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { createPortal } from 'react-dom';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { createPortal } from "react-dom";
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from "react-intl";
import { Icon } from 'mastodon/components/icon';
import { Icon } from "mastodon/components/icon";
export default class ColumnBackButton extends PureComponent {
@@ -26,7 +26,7 @@ export default class ColumnBackButton extends PureComponent {
} else if (router.history.location?.state?.fromMastodon) {
router.history.goBack();
} else {
router.history.push('/');
router.history.push("/");
}
};
@@ -46,7 +46,7 @@ export default class ColumnBackButton extends PureComponent {
// The portal container and the component may be rendered to the DOM in
// the same React render pass, so the container might not be available at
// the time `render()` is called.
const container = document.getElementById('tabs-bar__portal');
const container = document.getElementById("tabs-bar__portal");
if (container === null) {
// The container wasn't available, force a re-render so that the
// component can eventually be inserted in the container and not scroll

View File

@@ -1,8 +1,8 @@
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from "react-intl";
import { Icon } from 'mastodon/components/icon';
import { Icon } from "mastodon/components/icon";
import ColumnBackButton from './column_back_button';
import ColumnBackButton from "./column_back_button";
export default class ColumnBackButtonSlim extends ColumnBackButton {

View File

@@ -1,18 +1,18 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { createPortal } from 'react-dom';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { createPortal } from "react-dom";
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import { FormattedMessage, injectIntl, defineMessages } from "react-intl";
import classNames from 'classnames';
import classNames from "classnames";
import { Icon } from 'mastodon/components/icon';
import { Icon } from "mastodon/components/icon";
const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
show: { id: "column_header.show_settings", defaultMessage: "Show settings" },
hide: { id: "column_header.hide_settings", defaultMessage: "Hide settings" },
moveLeft: { id: "column_header.moveLeft_settings", defaultMessage: "Move column to the left" },
moveRight: { id: "column_header.moveRight_settings", defaultMessage: "Move column to the right" },
});
class ColumnHeader extends PureComponent {
@@ -68,7 +68,7 @@ class ColumnHeader extends PureComponent {
if (router.history.location?.state?.fromMastodon) {
router.history.goBack();
} else {
router.history.push('/');
router.history.push("/");
}
};
@@ -78,7 +78,7 @@ class ColumnHeader extends PureComponent {
handlePin = () => {
if (!this.props.pinned) {
this.context.router.history.replace('/');
this.context.router.history.replace("/");
}
this.props.onPin();
@@ -89,21 +89,21 @@ class ColumnHeader extends PureComponent {
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues } = this.props;
const { collapsed, animating } = this.state;
const wrapperClassName = classNames('column-header__wrapper', {
'active': active,
const wrapperClassName = classNames("column-header__wrapper", {
"active": active,
});
const buttonClassName = classNames('column-header', {
'active': active,
const buttonClassName = classNames("column-header", {
"active": active,
});
const collapsibleClassName = classNames('column-header__collapsible', {
'collapsed': collapsed,
'animating': animating,
const collapsibleClassName = classNames("column-header__collapsible", {
"collapsed": collapsed,
"animating": animating,
});
const collapsibleButtonClassName = classNames('column-header__button', {
'active': !collapsed,
const collapsibleButtonClassName = classNames("column-header__button", {
"active": !collapsed,
});
let extraContent, pinButton, moveButtons, backButton, collapseButton;
@@ -200,7 +200,7 @@ class ColumnHeader extends PureComponent {
// The portal container and the component may be rendered to the DOM in
// the same React render pass, so the container might not be available at
// the time `render()` is called.
const container = document.getElementById('tabs-bar__portal');
const container = document.getElementById("tabs-bar__portal");
if (container === null) {
// The container wasn't available, force a re-render so that the
// component can eventually be inserted in the container and not scroll

View File

@@ -1,6 +1,6 @@
import React from 'react';
import React from "react";
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from "react-intl";
export const StatusesCounter = (
displayNumber: React.ReactNode,

View File

@@ -1,18 +1,18 @@
import type { PropsWithChildren } from 'react';
import { useCallback, useState } from 'react';
import { type PropsWithChildren } from "react";
import { useCallback, useState } from "react";
import { defineMessages, useIntl } from 'react-intl';
import { defineMessages, useIntl } from "react-intl";
import { bannerSettings } from 'mastodon/settings';
import { bannerSettings } from "mastodon/settings";
import { IconButton } from './icon_button';
import { IconButton } from "./icon_button";
const messages = defineMessages({
dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' },
dismiss: { id: "dismissable_banner.dismiss", defaultMessage: "Dismiss" },
});
interface Props {
id: string;
id: string,
}
export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({

View File

@@ -1,16 +1,16 @@
import React from 'react';
import React from "react";
import type { List } from 'immutable';
import { type List } from "immutable";
import type { Account } from '../../types/resources';
import { autoPlayGif } from '../initial_state';
import { type Account } from "../../types/resources";
import { autoPlayGif } from "../initial_state";
import { Skeleton } from './skeleton';
import { Skeleton } from "./skeleton";
interface Props {
account?: Account;
others?: List<Account>;
localDomain?: string;
account?: Account,
others?: List<Account>,
localDomain?: string,
}
export class DisplayName extends React.PureComponent<Props> {
@@ -22,11 +22,13 @@ export class DisplayName extends React.PureComponent<Props> {
}
const emojis =
currentTarget.querySelectorAll<HTMLImageElement>('img.custom-emoji');
currentTarget.querySelectorAll<HTMLImageElement>("img.custom-emoji");
emojis.forEach((emoji) => {
const originalSrc = emoji.getAttribute('data-original');
if (originalSrc != null) emoji.src = originalSrc;
const originalSrc = emoji.getAttribute("data-original");
if (originalSrc != null) {
emoji.src = originalSrc;
}
});
};
@@ -38,11 +40,13 @@ export class DisplayName extends React.PureComponent<Props> {
}
const emojis =
currentTarget.querySelectorAll<HTMLImageElement>('img.custom-emoji');
currentTarget.querySelectorAll<HTMLImageElement>("img.custom-emoji");
emojis.forEach((emoji) => {
const staticSrc = emoji.getAttribute('data-static');
if (staticSrc != null) emoji.src = staticSrc;
const staticSrc = emoji.getAttribute("data-static");
if (staticSrc != null) {
emoji.src = staticSrc;
}
});
};
@@ -63,22 +67,22 @@ export class DisplayName extends React.PureComponent<Props> {
displayName = others
.take(2)
.map((a) => (
<bdi key={a.get('id')}>
<bdi key={a.get("id")}>
<strong
className='display-name__html'
dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }}
dangerouslySetInnerHTML={{ __html: a.get("display_name_html") }}
/>
</bdi>
))
.reduce((prev, cur) => [prev, ', ', cur]);
.reduce((prev, cur) => [prev, ", ", cur]);
if (others.size - 2 > 0) {
suffix = `+${others.size - 2}`;
}
} else if (account) {
let acct = account.get('acct');
let acct = account.get("acct");
if (!acct.includes('@') && localDomain) {
if (!acct.includes("@") && localDomain) {
acct = `${acct}@${localDomain}`;
}
@@ -87,7 +91,7 @@ export class DisplayName extends React.PureComponent<Props> {
<strong
className='display-name__html'
dangerouslySetInnerHTML={{
__html: account.get('display_name_html'),
__html: account.get("display_name_html"),
}}
/>
</bdi>

View File

@@ -1,19 +1,19 @@
import { useCallback } from 'react';
import { useCallback } from "react";
import { defineMessages, useIntl } from 'react-intl';
import { defineMessages, useIntl } from "react-intl";
import { IconButton } from './icon_button';
import { IconButton } from "./icon_button";
const messages = defineMessages({
unblockDomain: {
id: 'account.unblock_domain',
defaultMessage: 'Unblock domain {domain}',
id: "account.unblock_domain",
defaultMessage: "Unblock domain {domain}",
},
});
interface Props {
domain: string;
onUnblockDomain: (domain: string) => void;
domain: string,
onUnblockDomain: (domain: string) => void,
}
export const Domain: React.FC<Props> = ({ domain, onUnblockDomain }) => {

View File

@@ -1,15 +1,15 @@
import PropTypes from 'prop-types';
import { PureComponent, cloneElement, Children } from 'react';
import PropTypes from "prop-types";
import { PureComponent, cloneElement, Children } from "react";
import classNames from 'classnames';
import classNames from "classnames";
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePropTypes from "react-immutable-proptypes";
import { supportsPassiveEvents } from 'detect-passive-events';
import Overlay from 'react-overlays/Overlay';
import { supportsPassiveEvents } from "detect-passive-events";
import Overlay from "react-overlays/Overlay";
import { CircularProgress } from "./circular_progress";
import { IconButton } from './icon_button';
import { IconButton } from "./icon_button";
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
let id = 0;
@@ -44,9 +44,9 @@ class DropdownMenu extends PureComponent {
};
componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, { capture: true });
document.addEventListener('keydown', this.handleKeyDown, { capture: true });
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
document.addEventListener("click", this.handleDocumentClick, { capture: true });
document.addEventListener("keydown", this.handleKeyDown, { capture: true });
document.addEventListener("touchend", this.handleDocumentClick, listenerOptions);
if (this.focusedItem && this.props.openedViaKeyboard) {
this.focusedItem.focus({ preventScroll: true });
@@ -54,9 +54,9 @@ class DropdownMenu extends PureComponent {
}
componentWillUnmount () {
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
document.removeEventListener('keydown', this.handleKeyDown, { capture: true });
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
document.removeEventListener("click", this.handleDocumentClick, { capture: true });
document.removeEventListener("keydown", this.handleKeyDown, { capture: true });
document.removeEventListener("touchend", this.handleDocumentClick, listenerOptions);
}
setRef = c => {
@@ -68,33 +68,33 @@ class DropdownMenu extends PureComponent {
};
handleKeyDown = e => {
const items = Array.from(this.node.querySelectorAll('a, button'));
const items = Array.from(this.node.querySelectorAll("a, button"));
const index = items.indexOf(document.activeElement);
let element = null;
switch(e.key) {
case 'ArrowDown':
element = items[index+1] || items[0];
break;
case 'ArrowUp':
element = items[index-1] || items[items.length-1];
break;
case 'Tab':
if (e.shiftKey) {
element = items[index-1] || items[items.length-1];
} else {
case "ArrowDown":
element = items[index+1] || items[0];
}
break;
case 'Home':
element = items[0];
break;
case 'End':
element = items[items.length-1];
break;
case 'Escape':
this.props.onClose();
break;
break;
case "ArrowUp":
element = items[index-1] || items[items.length-1];
break;
case "Tab":
if (e.shiftKey) {
element = items[index-1] || items[items.length-1];
} else {
element = items[index+1] || items[0];
}
break;
case "Home":
element = items[0];
break;
case "End":
element = items[items.length-1];
break;
case "Escape":
this.props.onClose();
break;
}
if (element) {
@@ -105,7 +105,7 @@ class DropdownMenu extends PureComponent {
};
handleItemKeyPress = e => {
if (e.key === 'Enter' || e.key === ' ') {
if (e.key === "Enter" || e.key === " ") {
this.handleClick(e);
}
};
@@ -120,10 +120,10 @@ class DropdownMenu extends PureComponent {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { text, href = '#', target = '_blank', method, dangerous } = option;
const { text, href = "#", target = "_blank", method, dangerous } = option;
return (
<li className={classNames('dropdown-menu__item', { 'dropdown-menu__item--dangerous': dangerous })} key={`${text}-${i}`}>
<li className={classNames("dropdown-menu__item", { "dropdown-menu__item--dangerous": dangerous })} key={`${text}-${i}`}>
<a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex={0} ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
{text}
</a>
@@ -137,7 +137,7 @@ class DropdownMenu extends PureComponent {
let renderItem = this.props.renderItem || this.renderItem;
return (
<div className={classNames('dropdown-menu__container', { 'dropdown-menu__container--loading': loading })} ref={this.setRef}>
<div className={classNames("dropdown-menu__container", { "dropdown-menu__container--loading": loading })} ref={this.setRef}>
{loading && (
<CircularProgress size={30} strokeWidth={3.5} />
)}
@@ -149,7 +149,7 @@ class DropdownMenu extends PureComponent {
)}
{!loading && (
<ul className={classNames('dropdown-menu__container__list', { 'dropdown-menu__container__list--scrollable': scrollable })}>
<ul className={classNames("dropdown-menu__container__list", { "dropdown-menu__container__list--scrollable": scrollable })}>
{items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))}
</ul>
)}
@@ -186,7 +186,7 @@ export default class Dropdown extends PureComponent {
};
static defaultProps = {
title: 'Menu',
title: "Menu",
};
state = {
@@ -197,7 +197,7 @@ export default class Dropdown extends PureComponent {
if (this.state.id === this.props.openDropdownId) {
this.handleClose();
} else {
this.props.onOpen(this.state.id, this.handleItemClick, type !== 'click');
this.props.onOpen(this.state.id, this.handleItemClick, type !== "click");
}
};
@@ -217,35 +217,35 @@ export default class Dropdown extends PureComponent {
handleButtonKeyDown = (e) => {
switch(e.key) {
case ' ':
case 'Enter':
this.handleMouseDown();
break;
case " ":
case "Enter":
this.handleMouseDown();
break;
}
};
handleKeyPress = (e) => {
switch(e.key) {
case ' ':
case 'Enter':
this.handleClick(e);
e.stopPropagation();
e.preventDefault();
break;
case " ":
case "Enter":
this.handleClick(e);
e.stopPropagation();
e.preventDefault();
break;
}
};
handleItemClick = e => {
const { onItemClick } = this.props;
const i = Number(e.currentTarget.getAttribute('data-index'));
const i = Number(e.currentTarget.getAttribute("data-index"));
const item = this.props.items[i];
this.handleClose();
if (typeof onItemClick === 'function') {
if (typeof onItemClick === "function") {
e.preventDefault();
onItemClick(item, i);
} else if (item && typeof item.action === 'function') {
} else if (item && typeof item.action === "function") {
e.preventDefault();
item.action();
} else if (item && item.to) {
@@ -297,7 +297,7 @@ export default class Dropdown extends PureComponent {
onKeyPress: this.handleKeyPress,
}) : (
<IconButton
icon={!open ? icon : 'close'}
icon={!open ? icon : "close"}
title={title}
active={open}
disabled={disabled}
@@ -314,7 +314,7 @@ export default class Dropdown extends PureComponent {
<span ref={this.setTargetRef}>
{button}
</span>
<Overlay show={open} offset={[5, 5]} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
<Overlay show={open} offset={[5, 5]} placement={"bottom"} flip target={this.findTarget} popperConfig={{ strategy: "fixed" }}>
{({ props, arrowProps, placement }) => (
<div {...props}>
<div className={`dropdown-animation dropdown-menu ${placement}`}>

View File

@@ -1,8 +1,8 @@
import { connect } from 'react-redux';
import { connect } from "react-redux";
import { openDropdownMenu, closeDropdownMenu } from 'mastodon/actions/dropdown_menu';
import { fetchHistory } from 'mastodon/actions/history';
import DropdownMenu from 'mastodon/components/dropdown_menu';
import { openDropdownMenu, closeDropdownMenu } from "mastodon/actions/dropdown_menu";
import { fetchHistory } from "mastodon/actions/history";
import DropdownMenu from "mastodon/components/dropdown_menu";
/**
*
@@ -12,8 +12,8 @@ import DropdownMenu from 'mastodon/components/dropdown_menu';
const mapStateToProps = (state, { statusId }) => ({
openDropdownId: state.dropdownMenu.openId,
openedViaKeyboard: state.dropdownMenu.keyboard,
items: state.getIn(['history', statusId, 'items']),
loading: state.getIn(['history', statusId, 'loading']),
items: state.getIn(["history", statusId, "items"]),
loading: state.getIn(["history", statusId, "loading"]),
});
const mapDispatchToProps = (dispatch, { statusId }) => ({

View File

@@ -1,22 +1,22 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { FormattedMessage, injectIntl } from 'react-intl';
import { FormattedMessage, injectIntl } from "react-intl";
import { connect } from 'react-redux';
import { connect } from "react-redux";
import { openModal } from 'mastodon/actions/modal';
import { Icon } from 'mastodon/components/icon';
import InlineAccount from 'mastodon/components/inline_account';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { openModal } from "mastodon/actions/modal";
import { Icon } from "mastodon/components/icon";
import InlineAccount from "mastodon/components/inline_account";
import { RelativeTimestamp } from "mastodon/components/relative_timestamp";
import DropdownMenu from './containers/dropdown_menu_container';
import DropdownMenu from "./containers/dropdown_menu_container";
const mapDispatchToProps = (dispatch, { statusId }) => ({
onItemClick (index) {
dispatch(openModal({
modalType: 'COMPARE_HISTORY',
modalType: "COMPARE_HISTORY",
modalProps: { index, statusId },
}));
},
@@ -44,17 +44,17 @@ class EditedTimestamp extends PureComponent {
};
renderItem = (item, index, { onClick, onKeyPress }) => {
const formattedDate = <RelativeTimestamp timestamp={item.get('created_at')} short={false} />;
const formattedName = <InlineAccount accountId={item.get('account')} />;
const formattedDate = <RelativeTimestamp timestamp={item.get("created_at")} short={false} />;
const formattedName = <InlineAccount accountId={item.get("account")} />;
const label = item.get('original') ? (
const label = item.get("original") ? (
<FormattedMessage id='status.history.created' defaultMessage='{name} created {date}' values={{ name: formattedName, date: formattedDate }} />
) : (
<FormattedMessage id='status.history.edited' defaultMessage='{name} edited {date}' values={{ name: formattedName, date: formattedDate }} />
);
return (
<li className='dropdown-menu__item edited-timestamp__history__item' key={item.get('created_at')}>
<li className='dropdown-menu__item edited-timestamp__history__item' key={item.get("created_at")}>
<button data-index={index} onClick={onClick} onKeyPress={onKeyPress}>{label}</button>
</li>
);
@@ -66,7 +66,7 @@ class EditedTimestamp extends PureComponent {
return (
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
<button className='dropdown-menu__text-button'>
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' />
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { hour12: false, month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit" }) }} /> <Icon id='caret-down' />
</button>
</DropdownMenu>
);

View File

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

View File

@@ -1,13 +1,13 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from "react-intl";
import { Helmet } from 'react-helmet';
import { Helmet } from "react-helmet";
import StackTrace from 'stacktrace-js';
import StackTrace from "stacktrace-js";
import { version, source_url } from 'mastodon/initial_state';
import { version, source_url } from "mastodon/initial_state";
export default class ErrorBoundary extends PureComponent {
@@ -34,7 +34,7 @@ export default class ErrorBoundary extends PureComponent {
StackTrace.fromError(error).then((stackframes) => {
this.setState({
mappedStackTrace: stackframes.map((sf) => sf.toString()).join('\n'),
mappedStackTrace: stackframes.map((sf) => sf.toString()).join("\n"),
});
}).catch(() => {
this.setState({
@@ -45,23 +45,23 @@ export default class ErrorBoundary extends PureComponent {
handleCopyStackTrace = () => {
const { errorMessage, stackTrace, mappedStackTrace } = this.state;
const textarea = document.createElement('textarea');
const textarea = document.createElement("textarea");
let contents = [errorMessage, stackTrace];
if (mappedStackTrace) {
contents.push(mappedStackTrace);
}
textarea.textContent = contents.join('\n\n\n');
textarea.style.position = 'fixed';
textarea.textContent = contents.join("\n\n\n");
textarea.style.position = "fixed";
document.body.appendChild(textarea);
try {
textarea.select();
document.execCommand('copy');
document.execCommand("copy");
} catch (e) {
console.error(e);
} finally {
document.body.removeChild(textarea);
}
@@ -77,7 +77,7 @@ export default class ErrorBoundary extends PureComponent {
return this.props.children;
}
const likelyBrowserAddonIssue = errorMessage && errorMessage.includes('NotFoundError');
const likelyBrowserAddonIssue = errorMessage && errorMessage.includes("NotFoundError");
return (
<div className='error-boundary'>
@@ -98,7 +98,7 @@ export default class ErrorBoundary extends PureComponent {
)}
</p>
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied ? 'copied' : ''}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied ? "copied" : ""}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
</div>
<Helmet>

View File

@@ -1,13 +1,13 @@
import { useCallback, useState } from 'react';
import { useCallback, useState } from "react";
interface Props {
src: string;
key: string;
alt?: string;
lang?: string;
width: number;
height: number;
onClick?: () => void;
src: string,
key: string,
alt?: string,
lang?: string,
width: number,
height: number,
onClick?: () => void,
}
export const GIFV: React.FC<Props> = ({
@@ -36,7 +36,7 @@ export const GIFV: React.FC<Props> = ({
);
return (
<div className='gifv' style={{ position: 'relative' }}>
<div className='gifv' style={{ position: "relative" }}>
{loading && (
<canvas
width={width}
@@ -63,7 +63,7 @@ export const GIFV: React.FC<Props> = ({
playsInline
onClick={handleClick}
onLoadedData={handleLoadedData}
style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }}
style={{ position: loading ? "absolute" : "static", top: 0, left: 0 }}
/>
</div>
);

View File

@@ -1,18 +1,18 @@
// @ts-check
import PropTypes from 'prop-types';
import { Component } from 'react';
import PropTypes from "prop-types";
import { Component } from "react";
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from "react-intl";
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import classNames from "classnames";
import { Link } from "react-router-dom";
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePropTypes from "react-immutable-proptypes";
import { Sparklines, SparklinesCurve } from 'react-sparklines';
import { Sparklines, SparklinesCurve } from "react-sparklines";
import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton';
import { ShortNumber } from "mastodon/components/short_number";
import { Skeleton } from "mastodon/components/skeleton";
class SilentErrorBoundary extends Component {
@@ -57,11 +57,11 @@ export const accountsCountRenderer = (displayNumber, pluralReady) => (
// @ts-expect-error
export const ImmutableHashtag = ({ hashtag }) => (
<Hashtag
name={hashtag.get('name')}
to={`/tags/${hashtag.get('name')}`}
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
name={hashtag.get("name")}
to={`/tags/${hashtag.get("name")}`}
people={hashtag.getIn(["history", 0, "accounts"]) * 1 + hashtag.getIn(["history", 1, "accounts"]) * 1}
// @ts-expect-error
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
history={hashtag.get("history").reverse().map((day) => day.get("uses")).toArray()}
/>
);
@@ -71,7 +71,7 @@ ImmutableHashtag.propTypes = {
// @ts-expect-error
const Hashtag = ({ name, to, people, uses, history, className, description, withGraph }) => (
<div className={classNames('trends__item', className)}>
<div className={classNames("trends__item", className)}>
<div className='trends__item__name'>
<Link to={to}>
{name ? <>#<span>{name}</span></> : <Skeleton width={50} />}
@@ -80,11 +80,11 @@ const Hashtag = ({ name, to, people, uses, history, className, description, with
{description ? (
<span>{description}</span>
) : (
typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />
typeof people !== "undefined" ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />
)}
</div>
{typeof uses !== 'undefined' && (
{typeof uses !== "undefined" && (
<div className='trends__item__current'>
<ShortNumber value={uses} />
</div>
@@ -94,7 +94,7 @@ const Hashtag = ({ name, to, people, uses, history, className, description, with
<div className='trends__item__sparkline'>
<SilentErrorBoundary>
<Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
<SparklinesCurve style={{ fill: 'none' }} />
<SparklinesCurve style={{ fill: "none" }} />
</Sparklines>
</SilentErrorBoundary>
</div>

View File

@@ -1,14 +1,14 @@
import { useState, useCallback } from 'react';
import { useState, useCallback } from "react";
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from "react-intl";
import { Link } from 'react-router-dom';
import { Link } from "react-router-dom";
import type { List, Record } from 'immutable';
import { type List, type Record } from "immutable";
import { groupBy, minBy } from 'lodash';
import { groupBy, minBy } from "lodash";
import { getStatusContent } from './status_content';
import { getStatusContent } from "./status_content";
// Fit on a single line on desktop
const VISIBLE_HASHTAGS = 3;
@@ -16,27 +16,27 @@ const VISIBLE_HASHTAGS = 3;
// Those types are not correct, they need to be replaced once this part of the state is typed
export type TagLike = Record<{ name: string }>;
export type StatusLike = Record<{
tags: List<TagLike>;
contentHTML: string;
media_attachments: List<unknown>;
spoiler_text?: string;
tags: List<TagLike>,
contentHTML: string,
media_attachments: List<unknown>,
spoiler_text?: string,
}>;
function normalizeHashtag(hashtag: string) {
return (
hashtag && hashtag.startsWith('#') ? hashtag.slice(1) : hashtag
).normalize('NFKC');
hashtag && hashtag.startsWith("#") ? hashtag.slice(1) : hashtag
).normalize("NFKC");
}
function isNodeLinkHashtag(element: Node): element is HTMLLinkElement {
return (
element instanceof HTMLAnchorElement &&
// it may be a <a> starting with a hashtag
(element.textContent.startsWith('#') ||
(element.textContent.startsWith("#") ||
// or a #<a>
element.previousSibling?.textContent?.[
element.previousSibling.textContent.length - 1
] === '#')
] === "#")
);
}
@@ -48,11 +48,13 @@ function isNodeLinkHashtag(element: Node): element is HTMLLinkElement {
*/
function uniqueHashtagsWithCaseHandling(hashtags: string[]) {
const groups = groupBy(hashtags, (tag) =>
tag.normalize('NFKD').toLowerCase(),
tag.normalize("NFKD").toLowerCase(),
);
return Object.values(groups).map((tags) => {
if (tags.length === 1) return tags[0];
if (tags.length === 1) {
return tags[0];
}
// The best match is the one where we have the less difference between upper and lower case letter count
const best = minBy(tags, (tag) => {
@@ -72,27 +74,27 @@ function uniqueHashtagsWithCaseHandling(hashtags: string[]) {
// Create the collator once, this is much more efficient
const collator = new Intl.Collator(undefined, {
sensitivity: 'base', // we use this to emulate the ASCII folding done on the server-side, hopefuly more efficiently
sensitivity: "base", // we use this to emulate the ASCII folding done on the server-side, hopefuly more efficiently
});
function localeAwareInclude(collection: string[], value: string) {
const normalizedValue = value.normalize('NFKC');
const normalizedValue = value.normalize("NFKC");
return !!collection.find(
(item) => collator.compare(item.normalize('NFKC'), normalizedValue) === 0,
(item) => collator.compare(item.normalize("NFKC"), normalizedValue) === 0,
);
}
// We use an intermediate function here to make it easier to test
export function computeHashtagBarForStatus(status: StatusLike): {
statusContentProps: { statusContent: string };
hashtagsInBar: string[];
statusContentProps: { statusContent: string },
hashtagsInBar: string[],
} {
let statusContent = getStatusContent(status);
const tagNames = status
.get('tags')
.map((tag) => tag.get('name'))
.get("tags")
.map((tag) => tag.get("name"))
.toJS();
// this is returned if we stop the processing early, it does not change what is displayed
@@ -102,24 +104,30 @@ export function computeHashtagBarForStatus(status: StatusLike): {
};
// return early if this status does not have any tags
if (tagNames.length === 0) return defaultResult;
if (tagNames.length === 0) {
return defaultResult;
}
const template = document.createElement('template');
const template = document.createElement("template");
template.innerHTML = statusContent.trim();
const lastChild = template.content.lastChild;
if (!lastChild || lastChild.nodeType === Node.TEXT_NODE) return defaultResult;
if (!lastChild || lastChild.nodeType === Node.TEXT_NODE) {
return defaultResult;
}
template.content.removeChild(lastChild);
const contentWithoutLastLine = template;
// First, try to parse
const contentHashtags = Array.from(
contentWithoutLastLine.content.querySelectorAll<HTMLLinkElement>('a[href]'),
contentWithoutLastLine.content.querySelectorAll<HTMLLinkElement>("a[href]"),
).reduce<string[]>((result, link) => {
if (isNodeLinkHashtag(link)) {
if (link.textContent) result.push(normalizeHashtag(link.textContent));
if (link.textContent) {
result.push(normalizeHashtag(link.textContent));
}
}
return result;
}, []);
@@ -129,7 +137,7 @@ export function computeHashtagBarForStatus(status: StatusLike): {
// try to see if the last line is only hashtags
let onlyHashtags = true;
const normalizedTagNames = tagNames.map((tag) => tag.normalize('NFKC'));
const normalizedTagNames = tagNames.map((tag) => tag.normalize("NFKC"));
Array.from(lastChild.childNodes).forEach((node) => {
if (isNodeLinkHashtag(node) && node.textContent) {
@@ -141,9 +149,10 @@ export function computeHashtagBarForStatus(status: StatusLike): {
return;
}
if (!localeAwareInclude(contentHashtags, normalized))
// only add it if it does not appear in the rest of the content
// only add it if it does not appear in the rest of the content
if (!localeAwareInclude(contentHashtags, normalized)) {
lastLineHashtags.push(normalized);
}
} else if (node.nodeType !== Node.TEXT_NODE || node.nodeValue?.trim()) {
// not a space
onlyHashtags = false;
@@ -151,7 +160,7 @@ export function computeHashtagBarForStatus(status: StatusLike): {
});
const hashtagsInBar = tagNames.filter((tag) => {
const normalizedTag = tag.normalize('NFKC');
const normalizedTag = tag.normalize("NFKC");
// the tag does not appear at all in the status content, it is an out-of-band tag
return (
!localeAwareInclude(contentHashtags, normalizedTag) &&
@@ -160,8 +169,8 @@ export function computeHashtagBarForStatus(status: StatusLike): {
});
const isOnlyOneLine = contentWithoutLastLine.content.childElementCount === 0;
const hasMedia = status.get('media_attachments').size > 0;
const hasSpoiler = !!status.get('spoiler_text');
const hasMedia = status.get("media_attachments").size > 0;
const hasSpoiler = !!status.get("spoiler_text");
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- due to https://github.com/microsoft/TypeScript/issues/9998
if (onlyHashtags && ((hasMedia && !hasSpoiler) || !isOnlyOneLine)) {
@@ -179,25 +188,8 @@ export function computeHashtagBarForStatus(status: StatusLike): {
};
}
/**
* This function will process a status to, at the same time (avoiding parsing it twice):
* - build the HashtagBar for this status
* - remove the last-line hashtags from the status content
* @param status The status to process
* @returns Props to be passed to the <StatusContent> component, and the hashtagBar to render
*/
export function getHashtagBarForStatus(status: StatusLike) {
const { statusContentProps, hashtagsInBar } =
computeHashtagBarForStatus(status);
return {
statusContentProps,
hashtagBar: <HashtagBar hashtags={hashtagsInBar} />,
};
}
const HashtagBar: React.FC<{
hashtags: string[];
hashtags: string[],
}> = ({ hashtags }) => {
const [expanded, setExpanded] = useState(false);
const handleClick = useCallback(() => {
@@ -232,3 +224,21 @@ const HashtagBar: React.FC<{
</div>
);
};
/**
* This function will process a status to, at the same time (avoiding parsing it twice):
* - build the HashtagBar for this status
* - remove the last-line hashtags from the status content
* @param status The status to process
* @returns Props to be passed to the <StatusContent> component, and the hashtagBar to render
*/
export function getHashtagBarForStatus(status: StatusLike) {
const { statusContentProps, hashtagsInBar } =
computeHashtagBarForStatus(status);
return {
statusContentProps,
hashtagBar: <HashtagBar hashtags={hashtagsInBar} />,
};
}

View File

@@ -1,10 +1,10 @@
import classNames from 'classnames';
import classNames from "classnames";
interface Props extends React.HTMLAttributes<HTMLImageElement> {
id: string;
className?: string;
fixedWidth?: boolean;
children?: never;
id: string,
className?: string,
fixedWidth?: boolean,
children?: never,
}
export const Icon: React.FC<Props> = ({
@@ -14,7 +14,7 @@ export const Icon: React.FC<Props> = ({
...other
}) => (
<i
className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
className={classNames("fa", `fa-${id}`, className, { "fa-fw": fixedWidth })}
{...other}
/>
);

View File

@@ -1,35 +1,35 @@
import { PureComponent } from 'react';
import { PureComponent } from "react";
import classNames from 'classnames';
import classNames from "classnames";
import { AnimatedNumber } from './animated_number';
import { Icon } from './icon';
import { AnimatedNumber } from "./animated_number";
import { Icon } from "./icon";
interface Props {
className?: string;
title: string;
icon: string;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
onMouseDown?: React.MouseEventHandler<HTMLButtonElement>;
onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
onKeyPress?: React.KeyboardEventHandler<HTMLButtonElement>;
size: number;
active: boolean;
expanded?: boolean;
style?: React.CSSProperties;
activeStyle?: React.CSSProperties;
disabled: boolean;
inverted?: boolean;
animate: boolean;
overlay: boolean;
tabIndex: number;
counter?: number;
href?: string;
ariaHidden: boolean;
className?: string,
title: string,
icon: string,
onClick?: React.MouseEventHandler<HTMLButtonElement>,
onMouseDown?: React.MouseEventHandler<HTMLButtonElement>,
onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>,
onKeyPress?: React.KeyboardEventHandler<HTMLButtonElement>,
size: number,
active: boolean,
expanded?: boolean,
style?: React.CSSProperties,
activeStyle?: React.CSSProperties,
disabled: boolean,
inverted?: boolean,
animate: boolean,
overlay: boolean,
tabIndex: number,
counter?: number,
href?: string,
ariaHidden: boolean,
}
interface States {
activate: boolean;
deactivate: boolean;
activate: boolean,
deactivate: boolean,
}
export class IconButton extends PureComponent<Props, States> {
static defaultProps = {
@@ -48,7 +48,9 @@ export class IconButton extends PureComponent<Props, States> {
};
UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (!nextProps.animate) return;
if (!nextProps.animate) {
return;
}
if (this.props.active && !nextProps.active) {
this.setState({ activate: false, deactivate: true });
@@ -65,12 +67,6 @@ export class IconButton extends PureComponent<Props, States> {
}
};
handleKeyPress: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
if (this.props.onKeyPress && !this.props.disabled) {
this.props.onKeyPress(e);
}
};
handleMouseDown: React.MouseEventHandler<HTMLButtonElement> = (e) => {
if (!this.props.disabled && this.props.onMouseDown) {
this.props.onMouseDown(e);
@@ -110,24 +106,24 @@ export class IconButton extends PureComponent<Props, States> {
const { activate, deactivate } = this.state;
const classes = classNames(className, 'icon-button', {
const classes = classNames(className, "icon-button", {
active,
disabled,
inverted,
activate,
deactivate,
overlayed: overlay,
'icon-button--with-counter': typeof counter !== 'undefined',
"icon-button--with-counter": typeof counter !== "undefined",
});
if (typeof counter !== 'undefined') {
style.width = 'auto';
if (typeof counter !== "undefined") {
style.width = "auto";
}
let contents = (
<>
<Icon id={icon} fixedWidth aria-hidden='true' />{' '}
{typeof counter !== 'undefined' && (
<Icon id={icon} fixedWidth aria-hidden='true' />{" "}
{typeof counter !== "undefined" && (
<span className='icon-button__counter'>
<AnimatedNumber value={counter} />
</span>
@@ -154,7 +150,6 @@ export class IconButton extends PureComponent<Props, States> {
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleKeyDown}
onKeyPress={this.handleKeyPress}
style={style}
tabIndex={tabIndex}
disabled={disabled}

View File

@@ -1,12 +1,12 @@
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);
interface Props {
id: string;
count: number;
issueBadge: boolean;
className: string;
id: string,
count: number,
issueBadge: boolean,
className: string,
}
export const IconWithBadge: React.FC<Props> = ({
id,

View File

@@ -1,10 +1,10 @@
import { PureComponent } from 'react';
import { PureComponent } from "react";
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import ImmutablePropTypes from "react-immutable-proptypes";
import { connect } from "react-redux";
import { Avatar } from 'mastodon/components/avatar';
import { makeGetAccount } from 'mastodon/selectors';
import { Avatar } from "mastodon/components/avatar";
import { makeGetAccount } from "mastodon/selectors";
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
@@ -27,7 +27,7 @@ class InlineAccount extends PureComponent {
return (
<span className='inline-account'>
<Avatar size={13} account={account} /> <strong>{account.get('username')}</strong>
<Avatar size={13} account={account} /> <strong>{account.get("username")}</strong>
</span>
);
}

View File

@@ -1,11 +1,11 @@
import PropTypes from 'prop-types';
import { cloneElement, Component } from 'react';
import PropTypes from "prop-types";
import { cloneElement, Component } from "react";
import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
import getRectFromEntry from "../features/ui/util/get_rect_from_entry";
import scheduleIdleTask from "../features/ui/util/schedule_idle_task";
// Diff these props in the "unrendered" state
const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
const updateOnPropsForUnrendered = ["id", "index", "listLength", "cachedHeight"];
export default class IntersectionObserverArticle extends Component {
@@ -112,7 +112,7 @@ export default class IntersectionObserverArticle extends Component {
ref={this.handleRef}
aria-posinset={index + 1}
aria-setsize={listLength}
style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: "hidden" }}
data-id={id}
tabIndex={-1}
>

View File

@@ -1,17 +1,17 @@
import { useCallback } from 'react';
import { useCallback } from "react";
import { useIntl, defineMessages } from 'react-intl';
import { useIntl, defineMessages } from "react-intl";
import { Icon } from 'mastodon/components/icon';
import { Icon } from "mastodon/components/icon";
const messages = defineMessages({
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
load_more: { id: "status.load_more", defaultMessage: "Load more" },
});
interface Props {
disabled: boolean;
maxId: string;
onClick: (maxId: string) => void;
disabled: boolean,
maxId: string,
onClick: (maxId: string) => void,
}
export const LoadGap: React.FC<Props> = ({ disabled, maxId, onClick }) => {

View File

@@ -1,9 +1,9 @@
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from "react-intl";
interface Props {
onClick: (event: React.MouseEvent) => void;
disabled?: boolean;
visible?: boolean;
onClick: (event: React.MouseEvent) => void,
disabled?: boolean,
visible?: boolean,
}
export const LoadMore: React.FC<Props> = ({
onClick,
@@ -15,7 +15,7 @@ export const LoadMore: React.FC<Props> = ({
type='button'
className='load-more'
disabled={disabled || !visible}
style={{ visibility: visible ? 'visible' : 'hidden' }}
style={{ visibility: visible ? "visible" : "hidden" }}
onClick={onClick}
>
<FormattedMessage id='status.load_more' defaultMessage='Load more' />

View File

@@ -1,8 +1,8 @@
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from "react-intl";
interface Props {
onClick: (event: React.MouseEvent) => void;
count: number;
onClick: (event: React.MouseEvent) => void,
count: number,
}
export const LoadPending: React.FC<Props> = ({ onClick, count }) => {

View File

@@ -1,4 +1,4 @@
import { CircularProgress } from './circular_progress';
import { CircularProgress } from "./circular_progress";
export const LoadingIndicator: React.FC = () => (
<div className='loading-indicator'>

View File

@@ -1,12 +1,12 @@
import PropTypes from 'prop-types';
import PropTypes from "prop-types";
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from "react-immutable-proptypes";
import ImmutablePureComponent from "react-immutable-pure-component";
import noop from 'lodash/noop';
import noop from "lodash/noop";
import Bundle from 'mastodon/features/ui/components/bundle';
import { MediaGallery, Video, Audio } from 'mastodon/features/ui/util/async-components';
import Bundle from "mastodon/features/ui/components/bundle";
import { MediaGallery, Video, Audio } from "mastodon/features/ui/util/async-components";
export default class MediaAttachments extends ImmutablePureComponent {
@@ -23,7 +23,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
};
updateOnProps = [
'status',
"status",
];
renderLoadingMediaGallery = () => {
@@ -52,53 +52,53 @@ export default class MediaAttachments extends ImmutablePureComponent {
render () {
const { status, width, height } = this.props;
const mediaAttachments = status.get('media_attachments');
const language = status.getIn(['language', 'translation']) || status.get('language') || this.props.lang;
const mediaAttachments = status.get("media_attachments");
const language = status.getIn(["language", "translation"]) || status.get("language") || this.props.lang;
if (mediaAttachments.size === 0) {
return null;
}
if (mediaAttachments.getIn([0, 'type']) === 'audio') {
if (mediaAttachments.getIn([0, "type"]) === "audio") {
const audio = mediaAttachments.get(0);
const description = audio.getIn(['translation', 'description']) || audio.get('description');
const description = audio.getIn(["translation", "description"]) || audio.get("description");
return (
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
{Component => (
<Component
src={audio.get('url')}
src={audio.get("url")}
alt={description}
lang={language}
width={width}
height={height}
poster={audio.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={audio.getIn(['meta', 'colors', 'background'])}
foregroundColor={audio.getIn(['meta', 'colors', 'foreground'])}
accentColor={audio.getIn(['meta', 'colors', 'accent'])}
duration={audio.getIn(['meta', 'original', 'duration'], 0)}
poster={audio.get("preview_url") || status.getIn(["account", "avatar_static"])}
backgroundColor={audio.getIn(["meta", "colors", "background"])}
foregroundColor={audio.getIn(["meta", "colors", "foreground"])}
accentColor={audio.getIn(["meta", "colors", "accent"])}
duration={audio.getIn(["meta", "original", "duration"], 0)}
/>
)}
</Bundle>
);
} else if (mediaAttachments.getIn([0, 'type']) === 'video') {
} else if (mediaAttachments.getIn([0, "type"]) === "video") {
const video = mediaAttachments.get(0);
const description = video.getIn(['translation', 'description']) || video.get('description');
const description = video.getIn(["translation", "description"]) || video.get("description");
return (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
{Component => (
<Component
preview={video.get('preview_url')}
frameRate={video.getIn(['meta', 'original', 'frame_rate'])}
blurhash={video.get('blurhash')}
src={video.get('url')}
preview={video.get("preview_url")}
frameRate={video.getIn(["meta", "original", "frame_rate"])}
blurhash={video.get("blurhash")}
src={video.get("url")}
alt={description}
lang={language}
width={width}
height={height}
inline
sensitive={status.get('sensitive')}
sensitive={status.get("sensitive")}
onOpenVideo={noop}
/>
)}
@@ -111,7 +111,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
<Component
media={mediaAttachments}
lang={language}
sensitive={status.get('sensitive')}
sensitive={status.get("sensitive")}
defaultWidth={width}
height={height}
onOpenMedia={noop}

View File

@@ -1,23 +1,23 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from "react-intl";
import classNames from 'classnames';
import classNames from "classnames";
import { is } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { is } from "immutable";
import ImmutablePropTypes from "react-immutable-proptypes";
import { debounce } from 'lodash';
import { debounce } from "lodash";
import { Blurhash } from 'mastodon/components/blurhash';
import { Blurhash } from "mastodon/components/blurhash";
import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
import { autoPlayGif, displayMedia, useBlurhash } from "../initial_state";
import { IconButton } from './icon_button';
import { IconButton } from "./icon_button";
const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: '{number, plural, one {Hide image} other {Hide images}}' },
toggle_visible: { id: "media_gallery.toggle_visible", defaultMessage: "{number, plural, one {Hide image} other {Hide images}}" },
});
class Item extends PureComponent {
@@ -63,7 +63,7 @@ class Item extends PureComponent {
hoverToPlay () {
const { attachment } = this.props;
return !this.getAutoPlay() && attachment.get('type') === 'gifv';
return !this.getAutoPlay() && attachment.get("type") === "gifv";
}
handleClick = (e) => {
@@ -101,45 +101,45 @@ class Item extends PureComponent {
height = 50;
}
if (attachment.get('description')?.length > 0) {
if (attachment.get("description")?.length > 0) {
badges.push(<span key='alt' className='media-gallery__gifv__label'>ALT</span>);
}
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
const description = attachment.getIn(["translation", "description"]) || attachment.get("description");
if (attachment.get('type') === 'unknown') {
if (attachment.get("type") === "unknown") {
return (
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={description} lang={lang} target='_blank' rel='noopener noreferrer'>
<div className={classNames("media-gallery__item", { standalone, "media-gallery__item--tall": height === 100, "media-gallery__item--wide": width === 100 })} key={attachment.get("id")}>
<a className='media-gallery__item-thumbnail' href={attachment.get("remote_url") || attachment.get("url")} style={{ cursor: "pointer" }} title={description} lang={lang} target='_blank' rel='noopener noreferrer'>
<Blurhash
hash={attachment.get('blurhash')}
hash={attachment.get("blurhash")}
className='media-gallery__preview'
dummy={!useBlurhash}
/>
</a>
</div>
);
} else if (attachment.get('type') === 'image') {
const previewUrl = attachment.get('preview_url');
const previewWidth = attachment.getIn(['meta', 'small', 'width']);
} else if (attachment.get("type") === "image") {
const previewUrl = attachment.get("preview_url");
const previewWidth = attachment.getIn(["meta", "small", "width"]);
const originalUrl = attachment.get('url');
const originalWidth = attachment.getIn(['meta', 'original', 'width']);
const originalUrl = attachment.get("url");
const originalWidth = attachment.getIn(["meta", "original", "width"]);
const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
const hasSize = typeof originalWidth === "number" && typeof previewWidth === "number";
const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null;
const sizes = hasSize && (displayWidth > 0) ? `${displayWidth * (width / 100)}px` : null;
const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
const focusX = attachment.getIn(["meta", "focus", "x"]) || 0;
const focusY = attachment.getIn(["meta", "focus", "y"]) || 0;
const x = ((focusX / 2) + .5) * 100;
const y = ((focusY / -2) + .5) * 100;
thumbnail = (
<a
className='media-gallery__item-thumbnail'
href={attachment.get('remote_url') || originalUrl}
href={attachment.get("remote_url") || originalUrl}
onClick={this.handleClick}
target='_blank'
rel='noopener noreferrer'
@@ -156,20 +156,20 @@ class Item extends PureComponent {
/>
</a>
);
} else if (attachment.get('type') === 'gifv') {
} else if (attachment.get("type") === "gifv") {
const autoPlay = this.getAutoPlay();
badges.push(<span key='gif' className='media-gallery__gifv__label'>GIF</span>);
thumbnail = (
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
<div className={classNames("media-gallery__gifv", { autoplay: autoPlay })}>
<video
className='media-gallery__item-gifv-thumbnail'
aria-label={description}
title={description}
lang={lang}
role='application'
src={attachment.get('url')}
src={attachment.get("url")}
onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
@@ -183,12 +183,12 @@ class Item extends PureComponent {
}
return (
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
<div className={classNames("media-gallery__item", { standalone, "media-gallery__item--tall": height === 100, "media-gallery__item--wide": width === 100 })} key={attachment.get("id")}>
<Blurhash
hash={attachment.get('blurhash')}
hash={attachment.get("blurhash")}
dummy={!useBlurhash}
className={classNames('media-gallery__preview', {
'media-gallery__preview--hidden': visible && this.state.loaded,
className={classNames("media-gallery__preview", {
"media-gallery__preview--hidden": visible && this.state.loaded,
})}
/>
@@ -223,21 +223,21 @@ class MediaGallery extends PureComponent {
};
state = {
visible: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
visible: this.props.visible !== undefined ? this.props.visible : (displayMedia !== "hide_all" && !this.props.sensitive || displayMedia === "show_all"),
width: this.props.defaultWidth,
};
componentDidMount () {
window.addEventListener('resize', this.handleResize, { passive: true });
window.addEventListener("resize", this.handleResize, { passive: true });
}
componentWillUnmount () {
window.removeEventListener('resize', this.handleResize);
window.removeEventListener("resize", this.handleResize);
}
UNSAFE_componentWillReceiveProps (nextProps) {
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) {
this.setState({ visible: nextProps.visible });
}
@@ -286,7 +286,7 @@ class MediaGallery extends PureComponent {
isFullSizeEligible() {
const { media } = this.props;
return media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
return media.size === 1 && media.getIn([0, "meta", "small", "aspect"]);
}
render () {
@@ -299,18 +299,18 @@ class MediaGallery extends PureComponent {
const style = {};
if (this.isFullSizeEligible()) {
style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`;
style.aspectRatio = `${this.props.media.getIn([0, "meta", "small", "aspect"])}`;
} else {
style.aspectRatio = '3 / 2';
style.aspectRatio = "3 / 2";
}
const size = media.take(4).size;
const uncached = media.every(attachment => attachment.get('type') === 'unknown');
const uncached = media.every(attachment => attachment.get("type") === "unknown");
if (this.isFullSizeEligible()) {
children = <Item standalone autoplay={autoplay} onClick={this.handleClick} attachment={media.get(0)} lang={lang} displayWidth={width} visible={visible} />;
} else {
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} lang={lang} size={size} displayWidth={width} visible={visible || uncached} />);
children = media.take(4).map((attachment, i) => <Item key={attachment.get("id")} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} lang={lang} size={size} displayWidth={width} visible={visible || uncached} />);
}
if (uncached) {
@@ -337,7 +337,7 @@ class MediaGallery extends PureComponent {
return (
<div className='media-gallery' style={style} ref={this.handleRef}>
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}>
<div className={classNames("spoiler-button", { "spoiler-button--minified": visible && !uncached, "spoiler-button--click-thru": uncached })}>
{spoilerButton}
</div>

View File

@@ -1,9 +1,9 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import 'wicg-inert';
import { multiply } from 'color-blend';
import { createBrowserHistory } from 'history';
import "wicg-inert";
import { multiply } from "color-blend";
import { createBrowserHistory } from "history";
export default class ModalRoot extends PureComponent {
@@ -25,15 +25,15 @@ export default class ModalRoot extends PureComponent {
activeElement = this.props.children ? document.activeElement : null;
handleKeyUp = (e) => {
if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27)
if ((e.key === "Escape" || e.key === "Esc" || e.keyCode === 27)
&& !!this.props.children) {
this.props.onClose();
}
};
handleKeyDown = (e) => {
if (e.key === 'Tab') {
const focusable = Array.from(this.node.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')).filter((x) => window.getComputedStyle(x).display !== 'none');
if (e.key === "Tab") {
const focusable = Array.from(this.node.querySelectorAll("button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])")).filter((x) => window.getComputedStyle(x).display !== "none");
const index = focusable.indexOf(e.target);
let element;
@@ -53,8 +53,8 @@ export default class ModalRoot extends PureComponent {
};
componentDidMount () {
window.addEventListener('keyup', this.handleKeyUp, false);
window.addEventListener('keydown', this.handleKeyDown, false);
window.addEventListener("keyup", this.handleKeyUp, false);
window.addEventListener("keydown", this.handleKeyDown, false);
this.history = this.context.router ? this.context.router.history : createBrowserHistory();
}
@@ -62,13 +62,13 @@ export default class ModalRoot extends PureComponent {
if (!!nextProps.children && !this.props.children) {
this.activeElement = document.activeElement;
this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true));
this.getSiblings().forEach(sibling => sibling.setAttribute("inert", true));
}
}
componentDidUpdate (prevProps) {
if (!this.props.children && !!prevProps.children) {
this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
this.getSiblings().forEach(sibling => sibling.removeAttribute("inert"));
// Because of the wicg-inert polyfill, the activeElement may not be
// immediately selectable, we have to wait for observers to run, as
@@ -91,14 +91,14 @@ export default class ModalRoot extends PureComponent {
}
componentWillUnmount () {
window.removeEventListener('keyup', this.handleKeyUp);
window.removeEventListener('keydown', this.handleKeyDown);
window.removeEventListener("keyup", this.handleKeyUp);
window.removeEventListener("keydown", this.handleKeyDown);
}
_handleModalOpen () {
this._modalHistoryKey = Date.now();
this.unlistenHistory = this.history.listen((_, action) => {
if (action === 'POP') {
if (action === "POP") {
this.props.onClose();
}
});
@@ -147,7 +147,7 @@ export default class ModalRoot extends PureComponent {
return (
<div className='modal-root' ref={this.setRef}>
<div style={{ pointerEvents: visible ? 'auto' : 'none' }}>
<div style={{ pointerEvents: visible ? "auto" : "none" }}>
<div role='presentation' className='modal-root__overlay' onClick={onClose} style={{ backgroundColor: backgroundColor ? `rgba(${backgroundColor.r}, ${backgroundColor.g}, ${backgroundColor.b}, 0.7)` : null }} />
<div role='dialog' className='modal-root__container'>{children}</div>
</div>

View File

@@ -1,10 +1,10 @@
import { PureComponent } from 'react';
import { PureComponent } from "react";
import { Switch, Route, withRouter } from 'react-router-dom';
import { Switch, Route, withRouter } from "react-router-dom";
import AccountNavigation from 'mastodon/features/account/navigation';
import Trends from 'mastodon/features/getting_started/containers/trends_container';
import { showTrends } from 'mastodon/initial_state';
import AccountNavigation from "mastodon/features/account/navigation";
import Trends from "mastodon/features/getting_started/containers/trends_container";
import { showTrends } from "mastodon/initial_state";
const DefaultNavigation = () => (
showTrends ? (

View File

@@ -1,4 +1,4 @@
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from "react-intl";
export const NotSignedInIndicator: React.FC = () => (
<div className='scrollable scrollable--flex'>

View File

@@ -1,12 +1,12 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from "react-intl";
import { connect } from 'react-redux';
import { connect } from "react-redux";
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
import { Icon } from 'mastodon/components/icon';
import { removePictureInPicture } from "mastodon/actions/picture_in_picture";
import { Icon } from "mastodon/components/icon";
class PictureInPicturePlaceholder extends PureComponent {

View File

@@ -1,38 +1,38 @@
import PropTypes from 'prop-types';
import PropTypes from "prop-types";
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from "react-intl";
import classNames from 'classnames';
import classNames from "classnames";
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from "react-immutable-proptypes";
import ImmutablePureComponent from "react-immutable-pure-component";
import escapeTextContentForBrowser from 'escape-html';
import spring from 'react-motion/lib/spring';
import escapeTextContentForBrowser from "escape-html";
import spring from "react-motion/lib/spring";
import { Icon } from 'mastodon/components/icon';
import emojify from 'mastodon/features/emoji/emoji';
import Motion from 'mastodon/features/ui/util/optional_motion';
import { Icon } from "mastodon/components/icon";
import emojify from "mastodon/features/emoji/emoji";
import Motion from "mastodon/features/ui/util/optional_motion";
import { RelativeTimestamp } from './relative_timestamp';
import { RelativeTimestamp } from "./relative_timestamp";
const messages = defineMessages({
closed: {
id: 'poll.closed',
defaultMessage: 'Closed',
id: "poll.closed",
defaultMessage: "Closed",
},
voted: {
id: 'poll.voted',
defaultMessage: 'You voted for this answer',
id: "poll.voted",
defaultMessage: "You voted for this answer",
},
votes: {
id: 'poll.votes',
defaultMessage: '{votes, plural, one {# vote} other {# votes}}',
id: "poll.votes",
defaultMessage: "{votes, plural, one {# vote} other {# votes}}",
},
});
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
const makeEmojiMap = record => record.get("emojis").reduce((obj, emoji) => {
obj[`:${emoji.get("shortcode")}:`] = emoji.toJS();
return obj;
}, {});
@@ -58,8 +58,8 @@ class Poll extends ImmutablePureComponent {
static getDerivedStateFromProps (props, state) {
const { poll } = props;
const expires_at = poll.get('expires_at');
const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < Date.now();
const expires_at = poll.get("expires_at");
const expired = poll.get("expired") || expires_at !== null && (new Date(expires_at)).getTime() < Date.now();
return (expired === state.expired) ? null : { expired };
}
@@ -79,7 +79,7 @@ class Poll extends ImmutablePureComponent {
const { poll } = this.props;
clearTimeout(this._timer);
if (!this.state.expired) {
const delay = (new Date(poll.get('expires_at'))).getTime() - Date.now();
const delay = (new Date(poll.get("expires_at"))).getTime() - Date.now();
this._timer = setTimeout(() => {
this.setState({ expired: true });
}, delay);
@@ -87,7 +87,7 @@ class Poll extends ImmutablePureComponent {
}
_toggleOption = value => {
if (this.props.poll.get('multiple')) {
if (this.props.poll.get("multiple")) {
const tmp = { ...this.state.selected };
if (tmp[value]) {
delete tmp[value];
@@ -107,8 +107,8 @@ class Poll extends ImmutablePureComponent {
};
handleOptionKeyPress = (e) => {
if (e.key === 'Enter' || e.key === ' ') {
this._toggleOption(e.target.getAttribute('data-index'));
if (e.key === "Enter" || e.key === " ") {
this._toggleOption(e.target.getAttribute("data-index"));
e.stopPropagation();
e.preventDefault();
}
@@ -136,14 +136,14 @@ class Poll extends ImmutablePureComponent {
renderOption (option, optionIndex, showResults) {
const { poll, lang, disabled, intl } = this.props;
const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
const percent = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100;
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count'));
const pollVotesCount = poll.get("voters_count") || poll.get("votes_count");
const percent = pollVotesCount === 0 ? 0 : (option.get("votes_count") / pollVotesCount) * 100;
const leading = poll.get("options").filterNot(other => other.get("title") === option.get("title")).every(other => option.get("votes_count") >= other.get("votes_count"));
const active = !!this.state.selected[`${optionIndex}`];
const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));
const voted = option.get("voted") || (poll.get("own_votes") && poll.get("own_votes").includes(optionIndex));
const title = option.getIn(['translation', 'title']) || option.get('title');
let titleHtml = option.getIn(['translation', 'titleHtml']) || option.get('titleHtml');
const title = option.getIn(["translation", "title"]) || option.get("title");
let titleHtml = option.getIn(["translation", "titleHtml"]) || option.get("titleHtml");
if (!titleHtml) {
const emojiMap = makeEmojiMap(poll);
@@ -151,11 +151,11 @@ class Poll extends ImmutablePureComponent {
}
return (
<li key={option.get('title')}>
<label className={classNames('poll__option', { selectable: !showResults })}>
<li key={option.get("title")}>
<label className={classNames("poll__option", { selectable: !showResults })}>
<input
name='vote-options'
type={poll.get('multiple') ? 'checkbox' : 'radio'}
type={poll.get("multiple") ? "checkbox" : "radio"}
value={optionIndex}
checked={active}
onChange={this.handleOptionChange}
@@ -164,9 +164,9 @@ class Poll extends ImmutablePureComponent {
{!showResults && (
<span
className={classNames('poll__input', { checkbox: poll.get('multiple'), active })}
className={classNames("poll__input", { checkbox: poll.get("multiple"), active })}
tabIndex={0}
role={poll.get('multiple') ? 'checkbox' : 'radio'}
role={poll.get("multiple") ? "checkbox" : "radio"}
onKeyPress={this.handleOptionKeyPress}
aria-checked={active}
aria-label={title}
@@ -178,7 +178,7 @@ class Poll extends ImmutablePureComponent {
<span
className='poll__number'
title={intl.formatMessage(messages.votes, {
votes: option.get('votes_count'),
votes: option.get("votes_count"),
})}
>
{Math.round(percent)}%
@@ -199,7 +199,7 @@ class Poll extends ImmutablePureComponent {
{showResults && (
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
{({ width }) =>
<span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
<span className={classNames("poll__chart", { leading })} style={{ width: `${width}%` }} />
}
</Motion>
)}
@@ -215,22 +215,22 @@ class Poll extends ImmutablePureComponent {
return null;
}
const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
const showResults = poll.get('voted') || revealed || expired;
const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get("expires_at")} futureDate />;
const showResults = poll.get("voted") || revealed || expired;
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
let votesCount = null;
if (poll.get('voters_count') !== null && poll.get('voters_count') !== undefined) {
votesCount = <FormattedMessage id='poll.total_people' defaultMessage='{count, plural, one {# person} other {# people}}' values={{ count: poll.get('voters_count') }} />;
if (poll.get("voters_count") !== null && poll.get("voters_count") !== undefined) {
votesCount = <FormattedMessage id='poll.total_people' defaultMessage='{count, plural, one {# person} other {# people}}' values={{ count: poll.get("voters_count") }} />;
} else {
votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />;
votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get("votes_count") }} />;
}
return (
<div className='poll'>
<ul>
{poll.get('options').map((option, i) => this.renderOption(option, i, showResults))}
{poll.get("options").map((option, i) => this.renderOption(option, i, showResults))}
</ul>
<div className='poll__footer'>
@@ -238,7 +238,7 @@ class Poll extends ImmutablePureComponent {
{!showResults && <><button className='poll__link' onClick={this.handleReveal}><FormattedMessage id='poll.reveal' defaultMessage='See results' /></button> · </>}
{showResults && !this.props.disabled && <><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </>}
{votesCount}
{poll.get('expires_at') && <> · {timeRemaining}</>}
{poll.get("expires_at") && <> · {timeRemaining}</>}
</div>
</div>
);

View File

@@ -1,11 +1,11 @@
import classNames from 'classnames';
import classNames from "classnames";
interface Props {
value: string;
checked: boolean;
name: string;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
label: React.ReactNode;
value: string,
checked: boolean,
name: string,
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void,
label: React.ReactNode,
}
export const RadioButton: React.FC<Props> = ({
@@ -25,7 +25,7 @@ export const RadioButton: React.FC<Props> = ({
onChange={onChange}
/>
<span className={classNames('radio-button__input', { checked })} />
<span className={classNames("radio-button__input", { checked })} />
<span>{label}</span>
</label>

View File

@@ -1,6 +1,6 @@
import { FormattedMessage } from 'react-intl';
import { FormattedMessage } from "react-intl";
import illustration from 'mastodon/../images/elephant_ui_working.svg';
import illustration from "mastodon/../images/elephant_ui_working.svg";
const RegenerationIndicator = () => (
<div className='regeneration-indicator'>

View File

@@ -1,69 +1,69 @@
import { Component } from 'react';
import { Component } from "react";
import type { IntlShape } from 'react-intl';
import { injectIntl, defineMessages } from 'react-intl';
import { type IntlShape } from "react-intl";
import { injectIntl, defineMessages } from "react-intl";
const messages = defineMessages({
today: { id: 'relative_time.today', defaultMessage: 'today' },
just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
today: { id: "relative_time.today", defaultMessage: "today" },
just_now: { id: "relative_time.just_now", defaultMessage: "now" },
just_now_full: {
id: 'relative_time.full.just_now',
defaultMessage: 'just now',
id: "relative_time.full.just_now",
defaultMessage: "just now",
},
seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' },
seconds: { id: "relative_time.seconds", defaultMessage: "{number}s" },
seconds_full: {
id: 'relative_time.full.seconds',
defaultMessage: '{number, plural, one {# second} other {# seconds}} ago',
id: "relative_time.full.seconds",
defaultMessage: "{number, plural, one {# second} other {# seconds}} ago",
},
minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
minutes: { id: "relative_time.minutes", defaultMessage: "{number}m" },
minutes_full: {
id: 'relative_time.full.minutes',
defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago',
id: "relative_time.full.minutes",
defaultMessage: "{number, plural, one {# minute} other {# minutes}} ago",
},
hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
hours: { id: "relative_time.hours", defaultMessage: "{number}h" },
hours_full: {
id: 'relative_time.full.hours',
defaultMessage: '{number, plural, one {# hour} other {# hours}} ago',
id: "relative_time.full.hours",
defaultMessage: "{number, plural, one {# hour} other {# hours}} ago",
},
days: { id: 'relative_time.days', defaultMessage: '{number}d' },
days: { id: "relative_time.days", defaultMessage: "{number}d" },
days_full: {
id: 'relative_time.full.days',
defaultMessage: '{number, plural, one {# day} other {# days}} ago',
id: "relative_time.full.days",
defaultMessage: "{number, plural, one {# day} other {# days}} ago",
},
moments_remaining: {
id: 'time_remaining.moments',
defaultMessage: 'Moments remaining',
id: "time_remaining.moments",
defaultMessage: "Moments remaining",
},
seconds_remaining: {
id: 'time_remaining.seconds',
defaultMessage: '{number, plural, one {# second} other {# seconds}} left',
id: "time_remaining.seconds",
defaultMessage: "{number, plural, one {# second} other {# seconds}} left",
},
minutes_remaining: {
id: 'time_remaining.minutes',
defaultMessage: '{number, plural, one {# minute} other {# minutes}} left',
id: "time_remaining.minutes",
defaultMessage: "{number, plural, one {# minute} other {# minutes}} left",
},
hours_remaining: {
id: 'time_remaining.hours',
defaultMessage: '{number, plural, one {# hour} other {# hours}} left',
id: "time_remaining.hours",
defaultMessage: "{number, plural, one {# hour} other {# hours}} left",
},
days_remaining: {
id: 'time_remaining.days',
defaultMessage: '{number, plural, one {# day} other {# days}} left',
id: "time_remaining.days",
defaultMessage: "{number, plural, one {# day} other {# days}} left",
},
});
const dateFormatOptions = {
hour12: false,
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
year: "numeric",
month: "short",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
} as const;
const shortDateFormatOptions = {
month: 'short',
day: 'numeric',
month: "short",
day: "numeric",
} as const;
const SECOND = 1000;
@@ -77,25 +77,25 @@ const selectUnits = (delta: number) => {
const absDelta = Math.abs(delta);
if (absDelta < MINUTE) {
return 'second';
return "second";
} else if (absDelta < HOUR) {
return 'minute';
return "minute";
} else if (absDelta < DAY) {
return 'hour';
return "hour";
}
return 'day';
return "day";
};
const getUnitDelay = (units: string) => {
switch (units) {
case 'second':
case "second":
return SECOND;
case 'minute':
case "minute":
return MINUTE;
case 'hour':
case "hour":
return HOUR;
case 'day':
case "day":
return DAY;
default:
return MAX_DELAY;
@@ -147,7 +147,7 @@ export const timeAgoString = (
} else {
relativeTime = intl.formatDate(date, {
...shortDateFormatOptions,
year: 'numeric',
year: "numeric",
});
}
@@ -190,14 +190,14 @@ const timeRemainingString = (
};
interface Props {
intl: IntlShape;
timestamp: string;
year: number;
futureDate?: boolean;
short?: boolean;
intl: IntlShape,
timestamp: string,
year: number,
futureDate?: boolean,
short?: boolean,
}
interface States {
now: number;
now: number,
}
class RelativeTimestamp extends Component<Props, States> {
state = {
@@ -260,7 +260,7 @@ class RelativeTimestamp extends Component<Props, States> {
render() {
const { timestamp, intl, year, futureDate, short } = this.props;
const timeGiven = timestamp.includes('T');
const timeGiven = timestamp.includes("T");
const date = new Date(timestamp);
const relativeTime = futureDate
? timeRemainingString(intl, date, this.state.now, timeGiven)

View File

@@ -1,14 +1,14 @@
import type { PropsWithChildren } from 'react';
import React from 'react';
import { type PropsWithChildren } from "react";
import React from "react";
import { createBrowserHistory } from 'history';
import { Router as OriginalRouter } from 'react-router';
import { createBrowserHistory } from "history";
import { Router as OriginalRouter } from "react-router";
import { layoutFromWindow } from 'mastodon/is_mobile';
import { layoutFromWindow } from "mastodon/is_mobile";
interface MastodonLocationState {
fromMastodon?: boolean;
mastodonModalKey?: string;
fromMastodon?: boolean,
mastodonModalKey?: string,
}
const browserHistory = createBrowserHistory<
@@ -21,7 +21,7 @@ browserHistory.push = (path: string, state?: MastodonLocationState) => {
state = state ?? {};
state.fromMastodon = true;
if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
if (layoutFromWindow() === "multi-column" && !path.startsWith("/deck")) {
originalPush(`/deck${path}`, state);
} else {
originalPush(path, state);
@@ -34,7 +34,7 @@ browserHistory.replace = (path: string, state?: MastodonLocationState) => {
state.fromMastodon = true;
}
if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
if (layoutFromWindow() === "multi-column" && !path.startsWith("/deck")) {
originalReplace(`/deck${path}`, state);
} else {
originalReplace(path, state);

View File

@@ -1,23 +1,23 @@
import PropTypes from 'prop-types';
import { Children, cloneElement, PureComponent } from 'react';
import PropTypes from "prop-types";
import { Children, cloneElement, PureComponent } from "react";
import classNames from 'classnames';
import classNames from "classnames";
import { List as ImmutableList } from 'immutable';
import { connect } from 'react-redux';
import { List as ImmutableList } from "immutable";
import { connect } from "react-redux";
import { supportsPassiveEvents } from 'detect-passive-events';
import { throttle } from 'lodash';
import { supportsPassiveEvents } from "detect-passive-events";
import { throttle } from "lodash";
import ScrollContainer from 'mastodon/containers/scroll_container';
import ScrollContainer from "mastodon/containers/scroll_container";
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import IntersectionObserverArticleContainer from "../containers/intersection_observer_article_container";
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from "../features/ui/util/fullscreen";
import IntersectionObserverWrapper from "../features/ui/util/intersection_observer_wrapper";
import { LoadMore } from './load_more';
import { LoadPending } from './load_pending';
import { LoadingIndicator } from './loading_indicator';
import { LoadMore } from "./load_more";
import { LoadPending } from "./load_pending";
import { LoadingIndicator } from "./loading_indicator";
const MOUSE_IDLE_DELAY = 300;
@@ -236,7 +236,7 @@ class ScrollableList extends PureComponent {
attachIntersectionObserver () {
let nodeOptions = {
root: this.node,
rootMargin: '300% 0px',
rootMargin: "300% 0px",
};
this.intersectionObserverWrapper
@@ -249,21 +249,21 @@ class ScrollableList extends PureComponent {
attachScrollListener () {
if (this.props.bindToDocument) {
document.addEventListener('scroll', this.handleScroll);
document.addEventListener('wheel', this.handleWheel, listenerOptions);
document.addEventListener("scroll", this.handleScroll);
document.addEventListener("wheel", this.handleWheel, listenerOptions);
} else {
this.node.addEventListener('scroll', this.handleScroll);
this.node.addEventListener('wheel', this.handleWheel, listenerOptions);
this.node.addEventListener("scroll", this.handleScroll);
this.node.addEventListener("wheel", this.handleWheel, listenerOptions);
}
}
detachScrollListener () {
if (this.props.bindToDocument) {
document.removeEventListener('scroll', this.handleScroll);
document.removeEventListener('wheel', this.handleWheel, listenerOptions);
document.removeEventListener("scroll", this.handleScroll);
document.removeEventListener("wheel", this.handleWheel, listenerOptions);
} else {
this.node.removeEventListener('scroll', this.handleScroll);
this.node.removeEventListener('wheel', this.handleWheel, listenerOptions);
this.node.removeEventListener("scroll", this.handleScroll);
this.node.removeEventListener("wheel", this.handleWheel, listenerOptions);
}
}
@@ -324,7 +324,7 @@ class ScrollableList extends PureComponent {
);
} else if (isLoading || childrenCount > 0 || numPending > 0 || hasMore || !emptyMessage) {
scrollableArea = (
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
<div className={classNames("scrollable", { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
<div role='feed' className='item-list'>
{prepend}
@@ -356,7 +356,7 @@ class ScrollableList extends PureComponent {
);
} else {
scrollableArea = (
<div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef}>
<div className={classNames("scrollable scrollable--flex", { fullscreen })} ref={this.setRef}>
{alwaysPrepend && prepend}
<div className='empty-column-indicator'>

View File

@@ -1,25 +1,25 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { FormattedMessage, defineMessages, injectIntl } from "react-intl";
import { Link } from 'react-router-dom';
import { Link } from "react-router-dom";
import { connect } from 'react-redux';
import { connect } from "react-redux";
import { fetchServer } from 'mastodon/actions/server';
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton';
import Account from 'mastodon/containers/account_container';
import { domain } from 'mastodon/initial_state';
import { fetchServer } from "mastodon/actions/server";
import { ServerHeroImage } from "mastodon/components/server_hero_image";
import { ShortNumber } from "mastodon/components/short_number";
import { Skeleton } from "mastodon/components/skeleton";
import Account from "mastodon/containers/account_container";
import { domain } from "mastodon/initial_state";
const messages = defineMessages({
aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' },
aboutActiveUsers: { id: "server_banner.about_active_users", defaultMessage: "People using this server during the last 30 days (Monthly Active Users)" },
});
const mapStateToProps = state => ({
server: state.getIn(['server', 'server']),
server: state.getIn(["server", "server"]),
});
class ServerBanner extends PureComponent {
@@ -37,15 +37,15 @@ class ServerBanner extends PureComponent {
render () {
const { server, intl } = this.props;
const isLoading = server.get('isLoading');
const isLoading = server.get("isLoading");
return (
<div className='server-banner'>
<div className='server-banner__introduction'>
<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' rel="noreferrer">Mastodon</a> }} />
</div>
<ServerHeroImage 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'>
{isLoading ? (
@@ -56,14 +56,14 @@ class ServerBanner extends PureComponent {
<br />
<Skeleton width='70%' />
</>
) : server.get('description')}
) : server.get("description")}
</div>
<div className='server-banner__meta'>
<div className='server-banner__meta__column'>
<h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
<Account id={server.getIn(['contact', 'account', 'id'])} size={36} minimal />
<Account id={server.getIn(["contact", "account", "id"])} size={36} minimal />
</div>
<div className='server-banner__meta__column'>
@@ -77,7 +77,7 @@ class ServerBanner extends PureComponent {
</>
) : (
<>
<strong className='server-banner__number'><ShortNumber value={server.getIn(['usage', 'users', 'active_month'])} /></strong>
<strong className='server-banner__number'><ShortNumber value={server.getIn(["usage", "users", "active_month"])} /></strong>
<br />
<span className='server-banner__number-label' title={intl.formatMessage(messages.aboutActiveUsers)}><FormattedMessage id='server_banner.active_users' defaultMessage='active users' /></span>
</>

View File

@@ -1,14 +1,14 @@
import { useCallback, useState } from 'react';
import { useCallback, useState } from "react";
import classNames from 'classnames';
import classNames from "classnames";
import { Blurhash } from './blurhash';
import { Blurhash } from "./blurhash";
interface Props {
src: string;
srcSet?: string;
blurhash?: string;
className?: string;
src: string,
srcSet?: string,
blurhash?: string,
className?: string,
}
export const ServerHeroImage: React.FC<Props> = ({
@@ -25,7 +25,7 @@ export const ServerHeroImage: React.FC<Props> = ({
return (
<div
className={classNames('image', { loaded }, className)}
className={classNames("image", { loaded }, className)}
role='presentation'
>
{blurhash && <Blurhash hash={blurhash} className='image__preview' />}

View File

@@ -1,48 +1,19 @@
import { memo } from 'react';
import React, { memo } from "react";
import { FormattedMessage, FormattedNumber } from "react-intl";
import { FormattedMessage, FormattedNumber } from 'react-intl';
import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers';
import { toShortNumber, pluralReady, DECIMAL_UNITS } from "../utils/numbers";
type ShortNumberRenderer = (
displayNumber: JSX.Element,
displayNumber: React.JSX.Element,
pluralReady: number,
) => JSX.Element;
) => React.JSX.Element;
interface ShortNumberProps {
value: number;
renderer?: ShortNumberRenderer;
children?: ShortNumberRenderer;
value: number,
renderer?: ShortNumberRenderer,
children?: ShortNumberRenderer,
}
export const ShortNumberRenderer: React.FC<ShortNumberProps> = ({
value,
renderer,
children,
}) => {
const shortNumber = toShortNumber(value);
const [, division] = shortNumber;
if (children && renderer) {
console.warn(
'Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.',
);
}
const customRenderer = children ?? renderer ?? null;
const displayNumber = <ShortNumberCounter value={shortNumber} />;
return (
customRenderer?.(displayNumber, pluralReady(value, division)) ??
displayNumber
);
};
export const ShortNumber = memo(ShortNumberRenderer);
interface ShortNumberCounterProps {
value: number[];
}
const ShortNumberCounter: React.FC<ShortNumberCounterProps> = ({ value }) => {
const [rawNumber, unit, maxFractionDigits = 0] = value;
@@ -88,3 +59,33 @@ const ShortNumberCounter: React.FC<ShortNumberCounterProps> = ({ value }) => {
return count;
}
};
export const ShortNumberRenderer: React.FC<ShortNumberProps> = ({
value,
renderer,
children,
}) => {
const shortNumber = toShortNumber(value);
const [, division] = shortNumber;
if (children && renderer) {
console.warn(
"Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.",
);
}
const customRenderer = children ?? renderer ?? null;
const displayNumber = <ShortNumberCounter value={shortNumber} />;
return (
customRenderer?.(displayNumber, pluralReady(value, division)) ??
displayNumber
);
};
export const ShortNumber = memo(ShortNumberRenderer);
interface ShortNumberCounterProps {
value: number[],
}

View File

@@ -1,6 +1,6 @@
interface Props {
width?: number | string;
height?: number | string;
width?: number | string,
height?: number | string,
}
export const Skeleton: React.FC<Props> = ({ width, height }) => (

View File

@@ -1,53 +1,53 @@
import PropTypes from 'prop-types';
import PropTypes from "prop-types";
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
import { injectIntl, defineMessages, FormattedMessage } from "react-intl";
import classNames from 'classnames';
import classNames from "classnames";
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from "react-immutable-proptypes";
import ImmutablePureComponent from "react-immutable-pure-component";
import { HotKeys } from 'react-hotkeys';
import { HotKeys } from "react-hotkeys";
import { Icon } from 'mastodon/components/icon';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
import { Icon } from "mastodon/components/icon";
import PictureInPicturePlaceholder from "mastodon/components/picture_in_picture_placeholder";
import Card from '../features/status/components/card';
import Card from "../features/status/components/card";
// We use the component (and not the container) since we do not want
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import { displayMedia } from '../initial_state';
import Bundle from "../features/ui/components/bundle";
import { MediaGallery, Video, Audio } from "../features/ui/util/async-components";
import { displayMedia } from "../initial_state";
import { Avatar } from './avatar';
import { AvatarOverlay } from './avatar_overlay';
import { DisplayName } from './display_name';
import { getHashtagBarForStatus } from './hashtag_bar';
import { RelativeTimestamp } from './relative_timestamp';
import StatusActionBar from './status_action_bar';
import StatusContent from './status_content';
import { Avatar } from "./avatar";
import { AvatarOverlay } from "./avatar_overlay";
import { DisplayName } from "./display_name";
import { getHashtagBarForStatus } from "./hashtag_bar";
import { RelativeTimestamp } from "./relative_timestamp";
import StatusActionBar from "./status_action_bar";
import StatusContent from "./status_content";
const domParser = new DOMParser();
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
const displayName = status.getIn(['account', 'display_name']);
const displayName = status.getIn(["account", "display_name"]);
const spoilerText = status.getIn(['translation', 'spoiler_text']) || status.get('spoiler_text');
const contentHtml = status.getIn(['translation', 'contentHtml']) || status.get('contentHtml');
const contentText = domParser.parseFromString(contentHtml, 'text/html').documentElement.textContent;
const spoilerText = status.getIn(["translation", "spoiler_text"]) || status.get("spoiler_text");
const contentHtml = status.getIn(["translation", "contentHtml"]) || status.get("contentHtml");
const contentText = domParser.parseFromString(contentHtml, "text/html").documentElement.textContent;
const values = [
displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
spoilerText && status.get('hidden') ? spoilerText : contentText,
intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
status.getIn(['account', 'acct']),
displayName.length === 0 ? status.getIn(["account", "acct"]).split("@")[0] : displayName,
spoilerText && status.get("hidden") ? spoilerText : contentText,
intl.formatDate(status.get("created_at"), { hour: "2-digit", minute: "2-digit", month: "short", day: "numeric" }),
status.getIn(["account", "acct"]),
];
if (rebloggedByText) {
values.push(rebloggedByText);
}
return values.join(', ');
return values.join(", ");
};
export const defaultMediaVisibility = (status) => {
@@ -55,19 +55,19 @@ export const defaultMediaVisibility = (status) => {
return undefined;
}
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
status = status.get('reblog');
if (status.get("reblog", null) !== null && typeof status.get("reblog") === "object") {
status = status.get("reblog");
}
return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
return (displayMedia !== "hide_all" && !status.get("sensitive") || displayMedia === "show_all");
};
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
public_short: { id: "privacy.public.short", defaultMessage: "Public" },
unlisted_short: { id: "privacy.unlisted.short", defaultMessage: "Unlisted" },
private_short: { id: "privacy.private.short", defaultMessage: "Followers only" },
direct_short: { id: "privacy.direct.short", defaultMessage: "Mentioned people only" },
edited: { id: "status.edited", defaultMessage: "Edited {date}" },
});
class Status extends ImmutablePureComponent {
@@ -121,12 +121,12 @@ class Status extends ImmutablePureComponent {
// Avoid checking props that are functions (and whose equality will always
// evaluate to false. See react-immutable-pure-component for usage.
updateOnProps = [
'status',
'account',
'muted',
'hidden',
'unread',
'pictureInPicture',
"status",
"account",
"muted",
"hidden",
"unread",
"pictureInPicture",
];
state = {
@@ -136,10 +136,10 @@ class Status extends ImmutablePureComponent {
};
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
if (nextProps.status && nextProps.status.get("id") !== prevState.statusId) {
return {
showMedia: defaultMediaVisibility(nextProps.status),
statusId: nextProps.status.get('id'),
statusId: nextProps.status.get("id"),
};
} else {
return null;
@@ -192,14 +192,14 @@ class Status extends ImmutablePureComponent {
};
getAttachmentAspectRatio () {
const attachments = this._properStatus().get('media_attachments');
const attachments = this._properStatus().get("media_attachments");
if (attachments.getIn([0, 'type']) === 'video') {
return `${attachments.getIn([0, 'meta', 'original', 'width'])} / ${attachments.getIn([0, 'meta', 'original', 'height'])}`;
} else if (attachments.getIn([0, 'type']) === 'audio') {
return '16 / 9';
if (attachments.getIn([0, "type"]) === "video") {
return `${attachments.getIn([0, "meta", "original", "width"])} / ${attachments.getIn([0, "meta", "original", "height"])}`;
} else if (attachments.getIn([0, "type"]) === "audio") {
return "16 / 9";
} else {
return (attachments.size === 1 && attachments.getIn([0, 'meta', 'small', 'aspect'])) ? attachments.getIn([0, 'meta', 'small', 'aspect']) : '3 / 2';
return (attachments.size === 1 && attachments.getIn([0, "meta", "small", "aspect"])) ? attachments.getIn([0, "meta", "small", "aspect"]) : "3 / 2";
}
}
@@ -223,14 +223,14 @@ class Status extends ImmutablePureComponent {
handleOpenVideo = (options) => {
const status = this._properStatus();
const lang = status.getIn(['translation', 'language']) || status.get('language');
this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, options);
const lang = status.getIn(["translation", "language"]) || status.get("language");
this.props.onOpenVideo(status.get("id"), status.getIn(["media_attachments", 0]), lang, options);
};
handleOpenMedia = (media, index) => {
const status = this._properStatus();
const lang = status.getIn(['translation', 'language']) || status.get('language');
this.props.onOpenMedia(status.get('id'), media, index, lang);
const lang = status.getIn(["translation", "language"]) || status.get("language");
this.props.onOpenMedia(status.get("id"), media, index, lang);
};
handleHotkeyOpenMedia = e => {
@@ -239,12 +239,12 @@ class Status extends ImmutablePureComponent {
e.preventDefault();
if (status.get('media_attachments').size > 0) {
const lang = status.getIn(['translation', 'language']) || status.get('language');
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, { startTime: 0 });
if (status.get("media_attachments").size > 0) {
const lang = status.getIn(["translation", "language"]) || status.get("language");
if (status.getIn(["media_attachments", 0, "type"]) === "video") {
onOpenVideo(status.get("id"), status.getIn(["media_attachments", 0]), lang, { startTime: 0 });
} else {
onOpenMedia(status.get('id'), status.get('media_attachments'), 0, lang);
onOpenMedia(status.get("id"), status.get("media_attachments"), 0, lang);
}
}
};
@@ -271,7 +271,7 @@ class Status extends ImmutablePureComponent {
handleHotkeyMention = e => {
e.preventDefault();
this.props.onMention(this._properStatus().get('account'), this.context.router.history);
this.props.onMention(this._properStatus().get("account"), this.context.router.history);
};
handleHotkeyOpen = () => {
@@ -287,7 +287,7 @@ class Status extends ImmutablePureComponent {
return;
}
router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
router.history.push(`/@${status.getIn(["account", "acct"])}/${status.get("id")}`);
};
handleHotkeyOpenProfile = () => {
@@ -302,15 +302,15 @@ class Status extends ImmutablePureComponent {
return;
}
router.history.push(`/@${status.getIn(['account', 'acct'])}`);
router.history.push(`/@${status.getIn(["account", "acct"])}`);
};
handleHotkeyMoveUp = e => {
this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured'));
this.props.onMoveUp(this.props.status.get("id"), e.target.getAttribute("data-featured"));
};
handleHotkeyMoveDown = e => {
this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured'));
this.props.onMoveDown(this.props.status.get("id"), e.target.getAttribute("data-featured"));
};
handleHotkeyToggleHidden = () => {
@@ -333,8 +333,8 @@ class Status extends ImmutablePureComponent {
_properStatus () {
const { status } = this.props;
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
return status.get('reblog');
if (status.get("reblog", null) !== null && typeof status.get("reblog") === "object") {
return status.get("reblog");
} else {
return status;
}
@@ -372,18 +372,18 @@ class Status extends ImmutablePureComponent {
if (hidden) {
return (
<HotKeys handlers={handlers}>
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex={0}>
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
<span>{status.get('content')}</span>
<div ref={this.handleRef} className={classNames("status__wrapper", { focusable: !this.props.muted })} tabIndex={0}>
<span>{status.getIn(["account", "display_name"]) || status.getIn(["account", "username"])}</span>
<span>{status.get("content")}</span>
</div>
</HotKeys>
);
}
const connectUp = previousId && previousId === status.get('in_reply_to_id');
const connectToRoot = rootId && rootId === status.get('in_reply_to_id');
const connectReply = nextInReplyToId && nextInReplyToId === status.get('id');
const matchedFilters = status.get('matched_filters');
const connectUp = previousId && previousId === status.get("in_reply_to_id");
const connectToRoot = rootId && rootId === status.get("in_reply_to_id");
const connectReply = nextInReplyToId && nextInReplyToId === status.get("id");
const matchedFilters = status.get("matched_filters");
if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) {
const minHandlers = this.props.muted ? {} : {
@@ -394,8 +394,8 @@ class Status extends ImmutablePureComponent {
return (
<HotKeys handlers={minHandlers}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex={0} ref={this.handleRef}>
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(', ')}.
{' '}
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(", ")}.
{" "}
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
<FormattedMessage id='status.show_filter_reason' defaultMessage='Show anyway' />
</button>
@@ -411,89 +411,89 @@ class Status extends ImmutablePureComponent {
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
</div>
);
} else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
} else if (status.get("reblog", null) !== null && typeof status.get("reblog") === "object") {
const display_name_html = { __html: status.getIn(["account", "display_name_html"]) };
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='retweet' className='status__prepend-icon' fixedWidth /></div>
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(["account", "id"])} href={`/@${status.getIn(["account", "acct"])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
</div>
);
rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: status.getIn(['account', 'acct']) });
rebloggedByText = intl.formatMessage({ id: "status.reblogged_by", defaultMessage: "{name} boosted" }, { name: status.getIn(["account", "acct"]) });
account = status.get('account');
status = status.get('reblog');
} else if (status.get('visibility') === 'direct') {
account = status.get("account");
status = status.get("reblog");
} else if (status.get("visibility") === "direct") {
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='at' className='status__prepend-icon' fixedWidth /></div>
<FormattedMessage id='status.direct_indicator' defaultMessage='Private mention' />
</div>
);
} else if (showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id'])) {
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
} else if (showThread && status.get("in_reply_to_id") && status.get("in_reply_to_account_id") === status.getIn(["account", "id"])) {
const display_name_html = { __html: status.getIn(["account", "display_name_html"]) };
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='reply' className='status__prepend-icon' fixedWidth /></div>
<FormattedMessage id='status.replied_to' defaultMessage='Replied to {name}' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
<FormattedMessage id='status.replied_to' defaultMessage='Replied to {name}' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(["account", "id"])} href={`/@${status.getIn(["account", "acct"])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
</div>
);
}
if (pictureInPicture.get('inUse')) {
if (pictureInPicture.get("inUse")) {
media = <PictureInPicturePlaceholder aspectRatio={this.getAttachmentAspectRatio()} />;
} else if (status.get('media_attachments').size > 0) {
const language = status.getIn(['translation', 'language']) || status.get('language');
} else if (status.get("media_attachments").size > 0) {
const language = status.getIn(["translation", "language"]) || status.get("language");
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
if (status.getIn(["media_attachments", 0, "type"]) === "audio") {
const attachment = status.getIn(["media_attachments", 0]);
const description = attachment.getIn(["translation", "description"]) || attachment.get("description");
media = (
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
{Component => (
<Component
src={attachment.get('url')}
src={attachment.get("url")}
alt={description}
lang={language}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
poster={attachment.get("preview_url") || status.getIn(["account", "avatar_static"])}
backgroundColor={attachment.getIn(["meta", "colors", "background"])}
foregroundColor={attachment.getIn(["meta", "colors", "foreground"])}
accentColor={attachment.getIn(["meta", "colors", "accent"])}
duration={attachment.getIn(["meta", "original", "duration"], 0)}
width={this.props.cachedMediaWidth}
height={110}
cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
sensitive={status.get('sensitive')}
blurhash={attachment.get('blurhash')}
deployPictureInPicture={pictureInPicture.get("available") ? this.handleDeployPictureInPicture : undefined}
sensitive={status.get("sensitive")}
blurhash={attachment.get("blurhash")}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
} else if (status.getIn(["media_attachments", 0, "type"]) === "video") {
const attachment = status.getIn(["media_attachments", 0]);
const description = attachment.getIn(["translation", "description"]) || attachment.get("description");
media = (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
{Component => (
<Component
preview={attachment.get('preview_url')}
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
aspectRatio={`${attachment.getIn(['meta', 'original', 'width'])} / ${attachment.getIn(['meta', 'original', 'height'])}`}
blurhash={attachment.get('blurhash')}
src={attachment.get('url')}
preview={attachment.get("preview_url")}
frameRate={attachment.getIn(["meta", "original", "frame_rate"])}
aspectRatio={`${attachment.getIn(["meta", "original", "width"])} / ${attachment.getIn(["meta", "original", "height"])}`}
blurhash={attachment.get("blurhash")}
src={attachment.get("url")}
alt={description}
lang={language}
sensitive={status.get('sensitive')}
sensitive={status.get("sensitive")}
onOpenVideo={this.handleOpenVideo}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
deployPictureInPicture={pictureInPicture.get("available") ? this.handleDeployPictureInPicture : undefined}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
@@ -505,9 +505,9 @@ class Status extends ImmutablePureComponent {
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => (
<Component
media={status.get('media_attachments')}
media={status.get("media_attachments")}
lang={language}
sensitive={status.get('sensitive')}
sensitive={status.get("sensitive")}
height={110}
onOpenMedia={this.handleOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
@@ -519,56 +519,56 @@ class Status extends ImmutablePureComponent {
</Bundle>
);
}
} else if (status.get('spoiler_text').length === 0 && status.get('card')) {
} else if (status.get("spoiler_text").length === 0 && status.get("card")) {
media = (
<Card
onOpenMedia={this.handleOpenMedia}
card={status.get('card')}
card={status.get("card")}
compact
sensitive={status.get('sensitive')}
sensitive={status.get("sensitive")}
/>
);
}
if (account === undefined || account === null) {
statusAvatar = <Avatar account={status.get('account')} size={46} />;
statusAvatar = <Avatar account={status.get("account")} size={46} />;
} else {
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
statusAvatar = <AvatarOverlay account={status.get("account")} friend={account} />;
}
const visibilityIconInfo = {
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) },
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) },
"public": { icon: "globe", text: intl.formatMessage(messages.public_short) },
"unlisted": { icon: "unlock", text: intl.formatMessage(messages.unlisted_short) },
"private": { icon: "lock", text: intl.formatMessage(messages.private_short) },
"direct": { icon: "at", text: intl.formatMessage(messages.direct_short) },
};
const visibilityIcon = visibilityIconInfo[status.get('visibility')];
const visibilityIcon = visibilityIconInfo[status.get("visibility")];
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
const expanded = !status.get("hidden") || status.get("spoiler_text").length === 0;
return (
<HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
<div className={classNames("status__wrapper", `status__wrapper-${status.get("visibility")}`, { "status__wrapper-reply": !!status.get("in_reply_to_id"), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? "true" : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(["account", "noindex"], true) || undefined}>
{prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted })} data-id={status.get('id')}>
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
<div className={classNames("status", `status-${status.get("visibility")}`, { "status-reply": !!status.get("in_reply_to_id"), "status--in-thread": !!rootId, "status--first-in-thread": previousId && (!connectUp || connectToRoot), muted: this.props.muted })} data-id={status.get("id")}>
{(connectReply || connectUp || connectToRoot) && <div className={classNames("status__line", { "status__line--full": connectReply, "status__line--first": !status.get("in_reply_to_id") && !connectToRoot })} />}
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div onClick={this.handleClick} className='status__info'>
<a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<a href={`/@${status.getIn(["account", "acct"])}/${status.get("id")}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
<RelativeTimestamp timestamp={status.get("created_at")} />{status.get("edited_at") && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get("edited_at"), { hour12: false, year: "numeric", month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit" }) })}> *</abbr>}
</a>
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
<a onClick={this.handleAccountClick} href={`/@${status.getIn(["account", "acct"])}`} title={status.getIn(["account", "acct"])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
<div className='status__avatar'>
{statusAvatar}
</div>
<DisplayName account={status.get('account')} />
<DisplayName account={status.get("account")} />
</a>
</div>

View File

@@ -1,61 +1,61 @@
import PropTypes from 'prop-types';
import PropTypes from "prop-types";
import { defineMessages, injectIntl } from 'react-intl';
import { defineMessages, injectIntl } from "react-intl";
import classNames from 'classnames';
import classNames from "classnames";
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import ImmutablePropTypes from "react-immutable-proptypes";
import ImmutablePureComponent from "react-immutable-pure-component";
import { connect } from "react-redux";
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from "mastodon/permissions";
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { me } from '../initial_state';
import DropdownMenuContainer from "../containers/dropdown_menu_container";
import { me } from "../initial_state";
import { IconButton } from './icon_button';
import { IconButton } from "./icon_button";
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
edit: { id: 'status.edit', defaultMessage: 'Edit' },
direct: { id: 'status.direct', defaultMessage: 'Privately mention @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
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' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
open: { id: 'status.open', defaultMessage: 'Expand this status' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed: { id: 'status.embed', defaultMessage: 'Embed' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to post' },
hide: { id: 'status.hide', defaultMessage: 'Hide post' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
filter: { id: 'status.filter', defaultMessage: 'Filter this post' },
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
delete: { id: "status.delete", defaultMessage: "Delete" },
redraft: { id: "status.redraft", defaultMessage: "Delete & re-draft" },
edit: { id: "status.edit", defaultMessage: "Edit" },
direct: { id: "status.direct", defaultMessage: "Privately mention @{name}" },
mention: { id: "status.mention", defaultMessage: "Mention @{name}" },
mute: { id: "account.mute", defaultMessage: "Mute @{name}" },
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" },
reblog_private: { id: "status.reblog_private", defaultMessage: "Boost with original visibility" },
cancel_reblog_private: { id: "status.cancel_reblog_private", defaultMessage: "Unboost" },
cannot_reblog: { id: "status.cannot_reblog", defaultMessage: "This post cannot be boosted" },
favourite: { id: "status.favourite", defaultMessage: "Favorite" },
bookmark: { id: "status.bookmark", defaultMessage: "Bookmark" },
removeBookmark: { id: "status.remove_bookmark", defaultMessage: "Remove bookmark" },
open: { id: "status.open", defaultMessage: "Expand this status" },
report: { id: "status.report", defaultMessage: "Report @{name}" },
muteConversation: { id: "status.mute_conversation", defaultMessage: "Mute conversation" },
unmuteConversation: { id: "status.unmute_conversation", defaultMessage: "Unmute conversation" },
pin: { id: "status.pin", defaultMessage: "Pin on profile" },
unpin: { id: "status.unpin", defaultMessage: "Unpin from profile" },
embed: { id: "status.embed", defaultMessage: "Embed" },
admin_account: { id: "status.admin_account", defaultMessage: "Open moderation interface for @{name}" },
admin_status: { id: "status.admin_status", defaultMessage: "Open this post in the moderation interface" },
admin_domain: { id: "status.admin_domain", defaultMessage: "Open moderation interface for {domain}" },
copy: { id: "status.copy", defaultMessage: "Copy link to post" },
hide: { id: "status.hide", defaultMessage: "Hide post" },
blockDomain: { id: "account.block_domain", defaultMessage: "Block domain {domain}" },
unblockDomain: { id: "account.unblock_domain", defaultMessage: "Unblock domain {domain}" },
unmute: { id: "account.unmute", defaultMessage: "Unmute @{name}" },
unblock: { id: "account.unblock", defaultMessage: "Unblock @{name}" },
filter: { id: "status.filter", defaultMessage: "Filter this post" },
openOriginalPage: { id: "account.open_original_page", defaultMessage: "Open original page" },
});
const mapStateToProps = (state, { status }) => ({
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
relationship: state.getIn(["relationships", status.getIn(["account", "id"])]),
});
class StatusActionBar extends ImmutablePureComponent {
@@ -97,9 +97,9 @@ class StatusActionBar extends ImmutablePureComponent {
// Avoid checking props that are functions (and whose equality will always
// evaluate to false. See react-immutable-pure-component for usage.
updateOnProps = [
'status',
'relationship',
'withDismiss',
"status",
"relationship",
"withDismiss",
];
handleReplyClick = () => {
@@ -108,15 +108,17 @@ class StatusActionBar extends ImmutablePureComponent {
if (signedIn) {
this.props.onReply(this.props.status, this.context.router.history);
} else {
this.props.onInteractionModal('reply', this.props.status);
this.props.onInteractionModal("reply", this.props.status);
}
};
handleShareClick = () => {
navigator.share({
url: this.props.status.get('url'),
url: this.props.status.get("url"),
}).catch((e) => {
if (e.name !== 'AbortError') console.error(e);
if (e.name !== "AbortError") {
console.error(e);
}
});
};
@@ -126,7 +128,7 @@ class StatusActionBar extends ImmutablePureComponent {
if (signedIn) {
this.props.onFavourite(this.props.status);
} else {
this.props.onInteractionModal('favourite', this.props.status);
this.props.onInteractionModal("favourite", this.props.status);
}
};
@@ -136,7 +138,7 @@ class StatusActionBar extends ImmutablePureComponent {
if (signedIn) {
this.props.onReblog(this.props.status, e);
} else {
this.props.onInteractionModal('reblog', this.props.status);
this.props.onInteractionModal("reblog", this.props.status);
}
};
@@ -161,18 +163,18 @@ class StatusActionBar extends ImmutablePureComponent {
};
handleMentionClick = () => {
this.props.onMention(this.props.status.get('account'), this.context.router.history);
this.props.onMention(this.props.status.get("account"), this.context.router.history);
};
handleDirectClick = () => {
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
this.props.onDirect(this.props.status.get("account"), this.context.router.history);
};
handleMuteClick = () => {
const { status, relationship, onMute, onUnmute } = this.props;
const account = status.get('account');
const account = status.get("account");
if (relationship && relationship.get('muting')) {
if (relationship && relationship.get("muting")) {
onUnmute(account);
} else {
onMute(account);
@@ -181,9 +183,9 @@ class StatusActionBar extends ImmutablePureComponent {
handleBlockClick = () => {
const { status, relationship, onBlock, onUnblock } = this.props;
const account = status.get('account');
const account = status.get("account");
if (relationship && relationship.get('blocking')) {
if (relationship && relationship.get("blocking")) {
onUnblock(account);
} else {
onBlock(status);
@@ -192,20 +194,20 @@ class StatusActionBar extends ImmutablePureComponent {
handleBlockDomain = () => {
const { status, onBlockDomain } = this.props;
const account = status.get('account');
const account = status.get("account");
onBlockDomain(account.get('acct').split('@')[1]);
onBlockDomain(account.get("acct").split("@")[1]);
};
handleUnblockDomain = () => {
const { status, onUnblockDomain } = this.props;
const account = status.get('account');
const account = status.get("account");
onUnblockDomain(account.get('acct').split('@')[1]);
onUnblockDomain(account.get("acct").split("@")[1]);
};
handleOpen = () => {
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`);
this.context.router.history.push(`/@${this.props.status.getIn(["account", "acct"])}/${this.props.status.get("id")}`);
};
handleEmbed = () => {
@@ -225,7 +227,7 @@ class StatusActionBar extends ImmutablePureComponent {
};
handleCopy = () => {
const url = this.props.status.get('url');
const url = this.props.status.get("url");
navigator.clipboard.writeText(url);
};
@@ -237,24 +239,24 @@ class StatusActionBar extends ImmutablePureComponent {
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
const { signedIn, permissions } = this.context.identity;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
const mutingConversation = status.get('muted');
const account = status.get('account');
const writtenByMe = status.getIn(['account', 'id']) === me;
const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']);
const publicStatus = ["public", "unlisted"].includes(status.get("visibility"));
const pinnableStatus = ["public", "unlisted", "private"].includes(status.get("visibility"));
const mutingConversation = status.get("muted");
const account = status.get("account");
const writtenByMe = status.getIn(["account", "id"]) === me;
const isRemote = status.getIn(["account", "username"]) !== status.getIn(["account", "acct"]);
let menu = [];
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
if (publicStatus && isRemote) {
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get("url") });
}
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
if (publicStatus && 'share' in navigator) {
if (publicStatus && "share" in navigator) {
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick });
}
@@ -265,10 +267,10 @@ class StatusActionBar extends ImmutablePureComponent {
if (signedIn) {
menu.push(null);
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
menu.push({ text: intl.formatMessage(status.get("bookmarked") ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
if (writtenByMe && pinnableStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
menu.push({ text: intl.formatMessage(status.get("pinned") ? messages.unpin : messages.pin), action: this.handlePinClick });
}
menu.push(null);
@@ -283,20 +285,20 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
} else {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get("username") }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get("username") }), action: this.handleDirectClick });
menu.push(null);
if (relationship && relationship.get('muting')) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
if (relationship && relationship.get("muting")) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get("username") }), action: this.handleMuteClick });
} else {
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick, dangerous: true });
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get("username") }), action: this.handleMuteClick, dangerous: true });
}
if (relationship && relationship.get('blocking')) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
if (relationship && relationship.get("blocking")) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get("username") }), action: this.handleBlockClick });
} else {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick, dangerous: true });
menu.push({ text: intl.formatMessage(messages.block, { name: account.get("username") }), action: this.handleBlockClick, dangerous: true });
}
if (!this.props.onFilter) {
@@ -305,14 +307,14 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push(null);
}
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport, dangerous: true });
menu.push({ text: intl.formatMessage(messages.report, { name: account.get("username") }), action: this.handleReport, dangerous: true });
if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1];
if (account.get("acct") !== account.get("username")) {
const domain = account.get("acct").split("@")[1];
menu.push(null);
if (relationship && relationship.get('domain_blocking')) {
if (relationship && relationship.get("domain_blocking")) {
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
} else {
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain, dangerous: true });
@@ -322,11 +324,11 @@ class StatusActionBar extends ImmutablePureComponent {
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
menu.push(null);
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get("username") }), href: `/admin/accounts/${status.getIn(["account", "id"])}` });
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(["account", "id"])}/statuses/${status.get("id")}` });
}
if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
const domain = account.get('acct').split('@')[1];
const domain = account.get("acct").split("@")[1];
menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
}
}
@@ -335,18 +337,18 @@ class StatusActionBar extends ImmutablePureComponent {
let replyIcon;
let replyTitle;
if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply';
if (status.get("in_reply_to_id", null) === null) {
replyIcon = "reply";
replyTitle = intl.formatMessage(messages.reply);
} else {
replyIcon = 'reply-all';
replyIcon = "reply-all";
replyTitle = intl.formatMessage(messages.replyAll);
}
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
const reblogPrivate = status.getIn(["account", "id"]) === me && status.get("visibility") === "private";
let reblogTitle = '';
if (status.get('reblogged')) {
let reblogTitle = "";
if (status.get("reblogged")) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
@@ -362,10 +364,10 @@ class StatusActionBar extends ImmutablePureComponent {
return (
<div className='status__action-bar'>
<IconButton className='status__action-bar__button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
<IconButton className='status__action-bar__button' title={replyTitle} icon={status.get("in_reply_to_account_id") === status.getIn(["account", "id"]) ? "reply" : replyIcon} onClick={this.handleReplyClick} counter={status.get("replies_count")} />
<IconButton className={classNames("status__action-bar__button", { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get("reblogged")} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={withCounters ? status.get("reblogs_count") : undefined} />
<IconButton className='status__action-bar__button star-icon' animate active={status.get("favourited")} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get("favourites_count") : undefined} />
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get("bookmarked")} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
{filterButton}

View File

@@ -1,17 +1,17 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import PropTypes from "prop-types";
import { PureComponent } from "react";
import { FormattedMessage, injectIntl } from 'react-intl';
import { FormattedMessage, injectIntl } from "react-intl";
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import classnames from "classnames";
import { Link } from "react-router-dom";
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import ImmutablePropTypes from "react-immutable-proptypes";
import { connect } from "react-redux";
import { Icon } from 'mastodon/components/icon';
import PollContainer from 'mastodon/containers/poll_container';
import { autoPlayGif } from 'mastodon/initial_state';
import { Icon } from "mastodon/components/icon";
import PollContainer from "mastodon/containers/poll_container";
import { autoPlayGif } from "mastodon/initial_state";
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
@@ -21,7 +21,7 @@ const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
* @returns {string}
*/
export function getStatusContent(status) {
return status.getIn(['translation', 'contentHtml']) || status.get('contentHtml');
return status.getIn(["translation", "contentHtml"]) || status.get("contentHtml");
}
class StatusContent extends PureComponent {
@@ -54,42 +54,42 @@ class StatusContent extends PureComponent {
}
const { status, onCollapsedToggle } = this.props;
const links = node.querySelectorAll('a');
const links = node.querySelectorAll("a");
let link, mention;
for (var i = 0; i < links.length; ++i) {
link = links[i];
if (link.classList.contains('status-link')) {
if (link.classList.contains("status-link")) {
continue;
}
link.classList.add('status-link');
link.classList.add("status-link");
mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
mention = this.props.status.get("mentions").find(item => link.href === item.get("url"));
if (mention) {
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
link.setAttribute('title', `@${mention.get('acct')}`);
link.setAttribute('href', `/@${mention.get('acct')}`);
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`);
link.addEventListener("click", this.onMentionClick.bind(this, mention), false);
link.setAttribute("title", `@${mention.get("acct")}`);
link.setAttribute("href", `/@${mention.get("acct")}`);
} else if (link.textContent[0] === "#" || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === "#")) {
link.addEventListener("click", this.onHashtagClick.bind(this, link.text), false);
link.setAttribute("href", `/tags/${link.text.replace(/^#/, "")}`);
} else {
link.setAttribute('title', link.href);
link.classList.add('unhandled-link');
link.setAttribute("title", link.href);
link.classList.add("unhandled-link");
}
}
if (status.get('collapsed', null) === null && onCollapsedToggle) {
if (status.get("collapsed", null) === null && onCollapsedToggle) {
const { collapsible, onClick } = this.props;
const collapsed =
collapsible
&& onClick
&& node.clientHeight > MAX_HEIGHT
&& status.get('spoiler_text').length === 0;
&& status.get("spoiler_text").length === 0;
onCollapsedToggle(collapsed);
}
@@ -100,11 +100,11 @@ class StatusContent extends PureComponent {
return;
}
const emojis = currentTarget.querySelectorAll('.custom-emoji');
const emojis = currentTarget.querySelectorAll(".custom-emoji");
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-original');
emoji.src = emoji.getAttribute("data-original");
}
};
@@ -113,11 +113,11 @@ class StatusContent extends PureComponent {
return;
}
const emojis = currentTarget.querySelectorAll('.custom-emoji');
const emojis = currentTarget.querySelectorAll(".custom-emoji");
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-static');
emoji.src = emoji.getAttribute("data-static");
}
};
@@ -132,12 +132,12 @@ class StatusContent extends PureComponent {
onMentionClick = (mention, e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.context.router.history.push(`/@${mention.get('acct')}`);
this.context.router.history.push(`/@${mention.get("acct")}`);
}
};
onHashtagClick = (hashtag, e) => {
hashtag = hashtag.replace(/^#/, '');
hashtag = hashtag.replace(/^#/, "");
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
@@ -159,7 +159,7 @@ class StatusContent extends PureComponent {
let element = e.target;
while (element) {
if (element.localName === 'button' || element.localName === 'a' || element.localName === 'label') {
if (element.localName === "button" || element.localName === "a" || element.localName === "label") {
return;
}
element = element.parentNode;
@@ -195,15 +195,15 @@ class StatusContent extends PureComponent {
const { status, statusContent } = this.props;
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const renderReadMore = this.props.onClick && status.get('collapsed');
const renderReadMore = this.props.onClick && status.get("collapsed");
const content = { __html: statusContent ?? getStatusContent(status) };
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
const language = status.getIn(['translation', 'language']) || status.get('language');
const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.context.router,
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
'status__content--collapsed': renderReadMore,
const spoilerContent = { __html: status.getIn(["translation", "spoilerHtml"]) || status.get("spoilerHtml") };
const language = status.getIn(["translation", "language"]) || status.get("language");
const classNames = classnames("status__content", {
"status__content--with-action": this.props.onClick && this.context.router,
"status__content--with-spoiler": status.get("spoiler_text").length > 0,
"status__content--collapsed": renderReadMore,
});
const readMoreButton = renderReadMore && (
@@ -212,18 +212,18 @@ class StatusContent extends PureComponent {
</button>
);
const poll = !!status.get('poll') && (
<PollContainer pollId={status.get('poll')} lang={language} />
const poll = !!status.get("poll") && (
<PollContainer pollId={status.get("poll")} lang={language} />
);
if (status.get('spoiler_text').length > 0) {
let mentionsPlaceholder = '';
if (status.get("spoiler_text").length > 0) {
let mentionsPlaceholder = "";
const mentionLinks = status.get('mentions').map(item => (
<Link to={`/@${item.get('acct')}`} key={item.get('id')} className='status-link mention'>
@<span>{item.get('username')}</span>
const mentionLinks = status.get("mentions").map(item => (
<Link to={`/@${item.get("acct")}`} key={item.get("id")} className='status-link mention'>
@<span>{item.get("username")}</span>
</Link>
)).reduce((aggregate, item) => [...aggregate, item, ' '], []);
)).reduce((aggregate, item) => [...aggregate, item, " "], []);
const toggleText = hidden ? <FormattedMessage id='status.show_more' defaultMessage='Show more' /> : <FormattedMessage id='status.show_less' defaultMessage='Show less' />;
@@ -233,15 +233,15 @@ class StatusContent extends PureComponent {
return (
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
<p style={{ marginBottom: hidden && status.get("mentions").isEmpty() ? "0px" : null }}>
<span dangerouslySetInnerHTML={spoilerContent} className='translate' lang={language} />
{' '}
<button type='button' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick} aria-expanded={!hidden}>{toggleText}</button>
{" "}
<button type='button' className={`status__content__spoiler-link ${hidden ? "status__content__spoiler-link--show-more" : "status__content__spoiler-link--show-less"}`} onClick={this.handleSpoilerClick} aria-expanded={!hidden}>{toggleText}</button>
</p>
{mentionsPlaceholder}
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''} translate`} lang={language} dangerouslySetInnerHTML={content} />
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? "status__content__text--visible" : ""} translate`} lang={language} dangerouslySetInnerHTML={content} />
{!hidden && poll}
</div>

View File

@@ -1,16 +1,16 @@
import PropTypes from 'prop-types';
import PropTypes from "prop-types";
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from "react-immutable-proptypes";
import ImmutablePureComponent from "react-immutable-pure-component";
import { debounce } from 'lodash';
import { debounce } from "lodash";
import RegenerationIndicator from 'mastodon/components/regeneration_indicator';
import RegenerationIndicator from "mastodon/components/regeneration_indicator";
import StatusContainer from '../containers/status_container';
import StatusContainer from "../containers/status_container";
import { LoadGap } from './load_gap';
import ScrollableList from './scrollable_list';
import { LoadGap } from "./load_gap";
import ScrollableList from "./scrollable_list";
export default class StatusList extends ImmutablePureComponent {
@@ -93,7 +93,7 @@ export default class StatusList extends ImmutablePureComponent {
let scrollableContent = (isLoading || statusIds.size > 0) ? (
statusIds.map((statusId, index) => statusId === null ? (
<LoadGap
key={'gap:' + statusIds.get(index + 1)}
key={"gap:" + statusIds.get(index + 1)}
disabled={isLoading}
maxId={index > 0 ? statusIds.get(index - 1) : null}
onClick={onLoadMore}

View File

@@ -1,8 +1,9 @@
import { FormattedMessage } from 'react-intl';
import React from "react";
import { FormattedMessage } from "react-intl";
interface Props {
resource: JSX.Element;
url: string;
resource: React.JSX.Element,
url: string,
}
export const TimelineHint: React.FC<Props> = ({ resource, url }) => (

View File

@@ -1,23 +1,23 @@
import { Icon } from './icon';
import { Icon } from "./icon";
const domParser = new DOMParser();
const stripRelMe = (html: string) => {
const document = domParser.parseFromString(html, 'text/html').documentElement;
const document = domParser.parseFromString(html, "text/html").documentElement;
document.querySelectorAll<HTMLAnchorElement>('a[rel]').forEach((link) => {
document.querySelectorAll<HTMLAnchorElement>("a[rel]").forEach((link) => {
link.rel = link.rel
.split(' ')
.filter((x: string) => x !== 'me')
.join(' ');
.split(" ")
.filter((x: string) => x !== "me")
.join(" ");
});
const body = document.querySelector('body');
const body = document.querySelector("body");
return body ? { __html: body.innerHTML } : undefined;
};
interface Props {
link: string;
link: string,
}
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
<span className='verified-badge'>