Files
cinny/src/app/plugins/call/utils.ts
T
Lotus CIandClaude Opus 5.5 49dec686f1
CI / Build & Quality Checks (push) Successful in 4m32s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 12s
CI / Trigger Desktop Build (push) Successful in 9s
CI / Playwright smoke (e2e) (push) Successful in 16m32s
feat(call): screenshare from inside the frame where delegation is missing; host stops reading the call frame (#43)
Firefox, Safari and the WebKitGTK desktop app can't hand the user's click to
the call frame (no Capability Delegation), and getDisplayMedia needs it. The
host used to click EC's hidden footer button through the DOM instead, which
dies with same-origin. Now (pins element-call-embedded 0.25.0-lotus.20):

- those engines get `lotusFrameScreenshare`, the fork shows EC's own
  screenshare button in the frame, and the call bar and status bar hide
  theirs once controls_state reports `frameScreenshare`; the
  screenshare-audio mute stays;
- the room's call policy is pushed with io.lotus.set_frame_screenshare, so
  the frame button hides where the server would refuse a share, like ours;
- Chromium keeps the delegated io.lotus.set_screenshare from the host bar.

Removed the fallbacks for forks older than lotus.14, which read or clicked
EC's DOM: the screenshare/layout/settings/reactions/leave button lookups
and their MutationObservers, the frame-window hotkey binding, and the
speaking/muted tile scrape in useCallSpeakers (io.lotus.call_state is the
only source now). getCallDocument is gone; the host's only handle on the
frame is postMessage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-26 22:04:54 -04:00

128 lines
4.1 KiB
TypeScript

import {
type Capability,
EventDirection,
MatrixCapabilities,
WidgetEventCapability,
} from 'matrix-widget-api';
import { EventType } from 'matrix-js-sdk';
export function getCallCapabilities(
roomId: string,
userId: string,
deviceId: string,
): Set<Capability> {
const capabilities: Set<Capability> = new Set();
capabilities.add(MatrixCapabilities.Screenshots);
capabilities.add(MatrixCapabilities.AlwaysOnScreen);
capabilities.add(MatrixCapabilities.MSC4039UploadFile);
capabilities.add(MatrixCapabilities.MSC4039DownloadFile);
capabilities.add(MatrixCapabilities.MSC3846TurnServers);
capabilities.add(MatrixCapabilities.MSC4157SendDelayedEvent);
capabilities.add(MatrixCapabilities.MSC4157UpdateDelayedEvent);
// MSC4515: Element Call >= 0.22 in widget mode discovers its LiveKit
// transport by asking the host (it no longer reads .well-known itself), so
// without this capability every call fails with MISSING_MATRIX_RTC_TRANSPORT.
capabilities.add(MatrixCapabilities.MSC4515RtcTransports);
capabilities.add(`org.matrix.msc2762.timeline:${roomId}`);
capabilities.add(`org.matrix.msc2762.state:${roomId}`);
capabilities.add(
WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomMember).raw,
);
capabilities.add(
WidgetEventCapability.forStateEvent(EventDirection.Receive, 'org.matrix.msc3401.call').raw,
);
capabilities.add(
WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomEncryption).raw,
);
capabilities.add(
WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomName).raw,
);
capabilities.add(
WidgetEventCapability.forStateEvent(
EventDirection.Send,
'org.matrix.msc3401.call.member',
userId,
).raw,
);
capabilities.add(
WidgetEventCapability.forStateEvent(
EventDirection.Send,
'org.matrix.msc3401.call.member',
`_${userId}_${deviceId}_m.call`,
).raw,
);
capabilities.add(
WidgetEventCapability.forStateEvent(
EventDirection.Send,
'org.matrix.msc3401.call.member',
`${userId}_${deviceId}_m.call`,
).raw,
);
capabilities.add(
WidgetEventCapability.forStateEvent(
EventDirection.Send,
'org.matrix.msc3401.call.member',
`_${userId}_${deviceId}`,
).raw,
);
capabilities.add(
WidgetEventCapability.forStateEvent(
EventDirection.Send,
'org.matrix.msc3401.call.member',
`${userId}_${deviceId}`,
).raw,
);
capabilities.add(
WidgetEventCapability.forStateEvent(EventDirection.Receive, 'org.matrix.msc3401.call.member')
.raw,
);
capabilities.add(
WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomCreate).raw,
);
[
'io.element.call.encryption_keys',
'org.matrix.rageshake_request',
EventType.Reaction,
EventType.RoomRedaction,
'io.element.call.reaction',
'org.matrix.msc4075.rtc.notification',
'org.matrix.msc4310.rtc.decline',
].forEach((type) => {
capabilities.add(WidgetEventCapability.forRoomEvent(EventDirection.Send, type).raw);
capabilities.add(WidgetEventCapability.forRoomEvent(EventDirection.Receive, type).raw);
});
[
EventType.CallInvite,
EventType.CallCandidates,
EventType.CallAnswer,
EventType.CallHangup,
EventType.CallReject,
EventType.CallSelectAnswer,
EventType.CallNegotiate,
EventType.CallSDPStreamMetadataChanged,
EventType.CallSDPStreamMetadataChangedPrefix,
EventType.CallReplaces,
EventType.CallEncryptionKeysPrefix,
].forEach((type) => {
capabilities.add(WidgetEventCapability.forToDeviceEvent(EventDirection.Send, type).raw);
capabilities.add(WidgetEventCapability.forToDeviceEvent(EventDirection.Receive, type).raw);
});
return capabilities;
}
/**
* Capability Delegation (`postMessage(msg, { delegate })`) ships only in
* Chromium; other engines silently ignore the option, so the frame would get
* no activation. `navigator.userAgentData` is likewise Chromium-only, which
* makes it the practical feature test.
*/
export const canDelegateCapability = (): boolean =>
typeof navigator !== 'undefined' && 'userAgentData' in navigator;