This repository has no description
0

Configure Feed

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

core / appview / state / timeline.go
6.3 kB 230 lines
1package state 2 3import ( 4 "net/http" 5 "sort" 6 7 "github.com/bluesky-social/indigo/atproto/syntax" 8 "tangled.org/core/appview/db" 9 "tangled.org/core/appview/models" 10 "tangled.org/core/appview/oauth" 11 "tangled.org/core/appview/pages" 12 "tangled.org/core/appview/pagination" 13 "tangled.org/core/orm" 14) 15 16func (s *State) Home(w http.ResponseWriter, r *http.Request) { 17 // TODO: set this flag based on the UI 18 filtered := false 19 20 user := s.oauth.GetMultiAccountUser(r) 21 22 timeline, err := db.MakeTimeline(s.db, 50, "", filtered) 23 if err != nil { 24 s.logger.Error("failed to make timeline", "err", err) 25 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.") 26 return 27 } 28 29 blueskyPosts, err := db.GetBlueskyPosts(s.db, 8) 30 if err != nil { 31 s.logger.Error("failed to get bluesky posts", "err", err) 32 } 33 34 s.pages.Home(w, pages.TimelineParams{ 35 LoggedInUser: user, 36 Timeline: timeline, 37 BlueskyPosts: blueskyPosts, 38 ShowNewsletter: s.showNewsletter(user), 39 }) 40} 41func (s *State) HomeOrTimeline(w http.ResponseWriter, r *http.Request) { 42 if s.oauth.GetMultiAccountUser(r) != nil { 43 s.Timeline(w, r) 44 return 45 } 46 s.Home(w, r) 47} 48 49func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { 50 user := s.oauth.GetMultiAccountUser(r) 51 52 followingOnly := r.URL.Query().Get("following") == "true" && user != nil 53 54 var userDid string 55 if user != nil { 56 userDid = user.Did 57 } 58 timeline, err := db.MakeTimeline(s.db, 50, userDid, followingOnly) 59 if err != nil { 60 s.logger.Error("failed to make timeline", "err", err) 61 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.") 62 } 63 64 repos, err := db.GetTopStarredReposLastWeek(s.db) 65 if err != nil { 66 s.logger.Error("failed to get top starred repos", "err", err) 67 s.pages.Notice(w, "topstarredrepos", "Unable to load.") 68 return 69 } 70 71 gfiLabel, err := db.GetLabelDefinition(s.db, orm.FilterEq("at_uri", s.config.Label.GoodFirstIssue)) 72 if err != nil { 73 // non-fatal 74 } 75 76 var notifications []*models.NotificationWithEntity 77 if user != nil { 78 notifications, err = db.GetNotificationsWithEntities( 79 s.db, 80 pagination.Page{Limit: 5, Offset: 0}, 81 orm.FilterEq("recipient_did", user.Did), 82 ) 83 if err != nil { 84 s.logger.Error("failed to get notifications for timeline", "err", err) 85 } 86 } 87 88 var vouchSuggestions []models.VouchSuggestion 89 if user != nil { 90 vouchSuggestions, err = db.GetVouchSuggestions(s.db, user.Did, 3) 91 if err != nil { 92 s.logger.Error("failed to get vouch suggestions", "err", err) 93 } 94 if len(vouchSuggestions) > 0 { 95 suggestionDids := make([]syntax.DID, len(vouchSuggestions)) 96 for i, sv := range vouchSuggestions { 97 suggestionDids[i] = syntax.DID(sv.Did) 98 } 99 relationships, err := db.GetVouchRelationshipsBatch(s.db, syntax.DID(user.Did), suggestionDids) 100 if err != nil { 101 s.logger.Error("failed to get vouch relationships for suggestions", "err", err) 102 } else { 103 for i := range vouchSuggestions { 104 vouchSuggestions[i].VouchRelationship = relationships[vouchSuggestions[i].Did] 105 } 106 } 107 } 108 } 109 110 var recents []pages.RecentItem 111 if user != nil { 112 recents, err = s.buildRecents(user.Did) 113 if err != nil { 114 s.logger.Error("failed to build recents for timeline", "err", err) 115 } 116 } 117 118 s.pages.Timeline(w, pages.TimelineParams{ 119 LoggedInUser: user, 120 Timeline: timeline, 121 Repos: repos, 122 GfiLabel: gfiLabel, 123 VouchSuggestions: vouchSuggestions, 124 Notifications: notifications, 125 Recents: recents, 126 FollowingOnly: followingOnly, 127 ShowNewsletter: s.showNewsletter(user), 128 }) 129} 130 131func (s *State) buildRecents(userDid string) ([]pages.RecentItem, error) { 132 links, err := db.GetRecentLinks(s.db, orm.FilterEq("user_did", userDid)) 133 if err != nil { 134 return nil, err 135 } 136 if len(links) == 0 { 137 return nil, nil 138 } 139 140 // group targets by type. 141 var repoDids, issueAtUris, pullAtUris []string 142 for _, l := range links { 143 switch l.LinkType { 144 case models.RecentLinkTypeRepo: 145 repoDids = append(repoDids, l.Target) 146 case models.RecentLinkTypeIssue: 147 issueAtUris = append(issueAtUris, l.Target) 148 case models.RecentLinkTypePull: 149 pullAtUris = append(pullAtUris, l.Target) 150 } 151 } 152 153 // fetch repos by DID. 154 repoByDid := make(map[string]*models.Repo) 155 if len(repoDids) > 0 { 156 fetched, err := db.GetRepos(s.db, orm.FilterIn("repo_did", repoDids)) 157 if err != nil { 158 return nil, err 159 } 160 for i := range fetched { 161 repoByDid[fetched[i].RepoDid] = &fetched[i] 162 } 163 } 164 165 // fetch issues by aturi 166 issueByAtUri := make(map[string]*models.Issue) 167 if len(issueAtUris) > 0 { 168 issues, err := db.GetIssues(s.db, orm.FilterIn("at_uri", issueAtUris)) 169 if err != nil { 170 return nil, err 171 } 172 for _, issue := range issues { 173 issueByAtUri[issue.AtUri().String()] = &issue 174 } 175 } 176 177 // fetch pulls by aturi 178 pullByAtUri := make(map[string]*models.Pull) 179 if len(pullAtUris) > 0 { 180 fetched, err := db.GetPulls(s.db, orm.FilterIn("at_uri", pullAtUris)) 181 if err != nil { 182 return nil, err 183 } 184 for _, p := range fetched { 185 pullByAtUri[p.AtUri().String()] = p 186 } 187 } 188 189 // build result in original link order 190 var items []pages.RecentItem 191 for _, l := range links { 192 item := pages.RecentItem{Link: l} 193 switch l.LinkType { 194 case models.RecentLinkTypeRepo: 195 item.Repo = repoByDid[l.Target] 196 case models.RecentLinkTypeIssue: 197 item.Issue = issueByAtUri[l.Target] 198 case models.RecentLinkTypePull: 199 item.Pull = pullByAtUri[l.Target] 200 } 201 // skip if the entity could not be resolved (e.g. deleted). 202 if item.Repo == nil && item.Issue == nil && item.Pull == nil { 203 continue 204 } 205 items = append(items, item) 206 } 207 208 // re-sort by visited descending to restore recency order after map lookups. 209 sort.Slice(items, func(i, j int) bool { 210 return items[i].Link.Visited.After(items[j].Link.Visited) 211 }) 212 213 return items, nil 214} 215 216// showNewsletter decides whether the newsletter widget/CTA should render. 217// Anonymous visitors always see it (they can dismiss via localStorage); 218// logged-in users whose newsletter_preferences row exists (either 219// subscribed or dismissed) do not. 220func (s *State) showNewsletter(user *oauth.MultiAccountUser) bool { 221 if user == nil { 222 return true 223 } 224 pref, err := db.GetNewsletterPref(s.db, user.Did) 225 if err != nil { 226 s.logger.Error("failed to read newsletter preference", "did", user.Did, "err", err) 227 return true 228 } 229 return pref == nil 230}