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 @@ @@ -276,10 +276,11 @@
function cloneEvent() {
// 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 = {
kind: event.kind,
content: event.content,
tags: event.tags,
content: event.content || '', // Explicitly preserve content, even if empty string
tags: event.tags || [],
isClone: true
};
sessionStorage.setItem('aitherboard_cloneEvent', JSON.stringify(cloneData));

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

@ -12,7 +12,6 @@ @@ -12,7 +12,6 @@
let { open = $bindable(false), event = $bindable(null) }: Props = $props();
let jsonText = $derived(event ? JSON.stringify(event, null, 2) : '');
let copied = $state(false);
let wordWrap = $state(true); // Default to word-wrap enabled
let jsonPreviewRef: HTMLElement | null = $state(null);
// Highlight JSON when it changes
@ -73,16 +72,6 @@ @@ -73,16 +72,6 @@
<div class="modal-header">
<h2>Event JSON</h2>
<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">
<Icon name="x" size={20} />
</button>
@ -90,7 +79,7 @@ @@ -90,7 +79,7 @@
</div>
<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 class="modal-footer">
@ -205,47 +194,6 @@ @@ -205,47 +194,6 @@
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 {
background: none;
border: none;
@ -267,7 +215,7 @@ @@ -267,7 +215,7 @@
.modal-body {
padding: 1rem;
flex: 1;
overflow: auto;
overflow: hidden;
}
.json-preview {
@ -276,19 +224,15 @@ @@ -276,19 +224,15 @@
border-radius: 4px;
padding: 1rem;
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-y: auto !important;
max-height: 60vh;
white-space: pre-wrap !important;
word-wrap: break-word !important;
word-break: break-word !important;
overflow-wrap: break-word !important;
text-align: left; /* Ensure text aligns left */
position: relative; /* Ensure proper stacking context */
}
.json-preview code {
@ -299,20 +243,16 @@ @@ -299,20 +243,16 @@
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Droid Sans Mono', 'Source Code Pro', monospace;
font-size: 0.875rem;
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;
word-wrap: break-word !important;
word-break: 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.word-wrap :global(code.hljs *) {
.json-preview :global(code.hljs),
.json-preview :global(code.hljs *) {
overflow-x: visible !important;
white-space: pre-wrap !important;
word-wrap: break-word !important;

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

@ -297,13 +297,19 @@ @@ -297,13 +297,19 @@
closeMenu();
}
function cloneEvent() {
async function cloneEvent() {
// Ensure profileEvent is loaded before cloning
if (!profileEvent) {
await loadProfileEvent();
}
if (!profileEvent) return;
// 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 = {
kind: profileEvent.kind,
content: profileEvent.content,
tags: profileEvent.tags,
content: profileEvent.content || '', // Explicitly preserve content, even if empty string
tags: profileEvent.tags || [],
isClone: true
};
sessionStorage.setItem('aitherboard_cloneEvent', JSON.stringify(cloneData));

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

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
import { uploadFileToServer, buildImetaTag } from '../../services/nostr/file-upload.js';
import { relayManager } from '../../services/nostr/relay-manager.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 PublicationStatusModal from '../modals/PublicationStatusModal.svelte';
import MarkdownRenderer from '../content/MarkdownRenderer.svelte';
@ -13,7 +14,7 @@ @@ -13,7 +14,7 @@
import AdvancedEditor from './AdvancedEditor.svelte';
import { shouldIncludeClientTag } from '../../services/client-tag-preference.js';
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 type { NostrEvent } from '../../types/nostr.js';
import { autoExtractTags, ensureDTagForParameterizedReplaceable } from '../../services/auto-tagging.js';
@ -51,7 +52,8 @@ @@ -51,7 +52,8 @@
};
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[][] => {
@ -95,7 +97,8 @@ @@ -95,7 +97,8 @@
selectedKind = -1;
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] : [];
}
});
@ -179,11 +182,15 @@ @@ -179,11 +182,15 @@
const isKind30040 = $derived(selectedKind === 30040);
const isKind10895 = $derived(selectedKind === 10895);
// Clear content for metadata-only kinds
// Clear content for metadata-only kinds (but preserve content when cloning/editing)
$effect(() => {
const metadata = getKindMetadata(selectedKind);
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 @@ @@ -356,6 +363,14 @@
const signedEvent = await session.signer(eventTemplate);
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(
relayManager.getProfileReadRelays(),
true

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

@ -74,6 +74,15 @@ export async function cacheEvent(event: NostrEvent): Promise<void> { @@ -74,6 +74,15 @@ export async function cacheEvent(event: NostrEvent): Promise<void> {
cached_at: Date.now()
};
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) {
// Cache write failed (non-critical)
// 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> = { @@ -925,19 +925,25 @@ export const KIND_METADATA: Record<number, KindMetadata> = {
writable: true,
helpText: {
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) => ({
kind: KIND.REPO_ANNOUNCEMENT,
pubkey,
created_at: timestamp,
content: 'A Nostr-based Git repository',
content: '',
tags: [
['d', 'my-repo'],
['name', 'My Repository'],
['about', 'Repository description'],
['web', 'https://example.com/repo'],
['git', 'https://git.example.com/repo.git']
['d', 'Alexandria'],
['name', 'Alexandria'],
['description', 'Implementation of Nostr Knowledge Base (NKBIP-01) and vector embeddings (NKBIP-02). Can be used as an eReader app.'],
['clone', 'git@github.com:ShadowySupercode/gc-alexandria.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: '...',
sig: '...'

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

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

34
src/routes/repos/+page.svelte

@ -275,6 +275,12 @@ @@ -275,6 +275,12 @@
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
function decodePubkeyToHex(input: string): string | null {
@ -597,6 +603,11 @@ @@ -597,6 +603,11 @@
role="button"
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">
<h3 class="repo-name">{getRepoName(repo)}</h3>
<span class="repo-kind">Kind {repo.kind}</span>
@ -745,6 +756,29 @@ @@ -745,6 +756,29 @@
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 {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-post, #1f2937);

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

@ -855,6 +855,18 @@ @@ -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 {
if (!repoEvent || !Array.isArray(repoEvent.tags)) return false;
return repoEvent.tags.some(t => Array.isArray(t) && t[0] === 'primary');
@ -1078,26 +1090,40 @@ @@ -1078,26 +1090,40 @@
<p class="text-fog-text dark:text-fog-dark-text">Repository not found.</p>
</div>
{: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-title-row">
<h1 class="font-bold text-fog-text dark:text-fog-dark-text font-mono mb-6" style="font-size: 1.5em;">
{getRepoName()}
</h1>
{#if repoEvent}
<EventMenu event={repoEvent} showContentActions={true} />
<div class="repo-header-top">
{#if getImageUrl()}
<div class="repo-profile-image-container">
<img src={getImageUrl()!} alt="{getRepoName()}" class="repo-profile-image" />
</div>
{/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 class="repo-title-section">
<div class="repo-title-row">
<h1 class="font-bold text-fog-text dark:text-fog-dark-text font-mono mb-6" style="font-size: 1.5em;">
{getRepoName()}
</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>
{/if}
{#if getRepoDescription()}
<p class="text-fog-text-light dark:text-fog-dark-text-light mb-4">
{getRepoDescription()}
</p>
{/if}
</div>
<!-- Tabs -->
<div class="tabs">
@ -1484,6 +1510,29 @@ @@ -1484,6 +1510,29 @@
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 {
border-bottom: 1px solid var(--fog-border, #e5e7eb);
padding-bottom: 1rem;
@ -1493,6 +1542,42 @@ @@ -1493,6 +1542,42 @@
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 {
display: flex;
align-items: flex-start;

3
src/routes/write/+page.svelte

@ -23,12 +23,13 @@ @@ -23,12 +23,13 @@
const cloneData = JSON.parse(cloneDataStr);
// Construct event from clone data
// Use nullish coalescing to properly handle kind 0
// Explicitly preserve content (important for kind 0 which has stringified JSON)
initialEvent = {
id: '',
pubkey: '',
created_at: Math.floor(Date.now() / 1000),
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 || [],
sig: ''
};

Loading…
Cancel
Save