@ -27,19 +27,22 @@
@@ -27,19 +27,22 @@
let notifications = $state< NostrEvent [ ] > ([]);
let interactionsWithMe = $state< NostrEvent [ ] > ([]);
let wallComments = $state< NostrEvent [ ] > ([]); // Kind 1111 comments on the wall
let mediaEvents = $state< NostrEvent [ ] > ([]); // Kinds 20, 21, 22
let hasMedia = $state(false); // Whether kinds 20, 21, 22 are available
let loading = $state(true);
let loadingWall = $state(false);
let activeTab = $state< 'pins' | 'notifications' | 'interactions' | 'wall'>('pins');
let loadingMedia = $state(false);
let activeTab = $state< 'pins' | 'media' | 'notifications' | 'interactions' | 'wall'>('pins');
let nip05Validations = $state< Record < string , boolean | null > >({} ); // null = checking, true = valid, false = invalid
// Compute pubkey from route params
let profilePubkey = $derived.by(() => decodePubkey($page.params.pubkey));
// Initialize activeTab from URL parameter
function getTabFromUrl(): 'pins' | 'notifications' | 'interactions' | 'wall' {
function getTabFromUrl(): 'pins' | 'media' | ' notifications' | 'interactions' | 'wall' {
const tabParam = $page.url.searchParams.get('tab');
const validTabs: Array< 'pins' | 'notifications' | 'interactions' | 'wall'> = ['pins', 'notifications', 'interactions', 'wall'];
const validTabs: Array< 'pins' | 'media' | ' notifications' | 'interactions' | 'wall'> = ['pins', 'media ', 'notifications', 'interactions', 'wall'];
if (tabParam && validTabs.includes(tabParam as any)) {
return tabParam as 'pins' | 'notifications' | 'interactions' | 'wall';
return tabParam as 'pins' | 'media' | ' notifications' | 'interactions' | 'wall';
}
return 'pins'; // Default
}
@ -52,12 +55,14 @@
@@ -52,12 +55,14 @@
// Load data for the tab if needed
if (activeTab === 'wall' && profileEvent && wallComments.length === 0 && !loadingWall) {
loadWallComments(profileEvent.id);
} else if (activeTab === 'media' && profilePubkey && mediaEvents.length === 0 && !loadingMedia) {
loadMedia(profilePubkey);
}
}
});
// Function to change tab and update URL
async function setActiveTab(tab: 'pins' | 'notifications' | 'interactions' | 'wall') {
async function setActiveTab(tab: 'pins' | 'media' | ' notifications' | 'interactions' | 'wall') {
activeTab = tab;
const url = new URL($page.url);
url.searchParams.set('tab', tab);
@ -73,6 +78,11 @@
@@ -73,6 +78,11 @@
if (profileEvent && wallComments.length === 0 && !loadingWall) {
await loadWallComments(profileEvent.id);
}
} else if (tab === 'media') {
// Load media if not already loaded
if (profilePubkey && mediaEvents.length === 0 && !loadingMedia) {
await loadMedia(profilePubkey);
}
}
}
@ -163,6 +173,29 @@
@@ -163,6 +173,29 @@
async function loadProfileEvent(pubkey: string) {
if (!isMounted) return;
try {
// Check sessionStorage for a preloaded kind 0 event (from ProfileBadge click)
if (typeof window !== 'undefined') {
const preloadedEventStr = sessionStorage.getItem('aitherboard_preloadedProfileEvent');
if (preloadedEventStr) {
try {
const preloadedEvent = JSON.parse(preloadedEventStr) as NostrEvent;
// Verify the event is a kind 0 event and matches the pubkey
if (preloadedEvent.kind === KIND.METADATA && preloadedEvent.pubkey === pubkey) {
// Use the preloaded event
profileEvent = preloadedEvent;
// Clear it after reading
sessionStorage.removeItem('aitherboard_preloadedProfileEvent');
// Don't load wall comments here - load them when Wall tab is clicked
return;
}
} catch (parseError) {
// Invalid JSON in sessionStorage, continue with normal loading
console.warn('Failed to parse preloaded profile event from sessionStorage:', parseError);
sessionStorage.removeItem('aitherboard_preloadedProfileEvent');
}
}
}
// Try cache first
const cached = await getProfile(pubkey);
if (cached) {
@ -336,6 +369,70 @@
@@ -336,6 +369,70 @@
if (isMounted) pins = [];
}
}
async function checkMediaAvailability(pubkey: string) {
if (!isMounted) return;
try {
const profileRelays = relayManager.getProfileReadRelays();
// Check if any events of kinds 20, 21, 22 exist for this pubkey
const mediaCheck = await nostrClient.fetchEvents(
[{ kinds : [ KIND . PICTURE_NOTE , KIND . VIDEO_NOTE , KIND . SHORT_VIDEO_NOTE ], authors : [ pubkey ], limit : 1 } ],
profileRelays,
{ useCache : 'cache-first' , cacheResults : true , timeout : config.shortTimeout }
);
if (!isMounted) return;
hasMedia = mediaCheck.length > 0;
} catch (error) {
// Failed to check - assume no media
if (isMounted) hasMedia = false;
}
}
async function loadMedia(pubkey: string) {
if (!isMounted) return;
loadingMedia = true;
try {
const profileRelays = relayManager.getFeedReadRelays();
const fetchPromise = nostrClient.fetchEvents(
[{ kinds : [ KIND . PICTURE_NOTE , KIND . VIDEO_NOTE , KIND . SHORT_VIDEO_NOTE ], authors : [ pubkey ], limit : config.feedLimit } ],
profileRelays,
{
useCache: 'cache-first',
cacheResults: true,
timeout: config.shortTimeout,
onUpdate: (newMedia) => {
if (!isMounted) return;
// Merge with existing media
const mediaMap = new Map(mediaEvents.map(m => [m.id, m]));
for (const media of newMedia) {
mediaMap.set(media.id, media);
}
mediaEvents = Array.from(mediaMap.values()).sort((a, b) => b.created_at - a.created_at);
}
}
);
activeFetchPromises.add(fetchPromise);
const fetchedMedia = await fetchPromise;
activeFetchPromises.delete(fetchPromise);
if (!isMounted) return;
// Sort by created_at descending
mediaEvents = fetchedMedia.sort((a, b) => b.created_at - a.created_at);
hasMedia = mediaEvents.length > 0;
} catch (error) {
// Failed to load media
if (isMounted) {
mediaEvents = [];
hasMedia = false;
}
} finally {
if (isMounted) {
loadingMedia = false;
}
}
}
async function loadNotifications(pubkey: string) {
if (!isMounted) return;
@ -713,6 +810,11 @@
@@ -713,6 +810,11 @@
// Failed to load pins - non-critical
}));
// Check if media (kinds 20, 21, 22) are available
loadPromises.push(checkMediaAvailability(pubkey).catch(() => {
// Failed to check media - non-critical
}));
// Load notifications or interactions based on profile type
if (isOwnProfile) {
loadPromises.push(loadNotifications(pubkey).catch(() => {
@ -783,6 +885,8 @@
@@ -783,6 +885,8 @@
interactionsWithMe = [];
wallComments = [];
pins = [];
mediaEvents = [];
hasMedia = false;
await loadProfile();
}
export { refresh } ;
@ -877,6 +981,14 @@
@@ -877,6 +981,14 @@
>
Pins ({ pins . length } )
< / button >
{ #if hasMedia }
< button
onclick={() => setActiveTab ( 'media' )}
class="px-2 sm:px-4 py-2 font-semibold whitespace-nowrap flex-shrink-0 { activeTab === 'media' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : '' } "
>
Media ({ mediaEvents . length } )
< / button >
{ /if }
< button
onclick={() => setActiveTab ( 'wall' )}
class="px-2 sm:px-4 py-2 font-semibold whitespace-nowrap flex-shrink-0 { activeTab === 'wall' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : '' } "
@ -910,6 +1022,18 @@
@@ -910,6 +1022,18 @@
{ /each }
< / div >
{ /if }
{ :else if activeTab === 'media' }
{ #if loadingMedia }
< p class = "text-fog-text-light dark:text-fog-dark-text-light" > Loading media...< / p >
{ :else if mediaEvents . length === 0 }
< p class = "text-fog-text-light dark:text-fog-dark-text-light" > No media posts yet.< / p >
{ : else }
< div class = "media-list" >
{ #each mediaEvents as media ( media . id )}
< FeedPost post = { media } / >
{ /each }
< / div >
{ /if }
{ :else if activeTab === 'notifications' }
{ #if notifications . length === 0 }
< p class = "text-fog-text-light dark:text-fog-dark-text-light" > No notifications yet.< / p >