From 9f954718fa0f0ac9ca9cc597de3366d1fded14bb Mon Sep 17 00:00:00 2001 From: CatQuery Date: Tue, 24 Mar 2026 01:34:54 -0400 Subject: [PATCH 1/6] tweak(firehose): change default instance --- src/routes/firehose/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/firehose/+page.svelte b/src/routes/firehose/+page.svelte index b22337b..ec53347 100644 --- a/src/routes/firehose/+page.svelte +++ b/src/routes/firehose/+page.svelte @@ -81,7 +81,7 @@ if (instanceParam && instanceParam in instances) { instanceValue = instanceParam; } else { - instanceValue = "logs.spanix.team"; + instanceValue = "firehose.catquery.com"; } searchValue = q.get("s") || ""; From deb979e02646561b049aa8406489333f3b8a5dc5 Mon Sep 17 00:00:00 2001 From: CatQuery Date: Wed, 25 Mar 2026 03:37:38 -0400 Subject: [PATCH 2/6] unreviewed clanker generated --- src/lib/components/search-filter.svelte | 217 ++++++++++++++++++++++++ src/lib/twitch/logs.ts | 105 ++++++++++++ src/routes/firehose/+page.svelte | 90 +++++++++- src/routes/logs/+page.svelte | 122 +++++++++++-- 4 files changed, 512 insertions(+), 22 deletions(-) create mode 100644 src/lib/components/search-filter.svelte diff --git a/src/lib/components/search-filter.svelte b/src/lib/components/search-filter.svelte new file mode 100644 index 0000000..c422dc5 --- /dev/null +++ b/src/lib/components/search-filter.svelte @@ -0,0 +1,217 @@ + + +
+
+ + +
+ +
+ {#each criteria as criterion, index (criterion.id)} +
+ {#if index > 0} +
+ updateCriterion(criterion.id, { operator: v as "AND" | "OR" })} + > + + {criterion.operator || "AND"} + + + + + + +
+ {:else} +
+ {/if} + + updateCriterionType(criterion.id, v as CriterionType)} + > + + {criterionTypeLabels[criterion.type]} + + + + + + + + + + + updateCriterion(criterion.id, { negate: v === "NOT" })} + > + + {criterion.negate ? "NOT" : ""} + + + + + + + + {#if criterion.type === "badge" && availableBadges.length > 0} +
+ updateCriterion(criterion.id, { value: v.join(',') })} + > + +
+ {#each (criterion.value ? criterion.value.split(',').filter(Boolean) : []) as badgeId (badgeId)} + {@const badge = availableBadges.find(b => b.id === badgeId)} + {#if badge} + {badge.title} + {badge.title} + {/if} + {/each} + {#if !criterion.value || criterion.value.split(',').filter(Boolean).length === 0} + Select badges... + {/if} +
+
+ + {#each availableBadges as badge (badge.id)} + +
+ {badge.title} + {badge.title} +
+
+ {/each} +
+
+
+ {:else} + emitChange()} + placeholder={criterion.type === "badge" ? "e.g., moderator,subscriber" : "Enter value"} + class="h-8 flex-1 border" + onkeydown={handleKeydown} + /> + {/if} + + +
+ {/each} +
+ + {#if criteria.length === 0} +

No filters added. Click "Add Filter" to get started.

+ {/if} +
diff --git a/src/lib/twitch/logs.ts b/src/lib/twitch/logs.ts index d857a83..2c32fe1 100644 --- a/src/lib/twitch/logs.ts +++ b/src/lib/twitch/logs.ts @@ -33,6 +33,32 @@ export type BadgeProps = { title: string; }; +export type SearchCriterion = { + id: string; + type: "regex" | "text" | "user" | "channel" | "badge"; + value: string; + operator?: "AND" | "OR"; // operator before this criterion + negate?: boolean; // for NOT logic +}; + +export type SearchFilter = { + criteria: SearchCriterion[]; +}; + +// Serialize filter to URL-safe string +export const serializeFilter = (filter: SearchFilter): string => { + return btoa(JSON.stringify(filter)); +}; + +// Deserialize filter from URL-safe string +export const deserializeFilter = (encoded: string): SearchFilter => { + try { + return JSON.parse(atob(encoded)); + } catch { + return { criteria: [] }; + } +}; + const searchPrefixes: Record Message[]> = { regex(searchString, chatLogs) { try { @@ -63,6 +89,85 @@ const searchPrefixes: Record { + let result = false; + + switch (criterion.type) { + case "regex": { + try { + const regex = new RegExp(criterion.value, "i"); + result = regex.test(msg.text); + } catch { + result = false; + } + break; + } + case "text": { + result = msg.text.toLowerCase().includes(criterion.value.toLowerCase()); + break; + } + case "user": { + const users = criterion.value + .toLowerCase() + .split(",") + .map((u) => u.trim()); + result = users.includes(msg.displayName.toLowerCase()); + break; + } + case "channel": { + const channels = criterion.value + .toLowerCase() + .split(",") + .map((c) => c.trim()); + result = channels.includes(msg.channel?.toLowerCase() ?? ""); + break; + } + case "badge": { + const badges = criterion.value + .toLowerCase() + .split(",") + .map((b) => b.trim()); + const msgBadges = msg.tags["badges"]?.split(",").map((b) => b.split("/")[0].toLowerCase()) ?? []; + result = badges.some((badge) => msgBadges.includes(badge)); + break; + } + } + + return criterion.negate ? !result : result; +}; + +export const advancedMessageSearch = (filter: SearchFilter, chatLogs: Message[], scrollFromBottom: boolean | null): Message[] => { + if (!filter.criteria.length) return scrollFromBottom === false ? [...chatLogs].reverse() : chatLogs; + + const results = chatLogs.filter((msg) => { + // Group criteria by OR operators (AND has higher precedence) + const orGroups: SearchCriterion[][] = []; + for (const criterion of filter.criteria) { + if (orGroups.length === 0 || criterion.operator === "OR") { + orGroups.push([criterion]); + } else { + orGroups[orGroups.length - 1].push(criterion); + } + } + + // Evaluate each OR group (AND within groups) + let result = false; + for (const group of orGroups) { + let groupResult = true; + for (const criterion of group) { + const testResult = testCriterion(criterion, msg); + groupResult = groupResult && testResult; + } + result = result || groupResult; + } + + return result; + }); + + return scrollFromBottom === false ? [...results].reverse() : results; +}; + export const messageSearch = (searchValue: string, chatLogs: Message[], scrollFromBottom: boolean | null): Message[] => { const searchKey = searchValue.split(":", 1)[0].toLowerCase(); const searchString = searchValue.slice(searchKey.length + 1); diff --git a/src/routes/firehose/+page.svelte b/src/routes/firehose/+page.svelte index ec53347..523ba64 100644 --- a/src/routes/firehose/+page.svelte +++ b/src/routes/firehose/+page.svelte @@ -11,6 +11,7 @@ import * as Card from "$lib/components/ui/card/index.js"; import FocusTrap from "$lib/components/focus-trap.svelte"; + import SearchFilter from "$lib/components/search-filter.svelte"; import VirtualList from "svelte-tiny-virtual-list"; @@ -19,7 +20,7 @@ import Link from "$lib/components/message/link.svelte"; import Badge from "$lib/components/message/badge.svelte"; - import { ChevronsDownIcon } from "@lucide/svelte"; + import { ChevronsDownIcon, SettingsIcon } from "@lucide/svelte"; import { getContext, onDestroy, onMount, tick, untrack } from "svelte"; import { SvelteMap } from "svelte/reactivity"; @@ -30,8 +31,8 @@ import { timeFormat, type TitleContext } from "$lib/common"; - import type { EmoteProps, BadgeProps, Message, ChatComponents, TMIEmote } from "$lib/twitch/logs"; - import { messageSearch } from "$lib/twitch/logs"; + import type { EmoteProps, BadgeProps, Message, ChatComponents, TMIEmote, SearchFilter as SearchFilterType, SearchCriterion } from "$lib/twitch/logs"; + import { messageSearch, advancedMessageSearch, serializeFilter, deserializeFilter } from "$lib/twitch/logs"; import * as TwitchServices from "$lib/twitch/services/index.js"; @@ -67,9 +68,13 @@ // Badges const globalBadges = new SvelteMap(); let badgeUpdates = $state(0); + let availableBadges = $state>([]); let instanceValue = $state(""); let searchValue = $state(""); + let searchFilter = $state({ criteria: [] }); + let useAdvancedSearch = $state(false); + let isFilterOpen = $state(false); onMount(() => { fetchGlobalBadges(); @@ -85,6 +90,14 @@ } searchValue = q.get("s") || ""; + + // Load advanced search filter from URL + const filterParam = q.get("f"); + if (filterParam) { + searchFilter = deserializeFilter(filterParam); + useAdvancedSearch = true; + isFilterOpen = true; + } }); onDestroy(() => { @@ -95,6 +108,7 @@ $effect(() => { const i = instanceValue; const s = searchValue; + const f = useAdvancedSearch && searchFilter.criteria.length > 0 ? serializeFilter(searchFilter) : null; untrack(() => { const q = page.url.searchParams; @@ -103,6 +117,9 @@ if (s) q.set("s", s); else q.delete("s"); + if (f) q.set("f", f); + else q.delete("f"); + goto(page.url.search, { replaceState: true, keepFocus: true }); }); }); @@ -149,7 +166,12 @@ }); }); - let filteredChatLogs = $derived(messageSearch(searchValue, chatLogs, null)); + let filteredChatLogs = $derived.by(() => { + if (useAdvancedSearch && searchFilter.criteria.length > 0) { + return advancedMessageSearch(searchFilter, chatLogs, null); + } + return messageSearch(searchValue, chatLogs, null); + }); const logsAfterScroll = ({ detail }: { detail: { event: Event; offset: number } }) => { const el = detail.event.target as HTMLDivElement; @@ -195,6 +217,30 @@ return badges; }; + $effect(() => { + // Update available badges list for the filter component + badgeUpdates; + const badges: Array<{ id: string; title: string; imageUrl: string }> = []; + const seenIds = new Set(); + + // Add global badges + globalBadges.forEach((badge, key) => { + const badgeId = key.split("/")[0]; + if (!seenIds.has(badgeId)) { + badges.push({ + id: badgeId, + title: badge.title, + imageUrl: badge.url, + }); + seenIds.add(badgeId); + } + }); + + // Sort by title + badges.sort((a, b) => a.title.localeCompare(b.title)); + availableBadges = badges; + }); + const fetchGlobalBadges = async () => { const globalBadgesList = await TwitchServices.IVR.getGlobalBadges(); @@ -348,11 +394,43 @@ -
- +
+ {#if !useAdvancedSearch} + + {/if} +
+ {#if useAdvancedSearch && isFilterOpen} + (searchFilter = filter)} /> + {/if} +
diff --git a/src/routes/logs/+page.svelte b/src/routes/logs/+page.svelte index 12ddcbe..ffc3aa9 100644 --- a/src/routes/logs/+page.svelte +++ b/src/routes/logs/+page.svelte @@ -22,6 +22,7 @@ import * as Card from "$lib/components/ui/card/index.js"; import FocusTrap from "$lib/components/focus-trap.svelte"; + import SearchFilter from "$lib/components/search-filter.svelte"; import VirtualList from "svelte-tiny-virtual-list"; @@ -38,12 +39,12 @@ import { page } from "$app/state"; import { goto } from "$app/navigation"; - import { LoaderCircleIcon, FileTextIcon, ArrowDownWideNarrowIcon, ArrowUpNarrowWideIcon, CalendarIcon, ExternalLinkIcon, FilterIcon, SearchIcon, ChartColumnIcon } from "@lucide/svelte"; + import { LoaderCircleIcon, FileTextIcon, ArrowDownWideNarrowIcon, ArrowUpNarrowWideIcon, CalendarIcon, ExternalLinkIcon, FilterIcon, SearchIcon, ChartColumnIcon, SettingsIcon } from "@lucide/svelte"; import { dateTimeFormat, type TitleContext } from "$lib/common"; - import type { EmoteProps, BadgeProps, Message, ChatComponents, TMIEmote } from "$lib/twitch/logs"; - import { messageSearch } from "$lib/twitch/logs"; + import type { EmoteProps, BadgeProps, Message, ChatComponents, TMIEmote, SearchFilter as SearchFilterType, SearchCriterion } from "$lib/twitch/logs"; + import { messageSearch, advancedMessageSearch, serializeFilter, deserializeFilter } from "$lib/twitch/logs"; import * as TwitchServices from "$lib/twitch/services/index.js"; @@ -187,6 +188,14 @@ dateValue = q.get("d") || ""; searchValue = q.get("s") || ""; isJumpMode = (q.get("sm") || window.localStorage.getItem("logs-search-mode")) === "jump"; + + // Load advanced search filter from URL + const filterParam = q.get("f"); + if (filterParam) { + searchFilter = deserializeFilter(filterParam); + useAdvancedSearch = true; + isFilterOpen = true; + } }); onDestroy(() => { @@ -204,6 +213,12 @@ let channelId = $state(""); + // Search filter state + let searchFilter = $state({ criteria: [] }); + let isFilterOpen = $state(false); + let useAdvancedSearch = $state(false); + let availableBadges = $state>([]); + // Channel Stats let statsPopoverOpen = $state(false); let channelStats = $state(null); @@ -219,6 +234,43 @@ const globalBadges = new SvelteMap(); let badgeUpdates = $state(0); + $effect(() => { + // Update available badges list for the filter component + badgeUpdates; + const badges: Array<{ id: string; title: string; imageUrl: string }> = []; + const seenIds = new Set(); + + // Add global badges first + globalBadges.forEach((badge, key) => { + const badgeId = key.split("/")[0]; + if (!seenIds.has(badgeId)) { + badges.push({ + id: badgeId, + title: badge.title, + imageUrl: badge.url, + }); + seenIds.add(badgeId); + } + }); + + // Then add channel-specific badges + channelBadges.forEach((badge, key) => { + const badgeId = key.split("/")[0]; + if (!seenIds.has(badgeId)) { + badges.push({ + id: badgeId, + title: badge.title, + imageUrl: badge.url, + }); + seenIds.add(badgeId); + } + }); + + // Sort by title + badges.sort((a, b) => a.title.localeCompare(b.title)); + availableBadges = badges; + }); + $effect(() => { const search = { c: channelName, @@ -226,6 +278,7 @@ d: dateValue, s: searchValue, sm: searchValue && isJumpMode ? "jump" : null, + f: useAdvancedSearch && searchFilter.criteria.length > 0 ? serializeFilter(searchFilter) : null, }; untrack(() => { @@ -297,15 +350,20 @@ let contentRef = $state(null); let searchValue = $state(""); - let searchResults = $derived(messageSearch(searchValue, chatLogs, scrollFromBottom)); + let searchResults = $derived.by(() => { + if (searchFilter.criteria.length > 0) { + return advancedMessageSearch(searchFilter, chatLogs, scrollFromBottom); + } + return messageSearch(searchValue, chatLogs, scrollFromBottom); + }); let filteredChatLogs = $derived(isJumpMode ? messageSearch("", chatLogs, scrollFromBottom) : searchResults); - let isJumpSearching = $derived(isJumpMode && searchResults.length && searchValue); + let isJumpSearching = $derived(isJumpMode && searchResults.length && (searchValue || searchFilter.criteria.length > 0)); let jumpHighlights = $derived(isJumpSearching ? new Set(searchResults.map((m) => getMessageId(m))) : void 0); let jumpIndex = $derived(isJumpSearching ? searchResults.findIndex((m) => getMessageId(m) === page.url.hash.slice(1)) : -1); let jumpInputValue = $state(1); let displayMessageCount = $derived.by(() => { - if (searchValue && !isJumpMode) { + if ((searchValue || searchFilter.criteria.length > 0) && !isJumpMode) { return `${searchResults.length.toLocaleString()} / ${chatLogs.length.toLocaleString()}`; } return chatLogs.length.toLocaleString(); @@ -1063,14 +1121,16 @@ {/if} {#if chatLogs.length}
-
-
- - - {displayMessageCount} - -
-
+ {#if !useAdvancedSearch} +
+
+ + + {displayMessageCount} + +
+
+ {/if} {#if isJumpSearching} {@const width = searchResults.length.toString().length + 5}
@@ -1081,11 +1141,37 @@ {/if}
+
+ {#if useAdvancedSearch && isFilterOpen && chatLogs.length} + (searchFilter = filter)} /> + {/if} + {#if error}

{error}

{:else if chatLogs.length} From cacec6c2b115c239f027ac77c9b9ff43099e4c73 Mon Sep 17 00:00:00 2001 From: CatQuery Date: Wed, 25 Mar 2026 12:45:38 -0400 Subject: [PATCH 3/6] pass 2 --- src/lib/twitch/logs.ts | 162 ++++++++++++++++++++++++----------------- 1 file changed, 95 insertions(+), 67 deletions(-) diff --git a/src/lib/twitch/logs.ts b/src/lib/twitch/logs.ts index 2c32fe1..ab4e813 100644 --- a/src/lib/twitch/logs.ts +++ b/src/lib/twitch/logs.ts @@ -63,7 +63,6 @@ const searchPrefixes: Record regex.test(msg.text)); } catch { return []; @@ -74,7 +73,6 @@ const searchPrefixes: Record c.trim()); - return chatLogs.filter((msg) => channels.includes(msg.channel?.toLowerCase() ?? "")); }, from(searchString, chatLogs) { @@ -82,99 +80,129 @@ const searchPrefixes: Record u.trim()); - return chatLogs.filter((msg) => users.includes(msg.displayName.toLowerCase())); }, }; type SearchPrefixKey = keyof typeof searchPrefixes; -// Test a single criterion against a message -const testCriterion = (criterion: SearchCriterion, msg: Message): boolean => { - let result = false; - - switch (criterion.type) { - case "regex": { +/** + * Evaluates a single search criterion against a message. + * @param criterion The search criterion to evaluate. + * @param message The message to test against. + * @returns True if the criterion matches, false otherwise. + */ +const evaluateCriterion = (criterion: SearchCriterion, message: Message): boolean => { + const evaluators: Record boolean> = { + regex: (value, msg) => { try { - const regex = new RegExp(criterion.value, "i"); - result = regex.test(msg.text); + const regex = new RegExp(value, "i"); + return regex.test(msg.text); } catch { - result = false; + return false; } - break; - } - case "text": { - result = msg.text.toLowerCase().includes(criterion.value.toLowerCase()); - break; - } - case "user": { - const users = criterion.value - .toLowerCase() - .split(",") - .map((u) => u.trim()); - result = users.includes(msg.displayName.toLowerCase()); - break; - } - case "channel": { - const channels = criterion.value - .toLowerCase() - .split(",") - .map((c) => c.trim()); - result = channels.includes(msg.channel?.toLowerCase() ?? ""); - break; - } - case "badge": { - const badges = criterion.value - .toLowerCase() - .split(",") - .map((b) => b.trim()); - const msgBadges = msg.tags["badges"]?.split(",").map((b) => b.split("/")[0].toLowerCase()) ?? []; - result = badges.some((badge) => msgBadges.includes(badge)); - break; - } - } + }, + text: (value, msg) => msg.text.toLowerCase().includes(value.toLowerCase()), + user: (value, msg) => { + const users = value.toLowerCase().split(",").map((u) => u.trim()); + return users.includes(msg.displayName.toLowerCase()); + }, + channel: (value, msg) => { + const channels = value.toLowerCase().split(",").map((c) => c.trim()); + return channels.includes(msg.channel?.toLowerCase() ?? ""); + }, + badge: (value, msg) => { + const badges = value.toLowerCase().split(",").map((b) => b.trim()); + const messageBadges = msg.tags["badges"]?.split(",").map((b) => b.split("/")[0].toLowerCase()) ?? []; + return badges.some((badge) => messageBadges.includes(badge)); + }, + }; + const evaluator = evaluators[criterion.type]; + if (!evaluator) return false; + + const result = evaluator(criterion.value, message); return criterion.negate ? !result : result; }; -export const advancedMessageSearch = (filter: SearchFilter, chatLogs: Message[], scrollFromBottom: boolean | null): Message[] => { - if (!filter.criteria.length) return scrollFromBottom === false ? [...chatLogs].reverse() : chatLogs; - - const results = chatLogs.filter((msg) => { - // Group criteria by OR operators (AND has higher precedence) - const orGroups: SearchCriterion[][] = []; - for (const criterion of filter.criteria) { - if (orGroups.length === 0 || criterion.operator === "OR") { - orGroups.push([criterion]); - } else { - orGroups[orGroups.length - 1].push(criterion); - } +/** + * Groups search criteria into OR groups based on their operators. + * Criteria are grouped where consecutive ANDs are in the same group, and OR starts a new group. + * @param criteria The list of search criteria. + * @returns An array of criterion groups, where each group should be ANDed together, and groups ORed. + */ +const groupCriteriaByOr = (criteria: SearchCriterion[]): SearchCriterion[][] => { + const groups: SearchCriterion[][] = []; + for (const criterion of criteria) { + if (groups.length === 0 || criterion.operator === "OR") { + groups.push([criterion]); + } else { + groups[groups.length - 1].push(criterion); } + } + return groups; +}; - // Evaluate each OR group (AND within groups) - let result = false; - for (const group of orGroups) { - let groupResult = true; - for (const criterion of group) { - const testResult = testCriterion(criterion, msg); - groupResult = groupResult && testResult; +/** + * Evaluates whether a message matches the given search filter. + * The filter uses AND/OR logic: criteria are grouped by OR operators, with AND within groups. + * @param filter The search filter containing criteria. + * @param message The message to evaluate. + * @returns True if the message matches the filter, false otherwise. + */ +const messageMatchesFilter = (filter: SearchFilter, message: Message): boolean => { + if (!filter.criteria.length) return true; + + const orGroups = groupCriteriaByOr(filter.criteria); + + // Evaluate each OR group (AND within groups) + for (const group of orGroups) { + let groupMatches = true; + for (const criterion of group) { + if (!evaluateCriterion(criterion, message)) { + groupMatches = false; + break; } - result = result || groupResult; } + if (groupMatches) return true; + } - return result; - }); + return false; +}; +/** + * Performs advanced message search using a structured filter. + * @param filter The search filter with criteria. + * @param chatLogs The array of messages to search. + * @param scrollFromBottom Whether to reverse the order for bottom-scrolling. + * @returns The filtered array of messages. + */ +export const advancedMessageSearch = (filter: SearchFilter, chatLogs: Message[], scrollFromBottom: boolean | null): Message[] => { + if (!filter.criteria.length) { + return scrollFromBottom === false ? [...chatLogs].reverse() : chatLogs; + } + + const results = chatLogs.filter((msg) => messageMatchesFilter(filter, msg)); return scrollFromBottom === false ? [...results].reverse() : results; }; +/** + * Performs basic message search using prefixes or fuzzy search. + * @param searchValue The search string. + * @param chatLogs The array of messages to search. + * @param scrollFromBottom Whether to reverse the order for bottom-scrolling. + * @returns The filtered array of messages. + */ export const messageSearch = (searchValue: string, chatLogs: Message[], scrollFromBottom: boolean | null): Message[] => { const searchKey = searchValue.split(":", 1)[0].toLowerCase(); const searchString = searchValue.slice(searchKey.length + 1); + if (searchKey in searchPrefixes && searchString) { chatLogs = searchPrefixes[searchKey as SearchPrefixKey](searchString, chatLogs); } else if (searchValue) { - const searchOptions = scrollFromBottom === null ? { keys: ["channel", "displayName", "text"], threshold: 0.5 } : { keys: ["displayName", "text"], threshold: 0.5, limit: 5000 }; + const searchOptions = scrollFromBottom === null + ? { keys: ["channel", "displayName", "text"], threshold: 0.5 } + : { keys: ["displayName", "text"], threshold: 0.5, limit: 5000 }; chatLogs = fuzzysort .go(searchValue, chatLogs, searchOptions) From 5523b26707298951179750194b4bd6c18b657e31 Mon Sep 17 00:00:00 2001 From: CatQuery Date: Wed, 25 Mar 2026 19:55:35 -0400 Subject: [PATCH 4/6] lint --- src/routes/firehose/+page.svelte | 5 ++--- src/routes/logs/+page.svelte | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/routes/firehose/+page.svelte b/src/routes/firehose/+page.svelte index 523ba64..152f660 100644 --- a/src/routes/firehose/+page.svelte +++ b/src/routes/firehose/+page.svelte @@ -23,7 +23,7 @@ import { ChevronsDownIcon, SettingsIcon } from "@lucide/svelte"; import { getContext, onDestroy, onMount, tick, untrack } from "svelte"; - import { SvelteMap } from "svelte/reactivity"; + import { SvelteMap, SvelteSet } from "svelte/reactivity"; import { browser } from "$app/environment"; import { goto } from "$app/navigation"; @@ -219,9 +219,8 @@ $effect(() => { // Update available badges list for the filter component - badgeUpdates; const badges: Array<{ id: string; title: string; imageUrl: string }> = []; - const seenIds = new Set(); + const seenIds = new SvelteSet(); // Add global badges globalBadges.forEach((badge, key) => { diff --git a/src/routes/logs/+page.svelte b/src/routes/logs/+page.svelte index ffc3aa9..8817b9f 100644 --- a/src/routes/logs/+page.svelte +++ b/src/routes/logs/+page.svelte @@ -33,7 +33,7 @@ import Reply from "$lib/components/message/reply.svelte"; import { getContext, onDestroy, onMount, tick, untrack } from "svelte"; - import { SvelteMap } from "svelte/reactivity"; + import { SvelteMap, SvelteSet } from "svelte/reactivity"; import { browser } from "$app/environment"; import { page } from "$app/state"; @@ -236,9 +236,8 @@ $effect(() => { // Update available badges list for the filter component - badgeUpdates; const badges: Array<{ id: string; title: string; imageUrl: string }> = []; - const seenIds = new Set(); + const seenIds = new SvelteSet(); // Add global badges first globalBadges.forEach((badge, key) => { From 8f7761e263bdbdad352724d59d71463b0c7a9e49 Mon Sep 17 00:00:00 2001 From: CatQuery Date: Wed, 25 Mar 2026 21:41:53 -0400 Subject: [PATCH 5/6] remove comments --- src/routes/firehose/+page.svelte | 3 --- src/routes/logs/+page.svelte | 4 ---- 2 files changed, 7 deletions(-) diff --git a/src/routes/firehose/+page.svelte b/src/routes/firehose/+page.svelte index 152f660..91ce329 100644 --- a/src/routes/firehose/+page.svelte +++ b/src/routes/firehose/+page.svelte @@ -218,11 +218,9 @@ }; $effect(() => { - // Update available badges list for the filter component const badges: Array<{ id: string; title: string; imageUrl: string }> = []; const seenIds = new SvelteSet(); - // Add global badges globalBadges.forEach((badge, key) => { const badgeId = key.split("/")[0]; if (!seenIds.has(badgeId)) { @@ -235,7 +233,6 @@ } }); - // Sort by title badges.sort((a, b) => a.title.localeCompare(b.title)); availableBadges = badges; }); diff --git a/src/routes/logs/+page.svelte b/src/routes/logs/+page.svelte index 8817b9f..9aade47 100644 --- a/src/routes/logs/+page.svelte +++ b/src/routes/logs/+page.svelte @@ -235,11 +235,9 @@ let badgeUpdates = $state(0); $effect(() => { - // Update available badges list for the filter component const badges: Array<{ id: string; title: string; imageUrl: string }> = []; const seenIds = new SvelteSet(); - // Add global badges first globalBadges.forEach((badge, key) => { const badgeId = key.split("/")[0]; if (!seenIds.has(badgeId)) { @@ -252,7 +250,6 @@ } }); - // Then add channel-specific badges channelBadges.forEach((badge, key) => { const badgeId = key.split("/")[0]; if (!seenIds.has(badgeId)) { @@ -265,7 +262,6 @@ } }); - // Sort by title badges.sort((a, b) => a.title.localeCompare(b.title)); availableBadges = badges; }); From 1298e2e8cf67cfad0aa3a96df2f0905f401c7c4f Mon Sep 17 00:00:00 2001 From: CatQuery Date: Wed, 25 Mar 2026 21:51:32 -0400 Subject: [PATCH 6/6] remove useless comments --- src/lib/components/search-filter.svelte | 2 -- src/lib/twitch/logs.ts | 9 +++++---- src/routes/firehose/+page.svelte | 1 - src/routes/logs/+page.svelte | 1 - 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/lib/components/search-filter.svelte b/src/lib/components/search-filter.svelte index c422dc5..8614b53 100644 --- a/src/lib/components/search-filter.svelte +++ b/src/lib/components/search-filter.svelte @@ -47,7 +47,6 @@ const removeCriterion = (id: string) => { criteria = criteria.filter((c) => c.id !== id); - // Update operators if (criteria.length > 0) { criteria[0].operator = undefined; // First criterion doesn't have an operator } @@ -68,7 +67,6 @@ criteria[index] = { ...criteria[index], type, - // Don't reset value when changing type }; emitChange(); } diff --git a/src/lib/twitch/logs.ts b/src/lib/twitch/logs.ts index ab4e813..579fbf7 100644 --- a/src/lib/twitch/logs.ts +++ b/src/lib/twitch/logs.ts @@ -37,20 +37,18 @@ export type SearchCriterion = { id: string; type: "regex" | "text" | "user" | "channel" | "badge"; value: string; - operator?: "AND" | "OR"; // operator before this criterion - negate?: boolean; // for NOT logic + operator?: "AND" | "OR"; + negate?: boolean; }; export type SearchFilter = { criteria: SearchCriterion[]; }; -// Serialize filter to URL-safe string export const serializeFilter = (filter: SearchFilter): string => { return btoa(JSON.stringify(filter)); }; -// Deserialize filter from URL-safe string export const deserializeFilter = (encoded: string): SearchFilter => { try { return JSON.parse(atob(encoded)); @@ -63,6 +61,7 @@ const searchPrefixes: Record regex.test(msg.text)); } catch { return []; @@ -73,6 +72,7 @@ const searchPrefixes: Record c.trim()); + return chatLogs.filter((msg) => channels.includes(msg.channel?.toLowerCase() ?? "")); }, from(searchString, chatLogs) { @@ -80,6 +80,7 @@ const searchPrefixes: Record u.trim()); + return chatLogs.filter((msg) => users.includes(msg.displayName.toLowerCase())); }, }; diff --git a/src/routes/firehose/+page.svelte b/src/routes/firehose/+page.svelte index 91ce329..2b70b79 100644 --- a/src/routes/firehose/+page.svelte +++ b/src/routes/firehose/+page.svelte @@ -91,7 +91,6 @@ searchValue = q.get("s") || ""; - // Load advanced search filter from URL const filterParam = q.get("f"); if (filterParam) { searchFilter = deserializeFilter(filterParam); diff --git a/src/routes/logs/+page.svelte b/src/routes/logs/+page.svelte index 9aade47..a501bdc 100644 --- a/src/routes/logs/+page.svelte +++ b/src/routes/logs/+page.svelte @@ -189,7 +189,6 @@ searchValue = q.get("s") || ""; isJumpMode = (q.get("sm") || window.localStorage.getItem("logs-search-mode")) === "jump"; - // Load advanced search filter from URL const filterParam = q.get("f"); if (filterParam) { searchFilter = deserializeFilter(filterParam);