+
{{ template "timeline/fragments/notifications" . }}
+ {{ template "timeline/fragments/recents" . }}
diff --git a/appview/state/timeline.go b/appview/state/timeline.go
index 4064e5c3..7290b260 100644
--- a/appview/state/timeline.go
+++ b/appview/state/timeline.go
@@ -2,6 +2,7 @@ package state
import (
"net/http"
+ "sort"
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/appview/db"
@@ -79,7 +80,6 @@ func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
s.db,
pagination.Page{Limit: 5, Offset: 0},
orm.FilterEq("recipient_did", user.Did),
- orm.FilterEq("read", 0),
)
if err != nil {
s.logger.Error("failed to get notifications for timeline", "err", err)
@@ -108,6 +108,14 @@ func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
}
}
+ var recents []pages.RecentItem
+ if user != nil {
+ recents, err = s.buildRecents(user.Did)
+ if err != nil {
+ s.logger.Error("failed to build recents for timeline", "err", err)
+ }
+ }
+
s.pages.Timeline(w, pages.TimelineParams{
LoggedInUser: user,
Timeline: timeline,
@@ -115,10 +123,96 @@ func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
GfiLabel: gfiLabel,
VouchSuggestions: vouchSuggestions,
Notifications: notifications,
+ Recents: recents,
ShowNewsletter: s.showNewsletter(user),
})
}
+func (s *State) buildRecents(userDid string) ([]pages.RecentItem, error) {
+ links, err := db.GetRecentLinks(s.db, orm.FilterEq("user_did", userDid))
+ if err != nil {
+ return nil, err
+ }
+ if len(links) == 0 {
+ return nil, nil
+ }
+
+ // group targets by type.
+ var repoDids, issueAtUris, pullAtUris []string
+ for _, l := range links {
+ switch l.LinkType {
+ case models.RecentLinkTypeRepo:
+ repoDids = append(repoDids, l.Target)
+ case models.RecentLinkTypeIssue:
+ issueAtUris = append(issueAtUris, l.Target)
+ case models.RecentLinkTypePull:
+ pullAtUris = append(pullAtUris, l.Target)
+ }
+ }
+
+ // fetch repos by DID.
+ repoByDid := make(map[string]*models.Repo)
+ if len(repoDids) > 0 {
+ fetched, err := db.GetRepos(s.db, orm.FilterIn("repo_did", repoDids))
+ if err != nil {
+ return nil, err
+ }
+ for i := range fetched {
+ repoByDid[fetched[i].RepoDid] = &fetched[i]
+ }
+ }
+
+ // fetch issues by aturi
+ issueByAtUri := make(map[string]*models.Issue)
+ if len(issueAtUris) > 0 {
+ issues, err := db.GetIssues(s.db, orm.FilterIn("at_uri", issueAtUris))
+ if err != nil {
+ return nil, err
+ }
+ for _, issue := range issues {
+ issueByAtUri[issue.AtUri().String()] = &issue
+ }
+ }
+
+ // fetch pulls by aturi
+ pullByAtUri := make(map[string]*models.Pull)
+ if len(pullAtUris) > 0 {
+ fetched, err := db.GetPulls(s.db, orm.FilterIn("at_uri", pullAtUris))
+ if err != nil {
+ return nil, err
+ }
+ for _, p := range fetched {
+ pullByAtUri[p.AtUri().String()] = p
+ }
+ }
+
+ // build result in original link order
+ var items []pages.RecentItem
+ for _, l := range links {
+ item := pages.RecentItem{Link: l}
+ switch l.LinkType {
+ case models.RecentLinkTypeRepo:
+ item.Repo = repoByDid[l.Target]
+ case models.RecentLinkTypeIssue:
+ item.Issue = issueByAtUri[l.Target]
+ case models.RecentLinkTypePull:
+ item.Pull = pullByAtUri[l.Target]
+ }
+ // skip if the entity could not be resolved (e.g. deleted).
+ if item.Repo == nil && item.Issue == nil && item.Pull == nil {
+ continue
+ }
+ items = append(items, item)
+ }
+
+ // re-sort by visited descending to restore recency order after map lookups.
+ sort.Slice(items, func(i, j int) bool {
+ return items[i].Link.Visited.After(items[j].Link.Visited)
+ })
+
+ return items, nil
+}
+
// showNewsletter decides whether the newsletter widget/CTA should render.
// Anonymous visitors always see it (they can dismiss via localStorage);
// logged-in users whose newsletter_preferences row exists (either