diff --git a/app/javascript/mastodon/components/content_warning.tsx b/app/javascript/mastodon/components/content_warning.tsx index 6bcae1d6f..a407ec146 100644 --- a/app/javascript/mastodon/components/content_warning.tsx +++ b/app/javascript/mastodon/components/content_warning.tsx @@ -1,15 +1,38 @@ +import type { List } from 'immutable'; + +import type { CustomEmoji } from '../models/custom_emoji'; +import type { Status } from '../models/status'; + +import { EmojiHTML } from './emoji/html'; import { StatusBanner, BannerVariant } from './status_banner'; export const ContentWarning: React.FC<{ - text: string; + status: Status; expanded?: boolean; onClick?: () => void; -}> = ({ text, expanded, onClick }) => ( - - - -); +}> = ({ 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) { + return null; + } + + return ( + + } + /> + + ); +}; diff --git a/app/javascript/mastodon/components/poll.tsx b/app/javascript/mastodon/components/poll.tsx index 80444f640..a9229e6ee 100644 --- a/app/javascript/mastodon/components/poll.tsx +++ b/app/javascript/mastodon/components/poll.tsx @@ -8,6 +8,7 @@ import classNames from 'classnames'; import { animated, useSpring } from '@react-spring/web'; import escapeTextContentForBrowser from 'escape-html'; +import { EmojiHTML } from '@/mastodon/components/emoji/html'; import CheckIcon from '@/material-icons/400-24px/check.svg?react'; import { openModal } from 'mastodon/actions/modal'; import { fetchPoll, vote } from 'mastodon/actions/polls'; @@ -305,10 +306,11 @@ const PollOption: React.FC = (props) => { )} - {!!voted && ( diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 196da7c99..2a8c9bfb2 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -118,7 +118,7 @@ class Status extends ImmutablePureComponent { unread: PropTypes.bool, showThread: PropTypes.bool, isQuotedPost: PropTypes.bool, - shouldHighlightOnMount: PropTypes.bool, + shouldHighlightOnMount: PropTypes.bool, getScrollPosition: PropTypes.func, updateScrollBottom: PropTypes.func, cacheMediaWidth: PropTypes.func, @@ -600,7 +600,7 @@ class Status extends ImmutablePureComponent { {matchedFilters && } - {(status.get('spoiler_text').length > 0 && (!matchedFilters || this.state.showDespiteFilter)) && } + {(!matchedFilters || this.state.showDespiteFilter) && } {expanded && ( <> diff --git a/app/javascript/mastodon/components/status/handled_link.tsx b/app/javascript/mastodon/components/status/handled_link.tsx index d40303818..83262886e 100644 --- a/app/javascript/mastodon/components/status/handled_link.tsx +++ b/app/javascript/mastodon/components/status/handled_link.tsx @@ -83,14 +83,15 @@ export const HandledLink: FC> = ({ export const useElementHandledLink = ({ hashtagAccountId, - mentionAccountId, + hrefToMentionAccountId, }: { hashtagAccountId?: string; - mentionAccountId?: string; + hrefToMentionAccountId?: (href: string) => string | undefined; } = {}) => { const onElement = useCallback( (element, { key, ...props }) => { if (element instanceof HTMLAnchorElement) { + const mentionId = hrefToMentionAccountId?.(element.href); return ( ); } return undefined; }, - [hashtagAccountId, mentionAccountId], + [hashtagAccountId, hrefToMentionAccountId], ); return { onElement }; }; diff --git a/app/javascript/mastodon/components/status_banner.tsx b/app/javascript/mastodon/components/status_banner.tsx index e11b2c927..a1d200133 100644 --- a/app/javascript/mastodon/components/status_banner.tsx +++ b/app/javascript/mastodon/components/status_banner.tsx @@ -3,6 +3,8 @@ import { useCallback, useRef, useId } from 'react'; import { FormattedMessage } from 'react-intl'; +import { AnimateEmojiProvider } from './emoji/context'; + export enum BannerVariant { Warning = 'warning', Filter = 'filter', @@ -34,8 +36,7 @@ export const StatusBanner: React.FC<{ return ( // Element clicks are passed on to button - // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -
)} -
+ ); }; diff --git a/app/javascript/mastodon/features/compose/components/edit_indicator.jsx b/app/javascript/mastodon/features/compose/components/edit_indicator.jsx index 106ff7bda..3fa37bb8c 100644 --- a/app/javascript/mastodon/features/compose/components/edit_indicator.jsx +++ b/app/javascript/mastodon/features/compose/components/edit_indicator.jsx @@ -50,9 +50,7 @@ export const EditIndicator = () => { {(status.get('poll') || status.get('media_attachments').size > 0) && ( diff --git a/app/javascript/mastodon/features/compose/components/reply_indicator.jsx b/app/javascript/mastodon/features/compose/components/reply_indicator.jsx index 35733ac23..e746fe6a6 100644 --- a/app/javascript/mastodon/features/compose/components/reply_indicator.jsx +++ b/app/javascript/mastodon/features/compose/components/reply_indicator.jsx @@ -35,9 +35,7 @@ export const ReplyIndicator = () => { {(status.get('poll') || status.get('media_attachments').size > 0) && ( diff --git a/app/javascript/mastodon/features/directory/components/account_card.tsx b/app/javascript/mastodon/features/directory/components/account_card.tsx index 6dc70532a..562a72b4e 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.tsx +++ b/app/javascript/mastodon/features/directory/components/account_card.tsx @@ -2,6 +2,7 @@ import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom'; +import { EmojiHTML } from '@/mastodon/components/emoji/html'; import { Avatar } from 'mastodon/components/avatar'; import { DisplayName } from 'mastodon/components/display_name'; import { FollowButton } from 'mastodon/components/follow_button'; @@ -39,9 +40,10 @@ export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => { {account.get('note').length > 0 && ( -
)} diff --git a/app/javascript/mastodon/features/emoji/normalize.ts b/app/javascript/mastodon/features/emoji/normalize.ts index 65667dfe6..7c4252017 100644 --- a/app/javascript/mastodon/features/emoji/normalize.ts +++ b/app/javascript/mastodon/features/emoji/normalize.ts @@ -154,6 +154,12 @@ export function cleanExtraEmojis(extraEmojis?: CustomEmojiMapArg) { if (!extraEmojis) { return null; } + if (Array.isArray(extraEmojis)) { + return extraEmojis.reduce( + (acc, emoji) => ({ ...acc, [emoji.shortcode]: emoji }), + {}, + ); + } if (!isList(extraEmojis)) { return extraEmojis; } diff --git a/app/javascript/mastodon/features/emoji/types.ts b/app/javascript/mastodon/features/emoji/types.ts index 043b21361..a98d931ea 100644 --- a/app/javascript/mastodon/features/emoji/types.ts +++ b/app/javascript/mastodon/features/emoji/types.ts @@ -56,7 +56,8 @@ export type EmojiStateMap = LimitedCache; export type CustomEmojiMapArg = | ExtraCustomEmojiMap - | ImmutableList; + | ImmutableList + | CustomEmoji[]; export type ExtraCustomEmojiMap = Record< string, diff --git a/app/javascript/mastodon/features/follow_requests/components/account_authorize.jsx b/app/javascript/mastodon/features/follow_requests/components/account_authorize.jsx index dd308c87c..e865b606f 100644 --- a/app/javascript/mastodon/features/follow_requests/components/account_authorize.jsx +++ b/app/javascript/mastodon/features/follow_requests/components/account_authorize.jsx @@ -10,9 +10,10 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import CheckIcon from '@/material-icons/400-24px/check.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; -import { Avatar } from '../../../components/avatar'; -import { DisplayName } from '../../../components/display_name'; -import { IconButton } from '../../../components/icon_button'; +import { Avatar } from '@/mastodon/components/avatar'; +import { DisplayName } from '@/mastodon/components/display_name'; +import { IconButton } from '@/mastodon/components/icon_button'; +import { EmojiHTML } from '@/mastodon/components/emoji/html'; const messages = defineMessages({ authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' }, @@ -30,7 +31,6 @@ class AccountAuthorize extends ImmutablePureComponent { render () { const { intl, account, onAuthorize, onReject } = this.props; - const content = { __html: account.get('note_emojified') }; return (
@@ -40,7 +40,11 @@ class AccountAuthorize extends ImmutablePureComponent { -
+
diff --git a/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx b/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx index 8e5e72b6a..49bf364f0 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx @@ -6,6 +6,7 @@ import { useHistory } from 'react-router-dom'; import type { List as ImmutableList, RecordOf } from 'immutable'; +import type { ApiMentionJSON } from '@/mastodon/api_types/statuses'; import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context'; import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react'; import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react'; @@ -18,7 +19,7 @@ import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { EmbeddedStatusContent } from './embedded_status_content'; -export type Mention = RecordOf<{ url: string; acct: string }>; +export type Mention = RecordOf; export const EmbeddedStatus: React.FC<{ statusId: string }> = ({ statusId, @@ -86,12 +87,9 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({ } // Assign status attributes to variables with a forced type, as status is not yet properly typed - const contentHtml = status.get('contentHtml') as string; - const contentWarning = status.get('spoilerHtml') as string; + const hasContentWarning = !!status.get('spoiler_text'); const poll = status.get('poll'); - const language = status.get('language') as string; - const mentions = status.get('mentions') as ImmutableList; - const expanded = !status.get('hidden') || !contentWarning; + const expanded = !status.get('hidden') || !hasContentWarning; const mediaAttachmentsSize = ( status.get('media_attachments') as ImmutableList ).size; @@ -109,20 +107,16 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
- {contentWarning && ( - - )} + - {(!contentWarning || expanded) && ( + {(!hasContentWarning || expanded) && ( )} diff --git a/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx b/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx index 855e160fa..91c3abde3 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; @@ -6,16 +6,22 @@ import type { List } from 'immutable'; import type { History } from 'history'; +import type { ApiMentionJSON } from '@/mastodon/api_types/statuses'; +import { EmojiHTML } from '@/mastodon/components/emoji/html'; +import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; +import type { Status } from '@/mastodon/models/status'; +import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; + import type { Mention } from './embedded_status'; const handleMentionClick = ( history: History, - mention: Mention, + mention: ApiMentionJSON, e: MouseEvent, ) => { if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); - history.push(`/@${mention.get('acct')}`); + history.push(`/@${mention.acct}`); } }; @@ -31,16 +37,26 @@ const handleHashtagClick = ( }; export const EmbeddedStatusContent: React.FC<{ - content: string; - mentions: List; - language: string; + status: Status; className?: string; -}> = ({ content, mentions, language, className }) => { +}> = ({ status, className }) => { const history = useHistory(); + const mentions = useMemo( + () => (status.get('mentions') as List).toJS(), + [status], + ); + const htmlHandlers = useElementHandledLink({ + hashtagAccountId: status.get('account') as string | undefined, + hrefToMentionAccountId(href) { + const mention = mentions.find((item) => item.url === href); + return mention?.id; + }, + }); + const handleContentRef = useCallback( (node: HTMLDivElement | null) => { - if (!node) { + if (!node || isModernEmojiEnabled()) { return; } @@ -53,7 +69,7 @@ export const EmbeddedStatusContent: React.FC<{ link.classList.add('status-link'); - const mention = mentions.find((item) => link.href === item.get('url')); + const mention = mentions.find((item) => link.href === item.url); if (mention) { link.addEventListener( @@ -61,8 +77,8 @@ export const EmbeddedStatusContent: React.FC<{ handleMentionClick.bind(null, history, mention), false, ); - link.setAttribute('title', `@${mention.get('acct')}`); - link.setAttribute('href', `/@${mention.get('acct')}`); + link.setAttribute('title', `@${mention.acct}`); + link.setAttribute('href', `/@${mention.acct}`); } else if ( link.textContent.startsWith('#') || link.previousSibling?.textContent?.endsWith('#') @@ -83,11 +99,12 @@ export const EmbeddedStatusContent: React.FC<{ ); return ( -
); }; diff --git a/app/javascript/mastodon/features/status/components/detailed_status.tsx b/app/javascript/mastodon/features/status/components/detailed_status.tsx index b09e109af..9b525b616 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.tsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.tsx @@ -394,17 +394,13 @@ export const DetailedStatus: React.FC<{ /> )} - {status.get('spoiler_text').length > 0 && - (!matchedFilters || showDespiteFilter) && ( - - )} + {(!matchedFilters || showDespiteFilter) && ( + + )} {expanded && ( <>