From 7b8065ff38e85078406a61506ee4be38997fed0f Mon Sep 17 00:00:00 2001 From: WardPearce Date: Wed, 4 Mar 2026 02:01:38 +1300 Subject: [PATCH] More progress on content filtering --- .../lib/components/layout/ItemsList.svelte | 3 ++ materialious/src/lib/filtering.ts | 40 ++++++++++++++++--- materialious/src/lib/store.ts | 8 ++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/materialious/src/lib/components/layout/ItemsList.svelte b/materialious/src/lib/components/layout/ItemsList.svelte index a309d5a6..b0975967 100644 --- a/materialious/src/lib/components/layout/ItemsList.svelte +++ b/materialious/src/lib/components/layout/ItemsList.svelte @@ -14,6 +14,7 @@ import { SpatialMenu } from 'melt/builders'; import { mergeAttrs } from 'melt'; import { Capacitor } from '@capacitor/core'; + import { filterByBlocklist } from '$lib/filtering'; interface Props { items?: FeedItems; @@ -35,6 +36,8 @@ } onMount(async () => { + items = filterByBlocklist(items); + if (!$feedLastItemId) return; const element = document.getElementById($feedLastItemId); diff --git a/materialious/src/lib/filtering.ts b/materialious/src/lib/filtering.ts index 0bb71e96..c41a4e1e 100644 --- a/materialious/src/lib/filtering.ts +++ b/materialious/src/lib/filtering.ts @@ -1,6 +1,8 @@ import { z } from 'zod'; import type { FeedItem } from './feed'; import isSafeRegex from 'safe-regex2'; +import { blocklistStore } from './store'; +import { get } from 'svelte/store'; const zFilterOperatorEnum = z.enum([ 'equals', // equal to @@ -31,15 +33,23 @@ const zFilterGroup = z.object({ operator: z.enum(['AND', 'OR']).optional() // Logical grouping of conditions }); -const zBlockListSchema = z.array(zFilterGroup); +export const zBlockListSchema = z.array(zFilterGroup); + +const zBlocklistRootSchema = z.object({ + version: z.literal('v1'), + for: z.literal('materialious'), + blocklist: zBlockListSchema +}); + +export function filterByBlocklist(data: T[]): T[] { + const blocklist = get(blocklistStore); + if (!blocklist) return data; -export function filterByBlocklist( - data: T[], - blocklist: z.infer -): T[] { return data.filter((item) => { return blocklist.every((filterGroup) => { return filterGroup.conditions.every((condition) => { + if (!(condition.field in item)) return false; + const fieldValue = item[condition.field as keyof T]; switch (condition.operator) { @@ -77,3 +87,23 @@ export function filterByBlocklist( }); }); } + +export async function loadBlockListFromRemote(url: string) { + const resp = await fetch(url, { method: 'GET', credentials: 'omit' }); + if (!resp.ok) throw new Error('Response status code'); + + let respJson; + try { + respJson = await resp.json(); + } catch { + // Handled outside of catch + } + + if (!respJson) throw new Error('Invalid JSON'); + + const parsedBlocklist = zBlocklistRootSchema.safeParse(respJson); + + if (!parsedBlocklist.success) throw new Error(parsedBlocklist.error.message); + + blocklistStore.set(parsedBlocklist.data.blocklist); +} diff --git a/materialious/src/lib/store.ts b/materialious/src/lib/store.ts index eed39c50..d699db3b 100644 --- a/materialious/src/lib/store.ts +++ b/materialious/src/lib/store.ts @@ -22,6 +22,8 @@ import type { import type { ParsedDescription } from './description'; import { ensureNoTrailingSlash, getPublicEnv } from './misc'; import type { EngineFallback } from './api/misc'; +import type z from 'zod'; +import type { zBlockListSchema } from './filtering'; function createListenerFunctions(): { callListeners: (eventKey: string, newValue: any) => void; @@ -339,6 +341,12 @@ export const searchHistoryStore: Writable = persist( 'searchHistory' ); +export const blocklistStore: Writable | undefined> = persist( + writable(), + createStorage(), + 'blocklist' +); + export const feedLoadingStore: Writable = writable(false); export const feedCacheStore: Writable<{ [key: string]: (VideoBase | Video | PlaylistPageVideo)[];