nandi/gleanpublic⑂ Fork 0
⑂ 112adee
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.

Fix like count and liked status on article cardsUnverified

Julien Robert committed 2026-04-21T21:29:45+02:00 Browse files
112adee parent: a4c19e5
modified internal/atproto/stream_handler.go +2 -1
@@ -195,7 +195,8 @@ func (h *StreamDBHandler) handleMarginNote(ctx context.Context, event *Event) er
195195 return h.db.CreateAnnotation(ctx, a)
196196
197197 case "delete":
198- return h.db.DeleteAnnotation(ctx, event.URI)
198+ // TODO: I actually don't think we should delete an annotation on Glean if deleted from Margin
199+ // return h.db.DeleteAnnotation(ctx, event.URI)
199200 }
200201 return nil
201202 }
@@ -195,7 +195,8 @@ func (h *StreamDBHandler) handleMarginNote(ctx context.Context, event *Event) er
195 return h.db.CreateAnnotation(ctx, a)195 return h.db.CreateAnnotation(ctx, a)
196 196
197 case "delete":197 case "delete":
198- return h.db.DeleteAnnotation(ctx, event.URI)198+ // TODO: I actually don't think we should delete an annotation on Glean if deleted from Margin
199+ // return h.db.DeleteAnnotation(ctx, event.URI)
199 }200 }
200 return nil201 return nil
201 }202 }
modified internal/db/article.go +62 -18
@@ -24,6 +24,8 @@ type Article struct {
2424 Updated sql.NullTime
2525 FetchedAt sql.NullTime
2626 IsRead sql.NullBool
27+ LikeCount int
28+ HasLiked bool
2729 }
2830
2931 type ReadState struct {
@@ -71,25 +73,37 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit,
7173 query = `
7274 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
7375 a.published, a.updated, a.fetched_at,
74- COALESCE(r.is_read, 0)
76+ COALESCE(r.is_read, 0),
77+ COALESCE(lc.cnt, 0),
78+ COALESCE(ul.liked, 0)
7579 FROM articles a
7680 LEFT JOIN feeds f ON a.feed_url = f.feed_url
7781 LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id
82+ LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc
83+ ON lc.feed_url = a.feed_url AND lc.article_url = a.url
84+ LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul
85+ ON ul.feed_url = a.feed_url AND ul.article_url = a.url
7886 WHERE a.feed_url = ?
7987 `
80- args = []any{userDID, feedURL}
88+ args = []any{userDID, userDID, feedURL}
8189 } else {
8290 query = `
8391 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
8492 a.published, a.updated, a.fetched_at,
85- COALESCE(r.is_read, 0)
93+ COALESCE(r.is_read, 0),
94+ COALESCE(lc.cnt, 0),
95+ COALESCE(ul.liked, 0)
8696 FROM articles a
8797 JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ?
8898 LEFT JOIN feeds f ON a.feed_url = f.feed_url
8999 LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id
100+ LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc
101+ ON lc.feed_url = a.feed_url AND lc.article_url = a.url
102+ LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul
103+ ON ul.feed_url = a.feed_url AND ul.article_url = a.url
90104 WHERE 1=1
91105 `
92- args = []any{userDID, userDID}
106+ args = []any{userDID, userDID, userDID}
93107 }
94108
95109 query += ` ORDER BY a.published DESC LIMIT ? OFFSET ?`
@@ -106,7 +120,7 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit,
106120 a := &Article{}
107121 if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author,
108122 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,
109- &a.IsRead); err != nil {
123+ &a.IsRead, &a.LikeCount, &a.HasLiked); err != nil {
110124 return nil, err
111125 }
112126 articles = append(articles, a)
@@ -122,25 +136,37 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l
122136 query = `
123137 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
124138 a.published, a.updated, a.fetched_at,
125- COALESCE(r.is_read, 0)
139+ COALESCE(r.is_read, 0),
140+ COALESCE(lc.cnt, 0),
141+ COALESCE(ul.liked, 0)
126142 FROM articles a
127143 LEFT JOIN feeds f ON a.feed_url = f.feed_url
128144 LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id
145+ LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc
146+ ON lc.feed_url = a.feed_url AND lc.article_url = a.url
147+ LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul
148+ ON ul.feed_url = a.feed_url AND ul.article_url = a.url
129149 WHERE a.feed_url = ? AND (r.is_read = 0 OR r.is_read IS NULL)
130150 `
131- args = []any{userDID, feedURL}
151+ args = []any{userDID, userDID, feedURL}
132152 } else {
133153 query = `
134154 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
135155 a.published, a.updated, a.fetched_at,
136- COALESCE(r.is_read, 0)
156+ COALESCE(r.is_read, 0),
157+ COALESCE(lc.cnt, 0),
158+ COALESCE(ul.liked, 0)
137159 FROM articles a
138160 JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ?
139161 LEFT JOIN feeds f ON a.feed_url = f.feed_url
140162 LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id
163+ LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc
164+ ON lc.feed_url = a.feed_url AND lc.article_url = a.url
165+ LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul
166+ ON ul.feed_url = a.feed_url AND ul.article_url = a.url
141167 WHERE (r.is_read = 0 OR r.is_read IS NULL)
142168 `
143- args = []any{userDID, userDID}
169+ args = []any{userDID, userDID, userDID}
144170 }
145171
146172 query += ` ORDER BY a.published DESC LIMIT ? OFFSET ?`
@@ -157,7 +183,7 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l
157183 a := &Article{}
158184 if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author,
159185 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,
160- &a.IsRead); err != nil {
186+ &a.IsRead, &a.LikeCount, &a.HasLiked); err != nil {
161187 return nil, err
162188 }
163189 articles = append(articles, a)
@@ -173,25 +199,37 @@ func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, lim
173199 query = `
174200 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
175201 a.published, a.updated, a.fetched_at,
176- COALESCE(r.is_read, 0)
202+ COALESCE(r.is_read, 0),
203+ COALESCE(lc.cnt, 0),
204+ COALESCE(ul.liked, 0)
177205 FROM articles a
178206 LEFT JOIN feeds f ON a.feed_url = f.feed_url
179207 JOIN read_state r ON r.user_did = ? AND r.article_id = a.id
208+ LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc
209+ ON lc.feed_url = a.feed_url AND lc.article_url = a.url
210+ LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul
211+ ON ul.feed_url = a.feed_url AND ul.article_url = a.url
180212 WHERE r.is_read = 1 AND a.feed_url = ?
181213 `
182- args = []any{userDID, feedURL}
214+ args = []any{userDID, userDID, feedURL}
183215 } else {
184216 query = `
185217 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
186218 a.published, a.updated, a.fetched_at,
187- COALESCE(r.is_read, 0)
219+ COALESCE(r.is_read, 0),
220+ COALESCE(lc.cnt, 0),
221+ COALESCE(ul.liked, 0)
188222 FROM articles a
189223 JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ?
190224 LEFT JOIN feeds f ON a.feed_url = f.feed_url
191225 JOIN read_state r ON r.user_did = ? AND r.article_id = a.id
226+ LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc
227+ ON lc.feed_url = a.feed_url AND lc.article_url = a.url
228+ LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul
229+ ON ul.feed_url = a.feed_url AND ul.article_url = a.url
192230 WHERE r.is_read = 1
193231 `
194- args = []any{userDID, userDID}
232+ args = []any{userDID, userDID, userDID}
195233 }
196234
197235 query += ` ORDER BY a.published DESC LIMIT ? OFFSET ?`
@@ -208,7 +246,7 @@ func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, lim
208246 a := &Article{}
209247 if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author,
210248 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,
211- &a.IsRead); err != nil {
249+ &a.IsRead, &a.LikeCount, &a.HasLiked); err != nil {
212250 return nil, err
213251 }
214252 articles = append(articles, a)
@@ -354,16 +392,22 @@ func (db *DB) SearchArticles(ctx context.Context, userDID, query string, limit,
354392 rows, err := db.QueryContext(ctx, `
355393 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
356394 a.published, a.updated, a.fetched_at,
357- COALESCE(r.is_read, 0)
395+ COALESCE(r.is_read, 0),
396+ COALESCE(lc.cnt, 0),
397+ COALESCE(ul.liked, 0)
358398 FROM articles_fts ft
359399 JOIN articles a ON a.id = ft.rowid
360400 JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ?
361401 LEFT JOIN feeds f ON a.feed_url = f.feed_url
362402 LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id
403+ LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc
404+ ON lc.feed_url = a.feed_url AND lc.article_url = a.url
405+ LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul
406+ ON ul.feed_url = a.feed_url AND ul.article_url = a.url
363407 WHERE articles_fts MATCH ?
364408 ORDER BY ft.rank
365409 LIMIT ? OFFSET ?
366- `, userDID, userDID, columnQuery, limit, offset)
410+ `, userDID, userDID, userDID, columnQuery, limit, offset)
367411 if err != nil {
368412 return nil, err
369413 }
@@ -374,7 +418,7 @@ func (db *DB) SearchArticles(ctx context.Context, userDID, query string, limit,
374418 a := &Article{}
375419 if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author,
376420 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,
377- &a.IsRead); err != nil {
421+ &a.IsRead, &a.LikeCount, &a.HasLiked); err != nil {
378422 return nil, err
379423 }
380424 articles = append(articles, a)
@@ -24,6 +24,8 @@ type Article struct {
24 Updated sql.NullTime24 Updated sql.NullTime
25 FetchedAt sql.NullTime25 FetchedAt sql.NullTime
26 IsRead sql.NullBool26 IsRead sql.NullBool
27+ LikeCount int
28+ HasLiked bool
27 }29 }
28 30
29 type ReadState struct {31 type ReadState struct {
@@ -71,25 +73,37 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit,
71 query = `73 query = `
72 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,74 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
73 a.published, a.updated, a.fetched_at,75 a.published, a.updated, a.fetched_at,
74- COALESCE(r.is_read, 0)76+ COALESCE(r.is_read, 0),
77+ COALESCE(lc.cnt, 0),
78+ COALESCE(ul.liked, 0)
75 FROM articles a79 FROM articles a
76 LEFT JOIN feeds f ON a.feed_url = f.feed_url80 LEFT JOIN feeds f ON a.feed_url = f.feed_url
77 LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id81 LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id
82+ LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc
83+ ON lc.feed_url = a.feed_url AND lc.article_url = a.url
84+ LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul
85+ ON ul.feed_url = a.feed_url AND ul.article_url = a.url
78 WHERE a.feed_url = ?86 WHERE a.feed_url = ?
79 `87 `
80- args = []any{userDID, feedURL}88+ args = []any{userDID, userDID, feedURL}
81 } else {89 } else {
82 query = `90 query = `
83 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,91 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
84 a.published, a.updated, a.fetched_at,92 a.published, a.updated, a.fetched_at,
85- COALESCE(r.is_read, 0)93+ COALESCE(r.is_read, 0),
94+ COALESCE(lc.cnt, 0),
95+ COALESCE(ul.liked, 0)
86 FROM articles a96 FROM articles a
87 JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ?97 JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ?
88 LEFT JOIN feeds f ON a.feed_url = f.feed_url98 LEFT JOIN feeds f ON a.feed_url = f.feed_url
89 LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id99 LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id
100+ LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc
101+ ON lc.feed_url = a.feed_url AND lc.article_url = a.url
102+ LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul
103+ ON ul.feed_url = a.feed_url AND ul.article_url = a.url
90 WHERE 1=1104 WHERE 1=1
91 `105 `
92- args = []any{userDID, userDID}106+ args = []any{userDID, userDID, userDID}
93 }107 }
94 108
95 query += ` ORDER BY a.published DESC LIMIT ? OFFSET ?`109 query += ` ORDER BY a.published DESC LIMIT ? OFFSET ?`
@@ -106,7 +120,7 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit,
106 a := &Article{}120 a := &Article{}
107 if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author,121 if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author,
108 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,122 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,
109- &a.IsRead); err != nil {123+ &a.IsRead, &a.LikeCount, &a.HasLiked); err != nil {
110 return nil, err124 return nil, err
111 }125 }
112 articles = append(articles, a)126 articles = append(articles, a)
@@ -122,25 +136,37 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l
122 query = `136 query = `
123 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,137 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
124 a.published, a.updated, a.fetched_at,138 a.published, a.updated, a.fetched_at,
125- COALESCE(r.is_read, 0)139+ COALESCE(r.is_read, 0),
140+ COALESCE(lc.cnt, 0),
141+ COALESCE(ul.liked, 0)
126 FROM articles a142 FROM articles a
127 LEFT JOIN feeds f ON a.feed_url = f.feed_url143 LEFT JOIN feeds f ON a.feed_url = f.feed_url
128 LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id144 LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id
145+ LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc
146+ ON lc.feed_url = a.feed_url AND lc.article_url = a.url
147+ LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul
148+ ON ul.feed_url = a.feed_url AND ul.article_url = a.url
129 WHERE a.feed_url = ? AND (r.is_read = 0 OR r.is_read IS NULL)149 WHERE a.feed_url = ? AND (r.is_read = 0 OR r.is_read IS NULL)
130 `150 `
131- args = []any{userDID, feedURL}151+ args = []any{userDID, userDID, feedURL}
132 } else {152 } else {
133 query = `153 query = `
134 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,154 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
135 a.published, a.updated, a.fetched_at,155 a.published, a.updated, a.fetched_at,
136- COALESCE(r.is_read, 0)156+ COALESCE(r.is_read, 0),
157+ COALESCE(lc.cnt, 0),
158+ COALESCE(ul.liked, 0)
137 FROM articles a159 FROM articles a
138 JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ?160 JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ?
139 LEFT JOIN feeds f ON a.feed_url = f.feed_url161 LEFT JOIN feeds f ON a.feed_url = f.feed_url
140 LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id162 LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id
163+ LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc
164+ ON lc.feed_url = a.feed_url AND lc.article_url = a.url
165+ LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul
166+ ON ul.feed_url = a.feed_url AND ul.article_url = a.url
141 WHERE (r.is_read = 0 OR r.is_read IS NULL)167 WHERE (r.is_read = 0 OR r.is_read IS NULL)
142 `168 `
143- args = []any{userDID, userDID}169+ args = []any{userDID, userDID, userDID}
144 }170 }
145 171
146 query += ` ORDER BY a.published DESC LIMIT ? OFFSET ?`172 query += ` ORDER BY a.published DESC LIMIT ? OFFSET ?`
@@ -157,7 +183,7 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l
157 a := &Article{}183 a := &Article{}
158 if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author,184 if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author,
159 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,185 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,
160- &a.IsRead); err != nil {186+ &a.IsRead, &a.LikeCount, &a.HasLiked); err != nil {
161 return nil, err187 return nil, err
162 }188 }
163 articles = append(articles, a)189 articles = append(articles, a)
@@ -173,25 +199,37 @@ func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, lim
173 query = `199 query = `
174 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,200 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
175 a.published, a.updated, a.fetched_at,201 a.published, a.updated, a.fetched_at,
176- COALESCE(r.is_read, 0)202+ COALESCE(r.is_read, 0),
203+ COALESCE(lc.cnt, 0),
204+ COALESCE(ul.liked, 0)
177 FROM articles a205 FROM articles a
178 LEFT JOIN feeds f ON a.feed_url = f.feed_url206 LEFT JOIN feeds f ON a.feed_url = f.feed_url
179 JOIN read_state r ON r.user_did = ? AND r.article_id = a.id207 JOIN read_state r ON r.user_did = ? AND r.article_id = a.id
208+ LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc
209+ ON lc.feed_url = a.feed_url AND lc.article_url = a.url
210+ LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul
211+ ON ul.feed_url = a.feed_url AND ul.article_url = a.url
180 WHERE r.is_read = 1 AND a.feed_url = ?212 WHERE r.is_read = 1 AND a.feed_url = ?
181 `213 `
182- args = []any{userDID, feedURL}214+ args = []any{userDID, userDID, feedURL}
183 } else {215 } else {
184 query = `216 query = `
185 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,217 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
186 a.published, a.updated, a.fetched_at,218 a.published, a.updated, a.fetched_at,
187- COALESCE(r.is_read, 0)219+ COALESCE(r.is_read, 0),
220+ COALESCE(lc.cnt, 0),
221+ COALESCE(ul.liked, 0)
188 FROM articles a222 FROM articles a
189 JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ?223 JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ?
190 LEFT JOIN feeds f ON a.feed_url = f.feed_url224 LEFT JOIN feeds f ON a.feed_url = f.feed_url
191 JOIN read_state r ON r.user_did = ? AND r.article_id = a.id225 JOIN read_state r ON r.user_did = ? AND r.article_id = a.id
226+ LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc
227+ ON lc.feed_url = a.feed_url AND lc.article_url = a.url
228+ LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul
229+ ON ul.feed_url = a.feed_url AND ul.article_url = a.url
192 WHERE r.is_read = 1230 WHERE r.is_read = 1
193 `231 `
194- args = []any{userDID, userDID}232+ args = []any{userDID, userDID, userDID}
195 }233 }
196 234
197 query += ` ORDER BY a.published DESC LIMIT ? OFFSET ?`235 query += ` ORDER BY a.published DESC LIMIT ? OFFSET ?`
@@ -208,7 +246,7 @@ func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, lim
208 a := &Article{}246 a := &Article{}
209 if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author,247 if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author,
210 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,248 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,
211- &a.IsRead); err != nil {249+ &a.IsRead, &a.LikeCount, &a.HasLiked); err != nil {
212 return nil, err250 return nil, err
213 }251 }
214 articles = append(articles, a)252 articles = append(articles, a)
@@ -354,16 +392,22 @@ func (db *DB) SearchArticles(ctx context.Context, userDID, query string, limit,
354 rows, err := db.QueryContext(ctx, `392 rows, err := db.QueryContext(ctx, `
355 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,393 SELECT a.id, a.feed_url, COALESCE(f.title, ''), f.favicon_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
356 a.published, a.updated, a.fetched_at,394 a.published, a.updated, a.fetched_at,
357- COALESCE(r.is_read, 0)395+ COALESCE(r.is_read, 0),
396+ COALESCE(lc.cnt, 0),
397+ COALESCE(ul.liked, 0)
358 FROM articles_fts ft398 FROM articles_fts ft
359 JOIN articles a ON a.id = ft.rowid399 JOIN articles a ON a.id = ft.rowid
360 JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ?400 JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ?
361 LEFT JOIN feeds f ON a.feed_url = f.feed_url401 LEFT JOIN feeds f ON a.feed_url = f.feed_url
362 LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id402 LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id
403+ LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc
404+ ON lc.feed_url = a.feed_url AND lc.article_url = a.url
405+ LEFT JOIN (SELECT feed_url, article_url, 1 as liked FROM likes WHERE author_did = ?) ul
406+ ON ul.feed_url = a.feed_url AND ul.article_url = a.url
363 WHERE articles_fts MATCH ?407 WHERE articles_fts MATCH ?
364 ORDER BY ft.rank408 ORDER BY ft.rank
365 LIMIT ? OFFSET ?409 LIMIT ? OFFSET ?
366- `, userDID, userDID, columnQuery, limit, offset)410+ `, userDID, userDID, userDID, columnQuery, limit, offset)
367 if err != nil {411 if err != nil {
368 return nil, err412 return nil, err
369 }413 }
@@ -374,7 +418,7 @@ func (db *DB) SearchArticles(ctx context.Context, userDID, query string, limit,
374 a := &Article{}418 a := &Article{}
375 if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author,419 if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author,
376 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,420 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,
377- &a.IsRead); err != nil {421+ &a.IsRead, &a.LikeCount, &a.HasLiked); err != nil {
378 return nil, err422 return nil, err
379 }423 }
380 articles = append(articles, a)424 articles = append(articles, a)
modified internal/db/social.go +18 -9
@@ -227,6 +227,7 @@ type TrendingItem struct {
227227 FaviconURL string
228228 LikeCount int
229229 AnnotationCount int
230+ HasLiked bool
230231 }
231232
232233 func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since string, limit, offset int) ([]*TrendingItem, error) {
@@ -235,11 +236,13 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st
235236 COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''),
236237 COALESCE(f.favicon_url, ''),
237238 COUNT(DISTINCT l.id) AS like_count,
238- COUNT(DISTINCT a.id) AS annotation_count
239+ COUNT(DISTINCT a.id) AS annotation_count,
240+ COALESCE(MAX(CASE WHEN ul.id IS NOT NULL THEN 1 ELSE 0 END), 0)
239241 FROM likes l
240242 JOIN articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url
241243 LEFT JOIN feeds f ON f.feed_url = l.feed_url
242244 LEFT JOIN annotations a ON a.feed_url = l.feed_url AND a.article_url = l.article_url AND a.created_at >= ?
245+ LEFT JOIN likes ul ON ul.feed_url = l.feed_url AND ul.article_url = l.article_url AND ul.author_did = ?
243246 WHERE l.created_at >= ?
244247 AND l.author_did IN (
245248 SELECT CASE WHEN us.user_a = ? THEN us.user_b ELSE us.user_a END
@@ -251,7 +254,7 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st
251254 GROUP BY ar.id
252255 ORDER BY like_count DESC, annotation_count DESC
253256 LIMIT ? OFFSET ?
254- `, since, since, userDID, userDID, userDID, userDID, userDID, limit, offset)
257+ `, since, userDID, since, userDID, userDID, userDID, userDID, userDID, limit, offset)
255258 if err != nil {
256259 return nil, err
257260 }
@@ -262,7 +265,7 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st
262265 item := &TrendingItem{}
263266 if err := rows.Scan(&item.ArticleID, &item.Title, &item.URL, &item.Author,
264267 &item.Summary, &item.FeedURL, &item.FeedTitle, &item.FaviconURL,
265- &item.LikeCount, &item.AnnotationCount); err != nil {
268+ &item.LikeCount, &item.AnnotationCount, &item.HasLiked); err != nil {
266269 return nil, err
267270 }
268271 results = append(results, item)
@@ -270,22 +273,24 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st
270273 return results, rows.Err()
271274 }
272275
273-func (db *DB) ListTrendingArticles(ctx context.Context, since string, limit, offset int) ([]*TrendingItem, error) {
276+func (db *DB) ListTrendingArticles(ctx context.Context, userDID, since string, limit, offset int) ([]*TrendingItem, error) {
274277 rows, err := db.QueryContext(ctx, `
275278 SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''),
276279 COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''),
277280 COALESCE(f.favicon_url, ''),
278281 COUNT(DISTINCT l.id) AS like_count,
279- COUNT(DISTINCT a.id) AS annotation_count
282+ COUNT(DISTINCT a.id) AS annotation_count,
283+ COALESCE(MAX(CASE WHEN ul.id IS NOT NULL THEN 1 ELSE 0 END), 0)
280284 FROM likes l
281285 JOIN articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url
282286 LEFT JOIN feeds f ON f.feed_url = l.feed_url
283287 LEFT JOIN annotations a ON a.feed_url = l.feed_url AND a.article_url = l.article_url AND a.created_at >= ?
288+ LEFT JOIN likes ul ON ul.feed_url = l.feed_url AND ul.article_url = l.article_url AND ul.author_did = ?
284289 WHERE l.created_at >= ?
285290 GROUP BY ar.id
286291 ORDER BY like_count DESC, annotation_count DESC
287292 LIMIT ? OFFSET ?
288- `, since, since, limit, offset)
293+ `, since, userDID, since, limit, offset)
289294 if err != nil {
290295 return nil, err
291296 }
@@ -296,7 +301,7 @@ func (db *DB) ListTrendingArticles(ctx context.Context, since string, limit, off
296301 item := &TrendingItem{}
297302 if err := rows.Scan(&item.ArticleID, &item.Title, &item.URL, &item.Author,
298303 &item.Summary, &item.FeedURL, &item.FeedTitle, &item.FaviconURL,
299- &item.LikeCount, &item.AnnotationCount); err != nil {
304+ &item.LikeCount, &item.AnnotationCount, &item.HasLiked); err != nil {
300305 return nil, err
301306 }
302307 results = append(results, item)
@@ -309,11 +314,15 @@ func (db *DB) ListLikedArticles(ctx context.Context, userDID string, limit, offs
309314 SELECT DISTINCT a.id, a.feed_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
310315 a.published, a.updated, a.fetched_at,
311316 COALESCE(f.title, ''),
312- COALESCE(r.is_read, 0)
317+ COALESCE(r.is_read, 0),
318+ COALESCE(lc.cnt, 0),
319+ 1
313320 FROM likes l
314321 JOIN articles a ON a.url = l.article_url AND a.feed_url = l.feed_url
315322 LEFT JOIN feeds f ON f.feed_url = a.feed_url
316323 LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id
324+ LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc
325+ ON lc.feed_url = a.feed_url AND lc.article_url = a.url
317326 WHERE l.author_did = ?
318327 ORDER BY l.created_at DESC
319328 LIMIT ? OFFSET ?
@@ -328,7 +337,7 @@ func (db *DB) ListLikedArticles(ctx context.Context, userDID string, limit, offs
328337 a := &Article{}
329338 if err := rows.Scan(&a.ID, &a.FeedURL, &a.GUID, &a.Title, &a.URL, &a.Author,
330339 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,
331- &a.FeedTitle, &a.IsRead); err != nil {
340+ &a.FeedTitle, &a.IsRead, &a.LikeCount, &a.HasLiked); err != nil {
332341 return nil, err
333342 }
334343 articles = append(articles, a)
@@ -227,6 +227,7 @@ type TrendingItem struct {
227 FaviconURL string227 FaviconURL string
228 LikeCount int228 LikeCount int
229 AnnotationCount int229 AnnotationCount int
230+ HasLiked bool
230 }231 }
231 232
232 func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since string, limit, offset int) ([]*TrendingItem, error) {233 func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since string, limit, offset int) ([]*TrendingItem, error) {
@@ -235,11 +236,13 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st
235 COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''),236 COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''),
236 COALESCE(f.favicon_url, ''),237 COALESCE(f.favicon_url, ''),
237 COUNT(DISTINCT l.id) AS like_count,238 COUNT(DISTINCT l.id) AS like_count,
238- COUNT(DISTINCT a.id) AS annotation_count239+ COUNT(DISTINCT a.id) AS annotation_count,
240+ COALESCE(MAX(CASE WHEN ul.id IS NOT NULL THEN 1 ELSE 0 END), 0)
239 FROM likes l241 FROM likes l
240 JOIN articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url242 JOIN articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url
241 LEFT JOIN feeds f ON f.feed_url = l.feed_url243 LEFT JOIN feeds f ON f.feed_url = l.feed_url
242 LEFT JOIN annotations a ON a.feed_url = l.feed_url AND a.article_url = l.article_url AND a.created_at >= ?244 LEFT JOIN annotations a ON a.feed_url = l.feed_url AND a.article_url = l.article_url AND a.created_at >= ?
245+ LEFT JOIN likes ul ON ul.feed_url = l.feed_url AND ul.article_url = l.article_url AND ul.author_did = ?
243 WHERE l.created_at >= ?246 WHERE l.created_at >= ?
244 AND l.author_did IN (247 AND l.author_did IN (
245 SELECT CASE WHEN us.user_a = ? THEN us.user_b ELSE us.user_a END248 SELECT CASE WHEN us.user_a = ? THEN us.user_b ELSE us.user_a END
@@ -251,7 +254,7 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st
251 GROUP BY ar.id254 GROUP BY ar.id
252 ORDER BY like_count DESC, annotation_count DESC255 ORDER BY like_count DESC, annotation_count DESC
253 LIMIT ? OFFSET ?256 LIMIT ? OFFSET ?
254- `, since, since, userDID, userDID, userDID, userDID, userDID, limit, offset)257+ `, since, userDID, since, userDID, userDID, userDID, userDID, userDID, limit, offset)
255 if err != nil {258 if err != nil {
256 return nil, err259 return nil, err
257 }260 }
@@ -262,7 +265,7 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st
262 item := &TrendingItem{}265 item := &TrendingItem{}
263 if err := rows.Scan(&item.ArticleID, &item.Title, &item.URL, &item.Author,266 if err := rows.Scan(&item.ArticleID, &item.Title, &item.URL, &item.Author,
264 &item.Summary, &item.FeedURL, &item.FeedTitle, &item.FaviconURL,267 &item.Summary, &item.FeedURL, &item.FeedTitle, &item.FaviconURL,
265- &item.LikeCount, &item.AnnotationCount); err != nil {268+ &item.LikeCount, &item.AnnotationCount, &item.HasLiked); err != nil {
266 return nil, err269 return nil, err
267 }270 }
268 results = append(results, item)271 results = append(results, item)
@@ -270,22 +273,24 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st
270 return results, rows.Err()273 return results, rows.Err()
271 }274 }
272 275
273-func (db *DB) ListTrendingArticles(ctx context.Context, since string, limit, offset int) ([]*TrendingItem, error) {276+func (db *DB) ListTrendingArticles(ctx context.Context, userDID, since string, limit, offset int) ([]*TrendingItem, error) {
274 rows, err := db.QueryContext(ctx, `277 rows, err := db.QueryContext(ctx, `
275 SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''),278 SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''),
276 COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''),279 COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''),
277 COALESCE(f.favicon_url, ''),280 COALESCE(f.favicon_url, ''),
278 COUNT(DISTINCT l.id) AS like_count,281 COUNT(DISTINCT l.id) AS like_count,
279- COUNT(DISTINCT a.id) AS annotation_count282+ COUNT(DISTINCT a.id) AS annotation_count,
283+ COALESCE(MAX(CASE WHEN ul.id IS NOT NULL THEN 1 ELSE 0 END), 0)
280 FROM likes l284 FROM likes l
281 JOIN articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url285 JOIN articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url
282 LEFT JOIN feeds f ON f.feed_url = l.feed_url286 LEFT JOIN feeds f ON f.feed_url = l.feed_url
283 LEFT JOIN annotations a ON a.feed_url = l.feed_url AND a.article_url = l.article_url AND a.created_at >= ?287 LEFT JOIN annotations a ON a.feed_url = l.feed_url AND a.article_url = l.article_url AND a.created_at >= ?
288+ LEFT JOIN likes ul ON ul.feed_url = l.feed_url AND ul.article_url = l.article_url AND ul.author_did = ?
284 WHERE l.created_at >= ?289 WHERE l.created_at >= ?
285 GROUP BY ar.id290 GROUP BY ar.id
286 ORDER BY like_count DESC, annotation_count DESC291 ORDER BY like_count DESC, annotation_count DESC
287 LIMIT ? OFFSET ?292 LIMIT ? OFFSET ?
288- `, since, since, limit, offset)293+ `, since, userDID, since, limit, offset)
289 if err != nil {294 if err != nil {
290 return nil, err295 return nil, err
291 }296 }
@@ -296,7 +301,7 @@ func (db *DB) ListTrendingArticles(ctx context.Context, since string, limit, off
296 item := &TrendingItem{}301 item := &TrendingItem{}
297 if err := rows.Scan(&item.ArticleID, &item.Title, &item.URL, &item.Author,302 if err := rows.Scan(&item.ArticleID, &item.Title, &item.URL, &item.Author,
298 &item.Summary, &item.FeedURL, &item.FeedTitle, &item.FaviconURL,303 &item.Summary, &item.FeedURL, &item.FeedTitle, &item.FaviconURL,
299- &item.LikeCount, &item.AnnotationCount); err != nil {304+ &item.LikeCount, &item.AnnotationCount, &item.HasLiked); err != nil {
300 return nil, err305 return nil, err
301 }306 }
302 results = append(results, item)307 results = append(results, item)
@@ -309,11 +314,15 @@ func (db *DB) ListLikedArticles(ctx context.Context, userDID string, limit, offs
309 SELECT DISTINCT a.id, a.feed_url, a.guid, a.title, a.url, a.author, a.summary, a.content,314 SELECT DISTINCT a.id, a.feed_url, a.guid, a.title, a.url, a.author, a.summary, a.content,
310 a.published, a.updated, a.fetched_at,315 a.published, a.updated, a.fetched_at,
311 COALESCE(f.title, ''),316 COALESCE(f.title, ''),
312- COALESCE(r.is_read, 0)317+ COALESCE(r.is_read, 0),
318+ COALESCE(lc.cnt, 0),
319+ 1
313 FROM likes l320 FROM likes l
314 JOIN articles a ON a.url = l.article_url AND a.feed_url = l.feed_url321 JOIN articles a ON a.url = l.article_url AND a.feed_url = l.feed_url
315 LEFT JOIN feeds f ON f.feed_url = a.feed_url322 LEFT JOIN feeds f ON f.feed_url = a.feed_url
316 LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id323 LEFT JOIN read_state r ON r.user_did = ? AND r.article_id = a.id
324+ LEFT JOIN (SELECT feed_url, article_url, COUNT(*) as cnt FROM likes GROUP BY feed_url, article_url) lc
325+ ON lc.feed_url = a.feed_url AND lc.article_url = a.url
317 WHERE l.author_did = ?326 WHERE l.author_did = ?
318 ORDER BY l.created_at DESC327 ORDER BY l.created_at DESC
319 LIMIT ? OFFSET ?328 LIMIT ? OFFSET ?
@@ -328,7 +337,7 @@ func (db *DB) ListLikedArticles(ctx context.Context, userDID string, limit, offs
328 a := &Article{}337 a := &Article{}
329 if err := rows.Scan(&a.ID, &a.FeedURL, &a.GUID, &a.Title, &a.URL, &a.Author,338 if err := rows.Scan(&a.ID, &a.FeedURL, &a.GUID, &a.Title, &a.URL, &a.Author,
330 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,339 &a.Summary, &a.Content, &a.Published, &a.Updated, &a.FetchedAt,
331- &a.FeedTitle, &a.IsRead); err != nil {340+ &a.FeedTitle, &a.IsRead, &a.LikeCount, &a.HasLiked); err != nil {
332 return nil, err341 return nil, err
333 }342 }
334 articles = append(articles, a)343 articles = append(articles, a)
modified internal/server/articles_handler.go +1 -2
@@ -22,9 +22,8 @@ func writeLikeButton(w http.ResponseWriter, articleID int64, liked bool, count i
2222 fill = "currentColor"
2323 colorCls = "text-spot-red"
2424 }
25- const heartPath = `<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z"/>`
2625 w.Header().Set("Content-Type", "text/html")
27- _, _ = fmt.Fprintf(w, `<button hx-post="/articles/%d/like" hx-target="#like-btn" hx-swap="outerHTML" id="like-btn" 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 inline-flex items-center gap-1.5"><svg class="w-3.5 h-3.5 %s" fill="%s" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">%s</svg><span>%d</span></button>`, articleID, colorCls, fill, heartPath, count)
26+ _, _ = fmt.Fprintf(w, `<button hx-post="/articles/%d/like" hx-target="this" hx-swap="outerHTML" 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 inline-flex items-center gap-1.5"><svg class="w-3.5 h-3.5 %s" fill="%s" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z"/></svg><span>%d</span></button>`, articleID, colorCls, fill, count)
2827 }
2928
3029 func (s *Server) handleArticles(w http.ResponseWriter, r *http.Request) {
@@ -22,9 +22,8 @@ func writeLikeButton(w http.ResponseWriter, articleID int64, liked bool, count i
22 fill = "currentColor"22 fill = "currentColor"
23 colorCls = "text-spot-red"23 colorCls = "text-spot-red"
24 }24 }
25- const heartPath = `<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z"/>`
26 w.Header().Set("Content-Type", "text/html")25 w.Header().Set("Content-Type", "text/html")
27- _, _ = fmt.Fprintf(w, `<button hx-post="/articles/%d/like" hx-target="#like-btn" hx-swap="outerHTML" id="like-btn" 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 inline-flex items-center gap-1.5"><svg class="w-3.5 h-3.5 %s" fill="%s" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">%s</svg><span>%d</span></button>`, articleID, colorCls, fill, heartPath, count)26+ _, _ = fmt.Fprintf(w, `<button hx-post="/articles/%d/like" hx-target="this" hx-swap="outerHTML" 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 inline-flex items-center gap-1.5"><svg class="w-3.5 h-3.5 %s" fill="%s" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z"/></svg><span>%d</span></button>`, articleID, colorCls, fill, count)
28 }27 }
29 28
30 func (s *Server) handleArticles(w http.ResponseWriter, r *http.Request) {29 func (s *Server) handleArticles(w http.ResponseWriter, r *http.Request) {
modified internal/server/dashboard_handler.go +1 -1
@@ -25,7 +25,7 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
2525
2626 since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339)
2727 personalTrending, _ := s.db.ListTrendingArticlesForUser(r.Context(), user.DID, since, 5, 0)
28- globalTrending, _ := s.db.ListTrendingArticles(r.Context(), since, 10, 0)
28+ globalTrending, _ := s.db.ListTrendingArticles(r.Context(), user.DID, since, 10, 0)
2929
3030 s.render(w, r, "dashboard.html", map[string]any{
3131 "User": user,
@@ -25,7 +25,7 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
25 25
26 since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339)26 since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339)
27 personalTrending, _ := s.db.ListTrendingArticlesForUser(r.Context(), user.DID, since, 5, 0)27 personalTrending, _ := s.db.ListTrendingArticlesForUser(r.Context(), user.DID, since, 5, 0)
28- globalTrending, _ := s.db.ListTrendingArticles(r.Context(), since, 10, 0)28+ globalTrending, _ := s.db.ListTrendingArticles(r.Context(), user.DID, since, 10, 0)
29 29
30 s.render(w, r, "dashboard.html", map[string]any{30 s.render(w, r, "dashboard.html", map[string]any{
31 "User": user,31 "User": user,
modified internal/tmpl/article_detail.html +2 -3
@@ -23,8 +23,7 @@
2323 </div>
2424
2525 <div class="flex items-center gap-2 mt-6">
26- <button hx-post="/articles/{{.Article.ID}}/like" hx-target="#like-btn" hx-swap="outerHTML"
27- id="like-btn"
26+ <button hx-post="/articles/{{.Article.ID}}/like" hx-target="this" hx-swap="outerHTML"
2827 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 inline-flex items-center gap-1.5">
2928 <svg class="w-3.5 h-3.5 {{if .HasLiked}}text-spot-red{{else}}text-spot-muted{{end}}" fill="{{if .HasLiked}}currentColor{{else}}none{{end}}" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">{{template "icon-heart"}}</svg>
3029 <span>{{.LikeCount}}</span>
@@ -166,7 +165,7 @@
166165 document.addEventListener('keydown', function(e) {
167166 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
168167 if (e.key === 'l') {
169- var btn = document.getElementById('like-btn');
168+ var btn = document.querySelector('[hx-post*="/like"]');
170169 if (btn) btn.click();
171170 } else if (e.key === 'm') {
172171 var rbtn = document.getElementById('read-btn');
@@ -23,8 +23,7 @@
23 </div>23 </div>
24 24
25 <div class="flex items-center gap-2 mt-6">25 <div class="flex items-center gap-2 mt-6">
26- <button hx-post="/articles/{{.Article.ID}}/like" hx-target="#like-btn" hx-swap="outerHTML"26+ <button hx-post="/articles/{{.Article.ID}}/like" hx-target="this" hx-swap="outerHTML"
27- id="like-btn"
28 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 inline-flex items-center gap-1.5">27 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 inline-flex items-center gap-1.5">
29 <svg class="w-3.5 h-3.5 {{if .HasLiked}}text-spot-red{{else}}text-spot-muted{{end}}" fill="{{if .HasLiked}}currentColor{{else}}none{{end}}" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">{{template "icon-heart"}}</svg>28 <svg class="w-3.5 h-3.5 {{if .HasLiked}}text-spot-red{{else}}text-spot-muted{{end}}" fill="{{if .HasLiked}}currentColor{{else}}none{{end}}" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">{{template "icon-heart"}}</svg>
30 <span>{{.LikeCount}}</span>29 <span>{{.LikeCount}}</span>
@@ -166,7 +165,7 @@
166 document.addEventListener('keydown', function(e) {165 document.addEventListener('keydown', function(e) {
167 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;166 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
168 if (e.key === 'l') {167 if (e.key === 'l') {
169- var btn = document.getElementById('like-btn');168+ var btn = document.querySelector('[hx-post*="/like"]');
170 if (btn) btn.click();169 if (btn) btn.click();
171 } else if (e.key === 'm') {170 } else if (e.key === 'm') {
172 var rbtn = document.getElementById('read-btn');171 var rbtn = document.getElementById('read-btn');
modified internal/tmpl/feeds.html +1 -1
@@ -63,7 +63,7 @@
6363 {{range .Subscriptions}}
6464 <div class="feed-item px-5 py-4 flex items-center justify-between hover:bg-spot-hover-50 transition rounded-xl">
6565 <a href="/articles?feed={{.FeedURL}}" class="min-w-0 flex-1 flex items-center gap-3">
66- {{if .FaviconURL.Valid}}<img src="{{.FaviconURL.String}}" class="w-5 h-5 rounded shrink-0" loading="lazy">{{else}}<span class="shrink-0 w-5 h-5 flex items-center justify-center text-spot-muted">{{template "icon-globe"}}</span>{{end}}
66+ {{if .FaviconURL.Valid}}<img src="{{.FaviconURL.String}}" class="w-5 h-5 rounded shrink-0" loading="lazy">{{else}}<span class="shrink-0 w-5 h-5 flex items-center justify-center text-spot-muted"><svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">{{template "icon-globe"}}</svg></span>{{end}}
6767 <div class="min-w-0 flex-1">
6868 <div class="flex items-center gap-2">
6969 <span class="font-bold text-spot-text truncate">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</span>
@@ -63,7 +63,7 @@
63 {{range .Subscriptions}}63 {{range .Subscriptions}}
64 <div class="feed-item px-5 py-4 flex items-center justify-between hover:bg-spot-hover-50 transition rounded-xl">64 <div class="feed-item px-5 py-4 flex items-center justify-between hover:bg-spot-hover-50 transition rounded-xl">
65 <a href="/articles?feed={{.FeedURL}}" class="min-w-0 flex-1 flex items-center gap-3">65 <a href="/articles?feed={{.FeedURL}}" class="min-w-0 flex-1 flex items-center gap-3">
66- {{if .FaviconURL.Valid}}<img src="{{.FaviconURL.String}}" class="w-5 h-5 rounded shrink-0" loading="lazy">{{else}}<span class="shrink-0 w-5 h-5 flex items-center justify-center text-spot-muted">{{template "icon-globe"}}</span>{{end}}66+ {{if .FaviconURL.Valid}}<img src="{{.FaviconURL.String}}" class="w-5 h-5 rounded shrink-0" loading="lazy">{{else}}<span class="shrink-0 w-5 h-5 flex items-center justify-center text-spot-muted"><svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">{{template "icon-globe"}}</svg></span>{{end}}
67 <div class="min-w-0 flex-1">67 <div class="min-w-0 flex-1">
68 <div class="flex items-center gap-2">68 <div class="flex items-center gap-2">
69 <span class="font-bold text-spot-text truncate">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</span>69 <span class="font-bold text-spot-text truncate">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</span>
modified internal/tmpl/login.html +1 -1
@@ -23,7 +23,7 @@
2323
2424 <button type="submit" form="login-form"
2525 class="w-full flex items-center justify-center gap-3 border border-spot-outline text-spot-text rounded-pill px-4 py-3 text-sm font-bold uppercase tracking-button hover:bg-spot-hover-50 transition">
26- {{template "icon-globe"}}
26+ <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">{{template "icon-globe"}}</svg>
2727 Sign in with Atmosphere
2828 </button>
2929 </div>
@@ -23,7 +23,7 @@
23 23
24 <button type="submit" form="login-form"24 <button type="submit" form="login-form"
25 class="w-full flex items-center justify-center gap-3 border border-spot-outline text-spot-text rounded-pill px-4 py-3 text-sm font-bold uppercase tracking-button hover:bg-spot-hover-50 transition">25 class="w-full flex items-center justify-center gap-3 border border-spot-outline text-spot-text rounded-pill px-4 py-3 text-sm font-bold uppercase tracking-button hover:bg-spot-hover-50 transition">
26- {{template "icon-globe"}}26+ <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">{{template "icon-globe"}}</svg>
27 Sign in with Atmosphere27 Sign in with Atmosphere
28 </button>28 </button>
29 </div>29 </div>
modified internal/tmpl/partials/icon-globe.html +1 -1
@@ -1 +1 @@
1-{{define "icon-globe"}}<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10A15.3 15.3 0 0 1 12 2z"/><line x1="2" y1="12" x2="22" y2="12"/></svg>{{end}}
1+{{define "icon-globe"}}<circle cx="12" cy="12" r="10"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10A15.3 15.3 0 0 1 12 2z"/><line x1="2" y1="12" x2="22" y2="12"/>{{end}}
@@ -1 +1 @@
1-{{define "icon-globe"}}<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10A15.3 15.3 0 0 1 12 2z"/><line x1="2" y1="12" x2="22" y2="12"/></svg>{{end}}1+{{define "icon-globe"}}<circle cx="12" cy="12" r="10"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10A15.3 15.3 0 0 1 12 2z"/><line x1="2" y1="12" x2="22" y2="12"/>{{end}}
modified internal/tmpl/partials/icon-heart.html +1 -1
@@ -1 +1 @@
1-{{define "icon-heart"}}<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z"/></svg>{{end}}
1+{{define "icon-heart"}}<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z"/>{{end}}
@@ -1 +1 @@
1-{{define "icon-heart"}}<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z"/></svg>{{end}}1+{{define "icon-heart"}}<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z"/>{{end}}
modified internal/tmpl/partials/like-button.html +3 -2
@@ -1,6 +1,7 @@
11 {{define "like-button.html"}}
22 <button hx-post="/articles/{{.ID}}/like" hx-target="this" hx-swap="outerHTML"
3- class="text-spot-muted hover:text-spot-red transition">
4- <svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">{{template "icon-heart"}}</svg>
3+ 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 inline-flex items-center gap-1.5">
4+ <svg class="w-3.5 h-3.5 {{if .HasLiked}}text-spot-red{{else}}text-spot-muted{{end}}" fill="{{if .HasLiked}}currentColor{{else}}none{{end}}" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">{{template "icon-heart"}}</svg>
5+ <span>{{.LikeCount}}</span>
56 </button>
67 {{end}}
@@ -1,6 +1,7 @@
1 {{define "like-button.html"}}1 {{define "like-button.html"}}
2 <button hx-post="/articles/{{.ID}}/like" hx-target="this" hx-swap="outerHTML"2 <button hx-post="/articles/{{.ID}}/like" hx-target="this" hx-swap="outerHTML"
3- class="text-spot-muted hover:text-spot-red transition">3+ 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 inline-flex items-center gap-1.5">
4- <svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">{{template "icon-heart"}}</svg>4+ <svg class="w-3.5 h-3.5 {{if .HasLiked}}text-spot-red{{else}}text-spot-muted{{end}}" fill="{{if .HasLiked}}currentColor{{else}}none{{end}}" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">{{template "icon-heart"}}</svg>
5+ <span>{{.LikeCount}}</span>
5 </button>6 </button>
6 {{end}}7 {{end}}