A vibe coded tangled fork which supports pijul.
1package models
2
3import (
4 "fmt"
5 "sort"
6 "time"
7
8 "github.com/bluesky-social/indigo/atproto/syntax"
9)
10
11// DiscussionState represents the state of a discussion
12type DiscussionState int
13
14const (
15 DiscussionClosed DiscussionState = iota
16 DiscussionOpen
17 DiscussionMerged
18)
19
20func (s DiscussionState) String() string {
21 switch s {
22 case DiscussionOpen:
23 return "open"
24 case DiscussionMerged:
25 return "merged"
26 case DiscussionClosed:
27 return "closed"
28 default:
29 return "closed"
30 }
31}
32
33func (s DiscussionState) IsOpen() bool { return s == DiscussionOpen }
34func (s DiscussionState) IsMerged() bool { return s == DiscussionMerged }
35func (s DiscussionState) IsClosed() bool { return s == DiscussionClosed }
36
37// Discussion represents a discussion in a Pijul repository
38// Anyone can add patches to a discussion
39type Discussion struct {
40 // ids
41 Id int64
42 Did string
43 Rkey string
44 RepoAt syntax.ATURI
45 DiscussionId int
46
47 // content
48 Title string
49 Body string
50 TargetChannel string
51 State DiscussionState
52
53 // meta
54 Created time.Time
55 Edited *time.Time
56
57 // populated on query
58 Patches []*DiscussionPatch
59 Comments []DiscussionComment
60 Labels LabelState
61 Repo *Repo
62}
63
64const DiscussionNSID = "sh.tangled.repo.discussion"
65
66func (d *Discussion) AtUri() syntax.ATURI {
67 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", d.Did, DiscussionNSID, d.Rkey))
68}
69
70// ActivePatches returns only the patches that haven't been removed
71func (d *Discussion) ActivePatches() []*DiscussionPatch {
72 var active []*DiscussionPatch
73 for _, p := range d.Patches {
74 if p.IsActive() {
75 active = append(active, p)
76 }
77 }
78 return active
79}
80
81// Participants returns all DIDs that have participated in this discussion
82// (creator + patch pushers + commenters)
83func (d *Discussion) Participants() []string {
84 participantSet := make(map[string]struct{})
85 participants := []string{}
86
87 addParticipant := func(did string) {
88 if _, exists := participantSet[did]; !exists {
89 participantSet[did] = struct{}{}
90 participants = append(participants, did)
91 }
92 }
93
94 // Discussion creator
95 addParticipant(d.Did)
96
97 // Patch pushers
98 for _, p := range d.Patches {
99 addParticipant(p.PushedByDid)
100 }
101
102 // Commenters
103 for _, c := range d.Comments {
104 addParticipant(c.Did)
105 }
106
107 return participants
108}
109
110// CommentList returns a threaded comment list
111func (d *Discussion) CommentList() []DiscussionCommentListItem {
112 toplevel := make(map[string]*DiscussionCommentListItem)
113 var replies []*DiscussionComment
114
115 for i := range d.Comments {
116 comment := &d.Comments[i]
117 if comment.IsTopLevel() {
118 toplevel[comment.AtUri().String()] = &DiscussionCommentListItem{
119 Self: comment,
120 }
121 } else {
122 replies = append(replies, comment)
123 }
124 }
125
126 for _, r := range replies {
127 parentAt := *r.ReplyTo
128 if parent, exists := toplevel[parentAt]; exists {
129 parent.Replies = append(parent.Replies, r)
130 }
131 }
132
133 var listing []DiscussionCommentListItem
134 for _, v := range toplevel {
135 listing = append(listing, *v)
136 }
137
138 // Sort by creation time
139 sortFunc := func(a, b *DiscussionComment) bool {
140 return a.Created.Before(b.Created)
141 }
142 sort.Slice(listing, func(i, j int) bool {
143 return sortFunc(listing[i].Self, listing[j].Self)
144 })
145 for _, r := range listing {
146 sort.Slice(r.Replies, func(i, j int) bool {
147 return sortFunc(r.Replies[i], r.Replies[j])
148 })
149 }
150
151 return listing
152}
153
154// TotalComments returns the total number of comments
155func (d *Discussion) TotalComments() int {
156 return len(d.Comments)
157}
158
159// DiscussionPatch represents a patch added to a discussion
160// Key difference from PullSubmission: it has pushed_by_did
161type DiscussionPatch struct {
162 Id int64
163 DiscussionAt syntax.ATURI
164 PushedByDid string
165 PatchHash string
166 Patch string
167 Added time.Time
168 Removed *time.Time
169}
170
171// IsActive returns true if the patch hasn't been removed
172func (p *DiscussionPatch) IsActive() bool {
173 return p.Removed == nil
174}
175
176// CanRemove checks if the given user can remove this patch
177// A patch can be removed by:
178// 1. The person who pushed it
179// 2. Someone with edit permissions on the repo
180func (p *DiscussionPatch) CanRemove(userDid string, hasEditPerm bool) bool {
181 return p.PushedByDid == userDid || hasEditPerm
182}
183
184// DiscussionComment represents a comment on a discussion
185type DiscussionComment struct {
186 Id int64
187 Did string
188 Rkey string
189 DiscussionAt string
190 ReplyTo *string
191 Body string
192 Created time.Time
193 Edited *time.Time
194 Deleted *time.Time
195}
196
197const DiscussionCommentNSID = "sh.tangled.repo.discussion.comment"
198
199func (c *DiscussionComment) AtUri() syntax.ATURI {
200 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", c.Did, DiscussionCommentNSID, c.Rkey))
201}
202
203func (c *DiscussionComment) IsTopLevel() bool {
204 return c.ReplyTo == nil
205}
206
207func (c *DiscussionComment) IsReply() bool {
208 return c.ReplyTo != nil
209}
210
211// DiscussionCommentListItem represents a top-level comment with its replies
212type DiscussionCommentListItem struct {
213 Self *DiscussionComment
214 Replies []*DiscussionComment
215}
216
217// Participants returns all DIDs that participated in this comment thread
218func (item *DiscussionCommentListItem) Participants() []syntax.DID {
219 participantSet := make(map[syntax.DID]struct{})
220 participants := []syntax.DID{}
221
222 addParticipant := func(did syntax.DID) {
223 if _, exists := participantSet[did]; !exists {
224 participantSet[did] = struct{}{}
225 participants = append(participants, did)
226 }
227 }
228
229 addParticipant(syntax.DID(item.Self.Did))
230
231 for _, c := range item.Replies {
232 addParticipant(syntax.DID(c.Did))
233 }
234
235 return participants
236}
237
238// DiscussionCount holds counts for different discussion states
239type DiscussionCount struct {
240 Open int
241 Merged int
242 Closed int
243}