1
0
Fork 0

Compare commits

...

10 commits

Author SHA1 Message Date
Lim Chee Aun f21a65da9a Micro optimizations 2023-12-29 11:27:01 +08:00
Lim Chee Aun a97478097b Queue all the microtasks 2023-12-29 08:25:58 +08:00
Lim Chee Aun 71d2db31e0 Fix undefined sKey 2023-12-29 08:25:41 +08:00
Lim Chee Aun 88547fa403 Fix slow code blocking whole component render 2023-12-28 18:39:56 +08:00
Lim Chee Aun 1765defa56 Remove dup regex, add another GTS url pattern 2023-12-28 15:42:27 +08:00
Lim Chee Aun 437d721c26 Safari needs this on every element 2023-12-28 15:23:47 +08:00
Lim Chee Aun e13a2feec8 Prioritise local instance unfurl over remote 2023-12-28 11:58:50 +08:00
Lim Chee Aun 39bcb01894 Differentiate icon for group vs local 2023-12-28 11:57:48 +08:00
Lim Chee Aun 7fb0044471 More queueMicrotask 2023-12-28 10:50:54 +08:00
Lim Chee Aun f645815b84 Add small note on usage 2023-12-28 08:29:12 +08:00
11 changed files with 133 additions and 105 deletions

View file

@ -62,29 +62,32 @@ function Avatar({ url, size, alt = '', squircle, ...props }) {
if (avatarRef.current) avatarRef.current.dataset.loaded = true;
if (alphaCache[url] !== undefined) return;
if (isMissing) return;
try {
// Check if image has alpha channel
const { width, height } = e.target;
if (canvas.width !== width) canvas.width = width;
if (canvas.height !== height) canvas.height = height;
ctx.drawImage(e.target, 0, 0);
const allPixels = ctx.getImageData(0, 0, width, height);
// At least 10% of pixels have alpha <= 128
const hasAlpha =
allPixels.data.filter((pixel, i) => i % 4 === 3 && pixel <= 128)
.length /
(allPixels.data.length / 4) >
0.1;
if (hasAlpha) {
// console.log('hasAlpha', hasAlpha, allPixels.data);
avatarRef.current.classList.add('has-alpha');
queueMicrotask(() => {
try {
// Check if image has alpha channel
const { width, height } = e.target;
if (canvas.width !== width) canvas.width = width;
if (canvas.height !== height) canvas.height = height;
ctx.drawImage(e.target, 0, 0);
const allPixels = ctx.getImageData(0, 0, width, height);
// At least 10% of pixels have alpha <= 128
const hasAlpha =
allPixels.data.filter(
(pixel, i) => i % 4 === 3 && pixel <= 128,
).length /
(allPixels.data.length / 4) >
0.1;
if (hasAlpha) {
// console.log('hasAlpha', hasAlpha, allPixels.data);
avatarRef.current.classList.add('has-alpha');
}
alphaCache[url] = hasAlpha;
ctx.clearRect(0, 0, width, height);
} catch (e) {
// Silent fail
alphaCache[url] = false;
}
alphaCache[url] = hasAlpha;
ctx.clearRect(0, 0, width, height);
} catch (e) {
// Silent fail
alphaCache[url] = false;
}
});
}}
/>
)}

View file

@ -105,6 +105,7 @@ export const ICONS = {
month: () => import('@iconify-icons/mingcute/calendar-month-line'),
media: () => import('@iconify-icons/mingcute/photo-album-line'),
speak: () => import('@iconify-icons/mingcute/radar-line'),
building: () => import('@iconify-icons/mingcute/building-5-line'),
};
const ICONDATA = {};

View file

@ -202,7 +202,7 @@ function NavMenu(props) {
<Icon icon="search" size="l" /> <span>Search</span>
</MenuLink>
<MenuLink to={`/${instance}/p/l`}>
<Icon icon="group" size="l" /> <span>Local</span>
<Icon icon="building" size="l" /> <span>Local</span>
</MenuLink>
<MenuLink to={`/${instance}/p`}>
<Icon icon="earth" size="l" /> <span>Federated</span>

View file

@ -159,7 +159,7 @@ export const SHORTCUTS_META = {
title: ({ local }) => (local ? 'Local' : 'Federated'),
subtitle: ({ instance }) => instance || api().instance,
path: ({ local, instance }) => `/${instance}/p${local ? '/l' : ''}`,
icon: ({ local }) => (local ? 'group' : 'earth'),
icon: ({ local }) => (local ? 'building' : 'earth'),
},
trending: {
id: 'trending',

View file

@ -554,6 +554,13 @@
text-rendering: optimizeSpeed;
pointer-events: none;
user-select: none;
* {
text-decoration-color: inherit;
text-decoration-thickness: 1.5em;
text-decoration-line: line-through;
text-rendering: optimizeSpeed;
}
}
}

View file

@ -149,7 +149,7 @@ function Status({
const { instance: currentInstance } = api();
const sameInstance = instance === currentInstance;
let sKey = statusKey(statusID, instance);
let sKey = statusKey(statusID || status?.id, instance);
const snapStates = useSnapshot(states);
if (!status) {
status = snapStates.statuses[sKey] || snapStates.statuses[statusID];
@ -2364,9 +2364,18 @@ function _unfurlMastodonLink(instance, url) {
}
if (remoteInstanceFetch) {
return Promise.any([remoteInstanceFetch, mastoSearchFetch])
.then(handleFulfill)
.catch(handleCatch);
// return Promise.any([remoteInstanceFetch, mastoSearchFetch])
// .then(handleFulfill)
// .catch(handleCatch);
// If mastoSearchFetch is fulfilled within 3s, return it, else return remoteInstanceFetch
const finalPromise = Promise.race([
mastoSearchFetch,
new Promise((resolve, reject) => setTimeout(reject, 3000)),
]).catch(() => {
// If remoteInstanceFetch is fullfilled, return it, else return mastoSearchFetch
return remoteInstanceFetch.catch(() => mastoSearchFetch);
});
return finalPromise.then(handleFulfill).catch(handleCatch);
} else {
return mastoSearchFetch.then(handleFulfill).catch(handleCatch);
}

View file

@ -446,6 +446,9 @@ function Settings({ onClose }) {
Image description generator{' '}
<Icon icon="sparkles2" class="more-insignificant" />
</label>
<div class="sub-section insignificant">
<small>Only for new images while composing new posts.</small>
</div>
<div class="sub-section insignificant">
<small>
Note: This feature uses external AI service, powered by{' '}

View file

@ -10,22 +10,20 @@ function _enhanceContent(content, opts = {}) {
const dom = document.createElement('div');
dom.innerHTML = enhancedContent;
const hasLink = /<a/i.test(enhancedContent);
const hasCodeBlock = enhancedContent.indexOf('```') !== -1;
const hasCodeBlock = enhancedContent.includes('```');
if (hasLink) {
// Add target="_blank" to all links with no target="_blank"
// E.g. `note` in `account`
const noTargetBlankLinks = Array.from(
dom.querySelectorAll('a:not([target="_blank"])'),
);
const noTargetBlankLinks = dom.querySelectorAll('a:not([target="_blank"])');
noTargetBlankLinks.forEach((link) => {
link.setAttribute('target', '_blank');
});
// Remove all classes except `u-url`, `mention`, `hashtag`
const links = Array.from(dom.querySelectorAll('a[class]'));
const links = dom.querySelectorAll('a[class]');
links.forEach((link) => {
Array.from(link.classList).forEach((c) => {
link.classList.forEach((c) => {
if (!whitelistLinkClasses.includes(c)) {
link.classList.remove(c);
}
@ -35,7 +33,7 @@ function _enhanceContent(content, opts = {}) {
// Add 'has-url-text' to all links that contains a url
if (hasLink) {
const links = Array.from(dom.querySelectorAll('a[href]'));
const links = dom.querySelectorAll('a[href]');
links.forEach((link) => {
if (/^https?:\/\//i.test(link.textContent.trim())) {
link.classList.add('has-url-text');
@ -45,7 +43,7 @@ function _enhanceContent(content, opts = {}) {
// Spanify un-spanned mentions
if (hasLink) {
const links = Array.from(dom.querySelectorAll('a[href]'));
const links = dom.querySelectorAll('a[href]');
const usernames = [];
links.forEach((link) => {
const text = link.innerText.trim();
@ -56,8 +54,8 @@ function _enhanceContent(content, opts = {}) {
const [_, username, domain] = text.split('@');
if (!hasChildren) {
if (
!usernames.find(([u]) => u === username) ||
usernames.find(([u, d]) => u === username && d === domain)
!usernames.some(([u]) => u === username) ||
usernames.some(([u, d]) => u === username && d === domain)
) {
link.innerHTML = `@<span>${username}</span>`;
usernames.push([username, domain]);
@ -79,7 +77,7 @@ function _enhanceContent(content, opts = {}) {
// ======
// Convert :shortcode: to <img />
let textNodes;
if (enhancedContent.indexOf(':') !== -1) {
if (enhancedContent.includes(':')) {
textNodes = extractTextNodes(dom);
textNodes.forEach((node) => {
let html = node.nodeValue
@ -90,8 +88,8 @@ function _enhanceContent(content, opts = {}) {
html = emojifyText(html, emojis);
}
fauxDiv.innerHTML = html;
const nodes = Array.from(fauxDiv.childNodes);
node.replaceWith(...nodes);
// const nodes = [...fauxDiv.childNodes];
node.replaceWith(...fauxDiv.childNodes);
});
}
@ -99,7 +97,7 @@ function _enhanceContent(content, opts = {}) {
// ===========
// Convert ```code``` to <pre><code>code</code></pre>
if (hasCodeBlock) {
const blocks = Array.from(dom.querySelectorAll('p')).filter((p) =>
const blocks = [...dom.querySelectorAll('p')].filter((p) =>
/^```[^]+```$/g.test(p.innerText.trim()),
);
blocks.forEach((block) => {
@ -113,7 +111,7 @@ function _enhanceContent(content, opts = {}) {
// Convert multi-paragraph code blocks to <pre><code>code</code></pre>
if (hasCodeBlock) {
const paragraphs = Array.from(dom.querySelectorAll('p'));
const paragraphs = [...dom.querySelectorAll('p')];
// Filter out paragraphs with ``` in beginning only
const codeBlocks = paragraphs.filter((p) => /^```/g.test(p.innerText));
// For each codeBlocks, get all paragraphs until the last paragraph with ``` at the end only
@ -153,7 +151,7 @@ function _enhanceContent(content, opts = {}) {
// INLINE CODE
// ===========
// Convert `code` to <code>code</code>
if (enhancedContent.indexOf('`') !== -1) {
if (enhancedContent.includes('`')) {
textNodes = extractTextNodes(dom);
textNodes.forEach((node) => {
let html = node.nodeValue
@ -164,8 +162,8 @@ function _enhanceContent(content, opts = {}) {
html = html.replaceAll(/(`[^]+?`)/g, '<code>$1</code>');
}
fauxDiv.innerHTML = html;
const nodes = Array.from(fauxDiv.childNodes);
node.replaceWith(...nodes);
// const nodes = [...fauxDiv.childNodes];
node.replaceWith(...fauxDiv.childNodes);
});
}
@ -188,53 +186,53 @@ function _enhanceContent(content, opts = {}) {
);
}
fauxDiv.innerHTML = html;
const nodes = Array.from(fauxDiv.childNodes);
node.replaceWith(...nodes);
// const nodes = [...fauxDiv.childNodes];
node.replaceWith(...fauxDiv.childNodes);
});
}
// HASHTAG STUFFING
// ================
// Get the <p> that contains a lot of hashtags, add a class to it
if (enhancedContent.indexOf('#') !== -1) {
if (enhancedContent.includes('#')) {
let prevIndex = null;
const hashtagStuffedParagraphs = Array.from(
dom.querySelectorAll('p'),
).filter((p, index) => {
let hashtagCount = 0;
for (let i = 0; i < p.childNodes.length; i++) {
const node = p.childNodes[i];
const hashtagStuffedParagraphs = [...dom.querySelectorAll('p')].filter(
(p, index) => {
let hashtagCount = 0;
for (let i = 0; i < p.childNodes.length; i++) {
const node = p.childNodes[i];
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent.trim();
if (text !== '') {
return false;
}
} else if (node.tagName === 'BR') {
// Ignore <br />
} else if (node.tagName === 'A') {
const linkText = node.textContent.trim();
if (!linkText || !linkText.startsWith('#')) {
return false;
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent.trim();
if (text !== '') {
return false;
}
} else if (node.tagName === 'BR') {
// Ignore <br />
} else if (node.tagName === 'A') {
const linkText = node.textContent.trim();
if (!linkText || !linkText.startsWith('#')) {
return false;
} else {
hashtagCount++;
}
} else {
hashtagCount++;
return false;
}
} else {
return false;
}
}
// Only consider "stuffing" if:
// - there are more than 3 hashtags
// - there are more than 1 hashtag in adjacent paragraphs
if (hashtagCount > 3) {
prevIndex = index;
return true;
}
if (hashtagCount > 1 && prevIndex && index === prevIndex + 1) {
prevIndex = index;
return true;
}
});
// Only consider "stuffing" if:
// - there are more than 3 hashtags
// - there are more than 1 hashtag in adjacent paragraphs
if (hashtagCount > 3) {
prevIndex = index;
return true;
}
if (hashtagCount > 1 && prevIndex && index === prevIndex + 1) {
prevIndex = index;
return true;
}
},
);
if (hashtagStuffedParagraphs?.length) {
hashtagStuffedParagraphs.forEach((p) => {
p.classList.add('hashtag-stuffing');
@ -291,18 +289,20 @@ const defaultRejectFilterMap = Object.fromEntries(
);
function extractTextNodes(dom, opts = {}) {
const textNodes = [];
const rejectFilterMap = Object.assign(
{},
defaultRejectFilterMap,
opts.rejectFilter?.reduce((acc, cur) => {
acc[cur] = true;
return acc;
}, {}),
);
const walk = document.createTreeWalker(
dom,
NodeFilter.SHOW_TEXT,
{
acceptNode(node) {
if (defaultRejectFilterMap[node.parentNode.nodeName]) {
return NodeFilter.FILTER_REJECT;
}
if (
opts.rejectFilter &&
opts.rejectFilter.includes(node.parentNode.nodeName)
) {
if (rejectFilterMap[node.parentNode.nodeName]) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;

View file

@ -8,6 +8,12 @@ const locales = [
...navigator.languages,
];
const localeTargetLanguages = localeMatch(
locales,
translationTargetLanguages.map((l) => l.code.replace('_', '-')), // The underscore will fail Intl.Locale inside `match`
'en',
);
function getTranslateTargetLanguage(fromSettings = false) {
if (fromSettings) {
const { contentTranslationTargetLanguage } = states.settings;
@ -15,11 +21,7 @@ function getTranslateTargetLanguage(fromSettings = false) {
return contentTranslationTargetLanguage;
}
}
return localeMatch(
locales,
translationTargetLanguages.map((l) => l.code.replace('_', '-')), // The underscore will fail Intl.Locale inside `match`
'en',
);
return localeTargetLanguages;
}
export default getTranslateTargetLanguage;

View file

@ -3,9 +3,8 @@ export default function isMastodonLinkMaybe(url) {
const { pathname, hash } = new URL(url);
return (
/^\/.*\/\d+$/i.test(pathname) ||
/^\/@[^/]+\/(statuses|posts)\/\w+\/?$/i.test(pathname) || // GoToSocial, Takahe
/^\/(@[^/]+|users\/[^/]+)\/(statuses|posts)\/\w+\/?$/i.test(pathname) || // GoToSocial, Takahe
/^\/notes\/[a-z0-9]+$/i.test(pathname) || // Misskey, Firefish
/^\/notes\/[a-z0-9]+$/i.test(pathname) || // Misskey, Calckey
/^\/(notice|objects)\/[a-z0-9-]+$/i.test(pathname) || // Pleroma
/#\/[^\/]+\.[^\/]+\/s\/.+/i.test(hash) // Phanpy 🫣
);

View file

@ -175,21 +175,25 @@ export function saveStatus(status, instance, opts) {
if (!status) return;
const oldStatus = getStatus(status.id, instance);
if (!override && oldStatus) return;
const key = statusKey(status.id, instance);
if (oldStatus?._pinned) status._pinned = oldStatus._pinned;
// if (oldStatus?._filtered) status._filtered = oldStatus._filtered;
states.statuses[key] = status;
if (status.reblog) {
const key = statusKey(status.reblog.id, instance);
states.statuses[key] = status.reblog;
}
queueMicrotask(() => {
const key = statusKey(status.id, instance);
if (oldStatus?._pinned) status._pinned = oldStatus._pinned;
// if (oldStatus?._filtered) status._filtered = oldStatus._filtered;
states.statuses[key] = status;
if (status.reblog) {
const key = statusKey(status.reblog.id, instance);
states.statuses[key] = status.reblog;
}
});
// THREAD TRAVERSER
if (!skipThreading) {
queueMicrotask(() => {
threadifyStatus(status, instance);
if (status.reblog) {
threadifyStatus(status.reblog, instance);
queueMicrotask(() => {
threadifyStatus(status.reblog, instance);
});
}
});
}