From d76abc060915f12037ee5627603b4a519a139b9f Mon Sep 17 00:00:00 2001
From: scanash00
Date: Mon, 26 Jan 2026 19:45:30 -0900
Subject: [PATCH] Add iOS shortcut and fix being able to make duplicate
bookmarks
---
backend/internal/api/annotations.go | 3 +-
backend/internal/api/annotations_helpers.go | 7 +-
backend/internal/api/handler.go | 3 +
backend/internal/db/queries_bookmarks.go | 24 +++++
extension/background/service-worker.js | 1 +
extension/popup/popup.js | 19 +++-
extension/sidepanel/sidepanel.js | 19 +++-
web/src/css/base.css | 2 +
web/src/css/layout.css | 2 +-
web/src/pages/Feed.jsx | 97 ++++++++++++++++++---
web/src/pages/Profile.jsx | 39 +++++++--
11 files changed, 186 insertions(+), 30 deletions(-)
diff --git a/backend/internal/api/annotations.go b/backend/internal/api/annotations.go
index e6ad150..e6713bb 100644
--- a/backend/internal/api/annotations.go
+++ b/backend/internal/api/annotations.go
@@ -638,8 +638,7 @@ func (s *AnnotationService) CreateBookmark(w http.ResponseWriter, r *http.Reques
var result *xrpc.CreateRecordOutput
if existing, err := s.checkDuplicateBookmark(session.DID, req.URL); err == nil && existing != nil {
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]string{"uri": existing.URI, "cid": *existing.CID})
+ http.Error(w, "Bookmark already exists", http.StatusConflict)
return
}
diff --git a/backend/internal/api/annotations_helpers.go b/backend/internal/api/annotations_helpers.go
index f92697f..655b02c 100644
--- a/backend/internal/api/annotations_helpers.go
+++ b/backend/internal/api/annotations_helpers.go
@@ -46,12 +46,13 @@ func (s *AnnotationService) checkDuplicateHighlight(did, url string, selector js
}
func (s *AnnotationService) checkDuplicateBookmark(did, url string) (*db.Bookmark, error) {
- recentBooks, err := s.db.GetBookmarksByAuthor(did, 5, 0)
+ urlHash := db.HashURL(url)
+ bookmarks, err := s.db.GetBookmarksByTargetHash(urlHash, 50, 0)
if err != nil {
return nil, err
}
- for _, b := range recentBooks {
- if b.Source == url && time.Since(b.CreatedAt) < 10*time.Second {
+ for _, b := range bookmarks {
+ if b.AuthorDID == did && b.Source == url {
return &b, nil
}
}
diff --git a/backend/internal/api/handler.go b/backend/internal/api/handler.go
index 038a8e8..728c301 100644
--- a/backend/internal/api/handler.go
+++ b/backend/internal/api/handler.go
@@ -614,9 +614,11 @@ func (h *Handler) GetByTarget(w http.ResponseWriter, r *http.Request) {
annotations, _ := h.db.GetAnnotationsByTargetHash(urlHash, limit, offset)
highlights, _ := h.db.GetHighlightsByTargetHash(urlHash, limit, offset)
+ bookmarks, _ := h.db.GetBookmarksByTargetHash(urlHash, limit, offset)
enrichedAnnotations, _ := hydrateAnnotations(h.db, annotations, h.getViewerDID(r))
enrichedHighlights, _ := hydrateHighlights(h.db, highlights, h.getViewerDID(r))
+ enrichedBookmarks, _ := hydrateBookmarks(h.db, bookmarks, h.getViewerDID(r))
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
@@ -625,6 +627,7 @@ func (h *Handler) GetByTarget(w http.ResponseWriter, r *http.Request) {
"sourceHash": urlHash,
"annotations": enrichedAnnotations,
"highlights": enrichedHighlights,
+ "bookmarks": enrichedBookmarks,
})
}
diff --git a/backend/internal/db/queries_bookmarks.go b/backend/internal/db/queries_bookmarks.go
index bfcbd11..4446c5c 100644
--- a/backend/internal/db/queries_bookmarks.go
+++ b/backend/internal/db/queries_bookmarks.go
@@ -194,3 +194,27 @@ func (db *DB) GetBookmarkURIs(authorDID string) ([]string, error) {
}
return uris, nil
}
+
+func (db *DB) GetBookmarksByTargetHash(targetHash string, limit, offset int) ([]Bookmark, error) {
+ rows, err := db.Query(db.Rebind(`
+ SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
+ FROM bookmarks
+ WHERE source_hash = ?
+ ORDER BY created_at DESC
+ LIMIT ? OFFSET ?
+ `), targetHash, limit, offset)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var bookmarks []Bookmark
+ for rows.Next() {
+ var b Bookmark
+ if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
+ return nil, err
+ }
+ bookmarks = append(bookmarks, b)
+ }
+ return bookmarks, nil
+}
diff --git a/extension/background/service-worker.js b/extension/background/service-worker.js
index c3c6871..16a3bac 100644
--- a/extension/background/service-worker.js
+++ b/extension/background/service-worker.js
@@ -384,6 +384,7 @@ async function handleMessage(request, sender, sendResponse) {
const items = [
...(data.annotations || []),
...(data.highlights || []),
+ ...(data.bookmarks || []),
];
items.forEach((item) => {
const id = item.uri || item.id;
diff --git a/extension/popup/popup.js b/extension/popup/popup.js
index 23bc6b3..850ff5d 100644
--- a/extension/popup/popup.js
+++ b/extension/popup/popup.js
@@ -425,7 +425,24 @@ document.addEventListener("DOMContentLoaded", async () => {
});
if (res.success) {
- renderAnnotations(res.data);
+ if (currentUserDid) {
+ const isBookmarked = res.data.some(
+ (item) =>
+ item.type === "Bookmark" && item.creator.did === currentUserDid,
+ );
+ if (els.bookmarkBtn) {
+ if (isBookmarked) {
+ els.bookmarkBtn.textContent = "✓ Bookmarked";
+ els.bookmarkBtn.disabled = true;
+ } else {
+ els.bookmarkBtn.textContent = "Bookmark Page";
+ els.bookmarkBtn.disabled = false;
+ }
+ }
+ }
+
+ const listItems = res.data.filter((item) => item.type !== "Bookmark");
+ renderAnnotations(listItems);
}
} catch (err) {
console.error("Load annotations error:", err);
diff --git a/extension/sidepanel/sidepanel.js b/extension/sidepanel/sidepanel.js
index 2ca8d95..d1a9227 100644
--- a/extension/sidepanel/sidepanel.js
+++ b/extension/sidepanel/sidepanel.js
@@ -357,7 +357,24 @@ document.addEventListener("DOMContentLoaded", async () => {
});
if (res.success) {
- renderAnnotations(res.data);
+ if (currentUserDid) {
+ const isBookmarked = res.data.some(
+ (item) =>
+ item.type === "Bookmark" && item.creator.did === currentUserDid,
+ );
+ if (els.bookmarkBtn) {
+ if (isBookmarked) {
+ els.bookmarkBtn.textContent = "✓ Bookmarked";
+ els.bookmarkBtn.disabled = true;
+ } else {
+ els.bookmarkBtn.textContent = "Bookmark Page";
+ els.bookmarkBtn.disabled = false;
+ }
+ }
+ }
+
+ const listItems = res.data.filter((item) => item.type !== "Bookmark");
+ renderAnnotations(listItems);
}
} catch (err) {
console.error("Load annotations error:", err);
diff --git a/web/src/css/base.css b/web/src/css/base.css
index 9e77eb6..5e830e9 100644
--- a/web/src/css/base.css
+++ b/web/src/css/base.css
@@ -30,6 +30,7 @@
"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono:
"JetBrains Mono", source-code-pro, Menlo, Monaco, Consolas, monospace;
+ --nav-bg: rgba(9, 9, 11, 0.9);
}
[data-theme="light"] {
@@ -54,6 +55,7 @@
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md:
0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ --nav-bg: rgba(255, 255, 255, 0.9);
}
* {
diff --git a/web/src/css/layout.css b/web/src/css/layout.css
index dacb024..e60af30 100644
--- a/web/src/css/layout.css
+++ b/web/src/css/layout.css
@@ -407,7 +407,7 @@
bottom: 0;
left: 0;
right: 0;
- background: rgba(9, 9, 11, 0.9);
+ background: var(--nav-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-top: 1px solid var(--border);
diff --git a/web/src/pages/Feed.jsx b/web/src/pages/Feed.jsx
index 5e5ab7f..490705c 100644
--- a/web/src/pages/Feed.jsx
+++ b/web/src/pages/Feed.jsx
@@ -39,6 +39,23 @@ export default function Feed() {
uri: null,
});
+ const [showIosBanner, setShowIosBanner] = useState(false);
+
+ useEffect(() => {
+ const isIOS =
+ /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
+ const hasDismissed = localStorage.getItem("iosBannerDismissed");
+
+ if (isIOS && !hasDismissed) {
+ setShowIosBanner(true);
+ }
+ }, []);
+
+ const dismissIosBanner = () => {
+ setShowIosBanner(false);
+ localStorage.setItem("iosBannerDismissed", "true");
+ };
+
const { user } = useAuth();
useEffect(() => {
@@ -77,21 +94,21 @@ export default function Feed() {
const filteredAnnotations =
feedType === "all" ||
- feedType === "popular" ||
- feedType === "semble" ||
- feedType === "margin" ||
- feedType === "my-feed"
+ feedType === "popular" ||
+ feedType === "semble" ||
+ feedType === "margin" ||
+ feedType === "my-feed"
? filter === "all"
? annotations
: annotations.filter((a) => {
- if (filter === "commenting")
- return a.motivation === "commenting" || a.type === "Annotation";
- if (filter === "highlighting")
- return a.motivation === "highlighting" || a.type === "Highlight";
- if (filter === "bookmarking")
- return a.motivation === "bookmarking" || a.type === "Bookmark";
- return a.motivation === filter;
- })
+ if (filter === "commenting")
+ return a.motivation === "commenting" || a.type === "Annotation";
+ if (filter === "highlighting")
+ return a.motivation === "highlighting" || a.type === "Highlight";
+ if (filter === "bookmarking")
+ return a.motivation === "bookmarking" || a.type === "Bookmark";
+ return a.motivation === filter;
+ })
: annotations;
return (
@@ -132,7 +149,61 @@ export default function Feed() {
)}
- {}
+ {showIosBanner && (
+
+
+
+ Get the iOS Shortcut
+
+
+ Easily save links from Safari using our new shortcut.
+
+
+
+
+ )}
+
+ { }
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+
+ );
+ }
+
if (!handle) {
- return ;
+ return ;
}
useEffect(() => {
@@ -418,19 +441,17 @@ export default function Profile() {
Save bookmarks from Safari's share sheet.
e.preventDefault()}
>
- Coming Soon
+ Get Shortcut
--
2.51.2