set event seen_at times, sort by seen_at in ForYouFeed

This commit is contained in:
Martti Malmi 2024-02-05 11:06:39 +02:00
parent 5bc3c10d36
commit 7ceab04cbc
9 changed files with 100 additions and 51 deletions

View File

@ -1,11 +1,12 @@
import { EventKind, NostrLink } from "@snort/system"; import { EventKind, NostrLink } from "@snort/system";
import classNames from "classnames"; import classNames from "classnames";
import React, { useCallback, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { useInView } from "react-intersection-observer"; import { useInView } from "react-intersection-observer";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { LRUCache } from "typescript-lru-cache"; import { LRUCache } from "typescript-lru-cache";
import { Relay } from "@/Cache";
import NoteHeader from "@/Components/Event/Note/NoteHeader"; import NoteHeader from "@/Components/Event/Note/NoteHeader";
import { NoteText } from "@/Components/Event/Note/NoteText"; import { NoteText } from "@/Components/Event/Note/NoteText";
import { TranslationInfo } from "@/Components/Event/Note/TranslationInfo"; 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 baseClassName = classNames("note min-h-[110px] flex flex-col gap-4 card", className ?? "");
const { isEventMuted } = useModeration(); const { isEventMuted } = useModeration();
const { ref, inView } = useInView({ triggerOnce: true, rootMargin: "2000px" }); const { ref, inView } = useInView({ triggerOnce: true, rootMargin: "2000px" });
const { ref: setSeenAtRef, inView: setSeenAtInView } = useInView({ rootMargin: "0px", threshold: 1 });
const [showTranslation, setShowTranslation] = useState(true); const [showTranslation, setShowTranslation] = useState(true);
const [translated, setTranslated] = useState<NoteTranslation>(translationCache.get(ev.id)); const [translated, setTranslated] = useState<NoteTranslation>(translationCache.get(ev.id));
const cachedSetTranslated = useCallback( const cachedSetTranslated = useCallback(
@ -48,6 +50,16 @@ export function Note(props: NoteProps) {
[ev.id], [ev.id],
); );
useEffect(() => {
let timeout: ReturnType<typeof setTimeout>;
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 optionsMerged = { ...defaultOptions, ...opt };
const goToEvent = useGoToEvent(props, optionsMerged); const goToEvent = useGoToEvent(props, optionsMerged);
@ -71,7 +83,7 @@ export function Note(props: NoteProps) {
{translated && <TranslationInfo translated={translated} setShowTranslation={setShowTranslation} />} {translated && <TranslationInfo translated={translated} setShowTranslation={setShowTranslation} />}
{ev.kind === EventKind.Polls && <Poll ev={ev} />} {ev.kind === EventKind.Polls && <Poll ev={ev} />}
{optionsMerged.showFooter && ( {optionsMerged.showFooter && (
<div className="mt-4"> <div className="mt-4" ref={setSeenAtRef}>
<NoteFooter ev={ev} replyCount={props.threadChains?.get(chainKey(ev))?.length} /> <NoteFooter ev={ev} replyCount={props.threadChains?.get(chainKey(ev))?.length} />
</div> </div>
)} )}

View File

@ -1,9 +1,9 @@
import {EventKind, NostrEvent} from "@snort/system"; import { EventKind, NostrEvent } from "@snort/system";
import { memo, useEffect, useMemo, useState } from "react"; import { memo, useEffect, useMemo, useState } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import {Relay} from "@/Cache"; import { Relay } from "@/Cache";
import { DisplayAs, DisplayAsSelector } from "@/Components/Feed/DisplayAsSelector"; import { DisplayAs, DisplayAsSelector } from "@/Components/Feed/DisplayAsSelector";
import { TimelineRenderer } from "@/Components/Feed/TimelineRenderer"; import { TimelineRenderer } from "@/Components/Feed/TimelineRenderer";
import { TaskList } from "@/Components/Tasks/TaskList"; import { TaskList } from "@/Components/Tasks/TaskList";

View File

@ -1,7 +1,7 @@
import { NostrEvent, parseZap } from "@snort/system"; import { NostrEvent, parseZap } from "@snort/system";
import debug from "debug"; import debug from "debug";
import {RelayHandler} from "./types"; import { RelayHandler } from "./types";
const log = debug("getForYouFeed"); const log = debug("getForYouFeed");
@ -37,12 +37,9 @@ export async function getForYouFeed(relay: RelayHandler, pubkey: string): Promis
async function getMyReactedAuthors(relay: RelayHandler, myReactedEventIds: Set<string>, myPubkey: string) { async function getMyReactedAuthors(relay: RelayHandler, myReactedEventIds: Set<string>, myPubkey: string) {
const myReactedAuthors = new Map<string, number>(); const myReactedAuthors = new Map<string, number>();
const myReactions = relay.req( const myReactions = relay.req("getMyReactedAuthors", {
"getMyReactedAuthors", "#e": Array.from(myReactedEventIds),
{ }) as NostrEvent[];
"#e": Array.from(myReactedEventIds),
},
) as NostrEvent[];
myReactions.forEach(reaction => { myReactions.forEach(reaction => {
if (reaction.pubkey !== myPubkey) { if (reaction.pubkey !== myPubkey) {
@ -56,13 +53,10 @@ async function getMyReactedAuthors(relay: RelayHandler, myReactedEventIds: Set<s
async function getMyReactedEvents(relay: RelayHandler, pubkey: string) { async function getMyReactedEvents(relay: RelayHandler, pubkey: string) {
const myReactedEventIds = new Set<string>(); const myReactedEventIds = new Set<string>();
const myEvents = relay.req( const myEvents = relay.req("getMyReactedEventIds", {
"getMyReactedEventIds", authors: [pubkey],
{ kinds: [1, 6, 7, 9735],
authors: [pubkey], }) as NostrEvent[];
kinds: [1, 6, 7, 9735],
},
) as NostrEvent[];
myEvents.forEach(ev => { myEvents.forEach(ev => {
const targetEventId = ev.kind === 9735 ? parseZap(ev).event?.id : ev.tags.find(tag => tag[0] === "e")?.[1]; const targetEventId = ev.kind === 9735 ? parseZap(ev).event?.id : ev.tags.find(tag => tag[0] === "e")?.[1];
if (targetEventId) { if (targetEventId) {
@ -76,12 +70,9 @@ async function getMyReactedEvents(relay: RelayHandler, pubkey: string) {
async function getOthersWhoReacted(relay: RelayHandler, myReactedEventIds: Set<string>, myPubkey: string) { async function getOthersWhoReacted(relay: RelayHandler, myReactedEventIds: Set<string>, myPubkey: string) {
const othersWhoReacted = new Map<string, number>(); const othersWhoReacted = new Map<string, number>();
const otherReactions = relay.req( const otherReactions = relay.req("getOthersWhoReacted", {
"getOthersWhoReacted", "#e": Array.from(myReactedEventIds),
{ }) as NostrEvent[];
"#e": Array.from(myReactedEventIds),
},
) as NostrEvent[];
otherReactions.forEach(reaction => { otherReactions.forEach(reaction => {
if (reaction.pubkey !== myPubkey) { if (reaction.pubkey !== myPubkey) {
@ -100,13 +91,10 @@ async function getEventIdsReactedByOthers(
) { ) {
const eventIdsReactedByOthers = new Map<string, number>(); const eventIdsReactedByOthers = new Map<string, number>();
const events = relay.req( const events = relay.req("getEventIdsReactedByOthers", {
"getEventIdsReactedByOthers", authors: [...othersWhoReacted.keys()],
{ kinds: [1, 6, 7, 9735],
authors: [...othersWhoReacted.keys()], }) as NostrEvent[];
kinds: [1, 6, 7, 9735],
},
) as NostrEvent[];
events.forEach(event => { events.forEach(event => {
if (event.pubkey === myPub || myReactedEvents.has(event.id)) { if (event.pubkey === myPub || myReactedEvents.has(event.id)) {
@ -124,11 +112,19 @@ async function getEventIdsReactedByOthers(
return eventIdsReactedByOthers; return eventIdsReactedByOthers;
} }
async function getFeedEvents(relay: RelayHandler, reactedToIds: Map<string, number>, reactedToAuthors: Map<string, number>) { async function getFeedEvents(
const events = relay.sql( relay: RelayHandler,
`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`, reactedToIds: Map<string, number>,
Array.from(reactedToIds.keys()), reactedToAuthors: Map<string, number>,
).map(row => JSON.parse(row[0] as string) as NostrEvent); ) {
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<string>(events.map(ev => ev.id)); const seen = new Set<string>(events.map(ev => ev.id));
@ -138,11 +134,14 @@ async function getFeedEvents(relay: RelayHandler, reactedToIds: Map<string, numb
.sort((a, b) => reactedToAuthors.get(b)! - reactedToAuthors.get(a)!) .sort((a, b) => reactedToAuthors.get(b)! - reactedToAuthors.get(a)!)
.slice(20); .slice(20);
const eventsByFavoriteAuthors = relay.sql( const eventsByFavoriteAuthors = relay
`select json from events where pubkey in (${favoriteAuthors.map(() => "?").join(", ")}) and kind = 1 order by seen_at ASC, created DESC limit 100`, .sql(
favoriteAuthors, `select json from events where pubkey in (${favoriteAuthors
).map(row => JSON.parse(row[0] as string) as NostrEvent); .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 => { eventsByFavoriteAuthors.forEach(ev => {
if (!seen.has(ev.id)) { if (!seen.has(ev.id)) {

View File

@ -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"; import { v4 as uuid } from "uuid";
export class WorkerRelayInterface { export class WorkerRelayInterface {
@ -49,6 +57,10 @@ export class WorkerRelayInterface {
return await this.#workerRpc<string, Array<NostrEvent>>("forYouFeed", pubkey); return await this.#workerRpc<string, Array<NostrEvent>>("forYouFeed", pubkey);
} }
setEventMetadata(id: string, meta: EventMetadata) {
return this.#workerRpc<[string, EventMetadata], void>("setEventMetadata", [id, meta]);
}
#workerRpc<T, R>(cmd: WorkerMessageCommand, args?: T) { #workerRpc<T, R>(cmd: WorkerMessageCommand, args?: T) {
const id = uuid(); const id = uuid();
const msg = { const msg = {

View File

@ -1,5 +1,5 @@
import EventEmitter from "eventemitter3"; 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"; import debug from "debug";
/** /**
@ -79,4 +79,8 @@ export class InMemoryRelay extends EventEmitter<RelayHandlerEvents> implements R
} }
return ret; return ret;
} }
setEventMetadata(_id: string, _meta: EventMetadata) {
return;
}
} }

View File

@ -1,8 +1,8 @@
import {NostrEvent} from "./types"; import { NostrEvent } from "./types";
import {SqliteRelay} from "./sqlite-relay"; import { SqliteRelay } from "./sqlite-relay";
import debug from "debug"; import debug from "debug";
const log = debug('SqliteRelay:migrations'); const log = debug("SqliteRelay:migrations");
/** /**
* Do database migration * Do database migration
@ -98,4 +98,4 @@ async function migrate_v4(relay: SqliteRelay) {
}); });
} }
export default migrate; export default migrate;

View File

@ -1,6 +1,6 @@
import sqlite3InitModule, { Database, Sqlite3Static } from "@sqlite.org/sqlite-wasm"; import sqlite3InitModule, { Database, Sqlite3Static } from "@sqlite.org/sqlite-wasm";
import { EventEmitter } from "eventemitter3"; 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 debug from "debug";
import migrate from "./migrations"; import migrate from "./migrations";
@ -87,6 +87,15 @@ export class SqliteRelay extends EventEmitter<RelayHandlerEvents> implements Rel
return eventsInserted.length > 0; 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<string>) { #deleteById(db: Database, ids: Array<string>) {
db.exec(`delete from events where id in (${this.#repeatParams(ids.length)})`, { db.exec(`delete from events where id in (${this.#repeatParams(ids.length)})`, {
bind: ids, bind: ids,

View File

@ -10,7 +10,8 @@ export type WorkerMessageCommand =
| "close" | "close"
| "dumpDb" | "dumpDb"
| "emit-event" | "emit-event"
| "forYouFeed"; | "forYouFeed"
| "setEventMetadata";
export interface WorkerMessage<T> { export interface WorkerMessage<T> {
id: string; id: string;
@ -29,6 +30,10 @@ export interface NostrEvent {
relays?: Array<string>; relays?: Array<string>;
} }
export interface EventMetadata {
seen_at?: number;
}
export type ReqCommand = ["REQ", id: string, ...filters: Array<ReqFilter>]; export type ReqCommand = ["REQ", id: string, ...filters: Array<ReqFilter>];
export interface ReqFilter { export interface ReqFilter {
@ -65,6 +70,7 @@ export interface RelayHandler extends EventEmitter<RelayHandlerEvents> {
count(req: ReqFilter): number; count(req: ReqFilter): number;
summary(): Record<string, number>; summary(): Record<string, number>;
dump(): Promise<Uint8Array>; dump(): Promise<Uint8Array>;
setEventMetadata(id: string, meta: EventMetadata): void;
} }
export interface RelayHandlerEvents { export interface RelayHandlerEvents {

View File

@ -3,8 +3,8 @@
import { InMemoryRelay } from "./memory-relay"; import { InMemoryRelay } from "./memory-relay";
import { WorkQueueItem, barrierQueue, processWorkQueue } from "./queue"; import { WorkQueueItem, barrierQueue, processWorkQueue } from "./queue";
import { SqliteRelay } from "./sqlite-relay"; import { SqliteRelay } from "./sqlite-relay";
import { NostrEvent, RelayHandler, ReqCommand, ReqFilter, WorkerMessage, unixNowMs } from "./types"; import { NostrEvent, RelayHandler, ReqCommand, ReqFilter, WorkerMessage, unixNowMs, EventMetadata } from "./types";
import {getForYouFeed} from "./forYouFeed"; import { getForYouFeed } from "./forYouFeed";
let relay: RelayHandler | undefined; let relay: RelayHandler | undefined;
@ -138,6 +138,13 @@ globalThis.onmessage = async ev => {
}); });
break; break;
} }
case "setEventMetadata": {
await barrierQueue(cmdQueue, async () => {
const [id, metadata] = msg.args as [string, EventMetadata];
relay!.setEventMetadata(id, metadata);
});
break;
}
default: { default: {
reply(msg.id, { error: "Unknown command" }); reply(msg.id, { error: "Unknown command" });
break; break;