From d0bc8df6a1399e84467c9a1404dfcb831ea2db90 Mon Sep 17 00:00:00 2001 From: Martti Malmi Date: Sat, 3 Feb 2024 16:48:11 +0200 Subject: [PATCH] ForYou: events from favorite authors, debug logging --- packages/app/src/Db/getForYouFeed.ts | 84 +++++++++++++++++++---- packages/app/src/Pages/Root/ForYouTab.tsx | 34 ++++----- 2 files changed, 88 insertions(+), 30 deletions(-) diff --git a/packages/app/src/Db/getForYouFeed.ts b/packages/app/src/Db/getForYouFeed.ts index dc797b47..bb833714 100644 --- a/packages/app/src/Db/getForYouFeed.ts +++ b/packages/app/src/Db/getForYouFeed.ts @@ -1,35 +1,62 @@ import { NostrEvent, parseZap } from "@snort/system"; +import debug from "debug"; import { Relay } from "@/Cache"; +const log = debug("getForYouFeed"); + export async function getForYouFeed(pubkey: string): Promise { console.time("For You feed generation time"); - console.log("pubkey", pubkey); + log("pubkey", pubkey); // Get events reacted to by me - const myReactedEvents = await getMyReactedEventIds(pubkey); - console.log("my reacted events", myReactedEvents); + const myReactedEventIds = await getMyReactedEvents(pubkey); + log("my reacted events", myReactedEventIds); + + const myReactedAuthors = await getMyReactedAuthors(myReactedEventIds, pubkey); + log("my reacted authors", myReactedAuthors); // Get others who reacted to the same events as me - const othersWhoReacted = await getOthersWhoReacted(myReactedEvents, pubkey); + const othersWhoReacted = await getOthersWhoReacted(myReactedEventIds, pubkey); // this tends to be small when the user has just logged in, we should maybe subscribe for more from relays - console.log("others who reacted", othersWhoReacted); + log("others who reacted", othersWhoReacted); // Get event ids reacted to by those others - const reactedByOthers = await getEventIdsReactedByOthers(othersWhoReacted, myReactedEvents, pubkey); - console.log("reacted by others", reactedByOthers); + const reactedByOthers = await getEventIdsReactedByOthers(othersWhoReacted, myReactedEventIds, pubkey); + log("reacted by others", reactedByOthers); // Get full events in sorted order - const feed = await getFeedEvents(reactedByOthers); - console.log("feed.length", feed.length); + const feed = await getFeedEvents(reactedByOthers, myReactedAuthors); + log("feed.length", feed.length); console.timeEnd("For You feed generation time"); return feed; } -async function getMyReactedEventIds(pubkey: string) { +async function getMyReactedAuthors(myReactedEventIds: Set, myPubkey: string) { + const myReactedAuthors = new Map(); + + const myReactions = await Relay.query([ + "REQ", + "getMyReactedAuthors", + { + "#e": Array.from(myReactedEventIds), + }, + ]); + + myReactions.forEach(reaction => { + if (reaction.pubkey !== myPubkey) { + myReactedAuthors.set(reaction.pubkey, (myReactedAuthors.get(reaction.pubkey) || 0) + 1); + } + }); + + return myReactedAuthors; +} + +async function getMyReactedEvents(pubkey: string) { const myReactedEventIds = new Set(); + const myEvents = await Relay.query([ "REQ", "getMyReactedEventIds", @@ -100,17 +127,40 @@ async function getEventIdsReactedByOthers( return eventIdsReactedByOthers; } -async function getFeedEvents(reactedToIds: Map) { +async function getFeedEvents(reactedToIds: Map, reactedToAuthors: Map) { const events = await Relay.query([ "REQ", "getFeedEvents", { ids: Array.from(reactedToIds.keys()), kinds: [1], - // max 24h old since: Math.floor(Date.now() / 1000) - 60 * 60 * 24 * 7, }, ]); + const seen = new Set(events.map(ev => ev.id)); + + log('reactedToAuthors', reactedToAuthors); + + const favoriteAuthors = Array.from(reactedToAuthors.keys()) + .sort((a, b) => reactedToAuthors.get(b)! - reactedToAuthors.get(a)!) + .slice(20); + + const eventsByFavoriteAuthors = await Relay.query([ + "REQ", + "getFeedEvents", + { + authors: favoriteAuthors, + kinds: [1], + since: Math.floor(Date.now() / 1000) - 60 * 60 * 24, + limit: 100, + }, + ]); + + eventsByFavoriteAuthors.forEach(ev => { + if (!seen.has(ev.id)) { + events.push(ev); + } + }); // Filter out replies const filteredEvents = events.filter(ev => !ev.tags.some(tag => tag[0] === "e")); @@ -137,6 +187,9 @@ async function getFeedEvents(reactedToIds: Map) { const normalize = (value: number, min: number, max: number) => (value - min) / (max - min); + const maxFavoriteness = Math.max(...Array.from(reactedToAuthors.values())); + const favoritenessWeight = 0.5; + // Normalize and sort events by calculated score filteredEvents.sort((a, b) => { const aReactions = normalize(reactedToIds.get(a.id) || 0, minReactions, maxReactions); @@ -145,10 +198,13 @@ async function getFeedEvents(reactedToIds: Map) { const aAge = normalize(currentTime - new Date(a.created_at).getTime(), minAge, maxAge); const bAge = normalize(currentTime - new Date(b.created_at).getTime(), minAge, maxAge); + const aFavoriteness = normalize(reactedToAuthors.get(a.pubkey) || 0, 0, maxFavoriteness); + const bFavoriteness = normalize(reactedToAuthors.get(b.pubkey) || 0, 0, maxFavoriteness); + // randomly big or small weight for recentness const recentnessWeight = Math.random() > 0.5 ? -0.1 : -10; - const aScore = aReactions + recentnessWeight * aAge; - const bScore = bReactions + recentnessWeight * bAge; + const aScore = aReactions + recentnessWeight * aAge + aFavoriteness * favoritenessWeight; + const bScore = bReactions + recentnessWeight * bAge + bFavoriteness * favoritenessWeight; // Sort by descending score return bScore - aScore; diff --git a/packages/app/src/Pages/Root/ForYouTab.tsx b/packages/app/src/Pages/Root/ForYouTab.tsx index d8014e5b..4dfa53d2 100644 --- a/packages/app/src/Pages/Root/ForYouTab.tsx +++ b/packages/app/src/Pages/Root/ForYouTab.tsx @@ -1,4 +1,4 @@ -import {EventKind, TaggedNostrEvent} from "@snort/system"; +import { EventKind, TaggedNostrEvent } from "@snort/system"; import { memo, useEffect, useMemo, useState } from "react"; import { FormattedMessage } from "react-intl"; import { Link } from "react-router-dom"; @@ -7,10 +7,10 @@ import { DisplayAs, DisplayAsSelector } from "@/Components/Feed/DisplayAsSelecto import { TimelineRenderer } from "@/Components/Feed/TimelineRenderer"; import { TaskList } from "@/Components/Tasks/TaskList"; import { getForYouFeed } from "@/Db/getForYouFeed"; +import useTimelineFeed, { TimelineFeedOptions, TimelineSubject } from "@/Feed/TimelineFeed"; import useLogin from "@/Hooks/useLogin"; import messages from "@/Pages/messages"; import { System } from "@/system"; -import useTimelineFeed, {TimelineFeedOptions, TimelineSubject} from "@/Feed/TimelineFeed"; const FollowsHint = () => { const { publicKey: pubKey, follows } = useLogin(); @@ -71,7 +71,6 @@ export const ForYouTab = memo(function ForYouTab() { getForYouFeedPromise = getForYouFeed(publicKey); } getForYouFeedPromise!.then(notes => { - console.log("for you feed", notes); if (notes.length < 10) { setTimeout(() => { getForYouFeedPromise = null; @@ -98,13 +97,23 @@ export const ForYouTab = memo(function ForYouTab() { }, []); const combinedFeed = useMemo(() => { - // combine feeds: intermittently pick from both feeds const seen = new Set(); const combined = []; - let i = 0; - let j = 0; - while (i < notes.length || j < latestFeed.main?.length) { - if (i < notes.length) { + let i = 0; // Index for `notes` + let j = 0; // Index for `latestFeed.main` + let count = 0; // Combined feed count to decide when to insert from `latestFeed` + + while (i < notes.length || j < (latestFeed.main?.length ?? 0)) { + // Insert approximately 1 event from `latestFeed` for every 4 events from `notes` + if (count % 5 === 0 && j < (latestFeed.main?.length ?? 0)) { + const ev = latestFeed.main[j]; + if (!seen.has(ev.id) && !ev.tags.some((a: string[]) => a[0] === "e")) { + seen.add(ev.id); + combined.push(ev); + } + j++; + } else if (i < notes.length) { + // Add from `notes` otherwise const ev = notes[i]; if (!seen.has(ev.id)) { seen.add(ev.id); @@ -112,14 +121,7 @@ export const ForYouTab = memo(function ForYouTab() { } i++; } - if (j < latestFeed.main?.length) { - const ev = latestFeed.main[j]; - if (!seen.has(ev.id) && !ev.tags?.some((tag: string[]) => tag[0] === "e")) { - seen.add(ev.id); - combined.push(ev); - } - j++; - } + count++; } return combined; }, [notes, latestFeed.main]);