Monorepo for Tangled tangled.org
9

Configure Feed

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

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