Write on the margins of the internet. Powered by the AT Protocol.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add iOS shortcut and fix being able to make duplicate bookmarks

scanash00 d76abc06 92fc8b21

+186 -30
+1 -2
backend/internal/api/annotations.go
··· 638 638 var result *xrpc.CreateRecordOutput 639 639 640 640 if existing, err := s.checkDuplicateBookmark(session.DID, req.URL); err == nil && existing != nil { 641 - w.Header().Set("Content-Type", "application/json") 642 - json.NewEncoder(w).Encode(map[string]string{"uri": existing.URI, "cid": *existing.CID}) 641 + http.Error(w, "Bookmark already exists", http.StatusConflict) 643 642 return 644 643 } 645 644
+4 -3
backend/internal/api/annotations_helpers.go
··· 46 46 } 47 47 48 48 func (s *AnnotationService) checkDuplicateBookmark(did, url string) (*db.Bookmark, error) { 49 - recentBooks, err := s.db.GetBookmarksByAuthor(did, 5, 0) 49 + urlHash := db.HashURL(url) 50 + bookmarks, err := s.db.GetBookmarksByTargetHash(urlHash, 50, 0) 50 51 if err != nil { 51 52 return nil, err 52 53 } 53 - for _, b := range recentBooks { 54 - if b.Source == url && time.Since(b.CreatedAt) < 10*time.Second { 54 + for _, b := range bookmarks { 55 + if b.AuthorDID == did && b.Source == url { 55 56 return &b, nil 56 57 } 57 58 }
+3
backend/internal/api/handler.go
··· 614 614 615 615 annotations, _ := h.db.GetAnnotationsByTargetHash(urlHash, limit, offset) 616 616 highlights, _ := h.db.GetHighlightsByTargetHash(urlHash, limit, offset) 617 + bookmarks, _ := h.db.GetBookmarksByTargetHash(urlHash, limit, offset) 617 618 618 619 enrichedAnnotations, _ := hydrateAnnotations(h.db, annotations, h.getViewerDID(r)) 619 620 enrichedHighlights, _ := hydrateHighlights(h.db, highlights, h.getViewerDID(r)) 621 + enrichedBookmarks, _ := hydrateBookmarks(h.db, bookmarks, h.getViewerDID(r)) 620 622 621 623 w.Header().Set("Content-Type", "application/json") 622 624 json.NewEncoder(w).Encode(map[string]interface{}{ ··· 625 627 "sourceHash": urlHash, 626 628 "annotations": enrichedAnnotations, 627 629 "highlights": enrichedHighlights, 630 + "bookmarks": enrichedBookmarks, 628 631 }) 629 632 } 630 633
+24
backend/internal/db/queries_bookmarks.go
··· 194 194 } 195 195 return uris, nil 196 196 } 197 + 198 + func (db *DB) GetBookmarksByTargetHash(targetHash string, limit, offset int) ([]Bookmark, error) { 199 + rows, err := db.Query(db.Rebind(` 200 + SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid 201 + FROM bookmarks 202 + WHERE source_hash = ? 203 + ORDER BY created_at DESC 204 + LIMIT ? OFFSET ? 205 + `), targetHash, limit, offset) 206 + if err != nil { 207 + return nil, err 208 + } 209 + defer rows.Close() 210 + 211 + var bookmarks []Bookmark 212 + for rows.Next() { 213 + var b Bookmark 214 + 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 { 215 + return nil, err 216 + } 217 + bookmarks = append(bookmarks, b) 218 + } 219 + return bookmarks, nil 220 + }
+1
extension/background/service-worker.js
··· 384 384 const items = [ 385 385 ...(data.annotations || []), 386 386 ...(data.highlights || []), 387 + ...(data.bookmarks || []), 387 388 ]; 388 389 items.forEach((item) => { 389 390 const id = item.uri || item.id;
+18 -1
extension/popup/popup.js
··· 425 425 }); 426 426 427 427 if (res.success) { 428 - renderAnnotations(res.data); 428 + if (currentUserDid) { 429 + const isBookmarked = res.data.some( 430 + (item) => 431 + item.type === "Bookmark" && item.creator.did === currentUserDid, 432 + ); 433 + if (els.bookmarkBtn) { 434 + if (isBookmarked) { 435 + els.bookmarkBtn.textContent = "✓ Bookmarked"; 436 + els.bookmarkBtn.disabled = true; 437 + } else { 438 + els.bookmarkBtn.textContent = "Bookmark Page"; 439 + els.bookmarkBtn.disabled = false; 440 + } 441 + } 442 + } 443 + 444 + const listItems = res.data.filter((item) => item.type !== "Bookmark"); 445 + renderAnnotations(listItems); 429 446 } 430 447 } catch (err) { 431 448 console.error("Load annotations error:", err);
+18 -1
extension/sidepanel/sidepanel.js
··· 357 357 }); 358 358 359 359 if (res.success) { 360 - renderAnnotations(res.data); 360 + if (currentUserDid) { 361 + const isBookmarked = res.data.some( 362 + (item) => 363 + item.type === "Bookmark" && item.creator.did === currentUserDid, 364 + ); 365 + if (els.bookmarkBtn) { 366 + if (isBookmarked) { 367 + els.bookmarkBtn.textContent = "✓ Bookmarked"; 368 + els.bookmarkBtn.disabled = true; 369 + } else { 370 + els.bookmarkBtn.textContent = "Bookmark Page"; 371 + els.bookmarkBtn.disabled = false; 372 + } 373 + } 374 + } 375 + 376 + const listItems = res.data.filter((item) => item.type !== "Bookmark"); 377 + renderAnnotations(listItems); 361 378 } 362 379 } catch (err) { 363 380 console.error("Load annotations error:", err);
+2
web/src/css/base.css
··· 30 30 "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; 31 31 --font-mono: 32 32 "JetBrains Mono", source-code-pro, Menlo, Monaco, Consolas, monospace; 33 + --nav-bg: rgba(9, 9, 11, 0.9); 33 34 } 34 35 35 36 [data-theme="light"] { ··· 54 55 --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 55 56 --shadow-md: 56 57 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 58 + --nav-bg: rgba(255, 255, 255, 0.9); 57 59 } 58 60 59 61 * {
+1 -1
web/src/css/layout.css
··· 407 407 bottom: 0; 408 408 left: 0; 409 409 right: 0; 410 - background: rgba(9, 9, 11, 0.9); 410 + background: var(--nav-bg); 411 411 backdrop-filter: blur(12px); 412 412 -webkit-backdrop-filter: blur(12px); 413 413 border-top: 1px solid var(--border);
+84 -13
web/src/pages/Feed.jsx
··· 39 39 uri: null, 40 40 }); 41 41 42 + const [showIosBanner, setShowIosBanner] = useState(false); 43 + 44 + useEffect(() => { 45 + const isIOS = 46 + /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; 47 + const hasDismissed = localStorage.getItem("iosBannerDismissed"); 48 + 49 + if (isIOS && !hasDismissed) { 50 + setShowIosBanner(true); 51 + } 52 + }, []); 53 + 54 + const dismissIosBanner = () => { 55 + setShowIosBanner(false); 56 + localStorage.setItem("iosBannerDismissed", "true"); 57 + }; 58 + 42 59 const { user } = useAuth(); 43 60 44 61 useEffect(() => { ··· 77 94 78 95 const filteredAnnotations = 79 96 feedType === "all" || 80 - feedType === "popular" || 81 - feedType === "semble" || 82 - feedType === "margin" || 83 - feedType === "my-feed" 97 + feedType === "popular" || 98 + feedType === "semble" || 99 + feedType === "margin" || 100 + feedType === "my-feed" 84 101 ? filter === "all" 85 102 ? annotations 86 103 : annotations.filter((a) => { 87 - if (filter === "commenting") 88 - return a.motivation === "commenting" || a.type === "Annotation"; 89 - if (filter === "highlighting") 90 - return a.motivation === "highlighting" || a.type === "Highlight"; 91 - if (filter === "bookmarking") 92 - return a.motivation === "bookmarking" || a.type === "Bookmark"; 93 - return a.motivation === filter; 94 - }) 104 + if (filter === "commenting") 105 + return a.motivation === "commenting" || a.type === "Annotation"; 106 + if (filter === "highlighting") 107 + return a.motivation === "highlighting" || a.type === "Highlight"; 108 + if (filter === "bookmarking") 109 + return a.motivation === "bookmarking" || a.type === "Bookmark"; 110 + return a.motivation === filter; 111 + }) 95 112 : annotations; 96 113 97 114 return ( ··· 132 149 )} 133 150 </div> 134 151 135 - {} 152 + {showIosBanner && ( 153 + <div 154 + className="ios-banner" 155 + style={{ 156 + background: "var(--bg-secondary)", 157 + border: "1px solid var(--border)", 158 + borderRadius: "var(--radius-md)", 159 + padding: "12px", 160 + marginBottom: "20px", 161 + display: "flex", 162 + alignItems: "center", 163 + justifyContent: "space-between", 164 + gap: "12px", 165 + }} 166 + > 167 + <div style={{ flex: 1 }}> 168 + <h3 169 + style={{ 170 + fontSize: "0.9rem", 171 + fontWeight: 600, 172 + marginBottom: "4px", 173 + }} 174 + > 175 + Get the iOS Shortcut 176 + </h3> 177 + <p style={{ fontSize: "0.8rem", color: "var(--text-secondary)" }}> 178 + Easily save links from Safari using our new shortcut. 179 + </p> 180 + </div> 181 + <div style={{ display: "flex", gap: "8px", alignItems: "center" }}> 182 + <a 183 + href="https://www.icloud.com/shortcuts/21c87edf29b046db892c9e57dac6d1fd" 184 + target="_blank" 185 + rel="noopener noreferrer" 186 + className="btn btn-primary btn-sm" 187 + style={{ whiteSpace: "nowrap" }} 188 + > 189 + Get It 190 + </a> 191 + <button 192 + className="btn btn-sm" 193 + onClick={dismissIosBanner} 194 + style={{ 195 + color: "var(--text-tertiary)", 196 + padding: "4px", 197 + height: "auto", 198 + }} 199 + > 200 + 201 + </button> 202 + </div> 203 + </div> 204 + )} 205 + 206 + { } 136 207 <div 137 208 className="feed-filters" 138 209 style={{
+30 -9
web/src/pages/Profile.jsx
··· 62 62 } 63 63 64 64 export default function Profile() { 65 - const { handle } = useParams(); 66 - const { user } = useAuth(); 65 + const { handle: routeHandle } = useParams(); 66 + const { user, loading: authLoading } = useAuth(); 67 67 const [activeTab, setActiveTab] = useState("annotations"); 68 68 const [profile, setProfile] = useState(null); 69 69 const [annotations, setAnnotations] = useState([]); ··· 78 78 const [error, setError] = useState(null); 79 79 const [showEditModal, setShowEditModal] = useState(false); 80 80 81 + const handle = routeHandle || user?.did || user?.handle; 81 82 const isOwnProfile = user && (user.did === handle || user.handle === handle); 82 83 84 + if (authLoading) { 85 + return ( 86 + <div className="profile-page"> 87 + <div className="feed"> 88 + {[1, 2, 3].map((i) => ( 89 + <div key={i} className="card"> 90 + <div 91 + className="skeleton skeleton-text" 92 + style={{ width: "40%" }} 93 + /> 94 + <div className="skeleton skeleton-text" /> 95 + <div 96 + className="skeleton skeleton-text" 97 + style={{ width: "60%" }} 98 + /> 99 + </div> 100 + ))} 101 + </div> 102 + </div> 103 + ); 104 + } 105 + 83 106 if (!handle) { 84 - return <Navigate to={user ? `/profile/${user.did}` : "/login"} replace />; 107 + return <Navigate to="/login" replace />; 85 108 } 86 109 87 110 useEffect(() => { ··· 418 441 Save bookmarks from Safari&apos;s share sheet. 419 442 </p> 420 443 <a 421 - href="#" 444 + href="https://www.icloud.com/shortcuts/21c87edf29b046db892c9e57dac6d1fd" 445 + target="_blank" 446 + rel="noopener noreferrer" 422 447 className="btn btn-primary" 423 448 style={{ 424 449 display: "inline-flex", 425 450 alignItems: "center", 426 451 gap: "0.5rem", 427 - opacity: 0.5, 428 - pointerEvents: "none", 429 - cursor: "default", 430 452 }} 431 - onClick={(e) => e.preventDefault()} 432 453 > 433 - <AppleIcon size={16} /> Coming Soon 454 + <AppleIcon size={16} /> Get Shortcut 434 455 </a> 435 456 </div> 436 457 </div>