|
|
|
|
@ -8,10 +8,15 @@
@@ -8,10 +8,15 @@
|
|
|
|
|
export let showOnlyMyEvents = false; |
|
|
|
|
export let showFilterBuilder = false; |
|
|
|
|
|
|
|
|
|
import { createEventDispatcher } from "svelte"; |
|
|
|
|
import { createEventDispatcher, onMount } from "svelte"; |
|
|
|
|
import FilterBuilder from "./FilterBuilder.svelte"; |
|
|
|
|
import { fetchUserProfile } from "./nostr.js"; |
|
|
|
|
import { getKindName, truncatePubkey } from "./helpers.tsx"; |
|
|
|
|
const dispatch = createEventDispatcher(); |
|
|
|
|
|
|
|
|
|
// Profile cache to avoid fetching the same profile multiple times |
|
|
|
|
let profileCache = new Map(); |
|
|
|
|
|
|
|
|
|
// Local state for JSON editor toggle |
|
|
|
|
let showJsonEditor = false; |
|
|
|
|
|
|
|
|
|
@ -55,59 +60,43 @@
@@ -55,59 +60,43 @@
|
|
|
|
|
dispatch("filterClear"); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function truncatePubkey(pubkey) { |
|
|
|
|
if (!pubkey) return ""; |
|
|
|
|
return pubkey.slice(0, 8) + "..." + pubkey.slice(-8); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function getKindName(kind) { |
|
|
|
|
const kindNames = { |
|
|
|
|
0: "Profile", |
|
|
|
|
1: "Text Note", |
|
|
|
|
2: "Recommend Relay", |
|
|
|
|
3: "Contacts", |
|
|
|
|
4: "Encrypted DM", |
|
|
|
|
5: "Delete", |
|
|
|
|
6: "Repost", |
|
|
|
|
7: "Reaction", |
|
|
|
|
8: "Badge Award", |
|
|
|
|
16: "Generic Repost", |
|
|
|
|
40: "Channel Creation", |
|
|
|
|
41: "Channel Metadata", |
|
|
|
|
42: "Channel Message", |
|
|
|
|
43: "Channel Hide Message", |
|
|
|
|
44: "Channel Mute User", |
|
|
|
|
1984: "Reporting", |
|
|
|
|
9734: "Zap Request", |
|
|
|
|
9735: "Zap", |
|
|
|
|
10000: "Mute List", |
|
|
|
|
10001: "Pin List", |
|
|
|
|
10002: "Relay List", |
|
|
|
|
22242: "Client Auth", |
|
|
|
|
24133: "Nostr Connect", |
|
|
|
|
27235: "HTTP Auth", |
|
|
|
|
30000: "Categorized People", |
|
|
|
|
30001: "Categorized Bookmarks", |
|
|
|
|
30008: "Profile Badges", |
|
|
|
|
30009: "Badge Definition", |
|
|
|
|
30017: "Create or update a stall", |
|
|
|
|
30018: "Create or update a product", |
|
|
|
|
30023: "Long-form Content", |
|
|
|
|
30024: "Draft Long-form Content", |
|
|
|
|
30078: "Application-specific Data", |
|
|
|
|
30311: "Live Event", |
|
|
|
|
30315: "User Statuses", |
|
|
|
|
30402: "Classified Listing", |
|
|
|
|
30403: "Draft Classified Listing", |
|
|
|
|
31922: "Date-Based Calendar Event", |
|
|
|
|
31923: "Time-Based Calendar Event", |
|
|
|
|
31924: "Calendar", |
|
|
|
|
31925: "Calendar Event RSVP", |
|
|
|
|
31989: "Handler recommendation", |
|
|
|
|
31990: "Handler information", |
|
|
|
|
34550: "Community Definition", |
|
|
|
|
}; |
|
|
|
|
return kindNames[kind] || `Kind ${kind}`; |
|
|
|
|
// Fetch profile for a pubkey (with caching) |
|
|
|
|
async function getProfile(pubkey) { |
|
|
|
|
if (!pubkey) return null; |
|
|
|
|
|
|
|
|
|
// Check cache first |
|
|
|
|
if (profileCache.has(pubkey)) { |
|
|
|
|
return profileCache.get(pubkey); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Fetch profile |
|
|
|
|
try { |
|
|
|
|
const profile = await fetchUserProfile(pubkey); |
|
|
|
|
profileCache.set(pubkey, profile); |
|
|
|
|
return profile; |
|
|
|
|
} catch (error) { |
|
|
|
|
console.warn("Failed to fetch profile for", pubkey, error); |
|
|
|
|
// Cache null to avoid repeated failed fetches |
|
|
|
|
profileCache.set(pubkey, null); |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Load profiles for all unique pubkeys in filteredEvents |
|
|
|
|
$: if (filteredEvents.length > 0) { |
|
|
|
|
const uniquePubkeys = new Set( |
|
|
|
|
filteredEvents |
|
|
|
|
.map(e => e.pubkey) |
|
|
|
|
.filter(p => p && !profileCache.has(p)) |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// Fetch profiles for new pubkeys (fire-and-forget, errors are handled in getProfile) |
|
|
|
|
uniquePubkeys.forEach(pubkey => { |
|
|
|
|
getProfile(pubkey).catch(err => { |
|
|
|
|
// Error already logged in getProfile, just prevent unhandled rejection |
|
|
|
|
console.debug("Profile fetch failed (already handled):", pubkey); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function formatTimestamp(timestamp) { |
|
|
|
|
@ -125,6 +114,7 @@
@@ -125,6 +114,7 @@
|
|
|
|
|
<div class="events-view-content" on:scroll={handleScroll}> |
|
|
|
|
{#if filteredEvents.length > 0} |
|
|
|
|
{#each filteredEvents as event} |
|
|
|
|
{@const profile = profileCache.get(event.pubkey)} |
|
|
|
|
<div |
|
|
|
|
class="events-view-item" |
|
|
|
|
class:expanded={expandedEvents.has(event.id)} |
|
|
|
|
@ -139,11 +129,27 @@
@@ -139,11 +129,27 @@
|
|
|
|
|
tabindex="0" |
|
|
|
|
> |
|
|
|
|
<div class="events-view-avatar"> |
|
|
|
|
{#if profile?.picture} |
|
|
|
|
<img |
|
|
|
|
src={profile.picture} |
|
|
|
|
alt={profile.name || truncatePubkey(event.pubkey)} |
|
|
|
|
class="avatar-image" |
|
|
|
|
/> |
|
|
|
|
{:else} |
|
|
|
|
<div class="avatar-placeholder">👤</div> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
<div class="events-view-info"> |
|
|
|
|
<div class="events-view-author"> |
|
|
|
|
{truncatePubkey(event.pubkey)} |
|
|
|
|
{#if profile} |
|
|
|
|
<span class="author-name">{profile.name || truncatePubkey(event.pubkey)}</span> |
|
|
|
|
{#if profile.nip05} |
|
|
|
|
<span class="author-nip05" title={profile.nip05}>@{profile.nip05}</span> |
|
|
|
|
{/if} |
|
|
|
|
<span class="author-pubkey" title={event.pubkey}>{truncatePubkey(event.pubkey)}</span> |
|
|
|
|
{:else} |
|
|
|
|
<span class="author-pubkey">{truncatePubkey(event.pubkey)}</span> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
<div class="events-view-kind"> |
|
|
|
|
<span |
|
|
|
|
@ -387,6 +393,14 @@
@@ -387,6 +393,14 @@
|
|
|
|
|
border: 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.avatar-image { |
|
|
|
|
width: 40px; |
|
|
|
|
height: 40px; |
|
|
|
|
border-radius: 50%; |
|
|
|
|
object-fit: cover; |
|
|
|
|
border: 1px solid var(--border-color); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.events-view-info { |
|
|
|
|
flex-shrink: 0; |
|
|
|
|
min-width: 120px; |
|
|
|
|
@ -396,6 +410,27 @@
@@ -396,6 +410,27 @@
|
|
|
|
|
font-weight: 600; |
|
|
|
|
color: var(--text-color); |
|
|
|
|
font-size: 0.9em; |
|
|
|
|
display: flex; |
|
|
|
|
flex-direction: column; |
|
|
|
|
gap: 0.2em; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.author-name { |
|
|
|
|
font-weight: 600; |
|
|
|
|
color: var(--text-color); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.author-nip05 { |
|
|
|
|
font-size: 0.85em; |
|
|
|
|
color: var(--text-color); |
|
|
|
|
opacity: 0.8; |
|
|
|
|
font-style: italic; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.author-pubkey { |
|
|
|
|
font-size: 0.75em; |
|
|
|
|
color: var(--text-color); |
|
|
|
|
opacity: 0.6; |
|
|
|
|
font-family: monospace; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|