diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index fd58a6aa9..eb3f1b919 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -14,6 +14,7 @@ import { Direction, EventTimeline, EventTimelineSet, + EventType, EventTimelineSetHandlerMap, IContent, MatrixClient, @@ -1908,6 +1909,84 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli ); }, + // [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 = ( +