···559559 pull_id integer references pulls(id)
560560 );
561561562562+ create table if not exists recent_links (
563563+ id integer primary key autoincrement,
564564+ user_did text not null,
565565+ link_type text not null,
566566+ target text not null,
567567+ visited text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
568568+ unique(user_did, target)
569569+ );
570570+562571 create table if not exists notification_preferences (
563572 id integer primary key autoincrement,
564573 user_did text not null unique,
+90
appview/db/recents.go
···11+package db
22+33+import (
44+ "context"
55+ "fmt"
66+ "time"
77+88+ "tangled.org/core/appview/models"
99+ "tangled.org/core/orm"
1010+)
1111+1212+func UpsertRecentLink(e Execer, userDid string, linkType models.RecentLinkType, target string) error {
1313+ _, err := e.Exec(`
1414+ insert into recent_links (user_did, link_type, target, visited)
1515+ values (?, ?, ?, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
1616+ on conflict(user_did, target) do update set
1717+ visited = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
1818+ `, userDid, string(linkType), target)
1919+ if err != nil {
2020+ return fmt.Errorf("failed to upsert recent link: %w", err)
2121+ }
2222+2323+ _, err = e.Exec(`
2424+ delete from recent_links
2525+ where user_did = ?
2626+ and id not in (
2727+ select id from recent_links
2828+ where user_did = ?
2929+ order by visited desc
3030+ limit 5
3131+ )
3232+ `, userDid, userDid)
3333+ if err != nil {
3434+ return fmt.Errorf("failed to trim recent links: %w", err)
3535+ }
3636+3737+ return nil
3838+}
3939+4040+func GetRecentLinks(e Execer, filters ...orm.Filter) ([]*models.RecentLink, error) {
4141+ var conditions []string
4242+ var args []any
4343+4444+ for _, filter := range filters {
4545+ conditions = append(conditions, filter.Condition())
4646+ args = append(args, filter.Arg()...)
4747+ }
4848+4949+ whereClause := ""
5050+ if len(conditions) > 0 {
5151+ whereClause = "WHERE " + conditions[0]
5252+ for _, condition := range conditions[1:] {
5353+ whereClause += " AND " + condition
5454+ }
5555+ }
5656+5757+ args = append(args, 5)
5858+5959+ query := fmt.Sprintf(`
6060+ select id, user_did, link_type, target, visited
6161+ from recent_links
6262+ %s
6363+ order by visited desc
6464+ limit ?
6565+ `, whereClause)
6666+6767+ rows, err := e.QueryContext(context.Background(), query, args...)
6868+ if err != nil {
6969+ return nil, fmt.Errorf("failed to query recent links: %w", err)
7070+ }
7171+ defer rows.Close()
7272+7373+ var links []*models.RecentLink
7474+ for rows.Next() {
7575+ var l models.RecentLink
7676+ var linkTypeStr string
7777+ var visitedStr string
7878+ if err := rows.Scan(&l.Id, &l.UserDid, &linkTypeStr, &l.Target, &visitedStr); err != nil {
7979+ return nil, fmt.Errorf("failed to scan recent link: %w", err)
8080+ }
8181+ l.LinkType = models.RecentLinkType(linkTypeStr)
8282+ l.Visited, err = time.Parse(time.RFC3339, visitedStr)
8383+ if err != nil {
8484+ return nil, fmt.Errorf("failed to parse visited timestamp: %w", err)
8585+ }
8686+ links = append(links, &l)
8787+ }
8888+8989+ return links, nil
9090+}