A fork of Woomarks that saves to pds

search users

Changed files
+203 -31
+18
index.html
··· 35 35 class="param-input" 36 36 /> 37 37 </div> 38 + <div class="param-group"> 39 + <label for="guestSearchInput" class="param-label">Or view someone's bookmarks without login</label> 40 + <input 41 + type="text" 42 + id="guestSearchInput" 43 + class="param-input" 44 + placeholder="username.bsky.social" 45 + /> 46 + </div> 38 47 <menu class="param-menu"> 39 48 <button id="loginBtn" type="button" class="param-btn dark">Login</button> 49 + <button id="guestViewBtn" type="button" class="param-btn">View Bookmarks</button> 40 50 </menu> 41 51 </form> 42 52 </dialog> ··· 46 56 <b><a id="headerTitle" href="">woomarks</a></b> 47 57 <a href="./faq.html">FAQ</a> 48 58 <span id="connectionStatus" class="connection-status"></span> 59 + <span id="viewingUser" class="viewing-user" style="display: none;"></span> 49 60 </div> 61 + <input 62 + type="text" 63 + id="userSearchInput" 64 + placeholder="user.bsky.social" 65 + title="View another user's bookmarks" 66 + style="display: none; margin-right: 0.5vw;" 67 + /> 50 68 <button id="logoutBtn" class="param-btn" style="display: none;">Logout</button> 51 69 <button id="openEmptyDialogBtn" data-umami-event="Open creation modal" class="param-btn"><span class="btn-text">Add</span> ➕</button> 52 70
+185 -31
script.js
··· 32 32 let userDid = null; 33 33 let bookmarks = []; 34 34 let reversedOrder = false; 35 + let viewingUserDid = null; 36 + let viewingUserHandle = null; 37 + let isViewingOtherUser = false; 35 38 36 39 // ====== DOM Elements ====== 37 40 const loginDialog = document.getElementById("loginDialog"); ··· 50 53 const openEmptyDialogBtn = document.getElementById("openEmptyDialogBtn"); 51 54 const searchInput = document.getElementById("searchInput"); 52 55 const sortToggleBtn = document.getElementById("sortToggleBtn"); 56 + const userSearchInput = document.getElementById("userSearchInput"); 57 + const viewingUser = document.getElementById("viewingUser"); 58 + const guestSearchInput = document.getElementById("guestSearchInput"); 59 + const guestViewBtn = document.getElementById("guestViewBtn"); 53 60 54 61 // ====== AT Protocol Functions ====== 62 + 63 + /** 64 + * Resolve handle to DID and PDS 65 + */ 66 + async function resolveHandle(handle) { 67 + if (!atpAgent && !window.AtpAgent) return null; 68 + 69 + try { 70 + const agent = atpAgent || new window.AtpAgent({ 71 + service: "https://bsky.social", 72 + }); 73 + 74 + // First resolve handle to DID 75 + const response = await agent.com.atproto.identity.resolveHandle({ 76 + handle: handle.replace('@', '') 77 + }); 78 + 79 + const did = response.data.did; 80 + 81 + // Now resolve DID to get PDS URL 82 + const didDoc = await fetch(`https://plc.directory/${did}`).then(res => res.json()); 83 + 84 + // Find the PDS service endpoint 85 + let pdsUrl = "https://bsky.social"; // fallback 86 + if (didDoc.service) { 87 + const pdsService = didDoc.service.find(s => s.type === "AtprotoPersonalDataServer"); 88 + if (pdsService && pdsService.serviceEndpoint) { 89 + pdsUrl = pdsService.serviceEndpoint; 90 + } 91 + } 92 + 93 + return { did, pdsUrl }; 94 + } catch (error) { 95 + console.error("Failed to resolve handle:", error); 96 + return null; 97 + } 98 + } 55 99 56 100 /** 57 101 * Initialize AT Protocol agent with stored session ··· 141 185 /** 142 186 * Load bookmarks from PDS 143 187 */ 144 - async function loadBookmarks() { 145 - if (!atpAgent || !userDid) return; 188 + async function loadBookmarks(targetDid = null, targetPdsUrl = null) { 189 + const did = targetDid || userDid; 190 + if (!did) return; 191 + 192 + // Create agent if needed for public access 193 + let agent = atpAgent; 194 + if (!agent || targetPdsUrl) { 195 + const serviceUrl = targetPdsUrl || "https://bsky.social"; 196 + agent = new window.AtpAgent({ 197 + service: serviceUrl, 198 + }); 199 + } 146 200 147 201 try { 148 202 updateConnectionStatus("connecting"); 149 203 150 - const response = await atpAgent.com.atproto.repo.listRecords({ 151 - repo: userDid, 204 + // First try to describe the repo to see if it exists 205 + try { 206 + await agent.com.atproto.repo.describeRepo({ 207 + repo: did, 208 + }); 209 + } catch (describeError) { 210 + console.error("Repo describe failed:", describeError); 211 + bookmarks = []; 212 + renderBookmarks(); 213 + updateConnectionStatus("connected"); 214 + alert("User has no bookmarks or bookmarks are not accessible"); 215 + return; 216 + } 217 + 218 + const response = await agent.com.atproto.repo.listRecords({ 219 + repo: did, 152 220 collection: BOOKMARK_LEXICON, 153 221 }); 154 222 155 223 bookmarks = response.data.records.map(record => ({ 156 - uri: record.uri, 224 + atUri: record.uri, // AT Protocol record URI 157 225 cid: record.cid, 158 - ...record.value 226 + ...record.value // Contains subject, title, tags, etc. 159 227 })); 160 228 161 229 renderBookmarks(); 162 230 updateConnectionStatus("connected"); 163 231 } catch (error) { 164 232 console.error("Failed to load bookmarks:", error); 165 - updateConnectionStatus("disconnected"); 233 + if (error.message?.includes("Could not find repo") || error.message?.includes("not found") || error.message?.includes("RecordNotFound")) { 234 + bookmarks = []; 235 + renderBookmarks(); 236 + updateConnectionStatus("connected"); 237 + alert("User has no bookmarks with this lexicon"); 238 + } else { 239 + updateConnectionStatus("disconnected"); 240 + } 166 241 } 167 242 } 168 243 ··· 174 249 const url = urlInput.value.trim(); 175 250 const rawTags = tagsInput.value.trim(); 176 251 177 - if (!title || !url || !atpAgent || !userDid) return; 252 + if (!url || !atpAgent || !userDid) return; 178 253 179 254 const tags = rawTags.split(",").map(t => t.trim()).filter(Boolean); 180 255 181 256 const bookmarkRecord = { 182 257 $type: BOOKMARK_LEXICON, 183 - uri: url, 184 - title, 258 + subject: url, 185 259 tags, 186 260 createdAt: new Date().toISOString(), 187 261 }; 262 + 263 + // Add optional title if provided 264 + if (title) { 265 + bookmarkRecord.title = title; 266 + } 188 267 189 268 try { 190 269 updateConnectionStatus("connecting"); ··· 197 276 198 277 // Add to local array 199 278 bookmarks.push({ 200 - uri: response.data.uri, 279 + atUri: response.data.uri, 201 280 cid: response.data.cid, 202 281 ...bookmarkRecord 203 282 }); ··· 224 303 try { 225 304 updateConnectionStatus("connecting"); 226 305 306 + console.log("Deleting bookmark with URI:", uri); 227 307 const rkey = uri.split("/").pop(); 228 - await atpAgent.com.atproto.repo.deleteRecord({ 308 + console.log("Extracted rkey:", rkey); 309 + 310 + const deleteParams = { 229 311 repo: userDid, 230 312 collection: BOOKMARK_LEXICON, 231 313 rkey, 232 - }); 314 + }; 315 + console.log("Delete parameters:", deleteParams); 316 + 317 + const result = await atpAgent.com.atproto.repo.deleteRecord(deleteParams); 318 + console.log("Delete result:", result); 233 319 320 + console.log("Successfully deleted from PDS"); 321 + 234 322 // Remove from local array 235 - bookmarks = bookmarks.filter(bookmark => bookmark.uri !== uri); 323 + const beforeCount = bookmarks.length; 324 + bookmarks = bookmarks.filter(bookmark => bookmark.atUri !== uri); 325 + console.log(`Removed from local array: ${beforeCount} -> ${bookmarks.length}`); 326 + 236 327 renderBookmarks(); 237 328 updateConnectionStatus("connected"); 238 329 } catch (error) { 239 330 console.error("Failed to delete bookmark:", error); 331 + alert("Failed to delete bookmark: " + error.message); 240 332 updateConnectionStatus("disconnected"); 241 333 } 242 334 } ··· 267 359 } 268 360 269 361 function showMainUI() { 270 - openEmptyDialogBtn.style.display = "inline-block"; 362 + openEmptyDialogBtn.style.display = isViewingOtherUser ? "none" : "inline-block"; 271 363 sortToggleBtn.style.display = "inline-block"; 272 364 searchInput.style.display = "inline-block"; 365 + userSearchInput.style.display = "inline-block"; 273 366 logoutBtn.style.display = "inline-block"; 367 + } 368 + 369 + function updateViewingUserUI() { 370 + if (isViewingOtherUser) { 371 + viewingUser.textContent = `Viewing: ${viewingUserHandle}`; 372 + viewingUser.style.display = "inline"; 373 + openEmptyDialogBtn.style.display = "none"; 374 + } else { 375 + viewingUser.style.display = "none"; 376 + openEmptyDialogBtn.style.display = atpAgent ? "inline-block" : "none"; 377 + } 274 378 } 275 379 276 380 // ====== Utility Functions ====== ··· 317 421 const displayBookmarks = reversedOrder ? bookmarks : [...bookmarks].reverse(); 318 422 319 423 displayBookmarks.forEach(bookmark => { 320 - const title = bookmark.title; 321 - const url = bookmark.uri; 424 + const title = bookmark.title || bookmark.subject; // fallback to subject as title if no title 425 + const url = bookmark.subject || bookmark.uri; // support both old and new schema 322 426 const tags = bookmark.tags || []; 323 427 324 - if (!title || !url) return; 428 + if (!url) return; 325 429 326 430 const displayTitle = title.replace(/^https?:\/\/(www\.)?/i, ""); 327 431 const [bgColor, fontColor] = getColorPairByTitle(title, COLOR_PAIRS); ··· 333 437 container.style.color = fontColor; 334 438 container.style.fontFamily = `'${fontFamily}', sans-serif`; 335 439 336 - // Delete Button 337 - const closeBtn = document.createElement("button"); 338 - closeBtn.className = "delete-btn"; 339 - closeBtn.textContent = "x"; 340 - closeBtn.title = "Delete this bookmark"; 341 - closeBtn.addEventListener("click", e => { 342 - e.stopPropagation(); 343 - e.preventDefault(); 344 - if (confirm("Delete this bookmark?")) { 345 - deleteBookmark(bookmark.uri); 346 - } 347 - }); 348 - container.appendChild(closeBtn); 440 + // Delete Button (only show for own bookmarks) 441 + if (!isViewingOtherUser) { 442 + const closeBtn = document.createElement("button"); 443 + closeBtn.className = "delete-btn"; 444 + closeBtn.textContent = "x"; 445 + closeBtn.title = "Delete this bookmark"; 446 + closeBtn.addEventListener("click", e => { 447 + e.stopPropagation(); 448 + e.preventDefault(); 449 + if (confirm("Delete this bookmark?")) { 450 + deleteBookmark(bookmark.atUri); 451 + } 452 + }); 453 + container.appendChild(closeBtn); 454 + } 349 455 350 456 // Anchor (bookmark link) 351 457 const anchor = document.createElement("a"); ··· 491 597 loginBtn.addEventListener("click", login); 492 598 logoutBtn.addEventListener("click", logout); 493 599 600 + // Guest view functionality 601 + guestViewBtn?.addEventListener("click", async () => { 602 + const handle = guestSearchInput.value.trim(); 603 + if (!handle) return; 604 + 605 + updateConnectionStatus("connecting"); 606 + const result = await resolveHandle(handle); 607 + if (result) { 608 + isViewingOtherUser = true; 609 + viewingUserDid = result.did; 610 + viewingUserHandle = handle; 611 + loginDialog.close(); 612 + showMainUI(); 613 + await loadBookmarks(result.did, result.pdsUrl); 614 + updateViewingUserUI(); 615 + } else { 616 + alert("User not found"); 617 + updateConnectionStatus("disconnected"); 618 + } 619 + }); 620 + 494 621 // Dialog 495 622 saveBtn.addEventListener("click", saveBookmark); 496 623 cancelBtn?.addEventListener("click", () => { ··· 534 661 sortToggleBtn.lastChild.textContent = " ▼"; 535 662 } else { 536 663 sortToggleBtn.lastChild.textContent = " ▲"; 664 + } 665 + }); 666 + 667 + // User search 668 + userSearchInput?.addEventListener("keypress", async (e) => { 669 + if (e.key === "Enter") { 670 + const handle = e.target.value.trim(); 671 + if (!handle) { 672 + // Empty search - go back to own bookmarks 673 + isViewingOtherUser = false; 674 + viewingUserDid = null; 675 + viewingUserHandle = null; 676 + if (userDid) await loadBookmarks(); 677 + updateViewingUserUI(); 678 + return; 679 + } 680 + 681 + const result = await resolveHandle(handle); 682 + if (result) { 683 + isViewingOtherUser = true; 684 + viewingUserDid = result.did; 685 + viewingUserHandle = handle; 686 + await loadBookmarks(result.did, result.pdsUrl); 687 + updateViewingUserUI(); 688 + } else { 689 + alert("User not found"); 690 + } 537 691 } 538 692 }); 539 693