diff --git a/.gitignore b/.gitignore index db63bc07f0..4727d9ec27 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ /public/packs /public/packs-dev /public/packs-test +stats.html .env .env.production node_modules/ diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx index fea3eb0d79..dd1956446d 100644 --- a/app/javascript/entrypoints/public.tsx +++ b/app/javascript/entrypoints/public.tsx @@ -70,7 +70,7 @@ function loaded() { }; document.querySelectorAll('.emojify').forEach((content) => { - content.innerHTML = emojify(content.innerHTML, {}, true); // Force emojify as public doesn't load the new emoji system. + content.innerHTML = emojify(content.innerHTML); }); document diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 7723379804..32c3d76666 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -1,8 +1,5 @@ import escapeTextContentForBrowser from 'escape-html'; -import { makeEmojiMap } from 'mastodon/models/custom_emoji'; - -import emojify from '../../features/emoji/emoji'; import { expandSpoilers } from '../../initial_state'; const domParser = new DOMParser(); @@ -88,11 +85,10 @@ export function normalizeStatus(status, normalOldStatus) { const spoilerText = normalStatus.spoiler_text || ''; const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); - const emojiMap = makeEmojiMap(normalStatus.emojis); normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; - normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); - normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap); + normalStatus.contentHtml = normalStatus.content; + normalStatus.spoilerHtml = escapeTextContentForBrowser(spoilerText); normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive; // Remove quote fallback link from the DOM so it doesn't mess with paragraph margins @@ -128,14 +124,12 @@ export function normalizeStatus(status, normalOldStatus) { } export function normalizeStatusTranslation(translation, status) { - const emojiMap = makeEmojiMap(status.get('emojis').toJS()); - const normalTranslation = { detected_source_language: translation.detected_source_language, language: translation.language, provider: translation.provider, - contentHtml: emojify(translation.content, emojiMap), - spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap), + contentHtml: translation.content, + spoilerHtml: escapeTextContentForBrowser(translation.spoiler_text), spoiler_text: translation.spoiler_text, }; @@ -149,9 +143,8 @@ export function normalizeStatusTranslation(translation, status) { export function normalizeAnnouncement(announcement) { const normalAnnouncement = { ...announcement }; - const emojiMap = makeEmojiMap(normalAnnouncement.emojis); - normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap); + normalAnnouncement.contentHtml = normalAnnouncement.content; return normalAnnouncement; } diff --git a/app/javascript/mastodon/components/account_bio.tsx b/app/javascript/mastodon/components/account_bio.tsx index e87ae654fd..6d4ab1ddd4 100644 --- a/app/javascript/mastodon/components/account_bio.tsx +++ b/app/javascript/mastodon/components/account_bio.tsx @@ -1,11 +1,6 @@ -import { useCallback } from 'react'; - import classNames from 'classnames'; -import { useLinks } from 'mastodon/hooks/useLinks'; - import { useAppSelector } from '../store'; -import { isModernEmojiEnabled } from '../utils/environment'; import { EmojiHTML } from './emoji/html'; import { useElementHandledLink } from './status/handled_link'; @@ -21,22 +16,6 @@ export const AccountBio: React.FC = ({ accountId, showDropdown = false, }) => { - const handleClick = useLinks(showDropdown); - const handleNodeChange = useCallback( - (node: HTMLDivElement | null) => { - if ( - !showDropdown || - !node || - node.childNodes.length === 0 || - isModernEmojiEnabled() - ) { - return; - } - addDropdownToHashtags(node, accountId); - }, - [showDropdown, accountId], - ); - const htmlHandlers = useElementHandledLink({ hashtagAccountId: showDropdown ? accountId : undefined, }); @@ -62,30 +41,7 @@ export const AccountBio: React.FC = ({ htmlString={note} extraEmojis={extraEmojis} className={classNames(className, 'translate')} - onClickCapture={handleClick} - ref={handleNodeChange} {...htmlHandlers} /> ); }; - -function addDropdownToHashtags(node: HTMLElement | null, accountId: string) { - if (!node) { - return; - } - for (const childNode of node.childNodes) { - if (!(childNode instanceof HTMLElement)) { - continue; - } - if ( - childNode instanceof HTMLAnchorElement && - (childNode.classList.contains('hashtag') || - childNode.innerText.startsWith('#')) && - !childNode.dataset.menuHashtag - ) { - childNode.dataset.menuHashtag = accountId; - } else if (childNode.childNodes.length > 0) { - addDropdownToHashtags(childNode, accountId); - } - } -} diff --git a/app/javascript/mastodon/components/emoji/context.tsx b/app/javascript/mastodon/components/emoji/context.tsx index 730ae743ed..3682b94141 100644 --- a/app/javascript/mastodon/components/emoji/context.tsx +++ b/app/javascript/mastodon/components/emoji/context.tsx @@ -7,8 +7,6 @@ import { useState, } from 'react'; -import classNames from 'classnames'; - import { cleanExtraEmojis } from '@/mastodon/features/emoji/normalize'; import { autoPlayGif } from '@/mastodon/initial_state'; import { polymorphicForwardRef } from '@/types/polymorphic'; @@ -65,11 +63,7 @@ export const AnimateEmojiProvider = polymorphicForwardRef< const parentContext = useContext(AnimateEmojiContext); if (parentContext !== null) { return ( - + {children} ); @@ -78,7 +72,7 @@ export const AnimateEmojiProvider = polymorphicForwardRef< return ( ( +export const EmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>( ( { extraEmojis, @@ -59,32 +56,4 @@ export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>( ); }, ); -ModernEmojiHTML.displayName = 'ModernEmojiHTML'; - -export const LegacyEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>( - (props, ref) => { - const { - as: asElement, - htmlString, - extraEmojis, - className, - onElement, - onAttribute, - ...rest - } = props; - const Wrapper = asElement ?? 'div'; - return ( - - ); - }, -); -LegacyEmojiHTML.displayName = 'LegacyEmojiHTML'; - -export const EmojiHTML = isModernEmojiEnabled() - ? ModernEmojiHTML - : LegacyEmojiHTML; +EmojiHTML.displayName = 'EmojiHTML'; diff --git a/app/javascript/mastodon/components/hover_card_account.tsx b/app/javascript/mastodon/components/hover_card_account.tsx index 471d488415..b51af40e94 100644 --- a/app/javascript/mastodon/components/hover_card_account.tsx +++ b/app/javascript/mastodon/components/hover_card_account.tsx @@ -23,8 +23,6 @@ import { domain } from 'mastodon/initial_state'; import { getAccountHidden } from 'mastodon/selectors/accounts'; import { useAppSelector, useAppDispatch } from 'mastodon/store'; -import { useLinks } from '../hooks/useLinks'; - export const HoverCardAccount = forwardRef< HTMLDivElement, { accountId?: string } @@ -66,8 +64,6 @@ export const HoverCardAccount = forwardRef< !isMutual && !isFollower; - const handleClick = useLinks(); - return (

-
+
onParentElement?.(...args) ?? onLinkElement(...args), [onLinkElement, onParentElement], ); - return ; + return ; }, ); diff --git a/app/javascript/mastodon/components/poll.tsx b/app/javascript/mastodon/components/poll.tsx index a9229e6ee4..98954fc2d9 100644 --- a/app/javascript/mastodon/components/poll.tsx +++ b/app/javascript/mastodon/components/poll.tsx @@ -13,9 +13,7 @@ import CheckIcon from '@/material-icons/400-24px/check.svg?react'; import { openModal } from 'mastodon/actions/modal'; import { fetchPoll, vote } from 'mastodon/actions/polls'; import { Icon } from 'mastodon/components/icon'; -import emojify from 'mastodon/features/emoji/emoji'; import { useIdentity } from 'mastodon/identity_context'; -import { makeEmojiMap } from 'mastodon/models/custom_emoji'; import type * as Model from 'mastodon/models/poll'; import type { Status } from 'mastodon/models/status'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; @@ -235,12 +233,11 @@ const PollOption: React.FC = (props) => { let titleHtml = option.translation?.titleHtml ?? option.titleHtml; if (!titleHtml) { - const emojiMap = makeEmojiMap(poll.emojis); - titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap); + titleHtml = escapeTextContentForBrowser(title); } return titleHtml; - }, [option, poll, title]); + }, [option, title]); // Handlers const handleOptionChange = useCallback(() => { diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index cef2be6281..81396bb85e 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -15,8 +15,6 @@ import { Poll } from 'mastodon/components/poll'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { languages as preloadedLanguages } from 'mastodon/initial_state'; -import { isModernEmojiEnabled } from '../utils/environment'; - import { EmojiHTML } from './emoji/html'; import { HandledLink } from './status/handled_link'; @@ -119,41 +117,6 @@ class StatusContent extends PureComponent { onCollapsedToggle(collapsed); } - - // Exit if modern emoji is enabled, as it handles links using the HandledLink component. - if (isModernEmojiEnabled()) { - return; - } - - 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')) { - continue; - } - - link.classList.add('status-link'); - - mention = this.props.status.get('mentions').find(item => compareUrls(link, 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')}`); - link.setAttribute('data-hover-card-account', mention.get('id')); - } 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.setAttribute('data-menu-hashtag', this.props.status.getIn(['account', 'id'])); - } else { - link.setAttribute('title', link.href); - link.classList.add('unhandled-link'); - } - } } componentDidMount () { @@ -164,22 +127,6 @@ class StatusContent extends PureComponent { this._updateStatusLinks(); } - onMentionClick = (mention, e) => { - if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - this.props.history.push(`/@${mention.get('acct')}`); - } - }; - - onHashtagClick = (hashtag, e) => { - hashtag = hashtag.replace(/^#/, ''); - - if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - this.props.history.push(`/tags/${hashtag}`); - } - }; - handleMouseDown = (e) => { this.startXY = [e.clientX, e.clientY]; }; diff --git a/app/javascript/mastodon/components/verified_badge.tsx b/app/javascript/mastodon/components/verified_badge.tsx index 43edbc7951..cc8d8bf409 100644 --- a/app/javascript/mastodon/components/verified_badge.tsx +++ b/app/javascript/mastodon/components/verified_badge.tsx @@ -1,30 +1,10 @@ import { EmojiHTML } from '@/mastodon/components/emoji/html'; import CheckIcon from '@/material-icons/400-24px/check.svg?react'; -import { isModernEmojiEnabled } from '../utils/environment'; import type { OnAttributeHandler } from '../utils/html'; import { Icon } from './icon'; -const domParser = new DOMParser(); - -const stripRelMe = (html: string) => { - if (isModernEmojiEnabled()) { - return html; - } - const document = domParser.parseFromString(html, 'text/html').documentElement; - - document.querySelectorAll('a[rel]').forEach((link) => { - link.rel = link.rel - .split(' ') - .filter((x: string) => x !== 'me') - .join(' '); - }); - - const body = document.querySelector('body'); - return body?.innerHTML ?? ''; -}; - const onAttribute: OnAttributeHandler = (name, value, tagName) => { if (name === 'rel' && tagName === 'a') { if (value === 'me') { @@ -47,10 +27,6 @@ interface Props { export const VerifiedBadge: React.FC = ({ link }) => ( - + ); diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx index 2bf636d060..040ca16c72 100644 --- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx @@ -49,7 +49,6 @@ import { ShortNumber } from 'mastodon/components/short_number'; import { AccountNote } from 'mastodon/features/account/components/account_note'; import { DomainPill } from 'mastodon/features/account/components/domain_pill'; import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container'; -import { useLinks } from 'mastodon/hooks/useLinks'; import { useIdentity } from 'mastodon/identity_context'; import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state'; import type { Account } from 'mastodon/models/account'; @@ -198,7 +197,6 @@ export const AccountHeader: React.FC<{ state.relationships.get(accountId), ); const hidden = useAppSelector((state) => getAccountHidden(state, accountId)); - const handleLinkClick = useLinks(); const handleBlock = useCallback(() => { if (!account) { @@ -852,10 +850,7 @@ export const AccountHeader: React.FC<{ {!(suspended || hidden) && (
-
+
{account.id !== me && signedIn && ( )} diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js index cc0d6fe195..8ca86d16cd 100644 --- a/app/javascript/mastodon/features/emoji/emoji.js +++ b/app/javascript/mastodon/features/emoji/emoji.js @@ -1,6 +1,5 @@ import Trie from 'substring-trie'; -import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; import { assetHost } from 'mastodon/utils/config'; import { autoPlayGif } from '../../initial_state'; @@ -153,13 +152,9 @@ const emojifyNode = (node, customEmojis) => { * Legacy emoji processing function. * @param {string} str * @param {object} customEmojis - * @param {boolean} force If true, always emojify even if modern emoji is enabled * @returns {string} */ -const emojify = (str, customEmojis = {}, force = false) => { - if (isModernEmojiEnabled() && !force) { - return str; - } +const emojify = (str, customEmojis = {}) => { const wrapper = document.createElement('div'); wrapper.innerHTML = str; diff --git a/app/javascript/mastodon/features/emoji/emoji_compressed.mjs b/app/javascript/mastodon/features/emoji/emoji_compressed.mjs index db8a4bc122..c565b861fd 100644 --- a/app/javascript/mastodon/features/emoji/emoji_compressed.mjs +++ b/app/javascript/mastodon/features/emoji/emoji_compressed.mjs @@ -14,8 +14,7 @@ import { uncompress as emojiMartUncompress } from 'emoji-mart/dist/utils/data'; import data from './emoji_data.json'; import emojiMap from './emoji_map.json'; -import { unicodeToFilename } from './unicode_to_filename'; -import { unicodeToUnifiedName } from './unicode_to_unified_name'; +import { unicodeToFilename, unicodeToUnifiedName } from './unicode_utils'; emojiMartUncompress(data); diff --git a/app/javascript/mastodon/features/emoji/emoji_mart_data_light.ts b/app/javascript/mastodon/features/emoji/emoji_mart_data_light.ts index 1315518179..59d995e25f 100644 --- a/app/javascript/mastodon/features/emoji/emoji_mart_data_light.ts +++ b/app/javascript/mastodon/features/emoji/emoji_mart_data_light.ts @@ -9,7 +9,7 @@ import type { ShortCodesToEmojiData, } from 'virtual:mastodon-emoji-compressed'; -import { unicodeToUnifiedName } from './unicode_to_unified_name'; +import { unicodeToUnifiedName } from './unicode_utils'; type Emojis = Record< NonNullable, @@ -23,7 +23,7 @@ type Emojis = Record< const [ shortCodesToEmojiData, - skins, + _skins, categories, short_names, _emojisWithoutShortCodes, @@ -47,4 +47,4 @@ Object.keys(shortCodesToEmojiData).forEach((shortCode) => { }; }); -export { emojis, skins, categories, short_names }; +export { emojis, categories, short_names }; diff --git a/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js b/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js index 83e154b0b2..038dd120c9 100644 --- a/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js +++ b/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js @@ -1,7 +1,7 @@ // This code is largely borrowed from: // https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js -import * as data from './emoji_mart_data_light'; +import { emojis, categories } from './emoji_mart_data_light'; import { getData, getSanitizedData, uniq, intersect } from './emoji_utils'; let originalPool = {}; @@ -10,8 +10,8 @@ let emojisList = {}; let emoticonsList = {}; let customEmojisList = []; -for (let emoji in data.emojis) { - let emojiData = data.emojis[emoji]; +for (let emoji in emojis) { + let emojiData = emojis[emoji]; let { short_names, emoticons } = emojiData; let id = short_names[0]; @@ -84,14 +84,14 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo if (include.length || exclude.length) { pool = {}; - data.categories.forEach(category => { + categories.forEach(category => { let isIncluded = include && include.length ? include.indexOf(category.name.toLowerCase()) > -1 : true; let isExcluded = exclude && exclude.length ? exclude.indexOf(category.name.toLowerCase()) > -1 : false; if (!isIncluded || isExcluded) { return; } - category.emojis.forEach(emojiId => pool[emojiId] = data.emojis[emojiId]); + category.emojis.forEach(emojiId => pool[emojiId] = emojis[emojiId]); }); if (custom.length) { @@ -171,7 +171,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo if (results) { if (emojisToShowFilter) { - results = results.filter((result) => emojisToShowFilter(data.emojis[result.id])); + results = results.filter((result) => emojisToShowFilter(emojis[result.id])); } if (results && results.length > maxResults) { diff --git a/app/javascript/mastodon/features/emoji/emoji_picker.tsx b/app/javascript/mastodon/features/emoji/emoji_picker.tsx index 38363d4310..37fc94dde7 100644 --- a/app/javascript/mastodon/features/emoji/emoji_picker.tsx +++ b/app/javascript/mastodon/features/emoji/emoji_picker.tsx @@ -2,7 +2,6 @@ import type { EmojiProps, PickerProps } from 'emoji-mart'; import EmojiRaw from 'emoji-mart/dist-es/components/emoji/nimble-emoji'; import PickerRaw from 'emoji-mart/dist-es/components/picker/nimble-picker'; -import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; import { assetHost } from 'mastodon/utils/config'; import { EMOJI_MODE_NATIVE } from './constants'; @@ -27,7 +26,7 @@ const Emoji = ({ sheetSize={sheetSize} sheetColumns={sheetColumns} sheetRows={sheetRows} - native={mode === EMOJI_MODE_NATIVE && isModernEmojiEnabled()} + native={mode === EMOJI_MODE_NATIVE} backgroundImageFn={backgroundImageFn} {...props} /> @@ -51,7 +50,7 @@ const Picker = ({ sheetColumns={sheetColumns} sheetRows={sheetRows} backgroundImageFn={backgroundImageFn} - native={mode === EMOJI_MODE_NATIVE && isModernEmojiEnabled()} + native={mode === EMOJI_MODE_NATIVE} {...props} /> ); diff --git a/app/javascript/mastodon/features/emoji/emoji_unicode_mapping_light.ts b/app/javascript/mastodon/features/emoji/emoji_unicode_mapping_light.ts index a53496be2a..ecf36e3ea8 100644 --- a/app/javascript/mastodon/features/emoji/emoji_unicode_mapping_light.ts +++ b/app/javascript/mastodon/features/emoji/emoji_unicode_mapping_light.ts @@ -8,7 +8,7 @@ import type { ShortCodesToEmojiDataKey, } from 'virtual:mastodon-emoji-compressed'; -import { unicodeToFilename } from './unicode_to_filename'; +import { unicodeToFilename } from './unicode_utils'; type UnicodeMapping = Record< FilenameData[number][0], diff --git a/app/javascript/mastodon/features/emoji/emoji_utils.js b/app/javascript/mastodon/features/emoji/emoji_utils.js index c13d250567..75b2acafa5 100644 --- a/app/javascript/mastodon/features/emoji/emoji_utils.js +++ b/app/javascript/mastodon/features/emoji/emoji_utils.js @@ -209,50 +209,9 @@ function intersect(a, b) { return uniqA.filter(item => uniqB.indexOf(item) >= 0); } -function deepMerge(a, b) { - let o = {}; - - for (let key in a) { - let originalValue = a[key], - value = originalValue; - - if (Object.hasOwn(b, key)) { - value = b[key]; - } - - if (typeof value === 'object') { - value = deepMerge(originalValue, value); - } - - o[key] = value; - } - - return o; -} - -// https://github.com/sonicdoe/measure-scrollbar -function measureScrollbar() { - const div = document.createElement('div'); - - div.style.width = '100px'; - div.style.height = '100px'; - div.style.overflow = 'scroll'; - div.style.position = 'absolute'; - div.style.top = '-9999px'; - - document.body.appendChild(div); - const scrollbarWidth = div.offsetWidth - div.clientWidth; - document.body.removeChild(div); - - return scrollbarWidth; -} - export { getData, getSanitizedData, uniq, intersect, - deepMerge, - unifiedToNative, - measureScrollbar, }; diff --git a/app/javascript/mastodon/features/emoji/handlers.ts b/app/javascript/mastodon/features/emoji/handlers.ts deleted file mode 100644 index 3b02028f3c..0000000000 --- a/app/javascript/mastodon/features/emoji/handlers.ts +++ /dev/null @@ -1,61 +0,0 @@ -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/emoji/unicode_to_filename.js b/app/javascript/mastodon/features/emoji/unicode_to_filename.js deleted file mode 100644 index cfe5539c7b..0000000000 --- a/app/javascript/mastodon/features/emoji/unicode_to_filename.js +++ /dev/null @@ -1,26 +0,0 @@ -// taken from: -// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866 -export const unicodeToFilename = (str) => { - let result = ''; - let charCode = 0; - let p = 0; - let i = 0; - while (i < str.length) { - charCode = str.charCodeAt(i++); - if (p) { - if (result.length > 0) { - result += '-'; - } - result += (0x10000 + ((p - 0xD800) << 10) + (charCode - 0xDC00)).toString(16); - p = 0; - } else if (0xD800 <= charCode && charCode <= 0xDBFF) { - p = charCode; - } else { - if (result.length > 0) { - result += '-'; - } - result += charCode.toString(16); - } - } - return result; -}; diff --git a/app/javascript/mastodon/features/emoji/unicode_to_unified_name.js b/app/javascript/mastodon/features/emoji/unicode_to_unified_name.js deleted file mode 100644 index 15f60aa7c3..0000000000 --- a/app/javascript/mastodon/features/emoji/unicode_to_unified_name.js +++ /dev/null @@ -1,21 +0,0 @@ -function padLeft(str, num) { - while (str.length < num) { - str = '0' + str; - } - - return str; -} - -export const unicodeToUnifiedName = (str) => { - let output = ''; - - for (let i = 0; i < str.length; i += 2) { - if (i > 0) { - output += '-'; - } - - output += padLeft(str.codePointAt(i).toString(16).toUpperCase(), 4); - } - - return output; -}; diff --git a/app/javascript/mastodon/features/emoji/unicode_utils.ts b/app/javascript/mastodon/features/emoji/unicode_utils.ts new file mode 100644 index 0000000000..04175ee9f9 --- /dev/null +++ b/app/javascript/mastodon/features/emoji/unicode_utils.ts @@ -0,0 +1,43 @@ +// taken from: +// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866 +export function unicodeToFilename(str: string) { + let result = ''; + let charCode = 0; + let p = 0; + let i = 0; + while (i < str.length) { + charCode = str.charCodeAt(i++); + if (p) { + if (result.length > 0) { + result += '-'; + } + result += (0x10000 + ((p - 0xd800) << 10) + (charCode - 0xdc00)).toString( + 16, + ); + p = 0; + } else if (0xd800 <= charCode && charCode <= 0xdbff) { + p = charCode; + } else { + if (result.length > 0) { + result += '-'; + } + result += charCode.toString(16); + } + } + return result; +} + +export function unicodeToUnifiedName(str: string) { + let output = ''; + + for (let i = 0; i < str.length; i += 2) { + if (i > 0) { + output += '-'; + } + + output += + str.codePointAt(i)?.toString(16).toUpperCase().padStart(4, '0') ?? ''; + } + + return output; +} diff --git a/app/javascript/mastodon/features/getting_started/components/announcements.jsx b/app/javascript/mastodon/features/getting_started/components/announcements.jsx deleted file mode 100644 index 96bd995d2b..0000000000 --- a/app/javascript/mastodon/features/getting_started/components/announcements.jsx +++ /dev/null @@ -1,458 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent, useCallback, useMemo } from 'react'; - -import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl'; - -import classNames from 'classnames'; -import { withRouter } from 'react-router-dom'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -import { animated, useTransition } from '@react-spring/web'; -import ReactSwipeableViews from 'react-swipeable-views'; - -import elephantUIPlane from '@/images/elephant_ui_plane.svg'; -import AddIcon from '@/material-icons/400-24px/add.svg?react'; -import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react'; -import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; -import { AnimatedNumber } from 'mastodon/components/animated_number'; -import { Icon } from 'mastodon/components/icon'; -import { IconButton } from 'mastodon/components/icon_button'; -import EmojiPickerDropdown from 'mastodon/features/compose/containers/emoji_picker_dropdown_container'; -import { unicodeMapping } from 'mastodon/features/emoji/emoji_unicode_mapping_light'; -import { autoPlayGif, reduceMotion, disableSwiping, mascot } from 'mastodon/initial_state'; -import { assetHost } from 'mastodon/utils/config'; -import { WithRouterPropTypes } from 'mastodon/utils/react_router'; - -const messages = defineMessages({ - close: { id: 'lightbox.close', defaultMessage: 'Close' }, - previous: { id: 'lightbox.previous', defaultMessage: 'Previous' }, - next: { id: 'lightbox.next', defaultMessage: 'Next' }, -}); - -class ContentWithRouter extends ImmutablePureComponent { - static propTypes = { - announcement: ImmutablePropTypes.map.isRequired, - ...WithRouterPropTypes, - }; - - setRef = c => { - this.node = c; - }; - - componentDidMount () { - this._updateLinks(); - } - - componentDidUpdate () { - this._updateLinks(); - } - - _updateLinks () { - const node = this.node; - - if (!node) { - return; - } - - const links = node.querySelectorAll('a'); - - for (var i = 0; i < links.length; ++i) { - let link = links[i]; - - if (link.classList.contains('status-link')) { - continue; - } - - link.classList.add('status-link'); - - let mention = this.props.announcement.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')); - } 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); - } else { - let status = this.props.announcement.get('statuses').find(item => link.href === item.get('url')); - if (status) { - link.addEventListener('click', this.onStatusClick.bind(this, status), false); - } - link.setAttribute('title', link.href); - link.classList.add('unhandled-link'); - } - - link.setAttribute('target', '_blank'); - link.setAttribute('rel', 'noopener'); - } - } - - onMentionClick = (mention, e) => { - if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - this.props.history.push(`/@${mention.get('acct')}`); - } - }; - - onHashtagClick = (hashtag, e) => { - hashtag = hashtag.replace(/^#/, ''); - - if (this.props.history&& e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - this.props.history.push(`/tags/${hashtag}`); - } - }; - - onStatusClick = (status, e) => { - if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - this.props.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`); - } - }; - - render () { - const { announcement } = this.props; - - return ( -
- ); - } - -} - -const Content = withRouter(ContentWithRouter); - -class Emoji extends PureComponent { - - static propTypes = { - emoji: PropTypes.string.isRequired, - emojiMap: ImmutablePropTypes.map.isRequired, - hovered: PropTypes.bool.isRequired, - }; - - render () { - const { emoji, emojiMap, hovered } = this.props; - - if (unicodeMapping[emoji]) { - const { filename, shortCode } = unicodeMapping[this.props.emoji]; - const title = shortCode ? `:${shortCode}:` : ''; - - return ( - {emoji} - ); - } else if (emojiMap.get(emoji)) { - const filename = (autoPlayGif || hovered) ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']); - const shortCode = `:${emoji}:`; - - return ( - {shortCode} - ); - } else { - return null; - } - } - -} - -class Reaction extends ImmutablePureComponent { - - static propTypes = { - announcementId: PropTypes.string.isRequired, - reaction: ImmutablePropTypes.map.isRequired, - addReaction: PropTypes.func.isRequired, - removeReaction: PropTypes.func.isRequired, - emojiMap: ImmutablePropTypes.map.isRequired, - style: PropTypes.object, - }; - - state = { - hovered: false, - }; - - handleClick = () => { - const { reaction, announcementId, addReaction, removeReaction } = this.props; - - if (reaction.get('me')) { - removeReaction(announcementId, reaction.get('name')); - } else { - addReaction(announcementId, reaction.get('name')); - } - }; - - handleMouseEnter = () => this.setState({ hovered: true }); - - handleMouseLeave = () => this.setState({ hovered: false }); - - render () { - const { reaction } = this.props; - - let shortCode = reaction.get('name'); - - if (unicodeMapping[shortCode]) { - shortCode = unicodeMapping[shortCode].shortCode; - } - - return ( - - - - - - - - - ); - } - -} - -const ReactionsBar = ({ - announcementId, - reactions, - emojiMap, - addReaction, - removeReaction, -}) => { - const visibleReactions = useMemo(() => reactions.filter(x => x.get('count') > 0).toArray(), [reactions]); - - const handleEmojiPick = useCallback((emoji) => { - addReaction(announcementId, emoji.native.replaceAll(/:/g, '')); - }, [addReaction, announcementId]); - - const transitions = useTransition(visibleReactions, { - from: { - scale: 0, - }, - enter: { - scale: 1, - }, - leave: { - scale: 0, - }, - keys: visibleReactions.map(x => x.get('name')), - }); - - return ( -
- {transitions(({ scale }, reaction) => ( - `scale(${s})`) }} - addReaction={addReaction} - removeReaction={removeReaction} - announcementId={announcementId} - emojiMap={emojiMap} - /> - ))} - - {visibleReactions.length < 8 && ( - } - /> - )} -
- ); -}; -ReactionsBar.propTypes = { - announcementId: PropTypes.string.isRequired, - reactions: ImmutablePropTypes.list.isRequired, - addReaction: PropTypes.func.isRequired, - removeReaction: PropTypes.func.isRequired, - emojiMap: ImmutablePropTypes.map.isRequired, -}; - -class Announcement extends ImmutablePureComponent { - - static propTypes = { - announcement: ImmutablePropTypes.map.isRequired, - emojiMap: ImmutablePropTypes.map.isRequired, - addReaction: PropTypes.func.isRequired, - removeReaction: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - selected: PropTypes.bool, - }; - - state = { - unread: !this.props.announcement.get('read'), - }; - - componentDidUpdate () { - const { selected, announcement } = this.props; - if (!selected && this.state.unread !== !announcement.get('read')) { - this.setState({ unread: !announcement.get('read') }); - } - } - - render () { - const { announcement } = this.props; - const { unread } = this.state; - const startsAt = announcement.get('starts_at') && new Date(announcement.get('starts_at')); - const endsAt = announcement.get('ends_at') && new Date(announcement.get('ends_at')); - const now = new Date(); - const hasTimeRange = startsAt && endsAt; - const skipTime = announcement.get('all_day'); - - let timestamp = null; - if (hasTimeRange) { - const skipYear = startsAt.getFullYear() === endsAt.getFullYear() && endsAt.getFullYear() === now.getFullYear(); - const skipEndDate = startsAt.getDate() === endsAt.getDate() && startsAt.getMonth() === endsAt.getMonth() && startsAt.getFullYear() === endsAt.getFullYear(); - timestamp = ( - <> - - - - ); - } else { - const publishedAt = new Date(announcement.get('published_at')); - timestamp = ( - - ); - } - - return ( -
- - - ยท {timestamp} - - - - - - - {unread && } -
- ); - } - -} - -class Announcements extends ImmutablePureComponent { - - static propTypes = { - announcements: ImmutablePropTypes.list, - emojiMap: ImmutablePropTypes.map.isRequired, - dismissAnnouncement: PropTypes.func.isRequired, - addReaction: PropTypes.func.isRequired, - removeReaction: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - state = { - index: 0, - }; - - static getDerivedStateFromProps(props, state) { - if (props.announcements.size > 0 && state.index >= props.announcements.size) { - return { index: props.announcements.size - 1 }; - } else { - return null; - } - } - - componentDidMount () { - this._markAnnouncementAsRead(); - } - - componentDidUpdate () { - this._markAnnouncementAsRead(); - } - - _markAnnouncementAsRead () { - const { dismissAnnouncement, announcements } = this.props; - const { index } = this.state; - const announcement = announcements.get(announcements.size - 1 - index); - if (!announcement.get('read')) dismissAnnouncement(announcement.get('id')); - } - - handleChangeIndex = index => { - this.setState({ index: index % this.props.announcements.size }); - }; - - handleNextClick = () => { - this.setState({ index: (this.state.index + 1) % this.props.announcements.size }); - }; - - handlePrevClick = () => { - this.setState({ index: (this.props.announcements.size + this.state.index - 1) % this.props.announcements.size }); - }; - - render () { - const { announcements, intl } = this.props; - const { index } = this.state; - - if (announcements.isEmpty()) { - return null; - } - - return ( -
- - -
- - {announcements.map((announcement, idx) => ( - - )).reverse()} - - - {announcements.size > 1 && ( -
- - {index + 1} / {announcements.size} - -
- )} -
-
- ); - } - -} - -export default injectIntl(Announcements); diff --git a/app/javascript/mastodon/features/getting_started/containers/announcements_container.js b/app/javascript/mastodon/features/getting_started/containers/announcements_container.js deleted file mode 100644 index 3bb1b8e8d1..0000000000 --- a/app/javascript/mastodon/features/getting_started/containers/announcements_container.js +++ /dev/null @@ -1,23 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { Map as ImmutableMap } from 'immutable'; -import { connect } from 'react-redux'; - - -import { addReaction, removeReaction, dismissAnnouncement } from 'mastodon/actions/announcements'; - -import Announcements from '../components/announcements'; - -const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap())); - -const mapStateToProps = state => ({ - announcements: state.getIn(['announcements', 'items']), - emojiMap: customEmojiMap(state), -}); - -const mapDispatchToProps = dispatch => ({ - dismissAnnouncement: id => dispatch(dismissAnnouncement(id)), - addReaction: (id, name) => dispatch(addReaction(id, name)), - removeReaction: (id, name) => dispatch(removeReaction(id, name)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(Announcements); diff --git a/app/javascript/mastodon/features/home_timeline/components/announcements/index.tsx b/app/javascript/mastodon/features/home_timeline/components/announcements/index.tsx index 8c7c704849..335e0f1a38 100644 --- a/app/javascript/mastodon/features/home_timeline/components/announcements/index.tsx +++ b/app/javascript/mastodon/features/home_timeline/components/announcements/index.tsx @@ -10,10 +10,8 @@ import ReactSwipeableViews from 'react-swipeable-views'; import elephantUIPlane from '@/images/elephant_ui_plane.svg'; import { CustomEmojiProvider } from '@/mastodon/components/emoji/context'; import { IconButton } from '@/mastodon/components/icon_button'; -import LegacyAnnouncements from '@/mastodon/features/getting_started/containers/announcements_container'; import { mascot, reduceMotion } from '@/mastodon/initial_state'; import { createAppSelector, useAppSelector } from '@/mastodon/store'; -import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react'; import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; @@ -32,7 +30,7 @@ const announcementSelector = createAppSelector( (announcements.get('items')?.toJS() as IAnnouncement[] | undefined) ?? [], ); -export const ModernAnnouncements: FC = () => { +export const Announcements: FC = () => { const intl = useIntl(); const announcements = useAppSelector(announcementSelector); @@ -112,7 +110,3 @@ export const ModernAnnouncements: FC = () => {
); }; - -export const Announcements = isModernEmojiEnabled() - ? ModernAnnouncements - : LegacyAnnouncements; 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 1d1b684b80..9e7f66d112 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,47 +1,17 @@ import { useCallback, useMemo } from 'react'; -import { useHistory } from 'react-router-dom'; - 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: ApiMentionJSON, - e: MouseEvent, -) => { - if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - history.push(`/@${mention.acct}`); - } -}; - -const handleHashtagClick = ( - history: History, - hashtag: string, - e: MouseEvent, -) => { - if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - history.push(`/tags/${hashtag.replace(/^#/, '')}`); - } -}; - export const EmbeddedStatusContent: React.FC<{ status: Status; className?: string; }> = ({ status, className }) => { - const history = useHistory(); - const mentions = useMemo( () => (status.get('mentions') as List).toJS(), [status], @@ -57,55 +27,10 @@ export const EmbeddedStatusContent: React.FC<{ hrefToMention, }); - const handleContentRef = useCallback( - (node: HTMLDivElement | null) => { - if (!node || isModernEmojiEnabled()) { - return; - } - - const links = node.querySelectorAll('a'); - - for (const link of links) { - if (link.classList.contains('status-link')) { - continue; - } - - link.classList.add('status-link'); - - const mention = mentions.find((item) => link.href === item.url); - - if (mention) { - link.addEventListener( - 'click', - handleMentionClick.bind(null, history, mention), - false, - ); - link.setAttribute('title', `@${mention.acct}`); - link.setAttribute('href', `/@${mention.acct}`); - } else if ( - link.textContent.startsWith('#') || - link.previousSibling?.textContent?.endsWith('#') - ) { - link.addEventListener( - 'click', - handleHashtagClick.bind(null, history, link.text), - false, - ); - link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`); - } else { - link.setAttribute('title', link.href); - link.classList.add('unhandled-link'); - } - } - }, - [mentions, history], - ); - return ( diff --git a/app/javascript/mastodon/features/ui/components/compare_history_modal.jsx b/app/javascript/mastodon/features/ui/components/compare_history_modal.jsx index f260444265..ae4c4ed4f7 100644 --- a/app/javascript/mastodon/features/ui/components/compare_history_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/compare_history_modal.jsx @@ -14,7 +14,6 @@ import { IconButton } from 'mastodon/components/icon_button'; import InlineAccount from 'mastodon/components/inline_account'; import MediaAttachments from 'mastodon/components/media_attachments'; import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; -import emojify from 'mastodon/features/emoji/emoji'; import { EmojiHTML } from '@/mastodon/components/emoji/html'; import { CustomEmojiProvider } from '@/mastodon/components/emoji/context'; @@ -48,13 +47,8 @@ class CompareHistoryModal extends PureComponent { const { index, versions, language, onClose } = this.props; const currentVersion = versions.get(index); - const emojiMap = currentVersion.get('emojis').reduce((obj, emoji) => { - obj[`:${emoji.get('shortcode')}:`] = emoji.toJS(); - return obj; - }, {}); - - const content = emojify(currentVersion.get('content'), emojiMap); - const spoilerContent = emojify(escapeTextContentForBrowser(currentVersion.get('spoiler_text')), emojiMap); + const content = currentVersion.get('content'); + const spoilerContent = escapeTextContentForBrowser(currentVersion.get('spoiler_text')); const formattedDate = ; const formattedName = ; @@ -99,7 +93,7 @@ class CompareHistoryModal extends PureComponent { diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index f53870a314..209e4b4a87 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -22,12 +22,11 @@ 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, landingPage, localLiveFeedAccess, disableHoverCards, autoPlayGif } from '../../initial_state'; +import { initialState, me, owner, singleUserMode, trendsEnabled, landingPage, localLiveFeedAccess, disableHoverCards } from '../../initial_state'; import BundleColumnError from './components/bundle_column_error'; import { NavigationBar } from './components/navigation_bar'; @@ -382,11 +381,6 @@ 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); @@ -412,8 +406,6 @@ 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); diff --git a/app/javascript/mastodon/hooks/useLinks.ts b/app/javascript/mastodon/hooks/useLinks.ts deleted file mode 100644 index 77609181be..0000000000 --- a/app/javascript/mastodon/hooks/useLinks.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { useCallback } from 'react'; - -import { useHistory } from 'react-router-dom'; - -import { isFulfilled, isRejected } from '@reduxjs/toolkit'; - -import { openURL } from 'mastodon/actions/search'; -import { useAppDispatch } from 'mastodon/store'; - -import { isModernEmojiEnabled } from '../utils/environment'; - -const isMentionClick = (element: HTMLAnchorElement) => - element.classList.contains('mention') && - !element.classList.contains('hashtag'); - -const isHashtagClick = (element: HTMLAnchorElement) => - element.textContent.startsWith('#') || - element.previousSibling?.textContent?.endsWith('#'); - -export const useLinks = (skipHashtags?: boolean) => { - const history = useHistory(); - const dispatch = useAppDispatch(); - - const handleHashtagClick = useCallback( - (element: HTMLAnchorElement) => { - const { textContent } = element; - - if (!textContent) return; - - history.push(`/tags/${textContent.replace(/^#/, '')}`); - }, - [history], - ); - - const handleMentionClick = useCallback( - async (element: HTMLAnchorElement) => { - const result = await dispatch(openURL({ url: element.href })); - - if (isFulfilled(result)) { - if (result.payload.accounts[0]) { - history.push(`/@${result.payload.accounts[0].acct}`); - } else if (result.payload.statuses[0]) { - history.push( - `/@${result.payload.statuses[0].account.acct}/${result.payload.statuses[0].id}`, - ); - } else { - window.location.href = element.href; - } - } else if (isRejected(result)) { - window.location.href = element.href; - } - }, - [dispatch, history], - ); - - const handleClick = useCallback( - (e: React.MouseEvent) => { - // Exit early if modern emoji is enabled, as this is handled by HandledLink. - if (isModernEmojiEnabled()) { - return; - } - - const target = (e.target as HTMLElement).closest('a'); - - if (!target || e.button !== 0 || e.ctrlKey || e.metaKey) { - return; - } - - if (isMentionClick(target)) { - e.preventDefault(); - void handleMentionClick(target); - } else if (isHashtagClick(target) && !skipHashtags) { - e.preventDefault(); - handleHashtagClick(target); - } - }, - [skipHashtags, handleMentionClick, handleHashtagClick], - ); - - return handleClick; -}; diff --git a/app/javascript/mastodon/main.tsx b/app/javascript/mastodon/main.tsx index 456cc21c31..f89baf66cd 100644 --- a/app/javascript/mastodon/main.tsx +++ b/app/javascript/mastodon/main.tsx @@ -9,11 +9,8 @@ import { me, reduceMotion } from 'mastodon/initial_state'; import ready from 'mastodon/ready'; import { store } from 'mastodon/store'; -import { - isProduction, - isDevelopment, - isModernEmojiEnabled, -} from './utils/environment'; +import { initializeEmoji } from './features/emoji'; +import { isProduction, isDevelopment } from './utils/environment'; function main() { perf.start('main()'); @@ -33,10 +30,7 @@ function main() { }); } - if (isModernEmojiEnabled()) { - const { initializeEmoji } = await import('@/mastodon/features/emoji'); - initializeEmoji(); - } + initializeEmoji(); const root = createRoot(mountNode); root.render(); diff --git a/app/javascript/mastodon/models/account.ts b/app/javascript/mastodon/models/account.ts index 3b0c41be81..8fbc0cdf41 100644 --- a/app/javascript/mastodon/models/account.ts +++ b/app/javascript/mastodon/models/account.ts @@ -8,11 +8,10 @@ import type { ApiAccountRoleJSON, ApiAccountJSON, } from 'mastodon/api_types/accounts'; -import emojify from 'mastodon/features/emoji/emoji'; import { unescapeHTML } from 'mastodon/utils/html'; -import { CustomEmojiFactory, makeEmojiMap } from './custom_emoji'; -import type { CustomEmoji, EmojiMap } from './custom_emoji'; +import { CustomEmojiFactory } from './custom_emoji'; +import type { CustomEmoji } from './custom_emoji'; // AccountField interface AccountFieldShape extends Required { @@ -102,17 +101,11 @@ export const accountDefaultValues: AccountShape = { const AccountFactory = ImmutableRecord(accountDefaultValues); -function createAccountField( - jsonField: ApiAccountFieldJSON, - emojiMap: EmojiMap, -) { +function createAccountField(jsonField: ApiAccountFieldJSON) { return AccountFieldFactory({ ...jsonField, - name_emojified: emojify( - escapeTextContentForBrowser(jsonField.name), - emojiMap, - ), - value_emojified: emojify(jsonField.value, emojiMap), + name_emojified: escapeTextContentForBrowser(jsonField.name), + value_emojified: jsonField.value, value_plain: unescapeHTML(jsonField.value), }); } @@ -120,8 +113,6 @@ function createAccountField( export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) { const { moved, ...accountJSON } = serverJSON; - const emojiMap = makeEmojiMap(accountJSON.emojis); - const displayName = accountJSON.display_name.trim().length === 0 ? accountJSON.username @@ -134,7 +125,7 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) { ...accountJSON, moved: moved?.id, fields: ImmutableList( - serverJSON.fields.map((field) => createAccountField(field, emojiMap)), + serverJSON.fields.map((field) => createAccountField(field)), ), emojis: ImmutableList( serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji)), @@ -142,11 +133,8 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) { roles: ImmutableList( serverJSON.roles?.map((role) => AccountRoleFactory(role)), ), - display_name_html: emojify( - escapeTextContentForBrowser(displayName), - emojiMap, - ), - note_emojified: emojify(accountNote, emojiMap), + display_name_html: escapeTextContentForBrowser(displayName), + note_emojified: accountNote, note_plain: unescapeHTML(accountNote), url: accountJSON.url?.startsWith('http://') || diff --git a/app/javascript/mastodon/models/poll.ts b/app/javascript/mastodon/models/poll.ts index 6f5655680d..46cbb1111d 100644 --- a/app/javascript/mastodon/models/poll.ts +++ b/app/javascript/mastodon/models/poll.ts @@ -1,10 +1,9 @@ import escapeTextContentForBrowser from 'escape-html'; import type { ApiPollJSON, ApiPollOptionJSON } from 'mastodon/api_types/polls'; -import emojify from 'mastodon/features/emoji/emoji'; -import { CustomEmojiFactory, makeEmojiMap } from './custom_emoji'; -import type { CustomEmoji, EmojiMap } from './custom_emoji'; +import { CustomEmojiFactory } from './custom_emoji'; +import type { CustomEmoji } from './custom_emoji'; interface PollOptionTranslation { title: string; @@ -17,16 +16,12 @@ export interface PollOption extends ApiPollOptionJSON { translation: PollOptionTranslation | null; } -export function createPollOptionTranslationFromServerJSON( - translation: { title: string }, - emojiMap: EmojiMap, -) { +export function createPollOptionTranslationFromServerJSON(translation: { + title: string; +}) { return { ...translation, - titleHtml: emojify( - escapeTextContentForBrowser(translation.title), - emojiMap, - ), + titleHtml: escapeTextContentForBrowser(translation.title), } as PollOptionTranslation; } @@ -50,8 +45,6 @@ export function createPollFromServerJSON( serverJSON: ApiPollJSON, previousPoll?: Poll, ) { - const emojiMap = makeEmojiMap(serverJSON.emojis); - return { ...pollDefaultValues, ...serverJSON, @@ -60,20 +53,15 @@ export function createPollFromServerJSON( const option = { ...optionJSON, voted: serverJSON.own_votes?.includes(index) || false, - titleHtml: emojify( - escapeTextContentForBrowser(optionJSON.title), - emojiMap, - ), + titleHtml: escapeTextContentForBrowser(optionJSON.title), } as PollOption; const prevOption = previousPoll?.options[index]; if (prevOption?.translation && prevOption.title === option.title) { const { translation } = prevOption; - option.translation = createPollOptionTranslationFromServerJSON( - translation, - emojiMap, - ); + option.translation = + createPollOptionTranslationFromServerJSON(translation); } return option; diff --git a/app/javascript/mastodon/reducers/polls.ts b/app/javascript/mastodon/reducers/polls.ts index aadf6741c1..ac0917bd20 100644 --- a/app/javascript/mastodon/reducers/polls.ts +++ b/app/javascript/mastodon/reducers/polls.ts @@ -1,7 +1,6 @@ import type { Reducer } from '@reduxjs/toolkit'; import { importPolls } from 'mastodon/actions/importer/polls'; -import { makeEmojiMap } from 'mastodon/models/custom_emoji'; import { createPollOptionTranslationFromServerJSON } from 'mastodon/models/poll'; import type { Poll } from 'mastodon/models/poll'; @@ -20,16 +19,11 @@ const statusTranslateSuccess = (state: PollsState, pollTranslation?: Poll) => { if (!poll) return; - const emojiMap = makeEmojiMap(poll.emojis); - pollTranslation.options.forEach((item, index) => { const option = poll.options[index]; if (!option) return; - option.translation = createPollOptionTranslationFromServerJSON( - item, - emojiMap, - ); + option.translation = createPollOptionTranslationFromServerJSON(item); }); }; diff --git a/app/javascript/mastodon/utils/environment.ts b/app/javascript/mastodon/utils/environment.ts index aa125b0922..95075454f2 100644 --- a/app/javascript/mastodon/utils/environment.ts +++ b/app/javascript/mastodon/utils/environment.ts @@ -12,16 +12,8 @@ export function isProduction() { else return import.meta.env.PROD; } -export type Features = 'modern_emojis' | 'fasp'; +export type Features = 'fasp' | 'http_message_signatures'; export function isFeatureEnabled(feature: Features) { return initialState?.features.includes(feature) ?? false; } - -export function isModernEmojiEnabled() { - try { - return isFeatureEnabled('modern_emojis'); - } catch { - return false; - } -} diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index d562d90688..5f8921e246 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -31,7 +31,7 @@ class InitialStateSerializer < ActiveModel::Serializer store[:use_blurhash] = object_account_user.setting_use_blurhash store[:use_pending_items] = object_account_user.setting_use_pending_items store[:show_trends] = Setting.trends && object_account_user.setting_trends - store[:emoji_style] = object_account_user.settings['web.emoji_style'] if Mastodon::Feature.modern_emojis_enabled? + store[:emoji_style] = object_account_user.settings['web.emoji_style'] else store[:auto_play_gif] = Setting.auto_play_gif store[:display_media] = Setting.display_media diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index e1ee4ac0b0..e6e96f97d5 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -31,16 +31,15 @@ label: I18n.t('simple_form.labels.defaults.setting_theme'), wrapper: :with_label - - if Mastodon::Feature.modern_emojis_enabled? - .fields-group - = f.simple_fields_for :settings, current_user.settings do |ff| - = ff.input :'web.emoji_style', - collection: %w(auto twemoji native), - include_blank: false, - hint: I18n.t('simple_form.hints.defaults.setting_emoji_style'), - label: I18n.t('simple_form.labels.defaults.setting_emoji_style'), - label_method: ->(emoji_style) { I18n.t("emoji_styles.#{emoji_style}", default: emoji_style) }, - wrapper: :with_label + .fields-group + = f.simple_fields_for :settings, current_user.settings do |ff| + = ff.input :'web.emoji_style', + collection: %w(auto twemoji native), + include_blank: false, + hint: I18n.t('simple_form.hints.defaults.setting_emoji_style'), + label: I18n.t('simple_form.labels.defaults.setting_emoji_style'), + label_method: ->(emoji_style) { I18n.t("emoji_styles.#{emoji_style}", default: emoji_style) }, + wrapper: :with_label - unless I18n.locale == :en .flash-message.translation-prompt