mirror of
https://github.com/mastodon/mastodon.git
synced 2026-08-22 05:14:50 -05:00
Use status ID instead of passing status objects (#39727)
This commit is contained in:
@@ -1,25 +1,23 @@
|
||||
import { fromJS } from 'immutable';
|
||||
|
||||
import { render } from '@/testing/rendering';
|
||||
|
||||
import { AvatarOverlay } from '../avatar_overlay';
|
||||
|
||||
describe('<AvatarOverlay', () => {
|
||||
const account = fromJS({
|
||||
const account = {
|
||||
username: 'alice',
|
||||
acct: 'alice',
|
||||
display_name: 'Alice',
|
||||
avatar: '/animated/alice.gif',
|
||||
avatar_static: '/static/alice.jpg',
|
||||
});
|
||||
};
|
||||
|
||||
const friend = fromJS({
|
||||
const friend = {
|
||||
username: 'eve',
|
||||
acct: 'eve@blackhat.lair',
|
||||
display_name: 'Evelyn',
|
||||
avatar: '/animated/eve.gif',
|
||||
avatar_static: '/static/eve.jpg',
|
||||
});
|
||||
};
|
||||
|
||||
it('renders a overlay avatar', () => {
|
||||
const { container } = render(<AvatarOverlay account={account} friend={friend} />);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { fromJS } from 'immutable';
|
||||
import { mediaAttachmentFactoryAPI } from '@/testing/factories';
|
||||
|
||||
import type { StatusLike } from '../hashtag_bar';
|
||||
import { computeHashtagBarForStatus } from '../hashtag_bar';
|
||||
|
||||
function createStatus(
|
||||
@@ -9,25 +8,27 @@ function createStatus(
|
||||
hasMedia = false,
|
||||
spoilerText?: string,
|
||||
) {
|
||||
return fromJS({
|
||||
tags: hashtags.map((name) => ({ name })),
|
||||
return {
|
||||
tags: hashtags.map((name) => ({
|
||||
name,
|
||||
url: `https://example.com/tag/${name}`,
|
||||
})),
|
||||
contentHtml: content,
|
||||
media_attachments: hasMedia ? ['fakeMedia'] : [],
|
||||
media_attachments: hasMedia
|
||||
? [{ ...mediaAttachmentFactoryAPI(), remote_url: null }]
|
||||
: [],
|
||||
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>', []);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
const { hashtagsInBar, statusContent } = computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual([]);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||
`"<p>Simple text</p>"`,
|
||||
);
|
||||
expect(statusContent).toMatchInlineSnapshot(`"<p>Simple text</p>"`);
|
||||
});
|
||||
|
||||
it('displays out of band hashtags in the bar', () => {
|
||||
@@ -36,11 +37,10 @@ describe('computeHashtagBarForStatus', () => {
|
||||
['hashtag', 'test'],
|
||||
);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
const { hashtagsInBar, statusContent } = computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual(['test']);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||
expect(statusContent).toMatchInlineSnapshot(
|
||||
`"<p>Simple text <a href="test">#hashtag</a></p>"`,
|
||||
);
|
||||
});
|
||||
@@ -51,11 +51,10 @@ describe('computeHashtagBarForStatus', () => {
|
||||
['test'],
|
||||
);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
const { hashtagsInBar, statusContent } = computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual([]);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||
expect(statusContent).toMatchInlineSnapshot(
|
||||
`"this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text"`,
|
||||
);
|
||||
});
|
||||
@@ -66,13 +65,10 @@ describe('computeHashtagBarForStatus', () => {
|
||||
['hashtag'],
|
||||
);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
const { hashtagsInBar, statusContent } = computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual(['hashtag']);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||
`"<p>Simple text</p>"`,
|
||||
);
|
||||
expect(statusContent).toMatchInlineSnapshot(`"<p>Simple text</p>"`);
|
||||
});
|
||||
|
||||
it('does not include tags from content', () => {
|
||||
@@ -81,11 +77,10 @@ describe('computeHashtagBarForStatus', () => {
|
||||
['hashtag'],
|
||||
);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
const { hashtagsInBar, statusContent } = computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual([]);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||
expect(statusContent).toMatchInlineSnapshot(
|
||||
`"<p>Simple text with a <a href="test">#hashtag</a></p>"`,
|
||||
);
|
||||
});
|
||||
@@ -96,11 +91,10 @@ describe('computeHashtagBarForStatus', () => {
|
||||
['hashtag', 'test'],
|
||||
);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
const { hashtagsInBar, statusContent } = computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual([]);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||
expect(statusContent).toMatchInlineSnapshot(
|
||||
`"<p><a href="test">#test</a>. And another <a href="test">#hashtag</a></p>"`,
|
||||
);
|
||||
});
|
||||
@@ -111,13 +105,10 @@ describe('computeHashtagBarForStatus', () => {
|
||||
['éaa'],
|
||||
);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
const { hashtagsInBar, statusContent } = computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual(['Éaa']);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||
`"<p>Text</p>"`,
|
||||
);
|
||||
expect(statusContent).toMatchInlineSnapshot(`"<p>Text</p>"`);
|
||||
});
|
||||
|
||||
it('handles server-side normalized tags with accentuated characters', () => {
|
||||
@@ -126,13 +117,10 @@ describe('computeHashtagBarForStatus', () => {
|
||||
['eaa'], // The server may normalize the hashtags in the `tags` attribute
|
||||
);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
const { hashtagsInBar, statusContent } = computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual(['Éaa']);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||
`"<p>Text</p>"`,
|
||||
);
|
||||
expect(statusContent).toMatchInlineSnapshot(`"<p>Text</p>"`);
|
||||
});
|
||||
|
||||
it('does not display in bar a hashtag in content with a case difference', () => {
|
||||
@@ -141,11 +129,10 @@ describe('computeHashtagBarForStatus', () => {
|
||||
['éaa'],
|
||||
);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
const { hashtagsInBar, statusContent } = computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual([]);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||
expect(statusContent).toMatchInlineSnapshot(
|
||||
`"<p>Text <a href="test">#Éaa</a></p>"`,
|
||||
);
|
||||
});
|
||||
@@ -156,11 +143,10 @@ describe('computeHashtagBarForStatus', () => {
|
||||
['test', 'hashtag'],
|
||||
);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
const { hashtagsInBar, statusContent } = computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual([]);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||
expect(statusContent).toMatchInlineSnapshot(
|
||||
`"<p><a href="test">#test</a> <a href="test">#hashtag</a></p>"`,
|
||||
);
|
||||
});
|
||||
@@ -172,11 +158,10 @@ describe('computeHashtagBarForStatus', () => {
|
||||
true,
|
||||
);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
const { hashtagsInBar, statusContent } = computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual([]);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||
expect(statusContent).toMatchInlineSnapshot(
|
||||
`"<p>This is my content! <a href="test">#hashtag</a></p>"`,
|
||||
);
|
||||
});
|
||||
@@ -188,11 +173,10 @@ describe('computeHashtagBarForStatus', () => {
|
||||
true,
|
||||
);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
const { hashtagsInBar, statusContent } = computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual(['test', 'hashtag']);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(`""`);
|
||||
expect(statusContent).toMatchInlineSnapshot(`""`);
|
||||
});
|
||||
|
||||
it('does not use the hashtag bar if the status content is only hashtags, has a CW and a media', () => {
|
||||
@@ -203,11 +187,10 @@ describe('computeHashtagBarForStatus', () => {
|
||||
'My CW text',
|
||||
);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
const { hashtagsInBar, statusContent } = computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual([]);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||
expect(statusContent).toMatchInlineSnapshot(
|
||||
`"<p><a href="test">#test</a> <a href="test">#hashtag</a></p>"`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,14 +5,15 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
import { useHovering } from 'mastodon/hooks/useHovering';
|
||||
import { autoPlayGif } from 'mastodon/initial_state';
|
||||
import type { Account } from 'mastodon/models/account';
|
||||
import type { Account, AccountShapeFull } from 'mastodon/models/account';
|
||||
|
||||
import { useAccount } from '../hooks/useAccount';
|
||||
|
||||
interface Props {
|
||||
account:
|
||||
| Pick<Account, 'id' | 'acct' | 'avatar' | 'avatar_static'>
|
||||
| undefined; // FIXME: remove `undefined` once we know for sure its always there
|
||||
account?: Pick<
|
||||
Account | AccountShapeFull,
|
||||
'id' | 'acct' | 'avatar' | 'avatar_static'
|
||||
>;
|
||||
alt?: string;
|
||||
size?: number;
|
||||
style?: React.CSSProperties;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { useHovering } from 'mastodon/hooks/useHovering';
|
||||
import { autoPlayGif } from 'mastodon/initial_state';
|
||||
import type { Account } from 'mastodon/models/account';
|
||||
import type { Account, AccountShapeFull } from 'mastodon/models/account';
|
||||
|
||||
type AvatarAccount = Pick<
|
||||
Account | AccountShapeFull,
|
||||
'acct' | 'avatar' | 'avatar_static'
|
||||
>;
|
||||
|
||||
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
|
||||
account?: AvatarAccount;
|
||||
friend?: AvatarAccount;
|
||||
size?: number;
|
||||
baseSize?: number;
|
||||
overlaySize?: number;
|
||||
@@ -27,12 +32,8 @@ export const AvatarOverlay: React.FC<Props> = ({
|
||||
}) => {
|
||||
const { hovering, handleMouseEnter, handleMouseLeave } =
|
||||
useHovering(autoPlayGif);
|
||||
const accountSrc = hovering
|
||||
? account?.get('avatar')
|
||||
: account?.get('avatar_static');
|
||||
const friendSrc = hovering
|
||||
? friend?.get('avatar')
|
||||
: friend?.get('avatar_static');
|
||||
const accountSrc = hovering ? account?.avatar : account?.avatar_static;
|
||||
const friendSrc = hovering ? friend?.avatar : friend?.avatar_static;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -49,7 +50,7 @@ export const AvatarOverlay: React.FC<Props> = ({
|
||||
{accountSrc && (
|
||||
<img
|
||||
src={accountSrc}
|
||||
alt={account?.get('acct')}
|
||||
alt={account?.acct}
|
||||
onError={handleImgLoadError}
|
||||
/>
|
||||
)}
|
||||
@@ -63,7 +64,7 @@ export const AvatarOverlay: React.FC<Props> = ({
|
||||
{friendSrc && (
|
||||
<img
|
||||
src={friendSrc}
|
||||
alt={friend?.get('acct')}
|
||||
alt={friend?.acct}
|
||||
onError={handleImgLoadError}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
import type { List } from 'immutable';
|
||||
|
||||
import type { CustomEmoji } from '../models/custom_emoji';
|
||||
import type { Status } from '../models/status';
|
||||
import { useStatus } from '../hooks/useStatus';
|
||||
|
||||
import { EmojiHTML } from './emoji/html';
|
||||
import { StatusBanner, BannerVariant } from './status_banner';
|
||||
|
||||
export const ContentWarning: React.FC<{
|
||||
status: Status;
|
||||
statusId: string;
|
||||
expanded?: boolean;
|
||||
onClick?: () => void;
|
||||
}> = ({ status, expanded, onClick }) => {
|
||||
const hasSpoiler = !!status.get('spoiler_text');
|
||||
if (!hasSpoiler) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text =
|
||||
status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml');
|
||||
if (typeof text !== 'string' || text.length === 0) {
|
||||
}> = ({ statusId, expanded, onClick }) => {
|
||||
const status = useStatus(statusId);
|
||||
const hasSpoiler = !!status?.spoiler_text;
|
||||
const text = status?.translation?.spoilerHtml ?? status?.spoilerHtml;
|
||||
if (!hasSpoiler || !text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -28,11 +21,7 @@ export const ContentWarning: React.FC<{
|
||||
onClick={onClick}
|
||||
variant={BannerVariant.Warning}
|
||||
>
|
||||
<EmojiHTML
|
||||
as='span'
|
||||
htmlString={text}
|
||||
extraEmojis={status.get('emojis') as List<CustomEmoji>}
|
||||
/>
|
||||
<EmojiHTML as='span' htmlString={text} extraEmojis={status.emojis} />
|
||||
</StatusBanner>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ export function useAccountHandle(
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
let acct = account.get('acct');
|
||||
let acct = account.acct;
|
||||
|
||||
if (!acct.includes('@') && localDomain) {
|
||||
acct = `${acct}@${localDomain}`;
|
||||
|
||||
@@ -3,14 +3,14 @@ import type { ComponentPropsWithoutRef, FC } from 'react';
|
||||
import type { LinkProps } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type { Account } from '@/mastodon/models/account';
|
||||
import type { Account, AccountShapeFull } from '@/mastodon/models/account';
|
||||
|
||||
import { DisplayNameDefault } from './default';
|
||||
import { DisplayNameWithoutDomain } from './no-domain';
|
||||
import { DisplayNameSimple } from './simple';
|
||||
|
||||
export interface DisplayNameProps {
|
||||
account?: Account;
|
||||
account?: Account | AccountShapeFull;
|
||||
localDomain?: string;
|
||||
variant?: 'default' | 'simple' | 'noDomain';
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@ export const DisplayNameWithoutDomain: FC<
|
||||
{account ? (
|
||||
<EmojiHTML
|
||||
className='display-name__html'
|
||||
htmlString={account.get('display_name_html')}
|
||||
htmlString={account.display_name_html}
|
||||
as='strong'
|
||||
extraEmojis={account.get('emojis')}
|
||||
extraEmojis={account.emojis}
|
||||
/>
|
||||
) : (
|
||||
<strong className='display-name__html'>
|
||||
|
||||
@@ -16,8 +16,8 @@ export const DisplayNameSimple: FC<
|
||||
<EmojiHTML
|
||||
{...props}
|
||||
as='span'
|
||||
htmlString={account.get('display_name_html')}
|
||||
extraEmojis={account.get('emojis')}
|
||||
htmlString={account.display_name_html}
|
||||
extraEmojis={account.emojis}
|
||||
/>
|
||||
</bdi>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,12 @@ import type { List, Record } from 'immutable';
|
||||
|
||||
import { groupBy, minBy } from 'lodash';
|
||||
|
||||
import { getStatusContent } from './status_content';
|
||||
import type { ApiTagJSON } from '../api_types/statuses';
|
||||
import type {
|
||||
MediaAttachmentShape,
|
||||
StatusShape,
|
||||
StatusTranslation,
|
||||
} from '../models/status';
|
||||
|
||||
// Fit on a single line on desktop
|
||||
const VISIBLE_HASHTAGS = 3;
|
||||
@@ -17,10 +22,11 @@ const VISIBLE_HASHTAGS = 3;
|
||||
export type TagLike = Record<{ name: string }>;
|
||||
export type StatusLike = Record<{
|
||||
tags: List<TagLike>;
|
||||
contentHTML: string;
|
||||
contentHtml: string;
|
||||
media_attachments: List<unknown>;
|
||||
spoiler_text?: string;
|
||||
account: Record<{ id: string }>;
|
||||
translation?: Record<{ contentHtml: string }>;
|
||||
}>;
|
||||
|
||||
function normalizeHashtag(hashtag: string) {
|
||||
@@ -88,20 +94,23 @@ function localeAwareInclude(collection: string[], value: string) {
|
||||
}
|
||||
|
||||
// We use an intermediate function here to make it easier to test
|
||||
export function computeHashtagBarForStatus(status: StatusLike): {
|
||||
statusContentProps: { statusContent: string };
|
||||
hashtagsInBar: string[];
|
||||
} {
|
||||
let statusContent = getStatusContent(status);
|
||||
export function computeHashtagBarForStatus(
|
||||
status: Pick<
|
||||
StatusShape,
|
||||
| 'tags'
|
||||
| 'media_attachments'
|
||||
| 'spoiler_text'
|
||||
| 'translation'
|
||||
| 'contentHtml'
|
||||
>,
|
||||
) {
|
||||
let statusContent = status.translation?.contentHtml ?? status.contentHtml;
|
||||
|
||||
const tagNames = status
|
||||
.get('tags')
|
||||
.map((tag) => tag.get('name'))
|
||||
.toJS();
|
||||
const tagNames = status.tags.map((tag) => tag.name);
|
||||
|
||||
// this is returned if we stop the processing early, it does not change what is displayed
|
||||
const defaultResult = {
|
||||
statusContentProps: { statusContent },
|
||||
statusContent,
|
||||
hashtagsInBar: [],
|
||||
};
|
||||
|
||||
@@ -164,8 +173,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.media_attachments.length > 0;
|
||||
const hasSpoiler = !!status.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)) {
|
||||
@@ -178,7 +187,7 @@ export function computeHashtagBarForStatus(status: StatusLike): {
|
||||
}
|
||||
|
||||
return {
|
||||
statusContentProps: { statusContent },
|
||||
statusContent,
|
||||
hashtagsInBar: uniqueHashtagsWithCaseHandling(hashtagsInBar),
|
||||
};
|
||||
}
|
||||
@@ -191,11 +200,20 @@ export function computeHashtagBarForStatus(status: StatusLike): {
|
||||
* @returns Props to be passed to the <StatusContent> component, and the hashtagBar to render
|
||||
*/
|
||||
export function getHashtagBarForStatus(status: StatusLike) {
|
||||
const { statusContentProps, hashtagsInBar } =
|
||||
computeHashtagBarForStatus(status);
|
||||
const { statusContent, hashtagsInBar } = computeHashtagBarForStatus({
|
||||
tags: status.get('tags').toJS() as ApiTagJSON[],
|
||||
media_attachments: status
|
||||
.get('media_attachments')
|
||||
.toJS() as MediaAttachmentShape[],
|
||||
spoiler_text: status.get('spoiler_text'),
|
||||
translation: status
|
||||
.get('translation')
|
||||
?.toJSON() as unknown as StatusTranslation,
|
||||
contentHtml: status.get('contentHtml'),
|
||||
});
|
||||
|
||||
return {
|
||||
statusContentProps,
|
||||
statusContentProps: { statusContent },
|
||||
hashtagBar: (
|
||||
<HashtagBar
|
||||
hashtags={hashtagsInBar}
|
||||
@@ -205,7 +223,7 @@ export function getHashtagBarForStatus(status: StatusLike) {
|
||||
};
|
||||
}
|
||||
|
||||
const HashtagBar: React.FC<{
|
||||
export const HashtagBar: React.FC<{
|
||||
hashtags: string[];
|
||||
accountId: string;
|
||||
}> = ({ hashtags, accountId }) => {
|
||||
|
||||
@@ -555,7 +555,7 @@ class Status extends ImmutablePureComponent {
|
||||
const taggedCollection = (
|
||||
status.get('tagged_collections')
|
||||
).find((item) => compareUrls(item.get('url'), cardUrl));
|
||||
|
||||
|
||||
if (taggedCollection) {
|
||||
media = <CollectionPreviewCard collection={taggedCollection.toJS()} headingLevel='h2' />;
|
||||
} else {
|
||||
@@ -579,10 +579,10 @@ class Status extends ImmutablePureComponent {
|
||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||
|
||||
const header = this.props.headerRenderFn
|
||||
? this.props.headerRenderFn({ status, account, avatarSize, messages, onHeaderClick: this.handleHeaderClick, featured })
|
||||
? this.props.headerRenderFn({ statusId: status.get('id'), account, avatarSize, messages, onHeaderClick: this.handleHeaderClick, featured })
|
||||
: (
|
||||
<StatusHeader
|
||||
status={status}
|
||||
statusId={status.get('id')}
|
||||
account={account}
|
||||
avatarSize={avatarSize}
|
||||
onHeaderClick={this.handleHeaderClick}
|
||||
@@ -614,7 +614,7 @@ class Status extends ImmutablePureComponent {
|
||||
|
||||
{matchedFilters && <FilterWarning title={matchedFilters.join(', ')} expanded={this.state.showDespiteFilter} onClick={this.handleFilterToggle} />}
|
||||
|
||||
{(!matchedFilters || this.state.showDespiteFilter) && <ContentWarning status={status} expanded={expanded} onClick={this.handleExpandedToggle} />}
|
||||
{(!matchedFilters || this.state.showDespiteFilter) && <ContentWarning statusId={status.get('id')} expanded={expanded} onClick={this.handleExpandedToggle} />}
|
||||
|
||||
{expanded && (
|
||||
<>
|
||||
|
||||
@@ -23,10 +23,11 @@ const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
||||
|
||||
export const StatusContent: React.FC<{
|
||||
statusId: string;
|
||||
statusContent?: string;
|
||||
onClick?: React.MouseEventHandler;
|
||||
onTranslate?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
collapsible?: boolean;
|
||||
}> = ({ statusId, onClick, onTranslate, collapsible }) => {
|
||||
}> = ({ statusId, statusContent, onClick, onTranslate, collapsible }) => {
|
||||
const status = useStatus(statusId);
|
||||
const { signedIn } = useIdentity();
|
||||
const targetLanguages = useAppSelector(
|
||||
@@ -129,7 +130,9 @@ export const StatusContent: React.FC<{
|
||||
<EmojiHTML
|
||||
className='status__content__text status__content__text--visible translate'
|
||||
lang={language}
|
||||
htmlString={status.translation?.contentHtml ?? status.contentHtml}
|
||||
htmlString={
|
||||
statusContent ?? status.translation?.contentHtml ?? status.contentHtml
|
||||
}
|
||||
extraEmojis={status.emojis}
|
||||
{...htmlHandlers}
|
||||
/>
|
||||
|
||||
@@ -5,9 +5,9 @@ import { defineMessage, useIntl } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { isStatusVisibility } from '@/mastodon/api_types/statuses';
|
||||
import type { Account } from '@/mastodon/models/account';
|
||||
import type { Status } from '@/mastodon/models/status';
|
||||
import type { Account, AccountShapeFull } from '@/mastodon/models/account';
|
||||
import { selectAccountStatus } from '@/mastodon/selectors/statuses';
|
||||
import { useAppSelector } from '@/mastodon/store';
|
||||
|
||||
import { Avatar } from '../avatar';
|
||||
import { AvatarOverlay } from '../avatar_overlay';
|
||||
@@ -17,8 +17,8 @@ import { RelativeTimestamp } from '../relative_timestamp';
|
||||
import { VisibilityIcon } from '../visibility_icon';
|
||||
|
||||
export interface StatusHeaderProps {
|
||||
status: Status;
|
||||
account?: Account;
|
||||
statusId: string;
|
||||
account?: Account | AccountShapeFull;
|
||||
avatarSize?: number;
|
||||
contentBeforeDate?: ReactNode;
|
||||
contentAfterDate?: ReactNode;
|
||||
@@ -32,7 +32,7 @@ export interface StatusHeaderProps {
|
||||
export type StatusHeaderRenderFn = (args: StatusHeaderProps) => ReactNode;
|
||||
|
||||
export const StatusHeader: FC<StatusHeaderProps> = ({
|
||||
status,
|
||||
statusId,
|
||||
account,
|
||||
className,
|
||||
avatarSize = 48,
|
||||
@@ -41,8 +41,14 @@ export const StatusHeader: FC<StatusHeaderProps> = ({
|
||||
contentAfterDate,
|
||||
onHeaderClick,
|
||||
}) => {
|
||||
const statusAccount = status.get('account') as Account | undefined;
|
||||
const editedAt = status.get('edited_at') as string;
|
||||
const status = useAppSelector((state) =>
|
||||
selectAccountStatus(state, statusId),
|
||||
);
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
const statusAccount = status.account;
|
||||
const editedAt = status.edited_at;
|
||||
|
||||
return (
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
|
||||
@@ -62,11 +68,13 @@ export const StatusHeader: FC<StatusHeaderProps> = ({
|
||||
{contentBeforeDate}
|
||||
|
||||
<Link
|
||||
to={`/@${statusAccount?.acct}/${status.get('id') as string}`}
|
||||
to={`/@${statusAccount.acct}/${status.id}`}
|
||||
className='status__relative-time'
|
||||
>
|
||||
<StatusVisibility visibility={status.get('visibility')} />
|
||||
<RelativeTimestamp timestamp={status.get('created_at') as string} />
|
||||
<span className='status__visibility-icon'>
|
||||
<VisibilityIcon visibility={status.visibility} />
|
||||
</span>
|
||||
<RelativeTimestamp timestamp={status.created_at} />
|
||||
{editedAt && <StatusEditedAt editedAt={editedAt} />}
|
||||
</Link>
|
||||
|
||||
@@ -75,25 +83,12 @@ export const StatusHeader: FC<StatusHeaderProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const StatusVisibility: FC<{ visibility: unknown }> = ({
|
||||
visibility,
|
||||
}) => {
|
||||
if (typeof visibility !== 'string' || !isStatusVisibility(visibility)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span className='status__visibility-icon'>
|
||||
<VisibilityIcon visibility={visibility} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const editMessage = defineMessage({
|
||||
id: 'status.edited',
|
||||
defaultMessage: 'Edited {date}',
|
||||
});
|
||||
|
||||
export const StatusEditedAt: FC<{ editedAt: string }> = ({ editedAt }) => {
|
||||
const StatusEditedAt: FC<{ editedAt: string }> = ({ editedAt }) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<abbr
|
||||
@@ -113,9 +108,9 @@ export const StatusEditedAt: FC<{ editedAt: string }> = ({ editedAt }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const StatusDisplayName: FC<{
|
||||
statusAccount?: Account;
|
||||
friendAccount?: Account;
|
||||
const StatusDisplayName: FC<{
|
||||
statusAccount?: AccountShapeFull;
|
||||
friendAccount?: Account | AccountShapeFull;
|
||||
avatarSize: number;
|
||||
}> = ({ statusAccount, friendAccount, avatarSize }) => {
|
||||
const AccountComponent = friendAccount ? AvatarOverlay : Avatar;
|
||||
|
||||
@@ -109,7 +109,7 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
|
||||
</div>
|
||||
|
||||
<ContentWarning
|
||||
status={status}
|
||||
statusId={status.get('id') as string}
|
||||
onClick={handleContentWarningClick}
|
||||
expanded={expanded}
|
||||
/>
|
||||
|
||||
@@ -471,7 +471,7 @@ export const DetailedStatus: React.FC<{
|
||||
|
||||
{(!matchedFilters || showDespiteFilter) && (
|
||||
<ContentWarning
|
||||
status={status}
|
||||
statusId={status.get('id')}
|
||||
expanded={expanded}
|
||||
onClick={handleExpandedToggle}
|
||||
/>
|
||||
|
||||
@@ -55,8 +55,8 @@ export function makeGetAccount() {
|
||||
}
|
||||
|
||||
export const selectPlainAccount = createAppSelector(
|
||||
[(state, accountId: string) => state.accounts.get(accountId)],
|
||||
(account) => (account ? (account.toJS() as AccountShapeFull) : null),
|
||||
[(state, accountId?: string | null) => state.accounts.get(accountId ?? '')],
|
||||
(account) => (account?.toJS() as AccountShapeFull | undefined) ?? null,
|
||||
);
|
||||
|
||||
export const selectIsAccountLocal = createAppSelector(
|
||||
|
||||
Reference in New Issue
Block a user