fix: work through LOTUS_BUGS.md audit items
- ExportRoomHistory: make addEvents() async, call decryptEventIfNeeded() before inspecting type/content so E2EE rooms export decrypted text - UrlPreviewCard: remove Google S2 favicon (privacy leak); show generic Icons.Link instead — no third-party external calls - Profile: add statusDirtyRef so server presence sync cannot clobber in-flight emoji insertions or keystrokes; cleared on save/clear - useLocalMessageSearch: include m.sticker, m.poll.start, and org.matrix.msc3381.poll.start in encrypted room search; index poll question and answer bodies - SeasonalEffect: z-index 9997 → 9999 so overlays render above animated chat backgrounds - LOTUS_BUGS.md: mark all resolved, document remaining blocked items Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -775,7 +775,7 @@ function SeasonalOverlay({
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 9997,
|
||||
zIndex: 9999,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1652,7 +1652,6 @@ function GenericCard({
|
||||
const description = prev['og:description'] ?? '';
|
||||
const siteName = typeof prev['og:site_name'] === 'string' ? prev['og:site_name'] : undefined;
|
||||
const domain = getDomain(url);
|
||||
const faviconSrc = `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=16`;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -1687,13 +1686,11 @@ function GenericCard({
|
||||
priority="300"
|
||||
>
|
||||
{!thumbUrl && (
|
||||
<img
|
||||
className={previewCss.GenericFaviconImg}
|
||||
src={faviconSrc}
|
||||
alt=""
|
||||
<Icon
|
||||
src={Icons.Link}
|
||||
size="50"
|
||||
aria-hidden="true"
|
||||
loading="lazy"
|
||||
style={{ marginRight: '4px', verticalAlign: 'text-bottom' }}
|
||||
style={{ marginRight: '4px', verticalAlign: 'text-bottom', opacity: 0.5 }}
|
||||
/>
|
||||
)}
|
||||
{siteName ? `${siteName} | ` : ''}
|
||||
|
||||
@@ -63,15 +63,49 @@ export const useLocalMessageSearch = () => {
|
||||
for (let i = 0; i < events.length; i += 1) {
|
||||
const event = events[i];
|
||||
|
||||
if (event.getType() !== EventType.RoomMessage) continue;
|
||||
const evType = event.getType();
|
||||
const isMessage = evType === EventType.RoomMessage;
|
||||
const isSticker = evType === 'm.sticker';
|
||||
const isPoll =
|
||||
evType === 'm.poll.start' || evType === 'org.matrix.msc3381.poll.start';
|
||||
|
||||
if (!isMessage && !isSticker && !isPoll) continue;
|
||||
if (event.isDecryptionFailure()) continue;
|
||||
if (event.isRedacted()) continue;
|
||||
if (senderSet && !senderSet.has(event.getSender() ?? '')) continue;
|
||||
|
||||
// getContent() returns decrypted plaintext regardless of encryption
|
||||
const content = event.getContent();
|
||||
const body = (content.body as string | undefined) ?? '';
|
||||
const formattedBody = (content.formatted_body as string | undefined) ?? '';
|
||||
|
||||
let body = '';
|
||||
let formattedBody = '';
|
||||
if (isMessage || isSticker) {
|
||||
body = (content.body as string | undefined) ?? '';
|
||||
formattedBody = (content.formatted_body as string | undefined) ?? '';
|
||||
} else {
|
||||
// Poll — index question text and all answer options
|
||||
const poll = (content['m.poll'] ??
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
content['org.matrix.msc3381.poll.start']) as any;
|
||||
if (poll) {
|
||||
const qBody =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(poll.question?.['m.text'] as Array<{ body: string }> | undefined)?.[0]?.body ??
|
||||
(poll.question?.body as string | undefined) ??
|
||||
'';
|
||||
const answerBodies = ((poll.answers ?? []) as Array<Record<string, unknown>>)
|
||||
.map(
|
||||
(a) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
((a['m.text'] as Array<{ body: string }> | undefined)?.[0]?.body ??
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(a['org.matrix.msc3381.poll.answer'] as any)?.body ??
|
||||
'') as string,
|
||||
)
|
||||
.join(' ');
|
||||
body = `${qBody} ${answerBodies}`.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!body.toLowerCase().includes(termLower) &&
|
||||
|
||||
@@ -56,11 +56,17 @@ export function ExportRoomHistory({ requestClose }: ExportRoomHistoryProps) {
|
||||
const timeline = room.getLiveTimeline();
|
||||
let canLoadMore = true;
|
||||
|
||||
const addEvents = (events: ReturnType<typeof timeline.getEvents>) => {
|
||||
const addEvents = async (events: ReturnType<typeof timeline.getEvents>) => {
|
||||
for (const ev of events) {
|
||||
const evId = ev.getId();
|
||||
if (!evId || seen.has(evId)) continue;
|
||||
seen.add(evId);
|
||||
// Attempt decryption for events that haven't been decrypted yet
|
||||
// (paginateEventTimeline may fetch events before the SDK decrypts them)
|
||||
if (ev.isEncrypted() && !ev.getClearContent()) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await mx.decryptEventIfNeeded(ev).catch(() => undefined);
|
||||
}
|
||||
if (ev.getType() !== EventType.RoomMessage) continue;
|
||||
if (ev.isDecryptionFailure()) continue;
|
||||
const ts = ev.getTs();
|
||||
@@ -81,7 +87,7 @@ export function ExportRoomHistory({ requestClose }: ExportRoomHistoryProps) {
|
||||
setExportCount(collected.length);
|
||||
};
|
||||
|
||||
addEvents(timeline.getEvents());
|
||||
await addEvents(timeline.getEvents());
|
||||
|
||||
// Paginate backwards until start or date range exceeded
|
||||
while (canLoadMore) {
|
||||
@@ -98,7 +104,8 @@ export function ExportRoomHistory({ requestClose }: ExportRoomHistoryProps) {
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
addEvents(timeline.getEvents());
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await addEvents(timeline.getEvents());
|
||||
}
|
||||
|
||||
// Sort chronologically (oldest first)
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
@@ -349,6 +350,9 @@ function ProfileStatus() {
|
||||
const [statusMsg, setStatusMsg] = useState<string>(
|
||||
presence?.status ?? localStorage.getItem(STATUS_MSG_KEY(userId)) ?? '',
|
||||
);
|
||||
// True while the user has unsaved local edits — prevents a server presence
|
||||
// echo from overwriting what the user is currently typing/inserting.
|
||||
const statusDirtyRef = useRef(false);
|
||||
const [clearAfter, setClearAfter] = useState('0');
|
||||
const [emojiAnchor, setEmojiAnchor] = useState<RectCords>();
|
||||
|
||||
@@ -359,10 +363,10 @@ function ProfileStatus() {
|
||||
});
|
||||
|
||||
// Sync input when another device changes the status.
|
||||
// Only update if the server actually has a value — ignore empty sync events
|
||||
// caused by Synapse clearing status_msg on reconnect.
|
||||
// Skipped while the user has unsaved local edits to avoid clobbering
|
||||
// mid-flight input (e.g. an emoji being inserted).
|
||||
useEffect(() => {
|
||||
if (presence?.status) {
|
||||
if (!statusDirtyRef.current && presence?.status) {
|
||||
setStatusMsg(presence.status);
|
||||
localStorage.setItem(STATUS_MSG_KEY(userId), presence.status);
|
||||
}
|
||||
@@ -399,17 +403,20 @@ function ProfileStatus() {
|
||||
const saving = saveState.status === AsyncStatus.Loading;
|
||||
|
||||
const handleEmojiSelect = useCallback((unicode: string) => {
|
||||
statusDirtyRef.current = true;
|
||||
setStatusMsg((prev) => prev + unicode);
|
||||
setEmojiAnchor(undefined);
|
||||
}, []);
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
|
||||
statusDirtyRef.current = true;
|
||||
setStatusMsg(evt.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
||||
evt.preventDefault();
|
||||
if (saving) return;
|
||||
statusDirtyRef.current = false;
|
||||
const msg = statusMsg.trim();
|
||||
saveStatus(msg).catch(() => undefined);
|
||||
|
||||
@@ -431,6 +438,7 @@ function ProfileStatus() {
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
statusDirtyRef.current = false;
|
||||
setStatusMsg('');
|
||||
localStorage.removeItem(STATUS_MSG_KEY(userId));
|
||||
localStorage.removeItem(STATUS_EXPIRY_KEY(userId));
|
||||
|
||||
Reference in New Issue
Block a user