Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-redacted-state-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Display redacted state events (membership, room name/topic/avatar, pins and other state events) as a "deleted" tombstone in the timeline instead of rendering them with empty/redacted content or hiding them, so the reply-to of a redaction highlights the redacted event.
193 changes: 117 additions & 76 deletions src/app/hooks/timeline/useTimelineEventRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1178,9 +1178,10 @@ export function useTimelineEventRenderer({
if (membershipChanged && hideMembershipEvents) return null;
if (!membershipChanged && hideNickAvatarEvents) return null;

const isRedacted = mEvent.isRedacted();
const highlighted = focusItem?.index === item && focusItem.highlight;
const marked = activeReplyId === mEventId && !suppressMark;
const parsed = parseMemberEvent(mEvent);
const parsed = isRedacted ? null : parseMemberEvent(mEvent);

const timeJSX = (
<Time
Expand Down Expand Up @@ -1259,19 +1260,24 @@ export function useTimelineEventRenderer({
<EventContent
messageLayout={messageLayout}
time={timeJSX}
icon={parsed.icon}
icon={parsed?.icon ?? timelineIcon(Trash)}
content={
<Text size="T300" priority="300">
<Box direction="Row" style={{ flexWrap: 'wrap', columnGap: toRem(6) }}>
{parsed.body}
</Box>
</Text>
isRedacted ? (
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
) : (
<Text size="T300" priority="300">
<Box direction="Row" style={{ flexWrap: 'wrap', columnGap: toRem(6) }}>
{parsed?.body}
</Box>
</Text>
)
}
/>
</Message>
);
},
[EventType.RoomName]: (mEventId, mEvent, item, timelineSet, collapse) => {
const isRedacted = mEvent.isRedacted();
const highlighted = focusItem?.index === item && focusItem.highlight;
const marked = activeReplyId === mEventId && !suppressMark;
const senderId = mEvent.getSender() ?? '';
Expand Down Expand Up @@ -1353,20 +1359,25 @@ export function useTimelineEventRenderer({
<EventContent
messageLayout={messageLayout}
time={timeJSX}
icon={timelineIcon(Hash)}
icon={isRedacted ? timelineIcon(Trash) : timelineIcon(Hash)}
content={
<Box grow="Yes" direction="Column">
<Text size="T300" priority="300">
<DecoratedUser userId={senderId} userName={senderName} room={room} />
{t('Organisms.RoomCommon.changed_room_name')}
</Text>
</Box>
isRedacted ? (
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
) : (
<Box grow="Yes" direction="Column">
<Text size="T300" priority="300">
<DecoratedUser userId={senderId} userName={senderName} room={room} />
{t('Organisms.RoomCommon.changed_room_name')}
</Text>
</Box>
)
}
/>
</Message>
);
},
[EventType.RoomTopic]: (mEventId, mEvent, item, timelineSet, collapse) => {
const isRedacted = mEvent.isRedacted();
const highlighted = focusItem?.index === item && focusItem.highlight;
const marked = activeReplyId === mEventId && !suppressMark;
const senderId = mEvent.getSender() ?? '';
Expand Down Expand Up @@ -1449,20 +1460,25 @@ export function useTimelineEventRenderer({
<EventContent
messageLayout={messageLayout}
time={timeJSX}
icon={timelineIcon(Hash)}
icon={isRedacted ? timelineIcon(Trash) : timelineIcon(Hash)}
content={
<Box grow="Yes" direction="Column">
<Text size="T300" priority="300">
<DecoratedUser userId={senderId} userName={senderName} room={room} />
{' changed room topic'}
</Text>
</Box>
isRedacted ? (
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
) : (
<Box grow="Yes" direction="Column">
<Text size="T300" priority="300">
<DecoratedUser userId={senderId} userName={senderName} room={room} />
{' changed room topic'}
</Text>
</Box>
)
}
/>
</Message>
);
},
[EventType.RoomAvatar]: (mEventId, mEvent, item, timelineSet, collapse) => {
const isRedacted = mEvent.isRedacted();
const highlighted = focusItem?.index === item && focusItem.highlight;
const marked = activeReplyId === mEventId && !suppressMark;
const senderId = mEvent.getSender() ?? '';
Expand Down Expand Up @@ -1545,20 +1561,25 @@ export function useTimelineEventRenderer({
<EventContent
messageLayout={messageLayout}
time={timeJSX}
icon={timelineIcon(Hash)}
icon={isRedacted ? timelineIcon(Trash) : timelineIcon(Hash)}
content={
<Box grow="Yes" direction="Column">
<Text size="T300" priority="300">
<DecoratedUser userId={senderId} userName={senderName} room={room} />
{' changed room avatar'}
</Text>
</Box>
isRedacted ? (
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
) : (
<Box grow="Yes" direction="Column">
<Text size="T300" priority="300">
<DecoratedUser userId={senderId} userName={senderName} room={room} />
{' changed room avatar'}
</Text>
</Box>
)
}
/>
</Message>
);
},
[EventType.GroupCallMemberPrefix]: (mEventId, mEvent, item, timelineSet, collapse) => {
const isRedacted = mEvent.isRedacted();
const highlighted = focusItem?.index === item && focusItem.highlight;
const marked = activeReplyId === mEventId && !suppressMark;
const senderId = mEvent.getSender() ?? '';
Expand Down Expand Up @@ -1648,14 +1669,24 @@ export function useTimelineEventRenderer({
<EventContent
messageLayout={messageLayout}
time={timeJSX}
icon={callJoined ? timelineIcon(Phone) : timelineIcon(PhoneDisconnect)}
icon={
isRedacted
? timelineIcon(Trash)
: callJoined
? timelineIcon(Phone)
: timelineIcon(PhoneDisconnect)
}
content={
<Box grow="Yes" direction="Column">
<Text size="T300" priority="300">
<DecoratedUser userId={senderId} userName={senderName} room={room} />
{callJoined ? ' joined the call' : ' ended the call'}
</Text>
</Box>
isRedacted ? (
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
) : (
<Box grow="Yes" direction="Column">
<Text size="T300" priority="300">
<DecoratedUser userId={senderId} userName={senderName} room={room} />
{callJoined ? ' joined the call' : ' ended the call'}
</Text>
</Box>
)
}
/>
</Message>
Expand Down Expand Up @@ -1990,6 +2021,7 @@ export function useTimelineEventRenderer({
},
[EventType.RoomPinnedEvents]: (mEventId, mEvent, item, timelineSet, collapse) => {
if (!hiddenEventOther) return null;
const isRedacted = mEvent.isRedacted();
const highlighted = focusItem?.index === item && focusItem.highlight;
const marked = activeReplyId === mEventId && !suppressMark;
const senderId = mEvent.getSender() ?? '';
Expand Down Expand Up @@ -2083,39 +2115,43 @@ export function useTimelineEventRenderer({
<EventContent
messageLayout={messageLayout}
time={timeJSX}
icon={timelineIcon(PushPin)}
icon={isRedacted ? timelineIcon(Trash) : timelineIcon(PushPin)}
content={
<Box grow="Yes" direction="Column">
<Text size="T300" priority="300">
<DecoratedUser userId={senderId} userName={senderName} room={room} />
{(pinsAdded?.length > 0 &&
`pinned ${pinsAdded.length} message${pinsAdded.length > 1 ? 's' : ''}`) ||
''}
{(pinsAdded?.length > 0 && pinsRemoved?.length > 0 && ` and `) || ''}
{(pinsRemoved?.length > 0 &&
`unpinned ${pinsRemoved.length} message${
pinsRemoved.length > 1 ? 's' : ''
}`) ||
''}
{((!pinsAdded || pinsAdded.length <= 0) &&
(!pinsRemoved || pinsRemoved.length <= 0) &&
`has not changed the pins`) ||
`:`}
</Text>
{pinPreviewIds.length > 0 &&
pinPreviewIds
.slice(0, 4)
.map((x: string) => (
<Reply
key={x}
style={{ opacity: '80%' }}
room={room}
replyEventId={x}
onClick={handleOpenReply}
replyIcon={<>{menuIcon(pinnedSet.has(x) ? PushPin : PushPinSlash)}</>}
/>
))}
</Box>
isRedacted ? (
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
) : (
<Box grow="Yes" direction="Column">
<Text size="T300" priority="300">
<DecoratedUser userId={senderId} userName={senderName} room={room} />
{(pinsAdded?.length > 0 &&
`pinned ${pinsAdded.length} message${pinsAdded.length > 1 ? 's' : ''}`) ||
''}
{(pinsAdded?.length > 0 && pinsRemoved?.length > 0 && ` and `) || ''}
{(pinsRemoved?.length > 0 &&
`unpinned ${pinsRemoved.length} message${
pinsRemoved.length > 1 ? 's' : ''
}`) ||
''}
{((!pinsAdded || pinsAdded.length <= 0) &&
(!pinsRemoved || pinsRemoved.length <= 0) &&
`has not changed the pins`) ||
`:`}
</Text>
{pinPreviewIds.length > 0 &&
pinPreviewIds
.slice(0, 4)
.map((x: string) => (
<Reply
key={x}
style={{ opacity: '80%' }}
room={room}
replyEventId={x}
onClick={handleOpenReply}
replyIcon={<>{menuIcon(pinnedSet.has(x) ? PushPin : PushPinSlash)}</>}
/>
))}
</Box>
)
}
/>
</Message>
Expand All @@ -2124,6 +2160,7 @@ export function useTimelineEventRenderer({
},
(mEventId, mEvent, item, timelineSet, collapse) => {
if (!hiddenEventOther) return null;
const isRedacted = mEvent.isRedacted();
const highlighted = focusItem?.index === item && focusItem.highlight;
const marked = activeReplyId === mEventId && !suppressMark;
const senderId = mEvent.getSender() ?? '';
Expand Down Expand Up @@ -2205,16 +2242,20 @@ export function useTimelineEventRenderer({
<EventContent
messageLayout={messageLayout}
time={timeJSX}
icon={timelineIcon(Code)}
icon={isRedacted ? timelineIcon(Trash) : timelineIcon(Code)}
content={
<Box grow="Yes" direction="Column">
<Text size="T300" priority="300">
<DecoratedUser userId={senderId} userName={senderName} room={room} />
{' sent '}
<code className={customHtmlCss.Code}>{mEvent.getType()}</code>
{' state event'}
</Text>
</Box>
isRedacted ? (
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
) : (
<Box grow="Yes" direction="Column">
<Text size="T300" priority="300">
<DecoratedUser userId={senderId} userName={senderName} room={room} />
{' sent '}
<code className={customHtmlCss.Code}>{mEvent.getType()}</code>
{' state event'}
</Text>
</Box>
)
}
/>
</Message>
Expand Down
Loading