feat(calls): 'Missed call from X' line in the timeline (#162)
CI / Build & Quality Checks (push) Canceled after 0s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Secret scan (gitleaks) (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
CI / Build & Quality Checks (push) Canceled after 0s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Secret scan (gitleaks) (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
The ring event (m.rtc.notification) now renders — only once the ring is over (its lifetime lapsed or the caller hung up) and only if we did not join between the ring and the hang-up — as a red 'Missed call from bob' line with the ring's time, in the same style as the call-membership summaries. Derived entirely from events already in the timeline; nothing is sent; a call we answered shows nothing extra, and a later separate call cannot retro-actively 'answer' an earlier missed one. Verified headless in a DM: ring ignored → line appears once bob hangs up; a second call that alice answers adds no line and leaves the first one in place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
|||||||
Direction,
|
Direction,
|
||||||
EventTimeline,
|
EventTimeline,
|
||||||
EventTimelineSet,
|
EventTimelineSet,
|
||||||
|
EventType,
|
||||||
EventTimelineSetHandlerMap,
|
EventTimelineSetHandlerMap,
|
||||||
IContent,
|
IContent,
|
||||||
MatrixClient,
|
MatrixClient,
|
||||||
@@ -1908,6 +1909,84 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
</Event>
|
</Event>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
// [Gitea #162] A ring (m.rtc.notification) that we neither answered nor
|
||||||
|
// declined, and whose ring window has passed, renders as a local-only
|
||||||
|
// "Missed call from X" line. Derived from events already in the
|
||||||
|
// timeline; nothing is sent. Disappears if our own call membership
|
||||||
|
// follows within the ring window (we picked up).
|
||||||
|
[EventType.RTCNotification]: (mEventId, mEvent, item) => {
|
||||||
|
const senderId = mEvent.getSender() ?? '';
|
||||||
|
if (!senderId || senderId === mx.getUserId()) return null;
|
||||||
|
const content = mEvent.getContent();
|
||||||
|
const mention =
|
||||||
|
content['m.mentions']?.room ||
|
||||||
|
content['m.mentions']?.user_ids?.includes(mx.getSafeUserId());
|
||||||
|
if (!mention) return null;
|
||||||
|
const lifetime = Math.min(
|
||||||
|
typeof content.lifetime === 'number' ? content.lifetime : 30000,
|
||||||
|
120000,
|
||||||
|
);
|
||||||
|
const liveEvents = room.getLiveTimeline().getEvents();
|
||||||
|
const memberEventsAfter = liveEvents.filter(
|
||||||
|
(ev) => ev.getType() === StateEvent.GroupCallMemberPrefix && ev.getTs() >= mEvent.getTs(),
|
||||||
|
);
|
||||||
|
// The ring is over once its window lapsed or the caller hung up.
|
||||||
|
const callerEndTs = memberEventsAfter
|
||||||
|
.find((ev) => ev.getSender() === senderId && !ev.getContent().application)
|
||||||
|
?.getTs();
|
||||||
|
if (callerEndTs === undefined && Date.now() < mEvent.getTs() + lifetime) return null;
|
||||||
|
// "Answered" = our own join between the ring and the caller hanging up
|
||||||
|
// (a later, separate call must not retro-actively answer this one).
|
||||||
|
const windowEnd = Math.min(
|
||||||
|
mEvent.getTs() + lifetime + 60_000,
|
||||||
|
callerEndTs !== undefined ? callerEndTs + 5_000 : Infinity,
|
||||||
|
);
|
||||||
|
const answered = memberEventsAfter.some(
|
||||||
|
(ev) =>
|
||||||
|
ev.getSender() === mx.getUserId() &&
|
||||||
|
ev.getTs() <= windowEnd &&
|
||||||
|
!!ev.getContent().application,
|
||||||
|
);
|
||||||
|
if (answered) return null;
|
||||||
|
const highlighted = focusItem?.index === item && focusItem.highlight;
|
||||||
|
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||||
|
const timeJSX = (
|
||||||
|
<Time
|
||||||
|
ts={mEvent.getTs()}
|
||||||
|
compact={messageLayout === MessageLayout.Compact}
|
||||||
|
hour24Clock={hour24Clock}
|
||||||
|
dateFormatString={dateFormatString}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Event
|
||||||
|
key={mEvent.getId()}
|
||||||
|
data-message-item={item}
|
||||||
|
data-message-id={mEventId}
|
||||||
|
room={room}
|
||||||
|
mEvent={mEvent}
|
||||||
|
highlight={highlighted}
|
||||||
|
messageSpacing={messageSpacing}
|
||||||
|
canDelete={false}
|
||||||
|
hideReadReceipts={hideActivity}
|
||||||
|
showDeveloperTools={showDeveloperTools}
|
||||||
|
>
|
||||||
|
<EventContent
|
||||||
|
messageLayout={messageLayout}
|
||||||
|
time={timeJSX}
|
||||||
|
iconSrc={Icons.PhoneDown}
|
||||||
|
content={
|
||||||
|
<Box grow="Yes" direction="Column">
|
||||||
|
<Text size="T300" style={{ color: color.Critical.Main }}>
|
||||||
|
Missed {content['m.call.intent'] === 'video' ? 'video ' : ''}call from{' '}
|
||||||
|
<b>{senderName}</b>
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Event>
|
||||||
|
);
|
||||||
|
},
|
||||||
[StateEvent.GroupCallMemberPrefix]: (mEventId, mEvent, item) => {
|
[StateEvent.GroupCallMemberPrefix]: (mEventId, mEvent, item) => {
|
||||||
const highlighted = focusItem?.index === item && focusItem.highlight;
|
const highlighted = focusItem?.index === item && focusItem.highlight;
|
||||||
const senderId = mEvent.getSender() ?? '';
|
const senderId = mEvent.getSender() ?? '';
|
||||||
|
|||||||
Reference in New Issue
Block a user