Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments

fix various bugs

+225 -148
+28 -21
backend/internal/api/annotations.go
··· 72 72 } 73 73 74 74 record := xrpc.NewAnnotationRecordWithMotivation(req.URL, urlHash, req.Text, req.Selector, req.Title, motivation) 75 + if len(req.Tags) > 0 { 76 + record.Tags = req.Tags 77 + } 75 78 76 79 var result *xrpc.CreateRecordOutput 77 80 err = s.refresher.ExecuteWithAutoRefresh(r, session, func(client *xrpc.Client, did string) error { ··· 98 101 selectorJSONPtr = &selectorStr 99 102 } 100 103 104 + var tagsJSONPtr *string 105 + if len(req.Tags) > 0 { 106 + tagsBytes, _ := json.Marshal(req.Tags) 107 + tagsStr := string(tagsBytes) 108 + tagsJSONPtr = &tagsStr 109 + } 110 + 101 111 cid := result.CID 102 112 did := session.DID 103 113 annotation := &db.Annotation{ ··· 110 120 TargetHash: urlHash, 111 121 TargetTitle: targetTitlePtr, 112 122 SelectorJSON: selectorJSONPtr, 123 + TagsJSON: tagsJSONPtr, 113 124 CreatedAt: time.Now(), 114 125 IndexedAt: time.Now(), 115 126 } ··· 208 219 } 209 220 rkey := parts[2] 210 221 211 - var selector interface{} = nil 212 - if annotation.SelectorJSON != nil && *annotation.SelectorJSON != "" { 213 - json.Unmarshal([]byte(*annotation.SelectorJSON), &selector) 214 - } 215 - 216 222 tagsJSON := "" 217 223 if len(req.Tags) > 0 { 218 224 tagsBytes, _ := json.Marshal(req.Tags) 219 225 tagsJSON = string(tagsBytes) 220 226 } 221 227 222 - record := map[string]interface{}{ 223 - "$type": xrpc.CollectionAnnotation, 224 - "text": req.Text, 225 - "url": annotation.TargetSource, 226 - "createdAt": annotation.CreatedAt.Format(time.RFC3339), 227 - } 228 - if selector != nil { 229 - record["selector"] = selector 230 - } 231 - if len(req.Tags) > 0 { 232 - record["tags"] = req.Tags 233 - } 234 - if annotation.TargetTitle != nil { 235 - record["title"] = *annotation.TargetTitle 236 - } 237 - 238 228 if annotation.BodyValue != nil { 239 229 previousContent := *annotation.BodyValue 240 230 s.db.SaveEditHistory(uri, "annotation", previousContent, annotation.CID) ··· 242 232 243 233 var result *xrpc.PutRecordOutput 244 234 err = s.refresher.ExecuteWithAutoRefresh(r, session, func(client *xrpc.Client, did string) error { 235 + existing, getErr := client.GetRecord(r.Context(), did, xrpc.CollectionAnnotation, rkey) 236 + if getErr != nil { 237 + return fmt.Errorf("failed to fetch existing record: %w", getErr) 238 + } 239 + 240 + var record map[string]interface{} 241 + if err := json.Unmarshal(existing.Value, &record); err != nil { 242 + return fmt.Errorf("failed to parse existing record: %w", err) 243 + } 244 + 245 + record["text"] = req.Text 246 + if req.Tags != nil { 247 + record["tags"] = req.Tags 248 + } else { 249 + delete(record, "tags") 250 + } 251 + 245 252 var updateErr error 246 253 result, updateErr = client.PutRecord(r.Context(), did, xrpc.CollectionAnnotation, rkey, record) 247 254 if updateErr != nil {
+9 -3
backend/internal/api/collections.go
··· 278 278 279 279 enrichedItems := make([]EnrichedCollectionItem, 0, len(items)) 280 280 281 + session, err := s.refresher.GetSessionWithAutoRefresh(r) 282 + viewerDID := "" 283 + if err == nil { 284 + viewerDID = session.DID 285 + } 286 + 281 287 for _, item := range items { 282 288 enriched := EnrichedCollectionItem{ 283 289 URI: item.URI, ··· 290 296 if strings.Contains(item.AnnotationURI, "at.margin.annotation") { 291 297 enriched.Type = "annotation" 292 298 if a, err := s.db.GetAnnotationByURI(item.AnnotationURI); err == nil { 293 - hydrated, _ := hydrateAnnotations([]db.Annotation{*a}) 299 + hydrated, _ := hydrateAnnotations(s.db, []db.Annotation{*a}, viewerDID) 294 300 if len(hydrated) > 0 { 295 301 enriched.Annotation = &hydrated[0] 296 302 } ··· 298 304 } else if strings.Contains(item.AnnotationURI, "at.margin.highlight") { 299 305 enriched.Type = "highlight" 300 306 if h, err := s.db.GetHighlightByURI(item.AnnotationURI); err == nil { 301 - hydrated, _ := hydrateHighlights([]db.Highlight{*h}) 307 + hydrated, _ := hydrateHighlights(s.db, []db.Highlight{*h}, viewerDID) 302 308 if len(hydrated) > 0 { 303 309 enriched.Highlight = &hydrated[0] 304 310 } ··· 306 312 } else if strings.Contains(item.AnnotationURI, "at.margin.bookmark") { 307 313 enriched.Type = "bookmark" 308 314 if b, err := s.db.GetBookmarkByURI(item.AnnotationURI); err == nil { 309 - hydrated, _ := hydrateBookmarks([]db.Bookmark{*b}) 315 + hydrated, _ := hydrateBookmarks(s.db, []db.Bookmark{*b}, viewerDID) 310 316 if len(hydrated) > 0 { 311 317 enriched.Bookmark = &hydrated[0] 312 318 }
+34 -17
backend/internal/api/handler.go
··· 102 102 return 103 103 } 104 104 105 - enriched, _ := hydrateAnnotations(annotations) 105 + enriched, _ := hydrateAnnotations(h.db, annotations, h.getViewerDID(r)) 106 106 107 107 w.Header().Set("Content-Type", "application/json") 108 108 json.NewEncoder(w).Encode(map[string]interface{}{ ··· 136 136 bookmarks, _ = h.db.GetBookmarksByTag(tag, limit, 0) 137 137 collectionItems = []db.CollectionItem{} 138 138 } 139 + } else if creator != "" { 140 + annotations, _ = h.db.GetAnnotationsByAuthor(creator, limit, 0) 141 + highlights, _ = h.db.GetHighlightsByAuthor(creator, limit, 0) 142 + bookmarks, _ = h.db.GetBookmarksByAuthor(creator, limit, 0) 143 + collectionItems = []db.CollectionItem{} 139 144 } else { 140 145 annotations, _ = h.db.GetRecentAnnotations(limit, 0) 141 146 highlights, _ = h.db.GetRecentHighlights(limit, 0) ··· 146 151 } 147 152 } 148 153 149 - authAnnos, _ := hydrateAnnotations(annotations) 150 - authHighs, _ := hydrateHighlights(highlights) 151 - authBooks, _ := hydrateBookmarks(bookmarks) 154 + viewerDID := h.getViewerDID(r) 155 + authAnnos, _ := hydrateAnnotations(h.db, annotations, viewerDID) 156 + authHighs, _ := hydrateHighlights(h.db, highlights, viewerDID) 157 + authBooks, _ := hydrateBookmarks(h.db, bookmarks, viewerDID) 152 158 153 - authCollectionItems, _ := hydrateCollectionItems(h.db, collectionItems) 159 + authCollectionItems, _ := hydrateCollectionItems(h.db, collectionItems, viewerDID) 154 160 155 161 var feed []interface{} 156 162 for _, a := range authAnnos { ··· 222 228 } 223 229 224 230 if annotation, err := h.db.GetAnnotationByURI(uri); err == nil { 225 - if enriched, _ := hydrateAnnotations([]db.Annotation{*annotation}); len(enriched) > 0 { 231 + if enriched, _ := hydrateAnnotations(h.db, []db.Annotation{*annotation}, h.getViewerDID(r)); len(enriched) > 0 { 226 232 serveResponse(enriched[0], "http://www.w3.org/ns/anno.jsonld") 227 233 return 228 234 } 229 235 } 230 236 231 237 if highlight, err := h.db.GetHighlightByURI(uri); err == nil { 232 - if enriched, _ := hydrateHighlights([]db.Highlight{*highlight}); len(enriched) > 0 { 238 + if enriched, _ := hydrateHighlights(h.db, []db.Highlight{*highlight}, h.getViewerDID(r)); len(enriched) > 0 { 233 239 serveResponse(enriched[0], "http://www.w3.org/ns/anno.jsonld") 234 240 return 235 241 } ··· 238 244 if strings.Contains(uri, "at.margin.annotation") { 239 245 highlightURI := strings.Replace(uri, "at.margin.annotation", "at.margin.highlight", 1) 240 246 if highlight, err := h.db.GetHighlightByURI(highlightURI); err == nil { 241 - if enriched, _ := hydrateHighlights([]db.Highlight{*highlight}); len(enriched) > 0 { 247 + if enriched, _ := hydrateHighlights(h.db, []db.Highlight{*highlight}, h.getViewerDID(r)); len(enriched) > 0 { 242 248 serveResponse(enriched[0], "http://www.w3.org/ns/anno.jsonld") 243 249 return 244 250 } ··· 246 252 } 247 253 248 254 if bookmark, err := h.db.GetBookmarkByURI(uri); err == nil { 249 - if enriched, _ := hydrateBookmarks([]db.Bookmark{*bookmark}); len(enriched) > 0 { 255 + if enriched, _ := hydrateBookmarks(h.db, []db.Bookmark{*bookmark}, h.getViewerDID(r)); len(enriched) > 0 { 250 256 serveResponse(enriched[0], "http://www.w3.org/ns/anno.jsonld") 251 257 return 252 258 } ··· 255 261 if strings.Contains(uri, "at.margin.annotation") { 256 262 bookmarkURI := strings.Replace(uri, "at.margin.annotation", "at.margin.bookmark", 1) 257 263 if bookmark, err := h.db.GetBookmarkByURI(bookmarkURI); err == nil { 258 - if enriched, _ := hydrateBookmarks([]db.Bookmark{*bookmark}); len(enriched) > 0 { 264 + if enriched, _ := hydrateBookmarks(h.db, []db.Bookmark{*bookmark}, h.getViewerDID(r)); len(enriched) > 0 { 259 265 serveResponse(enriched[0], "http://www.w3.org/ns/anno.jsonld") 260 266 return 261 267 } ··· 284 290 annotations, _ := h.db.GetAnnotationsByTargetHash(urlHash, limit, offset) 285 291 highlights, _ := h.db.GetHighlightsByTargetHash(urlHash, limit, offset) 286 292 287 - enrichedAnnotations, _ := hydrateAnnotations(annotations) 288 - enrichedHighlights, _ := hydrateHighlights(highlights) 293 + enrichedAnnotations, _ := hydrateAnnotations(h.db, annotations, h.getViewerDID(r)) 294 + enrichedHighlights, _ := hydrateHighlights(h.db, highlights, h.getViewerDID(r)) 289 295 290 296 w.Header().Set("Content-Type", "application/json") 291 297 json.NewEncoder(w).Encode(map[string]interface{}{ ··· 319 325 return 320 326 } 321 327 322 - enriched, _ := hydrateHighlights(highlights) 328 + enriched, _ := hydrateHighlights(h.db, highlights, h.getViewerDID(r)) 323 329 324 330 w.Header().Set("Content-Type", "application/json") 325 331 json.NewEncoder(w).Encode(map[string]interface{}{ ··· 346 352 return 347 353 } 348 354 349 - enriched, _ := hydrateBookmarks(bookmarks) 355 + enriched, _ := hydrateBookmarks(h.db, bookmarks, h.getViewerDID(r)) 350 356 351 357 w.Header().Set("Content-Type", "application/json") 352 358 json.NewEncoder(w).Encode(map[string]interface{}{ ··· 371 377 return 372 378 } 373 379 374 - enriched, _ := hydrateAnnotations(annotations) 380 + enriched, _ := hydrateAnnotations(h.db, annotations, h.getViewerDID(r)) 375 381 376 382 w.Header().Set("Content-Type", "application/json") 377 383 json.NewEncoder(w).Encode(map[string]interface{}{ ··· 397 403 return 398 404 } 399 405 400 - enriched, _ := hydrateHighlights(highlights) 406 + enriched, _ := hydrateHighlights(h.db, highlights, h.getViewerDID(r)) 401 407 402 408 w.Header().Set("Content-Type", "application/json") 403 409 json.NewEncoder(w).Encode(map[string]interface{}{ ··· 423 429 return 424 430 } 425 431 426 - enriched, _ := hydrateBookmarks(bookmarks) 432 + enriched, _ := hydrateBookmarks(h.db, bookmarks, h.getViewerDID(r)) 427 433 428 434 w.Header().Set("Content-Type", "application/json") 429 435 json.NewEncoder(w).Encode(map[string]interface{}{ ··· 622 628 w.Header().Set("Content-Type", "application/json") 623 629 json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) 624 630 } 631 + func (h *Handler) getViewerDID(r *http.Request) string { 632 + cookie, err := r.Cookie("margin_session") 633 + if err != nil { 634 + return "" 635 + } 636 + did, _, _, _, _, err := h.db.GetSession(cookie.Value) 637 + if err != nil { 638 + return "" 639 + } 640 + return did 641 + }
+73 -36
backend/internal/api/hydration.go
··· 50 50 } 51 51 52 52 type APIAnnotation struct { 53 - ID string `json:"id"` 54 - CID string `json:"cid"` 55 - Type string `json:"type"` 56 - Motivation string `json:"motivation,omitempty"` 57 - Author Author `json:"creator"` 58 - Body *APIBody `json:"body,omitempty"` 59 - Target APITarget `json:"target"` 60 - Tags []string `json:"tags,omitempty"` 61 - Generator *APIGenerator `json:"generator,omitempty"` 62 - CreatedAt time.Time `json:"created"` 63 - IndexedAt time.Time `json:"indexed"` 53 + ID string `json:"id"` 54 + CID string `json:"cid"` 55 + Type string `json:"type"` 56 + Motivation string `json:"motivation,omitempty"` 57 + Author Author `json:"creator"` 58 + Body *APIBody `json:"body,omitempty"` 59 + Target APITarget `json:"target"` 60 + Tags []string `json:"tags,omitempty"` 61 + Generator *APIGenerator `json:"generator,omitempty"` 62 + CreatedAt time.Time `json:"created"` 63 + IndexedAt time.Time `json:"indexed"` 64 + LikeCount int `json:"likeCount"` 65 + ReplyCount int `json:"replyCount"` 66 + ViewerHasLiked bool `json:"viewerHasLiked"` 64 67 } 65 68 66 69 type APIHighlight struct { 67 - ID string `json:"id"` 68 - Type string `json:"type"` 69 - Author Author `json:"creator"` 70 - Target APITarget `json:"target"` 71 - Color string `json:"color,omitempty"` 72 - Tags []string `json:"tags,omitempty"` 73 - CreatedAt time.Time `json:"created"` 74 - CID string `json:"cid,omitempty"` 70 + ID string `json:"id"` 71 + Type string `json:"type"` 72 + Author Author `json:"creator"` 73 + Target APITarget `json:"target"` 74 + Color string `json:"color,omitempty"` 75 + Tags []string `json:"tags,omitempty"` 76 + CreatedAt time.Time `json:"created"` 77 + CID string `json:"cid,omitempty"` 78 + LikeCount int `json:"likeCount"` 79 + ReplyCount int `json:"replyCount"` 80 + ViewerHasLiked bool `json:"viewerHasLiked"` 75 81 } 76 82 77 83 type APIBookmark struct { 78 - ID string `json:"id"` 79 - Type string `json:"type"` 80 - Author Author `json:"creator"` 81 - Source string `json:"source"` 82 - Title string `json:"title,omitempty"` 83 - Description string `json:"description,omitempty"` 84 - Tags []string `json:"tags,omitempty"` 85 - CreatedAt time.Time `json:"created"` 86 - CID string `json:"cid,omitempty"` 84 + ID string `json:"id"` 85 + Type string `json:"type"` 86 + Author Author `json:"creator"` 87 + Source string `json:"source"` 88 + Title string `json:"title,omitempty"` 89 + Description string `json:"description,omitempty"` 90 + Tags []string `json:"tags,omitempty"` 91 + CreatedAt time.Time `json:"created"` 92 + CID string `json:"cid,omitempty"` 93 + LikeCount int `json:"likeCount"` 94 + ReplyCount int `json:"replyCount"` 95 + ViewerHasLiked bool `json:"viewerHasLiked"` 87 96 } 88 97 89 98 type APIReply struct { ··· 132 141 ReadAt *time.Time `json:"readAt,omitempty"` 133 142 } 134 143 135 - func hydrateAnnotations(annotations []db.Annotation) ([]APIAnnotation, error) { 144 + func hydrateAnnotations(database *db.DB, annotations []db.Annotation, viewerDID string) ([]APIAnnotation, error) { 136 145 if len(annotations) == 0 { 137 146 return []APIAnnotation{}, nil 138 147 } ··· 197 206 CreatedAt: a.CreatedAt, 198 207 IndexedAt: a.IndexedAt, 199 208 } 209 + 210 + if database != nil { 211 + result[i].LikeCount, _ = database.GetLikeCount(a.URI) 212 + result[i].ReplyCount, _ = database.GetReplyCount(a.URI) 213 + if viewerDID != "" { 214 + if _, err := database.GetLikeByUserAndSubject(viewerDID, a.URI); err == nil { 215 + result[i].ViewerHasLiked = true 216 + } 217 + } 218 + } 200 219 } 201 220 202 221 return result, nil 203 222 } 204 223 205 - func hydrateHighlights(highlights []db.Highlight) ([]APIHighlight, error) { 224 + func hydrateHighlights(database *db.DB, highlights []db.Highlight, viewerDID string) ([]APIHighlight, error) { 206 225 if len(highlights) == 0 { 207 226 return []APIHighlight{}, nil 208 227 } ··· 251 270 CreatedAt: h.CreatedAt, 252 271 CID: cid, 253 272 } 273 + 274 + if database != nil { 275 + result[i].LikeCount, _ = database.GetLikeCount(h.URI) 276 + result[i].ReplyCount, _ = database.GetReplyCount(h.URI) 277 + if viewerDID != "" { 278 + if _, err := database.GetLikeByUserAndSubject(viewerDID, h.URI); err == nil { 279 + result[i].ViewerHasLiked = true 280 + } 281 + } 282 + } 254 283 } 255 284 256 285 return result, nil 257 286 } 258 287 259 - func hydrateBookmarks(bookmarks []db.Bookmark) ([]APIBookmark, error) { 288 + func hydrateBookmarks(database *db.DB, bookmarks []db.Bookmark, viewerDID string) ([]APIBookmark, error) { 260 289 if len(bookmarks) == 0 { 261 290 return []APIBookmark{}, nil 262 291 } ··· 295 324 Tags: tags, 296 325 CreatedAt: b.CreatedAt, 297 326 CID: cid, 327 + } 328 + if database != nil { 329 + result[i].LikeCount, _ = database.GetLikeCount(b.URI) 330 + result[i].ReplyCount, _ = database.GetReplyCount(b.URI) 331 + if viewerDID != "" { 332 + if _, err := database.GetLikeByUserAndSubject(viewerDID, b.URI); err == nil { 333 + result[i].ViewerHasLiked = true 334 + } 335 + } 298 336 } 299 337 } 300 338 ··· 439 477 return result, nil 440 478 } 441 479 442 - func hydrateCollectionItems(database *db.DB, items []db.CollectionItem) ([]APICollectionItem, error) { 480 + func hydrateCollectionItems(database *db.DB, items []db.CollectionItem, viewerDID string) ([]APICollectionItem, error) { 443 481 if len(items) == 0 { 444 482 return []APICollectionItem{}, nil 445 483 } ··· 479 517 480 518 if strings.Contains(item.AnnotationURI, "at.margin.annotation") { 481 519 if a, err := database.GetAnnotationByURI(item.AnnotationURI); err == nil { 482 - hydrated, _ := hydrateAnnotations([]db.Annotation{*a}) 520 + hydrated, _ := hydrateAnnotations(database, []db.Annotation{*a}, viewerDID) 483 521 if len(hydrated) > 0 { 484 522 apiItem.Annotation = &hydrated[0] 485 523 } 486 524 } 487 525 } else if strings.Contains(item.AnnotationURI, "at.margin.highlight") { 488 526 if h, err := database.GetHighlightByURI(item.AnnotationURI); err == nil { 489 - hydrated, _ := hydrateHighlights([]db.Highlight{*h}) 527 + hydrated, _ := hydrateHighlights(database, []db.Highlight{*h}, viewerDID) 490 528 if len(hydrated) > 0 { 491 529 apiItem.Highlight = &hydrated[0] 492 530 } 493 531 } 494 532 } else if strings.Contains(item.AnnotationURI, "at.margin.bookmark") { 495 533 if b, err := database.GetBookmarkByURI(item.AnnotationURI); err == nil { 496 - hydrated, _ := hydrateBookmarks([]db.Bookmark{*b}) 534 + hydrated, _ := hydrateBookmarks(database, []db.Bookmark{*b}, viewerDID) 497 535 if len(hydrated) > 0 { 498 536 apiItem.Bookmark = &hydrated[0] 499 537 } else { 500 538 log.Printf("Failed to hydrate bookmark %s: empty hydration result\n", item.AnnotationURI) 501 539 } 502 540 } else { 503 - log.Printf("GetBookmarkByURI failed for %s: %v\n", item.AnnotationURI, err) 504 541 } 505 542 } else { 506 543 log.Printf("Unknown item type for URI: %s\n", item.AnnotationURI)
+6
backend/internal/db/queries.go
··· 634 634 return count, err 635 635 } 636 636 637 + func (db *DB) GetReplyCount(rootURI string) (int, error) { 638 + var count int 639 + err := db.QueryRow(db.Rebind(`SELECT COUNT(*) FROM replies WHERE root_uri = ?`), rootURI).Scan(&count) 640 + return count, err 641 + } 642 + 637 643 func (db *DB) GetLikeByUserAndSubject(userDID, subjectURI string) (*Like, error) { 638 644 var like Like 639 645 err := db.QueryRow(db.Rebind(`
+18
web/src/api/client.js
··· 314 314 tags: item.tags || [], 315 315 createdAt: item.createdAt || item.created, 316 316 cid: item.cid || item.CID, 317 + likeCount: item.likeCount || 0, 318 + replyCount: item.replyCount || 0, 319 + viewerHasLiked: item.viewerHasLiked || false, 317 320 }; 318 321 } 319 322 ··· 328 331 tags: item.tags || [], 329 332 createdAt: item.createdAt || item.created, 330 333 cid: item.cid || item.CID, 334 + likeCount: item.likeCount || 0, 335 + replyCount: item.replyCount || 0, 336 + viewerHasLiked: item.viewerHasLiked || false, 331 337 }; 332 338 } 333 339 ··· 343 349 tags: item.tags || [], 344 350 createdAt: item.createdAt || item.created, 345 351 cid: item.cid || item.CID, 352 + likeCount: item.likeCount || 0, 353 + replyCount: item.replyCount || 0, 354 + viewerHasLiked: item.viewerHasLiked || false, 346 355 }; 347 356 } 348 357 ··· 358 367 tags: item.tags || [], 359 368 createdAt: item.createdAt || item.created, 360 369 cid: item.cid || item.CID, 370 + likeCount: item.likeCount || 0, 371 + replyCount: item.replyCount || 0, 372 + viewerHasLiked: item.viewerHasLiked || false, 361 373 }; 362 374 } 363 375 ··· 371 383 color: highlight.color, 372 384 tags: highlight.tags || [], 373 385 createdAt: highlight.createdAt || highlight.created, 386 + likeCount: highlight.likeCount || 0, 387 + replyCount: highlight.replyCount || 0, 388 + viewerHasLiked: highlight.viewerHasLiked || false, 374 389 }; 375 390 } 376 391 ··· 383 398 description: bookmark.description, 384 399 tags: bookmark.tags || [], 385 400 createdAt: bookmark.createdAt || bookmark.created, 401 + likeCount: bookmark.likeCount || 0, 402 + replyCount: bookmark.replyCount || 0, 403 + viewerHasLiked: bookmark.viewerHasLiked || false, 386 404 }; 387 405 } 388 406
+16 -44
web/src/components/AnnotationCard.jsx
··· 67 67 const { user, login } = useAuth(); 68 68 const data = normalizeAnnotation(annotation); 69 69 70 - const [likeCount, setLikeCount] = useState(0); 71 - const [isLiked, setIsLiked] = useState(false); 70 + const [likeCount, setLikeCount] = useState(data.likeCount || 0); 71 + const [isLiked, setIsLiked] = useState(data.viewerHasLiked || false); 72 72 const [deleting, setDeleting] = useState(false); 73 73 const [isEditing, setIsEditing] = useState(false); 74 74 const [editText, setEditText] = useState(data.text || ""); ··· 80 80 const [loadingHistory, setLoadingHistory] = useState(false); 81 81 82 82 const [replies, setReplies] = useState([]); 83 - const [replyCount, setReplyCount] = useState(0); 83 + const [replyCount, setReplyCount] = useState(data.replyCount || 0); 84 84 const [showReplies, setShowReplies] = useState(false); 85 85 const [replyingTo, setReplyingTo] = useState(null); 86 86 const [replyText, setReplyText] = useState(""); ··· 90 90 91 91 const [hasEditHistory, setHasEditHistory] = useState(false); 92 92 93 - useEffect(() => { 94 - let mounted = true; 95 - async function fetchData() { 96 - try { 97 - const repliesRes = await getReplies(data.uri); 98 - if (mounted && repliesRes.items) { 99 - setReplies(repliesRes.items); 100 - setReplyCount(repliesRes.items.length); 101 - } 102 - 103 - const likeRes = await getLikeCount(data.uri); 104 - if (mounted) { 105 - if (likeRes.count !== undefined) { 106 - setLikeCount(likeRes.count); 107 - } 108 - if (likeRes.liked !== undefined) { 109 - setIsLiked(likeRes.liked); 110 - } 111 - } 112 - 113 - if (!data.color && !data.description) { 114 - try { 115 - const history = await getEditHistory(data.uri); 116 - if (mounted && history && history.length > 0) { 117 - setHasEditHistory(true); 118 - } 119 - } catch {} 120 - } 121 - } catch (err) { 122 - console.error("Failed to fetch data:", err); 123 - } 124 - } 125 - if (data.uri) { 126 - fetchData(); 127 - } 128 - return () => { 129 - mounted = false; 130 - }; 131 - }, [data.uri]); 93 + useEffect(() => {}, []); 132 94 133 95 const fetchHistory = async () => { 134 96 if (showHistory) { ··· 421 383 rel="noopener noreferrer" 422 384 className="annotation-highlight" 423 385 style={{ 424 - borderLeftColor: isEditing ? editColor : data.color || "#f59e0b", 386 + borderLeftColor: data.color || "#f59e0b", 425 387 }} 426 388 > 427 389 <mark>"{highlightedText}"</mark> ··· 497 459 </button> 498 460 <button 499 461 className={`annotation-action ${showReplies ? "active" : ""}`} 500 - onClick={() => setShowReplies(!showReplies)} 462 + onClick={async () => { 463 + if (!showReplies && replies.length === 0) { 464 + try { 465 + const res = await getReplies(data.uri); 466 + if (res.items) setReplies(res.items); 467 + } catch (err) { 468 + console.error("Failed to load replies:", err); 469 + } 470 + } 471 + setShowReplies(!showReplies); 472 + }} 501 473 > 502 474 <MessageIcon size={16} /> 503 475 <span>{replyCount > 0 ? `${replyCount}` : "Reply"}</span>
+35 -9
web/src/pages/Feed.jsx
··· 12 12 export default function Feed() { 13 13 const [searchParams, setSearchParams] = useSearchParams(); 14 14 const tagFilter = searchParams.get("tag"); 15 + const filter = searchParams.get("filter") || "all"; 16 + 15 17 const [annotations, setAnnotations] = useState([]); 16 18 const [loading, setLoading] = useState(true); 17 19 const [error, setError] = useState(null); 18 - const [filter, setFilter] = useState("all"); 20 + 21 + const updateFilter = (newFilter) => { 22 + setSearchParams( 23 + (prev) => { 24 + const next = new URLSearchParams(prev); 25 + next.set("filter", newFilter); 26 + return next; 27 + }, 28 + { replace: true }, 29 + ); 30 + }; 31 + 19 32 const [collectionModalState, setCollectionModalState] = useState({ 20 33 isOpen: false, 21 34 uri: null, ··· 28 41 try { 29 42 setLoading(true); 30 43 let creatorDid = ""; 31 - if (filter === "my-tags" && user?.did) { 32 - creatorDid = user.did; 44 + 45 + if (filter === "my-tags") { 46 + if (user?.did) { 47 + creatorDid = user.did; 48 + } else { 49 + setAnnotations([]); 50 + setLoading(false); 51 + return; 52 + } 33 53 } 34 54 35 55 const data = await getAnnotationFeed( ··· 83 103 Filtering by tag: <strong>#{tagFilter}</strong> 84 104 </span> 85 105 <button 86 - onClick={() => setSearchParams({})} 106 + onClick={() => 107 + setSearchParams((prev) => { 108 + const next = new URLSearchParams(prev); 109 + next.delete("tag"); 110 + return next; 111 + }) 112 + } 87 113 className="btn btn-sm" 88 114 style={{ padding: "2px 8px", fontSize: "0.8rem" }} 89 115 > ··· 97 123 <div className="feed-filters"> 98 124 <button 99 125 className={`filter-tab ${filter === "all" ? "active" : ""}`} 100 - onClick={() => setFilter("all")} 126 + onClick={() => updateFilter("all")} 101 127 > 102 128 All 103 129 </button> 104 130 {user && ( 105 131 <button 106 132 className={`filter-tab ${filter === "my-tags" ? "active" : ""}`} 107 - onClick={() => setFilter("my-tags")} 133 + onClick={() => updateFilter("my-tags")} 108 134 > 109 135 My Feed 110 136 </button> 111 137 )} 112 138 <button 113 139 className={`filter-tab ${filter === "commenting" ? "active" : ""}`} 114 - onClick={() => setFilter("commenting")} 140 + onClick={() => updateFilter("commenting")} 115 141 > 116 142 Annotations 117 143 </button> 118 144 <button 119 145 className={`filter-tab ${filter === "highlighting" ? "active" : ""}`} 120 - onClick={() => setFilter("highlighting")} 146 + onClick={() => updateFilter("highlighting")} 121 147 > 122 148 Highlights 123 149 </button> 124 150 <button 125 151 className={`filter-tab ${filter === "bookmarking" ? "active" : ""}`} 126 - onClick={() => setFilter("bookmarking")} 152 + onClick={() => updateFilter("bookmarking")} 127 153 > 128 154 Bookmarks 129 155 </button>
+5 -1
web/src/pages/New.jsx
··· 84 84 85 85 <div className="card"> 86 86 <Composer 87 - url={url || initialUrl} 87 + url={ 88 + (url || initialUrl) && !/^(?:f|ht)tps?:\/\//.test(url || initialUrl) 89 + ? `https://${url || initialUrl}` 90 + : url || initialUrl 91 + } 88 92 selector={initialSelector} 89 93 onSuccess={handleSuccess} 90 94 onCancel={() => navigate(-1)}
+1 -17
web/src/pages/Profile.jsx
··· 130 130 </div> 131 131 ); 132 132 } 133 - return bookmarks.map((b) => <BookmarkCard key={b.id} annotation={b} />); 134 - } 135 - if (activeTab === "bookmarks") { 136 - if (bookmarks.length === 0) { 137 - return ( 138 - <div className="empty-state"> 139 - <div className="empty-state-icon"> 140 - <BookmarkIcon size={32} /> 141 - </div> 142 - <h3 className="empty-state-title">No bookmarks</h3> 143 - <p className="empty-state-text"> 144 - This user hasn't bookmarked any pages. 145 - </p> 146 - </div> 147 - ); 148 - } 149 - return bookmarks.map((b) => <BookmarkCard key={b.id} annotation={b} />); 133 + return bookmarks.map((b) => <BookmarkCard key={b.uri} bookmark={b} />); 150 134 } 151 135 152 136 if (activeTab === "collections") {