1package db
2
3import (
4 "sort"
5
6 "github.com/bluesky-social/indigo/atproto/syntax"
7 "tangled.org/core/appview/models"
8)
9
10// TODO: this gathers heterogenous events from different sources and aggregates
11// them in code; if we did this entirely in sql, we could order and limit and paginate easily
12func MakeTimeline(e Execer, limit int, loggedInUserDid string, limitToUsersIsFollowing bool) ([]models.TimelineEvent, error) {
13 var events []models.TimelineEvent
14
15 var userIsFollowing []string
16 if limitToUsersIsFollowing {
17 following, err := GetFollowing(e, loggedInUserDid)
18 if err != nil {
19 return nil, err
20 }
21
22 userIsFollowing = make([]string, 0, len(following))
23 for _, follow := range following {
24 userIsFollowing = append(userIsFollowing, follow.SubjectDid)
25 }
26 }
27
28 repos, err := getTimelineRepos(e, limit, loggedInUserDid, userIsFollowing)
29 if err != nil {
30 return nil, err
31 }
32
33 stars, err := getTimelineStars(e, limit, loggedInUserDid, userIsFollowing)
34 if err != nil {
35 return nil, err
36 }
37
38 follows, err := getTimelineFollows(e, limit, loggedInUserDid, userIsFollowing)
39 if err != nil {
40 return nil, err
41 }
42
43 events = append(events, repos...)
44 events = append(events, stars...)
45 events = append(events, follows...)
46
47 sort.Slice(events, func(i, j int) bool {
48 return events[i].EventAt.After(events[j].EventAt)
49 })
50
51 // Limit the slice to 100 events
52 if len(events) > limit {
53 events = events[:limit]
54 }
55
56 return events, nil
57}
58
59func fetchStarStatuses(e Execer, loggedInUserDid string, repos []models.Repo) (map[string]bool, error) {
60 if loggedInUserDid == "" {
61 return nil, nil
62 }
63
64 var repoAts []syntax.ATURI
65 for _, r := range repos {
66 repoAts = append(repoAts, r.RepoAt())
67 }
68
69 return GetStarStatuses(e, loggedInUserDid, repoAts)
70}
71
72func getRepoStarInfo(repo *models.Repo, starStatuses map[string]bool) (bool, int64) {
73 var isStarred bool
74 if starStatuses != nil {
75 isStarred = starStatuses[repo.RepoAt().String()]
76 }
77
78 var starCount int64
79 if repo.RepoStats != nil {
80 starCount = int64(repo.RepoStats.StarCount)
81 }
82
83 return isStarred, starCount
84}
85
86func getTimelineRepos(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
87 filters := make([]filter, 0)
88 if userIsFollowing != nil {
89 filters = append(filters, FilterIn("did", userIsFollowing))
90 }
91
92 repos, err := GetRepos(e, limit, filters...)
93 if err != nil {
94 return nil, err
95 }
96
97 // fetch all source repos
98 var args []string
99 for _, r := range repos {
100 if r.Source != "" {
101 args = append(args, r.Source)
102 }
103 }
104
105 var origRepos []models.Repo
106 if args != nil {
107 origRepos, err = GetRepos(e, 0, FilterIn("at_uri", args))
108 }
109 if err != nil {
110 return nil, err
111 }
112
113 uriToRepo := make(map[string]models.Repo)
114 for _, r := range origRepos {
115 uriToRepo[r.RepoAt().String()] = r
116 }
117
118 starStatuses, err := fetchStarStatuses(e, loggedInUserDid, repos)
119 if err != nil {
120 return nil, err
121 }
122
123 var events []models.TimelineEvent
124 for _, r := range repos {
125 var source *models.Repo
126 if r.Source != "" {
127 if origRepo, ok := uriToRepo[r.Source]; ok {
128 source = &origRepo
129 }
130 }
131
132 isStarred, starCount := getRepoStarInfo(&r, starStatuses)
133
134 events = append(events, models.TimelineEvent{
135 Repo: &r,
136 EventAt: r.Created,
137 Source: source,
138 IsStarred: isStarred,
139 StarCount: starCount,
140 })
141 }
142
143 return events, nil
144}
145
146func getTimelineStars(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
147 filters := make([]filter, 0)
148 if userIsFollowing != nil {
149 filters = append(filters, FilterIn("did", userIsFollowing))
150 }
151
152 stars, err := GetRepoStars(e, limit, filters...)
153 if err != nil {
154 return nil, err
155 }
156
157 var repos []models.Repo
158 for _, s := range stars {
159 repos = append(repos, *s.Repo)
160 }
161
162 starStatuses, err := fetchStarStatuses(e, loggedInUserDid, repos)
163 if err != nil {
164 return nil, err
165 }
166
167 var events []models.TimelineEvent
168 for _, s := range stars {
169 isStarred, starCount := getRepoStarInfo(s.Repo, starStatuses)
170
171 events = append(events, models.TimelineEvent{
172 RepoStar: &s,
173 EventAt: s.Created,
174 IsStarred: isStarred,
175 StarCount: starCount,
176 })
177 }
178
179 return events, nil
180}
181
182func getTimelineFollows(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
183 filters := make([]filter, 0)
184 if userIsFollowing != nil {
185 filters = append(filters, FilterIn("user_did", userIsFollowing))
186 }
187
188 follows, err := GetFollows(e, limit, filters...)
189 if err != nil {
190 return nil, err
191 }
192
193 var subjects []string
194 for _, f := range follows {
195 subjects = append(subjects, f.SubjectDid)
196 }
197
198 if subjects == nil {
199 return nil, nil
200 }
201
202 profiles, err := GetProfiles(e, FilterIn("did", subjects))
203 if err != nil {
204 return nil, err
205 }
206
207 followStatMap, err := GetFollowerFollowingCounts(e, subjects)
208 if err != nil {
209 return nil, err
210 }
211
212 var followStatuses map[string]models.FollowStatus
213 if loggedInUserDid != "" {
214 followStatuses, err = GetFollowStatuses(e, loggedInUserDid, subjects)
215 if err != nil {
216 return nil, err
217 }
218 }
219
220 var events []models.TimelineEvent
221 for _, f := range follows {
222 profile, _ := profiles[f.SubjectDid]
223 followStatMap, _ := followStatMap[f.SubjectDid]
224
225 followStatus := models.IsNotFollowing
226 if followStatuses != nil {
227 followStatus = followStatuses[f.SubjectDid]
228 }
229
230 events = append(events, models.TimelineEvent{
231 Follow: &f,
232 Profile: profile,
233 FollowStats: &followStatMap,
234 FollowStatus: &followStatus,
235 EventAt: f.FollowedAt,
236 })
237 }
238
239 return events, nil
240}