Monorepo for Tangled tangled.org
11

Configure Feed

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

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