Monorepo for Tangled tangled.org
5

Configure Feed

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

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 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} 98 99func (t *Timeline) buildRecents(userDid string) ([]pages.RecentItem, error) { 100 links, err := db.GetRecentLinks(t.db, orm.FilterEq("user_did", userDid)) 101 if err != nil { 102 return nil, err 103 } 104 if len(links) == 0 { 105 return nil, nil 106 } 107 108 // group targets by type. 109 var repoDids, issueAtUris, pullAtUris []string 110 for _, l := range links { 111 switch l.LinkType { 112 case models.RecentLinkTypeRepo: 113 repoDids = append(repoDids, l.Target) 114 case models.RecentLinkTypeIssue: 115 issueAtUris = append(issueAtUris, l.Target) 116 case models.RecentLinkTypePull: 117 pullAtUris = append(pullAtUris, l.Target) 118 } 119 } 120 121 // fetch repos by DID. 122 repoByDid := make(map[string]*models.Repo) 123 if len(repoDids) > 0 { 124 fetched, err := db.GetRepos(t.db, orm.FilterIn("repo_did", repoDids)) 125 if err != nil { 126 return nil, err 127 } 128 for i := range fetched { 129 repoByDid[fetched[i].RepoDid] = &fetched[i] 130 } 131 } 132 133 // fetch issues by aturi 134 issueByAtUri := make(map[string]*models.Issue) 135 if len(issueAtUris) > 0 { 136 issues, err := db.GetIssues(t.db, orm.FilterIn("at_uri", issueAtUris)) 137 if err != nil { 138 return nil, err 139 } 140 for _, issue := range issues { 141 issueByAtUri[issue.AtUri().String()] = &issue 142 } 143 } 144 145 // fetch pulls by aturi 146 pullByAtUri := make(map[string]*models.Pull) 147 if len(pullAtUris) > 0 { 148 fetched, err := db.GetPulls(t.db, orm.FilterIn("at_uri", pullAtUris)) 149 if err != nil { 150 return nil, err 151 } 152 for _, p := range fetched { 153 pullByAtUri[p.AtUri().String()] = p 154 } 155 } 156 157 // build result in original link order 158 var items []pages.RecentItem 159 for _, l := range links { 160 item := pages.RecentItem{Link: l} 161 switch l.LinkType { 162 case models.RecentLinkTypeRepo: 163 item.Repo = repoByDid[l.Target] 164 case models.RecentLinkTypeIssue: 165 item.Issue = issueByAtUri[l.Target] 166 case models.RecentLinkTypePull: 167 item.Pull = pullByAtUri[l.Target] 168 } 169 // skip if the entity could not be resolved (e.g. deleted). 170 if item.Repo == nil && item.Issue == nil && item.Pull == nil { 171 continue 172 } 173 items = append(items, item) 174 } 175 176 // re-sort by visited descending to restore recency order after map lookups. 177 sort.Slice(items, func(i, j int) bool { 178 return items[i].Link.Visited.After(items[j].Link.Visited) 179 }) 180 181 return items, nil 182} 183 184// showNewsletter decides whether the newsletter widget/CTA should render. 185// Anonymous visitors always see it (they can dismiss via localStorage); 186// logged-in users whose newsletter_preferences row exists (either 187// subscribed or dismissed) do not. 188func (t *Timeline) showNewsletter(user *oauth.MultiAccountUser) bool { 189 if user == nil { 190 return true 191 } 192 pref, err := db.GetNewsletterPref(t.db, user.Did) 193 if err != nil { 194 t.logger.Error("failed to read newsletter preference", "did", user.Did, "err", err) 195 return true 196 } 197 return pref == nil 198}