BookmarksView.svelte raw
1 <script>
2 import { onMount } from 'svelte';
3 import { bookmarkList, bookmarksLoading } from './libraryStores.js';
4 import { fetchEvents, fetchUserProfile } from './nostr.js';
5
6 export let isLoggedIn = false;
7 export let userPubkey = "";
8
9 let initialized = false;
10 let resolvedBookmarks = [];
11
12 onMount(() => {
13 if (isLoggedIn && userPubkey && !initialized) {
14 initialized = true;
15 loadBookmarks();
16 }
17 });
18
19 async function loadBookmarks() {
20 if ($bookmarksLoading || !userPubkey) return;
21 bookmarksLoading.set(true);
22
23 try {
24 // Fetch kind 10003 (bookmark list)
25 const events = await fetchEvents(
26 [{ kinds: [10003], authors: [userPubkey], limit: 1 }],
27 { timeout: 10000, useCache: false }
28 );
29
30 if (!events || events.length === 0) {
31 bookmarksLoading.set(false);
32 return;
33 }
34
35 // Most recent kind 10003
36 const bookmarkEvent = events.sort((a, b) => b.created_at - a.created_at)[0];
37
38 // Extract bookmarked event IDs from e-tags and a-tags
39 const eTags = (bookmarkEvent.tags || []).filter(t => t[0] === "e");
40 const aTags = (bookmarkEvent.tags || []).filter(t => t[0] === "a");
41
42 bookmarkList.set([...eTags, ...aTags]);
43
44 // Resolve e-tag bookmarks (fetch the actual events)
45 if (eTags.length > 0) {
46 const ids = eTags.map(t => t[1]).filter(Boolean);
47 const resolved = await fetchEvents(
48 [{ ids, limit: 100 }],
49 { timeout: 10000, useCache: false }
50 );
51
52 resolvedBookmarks = (resolved || []).sort((a, b) => b.created_at - a.created_at);
53 }
54 } catch (err) {
55 console.error("[Bookmarks] Error:", err);
56 } finally {
57 bookmarksLoading.set(false);
58 }
59 }
60
61 function getKindLabel(kind) {
62 switch (kind) {
63 case 1: return 'Note';
64 case 30023: return 'Article';
65 case 30040: return 'Publication';
66 case 30041: return 'Section';
67 default: return `Kind ${kind}`;
68 }
69 }
70
71 function truncate(text, len = 120) {
72 if (!text || text.length <= len) return text || '';
73 return text.slice(0, len) + '...';
74 }
75
76 function formatDate(ts) {
77 if (!ts) return '';
78 return new Date(ts * 1000).toLocaleDateString();
79 }
80 </script>
81
82 <div class="bookmarks-view">
83 <div class="bookmarks-header">
84 <h2>Bookmarks</h2>
85 <span class="bookmark-count">{$bookmarkList.length} item{$bookmarkList.length !== 1 ? 's' : ''}</span>
86 </div>
87
88 {#if !isLoggedIn}
89 <div class="bookmarks-empty">Log in to see your bookmarks.</div>
90 {:else if $bookmarksLoading}
91 <div class="bookmarks-loading"><div class="spinner"></div></div>
92 {:else if resolvedBookmarks.length === 0 && $bookmarkList.length === 0}
93 <div class="bookmarks-empty">
94 <p>No bookmarks yet.</p>
95 <p class="hint">Bookmark notes and articles to find them here.</p>
96 </div>
97 {:else}
98 <div class="bookmark-list">
99 {#each resolvedBookmarks as item (item.id)}
100 <div class="bookmark-item">
101 <div class="bookmark-kind">{getKindLabel(item.kind)}</div>
102 <div class="bookmark-content">{truncate(item.content)}</div>
103 <div class="bookmark-meta">
104 <span class="bookmark-author">{item.pubkey?.slice(0, 10)}...</span>
105 <span class="bookmark-date">{formatDate(item.created_at)}</span>
106 </div>
107 </div>
108 {/each}
109
110 <!-- Unresolved a-tag bookmarks -->
111 {#each $bookmarkList.filter(t => t[0] === "a") as tag}
112 <div class="bookmark-item">
113 <div class="bookmark-kind">Reference</div>
114 <div class="bookmark-content bookmark-ref">{tag[1]}</div>
115 </div>
116 {/each}
117 </div>
118 {/if}
119 </div>
120
121 <style>
122 .bookmarks-view {
123 width: 100%;
124 max-width: 640px;
125 height: 100%;
126 overflow-y: auto;
127 margin: 0 auto;
128 }
129
130 .bookmarks-header {
131 display: flex;
132 align-items: center;
133 justify-content: space-between;
134 padding: 0.75em 1em;
135 border-bottom: 1px solid var(--border-color);
136 position: sticky;
137 top: 0;
138 background: var(--bg-color);
139 z-index: 1;
140 }
141
142 .bookmarks-header h2 {
143 margin: 0;
144 font-size: 1.1rem;
145 color: var(--text-color);
146 }
147
148 .bookmark-count {
149 font-size: 0.75rem;
150 color: var(--text-muted);
151 }
152
153 .bookmark-list {
154 padding: 0;
155 }
156
157 .bookmark-item {
158 padding: 0.75em 1em;
159 border-bottom: 1px solid var(--border-color);
160 transition: background 0.1s;
161 }
162
163 .bookmark-item:hover {
164 background: var(--primary-bg);
165 }
166
167 .bookmark-kind {
168 font-size: 0.7rem;
169 color: var(--primary);
170 font-weight: 600;
171 text-transform: uppercase;
172 letter-spacing: 0.5px;
173 margin-bottom: 0.25em;
174 }
175
176 .bookmark-content {
177 font-size: 0.85rem;
178 color: var(--text-color);
179 line-height: 1.4;
180 word-break: break-word;
181 }
182
183 .bookmark-ref {
184 font-family: monospace;
185 font-size: 0.75rem;
186 color: var(--text-muted);
187 }
188
189 .bookmark-meta {
190 display: flex;
191 gap: 0.75em;
192 margin-top: 0.3em;
193 font-size: 0.72rem;
194 color: var(--text-muted);
195 }
196
197 .bookmarks-empty {
198 text-align: center;
199 padding: 3em 1em;
200 color: var(--text-muted);
201 font-size: 0.85rem;
202 }
203
204 .bookmarks-empty p {
205 margin: 0 0 0.3em;
206 }
207
208 .hint {
209 font-size: 0.78rem;
210 }
211
212 .bookmarks-loading {
213 display: flex;
214 justify-content: center;
215 padding: 3em;
216 }
217
218 .spinner {
219 width: 24px;
220 height: 24px;
221 border: 2px solid var(--border-color);
222 border-top-color: var(--primary);
223 border-radius: 50%;
224 animation: spin 0.8s linear infinite;
225 }
226
227 @keyframes spin {
228 to { transform: rotate(360deg); }
229 }
230 </style>
231