From d00beee7cb2c3988736aecca41ab1500d8093d23 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Wed, 2 Jul 2025 11:30:39 +0900 Subject: [PATCH] appview: add reaction for issues/PRs (close #65) Change-Id: uoymosxlmxvlrqkrnrsltltzvxpswnyq Add `sh.tangled.feed.reaction` lexicon and UI to *react* to issues/PRs close #65 close #113 Signed-off-by: Seongmin Lee --- api/tangled/cbor_gen.go | 198 ++++++++++++++++++ api/tangled/feedreaction.go | 24 +++ appview/db/db.go | 10 + appview/db/issues.go | 4 +- appview/db/reaction.go | 141 +++++++++++++ appview/issues/issues.go | 16 ++ appview/pages/pages.go | 19 ++ appview/pages/templates/layouts/repobase.html | 2 +- .../templates/repo/fragments/reaction.html | 34 +++ .../repo/fragments/reactionsPopUp.html | 30 +++ .../pages/templates/repo/issues/issue.html | 14 ++ .../repo/pulls/fragments/pullHeader.html | 14 ++ appview/pulls/pulls.go | 15 ++ appview/state/reaction.go | 126 +++++++++++ appview/state/router.go | 5 + cmd/gen.go | 1 + lexicons/feed/reaction.json | 34 +++ 17 files changed, 684 insertions(+), 3 deletions(-) create mode 100644 api/tangled/feedreaction.go create mode 100644 appview/db/reaction.go create mode 100644 appview/pages/templates/repo/fragments/reaction.html create mode 100644 appview/pages/templates/repo/fragments/reactionsPopUp.html create mode 100644 appview/state/reaction.go create mode 100644 lexicons/feed/reaction.json diff --git a/api/tangled/cbor_gen.go b/api/tangled/cbor_gen.go index 00cd563..ac74d47 100644 --- a/api/tangled/cbor_gen.go +++ b/api/tangled/cbor_gen.go @@ -504,6 +504,204 @@ func (t *ActorProfile) UnmarshalCBOR(r io.Reader) (err error) { return nil } +func (t *FeedReaction) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{164}); err != nil { + return err + } + + // t.LexiconTypeID (string) (string) + if len("$type") > 1000000 { + return xerrors.Errorf("Value in field \"$type\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { + return err + } + if _, err := cw.WriteString(string("$type")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.feed.reaction"))); err != nil { + return err + } + if _, err := cw.WriteString(string("sh.tangled.feed.reaction")); err != nil { + return err + } + + // t.Subject (string) (string) + if len("subject") > 1000000 { + return xerrors.Errorf("Value in field \"subject\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subject"))); err != nil { + return err + } + if _, err := cw.WriteString(string("subject")); err != nil { + return err + } + + if len(t.Subject) > 1000000 { + return xerrors.Errorf("Value in field t.Subject was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Subject))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Subject)); err != nil { + return err + } + + // t.Reaction (string) (string) + if len("reaction") > 1000000 { + return xerrors.Errorf("Value in field \"reaction\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("reaction"))); err != nil { + return err + } + if _, err := cw.WriteString(string("reaction")); err != nil { + return err + } + + if len(t.Reaction) > 1000000 { + return xerrors.Errorf("Value in field t.Reaction was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Reaction))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Reaction)); err != nil { + return err + } + + // t.CreatedAt (string) (string) + if len("createdAt") > 1000000 { + return xerrors.Errorf("Value in field \"createdAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("createdAt")); err != nil { + return err + } + + if len(t.CreatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.CreatedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { + return err + } + return nil +} + +func (t *FeedReaction) UnmarshalCBOR(r io.Reader) (err error) { + *t = FeedReaction{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("FeedReaction: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 9) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.Subject (string) (string) + case "subject": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Subject = string(sval) + } + // t.Reaction (string) (string) + case "reaction": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Reaction = string(sval) + } + // t.CreatedAt (string) (string) + case "createdAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.CreatedAt = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} func (t *FeedStar) MarshalCBOR(w io.Writer) error { if t == nil { _, err := w.Write(cbg.CborNull) diff --git a/api/tangled/feedreaction.go b/api/tangled/feedreaction.go new file mode 100644 index 0000000..df300b4 --- /dev/null +++ b/api/tangled/feedreaction.go @@ -0,0 +1,24 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package tangled + +// schema: sh.tangled.feed.reaction + +import ( + "github.com/bluesky-social/indigo/lex/util" +) + +const ( + FeedReactionNSID = "sh.tangled.feed.reaction" +) + +func init() { + util.RegisterType("sh.tangled.feed.reaction", &FeedReaction{}) +} // +// RECORDTYPE: FeedReaction +type FeedReaction struct { + LexiconTypeID string `json:"$type,const=sh.tangled.feed.reaction" cborgen:"$type,const=sh.tangled.feed.reaction"` + CreatedAt string `json:"createdAt" cborgen:"createdAt"` + Reaction string `json:"reaction" cborgen:"reaction"` + Subject string `json:"subject" cborgen:"subject"` +} diff --git a/appview/db/db.go b/appview/db/db.go index b2f9dff..df9382f 100644 --- a/appview/db/db.go +++ b/appview/db/db.go @@ -199,6 +199,16 @@ func Make(dbPath string) (*DB, error) { unique(starred_by_did, repo_at) ); + create table if not exists reactions ( + id integer primary key autoincrement, + reacted_by_did text not null, + thread_at text not null, + kind text not null, + rkey text not null, + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + unique(reacted_by_did, thread_at, kind) + ); + create table if not exists emails ( id integer primary key autoincrement, did text not null, diff --git a/appview/db/issues.go b/appview/db/issues.go index fe5b441..3564727 100644 --- a/appview/db/issues.go +++ b/appview/db/issues.go @@ -277,12 +277,12 @@ func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) { } func GetIssueWithComments(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) { - query := `select owner_did, issue_id, created, title, body, open from issues where repo_at = ? and issue_id = ?` + query := `select owner_did, issue_id, created, title, body, open, issue_at from issues where repo_at = ? and issue_id = ?` row := e.QueryRow(query, repoAt, issueId) var issue Issue var createdAt string - err := row.Scan(&issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open) + err := row.Scan(&issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &issue.IssueAt) if err != nil { return nil, nil, err } diff --git a/appview/db/reaction.go b/appview/db/reaction.go new file mode 100644 index 0000000..81b854c --- /dev/null +++ b/appview/db/reaction.go @@ -0,0 +1,141 @@ +package db + +import ( + "log" + "time" + + "github.com/bluesky-social/indigo/atproto/syntax" +) + +type ReactionKind string + +const ( + Like ReactionKind = "👍" + Unlike = "👎" + Laugh = "😆" + Celebration = "🎉" + Confused = "🫤" + Heart = "❤️" + Rocket = "🚀" + Eyes = "👀" +) + +func (rk ReactionKind) String() string { + return string(rk) +} + +var OrderedReactionKinds = []ReactionKind{ + Like, + Unlike, + Laugh, + Celebration, + Confused, + Heart, + Rocket, + Eyes, +} + +func ParseReactionKind(raw string) (ReactionKind, bool) { + k, ok := (map[string]ReactionKind{ + "👍": Like, + "👎": Unlike, + "😆": Laugh, + "🎉": Celebration, + "🫤": Confused, + "❤️": Heart, + "🚀": Rocket, + "👀": Eyes, + })[raw] + return k, ok +} + +type Reaction struct { + ReactedByDid string + ThreadAt syntax.ATURI + Created time.Time + Rkey string + Kind ReactionKind +} + +func AddReaction(e Execer, reactedByDid string, threadAt syntax.ATURI, kind ReactionKind, rkey string) error { + query := `insert or ignore into reactions (reacted_by_did, thread_at, kind, rkey) values (?, ?, ?, ?)` + _, err := e.Exec(query, reactedByDid, threadAt, kind, rkey) + return err +} + +// Get a reaction record +func GetReaction(e Execer, reactedByDid string, threadAt syntax.ATURI, kind ReactionKind) (*Reaction, error) { + query := ` + select reacted_by_did, thread_at, created, rkey + from reactions + where reacted_by_did = ? and thread_at = ? and kind = ?` + row := e.QueryRow(query, reactedByDid, threadAt, kind) + + var reaction Reaction + var created string + err := row.Scan(&reaction.ReactedByDid, &reaction.ThreadAt, &created, &reaction.Rkey) + if err != nil { + return nil, err + } + + createdAtTime, err := time.Parse(time.RFC3339, created) + if err != nil { + log.Println("unable to determine followed at time") + reaction.Created = time.Now() + } else { + reaction.Created = createdAtTime + } + + return &reaction, nil +} + +// Remove a reaction +func DeleteReaction(e Execer, reactedByDid string, threadAt syntax.ATURI, kind ReactionKind) error { + _, err := e.Exec(`delete from reactions where reacted_by_did = ? and thread_at = ? and kind = ?`, reactedByDid, threadAt, kind) + return err +} + +// Remove a reaction +func DeleteReactionByRkey(e Execer, reactedByDid string, rkey string) error { + _, err := e.Exec(`delete from reactions where reacted_by_did = ? and rkey = ?`, reactedByDid, rkey) + return err +} + +func GetReactionCount(e Execer, threadAt syntax.ATURI, kind ReactionKind) (int, error) { + count := 0 + err := e.QueryRow( + `select count(reacted_by_did) from reactions where thread_at = ? and kind = ?`, threadAt, kind).Scan(&count) + if err != nil { + return 0, err + } + return count, nil +} + +func GetReactionCountMap(e Execer, threadAt syntax.ATURI) (map[ReactionKind]int, error) { + countMap := map[ReactionKind]int{} + for _, kind := range OrderedReactionKinds { + count, err := GetReactionCount(e, threadAt, kind) + if err != nil { + return map[ReactionKind]int{}, nil + } + countMap[kind] = count + } + return countMap, nil +} + +func GetReactionStatus(e Execer, userDid string, threadAt syntax.ATURI, kind ReactionKind) bool { + if _, err := GetReaction(e, userDid, threadAt, kind); err != nil { + return false + } else { + return true + } +} + +func GetReactionStatusMap(e Execer, userDid string, threadAt syntax.ATURI) map[ReactionKind]bool { + statusMap := map[ReactionKind]bool{} + for _, kind := range OrderedReactionKinds { + count := GetReactionStatus(e, userDid, threadAt, kind) + statusMap[kind] = count + } + return statusMap +} diff --git a/appview/issues/issues.go b/appview/issues/issues.go index fb227d3..4ecbd10 100644 --- a/appview/issues/issues.go +++ b/appview/issues/issues.go @@ -11,6 +11,7 @@ import ( comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/data" + "github.com/bluesky-social/indigo/atproto/syntax" lexutil "github.com/bluesky-social/indigo/lex/util" "github.com/go-chi/chi/v5" "github.com/posthog/posthog-go" @@ -79,6 +80,17 @@ func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { return } + reactionCountMap, err := db.GetReactionCountMap(rp.db, syntax.ATURI(issue.IssueAt)) + if err != nil { + log.Println("failed to get issue reactions") + rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") + } + + userReactions := map[db.ReactionKind]bool{} + if user != nil { + userReactions = db.GetReactionStatusMap(rp.db, user.Did, syntax.ATURI(issue.IssueAt)) + } + issueOwnerIdent, err := rp.idResolver.ResolveIdent(r.Context(), issue.OwnerDid) if err != nil { log.Println("failed to resolve issue owner", err) @@ -106,6 +118,10 @@ func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { IssueOwnerHandle: issueOwnerIdent.Handle.String(), DidHandleMap: didHandleMap, + + OrderedReactionKinds: db.OrderedReactionKinds, + Reactions: reactionCountMap, + UserReacted: userReactions, }) } diff --git a/appview/pages/pages.go b/appview/pages/pages.go index a7e7398..8922c04 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -690,9 +690,24 @@ type RepoSingleIssueParams struct { IssueOwnerHandle string DidHandleMap map[string]string + OrderedReactionKinds []db.ReactionKind + Reactions map[db.ReactionKind]int + UserReacted map[db.ReactionKind]bool + State string } +type ThreadReactionFragmentParams struct { + ThreadAt syntax.ATURI + Kind db.ReactionKind + Count int + IsReacted bool +} + +func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { + return p.executePlain("repo/fragments/reaction", w, params) +} + func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { params.Active = "issues" if params.Issue.Open { @@ -798,6 +813,10 @@ type RepoSinglePullParams struct { MergeCheck types.MergeCheckResponse ResubmitCheck ResubmitResult Pipelines map[string]db.Pipeline + + OrderedReactionKinds []db.ReactionKind + Reactions map[db.ReactionKind]int + UserReacted map[db.ReactionKind]bool } func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { diff --git a/appview/pages/templates/layouts/repobase.html b/appview/pages/templates/layouts/repobase.html index 940df80..a223495 100644 --- a/appview/pages/templates/layouts/repobase.html +++ b/appview/pages/templates/layouts/repobase.html @@ -64,7 +64,7 @@
{{ block "repoContent" . }}{{ end }}
diff --git a/appview/pages/templates/repo/fragments/reaction.html b/appview/pages/templates/repo/fragments/reaction.html new file mode 100644 index 0000000..c9d032f --- /dev/null +++ b/appview/pages/templates/repo/fragments/reaction.html @@ -0,0 +1,34 @@ +{{ define "repo/fragments/reaction" }} + +{{ end }} diff --git a/appview/pages/templates/repo/fragments/reactionsPopUp.html b/appview/pages/templates/repo/fragments/reactionsPopUp.html new file mode 100644 index 0000000..1464d3f --- /dev/null +++ b/appview/pages/templates/repo/fragments/reactionsPopUp.html @@ -0,0 +1,30 @@ +{{ define "repo/fragments/reactionsPopUp" }} +
+ + {{ i "smile" "size-4" }} + +
+ {{ range $kind := . }} + + {{ end }} +
+
+{{ end }} diff --git a/appview/pages/templates/repo/issues/issue.html b/appview/pages/templates/repo/issues/issue.html index f26a536..c2e916b 100644 --- a/appview/pages/templates/repo/issues/issue.html +++ b/appview/pages/templates/repo/issues/issue.html @@ -46,6 +46,20 @@ {{ .Issue.Body | markdown }} {{ end }} + +
+ {{ template "repo/fragments/reactionsPopUp" .OrderedReactionKinds }} + {{ range $kind := .OrderedReactionKinds }} + {{ + template "repo/fragments/reaction" + (dict + "Kind" $kind + "Count" (index $.Reactions $kind) + "IsReacted" (index $.UserReacted $kind) + "ThreadAt" $.Issue.IssueAt) + }} + {{ end }} +
{{ end }} diff --git a/appview/pages/templates/repo/pulls/fragments/pullHeader.html b/appview/pages/templates/repo/pulls/fragments/pullHeader.html index 24cb25a..87f49b5 100644 --- a/appview/pages/templates/repo/pulls/fragments/pullHeader.html +++ b/appview/pages/templates/repo/pulls/fragments/pullHeader.html @@ -61,6 +61,20 @@ {{ .Pull.Body | markdown }} {{ end }} + +
+ {{ template "repo/fragments/reactionsPopUp" .OrderedReactionKinds }} + {{ range $kind := .OrderedReactionKinds }} + {{ + template "repo/fragments/reaction" + (dict + "Kind" $kind + "Count" (index $.Reactions $kind) + "IsReacted" (index $.UserReacted $kind) + "ThreadAt" $.Pull.PullAt) + }} + {{ end }} +
diff --git a/appview/pulls/pulls.go b/appview/pulls/pulls.go index 37e82c8..5b30412 100644 --- a/appview/pulls/pulls.go +++ b/appview/pulls/pulls.go @@ -198,6 +198,17 @@ func (s *Pulls) RepoSinglePull(w http.ResponseWriter, r *http.Request) { m[p.Sha] = p } + reactionCountMap, err := db.GetReactionCountMap(s.db, pull.PullAt()) + if err != nil { + log.Println("failed to get pull reactions") + s.pages.Notice(w, "pulls", "Failed to load pull. Try again later.") + } + + userReactions := map[db.ReactionKind]bool{} + if user != nil { + userReactions = db.GetReactionStatusMap(s.db, user.Did, pull.PullAt()) + } + s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ LoggedInUser: user, RepoInfo: repoInfo, @@ -208,6 +219,10 @@ func (s *Pulls) RepoSinglePull(w http.ResponseWriter, r *http.Request) { MergeCheck: mergeCheckResponse, ResubmitCheck: resubmitResult, Pipelines: m, + + OrderedReactionKinds: db.OrderedReactionKinds, + Reactions: reactionCountMap, + UserReacted: userReactions, }) } diff --git a/appview/state/reaction.go b/appview/state/reaction.go new file mode 100644 index 0000000..e5e633a --- /dev/null +++ b/appview/state/reaction.go @@ -0,0 +1,126 @@ +package state + +import ( + "log" + "net/http" + "time" + + comatproto "github.com/bluesky-social/indigo/api/atproto" + "github.com/bluesky-social/indigo/atproto/syntax" + + lexutil "github.com/bluesky-social/indigo/lex/util" + "tangled.sh/tangled.sh/core/api/tangled" + "tangled.sh/tangled.sh/core/appview" + "tangled.sh/tangled.sh/core/appview/db" + "tangled.sh/tangled.sh/core/appview/pages" +) + +func (s *State) React(w http.ResponseWriter, r *http.Request) { + currentUser := s.oauth.GetUser(r) + + subject := r.URL.Query().Get("subject") + if subject == "" { + log.Println("invalid form") + return + } + + subjectUri, err := syntax.ParseATURI(subject) + if err != nil { + log.Println("invalid form") + return + } + + reactionKind, ok := db.ParseReactionKind(r.URL.Query().Get("kind")) + if !ok { + log.Println("invalid reaction kind") + return + } + + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to authorize client", err) + return + } + + switch r.Method { + case http.MethodPost: + createdAt := time.Now().Format(time.RFC3339) + rkey := appview.TID() + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ + Collection: tangled.FeedReactionNSID, + Repo: currentUser.Did, + Rkey: rkey, + Record: &lexutil.LexiconTypeDecoder{ + Val: &tangled.FeedReaction{ + Subject: subjectUri.String(), + Reaction: reactionKind.String(), + CreatedAt: createdAt, + }, + }, + }) + if err != nil { + log.Println("failed to create atproto record", err) + return + } + + err = db.AddReaction(s.db, currentUser.Did, subjectUri, reactionKind, rkey) + if err != nil { + log.Println("failed to react", err) + return + } + + count, err := db.GetReactionCount(s.db, subjectUri, reactionKind) + if err != nil { + log.Println("failed to get reaction count for ", subjectUri) + } + + log.Println("created atproto record: ", resp.Uri) + + s.pages.ThreadReactionFragment(w, pages.ThreadReactionFragmentParams{ + ThreadAt: subjectUri, + Kind: reactionKind, + Count: count, + IsReacted: true, + }) + + return + case http.MethodDelete: + reaction, err := db.GetReaction(s.db, currentUser.Did, subjectUri, reactionKind) + if err != nil { + log.Println("failed to get reaction relationship for", currentUser.Did, subjectUri) + return + } + + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ + Collection: tangled.FeedReactionNSID, + Repo: currentUser.Did, + Rkey: reaction.Rkey, + }) + + if err != nil { + log.Println("failed to remove reaction") + return + } + + err = db.DeleteReactionByRkey(s.db, currentUser.Did, reaction.Rkey) + if err != nil { + log.Println("failed to delete reaction from DB") + // this is not an issue, the firehose event might have already done this + } + + count, err := db.GetReactionCount(s.db, subjectUri, reactionKind) + if err != nil { + log.Println("failed to get reaction count for ", subjectUri) + return + } + + s.pages.ThreadReactionFragment(w, pages.ThreadReactionFragmentParams{ + ThreadAt: subjectUri, + Kind: reactionKind, + Count: count, + IsReacted: false, + }) + + return + } +} diff --git a/appview/state/router.go b/appview/state/router.go index da986e3..58efc91 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -137,6 +137,11 @@ func (s *State) StandardRouter(mw *middleware.Middleware) http.Handler { r.Delete("/", s.Star) }) + r.With(middleware.AuthMiddleware(s.oauth)).Route("/react", func(r chi.Router) { + r.Post("/", s.React) + r.Delete("/", s.React) + }) + r.Route("/profile", func(r chi.Router) { r.Use(middleware.AuthMiddleware(s.oauth)) r.Get("/edit-bio", s.EditBioFragment) diff --git a/cmd/gen.go b/cmd/gen.go index c04a035..b36fe98 100644 --- a/cmd/gen.go +++ b/cmd/gen.go @@ -15,6 +15,7 @@ func main() { "api/tangled/cbor_gen.go", "tangled", tangled.ActorProfile{}, + tangled.FeedReaction{}, tangled.FeedStar{}, tangled.GitRefUpdate{}, tangled.GitRefUpdate_Meta{}, diff --git a/lexicons/feed/reaction.json b/lexicons/feed/reaction.json new file mode 100644 index 0000000..ba03185 --- /dev/null +++ b/lexicons/feed/reaction.json @@ -0,0 +1,34 @@ +{ + "lexicon": 1, + "id": "sh.tangled.feed.reaction", + "needsCbor": true, + "needsType": true, + "defs": { + "main": { + "type": "record", + "key": "tid", + "record": { + "type": "object", + "required": [ + "subject", + "reaction", + "createdAt" + ], + "properties": { + "subject": { + "type": "string", + "format": "at-uri" + }, + "reaction": { + "type": "string", + "enum": [ "👍", "👎", "😆", "🎉", "🫤", "❤️", "🚀", "👀" ] + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + } + } +} -- 2.43.0