Merge pull request #1214 from cheeaun/main

Update from main
production
Chee Aun 2025-07-18 19:50:38 +08:00 zatwierdzone przez GitHub
commit 3f4b1a6394
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
58 zmienionych plików z 9707 dodań i 8790 usunięć

Wyświetl plik

@ -13,4 +13,4 @@ jobs:
with:
node-version: 20
- run: npm ci
- run: npx biome check
- run: npm run formatting-check

Wyświetl plik

@ -22,7 +22,7 @@ jobs:
- name: Install Playwright Browsers
run: npx playwright install --with-deps webkit --only-shell
- name: Run Playwright tests
run: npx playwright test
run: npm run test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:

Wyświetl plik

@ -392,6 +392,7 @@ Costs involved in running and developing this web app:
- <img src="https://crowdin-static.cf-downloads.crowdin.com/avatar/16538605/medium/bcdb6e3286b7d6237923f3a9383eed29.png" alt="" width="16" height="16" /> SadmL (Russian)
- <img src="https://crowdin-static.cf-downloads.crowdin.com/avatar/16539171/medium/0ce95ef6b3b0566136191fbedc1563d0.png" alt="" width="16" height="16" /> SadmL_AI (Russian)
- <img src="https://crowdin-static.cf-downloads.crowdin.com/avatar/16121928/medium/b1dd34dc3e93b64b93b94aedca0c5b7d.jpg" alt="" width="16" height="16" /> Schishka71 (Russian)
- <img src="https://crowdin-static.cf-downloads.crowdin.com/avatar/17206524/medium/1b0a8f9eafe7326be6968c6aed14c872.png" alt="" width="16" height="16" /> seizeheures (Esperanto)
- <img src="https://crowdin-static.cf-downloads.crowdin.com/avatar/12381015/medium/35e3557fd61d85f9a5b84545d9e3feb4.png" alt="" width="16" height="16" /> shuuji3 (Japanese)
- <img src="https://crowdin-static.cf-downloads.crowdin.com/avatar/14565190/medium/79100599131b7776e9803e4b696915a3_default.png" alt="" width="16" height="16" /> Sky_NiniKo (French)
- <img src="https://crowdin-static.cf-downloads.crowdin.com/avatar/13143526/medium/30871da23d51d7e41bb02f3c92d7f104.png" alt="" width="16" height="16" /> Steffo99 (Italian)
@ -400,6 +401,7 @@ Costs involved in running and developing this web app:
- <img src="https://crowdin-static.cf-downloads.crowdin.com/avatar/16530049/medium/683f3581620c6b4a5c753b416ed695a7.jpeg" alt="" width="16" height="16" /> tferrermo (Spanish)
- <img src="https://crowdin-static.cf-downloads.crowdin.com/avatar/15752199/medium/7e9efd828c4691368d063b19d19eb894.png" alt="" width="16" height="16" /> tkbremnes (Norwegian Bokmal)
- <img src="https://crowdin-static.cf-downloads.crowdin.com/avatar/16527851/medium/649e5a9a8a8cc61ced670d89e9cca082.png" alt="" width="16" height="16" /> tux93 (German)
- <img src="https://crowdin-static.cf-downloads.crowdin.com/avatar/16236470/medium/315b1ebbd38e0f7e41d44bee752afa33.jpg" alt="" width="16" height="16" /> Usia (Ukrainian)
- <img src="https://crowdin-static.cf-downloads.crowdin.com/avatar/16791511/medium/321c72613cd27efc3005e7c3bf383578.jpeg" alt="" width="16" height="16" /> uzaylul (Turkish)
- <img src="https://crowdin-static.cf-downloads.crowdin.com/avatar/14427566/medium/ab733b5044c21867fc5a9d1b22cd2c03.png" alt="" width="16" height="16" /> Vac31. (Lithuanian)
- <img src="https://crowdin-static.cf-downloads.crowdin.com/avatar/16026914/medium/4f2a96210b76cbc330584cfdd01fabc4_default.png" alt="" width="16" height="16" /> valtlai (Finnish)

Wyświetl plik

@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
"$schema": "https://biomejs.dev/schemas/2.1.2/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",

679
package-lock.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -13,7 +13,11 @@
"messages:extract:clean": "lingui extract --locale en --clean",
"messages:compile": "lingui compile",
"fetch-i18n-volunteers": "env $(cat .env.local | grep -v \"#\" | xargs) node scripts/fetch-i18n-volunteers.js",
"readme:i18n-volunteers": "node scripts/update-i18n-volunteers-readme.js"
"readme:i18n-volunteers": "node scripts/update-i18n-volunteers-readme.js",
"test": "playwright test",
"test:ui": "playwright test --ui",
"test:headed": "playwright test --headed",
"formatting-check": "npx biome check"
},
"dependencies": {
"@formatjs/intl-localematcher": "~0.6.1",
@ -22,9 +26,9 @@
"@github/text-expander-element": "~2.9.2",
"@iconify-icons/mingcute": "~1.2.9",
"@justinribeiro/lite-youtube": "~1.8.2",
"@lingui/detect-locale": "~5.3.2",
"@lingui/macro": "~5.3.2",
"@lingui/react": "~5.3.2",
"@lingui/detect-locale": "~5.3.3",
"@lingui/macro": "~5.3.3",
"@lingui/react": "~5.3.3",
"@szhsin/react-menu": "~4.4.1",
"chroma-js": "~3.1.2",
"compare-versions": "~6.1.1",
@ -37,11 +41,11 @@
"js-cookie": "~3.0.5",
"just-debounce-it": "~3.2.0",
"lz-string": "~1.5.0",
"masto": "~7.1.0",
"masto": "~7.2.0",
"moize": "~6.1.6",
"p-retry": "~6.2.1",
"p-throttle": "~7.0.0",
"preact": "10.26.8",
"preact": "10.26.9",
"punycode": "~2.3.1",
"react-hotkeys-hook": "~5.1.0",
"react-intersection-observer": "~9.16.0",
@ -49,6 +53,7 @@
"react-router-dom": "6.6.2",
"string-length": "6.0.0",
"swiped-events": "~1.2.0",
"temml": "~0.11.9",
"tinyld": "~1.3.4",
"toastify-js": "~1.12.0",
"uid": "~2.0.2",
@ -58,22 +63,22 @@
"valtio": "2.1.5"
},
"devDependencies": {
"@biomejs/biome": "2.0.0",
"@lingui/babel-plugin-lingui-macro": "~5.3.2",
"@lingui/cli": "~5.3.2",
"@lingui/vite-plugin": "~5.3.2",
"@playwright/test": "~1.53.0",
"@preact/preset-vite": "~2.10.1",
"@types/node": "~24.0.2",
"postcss": "~8.5.5",
"@biomejs/biome": "2.1.2",
"@lingui/babel-plugin-lingui-macro": "~5.3.3",
"@lingui/cli": "~5.3.3",
"@lingui/vite-plugin": "~5.3.3",
"@playwright/test": "~1.54.1",
"@preact/preset-vite": "~2.10.2",
"@types/node": "~24.0.14",
"postcss": "~8.5.6",
"postcss-dark-theme-class": "~1.3.0",
"postcss-preset-env": "~10.2.3",
"sonda": "~0.8.0",
"postcss-preset-env": "~10.2.4",
"sonda": "~0.9.0",
"twitter-text": "~3.1.0",
"vite": "~6.3.5",
"vite": "~7.0.5",
"vite-plugin-generate-file": "~0.3.1",
"vite-plugin-html-config": "~2.0.2",
"vite-plugin-pwa": "~1.0.0",
"vite-plugin-pwa": "~1.0.1",
"vite-plugin-remove-console": "~2.2.0",
"vite-plugin-run": "~0.6.1",
"workbox-cacheable-response": "~7.3.0",

Wyświetl plik

@ -448,7 +448,8 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
var(--line-radius), var(--line-radius); */
--curves-radius: calc(var(--curves-width) / 2);
height: calc(var(--curves-width) - var(--line-width));
background-image: radial-gradient(
background-image:
radial-gradient(
circle at bottom var(--forward),
transparent calc(var(--curves-radius) - var(--line-width)),
var(--comment-line-color) calc(var(--curves-radius) - var(--line-width))
@ -479,7 +480,8 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
transparent
);
&.hero:not(:has(+ .thread), :first-child, :only-child, :last-child) {
background-image: linear-gradient(
background-image:
linear-gradient(
var(--line-dir),
transparent,
transparent var(--indent-small-start),
@ -1083,7 +1085,8 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
position: absolute;
inset: 0;
pointer-events: none;
background-image: radial-gradient(
background-image:
radial-gradient(
ellipse 50% 32px at bottom center,
var(--carousel-faded-color),
transparent
@ -2358,7 +2361,8 @@ body > .szh-menu-container {
var(--bg-color) var(--middle-circle-radius),
transparent var(--middle-circle-radius)
);
background-image: var(--middle-circle),
background-image:
var(--middle-circle),
conic-gradient(var(--color) var(--fill), var(--outline-color) 0);
transform: scale(0.7);
&:dir(rtl) {

Wyświetl plik

@ -183,4 +183,5 @@ export const ICONS = {
module: () => import('@iconify-icons/mingcute/user-star-line'),
rtl: true,
},
formula: () => import('@iconify-icons/mingcute/formula-line'),
};

Wyświetl plik

@ -845,11 +845,9 @@
overflow: hidden;
border: 1px solid var(--outline-color);
/* checkerboard background */
background-image: linear-gradient(
45deg,
var(--img-bg-color) 25%,
transparent 25%
), linear-gradient(-45deg, var(--img-bg-color) 25%, transparent 25%),
background-image:
linear-gradient(45deg, var(--img-bg-color) 25%, transparent 25%),
linear-gradient(-45deg, var(--img-bg-color) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--img-bg-color) 75%),
linear-gradient(-45deg, transparent 75%, var(--img-bg-color) 75%);
background-size: 10px 10px;

Wyświetl plik

@ -417,11 +417,9 @@
width: 80px;
height: 80px;
/* checkerboard background */
background-image: linear-gradient(
45deg,
var(--img-bg-color) 25%,
transparent 25%
), linear-gradient(-45deg, var(--img-bg-color) 25%, transparent 25%),
background-image:
linear-gradient(45deg, var(--img-bg-color) 25%, transparent 25%),
linear-gradient(-45deg, var(--img-bg-color) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--img-bg-color) 75%),
linear-gradient(-45deg, transparent 75%, var(--img-bg-color) 75%);
background-size: 10px 10px;
@ -687,11 +685,9 @@
overflow: hidden;
box-shadow: 0 2px 16px var(--img-bg-color);
/* checkerboard background */
background-image: linear-gradient(
45deg,
var(--img-bg-color) 25%,
transparent 25%
), linear-gradient(-45deg, var(--img-bg-color) 25%, transparent 25%),
background-image:
linear-gradient(45deg, var(--img-bg-color) 25%, transparent 25%),
linear-gradient(-45deg, var(--img-bg-color) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--img-bg-color) 75%),
linear-gradient(-45deg, transparent 75%, var(--img-bg-color) 75%);
background-size: 20px 20px;

Wyświetl plik

@ -2324,7 +2324,11 @@ const Textarea = forwardRef((props, ref) => {
// Get line before cursor position after pressing 'Enter'
const { key, target } = e;
const hasTextExpander = hasTextExpanderRef.current;
if (key === 'Enter' && !(e.ctrlKey || e.metaKey || hasTextExpander)) {
if (
key === 'Enter' &&
!(e.ctrlKey || e.metaKey || hasTextExpander) &&
!e.isComposing
) {
try {
const { value, selectionStart } = target;
const textBeforeCursor = value.slice(0, selectionStart);

Wyświetl plik

@ -289,11 +289,21 @@ function Media({
onClick(e);
} else {
e.preventDefault();
el.style.viewTransitionName = mediaVTN;
document.startViewTransition(() => {
el.style.viewTransitionName = '';
if (el.dataset.viewTransitioned) {
el.style.viewTransitionName = mediaVTN;
try {
document.startViewTransition(() => {
el.style.viewTransitionName = '';
location.hash = `#${to}`;
});
} catch (e) {
console.error(e);
el.style.viewTransitionName = '';
location.hash = `#${to}`;
}
} else {
location.hash = `#${to}`;
});
}
}
} else {
onClick?.(e);

Wyświetl plik

@ -51,7 +51,8 @@
}
}
.nav-menu section:last-child {
background-image: linear-gradient(
background-image:
linear-gradient(
var(--to-forward),
var(--divider-color) 1px,
transparent 1px

Wyświetl plik

@ -6,6 +6,7 @@ import { Fragment } from 'preact';
import { useMemo, useRef, useState } from 'preact/hooks';
import { api } from '../utils/api';
import localeMatch from '../utils/locale-match';
import showToast from '../utils/show-toast';
import { getCurrentInstance } from '../utils/store-utils';
@ -17,7 +18,7 @@ import Status from './status';
// NOTE: `dislike` hidden for now, it's actually not used for reporting
// Mastodon shows another screen for unfollowing, muting or blocking instead of reporting
const CATEGORIES = [/*'dislike'*/ , 'spam', 'legal', 'violation', 'other'];
const CATEGORIES = [/*'dislike' ,*/ 'spam', 'legal', 'violation', 'other'];
// `violation` will be set if there are `rule_ids[]`
const CATEGORIES_INFO = {
@ -45,16 +46,58 @@ const CATEGORIES_INFO = {
},
};
function findMatchingLanguage(rule, currentLang) {
if (!rule.translations || !currentLang) return null;
const availableLanguages = Object.keys(rule.translations);
if (!availableLanguages?.length) return null;
let matchedLang = localeMatch([currentLang], availableLanguages, null);
if (!matchedLang) {
// localeMatch fails if there are keys like zhCn, zhTw
// Convert them something like zh-CN first, try again
// Detect uppercase, then split by dash
const normalizedLanguages = availableLanguages.map((lang) => {
const parts = lang.split(/(?=[A-Z])/);
return parts
.map((part, i) => (i === 0 ? part : part.toLowerCase()))
.join('-');
});
matchedLang = localeMatch([currentLang], normalizedLanguages, null);
}
// If matchedLang has dash, convert back to original format
// E.g. zh-cn to zhCn
if (matchedLang && matchedLang.includes('-')) {
const [lang, region] = matchedLang.split('-');
matchedLang = lang + region.charAt(0).toUpperCase() + region.slice(1);
}
return matchedLang;
}
function translateRules(rules, currentLang) {
if (!rules?.length) return [];
if (!currentLang) return rules;
return rules.map((rule) => {
const matchedLang = findMatchingLanguage(rule, currentLang);
return {
...rule,
_translatedText: rule.translations?.[matchedLang]?.text || null,
};
});
}
function ReportModal({ account, post, onClose }) {
const { _, t } = useLingui();
const { _, t, i18n } = useLingui();
const { masto } = api();
const [uiState, setUIState] = useState('default');
const [username, domain] = account.acct.split('@');
const [rules, currentDomain] = useMemo(() => {
const [translatedRules, currentDomain] = useMemo(() => {
const { rules, domain } = getCurrentInstance();
return [rules || [], domain];
});
const rawRules = rules || [];
return [translateRules(rawRules, i18n.locale), domain];
}, [i18n.locale]);
const [selectedCategory, setSelectedCategory] = useState(null);
const [showRules, setShowRules] = useState(false);
@ -165,7 +208,7 @@ function ReportModal({ account, post, onClose }) {
</p>
<section class="report-categories">
{CATEGORIES.map((category) =>
category === 'violation' && !rules?.length ? null : (
category === 'violation' && !translatedRules?.length ? null : (
<Fragment key={category}>
<label class="report-category">
<input
@ -186,14 +229,14 @@ function ReportModal({ account, post, onClose }) {
</small>
</span>
</label>
{category === 'violation' && !!rules?.length && (
{category === 'violation' && !!translatedRules?.length && (
<div
class="shazam-container no-animation"
hidden={!showRules}
>
<div class="shazam-container-inner">
<div class="report-rules" ref={rulesRef}>
{rules.map((rule, i) => (
{translatedRules.map((rule, i) => (
<label class="report-rule" key={rule.id}>
<input
type="checkbox"
@ -216,7 +259,7 @@ function ReportModal({ account, post, onClose }) {
}
}}
/>
<span>{rule.text}</span>
<span>{rule._translatedText || rule.text}</span>
</label>
))}
</div>

Wyświetl plik

@ -280,6 +280,12 @@
.icon {
color: var(--text-insignificant-color);
}
&.status-card-ghost {
color: var(--text-insignificant-color);
border: var(--hairline-width) dashed var(--text-insignificant-color);
box-shadow: none;
}
}
@keyframes skeleton-breathe {
@ -967,6 +973,11 @@
> :is(div, blockquote) > :is(ul, ol) li > :is(ul, ol) {
padding-inline-start: 1.5em;
}
/* Hide inline quote (RE: [LINK]) when there's a native quote */
&:has(~ .quote-post-native) .quote-inline {
display: none;
}
}
.status .content ul {
list-style-type: disc;
@ -1407,11 +1418,8 @@ body:has(#modal-container .carousel) .status .media img:hover {
width: 100%;
height: 100%;
min-height: var(--min-dimension);
background-image: radial-gradient(
circle at center center,
transparent,
var(--bg-faded-color)
),
background-image:
radial-gradient(circle at center center, transparent, var(--bg-faded-color)),
repeating-radial-gradient(
circle at center center,
transparent,
@ -2933,3 +2941,51 @@ a.card:is(:hover, :focus):visited {
transition-duration: 0.1s;
transition-behavior: allow-discrete;
}
/* MATH */
.math-block {
color: var(--text-insignificant-color);
margin: 8px 0;
border-top: 1px dashed var(--outline-color);
padding-top: 8px;
display: flex;
align-items: center;
gap: 4px;
font-size: 90%;
}
.status .inner-content math[display='block'] {
background-image:
linear-gradient(
var(--divider-color) var(--hairline-width),
transparent var(--hairline-width)
),
linear-gradient(
to right,
var(--divider-color) var(--hairline-width),
transparent var(--hairline-width)
);
background-size: var(--text-size) var(--text-size);
overflow: auto;
--padding: 8px;
background-position: center var(--padding);
&:has(> mrow) {
/* 0.5ex is for mrow from Temml-Local.css */
background-position-y: calc(var(--padding) + 0.5ex);
}
padding: var(--padding);
/* mask the padding with gradients */
mask-image:
linear-gradient(to top, transparent, black var(--padding)),
linear-gradient(to bottom, transparent, black var(--padding)),
linear-gradient(to left, transparent, black var(--padding)),
linear-gradient(to right, transparent, black var(--padding));
mask-composite: intersect;
animation: appear-smooth 0.3s ease-out;
*:nth-child(even):not(:has(*)) {
/* sprinkle a bit more gradual magical feel */
animation: appear 1s ease-out;
}
}

Wyświetl plik

@ -1,4 +1,6 @@
import './status.css';
import 'temml/dist/Temml-Local.css';
import '@justinribeiro/lite-youtube';
import { msg, plural } from '@lingui/core/macro';
@ -22,6 +24,7 @@ import {
useEffect,
useLayoutEffect,
useMemo,
useReducer,
useRef,
useState,
} from 'preact/hooks';
@ -222,6 +225,161 @@ function getHTMLTextForDetectLang(content, emojis) {
}
const HTTP_REGEX = /^http/i;
// Follow https://mathstodon.xyz/about
// > You can use LaTeX in toots here! Use \( and \) for inline, and \[ and \] for display mode.
const DELIMITERS_PATTERNS = [
// '\\$\\$[\\s\\S]*?\\$\\$', // $$...$$
'\\\\\\[[\\s\\S]*?\\\\\\]', // \[...\]
'\\\\\\([\\s\\S]*?\\\\\\)', // \(...\)
// '\\\\begin\\{(?:equation\\*?|align\\*?|alignat\\*?|gather\\*?|CD)\\}[\\s\\S]*?\\\\end\\{(?:equation\\*?|align\\*?|alignat\\*?|gather\\*?|CD)\\}', // AMS environments
// '\\\\(?:ref|eqref)\\{[^}]*\\}', // \ref{...}, \eqref{...}
];
const DELIMITERS_REGEX = new RegExp(DELIMITERS_PATTERNS.join('|'), 'g');
function cleanDOMForTemml(dom) {
// Define start and end delimiter patterns
const START_DELIMITERS = ['\\\\\\[', '\\\\\\(']; // \[ and \(
const startRegex = new RegExp(`(${START_DELIMITERS.join('|')})`);
// Walk through all text nodes
const walker = document.createTreeWalker(dom, NodeFilter.SHOW_TEXT);
const textNodes = [];
let node;
while ((node = walker.nextNode())) {
textNodes.push(node);
}
for (const textNode of textNodes) {
const text = textNode.textContent;
const startMatch = text.match(startRegex);
if (!startMatch) continue; // No start delimiter in this text node
// Find the matching end delimiter
const startDelimiter = startMatch[0];
const endDelimiter = startDelimiter === '\\[' ? '\\]' : '\\)';
// Collect nodes from start delimiter until end delimiter
const nodesToCombine = [textNode];
let currentNode = textNode;
let foundEnd = false;
let combinedText = text;
// Check if end delimiter is in the same text node
if (text.includes(endDelimiter)) {
foundEnd = true;
} else {
// Look through sibling nodes
while (currentNode.nextSibling && !foundEnd) {
const nextSibling = currentNode.nextSibling;
if (nextSibling.nodeType === Node.TEXT_NODE) {
nodesToCombine.push(nextSibling);
combinedText += nextSibling.textContent;
if (nextSibling.textContent.includes(endDelimiter)) {
foundEnd = true;
}
} else if (
nextSibling.nodeType === Node.ELEMENT_NODE &&
nextSibling.tagName === 'BR'
) {
nodesToCombine.push(nextSibling);
combinedText += '\n';
} else {
// Found a non-BR element, stop and don't process
break;
}
currentNode = nextSibling;
}
}
// Only process if we found the end delimiter and have nodes to combine
if (foundEnd && nodesToCombine.length > 1) {
// Replace the first text node with combined text
textNode.textContent = combinedText;
// Remove the other nodes
for (let i = 1; i < nodesToCombine.length; i++) {
nodesToCombine[i].remove();
}
}
}
}
const MathBlock = ({ content, contentRef, onRevert }) => {
DELIMITERS_REGEX.lastIndex = 0; // Reset index to prevent g trap
const hasLatexContent = DELIMITERS_REGEX.test(content);
if (!hasLatexContent) return null;
const { t } = useLingui();
const [mathRendered, setMathRendered] = useState(false);
const toggleMathRendering = useCallback(
async (e) => {
e.preventDefault();
e.stopPropagation();
if (mathRendered) {
// Revert to original content by refreshing PostContent
setMathRendered(false);
onRevert();
} else {
// Render math
try {
// This needs global because the codebase inside temml is calling a function from global.temml 🤦
const temml =
window.temml || (window.temml = (await import('temml'))?.default);
cleanDOMForTemml(contentRef.current);
const originalContentRefHTML = contentRef.current.innerHTML;
temml.renderMathInElement(contentRef.current, {
fences: '(', // This should sync with DELIMITERS_REGEX
annotate: true,
throwOnError: true,
errorCallback: (err) => {
console.warn('Failed to render LaTeX:', err);
},
});
const hasMath = contentRef.current.querySelector('math.tml-display');
const htmlChanged =
contentRef.current.innerHTML !== originalContentRefHTML;
if (hasMath && htmlChanged) {
setMathRendered(true);
} else {
showToast(t`Unable to format math`);
setMathRendered(false);
onRevert(); // Revert because DOM modified by cleanDOMForTemml
}
} catch (e) {
console.error('Failed to LaTeX:', e);
}
}
},
[mathRendered],
);
return (
<div class="math-block">
<Icon icon="formula" size="s" /> <span>{t`Math expressions found.`}</span>{' '}
<button type="button" class="light small" onClick={toggleMathRendering}>
{mathRendered
? t({
comment:
'Action to switch from rendered math back to raw (LaTeX) markup',
message: 'Show markup',
})
: t({
comment:
'Action to render math expressions from raw (LaTeX) markup',
message: 'Format math',
})}
</button>
</div>
);
};
const PostContent =
/*memo(*/
({ post, instance, previewMode }) => {
@ -709,6 +867,10 @@ function Status({
const mediaContainerRef = useTruncated();
const statusRef = useRef(null);
const [reloadPostContentCount, reloadPostContent] = useReducer(
(c) => c + 1,
0,
);
const unauthInteractionErrorMessage = t`Sorry, your current logged-in instance can't interact with this post from another instance.`;
@ -2201,6 +2363,7 @@ function Status({
inert={!!spoilerText && !showSpoiler ? true : undefined}
>
<PostContent
key={reloadPostContentCount}
post={status}
instance={instance}
previewMode={previewMode}
@ -2208,6 +2371,13 @@ function Status({
<QuoteStatuses id={id} instance={instance} level={quoted} />
</div>
)}
{!!content && (
<MathBlock
content={content}
contentRef={contentRef}
onRevert={reloadPostContent}
/>
)}
{!!poll && (
<Poll
lang={language}
@ -3875,6 +4045,22 @@ function FilteredStatus({
);
}
const handledUnfulfilledStates = [
'deleted',
'unauthorized',
'pending',
'rejected',
'revoked',
];
const unfulfilledText = {
filterHidden: msg`Post hidden by your filters`,
deleted: msg`Post removed by author.`,
unauthorized: msg`Youre not authorized to view this post.`,
pending: msg`Post pending author approval.`,
rejected: msg`Quoting not allowed by the author.`,
revoked: msg`Quoting not allowed by the author.`,
};
const QuoteStatuses = memo(({ id, instance, level = 0 }) => {
if (!id || !instance) return;
const { _ } = useLingui();
@ -3888,43 +4074,43 @@ const QuoteStatuses = memo(({ id, instance, level = 0 }) => {
if (!uniqueQuotes?.length) return;
if (level > 2) return;
const filterContext = useContext(FilterContext);
const currentAccount = getCurrentAccID();
return uniqueQuotes.map((q) => {
if (q.state === 'deleted')
let unfulfilledState;
const quoteStatus = snapStates.statuses[statusKey(q.id, q.instance)];
if (quoteStatus) {
const isSelf =
currentAccount && currentAccount === quoteStatus.account?.id;
const filterInfo =
!isSelf && isFiltered(quoteStatus.filtered, filterContext);
if (filterInfo?.action === 'hide') {
unfulfilledState = 'filterHidden';
}
}
if (!unfulfilledState) {
unfulfilledState = handledUnfulfilledStates.find(
(state) => q.state === state,
);
}
if (unfulfilledState) {
return (
<div class="status-card-unfulfilled">
<div
class={`status-card-unfulfilled ${
unfulfilledState === 'filterHidden' ? 'status-card-ghost' : ''
}`}
>
<Icon icon="quote" />
<i>
<Trans>Post removed by author.</Trans>
</i>
</div>
);
if (q.state === 'unauthorized')
return (
<div class="status-card-unfulfilled">
<Icon icon="quote" />
<i>
<Trans>Youre not authorized to view this post.</Trans>
</i>
</div>
);
if (q.state === 'pending')
return (
<div class="status-card-unfulfilled">
<Icon icon="quote" />
<i>
<Trans>Post pending author approval.</Trans>
</i>
</div>
);
if (q.state === 'rejected' || q.state === 'revoked')
return (
<div class="status-card-unfulfilled">
<Icon icon="quote" />
<i>
<Trans>Quoting not allowed by the author.</Trans>
</i>
<i>{_(unfulfilledText[unfulfilledState])}</i>
</div>
);
}
const Parent = q.native ? Fragment : LazyShazam;
return (
<Parent id={q.instance + q.id} key={q.instance + q.id}>

Wyświetl plik

@ -60,7 +60,9 @@ function _translangTranslate(text, source, target) {
} else {
// GET
fetchPromise = fetch(
`https://${instance}/api/v1/translate?sl=${encodeURIComponent(source)}&tl=${encodeURIComponent(target)}&text=${encodeURIComponent(text)}`,
`https://${instance}/api/v1/translate?sl=${encodeURIComponent(
source,
)}&tl=${encodeURIComponent(target)}&text=${encodeURIComponent(text)}`,
{
priority: 'low',
referrerPolicy: 'no-referrer',

Wyświetl plik

@ -10,7 +10,7 @@
"code": "ca-ES",
"nativeName": "català",
"name": "Catalan",
"completion": 100,
"completion": 101,
"listed": true
},
{
@ -38,14 +38,14 @@
"code": "es-ES",
"nativeName": "español",
"name": "Spanish",
"completion": 100,
"completion": 101,
"listed": true
},
{
"code": "eu-ES",
"nativeName": "euskara",
"name": "Basque",
"completion": 99,
"completion": 100,
"listed": true
},
{
@ -73,21 +73,21 @@
"code": "gl-ES",
"nativeName": "galego",
"name": "Galician",
"completion": 100,
"completion": 101,
"listed": true
},
{
"code": "he-IL",
"nativeName": "עברית",
"name": "Hebrew",
"completion": 9,
"completion": 23,
"listed": false
},
{
"code": "it-IT",
"nativeName": "italiano",
"name": "Italian",
"completion": 100,
"completion": 101,
"listed": true
},
{
@ -136,21 +136,21 @@
"code": "pl-PL",
"nativeName": "polski",
"name": "Polish",
"completion": 100,
"completion": 101,
"listed": true
},
{
"code": "pt-BR",
"nativeName": "português",
"name": "Portuguese",
"completion": 100,
"completion": 101,
"listed": true
},
{
"code": "pt-PT",
"nativeName": "português",
"name": "Portuguese",
"completion": 100,
"completion": 101,
"listed": true
},
{
@ -178,7 +178,7 @@
"code": "uk-UA",
"nativeName": "українська",
"name": "Ukrainian",
"completion": 90,
"completion": 94,
"listed": true
},
{

Wyświetl plik

@ -22,8 +22,9 @@
--main-width: 40em;
text-size-adjust: none;
--hairline-width: 1px;
--monospace-font: ui-monospace, 'SFMono-Regular', Consolas, 'Liberation Mono',
Menlo, Courier, monospace;
--monospace-font:
ui-monospace, 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier,
monospace;
--blue-color: royalblue;
--purple-color: blueviolet;

519
src/locales/ar-SA.po wygenerowano

Plik diff jest za duży Load Diff

585
src/locales/ca-ES.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/cs-CZ.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/de-DE.po wygenerowano

Plik diff jest za duży Load Diff

571
src/locales/en.po wygenerowano

Plik diff jest za duży Load Diff

527
src/locales/eo-UY.po wygenerowano

Plik diff jest za duży Load Diff

585
src/locales/es-ES.po wygenerowano

Plik diff jest za duży Load Diff

529
src/locales/eu-ES.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/fa-IR.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/fi-FI.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/fr-FR.po wygenerowano

Plik diff jest za duży Load Diff

521
src/locales/gl-ES.po wygenerowano

Plik diff jest za duży Load Diff

783
src/locales/he-IL.po wygenerowano

Plik diff jest za duży Load Diff

587
src/locales/it-IT.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/ja-JP.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/kab.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/ko-KR.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/lt-LT.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/nb-NO.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/nl-NL.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/oc-FR.po wygenerowano

Plik diff jest za duży Load Diff

653
src/locales/pl-PL.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/pt-BR.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/pt-PT.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/ru-RU.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/th-TH.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/tok.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/tr-TR.po wygenerowano

Plik diff jest za duży Load Diff

591
src/locales/uk-UA.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/zh-CN.po wygenerowano

Plik diff jest za duży Load Diff

519
src/locales/zh-TW.po wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -45,7 +45,8 @@
transform: translateY(-10vh);
color: var(--text-color);
background-color: var(--bg-color);
background-image: radial-gradient(
background-image:
radial-gradient(
farthest-corner at 25% 0,
transparent 80%,
var(--bg-faded-color) 95%,

Wyświetl plik

@ -93,7 +93,11 @@ function Login() {
setUIState('loading');
try {
let credentialApplication = getCredentialApplication(instanceURL);
if (!credentialApplication) {
if (
!credentialApplication ||
!credentialApplication.client_id ||
!credentialApplication.client_secret
) {
credentialApplication = await registerApplication({
instanceURL,
});

Wyświetl plik

@ -35,10 +35,9 @@
padding: 16px;
background-color: var(--bg-color);
/* checkered background */
background-image: linear-gradient(
var(--bg-faded-color) 2px,
transparent 2px
), linear-gradient(90deg, var(--bg-faded-color) 2px, transparent 2px),
background-image:
linear-gradient(var(--bg-faded-color) 2px, transparent 2px),
linear-gradient(90deg, var(--bg-faded-color) 2px, transparent 2px),
linear-gradient(var(--bg-faded-color) 1px, transparent 1px),
linear-gradient(90deg, var(--bg-faded-color) 1px, transparent 1px);
background-size:

Wyświetl plik

@ -45,18 +45,20 @@ const MOCK_STATUS = ({ toggles = {} } = {}) => {
showCard,
size,
filters,
quoteFilters,
userPreferences,
} = toggles;
const shortContent = 'This is a test status with short text content.';
const longContent = `<p>This is a test status with long text content. It contains multiple paragraphs and spans several lines to demonstrate how longer content appears.</p>
<p>Second paragraph goes here with more sample text. The Status component will render this appropriately based on the current size setting.</p>
<p>Third paragraph adds even more content to ensure we have a properly long post that might get truncated depending on the view settings.</p>`;
const linksContent = `<p>This is a test status with links. Check out <a href="https://example.com">this website</a> and <a href="https://google.com">Google</a>. Links should be clickable and properly styled.</p>`;
const hashtagsContent = `<p>This is a test status with hashtags. <a href="https://example.social/tags/coding" class="hashtag" rel="tag">#coding</a> <a href="https://example.social/tags/webdev" class="hashtag" rel="tag">#webdev</a> <a href="https://example.social/tags/javascript" class="hashtag" rel="tag">#javascript</a> <a href="https://example.social/tags/reactjs" class="hashtag" rel="tag">#reactjs</a> <a href="https://example.social/tags/preact" class="hashtag" rel="tag">#preact</a></p><p>Hashtags should be formatted and clickable.</p>`;
const mentionsContent = `<p>This is a test status with mentions. Hello <a href="https://example.social/@cheeaun" class="u-url mention">@cheeaun</a> and <a href="https://example.social/@test" class="u-url mention">@test</a>! What do you think about this <a href="https://example.social/@another_user" class="u-url mention">@another_user</a>?</p><p>Mentions should be highlighted and clickable.</p>`;
const mathContent = `<p>This is a test status with mathematical expressions. Here's an inline formula \\( E = mc^2 \\) and a display formula:</p><p>\\[ \\frac{\\left(n!\\right)^2}{2}\\sum _{k=0}^m\\frac{1}{n-k}{n-k \\choose k}^2 \\]</p><p>The MathBlock component should detect and offer to render these LaTeX expressions.</p>`;
const base = {
// Random ID to un-memoize Status
@ -79,7 +81,9 @@ const MOCK_STATUS = ({ toggles = {} } = {}) => {
? hashtagsContent
: contentType === 'mentions'
? mentionsContent
: shortContent
: contentType === 'math'
? mathContent
: shortContent
: '',
visibility: toggles.visibility || 'public',
createdAt: new Date().toISOString(),
@ -310,8 +314,10 @@ const INITIAL_STATE = {
showQuotes: false,
quotesCount: '1',
quoteNestingLevel: '0',
quoteState: 'accepted', // State for all quote posts
size: 'medium',
filters: [false, false, false], // hide, blur, warn
quoteFilters: [false, false, false], // hide, blur, warn for quotes
mediaPreference: 'default',
expandWarnings: false,
contextType: 'none', // Default context type
@ -396,8 +402,10 @@ export default function Sandbox() {
showQuotes: toggleState.showQuotes,
quotesCount: toggleState.quotesCount,
quoteNestingLevel: toggleState.quoteNestingLevel,
quoteState: toggleState.quoteState,
size: toggleState.size,
filters: toggleState.filters,
quoteFilters: toggleState.quoteFilters,
},
});
@ -525,136 +533,173 @@ export default function Sandbox() {
id: quoteId,
instance: DEFAULT_INSTANCE,
url: `https://example.social/s/${quoteId}`, // Include URL to ensure uniqueness check works
state:
toggleState.quoteState === 'accepted'
? undefined
: toggleState.quoteState, // Only set state if not accepted
};
// First, delete any existing status with this ID to avoid duplicates
delete states.statuses[quoteId];
const quoteStatusKey = statusKey(quoteId, DEFAULT_INSTANCE);
delete states.statuses[quoteStatusKey];
// Create the actual status object that will be retrieved by QuoteStatuses
states.statuses[quoteId] = {
id: quoteId,
content: `<p>This is quote post ${i + 1}${i % 2 === 0 ? '' : ' with some extra text'}</p>`,
account: {
id: `quote-account-${i}`,
username: `quote${i}`,
name: `Quote User ${i}`,
avatar: '/logo-192.png',
acct: `quote${i}@example.social`,
url: `https://example.social/@quote${i}`,
},
visibility: 'public',
createdAt: new Date(Date.now() - i * 3600000).toISOString(), // Each post 1 hour older
emojis: [],
// First quote post should be plain (no media, no poll)
mediaAttachments:
i > 0 && i % 2 === 0
? [
{
// Only non-first posts can have media (every 3rd post after the 1st)
id: `quote-media-${i}`,
type: 'image',
url: `https://picsum.photos/seed/quote-${i}/600/400`,
previewUrl: `https://picsum.photos/seed/quote-${i}/300/200`,
meta: {
original: { width: 600, height: 400 },
small: { width: 300, height: 200 },
},
},
]
: [],
poll:
i > 0 && i % 3 === 0
? {
// Only non-first posts can have polls (every 4th post after the 1st)
id: `quote-poll-${i}`,
options: [
{
title: 'Option A',
votesCount: Math.floor(Math.random() * 50),
},
{
title: 'Option B',
votesCount: Math.floor(Math.random() * 50),
},
],
expiresAt: new Date(
Date.now() + 24 * 60 * 60 * 1000,
).toISOString(),
multiple: false,
votesCount: Math.floor(Math.random() * 100),
}
: null,
};
// If nesting level > 0, add nested quotes to each quote post
if (nestingLevel > 0 && i % 2 === 0) {
// Add nested quotes to every other quote - use stable ID
const nestedQuoteId = `nested-quote-${i}-12345`;
// Add the nested quote post to states.statuses
states.statuses[nestedQuoteId] = {
id: nestedQuoteId,
content: `<p>This is a nested quote inside quote ${i + 1}</p>`,
// Create the actual status object for all quote states
// This allows filtering logic to run even for non-accepted states
{
// Create the actual status object that will be retrieved by QuoteStatuses
const quoteStatus = {
id: quoteId,
content: `<p>This is quote post ${i + 1}${i % 2 === 0 ? '' : ' with some extra text'}</p>`,
account: {
id: `nested-account-${i}`,
username: `nested${i}`,
name: `Nested User ${i}`,
id: `quote-account-${i}`,
username: `quote${i}`,
name: `Quote User ${i}`,
avatar: '/logo-192.png',
acct: `nested${i}@example.social`,
url: `https://example.social/@nested${i}`,
acct: `quote${i}@example.social`,
url: `https://example.social/@quote${i}`,
},
visibility: 'public',
createdAt: new Date(Date.now() - (i + 1) * 3600000).toISOString(),
createdAt: new Date(Date.now() - i * 3600000).toISOString(), // Each post 1 hour older
emojis: [],
mediaAttachments: [], // No media in nested quotes for simplicity
// First quote post should be plain (no media, no poll)
mediaAttachments:
i > 0 && i % 2 === 0
? [
{
// Only non-first posts can have media (every 3rd post after the 1st)
id: `quote-media-${i}`,
type: 'image',
url: `https://picsum.photos/seed/quote-${i}/600/400`,
previewUrl: `https://picsum.photos/seed/quote-${i}/300/200`,
meta: {
original: { width: 600, height: 400 },
small: { width: 300, height: 200 },
},
},
]
: [],
poll:
i > 0 && i % 3 === 0
? {
// Only non-first posts can have polls (every 4th post after the 1st)
id: `quote-poll-${i}`,
options: [
{
title: 'Option A',
votesCount: Math.floor(Math.random() * 50),
},
{
title: 'Option B',
votesCount: Math.floor(Math.random() * 50),
},
],
expiresAt: new Date(
Date.now() + 24 * 60 * 60 * 1000,
).toISOString(),
multiple: false,
votesCount: Math.floor(Math.random() * 100),
}
: null,
};
// Create reference object for nested quote - critical for proper rendering
const nestedQuoteRef = {
id: nestedQuoteId,
instance: DEFAULT_INSTANCE,
url: `https://example.social/s/${nestedQuoteId}`,
};
// Add filtering to quote posts if enabled
if (
toggleState.quoteFilters &&
toggleState.quoteFilters.some((f) => f)
) {
quoteStatus.filtered = toggleState.quoteFilters
.map((enabled, filterIndex) => {
if (!enabled) return null;
const filterTypes = ['hide', 'blur', 'warn'];
return {
filter: {
id: `quote-filter-${i}-${filterIndex}`,
title: `Quote ${filterTypes[filterIndex]} filter`,
context: ['home', 'public', 'thread', 'account'],
filterAction: filterTypes[filterIndex],
},
keywordMatches: [],
statusMatches: [],
};
})
.filter(Boolean);
}
// Add another level of nesting if specified
if (nestingLevel > 1 && i === 0) {
// Only add deepest nesting to first quote
const deepNestedId = `deep-nested-${i}-12345`;
// Assign the quote status to the states using the correct key format
states.statuses[quoteStatusKey] = quoteStatus;
states.statuses[deepNestedId] = {
id: deepNestedId,
content: `<p>This is a deeply nested quote (level 2)</p>`,
// If nesting level > 0, add nested quotes to each quote post
if (nestingLevel > 0 && i % 2 === 0) {
// Add nested quotes to every other quote - use stable ID
const nestedQuoteId = `nested-quote-${i}-12345`;
// Add the nested quote post to states.statuses
states.statuses[nestedQuoteId] = {
id: nestedQuoteId,
content: `<p>This is a nested quote inside quote ${i + 1}</p>`,
account: {
id: `deep-account-${i}`,
username: `deep${i}`,
name: `Deep User ${i}`,
id: `nested-account-${i}`,
username: `nested${i}`,
name: `Nested User ${i}`,
avatar: '/logo-192.png',
acct: `deep${i}@example.social`,
url: `https://example.social/@deep${i}`,
acct: `nested${i}@example.social`,
url: `https://example.social/@nested${i}`,
},
visibility: 'public',
createdAt: new Date(
Date.now() - (i + 2) * 3600000,
Date.now() - (i + 1) * 3600000,
).toISOString(),
emojis: [],
mediaAttachments: [], // No media in nested quotes for simplicity
};
// Create deep nested reference
const deepNestedRef = {
id: deepNestedId,
// Create reference object for nested quote - critical for proper rendering
const nestedQuoteRef = {
id: nestedQuoteId,
instance: DEFAULT_INSTANCE,
url: `https://example.social/s/${deepNestedId}`,
url: `https://example.social/s/${nestedQuoteId}`,
};
// Important: Use the proper key format for the nested quote
const nestedKey = statusKey(nestedQuoteId, DEFAULT_INSTANCE);
states.statusQuotes[nestedKey] = [deepNestedRef];
}
// Add another level of nesting if specified
if (nestingLevel > 1 && i === 0) {
// Only add deepest nesting to first quote
const deepNestedId = `deep-nested-${i}-12345`;
// Add nested quote to the quote's quotes using the proper key format
const quoteKey = statusKey(quoteId, DEFAULT_INSTANCE);
states.statusQuotes[quoteKey] = [nestedQuoteRef];
}
states.statuses[deepNestedId] = {
id: deepNestedId,
content: `<p>This is a deeply nested quote (level 2)</p>`,
account: {
id: `deep-account-${i}`,
username: `deep${i}`,
name: `Deep User ${i}`,
avatar: '/logo-192.png',
acct: `deep${i}@example.social`,
url: `https://example.social/@deep${i}`,
},
visibility: 'public',
createdAt: new Date(
Date.now() - (i + 2) * 3600000,
).toISOString(),
emojis: [],
};
// Create deep nested reference
const deepNestedRef = {
id: deepNestedId,
instance: DEFAULT_INSTANCE,
url: `https://example.social/s/${deepNestedId}`,
};
// Important: Use the proper key format for the nested quote
const nestedKey = statusKey(nestedQuoteId, DEFAULT_INSTANCE);
states.statusQuotes[nestedKey] = [deepNestedRef];
}
// Add nested quote to the quote's quotes using the proper key format
const quoteKey = statusKey(quoteId, DEFAULT_INSTANCE);
states.statusQuotes[quoteKey] = [nestedQuoteRef];
}
} // Close the quote status creation block
return quoteRef;
});
@ -670,6 +715,8 @@ export default function Sandbox() {
toggleState.showQuotes,
toggleState.quotesCount,
toggleState.quoteNestingLevel,
toggleState.quoteState,
toggleState.quoteFilters,
]);
// Handler for filter checkboxes
@ -679,6 +726,13 @@ export default function Sandbox() {
updateToggles({ filters: newFilters });
};
// Handler for quote filter checkboxes
const handleQuoteFilterChange = (index) => {
const newQuoteFilters = [...toggleState.quoteFilters];
newQuoteFilters[index] = !newQuoteFilters[index];
updateToggles({ quoteFilters: newQuoteFilters });
};
// Function to check if the current state is different from the initial state
const hasChanges = () => {
return Object.keys(INITIAL_STATE).some((key) => {
@ -713,7 +767,7 @@ export default function Sandbox() {
class={`sandbox-preview ${toggleState.displayStyle}`}
onClickCapture={(e) => {
const isAllowed = e.target.closest(
'.media, .media-caption, .spoiler-button, .spoiler-media-button',
'.media, .media-caption, .spoiler-button, .spoiler-media-button, .math-block button',
);
if (isAllowed) return;
e.preventDefault();
@ -938,6 +992,18 @@ export default function Sandbox() {
<span>With mentions</span>
</label>
</li>
<li>
<label>
<input
type="radio"
name="contentType"
checked={toggleState.contentType === 'math'}
onChange={() => updateToggles({ contentType: 'math' })}
disabled={!toggleState.hasContent}
/>
<span>With math</span>
</label>
</li>
</ul>
</li>
<li>
@ -1214,6 +1280,133 @@ export default function Sandbox() {
/>
</label>
</li>
<li>
<span>Quote state</span>
<ul>
<li>
<label>
<input
type="radio"
name="quoteState"
value="accepted"
checked={toggleState.quoteState === 'accepted'}
onChange={(e) => {
updateToggles({ quoteState: e.target.value });
}}
/>
<span>Accepted</span>
</label>
</li>
<li>
<label>
<input
type="radio"
name="quoteState"
value="deleted"
checked={toggleState.quoteState === 'deleted'}
onChange={(e) => {
updateToggles({ quoteState: e.target.value });
}}
/>
<span>Deleted</span>
</label>
</li>
<li>
<label>
<input
type="radio"
name="quoteState"
value="unauthorized"
checked={
toggleState.quoteState === 'unauthorized'
}
onChange={(e) => {
updateToggles({ quoteState: e.target.value });
}}
/>
<span>Unauthorized</span>
</label>
</li>
<li>
<label>
<input
type="radio"
name="quoteState"
value="pending"
checked={toggleState.quoteState === 'pending'}
onChange={(e) => {
updateToggles({ quoteState: e.target.value });
}}
/>
<span>Pending</span>
</label>
</li>
<li>
<label>
<input
type="radio"
name="quoteState"
value="rejected"
checked={toggleState.quoteState === 'rejected'}
onChange={(e) => {
updateToggles({ quoteState: e.target.value });
}}
/>
<span>Rejected</span>
</label>
</li>
<li>
<label>
<input
type="radio"
name="quoteState"
value="revoked"
checked={toggleState.quoteState === 'revoked'}
onChange={(e) => {
updateToggles({ quoteState: e.target.value });
}}
/>
<span>Revoked</span>
</label>
</li>
</ul>
</li>
<li>
<b>Quote Filters</b>
<ul>
<li>
<label>
<input
type="checkbox"
checked={toggleState.quoteFilters[0]}
onChange={() => handleQuoteFilterChange(0)}
/>
<span>Hide</span>
</label>
</li>
<li>
<label>
<input
type="checkbox"
checked={toggleState.quoteFilters[1]}
onChange={() => handleQuoteFilterChange(1)}
/>
<span>Blur</span>
</label>
</li>
<li>
<label>
<input
type="checkbox"
checked={toggleState.quoteFilters[2]}
onChange={() => handleQuoteFilterChange(2)}
/>
<span>Warn</span>
</label>
</li>
</ul>
</li>
</ul>
)}
</li>

Wyświetl plik

@ -193,6 +193,7 @@ function StatusPage(params) {
});
transition.ready.finally(() => {
el.style.viewTransitionName = '';
el.dataset.viewTransitioned = mediaVTN;
});
} else {
mediaClose();

Wyświetl plik

@ -1,10 +1,8 @@
#welcome {
text-align: center;
background-image: radial-gradient(
circle at center,
var(--bg-color),
transparent 16em
), radial-gradient(circle at center, var(--bg-color), transparent 8em);
background-image:
radial-gradient(circle at center, var(--bg-color), transparent 16em),
radial-gradient(circle at center, var(--bg-color), transparent 8em);
background-repeat: no-repeat;
background-attachment: fixed;
cursor: default;

Wyświetl plik

@ -37,12 +37,17 @@ export const translate = async (text, source, target) => {
let detectedSourceLanguage;
const originalSource = source;
if (source === 'auto') {
if (!langDetector?.detect) {
return {
error: 'No language detector',
};
}
try {
const results = await langDetector.detect(text);
source = results[0].detectedLanguage;
detectedSourceLanguage = source;
} catch (e) {
console.error(e);
console.warn(e);
return {
error: e,
};

Wyświetl plik

@ -6,7 +6,7 @@ import { lingui } from '@lingui/vite-plugin';
import preact from '@preact/preset-vite';
import Sonda from 'sonda/vite';
import { uid } from 'uid/single';
import { defineConfig, loadEnv, splitVendorChunkPlugin } from 'vite';
import { defineConfig, loadEnv } from 'vite';
import generateFile from 'vite-plugin-generate-file';
import htmlPlugin from 'vite-plugin-html-config';
import { VitePWA } from 'vite-plugin-pwa';
@ -83,7 +83,6 @@ export default defineConfig({
// },
],
}),
splitVendorChunkPlugin(),
removeConsole({
includes: ['log', 'debug', 'info', 'warn', 'error'],
}),
@ -177,10 +176,26 @@ export default defineConfig({
compose: resolve(__dirname, 'compose/index.html'),
},
output: {
manualChunks: {
// 'intl-segmenter-polyfill': ['@formatjs/intl-segmenter/polyfill'],
'tinyld-light': ['tinyld/light'],
},
// NOTE: Comment this for now. This messes up async imports.
// Without SplitVendorChunkPlugin, pushing everything to vendor is not "smart" enough
// manualChunks: (id, { getModuleInfo }) => {
// // if (id.includes('@formatjs/intl-segmenter/polyfill')) return 'intl-segmenter-polyfill';
// if (/tiny.*light/.test(id)) return 'tinyld-light';
// // Implement logic similar to splitVendorChunkPlugin
// if (id.includes('node_modules')) {
// // Check if this module is dynamically imported
// const moduleInfo = getModuleInfo(id);
// if (moduleInfo) {
// // If it's imported dynamically, don't put in vendor
// const isDynamicOnly =
// moduleInfo.importers.length === 0 &&
// moduleInfo.dynamicImporters.length > 0;
// if (isDynamicOnly) return null;
// }
// return 'vendor';
// }
// },
chunkFileNames: (chunkInfo) => {
const { facadeModuleId } = chunkInfo;
if (facadeModuleId && facadeModuleId.includes('icon')) {