Monorepo for Tangled
tangled.org
1package db
2
3import (
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
14var 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
24func 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
35func 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
44func 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
53func 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
68func 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?)
193func 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}