diff --git a/src/app/components/CallEmbedProvider.tsx b/src/app/components/CallEmbedProvider.tsx index 953249de3..4a4130de6 100644 --- a/src/app/components/CallEmbedProvider.tsx +++ b/src/app/components/CallEmbedProvider.tsx @@ -111,6 +111,8 @@ type IncomingCallInfo = { intent?: string; notificationType: RTCNotificationType; refEventId: string; + /** [Gitea #313] The rtc.notification event itself: what a decline must reference (MSC4310). */ + notificationEventId?: string; }; type IncomingCallProps = { dm: boolean; @@ -283,7 +285,9 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr fill="Soft" size="400" radii="400" - onClick={() => (dm ? onReject(room, info.refEventId) : onIgnore())} + onClick={() => + dm ? onReject(room, info.notificationEventId ?? info.refEventId) : onIgnore() + } before={} > @@ -455,7 +459,9 @@ function IncomingCallBanner({ dm, info, onIgnore, onAnswer, onReject }: Incoming size="300" radii="300" outlined - onClick={() => (dm ? onReject(room, info.refEventId) : onIgnore())} + onClick={() => + dm ? onReject(room, info.notificationEventId ?? info.refEventId) : onIgnore() + } before={} > @@ -597,6 +603,7 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps) : undefined, notificationType, refEventId, + notificationEventId: event.getId(), }; // [Gitea #312] Don't ring for a call that is already over or handled: after @@ -605,7 +612,10 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps) // stale RTC session). The caller's membership can arrive just after the // ring, so an absent one gets a short grace period before giving up. const myUserId = mx.getSafeUserId(); - if (declinedRing(room, refEventId, myUserId) || hasActiveCallMembership(room, myUserId)) { + if ( + declinedRing(room, [refEventId, event.getId()], myUserId) || + hasActiveCallMembership(room, myUserId) + ) { return; } if (hasActiveCallMembership(room, sender)) { @@ -614,7 +624,10 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps) } setTimeout(() => { if (Date.now() >= senderTs + lifetime) return; - if (declinedRing(room, refEventId, myUserId) || hasActiveCallMembership(room, myUserId)) { + if ( + declinedRing(room, [refEventId, event.getId()], myUserId) || + hasActiveCallMembership(room, myUserId) + ) { return; } if (hasActiveCallMembership(room, sender)) setCallInfo(info); @@ -687,7 +700,10 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps) if (isFromFilteredTimelineSet(data)) return; // [Gitea #259] if (event.getType() === EventType.RTCDecline && event.getSender() === myUserId) { const related = event.getRelation()?.event_id; - if (!related || related === refEventId) dismiss(); // declined elsewhere + // [Gitea #313] Correct clients reference the notification; older Lotus + // builds referenced the call membership. Accept both. + if (!related || related === refEventId || related === callInfo.notificationEventId) + dismiss(); // declined elsewhere } }; session.on(MatrixRTCSessionEvent.MembershipsChanged, checkMemberships); diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 5a2e70f08..f75277358 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -139,7 +139,7 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useIgnoredUsers } from '../../hooks/useIgnoredUsers'; import { useImagePackRooms } from '../../hooks/useImagePackRooms'; import { useIsDirectRoom } from '../../hooks/useRoom'; -import { isRingableRoom } from '../../utils/callRing'; +import { declinedRing, isRingableRoom } from '../../utils/callRing'; import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile'; import { useSpaceOptionally } from '../../hooks/useSpace'; import { useRoomCreators } from '../../hooks/useRoomCreators'; @@ -2041,6 +2041,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli // Same rule as ringing: no "Missed call" in voice rooms or space // channels, where starting a call is just joining the channel. if (!isRingableRoom(room, direct)) return null; + // [Gitea #313] A call we declined isn't a missed call. + const ringRef = mEvent.getRelation()?.event_id; + if (declinedRing(room, [mEventId, ringRef], mx.getSafeUserId())) return null; const content = mEvent.getContent(); const mention = content['m.mentions']?.room || diff --git a/src/app/utils/callRing.test.ts b/src/app/utils/callRing.test.ts index ac96ae3ee..bf94215a9 100644 --- a/src/app/utils/callRing.test.ts +++ b/src/app/utils/callRing.test.ts @@ -67,3 +67,18 @@ test('declinedRing finds our own decline of that ring only (#312)', () => { assert.equal(declinedRing(room, '$ring2', '@me:x'), false, "someone else's decline"); assert.equal(declinedRing(room, '$ring3', '@me:x'), false); }); + +test('declinedRing matches a decline of the notification or of the membership (#313)', () => { + const decline = (rel: string) => ({ + getType: () => 'org.matrix.msc4310.rtc.decline', + getSender: () => '@me:x', + getRelation: () => ({ event_id: rel }), + }); + const room = stateRoom([], [decline('$notification')]); + assert.equal(declinedRing(room, ['$membership', '$notification'], '@me:x'), true); + assert.equal(declinedRing(room, ['$membership'], '@me:x'), false); + assert.equal( + declinedRing(stateRoom([], [decline('$membership')]), ['$membership', undefined], '@me:x'), + true, + ); +}); diff --git a/src/app/utils/callRing.ts b/src/app/utils/callRing.ts index adbdd5e42..370ca1bcb 100644 --- a/src/app/utils/callRing.ts +++ b/src/app/utils/callRing.ts @@ -40,7 +40,18 @@ export const hasActiveCallMembership = (room: Room, userId: string): boolean => * 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, refEventId: string, myUserId: string): boolean => +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() @@ -48,5 +59,5 @@ export const declinedRing = (room: Room, refEventId: string, myUserId: string): (ev) => ev.getType() === 'org.matrix.msc4310.rtc.decline' && ev.getSender() === myUserId && - ev.getRelation()?.event_id === refEventId, + ids.has(ev.getRelation()?.event_id), );