Files
cinny/src/app/utils/callRing.ts
T
Lotus CIandClaude Opus 5.5 a425aa3572
CI / Build & Quality Checks (pull_request) Successful in 3m8s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 7s
CI / Playwright smoke (e2e) (pull_request) Failing after 18m4s
fix(calls): Reject sends the decline to the ring notification (#313)
handleReject related the MSC4310 decline to the caller's call.member event
(the notification's reference), but MSC4310 and Element Call match a
decline against the rtc.notification event itself, so the caller kept
"Calling…" for the full 90 s after being told "Declined", and declines
from other clients (correctly referencing the notification) didn't stop
Lotus ringing on other devices.

- The incoming call keeps the notification's own event id; Reject relates
  the decline to it.
- The "declined on another device" dismissal and declinedRing accept
  either id (older Lotus builds referenced the membership).
- The missed-call line is skipped for a call we declined.

Verified headless (DM, Bob rings, Reject): the decline used to reference
the membership; it now references the notification. Unit test added.

Fixes #313

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-17 18:01:06 -04:00

64 lines
2.6 KiB
TypeScript

import { JoinRule, Room } from 'matrix-js-sdk';
import { StateEvent } from '../../types/matrix/room';
import { getStateEvents } from './room';
/**
* Whether a call notification in this room may ring — and, by the same rule,
* whether an unanswered one may show as "Missed call" (Gitea #162 follow-up).
* Only DMs and invite-only groups outside any space ring, i.e. rooms where the
* call button is shown. Persistent voice rooms, space channels, restricted and
* public rooms never do: joining a voice channel alone is normal and isn't a
* "call" anyone missed.
*/
export const isRingableRoom = (room: Room, isDirect: boolean): boolean => {
if (room.isCallRoom()) return false;
if (isDirect) return true;
const isSpaceChild = getStateEvents(room, StateEvent.SpaceParent).length > 0;
return !isSpaceChild && room.getJoinRule() === JoinRule.Invite;
};
/** A call-membership state event that is still "in the call" (left = empty content). */
const isActiveMembership = (content: Record<string, unknown> | undefined): boolean =>
!!content &&
(typeof content.application === 'string' ||
(Array.isArray(content.memberships) && content.memberships.length > 0));
/**
* [Gitea #312] Whether `userId` is in a call in `room` according to the room's
* current STATE. After a reload the MatrixRTC session can be stale (a join and
* leave replayed in one catch-up batch), but the state is right.
*/
export const hasActiveCallMembership = (room: Room, userId: string): boolean =>
getStateEvents(room, StateEvent.GroupCallMemberPrefix).some(
(ev) =>
(ev.getSender() === userId || ev.getStateKey()?.includes(userId)) &&
isActiveMembership(ev.getContent()),
);
/**
* [Gitea #312] Whether we already handled the ring `refEventId` points at:
* declined it (our own RTCDecline relates to it) — e.g. replayed together with
* the ring in the first sync after a restart, before any dismiss listener.
*/
export const declinedRing = (
room: Room,
ringIds: string | Array<string | undefined>,
myUserId: string,
): boolean => {
// [Gitea #313] A decline references the notification event (MSC4310); older
// Lotus builds referenced the call membership instead. Match either.
const ids = new Set((Array.isArray(ringIds) ? ringIds : [ringIds]).filter(Boolean));
return declinedAny(room, ids, myUserId);
};
const declinedAny = (room: Room, ids: Set<string | undefined>, myUserId: string): boolean =>
room
.getLiveTimeline()
.getEvents()
.some(
(ev) =>
ev.getType() === 'org.matrix.msc4310.rtc.decline' &&
ev.getSender() === myUserId &&
ids.has(ev.getRelation()?.event_id),
);