Monorepo for Tangled tangled.org
5

Configure Feed

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

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