Monorepo for Tangled tangled.org
4

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