diff --git a/src/app.css b/src/app.css
index 3e2dbf5..2eeb9eb 100644
--- a/src/app.css
+++ b/src/app.css
@@ -175,7 +175,7 @@ main {
padding: 0.5rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.375rem;
- background: var(--fog-post, #ffffff);
+ background: var(--fog-surface, #f8fafc);
color: var(--fog-text, #1f2937);
text-decoration: none;
transition: all 0.2s;
@@ -187,7 +187,7 @@ main {
:global(.dark) .write-button {
border-color: var(--fog-dark-border, #374151);
- background: var(--fog-dark-post, #1f2937);
+ background: var(--fog-dark-surface, #1e293b);
color: var(--fog-dark-text, #f9fafb);
}
diff --git a/src/lib/components/content/MetadataCard.svelte b/src/lib/components/content/MetadataCard.svelte
index f660a8b..4b2b9cf 100644
--- a/src/lib/components/content/MetadataCard.svelte
+++ b/src/lib/components/content/MetadataCard.svelte
@@ -65,9 +65,11 @@
);
const hasMetadata = $derived(image || description || summary || author || title);
+ const hasContent = $derived(event.content && event.content.trim().length > 0);
+ const shouldShowMetadata = $derived(hasMetadata || !hasContent); // Show metadata if it exists OR if there's no content
-{#if hasMetadata}
+{#if shouldShowMetadata}
{/if}
@@ -176,4 +196,44 @@
:global(.dark) .metadata-author {
color: var(--fog-dark-text-light, #9ca3af);
}
+
+ .metadata-tags {
+ margin-top: 1rem;
+ padding-top: 1rem;
+ border-top: 1px solid var(--fog-border, #e5e7eb);
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ }
+
+ :global(.dark) .metadata-tags {
+ border-top-color: var(--fog-dark-border, #374151);
+ }
+
+ .metadata-tag {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ align-items: baseline;
+ font-size: 0.875rem;
+ }
+
+ .metadata-tag-name {
+ font-weight: 600;
+ color: var(--fog-text, #1f2937);
+ }
+
+ :global(.dark) .metadata-tag-name {
+ color: var(--fog-dark-text, #f9fafb);
+ }
+
+ .metadata-tag-value {
+ color: var(--fog-text-light, #6b7280);
+ font-family: monospace;
+ word-break: break-all;
+ }
+
+ :global(.dark) .metadata-tag-value {
+ color: var(--fog-dark-text-light, #9ca3af);
+ }
diff --git a/src/lib/components/content/RichTextEditor.svelte b/src/lib/components/content/RichTextEditor.svelte
index 0bcebab..208ebb4 100644
--- a/src/lib/components/content/RichTextEditor.svelte
+++ b/src/lib/components/content/RichTextEditor.svelte
@@ -5,6 +5,7 @@
import MentionsAutocomplete from './MentionsAutocomplete.svelte';
import GifPicker from './GifPicker.svelte';
import EmojiPicker from './EmojiPicker.svelte';
+ import Icon from '../ui/Icon.svelte';
interface Props {
value: string;
@@ -174,7 +175,7 @@
aria-label="Insert GIF"
{disabled}
>
- GIF
+
- 📤
+
{/if}
diff --git a/src/lib/components/find/SearchAddressableEvents.svelte b/src/lib/components/find/SearchAddressableEvents.svelte
index 9fc646f..87d73c4 100644
--- a/src/lib/components/find/SearchAddressableEvents.svelte
+++ b/src/lib/components/find/SearchAddressableEvents.svelte
@@ -5,7 +5,7 @@
import ProfileBadge from '../layout/ProfileBadge.svelte';
import RelayBadge from '../layout/RelayBadge.svelte';
import CacheBadge from '../layout/CacheBadge.svelte';
- import { getKindInfo } from '../../types/kind-lookup.js';
+ import { getKindInfo, KIND_LOOKUP, isParameterizedReplaceableKind } from '../../types/kind-lookup.js';
import { nip19 } from 'nostr-tools';
import { goto } from '$app/navigation';
import type { NostrEvent } from '../../types/nostr.js';
@@ -21,6 +21,11 @@
const searchTags = ['d', 'T', 'C', 'title', 'author', 'summary', 'description'];
const SEARCH_TIMEOUT = 10000; // 10 seconds
const CACHE_SEARCH_DEBOUNCE = 500; // 500ms debounce for cache search
+
+ // Get only the parameterized replaceable kinds that are actually defined (computed once)
+ const parameterizedKinds = Object.keys(KIND_LOOKUP)
+ .map(Number)
+ .filter(kind => isParameterizedReplaceableKind(kind));
// Map to track results by event ID to avoid duplicates
const resultsMap = new Map();
@@ -154,15 +159,10 @@
}
const queryLower = query.toLowerCase();
- // Fetch all cached events in kind range 30000-39999
- const kinds: number[] = [];
- for (let kind = 30000; kind <= 39999; kind++) {
- kinds.push(kind);
- }
// Get all cached events for these kinds
const allCachedEvents: NostrEvent[] = [];
- for (const kind of kinds) {
+ for (const kind of parameterizedKinds) {
const cached = await getEventsByKind(kind, 1000); // Get up to 1000 per kind
allCachedEvents.push(...cached);
}
@@ -179,24 +179,39 @@
}
// Debounced cache search
+ let isSearchingCache = $state(false);
+
$effect(() => {
+ // Only react to searchQuery changes, not cacheResults
+ const query = searchQuery.trim();
+
// Clear previous timeout
if (cacheSearchTimeoutId) {
clearTimeout(cacheSearchTimeoutId);
+ cacheSearchTimeoutId = null;
}
- if (searchQuery.trim()) {
+ if (query) {
cacheSearchTimeoutId = setTimeout(() => {
- searchCache();
+ if (!isSearchingCache) {
+ isSearchingCache = true;
+ searchCache().finally(() => {
+ isSearchingCache = false;
+ });
+ }
}, CACHE_SEARCH_DEBOUNCE);
} else {
- cacheResults = [];
- cacheResultsMap.clear();
+ // Only clear if we're not currently searching
+ if (!isSearchingCache) {
+ cacheResults = [];
+ cacheResultsMap.clear();
+ }
}
return () => {
if (cacheSearchTimeoutId) {
clearTimeout(cacheSearchTimeoutId);
+ cacheSearchTimeoutId = null;
}
};
});
@@ -268,17 +283,58 @@
}
const queryLower = query.toLowerCase();
- // Fetch all events in kind range 30000-39999
- const kinds: number[] = [];
- for (let kind = 30000; kind <= 39999; kind++) {
- kinds.push(kind);
- }
-
+ // Normalize query: remove spaces, handle hyphens
+ const normalizedQuery = queryLower.replace(/\s+/g, '-');
+ const queryWords = queryLower.split(/[\s-]+/).filter(w => w.length > 0);
+
const relays = relayManager.getProfileReadRelays();
+ const allQueries: any[] = [];
+
+ // Build queries with tag filters for better search results
+ // Only query the specific parameterized replaceable kinds we know about
+
+ // Search by d-tag (most specific) - try both original and normalized
+ // Also try each word as a potential d-tag match
+ allQueries.push(
+ { kinds: parameterizedKinds, '#d': [queryLower], limit: 50 },
+ { kinds: parameterizedKinds, '#d': [normalizedQuery], limit: 50 }
+ );
+ for (const word of queryWords) {
+ if (word.length > 2) { // Only search for words longer than 2 chars
+ allQueries.push({ kinds: parameterizedKinds, '#d': [word], limit: 50 });
+ }
+ }
+
+ // Search by T-tag (topics/tags) for each word
+ for (const word of queryWords) {
+ if (word.length > 0) {
+ allQueries.push({ kinds: parameterizedKinds, '#T': [word], limit: 50 });
+ }
+ }
+
+ // Search by C-tag (categories) for each word
+ for (const word of queryWords) {
+ if (word.length > 0) {
+ allQueries.push({ kinds: parameterizedKinds, '#C': [word], limit: 50 });
+ }
+ }
+
+ // Search by title tag
+ allQueries.push({ kinds: parameterizedKinds, '#title': [queryLower], limit: 50 });
+ for (const word of queryWords) {
+ if (word.length > 0) {
+ allQueries.push({ kinds: parameterizedKinds, '#title': [word], limit: 50 });
+ }
+ }
+
+ // Also do a broader search without tag filters to catch partial matches
+ // This will find events where the query appears in d-tag but not as exact match
+ allQueries.push({ kinds: parameterizedKinds, limit: 200 });
+
// Use onUpdateWithRelay to process events as they arrive with relay info
await nostrClient.fetchEvents(
- [{ kinds, limit: 100 }],
+ allQueries,
relays,
{
useCache: 'cache-first',
@@ -366,7 +422,6 @@
cacheResults = [];
resultsMap.clear();
cacheResultsMap.clear();
- eventRelayMap.clear();
searching = false;
if (timeoutId) {
clearTimeout(timeoutId);
@@ -377,6 +432,10 @@
cacheSearchTimeoutId = null;
}
}
+
+ export function hasActiveSearch(): boolean {
+ return searching || searchQuery.trim().length > 0 || results.length > 0 || cacheResults.length > 0;
+ }
@@ -386,20 +445,25 @@
{#if cacheResults.length > 0 || results.length > 0}
@@ -619,15 +683,7 @@