This repository has no description
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}