@ -13,9 +13,8 @@
@@ -13,9 +13,8 @@
import { userStore } from '$lib/stores/user-store.js';
import { fetchUserProfile , extractProfileData } from '$lib/utils/user-profile.js';
import { combineRelays } from '$lib/config.js';
import { KIND , isEphemeralKind , isReplaceableKind , isAddressableKind } from '$lib/types/nostr.js';
import { KIND , isEphemeralKind , isReplaceableKind } from '$lib/types/nostr.js';
import { hasUnlimitedAccess } from '$lib/utils/user-access.js';
import { eventCache } from '$lib/services/nostr/event-cache.js';
const npub = ($page.params as { npub? : string } ).npub || '';
@ -61,48 +60,6 @@
@@ -61,48 +60,6 @@
return quotedEvents.find(e => e.id === eventId);
};
// Helper to get author name from pubkey
function getAuthorName(pubkey: string): string {
// Try to find profile event in nostrLinkEvents cache
// Check both by profile key and by iterating values
const profileByKey = nostrLinkEvents.get(`profile:${ pubkey } `);
let profileEvent = profileByKey || Array.from(nostrLinkEvents.values()).find(
e => e.kind === 0 & & e.pubkey === pubkey
);
// If not found in nostrLinkEvents, try global eventCache
if (!profileEvent) {
try {
const cachedProfile = eventCache.getProfile(pubkey);
if (cachedProfile) {
profileEvent = cachedProfile;
}
} catch {
// eventCache not available, continue
}
}
if (profileEvent) {
try {
const profile = JSON.parse(profileEvent.content);
const name = profile.display_name || profile.name;
if (name & & name.trim()) return name.trim();
} catch {
// Try tags if JSON parse fails
const nameTag = profileEvent.tags.find(t => t[0] === 'name' || t[0] === 'display_name')?.[1];
if (nameTag & & nameTag.trim()) return nameTag.trim();
}
}
// Fallback to shortened pubkey
try {
const npub = nip19.npubEncode(pubkey);
return npub.slice(0, 12) + '...';
} catch {
return pubkey.slice(0, 8) + '...';
}
}
// Referenced events cache for activity (a-tags and e-tags) - use array for better reactivity
let referencedEvents = $state< NostrEvent [ ] > ([]);
@ -206,7 +163,6 @@
@@ -206,7 +163,6 @@
}
// Fetch events
const loadedEvents: NostrEvent[] = [];
if (eventIds.length > 0) {
try {
const events = await Promise.race([
@ -216,7 +172,6 @@
@@ -216,7 +172,6 @@
for (const event of events) {
nostrLinkEvents.set(event.id, event);
loadedEvents.push(event);
}
} catch {
// Ignore fetch errors
@ -239,7 +194,6 @@
@@ -239,7 +194,6 @@
if (events.length > 0) {
nostrLinkEvents.set(events[0].id, events[0]);
loadedEvents.push(events[0]);
}
} catch {
// Ignore fetch errors
@ -247,38 +201,6 @@
@@ -247,38 +201,6 @@
}
}
}
// Load profiles for authors of loaded events
if (loadedEvents.length > 0) {
const authorPubkeys = new Set< string > ();
for (const event of loadedEvents) {
if (event.pubkey) {
authorPubkeys.add(event.pubkey);
}
}
// Fetch profiles for all authors
if (authorPubkeys.size > 0) {
try {
const profiles = await Promise.race([
nostrClient.fetchEvents([{ kinds : [ 0 ], authors : Array.from ( authorPubkeys ), limit : authorPubkeys.size } ]),
new Promise< NostrEvent [ ] > ((resolve) => setTimeout(() => resolve([]), 10000))
]);
// Store profiles in cache (use a special key format: `profile:${ pubkey } `)
for (const profile of profiles) {
// Store with a key that includes the pubkey so we can find it
nostrLinkEvents.set(`profile:${ profile . pubkey } `, profile);
// Also store by ID if it has one
if (profile.id) {
nostrLinkEvents.set(profile.id, profile);
}
}
} catch {
// Ignore profile fetch errors
}
}
}
}
// Get event from nostr: link
@ -633,19 +555,6 @@ i *
@@ -633,19 +555,6 @@ i *
return true;
}
// Exclude addressable events (30000-39999) that are not repo-related
// Only allow known repo-related addressable events: REPO_ANNOUNCEMENT, REPO_STATE, BRANCH_PROTECTION
if (isAddressableKind(event.kind)) {
const allowedAddressableKinds: number[] = [
KIND.REPO_ANNOUNCEMENT,
KIND.REPO_STATE,
KIND.BRANCH_PROTECTION
];
if (!allowedAddressableKinds.includes(event.kind)) {
return true;
}
}
// Exclude specific regular kinds that are not repo-related:
// Kind 1: Keep this one in, just for the user's convenience
@ -957,99 +866,41 @@ i *
@@ -957,99 +866,41 @@ i *
return date.toLocaleDateString();
}
// Helper function to check if an event is repo-related
function isRepoRelatedEvent(event: NostrEvent): boolean {
const repoRelatedKinds: number[] = [
KIND.REPO_ANNOUNCEMENT,
KIND.REPO_STATE,
KIND.PATCH,
KIND.PULL_REQUEST,
KIND.PULL_REQUEST_UPDATE,
KIND.ISSUE,
KIND.STATUS_OPEN,
KIND.STATUS_APPLIED,
KIND.STATUS_CLOSED,
KIND.STATUS_DRAFT,
KIND.COMMIT_SIGNATURE,
KIND.OWNERSHIP_TRANSFER,
KIND.RELEASE,
KIND.COMMENT,
KIND.THREAD,
KIND.BRANCH_PROTECTION,
KIND.HIGHLIGHT
];
return repoRelatedKinds.includes(event.kind);
}
// Helper function to check if an event references a repo via a-tag
function eventReferencesRepo(event: NostrEvent, repoATags: Set< string > ): boolean {
const aTags = event.tags.filter(t => t[0] === 'a' & & t[1]);
for (const aTag of aTags) {
if (aTag[1] && repoATags.has(aTag[1])) {
return true;
}
}
return false;
}
// Helper function to check if an event has p-tag or q-tag referencing the user
function eventReferencesUser(event: NostrEvent, userPubkey: string): boolean {
// Check p-tags
const pTags = event.tags.filter(t => t[0] === 'p' & & t[1]);
for (const pTag of pTags) {
if (pTag[1] && pTag[1].toLowerCase() === userPubkey.toLowerCase()) {
return true;
}
}
// Check q-tags
const qTags = event.tags.filter(t => t[0] === 'q' & & t[1]);
for (const qTag of qTags) {
if (qTag[1] && qTag[1].toLowerCase() === userPubkey.toLowerCase()) {
return true;
}
}
return false;
}
async function loadActivity() {
if (!profileOwnerPubkeyHex || loadingActivity || activityLoaded) return;
const userPubkey = profileOwnerPubkeyHex; // Store in local variable for type safety
loadingActivity = true;
try {
// Step 1: Fetch all repo announcements where user is owner or maintainer
// Step 1: Fetch repo announcements in parallel (reduced limit)
const [repoAnnouncements, allAnnouncements] = await Promise.all([
nostrClient.fetchEvents([
{
kinds: [KIND.REPO_ANNOUNCEMENT],
authors: [userPubkey],
limit: 100
limit: 50 // Reduced from 100
}
]),
nostrClient.fetchEvents([
{
kinds: [KIND.REPO_ANNOUNCEMENT],
'#p': [userPubkey],
limit: 100
limit: 50 // Reduced from 100
}
])
]);
// Step 2: Extract a-tags from repos where user is owner or maintainer
const repoATags = new Set< string > ();
// Add a-tags from repos owned by user
// Step 2: Extract a-tags from repo announcements
const aTags = new Set< string > ();
for (const announcement of repoAnnouncements) {
const dTag = announcement.tags.find(t => t[0] === 'd')?.[1];
if (dTag) {
const aTag = `${ KIND . REPO_ANNOUNCEMENT } :${ announcement . pubkey } :${ dTag } `;
repoA Tags.add(aTag);
aTags.add(aTag);
}
}
// Add a-tags from repos where user is a maintainer
// Step 3: Check for repos where user is a maintainer
for (const announcement of allAnnouncements) {
const maintainersTag = announcement.tags.find(t => t[0] === 'maintainers');
if (maintainersTag) {
@ -1069,95 +920,43 @@ i *
@@ -1069,95 +920,43 @@ i *
const dTag = announcement.tags.find(t => t[0] === 'd')?.[1];
if (dTag) {
const aTag = `${ KIND . REPO_ANNOUNCEMENT } :${ announcement . pubkey } :${ dTag } `;
repoA Tags.add(aTag);
a Tags.add(aTag);
}
}
}
}
// If user has no repos, return empty activity
if (repoATags.size === 0) {
activityEvents = [];
activityLoaded = true;
loadingActivity = false;
return;
}
// Step 3: Define repo-related event kinds
const repoRelatedKinds = [
KIND.REPO_ANNOUNCEMENT,
KIND.REPO_STATE,
KIND.PATCH,
KIND.PULL_REQUEST,
KIND.PULL_REQUEST_UPDATE,
KIND.ISSUE,
KIND.STATUS_OPEN,
KIND.STATUS_APPLIED,
KIND.STATUS_CLOSED,
KIND.STATUS_DRAFT,
KIND.COMMIT_SIGNATURE,
KIND.OWNERSHIP_TRANSFER,
KIND.RELEASE,
KIND.COMMENT,
KIND.THREAD,
KIND.BRANCH_PROTECTION,
KIND.HIGHLIGHT
];
// Step 4: Fetch events that:
// - Have p-tags or q-tags referencing the user
// - AND reference repos where user is owner/maintainer (via a-tags)
// - AND are repo-related kinds
// Step 4: Fetch events that reference the user or their repos (reduced limits)
const filters: any[] = [];
// Events with user in p-tag AND repo a-tags AND repo-related kinds
// Events with user in p-tag
filters.push({
'#p': [userPubkey],
'#a': Array.from(repoATags),
kinds: repoRelatedKinds,
limit: 200
limit: 100 // Reduced from 200
});
// Events with user in q-tag AND repo a-tags AND repo-related kinds
// Events with user in q-tag
filters.push({
'#q': [userPubkey],
'#a': Array.from(repoATags),
kinds: repoRelatedKinds,
limit: 200
limit: 100 // Reduced from 200
});
// Events with repo a-tags
if (aTags.size > 0) {
filters.push({
'#a': Array.from(aTags),
limit: 100 // Reduced from 200
});
}
const allActivityEvents = await Promise.race([
nostrClient.fetchEvents(filters),
new Promise< NostrEvent [ ] > ((resolve) => setTimeout(() => resolve([]), 15000)) // 15s timeout
]);
// Step 5: Additional filtering to ensure events:
// - Reference the user via p-tag or q-tag
// - Reference a repo the user owns/maintains via a-tag
// - Are repo-related kinds
// Step 5: Deduplicate, filter, and sort by created_at (newest first)
const eventMap = new Map< string , NostrEvent > ();
for (const event of allActivityEvents) {
// Skip user's own events
if (event.pubkey === userPubkey) {
continue;
}
// Must be repo-related
if (!isRepoRelatedEvent(event)) {
continue;
}
// Must reference the user via p-tag or q-tag
if (!eventReferencesUser(event, userPubkey)) {
continue;
}
// Must reference a repo the user owns/maintains
if (!eventReferencesRepo(event, repoATags)) {
continue;
}
// Apply standard exclusions
if (shouldExcludeEvent(event, userPubkey, true)) {
continue;
}
@ -1168,7 +967,7 @@ i *
@@ -1168,7 +967,7 @@ i *
}
}
// Sort by created_at descending and limit to 50
// Sort by created_at descending and limit to 50 (reduced from 200)
activityEvents = Array.from(eventMap.values())
.sort((a, b) => b.created_at - a.created_at)
.slice(0, 50);
@ -2126,8 +1925,7 @@ i *
@@ -2126,8 +1925,7 @@ i *
{ #if quotedEvent }
< div class = "quoted-event" >
< div class = "quoted-event-header" >
< span class = "quoted-event-label" > Quoting:< / span >
< UserBadge pubkey = { quotedEvent . pubkey } disableLink= { true } inline = { true } / >
< UserBadge pubkey = { quotedEvent . pubkey } disableLink= { true } />
< span class = "quoted-event-time" > { formatMessageTime ( quotedEvent . created_at )} </ span >
< / div >
< div class = "quoted-event-content" > { quotedEvent . content || '(No content)' } </ div >
@ -2186,7 +1984,7 @@ i *
@@ -2186,7 +1984,7 @@ i *
{ :else if part . type === 'event' && part . event }
< div class = "nostr-link-event" >
< div class = "nostr-link-event-header" >
< span class = "nostr-link-event-author" > { getAuthorName ( part . event . pubkey )} </ span >
< UserBadge pubkey = { part . event . pubkey } disableLink= { true } / >
< span class = "nostr-link-event-time" > { formatMessageTime ( part . event . created_at )} </ span >
< / div >
< div class = "nostr-link-event-content" > { part . event . content || getEventContext ( part . event )} </ div >
@ -2241,22 +2039,13 @@ i *
@@ -2241,22 +2039,13 @@ i *
< div class = "zap-amount" > { zapData . amount } </ div >
< div class = "zap-details" >
{ #if zapData . senderPubkey }
< span class = "zap-detail-item" >
< span class = "zap-detail-label" > From< / span >
< span class = "zap-detail-value" > { getAuthorName ( zapData . senderPubkey )} </ span >
< / span >
< span > From < UserBadge pubkey = { zapData . senderPubkey } disableLink= { true } /></ span >
{ /if }
{ #if zapData . recipientPubkey && zapData . recipientPubkey !== profileOwnerPubkeyHex }
< span class = "zap-detail-item" >
< span class = "zap-detail-label" > To< / span >
< span class = "zap-detail-value" > { getAuthorName ( zapData . recipientPubkey )} </ span >
< / span >
< span > To < UserBadge pubkey = { zapData . recipientPubkey } disableLink= { true } /></ span >
{ /if }
{ #if zapData . eventId }
< span class = "zap-detail-item" >
< span class = "zap-detail-label" > on event< / span >
< span class = "zap-detail-value" > { zapData . eventId . slice ( 0 , 8 )} ...</ span >
< / span >
< span > on event { zapData . eventId . slice ( 0 , 8 )} ...</ span >
{ /if }
{ #if zapData . comment }
< div class = "zap-comment" > { zapData . comment } </ div >
@ -2289,7 +2078,7 @@ i *
@@ -2289,7 +2078,7 @@ i *
{ :else if part . type === 'event' && part . event }
< div class = "nostr-link-event" >
< div class = "nostr-link-event-header" >
< span class = "nostr-link-event-author" > { getAuthorName ( part . event . pubkey )} </ span >
< UserBadge pubkey = { part . event . pubkey } disableLink= { true } / >
< span class = "nostr-link-event-time" > { formatMessageTime ( part . event . created_at )} </ span >
< / div >
< div class = "nostr-link-event-content" > { part . event . content || getEventContext ( part . event )} </ div >
@ -2364,7 +2153,7 @@ i *
@@ -2364,7 +2153,7 @@ i *
{ :else if part . type === 'event' && part . event }
< div class = "nostr-link-event" >
< div class = "nostr-link-event-header" >
< span class = "nostr-link-event-author" > { getAuthorName ( part . event . pubkey )} </ span >
< UserBadge pubkey = { part . event . pubkey } disableLink= { true } / >
< span class = "nostr-link-event-time" > { formatMessageTime ( part . event . created_at )} </ span >
< / div >
< div class = "nostr-link-event-content" > { part . event . content || getEventContext ( part . event )} </ div >
@ -3249,33 +3038,17 @@ i *
@@ -3249,33 +3038,17 @@ i *
.message-icon {
width: 1.25rem;
height: 1.25rem;
filter: var(--icon-filter, brightness(0) saturate(100%) invert(1));
opacity: 0.8;
}
:global([data-theme="light"]) .message-icon {
filter: brightness(0) saturate(100%);
opacity: 0.7;
}
:global([data-theme="dark"]) .message-icon,
:global([data-theme="black"]) .message-icon {
filter: brightness(0) saturate(100%) invert(1);
opacity: 0.8;
}
.message-action-button:hover .message-icon {
opacity: 1;
filter: var(--icon-filter, none);
}
.quoted-event {
margin-bottom: 0.75 rem;
padding: 0.5rem;
background: var(--bg-secondary, var(--bg-primary) );
color: var(--text-muted, var(--text-secondary) );
border-radius: 4px ;
border-left: 2px solid var(--border-light, var(--border-color)) ;
opacity: 0.8 ;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-left: 3px solid var(--accent, #007bff);
border-radius: 0.375rem;
font-size: 0.875rem;
}
:global([data-theme="light"]) .quoted-event {
@ -3296,35 +3069,50 @@ i *
@@ -3296,35 +3069,50 @@ i *
.quoted-event-header {
display: flex;
align-items: center;
gap: 0.375rem;
margin-bottom: 0.25rem;
font-size: 0.75rem;
color: var(--text-muted, var(--text-secondary));
}
.quoted-event-label {
font-weight: 500;
color: var(--text-muted, var(--text-secondary));
gap: 0.5rem;
margin-bottom: 0.5rem;
flex-wrap: wrap;
}
.quoted-event-time {
color: var(--text-muted, var(--text-secondary)) ;
font-size: 0.7rem ;
font-size: 0.75rem;
color: var(--text-muted);
margin-left: auto;
}
.quoted-event-content {
font-size: 0.8rem;
color: var(--text-muted, var(--text-secondary));
line-height: 1.4;
color: var(--text-secondary);
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
line-height: 1.5;
max-height: 8rem;
overflow: hidden;
position: relative;
}
.quoted-event-content::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2rem;
background: linear-gradient(to bottom, transparent, var(--bg-secondary));
pointer-events: none;
}
:global([data-theme="light"]) .quoted-event-content::after {
background: linear-gradient(to bottom, transparent, #f5f5f5);
}
:global([data-theme="dark"]) .quoted-event-content::after {
background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.05));
}
:global([data-theme="black"]) .quoted-event-content::after {
background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.03));
}
.quoted-event-loading {
opacity: 0.6;
@ -3543,39 +3331,27 @@ i *
@@ -3543,39 +3331,27 @@ i *
.zap-details {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
font-size: 0.7rem;
color: var(--text-muted, var(--text-secondary));
line-height: 1.4;
margin-top: 0.25rem;
}
.zap-detail-item {
display: inline-flex;
align-items: center;
flex-direction: column;
gap: 0.25rem;
font-size: 0.875rem;
color: var(--text-primary);
line-height: 1.5;
}
.zap-detail-label {
color: var(--text-muted, var(--text-secondary));
font-weight: 500;
}
.zap-detail-value {
color: var(--text-muted, var(--text-secondary));
.zap-details span {
display: flex;
align-items: center;
gap: 0.5rem;
}
.zap-comment {
margin-top: 0.37 5rem;
padding: 0.3 75rem 0. 5rem;
background: var(--bg-secondary, var(--bg-primary) );
border-radius: 4px ;
border-left: 2px solid var(--border-light, var(--border-color) );
margin-top: 0.5rem;
padding: 0.75rem;
background: var(--bg-secondary);
border-radius: 0.5rem ;
border-left: 2px solid var(--accent );
font-style: italic;
font-size: 0.75rem;
color: var(--text-muted, var(--text-secondary));
opacity: 0.8;
color: var(--text-secondary);
}
.zap-receipt {
@ -3733,20 +3509,11 @@ i *
@@ -3733,20 +3509,11 @@ i *
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
line-height: 1;
font-size: 0.75rem;
color: var(--text-muted, var(--text-secondary));
}
.nostr-link-event-author {
font-weight: 500;
color: var(--text-muted, var(--text-secondary));
}
.nostr-link-event-time {
font-size: 0.7rem;
color: var(--text-muted, var(--text-secondary));
margin-left: auto;
font-size: 0.75rem;
color: var(--text-muted);
}
.nostr-link-event-content {