Browse Source

added pastel placeholders

master
silberengel 8 months ago
parent
commit
1a1612150e
  1. 29
      src/lib/components/cards/BlogHeader.svelte
  2. 26
      src/lib/components/cards/ProfileHeader.svelte
  3. 27
      src/lib/components/publications/PublicationHeader.svelte
  4. 23
      src/lib/components/util/Details.svelte
  5. 90
      src/lib/components/util/LazyImage.svelte
  6. 6
      src/lib/consts.ts
  7. 15
      src/lib/stores/userStore.ts
  8. 31
      src/lib/utils/image_utils.ts
  9. 18
      src/lib/utils/nostrUtils.ts
  10. 19
      src/lib/utils/relay_management.ts

29
src/lib/components/cards/BlogHeader.svelte

@ -1,12 +1,14 @@
<script lang="ts"> <script lang="ts">
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { scale } from "svelte/transition"; import { scale } from "svelte/transition";
import { Card, Img } from "flowbite-svelte"; import { Card } from "flowbite-svelte";
import { userBadge } from "$lib/snippets/UserSnippets.svelte"; import { userBadge } from "$lib/snippets/UserSnippets.svelte";
import Interactions from "$components/util/Interactions.svelte"; import Interactions from "$components/util/Interactions.svelte";
import { quintOut } from "svelte/easing"; import { quintOut } from "svelte/easing";
import CardActions from "$components/util/CardActions.svelte"; import CardActions from "$components/util/CardActions.svelte";
import { getMatchingTags } from "$lib/utils/nostrUtils"; import { getMatchingTags } from "$lib/utils/nostrUtils";
import LazyImage from "$components/util/LazyImage.svelte";
import { generateDarkPastelColor } from "$lib/utils/image_utils";
const { const {
rootId, rootId,
@ -62,18 +64,25 @@
</div> </div>
</div> </div>
{#if image && active} <div
<div class="ArticleBoxImage flex justify-center items-center p-2 h-40 -mt-2"
class="ArticleBoxImage flex justify-center items-center p-2 h-40 -mt-2" in:scale={{ start: 0.8, duration: 500, delay: 100, easing: quintOut }}
in:scale={{ start: 0.8, duration: 500, delay: 100, easing: quintOut }} >
> {#if image && active}
<Img <LazyImage
src={image} src={image}
class="rounded w-full h-full object-cover"
alt={title || "Publication image"} alt={title || "Publication image"}
eventId={event.id}
className="rounded w-full h-full object-cover"
/> />
</div> {:else}
{/if} <div
class="rounded w-full h-full"
style="background-color: {generateDarkPastelColor(event.id)};"
>
</div>
{/if}
</div>
<div class="flex flex-col space-y-4"> <div class="flex flex-col space-y-4">
<button onclick={() => showBlog()} class="text-left"> <button onclick={() => showBlog()} class="text-left">

26
src/lib/components/cards/ProfileHeader.svelte

@ -1,10 +1,12 @@
<script lang="ts"> <script lang="ts">
import { Card, Img, Modal, Button, P } from "flowbite-svelte"; import { Card, Modal, Button, P } from "flowbite-svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { userBadge } from "$lib/snippets/UserSnippets.svelte"; import { userBadge } from "$lib/snippets/UserSnippets.svelte";
import { type NostrProfile, toNpub } from "$lib/utils/nostrUtils.ts"; import { type NostrProfile, toNpub } from "$lib/utils/nostrUtils.ts";
import QrCode from "$components/util/QrCode.svelte"; import QrCode from "$components/util/QrCode.svelte";
import CopyToClipboard from "$components/util/CopyToClipboard.svelte"; import CopyToClipboard from "$components/util/CopyToClipboard.svelte";
import LazyImage from "$components/util/LazyImage.svelte";
import { generateDarkPastelColor } from "$lib/utils/image_utils";
import { import {
lnurlpWellKnownUrl, lnurlpWellKnownUrl,
checkCommunity, checkCommunity,
@ -62,18 +64,22 @@
{#if profile} {#if profile}
<Card class="ArticleBox card-leather w-full max-w-2xl"> <Card class="ArticleBox card-leather w-full max-w-2xl">
<div class="space-y-4"> <div class="space-y-4">
{#if profile.banner} <div class="ArticleBoxImage flex col justify-center">
<div class="ArticleBoxImage flex col justify-center"> {#if profile.banner}
<Img <LazyImage
src={profile.banner} src={profile.banner}
class="rounded w-full max-h-72 object-cover"
alt="Profile banner" alt="Profile banner"
onerror={(e) => { eventId={event.id}
(e.target as HTMLImageElement).style.display = "none"; className="rounded w-full max-h-72 object-cover"
}}
/> />
</div> {:else}
{/if} <div
class="rounded w-full max-h-72"
style="background-color: {generateDarkPastelColor(event.id)};"
>
</div>
{/if}
</div>
<div class="flex flex-row space-x-4 items-center"> <div class="flex flex-row space-x-4 items-center">
{#if profile.picture} {#if profile.picture}
<img <img

27
src/lib/components/publications/PublicationHeader.svelte

@ -2,9 +2,11 @@
import { naddrEncode } from "$lib/utils"; import { naddrEncode } from "$lib/utils";
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { activeInboxRelays } from "$lib/ndk"; import { activeInboxRelays } from "$lib/ndk";
import { Card, Img } from "flowbite-svelte"; import { Card } from "flowbite-svelte";
import CardActions from "$components/util/CardActions.svelte"; import CardActions from "$components/util/CardActions.svelte";
import { userBadge } from "$lib/snippets/UserSnippets.svelte"; import { userBadge } from "$lib/snippets/UserSnippets.svelte";
import LazyImage from "$components/util/LazyImage.svelte";
import { generateDarkPastelColor } from "$lib/utils/image_utils";
const { event } = $props<{ event: NDKEvent }>(); const { event } = $props<{ event: NDKEvent }>();
@ -40,17 +42,24 @@
{#if title != null && href != null} {#if title != null && href != null}
<Card class="ArticleBox card-leather max-w-md h-48 flex flex-row space-x-2 relative"> <Card class="ArticleBox card-leather max-w-md h-48 flex flex-row space-x-2 relative">
{#if image} <div
<div class="flex-shrink-0 w-32 h-40 overflow-hidden rounded flex items-center justify-center p-2 -mt-2"
class="flex-shrink-0 w-32 h-40 overflow-hidden rounded flex items-center justify-center p-2 -mt-2" >
> {#if image}
<Img <LazyImage
src={image} src={image}
class="w-full h-full object-cover"
alt={title || "Publication image"} alt={title || "Publication image"}
eventId={event.id}
className="w-full h-full object-cover"
/> />
</div> {:else}
{/if} <div
class="w-full h-full rounded"
style="background-color: {generateDarkPastelColor(event.id)};"
>
</div>
{/if}
</div>
<div class="flex flex-col flex-grow space-x-2"> <div class="flex flex-col flex-grow space-x-2">
<div class="flex flex-col flex-grow"> <div class="flex flex-col flex-grow">

23
src/lib/components/util/Details.svelte

@ -5,6 +5,8 @@
import { P } from "flowbite-svelte"; import { P } from "flowbite-svelte";
import { getMatchingTags } from "$lib/utils/nostrUtils"; import { getMatchingTags } from "$lib/utils/nostrUtils";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import LazyImage from "$components/util/LazyImage.svelte";
import { generateDarkPastelColor } from "$lib/utils/image_utils";
// isModal // isModal
// - don't show interactions in modal view // - don't show interactions in modal view
@ -68,15 +70,22 @@
<div <div
class="flex-grow grid grid-cols-1 md:grid-cols-[auto_1fr] gap-4 items-center" class="flex-grow grid grid-cols-1 md:grid-cols-[auto_1fr] gap-4 items-center"
> >
{#if image} <div class="my-2">
<div class="my-2"> {#if image}
<img <LazyImage
class="w-full md:max-w-48 object-contain rounded"
alt={title}
src={image} src={image}
alt={title}
eventId={event.id}
className="w-full md:max-w-48 object-contain rounded"
/> />
</div> {:else}
{/if} <div
class="w-full md:max-w-48 h-32 object-contain rounded"
style="background-color: {generateDarkPastelColor(event.id)};"
>
</div>
{/if}
</div>
<div class="space-y-4 my-4"> <div class="space-y-4 my-4">
<h1 class="text-3xl font-bold">{title}</h1> <h1 class="text-3xl font-bold">{title}</h1>
<h2 class="text-base font-bold"> <h2 class="text-base font-bold">

90
src/lib/components/util/LazyImage.svelte

@ -0,0 +1,90 @@
<script lang="ts">
import { generateDarkPastelColor } from '$lib/utils/image_utils';
import { fade } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
let {
src,
alt,
eventId,
className = 'w-full h-full object-cover',
placeholderClassName = '',
}: {
src: string;
alt: string;
eventId: string;
className?: string;
placeholderClassName?: string;
} = $props();
let imageLoaded = $state(false);
let imageError = $state(false);
let imgElement = $state<HTMLImageElement | null>(null);
const placeholderColor = $derived.by(() => generateDarkPastelColor(eventId));
function loadImage() {
if (!imgElement) return;
imgElement.onload = () => {
// Small delay to ensure smooth transition
setTimeout(() => {
imageLoaded = true;
}, 100);
};
imgElement.onerror = () => {
imageError = true;
};
// Set src after setting up event handlers
imgElement.src = src;
}
function bindImg(element: HTMLImageElement) {
imgElement = element;
// Load image immediately when element is bound
loadImage();
}
</script>
<div class="relative w-full h-full">
<!-- Placeholder -->
<div
class="absolute inset-0 {placeholderClassName}"
style="background-color: {placeholderColor};"
class:hidden={imageLoaded}
>
</div>
<!-- Image -->
<img
bind:this={imgElement}
{src}
{alt}
class="{className} {imageLoaded ? 'opacity-100' : 'opacity-0'}"
style="transition: opacity 0.2s ease-out;"
loading="lazy"
decoding="async"
class:hidden={imageError}
onload={() => {
setTimeout(() => {
imageLoaded = true;
}, 100);
}}
onerror={() => {
imageError = true;
}}
/>
<!-- Error state -->
{#if imageError}
<div
class="absolute inset-0 flex items-center justify-center bg-gray-200 dark:bg-gray-700 {placeholderClassName}"
>
<div class="text-gray-500 dark:text-gray-400 text-xs">
Failed to load
</div>
</div>
{/if}
</div>

6
src/lib/consts.ts

@ -36,9 +36,9 @@ export const lowbandwidthRelays = [
"wss://aggr.nostr.land" "wss://aggr.nostr.land"
]; ];
export const localRelays = [ export const localRelays: string[] = [
"wss://localhost:8080", // "wss://localhost:8080",
"wss://localhost:4869" // "wss://localhost:4869"
]; ];
export enum FeedType { export enum FeedType {

15
src/lib/stores/userStore.ts

@ -162,7 +162,20 @@ export async function loginWithExtension() {
const signer = new NDKNip07Signer(); const signer = new NDKNip07Signer();
const user = await signer.user(); const user = await signer.user();
const npub = user.npub; const npub = user.npub;
const profile = await getUserMetadata(npub);
// Try to fetch user metadata, but don't fail if it times out
let profile: NostrProfile | null = null;
try {
profile = await getUserMetadata(npub);
} catch (error) {
console.warn("Failed to fetch user metadata during login:", error);
// Continue with login even if metadata fetch fails
profile = {
name: npub.slice(0, 8) + "..." + npub.slice(-4),
displayName: npub.slice(0, 8) + "..." + npub.slice(-4),
};
}
// Fetch user's preferred relays // Fetch user's preferred relays
const [persistedInboxes, persistedOutboxes] = getPersistedRelays(user); const [persistedInboxes, persistedOutboxes] = getPersistedRelays(user);
for (const relay of persistedInboxes) { for (const relay of persistedInboxes) {

31
src/lib/utils/image_utils.ts

@ -0,0 +1,31 @@
/**
* Generate a dark-pastel color based on a string (like an event ID)
* @param seed - The string to generate a color from
* @returns A dark-pastel hex color
*/
export function generateDarkPastelColor(seed: string): string {
// Create a simple hash from the seed string
let hash = 0;
for (let i = 0; i < seed.length; i++) {
const char = seed.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
// Use the hash to generate lighter pastel colors
// Keep values in the 120-200 range for better pastel effect
const r = Math.abs(hash) % 80 + 120; // 120-200 range
const g = Math.abs(hash >> 8) % 80 + 120; // 120-200 range
const b = Math.abs(hash >> 16) % 80 + 120; // 120-200 range
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
}
/**
* Test function to verify color generation
* @param eventId - The event ID to test
* @returns The generated color
*/
export function testColorGeneration(eventId: string): string {
return generateDarkPastelColor(eventId);
}

18
src/lib/utils/nostrUtils.ts

@ -426,12 +426,18 @@ export async function fetchEventWithFallback(
// Use the active inbox relays from the relay management system // Use the active inbox relays from the relay management system
const inboxRelays = get(activeInboxRelays); const inboxRelays = get(activeInboxRelays);
// Check if we have any relays available
if (inboxRelays.length === 0) {
console.warn("No inbox relays available for event fetch");
return null;
}
// Create relay set from active inbox relays // Create relay set from active inbox relays
const relaySet = NDKRelaySetFromNDK.fromRelayUrls(inboxRelays, ndk); const relaySet = NDKRelaySetFromNDK.fromRelayUrls(inboxRelays, ndk);
try { try {
if (relaySet.relays.size === 0) { if (relaySet.relays.size === 0) {
console.warn("No inbox relays available for event fetch"); console.warn("No relays in relay set for event fetch");
return null; return null;
} }
@ -467,7 +473,15 @@ export async function fetchEventWithFallback(
// Always wrap as NDKEvent // Always wrap as NDKEvent
return found instanceof NDKEvent ? found : new NDKEvent(ndk, found); return found instanceof NDKEvent ? found : new NDKEvent(ndk, found);
} catch (err) { } catch (err) {
console.error("Error in fetchEventWithFallback:", err); if (err instanceof Error && err.message === 'Timeout') {
const timeoutSeconds = timeoutMs / 1000;
const relayUrls = Array.from(relaySet.relays).map((r: any) => r.url).join(", ");
console.warn(
`Event fetch timed out after ${timeoutSeconds}s. Tried inbox relays: ${relayUrls}. Some relays may be offline or slow.`,
);
} else {
console.error("Error in fetchEventWithFallback:", err);
}
return null; return null;
} }
} }

19
src/lib/utils/relay_management.ts

@ -147,19 +147,30 @@ function ensureSecureWebSocket(url: string): string {
async function testLocalRelays(localRelayUrls: string[], ndk: NDK): Promise<string[]> { async function testLocalRelays(localRelayUrls: string[], ndk: NDK): Promise<string[]> {
const workingRelays: string[] = []; const workingRelays: string[] = [];
if (localRelayUrls.length === 0) {
return workingRelays;
}
console.debug(`[relay_management.ts] Testing ${localRelayUrls.length} local relays...`);
await Promise.all( await Promise.all(
localRelayUrls.map(async (url) => { localRelayUrls.map(async (url) => {
try { try {
const result = await testRelayConnection(url, ndk); const result = await testRelayConnection(url, ndk);
if (result.connected) { if (result.connected) {
workingRelays.push(url); workingRelays.push(url);
console.debug(`[relay_management.ts] Local relay connected: ${url}`);
} else {
console.debug(`[relay_management.ts] Local relay failed: ${url} - ${result.error}`);
} }
} catch (error) { } catch (error) {
// Silently ignore local relay failures // Silently ignore local relay failures - they're optional
console.debug(`[relay_management.ts] Local relay error (ignored): ${url}`);
} }
}) })
); );
console.debug(`[relay_management.ts] Found ${workingRelays.length} working local relays`);
return workingRelays; return workingRelays;
} }
@ -170,6 +181,12 @@ async function testLocalRelays(localRelayUrls: string[], ndk: NDK): Promise<stri
*/ */
export async function discoverLocalRelays(ndk: NDK): Promise<string[]> { export async function discoverLocalRelays(ndk: NDK): Promise<string[]> {
try { try {
// If no local relays are configured, return empty array
if (localRelays.length === 0) {
console.debug('[relay_management.ts] No local relays configured');
return [];
}
// Convert wss:// URLs from consts to ws:// for local testing // Convert wss:// URLs from consts to ws:// for local testing
const localRelayUrls = localRelays.map(url => const localRelayUrls = localRelays.map(url =>
url.replace(/^wss:\/\//, 'ws://') url.replace(/^wss:\/\//, 'ws://')

Loading…
Cancel
Save