diff --git a/src/components/PublishSuccessSubtleIndicator/index.tsx b/src/components/PublishSuccessSubtleIndicator/index.tsx index 235388b0..8f917553 100644 --- a/src/components/PublishSuccessSubtleIndicator/index.tsx +++ b/src/components/PublishSuccessSubtleIndicator/index.tsx @@ -5,8 +5,8 @@ import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' /** - * When publish success toasts are off, {@link emitPublishSuccessSubtle} shows this instead: - * small green check + label, bottom-right, auto-dismiss. + * When publish success toasts are off, `publishing-feedback` dispatches {@link PUBLISH_SUCCESS_SUBTLE_EVENT} + * so we show a small green check + label, bottom-right, auto-dismiss. */ export default function PublishSuccessSubtleIndicator() { const { t } = useTranslation() diff --git a/src/contexts/suppress-embedded-note-context.tsx b/src/contexts/suppress-embedded-note-context.tsx index b8972471..f85ecb2c 100644 --- a/src/contexts/suppress-embedded-note-context.tsx +++ b/src/contexts/suppress-embedded-note-context.tsx @@ -6,7 +6,7 @@ export type SuppressEmbeddedNoteValue = { } /** When set, EmbeddedNote should not render notes whose id/coordinate matches (avoids redundancy when viewing "quotes of this note"). */ -export const SuppressEmbeddedNoteContext = createContext(undefined) +const SuppressEmbeddedNoteContext = createContext(undefined) export function useSuppressEmbeddedNoteId(): SuppressEmbeddedNoteValue | undefined { return useContext(SuppressEmbeddedNoteContext) diff --git a/src/hooks/index.tsx b/src/hooks/index.tsx index 66796fb9..a565b353 100644 --- a/src/hooks/index.tsx +++ b/src/hooks/index.tsx @@ -5,7 +5,6 @@ export * from './useFetchFollowings' export * from './useFetchNip05' export * from './useFetchProfile' export * from './useFetchRelayInfo' -export * from './useFetchRelayInfos' export * from './useFetchRelayList' export * from './useSearchProfiles' export * from './useMediaExtraction' diff --git a/src/hooks/useFetchRelayInfos.tsx b/src/hooks/useFetchRelayInfos.tsx deleted file mode 100644 index d5e481d1..00000000 --- a/src/hooks/useFetchRelayInfos.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { checkAlgoRelay } from '@/lib/relay' -import relayInfoService from '@/services/relay-info.service' -import { TRelayInfo } from '@/types' -import { useEffect, useState } from 'react' -import logger from '@/lib/logger' - -export function useFetchRelayInfos(urls: string[]) { - const [isFetching, setIsFetching] = useState(true) - const [relayInfos, setRelayInfos] = useState<(TRelayInfo | undefined)[]>([]) - const [areAlgoRelays, setAreAlgoRelays] = useState(false) - const [searchableRelayUrls, setSearchableRelayUrls] = useState([]) - const urlsString = JSON.stringify(urls) - - useEffect(() => { - const fetchRelayInfos = async () => { - setIsFetching(true) - if (urls.length === 0) { - return setIsFetching(false) - } - const timer = setTimeout(() => { - setIsFetching(false) - }, 5000) - try { - const relayInfos = await relayInfoService.getRelayInfos(urls) - setRelayInfos(relayInfos) - setAreAlgoRelays(relayInfos.every((relayInfo) => checkAlgoRelay(relayInfo))) - setSearchableRelayUrls( - relayInfos - .map((relayInfo, index) => ({ - url: urls[index], - searchable: relayInfo?.supported_nips?.includes(50) - })) - .filter((relayInfo) => relayInfo.searchable) - .map((relayInfo) => relayInfo.url) - ) - } catch (err) { - logger.error('Failed to fetch relay infos', { error: err, urls }) - } finally { - clearTimeout(timer) - setIsFetching(false) - } - } - - fetchRelayInfos() - }, [urlsString]) - - return { relayInfos, isFetching, areAlgoRelays, searchableRelayUrls } -} diff --git a/src/hooks/useProfileBadges.tsx b/src/hooks/useProfileBadges.tsx index 21380718..b8fe0752 100644 --- a/src/hooks/useProfileBadges.tsx +++ b/src/hooks/useProfileBadges.tsx @@ -5,19 +5,9 @@ import { fetchNip58BadgeDefinition, mergeNip58BadgeRelayPool } from '@/lib/fetch-badge-nip58' -import { - profileAccordionGetCachedBadges, - profileAccordionGetCachedRelayUrls, - profileAccordionRelayUrlsKey, - profileAccordionSetBadges -} from '@/lib/profile-accordion-session-cache' -import { queryService } from '@/services/client.service' import indexedDb from '@/services/indexed-db.service' -import { useCallback, useEffect, useRef, useState } from 'react' import { Event } from 'nostr-tools' import { tagNameEquals } from '@/lib/tag' -import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' -import { buildProfileRelayUrls } from '@/lib/profile-relay-urls' export type TProfileBadge = { /** Badge definition coordinate (e.g. "30009:alice:bravery") */ @@ -49,14 +39,7 @@ function parseATag(aTag: string): { kind: number; pubkey: string; d: string } | return { kind, pubkey: pk.toLowerCase(), d } } -/** True when we should re-resolve the badge definition (missing media but coordinate looks like kind 30009). */ -function badgeNeedsDefinitionMedia(b: TProfileBadge): boolean { - if (b.thumb || b.image) return false - const parsed = parseATag(b.a) - return !!(parsed && parsed.kind === ExtendedKind.BADGE_DEFINITION) -} - -export function mergeProfileBadgesByAwardId(seed: TProfileBadge[], fresh: TProfileBadge[]): TProfileBadge[] { +function mergeProfileBadgesByAwardId(seed: TProfileBadge[], fresh: TProfileBadge[]): TProfileBadge[] { const m = new Map() for (const b of seed) m.set(b.awardId, b) for (const b of fresh) m.set(b.awardId, b) @@ -89,123 +72,9 @@ export async function enrichBadgesFromIndexedDb(badges: TProfileBadge[]): Promis ) } -/** NIP-58: Fetches profile badges (kind 30008) and resolves badge definitions (kind 30009). */ -/** Pass relayUrls to share with other profile fetches. */ -export function useProfileBadges(pubkey: string | undefined, relayUrls?: string[]) { - const { blockedRelays } = useFavoriteRelays() - const blockedRelaysRef = useRef(blockedRelays) - blockedRelaysRef.current = blockedRelays - const relayUrlsRef = useRef(relayUrls) - relayUrlsRef.current = relayUrls - const blockedRelaysKey = profileAccordionRelayUrlsKey(blockedRelays) - const relayUrlsKey = profileAccordionRelayUrlsKey(relayUrls ?? []) - - const [badges, setBadges] = useState([]) - const [loading, setLoading] = useState(false) - const fetchIdRef = useRef(0) - - const fetchBadges = useCallback(async (force = false) => { - const myFetchId = (fetchIdRef.current += 1) - - if (!pubkey) { - if (myFetchId === fetchIdRef.current) { - setBadges([]) - setLoading(false) - } - return - } - - const relayUrlsLatest = relayUrlsRef.current - let urls = - relayUrlsLatest && relayUrlsLatest.length > 0 - ? relayUrlsLatest - : profileAccordionGetCachedRelayUrls(pubkey) ?? [] - - if (force || urls.length === 0) { - urls = await buildProfileRelayUrls(pubkey, blockedRelaysRef.current) - } - const relayKey = profileAccordionRelayUrlsKey(urls) - - const seedBadges = profileAccordionGetCachedBadges(pubkey, relayKey) - let deferLoading = !!(force && seedBadges?.length) - - if (!force) { - const cached = seedBadges - if (cached?.length) { - if (cached.some(badgeNeedsDefinitionMedia)) { - const enriched = await enrichBadgesFromIndexedDb(cached) - if (!enriched.some(badgeNeedsDefinitionMedia)) { - if (myFetchId !== fetchIdRef.current) return - setBadges(enriched) - profileAccordionSetBadges(pubkey, relayKey, enriched) - setLoading(false) - return - } - deferLoading = false - // Session cache was incomplete and IndexedDB has no definitions — fetch from network below. - } else { - if (myFetchId !== fetchIdRef.current) return - setBadges(cached) - setLoading(false) - return - } - } - } - - if (force && seedBadges?.length && myFetchId === fetchIdRef.current) { - setBadges(seedBadges) - } - - if (myFetchId !== fetchIdRef.current) return - if (!deferLoading) { - setLoading(true) - } - - try { - const events = await queryService.fetchEvents( - urls, - { authors: [pubkey], kinds: [ExtendedKind.PROFILE_BADGES], '#d': ['profile_badges'] }, - { eoseTimeout: 2000, globalTimeout: 15000, firstRelayResultGraceMs: false } - ) - const profileBadgesEvent = events.sort((a, b) => b.created_at - a.created_at)[0] - - if (!profileBadgesEvent || myFetchId !== fetchIdRef.current) { - if (myFetchId === fetchIdRef.current && !seedBadges?.length) setBadges([]) - return - } - - const merged = await resolveProfileBadgeList( - profileBadgesEvent, - urls, - blockedRelaysRef.current, - seedBadges - ) - - if (myFetchId !== fetchIdRef.current) return - setBadges(merged) - profileAccordionSetBadges(pubkey, relayKey, merged) - } catch { - if (myFetchId !== fetchIdRef.current) return - if (!seedBadges?.length) setBadges([]) - } finally { - if (myFetchId === fetchIdRef.current) setLoading(false) - } - }, [pubkey, blockedRelaysKey, relayUrlsKey]) - - const refresh = useCallback(() => { - void fetchBadges(true) - }, [pubkey, fetchBadges]) - - useEffect(() => { - void fetchBadges(false) - }, [fetchBadges]) - - return { badges, loading, refresh } -} - /** * Resolves NIP-58 badge definitions/awards for the newest kind-30008 `profile_badges` event. - * Shared by {@link useProfileBadges} and profile accordion bundle fetch. + * Used by profile accordion bundle fetch. */ export async function resolveProfileBadgeList( profileBadgesEvent: Event | undefined, diff --git a/src/hooks/useProfileFollowPacks.tsx b/src/hooks/useProfileFollowPacks.tsx index 8d4f7895..bae8382d 100644 --- a/src/hooks/useProfileFollowPacks.tsx +++ b/src/hooks/useProfileFollowPacks.tsx @@ -1,128 +1,6 @@ -import { ExtendedKind, FAST_READ_RELAY_URLS } from '@/constants' -import { - profileAccordionGetCachedFollowPacks, - profileAccordionGetCachedRelayUrls, - profileAccordionRelayUrlsKey, - profileAccordionSetFollowPacks -} from '@/lib/profile-accordion-session-cache' -import { replaceableEventDedupeKey } from '@/lib/event' -import { queryService } from '@/services/client.service' import { Event } from 'nostr-tools' -import { useCallback, useEffect, useRef, useState } from 'react' -import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' -import { buildProfileRelayUrls } from '@/lib/profile-relay-urls' export type TProfileFollowPack = { event: Event title: string } - -function getPackTitle(event: Event): string { - const titleTag = event.tags.find((tag) => tag[0] === 'title' || tag[0] === 'name') - return titleTag?.[1] || 'Follow Pack' -} - -/** Fetches follow packs (kind 39089) that contain this pubkey in #p tags. */ -export function useProfileFollowPacks( - pubkey: string | undefined, - relayUrls?: string[] -) { - const { blockedRelays } = useFavoriteRelays() - const blockedRelaysRef = useRef(blockedRelays) - blockedRelaysRef.current = blockedRelays - const relayUrlsRef = useRef(relayUrls) - relayUrlsRef.current = relayUrls - const blockedRelaysKey = profileAccordionRelayUrlsKey(blockedRelays) - const relayUrlsKey = profileAccordionRelayUrlsKey(relayUrls ?? []) - - const [packs, setPacks] = useState([]) - const [loading, setLoading] = useState(false) - const fetchIdRef = useRef(0) - - const fetchPacks = useCallback(async (force = false) => { - const myFetchId = (fetchIdRef.current += 1) - - if (!pubkey) { - if (myFetchId === fetchIdRef.current) { - setPacks([]) - setLoading(false) - } - return - } - - const relayUrlsLatest = relayUrlsRef.current - let urls = - relayUrlsLatest && relayUrlsLatest.length > 0 - ? relayUrlsLatest - : profileAccordionGetCachedRelayUrls(pubkey) ?? [] - - if (force || urls.length === 0) { - urls = await buildProfileRelayUrls(pubkey, blockedRelaysRef.current) - } - const queryUrls = urls.length > 0 ? urls : [...FAST_READ_RELAY_URLS] - const relayKey = profileAccordionRelayUrlsKey(queryUrls) - - if (!force) { - const cached = profileAccordionGetCachedFollowPacks(pubkey, relayKey) - if (cached) { - if (myFetchId !== fetchIdRef.current) return - setPacks(cached) - setLoading(false) - return - } - } - - const seed = profileAccordionGetCachedFollowPacks(pubkey, relayKey) - if (seed?.length && myFetchId === fetchIdRef.current) { - setPacks(seed) - } - - if (myFetchId !== fetchIdRef.current) return - if (!seed?.length) { - setLoading(true) - } - - try { - const events = await queryService.fetchEvents( - queryUrls, - [{ '#p': [pubkey], kinds: [ExtendedKind.FOLLOW_PACK], limit: 50 }], - { eoseTimeout: 2000, globalTimeout: 15000, firstRelayResultGraceMs: false } - ) - - if (myFetchId !== fetchIdRef.current) return - - const network: TProfileFollowPack[] = events.map((evt) => ({ - event: evt, - title: getPackTitle(evt) - })) - const byDedupeKey = new Map() - const put = (p: TProfileFollowPack) => { - const k = replaceableEventDedupeKey(p.event) - const prev = byDedupeKey.get(k) - if (!prev || p.event.created_at > prev.event.created_at) { - byDedupeKey.set(k, p) - } - } - for (const p of seed ?? []) put(p) - for (const p of network) put(p) - const merged = [...byDedupeKey.values()].sort((a, b) => b.event.created_at - a.event.created_at) - setPacks(merged) - profileAccordionSetFollowPacks(pubkey, relayKey, merged) - } catch { - if (myFetchId !== fetchIdRef.current) return - if (!seed?.length) setPacks([]) - } finally { - if (myFetchId === fetchIdRef.current) setLoading(false) - } - }, [pubkey, blockedRelaysKey, relayUrlsKey]) - - const refresh = useCallback(() => { - void fetchPacks(true) - }, [pubkey, fetchPacks]) - - useEffect(() => { - void fetchPacks(false) - }, [fetchPacks]) - - return { packs, loading, refresh } -} diff --git a/src/hooks/useProfileInteractions.tsx b/src/hooks/useProfileInteractions.tsx index 82d7be7e..dbf97ceb 100644 --- a/src/hooks/useProfileInteractions.tsx +++ b/src/hooks/useProfileInteractions.tsx @@ -1,18 +1,3 @@ -import { ExtendedKind } from '@/constants' -import { getZapInfoFromEvent } from '@/lib/event-metadata' -import { queryService, replaceableEventService } from '@/services/client.service' -import { hexPubkeysEqual } from '@/lib/pubkey' -import { Event, Filter, kinds } from 'nostr-tools' -import { useCallback, useEffect, useRef, useState } from 'react' -import { - profileAccordionGetCachedInteractions, - profileAccordionGetCachedRelayUrls, - profileAccordionRelayUrlsKey, - profileAccordionSetInteractions -} from '@/lib/profile-accordion-session-cache' -import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' -import { buildProfileRelayUrls } from '@/lib/profile-relay-urls' - export type TProfileZap = { pr: string pubkey: string @@ -20,263 +5,3 @@ export type TProfileZap = { created_at: number comment?: string } - -const NOTE_IDS_FOR_COMMENTS = 50 - -/** Fetches zaps, reactions (likes on the kind-0 profile metadata event only), and comments (on the user's notes + profile). */ -/** Uses profile owner's outboxes + PROFILE_FETCH_RELAY_URLS. Pass relayUrls to share with other profile fetches. */ -export function useProfileInteractions(pubkey: string | undefined, relayUrls?: string[]) { - const { blockedRelays } = useFavoriteRelays() - const blockedRelaysRef = useRef(blockedRelays) - blockedRelaysRef.current = blockedRelays - const relayUrlsRef = useRef(relayUrls) - relayUrlsRef.current = relayUrls - const blockedRelaysKey = profileAccordionRelayUrlsKey(blockedRelays) - const relayUrlsKey = profileAccordionRelayUrlsKey(relayUrls ?? []) - - const [zaps, setZaps] = useState([]) - const [reactions, setReactions] = useState([]) - const [comments, setComments] = useState([]) - const [loading, setLoading] = useState(false) - const fetchIdRef = useRef(0) - - const fetchAll = useCallback(async (force = false) => { - const myFetchId = (fetchIdRef.current += 1) - - if (!pubkey) { - if (myFetchId === fetchIdRef.current) { - setZaps([]) - setReactions([]) - setComments([]) - setLoading(false) - } - return - } - - const relayUrlsLatest = relayUrlsRef.current - let urls = - relayUrlsLatest && relayUrlsLatest.length > 0 - ? relayUrlsLatest - : profileAccordionGetCachedRelayUrls(pubkey) ?? [] - - if (force || urls.length === 0) { - urls = await buildProfileRelayUrls(pubkey, blockedRelaysRef.current) - } - const relayKey = profileAccordionRelayUrlsKey(urls) - - if (!force) { - const cached = profileAccordionGetCachedInteractions(pubkey, relayKey) - if (cached) { - if (myFetchId !== fetchIdRef.current) return - setZaps([...cached.zaps].sort((a, b) => b.amount - a.amount)) - setReactions([...cached.reactions].sort((a, b) => b.created_at - a.created_at)) - setComments([...cached.comments].sort((a, b) => b.created_at - a.created_at)) - setLoading(false) - return - } - } - - const seed = profileAccordionGetCachedInteractions(pubkey, relayKey) - - if (seed && myFetchId === fetchIdRef.current) { - setZaps([...seed.zaps].sort((a, b) => b.amount - a.amount)) - setReactions([...seed.reactions].sort((a, b) => b.created_at - a.created_at)) - setComments([...seed.comments].sort((a, b) => b.created_at - a.created_at)) - } - - if (myFetchId !== fetchIdRef.current) return - - const hasVisibleSeed = - !!seed && - (seed.zaps.length > 0 || seed.reactions.length > 0 || seed.comments.length > 0) - if (!hasVisibleSeed) { - setLoading(true) - } - - try { - const profileMetaPromise = replaceableEventService.fetchReplaceableEvent( - pubkey, - kinds.Metadata, - undefined, - urls - ) - - const collectedZaps: TProfileZap[] = seed ? [...seed.zaps] : [] - const reactionsByPubkey = new Map() // one reaction per npub, newest kept (profile event only) - if (seed) { - for (const e of seed.reactions) { - reactionsByPubkey.set(e.pubkey, e) - } - } - const collectedComments: Event[] = seed ? [...seed.comments] : [] - const seenZaps = new Set(collectedZaps.map((z) => z.pr)) - const seenProfileReactionEventIds = new Set() - if (seed) { - for (const e of seed.reactions) seenProfileReactionEventIds.add(e.id) - } - const seenCommentIds = new Set(collectedComments.map((c) => c.id)) - let noteIds: string[] = [] - - // Phase 1: zaps + profile's recent notes (for comments on those notes) - const phase1Filters: Filter[] = [ - { '#p': [pubkey], kinds: [kinds.Zap], limit: 100 }, - { authors: [pubkey], kinds: [kinds.ShortTextNote], limit: NOTE_IDS_FOR_COMMENTS } - ] - - const flushZaps = () => { - if (myFetchId !== fetchIdRef.current) return - const sorted = [...collectedZaps].sort((a, b) => b.amount - a.amount) - setZaps(sorted) - } - await queryService.fetchEvents(urls, phase1Filters, { - eoseTimeout: 2000, - globalTimeout: 15000, - firstRelayResultGraceMs: false, - onevent: (evt) => { - if (evt.kind === kinds.Zap) { - const info = getZapInfoFromEvent(evt) - if (!info || !hexPubkeysEqual(info.recipientPubkey ?? '', pubkey) || !info.amount || info.amount <= 0) return - const sender = info.senderPubkey ?? evt.pubkey - if (hexPubkeysEqual(sender, pubkey)) return // skip self-zaps (likely tests) - if (seenZaps.has(evt.id)) return - seenZaps.add(evt.id) - collectedZaps.push({ - pr: evt.id, - pubkey: sender, - amount: info.amount, - created_at: evt.created_at, - comment: info.comment - }) - flushZaps() // render incrementally as events arrive from slow relays - } else if (evt.kind === kinds.ShortTextNote) { - noteIds.push(evt.id) - } - } - }) - - noteIds = [...new Set(noteIds)].slice(0, NOTE_IDS_FOR_COMMENTS) - if (myFetchId !== fetchIdRef.current) return - - const profileMetaEvent = await profileMetaPromise - if (myFetchId !== fetchIdRef.current) return - - const profileReactionATags = new Set([`0:${pubkey}:`, `0:${pubkey}:profile`]) - const reactionTargetsKind0Profile = (evt: Event): boolean => { - if (evt.kind !== kinds.Reaction) return false - const aHit = evt.tags.some((t) => t[0] === 'a' && t[1] && profileReactionATags.has(t[1])) - if (aHit) return true - const pid = profileMetaEvent?.id - if (!pid) return false - return evt.tags.some( - (t) => t[0] === 'e' && t[1] && hexPubkeysEqual(t[1], pid) - ) - } - - const flushReactions = () => { - if (myFetchId !== fetchIdRef.current) return - setReactions(Array.from(reactionsByPubkey.values()).sort((a, b) => b.created_at - a.created_at)) - } - const flushComments = () => { - if (myFetchId !== fetchIdRef.current) return - setComments([...collectedComments].sort((a, b) => b.created_at - a.created_at)) - } - const ingestProfileReaction = (evt: Event) => { - if (!reactionTargetsKind0Profile(evt)) return - if (hexPubkeysEqual(evt.pubkey, pubkey)) return - if (seenProfileReactionEventIds.has(evt.id)) return - seenProfileReactionEventIds.add(evt.id) - const existing = reactionsByPubkey.get(evt.pubkey) - if (!existing || evt.created_at > existing.created_at) { - reactionsByPubkey.set(evt.pubkey, evt) - } - flushReactions() - } - const ingestComment = (evt: Event) => { - if (hexPubkeysEqual(evt.pubkey, pubkey)) return - if (seenCommentIds.has(evt.id)) return - seenCommentIds.add(evt.id) - collectedComments.push(evt) - flushComments() - } - - const phase2CommentOpts = { - eoseTimeout: 2000, - globalTimeout: 15000, - firstRelayResultGraceMs: false as const, - onevent: (evt: Event) => { - if (evt.kind === ExtendedKind.COMMENT) { - ingestComment(evt) - } - } - } - - // Phase 2a: comments on profile's notes (#e) only - if (noteIds.length > 0) { - await queryService.fetchEvents(urls, [{ - '#e': noteIds, - kinds: [ExtendedKind.COMMENT], - limit: 50 - }], phase2CommentOpts) - } - - // Phase 2b: comments ON the profile itself (kind 0) - use #a (required), p is optional - const profileAddrs = [`0:${pubkey}:`, `0:${pubkey}:profile`] - await queryService.fetchEvents(urls, [{ - '#a': profileAddrs, - kinds: [ExtendedKind.COMMENT], - limit: 50 - }], phase2CommentOpts) - - // Phase 2c: reactions (likes) on the kind-0 profile metadata event only (#e + event id, and/or #a coordinates) - const profileReactionFilters: Filter[] = [] - if (profileMetaEvent?.id) { - profileReactionFilters.push({ '#e': [profileMetaEvent.id], kinds: [kinds.Reaction], limit: 80 }) - } - profileReactionFilters.push({ '#a': [...profileReactionATags], kinds: [kinds.Reaction], limit: 80 }) - await queryService.fetchEvents(urls, profileReactionFilters, { - eoseTimeout: 2000, - globalTimeout: 15000, - firstRelayResultGraceMs: false, - onevent: (evt: Event) => { - if (evt.kind === kinds.Reaction) { - ingestProfileReaction(evt) - } - } - }) - - if (myFetchId !== fetchIdRef.current) return - collectedZaps.sort((a, b) => b.amount - a.amount) - const collectedReactions = Array.from(reactionsByPubkey.values()).sort((a, b) => b.created_at - a.created_at) - collectedComments.sort((a, b) => b.created_at - a.created_at) - setZaps(collectedZaps) - setReactions(collectedReactions) - setComments(collectedComments) - profileAccordionSetInteractions(pubkey, relayKey, { - zaps: collectedZaps, - reactions: collectedReactions, - comments: collectedComments - }) - } catch { - if (myFetchId !== fetchIdRef.current) return - } finally { - if (myFetchId === fetchIdRef.current) setLoading(false) - } - }, [pubkey, blockedRelaysKey, relayUrlsKey]) - - const refresh = useCallback(() => { - /** Keep session cache so refresh merges new relays/events onto what is already shown */ - void fetchAll(true) - }, [pubkey, fetchAll]) - - useEffect(() => { - void fetchAll(false) - }, [fetchAll]) - - return { zaps, reactions, comments, loading, refresh } -} - -/** @deprecated Use useProfileInteractions instead. Returns zaps only for compatibility. */ -export function useProfileZaps(pubkey: string | undefined) { - const result = useProfileInteractions(pubkey) - return { zaps: result.zaps, loading: result.loading, refresh: result.refresh } -} diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 22af94cc..1b1ac567 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -28,7 +28,7 @@ export type TLanguage = keyof typeof LANGUAGE_META export const LocalizedLanguageNames: { [key in TLanguage]: string } = { ...LANGUAGE_META } -export const supportedLanguages = Object.keys(LANGUAGE_META) as TLanguage[] +const supportedLanguages = Object.keys(LANGUAGE_META) as TLanguage[] const localeModules = import.meta.glob<{ default: Resource }>('./locales/*.ts') @@ -40,7 +40,7 @@ function normalizeToSupported(lng: string): TLanguage { return supportedLanguages.find((s) => lng.startsWith(s)) ?? 'en' } -export async function ensureLocaleLoaded(code: TLanguage): Promise { +async function ensureLocaleLoaded(code: TLanguage): Promise { if (code === 'en') return if (i18n.hasResourceBundle(code, 'translation')) return const load = localeModules[localePath(code)] diff --git a/src/layouts/SecondaryPageLayout/index.tsx b/src/layouts/SecondaryPageLayout/index.tsx index f42bac62..a3725f9e 100644 --- a/src/layouts/SecondaryPageLayout/index.tsx +++ b/src/layouts/SecondaryPageLayout/index.tsx @@ -145,7 +145,7 @@ const SecondaryPageLayout = forwardRef( SecondaryPageLayout.displayName = 'SecondaryPageLayout' export default SecondaryPageLayout -export function SecondaryPageTitlebar({ +function SecondaryPageTitlebar({ title, controls, hideBackButton = false, diff --git a/src/lib/compress-upload-media.ts b/src/lib/compress-upload-media.ts index a806dea7..001b458f 100644 --- a/src/lib/compress-upload-media.ts +++ b/src/lib/compress-upload-media.ts @@ -556,7 +556,7 @@ export type CompressMediaOptions = { } /** Default cap for raster image uploads (profile pics and inline media). */ -export const DEFAULT_IMAGE_UPLOAD_MAX_BYTES = 2 * 1024 * 1024 +const DEFAULT_IMAGE_UPLOAD_MAX_BYTES = 2 * 1024 * 1024 /** * Compress media before upload. Non-media types are returned unchanged. diff --git a/src/lib/content-parser.ts b/src/lib/content-parser.ts index 3c03c53c..315174ef 100644 --- a/src/lib/content-parser.ts +++ b/src/lib/content-parser.ts @@ -8,8 +8,7 @@ import { import { EMBEDDED_EVENT_REGEX, EMBEDDED_MENTION_REGEX, - EMOJI_SHORT_CODE_REGEX, - LEGACY_PROFILE_BECH32_REGEX + EMOJI_SHORT_CODE_REGEX } from '@/lib/content-patterns' import { PAYTO_URI_REGEX } from '@/lib/payto' import { logContentSpacing, reprString } from '@/lib/content-spacing-debug' @@ -59,12 +58,7 @@ export const EmbeddedMentionParser: TContentParser = { regex: EMBEDDED_MENTION_REGEX } -export const EmbeddedLegacyMentionParser: TContentParser = { - type: 'legacy-mention', - regex: LEGACY_PROFILE_BECH32_REGEX -} - -export const EmbeddedEventParser: TContentParser = { +const EmbeddedEventParser: TContentParser = { type: 'event', regex: EMBEDDED_EVENT_REGEX } @@ -74,12 +68,12 @@ export const EmbeddedWebsocketUrlParser: TContentParser = { regex: WS_URL_REGEX } -export const EmbeddedEmojiParser: TContentParser = { +const EmbeddedEmojiParser: TContentParser = { type: 'emoji', regex: EMOJI_SHORT_CODE_REGEX } -export const EmbeddedLNInvoiceParser: TContentParser = { +const EmbeddedLNInvoiceParser: TContentParser = { type: 'invoice', regex: LN_INVOICE_REGEX } diff --git a/src/lib/debug-utils.ts b/src/lib/debug-utils.ts index 465d66ad..e1405dd8 100644 --- a/src/lib/debug-utils.ts +++ b/src/lib/debug-utils.ts @@ -59,4 +59,3 @@ if (import.meta.env.DEV) { ;(window as any).jumbleDebug = debugUtils } -export default debugUtils diff --git a/src/lib/discussion-thread-composer.ts b/src/lib/discussion-thread-composer.ts index c77b0c52..ab4aa5f3 100644 --- a/src/lib/discussion-thread-composer.ts +++ b/src/lib/discussion-thread-composer.ts @@ -35,20 +35,20 @@ export type TDiscussionDynamicTopics = { }[] } -export type TTopicRow = { id: string; label: string; icon: LucideIcon } +type TTopicRow = { id: string; label: string; icon: LucideIcon } type TopicListEntry = { id: string; label: string } -export function extractImagesFromContent(content: string): string[] { +function extractImagesFromContent(content: string): string[] { const imageRegex = /(https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg)(\?[^\s]*)?)/gi return content.match(imageRegex) || [] } -export function generateImetaTagsFromUrls(imageUrls: string[]): string[][] { +function generateImetaTagsFromUrls(imageUrls: string[]): string[][] { return imageUrls.map((url) => ['imeta', 'url', url]) } -export function buildDiscussionNsfwTag(): string[] { +function buildDiscussionNsfwTag(): string[] { return ['content-warning', ''] } diff --git a/src/lib/discussion-topics.ts b/src/lib/discussion-topics.ts index 911e330a..c3730a46 100644 --- a/src/lib/discussion-topics.ts +++ b/src/lib/discussion-topics.ts @@ -84,151 +84,10 @@ export function extractHashtagsFromContent(content: string): string[] { return hashtags } -/** - * Extract t-tags from event tags - */ -export function extractTTagsFromEvent(event: NostrEvent): string[] { - return event.tags - .filter(tag => tag[0] === 't' && tag[1]) - .map(tag => normalizeTopic(tag[1])) -} - -/** - * Extract all topics (both hashtags and t-tags) from an event - */ -export function extractAllTopics(event: NostrEvent): string[] { - const hashtags = extractHashtagsFromContent(event.content) - const tTags = extractTTagsFromEvent(event) - - // Combine and deduplicate - const allTopics = [...new Set([...hashtags, ...tTags])] - - return allTopics -} - -/** - * Group threads by their primary topic and collect subtopic statistics - */ -export interface TopicAnalysis { - primaryTopic: string - subtopics: Map> // subtopic -> set of npubs - threads: NostrEvent[] -} - -export function analyzeThreadTopics( - threads: NostrEvent[], - availableTopicIds: string[] -): Map { - const topicMap = new Map() - - for (const thread of threads) { - const allTopics = extractAllTopics(thread) - - - // Find the primary topic (first match from available topics) - let primaryTopic = 'general' - for (const topic of allTopics) { - if (availableTopicIds.includes(topic)) { - primaryTopic = topic - break - } - } - - // Get or create topic analysis - if (!topicMap.has(primaryTopic)) { - topicMap.set(primaryTopic, { - primaryTopic, - subtopics: new Map(), - threads: [] - }) - } - - const analysis = topicMap.get(primaryTopic)! - analysis.threads.push(thread) - - // Track subtopics (all topics except the primary one and 'all'/'all-topics') - // For 'general' topic, include all other topics as subtopics - // Special case: Always include 'readings' as a subtopic for literature threads - const subtopics = allTopics.filter( - t => t !== primaryTopic && t !== 'all' && t !== 'all-topics' - ) - - // Special handling for literature threads with 'readings' hashtag - if (primaryTopic === 'literature' && allTopics.includes('readings')) { - // Ensure 'readings' is included as a subtopic - if (!subtopics.includes('readings')) { - subtopics.push('readings') - } - } - - for (const subtopic of subtopics) { - if (!analysis.subtopics.has(subtopic)) { - analysis.subtopics.set(subtopic, new Set()) - } - analysis.subtopics.get(subtopic)!.add(thread.pubkey) - } - } - - return topicMap -} - -/** - * Get dynamic subtopics for a given main topic - * Returns subtopics that have been used by more than minNpubs unique npubs - */ -export function getDynamicSubtopics( - analysis: TopicAnalysis | undefined, - minNpubs: number = 3 -): string[] { - if (!analysis) return [] - - const subtopics: string[] = [] - - - for (const [subtopic, npubs] of analysis.subtopics.entries()) { - if (npubs.size >= minNpubs) { - subtopics.push(subtopic) - } - } - - // Sort alphabetically - return subtopics.sort() -} - -/** - * Check if a thread matches a specific subtopic - */ -export function threadMatchesSubtopic( - thread: NostrEvent, - subtopic: string -): boolean { - const allTopics = extractAllTopics(thread) - return allTopics.includes(subtopic) -} - -/** - * Get the categorized topic for a thread - */ -export function getCategorizedTopic( - thread: NostrEvent, - availableTopicIds: string[] -): string { - const allTopics = extractAllTopics(thread) - - // Find the first matching topic from available topics - for (const topic of allTopics) { - if (availableTopicIds.includes(topic)) { - return topic - } - } - - return 'general' -} - /** * Extract h-tag (group ID) from event tags */ -export function extractHTagFromEvent(event: NostrEvent): string | null { +function extractHTagFromEvent(event: NostrEvent): string | null { const hTag = event.tags.find(tag => tag[0] === 'h' && tag[1]) return hTag ? hTag[1] : null } @@ -237,7 +96,7 @@ export function extractHTagFromEvent(event: NostrEvent): string | null { * Parse group identifier from h-tag and relay sources * Supports both "relay'group-id" format and bare group IDs */ -export function parseGroupIdentifier( +function parseGroupIdentifier( hTag: string, relaySources: string[] ): { groupId: string; groupRelay: string | null; fullIdentifier: string } { @@ -262,17 +121,10 @@ export function parseGroupIdentifier( } } -/** - * Check if a discussion belongs to a group - */ -export function isGroupDiscussion(event: NostrEvent): boolean { - return extractHTagFromEvent(event) !== null -} - /** * Build display name for a group */ -export function buildGroupDisplayName( +function buildGroupDisplayName( groupId: string, groupRelay: string | null ): string { diff --git a/src/lib/discussion-votes.ts b/src/lib/discussion-votes.ts index 95990dfe..c045df03 100644 --- a/src/lib/discussion-votes.ts +++ b/src/lib/discussion-votes.ts @@ -1,9 +1,9 @@ import type { TEmoji } from '@/types' /** Canonical reaction `content` for discussion upvotes (kind 7). */ -export const DISCUSSION_UPVOTE = '+' +const DISCUSSION_UPVOTE = '+' /** Canonical reaction `content` for discussion downvotes (kind 7). */ -export const DISCUSSION_DOWNVOTE = '-' +const DISCUSSION_DOWNVOTE = '-' /** Shown in discussion UIs; legacy reaction `content` used the same characters. */ export const DISCUSSION_UPVOTE_DISPLAY = '⬆️' @@ -42,15 +42,6 @@ export function isDiscussionVoteEmoji(emoji: TEmoji | string | undefined | null) return isDiscussionUpvoteEmoji(emoji) || isDiscussionDownvoteEmoji(emoji) } -/** Group legacy arrow reactions with +/- for one pill per direction. */ -export function canonicalDiscussionVoteKey( - emoji: TEmoji | string | undefined | null -): typeof DISCUSSION_UPVOTE | typeof DISCUSSION_DOWNVOTE | null { - if (isDiscussionUpvoteEmoji(emoji)) return DISCUSSION_UPVOTE - if (isDiscussionDownvoteEmoji(emoji)) return DISCUSSION_DOWNVOTE - return null -} - export const DISCUSSION_VOTE_EMOJIS = [DISCUSSION_UPVOTE, DISCUSSION_DOWNVOTE] as const /** Same vote direction, including legacy ⬆️/⬇️ vs +/-. */ diff --git a/src/lib/document-meta.ts b/src/lib/document-meta.ts index 75f6c559..079472ea 100644 --- a/src/lib/document-meta.ts +++ b/src/lib/document-meta.ts @@ -2,7 +2,7 @@ export const SITE_NAME = 'Imwald' -export const SITE_TAGLINE = +const SITE_TAGLINE = 'A user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery.' export function getSiteOrigin(): string { diff --git a/src/lib/draft-event.ts b/src/lib/draft-event.ts index 8c5c4028..7d8b034b 100644 --- a/src/lib/draft-event.ts +++ b/src/lib/draft-event.ts @@ -1471,7 +1471,7 @@ const IMWALD_ATTRIBUTION_ALT_TEXT = 'This event was published by https://jumble. * True for `alt` tags that are *our* app attribution (current or legacy Jumble/Imwald wording). * Does not match arbitrary user `alt` text unless it clearly points at this app. */ -export function isImwaldAppAttributionAltTag(tag: string[]): boolean { +function isImwaldAppAttributionAltTag(tag: string[]): boolean { if (!Array.isArray(tag) || tag[0] !== 'alt' || tag.length < 2) return false const raw = tag[1] if (typeof raw !== 'string') return false @@ -2440,44 +2440,3 @@ export function createCitationPromptDraftEvent( created_at: dayjs().unix() } } - -/** Git Republic release (kind 1642); mirrors `releases-service` tag layout. */ -export function createGitReleaseDraftEvent( - content: string, - options: { - repoOwnerPubkey: string - repoId: string - tagName: string - tagHash: string - title?: string - downloadUrl?: string - isDraft?: boolean - isPrerelease?: boolean - } -): TDraftEvent { - const repoAddress = `${ExtendedKind.GIT_REPO_ANNOUNCEMENT}:${options.repoOwnerPubkey}:${options.repoId}` - const tags: string[][] = [ - ['a', repoAddress], - ['p', options.repoOwnerPubkey], - ['tag', options.tagName], - ['r', options.tagHash, '', 'tag'] - ] - if (options.title) { - tags.push(['title', options.title]) - } - if (options.downloadUrl) { - tags.push(['r', options.downloadUrl, '', 'download']) - } - if (options.isDraft) { - tags.push(['draft', 'true']) - } - if (options.isPrerelease) { - tags.push(['prerelease', 'true']) - } - return { - kind: ExtendedKind.GIT_RELEASE, - content, - tags, - created_at: dayjs().unix() - } -} diff --git a/src/lib/dtag-search.ts b/src/lib/dtag-search.ts index 576c17fb..0334787a 100644 --- a/src/lib/dtag-search.ts +++ b/src/lib/dtag-search.ts @@ -1,7 +1,7 @@ import { ExtendedKind } from '@/constants' import type { Event } from 'nostr-tools' -export function getDTagValue(event: Event): string | undefined { +function getDTagValue(event: Event): string | undefined { const t = event.tags.find((x) => x[0] === 'd' && x[1])?.[1] return t } @@ -45,7 +45,7 @@ export function eventMatchesDTagLooseQuery(needle: string, event: Event): boolea } /** Sort key: exact d-tag match first, then prefix, substring, then non-d / content-only. */ -export function dTagMatchRank(needle: string, dVal: string | undefined): number { +function dTagMatchRank(needle: string, dVal: string | undefined): number { if (!dVal) return 4 const nl = needle.trim().toLowerCase() const dl = dVal.toLowerCase() diff --git a/src/lib/emoji-content.ts b/src/lib/emoji-content.ts index 7bd9d025..90f7b06e 100644 --- a/src/lib/emoji-content.ts +++ b/src/lib/emoji-content.ts @@ -1,36 +1,6 @@ import { EMOJI_SHORT_CODE_REGEX } from '@/lib/content-patterns' import { emojis, shortcodeToEmoji } from '@tiptap/extension-emoji' -const STANDARD_EMOJI_LIMIT = 20 - -/** - * Returns standard emoji shortcodes matching the query (for autocomplete). - */ -export function searchStandardEmojiShortcodes(query: string, limit = STANDARD_EMOJI_LIMIT): string[] { - const q = query.toLowerCase().trim() - if (!q) return [] - const seen = new Set() - const out: string[] = [] - for (const item of emojis) { - const shortcodes = item.shortcodes ?? [] - const tags = item.tags ?? [] - const name = item.name ?? '' - const match = - shortcodes.some((s) => String(s).toLowerCase().includes(q)) || - tags.some((t) => String(t).toLowerCase().includes(q)) || - name.toLowerCase().includes(q) - if (match) { - const shortcode = shortcodes[0] ?? name - if (shortcode && !seen.has(shortcode)) { - seen.add(shortcode) - out.push(shortcode) - if (out.length >= limit) break - } - } - } - return out -} - /** * Replaces standard (non-custom) :shortcode: in content with their Unicode emoji * so they render correctly in all content fields (preview, feed, note page, etc.). diff --git a/src/lib/error-suppression.ts b/src/lib/error-suppression.ts index e03e92c2..65807cf8 100644 --- a/src/lib/error-suppression.ts +++ b/src/lib/error-suppression.ts @@ -6,7 +6,7 @@ // Track suppressed errors to avoid spam const suppressedErrors = new Set() -export function suppressExpectedErrors() { +function suppressExpectedErrors() { // Override console.error to filter out expected errors const originalConsoleError = console.error diff --git a/src/lib/event-filtering.ts b/src/lib/event-filtering.ts index 59cad352..954d6616 100644 --- a/src/lib/event-filtering.ts +++ b/src/lib/event-filtering.ts @@ -5,7 +5,7 @@ import storage from '@/services/local-storage.service' /** * Check if an event has expired based on its expiration tag */ -export function isEventExpired(event: Event): boolean { +function isEventExpired(event: Event): boolean { const expirationTag = event.tags.find(tag => tag[0] === 'expiration') if (!expirationTag || !expirationTag[1]) { return false @@ -22,7 +22,7 @@ export function isEventExpired(event: Event): boolean { /** * Check if an event is in quiet mode based on its quiet tag */ -export function isEventInQuietMode(event: Event): boolean { +function isEventInQuietMode(event: Event): boolean { const quietTag = event.tags.find(tag => tag[0] === 'quiet') if (!quietTag || !quietTag[1]) { return false diff --git a/src/lib/event-ingest-filter.ts b/src/lib/event-ingest-filter.ts index d78ca3cf..949e7654 100644 --- a/src/lib/event-ingest-filter.ts +++ b/src/lib/event-ingest-filter.ts @@ -7,7 +7,7 @@ import { kinds } from 'nostr-tools' * Detects **kind-1 note** spam where `content` is a stringified JSON **object** (game/app payloads, etc.) * instead of human-readable text. Scoped to {@link kinds.ShortTextNote} only. */ -export function isStringifiedJsonObjectContentNostrEvent( +function isStringifiedJsonObjectContentNostrEvent( event: Pick ): boolean { if (event.kind !== kinds.ShortTextNote) return false @@ -26,7 +26,7 @@ export function isStringifiedJsonObjectContentNostrEvent( * Kind-31987 noise: missing `d` (relay URL). Rating formats differ across clients; do not drop at ingest * (feeds and cards already treat unknown ratings as zero stars). */ -export function isIncompleteRelayReviewIngest(event: NEvent): boolean { +function isIncompleteRelayReviewIngest(event: NEvent): boolean { if (event.kind !== ExtendedKind.RELAY_REVIEW) return false return !getRelayUrlFromRelayReviewEvent(event) } diff --git a/src/lib/event.ts b/src/lib/event.ts index 7d011908..20db8723 100644 --- a/src/lib/event.ts +++ b/src/lib/event.ts @@ -107,10 +107,6 @@ export function isReplaceableEvent(kind: number) { ) } -export function isPictureEvent(event: Event) { - return event.kind === ExtendedKind.PICTURE -} - export function isProtectedEvent(event: Event) { return event.tags.some(([tagName]) => tagName === '-') } @@ -320,49 +316,6 @@ export function resolveDeclaredThreadRootEventHex(startHexId: string): string { return cur } -/** True if event references target as root, parent, or quoted (#q, #a) — used to hide redundant preview when showing quotes of current note. */ -export function eventReferencesEventId( - event: Event | undefined, - targetHexIdOrEvent: string | Event -): boolean { - if (!event) return false - const targetEvent = typeof targetHexIdOrEvent === 'object' ? targetHexIdOrEvent : undefined - const targetHexId = - typeof targetHexIdOrEvent === 'string' - ? targetHexIdOrEvent.toLowerCase() - : targetHexIdOrEvent.id?.toLowerCase() - const targetCoordinate = - targetEvent && isReplaceableEvent(targetEvent.kind) - ? getReplaceableCoordinateFromEvent(targetEvent) - : undefined - - const qRef = getQuotedReferenceFromQTags(event) - - if (targetHexId) { - const rootId = getRootETag(event)?.[1]?.toLowerCase() - if (rootId === targetHexId) return true - const parentId = getParentETag(event)?.[1]?.toLowerCase() - if (parentId === targetHexId) return true - if (qRef?.hexId === targetHexId) return true - const eTags = event.tags.filter((t) => t[0] === 'e' || t[0] === 'E') - if (eTags.some((t) => t[1]?.toLowerCase() === targetHexId)) return true - } - - if (targetCoordinate) { - const targetCoordNorm = normalizeReplaceableCoordinateString(targetCoordinate) - const aTags = event.tags.filter((t) => t[0] === 'a' || t[0] === 'A') - if (aTags.some((t) => normalizeReplaceableCoordinateString(t[1] ?? '') === targetCoordNorm)) return true - if ( - qRef?.coordinate && - normalizeReplaceableCoordinateString(qRef.coordinate) === targetCoordNorm - ) { - return true - } - } - - return false -} - export function getRootBech32Id(event?: Event) { const eTag = getRootETag(event) if (!eTag) { @@ -396,7 +349,7 @@ export function replaceableEventDedupeKey(event: Event): string { } /** Normalize `kind:pubkey:d` for comparisons (lowercase pubkey; preserve d). */ -export function normalizeReplaceableCoordinateString(coord: string): string { +function normalizeReplaceableCoordinateString(coord: string): string { const m = /^(\d+):([0-9a-f]{64}):(.*)$/i.exec(coord.trim()) if (!m) return coord.trim().toLowerCase() return getReplaceableCoordinate(Number(m[1]), m[2].toLowerCase(), m[3]) @@ -411,7 +364,7 @@ function stripNostrUriScheme(s: string): string { /** * NIP-10 / NIP-18: `q` tag value is `` or `` (coordinate), or NIP-19 bech32. */ -export function parseQTagReferenceValue( +function parseQTagReferenceValue( raw: string | undefined | null ): { hexId?: string; coordinate?: string } | undefined { if (raw == null) return undefined @@ -476,13 +429,6 @@ export function getQuotedEventHexIdFromQTags(event: Event): string | undefined { return getQuotedReferenceFromQTags(event)?.hexId } -/** Kind 1 whose `q` points at this hex id (legacy helper). */ -export function kind1QuotesEventHexId(event: Event, hexId: string): boolean { - if (event.kind !== kinds.ShortTextNote) return false - const ref = getQuotedReferenceFromQTags(event) - return !!ref?.hexId && ref.hexId === hexId.trim().toLowerCase() -} - /** Kind 1 quote-of-root: match `q` hex and/or replaceable coordinate (and bech32 decoding). */ export function kind1QuotesThreadRoot( event: Event, @@ -549,7 +495,7 @@ export function getImetaInfosFromEvent(event: Event) { return imeta } -export function getEmbeddedNoteBech32Ids(event: Event) { +function getEmbeddedNoteBech32Ids(event: Event) { const cache = EVENT_EMBEDDED_NOTES_CACHE.get(event.id) if (cache) return cache @@ -619,7 +565,7 @@ export function collectEmbeddedEventPrefetchTargets(event: Event): { } } -export function getEmbeddedPubkeys(event: Event) { +function getEmbeddedPubkeys(event: Event) { const cache = EVENT_EMBEDDED_PUBKEYS_CACHE.get(event.id) if (cache) return cache @@ -731,37 +677,6 @@ export function compareEvents(a: Event, b: Event): number { return 0 } -// Returns the event that should be retained when comparing two events -export function getRetainedEvent(a: Event, b: Event): Event { - if (compareEvents(a, b) > 0) { - return a - } - return b -} - -/** - * Collapse replaceable/addressable events to one per NIP-01 coordinate (`kind:pubkey` or `kind:pubkey:d`), - * keeping the newest (`created_at`, then lexicographically smallest `id` on ties). - * Non-replaceable events are keyed by `id` only. - */ -export function dedupeToLatestPerReplaceableCoordinate(events: Event[]): Event[] { - const byKey = new Map() - for (const e of events) { - if (!isReplaceableEvent(e.kind)) { - byKey.set(e.id, e) - continue - } - const coord = getReplaceableCoordinateFromEvent(e) - const existing = byKey.get(coord) - if (!existing) { - byKey.set(coord, e) - continue - } - byKey.set(coord, getRetainedEvent(e, existing)) - } - return [...byKey.values()] -} - /** External article URL from `i` / `I` tags (e.g. kind 1111 comments on web content). */ export function getHttpUrlFromITags(event: Event): string | undefined { const lower = event.tags.find((t) => t[0] === 'i')?.[1]?.trim() diff --git a/src/lib/favorites-feed-relays.ts b/src/lib/favorites-feed-relays.ts index 0ce28332..8702726c 100644 --- a/src/lib/favorites-feed-relays.ts +++ b/src/lib/favorites-feed-relays.ts @@ -97,7 +97,7 @@ export function buildAuthorInboxOutboxRelayUrls( * Profile pins + Medien: author NIP-65 tier (pass from {@link buildAuthorInboxOutboxRelayUrls}), then * {@link READ_ONLY_RELAY_URLS}, then {@link FAST_READ_RELAY_URLS}; dedupe, blocked-stripped, capped. */ -export const PROFILE_AUGMENTED_READ_MAX_RELAYS = 16 +const PROFILE_AUGMENTED_READ_MAX_RELAYS = 16 export function buildProfileAugmentedReadRelayUrls( authorRelayUrls: string[], @@ -159,7 +159,7 @@ export function getRelayUrlsWithFavoritesFastReadAndInbox( * Profile page pins + feed: viewed author's NIP-65 read + write (REQ tier 1), then logged-in user's favorites, * then fast-read defaults from constants, deduped and blocked-stripped, capped at this count. */ -export const PROFILE_PAGE_FEED_MAX_RELAYS = 6 +const PROFILE_PAGE_FEED_MAX_RELAYS = 6 export const PROFILE_PAGE_PINS_RESOLVE_LIMIT = 10 diff --git a/src/lib/fetch-with-timeout.ts b/src/lib/fetch-with-timeout.ts index 43680d5c..1711f2d2 100644 --- a/src/lib/fetch-with-timeout.ts +++ b/src/lib/fetch-with-timeout.ts @@ -1,5 +1,5 @@ /** Default cap for HTTP fetches so tabs cannot hang indefinitely on bad networks or servers. */ -export const DEFAULT_FETCH_TIMEOUT_MS = 30_000 +const DEFAULT_FETCH_TIMEOUT_MS = 30_000 /** * `fetch` with a wall-clock timeout. Honors an optional caller `signal` (abort propagates both ways). diff --git a/src/lib/follow-outbox-aggregate-relays.ts b/src/lib/follow-outbox-aggregate-relays.ts index 009632c4..51babd40 100644 --- a/src/lib/follow-outbox-aggregate-relays.ts +++ b/src/lib/follow-outbox-aggregate-relays.ts @@ -8,7 +8,7 @@ import { relayUrlsLocalsFirst } from '@/lib/relay-url-priority' import type { TRelayList } from '@/types' /** First N NIP-65 `write` (outbox) URLs per followed pubkey, follow-list order; locals first per author. */ -export const FOLLOW_OUTBOX_AGGREGATE_PER_AUTHOR = 2 +const FOLLOW_OUTBOX_AGGREGATE_PER_AUTHOR = 2 /** Plain `ws://` relays are almost always someone else's LAN; the client cannot use them for third-party reads. */ function isNonPublicWsRelayUrl(normalizedUrl: string): boolean { diff --git a/src/lib/follow-set-spell.ts b/src/lib/follow-set-spell.ts index 56c90f86..03d9cc68 100644 --- a/src/lib/follow-set-spell.ts +++ b/src/lib/follow-set-spell.ts @@ -1,7 +1,7 @@ import { tagNameEquals } from '@/lib/tag' import type { Event } from 'nostr-tools' -export const FOLLOW_SET_SPELL_PREFIX = 'followset:' as const +const FOLLOW_SET_SPELL_PREFIX = 'followset:' as const export function isFollowSetSpellId(s: string): boolean { return s.startsWith(FOLLOW_SET_SPELL_PREFIX) diff --git a/src/lib/git-republic-event.ts b/src/lib/git-republic-event.ts index 04f05c7c..87f849ea 100644 --- a/src/lib/git-republic-event.ts +++ b/src/lib/git-republic-event.ts @@ -31,20 +31,6 @@ export function getGitRepublicRepoContext(event: Event): GitRepublicRepoContext return { ownerHex: parts[1], repoId: parts[2] } } -/** Accepts hex pubkey or `npub…` for Git Republic repo owner fields in forms. */ -export function parseRepoOwnerPubkeyInput(input: string): string | null { - const t = input.trim() - if (!t) return null - if (/^[0-9a-fA-F]{64}$/.test(t)) return t.toLowerCase() - try { - const dec = nip19.decode(t) - if (dec.type === 'npub') return dec.data as string - } catch { - return null - } - return null -} - export function gitRepublicRepoWebUrl(ctx: GitRepublicRepoContext): string | null { try { const npub = nip19.npubEncode(ctx.ownerHex) diff --git a/src/lib/image-extraction.ts b/src/lib/image-extraction.ts index 1552104f..d31ad136 100644 --- a/src/lib/image-extraction.ts +++ b/src/lib/image-extraction.ts @@ -1,121 +1,3 @@ -import { Event } from 'nostr-tools' -import { TImetaInfo } from '@/types' -import { getImetaInfosFromEvent } from '@/lib/event' - -/** - * Extract and normalize all images from an event - * This includes images from: - * - imeta tags - * - content (markdown images, HTML img tags, etc.) - * - metadata (title image, etc.) - */ -export function extractAllImagesFromEvent(event: Event): TImetaInfo[] { - const images: TImetaInfo[] = [] - const seenUrls = new Set() - - // Helper function to add media if not already seen - const addMedia = (url: string, pubkey: string = event.pubkey) => { - if (!url || seenUrls.has(url)) return - - // Normalize URL - const normalizedUrl = normalizeImageUrl(url) - if (!normalizedUrl) return - - // Check if it's media (image or video) - const isVideo = isVideoUrl(normalizedUrl) - const isImage = isImageUrl(normalizedUrl) - - if (!isImage && !isVideo) return - - images.push({ - url: normalizedUrl, - pubkey, - m: isVideo ? 'video/*' : 'image/*' - }) - seenUrls.add(normalizedUrl) - } - - // 1. Extract from imeta tags - const imetaMedia = getImetaInfosFromEvent(event) - imetaMedia.forEach((item: TImetaInfo) => { - if (item.m?.startsWith('image/') || item.m?.startsWith('video/')) { - addMedia(item.url, item.pubkey) - } - }) - - // 2. Extract from content - markdown images - const markdownImageRegex = /!\[.*?\]\((.*?)\)/g - let match - while ((match = markdownImageRegex.exec(event.content)) !== null) { - addMedia(match[1]) - } - - // 3. Extract from content - HTML img tags - const htmlImgRegex = /]+src=["']([^"']+)["'][^>]*>/gi - while ((match = htmlImgRegex.exec(event.content)) !== null) { - addMedia(match[1]) - } - - // 4. Extract from content - HTML video tags - const htmlVideoRegex = /]+src=["']([^"']+)["'][^>]*>/gi - while ((match = htmlVideoRegex.exec(event.content)) !== null) { - addMedia(match[1]) - } - - // 5. Extract from content - AsciiDoc images - const asciidocImageRegex = /image::([^\s\[]+)(?:\[.*?\])?/g - while ((match = asciidocImageRegex.exec(event.content)) !== null) { - addMedia(match[1]) - } - - // 6. Extract from metadata - const imageTag = event.tags.find(tag => tag[0] === 'image' && tag[1]) - - if (imageTag?.[1]) { - addMedia(imageTag[1]) - } - - // 7. Extract from content - general URL patterns that look like media - const mediaUrlRegex = - /https?:\/\/[^\s<>"']+\.(jpg|jpeg|png|gif|webp|svg|bmp|tiff|ico|mp4|webm|ogg|avi|mov|wmv|flv|mkv|mka|3gp|3g2|ogv)(?:\?[^\s<>"']*)?/gi - while ((match = mediaUrlRegex.exec(event.content)) !== null) { - addMedia(match[0]) - } - - return images -} - -/** - * Normalize image URL - */ -function normalizeImageUrl(url: string): string | null { - if (!url) return null - - // Remove common tracking parameters - const cleanUrl = url - .replace(/[?&](utm_[^&]*)/g, '') - .replace(/[?&](fbclid|gclid|msclkid)=[^&]*/g, '') - .replace(/[?&]w=\d+/g, '') - .replace(/[?&]h=\d+/g, '') - .replace(/[?&]q=\d+/g, '') - .replace(/[?&]f=\w+/g, '') - .replace(/[?&]auto=\w+/g, '') - .replace(/[?&]format=\w+/g, '') - .replace(/[?&]fit=\w+/g, '') - .replace(/[?&]crop=\w+/g, '') - .replace(/[?&]&+/g, '&') - .replace(/[?&]$/, '') - .replace(/\?$/, '') - - // Ensure it's a valid URL - try { - new URL(cleanUrl) - return cleanUrl - } catch { - return null - } -} - /** * Check if URL is likely an image (extension or known image host). */ @@ -140,50 +22,14 @@ export function isImageUrl(url: string): boolean { 'placehold.it' ] - // Check file extension if (imageExtensions.test(url)) { return true } - // Check known image domains - try { - const urlObj = new URL(url) - return imageDomains.some(domain => - urlObj.hostname === domain || urlObj.hostname.endsWith('.' + domain) - ) - } catch { - return false - } -} - -/** - * Check if URL is likely a video - */ -function isVideoUrl(url: string): boolean { - const videoExtensions = /\.(mp4|webm|ogg|avi|mov|wmv|flv|mkv|m4v|3gp|3g2|ogv)(\?.*)?$/i - const videoDomains = [ - 'youtube.com', - 'youtu.be', - 'vimeo.com', - 'dailymotion.com', - 'twitch.tv', - 'streamable.com', - 'gfycat.com', - 'redgifs.com', - 'cdn.discordapp.com', - 'media.discordapp.net' - ] - - // Check file extension - if (videoExtensions.test(url)) { - return true - } - - // Check known video domains try { const urlObj = new URL(url) - return videoDomains.some(domain => - urlObj.hostname === domain || urlObj.hostname.endsWith('.' + domain) + return imageDomains.some( + (domain) => urlObj.hostname === domain || urlObj.hostname.endsWith('.' + domain) ) } catch { return false diff --git a/src/lib/index-relay-http.ts b/src/lib/index-relay-http.ts index 237f8bf2..bcfe7be8 100644 --- a/src/lib/index-relay-http.ts +++ b/src/lib/index-relay-http.ts @@ -16,16 +16,16 @@ function trimSlash(base: string): string { return base.replace(/\/+$/, '') } -export function indexRelayFilterUrl(baseUrl: string): string { +function indexRelayFilterUrl(baseUrl: string): string { return `${trimSlash(normalizeHttpRelayUrl(baseUrl) || baseUrl)}/api/events/filter` } -export function indexRelayPublishUrl(baseUrl: string): string { +function indexRelayPublishUrl(baseUrl: string): string { return `${trimSlash(normalizeHttpRelayUrl(baseUrl) || baseUrl)}/api/events` } /** Map a Nostr filter to gc_index_relay POST body (requires `limit` 1–100; strips unsupported keys). */ -export function nostrFilterToIndexRelayBody(f: Filter): Record { +function nostrFilterToIndexRelayBody(f: Filter): Record { const body: Record = {} const lim = f.limit const capped = lim == null || lim < 1 ? 100 : Math.min(100, lim) diff --git a/src/lib/like-reaction-emojis.ts b/src/lib/like-reaction-emojis.ts index 24e531e6..8b84b69b 100644 --- a/src/lib/like-reaction-emojis.ts +++ b/src/lib/like-reaction-emojis.ts @@ -1,8 +1,5 @@ /** - * Single source for the quick-like emoji row used by the EmojiPicker / LikeButton - * reactions row. Also re-exported as EMOJI_PICKER_REACTIONS for LikeButton. + * Single source for the quick-like emoji row used by the EmojiPicker / LikeButton. + * EmojiPicker re-exports this list as EMOJI_PICKER_REACTIONS for LikeButton. */ export const DEFAULT_SUGGESTED_EMOJIS = ['❤️', '👍', '🔥', '😂', '😢', '🫂', '🚀'] as const - -/** Emoji characters for the reactions row in the like-button picker. */ -export const EMOJI_PICKER_REACTIONS: readonly string[] = DEFAULT_SUGGESTED_EMOJIS diff --git a/src/lib/link.ts b/src/lib/link.ts index 795ba72c..4a9e05fb 100644 --- a/src/lib/link.ts +++ b/src/lib/link.ts @@ -2,7 +2,6 @@ import { Event, nip19 } from 'nostr-tools' import { getNoteBech32Id } from './event' import { TSearchParams } from '@/types' -export const toHome = () => '/' export const toNote = (eventOrId: Event | string) => { if (typeof eventOrId === 'string') return `/notes/${eventOrId}` const nevent = getNoteBech32Id(eventOrId) @@ -62,7 +61,6 @@ export const toSearch = (params?: TSearchParams) => { } return `/search?${query.toString()}` } -export const toSettings = () => '/settings' export const toRelaySettings = (tag?: 'mailbox' | 'favorite-relays') => { return '/settings/relays' + (tag ? '#' + tag : '') } @@ -83,10 +81,8 @@ export const toBookmarksList = () => '/bookmarks' export const toPinsList = () => '/pins' export const toInterestsList = () => '/interests' -export const toSpells = () => '/spells' export const toChachiChat = (relay: string, d: string) => { return `https://chachi.chat/${relay.replace(/^wss?:\/\//, '').replace(/\/$/, '')}/${d}` } -export const toNjump = (id: string) => `https://njump.me/${id}` export const toAlexandria = (id: string) => `https://next-alexandria.gitcitadel.eu/events?id=${encodeURIComponent(id)}` diff --git a/src/lib/live-activities.ts b/src/lib/live-activities.ts index fa4051db..2d50db77 100644 --- a/src/lib/live-activities.ts +++ b/src/lib/live-activities.ts @@ -25,7 +25,7 @@ const CORNYCHAT_LABEL_NAMESPACE = 'com.cornychat' const EMPTY_PARENT_MAP = new Map() /** Max extra REQ filters when resolving 30312 parents for 30313 meetings (relay limits). */ -export const LIVE_ACTIVITIES_MAX_PARENT_FETCH = 32 +const LIVE_ACTIVITIES_MAX_PARENT_FETCH = 32 export type LiveActivitiesFetchEventsFn = ( urls: string[], @@ -36,7 +36,7 @@ export type LiveActivitiesFetchEventsFn = ( /** NIP-53 live streaming (30311), meeting space (30312), meeting (30313). */ export const LIVE_ACTIVITY_KINDS = [30311, 30312, 30313] as const -export const LIVE_ACTIVITIES_MAX_ITEMS = 10 +const LIVE_ACTIVITIES_MAX_ITEMS = 10 export const LIVE_ACTIVITIES_SLIDE_INTERVAL_MS = 30_000 @@ -342,7 +342,7 @@ function isActiveLiveActivityStatus(ev: Event): boolean { } /** Parse NIP-33 address `kind:hex64pubkey:d` (used in `a` tags and dedupe keys). */ -export function parseNip33Address(ref: string): { kind: number; pubkey: string; d: string } | null { +function parseNip33Address(ref: string): { kind: number; pubkey: string; d: string } | null { const m = /^(\d+):([0-9a-f]{64}):(.+)$/i.exec(ref.trim()) if (!m) return null const kind = Number(m[1]) @@ -351,7 +351,7 @@ export function parseNip33Address(ref: string): { kind: number; pubkey: string; } /** Parent meeting space (30312) address from a 30313 event’s `a` tag, if any. */ -export function firstParent30312Address(ev: Event): string | null { +function firstParent30312Address(ev: Event): string | null { for (const t of ev.tags) { if (t[0] !== 'a' || !t[1]) continue const p = parseNip33Address(t[1]) @@ -394,7 +394,7 @@ function dedupeLatestForLiveTicker(events: Event[]): Map { } /** Latest 30312 space event per address from an event list (no network). */ -export function parent30312MapFromEvents(events: Event[]): Map { +function parent30312MapFromEvents(events: Event[]): Map { const m = new Map() for (const ev of events) { if (ev.kind !== 30312) continue diff --git a/src/lib/nip84-highlight-display.ts b/src/lib/nip84-highlight-display.ts index 97f4071e..974b6914 100644 --- a/src/lib/nip84-highlight-display.ts +++ b/src/lib/nip84-highlight-display.ts @@ -8,7 +8,7 @@ import type { Event } from 'nostr-tools' * - `["textquoteselector", prefix, suffix]` (3 items) * - `["textquoteselector", "-", prefix, suffix]` — leading "-" = empty slot (Hypothesis-style) */ -export function parseTextQuoteSelectorParts(tag: readonly string[]): { prefix: string; suffix: string } { +function parseTextQuoteSelectorParts(tag: readonly string[]): { prefix: string; suffix: string } { if (tag.length < 2 || tag[0] !== 'textquoteselector') { return { prefix: '', suffix: '' } } @@ -28,7 +28,7 @@ export function parseTextQuoteSelectorParts(tag: readonly string[]): { prefix: s } /** `["textpositionselector", start, end]` — character offsets into a full document string. */ -export function parseTextPositionSelector(tag: readonly string[]): { start: number; end: number } | null { +function parseTextPositionSelector(tag: readonly string[]): { start: number; end: number } | null { if (tag.length < 3 || tag[0] !== 'textpositionselector') return null const start = parseInt(tag[1] ?? '', 10) const end = parseInt(tag[2] ?? '', 10) diff --git a/src/lib/nostr-address.ts b/src/lib/nostr-address.ts index cb1de24a..f215238a 100644 --- a/src/lib/nostr-address.ts +++ b/src/lib/nostr-address.ts @@ -48,39 +48,3 @@ export function prefixNostrAddresses(content: string): string { return `nostr:${match}` }) } - -/** - * Checks if a string contains nostr addresses that need prefixing - * @param content - The content to check - * @returns True if the content contains unprefixed nostr addresses - */ -export function containsUnprefixedNostrAddresses(content: string): boolean { - return NOSTR_ADDRESS_REGEX.test(content) -} - -/** - * Extracts all nostr addresses from content (both prefixed and unprefixed) - * @param content - The content to extract addresses from - * @returns Array of nostr addresses found - */ -export function extractNostrAddresses(content: string): string[] { - // Reset regex state - NOSTR_ADDRESS_REGEX.lastIndex = 0 - - const addresses: string[] = [] - let match - - while ((match = NOSTR_ADDRESS_REGEX.exec(content)) !== null) { - addresses.push(match[0]) - } - - // Also check for already prefixed addresses - const prefixedRegex = /\bnostr:(npub|nprofile|note|nevent|naddr|nrelay)1[a-z0-9]+/gi - prefixedRegex.lastIndex = 0 - - while ((match = prefixedRegex.exec(content)) !== null) { - addresses.push(match[0]) - } - - return addresses -} diff --git a/src/lib/nostr-build.ts b/src/lib/nostr-build.ts index cd97eaf7..b1db3e31 100644 --- a/src/lib/nostr-build.ts +++ b/src/lib/nostr-build.ts @@ -11,17 +11,6 @@ import { isVideo } from './url' const I_NOSTR_BUILD = 'i.nostr.build' -/** Returns true when a URL is hosted on any nostr.build domain. */ -export function isNostrBuildUrl(url: string): boolean { - const u = (url ?? '').trim() - if (!u) return false - try { - return new URL(u).hostname.endsWith('nostr.build') - } catch { - return false - } -} - /** * True when we may rewrite `url` to i.nostr.build’s `/thumb/…` variant. * Only **i.nostr.build** serves generated thumbs; cdn.nostr.build does not. diff --git a/src/lib/payto.ts b/src/lib/payto.ts index 0ab92663..2b00e86f 100644 --- a/src/lib/payto.ts +++ b/src/lib/payto.ts @@ -83,7 +83,7 @@ export const PAYTO_KNOWN_TYPES: Record< * Short labels accepted after payto:// that map to a canonical type. * e.g. payto://BTC/..., payto://LBTC/..., payto://DOGE/... are recognized as bitcoin, lightning, dogecoin. */ -export const PAYTO_TYPE_ALIASES: Record = { +const PAYTO_TYPE_ALIASES: Record = { btc: 'bitcoin', lbtc: 'lightning', doge: 'dogecoin', @@ -107,7 +107,7 @@ export function getPaytoIconChar(type: string): string | null { } /** Logo filename in /payto_logos/ for types that have an asset. Any image format works: .svg, .gif, .jpg, .png, .webp, etc. */ -export const PAYTO_LOGO_FILES: Record = { +const PAYTO_LOGO_FILES: Record = { ethereum: 'ethereum-eth-logo.svg', monero: 'Monero.png', litecoin: 'Litecoin.png', @@ -138,7 +138,7 @@ export const PAYTO_LOGO_FILES: Record = { } /** Profile/page URL template for types that have a web profile. Use {authority} as placeholder. Null = no direct link. */ -export const PAYTO_PROFILE_URL_TEMPLATES: Record = { +const PAYTO_PROFILE_URL_TEMPLATES: Record = { paypal: 'https://paypal.me/{authority}', venmo: 'https://venmo.com/{authority}', revolut: 'https://revolut.me/{authority}', diff --git a/src/lib/private-relays.ts b/src/lib/private-relays.ts index d02b4069..f0a1bca8 100644 --- a/src/lib/private-relays.ts +++ b/src/lib/private-relays.ts @@ -55,21 +55,6 @@ export async function getPrivateRelayUrls(pubkey: string): Promise { return Array.from(new Set(relayUrls)) } -/** - * Check if user has cache relays set - * @param pubkey - User's public key - * @returns Promise - true if user has at least one cache relay - */ -export async function hasCacheRelays(pubkey: string): Promise { - const cacheRelayEvent = await indexedDb.getReplaceableEvent(pubkey, ExtendedKind.CACHE_RELAYS) - if (cacheRelayEvent) { - // Check if cache relay event has any relays - const hasRelays = cacheRelayEvent.tags.some(tag => tag[0] === 'relay' && tag[1]) - return hasRelays - } - return false -} - /** * Get cache relay URLs only * @param pubkey - User's public key diff --git a/src/lib/profile-accordion-fetch.ts b/src/lib/profile-accordion-fetch.ts index 4875c612..6015bfed 100644 --- a/src/lib/profile-accordion-fetch.ts +++ b/src/lib/profile-accordion-fetch.ts @@ -3,7 +3,7 @@ * profile_badges list), then separate batches for comments on notes, comments on profile (#a), and * profile reactions (#e + #a); badge NIP-58 resolution and reports run after. `onPartial` fires as * relays return events (coalesced per microtask). Session cache writes stay at completion only. - * Ordering matches {@link useProfileInteractions}. + * Ordering matches the former standalone profile-interactions hook (removed; logic lives here). */ import { ExtendedKind } from '@/constants' @@ -230,7 +230,7 @@ export async function fetchProfileAccordionBundle(args: { } // Keep phase 1 free of #a reaction/comment: many relays handle those poorly when batched with - // zaps/notes/badges. Match {@link useProfileInteractions} — dedicated REQ(s) for profile comments + // zaps/notes/badges. Same ordering as interactions hook — dedicated REQ(s) for profile comments // and reactions after we have note ids + kind-0 id. const phase1Filters: Filter[] = [ { '#p': [pubkey], kinds: [kinds.Zap], limit: 100 }, diff --git a/src/lib/profile-accordion-session-cache.ts b/src/lib/profile-accordion-session-cache.ts index 8b2eccca..e4787aad 100644 --- a/src/lib/profile-accordion-session-cache.ts +++ b/src/lib/profile-accordion-session-cache.ts @@ -129,46 +129,3 @@ export function profileAccordionSetReports( e.reportsByViewer[viewerPubkey] = reports } -export type ProfileAccordionCacheSlice = - | 'relayUrls' - | 'interactions' - | 'badges' - | 'followPacks' - | 'reports' - | 'all' - -export function profileAccordionInvalidate(pubkey: string, slice: ProfileAccordionCacheSlice = 'all'): void { - if (slice === 'all') { - store.delete(pubkey) - return - } - const e = store.get(pubkey) - if (!e) return - switch (slice) { - case 'relayUrls': - delete e.relayUrls - delete e.relayUrlsKey - delete e.interactions - delete e.interactionsRelayKey - delete e.badges - delete e.badgesRelayKey - delete e.followPacks - delete e.followPacksRelayKey - break - case 'interactions': - delete e.interactions - delete e.interactionsRelayKey - break - case 'badges': - delete e.badges - delete e.badgesRelayKey - break - case 'followPacks': - delete e.followPacks - delete e.followPacksRelayKey - break - case 'reports': - delete e.reportsByViewer - break - } -} diff --git a/src/lib/publication-rendered-events.ts b/src/lib/publication-rendered-events.ts index 7d62c9f8..bb4e5502 100644 --- a/src/lib/publication-rendered-events.ts +++ b/src/lib/publication-rendered-events.ts @@ -32,11 +32,6 @@ export function getRenderedPublicationEventsVersion(): number { return renderedVersion } -export function getRenderedPublicationEvents(publicationId: string): Event[] { - const pubId = normId(publicationId) - return [...(renderedByPublication.get(pubId)?.values() ?? [])] -} - /** * Deep collection for nested 30040 publications that were rendered in this session. */ diff --git a/src/lib/publishing-feedback.tsx b/src/lib/publishing-feedback.tsx index 59172a57..e9fa030a 100644 --- a/src/lib/publishing-feedback.tsx +++ b/src/lib/publishing-feedback.tsx @@ -8,7 +8,7 @@ export type PublishSuccessSubtleDetail = { message?: string } export const PUBLISH_SUCCESS_SUBTLE_EVENT = 'jumble:publishSuccessSubtle' -export function emitPublishSuccessSubtle(message?: string): void { +function emitPublishSuccessSubtle(message?: string): void { if (typeof window === 'undefined') return window.dispatchEvent( new CustomEvent(PUBLISH_SUCCESS_SUBTLE_EVENT, { diff --git a/src/lib/react-remove-scroll-body-cleanup.ts b/src/lib/react-remove-scroll-body-cleanup.ts index 00b5fd62..928cf8a9 100644 --- a/src/lib/react-remove-scroll-body-cleanup.ts +++ b/src/lib/react-remove-scroll-body-cleanup.ts @@ -5,7 +5,7 @@ * that class is still present, the UI can paint on top but ignore all clicks (notably after closing * our Zap dialog from a secondary pane / sheet). */ -export function stripReactRemoveScrollBodyLocks(): void { +function stripReactRemoveScrollBodyLocks(): void { if (typeof document === 'undefined') return const body = document.body const toRemove: string[] = [] @@ -18,7 +18,7 @@ export function stripReactRemoveScrollBodyLocks(): void { } /** Slightly longer than Radix dialog exit animation (`duration-200` in our `DialogContent`). */ -export const MS_AFTER_RADIX_DIALOG_FOR_EXTERNAL_MODAL = 280 +const MS_AFTER_RADIX_DIALOG_FOR_EXTERNAL_MODAL = 280 /** * Call `closeOuterModel` (e.g. close Zap `Dialog`), wait for scroll-lock cleanup when applicable, diff --git a/src/lib/recently-used-emojis.ts b/src/lib/recently-used-emojis.ts index 1bebf71c..01636689 100644 --- a/src/lib/recently-used-emojis.ts +++ b/src/lib/recently-used-emojis.ts @@ -5,7 +5,7 @@ const MAX_ENTRIES = 18 type StoredEmoji = string | { shortcode: string; url: string } -export function getRecentlyUsedEmojis(): (string | TEmoji)[] { +function getRecentlyUsedEmojis(): (string | TEmoji)[] { try { const raw = localStorage.getItem(STORAGE_KEY) if (!raw) return [] diff --git a/src/lib/relay-list-builder.ts b/src/lib/relay-list-builder.ts index 16371dc8..81bdb170 100644 --- a/src/lib/relay-list-builder.ts +++ b/src/lib/relay-list-builder.ts @@ -33,7 +33,7 @@ function dedupeNormalizedRelayUrls(urls: string[]): string[] { * Relays to bootstrap Explore replaceable fetches (e.g. kind 10012 batch) before NIP-65 resolves. * PROFILE_FETCH + FAST_READ. */ -export function exploreDiscoveryBootstrapRelayUrls(): string[] { +function exploreDiscoveryBootstrapRelayUrls(): string[] { return dedupeNormalizedRelayUrls([...PROFILE_FETCH_RELAY_URLS, ...FAST_READ_RELAY_URLS]) } @@ -428,109 +428,3 @@ export async function buildReplyReadRelayList( blockedRelays }) } - -/** - * Build relay list for writing replies/comments - * WRITE to: OP author's outboxes + OP author's inboxes + reply-to author's inboxes + user's outboxes + local relay - */ -export async function buildReplyWriteRelayList( - opAuthorPubkey: string | undefined, - replyToAuthorPubkey: string | undefined, - userPubkey: string | undefined, - blockedRelays: string[] = [] -): Promise { - const relayUrls = new Set() - const normalizedBlocked = new Set( - (blockedRelays || []).map(url => { - const normalized = normalizeUrl(url) || url - return normalized.toLowerCase() - }).filter((url): url is string => !!url) - ) - - const addRelay = (url: string | undefined) => { - if (!url) return - if (isHttpRelayUrl(url)) return - const normalized = normalizeAnyRelayUrl(url) - if (!normalized) return - // Filter blocked (case-insensitive comparison) - if (normalizedBlocked.has(normalized.toLowerCase())) return - relayUrls.add(normalized) - } - - // OP author's outboxes - if (opAuthorPubkey) { - try { - // Add timeout to prevent hanging - 2 seconds max - const relayListPromise = client.fetchRelayList(opAuthorPubkey) - const timeoutPromise = new Promise((resolve) => { - setTimeout(() => resolve(null), 2000) - }) - const opRelayList = await Promise.race([relayListPromise, timeoutPromise]) - - if (opRelayList) { - const opOutboxes = [ - ...(opRelayList.write || []).slice(0, 10) - ] - opOutboxes.forEach(addRelay) - - const opInboxes = [ - ...(opRelayList.read || []).slice(0, 10) - ] - opInboxes.forEach(addRelay) - } - } catch (error) { - logger.debug('[RelayListBuilder] Failed to fetch OP author relay list', { error }) - } - } - - // Reply-to author's inboxes - if (replyToAuthorPubkey && replyToAuthorPubkey !== opAuthorPubkey) { - try { - // Add timeout to prevent hanging - 2 seconds max - const relayListPromise = client.fetchRelayList(replyToAuthorPubkey) - const timeoutPromise = new Promise((resolve) => { - setTimeout(() => resolve(null), 2000) - }) - const replyToRelayList = await Promise.race([relayListPromise, timeoutPromise]) - - if (replyToRelayList) { - const replyToInboxes = [ - ...(replyToRelayList.read || []).slice(0, 10) - ] - replyToInboxes.forEach(addRelay) - } - } catch (error) { - logger.debug('[RelayListBuilder] Failed to fetch reply-to author relay list', { error }) - } - } - - // User's outboxes - if (userPubkey) { - try { - // Add timeout to prevent hanging - 2 seconds max - const relayListPromise = client.fetchRelayList(userPubkey) - const timeoutPromise = new Promise((resolve) => { - setTimeout(() => resolve(null), 2000) - }) - const userRelayList = await Promise.race([relayListPromise, timeoutPromise]) - - if (userRelayList) { - const userOutboxes = [ - ...(userRelayList.write || []).slice(0, 10) - ] - userOutboxes.forEach(addRelay) - } - - // User's local relay (kind 10432) - const localRelays = await getCacheRelayUrls(userPubkey) - localRelays.forEach(addRelay) - } catch (error) { - logger.debug('[RelayListBuilder] Failed to fetch user relay list', { error }) - } - } - - // Fast write relays as fallback - FAST_WRITE_RELAY_URLS.forEach(addRelay) - - return Array.from(relayUrls) -} diff --git a/src/lib/relay-pulse-nip05.ts b/src/lib/relay-pulse-nip05.ts index 5fc5cc21..7235e894 100644 --- a/src/lib/relay-pulse-nip05.ts +++ b/src/lib/relay-pulse-nip05.ts @@ -27,10 +27,3 @@ export function collectAggregatedNip05sFromKind0(event: Event): string[] { } return [...set] } - -export function truncateAbout(about: string | undefined, maxLen: number): string { - if (!about) return '' - const t = about.trim() - if (t.length <= maxLen) return t - return `${t.slice(0, maxLen)}…` -} diff --git a/src/lib/relay-url-priority.ts b/src/lib/relay-url-priority.ts index 674844cd..2d4096f2 100644 --- a/src/lib/relay-url-priority.ts +++ b/src/lib/relay-url-priority.ts @@ -164,7 +164,7 @@ export function buildPrioritizedReadRelayUrls(opts: { /** * Ordered layers for publish / write (before merge, blocked strip, kind-1 strip, cap). */ -export function buildWriteRelayPriorityLayers(opts: { +function buildWriteRelayPriorityLayers(opts: { userWriteRelays: string[] authorReadRelays?: string[] favoriteRelays?: string[] diff --git a/src/lib/relay.ts b/src/lib/relay.ts index 738f1244..50070a2d 100644 --- a/src/lib/relay.ts +++ b/src/lib/relay.ts @@ -3,7 +3,3 @@ import { TRelayInfo } from '@/types' export function checkAlgoRelay(relayInfo: TRelayInfo | undefined) { return relayInfo?.software === 'https://github.com/bitvora/algo-relay' // hardcode for now } - -export function checkSearchRelay(relayInfo: TRelayInfo | undefined) { - return relayInfo?.supported_nips?.includes(50) -} diff --git a/src/services/content-parser.service.ts b/src/services/content-parser.service.ts index 7b2688da..c1818674 100644 --- a/src/services/content-parser.service.ts +++ b/src/services/content-parser.service.ts @@ -1150,4 +1150,3 @@ class ContentParserService { // Export singleton instance export const contentParserService = new ContentParserService() -export default contentParserService diff --git a/src/services/spell.service.ts b/src/services/spell.service.ts index d5da4d40..3f04a26e 100644 --- a/src/services/spell.service.ts +++ b/src/services/spell.service.ts @@ -24,7 +24,7 @@ const RELATIVE_UNIT_SECONDS: Record = { * Resolve relative time to Unix timestamp. * "now" -> current time; "7d" -> now - 7*86400; "1704067200" -> 1704067200. */ -export function resolveRelativeTime(value: string): number { +function resolveRelativeTime(value: string): number { const trimmed = (value || '').trim() if (trimmed === 'now' || trimmed === '') { return Math.floor(Date.now() / 1000) @@ -63,7 +63,7 @@ export const SPELL_CATALOG_SYNC_LIMIT = 200 export const SPELL_CATALOG_SYNC_LIMIT_WITH_FOLLOWS = 600 /** Max distinct pubkeys in one catalog REQ (relay compatibility). Your pubkey is always first. */ -export const SPELL_CATALOG_MAX_AUTHORS = 400 +const SPELL_CATALOG_MAX_AUTHORS = 400 /** * If no relay sends EOSE, stop showing the catalog sync state and close the sub after this long. @@ -145,15 +145,6 @@ export function getRelaysForSpell( return dedupeRelayUrls(primary) } -/** Spell lists at least one relay URL in its `relays` tag. */ -export function spellHasExplicitRelays(spell: Event): boolean { - const relayTag = spell.tags.find(tagNameEquals('relays')) - if (!relayTag || relayTag.length < 2) return false - return relayTag - .slice(1) - .some((u) => typeof u === 'string' && (u.startsWith('wss://') || u.startsWith('ws://'))) -} - /** * Resolve authors: replace $me with pubkey and $contacts with contacts array. */ @@ -248,11 +239,6 @@ export function spellEventToFilter(spell: Event, ctx: SpellExecutionContext): Fi return filter } -/** Spell uses COUNT: run filter against relays and show a numeric result (not a feed). */ -export function spellIsCount(spell: Event): boolean { - return spell.tags.find(tagNameEquals('cmd'))?.[1] === 'COUNT' -} - /** * Get display name for a spell (from "name" tag or content). */