(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
98
fork

Configure Feed

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

fix various bugs

scanash00 db36e33e 1a5da2ed

+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") {