Browse Source

bug-fixe

master
Silberengel 4 weeks ago
parent
commit
8153620ca0
  1. 5
      src/lib/components/EventMenu.svelte
  2. 80
      src/lib/components/modals/EventJsonModal.svelte
  3. 12
      src/lib/components/profile/ProfileMenu.svelte
  4. 25
      src/lib/components/write/CreateEventForm.svelte
  5. 9
      src/lib/services/cache/event-cache.ts
  6. 20
      src/lib/types/kind-metadata.ts
  7. 2
      src/routes/cache/+page.svelte
  8. 34
      src/routes/repos/+page.svelte
  9. 119
      src/routes/repos/[naddr]/+page.svelte
  10. 3
      src/routes/write/+page.svelte

5
src/lib/components/EventMenu.svelte

@ -276,10 +276,11 @@
function cloneEvent() { function cloneEvent() {
// Store event data in sessionStorage for the write page to pick up // Store event data in sessionStorage for the write page to pick up
// Ensure content is preserved (important for kind 0 which has stringified JSON)
const cloneData = { const cloneData = {
kind: event.kind, kind: event.kind,
content: event.content, content: event.content || '', // Explicitly preserve content, even if empty string
tags: event.tags, tags: event.tags || [],
isClone: true isClone: true
}; };
sessionStorage.setItem('aitherboard_cloneEvent', JSON.stringify(cloneData)); sessionStorage.setItem('aitherboard_cloneEvent', JSON.stringify(cloneData));

80
src/lib/components/modals/EventJsonModal.svelte

@ -12,7 +12,6 @@
let { open = $bindable(false), event = $bindable(null) }: Props = $props(); let { open = $bindable(false), event = $bindable(null) }: Props = $props();
let jsonText = $derived(event ? JSON.stringify(event, null, 2) : ''); let jsonText = $derived(event ? JSON.stringify(event, null, 2) : '');
let copied = $state(false); let copied = $state(false);
let wordWrap = $state(true); // Default to word-wrap enabled
let jsonPreviewRef: HTMLElement | null = $state(null); let jsonPreviewRef: HTMLElement | null = $state(null);
// Highlight JSON when it changes // Highlight JSON when it changes
@ -73,16 +72,6 @@
<div class="modal-header"> <div class="modal-header">
<h2>Event JSON</h2> <h2>Event JSON</h2>
<div class="header-actions"> <div class="header-actions">
<button
onclick={() => wordWrap = !wordWrap}
class="word-wrap-button"
class:active={wordWrap}
aria-label={wordWrap ? 'Disable word wrap' : 'Enable word wrap'}
title={wordWrap ? 'Disable word wrap' : 'Enable word wrap'}
>
<Icon name="code" size={18} />
<span>{wordWrap ? 'Wrap: ON' : 'Wrap: OFF'}</span>
</button>
<button onclick={close} class="close-button" aria-label="Close"> <button onclick={close} class="close-button" aria-label="Close">
<Icon name="x" size={20} /> <Icon name="x" size={20} />
</button> </button>
@ -90,7 +79,7 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<pre class="json-preview" class:word-wrap={wordWrap}><code bind:this={jsonPreviewRef} class="language-json">{jsonText}</code></pre> <pre class="json-preview"><code bind:this={jsonPreviewRef} class="language-json">{jsonText}</code></pre>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@ -205,47 +194,6 @@
color: var(--fog-dark-text, #f1f5f9); color: var(--fog-dark-text, #f1f5f9);
} }
.word-wrap-button {
background: var(--fog-border, #e5e7eb);
border: none;
border-radius: 4px;
cursor: pointer;
padding: 0.375rem 0.75rem;
color: var(--fog-text, #1f2937);
display: inline-flex;
align-items: center;
gap: 0.375rem;
font-size: 0.875rem;
transition: background-color 0.2s;
}
.word-wrap-button:hover {
background: var(--fog-highlight, #f3f4f6);
}
.word-wrap-button.active {
background: var(--fog-accent, #64748b);
color: white;
}
.word-wrap-button.active:hover {
background: var(--fog-accent-dark, #475569);
}
:global(.dark) .word-wrap-button {
background: var(--fog-dark-border, #374151);
color: var(--fog-dark-text, #f1f5f9);
}
:global(.dark) .word-wrap-button:hover {
background: var(--fog-dark-highlight, #475569);
}
:global(.dark) .word-wrap-button.active {
background: var(--fog-accent, #64748b);
color: white;
}
.close-button { .close-button {
background: none; background: none;
border: none; border: none;
@ -267,7 +215,7 @@
.modal-body { .modal-body {
padding: 1rem; padding: 1rem;
flex: 1; flex: 1;
overflow: auto; overflow: hidden;
} }
.json-preview { .json-preview {
@ -276,19 +224,15 @@
border-radius: 4px; border-radius: 4px;
padding: 1rem; padding: 1rem;
margin: 0; margin: 0;
overflow-x: auto;
max-height: 60vh;
white-space: pre;
text-align: left; /* Ensure text aligns left */
position: relative; /* Ensure proper stacking context */
}
.json-preview.word-wrap {
overflow-x: visible !important; overflow-x: visible !important;
overflow-y: auto !important;
max-height: 60vh;
white-space: pre-wrap !important; white-space: pre-wrap !important;
word-wrap: break-word !important; word-wrap: break-word !important;
word-break: break-word !important; word-break: break-word !important;
overflow-wrap: break-word !important; overflow-wrap: break-word !important;
text-align: left; /* Ensure text aligns left */
position: relative; /* Ensure proper stacking context */
} }
.json-preview code { .json-preview code {
@ -299,20 +243,16 @@
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Droid Sans Mono', 'Source Code Pro', monospace; font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Droid Sans Mono', 'Source Code Pro', monospace;
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.5; line-height: 1.5;
white-space: pre;
position: relative; /* Ensure proper stacking */
}
.json-preview.word-wrap code {
overflow-x: visible !important;
white-space: pre-wrap !important; white-space: pre-wrap !important;
word-wrap: break-word !important; word-wrap: break-word !important;
word-break: break-word !important; word-break: break-word !important;
overflow-wrap: break-word !important; overflow-wrap: break-word !important;
overflow-x: visible !important;
position: relative; /* Ensure proper stacking */
} }
.json-preview.word-wrap :global(code.hljs), .json-preview :global(code.hljs),
.json-preview.word-wrap :global(code.hljs *) { .json-preview :global(code.hljs *) {
overflow-x: visible !important; overflow-x: visible !important;
white-space: pre-wrap !important; white-space: pre-wrap !important;
word-wrap: break-word !important; word-wrap: break-word !important;

12
src/lib/components/profile/ProfileMenu.svelte

@ -297,13 +297,19 @@
closeMenu(); closeMenu();
} }
function cloneEvent() { async function cloneEvent() {
// Ensure profileEvent is loaded before cloning
if (!profileEvent) {
await loadProfileEvent();
}
if (!profileEvent) return; if (!profileEvent) return;
// Store event data in sessionStorage for the write page to pick up // Store event data in sessionStorage for the write page to pick up
// Ensure content is preserved (important for kind 0 which has stringified JSON)
const cloneData = { const cloneData = {
kind: profileEvent.kind, kind: profileEvent.kind,
content: profileEvent.content, content: profileEvent.content || '', // Explicitly preserve content, even if empty string
tags: profileEvent.tags, tags: profileEvent.tags || [],
isClone: true isClone: true
}; };
sessionStorage.setItem('aitherboard_cloneEvent', JSON.stringify(cloneData)); sessionStorage.setItem('aitherboard_cloneEvent', JSON.stringify(cloneData));

25
src/lib/components/write/CreateEventForm.svelte

@ -4,6 +4,7 @@
import { uploadFileToServer, buildImetaTag } from '../../services/nostr/file-upload.js'; import { uploadFileToServer, buildImetaTag } from '../../services/nostr/file-upload.js';
import { relayManager } from '../../services/nostr/relay-manager.js'; import { relayManager } from '../../services/nostr/relay-manager.js';
import { cacheEvent } from '../../services/cache/event-cache.js'; import { cacheEvent } from '../../services/cache/event-cache.js';
import { cacheProfile } from '../../services/cache/profile-cache.js';
import { getDraft, saveDraft, deleteDraft } from '../../services/cache/draft-store.js'; import { getDraft, saveDraft, deleteDraft } from '../../services/cache/draft-store.js';
import PublicationStatusModal from '../modals/PublicationStatusModal.svelte'; import PublicationStatusModal from '../modals/PublicationStatusModal.svelte';
import MarkdownRenderer from '../content/MarkdownRenderer.svelte'; import MarkdownRenderer from '../content/MarkdownRenderer.svelte';
@ -13,7 +14,7 @@
import AdvancedEditor from './AdvancedEditor.svelte'; import AdvancedEditor from './AdvancedEditor.svelte';
import { shouldIncludeClientTag } from '../../services/client-tag-preference.js'; import { shouldIncludeClientTag } from '../../services/client-tag-preference.js';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { KIND_LOOKUP } from '../../types/kind-lookup.js'; import { KIND, KIND_LOOKUP } from '../../types/kind-lookup.js';
import { getKindMetadata, getWritableKinds } from '../../types/kind-metadata.js'; import { getKindMetadata, getWritableKinds } from '../../types/kind-metadata.js';
import type { NostrEvent } from '../../types/nostr.js'; import type { NostrEvent } from '../../types/nostr.js';
import { autoExtractTags, ensureDTagForParameterizedReplaceable } from '../../services/auto-tagging.js'; import { autoExtractTags, ensureDTagForParameterizedReplaceable } from '../../services/auto-tagging.js';
@ -51,7 +52,8 @@
}; };
const getInitialContent = (): string => { const getInitialContent = (): string => {
return initialEvent?.content || ''; // Explicitly preserve content (important for kind 0 which has stringified JSON)
return initialEvent?.content !== undefined ? initialEvent.content : '';
}; };
const getInitialTags = (): string[][] => { const getInitialTags = (): string[][] => {
@ -95,7 +97,8 @@
selectedKind = -1; selectedKind = -1;
customKindId = String(initialEvent.kind); customKindId = String(initialEvent.kind);
} }
content = initialEvent.content || ''; // Explicitly preserve content (important for kind 0 which has stringified JSON)
content = initialEvent.content !== undefined ? initialEvent.content : '';
tags = initialEvent.tags ? [...initialEvent.tags] : []; tags = initialEvent.tags ? [...initialEvent.tags] : [];
} }
}); });
@ -179,11 +182,15 @@
const isKind30040 = $derived(selectedKind === 30040); const isKind30040 = $derived(selectedKind === 30040);
const isKind10895 = $derived(selectedKind === 10895); const isKind10895 = $derived(selectedKind === 10895);
// Clear content for metadata-only kinds // Clear content for metadata-only kinds (but preserve content when cloning/editing)
$effect(() => { $effect(() => {
const metadata = getKindMetadata(selectedKind); const metadata = getKindMetadata(selectedKind);
if (metadata.requiresContent === false) { if (metadata.requiresContent === false) {
content = ''; // Only clear content if we don't have an initialEvent (i.e., not cloning/editing)
// This preserves content when cloning kind 0 events which have stringified JSON in content
if (!initialEvent) {
content = '';
}
} }
}); });
@ -356,6 +363,14 @@
const signedEvent = await session.signer(eventTemplate); const signedEvent = await session.signer(eventTemplate);
await cacheEvent(signedEvent); await cacheEvent(signedEvent);
// Also cache profile events in the profile cache
if (effectiveKind === KIND.METADATA) {
await cacheProfile(signedEvent).catch((error) => {
console.error('Error caching profile:', error);
// Non-critical - continue even if profile cache fails
});
}
const relays = relayManager.getPublishRelays( const relays = relayManager.getPublishRelays(
relayManager.getProfileReadRelays(), relayManager.getProfileReadRelays(),
true true

9
src/lib/services/cache/event-cache.ts vendored

@ -74,6 +74,15 @@ export async function cacheEvent(event: NostrEvent): Promise<void> {
cached_at: Date.now() cached_at: Date.now()
}; };
await db.put('events', cached); await db.put('events', cached);
// Also cache profile events (kind 0) in the profile cache
if (event.kind === KIND.METADATA) {
const { cacheProfile } = await import('./profile-cache.js');
await cacheProfile(event).catch((error) => {
// Non-critical - profile cache failures shouldn't break event caching
console.debug('Error caching profile in profile cache:', error);
});
}
} catch (error) { } catch (error) {
// Cache write failed (non-critical) // Cache write failed (non-critical)
// Don't throw - caching failures shouldn't break the app // Don't throw - caching failures shouldn't break the app

20
src/lib/types/kind-metadata.ts

@ -925,19 +925,25 @@ export const KIND_METADATA: Record<number, KindMetadata> = {
writable: true, writable: true,
helpText: { helpText: {
description: 'Repository Announcement (NIP-34/GRASP). Announces a Git repository. Content contains repository description.', description: 'Repository Announcement (NIP-34/GRASP). Announces a Git repository. Content contains repository description.',
suggestedTags: ['d (repository identifier)', 'name (repository name)', 'about', 'web (repository URL)', 'git (git URL)', 'relays (relay URLs)'] suggestedTags: ['d (repository identifier)', 'name (repository name)', 'description', 'clone (git clone URL)', 'web (repository URL)', 'relays (relay URLs)', 'maintainers (pubkey list)', 'image (profile image URL)', 'banner (banner image URL)', 'primary (mark as primary repo)', 'documentation (naddr or kind:pubkey:d-tag format, optional relay)']
}, },
exampleJSON: (pubkey, eventId, relay, timestamp) => ({ exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.REPO_ANNOUNCEMENT, kind: KIND.REPO_ANNOUNCEMENT,
pubkey, pubkey,
created_at: timestamp, created_at: timestamp,
content: 'A Nostr-based Git repository', content: '',
tags: [ tags: [
['d', 'my-repo'], ['d', 'Alexandria'],
['name', 'My Repository'], ['name', 'Alexandria'],
['about', 'Repository description'], ['description', 'Implementation of Nostr Knowledge Base (NKBIP-01) and vector embeddings (NKBIP-02). Can be used as an eReader app.'],
['web', 'https://example.com/repo'], ['clone', 'git@github.com:ShadowySupercode/gc-alexandria.git'],
['git', 'https://git.example.com/repo.git'] ['web', 'https://gitworkshop.dev/repo/Alexandria'],
['relays', 'wss://theforest.nostr1.com', 'wss://thecitadel.nostr1.com'],
['maintainers', 'dc4cd086cd7ce5b1832adf4fdd1211289880d2c7e295bcb0e684c01acee77c06', pubkey, '70122128273bdc07af9be7725fa5c4bc0fc146866bec38d44360dc4bc6cc18b9'],
['primary'],
['documentation', '30818:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:nkbip-01', 'wss://thecitadel.nostr1.com'],
['image', 'https://example.com/repo-image.png'],
['banner', 'https://example.com/repo-banner.png']
], ],
id: '...', id: '...',
sig: '...' sig: '...'

2
src/routes/cache/+page.svelte vendored

@ -180,7 +180,7 @@
loadingMore = true; loadingMore = true;
try { try {
const newEvents = await getAllCachedEvents({ const newEvents = await getAllCachedEvents({
kind: selectedKind || undefined, kind: selectedKind !== null ? selectedKind : undefined,
pubkey: selectedPubkey || undefined, pubkey: selectedPubkey || undefined,
searchTerm: searchTerm || undefined, searchTerm: searchTerm || undefined,
limit: PAGE_SIZE, limit: PAGE_SIZE,

34
src/routes/repos/+page.svelte

@ -275,6 +275,12 @@
return dTag?.[1] || ''; return dTag?.[1] || '';
} }
function getImageUrl(event: NostrEvent): string | null {
if (!Array.isArray(event.tags)) return null;
const imageTag = event.tags.find(t => Array.isArray(t) && t[0] === 'image' && t[1]);
return imageTag?.[1] || null;
}
// Decode bech32 pubkey (npub or nprofile) to hex // Decode bech32 pubkey (npub or nprofile) to hex
function decodePubkeyToHex(input: string): string | null { function decodePubkeyToHex(input: string): string | null {
@ -597,6 +603,11 @@
role="button" role="button"
tabindex="0" tabindex="0"
> >
{#if getImageUrl(repo)}
<div class="repo-image-container">
<img src={getImageUrl(repo)!} alt="{getRepoName(repo)}" class="repo-image" />
</div>
{/if}
<div class="repo-header"> <div class="repo-header">
<h3 class="repo-name">{getRepoName(repo)}</h3> <h3 class="repo-name">{getRepoName(repo)}</h3>
<span class="repo-kind">Kind {repo.kind}</span> <span class="repo-kind">Kind {repo.kind}</span>
@ -745,6 +756,29 @@
min-width: 0; min-width: 0;
} }
.repo-image-container {
width: 100%;
height: 200px;
overflow: hidden;
border-radius: 0.375rem;
margin-bottom: 1rem;
background: var(--fog-highlight, #f3f4f6);
display: flex;
align-items: center;
justify-content: center;
}
:global(.dark) .repo-image-container {
background: var(--fog-dark-highlight, #475569);
}
.repo-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 0.375rem;
}
:global(.dark) .repo-item { :global(.dark) .repo-item {
border-color: var(--fog-dark-border, #374151); border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-post, #1f2937); background: var(--fog-dark-post, #1f2937);

119
src/routes/repos/[naddr]/+page.svelte

@ -855,6 +855,18 @@
}); });
} }
function getImageUrl(): string | null {
if (!repoEvent || !Array.isArray(repoEvent.tags)) return null;
const imageTag = repoEvent.tags.find(t => Array.isArray(t) && t[0] === 'image' && t[1]);
return imageTag?.[1] || null;
}
function getBannerUrl(): string | null {
if (!repoEvent || !Array.isArray(repoEvent.tags)) return null;
const bannerTag = repoEvent.tags.find(t => Array.isArray(t) && t[0] === 'banner' && t[1]);
return bannerTag?.[1] || null;
}
function isPrimary(): boolean { function isPrimary(): boolean {
if (!repoEvent || !Array.isArray(repoEvent.tags)) return false; if (!repoEvent || !Array.isArray(repoEvent.tags)) return false;
return repoEvent.tags.some(t => Array.isArray(t) && t[0] === 'primary'); return repoEvent.tags.some(t => Array.isArray(t) && t[0] === 'primary');
@ -1078,26 +1090,40 @@
<p class="text-fog-text dark:text-fog-dark-text">Repository not found.</p> <p class="text-fog-text dark:text-fog-dark-text">Repository not found.</p>
</div> </div>
{:else} {:else}
{#if getBannerUrl()}
<div class="repo-banner-container">
<img src={getBannerUrl()!} alt="{getRepoName()} banner" class="repo-banner" />
</div>
{/if}
<div class="repo-header mb-6"> <div class="repo-header mb-6">
<div class="repo-title-row"> <div class="repo-header-top">
<h1 class="font-bold text-fog-text dark:text-fog-dark-text font-mono mb-6" style="font-size: 1.5em;"> {#if getImageUrl()}
{getRepoName()} <div class="repo-profile-image-container">
</h1> <img src={getImageUrl()!} alt="{getRepoName()}" class="repo-profile-image" />
{#if repoEvent} </div>
<EventMenu event={repoEvent} showContentActions={true} />
{/if} {/if}
</div> <div class="repo-title-section">
{#if gitRepo?.usingGitHubToken} <div class="repo-title-row">
<div class="github-token-notice mb-4 p-3 bg-fog-highlight dark:bg-fog-dark-highlight border border-fog-border dark:border-fog-dark-border rounded text-sm text-fog-text dark:text-fog-dark-text"> <h1 class="font-bold text-fog-text dark:text-fog-dark-text font-mono mb-6" style="font-size: 1.5em;">
<Icon name="key" size={16} class="inline mr-2" /> {getRepoName()}
Using your saved GitHub API token for authenticated requests </h1>
{#if repoEvent}
<EventMenu event={repoEvent} showContentActions={true} />
{/if}
</div>
{#if gitRepo?.usingGitHubToken}
<div class="github-token-notice mb-4 p-3 bg-fog-highlight dark:bg-fog-dark-highlight border border-fog-border dark:border-fog-dark-border rounded text-sm text-fog-text dark:text-fog-dark-text">
<Icon name="key" size={16} class="inline mr-2" />
Using your saved GitHub API token for authenticated requests
</div>
{/if}
{#if getRepoDescription()}
<p class="text-fog-text-light dark:text-fog-dark-text-light mb-4">
{getRepoDescription()}
</p>
{/if}
</div> </div>
{/if} </div>
{#if getRepoDescription()}
<p class="text-fog-text-light dark:text-fog-dark-text-light mb-4">
{getRepoDescription()}
</p>
{/if}
<!-- Tabs --> <!-- Tabs -->
<div class="tabs"> <div class="tabs">
@ -1484,6 +1510,29 @@
text-align: center; text-align: center;
} }
.repo-banner-container {
width: 100%;
height: 300px;
overflow: hidden;
border-radius: 0.5rem;
margin-bottom: 2rem;
background: var(--fog-highlight, #f3f4f6);
display: flex;
align-items: center;
justify-content: center;
}
:global(.dark) .repo-banner-container {
background: var(--fog-dark-highlight, #475569);
}
.repo-banner {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 0.5rem;
}
.repo-header { .repo-header {
border-bottom: 1px solid var(--fog-border, #e5e7eb); border-bottom: 1px solid var(--fog-border, #e5e7eb);
padding-bottom: 1rem; padding-bottom: 1rem;
@ -1493,6 +1542,42 @@
border-bottom-color: var(--fog-dark-border, #374151); border-bottom-color: var(--fog-dark-border, #374151);
} }
.repo-header-top {
display: flex;
align-items: flex-start;
gap: 1.5rem;
flex-wrap: wrap;
}
.repo-profile-image-container {
flex-shrink: 0;
width: 120px;
height: 120px;
border-radius: 0.5rem;
overflow: hidden;
background: var(--fog-highlight, #f3f4f6);
border: 2px solid var(--fog-border, #e5e7eb);
display: flex;
align-items: center;
justify-content: center;
}
:global(.dark) .repo-profile-image-container {
background: var(--fog-dark-highlight, #475569);
border-color: var(--fog-dark-border, #374151);
}
.repo-profile-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.repo-title-section {
flex: 1;
min-width: 0;
}
.repo-title-row { .repo-title-row {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;

3
src/routes/write/+page.svelte

@ -23,12 +23,13 @@
const cloneData = JSON.parse(cloneDataStr); const cloneData = JSON.parse(cloneDataStr);
// Construct event from clone data // Construct event from clone data
// Use nullish coalescing to properly handle kind 0 // Use nullish coalescing to properly handle kind 0
// Explicitly preserve content (important for kind 0 which has stringified JSON)
initialEvent = { initialEvent = {
id: '', id: '',
pubkey: '', pubkey: '',
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
kind: cloneData.kind !== undefined && cloneData.kind !== null ? cloneData.kind : 1, kind: cloneData.kind !== undefined && cloneData.kind !== null ? cloneData.kind : 1,
content: cloneData.content || '', content: cloneData.content !== undefined ? cloneData.content : '', // Preserve content even if empty string
tags: cloneData.tags || [], tags: cloneData.tags || [],
sig: '' sig: ''
}; };

Loading…
Cancel
Save