You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

201 lines
5.9 KiB

<script lang="ts">
import { getActivityStatus, getActivityMessage } from '../../services/auth/activity-tracker.js';
import { fetchProfile, fetchUserStatus } from '../../services/user-data.js';
import { nip19 } from 'nostr-tools';
interface Props {
pubkey: string;
inline?: boolean; // If true, show only handle/name (no picture, status, etc.)
}
let { pubkey, inline = false }: Props = $props();
let profile = $state<{ name?: string; picture?: string } | null>(null);
let status = $state<string | null>(null);
let activityStatus = $state<'red' | 'yellow' | 'green' | null>(null);
let activityMessage = $state<string | null>(null);
let imageError = $state(false);
let loadingProfile = $state(false);
let loadingStatus = $state(false);
let loadingActivity = $state(false);
let lastLoadedPubkey = $state<string | null>(null);
$effect(() => {
// Only load if pubkey changed and we haven't loaded it yet
if (pubkey && pubkey !== lastLoadedPubkey) {
imageError = false; // Reset image error when pubkey changes
// Reset state for new pubkey
profile = null;
status = null;
activityStatus = null;
activityMessage = null;
// Load immediately - no debounce
loadProfile();
// Only load status and activity if not inline
if (!inline) {
loadStatus();
updateActivityStatus();
}
}
});
async function loadProfile() {
const currentPubkey = pubkey;
if (!currentPubkey || loadingProfile || lastLoadedPubkey === currentPubkey) {
return; // Already loading or already loaded this pubkey
}
loadingProfile = true;
try {
const p = await fetchProfile(currentPubkey);
// Only update if pubkey hasn't changed during load
if (pubkey === currentPubkey) {
if (p) {
profile = p;
}
lastLoadedPubkey = currentPubkey;
}
} finally {
// Only clear loading if this is still the current pubkey
if (pubkey === currentPubkey) {
loadingProfile = false;
}
}
}
async function loadStatus() {
const currentPubkey = pubkey;
if (!currentPubkey || loadingStatus || lastLoadedPubkey !== currentPubkey) return;
loadingStatus = true;
try {
const s = await fetchUserStatus(currentPubkey);
// Only update if pubkey hasn't changed during load
if (pubkey === currentPubkey && lastLoadedPubkey === currentPubkey) {
status = s;
}
} finally {
if (pubkey === currentPubkey) {
loadingStatus = false;
}
}
}
async function updateActivityStatus() {
const currentPubkey = pubkey;
if (!currentPubkey || loadingActivity || lastLoadedPubkey !== currentPubkey) return;
loadingActivity = true;
try {
const actStatus = await getActivityStatus(currentPubkey);
const actMessage = await getActivityMessage(currentPubkey);
// Only update if pubkey hasn't changed during load
if (pubkey === currentPubkey && lastLoadedPubkey === currentPubkey) {
activityStatus = actStatus;
activityMessage = actMessage;
}
} finally {
if (pubkey === currentPubkey) {
loadingActivity = false;
}
}
}
function getActivityColor(): string {
switch (activityStatus) {
case 'red':
return '#ef4444';
case 'yellow':
return '#eab308';
case 'green':
return '#22c55e';
default:
return '#9ca3af';
}
}
// Generate deterministic muted gray-blue avatar color from pubkey
let avatarColor = $derived.by(() => {
// Hash the pubkey to get consistent colors
let hash = 0;
for (let i = 0; i < pubkey.length; i++) {
hash = pubkey.charCodeAt(i) + ((hash << 5) - hash);
}
// Generate muted gray-blue tones: hue around 200-220 (blue range), low saturation, medium lightness
const hue = 200 + (Math.abs(hash) % 20); // 200-220 (blue range)
const saturation = 15 + (Math.abs(hash >> 8) % 10); // 15-25% (very low saturation for muted look)
const lightness = 45 + (Math.abs(hash >> 16) % 15); // 45-60% (medium lightness)
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
});
// Get avatar initials from pubkey
let avatarInitials = $derived(pubkey.slice(0, 2).toUpperCase());
// Get shortened npub for anonymous users (when no profile name)
let shortenedNpub = $derived.by(() => {
try {
const npub = nip19.npubEncode(pubkey);
// Return first 8 characters of npub (e.g., "npub1abc...")
return npub.slice(0, 8) + '...';
} catch {
// Fallback to hex if encoding fails
return pubkey.slice(0, 8) + '...';
}
});
</script>
<a href="/profile/{pubkey}" class="profile-badge inline-flex items-center gap-2 min-w-0 max-w-full">
{#if !inline}
{#if profile?.picture && !imageError}
<img
src={profile.picture}
alt={profile.name || pubkey}
class="profile-picture w-6 h-6 rounded flex-shrink-0"
loading="lazy"
onerror={() => {
imageError = true;
}}
/>
{:else}
<div
class="profile-placeholder w-6 h-6 rounded flex-shrink-0 flex items-center justify-center text-xs font-semibold"
style="background: {avatarColor}; color: white;"
title={pubkey}
>
{avatarInitials}
</div>
{/if}
{/if}
<span class="truncate min-w-0">{profile?.name || shortenedNpub}</span>
{#if !inline && status}
<span class="status-text text-sm text-fog-text-light dark:text-fog-dark-text-light flex-shrink-0 whitespace-nowrap">({status})</span>
{/if}
</a>
<style>
.profile-badge {
text-decoration: none;
color: inherit;
max-width: 100%;
transition: opacity 0.2s;
}
.profile-badge:hover {
text-decoration: underline;
opacity: 0.9;
}
.profile-picture {
object-fit: cover;
display: block;
}
.profile-placeholder {
user-select: none;
line-height: 1;
}
.status-text {
display: inline-block;
}
</style>