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.

Implement async feed refresh with polling and category filtering

+33 -18
+23 -14
internal/server/feeds_handler.go
··· 338 338 339 339 func (s *Server) handleFeedList(w http.ResponseWriter, r *http.Request) { 340 340 user := currentUser(r) 341 - subs, err := s.dbs.Articles.ListSubscriptions(r.Context(), user.DID, "", 100, 0) 341 + category := r.URL.Query().Get("category") 342 + subs, err := s.dbs.Articles.ListSubscriptions(r.Context(), user.DID, category, 100, 0) 342 343 if err != nil { 343 344 s.logger.Warn("failed to list subscriptions", "error", err, "did", user.DID) 344 345 } 345 - s.render(w, r, "feeds.html", map[string]any{ 346 + s.render(w, r, "feed-list.html", map[string]any{ 346 347 "User": user, 347 348 "Subscriptions": subs, 348 349 }) ··· 350 351 351 352 func (s *Server) handleRefreshFeeds(w http.ResponseWriter, r *http.Request) { 352 353 user := currentUser(r) 354 + ctx := r.Context() 353 355 354 - subs, err := s.dbs.Articles.ListSubscriptions(r.Context(), user.DID, "", 100, 0) 356 + go s.refreshUserFeeds(context.WithoutCancel(ctx), user.DID) 357 + 358 + category := r.URL.Query().Get("category") 359 + subs, err := s.dbs.Articles.ListSubscriptions(ctx, user.DID, category, 100, 0) 355 360 if err != nil { 356 361 s.logger.Warn("failed to list subscriptions", "error", err, "did", user.DID) 357 362 } 363 + s.render(w, r, "feed-list.html", map[string]any{ 364 + "User": user, 365 + "Subscriptions": subs, 366 + }) 367 + } 368 + 369 + func (s *Server) refreshUserFeeds(ctx context.Context, userDID string) { 370 + subs, err := s.dbs.Articles.ListSubscriptions(ctx, userDID, "", 100, 0) 371 + if err != nil { 372 + s.logger.Warn("failed to list subscriptions for refresh", "error", err, "did", userDID) 373 + return 374 + } 375 + 358 376 seen := make(map[string]bool) 359 377 for _, sub := range subs { 360 378 if seen[sub.FeedURL] { ··· 362 380 } 363 381 seen[sub.FeedURL] = true 364 382 365 - f, err := s.dbs.Articles.GetFeed(r.Context(), sub.FeedURL) 383 + f, err := s.dbs.Articles.GetFeed(ctx, sub.FeedURL) 366 384 if err != nil { 367 385 s.logger.Warn("failed to get feed", "error", err, "feed", sub.FeedURL) 368 386 continue ··· 376 394 ETag: f.Etag.String, 377 395 LastModified: f.LastModified.String, 378 396 } 379 - s.scheduler.FetchFeed(r.Context(), ff) 380 - } 381 - 382 - subs, err = s.dbs.Articles.ListSubscriptions(r.Context(), user.DID, "", 100, 0) 383 - if err != nil { 384 - s.logger.Warn("failed to list subscriptions", "error", err, "did", user.DID) 397 + s.scheduler.FetchFeed(ctx, ff) 385 398 } 386 - s.render(w, r, "feed-list.html", map[string]any{ 387 - "User": user, 388 - "Subscriptions": subs, 389 - }) 390 399 } 391 400 392 401 func (s *Server) handleRetryFeed(w http.ResponseWriter, r *http.Request) {
+10 -4
internal/tmpl/feeds.html
··· 2 2 <div class="flex items-center justify-between gap-3 mb-2 flex-wrap"> 3 3 <h1 class="text-2xl font-bold text-spot-text">Feeds <span class="text-base font-normal text-spot-secondary">({{.SubscriptionCount}})</span></h1> 4 4 <div class="flex items-center gap-3"> 5 - <button hx-post="/feeds/refresh" hx-target="#feed-list" hx-swap="innerHTML" 6 - hx-indicator="#refresh-indicator" 7 - 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"> 5 + <button id="refresh-btn" 6 + hx-post="/feeds/refresh" hx-target="#feed-list" hx-swap="innerHTML" 7 + hx-on::before-request="gleanRefreshStart()" 8 + hx-on::after-request="gleanRefreshPoll()" 9 + 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 disabled:opacity-50"> 8 10 Refresh feeds 9 11 </button> 10 - <span id="refresh-indicator" class="htmx-indicator text-sm text-spot-secondary">Fetching...</span> 12 + <span id="refresh-indicator" class="text-sm text-spot-secondary" style="display:none">Refreshing feeds...</span> 11 13 </div> 12 14 </div> 15 + <script> 16 + function gleanRefreshStart(){document.getElementById('refresh-indicator').style.display='inline';document.getElementById('refresh-btn').disabled=true} 17 + function gleanRefreshPoll(){setTimeout(function(){htmx.ajax('GET','/feeds/list',{target:'#feed-list',swap:'innerHTML'});document.getElementById('refresh-indicator').style.display='none';document.getElementById('refresh-btn').disabled=false},5000)} 18 + </script> 13 19 <p class="text-sm text-spot-secondary mb-6">Manage your RSS and Atom subscriptions.</p> 14 20 15 21 {{template "dead-feeds.html" (dict "DeadFeeds" .DeadFeeds "CSRFToken" .CSRFToken)}}