Fix like count and liked status on article cardsUnverified
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 | ||
| 195 | 195 | return h.db.CreateAnnotation(ctx, a) |
| 196 | 196 | |
| 197 | 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 | 201 | return nil |
| 201 | 202 | } |
| @@ -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 nil | 201 | return nil |
| 201 | } | 202 | } |
modified
internal/db/article.go +62 -18 | @@ -24,6 +24,8 @@ type Article struct { | ||
| 24 | 24 | Updated sql.NullTime |
| 25 | 25 | FetchedAt sql.NullTime |
| 26 | 26 | IsRead sql.NullBool |
| 27 | + LikeCount int | |
| 28 | + HasLiked bool | |
| 27 | 29 | } |
| 28 | 30 | |
| 29 | 31 | type ReadState struct { |
| @@ -71,25 +73,37 @@ func (db *DB) ListArticles(ctx context.Context, userDID, feedURL string, limit, | ||
| 71 | 73 | query = ` |
| 72 | 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 | 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 | 79 | FROM articles a |
| 76 | 80 | LEFT JOIN feeds f ON a.feed_url = f.feed_url |
| 77 | 81 | 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 | 86 | WHERE a.feed_url = ? |
| 79 | 87 | ` |
| 80 | - args = []any{userDID, feedURL} | |
| 88 | + args = []any{userDID, userDID, feedURL} | |
| 81 | 89 | } else { |
| 82 | 90 | query = ` |
| 83 | 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 | 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 | 96 | FROM articles a |
| 87 | 97 | JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? |
| 88 | 98 | LEFT JOIN feeds f ON a.feed_url = f.feed_url |
| 89 | 99 | 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 | 104 | WHERE 1=1 |
| 91 | 105 | ` |
| 92 | - args = []any{userDID, userDID} | |
| 106 | + args = []any{userDID, userDID, userDID} | |
| 93 | 107 | } |
| 94 | 108 | |
| 95 | 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 | 120 | a := &Article{} |
| 107 | 121 | if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author, |
| 108 | 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 | 124 | return nil, err |
| 111 | 125 | } |
| 112 | 126 | articles = append(articles, a) |
| @@ -122,25 +136,37 @@ func (db *DB) ListUnreadArticles(ctx context.Context, userDID, feedURL string, l | ||
| 122 | 136 | query = ` |
| 123 | 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 | 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 | 142 | FROM articles a |
| 127 | 143 | LEFT JOIN feeds f ON a.feed_url = f.feed_url |
| 128 | 144 | 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 | 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 | 152 | } else { |
| 133 | 153 | query = ` |
| 134 | 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 | 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 | 159 | FROM articles a |
| 138 | 160 | JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? |
| 139 | 161 | LEFT JOIN feeds f ON a.feed_url = f.feed_url |
| 140 | 162 | 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 | 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 | 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 | 183 | a := &Article{} |
| 158 | 184 | if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author, |
| 159 | 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 | 187 | return nil, err |
| 162 | 188 | } |
| 163 | 189 | articles = append(articles, a) |
| @@ -173,25 +199,37 @@ func (db *DB) ListReadArticles(ctx context.Context, userDID, feedURL string, lim | ||
| 173 | 199 | query = ` |
| 174 | 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 | 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 | 205 | FROM articles a |
| 178 | 206 | LEFT JOIN feeds f ON a.feed_url = f.feed_url |
| 179 | 207 | 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 | 212 | WHERE r.is_read = 1 AND a.feed_url = ? |
| 181 | 213 | ` |
| 182 | - args = []any{userDID, feedURL} | |
| 214 | + args = []any{userDID, userDID, feedURL} | |
| 183 | 215 | } else { |
| 184 | 216 | query = ` |
| 185 | 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 | 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 | 222 | FROM articles a |
| 189 | 223 | JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? |
| 190 | 224 | LEFT JOIN feeds f ON a.feed_url = f.feed_url |
| 191 | 225 | 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 | 230 | WHERE r.is_read = 1 |
| 193 | 231 | ` |
| 194 | - args = []any{userDID, userDID} | |
| 232 | + args = []any{userDID, userDID, userDID} | |
| 195 | 233 | } |
| 196 | 234 | |
| 197 | 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 | 246 | a := &Article{} |
| 209 | 247 | if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author, |
| 210 | 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 | 250 | return nil, err |
| 213 | 251 | } |
| 214 | 252 | articles = append(articles, a) |
| @@ -354,16 +392,22 @@ func (db *DB) SearchArticles(ctx context.Context, userDID, query string, limit, | ||
| 354 | 392 | rows, err := db.QueryContext(ctx, ` |
| 355 | 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 | 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 | 398 | FROM articles_fts ft |
| 359 | 399 | JOIN articles a ON a.id = ft.rowid |
| 360 | 400 | JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? |
| 361 | 401 | LEFT JOIN feeds f ON a.feed_url = f.feed_url |
| 362 | 402 | 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 | 407 | WHERE articles_fts MATCH ? |
| 364 | 408 | ORDER BY ft.rank |
| 365 | 409 | LIMIT ? OFFSET ? |
| 366 | - `, userDID, userDID, columnQuery, limit, offset) | |
| 410 | + `, userDID, userDID, userDID, columnQuery, limit, offset) | |
| 367 | 411 | if err != nil { |
| 368 | 412 | return nil, err |
| 369 | 413 | } |
| @@ -374,7 +418,7 @@ func (db *DB) SearchArticles(ctx context.Context, userDID, query string, limit, | ||
| 374 | 418 | a := &Article{} |
| 375 | 419 | if err := rows.Scan(&a.ID, &a.FeedURL, &a.FeedTitle, &a.FeedFaviconURL, &a.GUID, &a.Title, &a.URL, &a.Author, |
| 376 | 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 | 422 | return nil, err |
| 379 | 423 | } |
| 380 | 424 | articles = append(articles, a) |
| @@ -24,6 +24,8 @@ type Article struct { | |||
| 24 | Updated sql.NullTime | 24 | Updated sql.NullTime |
| 25 | FetchedAt sql.NullTime | 25 | FetchedAt sql.NullTime |
| 26 | IsRead sql.NullBool | 26 | 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 a | 79 | FROM articles a |
| 76 | LEFT JOIN feeds f ON a.feed_url = f.feed_url | 80 | 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.id | 81 | 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 a | 96 | 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_url | 98 | 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.id | 99 | 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=1 | 104 | 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, err | 124 | 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 a | 142 | FROM articles a |
| 127 | LEFT JOIN feeds f ON a.feed_url = f.feed_url | 143 | 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.id | 144 | 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 a | 159 | 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_url | 161 | 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.id | 162 | 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, err | 187 | 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 a | 205 | FROM articles a |
| 178 | LEFT JOIN feeds f ON a.feed_url = f.feed_url | 206 | 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.id | 207 | 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 a | 222 | 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_url | 224 | 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.id | 225 | 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 = 1 | 230 | 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, err | 250 | 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 ft | 398 | FROM articles_fts ft |
| 359 | JOIN articles a ON a.id = ft.rowid | 399 | 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_url | 401 | 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.id | 402 | 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.rank | 408 | 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, err | 412 | 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, err | 422 | 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 { | ||
| 227 | 227 | FaviconURL string |
| 228 | 228 | LikeCount int |
| 229 | 229 | AnnotationCount int |
| 230 | + HasLiked bool | |
| 230 | 231 | } |
| 231 | 232 | |
| 232 | 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 | 236 | COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''), |
| 236 | 237 | COALESCE(f.favicon_url, ''), |
| 237 | 238 | 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) | |
| 239 | 241 | FROM likes l |
| 240 | 242 | JOIN articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url |
| 241 | 243 | LEFT JOIN feeds f ON f.feed_url = l.feed_url |
| 242 | 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 | 246 | WHERE l.created_at >= ? |
| 244 | 247 | AND l.author_did IN ( |
| 245 | 248 | 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 | 254 | GROUP BY ar.id |
| 252 | 255 | ORDER BY like_count DESC, annotation_count DESC |
| 253 | 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 | 258 | if err != nil { |
| 256 | 259 | return nil, err |
| 257 | 260 | } |
| @@ -262,7 +265,7 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st | ||
| 262 | 265 | item := &TrendingItem{} |
| 263 | 266 | if err := rows.Scan(&item.ArticleID, &item.Title, &item.URL, &item.Author, |
| 264 | 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 | 269 | return nil, err |
| 267 | 270 | } |
| 268 | 271 | results = append(results, item) |
| @@ -270,22 +273,24 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st | ||
| 270 | 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 | 277 | rows, err := db.QueryContext(ctx, ` |
| 275 | 278 | SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''), |
| 276 | 279 | COALESCE(ar.summary, ''), l.feed_url, COALESCE(f.title, ''), |
| 277 | 280 | COALESCE(f.favicon_url, ''), |
| 278 | 281 | 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) | |
| 280 | 284 | FROM likes l |
| 281 | 285 | JOIN articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url |
| 282 | 286 | LEFT JOIN feeds f ON f.feed_url = l.feed_url |
| 283 | 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 | 289 | WHERE l.created_at >= ? |
| 285 | 290 | GROUP BY ar.id |
| 286 | 291 | ORDER BY like_count DESC, annotation_count DESC |
| 287 | 292 | LIMIT ? OFFSET ? |
| 288 | - `, since, since, limit, offset) | |
| 293 | + `, since, userDID, since, limit, offset) | |
| 289 | 294 | if err != nil { |
| 290 | 295 | return nil, err |
| 291 | 296 | } |
| @@ -296,7 +301,7 @@ func (db *DB) ListTrendingArticles(ctx context.Context, since string, limit, off | ||
| 296 | 301 | item := &TrendingItem{} |
| 297 | 302 | if err := rows.Scan(&item.ArticleID, &item.Title, &item.URL, &item.Author, |
| 298 | 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 | 305 | return nil, err |
| 301 | 306 | } |
| 302 | 307 | results = append(results, item) |
| @@ -309,11 +314,15 @@ func (db *DB) ListLikedArticles(ctx context.Context, userDID string, limit, offs | ||
| 309 | 314 | SELECT DISTINCT a.id, a.feed_url, a.guid, a.title, a.url, a.author, a.summary, a.content, |
| 310 | 315 | a.published, a.updated, a.fetched_at, |
| 311 | 316 | COALESCE(f.title, ''), |
| 312 | - COALESCE(r.is_read, 0) | |
| 317 | + COALESCE(r.is_read, 0), | |
| 318 | + COALESCE(lc.cnt, 0), | |
| 319 | + 1 | |
| 313 | 320 | FROM likes l |
| 314 | 321 | JOIN articles a ON a.url = l.article_url AND a.feed_url = l.feed_url |
| 315 | 322 | LEFT JOIN feeds f ON f.feed_url = a.feed_url |
| 316 | 323 | 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 | 326 | WHERE l.author_did = ? |
| 318 | 327 | ORDER BY l.created_at DESC |
| 319 | 328 | LIMIT ? OFFSET ? |
| @@ -328,7 +337,7 @@ func (db *DB) ListLikedArticles(ctx context.Context, userDID string, limit, offs | ||
| 328 | 337 | a := &Article{} |
| 329 | 338 | if err := rows.Scan(&a.ID, &a.FeedURL, &a.GUID, &a.Title, &a.URL, &a.Author, |
| 330 | 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 | 341 | return nil, err |
| 333 | 342 | } |
| 334 | 343 | articles = append(articles, a) |
| @@ -227,6 +227,7 @@ type TrendingItem struct { | |||
| 227 | FaviconURL string | 227 | FaviconURL string |
| 228 | LikeCount int | 228 | LikeCount int |
| 229 | AnnotationCount int | 229 | 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_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) | ||
| 239 | FROM likes l | 241 | FROM likes l |
| 240 | JOIN articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url | 242 | 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_url | 243 | 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 END | 248 | 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.id | 254 | GROUP BY ar.id |
| 252 | ORDER BY like_count DESC, annotation_count DESC | 255 | 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, err | 259 | 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, err | 269 | 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_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) | ||
| 280 | FROM likes l | 284 | FROM likes l |
| 281 | JOIN articles ar ON ar.url = l.article_url AND ar.feed_url = l.feed_url | 285 | 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_url | 286 | 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.id | 290 | GROUP BY ar.id |
| 286 | ORDER BY like_count DESC, annotation_count DESC | 291 | 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, err | 295 | 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, err | 305 | 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 l | 320 | FROM likes l |
| 314 | JOIN articles a ON a.url = l.article_url AND a.feed_url = l.feed_url | 321 | 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_url | 322 | 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.id | 323 | 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 DESC | 327 | 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, err | 341 | 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 | ||
| 22 | 22 | fill = "currentColor" |
| 23 | 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 | 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 | 29 | 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) { | ||
| 25 | 25 | |
| 26 | 26 | since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339) |
| 27 | 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 | 30 | s.render(w, r, "dashboard.html", map[string]any{ |
| 31 | 31 | "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/server/trending_handler.go +1 -1 | @@ -22,7 +22,7 @@ func (s *Server) handleTrending(w http.ResponseWriter, r *http.Request) { | ||
| 22 | 22 | if scope == "for-me" { |
| 23 | 23 | trending, _ = s.db.ListTrendingArticlesForUser(r.Context(), user.DID, since, page.Limit()+1, page.Offset()) |
| 24 | 24 | } else { |
| 25 | - trending, _ = s.db.ListTrendingArticles(r.Context(), since, page.Limit()+1, page.Offset()) | |
| 25 | + trending, _ = s.db.ListTrendingArticles(r.Context(), user.DID, since, page.Limit()+1, page.Offset()) | |
| 26 | 26 | } |
| 27 | 27 | |
| 28 | 28 | totalFetched := len(trending) |
| @@ -22,7 +22,7 @@ func (s *Server) handleTrending(w http.ResponseWriter, r *http.Request) { | |||
| 22 | if scope == "for-me" { | 22 | if scope == "for-me" { |
| 23 | trending, _ = s.db.ListTrendingArticlesForUser(r.Context(), user.DID, since, page.Limit()+1, page.Offset()) | 23 | trending, _ = s.db.ListTrendingArticlesForUser(r.Context(), user.DID, since, page.Limit()+1, page.Offset()) |
| 24 | } else { | 24 | } else { |
| 25 | - trending, _ = s.db.ListTrendingArticles(r.Context(), since, page.Limit()+1, page.Offset()) | 25 | + trending, _ = s.db.ListTrendingArticles(r.Context(), user.DID, since, page.Limit()+1, page.Offset()) |
| 26 | } | 26 | } |
| 27 | 27 | ||
| 28 | totalFetched := len(trending) | 28 | totalFetched := len(trending) |
modified
internal/tmpl/article_detail.html +2 -3 | @@ -23,8 +23,7 @@ | ||
| 23 | 23 | </div> |
| 24 | 24 | |
| 25 | 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" | |
| 27 | - id="like-btn" | |
| 26 | + <button hx-post="/articles/{{.Article.ID}}/like" hx-target="this" hx-swap="outerHTML" | |
| 28 | 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 | 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 | 29 | <span>{{.LikeCount}}</span> |
| @@ -166,7 +165,7 @@ | ||
| 166 | 165 | document.addEventListener('keydown', function(e) { |
| 167 | 166 | if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; |
| 168 | 167 | if (e.key === 'l') { |
| 169 | - var btn = document.getElementById('like-btn'); | |
| 168 | + var btn = document.querySelector('[hx-post*="/like"]'); | |
| 170 | 169 | if (btn) btn.click(); |
| 171 | 170 | } else if (e.key === 'm') { |
| 172 | 171 | 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 @@ | ||
| 63 | 63 | {{range .Subscriptions}} |
| 64 | 64 | <div class="feed-item px-5 py-4 flex items-center justify-between hover:bg-spot-hover-50 transition rounded-xl"> |
| 65 | 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 | 67 | <div class="min-w-0 flex-1"> |
| 68 | 68 | <div class="flex items-center gap-2"> |
| 69 | 69 | <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 @@ | ||
| 23 | 23 | |
| 24 | 24 | <button type="submit" form="login-form" |
| 25 | 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 | 27 | Sign in with Atmosphere |
| 28 | 28 | </button> |
| 29 | 29 | </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 Atmosphere | 27 | 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 @@ | ||
| 1 | 1 | {{define "like-button.html"}} |
| 2 | 2 | <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> | |
| 5 | 6 | </button> |
| 6 | 7 | {{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}} |
modified
internal/tmpl/partials/trending-card.html +9 -9 | @@ -1,20 +1,20 @@ | ||
| 1 | 1 | {{define "trending-card.html"}} |
| 2 | 2 | <div class="bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot"> |
| 3 | 3 | <div class="flex items-start justify-between gap-4"> |
| 4 | - <div class="min-w-0 flex-1 overflow-hidden"> | |
| 5 | - <a href="/articles/{{.ArticleID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight break-words">{{.Title}}</a> | |
| 6 | - <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2 min-w-0"> | |
| 4 | + <div class="min-w-0 flex-1"> | |
| 5 | + <a href="/articles/{{.ArticleID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a> | |
| 6 | + <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2"> | |
| 7 | 7 | {{if .FaviconURL}}<img src="{{.FaviconURL}}" class="w-4 h-4 rounded shrink-0" loading="lazy">{{end}} |
| 8 | - {{if .Author}}<span class="truncate max-w-32 shrink-0">{{.Author}}</span>{{end}} | |
| 9 | - <span class="text-spot-muted truncate min-w-0"><a href="/articles?feed={{.FeedURL}}" class="hover:text-spot-green transition">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</a></span> | |
| 8 | + {{if .Author}}<span>{{.Author}}</span>{{end}} | |
| 9 | + <span class="text-spot-muted"><a href="/articles?feed={{.FeedURL}}" class="hover:text-spot-green transition">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</a></span> | |
| 10 | 10 | </div> |
| 11 | 11 | {{if .Summary}} |
| 12 | - <p class="text-sm text-spot-secondary mt-2 line-clamp-2">{{.Summary}}</p> | |
| 12 | + <p class="text-sm text-spot-secondary mt-2 line-clamp-2">{{plainText .Summary}}</p> | |
| 13 | 13 | {{end}} |
| 14 | 14 | </div> |
| 15 | - <div class="flex flex-col items-end gap-1 shrink-0 text-sm text-spot-secondary"> | |
| 16 | - <span class="inline-flex items-center gap-1"><svg class="w-4 h-4 text-spot-muted" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">{{template "icon-heart"}}</svg> {{.LikeCount}}</span> | |
| 17 | - <span class="text-spot-muted">{{.AnnotationCount}} notes</span> | |
| 15 | + <div class="flex flex-col items-end gap-1 shrink-0"> | |
| 16 | + {{template "like-button.html" (dict "ID" .ArticleID "HasLiked" .HasLiked "LikeCount" .LikeCount)}} | |
| 17 | + <span class="text-xs text-spot-muted">{{.AnnotationCount}} notes</span> | |
| 18 | 18 | </div> |
| 19 | 19 | </div> |
| 20 | 20 | </div> |
| @@ -1,20 +1,20 @@ | |||
| 1 | {{define "trending-card.html"}} | 1 | {{define "trending-card.html"}} |
| 2 | <div class="bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot"> | 2 | <div class="bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot"> |
| 3 | <div class="flex items-start justify-between gap-4"> | 3 | <div class="flex items-start justify-between gap-4"> |
| 4 | - <div class="min-w-0 flex-1 overflow-hidden"> | 4 | + <div class="min-w-0 flex-1"> |
| 5 | - <a href="/articles/{{.ArticleID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight break-words">{{.Title}}</a> | 5 | + <a href="/articles/{{.ArticleID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a> |
| 6 | - <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2 min-w-0"> | 6 | + <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2"> |
| 7 | {{if .FaviconURL}}<img src="{{.FaviconURL}}" class="w-4 h-4 rounded shrink-0" loading="lazy">{{end}} | 7 | {{if .FaviconURL}}<img src="{{.FaviconURL}}" class="w-4 h-4 rounded shrink-0" loading="lazy">{{end}} |
| 8 | - {{if .Author}}<span class="truncate max-w-32 shrink-0">{{.Author}}</span>{{end}} | 8 | + {{if .Author}}<span>{{.Author}}</span>{{end}} |
| 9 | - <span class="text-spot-muted truncate min-w-0"><a href="/articles?feed={{.FeedURL}}" class="hover:text-spot-green transition">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</a></span> | 9 | + <span class="text-spot-muted"><a href="/articles?feed={{.FeedURL}}" class="hover:text-spot-green transition">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</a></span> |
| 10 | </div> | 10 | </div> |
| 11 | {{if .Summary}} | 11 | {{if .Summary}} |
| 12 | - <p class="text-sm text-spot-secondary mt-2 line-clamp-2">{{.Summary}}</p> | 12 | + <p class="text-sm text-spot-secondary mt-2 line-clamp-2">{{plainText .Summary}}</p> |
| 13 | {{end}} | 13 | {{end}} |
| 14 | </div> | 14 | </div> |
| 15 | - <div class="flex flex-col items-end gap-1 shrink-0 text-sm text-spot-secondary"> | 15 | + <div class="flex flex-col items-end gap-1 shrink-0"> |
| 16 | - <span class="inline-flex items-center gap-1"><svg class="w-4 h-4 text-spot-muted" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">{{template "icon-heart"}}</svg> {{.LikeCount}}</span> | 16 | + {{template "like-button.html" (dict "ID" .ArticleID "HasLiked" .HasLiked "LikeCount" .LikeCount)}} |
| 17 | - <span class="text-spot-muted">{{.AnnotationCount}} notes</span> | 17 | + <span class="text-xs text-spot-muted">{{.AnnotationCount}} notes</span> |
| 18 | </div> | 18 | </div> |
| 19 | </div> | 19 | </div> |
| 20 | </div> | 20 | </div> |