This repository has no description
1package db
2
3import (
4 "database/sql"
5 "errors"
6 "fmt"
7 "strings"
8 "time"
9
10 "github.com/bluesky-social/indigo/atproto/syntax"
11 "tangled.org/core/appview/models"
12)
13
14// notification types that qualify for focus mode
15var FocusEligibleTypes = []models.NotificationType{
16 models.NotificationTypeIssueCreated,
17 models.NotificationTypeIssueReopen,
18 models.NotificationTypeIssueCommented,
19 models.NotificationTypePullCreated,
20 models.NotificationTypePullReopen,
21 models.NotificationTypePullCommented,
22 models.NotificationTypeUserMentioned,
23}
24
25func focusEligiblePlaceholders() (string, []any) {
26 placeholders := make([]string, len(FocusEligibleTypes))
27 args := make([]any, len(FocusEligibleTypes))
28 for i, t := range FocusEligibleTypes {
29 placeholders[i] = "?"
30 args[i] = string(t)
31 }
32 return strings.Join(placeholders, ", "), args
33}
34
35// marks a user as currently focusing
36func BeginFocus(e Execer, did string) error {
37 _, err := e.Exec(`insert or replace into focusing (did) values (?)`, did)
38 if err != nil {
39 return fmt.Errorf("BeginFocus: %w", err)
40 }
41 return nil
42}
43
44// remove the focusing flag for a user
45func EndFocus(e Execer, did string) error {
46 _, err := e.Exec(`delete from focusing where did = ?`, did)
47 if err != nil {
48 return fmt.Errorf("EndFocus: %w", err)
49 }
50 return nil
51}
52
53// whether a user is currently in focus mode
54func GetFocusStatus(e Execer, did string) (bool, error) {
55 var exists bool
56 err := e.QueryRow(`select exists(select 1 from focusing where did = ?)`, did).Scan(&exists)
57 if errors.Is(err, sql.ErrNoRows) {
58 return false, nil
59 }
60 if err != nil {
61 return false, fmt.Errorf("GetFocusStatus: %w", err)
62 }
63 return exists, nil
64}
65
66// oldest unread focus-eligible notification for the user, with its related entity populated
67//
68// returns nil, nil when empty
69func GetNextFocusItem(e Execer, did string) (*models.NotificationWithEntity, error) {
70 placeholders, typeArgs := focusEligiblePlaceholders()
71
72 query := fmt.Sprintf(`
73 select
74 n.id, n.recipient_did, n.actor_did, n.type, n.entity_type, n.entity_id,
75 n.read, n.created, n.repo_id, n.issue_id, n.pull_id,
76 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,
77 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,
78 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
79 from notifications n
80 left join repos r on n.repo_id = r.id
81 left join issues i on n.issue_id = i.id
82 left join pulls p on n.pull_id = p.id
83 where n.recipient_did = ?
84 and n.read = 0
85 and n.type in (%s)
86 order by n.created asc
87 limit 1
88 `, placeholders)
89
90 args := append([]any{did}, typeArgs...)
91
92 row := e.QueryRow(query, args...)
93
94 var n models.Notification
95 var typeStr string
96 var createdStr string
97 var repo models.Repo
98 var issue models.Issue
99 var pull models.Pull
100 var rId, iId, pId sql.NullInt64
101 var rDid, rRkey, rName, rDescription, rWebsite, rTopicStr sql.NullString
102 var iDid sql.NullString
103 var iIssueId sql.NullInt64
104 var iTitle sql.NullString
105 var iOpen sql.NullBool
106 var pOwnerDid sql.NullString
107 var pPullId sql.NullInt64
108 var pTitle sql.NullString
109 var pState sql.NullInt64
110
111 err := row.Scan(
112 &n.ID, &n.RecipientDid, &n.ActorDid, &typeStr, &n.EntityType, &n.EntityId,
113 &n.Read, &createdStr, &n.RepoId, &n.IssueId, &n.PullId,
114 &rId, &rDid, &rRkey, &rName, &rDescription, &rWebsite, &rTopicStr,
115 &iId, &iDid, &iIssueId, &iTitle, &iOpen,
116 &pId, &pOwnerDid, &pPullId, &pTitle, &pState,
117 )
118 if errors.Is(err, sql.ErrNoRows) {
119 return nil, nil
120 }
121 if err != nil {
122 return nil, fmt.Errorf("GetNextFocusItem: %w", err)
123 }
124
125 n.Type = models.NotificationType(typeStr)
126 n.Created, err = time.Parse(time.RFC3339, createdStr)
127 if err != nil {
128 return nil, fmt.Errorf("GetNextFocusItem: parse created: %w", err)
129 }
130
131 entry := &models.NotificationWithEntity{Notification: &n}
132
133 if rId.Valid {
134 repo.Id = rId.Int64
135 if rDid.Valid {
136 repo.Did = rDid.String
137 }
138 if rRkey.Valid {
139 repo.Rkey = rRkey.String
140 }
141 if rName.Valid {
142 repo.Name = rName.String
143 }
144 if rDescription.Valid {
145 repo.Description = rDescription.String
146 }
147 if rWebsite.Valid {
148 repo.Website = rWebsite.String
149 }
150 if rTopicStr.Valid {
151 repo.Topics = strings.Fields(rTopicStr.String)
152 }
153 entry.Repo = &repo
154 }
155
156 if iId.Valid {
157 issue.Id = iId.Int64
158 if iDid.Valid {
159 issue.Did = iDid.String
160 }
161 if iIssueId.Valid {
162 issue.IssueId = int(iIssueId.Int64)
163 }
164 if iTitle.Valid {
165 issue.Title = iTitle.String
166 }
167 if iOpen.Valid {
168 issue.Open = iOpen.Bool
169 }
170 entry.Issue = &issue
171 }
172
173 if pId.Valid {
174 pull.ID = pId.Int64
175 if pOwnerDid.Valid {
176 pull.OwnerDid = syntax.DID(pOwnerDid.String)
177 }
178 if pPullId.Valid {
179 pull.PullId = pPullId.Int64
180 }
181 if pTitle.Valid {
182 pull.Title = pTitle.String
183 }
184 if pState.Valid {
185 pull.State = models.PullState(pState.Int64)
186 }
187 entry.Pull = &pull
188 }
189
190 return entry, nil
191}
192
193// returns the number of unread focus-eligible notifications for a user (not sure if we need this?)
194func CountFocusNotifs(e Execer, did string) (int64, error) {
195 placeholders, typeArgs := focusEligiblePlaceholders()
196
197 query := fmt.Sprintf(`
198 select count(1)
199 from notifications
200 where recipient_did = ?
201 and read = 0
202 and type in (%s)
203 `, placeholders)
204
205 args := append([]any{did}, typeArgs...)
206
207 var count int64
208 err := e.QueryRow(query, args...).Scan(&count)
209 if errors.Is(err, sql.ErrNoRows) {
210 return 0, nil
211 }
212 if err != nil {
213 return 0, fmt.Errorf("CountFocusNotifs: %w", err)
214 }
215 return count, nil
216}