ForYou: events from favorite authors, debug logging
continuous-integration/drone/push Build is running Details

This commit is contained in:
Martti Malmi 2024-02-03 16:48:11 +02:00
parent e746109f5c
commit d0bc8df6a1
2 changed files with 88 additions and 30 deletions

View File

@ -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<NostrEvent[]> {
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<string>, myPubkey: string) {
const myReactedAuthors = new Map<string, number>();
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<string>();
const myEvents = await Relay.query([
"REQ",
"getMyReactedEventIds",
@ -100,17 +127,40 @@ async function getEventIdsReactedByOthers(
return eventIdsReactedByOthers;
}
async function getFeedEvents(reactedToIds: Map<string, number>) {
async function getFeedEvents(reactedToIds: Map<string, number>, reactedToAuthors: Map<string, number>) {
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<string>(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<string, number>) {
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<string, number>) {
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;

View File

@ -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<string>();
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]);