Monorepo for Tangled tangled.org
2

Configure Feed

Select the types of activity you want to include in your feed.

at icy/kxpzqo 7.0 kB View raw
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}