Monorepo for Tangled
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279package db
import ( "sort"
"tangled.org/core/appview/models" "tangled.org/core/appview/pagination" "tangled.org/core/orm")
// followingFilter compiles to `key in (select subject_did from follows ...)`,// keeping the following-set check inside sqlite rather than materializing the// followed dids into a huge placeholder list.func followingFilter(key, loggedInUserDid string) orm.Filter { return orm.FilterInSubquery(key, "select subject_did from follows where did = ?", loggedInUserDid)}
// TODO: this gathers heterogenous events from different sources and aggregates// them in code; if we did this entirely in sql, we could order and limit and paginate easilyfunc MakeTimeline(e Execer, limit int, loggedInUserDid string, limitToUsersIsFollowing bool) ([]models.TimelineGroup, error) { var events []models.TimelineEvent
// Fetch more events than we need to so that when we collapse each individual // event into groups, we can still be relatively confident that we will have // `limit` groups to fill the timeline with. Adjust multiplier as necessary. fetchLimit := limit * 2
var followingOnly string if limitToUsersIsFollowing { followingOnly = loggedInUserDid }
repos, err := getTimelineRepos(e, fetchLimit, loggedInUserDid, followingOnly) if err != nil { return nil, err }
stars, err := getTimelineStars(e, fetchLimit, loggedInUserDid, followingOnly) if err != nil { return nil, err }
follows, err := getTimelineFollows(e, fetchLimit, loggedInUserDid, followingOnly) if err != nil { return nil, err }
events = append(events, repos...) events = append(events, stars...) events = append(events, follows...)
sort.Slice(events, func(i, j int) bool { return events[i].EventAt.After(events[j].EventAt) })
groups := collapseTimeline(events) if len(groups) > limit { groups = groups[:limit] } return groups, nil}
// collapseTimeline merges consecutive events that share the same operation// and target into one TimelineGroup (assumes events are sorted newest-first).func collapseTimeline(events []models.TimelineEvent) []models.TimelineGroup { var groups []models.TimelineGroup i := 0 for i < len(events) { group := models.TimelineGroup{Primary: events[i]} j := i + 1 for j < len(events) && canCollapse(events[i], events[j]) { group.Others = append(group.Others, events[j]) j++ } groups = append(groups, group) i = j } return groups}
// canCollapse reports whether two adjacent events in the timeline represent// the same operation on the same target (repo starred or user followed).func canCollapse(a, b models.TimelineEvent) bool { switch { case a.RepoStar != nil && b.RepoStar != nil: if a.RepoStar.Repo == nil || b.RepoStar.Repo == nil { return false } return a.RepoStar.Repo.RepoAt() == b.RepoStar.Repo.RepoAt() case a.Follow != nil && b.Follow != nil: return a.Follow.SubjectDid == b.Follow.SubjectDid default: return false }}
func fetchStarStatuses(e Execer, loggedInUserDid string, repos []models.Repo) (map[string]bool, error) { if loggedInUserDid == "" { return nil, nil }
var repoDids []string for _, r := range repos { repoDids = append(repoDids, r.RepoDid) }
return GetStarStatuses(e, loggedInUserDid, repoDids)}
func getRepoStarInfo(repo *models.Repo, starStatuses map[string]bool) (bool, int64) { var isStarred bool if starStatuses != nil { isStarred = starStatuses[repo.RepoDid] }
var starCount int64 if repo.RepoStats != nil { starCount = int64(repo.RepoStats.StarCount) }
return isStarred, starCount}
func getTimelineRepos(e Execer, limit int, loggedInUserDid string, followingOnly string) ([]models.TimelineEvent, error) { filters := make([]orm.Filter, 0) if followingOnly != "" { filters = append(filters, followingFilter("did", followingOnly)) }
repos, err := GetReposPaginated(e, pagination.Page{Limit: limit}, filters...) if err != nil { return nil, err }
// fetch all source repos var args []string for _, r := range repos { if r.Source != "" { args = append(args, r.Source) } }
var origRepos []models.Repo if args != nil { origRepos, err = GetRepos(e, orm.FilterIn("repo_did", args)) } if err != nil { return nil, err }
didToRepo := make(map[string]models.Repo) for _, r := range origRepos { didToRepo[r.RepoDid] = r }
starStatuses, err := fetchStarStatuses(e, loggedInUserDid, repos) if err != nil { return nil, err }
var events []models.TimelineEvent for _, r := range repos { var source *models.Repo if r.Source != "" { if origRepo, ok := didToRepo[r.Source]; ok { source = &origRepo } }
isStarred, starCount := getRepoStarInfo(&r, starStatuses)
events = append(events, models.TimelineEvent{ Repo: &r, EventAt: r.Created, Source: source, IsStarred: isStarred, StarCount: starCount, }) }
return events, nil}
func getTimelineStars(e Execer, limit int, loggedInUserDid string, followingOnly string) ([]models.TimelineEvent, error) { filters := make([]orm.Filter, 0) if followingOnly != "" { filters = append(filters, followingFilter("did", followingOnly)) }
stars, err := GetRepoStars(e, pagination.Page{Limit: limit}, filters...) if err != nil { return nil, err }
var repos []models.Repo for _, s := range stars { repos = append(repos, *s.Repo) }
starStatuses, err := fetchStarStatuses(e, loggedInUserDid, repos) if err != nil { return nil, err }
var events []models.TimelineEvent for _, s := range stars { isStarred, starCount := getRepoStarInfo(s.Repo, starStatuses)
events = append(events, models.TimelineEvent{ RepoStar: &s, EventAt: s.Created, IsStarred: isStarred, StarCount: starCount, }) }
return events, nil}
func getTimelineFollows(e Execer, limit int, loggedInUserDid string, followingOnly string) ([]models.TimelineEvent, error) { filters := make([]orm.Filter, 0) if followingOnly != "" { filters = append(filters, followingFilter("did", followingOnly)) }
follows, err := GetFollows(e, limit, filters...) if err != nil { return nil, err }
var subjects []string for _, f := range follows { subjects = append(subjects, f.SubjectDid) }
if subjects == nil { return nil, nil }
profiles, err := GetProfiles(e, orm.FilterIn("did", subjects)) if err != nil { return nil, err }
followStatMap, err := GetFollowerFollowingCounts(e, subjects) if err != nil { return nil, err }
var followStatuses map[string]models.FollowStatus if loggedInUserDid != "" { followStatuses, err = GetFollowStatuses(e, loggedInUserDid, subjects) if err != nil { return nil, err } }
var events []models.TimelineEvent for _, f := range follows { profile, _ := profiles[f.SubjectDid] followStatMap, _ := followStatMap[f.SubjectDid]
followStatus := models.IsNotFollowing if followStatuses != nil { followStatus = followStatuses[f.SubjectDid] }
events = append(events, models.TimelineEvent{ Follow: &f, Profile: profile, FollowStats: &followStatMap, FollowStatus: &followStatus, EventAt: f.FollowedAt, }) }
return events, nil}