Monorepo for Tangled tangled.org
5

Configure Feed

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

appview/db: add db queries for recent links

Signed-off-by: oppiliappan <me@oppi.li>

author
oppiliappan
date (Jun 8, 2026, 2:45 PM +0100) commit 4d2a3112 parent 92019ae0 change-id vuvopynp
+204 -3
+9
appview/db/db.go
··· 559 559 pull_id integer references pulls(id) 560 560 ); 561 561 562 + create table if not exists recent_links ( 563 + id integer primary key autoincrement, 564 + user_did text not null, 565 + link_type text not null, 566 + target text not null, 567 + visited text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 568 + unique(user_did, target) 569 + ); 570 + 562 571 create table if not exists notification_preferences ( 563 572 id integer primary key autoincrement, 564 573 user_did text not null unique,
+90
appview/db/recents.go
··· 1 + package db 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "time" 7 + 8 + "tangled.org/core/appview/models" 9 + "tangled.org/core/orm" 10 + ) 11 + 12 + func UpsertRecentLink(e Execer, userDid string, linkType models.RecentLinkType, target string) error { 13 + _, err := e.Exec(` 14 + insert into recent_links (user_did, link_type, target, visited) 15 + values (?, ?, ?, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 16 + on conflict(user_did, target) do update set 17 + visited = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 18 + `, userDid, string(linkType), target) 19 + if err != nil { 20 + return fmt.Errorf("failed to upsert recent link: %w", err) 21 + } 22 + 23 + _, err = e.Exec(` 24 + delete from recent_links 25 + where user_did = ? 26 + and id not in ( 27 + select id from recent_links 28 + where user_did = ? 29 + order by visited desc 30 + limit 5 31 + ) 32 + `, userDid, userDid) 33 + if err != nil { 34 + return fmt.Errorf("failed to trim recent links: %w", err) 35 + } 36 + 37 + return nil 38 + } 39 + 40 + func GetRecentLinks(e Execer, filters ...orm.Filter) ([]*models.RecentLink, error) { 41 + var conditions []string 42 + var args []any 43 + 44 + for _, filter := range filters { 45 + conditions = append(conditions, filter.Condition()) 46 + args = append(args, filter.Arg()...) 47 + } 48 + 49 + whereClause := "" 50 + if len(conditions) > 0 { 51 + whereClause = "WHERE " + conditions[0] 52 + for _, condition := range conditions[1:] { 53 + whereClause += " AND " + condition 54 + } 55 + } 56 + 57 + args = append(args, 5) 58 + 59 + query := fmt.Sprintf(` 60 + select id, user_did, link_type, target, visited 61 + from recent_links 62 + %s 63 + order by visited desc 64 + limit ? 65 + `, whereClause) 66 + 67 + rows, err := e.QueryContext(context.Background(), query, args...) 68 + if err != nil { 69 + return nil, fmt.Errorf("failed to query recent links: %w", err) 70 + } 71 + defer rows.Close() 72 + 73 + var links []*models.RecentLink 74 + for rows.Next() { 75 + var l models.RecentLink 76 + var linkTypeStr string 77 + var visitedStr string 78 + if err := rows.Scan(&l.Id, &l.UserDid, &linkTypeStr, &l.Target, &visitedStr); err != nil { 79 + return nil, fmt.Errorf("failed to scan recent link: %w", err) 80 + } 81 + l.LinkType = models.RecentLinkType(linkTypeStr) 82 + l.Visited, err = time.Parse(time.RFC3339, visitedStr) 83 + if err != nil { 84 + return nil, fmt.Errorf("failed to parse visited timestamp: %w", err) 85 + } 86 + links = append(links, &l) 87 + } 88 + 89 + return links, nil 90 + }
+102
appview/db/recents_test.go
··· 1 + package db 2 + 3 + import ( 4 + "fmt" 5 + "testing" 6 + 7 + "tangled.org/core/appview/models" 8 + "tangled.org/core/orm" 9 + ) 10 + 11 + func insertRecentLink(t *testing.T, d *DB, userDid string, linkType models.RecentLinkType, target, visited string) { 12 + t.Helper() 13 + if _, err := d.Exec( 14 + `insert into recent_links (user_did, link_type, target, visited) values (?, ?, ?, ?)`, 15 + userDid, string(linkType), target, visited, 16 + ); err != nil { 17 + t.Fatalf("insertRecentLink %q: %v", target, err) 18 + } 19 + } 20 + 21 + func TestUpsertRecentLink_LimitFive(t *testing.T) { 22 + d := newTestDB(t) 23 + const userDid = "did:plc:akshay" 24 + 25 + for i := range 6 { 26 + target := fmt.Sprintf("%d-repo-did", i) 27 + if err := UpsertRecentLink(d, userDid, models.RecentLinkTypeRepo, target); err != nil { 28 + t.Fatalf("UpsertRecentLink %d: %v", i, err) 29 + } 30 + } 31 + 32 + if got := countRows(t, d, "select count(*) from recent_links where user_did = ?", userDid); got != 5 { 33 + t.Errorf("recent_links count = %d, want 5", got) 34 + } 35 + } 36 + 37 + func TestUpsertRecentLink_DeduplicatesAndUpdatesTimestamp(t *testing.T) { 38 + d := newTestDB(t) 39 + const userDid = "did:plc:akshay" 40 + const target = "at://did:plc:akshay/sh.tangled.repo/myrepo" 41 + 42 + insertRecentLink(t, d, userDid, models.RecentLinkTypeIssue, target, "2024-01-01T00:00:00Z") 43 + 44 + if err := UpsertRecentLink(d, userDid, models.RecentLinkTypeIssue, target); err != nil { 45 + t.Fatalf("upsert: %v", err) 46 + } 47 + 48 + if got := countRows(t, d, "select count(*) from recent_links where user_did = ? and target = ?", userDid, target); got != 1 { 49 + t.Errorf("expected 1 row after duplicate upsert, got %d", got) 50 + } 51 + 52 + var visited string 53 + if err := d.QueryRow("select visited from recent_links where user_did = ? and target = ?", userDid, target).Scan(&visited); err != nil { 54 + t.Fatalf("scan visited: %v", err) 55 + } 56 + if visited <= "2024-01-01T00:00:00Z" { 57 + t.Errorf("visited not updated: got %q, want > %q", visited, "2024-01-01T00:00:00Z") 58 + } 59 + } 60 + 61 + func TestGetRecentLinks_LessThanFive(t *testing.T) { 62 + d := newTestDB(t) 63 + const userDid = "did:plc:akshay" 64 + 65 + for i := range 6 { 66 + name := fmt.Sprintf("repo-%d", i) 67 + insertRecentLink(t, d, userDid, models.RecentLinkTypeRepo, name, "2024-01-01T00:00:00Z") 68 + } 69 + 70 + links, err := GetRecentLinks(d, orm.FilterEq("user_did", userDid)) 71 + if err != nil { 72 + t.Fatalf("GetRecentLinks: %v", err) 73 + } 74 + 75 + if len(links) > 5 { 76 + t.Errorf("GetRecentLinks returned %d links, want <5", len(links)) 77 + } 78 + } 79 + 80 + func TestGetRecentLinks_OrderedByMostRecent(t *testing.T) { 81 + d := newTestDB(t) 82 + const userDid = "did:plc:akshay" 83 + 84 + insertRecentLink(t, d, userDid, models.RecentLinkTypeRepo, "repo-first", "2024-01-01T00:00:00Z") 85 + insertRecentLink(t, d, userDid, models.RecentLinkTypeRepo, "repo-second", "2024-01-02T00:00:00Z") 86 + insertRecentLink(t, d, userDid, models.RecentLinkTypeRepo, "repo-third", "2024-01-03T00:00:00Z") 87 + 88 + links, err := GetRecentLinks(d, orm.FilterEq("user_did", userDid)) 89 + if err != nil { 90 + t.Fatalf("GetRecentLinks: %v", err) 91 + } 92 + 93 + if len(links) != 3 { 94 + t.Fatalf("expected 3 links, got %d", len(links)) 95 + } 96 + if links[0].Target != "repo-third" { 97 + t.Errorf("links[0].Target = %q, want %q", links[0].Target, "repo-third") 98 + } 99 + if links[2].Target != "repo-first" { 100 + t.Errorf("links[2].Target = %q, want %q", links[2].Target, "repo-first") 101 + } 102 + }
+3 -3
flake.lock
··· 117 117 "ibm-plex-mono-src": { 118 118 "flake": false, 119 119 "locked": { 120 - "lastModified": 1731402378, 120 + "lastModified": 1731402384, 121 121 "narHash": "sha256-OwUmrPfEehLDz0fl2ChYLK8FQM2p0G1+EMrGsYEq+6g=", 122 122 "type": "tarball", 123 - "url": "https://github.com/IBM/plex/releases/download/@ibm/plex-mono@1.1.0/ibm-plex-mono.zip" 123 + "url": "https://github.com/IBM/plex/releases/download/@ibm%2Fplex-mono@1.1.0/ibm-plex-mono.zip" 124 124 }, 125 125 "original": { 126 126 "type": "tarball", 127 - "url": "https://github.com/IBM/plex/releases/download/@ibm/plex-mono@1.1.0/ibm-plex-mono.zip" 127 + "url": "https://github.com/IBM/plex/releases/download/@ibm%2Fplex-mono@1.1.0/ibm-plex-mono.zip" 128 128 } 129 129 }, 130 130 "indigo": {