···7676 return7777 }78787979- issue, ok := r.Context().Value("issue").(*db.Issue)7979+ issue, ok := r.Context().Value("issue").(*models.Issue)8080 if !ok {8181 l.Error("failed to get issue")8282 rp.pages.Error404(w)···130130 return131131 }132132133133- issue, ok := r.Context().Value("issue").(*db.Issue)133133+ issue, ok := r.Context().Value("issue").(*models.Issue)134134 if !ok {135135 l.Error("failed to get issue")136136 rp.pages.Error404(w)···226226 return227227 }228228229229- issue, ok := r.Context().Value("issue").(*db.Issue)229229+ issue, ok := r.Context().Value("issue").(*models.Issue)230230 if !ok {231231 l.Error("failed to get issue")232232 rp.pages.Notice(w, noticeId, "Failed to delete issue.")···273273 return274274 }275275276276- issue, ok := r.Context().Value("issue").(*db.Issue)276276+ issue, ok := r.Context().Value("issue").(*models.Issue)277277 if !ok {278278 l.Error("failed to get issue")279279 rp.pages.Error404(w)···319319 return320320 }321321322322- issue, ok := r.Context().Value("issue").(*db.Issue)322322+ issue, ok := r.Context().Value("issue").(*models.Issue)323323 if !ok {324324 l.Error("failed to get issue")325325 rp.pages.Error404(w)···363363 return364364 }365365366366- issue, ok := r.Context().Value("issue").(*db.Issue)366366+ issue, ok := r.Context().Value("issue").(*models.Issue)367367 if !ok {368368 l.Error("failed to get issue")369369 rp.pages.Error404(w)···382382 replyTo = &replyToUri383383 }384384385385- comment := db.IssueComment{385385+ comment := models.IssueComment{386386 Did: user.Did,387387 Rkey: tid.TID(),388388 IssueAt: issue.AtUri().String(),···446446 return447447 }448448449449- issue, ok := r.Context().Value("issue").(*db.Issue)449449+ issue, ok := r.Context().Value("issue").(*models.Issue)450450 if !ok {451451 l.Error("failed to get issue")452452 rp.pages.Error404(w)···487487 return488488 }489489490490- issue, ok := r.Context().Value("issue").(*db.Issue)490490+ issue, ok := r.Context().Value("issue").(*models.Issue)491491 if !ok {492492 l.Error("failed to get issue")493493 rp.pages.Error404(w)···591591 return592592 }593593594594- issue, ok := r.Context().Value("issue").(*db.Issue)594594+ issue, ok := r.Context().Value("issue").(*models.Issue)595595 if !ok {596596 l.Error("failed to get issue")597597 rp.pages.Error404(w)···632632 return633633 }634634635635- issue, ok := r.Context().Value("issue").(*db.Issue)635635+ issue, ok := r.Context().Value("issue").(*models.Issue)636636 if !ok {637637 l.Error("failed to get issue")638638 rp.pages.Error404(w)···673673 return674674 }675675676676- issue, ok := r.Context().Value("issue").(*db.Issue)676676+ issue, ok := r.Context().Value("issue").(*models.Issue)677677 if !ok {678678 l.Error("failed to get issue")679679 rp.pages.Error404(w)···829829 RepoInfo: f.RepoInfo(user),830830 })831831 case http.MethodPost:832832- issue := &db.Issue{832832+ issue := &models.Issue{833833 RepoAt: f.RepoAt(),834834 Rkey: tid.TID(),835835 Title: r.FormValue("title"),
+194
appview/models/issue.go
···11+package models22+33+import (44+ "fmt"55+ "sort"66+ "time"77+88+ "github.com/bluesky-social/indigo/atproto/syntax"99+ "tangled.org/core/api/tangled"1010+)1111+1212+type Issue struct {1313+ Id int641414+ Did string1515+ Rkey string1616+ RepoAt syntax.ATURI1717+ IssueId int1818+ Created time.Time1919+ Edited *time.Time2020+ Deleted *time.Time2121+ Title string2222+ Body string2323+ Open bool2424+2525+ // optionally, populate this when querying for reverse mappings2626+ // like comment counts, parent repo etc.2727+ Comments []IssueComment2828+ Labels LabelState2929+ Repo *Repo3030+}3131+3232+func (i *Issue) AtUri() syntax.ATURI {3333+ return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueNSID, i.Rkey))3434+}3535+3636+func (i *Issue) AsRecord() tangled.RepoIssue {3737+ return tangled.RepoIssue{3838+ Repo: i.RepoAt.String(),3939+ Title: i.Title,4040+ Body: &i.Body,4141+ CreatedAt: i.Created.Format(time.RFC3339),4242+ }4343+}4444+4545+func (i *Issue) State() string {4646+ if i.Open {4747+ return "open"4848+ }4949+ return "closed"5050+}5151+5252+type CommentListItem struct {5353+ Self *IssueComment5454+ Replies []*IssueComment5555+}5656+5757+func (i *Issue) CommentList() []CommentListItem {5858+ // Create a map to quickly find comments by their aturi5959+ toplevel := make(map[string]*CommentListItem)6060+ var replies []*IssueComment6161+6262+ // collect top level comments into the map6363+ for _, comment := range i.Comments {6464+ if comment.IsTopLevel() {6565+ toplevel[comment.AtUri().String()] = &CommentListItem{6666+ Self: &comment,6767+ }6868+ } else {6969+ replies = append(replies, &comment)7070+ }7171+ }7272+7373+ for _, r := range replies {7474+ parentAt := *r.ReplyTo7575+ if parent, exists := toplevel[parentAt]; exists {7676+ parent.Replies = append(parent.Replies, r)7777+ }7878+ }7979+8080+ var listing []CommentListItem8181+ for _, v := range toplevel {8282+ listing = append(listing, *v)8383+ }8484+8585+ // sort everything8686+ sortFunc := func(a, b *IssueComment) bool {8787+ return a.Created.Before(b.Created)8888+ }8989+ sort.Slice(listing, func(i, j int) bool {9090+ return sortFunc(listing[i].Self, listing[j].Self)9191+ })9292+ for _, r := range listing {9393+ sort.Slice(r.Replies, func(i, j int) bool {9494+ return sortFunc(r.Replies[i], r.Replies[j])9595+ })9696+ }9797+9898+ return listing9999+}100100+101101+func (i *Issue) Participants() []string {102102+ participantSet := make(map[string]struct{})103103+ participants := []string{}104104+105105+ addParticipant := func(did string) {106106+ if _, exists := participantSet[did]; !exists {107107+ participantSet[did] = struct{}{}108108+ participants = append(participants, did)109109+ }110110+ }111111+112112+ addParticipant(i.Did)113113+114114+ for _, c := range i.Comments {115115+ addParticipant(c.Did)116116+ }117117+118118+ return participants119119+}120120+121121+func IssueFromRecord(did, rkey string, record tangled.RepoIssue) Issue {122122+ created, err := time.Parse(time.RFC3339, record.CreatedAt)123123+ if err != nil {124124+ created = time.Now()125125+ }126126+127127+ body := ""128128+ if record.Body != nil {129129+ body = *record.Body130130+ }131131+132132+ return Issue{133133+ RepoAt: syntax.ATURI(record.Repo),134134+ Did: did,135135+ Rkey: rkey,136136+ Created: created,137137+ Title: record.Title,138138+ Body: body,139139+ Open: true, // new issues are open by default140140+ }141141+}142142+143143+type IssueComment struct {144144+ Id int64145145+ Did string146146+ Rkey string147147+ IssueAt string148148+ ReplyTo *string149149+ Body string150150+ Created time.Time151151+ Edited *time.Time152152+ Deleted *time.Time153153+}154154+155155+func (i *IssueComment) AtUri() syntax.ATURI {156156+ return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueCommentNSID, i.Rkey))157157+}158158+159159+func (i *IssueComment) AsRecord() tangled.RepoIssueComment {160160+ return tangled.RepoIssueComment{161161+ Body: i.Body,162162+ Issue: i.IssueAt,163163+ CreatedAt: i.Created.Format(time.RFC3339),164164+ ReplyTo: i.ReplyTo,165165+ }166166+}167167+168168+func (i *IssueComment) IsTopLevel() bool {169169+ return i.ReplyTo == nil170170+}171171+172172+func IssueCommentFromRecord(did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) {173173+ created, err := time.Parse(time.RFC3339, record.CreatedAt)174174+ if err != nil {175175+ created = time.Now()176176+ }177177+178178+ ownerDid := did179179+180180+ if _, err = syntax.ParseATURI(record.Issue); err != nil {181181+ return nil, err182182+ }183183+184184+ comment := IssueComment{185185+ Did: ownerDid,186186+ Rkey: rkey,187187+ Body: record.Body,188188+ IssueAt: record.Issue,189189+ ReplyTo: record.ReplyTo,190190+ Created: created,191191+ }192192+193193+ return &comment, nil194194+}
···467467 return nil468468}469469470470-func (s *State) addIssueItems(ctx context.Context, feed *feeds.Feed, issues []*db.Issue, author *feeds.Author) error {470470+func (s *State) addIssueItems(ctx context.Context, feed *feeds.Feed, issues []*models.Issue, author *feeds.Author) error {471471 for _, issue := range issues {472472 owner, err := s.idResolver.ResolveIdent(ctx, issue.Repo.Did)473473 if err != nil {···499499 }500500}501501502502-func (s *State) createIssueItem(issue *db.Issue, owner *identity.Identity, author *feeds.Author) *feeds.Item {502502+func (s *State) createIssueItem(issue *models.Issue, owner *identity.Identity, author *feeds.Author) *feeds.Item {503503 return &feeds.Item{504504 Title: fmt.Sprintf("%s created issue '%s' in @%s/%s", author.Name, issue.Title, owner.Handle, issue.Repo.Name),505505 Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/issues/%d", s.config.Core.AppviewHost, owner.Handle, issue.Repo.Name, issue.IssueId), Type: "text/html", Rel: "alternate"},
+3-2
appview/validator/issue.go
···55 "strings"6677 "tangled.org/core/appview/db"88+ "tangled.org/core/appview/models"89)9101010-func (v *Validator) ValidateIssueComment(comment *db.IssueComment) error {1111+func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error {1112 // if comments have parents, only ingest ones that are 1 level deep1213 if comment.ReplyTo != nil {1314 parents, err := db.GetIssueComments(v.db, db.FilterEq("at_uri", *comment.ReplyTo))···3332 return nil3433}35343636-func (v *Validator) ValidateIssue(issue *db.Issue) error {3535+func (v *Validator) ValidateIssue(issue *models.Issue) error {3736 if issue.Title == "" {3837 return fmt.Errorf("issue title is empty")3938 }