EmojiText component replacing dangerouslySetInnerHTML

This commit is contained in:
Lim Chee Aun 2023-06-14 17:37:41 +08:00
parent d2826085e1
commit 3b3e0e6fde
8 changed files with 69 additions and 46 deletions

View file

@ -2,11 +2,11 @@ import './account-block.css';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import emojifyText from '../utils/emojify-text';
import niceDateTime from '../utils/nice-date-time'; import niceDateTime from '../utils/nice-date-time';
import states from '../utils/states'; import states from '../utils/states';
import Avatar from './avatar'; import Avatar from './avatar';
import EmojiText from './emoji-text';
function AccountBlock({ function AccountBlock({
skeleton, skeleton,
@ -46,7 +46,6 @@ function AccountBlock({
lastStatusAt, lastStatusAt,
bot, bot,
} = account; } = account;
const displayNameWithEmoji = emojifyText(displayName, emojis);
const [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct]; const [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct];
return ( return (
@ -72,11 +71,9 @@ function AccountBlock({
<Avatar url={avatar} size={avatarSize} squircle={bot} /> <Avatar url={avatar} size={avatarSize} squircle={bot} />
<span> <span>
{displayName ? ( {displayName ? (
<b <b>
dangerouslySetInnerHTML={{ <EmojiText text={displayName} emojis={emojis} />
__html: displayNameWithEmoji, </b>
}}
/>
) : ( ) : (
<b>{username}</b> <b>{username}</b>
)} )}

View file

@ -4,7 +4,6 @@ import { Menu, MenuDivider, MenuItem, SubMenu } from '@szhsin/react-menu';
import { useEffect, useReducer, useRef, useState } from 'preact/hooks'; import { useEffect, useReducer, useRef, useState } from 'preact/hooks';
import { api } from '../utils/api'; import { api } from '../utils/api';
import emojifyText from '../utils/emojify-text';
import enhanceContent from '../utils/enhance-content'; import enhanceContent from '../utils/enhance-content';
import getHTMLText from '../utils/getHTMLText'; import getHTMLText from '../utils/getHTMLText';
import handleContentLinks from '../utils/handle-content-links'; import handleContentLinks from '../utils/handle-content-links';
@ -16,6 +15,7 @@ import store from '../utils/store';
import AccountBlock from './account-block'; import AccountBlock from './account-block';
import Avatar from './avatar'; import Avatar from './avatar';
import EmojiText from './emoji-text';
import Icon from './icon'; import Icon from './icon';
import Link from './link'; import Link from './link';
import ListAddEdit from './list-add-edit'; import ListAddEdit from './list-add-edit';
@ -301,11 +301,7 @@ function AccountInfo({
key={name} key={name}
> >
<b> <b>
<span <EmojiText text={name} emojis={emojis} />{' '}
dangerouslySetInnerHTML={{
__html: emojifyText(name, emojis),
}}
/>{' '}
{!!verifiedAt && <Icon icon="check-circle" size="s" />} {!!verifiedAt && <Icon icon="check-circle" size="s" />}
</b> </b>
<p <p

View file

@ -0,0 +1,42 @@
function EmojiText({ text, emojis }) {
if (!text) return '';
if (!emojis?.length) return text;
if (text.indexOf(':') === -1) return text;
const components = [];
let lastIndex = 0;
emojis.forEach((shortcodeObj) => {
const { shortcode, staticUrl, url } = shortcodeObj;
const regex = new RegExp(`:${shortcode}:`, 'g');
let match;
while ((match = regex.exec(text))) {
const beforeText = text.substring(lastIndex, match.index);
if (beforeText) {
components.push(beforeText);
}
components.push(
<img
src={url}
alt={shortcode}
class="shortcode-emoji emoji"
width="12"
height="12"
loading="lazy"
decoding="async"
/>,
);
lastIndex = match.index + match[0].length;
}
});
const afterText = text.substring(lastIndex);
if (afterText) {
components.push(afterText);
}
return components;
}
export default EmojiText;

View file

@ -1,9 +1,9 @@
import './name-text.css'; import './name-text.css';
import emojifyText from '../utils/emojify-text';
import states from '../utils/states'; import states from '../utils/states';
import Avatar from './avatar'; import Avatar from './avatar';
import EmojiText from './emoji-text';
function NameText({ function NameText({
account, account,
@ -18,8 +18,6 @@ function NameText({
account; account;
let { username } = account; let { username } = account;
const displayNameWithEmoji = emojifyText(displayName, emojis);
const trimmedUsername = username.toLowerCase().trim(); const trimmedUsername = username.toLowerCase().trim();
const trimmedDisplayName = (displayName || '').toLowerCase().trim(); const trimmedDisplayName = (displayName || '').toLowerCase().trim();
const shortenedDisplayName = trimmedDisplayName const shortenedDisplayName = trimmedDisplayName
@ -58,11 +56,9 @@ function NameText({
)} )}
{displayName && !short ? ( {displayName && !short ? (
<> <>
<b <b>
dangerouslySetInnerHTML={{ <EmojiText text={displayName} emojis={emojis} />
__html: displayNameWithEmoji, </b>
}}
/>
{!showAcct && username && ( {!showAcct && username && (
<> <>
{' '} {' '}

View file

@ -1,8 +1,8 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'preact/hooks';
import emojifyText from '../utils/emojify-text';
import shortenNumber from '../utils/shorten-number'; import shortenNumber from '../utils/shorten-number';
import EmojiText from './emoji-text';
import Icon from './icon'; import Icon from './icon';
import RelativeTime from './relative-time'; import RelativeTime from './relative-time';
@ -112,11 +112,9 @@ export default function Poll({
}} }}
> >
<div class="poll-option-title"> <div class="poll-option-title">
<span <span>
dangerouslySetInnerHTML={{ <EmojiText text={title} emojis={emojis} />
__html: emojifyText(title, emojis), </span>
}}
/>
{voted && ownVotes.includes(i) && ( {voted && ownVotes.includes(i) && (
<> <>
{' '} {' '}
@ -179,12 +177,9 @@ export default function Poll({
disabled={uiState === 'loading'} disabled={uiState === 'loading'}
readOnly={readOnly} readOnly={readOnly}
/> />
<span <span class="poll-option-title">
class="poll-option-title" <EmojiText text={title} emojis={emojis} />
dangerouslySetInnerHTML={{ </span>
__html: emojifyText(title, emojis),
}}
/>
</label> </label>
</div> </div>
); );

View file

@ -26,12 +26,12 @@ import { useSnapshot } from 'valtio';
import { snapshot } from 'valtio/vanilla'; import { snapshot } from 'valtio/vanilla';
import AccountBlock from '../components/account-block'; import AccountBlock from '../components/account-block';
import EmojiText from '../components/emoji-text';
import Loader from '../components/loader'; import Loader from '../components/loader';
import Modal from '../components/modal'; import Modal from '../components/modal';
import NameText from '../components/name-text'; import NameText from '../components/name-text';
import Poll from '../components/poll'; import Poll from '../components/poll';
import { api } from '../utils/api'; import { api } from '../utils/api';
import emojifyText from '../utils/emojify-text';
import enhanceContent from '../utils/enhance-content'; import enhanceContent from '../utils/enhance-content';
import getTranslateTargetLanguage from '../utils/get-translate-target-language'; import getTranslateTargetLanguage from '../utils/get-translate-target-language';
import getHTMLText from '../utils/getHTMLText'; import getHTMLText from '../utils/getHTMLText';
@ -926,11 +926,9 @@ function Status({
ref={spoilerContentRef} ref={spoilerContentRef}
data-read-more={readMoreText} data-read-more={readMoreText}
> >
<p <p>
dangerouslySetInnerHTML={{ <EmojiText text={spoilerText} emojis={emojis} />
__html: emojifyText(spoilerText, emojis), </p>
}}
/>
</div> </div>
<button <button
class={`light spoiler ${showSpoiler ? 'spoiling' : ''}`} class={`light spoiler ${showSpoiler ? 'spoiling' : ''}`}

View file

@ -4,12 +4,12 @@ import { useParams, useSearchParams } from 'react-router-dom';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
import AccountInfo from '../components/account-info'; import AccountInfo from '../components/account-info';
import EmojiText from '../components/emoji-text';
import Icon from '../components/icon'; import Icon from '../components/icon';
import Link from '../components/link'; import Link from '../components/link';
import Menu2 from '../components/menu2'; import Menu2 from '../components/menu2';
import Timeline from '../components/timeline'; import Timeline from '../components/timeline';
import { api } from '../utils/api'; import { api } from '../utils/api';
import emojifyText from '../utils/emojify-text';
import showToast from '../utils/show-toast'; import showToast from '../utils/show-toast';
import states from '../utils/states'; import states from '../utils/states';
import { saveStatus } from '../utils/states'; import { saveStatus } from '../utils/states';
@ -236,11 +236,9 @@ function AccountStatuses() {
// }; // };
// }} // }}
> >
<b <b>
dangerouslySetInnerHTML={{ <EmojiText text={displayName} emojis={emojis} />
__html: emojifyText(displayName, emojis), </b>
}}
/>
<div> <div>
<span>@{acct}</span> <span>@{acct}</span>
</div> </div>

View file

@ -1,13 +1,14 @@
function emojifyText(text, emojis = []) { function emojifyText(text, emojis = []) {
if (!text) return ''; if (!text) return '';
if (!emojis.length) return text; if (!emojis.length) return text;
if (text.indexOf(':') === -1) return text;
// Replace shortcodes in text with emoji // Replace shortcodes in text with emoji
// emojis = [{ shortcode: 'smile', url: 'https://example.com/emoji.png' }] // emojis = [{ shortcode: 'smile', url: 'https://example.com/emoji.png' }]
emojis.forEach((emoji) => { emojis.forEach((emoji) => {
const { shortcode, staticUrl, url } = emoji; const { shortcode, staticUrl, url } = emoji;
text = text.replace( text = text.replace(
new RegExp(`:${shortcode}:`, 'g'), new RegExp(`:${shortcode}:`, 'g'),
`<img class="shortcode-emoji emoji" src="${url}" alt=":${shortcode}:" width="12" height="12" loading="lazy" />`, `<img class="shortcode-emoji emoji" src="${url}" alt=":${shortcode}:" width="12" height="12" loading="lazy" decoding="async" />`,
); );
}); });
// console.log(text, emojis); // console.log(text, emojis);