Monorepo for Tangled
tangled.org
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}