+113
-65
appview/db/issues.go
+113
-65
appview/db/issues.go
···
3
3
import (
4
4
"database/sql"
5
5
"fmt"
6
-
mathrand "math/rand/v2"
6
+
"maps"
7
+
"slices"
8
+
"sort"
7
9
"strings"
8
10
"time"
9
11
···
13
15
)
14
16
15
17
type Issue struct {
16
-
ID int64
17
-
RepoAt syntax.ATURI
18
-
OwnerDid string
19
-
IssueId int
20
-
Rkey string
21
-
Created time.Time
22
-
Title string
23
-
Body string
24
-
Open bool
18
+
Id int64
19
+
Did string
20
+
Rkey string
21
+
RepoAt syntax.ATURI
22
+
IssueId int
23
+
Created time.Time
24
+
Edited *time.Time
25
+
Deleted *time.Time
26
+
Title string
27
+
Body string
28
+
Open bool
25
29
26
30
// optionally, populate this when querying for reverse mappings
27
31
// like comment counts, parent repo etc.
28
-
Metadata *IssueMetadata
32
+
Comments []IssueComment
33
+
Repo *Repo
29
34
}
30
35
31
-
type IssueMetadata struct {
32
-
CommentCount int
33
-
Repo *Repo
34
-
// labels, assignee etc.
36
+
func (i *Issue) AtUri() syntax.ATURI {
37
+
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueNSID, i.Rkey))
35
38
}
36
39
37
-
type Comment struct {
38
-
OwnerDid string
39
-
RepoAt syntax.ATURI
40
-
Rkey string
41
-
Issue int
42
-
CommentId int
43
-
Body string
44
-
Created *time.Time
45
-
Deleted *time.Time
46
-
Edited *time.Time
40
+
func (i *Issue) AsRecord() tangled.RepoIssue {
41
+
return tangled.RepoIssue{
42
+
Repo: i.RepoAt.String(),
43
+
Title: i.Title,
44
+
Body: &i.Body,
45
+
CreatedAt: i.Created.Format(time.RFC3339),
46
+
}
47
47
}
48
48
49
-
func (i *Issue) AtUri() syntax.ATURI {
50
-
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.OwnerDid, tangled.RepoIssueNSID, i.Rkey))
49
+
type CommentListItem struct {
50
+
Self *IssueComment
51
+
Replies []*IssueComment
52
+
}
53
+
54
+
func (i *Issue) CommentList() []CommentListItem {
55
+
// Create a map to quickly find comments by their aturi
56
+
toplevel := make(map[string]*CommentListItem)
57
+
var replies []*IssueComment
58
+
59
+
// collect top level comments into the map
60
+
for _, comment := range i.Comments {
61
+
if comment.IsTopLevel() {
62
+
toplevel[comment.AtUri().String()] = &CommentListItem{
63
+
Self: &comment,
64
+
}
65
+
} else {
66
+
replies = append(replies, &comment)
67
+
}
68
+
}
69
+
70
+
for _, r := range replies {
71
+
parentAt := *r.ReplyTo
72
+
if parent, exists := toplevel[parentAt]; exists {
73
+
parent.Replies = append(parent.Replies, r)
74
+
}
75
+
}
76
+
77
+
var listing []CommentListItem
78
+
for _, v := range toplevel {
79
+
listing = append(listing, *v)
80
+
}
81
+
82
+
// sort everything
83
+
sortFunc := func(a, b *IssueComment) bool {
84
+
return a.Created.Before(b.Created)
85
+
}
86
+
sort.Slice(listing, func(i, j int) bool {
87
+
return sortFunc(listing[i].Self, listing[j].Self)
88
+
})
89
+
for _, r := range listing {
90
+
sort.Slice(r.Replies, func(i, j int) bool {
91
+
return sortFunc(r.Replies[i], r.Replies[j])
92
+
})
93
+
}
94
+
95
+
return listing
51
96
}
52
97
53
98
func IssueFromRecord(did, rkey string, record tangled.RepoIssue) Issue {
···
62
107
}
63
108
64
109
return Issue{
65
-
RepoAt: syntax.ATURI(record.Repo),
66
-
OwnerDid: did,
67
-
Rkey: rkey,
68
-
Created: created,
69
-
Title: record.Title,
70
-
Body: body,
71
-
Open: true, // new issues are open by default
110
+
RepoAt: syntax.ATURI(record.Repo),
111
+
Did: did,
112
+
Rkey: rkey,
113
+
Created: created,
114
+
Title: record.Title,
115
+
Body: body,
116
+
Open: true, // new issues are open by default
72
117
}
73
118
}
74
119
75
-
func ResolveIssueFromAtUri(e Execer, issueUri syntax.ATURI) (syntax.ATURI, int, error) {
76
-
ownerDid := issueUri.Authority().String()
77
-
issueRkey := issueUri.RecordKey().String()
120
+
type IssueComment struct {
121
+
Id int64
122
+
Did string
123
+
Rkey string
124
+
IssueAt string
125
+
ReplyTo *string
126
+
Body string
127
+
Created time.Time
128
+
Edited *time.Time
129
+
Deleted *time.Time
130
+
}
78
131
79
-
var repoAt string
80
-
var issueId int
132
+
func (i *IssueComment) AtUri() syntax.ATURI {
133
+
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueCommentNSID, i.Rkey))
134
+
}
81
135
82
-
query := `select repo_at, issue_id from issues where owner_did = ? and rkey = ?`
83
-
err := e.QueryRow(query, ownerDid, issueRkey).Scan(&repoAt, &issueId)
84
-
if err != nil {
85
-
return "", 0, err
136
+
func (i *IssueComment) AsRecord() tangled.RepoIssueComment {
137
+
return tangled.RepoIssueComment{
138
+
Body: i.Body,
139
+
Issue: i.IssueAt,
140
+
CreatedAt: i.Created.Format(time.RFC3339),
141
+
ReplyTo: i.ReplyTo,
86
142
}
143
+
}
87
144
88
-
return syntax.ATURI(repoAt), issueId, nil
145
+
func (i *IssueComment) IsTopLevel() bool {
146
+
return i.ReplyTo == nil
89
147
}
90
148
91
-
func IssueCommentFromRecord(e Execer, did, rkey string, record tangled.RepoIssueComment) (Comment, error) {
149
+
func IssueCommentFromRecord(e Execer, did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) {
92
150
created, err := time.Parse(time.RFC3339, record.CreatedAt)
93
151
if err != nil {
94
152
created = time.Now()
95
153
}
96
154
97
155
ownerDid := did
98
-
if record.Owner != nil {
99
-
ownerDid = *record.Owner
100
-
}
101
-
102
-
issueUri, err := syntax.ParseATURI(record.Issue)
103
-
if err != nil {
104
-
return Comment{}, err
105
-
}
106
156
107
-
repoAt, issueId, err := ResolveIssueFromAtUri(e, issueUri)
108
-
if err != nil {
109
-
return Comment{}, err
157
+
if _, err = syntax.ParseATURI(record.Issue); err != nil {
158
+
return nil, err
110
159
}
111
160
112
-
comment := Comment{
113
-
OwnerDid: ownerDid,
114
-
RepoAt: repoAt,
115
-
Rkey: rkey,
116
-
Body: record.Body,
117
-
Issue: issueId,
118
-
CommentId: mathrand.IntN(1000000),
119
-
Created: &created,
161
+
comment := IssueComment{
162
+
Did: ownerDid,
163
+
Rkey: rkey,
164
+
Body: record.Body,
165
+
IssueAt: record.Issue,
166
+
ReplyTo: record.ReplyTo,
167
+
Created: created,
120
168
}
121
169
122
-
return comment, nil
170
+
return &comment, nil
123
171
}
124
172
125
173
func NewIssue(tx *sql.Tx, issue *Issue) error {