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