+18
index.html
+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
+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