diff --git a/appview/db/timeline.go b/appview/db/timeline.go --- a/appview/db/timeline.go +++ b/appview/db/timeline.go @@ -11,7 +11,7 @@ // 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 easily -func MakeTimeline(e Execer, limit int, loggedInUserDid string, limitToUsersIsFollowing bool) ([]models.TimelineEvent, error) { +func MakeTimeline(e Execer, limit int, loggedInUserDid string, limitToUsersIsFollowing bool) ([]models.TimelineGroup, error) { var events []models.TimelineEvent var userIsFollowing []string @@ -27,17 +27,22 @@ } } - repos, err := getTimelineRepos(e, limit, loggedInUserDid, userIsFollowing) + // 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 + + repos, err := getTimelineRepos(e, fetchLimit, loggedInUserDid, userIsFollowing) if err != nil { return nil, err } - stars, err := getTimelineStars(e, limit, loggedInUserDid, userIsFollowing) + stars, err := getTimelineStars(e, fetchLimit, loggedInUserDid, userIsFollowing) if err != nil { return nil, err } - follows, err := getTimelineFollows(e, limit, loggedInUserDid, userIsFollowing) + follows, err := getTimelineFollows(e, fetchLimit, loggedInUserDid, userIsFollowing) if err != nil { return nil, err } @@ -50,12 +55,45 @@ return events[i].EventAt.After(events[j].EventAt) }) - // Limit the slice to 100 events - if len(events) > limit { - events = events[:limit] + groups := collapseTimeline(events) + if len(groups) > limit { + groups = groups[:limit] } + return groups, nil +} - return events, 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) { diff --git a/appview/models/timeline.go b/appview/models/timeline.go --- a/appview/models/timeline.go +++ b/appview/models/timeline.go @@ -21,3 +21,21 @@ IsStarred bool StarCount int64 } + +// TimelineGroup is a primary TimelineEvent plus zero or more peer events +// that share the same operation+target (same repo starred, same user +// followed) and arrived consecutively. Primary is the newest of the group; +// Others holds the older peers in descending order. For non-collapsible +// events (repo create) Others is always empty. +type TimelineGroup struct { + Primary TimelineEvent + Others []TimelineEvent +} + +func (g TimelineGroup) IsCollapsed() bool { + return len(g.Others) > 0 +} + +func (g TimelineGroup) OthersCount() int { + return len(g.Others) +} diff --git a/appview/pages/pages.go b/appview/pages/pages.go --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -403,7 +403,7 @@ type TimelineParams struct { LoggedInUser *oauth.MultiAccountUser - Timeline []models.TimelineEvent + Timeline []models.TimelineGroup Repos []models.Repo GfiLabel *models.LabelDefinition BlueskyPosts []models.BskyPost diff --git a/appview/pages/templates/timeline/fragments/timeline.html b/appview/pages/templates/timeline/fragments/timeline.html --- a/appview/pages/templates/timeline/fragments/timeline.html +++ b/appview/pages/templates/timeline/fragments/timeline.html @@ -6,22 +6,21 @@