From 69ec48141b53938796d78d1ccc9e3823b8a0009e Mon Sep 17 00:00:00 2001 From: Kieran Date: Tue, 25 Apr 2023 12:57:09 +0100 Subject: [PATCH] feat: interaction cache --- .../app/src/Cache/EventInteractionCache.ts | 44 ++++++++++++++++ packages/app/src/Cache/index.ts | 2 + packages/app/src/Db/index.ts | 13 ++++- packages/app/src/Element/NoteFooter.tsx | 51 +++++-------------- packages/app/src/Hooks/useClientWidth.tsx | 20 -------- .../app/src/Hooks/useInteractionCache.tsx | 44 ++++++++++++++++ 6 files changed, 115 insertions(+), 59 deletions(-) create mode 100644 packages/app/src/Cache/EventInteractionCache.ts delete mode 100644 packages/app/src/Hooks/useClientWidth.tsx create mode 100644 packages/app/src/Hooks/useInteractionCache.tsx diff --git a/packages/app/src/Cache/EventInteractionCache.ts b/packages/app/src/Cache/EventInteractionCache.ts new file mode 100644 index 00000000..2ea3ac80 --- /dev/null +++ b/packages/app/src/Cache/EventInteractionCache.ts @@ -0,0 +1,44 @@ +import { db, EventInteraction } from "Db"; +import { LoginStore } from "Login"; +import { sha256 } from "Util"; +import FeedCache from "./FeedCache"; + +class EventInteractionCache extends FeedCache { + constructor() { + super("EventInteraction", db.eventInteraction); + } + + key(of: EventInteraction): string { + return sha256(of.event + of.by); + } + + override async preload(): Promise { + await super.preload(); + + const data = window.localStorage.getItem("zap-cache"); + if (data) { + const toImport = [...new Set(JSON.parse(data) as Array)].map(a => { + const ret = { + event: a, + by: LoginStore.takeSnapshot().publicKey, + zapped: true, + reacted: false, + reposted: false, + } as EventInteraction; + ret.id = this.key(ret); + return ret; + }); + await this.bulkSet(toImport); + + console.debug(`Imported dumb-zap-cache events: `, toImport.length); + window.localStorage.removeItem("zap-cache"); + } + await this.buffer([...this.onTable]); + } + + takeSnapshot(): EventInteraction[] { + return [...this.cache.values()]; + } +} + +export const InteractionCache = new EventInteractionCache(); diff --git a/packages/app/src/Cache/index.ts b/packages/app/src/Cache/index.ts index 9ecba32d..e194af9b 100644 --- a/packages/app/src/Cache/index.ts +++ b/packages/app/src/Cache/index.ts @@ -1,6 +1,7 @@ import { HexKey, RawEvent, UserMetadata } from "@snort/nostr"; import { hexToBech32, unixNowMs } from "Util"; import { DmCache } from "./DMCache"; +import { InteractionCache } from "./EventInteractionCache"; import { UserCache } from "./UserCache"; export interface MetadataCache extends UserMetadata { @@ -48,6 +49,7 @@ export function mapEventToProfile(ev: RawEvent) { export async function preload() { await UserCache.preload(); await DmCache.preload(); + await InteractionCache.preload(); } export { UserCache, DmCache }; diff --git a/packages/app/src/Db/index.ts b/packages/app/src/Db/index.ts index 7b30d582..4ed7ddaf 100644 --- a/packages/app/src/Db/index.ts +++ b/packages/app/src/Db/index.ts @@ -3,7 +3,7 @@ import { FullRelaySettings, HexKey, RawEvent, u256 } from "@snort/nostr"; import { MetadataCache } from "Cache"; export const NAME = "snortDB"; -export const VERSION = 7; +export const VERSION = 8; export interface SubCache { id: string; @@ -24,12 +24,22 @@ export interface UsersRelays { relays: FullRelaySettings[]; } +export interface EventInteraction { + id: u256; + event: u256; + by: HexKey; + reacted: boolean; + zapped: boolean; + reposted: boolean; +} + const STORES = { users: "++pubkey, name, display_name, picture, nip05, npub", relays: "++addr", userRelays: "++pubkey", events: "++id, pubkey, created_at", dms: "++id, pubkey", + eventInteraction: "++id", }; export class SnortDB extends Dexie { @@ -39,6 +49,7 @@ export class SnortDB extends Dexie { userRelays!: Table; events!: Table; dms!: Table; + eventInteraction!: Table; constructor() { super(NAME); diff --git a/packages/app/src/Element/NoteFooter.tsx b/packages/app/src/Element/NoteFooter.tsx index 2739ca49..22096dee 100644 --- a/packages/app/src/Element/NoteFooter.tsx +++ b/packages/app/src/Element/NoteFooter.tsx @@ -25,42 +25,10 @@ import { DonateLNURL } from "Pages/DonatePage"; import { useWallet } from "Wallet"; import useLogin from "Hooks/useLogin"; import { setBookmarked, setPinned } from "Login"; +import { useInteractionCache } from "Hooks/useInteractionCache"; import messages from "./messages"; -// a dumb cache to remember which notes we zapped -class DumbZapCache { - #set: Set = new Set(); - constructor() { - this.#load(); - } - - add(id: u256) { - this.#set.add(this.#truncId(id)); - this.#save(); - } - - has(id: u256) { - return this.#set.has(this.#truncId(id)); - } - - #truncId(id: u256) { - return id.slice(0, 12); - } - - #save() { - window.localStorage.setItem("zap-cache", JSON.stringify([...this.#set])); - } - - #load() { - const data = window.localStorage.getItem("zap-cache"); - if (data) { - this.#set = new Set(JSON.parse(data) as Array); - } - } -} -const ZapCache = new DumbZapCache(); - let isZapperBusy = false; const barrierZapper = async (then: () => Promise): Promise => { while (isZapperBusy) { @@ -99,6 +67,7 @@ export default function NoteFooter(props: NoteFooterProps) { const { pinned, bookmarked, publicKey, preferences: prefs, relays } = login; const { mute, block } = useModeration(); const author = useUserProfile(ev.pubkey); + const interactionCache = useInteractionCache(publicKey, ev.id); const publisher = useEventPublisher(); const showNoteCreatorModal = useSelector((s: RootState) => s.noteCreator.show); const replyTo = useSelector((s: RootState) => s.noteCreator.replyTo); @@ -114,7 +83,7 @@ export default function NoteFooter(props: NoteFooterProps) { type: "language", }); const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0); - const didZap = ZapCache.has(ev.id) || zaps.some(a => a.sender === publicKey); + const didZap = interactionCache.data.zapped || zaps.some(a => a.sender === publicKey); const longPress = useLongPress( e => { e.stopPropagation(); @@ -126,17 +95,21 @@ export default function NoteFooter(props: NoteFooterProps) { ); function hasReacted(emoji: string) { - return positive?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === publicKey); + return ( + interactionCache.data.reacted || + positive?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === publicKey) + ); } function hasReposted() { - return reposts.some(a => a.pubkey === publicKey); + return interactionCache.data.reposted || reposts.some(a => a.pubkey === publicKey); } async function react(content: string) { if (!hasReacted(content) && publisher) { const evLike = await publisher.react(ev, content); publisher.broadcast(evLike); + await interactionCache.react(); } } @@ -152,6 +125,7 @@ export default function NoteFooter(props: NoteFooterProps) { if (!prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.id }))) { const evRepost = await publisher.repost(ev); publisher.broadcast(evRepost); + await interactionCache.repost(); } } } @@ -201,6 +175,8 @@ export default function NoteFooter(props: NoteFooterProps) { const zap = handler.canZap && publisher ? await publisher.zap(amount * 1000, key, zr, id) : undefined; const invoice = await handler.getInvoice(amount, undefined, zap); await wallet?.payInvoice(unwrap(invoice.pr)); + + await interactionCache.zap(); }); } @@ -220,14 +196,13 @@ export default function NoteFooter(props: NoteFooterProps) { } useEffect(() => { - if (prefs.autoZap && !ZapCache.has(ev.id) && !isMine && !zapping) { + if (prefs.autoZap && !didZap && !isMine && !zapping) { const lnurl = getLNURL(); if (wallet?.isReady() && lnurl) { setZapping(true); queueMicrotask(async () => { try { await fastZapInner(lnurl, prefs.defaultZapAmount, ev.pubkey, ev.id); - ZapCache.add(ev.id); fastZapDonate(); } catch { // ignored diff --git a/packages/app/src/Hooks/useClientWidth.tsx b/packages/app/src/Hooks/useClientWidth.tsx deleted file mode 100644 index 031079bd..00000000 --- a/packages/app/src/Hooks/useClientWidth.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useRef, useState, useEffect } from "react"; - -export default function useClientWidth() { - const ref = useRef(document.querySelector(".page")); - const [width, setWidth] = useState(0); - - useEffect(() => { - const updateSize = () => { - if (ref.current) { - setWidth(ref.current.offsetWidth); - } - }; - - window.addEventListener("resize", updateSize); - updateSize(); - return () => window.removeEventListener("resize", updateSize); - }, [ref]); - - return width; -} diff --git a/packages/app/src/Hooks/useInteractionCache.tsx b/packages/app/src/Hooks/useInteractionCache.tsx new file mode 100644 index 00000000..3a4127f1 --- /dev/null +++ b/packages/app/src/Hooks/useInteractionCache.tsx @@ -0,0 +1,44 @@ +import { useSyncExternalStore } from "react"; +import { HexKey, u256 } from "@snort/nostr"; + +import { InteractionCache } from "Cache/EventInteractionCache"; +import { EventInteraction } from "Db"; +import { sha256, unwrap } from "Util"; + +export function useInteractionCache(pubkey?: HexKey, event?: u256) { + const id = event && pubkey ? sha256(event + pubkey) : undefined; + const EmptyInteraction = { + id, + event, + by: pubkey, + } as EventInteraction; + const data = + useSyncExternalStore( + c => InteractionCache.hook(c, id), + () => InteractionCache.snapshot()[0] + ) || EmptyInteraction; + return { + data: data, + react: () => + InteractionCache.set({ + ...data, + event: unwrap(event), + by: unwrap(pubkey), + reacted: true, + }), + zap: () => + InteractionCache.set({ + ...data, + event: unwrap(event), + by: unwrap(pubkey), + zapped: true, + }), + repost: () => + InteractionCache.set({ + ...data, + event: unwrap(event), + by: unwrap(pubkey), + reposted: true, + }), + }; +}