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
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>
|
|
|