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. +

+
+
+ + Get It + + +
+
+ )} + + { }
+
+ {[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