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 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<NoteTranslation>(translationCache.get(ev.id));
const cachedSetTranslated = useCallback(
@ -48,6 +50,16 @@ export function Note(props: NoteProps) {
[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 goToEvent = useGoToEvent(props, optionsMerged);
@ -71,7 +83,7 @@ export function Note(props: NoteProps) {
{translated && <TranslationInfo translated={translated} setShowTranslation={setShowTranslation} />}
{ev.kind === EventKind.Polls && <Poll ev={ev} />}
{optionsMerged.showFooter && (
<div className="mt-4">
<div className="mt-4" ref={setSeenAtRef}>
<NoteFooter ev={ev} replyCount={props.threadChains?.get(chainKey(ev))?.length} />
</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 { 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";

View File

@ -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<string>, myPubkey: string) {
const myReactedAuthors = new Map<string, number>();
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<s
async function getMyReactedEvents(relay: RelayHandler, pubkey: string) {
const myReactedEventIds = new Set<string>();
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<string>, myPubkey: string) {
const othersWhoReacted = new Map<string, number>();
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<string, number>();
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<string, number>, reactedToAuthors: Map<string, number>) {
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<string, number>,
reactedToAuthors: Map<string, number>,
) {
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));
@ -138,11 +134,14 @@ async function getFeedEvents(relay: RelayHandler, reactedToIds: Map<string, numb
.sort((a, b) => 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)) {

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";
export class WorkerRelayInterface {
@ -49,6 +57,10 @@ export class WorkerRelayInterface {
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) {
const id = uuid();
const msg = {

View File

@ -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<RelayHandlerEvents> implements R
}
return ret;
}
setEventMetadata(_id: string, _meta: EventMetadata) {
return;
}
}

View File

@ -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;
export default migrate;

View File

@ -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<RelayHandlerEvents> 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<string>) {
db.exec(`delete from events where id in (${this.#repeatParams(ids.length)})`, {
bind: ids,

View File

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

View File

@ -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;