Browse Source

unpin notes

imwald
Silberengel 1 month ago
parent
commit
e389c06b55
  1. 40
      src/components/NoteOptions/useMenuActions.tsx
  2. 47
      src/lib/replaceable-list-latest.ts

40
src/components/NoteOptions/useMenuActions.tsx

@ -7,7 +7,11 @@ import logger from '@/lib/logger' @@ -7,7 +7,11 @@ import logger from '@/lib/logger'
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
import { normalizeUrl, simplifyUrl } from '@/lib/url'
import { speakNoteReadAloud } from '@/lib/read-aloud'
import { buildPinListTagsAfterToggle, fetchLatestReplaceableListEvent } from '@/lib/replaceable-list-latest'
import {
buildPinListTagsAfterToggle,
fetchNewestPinListForPubkey,
isEventInPinList
} from '@/lib/replaceable-list-latest'
import { generateBech32IdFromATag } from '@/lib/tag'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
@ -151,28 +155,15 @@ export function useMenuActions({ @@ -151,28 +155,15 @@ export function useMenuActions({
const comprehensiveRelays = Array.from(new Set(normalizedRelays))
let pinListEvent: Event | null | undefined = await fetchLatestReplaceableListEvent(
pubkey,
10001,
comprehensiveRelays
)
if (!pinListEvent) {
try {
pinListEvent = (await client.fetchPinListEvent(pubkey)) ?? null
} catch (error) {
logger.component('PinStatus', 'Error fetching pin list fallback', {
error: (error as Error).message
})
pinListEvent = null
}
}
const pinListEvent = await fetchNewestPinListForPubkey(pubkey, comprehensiveRelays)
if (pinListEvent) {
const isEventPinned = pinListEvent.tags.some(tag => tag[0] === 'e' && tag[1] === event.id)
setIsPinned(isEventPinned)
setIsPinned(isEventInPinList(pinListEvent, event))
} else {
setIsPinned(false)
}
} catch (error) {
logger.component('PinStatus', 'Error checking pin status', { error: (error as Error).message })
setIsPinned(false)
}
}
checkIfPinned()
@ -197,18 +188,11 @@ export function useMenuActions({ @@ -197,18 +188,11 @@ export function useMenuActions({
const comprehensiveRelays = Array.from(new Set(normalizedRelays))
let latestPinList = await fetchLatestReplaceableListEvent(pubkey, 10001, comprehensiveRelays)
if (!latestPinList) {
try {
latestPinList = (await client.fetchPinListEvent(pubkey)) ?? undefined
} catch (error) {
logger.component('PinNote', 'Pin list fallback fetch failed', { error: (error as Error).message })
}
}
const latestPinList = await fetchNewestPinListForPubkey(pubkey, comprehensiveRelays)
logger.component('PinNote', 'Current pin list event', { hasEvent: !!latestPinList })
const newTags = buildPinListTagsAfterToggle(latestPinList ?? null, event.id, !isPinned)
const newTags = buildPinListTagsAfterToggle(latestPinList ?? null, event, !isPinned)
const successMessage = isPinned ? t('Note unpinned') : t('Note pinned')
logger.component('PinNote', 'Pin list tag count after merge', { count: newTags.length })

47
src/lib/replaceable-list-latest.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { METADATA_BATCH_QUERY_EOSE_TIMEOUT_MS, METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS } from '@/constants'
import { normalizeHexPubkey } from '@/lib/pubkey'
import { normalizeUrl } from '@/lib/url'
import { queryService } from '@/services/client.service'
import client, { queryService } from '@/services/client.service'
import type { Event } from 'nostr-tools'
/**
@ -29,6 +29,39 @@ export async function fetchLatestReplaceableListEvent( @@ -29,6 +29,39 @@ export async function fetchLatestReplaceableListEvent(
return rows.reduce((best, e) => (e.created_at > best.created_at ? e : best))
}
/**
* Kind 10001 from browsing relays can be stale vs the copy resolved via the authors relay set
* ({@link client.fetchPinListEvent}). Merge both and keep the newest `created_at` so pin UI and merges
* match the profile pin list.
*/
export async function fetchNewestPinListForPubkey(
pubkeyHex: string,
relayUrls: string[]
): Promise<Event | undefined> {
const pk = normalizeHexPubkey(pubkeyHex)
const [fromRelays, fromService] = await Promise.all([
relayUrls.length
? fetchLatestReplaceableListEvent(pk, 10001, relayUrls)
: Promise.resolve(undefined),
client.fetchPinListEvent(pk).catch(() => undefined)
])
if (!fromRelays) return fromService
if (!fromService) return fromRelays
return fromService.created_at >= fromRelays.created_at ? fromService : fromRelays
}
/** Whether this event is referenced by the pin list via `e` (hex id) or `a` (NIP-33 coordinate). */
export function isEventInPinList(pinList: Event, event: Event): boolean {
const idLower = event.id.toLowerCase()
const d = event.tags.find((t) => t[0] === 'd')?.[1] ?? ''
const coord = `${event.kind}:${event.pubkey}:${d}`.toLowerCase()
for (const tag of pinList.tags) {
if (tag[0] === 'e' && tag[1] && tag[1].toLowerCase() === idLower) return true
if (tag[0] === 'a' && tag[1] && tag[1].toLowerCase() === coord) return true
}
return false
}
function orderedUniqueEHexIds(tags: string[][]): string[] {
const seen = new Set<string>()
const out: string[] = []
@ -46,17 +79,23 @@ function orderedUniqueEHexIds(tags: string[][]): string[] { @@ -46,17 +79,23 @@ function orderedUniqueEHexIds(tags: string[][]): string[] {
/**
* Next pin list (kind 10001) tags: preserve non-`e`/`a` tags and `a` pins, merge `e` hex ids with dedupe.
* Unpin removes both the `e` id and an `a` coordinate when the list used NIP-33 pins.
*/
export function buildPinListTagsAfterToggle(
latest: Event | null | undefined,
noteHexId: string,
targetEvent: Event,
shouldPin: boolean
): string[][] {
const tags = latest?.tags ?? []
const meta = tags.filter((t) => t[0] !== 'e' && t[0] !== 'a')
const aKeep = tags.filter((t) => t[0] === 'a' && t[1])
const d = targetEvent.tags.find((t) => t[0] === 'd')?.[1] ?? ''
const coord = `${targetEvent.kind}:${targetEvent.pubkey}:${d}`.toLowerCase()
let aKeep = tags.filter((t) => t[0] === 'a' && t[1])
if (!shouldPin) {
aKeep = aKeep.filter((t) => t[1]!.toLowerCase() !== coord)
}
let eIds = orderedUniqueEHexIds(tags)
const id = noteHexId.toLowerCase()
const id = targetEvent.id.toLowerCase()
if (shouldPin) {
if (!eIds.includes(id)) eIds = [...eIds, id]
} else {

Loading…
Cancel
Save