4a401cf816
ML noise suppression produced loud static on real calls. RNNoise requires mono 48kHz float input; feeding it stereo or wrong-rate data is the classic cause of that static. Harden the shim: - request mono (channelCount:1) + 48kHz capture - run a 48kHz AudioContext and BAIL to the raw mic if the browser won't give a true 48kHz context (wrong-rate data -> static) - force the worklet node to explicit mono in/out - use the non-SIMD rnnoise.wasm (SIMD build artifacts on some GPUs) - share one AudioContext across captures Also fix the two CI-blocking eslint errors (unused vars in UrlPreviewCard and useLocalMessageSearch) and apply repo-wide prettier formatting so check:eslint and check:prettier pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
168 lines
6.1 KiB
TypeScript
168 lines
6.1 KiB
TypeScript
import { EventType } from 'matrix-js-sdk';
|
|
import { useCallback } from 'react';
|
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
import { ResultGroup, ResultItem } from './useMessageSearch';
|
|
|
|
export type LocalSearchParams = {
|
|
term: string;
|
|
roomIds: string[];
|
|
senders?: string[];
|
|
};
|
|
|
|
export type LocalSearchResult = {
|
|
groups: ResultGroup[];
|
|
/** How many rooms in scope are encrypted */
|
|
encryptedRoomsCount: number;
|
|
/** How many of those had locally cached events to scan */
|
|
searchedRoomsCount: number;
|
|
};
|
|
|
|
/**
|
|
* Client-side full-text search over locally cached events in encrypted rooms.
|
|
* The homeserver cannot search E2EE message content, so we scan whatever the
|
|
* client has already received and decrypted in memory.
|
|
*
|
|
* Limitation: only messages present in the live timeline window are covered.
|
|
* Rooms that haven't been opened yet will return no results.
|
|
*/
|
|
export const useLocalMessageSearch = () => {
|
|
const mx = useMatrixClient();
|
|
|
|
const search = useCallback(
|
|
({ term, roomIds, senders }: LocalSearchParams): LocalSearchResult => {
|
|
const trimmedTerm = term.trim();
|
|
const senderSet = senders && senders.length > 0 ? new Set(senders) : null;
|
|
|
|
// Sender-only mode: no text filter, search all rooms (server can't filter by sender alone)
|
|
const senderOnlyMode = !trimmedTerm && !!senderSet;
|
|
|
|
if (!trimmedTerm && !senderSet) {
|
|
return { groups: [], encryptedRoomsCount: 0, searchedRoomsCount: 0 };
|
|
}
|
|
|
|
const termLower = trimmedTerm.toLowerCase();
|
|
const groups: ResultGroup[] = [];
|
|
let encryptedRoomsCount = 0;
|
|
let searchedRoomsCount = 0;
|
|
|
|
for (const roomId of roomIds) {
|
|
const room = mx.getRoom(roomId);
|
|
if (!room) continue;
|
|
|
|
const isEncrypted = !!room.currentState.getStateEvents(EventType.RoomEncryption, '');
|
|
|
|
// Text search: encrypted rooms only — server already covers plaintext rooms
|
|
// Sender-only: all rooms — server has no sender-only search
|
|
if (!senderOnlyMode && !isEncrypted) continue;
|
|
|
|
if (isEncrypted) encryptedRoomsCount += 1;
|
|
|
|
const events = room
|
|
.getUnfilteredTimelineSet()
|
|
.getTimelines()
|
|
.flatMap((tl) => tl.getEvents());
|
|
if (events.length === 0) continue;
|
|
|
|
searchedRoomsCount += 1;
|
|
|
|
const items: ResultItem[] = [];
|
|
|
|
for (let i = 0; i < events.length; i += 1) {
|
|
const event = events[i];
|
|
|
|
// In sender-only mode: include all message types; skip non-message events
|
|
if (event.getType() !== EventType.RoomMessage) {
|
|
if (senderOnlyMode) continue;
|
|
const evType = event.getType();
|
|
const isSticker = evType === 'm.sticker';
|
|
const isPoll = evType === 'm.poll.start' || evType === 'org.matrix.msc3381.poll.start';
|
|
if (!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();
|
|
|
|
// Sender-only mode: no text filter needed
|
|
if (!senderOnlyMode) {
|
|
const evType = event.getType();
|
|
const isPoll = evType === 'm.poll.start' || evType === 'org.matrix.msc3381.poll.start';
|
|
|
|
let body = '';
|
|
let formattedBody = '';
|
|
if (!isPoll) {
|
|
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) &&
|
|
!formattedBody.toLowerCase().includes(termLower)
|
|
)
|
|
continue;
|
|
}
|
|
|
|
// Build a synthetic IEventWithRoomId using decrypted content so the
|
|
// existing SearchResultGroup renderer works without modification.
|
|
const syntheticEvent = {
|
|
room_id: roomId,
|
|
event_id: event.getId() ?? '',
|
|
type: event.getType(),
|
|
sender: event.getSender() ?? '',
|
|
origin_server_ts: event.getTs(),
|
|
content,
|
|
unsigned: event.getUnsigned(),
|
|
};
|
|
|
|
items.push({
|
|
rank: 0,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
event: syntheticEvent as any,
|
|
context: {
|
|
events_before: [],
|
|
events_after: [],
|
|
profile_info: {},
|
|
},
|
|
});
|
|
}
|
|
|
|
if (items.length > 0) {
|
|
items.sort((a, b) => (b.event.origin_server_ts ?? 0) - (a.event.origin_server_ts ?? 0));
|
|
groups.push({ roomId, items });
|
|
}
|
|
}
|
|
|
|
return { groups, encryptedRoomsCount, searchedRoomsCount };
|
|
},
|
|
[mx],
|
|
);
|
|
|
|
return search;
|
|
};
|