dm calls mvp: phase 2: handle ring/decline + timeline render in encrypted DMs
This commit is contained in:
parent
79bd0ccc4d
commit
6ced8246e6
3 changed files with 198 additions and 129 deletions
|
|
@ -1126,6 +1126,18 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
const { replyEventId, threadRootId } = mEvent;
|
||||
const highlighted = focusItem?.index === item && focusItem.highlight;
|
||||
|
||||
return (
|
||||
<EncryptedContent mEvent={mEvent}>
|
||||
{() => {
|
||||
// §5.9: after decrypt, DM-call service events still route through
|
||||
// this branch (outer typeToRenderer dispatched on the pre-decrypt
|
||||
// 'm.room.encrypted' type). Drop the whole row instead of falling
|
||||
// through to MessageUnsupportedContent. Keys mirror the hardcoded
|
||||
// literals in the outer filter — migrate together (§5.19).
|
||||
const decryptedType = mEvent.getType();
|
||||
if (decryptedType === 'org.matrix.msc4075.rtc.notification') return null;
|
||||
if (decryptedType === 'org.matrix.msc4310.rtc.decline') return null;
|
||||
|
||||
return (
|
||||
<Message
|
||||
key={mEvent.getId()}
|
||||
|
|
@ -1182,10 +1194,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
>
|
||||
<EncryptedContent mEvent={mEvent}>
|
||||
{() => {
|
||||
{(() => {
|
||||
if (mEvent.isRedacted()) return <RedactedContent />;
|
||||
if (mEvent.getType() === MessageEvent.Sticker)
|
||||
if (decryptedType === MessageEvent.Sticker)
|
||||
return (
|
||||
<MSticker
|
||||
content={mEvent.getContent()}
|
||||
|
|
@ -1199,7 +1210,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
)}
|
||||
/>
|
||||
);
|
||||
if (mEvent.getType() === MessageEvent.RoomMessage) {
|
||||
if (decryptedType === MessageEvent.RoomMessage) {
|
||||
const editedEvent = getEditedEvent(mEventId, mEvent, timelineSet);
|
||||
const getContent = (() =>
|
||||
editedEvent?.getContent()['m.new_content'] ??
|
||||
|
|
@ -1207,7 +1218,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
|
||||
const senderId = mEvent.getSender() ?? '';
|
||||
const senderDisplayName =
|
||||
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
|
||||
getMemberDisplayName(room, senderId) ??
|
||||
getMxIdLocalPart(senderId) ??
|
||||
senderId;
|
||||
return (
|
||||
<RenderMessageContent
|
||||
displayName={senderDisplayName}
|
||||
|
|
@ -1223,7 +1236,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
/>
|
||||
);
|
||||
}
|
||||
if (mEvent.getType() === MessageEvent.RoomMessageEncrypted)
|
||||
if (decryptedType === MessageEvent.RoomMessageEncrypted)
|
||||
return (
|
||||
<Text>
|
||||
<MessageNotDecryptedContent />
|
||||
|
|
@ -1234,9 +1247,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
<MessageUnsupportedContent />
|
||||
</Text>
|
||||
);
|
||||
})()}
|
||||
</Message>
|
||||
);
|
||||
}}
|
||||
</EncryptedContent>
|
||||
</Message>
|
||||
);
|
||||
},
|
||||
[MessageEvent.Sticker]: (mEventId, mEvent, item, timelineSet, collapse) => {
|
||||
|
|
|
|||
|
|
@ -11,18 +11,27 @@
|
|||
// 4. Peer membership flaps on LiveKit reconnect — grace absorbs the blip.
|
||||
//
|
||||
// KNOWN GAPS (also in dm_calls_techdebt.md):
|
||||
// §5.9 Does NOT handle encrypted DMs. `ev.getType()` returns
|
||||
// 'm.room.encrypted' on first fire; we early-exit. Needs
|
||||
// `MatrixEventEvent.Decrypted` listener + `decryptEventIfNeeded`,
|
||||
// pattern authoritative in CallEmbed.ts:233-234.
|
||||
// §5.16 Decline is not tied to our specific ring event id. Any RTCDecline
|
||||
// from peer in this room kills our call.
|
||||
// §5.18 Grace constants are empirically chosen, may need tuning with
|
||||
// real-world /sync + LiveKit reconnect metrics.
|
||||
//
|
||||
// Encrypted DMs: RTCDecline arrives as m.room.encrypted first and Timeline
|
||||
// does not re-emit post-decrypt (matrix-js-sdk 38.2). We mirror the
|
||||
// CallEmbed.ts:233-234 pattern — listen to both Timeline (kick decryption)
|
||||
// and MatrixEventEvent.Decrypted. `performHangup` is guarded by `disposed`,
|
||||
// so double delivery for cleartext rooms is a no-op.
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { EventType, RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk';
|
||||
import {
|
||||
EventType,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
MatrixEventHandlerMap,
|
||||
RoomEvent,
|
||||
RoomEventHandlerMap,
|
||||
} from 'matrix-js-sdk';
|
||||
import { CallMembership } from 'matrix-js-sdk/lib/matrixrtc/CallMembership';
|
||||
import { MatrixRTCSessionEvent } from 'matrix-js-sdk/lib/matrixrtc/MatrixRTCSession';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
|
|
@ -99,16 +108,35 @@ export const useCallerAutoHangup = (): void => {
|
|||
}
|
||||
};
|
||||
|
||||
const onTimeline: RoomEventHandlerMap[RoomEvent.Timeline] = (ev, room, _s, _r, data) => {
|
||||
if (!data.liveEvent || !room) return;
|
||||
if (room.roomId !== roomId) return;
|
||||
const maybeHangupOnDecline = (ev: MatrixEvent): void => {
|
||||
// onDecrypted fires for every decrypted event client-wide; gate on room
|
||||
// first to avoid walking type/sender on unrelated rooms.
|
||||
if (ev.getRoomId() !== roomId) return;
|
||||
if (ev.getType() !== EventType.RTCDecline) return;
|
||||
if (ev.getSender() === selfId) return;
|
||||
performHangup();
|
||||
};
|
||||
|
||||
const onTimeline: RoomEventHandlerMap[RoomEvent.Timeline] = (ev, room, _s, _r, data) => {
|
||||
if (!data.liveEvent || !room) return;
|
||||
if (room.roomId !== roomId) return;
|
||||
// Encrypted RTCDecline lands as m.room.encrypted here; let the Decrypted
|
||||
// handler see it once cleartext is available.
|
||||
if (ev.isEncrypted()) {
|
||||
mx.decryptEventIfNeeded(ev).catch(() => {});
|
||||
return;
|
||||
}
|
||||
maybeHangupOnDecline(ev);
|
||||
};
|
||||
|
||||
const onDecrypted: MatrixEventHandlerMap[MatrixEventEvent.Decrypted] = (ev, err) => {
|
||||
if (err) return;
|
||||
maybeHangupOnDecline(ev);
|
||||
};
|
||||
|
||||
session.on(MatrixRTCSessionEvent.MembershipsChanged, onMemberships);
|
||||
mx.on(RoomEvent.Timeline, onTimeline);
|
||||
mx.on(MatrixEventEvent.Decrypted, onDecrypted);
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
|
|
@ -116,6 +144,7 @@ export const useCallerAutoHangup = (): void => {
|
|||
clearPeerLeaveTimer();
|
||||
session.off(MatrixRTCSessionEvent.MembershipsChanged, onMemberships);
|
||||
mx.removeListener(RoomEvent.Timeline, onTimeline);
|
||||
mx.removeListener(MatrixEventEvent.Decrypted, onDecrypted);
|
||||
};
|
||||
}, [mx, callEmbed, mDirect, setCallEmbed]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
// Incoming DM ring: watches `m.rtc.notification` in the live timeline and
|
||||
// populates `incomingCallsAtom` so the bottom strip can render.
|
||||
//
|
||||
// KNOWN GAP §5.9 (blocks Phase 2 commit): encrypted DMs.
|
||||
// `RoomEvent.Timeline` fires once with `ev.getType() === 'm.room.encrypted'`
|
||||
// (verified in matrix-js-sdk 38.2 event-timeline-set.js:563, no re-emit post
|
||||
// decrypt). Our `!== EventType.RTCNotification` filter early-exits and the
|
||||
// ring never reaches the strip. Authoritative fix pattern: also
|
||||
// `mx.on(MatrixEventEvent.Decrypted, h)` and `mx.decryptEventIfNeeded(ev)` —
|
||||
// see CallEmbed.ts:233-234 for the reference implementation.
|
||||
// Encrypted DMs (§5.9): `RoomEvent.Timeline` fires once with
|
||||
// `ev.getType() === 'm.room.encrypted'` and is never re-emitted after decrypt
|
||||
// (matrix-js-sdk 38.2 event-timeline-set.js:563). We listen to both Timeline
|
||||
// and `MatrixEventEvent.Decrypted` and kick decryption from the Timeline
|
||||
// handler — mirrors CallEmbed.ts:233-234. Dedup relies on `registry.has(key)`
|
||||
// for RTCNotification and on idempotent `removeByNotifId` for RTCDecline, so
|
||||
// double delivery (Timeline for cleartext + Decrypted for encrypted) is safe.
|
||||
//
|
||||
// The `registryRef` sync-effect below handles an asymmetry §5.6: REMOVE on the
|
||||
// atom can come from outside the hook (strip buttons), and we need to drop
|
||||
|
|
@ -20,6 +20,8 @@ import {
|
|||
EventType,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
MatrixEventHandlerMap,
|
||||
RelationType,
|
||||
Room,
|
||||
RoomEvent,
|
||||
|
|
@ -204,15 +206,7 @@ export const useIncomingRtcNotifications = (): void => {
|
|||
return setTimeout(() => removeByKey(key), delay);
|
||||
};
|
||||
|
||||
const handleTimeline: RoomEventHandlerMap[RoomEvent.Timeline] = async (
|
||||
ev,
|
||||
room,
|
||||
_toStartOfTimeline,
|
||||
_removed,
|
||||
data
|
||||
) => {
|
||||
if (!data.liveEvent || !room) return;
|
||||
|
||||
const processEvent = async (ev: MatrixEvent, room: Room): Promise<void> => {
|
||||
if (ev.getType() === EventType.RTCDecline) {
|
||||
const rel = ev.getRelation();
|
||||
if (rel?.event_id) removeByNotifId(rel.event_id);
|
||||
|
|
@ -269,6 +263,35 @@ export const useIncomingRtcNotifications = (): void => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleTimeline: RoomEventHandlerMap[RoomEvent.Timeline] = (
|
||||
ev,
|
||||
room,
|
||||
_toStartOfTimeline,
|
||||
_removed,
|
||||
data
|
||||
) => {
|
||||
if (!data.liveEvent || !room) return;
|
||||
// Encrypted events land here as m.room.encrypted; kick decryption and let
|
||||
// the Decrypted handler pick them up once the cleartext is available.
|
||||
if (ev.isEncrypted()) {
|
||||
mx.decryptEventIfNeeded(ev).catch(() => {});
|
||||
return;
|
||||
}
|
||||
processEvent(ev, room);
|
||||
};
|
||||
|
||||
const handleDecrypted: MatrixEventHandlerMap[MatrixEventEvent.Decrypted] = (ev, err) => {
|
||||
if (err) return;
|
||||
const roomId = ev.getRoomId();
|
||||
if (!roomId) return;
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) return;
|
||||
// No liveEvent flag on Decrypted. Backfill safety relies on
|
||||
// `isRtcNotificationExpired` (ring branch) and `registry.has(key)` dedup;
|
||||
// a stale decline just no-ops via `removeByNotifId`.
|
||||
processEvent(ev, room);
|
||||
};
|
||||
|
||||
const handleRedaction: RoomEventHandlerMap[RoomEvent.Redaction] = (ev) => {
|
||||
const redacted = ev.event.redacts;
|
||||
if (redacted) removeByNotifId(redacted);
|
||||
|
|
@ -279,11 +302,13 @@ export const useIncomingRtcNotifications = (): void => {
|
|||
};
|
||||
|
||||
mx.on(RoomEvent.Timeline, handleTimeline);
|
||||
mx.on(MatrixEventEvent.Decrypted, handleDecrypted);
|
||||
mx.on(RoomEvent.Redaction, handleRedaction);
|
||||
mx.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, handleSessionEnded);
|
||||
|
||||
return () => {
|
||||
mx.removeListener(RoomEvent.Timeline, handleTimeline);
|
||||
mx.removeListener(MatrixEventEvent.Decrypted, handleDecrypted);
|
||||
mx.removeListener(RoomEvent.Redaction, handleRedaction);
|
||||
mx.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, handleSessionEnded);
|
||||
registry.forEach((entry) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue