diff --git a/src/components/avatar.jsx b/src/components/avatar.jsx index ab6f54a8..a29a37af 100644 --- a/src/components/avatar.jsx +++ b/src/components/avatar.jsx @@ -23,10 +23,12 @@ const ctx = canvas.getContext('2d', { }); ctx.imageSmoothingEnabled = false; +const MISSING_IMAGE_PATH_REGEX = /missing\.png$/; + function Avatar({ url, size, alt = '', squircle, ...props }) { size = SIZES[size] || size || SIZES.m; const avatarRef = useRef(); - const isMissing = /missing\.png$/.test(url); + const isMissing = MISSING_IMAGE_PATH_REGEX.test(url); return ( { + return new RegExp(`:(${shortcodes.join('|')}):`, 'g'); +}); + function EmojiText({ text, emojis }) { if (!text) return ''; if (!emojis?.length) return text; if (text.indexOf(':') === -1) return text; - const regex = new RegExp( - `:(${emojis.map((e) => e.shortcode).join('|')}):`, - 'g', - ); + // const regex = new RegExp( + // `:(${emojis.map((e) => e.shortcode).join('|')}):`, + // 'g', + // ); + const regex = shortcodesRegexp(emojis.map((e) => e.shortcode)); const elements = text.split(regex).map((word) => { const emoji = emojis.find((e) => e.shortcode === word); if (emoji) { const { url, staticUrl } = emoji; - return ; + return ( + + ); } return word; }); return elements; } -export default memo( - EmojiText, - (oldProps, newProps) => - oldProps.text === newProps.text && - oldProps.emojis?.length === newProps.emojis?.length, -); +export default mem(EmojiText); + +// export default memo( +// EmojiText, +// (oldProps, newProps) => +// oldProps.text === newProps.text && +// oldProps.emojis?.length === newProps.emojis?.length, +// ); diff --git a/src/components/name-text.jsx b/src/components/name-text.jsx index 1a1425e4..82fa8c07 100644 --- a/src/components/name-text.jsx +++ b/src/components/name-text.jsx @@ -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({ account, instance, @@ -42,17 +47,17 @@ function NameText({ bot, username, } = account; - const [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct]; + const [_, acct1, acct2] = acct.match(ACCT_REGEX) || [, acct]; if (!instance) instance = api().instance; const trimmedUsername = username.toLowerCase().trim(); const trimmedDisplayName = (displayName || '').toLowerCase().trim(); const shortenedDisplayName = trimmedDisplayName - .replace(/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1 - .replace(/\s+/g, ''); // E.g. "My name" === "myname" + .replace(SHORTCODES_REGEX, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1 + .replace(SPACES_REGEX, ''); // E.g. "My name" === "myname" const shortenedAlphaNumericDisplayName = shortenedDisplayName.replace( - /[^a-z0-9@\.]/gi, + NON_ALPHA_NUMERIC_REGEX, '', ); // Remove non-alphanumeric characters @@ -130,9 +135,11 @@ function NameText({ ); } -export default memo(NameText, (oldProps, newProps) => { - // Only care about account.id, the other props usually don't change - const { account } = oldProps; - const { account: newAccount } = newProps; - return account?.acct === newAccount?.acct; -}); +export default mem(NameText); + +// export default memo(NameText, (oldProps, newProps) => { +// // Only care about account.id, the other props usually don't change +// const { account } = oldProps; +// const { account: newAccount } = newProps; +// return account?.acct === newAccount?.acct; +// }); diff --git a/src/components/status.jsx b/src/components/status.jsx index 2492ff7f..3fa8f88a 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -124,11 +124,31 @@ function getPostText(status) { ); } -const PostContent = memo( +const HTTP_REGEX = /^http/i; +const PostContent = + /*memo(*/ ({ post, instance, previewMode }) => { 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 (
{ - // Remove target="_blank" from links - dom.querySelectorAll('a.u-url[target="_blank"]').forEach((a) => { - if (!/http/i.test(a.innerText.trim())) { - a.removeAttribute('target'); - } - }); - }, - }), - }} + // dangerouslySetInnerHTML={{ + // __html: enhanceContent(content, { + // emojis, + // postEnhanceDOM: (dom) => { + // // Remove target="_blank" from links + // dom.querySelectorAll('a.u-url[target="_blank"]').forEach((a) => { + // if (!/http/i.test(a.innerText.trim())) { + // a.removeAttribute('target'); + // } + // }); + // }, + // }), + // }} /> ); - }, + }; /*, (oldProps, newProps) => { const { post: oldPost } = oldProps; const { post: newPost } = newProps; return oldPost.content === newPost.content; }, -); +);*/ const SIZE_CLASS = { s: 'small', diff --git a/src/locales/en.po b/src/locales/en.po index e05e27c8..daa0d8dd 100644 --- a/src/locales/en.po +++ b/src/locales/en.po @@ -13,40 +13,40 @@ msgstr "" "Language-Team: \n" "Plural-Forms: \n" -#: src/components/account-block.jsx:133 +#: src/components/account-block.jsx:135 msgid "Locked" msgstr "" -#: src/components/account-block.jsx:139 +#: src/components/account-block.jsx:141 msgid "Posts: {0}" msgstr "" -#: src/components/account-block.jsx:144 +#: src/components/account-block.jsx:146 msgid "Last posted: {0}" msgstr "" -#: src/components/account-block.jsx:159 +#: src/components/account-block.jsx:161 #: src/components/account-info.jsx:634 msgid "Automated" msgstr "" -#: src/components/account-block.jsx:166 +#: src/components/account-block.jsx:168 #: src/components/account-info.jsx:639 -#: src/components/status.jsx:440 +#: src/components/status.jsx:460 #: src/pages/catchup.jsx:1471 msgid "Group" msgstr "" -#: src/components/account-block.jsx:176 +#: src/components/account-block.jsx:178 msgid "Mutual" msgstr "" -#: src/components/account-block.jsx:180 +#: src/components/account-block.jsx:182 #: src/components/account-info.jsx:1677 msgid "Requested" msgstr "" -#: src/components/account-block.jsx:184 +#: src/components/account-block.jsx:186 #: src/components/account-info.jsx:416 #: src/components/account-info.jsx:742 #: src/components/account-info.jsx:756 @@ -58,21 +58,21 @@ msgstr "" msgid "Following" msgstr "" -#: src/components/account-block.jsx:188 +#: src/components/account-block.jsx:190 #: src/components/account-info.jsx:1059 msgid "Follows you" msgstr "" -#: src/components/account-block.jsx:196 +#: src/components/account-block.jsx:198 msgid "{followersCount, plural, one {# follower} other {# followers}}" msgstr "" -#: src/components/account-block.jsx:205 +#: src/components/account-block.jsx:207 #: src/components/account-info.jsx:680 msgid "Verified" msgstr "" -#: src/components/account-block.jsx:220 +#: src/components/account-block.jsx:222 #: src/components/account-info.jsx:777 msgid "Joined <0>{0}" msgstr "" @@ -108,11 +108,11 @@ msgstr "" #: src/components/compose.jsx:2463 #: src/components/media-alt-modal.jsx:45 #: src/components/media-modal.jsx:283 -#: src/components/status.jsx:1636 -#: src/components/status.jsx:1653 -#: src/components/status.jsx:1777 -#: src/components/status.jsx:2375 -#: src/components/status.jsx:2378 +#: src/components/status.jsx:1656 +#: src/components/status.jsx:1673 +#: src/components/status.jsx:1797 +#: src/components/status.jsx:2395 +#: src/components/status.jsx:2398 #: src/pages/account-statuses.jsx:528 #: src/pages/accounts.jsx:109 #: src/pages/hashtag.jsx:199 @@ -181,7 +181,7 @@ msgid "Original" msgstr "" #: 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:1445 #: src/pages/catchup.jsx:2056 @@ -277,30 +277,30 @@ msgid "Add/Remove from Lists" msgstr "" #: src/components/account-info.jsx:1299 -#: src/components/status.jsx:1079 +#: src/components/status.jsx:1099 msgid "Link copied" msgstr "" #: src/components/account-info.jsx:1302 -#: src/components/status.jsx:1082 +#: src/components/status.jsx:1102 msgid "Unable to copy link" msgstr "" #: src/components/account-info.jsx:1308 #: src/components/shortcuts-settings.jsx:1056 -#: src/components/status.jsx:1088 -#: src/components/status.jsx:3150 +#: src/components/status.jsx:1108 +#: src/components/status.jsx:3170 msgid "Copy" msgstr "" #: src/components/account-info.jsx:1323 #: src/components/shortcuts-settings.jsx:1074 -#: src/components/status.jsx:1104 +#: src/components/status.jsx:1124 msgid "Sharing doesn't seem to work." msgstr "" #: src/components/account-info.jsx:1329 -#: src/components/status.jsx:1110 +#: src/components/status.jsx:1130 msgid "Share…" msgstr "" @@ -417,9 +417,9 @@ msgstr "" #: src/components/shortcuts-settings.jsx:227 #: src/components/shortcuts-settings.jsx:580 #: src/components/shortcuts-settings.jsx:780 -#: src/components/status.jsx:2875 -#: src/components/status.jsx:3114 -#: src/components/status.jsx:3612 +#: src/components/status.jsx:2895 +#: src/components/status.jsx:3134 +#: src/components/status.jsx:3632 #: src/pages/accounts.jsx:36 #: src/pages/catchup.jsx:1581 #: src/pages/filters.jsx:224 @@ -605,7 +605,7 @@ msgid "Attachment #{i} failed" msgstr "" #: src/components/compose.jsx:1118 -#: src/components/status.jsx:1962 +#: src/components/status.jsx:1982 #: src/components/timeline.jsx:984 msgid "Content warning" msgstr "" @@ -641,7 +641,7 @@ msgstr "" #: src/components/compose.jsx:1185 #: src/components/status.jsx:96 -#: src/components/status.jsx:1840 +#: src/components/status.jsx:1860 msgid "Private mention" msgstr "" @@ -671,10 +671,10 @@ msgstr "" #: src/components/compose.jsx:1479 #: src/components/keyboard-shortcuts-help.jsx:143 -#: src/components/status.jsx:831 -#: src/components/status.jsx:1616 -#: src/components/status.jsx:1617 -#: src/components/status.jsx:2271 +#: src/components/status.jsx:851 +#: src/components/status.jsx:1636 +#: src/components/status.jsx:1637 +#: src/components/status.jsx:2291 msgid "Reply" msgstr "" @@ -889,7 +889,7 @@ msgstr "" #: src/components/drafts.jsx:127 #: src/components/list-add-edit.jsx:183 -#: src/components/status.jsx:1251 +#: src/components/status.jsx:1271 #: src/pages/filters.jsx:587 msgid "Delete…" msgstr "" @@ -1089,10 +1089,10 @@ msgid "<0>l or <1>f" msgstr "" #: src/components/keyboard-shortcuts-help.jsx:164 -#: src/components/status.jsx:839 -#: src/components/status.jsx:2297 -#: src/components/status.jsx:2329 -#: src/components/status.jsx:2330 +#: src/components/status.jsx:859 +#: src/components/status.jsx:2317 +#: src/components/status.jsx:2349 +#: src/components/status.jsx:2350 msgid "Boost" msgstr "" @@ -1101,9 +1101,9 @@ msgid "<0>Shift + <1>b" msgstr "" #: src/components/keyboard-shortcuts-help.jsx:172 -#: src/components/status.jsx:924 -#: src/components/status.jsx:2354 -#: src/components/status.jsx:2355 +#: src/components/status.jsx:944 +#: src/components/status.jsx:2374 +#: src/components/status.jsx:2375 msgid "Bookmark" msgstr "" @@ -1162,15 +1162,15 @@ msgid "Media description" msgstr "" #: src/components/media-alt-modal.jsx:57 -#: src/components/status.jsx:968 -#: src/components/status.jsx:995 +#: src/components/status.jsx:988 +#: src/components/status.jsx:1015 #: src/components/translation-block.jsx:195 msgid "Translate" msgstr "" #: src/components/media-alt-modal.jsx:68 -#: src/components/status.jsx:982 -#: src/components/status.jsx:1009 +#: src/components/status.jsx:1002 +#: src/components/status.jsx:1029 msgid "Speak" msgstr "" @@ -1207,9 +1207,9 @@ msgid "Filtered: {filterTitleStr}" msgstr "" #: src/components/media-post.jsx:133 -#: src/components/status.jsx:3442 -#: src/components/status.jsx:3538 -#: src/components/status.jsx:3616 +#: src/components/status.jsx:3462 +#: src/components/status.jsx:3558 +#: src/components/status.jsx:3636 #: src/components/timeline.jsx:973 #: src/pages/catchup.jsx:75 #: src/pages/catchup.jsx:1876 @@ -1489,8 +1489,8 @@ msgid "[Unknown notification type: {type}]" msgstr "" #: src/components/notification.jsx:433 -#: src/components/status.jsx:938 -#: src/components/status.jsx:948 +#: src/components/status.jsx:958 +#: src/components/status.jsx:968 msgid "Boosted/Liked by…" msgstr "" @@ -1512,7 +1512,7 @@ msgid "Learn more <0/>" msgstr "" #: src/components/notification.jsx:745 -#: src/components/status.jsx:190 +#: src/components/status.jsx:210 msgid "Read more →" msgstr "" @@ -1805,7 +1805,7 @@ msgid "Move down" msgstr "" #: src/components/shortcuts-settings.jsx:376 -#: src/components/status.jsx:1216 +#: src/components/status.jsx:1236 #: src/pages/list.jsx:170 msgid "Edit" msgstr "" @@ -2003,297 +2003,297 @@ msgstr "" msgid "Import/export settings from/to instance server (Very experimental)" msgstr "" -#: src/components/status.jsx:464 +#: src/components/status.jsx:484 msgid "<0/> <1>boosted" 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." msgstr "" -#: src/components/status.jsx:716 +#: src/components/status.jsx:736 msgid "Unliked @{0}'s post" msgstr "Unliked @{0}'s post" -#: src/components/status.jsx:717 +#: src/components/status.jsx:737 msgid "Liked @{0}'s post" msgstr "Liked @{0}'s post" -#: src/components/status.jsx:756 +#: src/components/status.jsx:776 msgid "Unbookmarked @{0}'s post" msgstr "" -#: src/components/status.jsx:757 +#: src/components/status.jsx:777 msgid "Bookmarked @{0}'s post" msgstr "" -#: src/components/status.jsx:839 -#: src/components/status.jsx:901 -#: src/components/status.jsx:2297 -#: src/components/status.jsx:2329 +#: src/components/status.jsx:859 +#: src/components/status.jsx:921 +#: src/components/status.jsx:2317 +#: src/components/status.jsx:2349 msgid "Unboost" msgstr "" -#: src/components/status.jsx:855 -#: src/components/status.jsx:2312 +#: src/components/status.jsx:875 +#: src/components/status.jsx:2332 msgid "Quote" msgstr "" -#: src/components/status.jsx:863 -#: src/components/status.jsx:2321 +#: src/components/status.jsx:883 +#: src/components/status.jsx:2341 msgid "Some media have no descriptions." msgstr "" -#: src/components/status.jsx:870 +#: src/components/status.jsx:890 msgid "Old post (<0>{0})" msgstr "" -#: src/components/status.jsx:889 -#: src/components/status.jsx:1341 +#: src/components/status.jsx:909 +#: src/components/status.jsx:1361 msgid "Unboosted @{0}'s post" msgstr "" -#: src/components/status.jsx:890 -#: src/components/status.jsx:1342 +#: src/components/status.jsx:910 +#: src/components/status.jsx:1362 msgid "Boosted @{0}'s post" msgstr "" -#: src/components/status.jsx:902 +#: src/components/status.jsx:922 msgid "Boost…" msgstr "" -#: src/components/status.jsx:914 -#: src/components/status.jsx:1626 -#: src/components/status.jsx:2342 +#: src/components/status.jsx:934 +#: src/components/status.jsx:1646 +#: src/components/status.jsx:2362 msgid "Unlike" msgstr "" -#: src/components/status.jsx:915 -#: src/components/status.jsx:1626 -#: src/components/status.jsx:1627 -#: src/components/status.jsx:2342 -#: src/components/status.jsx:2343 +#: src/components/status.jsx:935 +#: src/components/status.jsx:1646 +#: src/components/status.jsx:1647 +#: src/components/status.jsx:2362 +#: src/components/status.jsx:2363 msgid "Like" msgstr "" -#: src/components/status.jsx:924 -#: src/components/status.jsx:2354 +#: src/components/status.jsx:944 +#: src/components/status.jsx:2374 msgid "Unbookmark" msgstr "" -#: src/components/status.jsx:1032 +#: src/components/status.jsx:1052 msgid "View post by <0>@{0}" msgstr "" -#: src/components/status.jsx:1053 +#: src/components/status.jsx:1073 msgid "Show Edit History" msgstr "" -#: src/components/status.jsx:1056 +#: src/components/status.jsx:1076 msgid "Edited: {editedDateText}" msgstr "" -#: src/components/status.jsx:1123 -#: src/components/status.jsx:3119 +#: src/components/status.jsx:1143 +#: src/components/status.jsx:3139 msgid "Embed post" msgstr "" -#: src/components/status.jsx:1137 +#: src/components/status.jsx:1157 msgid "Conversation unmuted" msgstr "" -#: src/components/status.jsx:1137 +#: src/components/status.jsx:1157 msgid "Conversation muted" msgstr "" -#: src/components/status.jsx:1143 +#: src/components/status.jsx:1163 msgid "Unable to unmute conversation" msgstr "" -#: src/components/status.jsx:1144 +#: src/components/status.jsx:1164 msgid "Unable to mute conversation" msgstr "" -#: src/components/status.jsx:1153 +#: src/components/status.jsx:1173 msgid "Unmute conversation" msgstr "" -#: src/components/status.jsx:1160 +#: src/components/status.jsx:1180 msgid "Mute conversation" msgstr "" -#: src/components/status.jsx:1176 +#: src/components/status.jsx:1196 msgid "Post unpinned from profile" msgstr "" -#: src/components/status.jsx:1177 +#: src/components/status.jsx:1197 msgid "Post pinned to profile" msgstr "" -#: src/components/status.jsx:1182 +#: src/components/status.jsx:1202 msgid "Unable to unpin post" msgstr "" -#: src/components/status.jsx:1182 +#: src/components/status.jsx:1202 msgid "Unable to pin post" msgstr "" -#: src/components/status.jsx:1191 +#: src/components/status.jsx:1211 msgid "Unpin from profile" msgstr "" -#: src/components/status.jsx:1198 +#: src/components/status.jsx:1218 msgid "Pin to profile" msgstr "" -#: src/components/status.jsx:1227 +#: src/components/status.jsx:1247 msgid "Delete this post?" msgstr "" -#: src/components/status.jsx:1240 +#: src/components/status.jsx:1260 msgid "Post deleted" msgstr "" -#: src/components/status.jsx:1243 +#: src/components/status.jsx:1263 msgid "Unable to delete post" msgstr "" -#: src/components/status.jsx:1271 +#: src/components/status.jsx:1291 msgid "Report post…" msgstr "" -#: src/components/status.jsx:1627 -#: src/components/status.jsx:1663 -#: src/components/status.jsx:2343 +#: src/components/status.jsx:1647 +#: src/components/status.jsx:1683 +#: src/components/status.jsx:2363 msgid "Liked" msgstr "" -#: src/components/status.jsx:1660 -#: src/components/status.jsx:2330 +#: src/components/status.jsx:1680 +#: src/components/status.jsx:2350 msgid "Boosted" msgstr "" -#: src/components/status.jsx:1670 -#: src/components/status.jsx:2355 +#: src/components/status.jsx:1690 +#: src/components/status.jsx:2375 msgid "Bookmarked" msgstr "" -#: src/components/status.jsx:1674 +#: src/components/status.jsx:1694 msgid "Pinned" msgstr "" -#: src/components/status.jsx:1719 -#: src/components/status.jsx:2174 +#: src/components/status.jsx:1739 +#: src/components/status.jsx:2194 msgid "Deleted" msgstr "" -#: src/components/status.jsx:1760 +#: src/components/status.jsx:1780 msgid "{repliesCount, plural, one {# reply} other {# replies}}" msgstr "" -#: src/components/status.jsx:1849 +#: src/components/status.jsx:1869 msgid "Thread{0}" msgstr "" -#: src/components/status.jsx:1925 -#: src/components/status.jsx:1987 -#: src/components/status.jsx:2072 +#: src/components/status.jsx:1945 +#: src/components/status.jsx:2007 +#: src/components/status.jsx:2092 msgid "Show less" msgstr "" -#: src/components/status.jsx:1925 -#: src/components/status.jsx:1987 +#: src/components/status.jsx:1945 +#: src/components/status.jsx:2007 msgid "Show content" msgstr "" -#: src/components/status.jsx:2072 +#: src/components/status.jsx:2092 msgid "Show media" msgstr "" -#: src/components/status.jsx:2195 +#: src/components/status.jsx:2215 msgid "Edited" msgstr "" -#: src/components/status.jsx:2272 +#: src/components/status.jsx:2292 msgid "Comments" msgstr "" #. More from [Author] -#: src/components/status.jsx:2581 +#: src/components/status.jsx:2601 msgid "More from <0/>" msgstr "More from <0/>" -#: src/components/status.jsx:2880 +#: src/components/status.jsx:2900 msgid "Edit History" msgstr "" -#: src/components/status.jsx:2884 +#: src/components/status.jsx:2904 msgid "Failed to load history" msgstr "" -#: src/components/status.jsx:2889 +#: src/components/status.jsx:2909 msgid "Loading…" msgstr "" -#: src/components/status.jsx:3124 +#: src/components/status.jsx:3144 msgid "HTML Code" msgstr "" -#: src/components/status.jsx:3141 +#: src/components/status.jsx:3161 msgid "HTML code copied" msgstr "" -#: src/components/status.jsx:3144 +#: src/components/status.jsx:3164 msgid "Unable to copy HTML code" msgstr "" -#: src/components/status.jsx:3156 +#: src/components/status.jsx:3176 msgid "Media attachments:" msgstr "" -#: src/components/status.jsx:3178 +#: src/components/status.jsx:3198 msgid "Account Emojis:" msgstr "" -#: src/components/status.jsx:3209 -#: src/components/status.jsx:3254 +#: src/components/status.jsx:3229 +#: src/components/status.jsx:3274 msgid "static URL" msgstr "" -#: src/components/status.jsx:3223 +#: src/components/status.jsx:3243 msgid "Emojis:" msgstr "" -#: src/components/status.jsx:3268 +#: src/components/status.jsx:3288 msgid "Notes:" 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." msgstr "" -#: src/components/status.jsx:3278 +#: src/components/status.jsx:3298 msgid "Polls are not interactive, becomes a list with vote counts." msgstr "" -#: src/components/status.jsx:3283 +#: src/components/status.jsx:3303 msgid "Media attachments can be images, videos, audios or any file types." msgstr "" -#: src/components/status.jsx:3289 +#: src/components/status.jsx:3309 msgid "Post could be edited or deleted later." msgstr "" -#: src/components/status.jsx:3295 +#: src/components/status.jsx:3315 msgid "Preview" msgstr "" -#: src/components/status.jsx:3304 +#: src/components/status.jsx:3324 msgid "Note: This preview is lightly styled." msgstr "" #. [Name] [Visibility icon] boosted -#: src/components/status.jsx:3546 +#: src/components/status.jsx:3566 msgid "<0/> <1/> boosted" msgstr "" diff --git a/src/utils/enhance-content.js b/src/utils/enhance-content.js index 4a30481e..6796341d 100644 --- a/src/utils/enhance-content.js +++ b/src/utils/enhance-content.js @@ -4,53 +4,85 @@ import mem from './mem'; const fauxDiv = document.createElement('div'); const whitelistLinkClasses = ['u-url', 'mention', 'hashtag']; +const HTML_CHARS_REGEX = /[&<>]/g; +function escapeHTML(html) { + return html.replace( + HTML_CHARS_REGEX, + (c) => + ({ + '&': '&', + '<': '<', + '>': '>', + }[c]), + ); +} + +const LINK_REGEX = / {} } = opts; + const { emojis, returnDOM, postEnhanceDOM = () => {} } = opts; let enhancedContent = content; - const dom = document.createElement('div'); - dom.innerHTML = enhancedContent; - const hasLink = / { + for (const link of noTargetBlankLinks) { link.setAttribute('target', '_blank'); - }); + } // Remove all classes except `u-url`, `mention`, `hashtag` const links = dom.querySelectorAll('a[class]'); - links.forEach((link) => { - link.classList.forEach((c) => { + for (const link of links) { + for (const c of link.classList) { if (!whitelistLinkClasses.includes(c)) { link.classList.remove(c); } - }); - }); + } + } } // Add 'has-url-text' to all links that contains a url if (hasLink) { const links = dom.querySelectorAll('a[href]'); - links.forEach((link) => { - if (/^https?:\/\//i.test(link.textContent.trim())) { + for (const link of links) { + if (HTTP_LINK_REGEX.test(link.textContent.trim())) { link.classList.add('has-url-text'); shortenLink(link); } - }); + } } // Spanify un-spanned mentions if (hasLink) { const links = dom.querySelectorAll('a[href]'); const usernames = []; - links.forEach((link) => { + for (const link of links) { const text = link.innerText.trim(); const hasChildren = link.querySelector('*'); // If text looks like @username@domain, then it's a mention - if (/^@[^@]+(@[^@]+)?$/g.test(text)) { + if (MENTION_REGEX.test(text)) { // Only show @username const [_, username, domain] = text.split('@'); if (!hasChildren) { @@ -67,11 +99,11 @@ function _enhanceContent(content, opts = {}) { link.classList.add('mention'); } // If text looks like #hashtag, then it's a hashtag - if (/^#[^#]+$/g.test(text)) { + if (HASHTAG_REGEX.test(text)) { if (!hasChildren) link.innerHTML = `#${text.slice(1)}`; link.classList.add('mention', 'hashtag'); } - }); + } } // EMOJIS @@ -80,18 +112,14 @@ function _enhanceContent(content, opts = {}) { let textNodes; if (enhancedContent.includes(':')) { textNodes = extractTextNodes(dom); - textNodes.forEach((node) => { - let html = node.nodeValue - .replace(/&/g, '&') - .replace(//g, '>'); + for (const node of textNodes) { + let html = escapeHTML(node.nodeValue); if (emojis) { html = emojifyText(html, emojis); } fauxDiv.innerHTML = html; - // const nodes = [...fauxDiv.childNodes]; node.replaceWith(...fauxDiv.childNodes); - }); + } } // CODE BLOCKS @@ -99,31 +127,35 @@ function _enhanceContent(content, opts = {}) { // Convert ```code``` to
code
if (hasCodeBlock) { 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'); // Replace
with newlines - block.querySelectorAll('br').forEach((br) => br.replaceWith('\n')); + for (const br of block.querySelectorAll('br')) { + br.replaceWith('\n'); + } pre.innerHTML = `${block.innerHTML.trim()}`; block.replaceWith(pre); - }); + } } // Convert multi-paragraph code blocks to
code
if (hasCodeBlock) { const paragraphs = [...dom.querySelectorAll('p')]; // 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 - codeBlocks.forEach((block) => { + for (const block of codeBlocks) { const nextParagraphs = [block]; let hasCodeBlock = false; let currentBlock = block; while (currentBlock.nextElementSibling) { const next = currentBlock.nextElementSibling; if (next && next.tagName === 'P') { - if (/```$/g.test(next.innerText)) { + if (CODE_BLOCK_END_REGEX.test(next.innerText)) { nextParagraphs.push(next); hasCodeBlock = true; break; @@ -137,16 +169,20 @@ function _enhanceContent(content, opts = {}) { } if (hasCodeBlock) { const pre = document.createElement('pre'); - nextParagraphs.forEach((p) => { + for (const p of nextParagraphs) { // Replace
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'); pre.innerHTML = `${codeText}`; block.replaceWith(pre); - nextParagraphs.forEach((p) => p.remove()); + for (const p of nextParagraphs) { + p.remove(); + } } - }); + } } // INLINE CODE @@ -154,42 +190,36 @@ function _enhanceContent(content, opts = {}) { // Convert `code` to code if (enhancedContent.includes('`')) { textNodes = extractTextNodes(dom); - textNodes.forEach((node) => { - let html = node.nodeValue - .replace(/&/g, '&') - .replace(//g, '>'); - if (/`[^`]+`/g.test(html)) { + for (const node of textNodes) { + let html = escapeHTML(node.nodeValue); + if (INLINE_CODE_REGEX.test(html)) { html = html.replaceAll(/(`[^]+?`)/g, '$1'); } fauxDiv.innerHTML = html; // const nodes = [...fauxDiv.childNodes]; node.replaceWith(...fauxDiv.childNodes); - }); + } } // TWITTER USERNAMES // ================= // Convert @username@twitter.com to
@username@twitter.com - if (/twitter\.com/i.test(enhancedContent)) { + if (TWITTER_DOMAIN_REGEX.test(enhancedContent)) { textNodes = extractTextNodes(dom, { rejectFilter: ['A'], }); - textNodes.forEach((node) => { - let html = node.nodeValue - .replace(/&/g, '&') - .replace(//g, '>'); - if (/@[a-zA-Z0-9_]+@twitter\.com/g.test(html)) { + for (const node of textNodes) { + let html = escapeHTML(node.nodeValue); + if (TWITTER_MENTION_REGEX.test(html)) { html = html.replaceAll( - /(@([a-zA-Z0-9_]+)@twitter\.com)/g, + TWITTER_MENTION_CAPTURE_REGEX, '$1', ); } fauxDiv.innerHTML = html; // const nodes = [...fauxDiv.childNodes]; node.replaceWith(...fauxDiv.childNodes); - }); + } } // HASHTAG STUFFING @@ -235,22 +265,24 @@ function _enhanceContent(content, opts = {}) { }, ); if (hashtagStuffedParagraphs?.length) { - hashtagStuffedParagraphs.forEach((p) => { + for (const p of hashtagStuffedParagraphs) { p.classList.add('hashtag-stuffing'); p.title = p.innerText; - }); + } } } // ADD ASPECT RATIO TO ALL IMAGES if (enhancedContent.includes(' { + 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 height = img.getAttribute('height') || img.naturalHeight; if (width && height) { img.style.setProperty('--original-aspect-ratio', `${width}/${height}`); } - }); + } } if (postEnhanceDOM) { @@ -258,9 +290,7 @@ function _enhanceContent(content, opts = {}) { // postEnhanceDOM(dom); // mutate dom } - enhancedContent = dom.innerHTML; - - return enhancedContent; + return returnDOM ? dom : dom.innerHTML; } const enhanceContent = mem(_enhanceContent); diff --git a/src/utils/mem.js b/src/utils/mem.js index 620ab6df..c4fcb1b8 100644 --- a/src/utils/mem.js +++ b/src/utils/mem.js @@ -3,5 +3,9 @@ import moize from 'moize'; window._moize = moize; export default function mem(fn, opts = {}) { - return moize(fn, { ...opts, maxSize: 50, isDeepEqual: true }); + return moize(fn, { + ...opts, + maxSize: 30, + isDeepEqual: true, + }); }