Monorepo for Tangled
tangled.org
1package db
2
3import (
4 "sort"
5
6 "tangled.org/core/appview/models"
7 "tangled.org/core/appview/pagination"
8 "tangled.org/core/orm"
9)
10
11// TODO: this gathers heterogenous events from different sources and aggregates
12// them in code; if we did this entirely in sql, we could order and limit and paginate easily
13func MakeTimeline(e Execer, limit int, loggedInUserDid string, limitToUsersIsFollowing bool) ([]models.TimelineGroup, error) {
14 var events []models.TimelineEvent
15
16 var userIsFollowing []string
17 if limitToUsersIsFollowing {
18 following, err := GetFollowing(e, loggedInUserDid)
19 if err != nil {
20 return nil, err
21 }
22
23 userIsFollowing = make([]string, 0, len(following))
24 for _, follow := range following {
25 userIsFollowing = append(userIsFollowing, follow.SubjectDid)
26 }
27 }
28
29 // Fetch more events than we need to so that when we collapse each individual
30 // event into groups, we can still be relatively confident that we will have
31 // `limit` groups to fill the timeline with. Adjust multiplier as necessary.
32 fetchLimit := limit * 2
33
34 repos, err := getTimelineRepos(e, fetchLimit, loggedInUserDid, userIsFollowing)
35 if err != nil {
36 return nil, err
37 }
38
39 stars, err := getTimelineStars(e, fetchLimit, loggedInUserDid, userIsFollowing)
40 if err != nil {
41 return nil, err
42 }
43
44 follows, err := getTimelineFollows(e, fetchLimit, loggedInUserDid, userIsFollowing)
45 if err != nil {
46 return nil, err
47 }
48
49 events = append(events, repos...)
50 events = append(events, stars...)
51 events = append(events, follows...)
52
53 sort.Slice(events, func(i, j int) bool {
54 return events[i].EventAt.After(events[j].EventAt)
55 })
56
57 groups := collapseTimeline(events)
58 if len(groups) > limit {
59 groups = groups[:limit]
60 }
61 return groups, nil
62}
63
64// collapseTimeline merges consecutive events that share the same operation
65// and target into one TimelineGroup (assumes events are sorted newest-first).
66func collapseTimeline(events []models.TimelineEvent) []models.TimelineGroup {
67 var groups []models.TimelineGroup
68 i := 0
69 for i < len(events) {
70 group := models.TimelineGroup{Primary: events[i]}
71 j := i + 1
72 for j < len(events) && canCollapse(events[i], events[j]) {
73 group.Others = append(group.Others, events[j])
74 j++
75 }
76 groups = append(groups, group)
77 i = j
78 }
79 return groups
80}
81
82// canCollapse reports whether two adjacent events in the timeline represent
83// the same operation on the same target (repo starred or user followed).
84func canCollapse(a, b models.TimelineEvent) bool {
85 switch {
86 case a.RepoStar != nil && b.RepoStar != nil:
87 if a.RepoStar.Repo == nil || b.RepoStar.Repo == nil {
88 return false
89 }
90 return a.RepoStar.Repo.RepoAt() == b.RepoStar.Repo.RepoAt()
91 case a.Follow != nil && b.Follow != nil:
92 return a.Follow.SubjectDid == b.Follow.SubjectDid
93 default:
94 return false
95 }
96}
97
98func fetchStarStatuses(e Execer, loggedInUserDid string, repos []models.Repo) (map[string]bool, error) {
99 if loggedInUserDid == "" {
100 return nil, nil
101 }
102
103 var repoDids []string
104 for _, r := range repos {
105 repoDids = append(repoDids, r.RepoDid)
106 }
107
108 return GetStarStatuses(e, loggedInUserDid, repoDids)
109}
110
111func getRepoStarInfo(repo *models.Repo, starStatuses map[string]bool) (bool, int64) {
112 var isStarred bool
113 if starStatuses != nil {
114 isStarred = starStatuses[repo.RepoDid]
115 }
116
117 var starCount int64
118 if repo.RepoStats != nil {
119 starCount = int64(repo.RepoStats.StarCount)
120 }
121
122 return isStarred, starCount
123}
124
125func getTimelineRepos(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
126 filters := make([]orm.Filter, 0)
127 if userIsFollowing != nil {
128 filters = append(filters, orm.FilterIn("did", userIsFollowing))
129 }
130
131 repos, err := GetReposPaginated(e, pagination.Page{Limit: limit}, filters...)
132 if err != nil {
133 return nil, err
134 }
135
136 // fetch all source repos
137 var args []string
138 for _, r := range repos {
139 if r.Source != "" {
140 args = append(args, r.Source)
141 }
142 }
143
144 var origRepos []models.Repo
145 if args != nil {
146 origRepos, err = GetRepos(e, orm.FilterIn("at_uri", args))
147 }
148 if err != nil {
149 return nil, err
150 }
151
152 uriToRepo := make(map[string]models.Repo)
153 for _, r := range origRepos {
154 uriToRepo[r.RepoAt().String()] = r
155 }
156
157 starStatuses, err := fetchStarStatuses(e, loggedInUserDid, repos)
158 if err != nil {
159 return nil, err
160 }
161
162 var events []models.TimelineEvent
163 for _, r := range repos {
164 var source *models.Repo
165 if r.Source != "" {
166 if origRepo, ok := uriToRepo[r.Source]; ok {
167 source = &origRepo
168 }
169 }
170
171 isStarred, starCount := getRepoStarInfo(&r, starStatuses)
172
173 events = append(events, models.TimelineEvent{
174 Repo: &r,
175 EventAt: r.Created,
176 Source: source,
177 IsStarred: isStarred,
178 StarCount: starCount,
179 })
180 }
181
182 return events, nil
183}
184
185func getTimelineStars(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
186 filters := make([]orm.Filter, 0)
187 if userIsFollowing != nil {
188 filters = append(filters, orm.FilterIn("did", userIsFollowing))
189 }
190
191 stars, err := GetRepoStars(e, pagination.Page{Limit: limit}, filters...)
192 if err != nil {
193 return nil, err
194 }
195
196 var repos []models.Repo
197 for _, s := range stars {
198 repos = append(repos, *s.Repo)
199 }
200
201 starStatuses, err := fetchStarStatuses(e, loggedInUserDid, repos)
202 if err != nil {
203 return nil, err
204 }
205
206 var events []models.TimelineEvent
207 for _, s := range stars {
208 isStarred, starCount := getRepoStarInfo(s.Repo, starStatuses)
209
210 events = append(events, models.TimelineEvent{
211 RepoStar: &s,
212 EventAt: s.Created,
213 IsStarred: isStarred,
214 StarCount: starCount,
215 })
216 }
217
218 return events, nil
219}
220
221func getTimelineFollows(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
222 filters := make([]orm.Filter, 0)
223 if userIsFollowing != nil {
224 filters = append(filters, orm.FilterIn("user_did", userIsFollowing))
225 }
226
227 follows, err := GetFollows(e, limit, filters...)
228 if err != nil {
229 return nil, err
230 }
231
232 var subjects []string
233 for _, f := range follows {
234 subjects = append(subjects, f.SubjectDid)
235 }
236
237 if subjects == nil {
238 return nil, nil
239 }
240
241 profiles, err := GetProfiles(e, orm.FilterIn("did", subjects))
242 if err != nil {
243 return nil, err
244 }
245
246 followStatMap, err := GetFollowerFollowingCounts(e, subjects)
247 if err != nil {
248 return nil, err
249 }
250
251 var followStatuses map[string]models.FollowStatus
252 if loggedInUserDid != "" {
253 followStatuses, err = GetFollowStatuses(e, loggedInUserDid, subjects)
254 if err != nil {
255 return nil, err
256 }
257 }
258
259 var events []models.TimelineEvent
260 for _, f := range follows {
261 profile, _ := profiles[f.SubjectDid]
262 followStatMap, _ := followStatMap[f.SubjectDid]
263
264 followStatus := models.IsNotFollowing
265 if followStatuses != nil {
266 followStatus = followStatuses[f.SubjectDid]
267 }
268
269 events = append(events, models.TimelineEvent{
270 Follow: &f,
271 Profile: profile,
272 FollowStats: &followStatMap,
273 FollowStatus: &followStatus,
274 EventAt: f.FollowedAt,
275 })
276 }
277
278 return events, nil
279}