From f04360c43e34a6aba86d72f8983474067efa569c Mon Sep 17 00:00:00 2001 From: ChaosExAnima Date: Wed, 30 Jul 2025 18:16:41 +0200 Subject: [PATCH] replace duplicated handler code --- .../components/display_name/index.tsx | 10 ++- .../mastodon/components/status_content.jsx | 43 ++++------ .../components/account_header.tsx | 40 ++-------- .../components/conversation.jsx | 31 +------- .../directory/components/account_card.tsx | 41 ++-------- .../mastodon/features/emoji/emoji_html.tsx | 17 ++-- .../mastodon/features/emoji/handlers.ts | 78 +++++++++++++++++++ .../mastodon/features/emoji/index.ts | 28 ++++--- .../components/announcements.jsx | 50 +++++------- .../components/embedded_status.tsx | 30 ++----- 10 files changed, 168 insertions(+), 200 deletions(-) create mode 100644 app/javascript/mastodon/features/emoji/handlers.ts diff --git a/app/javascript/mastodon/components/display_name/index.tsx b/app/javascript/mastodon/components/display_name/index.tsx index 31abfb673ea..cc110da36b9 100644 --- a/app/javascript/mastodon/components/display_name/index.tsx +++ b/app/javascript/mastodon/components/display_name/index.tsx @@ -61,13 +61,19 @@ export const DisplayName: FC> = ({ if (simple) { return ( - + ); } return ( - + { - if (autoPlayGif) { - return; - } - - const emojis = currentTarget.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - emoji.src = emoji.getAttribute('data-original'); - } - }; - - handleMouseLeave = ({ currentTarget }) => { - if (autoPlayGif) { - return; - } - - const emojis = currentTarget.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - emoji.src = emoji.getAttribute('data-static'); - } - }; - componentDidMount () { this._updateStatusLinks(); } @@ -245,7 +220,7 @@ class StatusContent extends PureComponent { const content = statusContent ?? getStatusContent(status); const language = status.getIn(['translation', 'language']) || status.get('language'); - const classNames = classnames('status__content', { + const classNames = classnames('status__content animate-parent', { 'status__content--with-action': this.props.onClick && this.props.history, 'status__content--collapsed': renderReadMore, }); @@ -267,7 +242,15 @@ class StatusContent extends PureComponent { if (this.props.onClick) { return ( <> -
+
+
{ - if (autoPlayGif) { - return; - } - - currentTarget - .querySelectorAll('.custom-emoji') - .forEach((emoji) => { - emoji.src = emoji.getAttribute('data-original') ?? ''; - }); - }, - [], - ); - - const handleMouseLeave = useCallback( - ({ currentTarget }: React.MouseEvent) => { - if (autoPlayGif) { - return; - } - - currentTarget - .querySelectorAll('.custom-emoji') - .forEach((emoji) => { - emoji.src = emoji.getAttribute('data-static') ?? ''; - }); - }, - [], - ); - const suspended = account?.suspended; const isRemote = account?.acct !== account?.username; const remoteDomain = isRemote ? account?.acct.split('@')[1] : null; @@ -808,11 +782,11 @@ export const AccountHeader: React.FC<{ )}
{!(suspended || hidden || account.moved) && relationship?.requested_by && ( diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx index 0f5bea891c7..0f1c1179799 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx @@ -23,9 +23,9 @@ import { IconButton } from 'mastodon/components/icon_button'; import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; import StatusContent from 'mastodon/components/status_content'; import { Dropdown } from 'mastodon/components/dropdown_menu'; -import { autoPlayGif } from 'mastodon/initial_state'; import { makeGetStatus } from 'mastodon/selectors'; import { LinkedDisplayName } from '@/mastodon/components/display_name'; +import { handleAnimateEnter, handleAnimateLeave } from '@/mastodon/features/emoji/handlers'; const messages = defineMessages({ more: { id: 'status.more', defaultMessage: 'More' }, @@ -57,31 +57,8 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown }) const lastStatus = useSelector(state => getStatus(state, { id: lastStatusId })); const accounts = useSelector(state => getAccounts(state, accountIds)); - const handleMouseEnter = useCallback(({ currentTarget }) => { - if (autoPlayGif) { - return; - } - - const emojis = currentTarget.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - emoji.src = emoji.getAttribute('data-original'); - } - }, []); - - const handleMouseLeave = useCallback(({ currentTarget }) => { - if (autoPlayGif) { - return; - } - - const emojis = currentTarget.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - emoji.src = emoji.getAttribute('data-static'); - } - }, []); + const handleMouseEnter = useCallback(handleAnimateEnter, []); + const handleMouseLeave = useCallback(handleAnimateLeave, []); const handleClick = useCallback(() => { if (unread) { @@ -173,7 +150,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown }) {unread && }
-
+
{names} }} />
diff --git a/app/javascript/mastodon/features/directory/components/account_card.tsx b/app/javascript/mastodon/features/directory/components/account_card.tsx index 2a0470bb728..3fe9db4db61 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.tsx +++ b/app/javascript/mastodon/features/directory/components/account_card.tsx @@ -1,4 +1,3 @@ -import type { MouseEventHandler } from 'react'; import { useCallback } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; @@ -6,6 +5,10 @@ import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import classNames from 'classnames'; import { Link } from 'react-router-dom'; +import { + handleAnimateEnter, + handleAnimateLeave, +} from '@/mastodon/features/emoji/handlers'; import { followAccount, unblockAccount, @@ -44,38 +47,8 @@ export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => { const account = useAppSelector((s) => getAccount(s, accountId)); const dispatch = useAppDispatch(); - const handleMouseEnter = useCallback( - ({ currentTarget }) => { - if (autoPlayGif) { - return; - } - const emojis = - currentTarget.querySelectorAll('.custom-emoji'); - - emojis.forEach((emoji) => { - const original = emoji.getAttribute('data-original'); - if (original) emoji.src = original; - }); - }, - [], - ); - - const handleMouseLeave = useCallback( - ({ currentTarget }) => { - if (autoPlayGif) { - return; - } - - const emojis = - currentTarget.querySelectorAll('.custom-emoji'); - - emojis.forEach((emoji) => { - const staticUrl = emoji.getAttribute('data-static'); - if (staticUrl) emoji.src = staticUrl; - }); - }, - [], - ); + const handleMouseEnter = useCallback(handleAnimateEnter, []); + const handleMouseLeave = useCallback(handleAnimateLeave, []); const handleFollow = useCallback(() => { if (!account) return; @@ -185,7 +158,7 @@ export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => { {account.get('note').length > 0 && (
= Omit< ComponentPropsWithoutRef, - 'dangerouslySetInnerHTML' + 'dangerouslySetInnerHTML' | 'className' > & { htmlString: string; extraEmojis?: CustomEmojiMapArg; as?: Element; shallow?: boolean; + className?: string; }; -export const EmojiHTML = ({ +export const EmojiHTML = ({ extraEmojis, htmlString, - as: asElement, // Rename for syntax highlighting + as: Wrapper = 'div', // Rename for syntax highlighting shallow, + className = '', ...props -}: EmojiHTMLProps) => { - const Wrapper = asElement ?? 'div'; +}: EmojiHTMLProps) => { const emojifiedHtml = useEmojify({ text: htmlString, extraEmojis, @@ -32,6 +33,10 @@ export const EmojiHTML = ({ } return ( - + ); }; diff --git a/app/javascript/mastodon/features/emoji/handlers.ts b/app/javascript/mastodon/features/emoji/handlers.ts new file mode 100644 index 00000000000..6b4c641fb21 --- /dev/null +++ b/app/javascript/mastodon/features/emoji/handlers.ts @@ -0,0 +1,78 @@ +import type { MouseEventHandler } from 'react'; + +import { autoPlayGif } from '@/mastodon/initial_state'; +import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; + +export const handleAnimateEnter: MouseEventHandler = ({ currentTarget }) => { + if (autoPlayGif || isModernEmojiEnabled()) { + return; + } + + currentTarget + .querySelectorAll('img.custom-emoji') + .forEach((emoji) => { + toggleAnimatedGif(emoji, true); + }); +}; + +export const handleAnimateLeave: MouseEventHandler = ({ currentTarget }) => { + if (autoPlayGif || isModernEmojiEnabled()) { + return; + } + + currentTarget + .querySelectorAll('img.custom-emoji') + .forEach((emoji) => { + toggleAnimatedGif(emoji, false); + }); +}; + +const PARENT_MAX_DEPTH = 10; + +export function handleAnimateGif(event: MouseEvent) { + const { target, type } = event; + const animate = type === 'mouseover'; + if (target instanceof HTMLImageElement) { + toggleAnimatedGif(target, animate); + } else if (!(target instanceof HTMLElement) || target === document.body) { + return; + } + let parent: HTMLElement | null = null; + let iter = 0; + if (target.classList.contains('animate-parent')) { + parent = target; + } else { + let current: HTMLElement | null = target; + while (current) { + if (iter >= PARENT_MAX_DEPTH) { + return; // We can just exit right now. + } + current = current.parentElement; + if (current?.classList.contains('animate-parent')) { + parent = current; + break; + } + iter++; + } + } + + if (parent) { + const animatedChildren = + parent.querySelectorAll('img.custom-emoji'); + for (const child of animatedChildren) { + toggleAnimatedGif(child, animate); + } + } +} + +function toggleAnimatedGif(image: HTMLImageElement, animate: boolean) { + const { classList, dataset } = image; + if ( + !classList.contains('custom-emoji') || + !dataset.static || + !dataset.original + ) { + return; + } + image.src = animate ? dataset.original : dataset.static; +} diff --git a/app/javascript/mastodon/features/emoji/index.ts b/app/javascript/mastodon/features/emoji/index.ts index 99c16fe361c..6d8535ca612 100644 --- a/app/javascript/mastodon/features/emoji/index.ts +++ b/app/javascript/mastodon/features/emoji/index.ts @@ -1,6 +1,7 @@ -import initialState from '@/mastodon/initial_state'; +import initialState, { autoPlayGif } from '@/mastodon/initial_state'; import { loadWorker } from '@/mastodon/utils/workers'; +import { handleAnimateGif } from './handlers'; import { toSupportedLocale } from './locale'; import { emojiLogger } from './utils'; @@ -22,6 +23,11 @@ export function initializeEmoji() { } } + if (typeof document !== 'undefined' && !autoPlayGif) { + document.addEventListener('mouseover', handleAnimateGif, { passive: true }); + document.addEventListener('mouseout', handleAnimateGif, { passive: true }); + } + if (worker) { // Assign worker to const to make TS happy inside the event listener. const thisWorker = worker; @@ -51,16 +57,6 @@ export function initializeEmoji() { } } -async function fallbackLoad() { - log('falling back to main thread for loading'); - const { importCustomEmojiData } = await import('./loader'); - await importCustomEmojiData(); - await loadEmojiLocale(userLocale); - if (userLocale !== 'en') { - await loadEmojiLocale('en'); - } -} - export async function loadEmojiLocale(localeString: string) { const locale = toSupportedLocale(localeString); @@ -71,3 +67,13 @@ export async function loadEmojiLocale(localeString: string) { await importEmojiData(locale); } } + +async function fallbackLoad() { + log('falling back to main thread for loading'); + const { importCustomEmojiData } = await import('./loader'); + await importCustomEmojiData(); + await loadEmojiLocale(userLocale); + if (userLocale !== 'en') { + await loadEmojiLocale('en'); + } +} diff --git a/app/javascript/mastodon/features/getting_started/components/announcements.jsx b/app/javascript/mastodon/features/getting_started/components/announcements.jsx index 87d7e2a3bec..1240c422501 100644 --- a/app/javascript/mastodon/features/getting_started/components/announcements.jsx +++ b/app/javascript/mastodon/features/getting_started/components/announcements.jsx @@ -24,6 +24,7 @@ import { unicodeMapping } from 'mastodon/features/emoji/emoji_unicode_mapping_li import { autoPlayGif, reduceMotion, disableSwiping, mascot } from 'mastodon/initial_state'; import { assetHost } from 'mastodon/utils/config'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; +import { handleAnimateEnter, handleAnimateLeave } from '../../emoji/handlers'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -111,42 +112,16 @@ class ContentWithRouter extends ImmutablePureComponent { } }; - handleMouseEnter = ({ currentTarget }) => { - if (autoPlayGif) { - return; - } - - const emojis = currentTarget.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - emoji.src = emoji.getAttribute('data-original'); - } - }; - - handleMouseLeave = ({ currentTarget }) => { - if (autoPlayGif) { - return; - } - - const emojis = currentTarget.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - emoji.src = emoji.getAttribute('data-static'); - } - }; - render () { const { announcement } = this.props; return (
); } @@ -238,9 +213,20 @@ class Reaction extends ImmutablePureComponent { } return ( - - - + + + + + + + ); } 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 f63d42f8260..abf6297ca31 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx @@ -15,6 +15,8 @@ import { DisplayName } from 'mastodon/components/display_name'; import { Icon } from 'mastodon/components/icon'; import { useAppSelector, useAppDispatch } from 'mastodon/store'; +import { handleAnimateEnter, handleAnimateLeave } from '../../emoji/handlers'; + import { EmbeddedStatusContent } from './embedded_status_content'; export type Mention = RecordOf<{ url: string; acct: string }>; @@ -76,31 +78,9 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({ [clickCoordinatesRef, statusId, account, history], ); - const handleMouseEnter = useCallback>( - ({ currentTarget }) => { - const emojis = - currentTarget.querySelectorAll('.custom-emoji'); + const handleMouseEnter = useCallback(handleAnimateEnter, []); - for (const emoji of emojis) { - const newSrc = emoji.getAttribute('data-original'); - if (newSrc) emoji.src = newSrc; - } - }, - [], - ); - - const handleMouseLeave = useCallback>( - ({ currentTarget }) => { - const emojis = - currentTarget.querySelectorAll('.custom-emoji'); - - for (const emoji of emojis) { - const newSrc = emoji.getAttribute('data-static'); - if (newSrc) emoji.src = newSrc; - } - }, - [], - ); + const handleMouseLeave = useCallback(handleAnimateLeave, []); const handleContentWarningClick = useCallback(() => { dispatch(toggleStatusSpoilers(statusId)); @@ -123,7 +103,7 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({ return (