diff --git a/app/javascript/mastodon/components/display_name/no-domain.tsx b/app/javascript/mastodon/components/display_name/no-domain.tsx index ccb9a62ab76..3a66fe5042c 100644 --- a/app/javascript/mastodon/components/display_name/no-domain.tsx +++ b/app/javascript/mastodon/components/display_name/no-domain.tsx @@ -14,7 +14,10 @@ export const DisplayNameWithoutDomain: FC< ComponentPropsWithoutRef<'span'> > = ({ account, className, children, ...props }) => { return ( - + {account ? ( { - 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(); } @@ -257,7 +231,13 @@ 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 +778,9 @@ 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 9aae588bcc7..dc1461e5b41 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx @@ -23,7 +23,6 @@ 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'; @@ -57,32 +56,6 @@ export const Conversation = ({ conversation, scrollKey }) => { 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 handleClick = useCallback(() => { if (unread) { dispatch(markConversationRead(id)); @@ -163,7 +136,7 @@ export const Conversation = ({ conversation, scrollKey }) => { {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..9d317efd437 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'; @@ -44,39 +43,6 @@ 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 handleFollow = useCallback(() => { if (!account) return; @@ -185,9 +151,7 @@ export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => { {account.get('note').length > 0 && (
)} diff --git a/app/javascript/mastodon/features/emoji/emoji_html.tsx b/app/javascript/mastodon/features/emoji/emoji_html.tsx index e143c9fc166..08d62b2c37a 100644 --- a/app/javascript/mastodon/features/emoji/emoji_html.tsx +++ b/app/javascript/mastodon/features/emoji/emoji_html.tsx @@ -1,5 +1,7 @@ import type { ComponentPropsWithoutRef, ElementType } from 'react'; +import classNames from 'classnames'; + import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; import { useEmojify } from './hooks'; @@ -7,12 +9,13 @@ import type { CustomEmojiMapArg } from './types'; type EmojiHTMLProps = Omit< ComponentPropsWithoutRef, - 'dangerouslySetInnerHTML' + 'dangerouslySetInnerHTML' | 'className' > & { htmlString: string; extraEmojis?: CustomEmojiMapArg; as?: Element; shallow?: boolean; + className?: string; }; export const ModernEmojiHTML = ({ @@ -20,6 +23,7 @@ export const ModernEmojiHTML = ({ htmlString, as: Wrapper = 'div', // Rename for syntax highlighting shallow, + className = '', ...props }: EmojiHTMLProps) => { const emojifiedHtml = useEmojify({ @@ -33,7 +37,11 @@ export const ModernEmojiHTML = ({ } return ( - + ); }; @@ -43,7 +51,13 @@ export const EmojiHTML = ( if (isModernEmojiEnabled()) { return ; } - const { as: asElement, htmlString, extraEmojis, ...rest } = props; + const { as: asElement, htmlString, extraEmojis, className, ...rest } = props; const Wrapper = asElement ?? 'div'; - return ; + 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..3b02028f3c3 --- /dev/null +++ b/app/javascript/mastodon/features/emoji/handlers.ts @@ -0,0 +1,61 @@ +import { autoPlayGif } from '@/mastodon/initial_state'; + +const PARENT_MAX_DEPTH = 10; + +export function handleAnimateGif(event: MouseEvent) { + // We already check this in ui/index.jsx, but just to be sure. + if (autoPlayGif) { + return; + } + + const { target, type } = event; + const animate = type === 'mouseover'; // Mouse over = animate, mouse out = don't animate. + + if (target instanceof HTMLImageElement) { + setAnimateGif(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 { + // Iterate up to PARENT_MAX_DEPTH levels up the DOM tree to find a parent with the class 'animate-parent'. + 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++; + } + } + + // Affect all animated children within the parent. + if (parent) { + const animatedChildren = + parent.querySelectorAll('img.custom-emoji'); + for (const child of animatedChildren) { + setAnimateGif(child, animate); + } + } +} + +function setAnimateGif(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/getting_started/components/announcements.jsx b/app/javascript/mastodon/features/getting_started/components/announcements.jsx index 87d7e2a3bec..96bd995d2b1 100644 --- a/app/javascript/mastodon/features/getting_started/components/announcements.jsx +++ b/app/javascript/mastodon/features/getting_started/components/announcements.jsx @@ -111,42 +111,14 @@ 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 +210,21 @@ 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..a17425169b8 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/embedded_status.tsx @@ -76,32 +76,6 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({ [clickCoordinatesRef, statusId, account, history], ); - const handleMouseEnter = useCallback>( - ({ currentTarget }) => { - const emojis = - currentTarget.querySelectorAll('.custom-emoji'); - - 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 handleContentWarningClick = useCallback(() => { dispatch(toggleStatusSpoilers(statusId)); }, [dispatch, statusId]); @@ -123,13 +97,11 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({ return (
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 0583bf99c5e..efec38caf4b 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -22,11 +22,12 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex import { layoutFromWindow } from 'mastodon/is_mobile'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; +import { handleAnimateGif } from '../emoji/handlers'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose'; import { clearHeight } from '../../actions/height_cache'; import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server'; import { expandHomeTimeline } from '../../actions/timelines'; -import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards } from '../../initial_state'; +import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards, autoPlayGif } from '../../initial_state'; import BundleColumnError from './components/bundle_column_error'; import { NavigationBar } from './components/navigation_bar'; @@ -379,6 +380,11 @@ class UI extends PureComponent { window.addEventListener('beforeunload', this.handleBeforeUnload, false); window.addEventListener('resize', this.handleResize, { passive: true }); + if (!autoPlayGif) { + window.addEventListener('mouseover', handleAnimateGif, { passive: true }); + window.addEventListener('mouseout', handleAnimateGif, { passive: true }); + } + document.addEventListener('dragenter', this.handleDragEnter, false); document.addEventListener('dragover', this.handleDragOver, false); document.addEventListener('drop', this.handleDrop, false); @@ -404,6 +410,8 @@ class UI extends PureComponent { window.removeEventListener('blur', this.handleWindowBlur); window.removeEventListener('beforeunload', this.handleBeforeUnload); window.removeEventListener('resize', this.handleResize); + window.removeEventListener('mouseover', handleAnimateGif); + window.removeEventListener('mouseout', handleAnimateGif); document.removeEventListener('dragenter', this.handleDragEnter); document.removeEventListener('dragover', this.handleDragOver);