nandi/gleanpublic⑂ Fork 0
⑂ 6017bda
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Implement annotation editing and deletionUnverified

Julien Robert committed 2026-04-20T20:24:27+02:00 Browse files
6017bda parent: 9681c7f
modified go.mod +1 -1
@@ -10,6 +10,7 @@ require (
1010 github.com/go-chi/cors v1.2.2
1111 github.com/gorilla/websocket v1.5.3
1212 github.com/mattn/go-sqlite3 v1.14.22
13+ gotest.tools/v3 v3.5.2
1314 )
1415
1516 require (
@@ -204,7 +205,6 @@ require (
204205 gopkg.in/ini.v1 v1.67.0 // indirect
205206 gopkg.in/yaml.v2 v2.4.0 // indirect
206207 gopkg.in/yaml.v3 v3.0.1 // indirect
207- gotest.tools/v3 v3.5.2 // indirect
208208 honnef.co/go/tools v0.6.1 // indirect
209209 mvdan.cc/gofumpt v0.7.0 // indirect
210210 mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect
@@ -10,6 +10,7 @@ require (
10 github.com/go-chi/cors v1.2.210 github.com/go-chi/cors v1.2.2
11 github.com/gorilla/websocket v1.5.311 github.com/gorilla/websocket v1.5.3
12 github.com/mattn/go-sqlite3 v1.14.2212 github.com/mattn/go-sqlite3 v1.14.22
13+ gotest.tools/v3 v3.5.2
13 )14 )
14 15
15 require (16 require (
@@ -204,7 +205,6 @@ require (
204 gopkg.in/ini.v1 v1.67.0 // indirect205 gopkg.in/ini.v1 v1.67.0 // indirect
205 gopkg.in/yaml.v2 v2.4.0 // indirect206 gopkg.in/yaml.v2 v2.4.0 // indirect
206 gopkg.in/yaml.v3 v3.0.1 // indirect207 gopkg.in/yaml.v3 v3.0.1 // indirect
207- gotest.tools/v3 v3.5.2 // indirect
208 honnef.co/go/tools v0.6.1 // indirect208 honnef.co/go/tools v0.6.1 // indirect
209 mvdan.cc/gofumpt v0.7.0 // indirect209 mvdan.cc/gofumpt v0.7.0 // indirect
210 mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect210 mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect
modified internal/atproto/firehose.go +3 -2
@@ -41,6 +41,7 @@ func NewFirehoseConsumer(relayURL string, handler FirehoseHandler, logger *slog.
4141 "at.glean.subscription": true,
4242 "at.glean.annotation": true,
4343 "at.glean.like": true,
44+ // TODO: support at.margin.annotation as well (ref: https://tangled.org/did:plc:rgvlxa3ecwx3bfyzlrzrwtrs/issues/1)
4445 },
4546 }
4647 }
@@ -127,8 +128,8 @@ func (fc *FirehoseConsumer) handleMessage(ctx context.Context, msg []byte) {
127128
128129 func (fc *FirehoseConsumer) parseCommit(ctx context.Context, raw json.RawMessage) {
129130 var commit struct {
130- Did string `json:"did"`
131- Ops []struct {
131+ Did string `json:"did"`
132+ Ops []struct {
132133 Action string `json:"action"`
133134 Path string `json:"path"`
134135 CID json.RawMessage `json:"cid"`
@@ -41,6 +41,7 @@ func NewFirehoseConsumer(relayURL string, handler FirehoseHandler, logger *slog.
41 "at.glean.subscription": true,41 "at.glean.subscription": true,
42 "at.glean.annotation": true,42 "at.glean.annotation": true,
43 "at.glean.like": true,43 "at.glean.like": true,
44+ // TODO: support at.margin.annotation as well (ref: https://tangled.org/did:plc:rgvlxa3ecwx3bfyzlrzrwtrs/issues/1)
44 },45 },
45 }46 }
46 }47 }
@@ -127,8 +128,8 @@ func (fc *FirehoseConsumer) handleMessage(ctx context.Context, msg []byte) {
127 128
128 func (fc *FirehoseConsumer) parseCommit(ctx context.Context, raw json.RawMessage) {129 func (fc *FirehoseConsumer) parseCommit(ctx context.Context, raw json.RawMessage) {
129 var commit struct {130 var commit struct {
130- Did string `json:"did"`131+ Did string `json:"did"`
131- Ops []struct {132+ Ops []struct {
132 Action string `json:"action"`133 Action string `json:"action"`
133 Path string `json:"path"`134 Path string `json:"path"`
134 CID json.RawMessage `json:"cid"`135 CID json.RawMessage `json:"cid"`
modified internal/atproto/firehose_handler.go +1 -1
@@ -2,8 +2,8 @@ package atproto
22
33 import (
44 "context"
5- "encoding/json"
65 "database/sql"
6+ "encoding/json"
77 "log/slog"
88 "time"
99
@@ -2,8 +2,8 @@ package atproto
2 2
3 import (3 import (
4 "context"4 "context"
5- "encoding/json"
6 "database/sql"5 "database/sql"
6+ "encoding/json"
7 "log/slog"7 "log/slog"
8 "time"8 "time"
9 9
modified internal/atproto/lexicon.go +4 -4
@@ -51,13 +51,13 @@ type ListSubscriptionsResponse struct {
5151 }
5252
5353 type FeedListEntry struct {
54- DID string `json:"did"`
55- SubscriptionCount int `json:"subscriptionCount"`
54+ DID string `json:"did"`
55+ SubscriptionCount int `json:"subscriptionCount"`
5656 Subscriptions []SubscriptionRecord `json:"subscriptions"`
5757 }
5858
5959 type ListFeedListsResponse struct {
60- Cursor string `json:"cursor,omitempty"`
60+ Cursor string `json:"cursor,omitempty"`
6161 Feeds []FeedListEntry `json:"feeds"`
6262 }
6363
@@ -124,7 +124,7 @@ type RecommendedPerson struct {
124124 }
125125
126126 type GetRecommendationsResponse struct {
127- Feeds []RecommendedFeed `json:"feeds"`
127+ Feeds []RecommendedFeed `json:"feeds"`
128128 People []RecommendedPerson `json:"people"`
129129 }
130130
@@ -51,13 +51,13 @@ type ListSubscriptionsResponse struct {
51 }51 }
52 52
53 type FeedListEntry struct {53 type FeedListEntry struct {
54- DID string `json:"did"`54+ DID string `json:"did"`
55- SubscriptionCount int `json:"subscriptionCount"`55+ SubscriptionCount int `json:"subscriptionCount"`
56 Subscriptions []SubscriptionRecord `json:"subscriptions"`56 Subscriptions []SubscriptionRecord `json:"subscriptions"`
57 }57 }
58 58
59 type ListFeedListsResponse struct {59 type ListFeedListsResponse struct {
60- Cursor string `json:"cursor,omitempty"`60+ Cursor string `json:"cursor,omitempty"`
61 Feeds []FeedListEntry `json:"feeds"`61 Feeds []FeedListEntry `json:"feeds"`
62 }62 }
63 63
@@ -124,7 +124,7 @@ type RecommendedPerson struct {
124 }124 }
125 125
126 type GetRecommendationsResponse struct {126 type GetRecommendationsResponse struct {
127- Feeds []RecommendedFeed `json:"feeds"`127+ Feeds []RecommendedFeed `json:"feeds"`
128 People []RecommendedPerson `json:"people"`128 People []RecommendedPerson `json:"people"`
129 }129 }
130 130
modified internal/db/article.go +20 -20
@@ -7,29 +7,29 @@ import (
77 )
88
99 type Article struct {
10- ID int64
11- FeedURL string
12- FeedTitle string
13- GUID string
14- Title string
15- URL sql.NullString
16- Author sql.NullString
17- Summary sql.NullString
18- Content sql.NullString
19- Published sql.NullTime
20- Updated sql.NullTime
21- FetchedAt sql.NullTime
22- IsRead sql.NullBool
23- IsStarred sql.NullBool
10+ ID int64
11+ FeedURL string
12+ FeedTitle string
13+ GUID string
14+ Title string
15+ URL sql.NullString
16+ Author sql.NullString
17+ Summary sql.NullString
18+ Content sql.NullString
19+ Published sql.NullTime
20+ Updated sql.NullTime
21+ FetchedAt sql.NullTime
22+ IsRead sql.NullBool
23+ IsStarred sql.NullBool
2424 }
2525
2626 type ReadState struct {
27- UserDID string
28- ArticleID int64
29- IsRead bool
30- ReadAt sql.NullTime
31- IsStarred bool
32- StarredAt sql.NullTime
27+ UserDID string
28+ ArticleID int64
29+ IsRead bool
30+ ReadAt sql.NullTime
31+ IsStarred bool
32+ StarredAt sql.NullTime
3333 }
3434
3535 func (db *DB) UpsertArticle(ctx context.Context, article *Article) (int64, error) {
@@ -7,29 +7,29 @@ import (
7 )7 )
8 8
9 type Article struct {9 type Article struct {
10- ID int6410+ ID int64
11- FeedURL string11+ FeedURL string
12- FeedTitle string12+ FeedTitle string
13- GUID string13+ GUID string
14- Title string14+ Title string
15- URL sql.NullString15+ URL sql.NullString
16- Author sql.NullString16+ Author sql.NullString
17- Summary sql.NullString17+ Summary sql.NullString
18- Content sql.NullString18+ Content sql.NullString
19- Published sql.NullTime19+ Published sql.NullTime
20- Updated sql.NullTime20+ Updated sql.NullTime
21- FetchedAt sql.NullTime21+ FetchedAt sql.NullTime
22- IsRead sql.NullBool22+ IsRead sql.NullBool
23- IsStarred sql.NullBool23+ IsStarred sql.NullBool
24 }24 }
25 25
26 type ReadState struct {26 type ReadState struct {
27- UserDID string27+ UserDID string
28- ArticleID int6428+ ArticleID int64
29- IsRead bool29+ IsRead bool
30- ReadAt sql.NullTime30+ ReadAt sql.NullTime
31- IsStarred bool31+ IsStarred bool
32- StarredAt sql.NullTime32+ StarredAt sql.NullTime
33 }33 }
34 34
35 func (db *DB) UpsertArticle(ctx context.Context, article *Article) (int64, error) {35 func (db *DB) UpsertArticle(ctx context.Context, article *Article) (int64, error) {
modified internal/db/cluster.go +14 -14
@@ -227,11 +227,11 @@ func (db *DB) GetFeedRecommendations(ctx context.Context, userDID string, limit
227227 return nil, err
228228 }
229229 results = append(results, map[string]any{
230- "feed_url": feedURL,
231- "score": score,
232- "title": title,
233- "site_url": siteURL,
234- "description": description,
230+ "feed_url": feedURL,
231+ "score": score,
232+ "title": title,
233+ "site_url": siteURL,
234+ "description": description,
235235 "subscriber_count": subCount,
236236 })
237237 }
@@ -306,15 +306,15 @@ func (db *DB) GetSimilarFeeds(ctx context.Context, feedURL string, limit int) ([
306306 }
307307
308308 type ArticleRecommendation struct {
309- ArticleID int64
310- Title string
311- URL string
312- FeedURL string
313- FeedTitle string
314- Author string
315- Summary string
316- Published sql.NullTime
317- Score float64
309+ ArticleID int64
310+ Title string
311+ URL string
312+ FeedURL string
313+ FeedTitle string
314+ Author string
315+ Summary string
316+ Published sql.NullTime
317+ Score float64
318318 }
319319
320320 func (db *DB) GetArticleRecommendations(ctx context.Context, userDID string, limit int) ([]*ArticleRecommendation, error) {
@@ -227,11 +227,11 @@ func (db *DB) GetFeedRecommendations(ctx context.Context, userDID string, limit
227 return nil, err227 return nil, err
228 }228 }
229 results = append(results, map[string]any{229 results = append(results, map[string]any{
230- "feed_url": feedURL,230+ "feed_url": feedURL,
231- "score": score,231+ "score": score,
232- "title": title,232+ "title": title,
233- "site_url": siteURL,233+ "site_url": siteURL,
234- "description": description,234+ "description": description,
235 "subscriber_count": subCount,235 "subscriber_count": subCount,
236 })236 })
237 }237 }
@@ -306,15 +306,15 @@ func (db *DB) GetSimilarFeeds(ctx context.Context, feedURL string, limit int) ([
306 }306 }
307 307
308 type ArticleRecommendation struct {308 type ArticleRecommendation struct {
309- ArticleID int64309+ ArticleID int64
310- Title string310+ Title string
311- URL string311+ URL string
312- FeedURL string312+ FeedURL string
313- FeedTitle string313+ FeedTitle string
314- Author string314+ Author string
315- Summary string315+ Summary string
316- Published sql.NullTime316+ Published sql.NullTime
317- Score float64317+ Score float64
318 }318 }
319 319
320 func (db *DB) GetArticleRecommendations(ctx context.Context, userDID string, limit int) ([]*ArticleRecommendation, error) {320 func (db *DB) GetArticleRecommendations(ctx context.Context, userDID string, limit int) ([]*ArticleRecommendation, error) {
modified internal/db/db.go +1 -1
@@ -2,9 +2,9 @@ package db
22
33 import (
44 "database/sql"
5+ _ "github.com/mattn/go-sqlite3"
56 "strings"
67 "time"
7- _ "github.com/mattn/go-sqlite3"
88 )
99
1010 func NullStr(s string) sql.NullString {
@@ -2,9 +2,9 @@ package db
2 2
3 import (3 import (
4 "database/sql"4 "database/sql"
5+ _ "github.com/mattn/go-sqlite3"
5 "strings"6 "strings"
6 "time"7 "time"
7- _ "github.com/mattn/go-sqlite3"
8 )8 )
9 9
10 func NullStr(s string) sql.NullString {10 func NullStr(s string) sql.NullString {
modified internal/db/social.go +26 -11
@@ -8,18 +8,18 @@ import (
88 )
99
1010 type Annotation struct {
11- ID int64
12- URI string
13- AuthorDID string
11+ ID int64
12+ URI string
13+ AuthorDID string
1414 AuthorHandle string
15- FeedURL string
16- ArticleURL string
17- Quote sql.NullString
18- Note sql.NullString
19- Tags sql.NullString
20- Rating sql.NullInt64
21- CreatedAt sql.NullTime
22- CID sql.NullString
15+ FeedURL string
16+ ArticleURL string
17+ Quote sql.NullString
18+ Note sql.NullString
19+ Tags sql.NullString
20+ Rating sql.NullInt64
21+ CreatedAt sql.NullTime
22+ CID sql.NullString
2323 }
2424
2525 type Like struct {
@@ -40,6 +40,21 @@ func (db *DB) CreateAnnotation(ctx context.Context, a *Annotation) error {
4040 return err
4141 }
4242
43+func (db *DB) GetAnnotation(ctx context.Context, id int64) (*Annotation, error) {
44+ a := &Annotation{}
45+ err := db.QueryRowContext(ctx, `
46+ SELECT a.id, a.uri, a.author_did, COALESCE(u.handle, ''), a.feed_url, a.article_url, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid
47+ FROM annotations a
48+ LEFT JOIN users u ON a.author_did = u.did
49+ WHERE a.id = ?
50+ `, id).Scan(&a.ID, &a.URI, &a.AuthorDID, &a.AuthorHandle, &a.FeedURL, &a.ArticleURL,
51+ &a.Quote, &a.Note, &a.Tags, &a.Rating, &a.CreatedAt, &a.CID)
52+ if err != nil {
53+ return nil, err
54+ }
55+ return a, nil
56+}
57+
4358 func (db *DB) DeleteAnnotation(ctx context.Context, uri string) error {
4459 _, err := db.ExecContext(ctx, `DELETE FROM annotations WHERE uri = ?`, uri)
4560 return err
@@ -8,18 +8,18 @@ import (
8 )8 )
9 9
10 type Annotation struct {10 type Annotation struct {
11- ID int6411+ ID int64
12- URI string12+ URI string
13- AuthorDID string13+ AuthorDID string
14 AuthorHandle string14 AuthorHandle string
15- FeedURL string15+ FeedURL string
16- ArticleURL string16+ ArticleURL string
17- Quote sql.NullString17+ Quote sql.NullString
18- Note sql.NullString18+ Note sql.NullString
19- Tags sql.NullString19+ Tags sql.NullString
20- Rating sql.NullInt6420+ Rating sql.NullInt64
21- CreatedAt sql.NullTime21+ CreatedAt sql.NullTime
22- CID sql.NullString22+ CID sql.NullString
23 }23 }
24 24
25 type Like struct {25 type Like struct {
@@ -40,6 +40,21 @@ func (db *DB) CreateAnnotation(ctx context.Context, a *Annotation) error {
40 return err40 return err
41 }41 }
42 42
43+func (db *DB) GetAnnotation(ctx context.Context, id int64) (*Annotation, error) {
44+ a := &Annotation{}
45+ err := db.QueryRowContext(ctx, `
46+ SELECT a.id, a.uri, a.author_did, COALESCE(u.handle, ''), a.feed_url, a.article_url, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid
47+ FROM annotations a
48+ LEFT JOIN users u ON a.author_did = u.did
49+ WHERE a.id = ?
50+ `, id).Scan(&a.ID, &a.URI, &a.AuthorDID, &a.AuthorHandle, &a.FeedURL, &a.ArticleURL,
51+ &a.Quote, &a.Note, &a.Tags, &a.Rating, &a.CreatedAt, &a.CID)
52+ if err != nil {
53+ return nil, err
54+ }
55+ return a, nil
56+}
57+
43 func (db *DB) DeleteAnnotation(ctx context.Context, uri string) error {58 func (db *DB) DeleteAnnotation(ctx context.Context, uri string) error {
44 _, err := db.ExecContext(ctx, `DELETE FROM annotations WHERE uri = ?`, uri)59 _, err := db.ExecContext(ctx, `DELETE FROM annotations WHERE uri = ?`, uri)
45 return err60 return err
modified internal/feed/parser.go +7 -7
@@ -84,13 +84,13 @@ type jsonFeed struct {
8484 HomePageURL string `json:"home_page_url"`
8585 Description string `json:"description"`
8686 Items []struct {
87- ID string `json:"id"`
88- URL string `json:"url"`
89- Title string `json:"title"`
90- ContentHTML string `json:"content_html"`
91- ContentText string `json:"content_text"`
92- Summary string `json:"summary"`
93- Author struct {
87+ ID string `json:"id"`
88+ URL string `json:"url"`
89+ Title string `json:"title"`
90+ ContentHTML string `json:"content_html"`
91+ ContentText string `json:"content_text"`
92+ Summary string `json:"summary"`
93+ Author struct {
9494 Name string `json:"name"`
9595 } `json:"author"`
9696 DatePublished string `json:"date_published"`
@@ -84,13 +84,13 @@ type jsonFeed struct {
84 HomePageURL string `json:"home_page_url"`84 HomePageURL string `json:"home_page_url"`
85 Description string `json:"description"`85 Description string `json:"description"`
86 Items []struct {86 Items []struct {
87- ID string `json:"id"`87+ ID string `json:"id"`
88- URL string `json:"url"`88+ URL string `json:"url"`
89- Title string `json:"title"`89+ Title string `json:"title"`
90- ContentHTML string `json:"content_html"`90+ ContentHTML string `json:"content_html"`
91- ContentText string `json:"content_text"`91+ ContentText string `json:"content_text"`
92- Summary string `json:"summary"`92+ Summary string `json:"summary"`
93- Author struct {93+ Author struct {
94 Name string `json:"name"`94 Name string `json:"name"`
95 } `json:"author"`95 } `json:"author"`
96 DatePublished string `json:"date_published"`96 DatePublished string `json:"date_published"`
modified internal/server/annotations_handler.go +46 -5
@@ -7,6 +7,8 @@ import (
77 "strconv"
88 "time"
99
10+ "github.com/go-chi/chi/v5"
11+
1012 "pkg.rbrt.fr/glean/internal/atproto"
1113 "pkg.rbrt.fr/glean/internal/db"
1214 )
@@ -51,11 +53,12 @@ func (s *Server) handleLibrary(w http.ResponseWriter, r *http.Request) {
5153 }
5254
5355 s.render(w, r, "library.html", map[string]any{
54- "User": user,
55- "Articles": articles,
56- "Annotations": annotations,
57- "LikedPage": likedPage,
58- "AnnotPage": annotPage,
56+ "User": user,
57+ "CurrentUserDID": user.DID,
58+ "Articles": articles,
59+ "Annotations": annotations,
60+ "LikedPage": likedPage,
61+ "AnnotPage": annotPage,
5962 })
6063 }
6164
@@ -105,3 +108,41 @@ func (s *Server) handleCreateAnnotation(w http.ResponseWriter, r *http.Request)
105108
106109 w.WriteHeader(http.StatusNoContent)
107110 }
111+
112+func (s *Server) handleDeleteAnnotation(w http.ResponseWriter, r *http.Request) {
113+ user := currentUser(r)
114+ id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
115+ if err != nil {
116+ http.Error(w, "invalid id", http.StatusBadRequest)
117+ return
118+ }
119+
120+ annotation, err := s.db.GetAnnotation(r.Context(), id)
121+ if err != nil {
122+ http.Error(w, "annotation not found", http.StatusNotFound)
123+ return
124+ }
125+
126+ if annotation.AuthorDID != user.DID {
127+ http.Error(w, "forbidden", http.StatusForbidden)
128+ return
129+ }
130+
131+ if annotation.URI != "" {
132+ if client := s.pdsClientForUser(r); client != nil {
133+ parsed, ok := atproto.ParseRecordURI(annotation.URI)
134+ if ok {
135+ if delErr := client.DeleteRecord(r.Context(), user.DID, parsed.Collection, parsed.RKey); delErr != nil {
136+ s.logger.Error("failed to delete annotation from PDS", "error", delErr)
137+ }
138+ }
139+ }
140+ }
141+
142+ if err := s.db.DeleteAnnotation(r.Context(), annotation.URI); err != nil {
143+ http.Error(w, err.Error(), http.StatusInternalServerError)
144+ return
145+ }
146+
147+ w.WriteHeader(http.StatusOK)
148+}
@@ -7,6 +7,8 @@ import (
7 "strconv"7 "strconv"
8 "time"8 "time"
9 9
10+ "github.com/go-chi/chi/v5"
11+
10 "pkg.rbrt.fr/glean/internal/atproto"12 "pkg.rbrt.fr/glean/internal/atproto"
11 "pkg.rbrt.fr/glean/internal/db"13 "pkg.rbrt.fr/glean/internal/db"
12 )14 )
@@ -51,11 +53,12 @@ func (s *Server) handleLibrary(w http.ResponseWriter, r *http.Request) {
51 }53 }
52 54
53 s.render(w, r, "library.html", map[string]any{55 s.render(w, r, "library.html", map[string]any{
54- "User": user,56+ "User": user,
55- "Articles": articles,57+ "CurrentUserDID": user.DID,
56- "Annotations": annotations,58+ "Articles": articles,
57- "LikedPage": likedPage,59+ "Annotations": annotations,
58- "AnnotPage": annotPage,60+ "LikedPage": likedPage,
61+ "AnnotPage": annotPage,
59 })62 })
60 }63 }
61 64
@@ -105,3 +108,41 @@ func (s *Server) handleCreateAnnotation(w http.ResponseWriter, r *http.Request)
105 108
106 w.WriteHeader(http.StatusNoContent)109 w.WriteHeader(http.StatusNoContent)
107 }110 }
111+
112+func (s *Server) handleDeleteAnnotation(w http.ResponseWriter, r *http.Request) {
113+ user := currentUser(r)
114+ id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
115+ if err != nil {
116+ http.Error(w, "invalid id", http.StatusBadRequest)
117+ return
118+ }
119+
120+ annotation, err := s.db.GetAnnotation(r.Context(), id)
121+ if err != nil {
122+ http.Error(w, "annotation not found", http.StatusNotFound)
123+ return
124+ }
125+
126+ if annotation.AuthorDID != user.DID {
127+ http.Error(w, "forbidden", http.StatusForbidden)
128+ return
129+ }
130+
131+ if annotation.URI != "" {
132+ if client := s.pdsClientForUser(r); client != nil {
133+ parsed, ok := atproto.ParseRecordURI(annotation.URI)
134+ if ok {
135+ if delErr := client.DeleteRecord(r.Context(), user.DID, parsed.Collection, parsed.RKey); delErr != nil {
136+ s.logger.Error("failed to delete annotation from PDS", "error", delErr)
137+ }
138+ }
139+ }
140+ }
141+
142+ if err := s.db.DeleteAnnotation(r.Context(), annotation.URI); err != nil {
143+ http.Error(w, err.Error(), http.StatusInternalServerError)
144+ return
145+ }
146+
147+ w.WriteHeader(http.StatusOK)
148+}
modified internal/server/articles_handler.go +8 -7
@@ -91,13 +91,14 @@ func (s *Server) handleArticleDetail(w http.ResponseWriter, r *http.Request) {
9191 feed, _ := s.db.GetFeed(r.Context(), article.FeedURL)
9292
9393 s.render(w, r, "article_detail.html", map[string]any{
94- "User": user,
95- "Article": article,
96- "Feed": feed,
97- "ReadState": readState,
98- "LikeCount": likeCount,
99- "HasLiked": liked,
100- "Annotations": annotations,
94+ "User": user,
95+ "CurrentUserDID": user.DID,
96+ "Article": article,
97+ "Feed": feed,
98+ "ReadState": readState,
99+ "LikeCount": likeCount,
100+ "HasLiked": liked,
101+ "Annotations": annotations,
101102 })
102103 }
103104
@@ -91,13 +91,14 @@ func (s *Server) handleArticleDetail(w http.ResponseWriter, r *http.Request) {
91 feed, _ := s.db.GetFeed(r.Context(), article.FeedURL)91 feed, _ := s.db.GetFeed(r.Context(), article.FeedURL)
92 92
93 s.render(w, r, "article_detail.html", map[string]any{93 s.render(w, r, "article_detail.html", map[string]any{
94- "User": user,94+ "User": user,
95- "Article": article,95+ "CurrentUserDID": user.DID,
96- "Feed": feed,96+ "Article": article,
97- "ReadState": readState,97+ "Feed": feed,
98- "LikeCount": likeCount,98+ "ReadState": readState,
99- "HasLiked": liked,99+ "LikeCount": likeCount,
100- "Annotations": annotations,100+ "HasLiked": liked,
101+ "Annotations": annotations,
101 })102 })
102 }103 }
103 104
modified internal/server/profile_handler.go +7 -4
@@ -18,12 +18,15 @@ func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) {
1818 annotations, _ := s.db.ListAnnotations(r.Context(), "", "", did, 50, 0)
1919 subCount, _ := s.db.GetSubscriptionCount(r.Context(), did)
2020
21+ user := currentUser(r)
22+
2123 s.render(w, r, "profile.html", map[string]any{
22- "User": currentUser(r),
24+ "User": user,
25+ "CurrentUserDID": user.DID,
2326 "ProfileUser": profileUser,
24- "Subscriptions": subs,
25- "Annotations": annotations,
27+ "Subscriptions": subs,
28+ "Annotations": annotations,
2629 "SubscriptionCount": subCount,
27- "AnnotationCount": len(annotations),
30+ "AnnotationCount": len(annotations),
2831 })
2932 }
@@ -18,12 +18,15 @@ func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) {
18 annotations, _ := s.db.ListAnnotations(r.Context(), "", "", did, 50, 0)18 annotations, _ := s.db.ListAnnotations(r.Context(), "", "", did, 50, 0)
19 subCount, _ := s.db.GetSubscriptionCount(r.Context(), did)19 subCount, _ := s.db.GetSubscriptionCount(r.Context(), did)
20 20
21+ user := currentUser(r)
22+
21 s.render(w, r, "profile.html", map[string]any{23 s.render(w, r, "profile.html", map[string]any{
22- "User": currentUser(r),24+ "User": user,
25+ "CurrentUserDID": user.DID,
23 "ProfileUser": profileUser,26 "ProfileUser": profileUser,
24- "Subscriptions": subs,27+ "Subscriptions": subs,
25- "Annotations": annotations,28+ "Annotations": annotations,
26 "SubscriptionCount": subCount,29 "SubscriptionCount": subCount,
27- "AnnotationCount": len(annotations),30+ "AnnotationCount": len(annotations),
28 })31 })
29 }32 }
modified internal/server/server.go +15 -0
@@ -135,6 +135,7 @@ func (s *Server) setupRoutes() {
135135 r.Use(s.requireAuth)
136136 r.Get("/", s.handleLibrary)
137137 r.Post("/create", s.handleCreateAnnotation)
138+ r.Post("/{id}/delete", s.handleDeleteAnnotation)
138139 })
139140
140141 s.router.Get("/auth/login", s.handleAuthLogin)
@@ -156,6 +157,20 @@ func (s *Server) setupRoutes() {
156157
157158 func (s *Server) loadTemplates() {
158159 fm := template.FuncMap{
160+ "dict": func(values ...any) (map[string]any, error) {
161+ if len(values)%2 != 0 {
162+ return nil, fmt.Errorf("dict requires even number of arguments")
163+ }
164+ m := make(map[string]any, len(values)/2)
165+ for i := 0; i < len(values); i += 2 {
166+ key, ok := values[i].(string)
167+ if !ok {
168+ return nil, fmt.Errorf("dict key must be string")
169+ }
170+ m[key] = values[i+1]
171+ }
172+ return m, nil
173+ },
159174 "formatDate": func(t time.Time) string {
160175 return t.Format("Jan 02, 2006")
161176 },
@@ -135,6 +135,7 @@ func (s *Server) setupRoutes() {
135 r.Use(s.requireAuth)135 r.Use(s.requireAuth)
136 r.Get("/", s.handleLibrary)136 r.Get("/", s.handleLibrary)
137 r.Post("/create", s.handleCreateAnnotation)137 r.Post("/create", s.handleCreateAnnotation)
138+ r.Post("/{id}/delete", s.handleDeleteAnnotation)
138 })139 })
139 140
140 s.router.Get("/auth/login", s.handleAuthLogin)141 s.router.Get("/auth/login", s.handleAuthLogin)
@@ -156,6 +157,20 @@ func (s *Server) setupRoutes() {
156 157
157 func (s *Server) loadTemplates() {158 func (s *Server) loadTemplates() {
158 fm := template.FuncMap{159 fm := template.FuncMap{
160+ "dict": func(values ...any) (map[string]any, error) {
161+ if len(values)%2 != 0 {
162+ return nil, fmt.Errorf("dict requires even number of arguments")
163+ }
164+ m := make(map[string]any, len(values)/2)
165+ for i := 0; i < len(values); i += 2 {
166+ key, ok := values[i].(string)
167+ if !ok {
168+ return nil, fmt.Errorf("dict key must be string")
169+ }
170+ m[key] = values[i+1]
171+ }
172+ return m, nil
173+ },
159 "formatDate": func(t time.Time) string {174 "formatDate": func(t time.Time) string {
160 return t.Format("Jan 02, 2006")175 return t.Format("Jan 02, 2006")
161 },176 },
modified internal/tmpl/article_detail.html +1 -1
@@ -99,7 +99,7 @@
9999
100100 <div id="annotations-list" class="space-y-3">
101101 {{range .Annotations}}
102- {{template "annotation-card.html" .}}
102+ {{template "annotation-card.html" dict "annotation" . "userDID" $.CurrentUserDID}}
103103 {{else}}
104104 <p class="text-sm text-spot-secondary">No annotations yet. Be the first to add one.</p>
105105 {{end}}
@@ -99,7 +99,7 @@
99 99
100 <div id="annotations-list" class="space-y-3">100 <div id="annotations-list" class="space-y-3">
101 {{range .Annotations}}101 {{range .Annotations}}
102- {{template "annotation-card.html" .}}102+ {{template "annotation-card.html" dict "annotation" . "userDID" $.CurrentUserDID}}
103 {{else}}103 {{else}}
104 <p class="text-sm text-spot-secondary">No annotations yet. Be the first to add one.</p>104 <p class="text-sm text-spot-secondary">No annotations yet. Be the first to add one.</p>
105 {{end}}105 {{end}}
modified internal/tmpl/base.html +30 -0
@@ -262,6 +262,36 @@
262262 (quote ? form.querySelector('input[name="note"]') : quoteInput).focus();
263263 }
264264
265+ function editAnnotation(btn, id, feedURL, articleURL, quote, note, tags) {
266+ var card = btn.closest('[id^="annotation-"]');
267+ card.innerHTML = '<form hx-post="/library/create" hx-swap="none" hx-on::after-request="editAnnotationDone(' + id + ')" class="space-y-3">' +
268+ '<input type="hidden" name="feed_url" value="' + feedURL + '">' +
269+ '<input type="hidden" name="article_url" value="' + articleURL + '">' +
270+ '<input type="text" name="quote" value="' + (quote || '').replace(/"/g, '&quot;') + '" placeholder="Quote a passage..." class="w-full bg-spot-hover text-spot-text rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">' +
271+ '<textarea name="note" rows="3" placeholder="Add a note..." class="w-full bg-spot-hover text-spot-text rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder resize-none">' + (note || '') + '</textarea>' +
272+ '<input type="text" name="tags" value="' + (tags || '').replace(/"/g, '&quot;') + '" placeholder="Tags (comma separated)" class="w-full bg-spot-hover text-spot-text rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">' +
273+ '<div class="flex gap-2 justify-end">' +
274+ '<button type="button" onclick="cancelEdit(' + id + ')" class="border border-spot-outline text-spot-text rounded-pill px-4 py-1.5 text-xs font-bold uppercase tracking-button hover:border-spot-text transition">Cancel</button>' +
275+ '<button type="submit" class="bg-spot-green text-white rounded-pill px-5 py-1.5 text-xs font-bold uppercase tracking-button hover:brightness-110 transition">Save</button>' +
276+ '</div></form>';
277+ htmx.process(card);
278+ var deleteReq = new XMLHttpRequest();
279+ deleteReq.open('POST', '/library/' + id + '/delete');
280+ deleteReq.setRequestHeader('HX-Request', 'true');
281+ deleteReq.send();
282+ }
283+
284+ function editAnnotationDone(id) {
285+ var params = new URLSearchParams(window.location.search);
286+ var url = window.location.pathname;
287+ if (params.toString()) url += '?' + params.toString();
288+ htmx.ajax('GET', url, '#main-content');
289+ }
290+
291+ function cancelEdit(id) {
292+ htmx.ajax('GET', window.location.pathname + window.location.search, '#main-content');
293+ }
294+
265295 document.addEventListener('mouseup', function(e) {
266296 var sel = window.getSelection();
267297 var text = sel.toString().trim();
@@ -262,6 +262,36 @@
262 (quote ? form.querySelector('input[name="note"]') : quoteInput).focus();262 (quote ? form.querySelector('input[name="note"]') : quoteInput).focus();
263 }263 }
264 264
265+ function editAnnotation(btn, id, feedURL, articleURL, quote, note, tags) {
266+ var card = btn.closest('[id^="annotation-"]');
267+ card.innerHTML = '<form hx-post="/library/create" hx-swap="none" hx-on::after-request="editAnnotationDone(' + id + ')" class="space-y-3">' +
268+ '<input type="hidden" name="feed_url" value="' + feedURL + '">' +
269+ '<input type="hidden" name="article_url" value="' + articleURL + '">' +
270+ '<input type="text" name="quote" value="' + (quote || '').replace(/"/g, '&quot;') + '" placeholder="Quote a passage..." class="w-full bg-spot-hover text-spot-text rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">' +
271+ '<textarea name="note" rows="3" placeholder="Add a note..." class="w-full bg-spot-hover text-spot-text rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder resize-none">' + (note || '') + '</textarea>' +
272+ '<input type="text" name="tags" value="' + (tags || '').replace(/"/g, '&quot;') + '" placeholder="Tags (comma separated)" class="w-full bg-spot-hover text-spot-text rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">' +
273+ '<div class="flex gap-2 justify-end">' +
274+ '<button type="button" onclick="cancelEdit(' + id + ')" class="border border-spot-outline text-spot-text rounded-pill px-4 py-1.5 text-xs font-bold uppercase tracking-button hover:border-spot-text transition">Cancel</button>' +
275+ '<button type="submit" class="bg-spot-green text-white rounded-pill px-5 py-1.5 text-xs font-bold uppercase tracking-button hover:brightness-110 transition">Save</button>' +
276+ '</div></form>';
277+ htmx.process(card);
278+ var deleteReq = new XMLHttpRequest();
279+ deleteReq.open('POST', '/library/' + id + '/delete');
280+ deleteReq.setRequestHeader('HX-Request', 'true');
281+ deleteReq.send();
282+ }
283+
284+ function editAnnotationDone(id) {
285+ var params = new URLSearchParams(window.location.search);
286+ var url = window.location.pathname;
287+ if (params.toString()) url += '?' + params.toString();
288+ htmx.ajax('GET', url, '#main-content');
289+ }
290+
291+ function cancelEdit(id) {
292+ htmx.ajax('GET', window.location.pathname + window.location.search, '#main-content');
293+ }
294+
265 document.addEventListener('mouseup', function(e) {295 document.addEventListener('mouseup', function(e) {
266 var sel = window.getSelection();296 var sel = window.getSelection();
267 var text = sel.toString().trim();297 var text = sel.toString().trim();
modified internal/tmpl/library.html +1 -8
@@ -45,14 +45,7 @@
4545 <h2 class="text-lg font-semibold text-spot-text mb-4">Annotations</h2>
4646 <div id="annotations-list" class="space-y-3">
4747 {{range .Annotations}}
48- <div class="bg-spot-surface rounded-xl p-4">
49- {{if .ArticleURL}}
50- <div class="text-xs text-spot-muted mb-2">
51- <a href="{{.ArticleURL}}" class="text-spot-secondary hover:text-spot-green transition truncate block">{{.ArticleURL}}</a>
52- </div>
53- {{end}}
54- {{template "annotation-card.html" .}}
55- </div>
48+ {{template "annotation-card.html" dict "annotation" . "userDID" $.CurrentUserDID}}
5649 {{else}}
5750 <div class="flex flex-col items-center justify-center py-12 text-center">
5851 <div class="w-14 h-14 rounded-full bg-spot-hover flex items-center justify-center mb-4">
@@ -45,14 +45,7 @@
45 <h2 class="text-lg font-semibold text-spot-text mb-4">Annotations</h2>45 <h2 class="text-lg font-semibold text-spot-text mb-4">Annotations</h2>
46 <div id="annotations-list" class="space-y-3">46 <div id="annotations-list" class="space-y-3">
47 {{range .Annotations}}47 {{range .Annotations}}
48- <div class="bg-spot-surface rounded-xl p-4">48+ {{template "annotation-card.html" dict "annotation" . "userDID" $.CurrentUserDID}}
49- {{if .ArticleURL}}
50- <div class="text-xs text-spot-muted mb-2">
51- <a href="{{.ArticleURL}}" class="text-spot-secondary hover:text-spot-green transition truncate block">{{.ArticleURL}}</a>
52- </div>
53- {{end}}
54- {{template "annotation-card.html" .}}
55- </div>
56 {{else}}49 {{else}}
57 <div class="flex flex-col items-center justify-center py-12 text-center">50 <div class="flex flex-col items-center justify-center py-12 text-center">
58 <div class="w-14 h-14 rounded-full bg-spot-hover flex items-center justify-center mb-4">51 <div class="w-14 h-14 rounded-full bg-spot-hover flex items-center justify-center mb-4">
modified internal/tmpl/partials/annotation-card.html +27 -13
@@ -1,22 +1,36 @@
11 {{define "annotation-card.html"}}
2-<div class="bg-spot-surface rounded-xl p-4">
3- {{if .Quote.Valid}}
4- <blockquote class="border-l-2 border-spot-green pl-3 text-sm text-spot-body italic">{{.Quote.String}}</blockquote>
2+<div id="annotation-{{.annotation.ID}}" class="bg-spot-surface rounded-xl p-4 shadow-spot">
3+ {{if .annotation.ArticleURL}}
4+ <div class="text-xs text-spot-muted mb-2">
5+ <a href="{{.annotation.ArticleURL}}" class="text-spot-secondary hover:text-spot-green transition truncate block">{{.annotation.ArticleURL}}</a>
6+ </div>
7+ {{end}}
8+ {{if .annotation.Quote.Valid}}
9+ <blockquote class="border-l-2 border-spot-green pl-3 text-sm text-spot-body italic leading-relaxed">{{.annotation.Quote.String}}</blockquote>
510 {{end}}
6- {{if .Note.Valid}}
7- <p class="text-sm text-spot-body mt-2">{{.Note.String}}</p>
11+ {{if .annotation.Note.Valid}}
12+ <p class="text-sm text-spot-body mt-2 leading-relaxed">{{.annotation.Note.String}}</p>
813 {{end}}
9- {{if .Tags.Valid}}
10- <div class="flex gap-1 mt-2 flex-wrap">
11- {{range $tag := split .Tags.String ","}}<span class="text-xs bg-spot-hover text-spot-secondary px-2.5 py-0.5 rounded-full">{{$tag}}</span>{{end}}
14+ {{if .annotation.Tags.Valid}}
15+ <div class="flex gap-1.5 mt-3 flex-wrap">
16+ {{range $tag := split .annotation.Tags.String ","}}<span class="text-xs bg-spot-hover text-spot-secondary px-2.5 py-0.5 rounded-full">{{$tag}}</span>{{end}}
1217 </div>
1318 {{end}}
14- {{if .Rating.Valid}}
15- <div class="text-sm text-spot-orange mt-1">{{repeat "&#9733;" (int .Rating.Int64)}}</div>
19+ {{if .annotation.Rating.Valid}}
20+ <div class="text-sm text-spot-orange mt-2 tracking-wide">{{repeat "&#9733;" (int .annotation.Rating.Int64)}}</div>
1621 {{end}}
17- <div class="text-xs text-spot-muted mt-2">
18- <a href="/profile/{{.AuthorDID}}" class="hover:text-spot-green transition">{{.AuthorHandle}}</a>
19- {{if .CreatedAt.Valid}}<span class="ml-2">{{.CreatedAt.Time.Format "Jan 2, 2006 15:04"}}</span>{{end}}
22+ <div class="flex items-center justify-between mt-3 pt-3 border-t border-spot-divider">
23+ <div class="flex items-center gap-2 text-xs text-spot-muted">
24+ <a href="/profile/{{.annotation.AuthorDID}}" class="hover:text-spot-green transition">{{.annotation.AuthorHandle}}</a>
25+ {{if .annotation.CreatedAt.Valid}}<span>&middot;</span><span>{{.annotation.CreatedAt.Time.Format "Jan 2, 2006 15:04"}}</span>{{end}}
26+ </div>
27+ {{if eq .annotation.AuthorDID .userDID}}
28+ <div class="flex items-center gap-2">
29+ <button class="text-xs text-spot-secondary hover:text-spot-text transition" onclick="editAnnotation(this, {{.annotation.ID}}, '{{js .annotation.FeedURL}}', '{{js .annotation.ArticleURL}}', '{{js .annotation.Quote.String}}', '{{js .annotation.Note.String}}', '{{js .annotation.Tags.String}}')">Edit</button>
30+ <button hx-post="/library/{{.annotation.ID}}/delete" hx-target="#annotation-{{.annotation.ID}}" hx-swap="outerHTML swap:0.3s" hx-confirm="Delete this annotation?"
31+ class="text-xs text-spot-secondary hover:text-spot-red transition">Delete</button>
32+ </div>
33+ {{end}}
2034 </div>
2135 </div>
2236 {{end}}
@@ -1,22 +1,36 @@
1 {{define "annotation-card.html"}}1 {{define "annotation-card.html"}}
2-<div class="bg-spot-surface rounded-xl p-4">2+<div id="annotation-{{.annotation.ID}}" class="bg-spot-surface rounded-xl p-4 shadow-spot">
3- {{if .Quote.Valid}}3+ {{if .annotation.ArticleURL}}
4- <blockquote class="border-l-2 border-spot-green pl-3 text-sm text-spot-body italic">{{.Quote.String}}</blockquote>4+ <div class="text-xs text-spot-muted mb-2">
5+ <a href="{{.annotation.ArticleURL}}" class="text-spot-secondary hover:text-spot-green transition truncate block">{{.annotation.ArticleURL}}</a>
6+ </div>
7+ {{end}}
8+ {{if .annotation.Quote.Valid}}
9+ <blockquote class="border-l-2 border-spot-green pl-3 text-sm text-spot-body italic leading-relaxed">{{.annotation.Quote.String}}</blockquote>
5 {{end}}10 {{end}}
6- {{if .Note.Valid}}11+ {{if .annotation.Note.Valid}}
7- <p class="text-sm text-spot-body mt-2">{{.Note.String}}</p>12+ <p class="text-sm text-spot-body mt-2 leading-relaxed">{{.annotation.Note.String}}</p>
8 {{end}}13 {{end}}
9- {{if .Tags.Valid}}14+ {{if .annotation.Tags.Valid}}
10- <div class="flex gap-1 mt-2 flex-wrap">15+ <div class="flex gap-1.5 mt-3 flex-wrap">
11- {{range $tag := split .Tags.String ","}}<span class="text-xs bg-spot-hover text-spot-secondary px-2.5 py-0.5 rounded-full">{{$tag}}</span>{{end}}16+ {{range $tag := split .annotation.Tags.String ","}}<span class="text-xs bg-spot-hover text-spot-secondary px-2.5 py-0.5 rounded-full">{{$tag}}</span>{{end}}
12 </div>17 </div>
13 {{end}}18 {{end}}
14- {{if .Rating.Valid}}19+ {{if .annotation.Rating.Valid}}
15- <div class="text-sm text-spot-orange mt-1">{{repeat "&#9733;" (int .Rating.Int64)}}</div>20+ <div class="text-sm text-spot-orange mt-2 tracking-wide">{{repeat "&#9733;" (int .annotation.Rating.Int64)}}</div>
16 {{end}}21 {{end}}
17- <div class="text-xs text-spot-muted mt-2">22+ <div class="flex items-center justify-between mt-3 pt-3 border-t border-spot-divider">
18- <a href="/profile/{{.AuthorDID}}" class="hover:text-spot-green transition">{{.AuthorHandle}}</a>23+ <div class="flex items-center gap-2 text-xs text-spot-muted">
19- {{if .CreatedAt.Valid}}<span class="ml-2">{{.CreatedAt.Time.Format "Jan 2, 2006 15:04"}}</span>{{end}}24+ <a href="/profile/{{.annotation.AuthorDID}}" class="hover:text-spot-green transition">{{.annotation.AuthorHandle}}</a>
25+ {{if .annotation.CreatedAt.Valid}}<span>&middot;</span><span>{{.annotation.CreatedAt.Time.Format "Jan 2, 2006 15:04"}}</span>{{end}}
26+ </div>
27+ {{if eq .annotation.AuthorDID .userDID}}
28+ <div class="flex items-center gap-2">
29+ <button class="text-xs text-spot-secondary hover:text-spot-text transition" onclick="editAnnotation(this, {{.annotation.ID}}, '{{js .annotation.FeedURL}}', '{{js .annotation.ArticleURL}}', '{{js .annotation.Quote.String}}', '{{js .annotation.Note.String}}', '{{js .annotation.Tags.String}}')">Edit</button>
30+ <button hx-post="/library/{{.annotation.ID}}/delete" hx-target="#annotation-{{.annotation.ID}}" hx-swap="outerHTML swap:0.3s" hx-confirm="Delete this annotation?"
31+ class="text-xs text-spot-secondary hover:text-spot-red transition">Delete</button>
32+ </div>
33+ {{end}}
20 </div>34 </div>
21 </div>35 </div>
22 {{end}}36 {{end}}
modified internal/tmpl/profile.html +1 -1
@@ -38,7 +38,7 @@
3838 <h2 class="text-lg font-semibold text-spot-text mb-3">Recent annotations</h2>
3939 <div class="space-y-3">
4040 {{range .Annotations}}
41- {{template "annotation-card.html" .}}
41+ {{template "annotation-card.html" dict "annotation" . "userDID" $.CurrentUserDID}}
4242 {{else}}
4343 <p class="text-sm text-spot-secondary">No public annotations.</p>
4444 {{end}}
@@ -38,7 +38,7 @@
38 <h2 class="text-lg font-semibold text-spot-text mb-3">Recent annotations</h2>38 <h2 class="text-lg font-semibold text-spot-text mb-3">Recent annotations</h2>
39 <div class="space-y-3">39 <div class="space-y-3">
40 {{range .Annotations}}40 {{range .Annotations}}
41- {{template "annotation-card.html" .}}41+ {{template "annotation-card.html" dict "annotation" . "userDID" $.CurrentUserDID}}
42 {{else}}42 {{else}}
43 <p class="text-sm text-spot-secondary">No public annotations.</p>43 <p class="text-sm text-spot-secondary">No public annotations.</p>
44 {{end}}44 {{end}}
modified main.go +1 -0
@@ -99,6 +99,7 @@ func main() {
9999 cancel()
100100
101101 fmt.Println("glean stopped")
102+ os.Exit(0)
102103 }
103104
104105 func envOr(key, fallback string) string {
@@ -99,6 +99,7 @@ func main() {
99 cancel()99 cancel()
100 100
101 fmt.Println("glean stopped")101 fmt.Println("glean stopped")
102+ os.Exit(0)
102 }103 }
103 104
104 func envOr(key, fallback string) string {105 func envOr(key, fallback string) string {