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:
@@ -42,24 +42,27 @@ function useRingTone(active: boolean) {
|
||||
osc.stop(t + dur);
|
||||
};
|
||||
|
||||
let ringTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const ring = () => {
|
||||
if (cancelled) return;
|
||||
const now = ctx.currentTime;
|
||||
pulse(now, 880, 0.18);
|
||||
pulse(now + 0.28, 880, 0.18);
|
||||
setTimeout(ring, 2200);
|
||||
ringTimer = setTimeout(ring, 2200);
|
||||
};
|
||||
|
||||
ring();
|
||||
|
||||
stopRef.current = () => {
|
||||
const stop = () => {
|
||||
cancelled = true;
|
||||
if (ringTimer !== null) clearTimeout(ringTimer);
|
||||
ctx.close();
|
||||
};
|
||||
|
||||
stopRef.current = stop;
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
ctx.close();
|
||||
stop();
|
||||
stopRef.current = null;
|
||||
};
|
||||
}, [active]);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { Box, Text } from 'folds';
|
||||
|
||||
type PollTextValue = Array<{ body: string }> | string;
|
||||
|
||||
function extractText(val: PollTextValue | undefined): string {
|
||||
if (!val) return '';
|
||||
if (typeof val === 'string') return val;
|
||||
return val[0]?.body ?? '';
|
||||
}
|
||||
|
||||
type PollAnswer = {
|
||||
'm.id'?: string;
|
||||
id?: string;
|
||||
'm.text'?: PollTextValue;
|
||||
'org.matrix.msc3381.poll.answer'?: { body: string };
|
||||
};
|
||||
|
||||
type PollData = {
|
||||
question?: { body?: string; 'm.text'?: PollTextValue };
|
||||
answers?: PollAnswer[];
|
||||
};
|
||||
|
||||
export function PollContent({ content }: { content: Record<string, unknown> }) {
|
||||
const poll = (
|
||||
content['m.poll'] ?? content['org.matrix.msc3381.poll.start']
|
||||
) as PollData | undefined;
|
||||
|
||||
if (!poll) {
|
||||
return (
|
||||
<Text style={{ opacity: 0.6 }}>
|
||||
<i>Poll (unreadable format)</i>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const questionText =
|
||||
extractText((poll.question as any)?.['m.text']) ||
|
||||
(poll.question as any)?.body ||
|
||||
'Untitled poll';
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="200" style={{ maxWidth: '340px', paddingTop: '2px', paddingBottom: '4px' }}>
|
||||
<Box
|
||||
alignItems="Center"
|
||||
gap="100"
|
||||
style={{
|
||||
fontSize: '0.68rem',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.12em',
|
||||
textTransform: 'uppercase',
|
||||
opacity: 0.55,
|
||||
marginBottom: '2px',
|
||||
}}
|
||||
>
|
||||
◉ Poll
|
||||
</Box>
|
||||
<Text size="T400" style={{ fontWeight: 600 }}>
|
||||
{questionText}
|
||||
</Text>
|
||||
<Box direction="Column" gap="100" style={{ marginTop: '2px' }}>
|
||||
{(poll.answers ?? []).map((answer, i) => {
|
||||
const text =
|
||||
extractText((answer as any)['m.text']) ||
|
||||
(answer as any)['org.matrix.msc3381.poll.answer']?.body ||
|
||||
`Option ${i + 1}`;
|
||||
const id = answer['m.id'] ?? answer.id ?? String(i);
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
style={{
|
||||
padding: '7px 12px',
|
||||
borderRadius: '8px',
|
||||
background: 'var(--bg-surface-low)',
|
||||
border: '1px solid var(--bg-surface-border)',
|
||||
fontSize: '0.88rem',
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
<Text size="T200" style={{ opacity: 0.4, marginTop: '2px' }}>
|
||||
<i>Open in Element to vote</i>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -5,3 +5,4 @@ export * from './AudioContent';
|
||||
export * from './FileContent';
|
||||
export * from './FallbackContent';
|
||||
export * from './EventContent';
|
||||
export * from './PollContent';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user