hidden shit, and online status bars, search settings

This commit is contained in:
2026-07-12 07:49:17 +10:00
parent 5374c10c61
commit 02d96a9758
31 changed files with 1323 additions and 265 deletions

114
src/app/utils/fuzzy.ts Normal file
View File

@@ -0,0 +1,114 @@
export type FuzzyMatch = {
score: number;
indices: number[];
};
/**
* Subsequence fuzzy match with fzf-style scoring.
* All query characters must appear in order; higher scores rank first.
*/
export function fuzzyMatch(query: string, text: string): FuzzyMatch | null {
const q = query.trim().toLowerCase();
if (!q) return { score: 0, indices: [] };
const t = text.toLowerCase();
if (!t) return null;
// Fast path: contiguous substring
const substringIndex = t.indexOf(q);
if (substringIndex !== -1) {
const indices = Array.from({ length: q.length }, (_, i) => substringIndex + i);
// Prefer earlier matches and shorter haystacks
const score = 10_000 - substringIndex * 10 - (t.length - q.length);
return { score, indices };
}
let ti = 0;
const indices: number[] = [];
let score = 0;
let prevIndex = -1;
let consecutive = 0;
for (let qi = 0; qi < q.length; qi += 1) {
const ch = q[qi];
let found = -1;
for (; ti < t.length; ti += 1) {
if (t[ti] === ch) {
found = ti;
ti += 1;
break;
}
}
if (found === -1) return null;
indices.push(found);
// Consecutive bonus
if (prevIndex === found - 1) {
consecutive += 1;
score += 15 + consecutive * 5;
} else {
consecutive = 0;
score += 1;
}
// Word-boundary / start bonus
if (
found === 0 ||
t[found - 1] === ' ' ||
t[found - 1] === '-' ||
t[found - 1] === '_' ||
t[found - 1] === '&' ||
t[found - 1] === '/'
) {
score += 20;
}
// Prefer earlier matches
score -= found;
prevIndex = found;
}
// Prefer tighter overall spans
const span = indices[indices.length - 1] - indices[0] + 1;
score += Math.max(0, 50 - (span - q.length) * 2);
// Prefer shorter labels slightly
score -= Math.max(0, t.length - q.length) * 0.1;
return { score, indices };
}
/** Best fuzzy match across multiple strings (title, keywords, etc.). */
export function fuzzyMatchAny(query: string, texts: Array<string | undefined | null>): FuzzyMatch | null {
let best: FuzzyMatch | null = null;
for (const text of texts) {
if (!text) continue;
const match = fuzzyMatch(query, text);
if (match && (!best || match.score > best.score)) {
best = match;
}
}
return best;
}
export function fuzzyFilter<T>(
items: T[],
query: string,
getTexts: (item: T) => Array<string | undefined | null>
): Array<{ item: T; score: number }> {
const q = query.trim();
if (!q) {
return items.map((item) => ({ item, score: 0 }));
}
const results: Array<{ item: T; score: number }> = [];
for (const item of items) {
const match = fuzzyMatchAny(q, getTexts(item));
if (match) {
results.push({ item, score: match.score });
}
}
results.sort((a, b) => b.score - a.score);
return results;
}

View File

@@ -181,10 +181,8 @@ export function isValidChild(mEvent: MatrixEvent): boolean {
if (mEvent.getType() !== StateEvent.SpaceChild) return false;
const stateKey = mEvent.getStateKey();
if (!stateKey || !stateKey.startsWith('!')) return false;
const { via } = mEvent.getContent<{ via?: string[] }>();
// via is optional on m.space.child; only reject malformed values
if (via !== undefined && !Array.isArray(via)) return false;
return true;
// via is required; omitting it (e.g. empty {}) removes the child per Matrix spec
return Array.isArray(mEvent.getContent<{ via?: string[] }>().via);
}
export const getAllParents = (roomToParents: RoomToParents, roomId: string): Set<string> => {