mirror of
https://github.com/hoornet/vega.git
synced 2026-06-08 06:01:57 -07:00
Add opt-in WoT feed filter (hop-1 + hop-2 social graph)
This commit is contained in:
@@ -3,6 +3,7 @@ import { useFeedStore } from "../../stores/feed";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { useMuteStore } from "../../stores/mute";
|
||||
import { useUIStore } from "../../stores/ui";
|
||||
import { useWoTStore } from "../../stores/wot";
|
||||
import { fetchFollowFeed, getNDK, ensureConnected } from "../../lib/nostr";
|
||||
import { diagWrapFetch, logDiag } from "../../lib/feedDiagnostics";
|
||||
import { detectScript, getEventLanguageTag, FILTER_SCRIPTS } from "../../lib/language";
|
||||
@@ -44,6 +45,11 @@ export function Feed() {
|
||||
const openHashtag = useUIStore((s) => s.openHashtag);
|
||||
const feedLanguageFilter = useUIStore((s) => s.feedLanguageFilter);
|
||||
const setFeedLanguageFilter = useUIStore((s) => s.setFeedLanguageFilter);
|
||||
const wotEnabled = useWoTStore((s) => s.enabled);
|
||||
const wotSet = useWoTStore((s) => s.wotSet);
|
||||
const wotLoading = useWoTStore((s) => s.loading);
|
||||
const buildWoT = useWoTStore((s) => s.buildWoT);
|
||||
const myPubkey = useUserStore((s) => s.pubkey);
|
||||
const [followNotes, setFollowNotes] = useState<NDKEvent[]>([]);
|
||||
const [followLoading, setFollowLoading] = useState(false);
|
||||
const [, setTick] = useState(0);
|
||||
@@ -70,6 +76,12 @@ export function Feed() {
|
||||
}
|
||||
}, [tab, follows]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wotEnabled && myPubkey && follows.length > 0 && wotSet.size === 0 && !wotLoading) {
|
||||
buildWoT(myPubkey, follows);
|
||||
}
|
||||
}, [wotEnabled, myPubkey, follows]);
|
||||
|
||||
const loadFollowFeed = useCallback(async () => {
|
||||
setFollowLoading(true);
|
||||
try {
|
||||
@@ -91,6 +103,7 @@ export function Feed() {
|
||||
const filteredNotes = activeNotes.filter((event) => {
|
||||
if (mutedPubkeys.includes(event.pubkey)) return false;
|
||||
if (contentMatchesMutedKeyword(event.content)) return false;
|
||||
if (tab === "global" && wotEnabled && wotSet.size > 0 && !wotSet.has(event.pubkey)) return false;
|
||||
const c = event.content.trim();
|
||||
if (!c || c.startsWith("{") || c.startsWith("[")) return false;
|
||||
// Filter out notes that look like base64 blobs or relay protocol messages
|
||||
|
||||
@@ -3,6 +3,7 @@ import { save } from "@tauri-apps/plugin-dialog";
|
||||
import { writeTextFile } from "@tauri-apps/plugin-fs";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { useUIStore } from "../../stores/ui";
|
||||
import { useWoTStore } from "../../stores/wot";
|
||||
import { themes } from "../../lib/themes";
|
||||
import { useMuteStore } from "../../stores/mute";
|
||||
import { useBookmarkStore } from "../../stores/bookmark";
|
||||
@@ -136,6 +137,73 @@ function MutedKeywordsSection() {
|
||||
);
|
||||
}
|
||||
|
||||
function WoTSection() {
|
||||
const { enabled, wotSet, loading, setEnabled, buildWoT } = useWoTStore();
|
||||
const { pubkey, follows } = useUserStore();
|
||||
const noFollows = follows.length === 0;
|
||||
|
||||
const toggle = () => {
|
||||
const next = !enabled;
|
||||
setEnabled(next);
|
||||
if (next && pubkey && follows.length > 0 && wotSet.size === 0 && !loading) {
|
||||
buildWoT(pubkey, follows);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRebuild = () => {
|
||||
if (pubkey && follows.length > 0 && !loading) {
|
||||
buildWoT(pubkey, follows);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2 className="text-text text-[11px] font-medium uppercase tracking-widest mb-2 text-text-dim">
|
||||
Web of Trust filter
|
||||
</h2>
|
||||
<p className="text-text-dim text-[11px] mb-3">
|
||||
Only show global feed notes from people you follow or people they follow.
|
||||
</p>
|
||||
<label className={`flex items-center gap-3 ${noFollows ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}>
|
||||
<button
|
||||
onClick={noFollows ? undefined : toggle}
|
||||
disabled={noFollows}
|
||||
className={`w-9 h-5 rounded-full transition-colors relative shrink-0 ${
|
||||
enabled && !noFollows ? "bg-accent" : "bg-border"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-bg transition-transform ${
|
||||
enabled && !noFollows ? "translate-x-4" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<span className="text-text text-[12px]">WoT feed filter</span>
|
||||
</label>
|
||||
{noFollows && (
|
||||
<p className="text-text-dim text-[10px] mt-1.5 ml-12">Follow some people first.</p>
|
||||
)}
|
||||
{enabled && !noFollows && (
|
||||
<div className="mt-2 ml-12 space-y-1">
|
||||
{loading ? (
|
||||
<p className="text-text-dim text-[10px]">Building…</p>
|
||||
) : wotSet.size > 0 ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="text-text-dim text-[10px]">Trusted accounts: {wotSet.size}</p>
|
||||
<button
|
||||
onClick={handleRebuild}
|
||||
className="text-[10px] text-text-dim hover:text-accent transition-colors"
|
||||
>
|
||||
Rebuild
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function IdentitySection() {
|
||||
const { npub, loggedIn } = useUserStore();
|
||||
const [copied, setCopied] = useState(false);
|
||||
@@ -453,6 +521,7 @@ export function SettingsView() {
|
||||
<IdentitySection />
|
||||
<MuteSection />
|
||||
<MutedKeywordsSection />
|
||||
<WoTSection />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { getNDK, fetchWithTimeout, FEED_TIMEOUT } from "./core";
|
||||
|
||||
export async function buildWoTSet(myPubkey: string, directFollows: string[]): Promise<Set<string>> {
|
||||
const trusted = new Set<string>();
|
||||
trusted.add(myPubkey); // own notes always pass
|
||||
|
||||
// Hop 1 — direct follows
|
||||
for (const pk of directFollows) trusted.add(pk);
|
||||
|
||||
if (directFollows.length === 0) return trusted;
|
||||
|
||||
// Hop 2 — fetch all their contact lists in one batch
|
||||
const ndk = getNDK();
|
||||
const events = await fetchWithTimeout(ndk, { kinds: [3], authors: directFollows }, FEED_TIMEOUT);
|
||||
for (const event of events) {
|
||||
for (const tag of event.tags) {
|
||||
if (tag[0] === "p" && tag[1]) trusted.add(tag[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return trusted;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { create } from "zustand";
|
||||
import { buildWoTSet } from "../lib/nostr/wot";
|
||||
|
||||
interface WoTState {
|
||||
enabled: boolean;
|
||||
wotSet: Set<string>;
|
||||
loading: boolean;
|
||||
lastBuilt: number | null;
|
||||
setEnabled: (v: boolean) => void;
|
||||
buildWoT: (myPubkey: string, follows: string[]) => Promise<void>;
|
||||
}
|
||||
|
||||
const WOT_ENABLED_KEY = "vega_wot_enabled";
|
||||
|
||||
export const useWoTStore = create<WoTState>((set) => ({
|
||||
enabled: localStorage.getItem(WOT_ENABLED_KEY) === "true",
|
||||
wotSet: new Set(),
|
||||
loading: false,
|
||||
lastBuilt: null,
|
||||
setEnabled: (v) => {
|
||||
localStorage.setItem(WOT_ENABLED_KEY, String(v));
|
||||
set({ enabled: v });
|
||||
},
|
||||
buildWoT: async (myPubkey, follows) => {
|
||||
set({ loading: true });
|
||||
try {
|
||||
const wotSet = await buildWoTSet(myPubkey, follows);
|
||||
set({ wotSet, lastBuilt: Date.now() });
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user