Compare commits

..
Author SHA1 Message Date
Lotus CIandClaude Opus 5.5 6f7a8f64e2 fix(settings): valid HTML in setting tiles; start uploads in an effect (#291)
CI / Build & Quality Checks (pull_request) Successful in 4m24s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 11s
CI / Playwright smoke (e2e) (pull_request) Successful in 15m43s
SettingTile rendered its description in a <p>, and many tiles pass
blocks (badges, Box, Text), so Settings had <div>/<p> inside <p> and
React warned on every page. Render it as a <div>. UploadCardRenderer
called startUpload() during render, which now updates the desktop
taskbar-progress hook mid-render; start it from an effect instead.

Refs #291

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-17 23:51:19 -04:00
5 changed files with 12 additions and 32 deletions
@@ -43,7 +43,9 @@ export function SettingTile({ title, description, before, after, children }: Set
</Text> </Text>
)} )}
{description && ( {description && (
<Text className={BreakWord} size="T200" priority="300"> // [Gitea #291] A <div>, not a <p>: many tiles pass blocks (badges,
// <Box>, <Text>) as the description, which is invalid inside a <p>.
<Text as="div" className={BreakWord} size="T200" priority="300">
{description} {description}
</Text> </Text>
)} )}
@@ -310,9 +310,13 @@ export function UploadCardRenderer({
const { file } = upload; const { file } = upload;
const fileSizeExceeded = file.size >= allowSize; const fileSizeExceeded = file.size >= allowSize;
if (upload.status === UploadStatus.Idle && !fileSizeExceeded) { // [Gitea #291] Start in an effect, not during render: the desktop taskbar
startUpload(); // progress hook subscribes to the upload atoms, so a render-time start
} // updated another component mid-render (React warning on every image send).
const idle = upload.status === UploadStatus.Idle;
useEffect(() => {
if (idle && !fileSizeExceeded) startUpload();
}, [idle, fileSizeExceeded, startUpload]);
const handleSpoiler = (marked: boolean) => { const handleSpoiler = (marked: boolean) => {
setMetadata(fileItem, { ...metadata, markedAsSpoiler: marked }); setMetadata(fileItem, { ...metadata, markedAsSpoiler: marked });
+2 -14
View File
@@ -12,8 +12,7 @@ import {
markAutoTranslateTried, markAutoTranslateTried,
} from '../state/translation'; } from '../state/translation';
import { chromeTranslationEngine as engine } from '../utils/translation/chromeEngine'; import { chromeTranslationEngine as engine } from '../utils/translation/chromeEngine';
import { sameLanguage, translateSkipMessage } from '../utils/translation/langUtils'; import { sameLanguage } from '../utils/translation/langUtils';
import { toastQueueAtom } from '../state/toast';
export type MsgTranslationStatus = export type MsgTranslationStatus =
| 'idle' // not translating (original shown) | 'idle' // not translating (original shown)
@@ -53,7 +52,6 @@ export const useMessageTranslation = (eventId: string, text: string): MsgTransla
// Write-only: reading the whole cache array here (via useAtom) would // Write-only: reading the whole cache array here (via useAtom) would
// resubscribe this message to every other message's writes. [Gitea #39] // resubscribe this message to every other message's writes. [Gitea #39]
const setCache = useSetAtom(translationCacheAtom); const setCache = useSetAtom(translationCacheAtom);
const setToast = useSetAtom(toastQueueAtom);
// The key includes a fingerprint of `text`, so an edit (same event id, new // The key includes a fingerprint of `text`, so an edit (same event id, new
// body) misses the old entry and re-translates instead of showing stale // body) misses the old entry and re-translates instead of showing stale
@@ -126,16 +124,6 @@ export const useMessageTranslation = (eventId: string, text: string): MsgTransla
if (!from || sameLanguage(from, targetLang)) { if (!from || sameLanguage(from, targetLang)) {
setStatus('skipped'); setStatus('skipped');
setActive(false); setActive(false);
// [Gitea #287] Say why nothing happened instead of silently undoing
// the click (no chip shows once translation is switched back off).
setToast({
id: `translate-skipped-${eventId}-${Date.now()}`,
displayName: 'Translate',
body: translateSkipMessage(from, targetLang),
roomName: '',
roomId: '',
onClick: () => undefined,
});
return; return;
} }
setFromLang(from); setFromLang(from);
@@ -166,7 +154,7 @@ export const useMessageTranslation = (eventId: string, text: string): MsgTransla
// Invalidate this run's writes if inputs change / unmount. // Invalidate this run's writes if inputs change / unmount.
runIdRef.current += 1; runIdRef.current += 1;
}; };
}, [active, cached, supported, text, targetLang, key, setActive, setCache, setToast, eventId]); }, [active, cached, supported, text, targetLang, key, setActive, setCache]);
// When switched off, drop back to idle (keep the cached translation around so // When switched off, drop back to idle (keep the cached translation around so
// toggling back on is instant). // toggling back on is instant).
-6
View File
@@ -85,9 +85,3 @@ export function languageName(code: string, uiLocale = 'en'): string {
export function isSupportedTargetLang(code: string): boolean { export function isSupportedTargetLang(code: string): boolean {
return CURATED_NAME.has(normalizeLang(code)); return CURATED_NAME.has(normalizeLang(code));
} }
/** Pure: why a manual Translate did nothing (#287). */
export const translateSkipMessage = (from: string | undefined, targetLang: string): string =>
from
? `This message is already in ${languageName(targetLang) || targetLang}.`
: "Couldn't tell what language this message is in, so it wasn't translated.";
@@ -1,8 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { translateSkipMessage } from './langUtils';
test('translateSkipMessage explains a Translate that did nothing (#287)', () => {
assert.equal(translateSkipMessage('en', 'en'), 'This message is already in English.');
assert.match(translateSkipMessage(undefined, 'en'), /^Couldn't tell what language/);
});