feat: backup/restore, user profile popover, image compress, video blur, wake lock
- backup/restore dialog + user profile popover components - image compression, video blur, wake lock utilities - message cache + conversation messages hook refinements - call context, active speakers, screen share dialog tweaks - audio + screen share settings persistence - refreshed app icons (smaller sizes) across all platforms
This commit is contained in:
@@ -54,6 +54,44 @@ async function loadDb(): Promise<Database | null> {
|
||||
await db.execute(
|
||||
'CREATE INDEX IF NOT EXISTS idx_msgs_conv_created ON messages(conversation_id, created_at);',
|
||||
);
|
||||
// FTS5 virtual table for instant full-text search across the cache.
|
||||
// Keeps only the searchable text columns (plaintext + sender), keyed
|
||||
// by the message id so we can join back to the main row. Triggers
|
||||
// mirror inserts/updates/deletes so the index never drifts.
|
||||
//
|
||||
// Falls back gracefully on FTS5-less builds: the IF NOT EXISTS keeps
|
||||
// the call idempotent, and the surrounding try/catch already handles
|
||||
// a CREATE failure by skipping the whole cache.
|
||||
try {
|
||||
await db.execute(
|
||||
`CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
||||
message_id UNINDEXED,
|
||||
conversation_id UNINDEXED,
|
||||
plaintext,
|
||||
tokenize = 'unicode61 remove_diacritics 2'
|
||||
);`,
|
||||
);
|
||||
await db.execute(
|
||||
`CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
|
||||
INSERT INTO messages_fts (message_id, conversation_id, plaintext)
|
||||
VALUES (new.id, new.conversation_id, COALESCE(new.plaintext, ''));
|
||||
END;`,
|
||||
);
|
||||
await db.execute(
|
||||
`CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN
|
||||
DELETE FROM messages_fts WHERE message_id = old.id;
|
||||
END;`,
|
||||
);
|
||||
await db.execute(
|
||||
`CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN
|
||||
DELETE FROM messages_fts WHERE message_id = old.id;
|
||||
INSERT INTO messages_fts (message_id, conversation_id, plaintext)
|
||||
VALUES (new.id, new.conversation_id, COALESCE(new.plaintext, ''));
|
||||
END;`,
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
console.warn('FTS5 init failed — search falls back to in-memory scan', err);
|
||||
}
|
||||
return db;
|
||||
} catch (err: unknown) {
|
||||
console.warn('messageCache init failed — falling back to memory-only', err);
|
||||
@@ -157,6 +195,54 @@ export async function deleteCachedMessage(id: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// Full-text search across cached messages. Returns rows in newest-first
|
||||
// order; caller maps to DecryptedMessage. Empty result on cache-miss or
|
||||
// FTS5 not available — caller should fall back to in-memory regex.
|
||||
export async function searchCachedMessages(
|
||||
conversationId: string,
|
||||
query: string,
|
||||
limit = 200,
|
||||
): Promise<DecryptedMessage[]> {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
const trimmed = query.trim();
|
||||
if (trimmed.length === 0) return [];
|
||||
// Sanitize for FTS5 MATCH syntax. Strip quotes + control chars; wrap in
|
||||
// an OR over each token with a trailing wildcard so partial words match.
|
||||
// Hyphens + colons are FTS5 operators so we drop them.
|
||||
const tokens = trimmed
|
||||
.replace(/["'\u0000-\u001f]/g, ' ')
|
||||
.replace(/[-:^]/g, ' ')
|
||||
.split(/\s+/)
|
||||
.filter(Boolean);
|
||||
if (tokens.length === 0) return [];
|
||||
const matchExpr = tokens.map((t) => '"' + t.replace(/"/g, '') + '"*').join(' AND ');
|
||||
try {
|
||||
const rows = await db.select<Row>(
|
||||
`SELECT m.* FROM messages m
|
||||
JOIN messages_fts f ON f.message_id = m.id
|
||||
WHERE f.conversation_id = $1 AND messages_fts MATCH $2
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT $3`,
|
||||
[conversationId, matchExpr, limit],
|
||||
);
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
conversationId: r.conversation_id,
|
||||
senderId: r.sender_id,
|
||||
senderDeviceId: r.sender_device_id,
|
||||
replyToId: r.reply_to_id,
|
||||
editedAt: r.edited_at,
|
||||
deletedAt: r.deleted_at,
|
||||
createdAt: r.created_at,
|
||||
plaintext: r.plaintext,
|
||||
}));
|
||||
} catch (err: unknown) {
|
||||
console.warn('searchCachedMessages failed', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Housekeeping — run once per session to bound the cache size. Keeps the
|
||||
// latest KEEP_PER_CONV messages per conversation.
|
||||
const KEEP_PER_CONV = 1000;
|
||||
|
||||
Reference in New Issue
Block a user