forked from tangled.org/core
this repo has no description

Compare changes

Choose any two refs to compare.

Changed files
+740 -43
api
appview
cmd
docs
spindle
lexicons
spindle
engine
+198
api/tangled/cbor_gen.go
··· 504 504 505 505 return nil 506 506 } 507 + func (t *FeedReaction) MarshalCBOR(w io.Writer) error { 508 + if t == nil { 509 + _, err := w.Write(cbg.CborNull) 510 + return err 511 + } 512 + 513 + cw := cbg.NewCborWriter(w) 514 + 515 + if _, err := cw.Write([]byte{164}); err != nil { 516 + return err 517 + } 518 + 519 + // t.LexiconTypeID (string) (string) 520 + if len("$type") > 1000000 { 521 + return xerrors.Errorf("Value in field \"$type\" was too long") 522 + } 523 + 524 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 525 + return err 526 + } 527 + if _, err := cw.WriteString(string("$type")); err != nil { 528 + return err 529 + } 530 + 531 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.feed.reaction"))); err != nil { 532 + return err 533 + } 534 + if _, err := cw.WriteString(string("sh.tangled.feed.reaction")); err != nil { 535 + return err 536 + } 537 + 538 + // t.Subject (string) (string) 539 + if len("subject") > 1000000 { 540 + return xerrors.Errorf("Value in field \"subject\" was too long") 541 + } 542 + 543 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subject"))); err != nil { 544 + return err 545 + } 546 + if _, err := cw.WriteString(string("subject")); err != nil { 547 + return err 548 + } 549 + 550 + if len(t.Subject) > 1000000 { 551 + return xerrors.Errorf("Value in field t.Subject was too long") 552 + } 553 + 554 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Subject))); err != nil { 555 + return err 556 + } 557 + if _, err := cw.WriteString(string(t.Subject)); err != nil { 558 + return err 559 + } 560 + 561 + // t.Reaction (string) (string) 562 + if len("reaction") > 1000000 { 563 + return xerrors.Errorf("Value in field \"reaction\" was too long") 564 + } 565 + 566 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("reaction"))); err != nil { 567 + return err 568 + } 569 + if _, err := cw.WriteString(string("reaction")); err != nil { 570 + return err 571 + } 572 + 573 + if len(t.Reaction) > 1000000 { 574 + return xerrors.Errorf("Value in field t.Reaction was too long") 575 + } 576 + 577 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Reaction))); err != nil { 578 + return err 579 + } 580 + if _, err := cw.WriteString(string(t.Reaction)); err != nil { 581 + return err 582 + } 583 + 584 + // t.CreatedAt (string) (string) 585 + if len("createdAt") > 1000000 { 586 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 587 + } 588 + 589 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 590 + return err 591 + } 592 + if _, err := cw.WriteString(string("createdAt")); err != nil { 593 + return err 594 + } 595 + 596 + if len(t.CreatedAt) > 1000000 { 597 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 598 + } 599 + 600 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 601 + return err 602 + } 603 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 604 + return err 605 + } 606 + return nil 607 + } 608 + 609 + func (t *FeedReaction) UnmarshalCBOR(r io.Reader) (err error) { 610 + *t = FeedReaction{} 611 + 612 + cr := cbg.NewCborReader(r) 613 + 614 + maj, extra, err := cr.ReadHeader() 615 + if err != nil { 616 + return err 617 + } 618 + defer func() { 619 + if err == io.EOF { 620 + err = io.ErrUnexpectedEOF 621 + } 622 + }() 623 + 624 + if maj != cbg.MajMap { 625 + return fmt.Errorf("cbor input should be of type map") 626 + } 627 + 628 + if extra > cbg.MaxLength { 629 + return fmt.Errorf("FeedReaction: map struct too large (%d)", extra) 630 + } 631 + 632 + n := extra 633 + 634 + nameBuf := make([]byte, 9) 635 + for i := uint64(0); i < n; i++ { 636 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 637 + if err != nil { 638 + return err 639 + } 640 + 641 + if !ok { 642 + // Field doesn't exist on this type, so ignore it 643 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 644 + return err 645 + } 646 + continue 647 + } 648 + 649 + switch string(nameBuf[:nameLen]) { 650 + // t.LexiconTypeID (string) (string) 651 + case "$type": 652 + 653 + { 654 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 655 + if err != nil { 656 + return err 657 + } 658 + 659 + t.LexiconTypeID = string(sval) 660 + } 661 + // t.Subject (string) (string) 662 + case "subject": 663 + 664 + { 665 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 666 + if err != nil { 667 + return err 668 + } 669 + 670 + t.Subject = string(sval) 671 + } 672 + // t.Reaction (string) (string) 673 + case "reaction": 674 + 675 + { 676 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 677 + if err != nil { 678 + return err 679 + } 680 + 681 + t.Reaction = string(sval) 682 + } 683 + // t.CreatedAt (string) (string) 684 + case "createdAt": 685 + 686 + { 687 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 688 + if err != nil { 689 + return err 690 + } 691 + 692 + t.CreatedAt = string(sval) 693 + } 694 + 695 + default: 696 + // Field doesn't exist on this type, so ignore it 697 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 698 + return err 699 + } 700 + } 701 + } 702 + 703 + return nil 704 + } 507 705 func (t *FeedStar) MarshalCBOR(w io.Writer) error { 508 706 if t == nil { 509 707 _, err := w.Write(cbg.CborNull)
+24
api/tangled/feedreaction.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package tangled 4 + 5 + // schema: sh.tangled.feed.reaction 6 + 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 11 + const ( 12 + FeedReactionNSID = "sh.tangled.feed.reaction" 13 + ) 14 + 15 + func init() { 16 + util.RegisterType("sh.tangled.feed.reaction", &FeedReaction{}) 17 + } // 18 + // RECORDTYPE: FeedReaction 19 + type FeedReaction struct { 20 + LexiconTypeID string `json:"$type,const=sh.tangled.feed.reaction" cborgen:"$type,const=sh.tangled.feed.reaction"` 21 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 22 + Reaction string `json:"reaction" cborgen:"reaction"` 23 + Subject string `json:"subject" cborgen:"subject"` 24 + }
+10
appview/db/db.go
··· 199 199 unique(starred_by_did, repo_at) 200 200 ); 201 201 202 + create table if not exists reactions ( 203 + id integer primary key autoincrement, 204 + reacted_by_did text not null, 205 + thread_at text not null, 206 + kind text not null, 207 + rkey text not null, 208 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 209 + unique(reacted_by_did, thread_at, kind) 210 + ); 211 + 202 212 create table if not exists emails ( 203 213 id integer primary key autoincrement, 204 214 did text not null,
+2 -2
appview/db/issues.go
··· 277 277 } 278 278 279 279 func GetIssueWithComments(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) { 280 - query := `select owner_did, issue_id, created, title, body, open from issues where repo_at = ? and issue_id = ?` 280 + query := `select owner_did, issue_id, created, title, body, open, issue_at from issues where repo_at = ? and issue_id = ?` 281 281 row := e.QueryRow(query, repoAt, issueId) 282 282 283 283 var issue Issue 284 284 var createdAt string 285 - err := row.Scan(&issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open) 285 + err := row.Scan(&issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &issue.IssueAt) 286 286 if err != nil { 287 287 return nil, nil, err 288 288 }
+141
appview/db/reaction.go
··· 1 + package db 2 + 3 + import ( 4 + "log" 5 + "time" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + ) 9 + 10 + type ReactionKind string 11 + 12 + const ( 13 + Like ReactionKind = "๐Ÿ‘" 14 + Unlike = "๐Ÿ‘Ž" 15 + Laugh = "๐Ÿ˜†" 16 + Celebration = "๐ŸŽ‰" 17 + Confused = "๐Ÿซค" 18 + Heart = "โค๏ธ" 19 + Rocket = "๐Ÿš€" 20 + Eyes = "๐Ÿ‘€" 21 + ) 22 + 23 + func (rk ReactionKind) String() string { 24 + return string(rk) 25 + } 26 + 27 + var OrderedReactionKinds = []ReactionKind{ 28 + Like, 29 + Unlike, 30 + Laugh, 31 + Celebration, 32 + Confused, 33 + Heart, 34 + Rocket, 35 + Eyes, 36 + } 37 + 38 + func ParseReactionKind(raw string) (ReactionKind, bool) { 39 + k, ok := (map[string]ReactionKind{ 40 + "๐Ÿ‘": Like, 41 + "๐Ÿ‘Ž": Unlike, 42 + "๐Ÿ˜†": Laugh, 43 + "๐ŸŽ‰": Celebration, 44 + "๐Ÿซค": Confused, 45 + "โค๏ธ": Heart, 46 + "๐Ÿš€": Rocket, 47 + "๐Ÿ‘€": Eyes, 48 + })[raw] 49 + return k, ok 50 + } 51 + 52 + type Reaction struct { 53 + ReactedByDid string 54 + ThreadAt syntax.ATURI 55 + Created time.Time 56 + Rkey string 57 + Kind ReactionKind 58 + } 59 + 60 + func AddReaction(e Execer, reactedByDid string, threadAt syntax.ATURI, kind ReactionKind, rkey string) error { 61 + query := `insert or ignore into reactions (reacted_by_did, thread_at, kind, rkey) values (?, ?, ?, ?)` 62 + _, err := e.Exec(query, reactedByDid, threadAt, kind, rkey) 63 + return err 64 + } 65 + 66 + // Get a reaction record 67 + func GetReaction(e Execer, reactedByDid string, threadAt syntax.ATURI, kind ReactionKind) (*Reaction, error) { 68 + query := ` 69 + select reacted_by_did, thread_at, created, rkey 70 + from reactions 71 + where reacted_by_did = ? and thread_at = ? and kind = ?` 72 + row := e.QueryRow(query, reactedByDid, threadAt, kind) 73 + 74 + var reaction Reaction 75 + var created string 76 + err := row.Scan(&reaction.ReactedByDid, &reaction.ThreadAt, &created, &reaction.Rkey) 77 + if err != nil { 78 + return nil, err 79 + } 80 + 81 + createdAtTime, err := time.Parse(time.RFC3339, created) 82 + if err != nil { 83 + log.Println("unable to determine followed at time") 84 + reaction.Created = time.Now() 85 + } else { 86 + reaction.Created = createdAtTime 87 + } 88 + 89 + return &reaction, nil 90 + } 91 + 92 + // Remove a reaction 93 + func DeleteReaction(e Execer, reactedByDid string, threadAt syntax.ATURI, kind ReactionKind) error { 94 + _, err := e.Exec(`delete from reactions where reacted_by_did = ? and thread_at = ? and kind = ?`, reactedByDid, threadAt, kind) 95 + return err 96 + } 97 + 98 + // Remove a reaction 99 + func DeleteReactionByRkey(e Execer, reactedByDid string, rkey string) error { 100 + _, err := e.Exec(`delete from reactions where reacted_by_did = ? and rkey = ?`, reactedByDid, rkey) 101 + return err 102 + } 103 + 104 + func GetReactionCount(e Execer, threadAt syntax.ATURI, kind ReactionKind) (int, error) { 105 + count := 0 106 + err := e.QueryRow( 107 + `select count(reacted_by_did) from reactions where thread_at = ? and kind = ?`, threadAt, kind).Scan(&count) 108 + if err != nil { 109 + return 0, err 110 + } 111 + return count, nil 112 + } 113 + 114 + func GetReactionCountMap(e Execer, threadAt syntax.ATURI) (map[ReactionKind]int, error) { 115 + countMap := map[ReactionKind]int{} 116 + for _, kind := range OrderedReactionKinds { 117 + count, err := GetReactionCount(e, threadAt, kind) 118 + if err != nil { 119 + return map[ReactionKind]int{}, nil 120 + } 121 + countMap[kind] = count 122 + } 123 + return countMap, nil 124 + } 125 + 126 + func GetReactionStatus(e Execer, userDid string, threadAt syntax.ATURI, kind ReactionKind) bool { 127 + if _, err := GetReaction(e, userDid, threadAt, kind); err != nil { 128 + return false 129 + } else { 130 + return true 131 + } 132 + } 133 + 134 + func GetReactionStatusMap(e Execer, userDid string, threadAt syntax.ATURI) map[ReactionKind]bool { 135 + statusMap := map[ReactionKind]bool{} 136 + for _, kind := range OrderedReactionKinds { 137 + count := GetReactionStatus(e, userDid, threadAt, kind) 138 + statusMap[kind] = count 139 + } 140 + return statusMap 141 + }
+16
appview/issues/issues.go
··· 11 11 12 12 comatproto "github.com/bluesky-social/indigo/api/atproto" 13 13 "github.com/bluesky-social/indigo/atproto/data" 14 + "github.com/bluesky-social/indigo/atproto/syntax" 14 15 lexutil "github.com/bluesky-social/indigo/lex/util" 15 16 "github.com/go-chi/chi/v5" 16 17 "github.com/posthog/posthog-go" ··· 79 80 return 80 81 } 81 82 83 + reactionCountMap, err := db.GetReactionCountMap(rp.db, syntax.ATURI(issue.IssueAt)) 84 + if err != nil { 85 + log.Println("failed to get issue reactions") 86 + rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 87 + } 88 + 89 + userReactions := map[db.ReactionKind]bool{} 90 + if user != nil { 91 + userReactions = db.GetReactionStatusMap(rp.db, user.Did, syntax.ATURI(issue.IssueAt)) 92 + } 93 + 82 94 issueOwnerIdent, err := rp.idResolver.ResolveIdent(r.Context(), issue.OwnerDid) 83 95 if err != nil { 84 96 log.Println("failed to resolve issue owner", err) ··· 106 118 107 119 IssueOwnerHandle: issueOwnerIdent.Handle.String(), 108 120 DidHandleMap: didHandleMap, 121 + 122 + OrderedReactionKinds: db.OrderedReactionKinds, 123 + Reactions: reactionCountMap, 124 + UserReacted: userReactions, 109 125 }) 110 126 111 127 }
+35 -14
appview/pages/pages.go
··· 690 690 IssueOwnerHandle string 691 691 DidHandleMap map[string]string 692 692 693 + OrderedReactionKinds []db.ReactionKind 694 + Reactions map[db.ReactionKind]int 695 + UserReacted map[db.ReactionKind]bool 696 + 693 697 State string 698 + } 699 + 700 + type ThreadReactionFragmentParams struct { 701 + ThreadAt syntax.ATURI 702 + Kind db.ReactionKind 703 + Count int 704 + IsReacted bool 705 + } 706 + 707 + func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 708 + return p.executePlain("repo/fragments/reaction", w, params) 694 709 } 695 710 696 711 func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { ··· 798 813 MergeCheck types.MergeCheckResponse 799 814 ResubmitCheck ResubmitResult 800 815 Pipelines map[string]db.Pipeline 816 + 817 + OrderedReactionKinds []db.ReactionKind 818 + Reactions map[db.ReactionKind]int 819 + UserReacted map[db.ReactionKind]bool 801 820 } 802 821 803 822 func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { ··· 806 825 } 807 826 808 827 type RepoPullPatchParams struct { 809 - LoggedInUser *oauth.User 810 - DidHandleMap map[string]string 811 - RepoInfo repoinfo.RepoInfo 812 - Pull *db.Pull 813 - Stack db.Stack 814 - Diff *types.NiceDiff 815 - Round int 816 - Submission *db.PullSubmission 828 + LoggedInUser *oauth.User 829 + DidHandleMap map[string]string 830 + RepoInfo repoinfo.RepoInfo 831 + Pull *db.Pull 832 + Stack db.Stack 833 + Diff *types.NiceDiff 834 + Round int 835 + Submission *db.PullSubmission 836 + OrderedReactionKinds []db.ReactionKind 817 837 } 818 838 819 839 // this name is a mouthful ··· 822 842 } 823 843 824 844 type RepoPullInterdiffParams struct { 825 - LoggedInUser *oauth.User 826 - DidHandleMap map[string]string 827 - RepoInfo repoinfo.RepoInfo 828 - Pull *db.Pull 829 - Round int 830 - Interdiff *patchutil.InterdiffResult 845 + LoggedInUser *oauth.User 846 + DidHandleMap map[string]string 847 + RepoInfo repoinfo.RepoInfo 848 + Pull *db.Pull 849 + Round int 850 + Interdiff *patchutil.InterdiffResult 851 + OrderedReactionKinds []db.ReactionKind 831 852 } 832 853 833 854 // this name is a mouthful
+1 -1
appview/pages/templates/layouts/repobase.html
··· 64 64 </div> 65 65 </nav> 66 66 <section 67 - class="bg-white dark:bg-gray-800 p-6 rounded relative w-full drop-shadow-sm dark:text-white" 67 + class="bg-white dark:bg-gray-800 p-6 rounded relative w-full dark:text-white" 68 68 > 69 69 {{ block "repoContent" . }}{{ end }} 70 70 </section>
+1 -1
appview/pages/templates/layouts/topbar.html
··· 39 39 class="cursor-pointer list-none flex items-center" 40 40 > 41 41 {{ $user := didOrHandle .Did .Handle }} 42 - {{ template "user/fragments/picHandle" $user }} 42 + {{ template "user/fragments/picHandleLink" $user }} 43 43 </summary> 44 44 <div 45 45 class="absolute flex flex-col right-0 mt-4 p-4 rounded w-48 bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700"
+1 -1
appview/pages/templates/repo/commit.html
··· 59 59 <div class="flex items-center gap-2 my-2"> 60 60 {{ i "user" "w-4 h-4" }} 61 61 {{ $committerDidOrHandle := index $.EmailToDidOrHandle $commit.Committer.Email }} 62 - <a href="/{{ $committerDidOrHandle }}">{{ $committerDidOrHandle }}</a> 62 + <a href="/{{ $committerDidOrHandle }}">{{ template "user/fragments/picHandleLink" $committerDidOrHandle }}</a> 63 63 </div> 64 64 <div class="my-1 pt-2 text-xs border-t"> 65 65 <div class="text-gray-600 dark:text-gray-300">SSH Key Fingerprint:</div>
+34
appview/pages/templates/repo/fragments/reaction.html
··· 1 + {{ define "repo/fragments/reaction" }} 2 + <button 3 + id="reactIndi-{{ .Kind }}" 4 + class="flex justify-center items-center min-w-8 min-h-8 rounded border 5 + leading-4 px-3 gap-1 6 + {{ if eq .Count 0 }} 7 + hidden 8 + {{ end }} 9 + {{ if .IsReacted }} 10 + bg-sky-100 11 + border-sky-400 12 + dark:bg-sky-900 13 + dark:border-sky-500 14 + {{ else }} 15 + border-gray-200 16 + hover:bg-gray-50 17 + hover:border-gray-300 18 + dark:border-gray-700 19 + dark:hover:bg-gray-700 20 + dark:hover:border-gray-600 21 + {{ end }} 22 + " 23 + {{ if .IsReacted }} 24 + hx-delete="/react?subject={{ .ThreadAt }}&kind={{ .Kind }}" 25 + {{ else }} 26 + hx-post="/react?subject={{ .ThreadAt }}&kind={{ .Kind }}" 27 + {{ end }} 28 + hx-swap="outerHTML" 29 + hx-trigger="click from:(#reactBtn-{{ .Kind }}, #reactIndi-{{ .Kind }})" 30 + hx-disabled-elt="this" 31 + > 32 + <span>{{ .Kind }}</span> <span>{{ .Count }}</span> 33 + </button> 34 + {{ end }}
+30
appview/pages/templates/repo/fragments/reactionsPopUp.html
··· 1 + {{ define "repo/fragments/reactionsPopUp" }} 2 + <details 3 + id="reactionsPopUp" 4 + class="relative inline-block" 5 + > 6 + <summary 7 + class="flex justify-center items-center min-w-8 min-h-8 rounded border border-gray-200 dark:border-gray-700 8 + hover:bg-gray-50 9 + hover:border-gray-300 10 + dark:hover:bg-gray-700 11 + dark:hover:border-gray-600 12 + cursor-pointer list-none" 13 + > 14 + {{ i "smile" "size-4" }} 15 + </summary> 16 + <div 17 + class="absolute flex left-0 z-10 mt-4 rounded bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700 shadow-lg" 18 + > 19 + {{ range $kind := . }} 20 + <button 21 + id="reactBtn-{{ $kind }}" 22 + class="size-12 hover:bg-gray-100 dark:hover:bg-gray-700" 23 + hx-on:click="this.parentElement.parentElement.removeAttribute('open')" 24 + > 25 + {{ $kind }} 26 + </button> 27 + {{ end }} 28 + </div> 29 + </details> 30 + {{ end }}
+1 -1
appview/pages/templates/repo/index.html
··· 266 266 {{ end }}" 267 267 class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 268 268 >{{ if $didOrHandle }} 269 - {{ $didOrHandle }} 269 + {{ template "user/fragments/picHandleLink" $didOrHandle }} 270 270 {{ else }} 271 271 {{ .Author.Name }} 272 272 {{ end }}</a
+6 -6
appview/pages/templates/repo/issues/fragments/issueComment.html
··· 1 1 {{ define "repo/issues/fragments/issueComment" }} 2 2 {{ with .Comment }} 3 3 <div id="comment-container-{{.CommentId}}"> 4 - <div class="flex items-center gap-2 mb-2 text-gray-500 dark:text-gray-400 text-sm"> 4 + <div class="flex items-center gap-2 mb-2 text-gray-500 dark:text-gray-400 text-sm flex-wrap"> 5 5 {{ $owner := index $.DidHandleMap .OwnerDid }} 6 - <a href="/{{ $owner }}" class="no-underline hover:underline">{{ $owner }}</a> 6 + {{ template "user/fragments/picHandleLink" $owner }} 7 7 8 8 <span class="before:content-['ยท']"></span> 9 9 <a ··· 18 18 {{ .Created | timeFmt }} 19 19 {{ end }} 20 20 </a> 21 - 21 + 22 22 <!-- show user "hats" --> 23 23 {{ $isIssueAuthor := eq .OwnerDid $.Issue.OwnerDid }} 24 24 {{ if $isIssueAuthor }} ··· 29 29 30 30 {{ $isCommentOwner := and $.LoggedInUser (eq $.LoggedInUser.Did .OwnerDid) }} 31 31 {{ if and $isCommentOwner (not .Deleted) }} 32 - <button 33 - class="btn px-2 py-1 text-sm" 32 + <button 33 + class="btn px-2 py-1 text-sm" 34 34 hx-get="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/edit" 35 35 hx-swap="outerHTML" 36 36 hx-target="#comment-container-{{.CommentId}}" 37 37 > 38 38 {{ i "pencil" "w-4 h-4" }} 39 39 </button> 40 - <button 40 + <button 41 41 class="btn px-2 py-1 text-sm text-red-500 flex gap-2 items-center group" 42 42 hx-delete="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/" 43 43 hx-confirm="Are you sure you want to delete your comment?"
+15 -1
appview/pages/templates/repo/issues/issue.html
··· 46 46 {{ .Issue.Body | markdown }} 47 47 </article> 48 48 {{ end }} 49 + 50 + <div class="flex items-center gap-2 mt-2"> 51 + {{ template "repo/fragments/reactionsPopUp" .OrderedReactionKinds }} 52 + {{ range $kind := .OrderedReactionKinds }} 53 + {{ 54 + template "repo/fragments/reaction" 55 + (dict 56 + "Kind" $kind 57 + "Count" (index $.Reactions $kind) 58 + "IsReacted" (index $.UserReacted $kind) 59 + "ThreadAt" $.Issue.IssueAt) 60 + }} 61 + {{ end }} 62 + </div> 49 63 </section> 50 64 {{ end }} 51 65 ··· 76 90 > 77 91 <div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-full md:w-3/5"> 78 92 <div class="text-sm pb-2 text-gray-500 dark:text-gray-400"> 79 - {{ didOrHandle .LoggedInUser.Did .LoggedInUser.Handle }} 93 + {{ template "user/fragments/picHandleLink" (didOrHandle .LoggedInUser.Did .LoggedInUser.Handle) }} 80 94 </div> 81 95 <textarea 82 96 id="comment-textarea"
+2 -2
appview/pages/templates/repo/log.html
··· 31 31 <td class=" py-3 align-top"> 32 32 {{ $didOrHandle := index $.EmailToDidOrHandle $commit.Author.Email }} 33 33 {{ if $didOrHandle }} 34 - <a href="/{{ $didOrHandle }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ $didOrHandle }}</a> 34 + {{ template "user/fragments/picHandleLink" $didOrHandle }} 35 35 {{ else }} 36 36 <a href="mailto:{{ $commit.Author.Email }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ $commit.Author.Name }}</a> 37 37 {{ end }} ··· 159 159 {{ $didOrHandle := index $.EmailToDidOrHandle $commit.Author.Email }} 160 160 <a href="{{ if $didOrHandle }}/{{ $didOrHandle }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}" 161 161 class="text-gray-500 dark:text-gray-400 no-underline hover:underline"> 162 - {{ if $didOrHandle }}{{ $didOrHandle }}{{ else }}{{ $commit.Author.Name }}{{ end }} 162 + {{ if $didOrHandle }}{{ template "user/fragments/picHandleLink" $didOrHandle }}{{ else }}{{ $commit.Author.Name }}{{ end }} 163 163 </a> 164 164 </span> 165 165 <div class="inline-block px-1 select-none after:content-['ยท']"></div>
+16
appview/pages/templates/repo/pulls/fragments/pullHeader.html
··· 61 61 {{ .Pull.Body | markdown }} 62 62 </article> 63 63 {{ end }} 64 + 65 + {{ with .OrderedReactionKinds }} 66 + <div class="flex items-center gap-2 mt-2"> 67 + {{ template "repo/fragments/reactionsPopUp" . }} 68 + {{ range $kind := . }} 69 + {{ 70 + template "repo/fragments/reaction" 71 + (dict 72 + "Kind" $kind 73 + "Count" (index $.Reactions $kind) 74 + "IsReacted" (index $.UserReacted $kind) 75 + "ThreadAt" $.Pull.PullAt) 76 + }} 77 + {{ end }} 78 + </div> 79 + {{ end }} 64 80 </section> 65 81 66 82
+3 -4
appview/pages/templates/repo/pulls/fragments/pullNewComment.html
··· 1 1 {{ define "repo/pulls/fragments/pullNewComment" }} 2 - <div 3 - id="pull-comment-card-{{ .RoundNumber }}" 2 + <div 3 + id="pull-comment-card-{{ .RoundNumber }}" 4 4 class="bg-white dark:bg-gray-800 rounded drop-shadow-sm p-4 relative w-full flex flex-col gap-2"> 5 5 <div class="text-sm text-gray-500 dark:text-gray-400"> 6 - {{ didOrHandle .LoggedInUser.Did .LoggedInUser.Handle }} 6 + {{ template "user/fragments/picHandleLink" (didOrHandle .LoggedInUser.Did .LoggedInUser.Handle) }} 7 7 </div> 8 8 <form 9 9 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/comment" ··· 38 38 </form> 39 39 </div> 40 40 {{ end }} 41 -
+7 -7
appview/pages/templates/repo/pulls/pull.html
··· 5 5 {{ define "extrameta" }} 6 6 {{ $title := printf "%s &middot; pull #%d &middot; %s" .Pull.Title .Pull.PullId .RepoInfo.FullName }} 7 7 {{ $url := printf "https://tangled.sh/%s/pulls/%d" .RepoInfo.FullName .Pull.PullId }} 8 - 8 + 9 9 {{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }} 10 10 {{ end }} 11 11 ··· 46 46 </div> 47 47 <!-- round summary --> 48 48 <div class="rounded drop-shadow-sm bg-white dark:bg-gray-800 p-2 text-gray-500 dark:text-gray-400"> 49 - <span> 49 + <span class="gap-1 flex items-center"> 50 50 {{ $owner := index $.DidHandleMap $.Pull.OwnerDid }} 51 51 {{ $re := "re" }} 52 52 {{ if eq .RoundNumber 0 }} 53 53 {{ $re = "" }} 54 54 {{ end }} 55 55 <span class="hidden md:inline">{{$re}}submitted</span> 56 - by <a href="/{{ $owner }}">{{ $owner }}</a> 56 + by {{ template "user/fragments/picHandleLink" $owner }} 57 57 <span class="select-none before:content-['\00B7']"></span> 58 58 <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500" href="#round-#{{ .RoundNumber }}"><time>{{ .Created | shortTimeFmt }}</time></a> 59 59 <span class="select-none before:content-['ยท']"></span> ··· 68 68 <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 69 69 hx-boost="true" 70 70 href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}"> 71 - {{ i "file-diff" "w-4 h-4" }} 71 + {{ i "file-diff" "w-4 h-4" }} 72 72 <span class="hidden md:inline">diff</span> 73 73 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 74 74 </a> ··· 150 150 {{ if gt $cidx 0 }} 151 151 <div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div> 152 152 {{ end }} 153 - <div class="text-sm text-gray-500 dark:text-gray-400"> 154 - {{ $owner := index $.DidHandleMap $c.OwnerDid }} 155 - <a href="/{{$owner}}">{{$owner}}</a> 153 + <div class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1"> 154 + {{ $owner := index $.DidHandleMap $c.OwnerDid }} 155 + {{ template "user/fragments/picHandleLink" $owner }} 156 156 <span class="before:content-['ยท']"></span> 157 157 <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.ID}}"><time>{{ $c.Created | shortTimeFmt }}</time></a> 158 158 </div>
+7 -1
appview/pages/templates/repo/tree.html
··· 11 11 {{ template "repo/fragments/meta" . }} 12 12 {{ $title := printf "%s at %s &middot; %s" $path .Ref .RepoInfo.FullName }} 13 13 {{ $url := printf "https://tangled.sh/%s/tree/%s%s" .RepoInfo.FullName .Ref $path }} 14 - 14 + 15 15 {{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }} 16 16 {{ end }} 17 17 ··· 63 63 </div> 64 64 </a> 65 65 {{ if .LastCommit}} 66 + <div class="flex items-end gap-2"> 67 + <span class="text text-gray-500 dark:text-gray-400 mr-6">{{ .LastCommit.Message }}</span> 66 68 <time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time> 69 + </div> 67 70 {{ end }} 68 71 </div> 69 72 </div> ··· 80 83 </div> 81 84 </a> 82 85 {{ if .LastCommit}} 86 + <div class="flex items-end gap-2"> 87 + <span class="text text-gray-500 dark:text-gray-400 mr-6">{{ .LastCommit.Message }}</span> 83 88 <time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time> 89 + </div> 84 90 {{ end }} 85 91 </div> 86 92 </div>
+15
appview/pulls/pulls.go
··· 198 198 m[p.Sha] = p 199 199 } 200 200 201 + reactionCountMap, err := db.GetReactionCountMap(s.db, pull.PullAt()) 202 + if err != nil { 203 + log.Println("failed to get pull reactions") 204 + s.pages.Notice(w, "pulls", "Failed to load pull. Try again later.") 205 + } 206 + 207 + userReactions := map[db.ReactionKind]bool{} 208 + if user != nil { 209 + userReactions = db.GetReactionStatusMap(s.db, user.Did, pull.PullAt()) 210 + } 211 + 201 212 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 202 213 LoggedInUser: user, 203 214 RepoInfo: repoInfo, ··· 208 219 MergeCheck: mergeCheckResponse, 209 220 ResubmitCheck: resubmitResult, 210 221 Pipelines: m, 222 + 223 + OrderedReactionKinds: db.OrderedReactionKinds, 224 + Reactions: reactionCountMap, 225 + UserReacted: userReactions, 211 226 }) 212 227 } 213 228
+126
appview/state/reaction.go
··· 1 + package state 2 + 3 + import ( 4 + "log" 5 + "net/http" 6 + "time" 7 + 8 + comatproto "github.com/bluesky-social/indigo/api/atproto" 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + 11 + lexutil "github.com/bluesky-social/indigo/lex/util" 12 + "tangled.sh/tangled.sh/core/api/tangled" 13 + "tangled.sh/tangled.sh/core/appview" 14 + "tangled.sh/tangled.sh/core/appview/db" 15 + "tangled.sh/tangled.sh/core/appview/pages" 16 + ) 17 + 18 + func (s *State) React(w http.ResponseWriter, r *http.Request) { 19 + currentUser := s.oauth.GetUser(r) 20 + 21 + subject := r.URL.Query().Get("subject") 22 + if subject == "" { 23 + log.Println("invalid form") 24 + return 25 + } 26 + 27 + subjectUri, err := syntax.ParseATURI(subject) 28 + if err != nil { 29 + log.Println("invalid form") 30 + return 31 + } 32 + 33 + reactionKind, ok := db.ParseReactionKind(r.URL.Query().Get("kind")) 34 + if !ok { 35 + log.Println("invalid reaction kind") 36 + return 37 + } 38 + 39 + client, err := s.oauth.AuthorizedClient(r) 40 + if err != nil { 41 + log.Println("failed to authorize client", err) 42 + return 43 + } 44 + 45 + switch r.Method { 46 + case http.MethodPost: 47 + createdAt := time.Now().Format(time.RFC3339) 48 + rkey := appview.TID() 49 + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 50 + Collection: tangled.FeedReactionNSID, 51 + Repo: currentUser.Did, 52 + Rkey: rkey, 53 + Record: &lexutil.LexiconTypeDecoder{ 54 + Val: &tangled.FeedReaction{ 55 + Subject: subjectUri.String(), 56 + Reaction: reactionKind.String(), 57 + CreatedAt: createdAt, 58 + }, 59 + }, 60 + }) 61 + if err != nil { 62 + log.Println("failed to create atproto record", err) 63 + return 64 + } 65 + 66 + err = db.AddReaction(s.db, currentUser.Did, subjectUri, reactionKind, rkey) 67 + if err != nil { 68 + log.Println("failed to react", err) 69 + return 70 + } 71 + 72 + count, err := db.GetReactionCount(s.db, subjectUri, reactionKind) 73 + if err != nil { 74 + log.Println("failed to get reaction count for ", subjectUri) 75 + } 76 + 77 + log.Println("created atproto record: ", resp.Uri) 78 + 79 + s.pages.ThreadReactionFragment(w, pages.ThreadReactionFragmentParams{ 80 + ThreadAt: subjectUri, 81 + Kind: reactionKind, 82 + Count: count, 83 + IsReacted: true, 84 + }) 85 + 86 + return 87 + case http.MethodDelete: 88 + reaction, err := db.GetReaction(s.db, currentUser.Did, subjectUri, reactionKind) 89 + if err != nil { 90 + log.Println("failed to get reaction relationship for", currentUser.Did, subjectUri) 91 + return 92 + } 93 + 94 + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 95 + Collection: tangled.FeedReactionNSID, 96 + Repo: currentUser.Did, 97 + Rkey: reaction.Rkey, 98 + }) 99 + 100 + if err != nil { 101 + log.Println("failed to remove reaction") 102 + return 103 + } 104 + 105 + err = db.DeleteReactionByRkey(s.db, currentUser.Did, reaction.Rkey) 106 + if err != nil { 107 + log.Println("failed to delete reaction from DB") 108 + // this is not an issue, the firehose event might have already done this 109 + } 110 + 111 + count, err := db.GetReactionCount(s.db, subjectUri, reactionKind) 112 + if err != nil { 113 + log.Println("failed to get reaction count for ", subjectUri) 114 + return 115 + } 116 + 117 + s.pages.ThreadReactionFragment(w, pages.ThreadReactionFragmentParams{ 118 + ThreadAt: subjectUri, 119 + Kind: reactionKind, 120 + Count: count, 121 + IsReacted: false, 122 + }) 123 + 124 + return 125 + } 126 + }
+5
appview/state/router.go
··· 137 137 r.Delete("/", s.Star) 138 138 }) 139 139 140 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/react", func(r chi.Router) { 141 + r.Post("/", s.React) 142 + r.Delete("/", s.React) 143 + }) 144 + 140 145 r.Route("/profile", func(r chi.Router) { 141 146 r.Use(middleware.AuthMiddleware(s.oauth)) 142 147 r.Get("/edit-bio", s.EditBioFragment)
+1
cmd/gen.go
··· 15 15 "api/tangled/cbor_gen.go", 16 16 "tangled", 17 17 tangled.ActorProfile{}, 18 + tangled.FeedReaction{}, 18 19 tangled.FeedStar{}, 19 20 tangled.GitRefUpdate{}, 20 21 tangled.GitRefUpdate_Meta{},
+8 -1
docs/spindle/hosting.md
··· 36 36 go build -o cmd/spindle/spindle cmd/spindle/main.go 37 37 ``` 38 38 39 - 3. **Run the Spindle binary.** 39 + 3. **Create the log directory.** 40 + 41 + ```shell 42 + sudo mkdir -p /var/log/spindle 43 + sudo chown $USER:$USER -R /var/log/spindle 44 + ``` 45 + 46 + 4. **Run the Spindle binary.** 40 47 41 48 ```shell 42 49 ./cmd/spindle/spindle
+34
lexicons/feed/reaction.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.feed.reaction", 4 + "needsCbor": true, 5 + "needsType": true, 6 + "defs": { 7 + "main": { 8 + "type": "record", 9 + "key": "tid", 10 + "record": { 11 + "type": "object", 12 + "required": [ 13 + "subject", 14 + "reaction", 15 + "createdAt" 16 + ], 17 + "properties": { 18 + "subject": { 19 + "type": "string", 20 + "format": "at-uri" 21 + }, 22 + "reaction": { 23 + "type": "string", 24 + "enum": [ "๐Ÿ‘", "๐Ÿ‘Ž", "๐Ÿ˜†", "๐ŸŽ‰", "๐Ÿซค", "โค๏ธ", "๐Ÿš€", "๐Ÿ‘€" ] 25 + }, 26 + "createdAt": { 27 + "type": "string", 28 + "format": "datetime" 29 + } 30 + } 31 + } 32 + } 33 + } 34 + }
+1 -1
spindle/engine/engine.go
··· 90 90 91 91 reader, err := e.docker.ImagePull(ctx, w.Image, image.PullOptions{}) 92 92 if err != nil { 93 - e.l.Error("pipeline failed!", "workflowId", wid, "error", err.Error()) 93 + e.l.Error("pipeline image pull failed!", "image", w.Image, "workflowId", wid, "error", err.Error()) 94 94 95 95 err := e.db.StatusFailed(wid, err.Error(), -1, e.n) 96 96 if err != nil {