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 | 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, 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, myUserId: string): boolean => room .getLiveTimeline() .getEvents() .some( (ev) => ev.getType() === 'org.matrix.msc4310.rtc.decline' && ev.getSender() === myUserId && ids.has(ev.getRelation()?.event_id), );