diff --git a/src/app/components/CallEmbedProvider.tsx b/src/app/components/CallEmbedProvider.tsx
index 17e054da5..4d3b614bf 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={}
>
@@ -596,6 +602,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
@@ -604,7 +611,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)) {
@@ -613,7 +623,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);
@@ -685,7 +698,10 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
if (eventRoom?.roomId !== room.roomId || !data.liveEvent) return;
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 2500feda6..9512232e1 100644
--- a/src/app/features/room/RoomTimeline.tsx
+++ b/src/app/features/room/RoomTimeline.tsx
@@ -138,7 +138,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';
@@ -2037,6 +2037,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),
);