Emoji: Remove final flag (#36409)

pull/35080/head
Echo 5 days ago committed by GitHub
parent e4fc18abfd
commit 85d0cdb5f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

1
.gitignore vendored

@ -23,6 +23,7 @@
/public/packs
/public/packs-dev
/public/packs-test
stats.html
.env
.env.production
node_modules/

@ -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

@ -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(/<br\s*\/?>/g, '\n').replace(/<\/p><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;
}

@ -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<AccountBioProps> = ({
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<AccountBioProps> = ({
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);
}
}
}

@ -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 (
<Wrapper
{...props}
className={classNames(className, 'animate-parent')}
ref={ref}
>
<Wrapper {...props} className={className} ref={ref}>
{children}
</Wrapper>
);
@ -78,7 +72,7 @@ export const AnimateEmojiProvider = polymorphicForwardRef<
return (
<Wrapper
{...props}
className={classNames(className, 'animate-parent')}
className={className}
onMouseEnter={handleEnter}
onMouseLeave={handleLeave}
ref={ref}

@ -1,9 +1,6 @@
import { useMemo } from 'react';
import classNames from 'classnames';
import type { CustomEmojiMapArg } from '@/mastodon/features/emoji/types';
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
import type {
OnAttributeHandler,
OnElementHandler,
@ -22,7 +19,7 @@ export interface EmojiHTMLProps {
onAttribute?: OnAttributeHandler;
}
export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
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 (
<Wrapper
{...rest}
ref={ref}
dangerouslySetInnerHTML={{ __html: htmlString }}
className={classNames(className, 'animate-parent')}
/>
);
},
);
LegacyEmojiHTML.displayName = 'LegacyEmojiHTML';
export const EmojiHTML = isModernEmojiEnabled()
? ModernEmojiHTML
: LegacyEmojiHTML;
EmojiHTML.displayName = 'EmojiHTML';

@ -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 (
<div
ref={ref}
@ -110,7 +106,7 @@ export const HoverCardAccount = forwardRef<
className='hover-card__bio'
/>
<div className='account-fields' onClickCapture={handleClick}>
<div className='account-fields'>
<AccountFields
fields={account.fields.take(2)}
emojis={account.emojis}

@ -4,7 +4,7 @@ import type { OnElementHandler } from '@/mastodon/utils/html';
import { polymorphicForwardRef } from '@/types/polymorphic';
import type { EmojiHTMLProps } from '../emoji/html';
import { ModernEmojiHTML } from '../emoji/html';
import { EmojiHTML } from '../emoji/html';
import { useElementHandledLink } from '../status/handled_link';
export const HTMLBlock = polymorphicForwardRef<
@ -25,6 +25,6 @@ export const HTMLBlock = polymorphicForwardRef<
(...args) => onParentElement?.(...args) ?? onLinkElement(...args),
[onLinkElement, onParentElement],
);
return <ModernEmojiHTML {...props} onElement={onElement} />;
return <EmojiHTML {...props} onElement={onElement} />;
},
);

@ -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<PollOptionProps> = (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(() => {

@ -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];
};

@ -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<HTMLAnchorElement>('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<Props> = ({ link }) => (
<span className='verified-badge'>
<Icon id='check' icon={CheckIcon} className='verified-badge__mark' />
<EmojiHTML
as='span'
htmlString={stripRelMe(link)}
onAttribute={onAttribute}
/>
<EmojiHTML as='span' htmlString={link} onAttribute={onAttribute} />
</span>
);

@ -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) && (
<div className='account__header__extra'>
<div
className='account__header__bio'
onClickCapture={handleLinkClick}
>
<div className='account__header__bio'>
{account.id !== me && signedIn && (
<AccountNote accountId={accountId} />
)}

@ -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;

@ -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);

@ -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<keyof ShortCodesToEmojiData>,
@ -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 };

@ -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) {

@ -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}
/>
);

@ -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],

@ -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,
};

@ -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<HTMLImageElement>('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;
}

@ -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;
};

@ -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;
};

@ -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;
}

@ -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 (
<div
className='announcements__item__content translate animate-parent'
ref={this.setRef}
dangerouslySetInnerHTML={{ __html: announcement.get('contentHtml') }}
/>
);
}
}
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 (
<img
draggable='false'
className='emojione'
alt={emoji}
title={title}
src={`${assetHost}/emoji/${filename}.svg`}
/>
);
} else if (emojiMap.get(emoji)) {
const filename = (autoPlayGif || hovered) ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']);
const shortCode = `:${emoji}:`;
return (
<img
draggable='false'
className='emojione custom-emoji'
alt={shortCode}
title={shortCode}
src={filename}
/>
);
} 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 (
<animated.button
className={classNames('reactions-bar__item', { active: reaction.get('me') })}
onClick={this.handleClick}
title={`:${shortCode}:`}
style={this.props.style}
// This does not use animate-parent as this component is directly rendered by React.
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
<span className='reactions-bar__item__emoji'>
<Emoji hovered={this.state.hovered} emoji={reaction.get('name')} emojiMap={this.props.emojiMap} />
</span>
<span className='reactions-bar__item__count'>
<AnimatedNumber value={reaction.get('count')} />
</span>
</animated.button>
);
}
}
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 (
<div
className={classNames('reactions-bar', {
'reactions-bar--empty': visibleReactions.length === 0
})}
>
{transitions(({ scale }, reaction) => (
<Reaction
key={reaction.get('name')}
reaction={reaction}
style={{ transform: scale.to((s) => `scale(${s})`) }}
addReaction={addReaction}
removeReaction={removeReaction}
announcementId={announcementId}
emojiMap={emojiMap}
/>
))}
{visibleReactions.length < 8 && (
<EmojiPickerDropdown
onPickEmoji={handleEmojiPick}
button={<Icon id='plus' icon={AddIcon} />}
/>
)}
</div>
);
};
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 = (
<>
<FormattedDate value={startsAt} year={(skipYear || startsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /> - <FormattedDate value={endsAt} year={(skipYear || endsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month={skipEndDate ? undefined : 'short'} day={skipEndDate ? undefined : '2-digit'} hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} />
</>
);
} else {
const publishedAt = new Date(announcement.get('published_at'));
timestamp = (
<FormattedDate value={publishedAt} year={publishedAt.getFullYear() === now.getFullYear() ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} />
);
}
return (
<div className='announcements__item'>
<strong className='announcements__item__range'>
<FormattedMessage id='announcement.announcement' defaultMessage='Announcement' />
<span> · {timestamp}</span>
</strong>
<Content announcement={announcement} />
<ReactionsBar
reactions={announcement.get('reactions')}
announcementId={announcement.get('id')}
addReaction={this.props.addReaction}
removeReaction={this.props.removeReaction}
emojiMap={this.props.emojiMap}
/>
{unread && <span className='announcements__item__unread' />}
</div>
);
}
}
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 (
<div className='announcements'>
<img className='announcements__mastodon' alt='' draggable='false' src={mascot || elephantUIPlane} />
<div className='announcements__container'>
<ReactSwipeableViews animateHeight animateTransitions={!reduceMotion} index={index} onChangeIndex={this.handleChangeIndex}>
{announcements.map((announcement, idx) => (
<Announcement
key={announcement.get('id')}
announcement={announcement}
emojiMap={this.props.emojiMap}
addReaction={this.props.addReaction}
removeReaction={this.props.removeReaction}
intl={intl}
selected={index === idx}
disabled={disableSwiping}
/>
)).reverse()}
</ReactSwipeableViews>
{announcements.size > 1 && (
<div className='announcements__pagination'>
<IconButton disabled={announcements.size === 1} title={intl.formatMessage(messages.previous)} icon='chevron-left' iconComponent={ChevronLeftIcon} onClick={this.handlePrevClick} size={13} />
<span>{index + 1} / {announcements.size}</span>
<IconButton disabled={announcements.size === 1} title={intl.formatMessage(messages.next)} icon='chevron-right' iconComponent={ChevronRightIcon} onClick={this.handleNextClick} size={13} />
</div>
)}
</div>
</div>
);
}
}
export default injectIntl(Announcements);

@ -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);

@ -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 = () => {
</div>
);
};
export const Announcements = isModernEmojiEnabled()
? ModernAnnouncements
: LegacyAnnouncements;

@ -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<Mention>).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<HTMLAnchorElement>('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 (
<EmojiHTML
{...htmlHandlers}
className={className}
ref={handleContentRef}
lang={status.get('language') as string}
htmlString={status.get('contentHtml') as string}
/>

@ -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 = <RelativeTimestamp timestamp={currentVersion.get('created_at')} short={false} />;
const formattedName = <InlineAccount accountId={currentVersion.get('account')} />;
@ -99,7 +93,7 @@ class CompareHistoryModal extends PureComponent {
<EmojiHTML
as="span"
className='poll__option__text translate'
htmlString={emojify(escapeTextContentForBrowser(option.get('title')), emojiMap)}
htmlString={escapeTextContentForBrowser(option.get('title'))}
lang={language}
/>
</label>

@ -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);

@ -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;
};

@ -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(<Mastodon {...props} />);

@ -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<ApiAccountFieldJSON> {
@ -102,17 +101,11 @@ export const accountDefaultValues: AccountShape = {
const AccountFactory = ImmutableRecord<AccountShape>(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://') ||

@ -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;

@ -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);
});
};

@ -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;
}
}

@ -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

@ -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

Loading…
Cancel
Save