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
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
64 lines
2.6 KiB
TypeScript
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),
|
|
);
|