You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
103 lines
3.2 KiB
103 lines
3.2 KiB
import { tagNameEquals } from '@/lib/tag' |
|
import type { Event } from 'nostr-tools' |
|
|
|
export const FOLLOW_SET_SPELL_PREFIX = 'followset:' as const |
|
|
|
export function isFollowSetSpellId(s: string): boolean { |
|
return s.startsWith(FOLLOW_SET_SPELL_PREFIX) |
|
} |
|
|
|
export function encodeFollowSetSpellId(dTag: string): string { |
|
return `${FOLLOW_SET_SPELL_PREFIX}${encodeURIComponent(dTag)}` |
|
} |
|
|
|
export function decodeFollowSetSpellId(spellId: string): string | null { |
|
if (!isFollowSetSpellId(spellId)) return null |
|
try { |
|
const d = decodeURIComponent(spellId.slice(FOLLOW_SET_SPELL_PREFIX.length)) |
|
return d.length > 0 ? d : null |
|
} catch { |
|
return null |
|
} |
|
} |
|
|
|
export function getFollowSetDTag(event: Event): string | undefined { |
|
return event.tags.find(tagNameEquals('d'))?.[1] |
|
} |
|
|
|
export function labelFollowSetEvent(event: Event): string { |
|
const title = event.tags.find(tagNameEquals('title'))?.[1]?.trim() |
|
if (title) return title |
|
const d = getFollowSetDTag(event) |
|
return d ?? 'follow set' |
|
} |
|
|
|
/** Hex pubkeys from `p` tags (NIP-51 follow sets). */ |
|
export function pubkeysFromFollowSetEvent(event: Event): string[] { |
|
const out: string[] = [] |
|
const seen = new Set<string>() |
|
for (const t of event.tags) { |
|
if (t[0] !== 'p' || !t[1]) continue |
|
const pk = t[1].trim().toLowerCase() |
|
if (!/^[0-9a-f]{64}$/.test(pk)) continue |
|
if (seen.has(pk)) continue |
|
seen.add(pk) |
|
out.push(pk) |
|
} |
|
return out |
|
} |
|
|
|
/** |
|
* Latest event per `d` tag. Skips deprecated NIP-51 kind 30000 + `d`=mute (use kind 10000 mute list). |
|
*/ |
|
/** Build NIP-51 kind 30000 tags (`d` required; optional metadata; then `p` in order). */ |
|
export function buildFollowSetTags(params: { |
|
d: string |
|
title?: string |
|
description?: string |
|
image?: string |
|
pubkeys: string[] |
|
}): string[][] { |
|
const d = params.d.trim() |
|
if (!d || d === 'mute') throw new Error('Invalid list id') |
|
const tags: string[][] = [['d', d]] |
|
const title = params.title?.trim() |
|
if (title) tags.push(['title', title]) |
|
const description = params.description?.trim() |
|
if (description) tags.push(['description', description]) |
|
const image = params.image?.trim() |
|
if (image) tags.push(['image', image]) |
|
for (const pk of params.pubkeys) { |
|
const hex = pk.trim().toLowerCase() |
|
if (/^[0-9a-f]{64}$/.test(hex)) tags.push(['p', hex]) |
|
} |
|
return tags |
|
} |
|
|
|
export function extractFollowSetEditorFields(event: Event): { |
|
d: string |
|
title: string |
|
description: string |
|
image: string |
|
pubkeys: string[] |
|
} { |
|
return { |
|
d: getFollowSetDTag(event) ?? '', |
|
title: event.tags.find(tagNameEquals('title'))?.[1] ?? '', |
|
description: event.tags.find(tagNameEquals('description'))?.[1] ?? '', |
|
image: event.tags.find(tagNameEquals('image'))?.[1] ?? '', |
|
pubkeys: pubkeysFromFollowSetEvent(event) |
|
} |
|
} |
|
|
|
export function dedupeFollowSetEventsByD(events: Event[]): Event[] { |
|
const byD = new Map<string, Event>() |
|
for (const e of [...events].sort((a, b) => b.created_at - a.created_at)) { |
|
const d = getFollowSetDTag(e) |
|
if (!d || d === 'mute') continue |
|
if (!byD.has(d)) byD.set(d, e) |
|
} |
|
return [...byD.values()].sort((a, b) => |
|
labelFollowSetEvent(a).localeCompare(labelFollowSetEvent(b), undefined, { sensitivity: 'base' }) |
|
) |
|
}
|
|
|