Monorepo for Tangled tangled.org
2

Configure Feed

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

appview/db: add tables and queries for focus mode

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

author
oppiliappan
date (Jun 15, 2026, 2:01 PM +0100) commit baab1e2b parent ed88d4db change-id yzrowtly
+235 -10
+20 -10
appview/db/db.go
··· 2178 2178 return err 2179 2179 }) 2180 2180 2181 + orm.RunMigration(conn, logger, "delete-unused-pipeline-statuses", func(tx *sql.Tx) error { 2182 + _, err := tx.Exec(` 2183 + delete from pipeline_statuses as p 2184 + where p.status = 'pending' 2185 + and exists ( 2186 + select 1 from pipeline_statuses as q 2187 + where q.pipeline_knot = p.pipeline_knot 2188 + and q.pipeline_rkey = p.pipeline_rkey 2189 + and q.workflow = p.workflow 2190 + and q.status = 'pending' 2191 + and q.created < p.created 2192 + ); 2193 + `) 2194 + return err 2195 + }) 2196 + 2181 2197 orm.RunMigration(conn, logger, "timeline-query-indexes", func(tx *sql.Tx) error { 2182 2198 _, err := tx.Exec(` 2183 2199 -- following timeline: stars by a set of users, newest first ··· 2192 2208 return err 2193 2209 }) 2194 2210 2195 - orm.RunMigration(conn, logger, "delete-unused-pipeline-statuses", func(tx *sql.Tx) error { 2211 + orm.RunMigration(conn, logger, "add-focusing-table", func(tx *sql.Tx) error { 2196 2212 _, err := tx.Exec(` 2197 - delete from pipeline_statuses as p 2198 - where p.status = 'pending' 2199 - and exists ( 2200 - select 1 from pipeline_statuses as q 2201 - where q.pipeline_knot = p.pipeline_knot 2202 - and q.pipeline_rkey = p.pipeline_rkey 2203 - and q.workflow = p.workflow 2204 - and q.status = 'pending' 2205 - and q.created < p.created 2213 + create table if not exists focusing ( 2214 + did text primary key, 2215 + started text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 2206 2216 ); 2207 2217 `) 2208 2218 return err
+215
appview/db/focus.go
··· 1 + package db 2 + 3 + import ( 4 + "database/sql" 5 + "errors" 6 + "fmt" 7 + "strings" 8 + "time" 9 + 10 + "tangled.org/core/appview/models" 11 + ) 12 + 13 + // notification types that qualify for focus mode 14 + var FocusEligibleTypes = []models.NotificationType{ 15 + models.NotificationTypeIssueCreated, 16 + models.NotificationTypeIssueReopen, 17 + models.NotificationTypeIssueCommented, 18 + models.NotificationTypePullCreated, 19 + models.NotificationTypePullReopen, 20 + models.NotificationTypePullCommented, 21 + models.NotificationTypeUserMentioned, 22 + } 23 + 24 + func focusEligiblePlaceholders() (string, []any) { 25 + placeholders := make([]string, len(FocusEligibleTypes)) 26 + args := make([]any, len(FocusEligibleTypes)) 27 + for i, t := range FocusEligibleTypes { 28 + placeholders[i] = "?" 29 + args[i] = string(t) 30 + } 31 + return strings.Join(placeholders, ", "), args 32 + } 33 + 34 + // marks a user as currently focusing 35 + func BeginFocus(e Execer, did string) error { 36 + _, err := e.Exec(`insert or replace into focusing (did) values (?)`, did) 37 + if err != nil { 38 + return fmt.Errorf("BeginFocus: %w", err) 39 + } 40 + return nil 41 + } 42 + 43 + // remove the focusing flag for a user 44 + func EndFocus(e Execer, did string) error { 45 + _, err := e.Exec(`delete from focusing where did = ?`, did) 46 + if err != nil { 47 + return fmt.Errorf("EndFocus: %w", err) 48 + } 49 + return nil 50 + } 51 + 52 + // whether a user is currently in focus mode 53 + func GetFocusStatus(e Execer, did string) (bool, error) { 54 + var exists bool 55 + err := e.QueryRow(`select exists(select 1 from focusing where did = ?)`, did).Scan(&exists) 56 + if errors.Is(err, sql.ErrNoRows) { 57 + return false, nil 58 + } 59 + if err != nil { 60 + return false, fmt.Errorf("GetFocusStatus: %w", err) 61 + } 62 + return exists, nil 63 + } 64 + 65 + // oldest unread focus-eligible notification for the user, with its related entity populated 66 + // 67 + // returns nil, nil when empty 68 + func GetNextFocusItem(e Execer, did string) (*models.NotificationWithEntity, error) { 69 + placeholders, typeArgs := focusEligiblePlaceholders() 70 + 71 + query := fmt.Sprintf(` 72 + select 73 + n.id, n.recipient_did, n.actor_did, n.type, n.entity_type, n.entity_id, 74 + n.read, n.created, n.repo_id, n.issue_id, n.pull_id, 75 + r.id as r_id, r.did as r_did, r.rkey as r_rkey, r.name as r_name, r.description as r_description, r.website as r_website, r.topics as r_topics, 76 + i.id as i_id, i.did as i_did, i.issue_id as i_issue_id, i.title as i_title, i.open as i_open, 77 + p.id as p_id, p.owner_did as p_owner_did, p.pull_id as p_pull_id, p.title as p_title, p.state as p_state 78 + from notifications n 79 + left join repos r on n.repo_id = r.id 80 + left join issues i on n.issue_id = i.id 81 + left join pulls p on n.pull_id = p.id 82 + where n.recipient_did = ? 83 + and n.read = 0 84 + and n.type in (%s) 85 + order by n.created asc 86 + limit 1 87 + `, placeholders) 88 + 89 + args := append([]any{did}, typeArgs...) 90 + 91 + row := e.QueryRow(query, args...) 92 + 93 + var n models.Notification 94 + var typeStr string 95 + var createdStr string 96 + var repo models.Repo 97 + var issue models.Issue 98 + var pull models.Pull 99 + var rId, iId, pId sql.NullInt64 100 + var rDid, rRkey, rName, rDescription, rWebsite, rTopicStr sql.NullString 101 + var iDid sql.NullString 102 + var iIssueId sql.NullInt64 103 + var iTitle sql.NullString 104 + var iOpen sql.NullBool 105 + var pOwnerDid sql.NullString 106 + var pPullId sql.NullInt64 107 + var pTitle sql.NullString 108 + var pState sql.NullInt64 109 + 110 + err := row.Scan( 111 + &n.ID, &n.RecipientDid, &n.ActorDid, &typeStr, &n.EntityType, &n.EntityId, 112 + &n.Read, &createdStr, &n.RepoId, &n.IssueId, &n.PullId, 113 + &rId, &rDid, &rRkey, &rName, &rDescription, &rWebsite, &rTopicStr, 114 + &iId, &iDid, &iIssueId, &iTitle, &iOpen, 115 + &pId, &pOwnerDid, &pPullId, &pTitle, &pState, 116 + ) 117 + if errors.Is(err, sql.ErrNoRows) { 118 + return nil, nil 119 + } 120 + if err != nil { 121 + return nil, fmt.Errorf("GetNextFocusItem: %w", err) 122 + } 123 + 124 + n.Type = models.NotificationType(typeStr) 125 + n.Created, err = time.Parse(time.RFC3339, createdStr) 126 + if err != nil { 127 + return nil, fmt.Errorf("GetNextFocusItem: parse created: %w", err) 128 + } 129 + 130 + entry := &models.NotificationWithEntity{Notification: &n} 131 + 132 + if rId.Valid { 133 + repo.Id = rId.Int64 134 + if rDid.Valid { 135 + repo.Did = rDid.String 136 + } 137 + if rRkey.Valid { 138 + repo.Rkey = rRkey.String 139 + } 140 + if rName.Valid { 141 + repo.Name = rName.String 142 + } 143 + if rDescription.Valid { 144 + repo.Description = rDescription.String 145 + } 146 + if rWebsite.Valid { 147 + repo.Website = rWebsite.String 148 + } 149 + if rTopicStr.Valid { 150 + repo.Topics = strings.Fields(rTopicStr.String) 151 + } 152 + entry.Repo = &repo 153 + } 154 + 155 + if iId.Valid { 156 + issue.Id = iId.Int64 157 + if iDid.Valid { 158 + issue.Did = iDid.String 159 + } 160 + if iIssueId.Valid { 161 + issue.IssueId = int(iIssueId.Int64) 162 + } 163 + if iTitle.Valid { 164 + issue.Title = iTitle.String 165 + } 166 + if iOpen.Valid { 167 + issue.Open = iOpen.Bool 168 + } 169 + entry.Issue = &issue 170 + } 171 + 172 + if pId.Valid { 173 + pull.ID = int(pId.Int64) 174 + if pOwnerDid.Valid { 175 + pull.OwnerDid = pOwnerDid.String 176 + } 177 + if pPullId.Valid { 178 + pull.PullId = int(pPullId.Int64) 179 + } 180 + if pTitle.Valid { 181 + pull.Title = pTitle.String 182 + } 183 + if pState.Valid { 184 + pull.State = models.PullState(pState.Int64) 185 + } 186 + entry.Pull = &pull 187 + } 188 + 189 + return entry, nil 190 + } 191 + 192 + // returns the number of unread focus-eligible notifications for a user (not sure if we need this?) 193 + func CountFocusNotifs(e Execer, did string) (int64, error) { 194 + placeholders, typeArgs := focusEligiblePlaceholders() 195 + 196 + query := fmt.Sprintf(` 197 + select count(1) 198 + from notifications 199 + where recipient_did = ? 200 + and read = 0 201 + and type in (%s) 202 + `, placeholders) 203 + 204 + args := append([]any{did}, typeArgs...) 205 + 206 + var count int64 207 + err := e.QueryRow(query, args...).Scan(&count) 208 + if errors.Is(err, sql.ErrNoRows) { 209 + return 0, nil 210 + } 211 + if err != nil { 212 + return 0, fmt.Errorf("CountFocusNotifs: %w", err) 213 + } 214 + return count, nil 215 + }