From 7ceab04cbc0639710076bc95b8b5bc57c8aa3d53 Mon Sep 17 00:00:00 2001 From: Martti Malmi Date: Mon, 5 Feb 2024 11:06:39 +0200 Subject: [PATCH] set event seen_at times, sort by seen_at in ForYouFeed --- .../app/src/Components/Event/Note/Note.tsx | 16 +++- packages/app/src/Pages/Root/ForYouTab.tsx | 4 +- packages/worker-relay/src/forYouFeed.ts | 73 +++++++++---------- packages/worker-relay/src/interface.ts | 14 +++- packages/worker-relay/src/memory-relay.ts | 6 +- packages/worker-relay/src/migrations.ts | 8 +- packages/worker-relay/src/sqlite-relay.ts | 11 ++- packages/worker-relay/src/types.ts | 8 +- packages/worker-relay/src/worker.ts | 11 ++- 9 files changed, 100 insertions(+), 51 deletions(-) diff --git a/packages/app/src/Components/Event/Note/Note.tsx b/packages/app/src/Components/Event/Note/Note.tsx index 08857282..02e76302 100644 --- a/packages/app/src/Components/Event/Note/Note.tsx +++ b/packages/app/src/Components/Event/Note/Note.tsx @@ -1,11 +1,12 @@ import { EventKind, NostrLink } from "@snort/system"; import classNames from "classnames"; -import React, { useCallback, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { useInView } from "react-intersection-observer"; import { FormattedMessage } from "react-intl"; import { useNavigate } from "react-router-dom"; import { LRUCache } from "typescript-lru-cache"; +import { Relay } from "@/Cache"; import NoteHeader from "@/Components/Event/Note/NoteHeader"; import { NoteText } from "@/Components/Event/Note/NoteText"; import { TranslationInfo } from "@/Components/Event/Note/TranslationInfo"; @@ -38,6 +39,7 @@ export function Note(props: NoteProps) { const baseClassName = classNames("note min-h-[110px] flex flex-col gap-4 card", className ?? ""); const { isEventMuted } = useModeration(); const { ref, inView } = useInView({ triggerOnce: true, rootMargin: "2000px" }); + const { ref: setSeenAtRef, inView: setSeenAtInView } = useInView({ rootMargin: "0px", threshold: 1 }); const [showTranslation, setShowTranslation] = useState(true); const [translated, setTranslated] = useState(translationCache.get(ev.id)); const cachedSetTranslated = useCallback( @@ -48,6 +50,16 @@ export function Note(props: NoteProps) { [ev.id], ); + useEffect(() => { + let timeout: ReturnType; + if (setSeenAtInView) { + timeout = setTimeout(() => { + Relay.setEventMetadata(ev.id, { seen_at: Math.round(Date.now() / 1000) }); + }, 5000); + } + return () => clearTimeout(timeout); + }, [setSeenAtInView]); + const optionsMerged = { ...defaultOptions, ...opt }; const goToEvent = useGoToEvent(props, optionsMerged); @@ -71,7 +83,7 @@ export function Note(props: NoteProps) { {translated && } {ev.kind === EventKind.Polls && } {optionsMerged.showFooter && ( -
+
)} diff --git a/packages/app/src/Pages/Root/ForYouTab.tsx b/packages/app/src/Pages/Root/ForYouTab.tsx index 0fa73ac9..cb5cb88a 100644 --- a/packages/app/src/Pages/Root/ForYouTab.tsx +++ b/packages/app/src/Pages/Root/ForYouTab.tsx @@ -1,9 +1,9 @@ -import {EventKind, NostrEvent} from "@snort/system"; +import { EventKind, NostrEvent } from "@snort/system"; import { memo, useEffect, useMemo, useState } from "react"; import { FormattedMessage } from "react-intl"; import { Link } from "react-router-dom"; -import {Relay} from "@/Cache"; +import { Relay } from "@/Cache"; import { DisplayAs, DisplayAsSelector } from "@/Components/Feed/DisplayAsSelector"; import { TimelineRenderer } from "@/Components/Feed/TimelineRenderer"; import { TaskList } from "@/Components/Tasks/TaskList"; diff --git a/packages/worker-relay/src/forYouFeed.ts b/packages/worker-relay/src/forYouFeed.ts index eb54c593..ceba1f34 100644 --- a/packages/worker-relay/src/forYouFeed.ts +++ b/packages/worker-relay/src/forYouFeed.ts @@ -1,7 +1,7 @@ import { NostrEvent, parseZap } from "@snort/system"; import debug from "debug"; -import {RelayHandler} from "./types"; +import { RelayHandler } from "./types"; const log = debug("getForYouFeed"); @@ -37,12 +37,9 @@ export async function getForYouFeed(relay: RelayHandler, pubkey: string): Promis async function getMyReactedAuthors(relay: RelayHandler, myReactedEventIds: Set, myPubkey: string) { const myReactedAuthors = new Map(); - const myReactions = relay.req( - "getMyReactedAuthors", - { - "#e": Array.from(myReactedEventIds), - }, - ) as NostrEvent[]; + const myReactions = relay.req("getMyReactedAuthors", { + "#e": Array.from(myReactedEventIds), + }) as NostrEvent[]; myReactions.forEach(reaction => { if (reaction.pubkey !== myPubkey) { @@ -56,13 +53,10 @@ async function getMyReactedAuthors(relay: RelayHandler, myReactedEventIds: Set(); - const myEvents = relay.req( - "getMyReactedEventIds", - { - authors: [pubkey], - kinds: [1, 6, 7, 9735], - }, - ) as NostrEvent[]; + const myEvents = relay.req("getMyReactedEventIds", { + authors: [pubkey], + kinds: [1, 6, 7, 9735], + }) as NostrEvent[]; myEvents.forEach(ev => { const targetEventId = ev.kind === 9735 ? parseZap(ev).event?.id : ev.tags.find(tag => tag[0] === "e")?.[1]; if (targetEventId) { @@ -76,12 +70,9 @@ async function getMyReactedEvents(relay: RelayHandler, pubkey: string) { async function getOthersWhoReacted(relay: RelayHandler, myReactedEventIds: Set, myPubkey: string) { const othersWhoReacted = new Map(); - const otherReactions = relay.req( - "getOthersWhoReacted", - { - "#e": Array.from(myReactedEventIds), - }, - ) as NostrEvent[]; + const otherReactions = relay.req("getOthersWhoReacted", { + "#e": Array.from(myReactedEventIds), + }) as NostrEvent[]; otherReactions.forEach(reaction => { if (reaction.pubkey !== myPubkey) { @@ -100,13 +91,10 @@ async function getEventIdsReactedByOthers( ) { const eventIdsReactedByOthers = new Map(); - const events = relay.req( - "getEventIdsReactedByOthers", - { - authors: [...othersWhoReacted.keys()], - kinds: [1, 6, 7, 9735], - }, - ) as NostrEvent[]; + const events = relay.req("getEventIdsReactedByOthers", { + authors: [...othersWhoReacted.keys()], + kinds: [1, 6, 7, 9735], + }) as NostrEvent[]; events.forEach(event => { if (event.pubkey === myPub || myReactedEvents.has(event.id)) { @@ -124,11 +112,19 @@ async function getEventIdsReactedByOthers( return eventIdsReactedByOthers; } -async function getFeedEvents(relay: RelayHandler, reactedToIds: Map, reactedToAuthors: Map) { - const events = relay.sql( - `select json from events where id in (${Array.from(reactedToIds.keys()).map(() => "?").join(", ")}) and kind = 1 order by seen_at ASC, created DESC limit 1000`, - Array.from(reactedToIds.keys()), - ).map(row => JSON.parse(row[0] as string) as NostrEvent); +async function getFeedEvents( + relay: RelayHandler, + reactedToIds: Map, + reactedToAuthors: Map, +) { + const events = relay + .sql( + `select json from events where id in (${Array.from(reactedToIds.keys()) + .map(() => "?") + .join(", ")}) and kind = 1 order by seen_at ASC, created DESC limit 1000`, + Array.from(reactedToIds.keys()), + ) + .map(row => JSON.parse(row[0] as string) as NostrEvent); const seen = new Set(events.map(ev => ev.id)); @@ -138,11 +134,14 @@ async function getFeedEvents(relay: RelayHandler, reactedToIds: Map reactedToAuthors.get(b)! - reactedToAuthors.get(a)!) .slice(20); - const eventsByFavoriteAuthors = relay.sql( - `select json from events where pubkey in (${favoriteAuthors.map(() => "?").join(", ")}) and kind = 1 order by seen_at ASC, created DESC limit 100`, - favoriteAuthors, - ).map(row => JSON.parse(row[0] as string) as NostrEvent); - + const eventsByFavoriteAuthors = relay + .sql( + `select json from events where pubkey in (${favoriteAuthors + .map(() => "?") + .join(", ")}) and kind = 1 order by seen_at ASC, created DESC limit 100`, + favoriteAuthors, + ) + .map(row => JSON.parse(row[0] as string) as NostrEvent); eventsByFavoriteAuthors.forEach(ev => { if (!seen.has(ev.id)) { diff --git a/packages/worker-relay/src/interface.ts b/packages/worker-relay/src/interface.ts index 3e344212..580cb967 100644 --- a/packages/worker-relay/src/interface.ts +++ b/packages/worker-relay/src/interface.ts @@ -1,4 +1,12 @@ -import { NostrEvent, OkResponse, ReqCommand, ReqFilter, WorkerMessage, WorkerMessageCommand } from "./types"; +import { + EventMetadata, + NostrEvent, + OkResponse, + ReqCommand, + ReqFilter, + WorkerMessage, + WorkerMessageCommand, +} from "./types"; import { v4 as uuid } from "uuid"; export class WorkerRelayInterface { @@ -49,6 +57,10 @@ export class WorkerRelayInterface { return await this.#workerRpc>("forYouFeed", pubkey); } + setEventMetadata(id: string, meta: EventMetadata) { + return this.#workerRpc<[string, EventMetadata], void>("setEventMetadata", [id, meta]); + } + #workerRpc(cmd: WorkerMessageCommand, args?: T) { const id = uuid(); const msg = { diff --git a/packages/worker-relay/src/memory-relay.ts b/packages/worker-relay/src/memory-relay.ts index 9272b654..ea1d8de7 100644 --- a/packages/worker-relay/src/memory-relay.ts +++ b/packages/worker-relay/src/memory-relay.ts @@ -1,5 +1,5 @@ import EventEmitter from "eventemitter3"; -import { NostrEvent, RelayHandler, RelayHandlerEvents, ReqFilter, eventMatchesFilter } from "./types"; +import { NostrEvent, RelayHandler, RelayHandlerEvents, ReqFilter, eventMatchesFilter, EventMetadata } from "./types"; import debug from "debug"; /** @@ -79,4 +79,8 @@ export class InMemoryRelay extends EventEmitter implements R } return ret; } + + setEventMetadata(_id: string, _meta: EventMetadata) { + return; + } } diff --git a/packages/worker-relay/src/migrations.ts b/packages/worker-relay/src/migrations.ts index 219d91de..cfba0662 100644 --- a/packages/worker-relay/src/migrations.ts +++ b/packages/worker-relay/src/migrations.ts @@ -1,8 +1,8 @@ -import {NostrEvent} from "./types"; -import {SqliteRelay} from "./sqlite-relay"; +import { NostrEvent } from "./types"; +import { SqliteRelay } from "./sqlite-relay"; import debug from "debug"; -const log = debug('SqliteRelay:migrations'); +const log = debug("SqliteRelay:migrations"); /** * Do database migration @@ -98,4 +98,4 @@ async function migrate_v4(relay: SqliteRelay) { }); } -export default migrate; \ No newline at end of file +export default migrate; diff --git a/packages/worker-relay/src/sqlite-relay.ts b/packages/worker-relay/src/sqlite-relay.ts index f1dfc39d..78f46faa 100644 --- a/packages/worker-relay/src/sqlite-relay.ts +++ b/packages/worker-relay/src/sqlite-relay.ts @@ -1,6 +1,6 @@ import sqlite3InitModule, { Database, Sqlite3Static } from "@sqlite.org/sqlite-wasm"; import { EventEmitter } from "eventemitter3"; -import { NostrEvent, RelayHandler, RelayHandlerEvents, ReqFilter, unixNowMs } from "./types"; +import { EventMetadata, NostrEvent, RelayHandler, RelayHandlerEvents, ReqFilter, unixNowMs } from "./types"; import debug from "debug"; import migrate from "./migrations"; @@ -87,6 +87,15 @@ export class SqliteRelay extends EventEmitter implements Rel return eventsInserted.length > 0; } + setEventMetadata(id: string, meta: EventMetadata) { + if (meta.seen_at) { + console.log("update seen_at", id, meta.seen_at); + this.db?.exec("update events set seen_at = ? where id = ?", { + bind: [meta.seen_at, id], + }); + } + } + #deleteById(db: Database, ids: Array) { db.exec(`delete from events where id in (${this.#repeatParams(ids.length)})`, { bind: ids, diff --git a/packages/worker-relay/src/types.ts b/packages/worker-relay/src/types.ts index 6da36b31..f024a3ec 100644 --- a/packages/worker-relay/src/types.ts +++ b/packages/worker-relay/src/types.ts @@ -10,7 +10,8 @@ export type WorkerMessageCommand = | "close" | "dumpDb" | "emit-event" - | "forYouFeed"; + | "forYouFeed" + | "setEventMetadata"; export interface WorkerMessage { id: string; @@ -29,6 +30,10 @@ export interface NostrEvent { relays?: Array; } +export interface EventMetadata { + seen_at?: number; +} + export type ReqCommand = ["REQ", id: string, ...filters: Array]; export interface ReqFilter { @@ -65,6 +70,7 @@ export interface RelayHandler extends EventEmitter { count(req: ReqFilter): number; summary(): Record; dump(): Promise; + setEventMetadata(id: string, meta: EventMetadata): void; } export interface RelayHandlerEvents { diff --git a/packages/worker-relay/src/worker.ts b/packages/worker-relay/src/worker.ts index 3f86458c..0486e057 100644 --- a/packages/worker-relay/src/worker.ts +++ b/packages/worker-relay/src/worker.ts @@ -3,8 +3,8 @@ import { InMemoryRelay } from "./memory-relay"; import { WorkQueueItem, barrierQueue, processWorkQueue } from "./queue"; import { SqliteRelay } from "./sqlite-relay"; -import { NostrEvent, RelayHandler, ReqCommand, ReqFilter, WorkerMessage, unixNowMs } from "./types"; -import {getForYouFeed} from "./forYouFeed"; +import { NostrEvent, RelayHandler, ReqCommand, ReqFilter, WorkerMessage, unixNowMs, EventMetadata } from "./types"; +import { getForYouFeed } from "./forYouFeed"; let relay: RelayHandler | undefined; @@ -138,6 +138,13 @@ globalThis.onmessage = async ev => { }); break; } + case "setEventMetadata": { + await barrierQueue(cmdQueue, async () => { + const [id, metadata] = msg.args as [string, EventMetadata]; + relay!.setEventMetadata(id, metadata); + }); + break; + } default: { reply(msg.id, { error: "Unknown command" }); break;