Compare commits

...
Author SHA1 Message Date
Lotus CIandClaude Opus 5.5 3e11a6fc0b fix(translate): say why Translate did nothing (#287)
CI / Build & Quality Checks (pull_request) Successful in 3m5s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 8s
CI / Playwright smoke (e2e) (pull_request) Successful in 13m42s
When a message was already in the target language, or its language
could not be detected, Translate switched itself back off with no chip,
no "Translating…" and no error. Show a short toast instead: "This
message is already in English." or "Couldn't tell what language this
message is in, so it wasn't translated."

Fixes #287

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-18 00:17:04 -04:00
jared 906a034e26 Merge pull request 'fix(calls): starting a call in a voice channel no longer @room-pings everyone (#295)' (#378) from fix/voice-room-no-room-ping into lotus
CI / Build & Quality Checks (push) Canceled after 0s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Secret scan (gitleaks) (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
2026-10-17 20:38:37 -04:00
jared b855eb505d Merge pull request 'fix(reactions): the reactions row no longer flickers or loses its spacing (#305)' (#377) from fix/reactions-collapse-loop into lotus
CI / Build & Quality Checks (push) Canceled after 0s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Secret scan (gitleaks) (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
2026-10-17 20:07:27 -04:00
jared cc8e7444f6 Merge pull request 'fix(rooms): members can start and join calls in new private rooms (#328)' (#376) from fix/group-call-permission into lotus
CI / Build & Quality Checks (push) Canceled after 0s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Secret scan (gitleaks) (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
2026-10-17 19:54:43 -04:00
Lotus CIandClaude Opus 5.5 d7ea408fd0 chore(call): pin element-call-embedded 0.25.0-lotus.23 (sendNotificationType=none, #295)
CI / Build & Quality Checks (pull_request) Successful in 2m48s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 10s
CI / Playwright smoke (e2e) (pull_request) Successful in 15m7s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-17 18:42:52 -04:00
Lotus CI d3f01b5116 Merge remote-tracking branch 'origin/lotus' into fix/voice-room-no-room-ping 2026-10-17 18:40:48 -04:00
jared f84c0d6e9e Merge pull request 'fix(dialogs): Enter in Poll, Schedule, Report dialogs no longer discards input (#315)' (#369) from fix/dialog-enter-closes into lotus
CI / Build & Quality Checks (push) Successful in 3m14s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 17s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
2026-10-17 18:35:54 -04:00
Lotus CIandClaude Opus 5.5 a6c7333ea3 fix(rooms): members can start and join calls in new private rooms (#328)
CI / Build & Quality Checks (pull_request) Successful in 2m58s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 13s
CI / Playwright smoke (e2e) (pull_request) Successful in 14m30s
createRoom granted org.matrix.msc3401.call.member at user level only for
voice rooms. Every other room fell back to Synapse's state_default (50),
so in a private group room (where Lotus shows the call button and rings)
only admins could start a call; members saw "1 Live · Join" and then
"You don't have permission to join!". All new non-space rooms now get
call.member: 0 (spaces keep their own override).

Verified headless through Create Room → Private: before, no call.member
entry (state_default 50); now 0. Existing rooms are unchanged; an admin
can set it in room permissions.

Fixes #328

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-17 18:09:33 -04:00
jared f0b7400d1b Merge branch 'lotus' into fix/dialog-enter-closes
CI / Build & Quality Checks (pull_request) Successful in 3m12s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 14s
CI / Playwright smoke (e2e) (pull_request) Successful in 13m57s
2026-10-17 17:53:08 -04:00
Lotus CIandClaude Opus 5.5 a86947be48 fix(calls): starting a call in a voice channel no longer @room-pings everyone (#295)
CallEmbed asked Element Call to send a call-start notification for every
non-DM call. In persistent voice channels and space rooms it carries an
@room mention: every member got a highlighted ping (and phone push)
whenever a moderator or admin joined first, and other clients showed it
as a missed call. Now only rooms that can ring (isRingableRoom: DMs and
invite-only groups outside a space) get ring/notification; everything
else passes sendNotificationType=none (element-call-embedded 0.25.0-lotus.23).

Verified with a local build of the fork: an admin joining the Voice Lounge
sends none, and Bob gets no new notification (before: +1 highlighted).

Fixes #295

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-16 22:08:22 -04:00
Lotus CIandClaude Opus 5.5 b6f9af3f58 fix(dialogs): Enter in Poll, Schedule, Report dialogs no longer discards input (#315)
CI / Build & Quality Checks (pull_request) Successful in 3m58s
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) Failing after 18m27s
Several dialogs are <Box as="form"> with the header ✕ (folds IconButton,
no type attribute, so a submit button) as the form's first button. On
implicit submission, Enter in a text field "clicked" ✕, closing the dialog
and throwing away what was typed (a poll's question and answers, a
schedule, a report reason).

The header close/cancel IconButtons now have type="button": Create Poll,
Schedule Message, Report User, Report Room, Account Data editor, State
Event editor, device verification setup, and the message delete/report
dialogs.

Verified headless: in Create Poll, typing a question and pressing Enter
used to close the dialog; now it stays open with the question kept.

Fixes #315

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-16 21:58:05 -04:00
15 changed files with 72 additions and 20 deletions
+4 -4
View File
@@ -81,7 +81,7 @@
}, },
"devDependencies": { "devDependencies": {
"@axe-core/playwright": "4.13.0", "@axe-core/playwright": "4.13.0",
"@lotusguild/element-call-embedded": "0.25.0-lotus.22", "@lotusguild/element-call-embedded": "0.25.0-lotus.23",
"@playwright/test": "1.63.0", "@playwright/test": "1.63.0",
"@rollup/plugin-inject": "5.0.5", "@rollup/plugin-inject": "5.0.5",
"@rollup/plugin-wasm": "6.2.2", "@rollup/plugin-wasm": "6.2.2",
@@ -2695,9 +2695,9 @@
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
}, },
"node_modules/@lotusguild/element-call-embedded": { "node_modules/@lotusguild/element-call-embedded": {
"version": "0.25.0-lotus.22", "version": "0.25.0-lotus.23",
"resolved": "https://code.lotusguild.org/api/packages/LotusGuild/npm/%40lotusguild%2Felement-call-embedded/-/0.25.0-lotus.22/element-call-embedded-0.25.0-lotus.22.tgz", "resolved": "https://code.lotusguild.org/api/packages/LotusGuild/npm/%40lotusguild%2Felement-call-embedded/-/0.25.0-lotus.23/element-call-embedded-0.25.0-lotus.23.tgz",
"integrity": "sha512-AimDKZA4xUKEEyl2XUFUSUDl03tBKFv88T/3igKyH2VqNSPMGnqcQJC5HXIkHLTWrOmg8kLcJ//FFbqlUZiLcA==", "integrity": "sha512-vRmcWeJcU0Accyl2x3mqNEzrIe8djE5BekyYbYOSCsMflC98rZgD1mO7KOe7bA1cX7gparRf6sAt1fotHoU+bA==",
"dev": true "dev": true
}, },
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": { "node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
+1 -1
View File
@@ -108,7 +108,7 @@
}, },
"devDependencies": { "devDependencies": {
"@axe-core/playwright": "4.13.0", "@axe-core/playwright": "4.13.0",
"@lotusguild/element-call-embedded": "0.25.0-lotus.22", "@lotusguild/element-call-embedded": "0.25.0-lotus.23",
"@playwright/test": "1.63.0", "@playwright/test": "1.63.0",
"@rollup/plugin-inject": "5.0.5", "@rollup/plugin-inject": "5.0.5",
"@rollup/plugin-wasm": "6.2.2", "@rollup/plugin-wasm": "6.2.2",
+1 -1
View File
@@ -297,7 +297,7 @@ export function AccountDataEditor({
</Chip> </Chip>
</Box> </Box>
<Box shrink="No"> <Box shrink="No">
<IconButton onClick={requestClose} variant="Surface" aria-label="Close"> <IconButton type="button" onClick={requestClose} variant="Surface" aria-label="Close">
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
</IconButton> </IconButton>
</Box> </Box>
@@ -315,7 +315,7 @@ export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerifica
Setup Device Verification Setup Device Verification
</Text> </Text>
</Box> </Box>
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel"> <IconButton type="button" size="300" radii="300" onClick={onCancel} aria-label="Cancel">
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
</IconButton> </IconButton>
</Header> </Header>
@@ -361,7 +361,7 @@ export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerifica
Reset Device Verification Reset Device Verification
</Text> </Text>
</Box> </Box>
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel"> <IconButton type="button" size="300" radii="300" onClick={onCancel} aria-label="Cancel">
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
</IconButton> </IconButton>
</Header> </Header>
+5 -2
View File
@@ -139,8 +139,11 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis
data.allowFederation, data.allowFederation,
data.additionalCreators, data.additionalCreators,
), ),
power_level_content_override: // [Gitea #328] Every non-space room gets call.member at user level, not just
data.type === RoomType.Call ? createVoiceRoomPowerLevelsOverride() : undefined, // voice rooms: the call button is shown in private group rooms too, and with
// Synapse's state_default (50) only admins could start or join a call there
// ("You don't have permission to join!").
power_level_content_override: createVoiceRoomPowerLevelsOverride(),
initial_state: initialState, initial_state: initialState,
}; };
@@ -275,7 +275,7 @@ export function StateEventEditor({ type, stateKey, requestClose }: StateEventEdi
</Chip> </Chip>
</Box> </Box>
<Box shrink="No"> <Box shrink="No">
<IconButton onClick={requestClose} variant="Surface" aria-label="Close"> <IconButton type="button" onClick={requestClose} variant="Surface" aria-label="Close">
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
</IconButton> </IconButton>
</Box> </Box>
+1 -1
View File
@@ -143,7 +143,7 @@ export function PollCreator({ roomId, threadRootId, onClose }: PollCreatorProps)
Create Poll Create Poll
</Text> </Text>
</Box> </Box>
<IconButton size="300" radii="300" onClick={onClose} aria-label="Close"> <IconButton type="button" size="300" radii="300" onClick={onClose} aria-label="Close">
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
</IconButton> </IconButton>
</Header> </Header>
+1 -1
View File
@@ -118,7 +118,7 @@ export function ReportRoomModal({ roomId, onClose }: ReportRoomModalProps) {
Report Room Report Room
</Text> </Text>
</Box> </Box>
<IconButton size="300" onClick={onClose} radii="300" aria-label="Close"> <IconButton type="button" size="300" onClick={onClose} radii="300" aria-label="Close">
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
</IconButton> </IconButton>
</Header> </Header>
+1 -1
View File
@@ -124,7 +124,7 @@ export function ReportUserModal({ userId, onClose }: ReportUserModalProps) {
Report User Report User
</Text> </Text>
</Box> </Box>
<IconButton size="300" onClick={onClose} radii="300" aria-label="Close"> <IconButton type="button" size="300" onClick={onClose} radii="300" aria-label="Close">
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
</IconButton> </IconButton>
</Header> </Header>
@@ -202,7 +202,7 @@ export function ScheduleMessageModal({
{title} {title}
</Text> </Text>
</Box> </Box>
<IconButton size="300" radii="300" onClick={onClose} aria-label="Close"> <IconButton type="button" size="300" radii="300" onClick={onClose} aria-label="Close">
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
</IconButton> </IconButton>
</Header> </Header>
+14 -2
View File
@@ -725,7 +725,13 @@ export const MessageDeleteItem = as<
Delete Message Delete Message
</Text> </Text>
</Box> </Box>
<IconButton size="300" onClick={handleClose} radii="300" aria-label="Close"> <IconButton
type="button"
size="300"
onClick={handleClose}
radii="300"
aria-label="Close"
>
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
</IconButton> </IconButton>
</Header> </Header>
@@ -865,7 +871,13 @@ export const MessageReportItem = as<
Report Message Report Message
</Text> </Text>
</Box> </Box>
<IconButton size="300" onClick={handleClose} radii="300" aria-label="Close"> <IconButton
type="button"
size="300"
onClick={handleClose}
radii="300"
aria-label="Close"
>
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
</IconButton> </IconButton>
</Header> </Header>
+14 -2
View File
@@ -12,7 +12,8 @@ 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 } from '../utils/translation/langUtils'; import { sameLanguage, translateSkipMessage } 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)
@@ -52,6 +53,7 @@ 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
@@ -124,6 +126,16 @@ 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);
@@ -154,7 +166,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]); }, [active, cached, supported, text, targetLang, key, setActive, setCache, setToast, eventId]);
// 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).
+12 -1
View File
@@ -31,6 +31,7 @@ import { verifyDenoiseAssets } from './denoiseSmokeCheck';
import { canDelegateCapability } from './utils'; import { canDelegateCapability } from './utils';
import { getCallPageUrl } from './callPageUrl'; import { getCallPageUrl } from './callPageUrl';
import { restrictWidgetMessages } from '../widgetTransport'; import { restrictWidgetMessages } from '../widgetTransport';
import { isRingableRoom } from '../../utils/callRing';
// Maximum time to wait for the embedded Element Call iframe to progress from // Maximum time to wait for the embedded Element Call iframe to progress from
// initial load to a ready/joined state. If it hasn't by then, we assume the // initial load to a ready/joined state. If it hasn't by then, we assume the
@@ -251,8 +252,18 @@ export class CallEmbed {
verifyDenoiseAssets(denoiseModel).catch(() => undefined); verifyDenoiseAssets(denoiseModel).catch(() => undefined);
} }
// [Gitea #295] Only ask Element Call to notify where a call can ring at all
// (DMs, invite-only groups outside a space). In a voice channel or space
// room the notification carries an @room mention, so every member got a
// highlighted ping (and phone push) whenever a moderator hopped in, and
// other clients showed it as a missed call.
// `none` needs element-call-embedded ≥ 0.25.0-lotus.23 (older builds ignore it).
if (CallEmbed.startingCall(intent)) { if (CallEmbed.startingCall(intent)) {
params.append('sendNotificationType', CallEmbed.dmCall(intent) ? 'ring' : 'notification'); let notify = 'none';
if (isRingableRoom(room, CallEmbed.dmCall(intent))) {
notify = CallEmbed.dmCall(intent) ? 'ring' : 'notification';
}
params.append('sendNotificationType', notify);
} }
// [Gitea #43] On its own origin when config.json sets elementCallUrl. // [Gitea #43] On its own origin when config.json sets elementCallUrl.
+6
View File
@@ -85,3 +85,9 @@ 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.";
@@ -0,0 +1,8 @@
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/);
});