fix+feat: bug fixes, deleted message placeholder, poll display

Bug fixes:
- IncomingCallNotification: track ring setTimeout ID and clear it on
  cleanup/dismiss — prevents orphaned callbacks after unmount
- RoomTimeline: allow redacted m.room.message, m.room.encrypted and
  m.sticker events past the early-return filter so they hit the
  existing RedactedContent renderer showing the trash-icon placeholder

New features:
- PollContent component: read-only display of m.poll.start and
  org.matrix.msc3381.poll.start events (both stable Matrix 1.7 and
  MSC3381 unstable content keys); renders poll question + answer
  options inside the standard Message bubble; registered both as
  top-level event renderers and inside EncryptedContent callback
  so encrypted polls also render after decryption
- Deleted message placeholder: m.room.message and m.room.encrypted
  redacted events now show the existing MessageDeletedContent
  component (trash icon + italic notice) instead of disappearing
  entirely — matches Element, FluffyChat, Commet, Nheko behaviour
This commit is contained in:
root
2026-05-15 00:47:21 -04:00
parent c6fedb7997
commit 0a13f3cb68
4 changed files with 218 additions and 5 deletions
+120 -1
View File
@@ -63,6 +63,7 @@ import {
MessageNotDecryptedContent,
RedactedContent,
MSticker,
PollContent,
ImageContent,
EventContent,
} from '../../components/message';
@@ -1217,6 +1218,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
/>
);
}
if (
mEvent.getType() === 'm.poll.start' ||
mEvent.getType() === 'org.matrix.msc3381.poll.start'
)
return <PollContent content={mEvent.getContent()} />;
if (mEvent.getType() === MessageEvent.RoomMessageEncrypted)
return (
<Text>
@@ -1297,6 +1303,112 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
</Message>
);
},
'org.matrix.msc3381.poll.start': (mEventId, mEvent, item, timelineSet, collapse) => {
const reactionRelations = getEventReactions(timelineSet, mEventId);
const reactions = reactionRelations && reactionRelations.getSortedAnnotationsByKey();
const hasReactions = reactions && reactions.length > 0;
const highlighted = focusItem?.index === item && focusItem.highlight;
return (
<Message
key={mEvent.getId()}
data-message-item={item}
data-message-id={mEventId}
room={room}
mEvent={mEvent}
messageSpacing={messageSpacing}
messageLayout={messageLayout}
collapse={collapse}
highlight={highlighted}
canDelete={canRedact || (canDeleteOwn && mEvent.getSender() === mx.getUserId())}
canSendReaction={canSendReaction}
canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms}
relations={hasReactions ? reactionRelations : undefined}
onUserClick={handleUserClick}
onUsernameClick={handleUsernameClick}
onReplyClick={handleReplyClick}
onReactionToggle={handleReactionToggle}
reactions={
reactionRelations && (
<Reactions
style={{ marginTop: config.space.S200 }}
room={room}
relations={reactionRelations}
mEventId={mEventId}
canSendReaction={canSendReaction}
onReactionToggle={handleReactionToggle}
/>
)
}
hideReadReceipts={hideActivity}
showDeveloperTools={showDeveloperTools}
memberPowerTag={getMemberPowerTag(mEvent.getSender() ?? '')}
accessibleTagColors={accessiblePowerTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
>
{mEvent.isRedacted() ? (
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
) : (
<PollContent content={mEvent.getContent()} />
)}
</Message>
);
},
'm.poll.start': (mEventId, mEvent, item, timelineSet, collapse) => {
const reactionRelations = getEventReactions(timelineSet, mEventId);
const reactions = reactionRelations && reactionRelations.getSortedAnnotationsByKey();
const hasReactions = reactions && reactions.length > 0;
const highlighted = focusItem?.index === item && focusItem.highlight;
return (
<Message
key={mEvent.getId()}
data-message-item={item}
data-message-id={mEventId}
room={room}
mEvent={mEvent}
messageSpacing={messageSpacing}
messageLayout={messageLayout}
collapse={collapse}
highlight={highlighted}
canDelete={canRedact || (canDeleteOwn && mEvent.getSender() === mx.getUserId())}
canSendReaction={canSendReaction}
canPinEvent={canPinEvent}
imagePackRooms={imagePackRooms}
relations={hasReactions ? reactionRelations : undefined}
onUserClick={handleUserClick}
onUsernameClick={handleUsernameClick}
onReplyClick={handleReplyClick}
onReactionToggle={handleReactionToggle}
reactions={
reactionRelations && (
<Reactions
style={{ marginTop: config.space.S200 }}
room={room}
relations={reactionRelations}
mEventId={mEventId}
canSendReaction={canSendReaction}
onReactionToggle={handleReactionToggle}
/>
)
}
hideReadReceipts={hideActivity}
showDeveloperTools={showDeveloperTools}
memberPowerTag={getMemberPowerTag(mEvent.getSender() ?? '')}
accessibleTagColors={accessiblePowerTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
>
{mEvent.isRedacted() ? (
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
) : (
<PollContent content={mEvent.getContent()} />
)}
</Message>
);
},
[StateEvent.RoomMember]: (mEventId, mEvent, item) => {
const membershipChanged = isMembershipChanged(mEvent);
if (membershipChanged && hideMembershipEvents) return null;
@@ -1639,7 +1751,14 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
return null;
}
if (mEvent.isRedacted() && !showHiddenEvents) {
return null;
const t = mEvent.getType();
if (
t !== MessageEvent.RoomMessage &&
t !== MessageEvent.RoomMessageEncrypted &&
t !== MessageEvent.Sticker
) {
return null;
}
}
if (!newDivider && readUptoEventIdRef.current) {