From a737f964c871191e6c3df332c04b14c4a5c79a65 Mon Sep 17 00:00:00 2001 From: Simon Date: Sun, 11 Aug 2024 11:35:15 +0200 Subject: [PATCH 1/3] use local cache for profiles --- src/map.ts | 197 ++++++++++++++++++++++------------------- src/nostr/subscribe.ts | 50 ++++++----- src/nostr/utils.ts | 2 +- src/types.ts | 8 ++ 4 files changed, 143 insertions(+), 114 deletions(-) diff --git a/src/map.ts b/src/map.ts index b783095..97129e3 100644 --- a/src/map.ts +++ b/src/map.ts @@ -8,13 +8,15 @@ import { CONTENT_MAXIMUM_LENGTH, CONTENT_MINIMUM_LENGTH, PANEL_CONTAINER_ID, + PLUS_CODE_TAG_KEY, } from "./constants"; import { hasPrivateKey } from "./nostr/keys"; import { createNote } from "./nostr/notes"; import { _initRelays } from "./nostr/relays"; -import { subscribe } from "./nostr/subscribe"; +import { getMetadataEvent, subscribe } from "./nostr/subscribe"; import { startUserOnboarding } from "./onboarding"; -import { Note } from "./types"; +import { Note, NostrEvent, Kind30398Event, MetadataEvent } from "./types"; +import { getProfileFromEvent, getTagFirstValueFromEvent } from "./nostr/utils"; const map = L.map("map", { zoomControl: false, @@ -116,28 +118,35 @@ globalThis.addEventListener("popstate", (event) => { globalThis.document.location.reload(); }); -function generateDatetimeFromNote(note: Note): string { - const { createdAt } = note; +function generateDatetimeFromEvent(event: Kind30398Event): string { + const createdAt = + parseInt( + getTagFirstValueFromEvent({ + event, + tag: "original_created_at", + }) ?? "0" + ) || 0; const date = new Date(createdAt * 1000); return date.toLocaleString(); } -function generateLinkFromNote(note: Note): string { - const { authorName, authorTrustrootsUsername, authorTripHoppingUserId } = - note; - if (authorTrustrootsUsername.length > 3) { - if (authorName.length > 1) { - return ` ${authorName}`; +function generateLinkFromMetadataEvent(event: MetadataEvent): string { + const profile = getProfileFromEvent({ event }); + + const { name, trustrootsUsername, tripHoppingUserId } = profile; + if (trustrootsUsername.length > 3) { + if (name.length > 1) { + return ` ${name}`; } - return ` ${authorTrustrootsUsername}`; + return ` ${trustrootsUsername}`; } - if (authorTripHoppingUserId.length > 3) { - if (authorName.length > 1) { - return ` ${authorName}`; + if (tripHoppingUserId.length > 3) { + if (name.length > 1) { + return ` ${name}`; } - return ` ${authorTripHoppingUserId.slice( + return ` ${tripHoppingUserId.slice( 0, 5 )}`; @@ -145,92 +154,94 @@ function generateLinkFromNote(note: Note): string { return ""; } -function generateMapContentFromNotes(notes: Note[]) { - const lines = notes.reduce((existingLines, note) => { - const link = generateLinkFromNote(note); - const datetime = generateDatetimeFromNote(note); - const noteContent = `${note.content}${link} ${datetime}`; - return existingLines.concat(noteContent); - }, [] as string[]); - const content = lines.join("
"); - return content; +function generateMapContentFromEvent( + event: Kind30398Event, + metadataEvent?: MetadataEvent +) { + const link = metadataEvent + ? generateLinkFromMetadataEvent(metadataEvent) + : ""; + const datetime = generateDatetimeFromEvent(event); + const noteContent = `${event.content}${link} ${datetime}`; + return noteContent; } // todo: needs to be DRYed up -function generateChatContentFromNotes(notes: Note[]) { - const lines = notes.reduce((existingLines, note) => { - const link = generateLinkFromNote(note); - const datetime = generateDatetimeFromNote(note); - const noteContent = `${datetime}, ${link}: ${note.content}`; - return existingLines.concat(noteContent); - }, [] as string[]); - const content = lines.join("
"); - return content; +function generateChatContentFromNotes( + event: Kind30398Event, + metadataEvent: MetadataEvent +) { + const link = generateLinkFromMetadataEvent(metadataEvent); + const datetime = generateDatetimeFromEvent(event); + const noteContent = `${datetime}, ${link}: ${event.content}`; + return noteContent; } -function addNoteToMap(note: Note) { - let existing = plusCodesWithPopupsAndNotes[note.plusCode]; +function addNoteToMap(event: Kind30398Event) { + const plusCode = + getTagFirstValueFromEvent({ + event, + tag: PLUS_CODE_TAG_KEY, + }) ?? ""; + + const decodedCoords = decode(plusCode); + const { resolution: res, longitude: cLong, latitude: cLat } = decodedCoords!; + + let color; + let fillColor; + const hitchWikiYellow = "#F3DA71"; + const hitchWikiYellowLight = "#FFFBEE"; + const trGreen = "#12B591"; + + if (event.pubkey === HITCHMAPS_AUTHOR_PUBLIC_KEY) { + color = hitchWikiYellow; + fillColor = hitchWikiYellowLight; + } else { + color = trGreen; + } - if (existing) { - const popup = existing.popup; + const marker = L.circleMarker([cLat, cLong], { + ...circleMarker, + color: color, + fillColor: fillColor, + }); // Create marker with decoded coordinates + marker.addTo(map); - // When using multiple NOSTR relays, deduplicate the notes by ID to ensure - // that we don't show the same note multiple times. - const noteAlreadyOnTheMap = existing.notes.find((n) => n.id === note.id); - if (typeof noteAlreadyOnTheMap !== "undefined") { - return; - } + // const contentChat = generateChatContentFromNotes([event]); - const notes = [...existing.notes, note]; - popup.setContent(generateMapContentFromNotes(notes)); - } else { - const decodedCoords = decode(note.plusCode); - const { - resolution: res, - longitude: cLong, - latitude: cLat, - } = decodedCoords!; - - let color; - let fillColor; - const hitchWikiYellow = "#F3DA71"; - const hitchWikiYellowLight = "#FFFBEE"; - const trGreen = "#12B591"; - - if (note.authorPublicKey === HITCHMAPS_AUTHOR_PUBLIC_KEY) { - color = hitchWikiYellow; - fillColor = hitchWikiYellowLight; - } else { - color = trGreen; - } + //todo: rename addNoteToMap and other map + console.log(event); + const geochatNotes = document.getElementById("geochat-notes") as HTMLElement; + const li = document.createElement("li"); + // li.innerHTML = contentChat; + geochatNotes.appendChild(li); - const marker = L.circleMarker([cLat, cLong], { - ...circleMarker, - color: color, - fillColor: fillColor, - }); // Create marker with decoded coordinates - marker.addTo(map); - - const contentMap = generateMapContentFromNotes([note]); - const contentChat = generateChatContentFromNotes([note]); - - //todo: rename addNoteToMap and other map - console.log(note); - const geochatNotes = document.getElementById( - "geochat-notes" - ) as HTMLElement; - const li = document.createElement("li"); - li.innerHTML = contentChat; - geochatNotes.appendChild(li); - - const popup = L.popup().setContent(contentMap); - marker.bindPopup(popup); - marker.on("click", () => marker.openPopup()); - plusCodesWithPopupsAndNotes[note.plusCode] = { - popup, - notes: [note], - }; - } + marker.on( + "click", + async (markerClickEvent) => + await populateAndOpenPopup(markerClickEvent, event) + ); +} + +async function populateAndOpenPopup( + markerClickEvent: L.LeafletEvent, + kind30398Event: Kind30398Event +) { + const marker = markerClickEvent.target as L.Marker; + + const authorPubkey = getTagFirstValueFromEvent({ + event: kind30398Event, + tag: "p", + }); + const metadataEvent = await getMetadataEvent(authorPubkey); + if (!metadataEvent) + console.warn( + `Could not get metadata event for "${kind30398Event.content}"` + ); + const contentMap = generateMapContentFromEvent(kind30398Event, metadataEvent); + const popup = L.popup().setContent(contentMap); + marker.bindPopup(popup); + marker.openPopup(); } function createPopupHtml(createNoteCallback) { @@ -285,6 +296,6 @@ const mapStartup = async () => { L.DomUtil.addClass(badge, "hide"); L.DomUtil.removeClass(badge, "show"); await _initRelays(); - subscribe({ onNoteReceived: addNoteToMap }); + subscribe({ onEventReceived: addNoteToMap }); }; mapStartup(); diff --git a/src/nostr/subscribe.ts b/src/nostr/subscribe.ts index d692f02..3f34d57 100644 --- a/src/nostr/subscribe.ts +++ b/src/nostr/subscribe.ts @@ -6,8 +6,14 @@ import { PLUS_CODE_TAG_KEY, TRUSTED_VALIDATION_PUBKEYS, } from "../constants"; -import { NostrEvent, Note, Profile } from "../types"; -import { _query } from "./relays"; +import { + Kind30398Event, + MetadataEvent, + NostrEvent, + Note, + Profile, +} from "../types"; +import { _initRelays, _query } from "./relays"; import { doesStringPassSanitisation, getProfileFromEvent, @@ -85,6 +91,14 @@ const eventToNote = ({ }; }; +const metadataEvents = new nostrify.NCache({ max: 1000 }); + +export async function getMetadataEvent(pubkey) { + const events = await metadataEvents.query([{ authors: [pubkey] }]); + if (events.length === 0) return; + else return events[0] as MetadataEvent; +} + type SubscribeParams = { /** The public key of the user to fetch events for, or undefined to fetch events from all users */ publicKey?: string; @@ -92,11 +106,11 @@ type SubscribeParams = { * @default 200 */ limit?: number; - onNoteReceived: (note: Note) => void; + onEventReceived: (event: Kind30398Event) => void; }; export const subscribe = async ({ publicKey, - onNoteReceived, + onEventReceived: onEventReceived, limit = 200, }: SubscribeParams) => { console.log("#qnvvsm nostr/subscribe", publicKey); @@ -119,9 +133,9 @@ export const subscribe = async ({ : eventsBaseFilter; const eventsFilterWithLimit = { ...eventsFilter, limit }; - const noteEventsQueue: NostrEvent[] = []; + const noteEventsQueue: Kind30398Event[] = []; - const onNoteEvent = (event: NostrEvent) => { + const onNoteEvent = (event: Kind30398Event) => { // if (isDev()) console.log("#gITVd2 gotNoteEvent", event); if ( @@ -160,7 +174,9 @@ export const subscribe = async ({ authors, }; - const onProfileEvent = (event: NostrEvent) => { + noteEventsQueue.forEach((event) => onEventReceived(event)); + + const onProfileEvent = (event: MetadataEvent) => { // if (isDev()) console.log("#zD1Iau got profile event", event); const profile = getProfileFromEvent({ event }); @@ -177,19 +193,13 @@ export const subscribe = async ({ return; } - profiles[publicKey] = profile; - }; - - await _query({ - filters: [profileFilter], - onEvent: onProfileEvent, - }); + metadataEvents.add(event); - // NOTE: At this point we should have fetched all the stored events, and all - // the profiles of the authors of all of those events - const notes = noteEventsQueue.map((event) => - eventToNote({ event, profiles }) - ); + // profiles[publicKey] = profile; + }; - notes.forEach((note) => onNoteReceived(note)); + const relayPool = await _initRelays(); + for await (const msg of relayPool.req([profileFilter])) { + if (msg[0] === "EVENT") onProfileEvent(msg[2] as MetadataEvent); + } }; diff --git a/src/nostr/utils.ts b/src/nostr/utils.ts index a357afd..65b7a49 100644 --- a/src/nostr/utils.ts +++ b/src/nostr/utils.ts @@ -29,7 +29,7 @@ export const getProfileFromEvent = ({ }: { event: NostrEvent; }): Profile => { - if (event.kind !== Kind.Metadata) { + if (event?.kind !== Kind.Metadata) { throw new Error("#pC5T6P Trying to get profile from non metadata event"); } diff --git a/src/types.ts b/src/types.ts index b3cd008..5d9a379 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,6 +9,14 @@ export type UnsignedEvent = Omit< "created_at" | "pubkey" | "id" | "sig" >; +export type Kind30398Event = NostrEvent & { + kind: 30398; +}; + +export type MetadataEvent = NostrEvent & { + kind: 0; +}; + export type Note = { id: string; plusCode: string; From be7535a05db5d2908489fb66424d88c07dc19e53 Mon Sep 17 00:00:00 2001 From: Simon Date: Sun, 11 Aug 2024 11:41:30 +0200 Subject: [PATCH 2/3] run profile fetching in background --- src/nostr/subscribe.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/nostr/subscribe.ts b/src/nostr/subscribe.ts index 3f34d57..cec919c 100644 --- a/src/nostr/subscribe.ts +++ b/src/nostr/subscribe.ts @@ -175,7 +175,10 @@ export const subscribe = async ({ }; noteEventsQueue.forEach((event) => onEventReceived(event)); + backgroundProfileFetching(profileFilter); +}; +async function backgroundProfileFetching(profileFilter) { const onProfileEvent = (event: MetadataEvent) => { // if (isDev()) console.log("#zD1Iau got profile event", event); @@ -197,9 +200,8 @@ export const subscribe = async ({ // profiles[publicKey] = profile; }; - const relayPool = await _initRelays(); for await (const msg of relayPool.req([profileFilter])) { if (msg[0] === "EVENT") onProfileEvent(msg[2] as MetadataEvent); } -}; +} From e1504161f8b989a2e4e6d2ea8c94bd0dcc81c3c1 Mon Sep 17 00:00:00 2001 From: Simon Date: Sun, 11 Aug 2024 12:37:30 +0200 Subject: [PATCH 3/3] pull new events --- src/map.ts | 9 --------- src/nostr/subscribe.ts | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/map.ts b/src/map.ts index 97129e3..8104058 100644 --- a/src/map.ts +++ b/src/map.ts @@ -207,15 +207,6 @@ function addNoteToMap(event: Kind30398Event) { }); // Create marker with decoded coordinates marker.addTo(map); - // const contentChat = generateChatContentFromNotes([event]); - - //todo: rename addNoteToMap and other map - console.log(event); - const geochatNotes = document.getElementById("geochat-notes") as HTMLElement; - const li = document.createElement("li"); - // li.innerHTML = contentChat; - geochatNotes.appendChild(li); - marker.on( "click", async (markerClickEvent) => diff --git a/src/nostr/subscribe.ts b/src/nostr/subscribe.ts index cec919c..5843f72 100644 --- a/src/nostr/subscribe.ts +++ b/src/nostr/subscribe.ts @@ -176,8 +176,22 @@ export const subscribe = async ({ noteEventsQueue.forEach((event) => onEventReceived(event)); backgroundProfileFetching(profileFilter); + backgroundNoteEventsFetching(onEventReceived); }; +async function backgroundNoteEventsFetching(onEventReceived) { + const relayPool = await _initRelays(); + const filter = { + kinds: [MAP_NOTE_REPOST_KIND], + "#L": ["open-location-code"], + since: Math.floor(Date.now() / 1000), + authors: TRUSTED_VALIDATION_PUBKEYS, + }; + for await (const msg of relayPool.req([filter])) { + if (msg[0] === "EVENT") onEventReceived(msg[2] as Kind30398Event); + } +} + async function backgroundProfileFetching(profileFilter) { const onProfileEvent = (event: MetadataEvent) => { // if (isDev()) console.log("#zD1Iau got profile event", event);