It's micro-optimization week

Includes experimental replacing dangerouslySetInnerHTML with setting actual DOM
This commit is contained in:
Lim Chee Aun 2024-09-25 17:18:20 +08:00
parent 4d35c9d2c3
commit 65bd6fb1ef
7 changed files with 316 additions and 242 deletions

View file

@ -23,10 +23,12 @@ const ctx = canvas.getContext('2d', {
}); });
ctx.imageSmoothingEnabled = false; ctx.imageSmoothingEnabled = false;
const MISSING_IMAGE_PATH_REGEX = /missing\.png$/;
function Avatar({ url, size, alt = '', squircle, ...props }) { function Avatar({ url, size, alt = '', squircle, ...props }) {
size = SIZES[size] || size || SIZES.m; size = SIZES[size] || size || SIZES.m;
const avatarRef = useRef(); const avatarRef = useRef();
const isMissing = /missing\.png$/.test(url); const isMissing = MISSING_IMAGE_PATH_REGEX.test(url);
return ( return (
<span <span
ref={avatarRef} ref={avatarRef}

View file

@ -1,29 +1,40 @@
import { memo } from 'preact/compat'; import { memo } from 'preact/compat';
import mem from '../utils/mem';
import CustomEmoji from './custom-emoji'; import CustomEmoji from './custom-emoji';
const shortcodesRegexp = mem((shortcodes) => {
return new RegExp(`:(${shortcodes.join('|')}):`, 'g');
});
function EmojiText({ text, emojis }) { function EmojiText({ text, emojis }) {
if (!text) return ''; if (!text) return '';
if (!emojis?.length) return text; if (!emojis?.length) return text;
if (text.indexOf(':') === -1) return text; if (text.indexOf(':') === -1) return text;
const regex = new RegExp( // const regex = new RegExp(
`:(${emojis.map((e) => e.shortcode).join('|')}):`, // `:(${emojis.map((e) => e.shortcode).join('|')}):`,
'g', // 'g',
); // );
const regex = shortcodesRegexp(emojis.map((e) => e.shortcode));
const elements = text.split(regex).map((word) => { const elements = text.split(regex).map((word) => {
const emoji = emojis.find((e) => e.shortcode === word); const emoji = emojis.find((e) => e.shortcode === word);
if (emoji) { if (emoji) {
const { url, staticUrl } = emoji; const { url, staticUrl } = emoji;
return <CustomEmoji staticUrl={staticUrl} alt={word} url={url} />; return (
<CustomEmoji staticUrl={staticUrl} alt={word} url={url} key={word} />
);
} }
return word; return word;
}); });
return elements; return elements;
} }
export default memo( export default mem(EmojiText);
EmojiText,
(oldProps, newProps) => // export default memo(
oldProps.text === newProps.text && // EmojiText,
oldProps.emojis?.length === newProps.emojis?.length, // (oldProps, newProps) =>
); // oldProps.text === newProps.text &&
// oldProps.emojis?.length === newProps.emojis?.length,
// );

View file

@ -21,6 +21,11 @@ const nameCollator = mem((locale) => {
} }
}); });
const ACCT_REGEX = /([^@]+)(@.+)/i;
const SHORTCODES_REGEX = /(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g;
const SPACES_REGEX = /\s+/g;
const NON_ALPHA_NUMERIC_REGEX = /[^a-z0-9@\.]/gi;
function NameText({ function NameText({
account, account,
instance, instance,
@ -42,17 +47,17 @@ function NameText({
bot, bot,
username, username,
} = account; } = account;
const [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct]; const [_, acct1, acct2] = acct.match(ACCT_REGEX) || [, acct];
if (!instance) instance = api().instance; if (!instance) instance = api().instance;
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
.replace(/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1 .replace(SHORTCODES_REGEX, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1
.replace(/\s+/g, ''); // E.g. "My name" === "myname" .replace(SPACES_REGEX, ''); // E.g. "My name" === "myname"
const shortenedAlphaNumericDisplayName = shortenedDisplayName.replace( const shortenedAlphaNumericDisplayName = shortenedDisplayName.replace(
/[^a-z0-9@\.]/gi, NON_ALPHA_NUMERIC_REGEX,
'', '',
); // Remove non-alphanumeric characters ); // Remove non-alphanumeric characters
@ -130,9 +135,11 @@ function NameText({
); );
} }
export default memo(NameText, (oldProps, newProps) => { export default mem(NameText);
// Only care about account.id, the other props usually don't change
const { account } = oldProps; // export default memo(NameText, (oldProps, newProps) => {
const { account: newAccount } = newProps; // // Only care about account.id, the other props usually don't change
return account?.acct === newAccount?.acct; // const { account } = oldProps;
}); // const { account: newAccount } = newProps;
// return account?.acct === newAccount?.acct;
// });

View file

@ -124,11 +124,31 @@ function getPostText(status) {
); );
} }
const PostContent = memo( const HTTP_REGEX = /^http/i;
const PostContent =
/*memo(*/
({ post, instance, previewMode }) => { ({ post, instance, previewMode }) => {
const { content, emojis, language, mentions, url } = post; const { content, emojis, language, mentions, url } = post;
const divRef = useRef();
useEffect(() => {
if (!divRef.current) return;
const dom = enhanceContent(content, {
emojis,
returnDOM: true,
});
// Remove target="_blank" from links
for (const a of dom.querySelectorAll('a.u-url[target="_blank"]')) {
if (!HTTP_REGEX.test(a.innerText.trim())) {
a.removeAttribute('target');
}
}
divRef.current.replaceChildren(dom.cloneNode(true));
}, [content, emojis.length]);
return ( return (
<div <div
ref={divRef}
lang={language} lang={language}
dir="auto" dir="auto"
class="inner-content" class="inner-content"
@ -138,28 +158,28 @@ const PostContent = memo(
previewMode, previewMode,
statusURL: url, statusURL: url,
})} })}
dangerouslySetInnerHTML={{ // dangerouslySetInnerHTML={{
__html: enhanceContent(content, { // __html: enhanceContent(content, {
emojis, // emojis,
postEnhanceDOM: (dom) => { // postEnhanceDOM: (dom) => {
// Remove target="_blank" from links // // Remove target="_blank" from links
dom.querySelectorAll('a.u-url[target="_blank"]').forEach((a) => { // dom.querySelectorAll('a.u-url[target="_blank"]').forEach((a) => {
if (!/http/i.test(a.innerText.trim())) { // if (!/http/i.test(a.innerText.trim())) {
a.removeAttribute('target'); // a.removeAttribute('target');
} // }
}); // });
}, // },
}), // }),
}} // }}
/> />
); );
}, }; /*,
(oldProps, newProps) => { (oldProps, newProps) => {
const { post: oldPost } = oldProps; const { post: oldPost } = oldProps;
const { post: newPost } = newProps; const { post: newPost } = newProps;
return oldPost.content === newPost.content; return oldPost.content === newPost.content;
}, },
); );*/
const SIZE_CLASS = { const SIZE_CLASS = {
s: 'small', s: 'small',

288
src/locales/en.po generated
View file

@ -13,40 +13,40 @@ msgstr ""
"Language-Team: \n" "Language-Team: \n"
"Plural-Forms: \n" "Plural-Forms: \n"
#: src/components/account-block.jsx:133 #: src/components/account-block.jsx:135
msgid "Locked" msgid "Locked"
msgstr "" msgstr ""
#: src/components/account-block.jsx:139 #: src/components/account-block.jsx:141
msgid "Posts: {0}" msgid "Posts: {0}"
msgstr "" msgstr ""
#: src/components/account-block.jsx:144 #: src/components/account-block.jsx:146
msgid "Last posted: {0}" msgid "Last posted: {0}"
msgstr "" msgstr ""
#: src/components/account-block.jsx:159 #: src/components/account-block.jsx:161
#: src/components/account-info.jsx:634 #: src/components/account-info.jsx:634
msgid "Automated" msgid "Automated"
msgstr "" msgstr ""
#: src/components/account-block.jsx:166 #: src/components/account-block.jsx:168
#: src/components/account-info.jsx:639 #: src/components/account-info.jsx:639
#: src/components/status.jsx:440 #: src/components/status.jsx:460
#: src/pages/catchup.jsx:1471 #: src/pages/catchup.jsx:1471
msgid "Group" msgid "Group"
msgstr "" msgstr ""
#: src/components/account-block.jsx:176 #: src/components/account-block.jsx:178
msgid "Mutual" msgid "Mutual"
msgstr "" msgstr ""
#: src/components/account-block.jsx:180 #: src/components/account-block.jsx:182
#: src/components/account-info.jsx:1677 #: src/components/account-info.jsx:1677
msgid "Requested" msgid "Requested"
msgstr "" msgstr ""
#: src/components/account-block.jsx:184 #: src/components/account-block.jsx:186
#: src/components/account-info.jsx:416 #: src/components/account-info.jsx:416
#: src/components/account-info.jsx:742 #: src/components/account-info.jsx:742
#: src/components/account-info.jsx:756 #: src/components/account-info.jsx:756
@ -58,21 +58,21 @@ msgstr ""
msgid "Following" msgid "Following"
msgstr "" msgstr ""
#: src/components/account-block.jsx:188 #: src/components/account-block.jsx:190
#: src/components/account-info.jsx:1059 #: src/components/account-info.jsx:1059
msgid "Follows you" msgid "Follows you"
msgstr "" msgstr ""
#: src/components/account-block.jsx:196 #: src/components/account-block.jsx:198
msgid "{followersCount, plural, one {# follower} other {# followers}}" msgid "{followersCount, plural, one {# follower} other {# followers}}"
msgstr "" msgstr ""
#: src/components/account-block.jsx:205 #: src/components/account-block.jsx:207
#: src/components/account-info.jsx:680 #: src/components/account-info.jsx:680
msgid "Verified" msgid "Verified"
msgstr "" msgstr ""
#: src/components/account-block.jsx:220 #: src/components/account-block.jsx:222
#: src/components/account-info.jsx:777 #: src/components/account-info.jsx:777
msgid "Joined <0>{0}</0>" msgid "Joined <0>{0}</0>"
msgstr "" msgstr ""
@ -108,11 +108,11 @@ msgstr ""
#: src/components/compose.jsx:2463 #: src/components/compose.jsx:2463
#: src/components/media-alt-modal.jsx:45 #: src/components/media-alt-modal.jsx:45
#: src/components/media-modal.jsx:283 #: src/components/media-modal.jsx:283
#: src/components/status.jsx:1636 #: src/components/status.jsx:1656
#: src/components/status.jsx:1653 #: src/components/status.jsx:1673
#: src/components/status.jsx:1777 #: src/components/status.jsx:1797
#: src/components/status.jsx:2375 #: src/components/status.jsx:2395
#: src/components/status.jsx:2378 #: src/components/status.jsx:2398
#: src/pages/account-statuses.jsx:528 #: src/pages/account-statuses.jsx:528
#: src/pages/accounts.jsx:109 #: src/pages/accounts.jsx:109
#: src/pages/hashtag.jsx:199 #: src/pages/hashtag.jsx:199
@ -181,7 +181,7 @@ msgid "Original"
msgstr "" msgstr ""
#: src/components/account-info.jsx:859 #: src/components/account-info.jsx:859
#: src/components/status.jsx:2166 #: src/components/status.jsx:2186
#: src/pages/catchup.jsx:71 #: src/pages/catchup.jsx:71
#: src/pages/catchup.jsx:1445 #: src/pages/catchup.jsx:1445
#: src/pages/catchup.jsx:2056 #: src/pages/catchup.jsx:2056
@ -277,30 +277,30 @@ msgid "Add/Remove from Lists"
msgstr "" msgstr ""
#: src/components/account-info.jsx:1299 #: src/components/account-info.jsx:1299
#: src/components/status.jsx:1079 #: src/components/status.jsx:1099
msgid "Link copied" msgid "Link copied"
msgstr "" msgstr ""
#: src/components/account-info.jsx:1302 #: src/components/account-info.jsx:1302
#: src/components/status.jsx:1082 #: src/components/status.jsx:1102
msgid "Unable to copy link" msgid "Unable to copy link"
msgstr "" msgstr ""
#: src/components/account-info.jsx:1308 #: src/components/account-info.jsx:1308
#: src/components/shortcuts-settings.jsx:1056 #: src/components/shortcuts-settings.jsx:1056
#: src/components/status.jsx:1088 #: src/components/status.jsx:1108
#: src/components/status.jsx:3150 #: src/components/status.jsx:3170
msgid "Copy" msgid "Copy"
msgstr "" msgstr ""
#: src/components/account-info.jsx:1323 #: src/components/account-info.jsx:1323
#: src/components/shortcuts-settings.jsx:1074 #: src/components/shortcuts-settings.jsx:1074
#: src/components/status.jsx:1104 #: src/components/status.jsx:1124
msgid "Sharing doesn't seem to work." msgid "Sharing doesn't seem to work."
msgstr "" msgstr ""
#: src/components/account-info.jsx:1329 #: src/components/account-info.jsx:1329
#: src/components/status.jsx:1110 #: src/components/status.jsx:1130
msgid "Share…" msgid "Share…"
msgstr "" msgstr ""
@ -417,9 +417,9 @@ msgstr ""
#: src/components/shortcuts-settings.jsx:227 #: src/components/shortcuts-settings.jsx:227
#: src/components/shortcuts-settings.jsx:580 #: src/components/shortcuts-settings.jsx:580
#: src/components/shortcuts-settings.jsx:780 #: src/components/shortcuts-settings.jsx:780
#: src/components/status.jsx:2875 #: src/components/status.jsx:2895
#: src/components/status.jsx:3114 #: src/components/status.jsx:3134
#: src/components/status.jsx:3612 #: src/components/status.jsx:3632
#: src/pages/accounts.jsx:36 #: src/pages/accounts.jsx:36
#: src/pages/catchup.jsx:1581 #: src/pages/catchup.jsx:1581
#: src/pages/filters.jsx:224 #: src/pages/filters.jsx:224
@ -605,7 +605,7 @@ msgid "Attachment #{i} failed"
msgstr "" msgstr ""
#: src/components/compose.jsx:1118 #: src/components/compose.jsx:1118
#: src/components/status.jsx:1962 #: src/components/status.jsx:1982
#: src/components/timeline.jsx:984 #: src/components/timeline.jsx:984
msgid "Content warning" msgid "Content warning"
msgstr "" msgstr ""
@ -641,7 +641,7 @@ msgstr ""
#: src/components/compose.jsx:1185 #: src/components/compose.jsx:1185
#: src/components/status.jsx:96 #: src/components/status.jsx:96
#: src/components/status.jsx:1840 #: src/components/status.jsx:1860
msgid "Private mention" msgid "Private mention"
msgstr "" msgstr ""
@ -671,10 +671,10 @@ msgstr ""
#: src/components/compose.jsx:1479 #: src/components/compose.jsx:1479
#: src/components/keyboard-shortcuts-help.jsx:143 #: src/components/keyboard-shortcuts-help.jsx:143
#: src/components/status.jsx:831 #: src/components/status.jsx:851
#: src/components/status.jsx:1616 #: src/components/status.jsx:1636
#: src/components/status.jsx:1617 #: src/components/status.jsx:1637
#: src/components/status.jsx:2271 #: src/components/status.jsx:2291
msgid "Reply" msgid "Reply"
msgstr "" msgstr ""
@ -889,7 +889,7 @@ msgstr ""
#: src/components/drafts.jsx:127 #: src/components/drafts.jsx:127
#: src/components/list-add-edit.jsx:183 #: src/components/list-add-edit.jsx:183
#: src/components/status.jsx:1251 #: src/components/status.jsx:1271
#: src/pages/filters.jsx:587 #: src/pages/filters.jsx:587
msgid "Delete…" msgid "Delete…"
msgstr "" msgstr ""
@ -1089,10 +1089,10 @@ msgid "<0>l</0> or <1>f</1>"
msgstr "" msgstr ""
#: src/components/keyboard-shortcuts-help.jsx:164 #: src/components/keyboard-shortcuts-help.jsx:164
#: src/components/status.jsx:839 #: src/components/status.jsx:859
#: src/components/status.jsx:2297 #: src/components/status.jsx:2317
#: src/components/status.jsx:2329 #: src/components/status.jsx:2349
#: src/components/status.jsx:2330 #: src/components/status.jsx:2350
msgid "Boost" msgid "Boost"
msgstr "" msgstr ""
@ -1101,9 +1101,9 @@ msgid "<0>Shift</0> + <1>b</1>"
msgstr "" msgstr ""
#: src/components/keyboard-shortcuts-help.jsx:172 #: src/components/keyboard-shortcuts-help.jsx:172
#: src/components/status.jsx:924 #: src/components/status.jsx:944
#: src/components/status.jsx:2354 #: src/components/status.jsx:2374
#: src/components/status.jsx:2355 #: src/components/status.jsx:2375
msgid "Bookmark" msgid "Bookmark"
msgstr "" msgstr ""
@ -1162,15 +1162,15 @@ msgid "Media description"
msgstr "" msgstr ""
#: src/components/media-alt-modal.jsx:57 #: src/components/media-alt-modal.jsx:57
#: src/components/status.jsx:968 #: src/components/status.jsx:988
#: src/components/status.jsx:995 #: src/components/status.jsx:1015
#: src/components/translation-block.jsx:195 #: src/components/translation-block.jsx:195
msgid "Translate" msgid "Translate"
msgstr "" msgstr ""
#: src/components/media-alt-modal.jsx:68 #: src/components/media-alt-modal.jsx:68
#: src/components/status.jsx:982 #: src/components/status.jsx:1002
#: src/components/status.jsx:1009 #: src/components/status.jsx:1029
msgid "Speak" msgid "Speak"
msgstr "" msgstr ""
@ -1207,9 +1207,9 @@ msgid "Filtered: {filterTitleStr}"
msgstr "" msgstr ""
#: src/components/media-post.jsx:133 #: src/components/media-post.jsx:133
#: src/components/status.jsx:3442 #: src/components/status.jsx:3462
#: src/components/status.jsx:3538 #: src/components/status.jsx:3558
#: src/components/status.jsx:3616 #: src/components/status.jsx:3636
#: src/components/timeline.jsx:973 #: src/components/timeline.jsx:973
#: src/pages/catchup.jsx:75 #: src/pages/catchup.jsx:75
#: src/pages/catchup.jsx:1876 #: src/pages/catchup.jsx:1876
@ -1489,8 +1489,8 @@ msgid "[Unknown notification type: {type}]"
msgstr "" msgstr ""
#: src/components/notification.jsx:433 #: src/components/notification.jsx:433
#: src/components/status.jsx:938 #: src/components/status.jsx:958
#: src/components/status.jsx:948 #: src/components/status.jsx:968
msgid "Boosted/Liked by…" msgid "Boosted/Liked by…"
msgstr "" msgstr ""
@ -1512,7 +1512,7 @@ msgid "Learn more <0/>"
msgstr "" msgstr ""
#: src/components/notification.jsx:745 #: src/components/notification.jsx:745
#: src/components/status.jsx:190 #: src/components/status.jsx:210
msgid "Read more →" msgid "Read more →"
msgstr "" msgstr ""
@ -1805,7 +1805,7 @@ msgid "Move down"
msgstr "" msgstr ""
#: src/components/shortcuts-settings.jsx:376 #: src/components/shortcuts-settings.jsx:376
#: src/components/status.jsx:1216 #: src/components/status.jsx:1236
#: src/pages/list.jsx:170 #: src/pages/list.jsx:170
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
@ -2003,297 +2003,297 @@ msgstr ""
msgid "Import/export settings from/to instance server (Very experimental)" msgid "Import/export settings from/to instance server (Very experimental)"
msgstr "" msgstr ""
#: src/components/status.jsx:464 #: src/components/status.jsx:484
msgid "<0/> <1>boosted</1>" msgid "<0/> <1>boosted</1>"
msgstr "" msgstr ""
#: src/components/status.jsx:563 #: src/components/status.jsx:583
msgid "Sorry, your current logged-in instance can't interact with this post from another instance." msgid "Sorry, your current logged-in instance can't interact with this post from another instance."
msgstr "" msgstr ""
#: src/components/status.jsx:716 #: src/components/status.jsx:736
msgid "Unliked @{0}'s post" msgid "Unliked @{0}'s post"
msgstr "Unliked @{0}'s post" msgstr "Unliked @{0}'s post"
#: src/components/status.jsx:717 #: src/components/status.jsx:737
msgid "Liked @{0}'s post" msgid "Liked @{0}'s post"
msgstr "Liked @{0}'s post" msgstr "Liked @{0}'s post"
#: src/components/status.jsx:756 #: src/components/status.jsx:776
msgid "Unbookmarked @{0}'s post" msgid "Unbookmarked @{0}'s post"
msgstr "" msgstr ""
#: src/components/status.jsx:757 #: src/components/status.jsx:777
msgid "Bookmarked @{0}'s post" msgid "Bookmarked @{0}'s post"
msgstr "" msgstr ""
#: src/components/status.jsx:839 #: src/components/status.jsx:859
#: src/components/status.jsx:901 #: src/components/status.jsx:921
#: src/components/status.jsx:2297 #: src/components/status.jsx:2317
#: src/components/status.jsx:2329 #: src/components/status.jsx:2349
msgid "Unboost" msgid "Unboost"
msgstr "" msgstr ""
#: src/components/status.jsx:855 #: src/components/status.jsx:875
#: src/components/status.jsx:2312 #: src/components/status.jsx:2332
msgid "Quote" msgid "Quote"
msgstr "" msgstr ""
#: src/components/status.jsx:863 #: src/components/status.jsx:883
#: src/components/status.jsx:2321 #: src/components/status.jsx:2341
msgid "Some media have no descriptions." msgid "Some media have no descriptions."
msgstr "" msgstr ""
#: src/components/status.jsx:870 #: src/components/status.jsx:890
msgid "Old post (<0>{0}</0>)" msgid "Old post (<0>{0}</0>)"
msgstr "" msgstr ""
#: src/components/status.jsx:889 #: src/components/status.jsx:909
#: src/components/status.jsx:1341 #: src/components/status.jsx:1361
msgid "Unboosted @{0}'s post" msgid "Unboosted @{0}'s post"
msgstr "" msgstr ""
#: src/components/status.jsx:890 #: src/components/status.jsx:910
#: src/components/status.jsx:1342 #: src/components/status.jsx:1362
msgid "Boosted @{0}'s post" msgid "Boosted @{0}'s post"
msgstr "" msgstr ""
#: src/components/status.jsx:902 #: src/components/status.jsx:922
msgid "Boost…" msgid "Boost…"
msgstr "" msgstr ""
#: src/components/status.jsx:914 #: src/components/status.jsx:934
#: src/components/status.jsx:1626 #: src/components/status.jsx:1646
#: src/components/status.jsx:2342 #: src/components/status.jsx:2362
msgid "Unlike" msgid "Unlike"
msgstr "" msgstr ""
#: src/components/status.jsx:915 #: src/components/status.jsx:935
#: src/components/status.jsx:1626 #: src/components/status.jsx:1646
#: src/components/status.jsx:1627 #: src/components/status.jsx:1647
#: src/components/status.jsx:2342 #: src/components/status.jsx:2362
#: src/components/status.jsx:2343 #: src/components/status.jsx:2363
msgid "Like" msgid "Like"
msgstr "" msgstr ""
#: src/components/status.jsx:924 #: src/components/status.jsx:944
#: src/components/status.jsx:2354 #: src/components/status.jsx:2374
msgid "Unbookmark" msgid "Unbookmark"
msgstr "" msgstr ""
#: src/components/status.jsx:1032 #: src/components/status.jsx:1052
msgid "View post by <0>@{0}</0>" msgid "View post by <0>@{0}</0>"
msgstr "" msgstr ""
#: src/components/status.jsx:1053 #: src/components/status.jsx:1073
msgid "Show Edit History" msgid "Show Edit History"
msgstr "" msgstr ""
#: src/components/status.jsx:1056 #: src/components/status.jsx:1076
msgid "Edited: {editedDateText}" msgid "Edited: {editedDateText}"
msgstr "" msgstr ""
#: src/components/status.jsx:1123 #: src/components/status.jsx:1143
#: src/components/status.jsx:3119 #: src/components/status.jsx:3139
msgid "Embed post" msgid "Embed post"
msgstr "" msgstr ""
#: src/components/status.jsx:1137 #: src/components/status.jsx:1157
msgid "Conversation unmuted" msgid "Conversation unmuted"
msgstr "" msgstr ""
#: src/components/status.jsx:1137 #: src/components/status.jsx:1157
msgid "Conversation muted" msgid "Conversation muted"
msgstr "" msgstr ""
#: src/components/status.jsx:1143 #: src/components/status.jsx:1163
msgid "Unable to unmute conversation" msgid "Unable to unmute conversation"
msgstr "" msgstr ""
#: src/components/status.jsx:1144 #: src/components/status.jsx:1164
msgid "Unable to mute conversation" msgid "Unable to mute conversation"
msgstr "" msgstr ""
#: src/components/status.jsx:1153 #: src/components/status.jsx:1173
msgid "Unmute conversation" msgid "Unmute conversation"
msgstr "" msgstr ""
#: src/components/status.jsx:1160 #: src/components/status.jsx:1180
msgid "Mute conversation" msgid "Mute conversation"
msgstr "" msgstr ""
#: src/components/status.jsx:1176 #: src/components/status.jsx:1196
msgid "Post unpinned from profile" msgid "Post unpinned from profile"
msgstr "" msgstr ""
#: src/components/status.jsx:1177 #: src/components/status.jsx:1197
msgid "Post pinned to profile" msgid "Post pinned to profile"
msgstr "" msgstr ""
#: src/components/status.jsx:1182 #: src/components/status.jsx:1202
msgid "Unable to unpin post" msgid "Unable to unpin post"
msgstr "" msgstr ""
#: src/components/status.jsx:1182 #: src/components/status.jsx:1202
msgid "Unable to pin post" msgid "Unable to pin post"
msgstr "" msgstr ""
#: src/components/status.jsx:1191 #: src/components/status.jsx:1211
msgid "Unpin from profile" msgid "Unpin from profile"
msgstr "" msgstr ""
#: src/components/status.jsx:1198 #: src/components/status.jsx:1218
msgid "Pin to profile" msgid "Pin to profile"
msgstr "" msgstr ""
#: src/components/status.jsx:1227 #: src/components/status.jsx:1247
msgid "Delete this post?" msgid "Delete this post?"
msgstr "" msgstr ""
#: src/components/status.jsx:1240 #: src/components/status.jsx:1260
msgid "Post deleted" msgid "Post deleted"
msgstr "" msgstr ""
#: src/components/status.jsx:1243 #: src/components/status.jsx:1263
msgid "Unable to delete post" msgid "Unable to delete post"
msgstr "" msgstr ""
#: src/components/status.jsx:1271 #: src/components/status.jsx:1291
msgid "Report post…" msgid "Report post…"
msgstr "" msgstr ""
#: src/components/status.jsx:1627 #: src/components/status.jsx:1647
#: src/components/status.jsx:1663 #: src/components/status.jsx:1683
#: src/components/status.jsx:2343 #: src/components/status.jsx:2363
msgid "Liked" msgid "Liked"
msgstr "" msgstr ""
#: src/components/status.jsx:1660 #: src/components/status.jsx:1680
#: src/components/status.jsx:2330 #: src/components/status.jsx:2350
msgid "Boosted" msgid "Boosted"
msgstr "" msgstr ""
#: src/components/status.jsx:1670 #: src/components/status.jsx:1690
#: src/components/status.jsx:2355 #: src/components/status.jsx:2375
msgid "Bookmarked" msgid "Bookmarked"
msgstr "" msgstr ""
#: src/components/status.jsx:1674 #: src/components/status.jsx:1694
msgid "Pinned" msgid "Pinned"
msgstr "" msgstr ""
#: src/components/status.jsx:1719 #: src/components/status.jsx:1739
#: src/components/status.jsx:2174 #: src/components/status.jsx:2194
msgid "Deleted" msgid "Deleted"
msgstr "" msgstr ""
#: src/components/status.jsx:1760 #: src/components/status.jsx:1780
msgid "{repliesCount, plural, one {# reply} other {# replies}}" msgid "{repliesCount, plural, one {# reply} other {# replies}}"
msgstr "" msgstr ""
#: src/components/status.jsx:1849 #: src/components/status.jsx:1869
msgid "Thread{0}" msgid "Thread{0}"
msgstr "" msgstr ""
#: src/components/status.jsx:1925 #: src/components/status.jsx:1945
#: src/components/status.jsx:1987 #: src/components/status.jsx:2007
#: src/components/status.jsx:2072 #: src/components/status.jsx:2092
msgid "Show less" msgid "Show less"
msgstr "" msgstr ""
#: src/components/status.jsx:1925 #: src/components/status.jsx:1945
#: src/components/status.jsx:1987 #: src/components/status.jsx:2007
msgid "Show content" msgid "Show content"
msgstr "" msgstr ""
#: src/components/status.jsx:2072 #: src/components/status.jsx:2092
msgid "Show media" msgid "Show media"
msgstr "" msgstr ""
#: src/components/status.jsx:2195 #: src/components/status.jsx:2215
msgid "Edited" msgid "Edited"
msgstr "" msgstr ""
#: src/components/status.jsx:2272 #: src/components/status.jsx:2292
msgid "Comments" msgid "Comments"
msgstr "" msgstr ""
#. More from [Author] #. More from [Author]
#: src/components/status.jsx:2581 #: src/components/status.jsx:2601
msgid "More from <0/>" msgid "More from <0/>"
msgstr "More from <0/>" msgstr "More from <0/>"
#: src/components/status.jsx:2880 #: src/components/status.jsx:2900
msgid "Edit History" msgid "Edit History"
msgstr "" msgstr ""
#: src/components/status.jsx:2884 #: src/components/status.jsx:2904
msgid "Failed to load history" msgid "Failed to load history"
msgstr "" msgstr ""
#: src/components/status.jsx:2889 #: src/components/status.jsx:2909
msgid "Loading…" msgid "Loading…"
msgstr "" msgstr ""
#: src/components/status.jsx:3124 #: src/components/status.jsx:3144
msgid "HTML Code" msgid "HTML Code"
msgstr "" msgstr ""
#: src/components/status.jsx:3141 #: src/components/status.jsx:3161
msgid "HTML code copied" msgid "HTML code copied"
msgstr "" msgstr ""
#: src/components/status.jsx:3144 #: src/components/status.jsx:3164
msgid "Unable to copy HTML code" msgid "Unable to copy HTML code"
msgstr "" msgstr ""
#: src/components/status.jsx:3156 #: src/components/status.jsx:3176
msgid "Media attachments:" msgid "Media attachments:"
msgstr "" msgstr ""
#: src/components/status.jsx:3178 #: src/components/status.jsx:3198
msgid "Account Emojis:" msgid "Account Emojis:"
msgstr "" msgstr ""
#: src/components/status.jsx:3209 #: src/components/status.jsx:3229
#: src/components/status.jsx:3254 #: src/components/status.jsx:3274
msgid "static URL" msgid "static URL"
msgstr "" msgstr ""
#: src/components/status.jsx:3223 #: src/components/status.jsx:3243
msgid "Emojis:" msgid "Emojis:"
msgstr "" msgstr ""
#: src/components/status.jsx:3268 #: src/components/status.jsx:3288
msgid "Notes:" msgid "Notes:"
msgstr "" msgstr ""
#: src/components/status.jsx:3272 #: src/components/status.jsx:3292
msgid "This is static, unstyled and scriptless. You may need to apply your own styles and edit as needed." msgid "This is static, unstyled and scriptless. You may need to apply your own styles and edit as needed."
msgstr "" msgstr ""
#: src/components/status.jsx:3278 #: src/components/status.jsx:3298
msgid "Polls are not interactive, becomes a list with vote counts." msgid "Polls are not interactive, becomes a list with vote counts."
msgstr "" msgstr ""
#: src/components/status.jsx:3283 #: src/components/status.jsx:3303
msgid "Media attachments can be images, videos, audios or any file types." msgid "Media attachments can be images, videos, audios or any file types."
msgstr "" msgstr ""
#: src/components/status.jsx:3289 #: src/components/status.jsx:3309
msgid "Post could be edited or deleted later." msgid "Post could be edited or deleted later."
msgstr "" msgstr ""
#: src/components/status.jsx:3295 #: src/components/status.jsx:3315
msgid "Preview" msgid "Preview"
msgstr "" msgstr ""
#: src/components/status.jsx:3304 #: src/components/status.jsx:3324
msgid "Note: This preview is lightly styled." msgid "Note: This preview is lightly styled."
msgstr "" msgstr ""
#. [Name] [Visibility icon] boosted #. [Name] [Visibility icon] boosted
#: src/components/status.jsx:3546 #: src/components/status.jsx:3566
msgid "<0/> <1/> boosted" msgid "<0/> <1/> boosted"
msgstr "" msgstr ""

View file

@ -4,53 +4,85 @@ import mem from './mem';
const fauxDiv = document.createElement('div'); const fauxDiv = document.createElement('div');
const whitelistLinkClasses = ['u-url', 'mention', 'hashtag']; const whitelistLinkClasses = ['u-url', 'mention', 'hashtag'];
const HTML_CHARS_REGEX = /[&<>]/g;
function escapeHTML(html) {
return html.replace(
HTML_CHARS_REGEX,
(c) =>
({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
}[c]),
);
}
const LINK_REGEX = /<a/i;
const HTTP_LINK_REGEX = /^https?:\/\//i;
const MENTION_REGEX = /^@[^@]+(@[^@]+)?$/g;
const HASHTAG_REGEX = /^#[^#]+$/g;
const CODE_BLOCK_REGEX = /^```[^]+```$/g;
const CODE_BLOCK_START_REGEX = /^```/g;
const CODE_BLOCK_END_REGEX = /```$/g;
const INLINE_CODE_REGEX = /`[^`]+`/g;
const TWITTER_DOMAIN_REGEX = /(twitter|x)\.com/i;
const TWITTER_MENTION_REGEX = /@[a-zA-Z0-9_]+@(twitter|x)\.com/g;
const TWITTER_MENTION_CAPTURE_REGEX = /(@([a-zA-Z0-9_]+)@(twitter|x)\.com)/g;
function createDOM(html, isDocumentFragment) {
const tpl = document.createElement('template');
tpl.innerHTML = html;
return isDocumentFragment ? tpl.content : tpl;
}
function _enhanceContent(content, opts = {}) { function _enhanceContent(content, opts = {}) {
const { emojis, postEnhanceDOM = () => {} } = opts; const { emojis, returnDOM, postEnhanceDOM = () => {} } = opts;
let enhancedContent = content; let enhancedContent = content;
const dom = document.createElement('div'); // const dom = document.createElement('div');
dom.innerHTML = enhancedContent; // dom.innerHTML = enhancedContent;
const hasLink = /<a/i.test(enhancedContent); const dom = createDOM(enhancedContent, returnDOM);
const hasLink = LINK_REGEX.test(enhancedContent);
const hasCodeBlock = enhancedContent.includes('```'); const hasCodeBlock = enhancedContent.includes('```');
if (hasLink) { if (hasLink) {
// Add target="_blank" to all links with no target="_blank" // Add target="_blank" to all links with no target="_blank"
// E.g. `note` in `account` // E.g. `note` in `account`
const noTargetBlankLinks = dom.querySelectorAll('a:not([target="_blank"])'); const noTargetBlankLinks = dom.querySelectorAll('a:not([target="_blank"])');
noTargetBlankLinks.forEach((link) => { for (const link of noTargetBlankLinks) {
link.setAttribute('target', '_blank'); link.setAttribute('target', '_blank');
}); }
// Remove all classes except `u-url`, `mention`, `hashtag` // Remove all classes except `u-url`, `mention`, `hashtag`
const links = dom.querySelectorAll('a[class]'); const links = dom.querySelectorAll('a[class]');
links.forEach((link) => { for (const link of links) {
link.classList.forEach((c) => { for (const c of link.classList) {
if (!whitelistLinkClasses.includes(c)) { if (!whitelistLinkClasses.includes(c)) {
link.classList.remove(c); link.classList.remove(c);
} }
}); }
}); }
} }
// Add 'has-url-text' to all links that contains a url // Add 'has-url-text' to all links that contains a url
if (hasLink) { if (hasLink) {
const links = dom.querySelectorAll('a[href]'); const links = dom.querySelectorAll('a[href]');
links.forEach((link) => { for (const link of links) {
if (/^https?:\/\//i.test(link.textContent.trim())) { if (HTTP_LINK_REGEX.test(link.textContent.trim())) {
link.classList.add('has-url-text'); link.classList.add('has-url-text');
shortenLink(link); shortenLink(link);
} }
}); }
} }
// Spanify un-spanned mentions // Spanify un-spanned mentions
if (hasLink) { if (hasLink) {
const links = dom.querySelectorAll('a[href]'); const links = dom.querySelectorAll('a[href]');
const usernames = []; const usernames = [];
links.forEach((link) => { for (const link of links) {
const text = link.innerText.trim(); const text = link.innerText.trim();
const hasChildren = link.querySelector('*'); const hasChildren = link.querySelector('*');
// If text looks like @username@domain, then it's a mention // If text looks like @username@domain, then it's a mention
if (/^@[^@]+(@[^@]+)?$/g.test(text)) { if (MENTION_REGEX.test(text)) {
// Only show @username // Only show @username
const [_, username, domain] = text.split('@'); const [_, username, domain] = text.split('@');
if (!hasChildren) { if (!hasChildren) {
@ -67,11 +99,11 @@ function _enhanceContent(content, opts = {}) {
link.classList.add('mention'); link.classList.add('mention');
} }
// If text looks like #hashtag, then it's a hashtag // If text looks like #hashtag, then it's a hashtag
if (/^#[^#]+$/g.test(text)) { if (HASHTAG_REGEX.test(text)) {
if (!hasChildren) link.innerHTML = `#<span>${text.slice(1)}</span>`; if (!hasChildren) link.innerHTML = `#<span>${text.slice(1)}</span>`;
link.classList.add('mention', 'hashtag'); link.classList.add('mention', 'hashtag');
} }
}); }
} }
// EMOJIS // EMOJIS
@ -80,18 +112,14 @@ function _enhanceContent(content, opts = {}) {
let textNodes; let textNodes;
if (enhancedContent.includes(':')) { if (enhancedContent.includes(':')) {
textNodes = extractTextNodes(dom); textNodes = extractTextNodes(dom);
textNodes.forEach((node) => { for (const node of textNodes) {
let html = node.nodeValue let html = escapeHTML(node.nodeValue);
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
if (emojis) { if (emojis) {
html = emojifyText(html, emojis); html = emojifyText(html, emojis);
} }
fauxDiv.innerHTML = html; fauxDiv.innerHTML = html;
// const nodes = [...fauxDiv.childNodes];
node.replaceWith(...fauxDiv.childNodes); node.replaceWith(...fauxDiv.childNodes);
}); }
} }
// CODE BLOCKS // CODE BLOCKS
@ -99,31 +127,35 @@ function _enhanceContent(content, opts = {}) {
// Convert ```code``` to <pre><code>code</code></pre> // Convert ```code``` to <pre><code>code</code></pre>
if (hasCodeBlock) { if (hasCodeBlock) {
const blocks = [...dom.querySelectorAll('p')].filter((p) => const blocks = [...dom.querySelectorAll('p')].filter((p) =>
/^```[^]+```$/g.test(p.innerText.trim()), CODE_BLOCK_REGEX.test(p.innerText.trim()),
); );
blocks.forEach((block) => { for (const block of blocks) {
const pre = document.createElement('pre'); const pre = document.createElement('pre');
// Replace <br /> with newlines // Replace <br /> with newlines
block.querySelectorAll('br').forEach((br) => br.replaceWith('\n')); for (const br of block.querySelectorAll('br')) {
br.replaceWith('\n');
}
pre.innerHTML = `<code>${block.innerHTML.trim()}</code>`; pre.innerHTML = `<code>${block.innerHTML.trim()}</code>`;
block.replaceWith(pre); block.replaceWith(pre);
}); }
} }
// Convert multi-paragraph code blocks to <pre><code>code</code></pre> // Convert multi-paragraph code blocks to <pre><code>code</code></pre>
if (hasCodeBlock) { if (hasCodeBlock) {
const paragraphs = [...dom.querySelectorAll('p')]; const paragraphs = [...dom.querySelectorAll('p')];
// Filter out paragraphs with ``` in beginning only // Filter out paragraphs with ``` in beginning only
const codeBlocks = paragraphs.filter((p) => /^```/g.test(p.innerText)); const codeBlocks = paragraphs.filter((p) =>
CODE_BLOCK_START_REGEX.test(p.innerText),
);
// For each codeBlocks, get all paragraphs until the last paragraph with ``` at the end only // For each codeBlocks, get all paragraphs until the last paragraph with ``` at the end only
codeBlocks.forEach((block) => { for (const block of codeBlocks) {
const nextParagraphs = [block]; const nextParagraphs = [block];
let hasCodeBlock = false; let hasCodeBlock = false;
let currentBlock = block; let currentBlock = block;
while (currentBlock.nextElementSibling) { while (currentBlock.nextElementSibling) {
const next = currentBlock.nextElementSibling; const next = currentBlock.nextElementSibling;
if (next && next.tagName === 'P') { if (next && next.tagName === 'P') {
if (/```$/g.test(next.innerText)) { if (CODE_BLOCK_END_REGEX.test(next.innerText)) {
nextParagraphs.push(next); nextParagraphs.push(next);
hasCodeBlock = true; hasCodeBlock = true;
break; break;
@ -137,16 +169,20 @@ function _enhanceContent(content, opts = {}) {
} }
if (hasCodeBlock) { if (hasCodeBlock) {
const pre = document.createElement('pre'); const pre = document.createElement('pre');
nextParagraphs.forEach((p) => { for (const p of nextParagraphs) {
// Replace <br /> with newlines // Replace <br /> with newlines
p.querySelectorAll('br').forEach((br) => br.replaceWith('\n')); for (const br of p.querySelectorAll('br')) {
}); br.replaceWith('\n');
}
}
const codeText = nextParagraphs.map((p) => p.innerHTML).join('\n\n'); const codeText = nextParagraphs.map((p) => p.innerHTML).join('\n\n');
pre.innerHTML = `<code tabindex="0">${codeText}</code>`; pre.innerHTML = `<code tabindex="0">${codeText}</code>`;
block.replaceWith(pre); block.replaceWith(pre);
nextParagraphs.forEach((p) => p.remove()); for (const p of nextParagraphs) {
p.remove();
}
} }
}); }
} }
// INLINE CODE // INLINE CODE
@ -154,42 +190,36 @@ function _enhanceContent(content, opts = {}) {
// Convert `code` to <code>code</code> // Convert `code` to <code>code</code>
if (enhancedContent.includes('`')) { if (enhancedContent.includes('`')) {
textNodes = extractTextNodes(dom); textNodes = extractTextNodes(dom);
textNodes.forEach((node) => { for (const node of textNodes) {
let html = node.nodeValue let html = escapeHTML(node.nodeValue);
.replace(/&/g, '&amp;') if (INLINE_CODE_REGEX.test(html)) {
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
if (/`[^`]+`/g.test(html)) {
html = html.replaceAll(/(`[^]+?`)/g, '<code>$1</code>'); html = html.replaceAll(/(`[^]+?`)/g, '<code>$1</code>');
} }
fauxDiv.innerHTML = html; fauxDiv.innerHTML = html;
// const nodes = [...fauxDiv.childNodes]; // const nodes = [...fauxDiv.childNodes];
node.replaceWith(...fauxDiv.childNodes); node.replaceWith(...fauxDiv.childNodes);
}); }
} }
// TWITTER USERNAMES // TWITTER USERNAMES
// ================= // =================
// Convert @username@twitter.com to <a href="https://twitter.com/username">@username@twitter.com</a> // Convert @username@twitter.com to <a href="https://twitter.com/username">@username@twitter.com</a>
if (/twitter\.com/i.test(enhancedContent)) { if (TWITTER_DOMAIN_REGEX.test(enhancedContent)) {
textNodes = extractTextNodes(dom, { textNodes = extractTextNodes(dom, {
rejectFilter: ['A'], rejectFilter: ['A'],
}); });
textNodes.forEach((node) => { for (const node of textNodes) {
let html = node.nodeValue let html = escapeHTML(node.nodeValue);
.replace(/&/g, '&amp;') if (TWITTER_MENTION_REGEX.test(html)) {
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
if (/@[a-zA-Z0-9_]+@twitter\.com/g.test(html)) {
html = html.replaceAll( html = html.replaceAll(
/(@([a-zA-Z0-9_]+)@twitter\.com)/g, TWITTER_MENTION_CAPTURE_REGEX,
'<a href="https://twitter.com/$2" rel="nofollow noopener noreferrer" target="_blank">$1</a>', '<a href="https://twitter.com/$2" rel="nofollow noopener noreferrer" target="_blank">$1</a>',
); );
} }
fauxDiv.innerHTML = html; fauxDiv.innerHTML = html;
// const nodes = [...fauxDiv.childNodes]; // const nodes = [...fauxDiv.childNodes];
node.replaceWith(...fauxDiv.childNodes); node.replaceWith(...fauxDiv.childNodes);
}); }
} }
// HASHTAG STUFFING // HASHTAG STUFFING
@ -235,22 +265,24 @@ function _enhanceContent(content, opts = {}) {
}, },
); );
if (hashtagStuffedParagraphs?.length) { if (hashtagStuffedParagraphs?.length) {
hashtagStuffedParagraphs.forEach((p) => { for (const p of hashtagStuffedParagraphs) {
p.classList.add('hashtag-stuffing'); p.classList.add('hashtag-stuffing');
p.title = p.innerText; p.title = p.innerText;
}); }
} }
} }
// ADD ASPECT RATIO TO ALL IMAGES // ADD ASPECT RATIO TO ALL IMAGES
if (enhancedContent.includes('<img')) { if (enhancedContent.includes('<img')) {
dom.querySelectorAll('img').forEach((img) => { const imgs = dom.querySelectorAll('img');
for (let i = 0; i < imgs.length; i++) {
const img = imgs[i];
const width = img.getAttribute('width') || img.naturalWidth; const width = img.getAttribute('width') || img.naturalWidth;
const height = img.getAttribute('height') || img.naturalHeight; const height = img.getAttribute('height') || img.naturalHeight;
if (width && height) { if (width && height) {
img.style.setProperty('--original-aspect-ratio', `${width}/${height}`); img.style.setProperty('--original-aspect-ratio', `${width}/${height}`);
} }
}); }
} }
if (postEnhanceDOM) { if (postEnhanceDOM) {
@ -258,9 +290,7 @@ function _enhanceContent(content, opts = {}) {
// postEnhanceDOM(dom); // mutate dom // postEnhanceDOM(dom); // mutate dom
} }
enhancedContent = dom.innerHTML; return returnDOM ? dom : dom.innerHTML;
return enhancedContent;
} }
const enhanceContent = mem(_enhanceContent); const enhanceContent = mem(_enhanceContent);

View file

@ -3,5 +3,9 @@ import moize from 'moize';
window._moize = moize; window._moize = moize;
export default function mem(fn, opts = {}) { export default function mem(fn, opts = {}) {
return moize(fn, { ...opts, maxSize: 50, isDeepEqual: true }); return moize(fn, {
...opts,
maxSize: 30,
isDeepEqual: true,
});
} }