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