A social RSS reader built on the AT Protocol. glean.at
glean atproto atmosphere rss feed social app
14
fork

Configure Feed

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

Improve article links and annotation enhancements

+59 -25
+21
internal/db/article.go
··· 355 355 return a, nil 356 356 } 357 357 358 + func (db *DB) GetReadCount(ctx context.Context, userDID, feedURL string) (int, error) { 359 + var count int 360 + if feedURL != "" { 361 + err := db.QueryRowContext(ctx, ` 362 + SELECT COUNT(*) 363 + FROM articles a 364 + JOIN read_state r ON r.user_did = ? AND r.article_id = a.id 365 + WHERE a.feed_url = ? AND r.is_read = 1 366 + `, userDID, feedURL).Scan(&count) 367 + return count, err 368 + } 369 + err := db.QueryRowContext(ctx, ` 370 + SELECT COUNT(*) 371 + FROM articles a 372 + JOIN subscriptions s ON a.feed_url = s.feed_url AND s.user_did = ? 373 + JOIN read_state r ON r.user_did = ? AND r.article_id = a.id 374 + WHERE r.is_read = 1 375 + `, userDID, userDID).Scan(&count) 376 + return count, err 377 + } 378 + 358 379 func (db *DB) CountNewArticles(ctx context.Context, userDID string, since time.Time) (int, error) { 359 380 var count int 360 381 err := db.QueryRowContext(ctx, `
+11 -8
internal/db/social.go
··· 16 16 AuthorHandle string 17 17 FeedURL string 18 18 ArticleURL string 19 + ArticleID sql.NullInt64 19 20 Quote sql.NullString 20 21 Note sql.NullString 21 22 Tags sql.NullString ··· 45 46 func (db *DB) GetAnnotation(ctx context.Context, id int64) (*Annotation, error) { 46 47 a := &Annotation{} 47 48 err := db.QueryRowContext(ctx, ` 48 - SELECT a.id, a.uri, a.author_did, COALESCE(u.handle, ''), a.feed_url, a.article_url, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid 49 + SELECT a.id, a.uri, a.author_did, COALESCE(u.handle, ''), a.feed_url, a.article_url, ar.id, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid 49 50 FROM annotations a 50 51 LEFT JOIN users u ON a.author_did = u.did 52 + LEFT JOIN articles ar ON ar.url = a.article_url AND ar.feed_url = a.feed_url 51 53 WHERE a.id = ? 52 - `, id).Scan(&a.ID, &a.URI, &a.AuthorDID, &a.AuthorHandle, &a.FeedURL, &a.ArticleURL, 54 + `, id).Scan(&a.ID, &a.URI, &a.AuthorDID, &a.AuthorHandle, &a.FeedURL, &a.ArticleURL, &a.ArticleID, 53 55 &a.Quote, &a.Note, &a.Tags, &a.Rating, &a.CreatedAt, &a.CID) 54 56 if err != nil { 55 57 return nil, err ··· 79 81 var args []any 80 82 81 83 if feedURL != "" { 82 - conds = append(conds, "feed_url = ?") 84 + conds = append(conds, "a.feed_url = ?") 83 85 args = append(args, feedURL) 84 86 } 85 87 if articleURL != "" { 86 - conds = append(conds, "article_url = ?") 88 + conds = append(conds, "a.article_url = ?") 87 89 args = append(args, articleURL) 88 90 } 89 91 if authorDID != "" { 90 - conds = append(conds, "author_did = ?") 92 + conds = append(conds, "a.author_did = ?") 91 93 args = append(args, authorDID) 92 94 } 93 95 94 - query := `SELECT a.id, a.uri, a.author_did, COALESCE(u.handle, ''), a.feed_url, a.article_url, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid 96 + query := `SELECT a.id, a.uri, a.author_did, COALESCE(u.handle, ''), a.feed_url, a.article_url, ar.id, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid 95 97 FROM annotations a 96 - LEFT JOIN users u ON a.author_did = u.did` 98 + LEFT JOIN users u ON a.author_did = u.did 99 + LEFT JOIN articles ar ON ar.url = a.article_url AND ar.feed_url = a.feed_url` 97 100 if len(conds) > 0 { 98 101 query += ` WHERE ` + strings.Join(conds, " AND ") 99 102 } ··· 109 112 var annotations []*Annotation 110 113 for rows.Next() { 111 114 a := &Annotation{} 112 - if err := rows.Scan(&a.ID, &a.URI, &a.AuthorDID, &a.AuthorHandle, &a.FeedURL, &a.ArticleURL, 115 + if err := rows.Scan(&a.ID, &a.URI, &a.AuthorDID, &a.AuthorHandle, &a.FeedURL, &a.ArticleURL, &a.ArticleID, 113 116 &a.Quote, &a.Note, &a.Tags, &a.Rating, &a.CreatedAt, &a.CID); err != nil { 114 117 return nil, err 115 118 }
+7
internal/server/articles_handler.go
··· 38 38 39 39 page := pageFromRequest(r, 50) 40 40 41 + if status == "" && searchQuery == "" { 42 + readCount, _ := s.db.GetReadCount(r.Context(), user.DID, feedURL) 43 + if readCount > 0 { 44 + status = "unread" 45 + } 46 + } 47 + 41 48 var articles []*db.Article 42 49 var err error 43 50
+5 -5
internal/tmpl/article_detail.html
··· 1 1 {{define "article_detail.html"}} 2 2 <div class="max-w-3xl mx-auto"> 3 - <a href="/articles" class="text-sm text-spot-secondary hover:text-spot-text mb-6 inline-flex items-center gap-1.5 transition"> 3 + <a href="javascript:history.back()" class="text-sm text-spot-secondary hover:text-spot-text mb-6 inline-flex items-center gap-1.5 transition"> 4 4 <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"/></svg> 5 - Back to articles 5 + Back 6 6 </a> 7 7 8 8 <article> ··· 138 138 </div> 139 139 </section> 140 140 141 - <a href="/articles" class="text-sm text-spot-secondary hover:text-spot-text mt-8 inline-flex items-center gap-1.5 transition"> 141 + <a href="javascript:history.back()" class="text-sm text-spot-secondary hover:text-spot-text mt-8 inline-flex items-center gap-1.5 transition"> 142 142 <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"/></svg> 143 - Back to articles 143 + Back 144 144 </a> 145 145 </div> 146 146 ··· 255 255 var rbtn = document.getElementById('read-btn'); 256 256 if (rbtn) rbtn.click(); 257 257 } else if (e.key === 'Escape') { 258 - window.location.href = '/articles'; 258 + history.back(); 259 259 } else if (e.key === 'o') { 260 260 var origLink = document.querySelector('a[target="_blank"]'); 261 261 if (origLink && origLink.href) window.open(origLink.href, '_blank');
+10 -10
internal/tmpl/articles.html
··· 48 48 </div> 49 49 </form> 50 50 <div class="flex items-center gap-2 shrink-0"> 51 - <a href="/articles?status=all{{if .FeedURL}}&feed={{.FeedURL}}{{end}}" 52 - class="px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-button transition 53 - {{if or (eq .Status "") (eq .Status "all")}}bg-spot-active-pill-bg text-spot-active-pill-text{{else}}bg-spot-hover text-spot-secondary hover:text-spot-text{{end}}"> 54 - All 55 - </a> 56 - <a href="/articles?status=unread{{if .FeedURL}}&feed={{.FeedURL}}{{end}}" 57 - class="px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-button transition 58 - {{if eq .Status "unread"}}bg-spot-active-pill-bg text-spot-active-pill-text{{else}}bg-spot-hover text-spot-secondary hover:text-spot-text{{end}}"> 59 - Unread 60 - </a> 51 + <a href="/articles?status=all{{if .FeedURL}}&feed={{.FeedURL}}{{end}}" 52 + class="px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-button transition 53 + {{if eq .Status "all"}}bg-spot-active-pill-bg text-spot-active-pill-text{{else}}bg-spot-hover text-spot-secondary hover:text-spot-text{{end}}"> 54 + All 55 + </a> 56 + <a href="/articles?status=unread{{if .FeedURL}}&feed={{.FeedURL}}{{end}}" 57 + class="px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-button transition 58 + {{if or (eq .Status "unread") (eq .Status "")}}bg-spot-active-pill-bg text-spot-active-pill-text{{else}}bg-spot-hover text-spot-secondary hover:text-spot-text{{end}}"> 59 + Unread 60 + </a> 61 61 <a href="/articles?status=read{{if .FeedURL}}&feed={{.FeedURL}}{{end}}" 62 62 class="px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-button transition 63 63 {{if eq .Status "read"}}bg-spot-active-pill-bg text-spot-active-pill-text{{else}}bg-spot-hover text-spot-secondary hover:text-spot-text{{end}}">
+5 -2
internal/tmpl/partials/annotation-card.html
··· 1 1 {{define "annotation-card.html"}} 2 2 <div id="annotation-{{.annotation.ID}}" class="bg-spot-surface rounded-xl p-5 border border-spot-divider shadow-sm"> 3 3 {{if .annotation.ArticleURL}} 4 - <div class="text-xs mb-3"> 5 - <a href="{{.annotation.ArticleURL}}" class="text-spot-secondary hover:text-spot-green transition truncate block">{{.annotation.ArticleURL}}</a> 4 + <div class="text-xs mb-3 flex items-center gap-2"> 5 + {{if .annotation.ArticleID.Valid}}<a href="/articles/{{.annotation.ArticleID.Int64}}" class="text-spot-green hover:text-spot-text transition truncate inline-flex items-center gap-1"> 6 + <svg class="w-3 h-3 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg> 7 + </a>{{end}} 8 + <a href="{{.annotation.ArticleURL}}" class="text-spot-secondary hover:text-spot-green transition truncate">{{.annotation.ArticleURL}}</a> 6 9 </div> 7 10 {{end}} 8 11 {{if .annotation.Quote.Valid}}