Monorepo for Tangled tangled.org

Compare changes

Choose any two refs to compare.

-416
api/tangled/cbor_gen.go
··· 561 561 562 562 return nil 563 563 } 564 - func (t *Comment) MarshalCBOR(w io.Writer) error { 565 - if t == nil { 566 - _, err := w.Write(cbg.CborNull) 567 - return err 568 - } 569 - 570 - cw := cbg.NewCborWriter(w) 571 - fieldCount := 7 572 - 573 - if t.Mentions == nil { 574 - fieldCount-- 575 - } 576 - 577 - if t.References == nil { 578 - fieldCount-- 579 - } 580 - 581 - if t.ReplyTo == nil { 582 - fieldCount-- 583 - } 584 - 585 - if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 586 - return err 587 - } 588 - 589 - // t.Body (string) (string) 590 - if len("body") > 1000000 { 591 - return xerrors.Errorf("Value in field \"body\" was too long") 592 - } 593 - 594 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("body"))); err != nil { 595 - return err 596 - } 597 - if _, err := cw.WriteString(string("body")); err != nil { 598 - return err 599 - } 600 - 601 - if len(t.Body) > 1000000 { 602 - return xerrors.Errorf("Value in field t.Body was too long") 603 - } 604 - 605 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Body))); err != nil { 606 - return err 607 - } 608 - if _, err := cw.WriteString(string(t.Body)); err != nil { 609 - return err 610 - } 611 - 612 - // t.LexiconTypeID (string) (string) 613 - if len("$type") > 1000000 { 614 - return xerrors.Errorf("Value in field \"$type\" was too long") 615 - } 616 - 617 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 618 - return err 619 - } 620 - if _, err := cw.WriteString(string("$type")); err != nil { 621 - return err 622 - } 623 - 624 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.comment"))); err != nil { 625 - return err 626 - } 627 - if _, err := cw.WriteString(string("sh.tangled.comment")); err != nil { 628 - return err 629 - } 630 - 631 - // t.ReplyTo (string) (string) 632 - if t.ReplyTo != nil { 633 - 634 - if len("replyTo") > 1000000 { 635 - return xerrors.Errorf("Value in field \"replyTo\" was too long") 636 - } 637 - 638 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("replyTo"))); err != nil { 639 - return err 640 - } 641 - if _, err := cw.WriteString(string("replyTo")); err != nil { 642 - return err 643 - } 644 - 645 - if t.ReplyTo == nil { 646 - if _, err := cw.Write(cbg.CborNull); err != nil { 647 - return err 648 - } 649 - } else { 650 - if len(*t.ReplyTo) > 1000000 { 651 - return xerrors.Errorf("Value in field t.ReplyTo was too long") 652 - } 653 - 654 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.ReplyTo))); err != nil { 655 - return err 656 - } 657 - if _, err := cw.WriteString(string(*t.ReplyTo)); err != nil { 658 - return err 659 - } 660 - } 661 - } 662 - 663 - // t.Subject (string) (string) 664 - if len("subject") > 1000000 { 665 - return xerrors.Errorf("Value in field \"subject\" was too long") 666 - } 667 - 668 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subject"))); err != nil { 669 - return err 670 - } 671 - if _, err := cw.WriteString(string("subject")); err != nil { 672 - return err 673 - } 674 - 675 - if len(t.Subject) > 1000000 { 676 - return xerrors.Errorf("Value in field t.Subject was too long") 677 - } 678 - 679 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Subject))); err != nil { 680 - return err 681 - } 682 - if _, err := cw.WriteString(string(t.Subject)); err != nil { 683 - return err 684 - } 685 - 686 - // t.Mentions ([]string) (slice) 687 - if t.Mentions != nil { 688 - 689 - if len("mentions") > 1000000 { 690 - return xerrors.Errorf("Value in field \"mentions\" was too long") 691 - } 692 - 693 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil { 694 - return err 695 - } 696 - if _, err := cw.WriteString(string("mentions")); err != nil { 697 - return err 698 - } 699 - 700 - if len(t.Mentions) > 8192 { 701 - return xerrors.Errorf("Slice value in field t.Mentions was too long") 702 - } 703 - 704 - if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil { 705 - return err 706 - } 707 - for _, v := range t.Mentions { 708 - if len(v) > 1000000 { 709 - return xerrors.Errorf("Value in field v was too long") 710 - } 711 - 712 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { 713 - return err 714 - } 715 - if _, err := cw.WriteString(string(v)); err != nil { 716 - return err 717 - } 718 - 719 - } 720 - } 721 - 722 - // t.CreatedAt (string) (string) 723 - if len("createdAt") > 1000000 { 724 - return xerrors.Errorf("Value in field \"createdAt\" was too long") 725 - } 726 - 727 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 728 - return err 729 - } 730 - if _, err := cw.WriteString(string("createdAt")); err != nil { 731 - return err 732 - } 733 - 734 - if len(t.CreatedAt) > 1000000 { 735 - return xerrors.Errorf("Value in field t.CreatedAt was too long") 736 - } 737 - 738 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 739 - return err 740 - } 741 - if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 742 - return err 743 - } 744 - 745 - // t.References ([]string) (slice) 746 - if t.References != nil { 747 - 748 - if len("references") > 1000000 { 749 - return xerrors.Errorf("Value in field \"references\" was too long") 750 - } 751 - 752 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil { 753 - return err 754 - } 755 - if _, err := cw.WriteString(string("references")); err != nil { 756 - return err 757 - } 758 - 759 - if len(t.References) > 8192 { 760 - return xerrors.Errorf("Slice value in field t.References was too long") 761 - } 762 - 763 - if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil { 764 - return err 765 - } 766 - for _, v := range t.References { 767 - if len(v) > 1000000 { 768 - return xerrors.Errorf("Value in field v was too long") 769 - } 770 - 771 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { 772 - return err 773 - } 774 - if _, err := cw.WriteString(string(v)); err != nil { 775 - return err 776 - } 777 - 778 - } 779 - } 780 - return nil 781 - } 782 - 783 - func (t *Comment) UnmarshalCBOR(r io.Reader) (err error) { 784 - *t = Comment{} 785 - 786 - cr := cbg.NewCborReader(r) 787 - 788 - maj, extra, err := cr.ReadHeader() 789 - if err != nil { 790 - return err 791 - } 792 - defer func() { 793 - if err == io.EOF { 794 - err = io.ErrUnexpectedEOF 795 - } 796 - }() 797 - 798 - if maj != cbg.MajMap { 799 - return fmt.Errorf("cbor input should be of type map") 800 - } 801 - 802 - if extra > cbg.MaxLength { 803 - return fmt.Errorf("Comment: map struct too large (%d)", extra) 804 - } 805 - 806 - n := extra 807 - 808 - nameBuf := make([]byte, 10) 809 - for i := uint64(0); i < n; i++ { 810 - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 811 - if err != nil { 812 - return err 813 - } 814 - 815 - if !ok { 816 - // Field doesn't exist on this type, so ignore it 817 - if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 818 - return err 819 - } 820 - continue 821 - } 822 - 823 - switch string(nameBuf[:nameLen]) { 824 - // t.Body (string) (string) 825 - case "body": 826 - 827 - { 828 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 829 - if err != nil { 830 - return err 831 - } 832 - 833 - t.Body = string(sval) 834 - } 835 - // t.LexiconTypeID (string) (string) 836 - case "$type": 837 - 838 - { 839 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 840 - if err != nil { 841 - return err 842 - } 843 - 844 - t.LexiconTypeID = string(sval) 845 - } 846 - // t.ReplyTo (string) (string) 847 - case "replyTo": 848 - 849 - { 850 - b, err := cr.ReadByte() 851 - if err != nil { 852 - return err 853 - } 854 - if b != cbg.CborNull[0] { 855 - if err := cr.UnreadByte(); err != nil { 856 - return err 857 - } 858 - 859 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 860 - if err != nil { 861 - return err 862 - } 863 - 864 - t.ReplyTo = (*string)(&sval) 865 - } 866 - } 867 - // t.Subject (string) (string) 868 - case "subject": 869 - 870 - { 871 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 872 - if err != nil { 873 - return err 874 - } 875 - 876 - t.Subject = string(sval) 877 - } 878 - // t.Mentions ([]string) (slice) 879 - case "mentions": 880 - 881 - maj, extra, err = cr.ReadHeader() 882 - if err != nil { 883 - return err 884 - } 885 - 886 - if extra > 8192 { 887 - return fmt.Errorf("t.Mentions: array too large (%d)", extra) 888 - } 889 - 890 - if maj != cbg.MajArray { 891 - return fmt.Errorf("expected cbor array") 892 - } 893 - 894 - if extra > 0 { 895 - t.Mentions = make([]string, extra) 896 - } 897 - 898 - for i := 0; i < int(extra); i++ { 899 - { 900 - var maj byte 901 - var extra uint64 902 - var err error 903 - _ = maj 904 - _ = extra 905 - _ = err 906 - 907 - { 908 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 909 - if err != nil { 910 - return err 911 - } 912 - 913 - t.Mentions[i] = string(sval) 914 - } 915 - 916 - } 917 - } 918 - // t.CreatedAt (string) (string) 919 - case "createdAt": 920 - 921 - { 922 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 923 - if err != nil { 924 - return err 925 - } 926 - 927 - t.CreatedAt = string(sval) 928 - } 929 - // t.References ([]string) (slice) 930 - case "references": 931 - 932 - maj, extra, err = cr.ReadHeader() 933 - if err != nil { 934 - return err 935 - } 936 - 937 - if extra > 8192 { 938 - return fmt.Errorf("t.References: array too large (%d)", extra) 939 - } 940 - 941 - if maj != cbg.MajArray { 942 - return fmt.Errorf("expected cbor array") 943 - } 944 - 945 - if extra > 0 { 946 - t.References = make([]string, extra) 947 - } 948 - 949 - for i := 0; i < int(extra); i++ { 950 - { 951 - var maj byte 952 - var extra uint64 953 - var err error 954 - _ = maj 955 - _ = extra 956 - _ = err 957 - 958 - { 959 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 960 - if err != nil { 961 - return err 962 - } 963 - 964 - t.References[i] = string(sval) 965 - } 966 - 967 - } 968 - } 969 - 970 - default: 971 - // Field doesn't exist on this type, so ignore it 972 - if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 973 - return err 974 - } 975 - } 976 - } 977 - 978 - return nil 979 - } 980 564 func (t *FeedReaction) MarshalCBOR(w io.Writer) error { 981 565 if t == nil { 982 566 _, err := w.Write(cbg.CborNull)
-27
api/tangled/tangledcomment.go
··· 1 - // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 - 3 - package tangled 4 - 5 - // schema: sh.tangled.comment 6 - 7 - import ( 8 - "github.com/bluesky-social/indigo/lex/util" 9 - ) 10 - 11 - const ( 12 - CommentNSID = "sh.tangled.comment" 13 - ) 14 - 15 - func init() { 16 - util.RegisterType("sh.tangled.comment", &Comment{}) 17 - } // 18 - // RECORDTYPE: Comment 19 - type Comment struct { 20 - LexiconTypeID string `json:"$type,const=sh.tangled.comment" cborgen:"$type,const=sh.tangled.comment"` 21 - Body string `json:"body" cborgen:"body"` 22 - CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 - Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` 24 - References []string `json:"references,omitempty" cborgen:"references,omitempty"` 25 - ReplyTo *string `json:"replyTo,omitempty" cborgen:"replyTo,omitempty"` 26 - Subject string `json:"subject" cborgen:"subject"` 27 - }
-199
appview/db/comments.go
··· 1 - package db 2 - 3 - import ( 4 - "database/sql" 5 - "fmt" 6 - "maps" 7 - "slices" 8 - "sort" 9 - "strings" 10 - "time" 11 - 12 - "github.com/bluesky-social/indigo/atproto/syntax" 13 - "tangled.org/core/appview/models" 14 - "tangled.org/core/orm" 15 - ) 16 - 17 - func PutComment(tx *sql.Tx, c *models.Comment) error { 18 - result, err := tx.Exec( 19 - `insert into comments ( 20 - did, 21 - rkey, 22 - subject_at, 23 - reply_to, 24 - body, 25 - pull_submission_id, 26 - created 27 - ) 28 - values (?, ?, ?, ?, ?, ?, ?) 29 - on conflict(did, rkey) do update set 30 - subject_at = excluded.subject_at, 31 - reply_to = excluded.reply_to, 32 - body = excluded.body, 33 - edited = case 34 - when 35 - comments.subject_at != excluded.subject_at 36 - or comments.body != excluded.body 37 - or comments.reply_to != excluded.reply_to 38 - then ? 39 - else comments.edited 40 - end`, 41 - c.Did, 42 - c.Rkey, 43 - c.Subject, 44 - c.ReplyTo, 45 - c.Body, 46 - c.PullSubmissionId, 47 - c.Created.Format(time.RFC3339), 48 - time.Now().Format(time.RFC3339), 49 - ) 50 - if err != nil { 51 - return err 52 - } 53 - 54 - c.Id, err = result.LastInsertId() 55 - if err != nil { 56 - return err 57 - } 58 - 59 - if err := putReferences(tx, c.AtUri(), c.References); err != nil { 60 - return fmt.Errorf("put reference_links: %w", err) 61 - } 62 - 63 - return nil 64 - } 65 - 66 - func DeleteComments(e Execer, filters ...orm.Filter) error { 67 - var conditions []string 68 - var args []any 69 - for _, filter := range filters { 70 - conditions = append(conditions, filter.Condition()) 71 - args = append(args, filter.Arg()...) 72 - } 73 - 74 - whereClause := "" 75 - if conditions != nil { 76 - whereClause = " where " + strings.Join(conditions, " and ") 77 - } 78 - 79 - query := fmt.Sprintf(`update comments set body = "", deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause) 80 - 81 - _, err := e.Exec(query, args...) 82 - return err 83 - } 84 - 85 - func GetComments(e Execer, filters ...orm.Filter) ([]models.Comment, error) { 86 - commentMap := make(map[string]*models.Comment) 87 - 88 - var conditions []string 89 - var args []any 90 - for _, filter := range filters { 91 - conditions = append(conditions, filter.Condition()) 92 - args = append(args, filter.Arg()...) 93 - } 94 - 95 - whereClause := "" 96 - if conditions != nil { 97 - whereClause = " where " + strings.Join(conditions, " and ") 98 - } 99 - 100 - query := fmt.Sprintf(` 101 - select 102 - id, 103 - did, 104 - rkey, 105 - subject_at, 106 - reply_to, 107 - body, 108 - pull_submission_id, 109 - created, 110 - edited, 111 - deleted 112 - from 113 - comments 114 - %s 115 - `, whereClause) 116 - 117 - rows, err := e.Query(query, args...) 118 - if err != nil { 119 - return nil, err 120 - } 121 - 122 - for rows.Next() { 123 - var comment models.Comment 124 - var created string 125 - var rkey, edited, deleted, replyTo sql.Null[string] 126 - err := rows.Scan( 127 - &comment.Id, 128 - &comment.Did, 129 - &rkey, 130 - &comment.Subject, 131 - &replyTo, 132 - &comment.Body, 133 - &comment.PullSubmissionId, 134 - &created, 135 - &edited, 136 - &deleted, 137 - ) 138 - if err != nil { 139 - return nil, err 140 - } 141 - 142 - // this is a remnant from old times, newer comments always have rkey 143 - if rkey.Valid { 144 - comment.Rkey = rkey.V 145 - } 146 - 147 - if t, err := time.Parse(time.RFC3339, created); err == nil { 148 - comment.Created = t 149 - } 150 - 151 - if edited.Valid { 152 - if t, err := time.Parse(time.RFC3339, edited.V); err == nil { 153 - comment.Edited = &t 154 - } 155 - } 156 - 157 - if deleted.Valid { 158 - if t, err := time.Parse(time.RFC3339, deleted.V); err == nil { 159 - comment.Deleted = &t 160 - } 161 - } 162 - 163 - if replyTo.Valid { 164 - rt := syntax.ATURI(replyTo.V) 165 - comment.ReplyTo = &rt 166 - } 167 - 168 - atUri := comment.AtUri().String() 169 - commentMap[atUri] = &comment 170 - } 171 - 172 - if err := rows.Err(); err != nil { 173 - return nil, err 174 - } 175 - defer rows.Close() 176 - 177 - // collect references from each comments 178 - commentAts := slices.Collect(maps.Keys(commentMap)) 179 - allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts)) 180 - if err != nil { 181 - return nil, fmt.Errorf("failed to query reference_links: %w", err) 182 - } 183 - for commentAt, references := range allReferencs { 184 - if comment, ok := commentMap[commentAt.String()]; ok { 185 - comment.References = references 186 - } 187 - } 188 - 189 - var comments []models.Comment 190 - for _, c := range commentMap { 191 - comments = append(comments, *c) 192 - } 193 - 194 - sort.Slice(comments, func(i, j int) bool { 195 - return comments[i].Created.After(comments[j].Created) 196 - }) 197 - 198 - return comments, nil 199 - }
-81
appview/db/db.go
··· 1173 1173 return err 1174 1174 }) 1175 1175 1176 - orm.RunMigration(conn, logger, "add-comments-table", func(tx *sql.Tx) error { 1177 - _, err := tx.Exec(` 1178 - drop table if exists comments; 1179 - 1180 - create table comments ( 1181 - -- identifiers 1182 - id integer primary key autoincrement, 1183 - did text not null, 1184 - collection text not null default 'sh.tangled.comment', 1185 - rkey text not null, 1186 - at_uri text generated always as ('at://' || did || '/' || collection || '/' || rkey) stored, 1187 - 1188 - -- at identifiers 1189 - subject_at text not null, 1190 - reply_to text, -- at_uri of parent comment 1191 - 1192 - pull_submission_id integer, -- dirty fix until we atprotate the pull-rounds 1193 - 1194 - -- content 1195 - body text not null, 1196 - created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1197 - edited text, 1198 - deleted text, 1199 - 1200 - -- constraints 1201 - unique(did, rkey) 1202 - ); 1203 - 1204 - insert into comments ( 1205 - did, 1206 - collection, 1207 - rkey, 1208 - subject_at, 1209 - reply_to, 1210 - body, 1211 - created, 1212 - edited, 1213 - deleted 1214 - ) 1215 - select 1216 - did, 1217 - 'sh.tangled.repo.issue.comment', 1218 - rkey, 1219 - issue_at, 1220 - reply_to, 1221 - body, 1222 - created, 1223 - edited, 1224 - deleted 1225 - from issue_comments 1226 - where rkey is not null; 1227 - 1228 - insert into comments ( 1229 - did, 1230 - collection, 1231 - rkey, 1232 - subject_at, 1233 - pull_submission_id, 1234 - body, 1235 - created 1236 - ) 1237 - select 1238 - c.owner_did, 1239 - 'sh.tangled.repo.pull.comment', 1240 - substr( 1241 - substr(c.comment_at, 6 + instr(substr(c.comment_at, 6), '/')), -- nsid/rkey 1242 - instr( 1243 - substr(c.comment_at, 6 + instr(substr(c.comment_at, 6), '/')), -- nsid/rkey 1244 - '/' 1245 - ) + 1 1246 - ), -- rkey 1247 - p.at_uri, 1248 - c.submission_id, 1249 - c.body, 1250 - c.created 1251 - from pull_comments c 1252 - join pulls p on c.repo_at = p.repo_at and c.pull_id = p.pull_id; 1253 - `) 1254 - return err 1255 - }) 1256 - 1257 1176 return &DB{ 1258 1177 db, 1259 1178 logger,
+186 -6
appview/db/issues.go
··· 100 100 } 101 101 102 102 func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Issue, error) { 103 - issueMap := make(map[syntax.ATURI]*models.Issue) // at-uri -> issue 103 + issueMap := make(map[string]*models.Issue) // at-uri -> issue 104 104 105 105 var conditions []string 106 106 var args []any ··· 196 196 } 197 197 } 198 198 199 - issueMap[issue.AtUri()] = &issue 199 + atUri := issue.AtUri().String() 200 + issueMap[atUri] = &issue 200 201 } 201 202 202 203 // collect reverse repos ··· 228 229 // collect comments 229 230 issueAts := slices.Collect(maps.Keys(issueMap)) 230 231 231 - comments, err := GetComments(e, orm.FilterIn("subject_at", issueAts)) 232 + comments, err := GetIssueComments(e, orm.FilterIn("issue_at", issueAts)) 232 233 if err != nil { 233 234 return nil, fmt.Errorf("failed to query comments: %w", err) 234 235 } 235 236 for i := range comments { 236 - issueAt := comments[i].Subject 237 + issueAt := comments[i].IssueAt 237 238 if issue, ok := issueMap[issueAt]; ok { 238 239 issue.Comments = append(issue.Comments, comments[i]) 239 240 } ··· 245 246 return nil, fmt.Errorf("failed to query labels: %w", err) 246 247 } 247 248 for issueAt, labels := range allLabels { 248 - if issue, ok := issueMap[issueAt]; ok { 249 + if issue, ok := issueMap[issueAt.String()]; ok { 249 250 issue.Labels = labels 250 251 } 251 252 } ··· 256 257 return nil, fmt.Errorf("failed to query reference_links: %w", err) 257 258 } 258 259 for issueAt, references := range allReferencs { 259 - if issue, ok := issueMap[issueAt]; ok { 260 + if issue, ok := issueMap[issueAt.String()]; ok { 260 261 issue.References = references 261 262 } 262 263 } ··· 348 349 } 349 350 350 351 return ids, nil 352 + } 353 + 354 + func AddIssueComment(tx *sql.Tx, c models.IssueComment) (int64, error) { 355 + result, err := tx.Exec( 356 + `insert into issue_comments ( 357 + did, 358 + rkey, 359 + issue_at, 360 + body, 361 + reply_to, 362 + created, 363 + edited 364 + ) 365 + values (?, ?, ?, ?, ?, ?, null) 366 + on conflict(did, rkey) do update set 367 + issue_at = excluded.issue_at, 368 + body = excluded.body, 369 + edited = case 370 + when 371 + issue_comments.issue_at != excluded.issue_at 372 + or issue_comments.body != excluded.body 373 + or issue_comments.reply_to != excluded.reply_to 374 + then ? 375 + else issue_comments.edited 376 + end`, 377 + c.Did, 378 + c.Rkey, 379 + c.IssueAt, 380 + c.Body, 381 + c.ReplyTo, 382 + c.Created.Format(time.RFC3339), 383 + time.Now().Format(time.RFC3339), 384 + ) 385 + if err != nil { 386 + return 0, err 387 + } 388 + 389 + id, err := result.LastInsertId() 390 + if err != nil { 391 + return 0, err 392 + } 393 + 394 + if err := putReferences(tx, c.AtUri(), c.References); err != nil { 395 + return 0, fmt.Errorf("put reference_links: %w", err) 396 + } 397 + 398 + return id, nil 399 + } 400 + 401 + func DeleteIssueComments(e Execer, filters ...orm.Filter) error { 402 + var conditions []string 403 + var args []any 404 + for _, filter := range filters { 405 + conditions = append(conditions, filter.Condition()) 406 + args = append(args, filter.Arg()...) 407 + } 408 + 409 + whereClause := "" 410 + if conditions != nil { 411 + whereClause = " where " + strings.Join(conditions, " and ") 412 + } 413 + 414 + query := fmt.Sprintf(`update issue_comments set body = "", deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause) 415 + 416 + _, err := e.Exec(query, args...) 417 + return err 418 + } 419 + 420 + func GetIssueComments(e Execer, filters ...orm.Filter) ([]models.IssueComment, error) { 421 + commentMap := make(map[string]*models.IssueComment) 422 + 423 + var conditions []string 424 + var args []any 425 + for _, filter := range filters { 426 + conditions = append(conditions, filter.Condition()) 427 + args = append(args, filter.Arg()...) 428 + } 429 + 430 + whereClause := "" 431 + if conditions != nil { 432 + whereClause = " where " + strings.Join(conditions, " and ") 433 + } 434 + 435 + query := fmt.Sprintf(` 436 + select 437 + id, 438 + did, 439 + rkey, 440 + issue_at, 441 + reply_to, 442 + body, 443 + created, 444 + edited, 445 + deleted 446 + from 447 + issue_comments 448 + %s 449 + `, whereClause) 450 + 451 + rows, err := e.Query(query, args...) 452 + if err != nil { 453 + return nil, err 454 + } 455 + defer rows.Close() 456 + 457 + for rows.Next() { 458 + var comment models.IssueComment 459 + var created string 460 + var rkey, edited, deleted, replyTo sql.Null[string] 461 + err := rows.Scan( 462 + &comment.Id, 463 + &comment.Did, 464 + &rkey, 465 + &comment.IssueAt, 466 + &replyTo, 467 + &comment.Body, 468 + &created, 469 + &edited, 470 + &deleted, 471 + ) 472 + if err != nil { 473 + return nil, err 474 + } 475 + 476 + // this is a remnant from old times, newer comments always have rkey 477 + if rkey.Valid { 478 + comment.Rkey = rkey.V 479 + } 480 + 481 + if t, err := time.Parse(time.RFC3339, created); err == nil { 482 + comment.Created = t 483 + } 484 + 485 + if edited.Valid { 486 + if t, err := time.Parse(time.RFC3339, edited.V); err == nil { 487 + comment.Edited = &t 488 + } 489 + } 490 + 491 + if deleted.Valid { 492 + if t, err := time.Parse(time.RFC3339, deleted.V); err == nil { 493 + comment.Deleted = &t 494 + } 495 + } 496 + 497 + if replyTo.Valid { 498 + comment.ReplyTo = &replyTo.V 499 + } 500 + 501 + atUri := comment.AtUri().String() 502 + commentMap[atUri] = &comment 503 + } 504 + 505 + if err = rows.Err(); err != nil { 506 + return nil, err 507 + } 508 + 509 + // collect references for each comments 510 + commentAts := slices.Collect(maps.Keys(commentMap)) 511 + allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts)) 512 + if err != nil { 513 + return nil, fmt.Errorf("failed to query reference_links: %w", err) 514 + } 515 + for commentAt, references := range allReferencs { 516 + if comment, ok := commentMap[commentAt.String()]; ok { 517 + comment.References = references 518 + } 519 + } 520 + 521 + var comments []models.IssueComment 522 + for _, c := range commentMap { 523 + comments = append(comments, *c) 524 + } 525 + 526 + sort.Slice(comments, func(i, j int) bool { 527 + return comments[i].Created.After(comments[j].Created) 528 + }) 529 + 530 + return comments, nil 351 531 } 352 532 353 533 func DeleteIssues(tx *sql.Tx, did, rkey string) error {
+121 -6
appview/db/pulls.go
··· 447 447 return nil, err 448 448 } 449 449 450 - // Get comments for all submissions using GetComments 450 + // Get comments for all submissions using GetPullComments 451 451 submissionIds := slices.Collect(maps.Keys(submissionMap)) 452 - comments, err := GetComments(e, orm.FilterIn("pull_submission_id", submissionIds)) 452 + comments, err := GetPullComments(e, orm.FilterIn("submission_id", submissionIds)) 453 453 if err != nil { 454 454 return nil, fmt.Errorf("failed to get pull comments: %w", err) 455 455 } 456 456 for _, comment := range comments { 457 - if comment.PullSubmissionId != nil { 458 - if submission, ok := submissionMap[*comment.PullSubmissionId]; ok { 459 - submission.Comments = append(submission.Comments, comment) 460 - } 457 + if submission, ok := submissionMap[comment.SubmissionId]; ok { 458 + submission.Comments = append(submission.Comments, comment) 461 459 } 462 460 } 463 461 ··· 477 475 return m, nil 478 476 } 479 477 478 + func GetPullComments(e Execer, filters ...orm.Filter) ([]models.PullComment, error) { 479 + var conditions []string 480 + var args []any 481 + for _, filter := range filters { 482 + conditions = append(conditions, filter.Condition()) 483 + args = append(args, filter.Arg()...) 484 + } 485 + 486 + whereClause := "" 487 + if conditions != nil { 488 + whereClause = " where " + strings.Join(conditions, " and ") 489 + } 490 + 491 + query := fmt.Sprintf(` 492 + select 493 + id, 494 + pull_id, 495 + submission_id, 496 + repo_at, 497 + owner_did, 498 + comment_at, 499 + body, 500 + created 501 + from 502 + pull_comments 503 + %s 504 + order by 505 + created asc 506 + `, whereClause) 507 + 508 + rows, err := e.Query(query, args...) 509 + if err != nil { 510 + return nil, err 511 + } 512 + defer rows.Close() 513 + 514 + commentMap := make(map[string]*models.PullComment) 515 + for rows.Next() { 516 + var comment models.PullComment 517 + var createdAt string 518 + err := rows.Scan( 519 + &comment.ID, 520 + &comment.PullId, 521 + &comment.SubmissionId, 522 + &comment.RepoAt, 523 + &comment.OwnerDid, 524 + &comment.CommentAt, 525 + &comment.Body, 526 + &createdAt, 527 + ) 528 + if err != nil { 529 + return nil, err 530 + } 531 + 532 + if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 533 + comment.Created = t 534 + } 535 + 536 + atUri := comment.AtUri().String() 537 + commentMap[atUri] = &comment 538 + } 539 + 540 + if err := rows.Err(); err != nil { 541 + return nil, err 542 + } 543 + 544 + // collect references for each comments 545 + commentAts := slices.Collect(maps.Keys(commentMap)) 546 + allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts)) 547 + if err != nil { 548 + return nil, fmt.Errorf("failed to query reference_links: %w", err) 549 + } 550 + for commentAt, references := range allReferencs { 551 + if comment, ok := commentMap[commentAt.String()]; ok { 552 + comment.References = references 553 + } 554 + } 555 + 556 + var comments []models.PullComment 557 + for _, c := range commentMap { 558 + comments = append(comments, *c) 559 + } 560 + 561 + sort.Slice(comments, func(i, j int) bool { 562 + return comments[i].Created.Before(comments[j].Created) 563 + }) 564 + 565 + return comments, nil 566 + } 567 + 480 568 // timeframe here is directly passed into the sql query filter, and any 481 569 // timeframe in the past should be negative; e.g.: "-3 months" 482 570 func GetPullsByOwnerDid(e Execer, did, timeframe string) ([]models.Pull, error) { ··· 551 639 } 552 640 553 641 return pulls, nil 642 + } 643 + 644 + func NewPullComment(tx *sql.Tx, comment *models.PullComment) (int64, error) { 645 + query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)` 646 + res, err := tx.Exec( 647 + query, 648 + comment.OwnerDid, 649 + comment.RepoAt, 650 + comment.SubmissionId, 651 + comment.CommentAt, 652 + comment.PullId, 653 + comment.Body, 654 + ) 655 + if err != nil { 656 + return 0, err 657 + } 658 + 659 + i, err := res.LastInsertId() 660 + if err != nil { 661 + return 0, err 662 + } 663 + 664 + if err := putReferences(tx, comment.AtUri(), comment.References); err != nil { 665 + return 0, fmt.Errorf("put reference_links: %w", err) 666 + } 667 + 668 + return i, nil 554 669 } 555 670 556 671 func SetPullState(e Execer, repoAt syntax.ATURI, pullId int, pullState models.PullState) error {
+32 -20
appview/db/reference.go
··· 11 11 "tangled.org/core/orm" 12 12 ) 13 13 14 - // ValidateReferenceLinks resolves refLinks to Issue/PR/Comment ATURIs. 14 + // ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs. 15 15 // It will ignore missing refLinks. 16 16 func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 17 17 var ( ··· 53 53 values %s 54 54 ) 55 55 select 56 - i.at_uri, c.at_uri 56 + i.did, i.rkey, 57 + c.did, c.rkey 57 58 from input inp 58 59 join repos r 59 60 on r.did = inp.owner_did ··· 61 62 join issues i 62 63 on i.repo_at = r.at_uri 63 64 and i.issue_id = inp.issue_id 64 - left join comments c 65 + left join issue_comments c 65 66 on inp.comment_id is not null 66 - and c.subject_at = i.at_uri 67 + and c.issue_at = i.at_uri 67 68 and c.id = inp.comment_id 68 69 `, 69 70 strings.Join(vals, ","), ··· 78 79 79 80 for rows.Next() { 80 81 // Scan rows 81 - var issueUri string 82 - var commentUri sql.NullString 82 + var issueOwner, issueRkey string 83 + var commentOwner, commentRkey sql.NullString 83 84 var uri syntax.ATURI 84 - if err := rows.Scan(&issueUri, &commentUri); err != nil { 85 + if err := rows.Scan(&issueOwner, &issueRkey, &commentOwner, &commentRkey); err != nil { 85 86 return nil, err 86 87 } 87 - if commentUri.Valid { 88 - uri = syntax.ATURI(commentUri.String) 88 + if commentOwner.Valid && commentRkey.Valid { 89 + uri = syntax.ATURI(fmt.Sprintf( 90 + "at://%s/%s/%s", 91 + commentOwner.String, 92 + tangled.RepoIssueCommentNSID, 93 + commentRkey.String, 94 + )) 89 95 } else { 90 - uri = syntax.ATURI(issueUri) 96 + uri = syntax.ATURI(fmt.Sprintf( 97 + "at://%s/%s/%s", 98 + issueOwner, 99 + tangled.RepoIssueNSID, 100 + issueRkey, 101 + )) 91 102 } 92 103 uris = append(uris, uri) 93 104 } ··· 113 124 values %s 114 125 ) 115 126 select 116 - p.owner_did, p.rkey, c.at_uri 127 + p.owner_did, p.rkey, 128 + c.comment_at 117 129 from input inp 118 130 join repos r 119 131 on r.did = inp.owner_did ··· 121 133 join pulls p 122 134 on p.repo_at = r.at_uri 123 135 and p.pull_id = inp.pull_id 124 - left join comments c 136 + left join pull_comments c 125 137 on inp.comment_id is not null 126 - and c.subject_at = ('at://' || p.owner_did || '/' || 'sh.tangled.repo.pull' || '/' || p.rkey) 138 + and c.repo_at = r.at_uri and c.pull_id = p.pull_id 127 139 and c.id = inp.comment_id 128 140 `, 129 141 strings.Join(vals, ","), ··· 271 283 return nil, fmt.Errorf("get issue backlinks: %w", err) 272 284 } 273 285 backlinks = append(backlinks, ls...) 274 - ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.CommentNSID]) 286 + ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.RepoIssueCommentNSID]) 275 287 if err != nil { 276 288 return nil, fmt.Errorf("get issue_comment backlinks: %w", err) 277 289 } ··· 281 293 return nil, fmt.Errorf("get pull backlinks: %w", err) 282 294 } 283 295 backlinks = append(backlinks, ls...) 284 - ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.CommentNSID]) 296 + ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.RepoPullCommentNSID]) 285 297 if err != nil { 286 298 return nil, fmt.Errorf("get pull_comment backlinks: %w", err) 287 299 } ··· 340 352 rows, err := e.Query( 341 353 fmt.Sprintf( 342 354 `select r.did, r.name, i.issue_id, c.id, i.title, i.open 343 - from comments c 355 + from issue_comments c 344 356 join issues i 345 - on i.at_uri = c.subject_at 357 + on i.at_uri = c.issue_at 346 358 join repos r 347 359 on r.at_uri = i.repo_at 348 360 where %s`, ··· 416 428 if len(aturis) == 0 { 417 429 return nil, nil 418 430 } 419 - filter := orm.FilterIn("c.at_uri", aturis) 431 + filter := orm.FilterIn("c.comment_at", aturis) 420 432 rows, err := e.Query( 421 433 fmt.Sprintf( 422 434 `select r.did, r.name, p.pull_id, c.id, p.title, p.state 423 435 from repos r 424 436 join pulls p 425 437 on r.at_uri = p.repo_at 426 - join comments c 427 - on ('at://' || p.owner_did || '/' || 'sh.tangled.repo.pull' || '/' || p.rkey) = c.subject_at 438 + join pull_comments c 439 + on r.at_uri = c.repo_at and p.pull_id = c.pull_id 428 440 where %s`, 429 441 filter.Condition(), 430 442 ),
+11 -19
appview/ingester.go
··· 79 79 err = i.ingestString(e) 80 80 case tangled.RepoIssueNSID: 81 81 err = i.ingestIssue(ctx, e) 82 - case tangled.CommentNSID: 83 - err = i.ingestComment(e) 82 + case tangled.RepoIssueCommentNSID: 83 + err = i.ingestIssueComment(e) 84 84 case tangled.LabelDefinitionNSID: 85 85 err = i.ingestLabelDefinition(e) 86 86 case tangled.LabelOpNSID: ··· 868 868 return nil 869 869 } 870 870 871 - func (i *Ingester) ingestComment(e *jmodels.Event) error { 871 + func (i *Ingester) ingestIssueComment(e *jmodels.Event) error { 872 872 did := e.Did 873 873 rkey := e.Commit.RKey 874 874 875 875 var err error 876 876 877 - l := i.Logger.With("handler", "ingestComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) 877 + l := i.Logger.With("handler", "ingestIssueComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) 878 878 l.Info("ingesting record") 879 879 880 880 ddb, ok := i.Db.Execer.(*db.DB) ··· 885 885 switch e.Commit.Operation { 886 886 case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate: 887 887 raw := json.RawMessage(e.Commit.Record) 888 - record := tangled.Comment{} 888 + record := tangled.RepoIssueComment{} 889 889 err = json.Unmarshal(raw, &record) 890 890 if err != nil { 891 891 return fmt.Errorf("invalid record: %w", err) 892 892 } 893 893 894 - comment, err := models.CommentFromRecord(did, rkey, record) 894 + comment, err := models.IssueCommentFromRecord(did, rkey, record) 895 895 if err != nil { 896 896 return fmt.Errorf("failed to parse comment from record: %w", err) 897 897 } 898 898 899 - // TODO: ingest pull comments 900 - // we aren't ingesting pull comments yet because pull itself isn't fully atprotated. 901 - // so we cannot know which round this comment is pointing to 902 - if comment.Subject.Collection().String() == tangled.RepoPullNSID { 903 - l.Info("skip ingesting pull comments") 904 - return nil 905 - } 906 - 907 - if err := comment.Validate(); err != nil { 899 + if err := i.Validator.ValidateIssueComment(comment); err != nil { 908 900 return fmt.Errorf("failed to validate comment: %w", err) 909 901 } 910 902 ··· 914 906 } 915 907 defer tx.Rollback() 916 908 917 - err = db.PutComment(tx, comment) 909 + _, err = db.AddIssueComment(tx, *comment) 918 910 if err != nil { 919 - return fmt.Errorf("failed to create comment: %w", err) 911 + return fmt.Errorf("failed to create issue comment: %w", err) 920 912 } 921 913 922 914 return tx.Commit() 923 915 924 916 case jmodels.CommitOperationDelete: 925 - if err := db.DeleteComments( 917 + if err := db.DeleteIssueComments( 926 918 ddb, 927 919 orm.FilterEq("did", did), 928 920 orm.FilterEq("rkey", rkey), 929 921 ); err != nil { 930 - return fmt.Errorf("failed to delete comment record: %w", err) 922 + return fmt.Errorf("failed to delete issue comment record: %w", err) 931 923 } 932 924 933 925 return nil
+29 -31
appview/issues/issues.go
··· 403 403 404 404 body := r.FormValue("body") 405 405 if body == "" { 406 - rp.pages.Notice(w, "issue-comment", "Body is required") 406 + rp.pages.Notice(w, "issue", "Body is required") 407 407 return 408 408 } 409 409 410 - var replyTo *syntax.ATURI 411 - replyToRaw := r.FormValue("reply-to") 412 - if replyToRaw != "" { 413 - aturi, err := syntax.ParseATURI(r.FormValue("reply-to")) 414 - if err != nil { 415 - rp.pages.Notice(w, "issue-comment", "reply-to should be valid AT-URI") 416 - return 417 - } 418 - replyTo = &aturi 410 + replyToUri := r.FormValue("reply-to") 411 + var replyTo *string 412 + if replyToUri != "" { 413 + replyTo = &replyToUri 419 414 } 420 415 421 416 mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) 422 417 423 - comment := models.Comment{ 424 - Did: syntax.DID(user.Did), 418 + comment := models.IssueComment{ 419 + Did: user.Did, 425 420 Rkey: tid.TID(), 426 - Subject: issue.AtUri(), 421 + IssueAt: issue.AtUri().String(), 427 422 ReplyTo: replyTo, 428 423 Body: body, 429 424 Created: time.Now(), 430 425 Mentions: mentions, 431 426 References: references, 432 427 } 433 - if err = comment.Validate(); err != nil { 428 + if err = rp.validator.ValidateIssueComment(&comment); err != nil { 434 429 l.Error("failed to validate comment", "err", err) 435 430 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 436 431 return ··· 446 441 447 442 // create a record first 448 443 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 449 - Collection: tangled.CommentNSID, 450 - Repo: user.Did, 444 + Collection: tangled.RepoIssueCommentNSID, 445 + Repo: comment.Did, 451 446 Rkey: comment.Rkey, 452 447 Record: &lexutil.LexiconTypeDecoder{ 453 448 Val: &record, ··· 473 468 } 474 469 defer tx.Rollback() 475 470 476 - err = db.PutComment(tx, &comment) 471 + commentId, err := db.AddIssueComment(tx, comment) 477 472 if err != nil { 478 473 l.Error("failed to create comment", "err", err) 479 474 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") ··· 489 484 // reset atUri to make rollback a no-op 490 485 atUri = "" 491 486 492 - rp.notifier.NewComment(r.Context(), &comment) 487 + // notify about the new comment 488 + comment.Id = commentId 489 + 490 + rp.notifier.NewIssueComment(r.Context(), &comment, mentions) 493 491 494 492 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 495 - rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, comment.Id)) 493 + rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, commentId)) 496 494 } 497 495 498 496 func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) { ··· 507 505 } 508 506 509 507 commentId := chi.URLParam(r, "commentId") 510 - comments, err := db.GetComments( 508 + comments, err := db.GetIssueComments( 511 509 rp.db, 512 510 orm.FilterEq("id", commentId), 513 511 ) ··· 543 541 } 544 542 545 543 commentId := chi.URLParam(r, "commentId") 546 - comments, err := db.GetComments( 544 + comments, err := db.GetIssueComments( 547 545 rp.db, 548 546 orm.FilterEq("id", commentId), 549 547 ) ··· 559 557 } 560 558 comment := comments[0] 561 559 562 - if comment.Did.String() != user.Did { 560 + if comment.Did != user.Did { 563 561 l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did) 564 562 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 565 563 return ··· 599 597 } 600 598 defer tx.Rollback() 601 599 602 - err = db.PutComment(tx, &newComment) 600 + _, err = db.AddIssueComment(tx, newComment) 603 601 if err != nil { 604 602 l.Error("failed to perferom update-description query", "err", err) 605 603 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") ··· 610 608 // rkey is optional, it was introduced later 611 609 if newComment.Rkey != "" { 612 610 // update the record on pds 613 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.CommentNSID, user.Did, comment.Rkey) 611 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey) 614 612 if err != nil { 615 613 l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey) 616 614 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") ··· 618 616 } 619 617 620 618 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 621 - Collection: tangled.CommentNSID, 619 + Collection: tangled.RepoIssueCommentNSID, 622 620 Repo: user.Did, 623 621 Rkey: newComment.Rkey, 624 622 SwapRecord: ex.Cid, ··· 653 651 } 654 652 655 653 commentId := chi.URLParam(r, "commentId") 656 - comments, err := db.GetComments( 654 + comments, err := db.GetIssueComments( 657 655 rp.db, 658 656 orm.FilterEq("id", commentId), 659 657 ) ··· 689 687 } 690 688 691 689 commentId := chi.URLParam(r, "commentId") 692 - comments, err := db.GetComments( 690 + comments, err := db.GetIssueComments( 693 691 rp.db, 694 692 orm.FilterEq("id", commentId), 695 693 ) ··· 725 723 } 726 724 727 725 commentId := chi.URLParam(r, "commentId") 728 - comments, err := db.GetComments( 726 + comments, err := db.GetIssueComments( 729 727 rp.db, 730 728 orm.FilterEq("id", commentId), 731 729 ) ··· 741 739 } 742 740 comment := comments[0] 743 741 744 - if comment.Did.String() != user.Did { 742 + if comment.Did != user.Did { 745 743 l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did) 746 744 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 747 745 return ··· 754 752 755 753 // optimistic deletion 756 754 deleted := time.Now() 757 - err = db.DeleteComments(rp.db, orm.FilterEq("id", comment.Id)) 755 + err = db.DeleteIssueComments(rp.db, orm.FilterEq("id", comment.Id)) 758 756 if err != nil { 759 757 l.Error("failed to delete comment", "err", err) 760 758 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") ··· 770 768 return 771 769 } 772 770 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 773 - Collection: tangled.CommentNSID, 771 + Collection: tangled.RepoIssueCommentNSID, 774 772 Repo: user.Did, 775 773 Rkey: comment.Rkey, 776 774 })
+2 -2
appview/issues/opengraph.go
··· 193 193 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 194 194 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 195 195 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 196 - err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor) 196 + err = dollyArea.DrawDolly(dollyX, dollyY, dollySize, dollyColor) 197 197 if err != nil { 198 - log.Printf("dolly silhouette not available (this is ok): %v", err) 198 + log.Printf("dolly not available (this is ok): %v", err) 199 199 } 200 200 201 201 // Draw "opened by @author" and date at the bottom with more spacing
-117
appview/models/comment.go
··· 1 - package models 2 - 3 - import ( 4 - "fmt" 5 - "strings" 6 - "time" 7 - 8 - "github.com/bluesky-social/indigo/atproto/syntax" 9 - "tangled.org/core/api/tangled" 10 - ) 11 - 12 - type Comment struct { 13 - Id int64 14 - Did syntax.DID 15 - Rkey string 16 - Subject syntax.ATURI 17 - ReplyTo *syntax.ATURI 18 - Body string 19 - Created time.Time 20 - Edited *time.Time 21 - Deleted *time.Time 22 - Mentions []syntax.DID 23 - References []syntax.ATURI 24 - PullSubmissionId *int 25 - } 26 - 27 - func (c *Comment) AtUri() syntax.ATURI { 28 - return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", c.Did, tangled.CommentNSID, c.Rkey)) 29 - } 30 - 31 - func (c *Comment) AsRecord() tangled.Comment { 32 - mentions := make([]string, len(c.Mentions)) 33 - for i, did := range c.Mentions { 34 - mentions[i] = string(did) 35 - } 36 - references := make([]string, len(c.References)) 37 - for i, uri := range c.References { 38 - references[i] = string(uri) 39 - } 40 - var replyTo *string 41 - if c.ReplyTo != nil { 42 - replyToStr := c.ReplyTo.String() 43 - replyTo = &replyToStr 44 - } 45 - return tangled.Comment{ 46 - Subject: c.Subject.String(), 47 - Body: c.Body, 48 - CreatedAt: c.Created.Format(time.RFC3339), 49 - ReplyTo: replyTo, 50 - Mentions: mentions, 51 - References: references, 52 - } 53 - } 54 - 55 - func (c *Comment) IsTopLevel() bool { 56 - return c.ReplyTo == nil 57 - } 58 - 59 - func (c *Comment) IsReply() bool { 60 - return c.ReplyTo != nil 61 - } 62 - 63 - func (c *Comment) Validate() error { 64 - // TODO: sanitize the body and then trim space 65 - if sb := strings.TrimSpace(c.Body); sb == "" { 66 - return fmt.Errorf("body is empty after HTML sanitization") 67 - } 68 - 69 - // if it's for PR, PullSubmissionId should not be nil 70 - if c.Subject.Collection().String() == tangled.RepoPullNSID { 71 - if c.PullSubmissionId == nil { 72 - return fmt.Errorf("PullSubmissionId should not be nil") 73 - } 74 - } 75 - return nil 76 - } 77 - 78 - func CommentFromRecord(did, rkey string, record tangled.Comment) (*Comment, error) { 79 - created, err := time.Parse(time.RFC3339, record.CreatedAt) 80 - if err != nil { 81 - created = time.Now() 82 - } 83 - 84 - ownerDid := did 85 - 86 - if _, err = syntax.ParseATURI(record.Subject); err != nil { 87 - return nil, err 88 - } 89 - 90 - i := record 91 - mentions := make([]syntax.DID, len(record.Mentions)) 92 - for i, did := range record.Mentions { 93 - mentions[i] = syntax.DID(did) 94 - } 95 - references := make([]syntax.ATURI, len(record.References)) 96 - for i, uri := range i.References { 97 - references[i] = syntax.ATURI(uri) 98 - } 99 - var replyTo *syntax.ATURI 100 - if record.ReplyTo != nil { 101 - replyToAtUri := syntax.ATURI(*record.ReplyTo) 102 - replyTo = &replyToAtUri 103 - } 104 - 105 - comment := Comment{ 106 - Did: syntax.DID(ownerDid), 107 - Rkey: rkey, 108 - Body: record.Body, 109 - Subject: syntax.ATURI(record.Subject), 110 - ReplyTo: replyTo, 111 - Created: created, 112 - Mentions: mentions, 113 - References: references, 114 - } 115 - 116 - return &comment, nil 117 - }
+89 -8
appview/models/issue.go
··· 26 26 27 27 // optionally, populate this when querying for reverse mappings 28 28 // like comment counts, parent repo etc. 29 - Comments []Comment 29 + Comments []IssueComment 30 30 Labels LabelState 31 31 Repo *Repo 32 32 } ··· 62 62 } 63 63 64 64 type CommentListItem struct { 65 - Self *Comment 66 - Replies []*Comment 65 + Self *IssueComment 66 + Replies []*IssueComment 67 67 } 68 68 69 69 func (it *CommentListItem) Participants() []syntax.DID { ··· 88 88 89 89 func (i *Issue) CommentList() []CommentListItem { 90 90 // Create a map to quickly find comments by their aturi 91 - toplevel := make(map[syntax.ATURI]*CommentListItem) 92 - var replies []*Comment 91 + toplevel := make(map[string]*CommentListItem) 92 + var replies []*IssueComment 93 93 94 94 // collect top level comments into the map 95 95 for _, comment := range i.Comments { 96 96 if comment.IsTopLevel() { 97 - toplevel[comment.AtUri()] = &CommentListItem{ 97 + toplevel[comment.AtUri().String()] = &CommentListItem{ 98 98 Self: &comment, 99 99 } 100 100 } else { ··· 115 115 } 116 116 117 117 // sort everything 118 - sortFunc := func(a, b *Comment) bool { 118 + sortFunc := func(a, b *IssueComment) bool { 119 119 return a.Created.Before(b.Created) 120 120 } 121 121 sort.Slice(listing, func(i, j int) bool { ··· 144 144 addParticipant(i.Did) 145 145 146 146 for _, c := range i.Comments { 147 - addParticipant(c.Did.String()) 147 + addParticipant(c.Did) 148 148 } 149 149 150 150 return participants ··· 171 171 Open: true, // new issues are open by default 172 172 } 173 173 } 174 + 175 + type IssueComment struct { 176 + Id int64 177 + Did string 178 + Rkey string 179 + IssueAt string 180 + ReplyTo *string 181 + Body string 182 + Created time.Time 183 + Edited *time.Time 184 + Deleted *time.Time 185 + Mentions []syntax.DID 186 + References []syntax.ATURI 187 + } 188 + 189 + func (i *IssueComment) AtUri() syntax.ATURI { 190 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueCommentNSID, i.Rkey)) 191 + } 192 + 193 + func (i *IssueComment) AsRecord() tangled.RepoIssueComment { 194 + mentions := make([]string, len(i.Mentions)) 195 + for i, did := range i.Mentions { 196 + mentions[i] = string(did) 197 + } 198 + references := make([]string, len(i.References)) 199 + for i, uri := range i.References { 200 + references[i] = string(uri) 201 + } 202 + return tangled.RepoIssueComment{ 203 + Body: i.Body, 204 + Issue: i.IssueAt, 205 + CreatedAt: i.Created.Format(time.RFC3339), 206 + ReplyTo: i.ReplyTo, 207 + Mentions: mentions, 208 + References: references, 209 + } 210 + } 211 + 212 + func (i *IssueComment) IsTopLevel() bool { 213 + return i.ReplyTo == nil 214 + } 215 + 216 + func (i *IssueComment) IsReply() bool { 217 + return i.ReplyTo != nil 218 + } 219 + 220 + func IssueCommentFromRecord(did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) { 221 + created, err := time.Parse(time.RFC3339, record.CreatedAt) 222 + if err != nil { 223 + created = time.Now() 224 + } 225 + 226 + ownerDid := did 227 + 228 + if _, err = syntax.ParseATURI(record.Issue); err != nil { 229 + return nil, err 230 + } 231 + 232 + i := record 233 + mentions := make([]syntax.DID, len(record.Mentions)) 234 + for i, did := range record.Mentions { 235 + mentions[i] = syntax.DID(did) 236 + } 237 + references := make([]syntax.ATURI, len(record.References)) 238 + for i, uri := range i.References { 239 + references[i] = syntax.ATURI(uri) 240 + } 241 + 242 + comment := IssueComment{ 243 + Did: ownerDid, 244 + Rkey: rkey, 245 + Body: record.Body, 246 + IssueAt: record.Issue, 247 + ReplyTo: record.ReplyTo, 248 + Created: created, 249 + Mentions: mentions, 250 + References: references, 251 + } 252 + 253 + return &comment, nil 254 + }
+46 -2
appview/models/pull.go
··· 138 138 RoundNumber int 139 139 Patch string 140 140 Combined string 141 - Comments []Comment 141 + Comments []PullComment 142 142 SourceRev string // include the rev that was used to create this submission: only for branch/fork PRs 143 143 144 144 // meta 145 145 Created time.Time 146 146 } 147 + 148 + type PullComment struct { 149 + // ids 150 + ID int 151 + PullId int 152 + SubmissionId int 153 + 154 + // at ids 155 + RepoAt string 156 + OwnerDid string 157 + CommentAt string 158 + 159 + // content 160 + Body string 161 + 162 + // meta 163 + Mentions []syntax.DID 164 + References []syntax.ATURI 165 + 166 + // meta 167 + Created time.Time 168 + } 169 + 170 + func (p *PullComment) AtUri() syntax.ATURI { 171 + return syntax.ATURI(p.CommentAt) 172 + } 173 + 174 + // func (p *PullComment) AsRecord() tangled.RepoPullComment { 175 + // mentions := make([]string, len(p.Mentions)) 176 + // for i, did := range p.Mentions { 177 + // mentions[i] = string(did) 178 + // } 179 + // references := make([]string, len(p.References)) 180 + // for i, uri := range p.References { 181 + // references[i] = string(uri) 182 + // } 183 + // return tangled.RepoPullComment{ 184 + // Pull: p.PullAt, 185 + // Body: p.Body, 186 + // Mentions: mentions, 187 + // References: references, 188 + // CreatedAt: p.Created.Format(time.RFC3339), 189 + // } 190 + // } 147 191 148 192 func (p *Pull) LastRoundNumber() int { 149 193 return len(p.Submissions) - 1 ··· 245 289 addParticipant(s.PullAt.Authority().String()) 246 290 247 291 for _, c := range s.Comments { 248 - addParticipant(c.Did.String()) 292 + addParticipant(c.OwnerDid) 249 293 } 250 294 251 295 return participants
+113 -111
appview/notify/db/db.go
··· 74 74 // no-op 75 75 } 76 76 77 - func (n *databaseNotifier) NewComment(ctx context.Context, comment *models.Comment) { 78 - var ( 79 - // built the recipients list: 80 - // - the owner of the repo 81 - // - | if the comment is a reply -> everybody on that thread 82 - // | if the comment is a top level -> just the issue owner 83 - // - remove mentioned users from the recipients list 84 - recipients = sets.New[syntax.DID]() 85 - entityType string 86 - entityId string 87 - repoId *int64 88 - issueId *int64 89 - pullId *int64 90 - ) 91 - 92 - subjectDid, err := comment.Subject.Authority().AsDID() 77 + func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 78 + collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt())) 93 79 if err != nil { 94 - log.Printf("NewComment: expected did based at-uri for comment.subject") 80 + log.Printf("failed to fetch collaborators: %v", err) 95 81 return 96 82 } 97 - switch comment.Subject.Collection() { 98 - case tangled.RepoIssueNSID: 99 - issues, err := db.GetIssues( 100 - n.db, 101 - orm.FilterEq("did", subjectDid), 102 - orm.FilterEq("rkey", comment.Subject.RecordKey()), 103 - ) 104 - if err != nil { 105 - log.Printf("NewComment: failed to get issues: %v", err) 106 - return 107 - } 108 - if len(issues) == 0 { 109 - log.Printf("NewComment: no issue found for %s", comment.Subject) 110 - return 111 - } 112 - issue := issues[0] 113 83 114 - recipients.Insert(syntax.DID(issue.Repo.Did)) 115 - if comment.IsReply() { 116 - // if this comment is a reply, then notify everybody in that thread 117 - parentAtUri := *comment.ReplyTo 118 - 119 - // find the parent thread, and add all DIDs from here to the recipient list 120 - for _, t := range issue.CommentList() { 121 - if t.Self.AtUri() == parentAtUri { 122 - for _, p := range t.Participants() { 123 - recipients.Insert(p) 124 - } 125 - } 126 - } 127 - } else { 128 - // not a reply, notify just the issue author 129 - recipients.Insert(syntax.DID(issue.Did)) 130 - } 131 - 132 - entityType = "issue" 133 - entityId = issue.AtUri().String() 134 - repoId = &issue.Repo.Id 135 - issueId = &issue.Id 136 - case tangled.RepoPullNSID: 137 - pulls, err := db.GetPullsWithLimit( 138 - n.db, 139 - 1, 140 - orm.FilterEq("owner_did", subjectDid), 141 - orm.FilterEq("rkey", comment.Subject.RecordKey()), 142 - ) 143 - if err != nil { 144 - log.Printf("NewComment: failed to get pulls: %v", err) 145 - return 146 - } 147 - if len(pulls) == 0 { 148 - log.Printf("NewComment: no pull found for %s", comment.Subject) 149 - return 150 - } 151 - pull := pulls[0] 152 - 153 - pull.Repo, err = db.GetRepo(n.db, orm.FilterEq("at_uri", pull.RepoAt)) 154 - if err != nil { 155 - log.Printf("NewComment: failed to get repos: %v", err) 156 - return 157 - } 158 - 159 - recipients.Insert(syntax.DID(pull.Repo.Did)) 160 - for _, p := range pull.Participants() { 161 - recipients.Insert(syntax.DID(p)) 162 - } 163 - 164 - entityType = "pull" 165 - entityId = pull.AtUri().String() 166 - repoId = &pull.Repo.Id 167 - p := int64(pull.ID) 168 - pullId = &p 169 - default: 170 - return // no-op 84 + // build the recipients list 85 + // - owner of the repo 86 + // - collaborators in the repo 87 + // - remove users already mentioned 88 + recipients := sets.Singleton(syntax.DID(issue.Repo.Did)) 89 + for _, c := range collaborators { 90 + recipients.Insert(c.SubjectDid) 171 91 } 172 - 173 - for _, m := range comment.Mentions { 92 + for _, m := range mentions { 174 93 recipients.Remove(m) 175 94 } 176 95 96 + actorDid := syntax.DID(issue.Did) 97 + entityType := "issue" 98 + entityId := issue.AtUri().String() 99 + repoId := &issue.Repo.Id 100 + issueId := &issue.Id 101 + var pullId *int64 102 + 177 103 n.notifyEvent( 178 - comment.Did, 104 + actorDid, 179 105 recipients, 180 - models.NotificationTypeIssueCommented, 106 + models.NotificationTypeIssueCreated, 181 107 entityType, 182 108 entityId, 183 109 repoId, ··· 185 111 pullId, 186 112 ) 187 113 n.notifyEvent( 188 - comment.Did, 189 - sets.Collect(slices.Values(comment.Mentions)), 114 + actorDid, 115 + sets.Collect(slices.Values(mentions)), 190 116 models.NotificationTypeUserMentioned, 191 117 entityType, 192 118 entityId, ··· 196 122 ) 197 123 } 198 124 199 - func (n *databaseNotifier) DeleteComment(ctx context.Context, comment *models.Comment) { 200 - // no-op 201 - } 202 - 203 - func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 204 - collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt())) 125 + func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 126 + issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.IssueAt)) 205 127 if err != nil { 206 - log.Printf("failed to fetch collaborators: %v", err) 128 + log.Printf("NewIssueComment: failed to get issues: %v", err) 129 + return 130 + } 131 + if len(issues) == 0 { 132 + log.Printf("NewIssueComment: no issue found for %s", comment.IssueAt) 207 133 return 208 134 } 135 + issue := issues[0] 209 136 210 - // build the recipients list 211 - // - owner of the repo 212 - // - collaborators in the repo 213 - // - remove users already mentioned 137 + // built the recipients list: 138 + // - the owner of the repo 139 + // - | if the comment is a reply -> everybody on that thread 140 + // | if the comment is a top level -> just the issue owner 141 + // - remove mentioned users from the recipients list 214 142 recipients := sets.Singleton(syntax.DID(issue.Repo.Did)) 215 - for _, c := range collaborators { 216 - recipients.Insert(c.SubjectDid) 143 + 144 + if comment.IsReply() { 145 + // if this comment is a reply, then notify everybody in that thread 146 + parentAtUri := *comment.ReplyTo 147 + 148 + // find the parent thread, and add all DIDs from here to the recipient list 149 + for _, t := range issue.CommentList() { 150 + if t.Self.AtUri().String() == parentAtUri { 151 + for _, p := range t.Participants() { 152 + recipients.Insert(p) 153 + } 154 + } 155 + } 156 + } else { 157 + // not a reply, notify just the issue author 158 + recipients.Insert(syntax.DID(issue.Did)) 217 159 } 160 + 218 161 for _, m := range mentions { 219 162 recipients.Remove(m) 220 163 } 221 164 222 - actorDid := syntax.DID(issue.Did) 165 + actorDid := syntax.DID(comment.Did) 223 166 entityType := "issue" 224 167 entityId := issue.AtUri().String() 225 168 repoId := &issue.Repo.Id ··· 229 172 n.notifyEvent( 230 173 actorDid, 231 174 recipients, 232 - models.NotificationTypeIssueCreated, 175 + models.NotificationTypeIssueCommented, 233 176 entityType, 234 177 entityId, 235 178 repoId, ··· 309 252 actorDid, 310 253 recipients, 311 254 eventType, 255 + entityType, 256 + entityId, 257 + repoId, 258 + issueId, 259 + pullId, 260 + ) 261 + } 262 + 263 + func (n *databaseNotifier) NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) { 264 + pull, err := db.GetPull(n.db, 265 + syntax.ATURI(comment.RepoAt), 266 + comment.PullId, 267 + ) 268 + if err != nil { 269 + log.Printf("NewPullComment: failed to get pulls: %v", err) 270 + return 271 + } 272 + 273 + repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", comment.RepoAt)) 274 + if err != nil { 275 + log.Printf("NewPullComment: failed to get repos: %v", err) 276 + return 277 + } 278 + 279 + // build up the recipients list: 280 + // - repo owner 281 + // - all pull participants 282 + // - remove those already mentioned 283 + recipients := sets.Singleton(syntax.DID(repo.Did)) 284 + for _, p := range pull.Participants() { 285 + recipients.Insert(syntax.DID(p)) 286 + } 287 + for _, m := range mentions { 288 + recipients.Remove(m) 289 + } 290 + 291 + actorDid := syntax.DID(comment.OwnerDid) 292 + eventType := models.NotificationTypePullCommented 293 + entityType := "pull" 294 + entityId := pull.AtUri().String() 295 + repoId := &repo.Id 296 + var issueId *int64 297 + p := int64(pull.ID) 298 + pullId := &p 299 + 300 + n.notifyEvent( 301 + actorDid, 302 + recipients, 303 + eventType, 304 + entityType, 305 + entityId, 306 + repoId, 307 + issueId, 308 + pullId, 309 + ) 310 + n.notifyEvent( 311 + actorDid, 312 + sets.Collect(slices.Values(mentions)), 313 + models.NotificationTypeUserMentioned, 312 314 entityType, 313 315 entityId, 314 316 repoId,
+8 -8
appview/notify/merged_notifier.go
··· 53 53 m.fanout("DeleteStar", ctx, star) 54 54 } 55 55 56 - func (m *mergedNotifier) NewComment(ctx context.Context, comment *models.Comment) { 57 - m.fanout("NewComment", ctx, comment) 58 - } 59 - 60 - func (m *mergedNotifier) DeleteComment(ctx context.Context, comment *models.Comment) { 61 - m.fanout("DeleteComment", ctx, comment) 62 - } 63 - 64 56 func (m *mergedNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 65 57 m.fanout("NewIssue", ctx, issue, mentions) 58 + } 59 + 60 + func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 61 + m.fanout("NewIssueComment", ctx, comment, mentions) 66 62 } 67 63 68 64 func (m *mergedNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) { ··· 83 79 84 80 func (m *mergedNotifier) NewPull(ctx context.Context, pull *models.Pull) { 85 81 m.fanout("NewPull", ctx, pull) 82 + } 83 + 84 + func (m *mergedNotifier) NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) { 85 + m.fanout("NewPullComment", ctx, comment, mentions) 86 86 } 87 87 88 88 func (m *mergedNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {
+7 -7
appview/notify/notifier.go
··· 13 13 NewStar(ctx context.Context, star *models.Star) 14 14 DeleteStar(ctx context.Context, star *models.Star) 15 15 16 - NewComment(ctx context.Context, comment *models.Comment) 17 - DeleteComment(ctx context.Context, comment *models.Comment) 18 - 19 16 NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) 17 + NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) 20 18 NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) 21 19 DeleteIssue(ctx context.Context, issue *models.Issue) 22 20 ··· 24 22 DeleteFollow(ctx context.Context, follow *models.Follow) 25 23 26 24 NewPull(ctx context.Context, pull *models.Pull) 25 + NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) 27 26 NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) 28 27 29 28 UpdateProfile(ctx context.Context, profile *models.Profile) ··· 43 42 func (m *BaseNotifier) NewStar(ctx context.Context, star *models.Star) {} 44 43 func (m *BaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {} 45 44 46 - func (m *BaseNotifier) NewComment(ctx context.Context, comment *models.Comment) {} 47 - func (m *BaseNotifier) DeleteComment(ctx context.Context, comment *models.Comment) {} 48 - 49 45 func (m *BaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {} 46 + func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 47 + } 50 48 func (m *BaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {} 51 49 func (m *BaseNotifier) DeleteIssue(ctx context.Context, issue *models.Issue) {} 52 50 53 51 func (m *BaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {} 54 52 func (m *BaseNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {} 55 53 56 - func (m *BaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {} 54 + func (m *BaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {} 55 + func (m *BaseNotifier) NewPullComment(ctx context.Context, models *models.PullComment, mentions []syntax.DID) { 56 + } 57 57 func (m *BaseNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {} 58 58 59 59 func (m *BaseNotifier) UpdateProfile(ctx context.Context, profile *models.Profile) {}
+20 -5
appview/notify/posthog/notifier.go
··· 86 86 } 87 87 } 88 88 89 + func (n *posthogNotifier) NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) { 90 + err := n.client.Enqueue(posthog.Capture{ 91 + DistinctId: comment.OwnerDid, 92 + Event: "new_pull_comment", 93 + Properties: posthog.Properties{ 94 + "repo_at": comment.RepoAt, 95 + "pull_id": comment.PullId, 96 + "mentions": mentions, 97 + }, 98 + }) 99 + if err != nil { 100 + log.Println("failed to enqueue posthog event:", err) 101 + } 102 + } 103 + 89 104 func (n *posthogNotifier) NewPullClosed(ctx context.Context, pull *models.Pull) { 90 105 err := n.client.Enqueue(posthog.Capture{ 91 106 DistinctId: pull.OwnerDid, ··· 165 180 } 166 181 } 167 182 168 - func (n *posthogNotifier) NewComment(ctx context.Context, comment *models.Comment) { 183 + func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 169 184 err := n.client.Enqueue(posthog.Capture{ 170 - DistinctId: comment.Did.String(), 171 - Event: "new_comment", 185 + DistinctId: comment.Did, 186 + Event: "new_issue_comment", 172 187 Properties: posthog.Properties{ 173 - "subject_at": comment.Subject, 174 - "mentions": comment.Mentions, 188 + "issue_at": comment.IssueAt, 189 + "mentions": mentions, 175 190 }, 176 191 }) 177 192 if err != nil {
+9 -9
appview/ogcard/card.go
··· 334 334 return nil 335 335 } 336 336 337 - func (c *Card) DrawDollySilhouette(x, y, size int, iconColor color.Color) error { 337 + func (c *Card) DrawDolly(x, y, size int, iconColor color.Color) error { 338 338 tpl, err := template.New("dolly"). 339 - ParseFS(pages.Files, "templates/fragments/dolly/silhouette.html") 339 + ParseFS(pages.Files, "templates/fragments/dolly/logo.html") 340 340 if err != nil { 341 - return fmt.Errorf("failed to read dolly silhouette template: %w", err) 341 + return fmt.Errorf("failed to read dolly template: %w", err) 342 342 } 343 343 344 344 var svgData bytes.Buffer 345 - if err = tpl.ExecuteTemplate(&svgData, "fragments/dolly/silhouette", nil); err != nil { 346 - return fmt.Errorf("failed to execute dolly silhouette template: %w", err) 345 + if err = tpl.ExecuteTemplate(&svgData, "fragments/dolly/logo", nil); err != nil { 346 + return fmt.Errorf("failed to execute dolly template: %w", err) 347 347 } 348 348 349 349 icon, err := BuildSVGIconFromData(svgData.Bytes(), iconColor) ··· 453 453 454 454 // Handle SVG separately 455 455 if contentType == "image/svg+xml" || strings.HasSuffix(url, ".svg") { 456 - return c.convertSVGToPNG(bodyBytes) 456 + return convertSVGToPNG(bodyBytes) 457 457 } 458 458 459 459 // Support content types are in-sync with the allowed custom avatar file types ··· 493 493 } 494 494 495 495 // convertSVGToPNG converts SVG data to a PNG image 496 - func (c *Card) convertSVGToPNG(svgData []byte) (image.Image, bool) { 496 + func convertSVGToPNG(svgData []byte) (image.Image, bool) { 497 497 // Parse the SVG 498 498 icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData)) 499 499 if err != nil { ··· 547 547 draw.CatmullRom.Scale(scaledImg, scaledImg.Bounds(), img, srcBounds, draw.Src, nil) 548 548 549 549 // Draw the image with circular clipping 550 - for cy := 0; cy < size; cy++ { 551 - for cx := 0; cx < size; cx++ { 550 + for cy := range size { 551 + for cx := range size { 552 552 // Calculate distance from center 553 553 dx := float64(cx - center) 554 554 dy := float64(cy - center)
+16 -5
appview/pages/pages.go
··· 210 210 return tpl.ExecuteTemplate(w, "layouts/base", params) 211 211 } 212 212 213 + type DollyParams struct { 214 + Classes string 215 + FillColor string 216 + } 217 + 218 + func (p *Pages) Dolly(w io.Writer, params DollyParams) error { 219 + return p.executePlain("fragments/dolly/logo", w, params) 220 + } 221 + 213 222 func (p *Pages) Favicon(w io.Writer) error { 214 - return p.executePlain("fragments/dolly/silhouette", w, nil) 223 + return p.Dolly(w, DollyParams{ 224 + Classes: "text-black dark:text-white", 225 + }) 215 226 } 216 227 217 228 type LoginParams struct { ··· 988 999 LoggedInUser *oauth.User 989 1000 RepoInfo repoinfo.RepoInfo 990 1001 Issue *models.Issue 991 - Comment *models.Comment 1002 + Comment *models.IssueComment 992 1003 } 993 1004 994 1005 func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { ··· 999 1010 LoggedInUser *oauth.User 1000 1011 RepoInfo repoinfo.RepoInfo 1001 1012 Issue *models.Issue 1002 - Comment *models.Comment 1013 + Comment *models.IssueComment 1003 1014 } 1004 1015 1005 1016 func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { ··· 1010 1021 LoggedInUser *oauth.User 1011 1022 RepoInfo repoinfo.RepoInfo 1012 1023 Issue *models.Issue 1013 - Comment *models.Comment 1024 + Comment *models.IssueComment 1014 1025 } 1015 1026 1016 1027 func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { ··· 1021 1032 LoggedInUser *oauth.User 1022 1033 RepoInfo repoinfo.RepoInfo 1023 1034 Issue *models.Issue 1024 - Comment *models.Comment 1035 + Comment *models.IssueComment 1025 1036 } 1026 1037 1027 1038 func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error {
+9 -29
appview/pages/templates/brand/brand.html
··· 4 4 <div class="grid grid-cols-10"> 5 5 <header class="col-span-full md:col-span-10 px-6 py-2 mb-4"> 6 6 <h1 class="text-2xl font-bold dark:text-white mb-1">Brand</h1> 7 - <p class="text-gray-600 dark:text-gray-400 mb-1"> 7 + <p class="text-gray-500 dark:text-gray-300 mb-1"> 8 8 Assets and guidelines for using Tangled's logo and brand elements. 9 9 </p> 10 10 </header> ··· 14 14 15 15 <!-- Introduction Section --> 16 16 <section> 17 - <p class="text-gray-600 dark:text-gray-400 mb-2"> 17 + <p class="text-gray-500 dark:text-gray-300 mb-2"> 18 18 Tangled's logo and mascot is <strong>Dolly</strong>, the first ever <em>cloned</em> mammal. Please 19 19 follow the below guidelines when using Dolly and the logotype. 20 20 </p> 21 - <p class="text-gray-600 dark:text-gray-400 mb-2"> 21 + <p class="text-gray-500 dark:text-gray-300 mb-2"> 22 22 All assets are served as SVGs, and can be downloaded by right-clicking and clicking "Save image as". 23 23 </p> 24 24 </section> ··· 34 34 </div> 35 35 <div class="order-1 lg:order-2"> 36 36 <h2 class="text-xl font-semibold dark:text-white mb-3">Black logotype</h2> 37 - <p class="text-gray-600 dark:text-gray-400 mb-4">For use on light-colored backgrounds.</p> 37 + <p class="text-gray-500 dark:text-gray-300 mb-4">For use on light-colored backgrounds.</p> 38 38 <p class="text-gray-700 dark:text-gray-300"> 39 39 This is the preferred version of the logotype, featuring dark text and elements, ideal for light 40 40 backgrounds and designs. ··· 53 53 </div> 54 54 <div class="order-1 lg:order-2"> 55 55 <h2 class="text-xl font-semibold dark:text-white mb-3">White logotype</h2> 56 - <p class="text-gray-600 dark:text-gray-400 mb-4">For use on dark-colored backgrounds.</p> 56 + <p class="text-gray-500 dark:text-gray-300 mb-4">For use on dark-colored backgrounds.</p> 57 57 <p class="text-gray-700 dark:text-gray-300"> 58 58 This version features white text and elements, ideal for dark backgrounds 59 59 and inverted designs. ··· 81 81 </div> 82 82 <div class="order-1 lg:order-2"> 83 83 <h2 class="text-xl font-semibold dark:text-white mb-3">Mark only</h2> 84 - <p class="text-gray-600 dark:text-gray-400 mb-4"> 84 + <p class="text-gray-500 dark:text-gray-300 mb-4"> 85 85 When a smaller 1:1 logo or icon is needed, Dolly's face may be used on its own. 86 86 </p> 87 87 <p class="text-gray-700 dark:text-gray-300 mb-4"> ··· 123 123 </div> 124 124 <div class="order-1 lg:order-2"> 125 125 <h2 class="text-xl font-semibold dark:text-white mb-3">Colored backgrounds</h2> 126 - <p class="text-gray-600 dark:text-gray-400 mb-4"> 126 + <p class="text-gray-500 dark:text-gray-300 mb-4"> 127 127 White logo mark on colored backgrounds. 128 128 </p> 129 129 <p class="text-gray-700 dark:text-gray-300 mb-4"> ··· 165 165 </div> 166 166 <div class="order-1 lg:order-2"> 167 167 <h2 class="text-xl font-semibold dark:text-white mb-3">Lighter backgrounds</h2> 168 - <p class="text-gray-600 dark:text-gray-400 mb-4"> 168 + <p class="text-gray-500 dark:text-gray-300 mb-4"> 169 169 Dark logo mark on lighter, pastel backgrounds. 170 170 </p> 171 171 <p class="text-gray-700 dark:text-gray-300 mb-4"> ··· 186 186 </div> 187 187 <div class="order-1 lg:order-2"> 188 188 <h2 class="text-xl font-semibold dark:text-white mb-3">Recoloring</h2> 189 - <p class="text-gray-600 dark:text-gray-400 mb-4"> 189 + <p class="text-gray-500 dark:text-gray-300 mb-4"> 190 190 Custom coloring of the logotype is permitted. 191 191 </p> 192 192 <p class="text-gray-700 dark:text-gray-300 mb-4"> ··· 194 194 </p> 195 195 <p class="text-gray-700 dark:text-gray-300 text-sm"> 196 196 <strong>Example:</strong> Gray/sand colored logotype on a light yellow/tan background. 197 - </p> 198 - </div> 199 - </section> 200 - 201 - <!-- Silhouette Section --> 202 - <section class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center"> 203 - <div class="order-2 lg:order-1"> 204 - <div class="border border-gray-200 dark:border-gray-700 p-8 sm:p-16 bg-gray-50 dark:bg-gray-100 rounded"> 205 - <img src="https://assets.tangled.network/tangled_dolly_silhouette.svg" 206 - alt="Dolly silhouette" 207 - class="w-full max-w-32 mx-auto" /> 208 - </div> 209 - </div> 210 - <div class="order-1 lg:order-2"> 211 - <h2 class="text-xl font-semibold dark:text-white mb-3">Dolly silhouette</h2> 212 - <p class="text-gray-600 dark:text-gray-400 mb-4">A minimalist version of Dolly.</p> 213 - <p class="text-gray-700 dark:text-gray-300"> 214 - The silhouette can be used where a subtle brand presence is needed, 215 - or as a background element. Works on any background color with proper contrast. 216 - For example, we use this as the site's favicon. 217 197 </p> 218 198 </div> 219 199 </section>
+14 -2
appview/pages/templates/fragments/dolly/logo.html
··· 2 2 <svg 3 3 version="1.1" 4 4 id="svg1" 5 - class="{{ . }}" 5 + class="{{ .Classes }}" 6 6 width="25" 7 7 height="25" 8 8 viewBox="0 0 25 25" ··· 17 17 xmlns:svg="http://www.w3.org/2000/svg" 18 18 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 19 19 xmlns:cc="http://creativecommons.org/ns#"> 20 + <style> 21 + .dolly { 22 + color: #000000; 23 + } 24 + 25 + @media (prefers-color-scheme: dark) { 26 + .dolly { 27 + color: #ffffff; 28 + } 29 + } 30 + </style> 20 31 <sodipodi:namedview 21 32 id="namedview1" 22 33 pagecolor="#ffffff" ··· 51 62 id="g1" 52 63 transform="translate(-0.42924038,-0.87777209)"> 53 64 <path 54 - fill="currentColor" 65 + class="dolly" 66 + fill="{{ or .FillColor "currentColor" }}" 55 67 style="stroke-width:0.111183;" 56 68 d="m 16.775491,24.987061 c -0.78517,-0.0064 -1.384202,-0.234614 -2.033994,-0.631295 -0.931792,-0.490188 -1.643475,-1.31368 -2.152014,-2.221647 C 11.781409,23.136647 10.701392,23.744942 9.4922931,24.0886 8.9774725,24.238111 8.0757679,24.389777 6.5811304,23.84827 4.4270703,23.124679 2.8580086,20.883331 3.0363279,18.599583 3.0037061,17.652919 3.3488675,16.723769 3.8381157,15.925061 2.5329485,15.224503 1.4686756,14.048584 1.0611184,12.606459 0.81344502,11.816973 0.82385989,10.966486 0.91519098,10.154906 1.2422711,8.2387903 2.6795811,6.5725716 4.5299585,5.9732484 5.2685364,4.290122 6.8802592,3.0349975 8.706276,2.7794663 c 1.2124148,-0.1688264 2.46744,0.084987 3.52811,0.7011837 1.545426,-1.7139736 4.237779,-2.2205077 6.293579,-1.1676231 1.568222,0.7488935 2.689625,2.3113526 2.961888,4.0151464 1.492195,0.5977882 2.749007,1.8168898 3.242225,3.3644951 0.329805,0.9581836 0.340709,2.0135956 0.127128,2.9974286 -0.381606,1.535184 -1.465322,2.842146 -2.868035,3.556463 0.0034,0.273204 0.901506,2.243045 0.751284,3.729647 -0.03281,1.858525 -1.211631,3.619894 -2.846433,4.475452 -0.953967,0.556812 -2.084452,0.546309 -3.120531,0.535398 z m -4.470079,-5.349839 c 1.322246,-0.147248 2.189053,-1.300106 2.862307,-2.338363 0.318287,-0.472954 0.561404,-1.002348 0.803,-1.505815 0.313265,0.287151 0.578698,0.828085 1.074141,0.956909 0.521892,0.162542 1.133743,0.03052 1.45325,-0.443554 0.611414,-1.140449 0.31004,-2.516537 -0.04602,-3.698347 C 18.232844,11.92927 17.945151,11.232927 17.397785,10.751793 17.514522,9.9283111 17.026575,9.0919791 16.332883,8.6609491 15.741721,9.1323278 14.842258,9.1294949 14.271975,8.6252369 13.178927,9.7400102 12.177239,9.7029996 11.209704,8.8195135 10.992255,8.6209543 10.577326,10.031484 9.1211947,9.2324497 8.2846288,9.9333947 7.6359672,10.607693 7.0611981,11.578553 6.5026891,12.62523 5.9177873,13.554793 5.867393,14.69141 c -0.024234,0.66432 0.4948601,1.360337 1.1982269,1.306329 0.702996,0.06277 1.1815208,-0.629091 1.7138087,-0.916491 0.079382,0.927141 0.1688108,1.923227 0.4821259,2.828358 0.3596254,1.171275 1.6262605,1.915695 2.8251855,1.745211 0.08481,-0.0066 0.218672,-0.01769 0.218672,-0.0176 z m 0.686342,-3.497495 c -0.643126,-0.394168 -0.33365,-1.249599 -0.359402,-1.870938 0.064,-0.749774 0.115321,-1.538054 0.452402,-2.221125 0.356724,-0.487008 1.226721,-0.299139 1.265134,0.325689 -0.02558,0.628509 -0.314101,1.25416 -0.279646,1.9057 -0.07482,0.544043 0.05418,1.155133 -0.186476,1.652391 -0.197455,0.275121 -0.599638,0.355105 -0.892012,0.208283 z m -2.808766,-0.358124 c -0.605767,-0.328664 -0.4133176,-1.155655 -0.5083256,-1.73063 0.078762,-0.66567 0.013203,-1.510085 0.5705316,-1.976886 0.545037,-0.380109 1.286917,0.270803 1.029164,0.868384 -0.274913,0.755214 -0.09475,1.580345 -0.08893,2.34609 -0.104009,0.451702 -0.587146,0.691508 -1.002445,0.493042 z" 57 69 id="path4"
-95
appview/pages/templates/fragments/dolly/silhouette.html
··· 1 - {{ define "fragments/dolly/silhouette" }} 2 - <svg 3 - version="1.1" 4 - id="svg1" 5 - width="25" 6 - height="25" 7 - viewBox="0 0 25 25" 8 - sodipodi:docname="tangled_dolly_face_only_black_on_trans.svg" 9 - inkscape:export-filename="tangled_dolly_silhouette_black_on_trans.svg" 10 - inkscape:export-xdpi="96" 11 - inkscape:export-ydpi="96" 12 - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" 13 - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 14 - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 15 - xmlns="http://www.w3.org/2000/svg" 16 - xmlns:svg="http://www.w3.org/2000/svg" 17 - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 18 - xmlns:cc="http://creativecommons.org/ns#"> 19 - <style> 20 - .dolly { 21 - color: #000000; 22 - } 23 - 24 - @media (prefers-color-scheme: dark) { 25 - .dolly { 26 - color: #ffffff; 27 - } 28 - } 29 - </style> 30 - <sodipodi:namedview 31 - id="namedview1" 32 - pagecolor="#ffffff" 33 - bordercolor="#000000" 34 - borderopacity="0.25" 35 - inkscape:showpageshadow="2" 36 - inkscape:pageopacity="0.0" 37 - inkscape:pagecheckerboard="true" 38 - inkscape:deskcolor="#d5d5d5" 39 - inkscape:zoom="64" 40 - inkscape:cx="4.96875" 41 - inkscape:cy="13.429688" 42 - inkscape:window-width="3840" 43 - inkscape:window-height="2160" 44 - inkscape:window-x="0" 45 - inkscape:window-y="0" 46 - inkscape:window-maximized="0" 47 - inkscape:current-layer="g1" 48 - borderlayer="true"> 49 - <inkscape:page 50 - x="0" 51 - y="0" 52 - width="25" 53 - height="25" 54 - id="page2" 55 - margin="0" 56 - bleed="0" /> 57 - </sodipodi:namedview> 58 - <g 59 - inkscape:groupmode="layer" 60 - inkscape:label="Image" 61 - id="g1" 62 - transform="translate(-0.42924038,-0.87777209)"> 63 - <path 64 - class="dolly" 65 - fill="currentColor" 66 - style="stroke-width:0.111183" 67 - d="m 16.775491,24.987061 c -0.78517,-0.0064 -1.384202,-0.234614 -2.033994,-0.631295 -0.931792,-0.490188 -1.643475,-1.31368 -2.152014,-2.221647 C 11.781409,23.136647 10.701392,23.744942 9.4922931,24.0886 8.9774725,24.238111 8.0757679,24.389777 6.5811304,23.84827 4.4270703,23.124679 2.8580086,20.883331 3.0363279,18.599583 3.0037061,17.652919 3.3488675,16.723769 3.8381157,15.925061 2.5329485,15.224503 1.4686756,14.048584 1.0611184,12.606459 0.81344502,11.816973 0.82385989,10.966486 0.91519098,10.154906 1.2422711,8.2387903 2.6795811,6.5725716 4.5299585,5.9732484 5.2685364,4.290122 6.8802592,3.0349975 8.706276,2.7794663 c 1.2124148,-0.1688264 2.46744,0.084987 3.52811,0.7011837 1.545426,-1.7139736 4.237779,-2.2205077 6.293579,-1.1676231 1.568222,0.7488935 2.689625,2.3113526 2.961888,4.0151464 1.492195,0.5977882 2.749007,1.8168898 3.242225,3.3644951 0.329805,0.9581836 0.340709,2.0135956 0.127128,2.9974286 -0.381606,1.535184 -1.465322,2.842146 -2.868035,3.556463 0.0034,0.273204 0.901506,2.243045 0.751284,3.729647 -0.03281,1.858525 -1.211631,3.619894 -2.846433,4.475452 -0.953967,0.556812 -2.084452,0.546309 -3.120531,0.535398 z m -4.470079,-5.349839 c 1.322246,-0.147248 2.189053,-1.300106 2.862307,-2.338363 0.318287,-0.472954 0.561404,-1.002348 0.803,-1.505815 0.313265,0.287151 0.578698,0.828085 1.074141,0.956909 0.521892,0.162542 1.133743,0.03052 1.45325,-0.443554 0.611414,-1.140449 0.31004,-2.516537 -0.04602,-3.698347 C 18.232844,11.92927 17.945151,11.232927 17.397785,10.751793 17.514522,9.9283111 17.026575,9.0919791 16.332883,8.6609491 15.741721,9.1323278 14.842258,9.1294949 14.271975,8.6252369 13.178927,9.7400102 12.177239,9.7029996 11.209704,8.8195135 10.992255,8.6209543 10.577326,10.031484 9.1211947,9.2324497 8.2846288,9.9333947 7.6359672,10.607693 7.0611981,11.578553 6.5026891,12.62523 5.9177873,13.554793 5.867393,14.69141 c -0.024234,0.66432 0.4948601,1.360337 1.1982269,1.306329 0.702996,0.06277 1.1815208,-0.629091 1.7138087,-0.916491 0.079382,0.927141 0.1688108,1.923227 0.4821259,2.828358 0.3596254,1.171275 1.6262605,1.915695 2.8251855,1.745211 0.08481,-0.0066 0.218672,-0.01769 0.218672,-0.0176 z" 68 - id="path7" 69 - sodipodi:nodetypes="sccccccccccccccccccsscccccccccscccccccsc" /> 70 - </g> 71 - <metadata 72 - id="metadata1"> 73 - <rdf:RDF> 74 - <cc:Work 75 - rdf:about=""> 76 - <cc:license 77 - rdf:resource="http://creativecommons.org/licenses/by/4.0/" /> 78 - </cc:Work> 79 - <cc:License 80 - rdf:about="http://creativecommons.org/licenses/by/4.0/"> 81 - <cc:permits 82 - rdf:resource="http://creativecommons.org/ns#Reproduction" /> 83 - <cc:permits 84 - rdf:resource="http://creativecommons.org/ns#Distribution" /> 85 - <cc:requires 86 - rdf:resource="http://creativecommons.org/ns#Notice" /> 87 - <cc:requires 88 - rdf:resource="http://creativecommons.org/ns#Attribution" /> 89 - <cc:permits 90 - rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /> 91 - </cc:License> 92 - </rdf:RDF> 93 - </metadata> 94 - </svg> 95 - {{ end }}
+1 -1
appview/pages/templates/fragments/logotype.html
··· 1 1 {{ define "fragments/logotype" }} 2 2 <span class="flex items-center gap-2"> 3 - {{ template "fragments/dolly/logo" "size-16 text-black dark:text-white" }} 3 + {{ template "fragments/dolly/logo" (dict "Classes" "size-16 text-black dark:text-white") }} 4 4 <span class="font-bold text-4xl not-italic">tangled</span> 5 5 <span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1"> 6 6 alpha
+1 -1
appview/pages/templates/fragments/logotypeSmall.html
··· 1 1 {{ define "fragments/logotypeSmall" }} 2 2 <span class="flex items-center gap-2"> 3 - {{ template "fragments/dolly/logo" "size-8 text-black dark:text-white" }} 3 + {{ template "fragments/dolly/logo" (dict "Classes" "size-8 text-black dark:text-white")}} 4 4 <span class="font-bold text-xl not-italic">tangled</span> 5 5 <span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1"> 6 6 alpha
+4
appview/pages/templates/layouts/base.html
··· 11 11 <script defer src="/static/htmx-ext-ws.min.js"></script> 12 12 <script defer src="/static/actor-typeahead.js" type="module"></script> 13 13 14 + <link rel="icon" href="/static/logos/dolly.ico" sizes="48x48"/> 15 + <link rel="icon" href="/static/logos/dolly.svg" sizes="any" type="image/svg+xml"/> 16 + <link rel="apple-touch-icon" href="/static/logos/dolly.png"/> 17 + 14 18 <!-- preconnect to image cdn --> 15 19 <link rel="preconnect" href="https://avatar.tangled.sh" /> 16 20 <link rel="preconnect" href="https://camo.tangled.sh" />
+1 -5
appview/pages/templates/layouts/fragments/topbar.html
··· 3 3 <div class="flex justify-between p-0 items-center"> 4 4 <div id="left-items"> 5 5 <a href="/" hx-boost="true" class="text-2xl no-underline hover:no-underline flex items-center gap-2"> 6 - {{ template "fragments/dolly/logo" "size-8 text-black dark:text-white" }} 7 - <span class="font-bold text-xl not-italic hidden md:inline">tangled</span> 8 - <span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1 hidden md:inline"> 9 - alpha 10 - </span> 6 + {{ template "fragments/logotypeSmall" }} 11 7 </a> 12 8 </div> 13 9
+2 -2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
··· 1 1 {{ define "repo/issues/fragments/issueCommentHeader" }} 2 2 <div class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400 "> 3 - {{ template "user/fragments/picHandleLink" .Comment.Did.String }} 3 + {{ template "user/fragments/picHandleLink" .Comment.Did }} 4 4 {{ template "hats" $ }} 5 5 {{ template "timestamp" . }} 6 - {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did.String) }} 6 + {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }} 7 7 {{ if and $isCommentOwner (not .Comment.Deleted) }} 8 8 {{ template "editIssueComment" . }} 9 9 {{ template "deleteIssueComment" . }}
+3 -3
appview/pages/templates/repo/pulls/pull.html
··· 165 165 166 166 <div class="md:pl-[3.5rem] flex flex-col gap-2 mt-2 relative"> 167 167 {{ range $cidx, $c := .Comments }} 168 - <div id="comment-{{$c.Id}}" class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-2 px-4 relative w-full"> 168 + <div id="comment-{{$c.ID}}" class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-2 px-4 relative w-full"> 169 169 {{ if gt $cidx 0 }} 170 170 <div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div> 171 171 {{ end }} 172 172 <div class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1"> 173 - {{ template "user/fragments/picHandleLink" $c.Did.String }} 173 + {{ template "user/fragments/picHandleLink" $c.OwnerDid }} 174 174 <span class="before:content-['ยท']"></span> 175 - <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.Id}}">{{ template "repo/fragments/time" $c.Created }}</a> 175 + <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.ID}}">{{ template "repo/fragments/time" $c.Created }}</a> 176 176 </div> 177 177 <div class="prose dark:prose-invert"> 178 178 {{ $c.Body | markdown }}
+2 -2
appview/pulls/opengraph.go
··· 242 242 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 243 243 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 244 244 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 245 - err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor) 245 + err = dollyArea.DrawDolly(dollyX, dollyY, dollySize, dollyColor) 246 246 if err != nil { 247 247 log.Printf("dolly silhouette not available (this is ok): %v", err) 248 248 } ··· 277 277 } 278 278 279 279 // Get comment count from database 280 - comments, err := db.GetComments(s.db, orm.FilterEq("subject_at", pull.AtUri())) 280 + comments, err := db.GetPullComments(s.db, orm.FilterEq("pull_id", pull.ID)) 281 281 if err != nil { 282 282 log.Printf("failed to get pull comments: %v", err) 283 283 }
+23 -24
appview/pulls/pulls.go
··· 741 741 } 742 742 defer tx.Rollback() 743 743 744 - comment := models.Comment{ 745 - Did: syntax.DID(user.Did), 746 - Rkey: tid.TID(), 747 - Subject: pull.AtUri(), 748 - ReplyTo: nil, 749 - Body: body, 750 - Created: time.Now(), 751 - Mentions: mentions, 752 - References: references, 753 - PullSubmissionId: &pull.Submissions[roundNumber].ID, 754 - } 755 - if err = comment.Validate(); err != nil { 756 - log.Println("failed to validate comment", err) 757 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 758 - return 759 - } 760 - record := comment.AsRecord() 744 + createdAt := time.Now().Format(time.RFC3339) 761 745 762 746 client, err := s.oauth.AuthorizedClient(r) 763 747 if err != nil { ··· 765 749 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 766 750 return 767 751 } 768 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 769 - Collection: tangled.CommentNSID, 752 + atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 753 + Collection: tangled.RepoPullCommentNSID, 770 754 Repo: user.Did, 771 - Rkey: comment.Rkey, 755 + Rkey: tid.TID(), 772 756 Record: &lexutil.LexiconTypeDecoder{ 773 - Val: &record, 757 + Val: &tangled.RepoPullComment{ 758 + Pull: pull.AtUri().String(), 759 + Body: body, 760 + CreatedAt: createdAt, 761 + }, 774 762 }, 775 763 }) 776 764 if err != nil { ··· 779 767 return 780 768 } 781 769 770 + comment := &models.PullComment{ 771 + OwnerDid: user.Did, 772 + RepoAt: f.RepoAt().String(), 773 + PullId: pull.PullId, 774 + Body: body, 775 + CommentAt: atResp.Uri, 776 + SubmissionId: pull.Submissions[roundNumber].ID, 777 + Mentions: mentions, 778 + References: references, 779 + } 780 + 782 781 // Create the pull comment in the database with the commentAt field 783 - err = db.PutComment(tx, &comment) 782 + commentId, err := db.NewPullComment(tx, comment) 784 783 if err != nil { 785 784 log.Println("failed to create pull comment", err) 786 785 s.pages.Notice(w, "pull-comment", "Failed to create comment.") ··· 794 793 return 795 794 } 796 795 797 - s.notifier.NewComment(r.Context(), &comment) 796 + s.notifier.NewPullComment(r.Context(), comment, mentions) 798 797 799 798 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 800 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, comment.Id)) 799 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, commentId)) 801 800 return 802 801 } 803 802 }
+1 -1
appview/repo/opengraph.go
··· 237 237 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 238 238 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 239 239 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 240 - err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor) 240 + err = dollyArea.DrawDolly(dollyX, dollyY, dollySize, dollyColor) 241 241 if err != nil { 242 242 log.Printf("dolly silhouette not available (this is ok): %v", err) 243 243 }
+29
appview/state/manifest.go
··· 1 + package state 2 + 3 + import ( 4 + "encoding/json" 5 + "net/http" 6 + ) 7 + 8 + // https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest 9 + // https://www.w3.org/TR/appmanifest/ 10 + var manifestData = map[string]any{ 11 + "name": "tangled", 12 + "description": "tightly-knit social coding.", 13 + "icons": []map[string]string{ 14 + { 15 + "src": "/static/logos/dolly.svg", 16 + "sizes": "144x144", 17 + }, 18 + }, 19 + "start_url": "/", 20 + "id": "https://tangled.org", 21 + "display": "standalone", 22 + "background_color": "#111827", 23 + "theme_color": "#111827", 24 + } 25 + 26 + func (p *State) WebAppManifest(w http.ResponseWriter, r *http.Request) { 27 + w.Header().Set("Content-Type", "application/manifest+json") 28 + json.NewEncoder(w).Encode(manifestData) 29 + }
+1 -3
appview/state/router.go
··· 32 32 s.pages, 33 33 ) 34 34 35 - router.Get("/favicon.svg", s.Favicon) 36 - router.Get("/favicon.ico", s.Favicon) 37 - router.Get("/pwa-manifest.json", s.PWAManifest) 35 + router.Get("/pwa-manifest.json", s.WebAppManifest) 38 36 router.Get("/robots.txt", s.RobotsTxt) 39 37 40 38 userRouter := s.UserRouter(&middleware)
+1 -37
appview/state/state.go
··· 117 117 tangled.SpindleNSID, 118 118 tangled.StringNSID, 119 119 tangled.RepoIssueNSID, 120 - tangled.CommentNSID, 120 + tangled.RepoIssueCommentNSID, 121 121 tangled.LabelDefinitionNSID, 122 122 tangled.LabelOpNSID, 123 123 }, ··· 202 202 return s.db.Close() 203 203 } 204 204 205 - func (s *State) Favicon(w http.ResponseWriter, r *http.Request) { 206 - w.Header().Set("Content-Type", "image/svg+xml") 207 - w.Header().Set("Cache-Control", "public, max-age=31536000") // one year 208 - w.Header().Set("ETag", `"favicon-svg-v1"`) 209 - 210 - if match := r.Header.Get("If-None-Match"); match == `"favicon-svg-v1"` { 211 - w.WriteHeader(http.StatusNotModified) 212 - return 213 - } 214 - 215 - s.pages.Favicon(w) 216 - } 217 - 218 205 func (s *State) RobotsTxt(w http.ResponseWriter, r *http.Request) { 219 206 w.Header().Set("Content-Type", "text/plain") 220 207 w.Header().Set("Cache-Control", "public, max-age=86400") // one day ··· 223 210 Allow: / 224 211 ` 225 212 w.Write([]byte(robotsTxt)) 226 - } 227 - 228 - // https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest 229 - const manifestJson = `{ 230 - "name": "tangled", 231 - "description": "tightly-knit social coding.", 232 - "icons": [ 233 - { 234 - "src": "/favicon.svg", 235 - "sizes": "144x144" 236 - } 237 - ], 238 - "start_url": "/", 239 - "id": "org.tangled", 240 - 241 - "display": "standalone", 242 - "background_color": "#111827", 243 - "theme_color": "#111827" 244 - }` 245 - 246 - func (p *State) PWAManifest(w http.ResponseWriter, r *http.Request) { 247 - w.Header().Set("Content-Type", "application/json") 248 - w.Write([]byte(manifestJson)) 249 213 } 250 214 251 215 func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) {
+27
appview/validator/issue.go
··· 4 4 "fmt" 5 5 "strings" 6 6 7 + "tangled.org/core/appview/db" 7 8 "tangled.org/core/appview/models" 9 + "tangled.org/core/orm" 8 10 ) 11 + 12 + func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error { 13 + // if comments have parents, only ingest ones that are 1 level deep 14 + if comment.ReplyTo != nil { 15 + parents, err := db.GetIssueComments(v.db, orm.FilterEq("at_uri", *comment.ReplyTo)) 16 + if err != nil { 17 + return fmt.Errorf("failed to fetch parent comment: %w", err) 18 + } 19 + if len(parents) != 1 { 20 + return fmt.Errorf("incorrect number of parent comments returned: %d", len(parents)) 21 + } 22 + 23 + // depth check 24 + parent := parents[0] 25 + if parent.ReplyTo != nil { 26 + return fmt.Errorf("incorrect depth, this comment is replying at depth >1") 27 + } 28 + } 29 + 30 + if sb := strings.TrimSpace(v.sanitizer.SanitizeDefault(comment.Body)); sb == "" { 31 + return fmt.Errorf("body is empty after HTML sanitization") 32 + } 33 + 34 + return nil 35 + } 9 36 10 37 func (v *Validator) ValidateIssue(issue *models.Issue) error { 11 38 if issue.Title == "" {
-1
cmd/cborgen/cborgen.go
··· 15 15 "api/tangled/cbor_gen.go", 16 16 "tangled", 17 17 tangled.ActorProfile{}, 18 - tangled.Comment{}, 19 18 tangled.FeedReaction{}, 20 19 tangled.FeedStar{}, 21 20 tangled.GitRefUpdate{},
+182
cmd/dolly/main.go
··· 1 + package main 2 + 3 + import ( 4 + "bytes" 5 + "flag" 6 + "fmt" 7 + "image" 8 + "image/color" 9 + "image/png" 10 + "os" 11 + "path/filepath" 12 + "strconv" 13 + "strings" 14 + "text/template" 15 + 16 + "github.com/srwiley/oksvg" 17 + "github.com/srwiley/rasterx" 18 + "golang.org/x/image/draw" 19 + "tangled.org/core/appview/pages" 20 + "tangled.org/core/ico" 21 + ) 22 + 23 + func main() { 24 + var ( 25 + size string 26 + fillColor string 27 + output string 28 + ) 29 + 30 + flag.StringVar(&size, "size", "512x512", "Output size in format WIDTHxHEIGHT (e.g., 512x512)") 31 + flag.StringVar(&fillColor, "color", "#000000", "Fill color in hex format (e.g., #FF5733)") 32 + flag.StringVar(&output, "output", "dolly.svg", "Output file path (format detected from extension: .svg, .png, or .ico)") 33 + flag.Parse() 34 + 35 + width, height, err := parseSize(size) 36 + if err != nil { 37 + fmt.Fprintf(os.Stderr, "Error parsing size: %v\n", err) 38 + os.Exit(1) 39 + } 40 + 41 + // Detect format from file extension 42 + ext := strings.ToLower(filepath.Ext(output)) 43 + format := strings.TrimPrefix(ext, ".") 44 + 45 + if format != "svg" && format != "png" && format != "ico" { 46 + fmt.Fprintf(os.Stderr, "Invalid file extension: %s. Must be .svg, .png, or .ico\n", ext) 47 + os.Exit(1) 48 + } 49 + 50 + if fillColor != "currentColor" && !isValidHexColor(fillColor) { 51 + fmt.Fprintf(os.Stderr, "Invalid color format: %s. Use hex format like #FF5733\n", fillColor) 52 + os.Exit(1) 53 + } 54 + 55 + svgData, err := dolly(fillColor) 56 + if err != nil { 57 + fmt.Fprintf(os.Stderr, "Error generating SVG: %v\n", err) 58 + os.Exit(1) 59 + } 60 + 61 + // Create output directory if it doesn't exist 62 + dir := filepath.Dir(output) 63 + if dir != "" && dir != "." { 64 + if err := os.MkdirAll(dir, 0755); err != nil { 65 + fmt.Fprintf(os.Stderr, "Error creating output directory: %v\n", err) 66 + os.Exit(1) 67 + } 68 + } 69 + 70 + switch format { 71 + case "svg": 72 + err = saveSVG(svgData, output, width, height) 73 + case "png": 74 + err = savePNG(svgData, output, width, height) 75 + case "ico": 76 + err = saveICO(svgData, output, width, height) 77 + } 78 + 79 + if err != nil { 80 + fmt.Fprintf(os.Stderr, "Error saving file: %v\n", err) 81 + os.Exit(1) 82 + } 83 + 84 + fmt.Printf("Successfully generated %s (%dx%d)\n", output, width, height) 85 + } 86 + 87 + func dolly(hexColor string) ([]byte, error) { 88 + tpl, err := template.New("dolly"). 89 + ParseFS(pages.Files, "templates/fragments/dolly/logo.html") 90 + if err != nil { 91 + return nil, err 92 + } 93 + 94 + var svgData bytes.Buffer 95 + if err := tpl.ExecuteTemplate(&svgData, "fragments/dolly/logo", pages.DollyParams{ 96 + FillColor: hexColor, 97 + }); err != nil { 98 + return nil, err 99 + } 100 + 101 + return svgData.Bytes(), nil 102 + } 103 + 104 + func svgToImage(svgData []byte, w, h int) (image.Image, error) { 105 + icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData)) 106 + if err != nil { 107 + return nil, fmt.Errorf("error parsing SVG: %v", err) 108 + } 109 + 110 + icon.SetTarget(0, 0, float64(w), float64(h)) 111 + rgba := image.NewRGBA(image.Rect(0, 0, w, h)) 112 + draw.Draw(rgba, rgba.Bounds(), &image.Uniform{color.Transparent}, image.Point{}, draw.Src) 113 + scanner := rasterx.NewScannerGV(w, h, rgba, rgba.Bounds()) 114 + raster := rasterx.NewDasher(w, h, scanner) 115 + icon.Draw(raster, 1.0) 116 + 117 + return rgba, nil 118 + } 119 + 120 + func parseSize(size string) (int, int, error) { 121 + parts := strings.Split(size, "x") 122 + if len(parts) != 2 { 123 + return 0, 0, fmt.Errorf("invalid size format, use WIDTHxHEIGHT") 124 + } 125 + 126 + width, err := strconv.Atoi(parts[0]) 127 + if err != nil { 128 + return 0, 0, fmt.Errorf("invalid width: %v", err) 129 + } 130 + 131 + height, err := strconv.Atoi(parts[1]) 132 + if err != nil { 133 + return 0, 0, fmt.Errorf("invalid height: %v", err) 134 + } 135 + 136 + if width <= 0 || height <= 0 { 137 + return 0, 0, fmt.Errorf("width and height must be positive") 138 + } 139 + 140 + return width, height, nil 141 + } 142 + 143 + func isValidHexColor(hex string) bool { 144 + if len(hex) != 7 || hex[0] != '#' { 145 + return false 146 + } 147 + _, err := strconv.ParseUint(hex[1:], 16, 32) 148 + return err == nil 149 + } 150 + 151 + func saveSVG(svgData []byte, filepath string, _, _ int) error { 152 + return os.WriteFile(filepath, svgData, 0644) 153 + } 154 + 155 + func savePNG(svgData []byte, filepath string, width, height int) error { 156 + img, err := svgToImage(svgData, width, height) 157 + if err != nil { 158 + return err 159 + } 160 + 161 + f, err := os.Create(filepath) 162 + if err != nil { 163 + return err 164 + } 165 + defer f.Close() 166 + 167 + return png.Encode(f, img) 168 + } 169 + 170 + func saveICO(svgData []byte, filepath string, width, height int) error { 171 + img, err := svgToImage(svgData, width, height) 172 + if err != nil { 173 + return err 174 + } 175 + 176 + icoData, err := ico.ImageToIco(img) 177 + if err != nil { 178 + return err 179 + } 180 + 181 + return os.WriteFile(filepath, icoData, 0644) 182 + }
+6
docs/logo.html
··· 1 + <div class="flex items-center gap-2 w-fit mx-auto"> 2 + <span class="w-16 h-16 [&>svg]:w-full [&>svg]:h-full text-black dark:text-white"> 3 + ${ dolly.svg() } 4 + </span> 5 + <span class="font-bold text-4xl not-italic text-black dark:text-white">tangled</span> 6 + </div>
+2
docs/template.html
··· 74 74 ${ x.svg() } 75 75 $if(toc-title)$$toc-title$$else$Table of Contents$endif$ 76 76 </button> 77 + ${ logo.html() } 77 78 ${ search.html() } 78 79 ${ table-of-contents:toc.html() } 79 80 </div> ··· 88 89 class="hidden md:flex md:flex-col gap-4 fixed left-0 top-0 w-80 h-screen 89 90 bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 90 91 p-4 z-50 overflow-y-auto"> 92 + ${ logo.html() } 91 93 ${ search.html() } 92 94 <div class="flex-1"> 93 95 $if(toc-title)$
+17 -2
flake.nix
··· 94 94 spindle = self.callPackage ./nix/pkgs/spindle.nix {}; 95 95 knot-unwrapped = self.callPackage ./nix/pkgs/knot-unwrapped.nix {}; 96 96 knot = self.callPackage ./nix/pkgs/knot.nix {}; 97 + dolly = self.callPackage ./nix/pkgs/dolly.nix {}; 97 98 }); 98 99 in { 99 100 overlays.default = final: prev: { 100 - inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs; 101 + inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs dolly; 101 102 }; 102 103 103 104 packages = forAllSystems (system: let ··· 106 107 staticPackages = mkPackageSet pkgs.pkgsStatic; 107 108 crossPackages = mkPackageSet pkgs.pkgsCross.gnu64.pkgsStatic; 108 109 in { 109 - inherit (packages) appview appview-static-files lexgen goat spindle knot knot-unwrapped sqlite-lib docs; 110 + inherit 111 + (packages) 112 + appview 113 + appview-static-files 114 + lexgen 115 + goat 116 + spindle 117 + knot 118 + knot-unwrapped 119 + sqlite-lib 120 + docs 121 + dolly 122 + ; 110 123 111 124 pkgsStatic-appview = staticPackages.appview; 112 125 pkgsStatic-knot = staticPackages.knot; 113 126 pkgsStatic-knot-unwrapped = staticPackages.knot-unwrapped; 114 127 pkgsStatic-spindle = staticPackages.spindle; 115 128 pkgsStatic-sqlite-lib = staticPackages.sqlite-lib; 129 + pkgsStatic-dolly = staticPackages.dolly; 116 130 117 131 pkgsCross-gnu64-pkgsStatic-appview = crossPackages.appview; 118 132 pkgsCross-gnu64-pkgsStatic-knot = crossPackages.knot; 119 133 pkgsCross-gnu64-pkgsStatic-knot-unwrapped = crossPackages.knot-unwrapped; 120 134 pkgsCross-gnu64-pkgsStatic-spindle = crossPackages.spindle; 135 + pkgsCross-gnu64-pkgsStatic-dolly = crossPackages.dolly; 121 136 122 137 treefmt-wrapper = pkgs.treefmt.withConfig { 123 138 settings.formatter = {
+88
ico/ico.go
··· 1 + package ico 2 + 3 + import ( 4 + "bytes" 5 + "encoding/binary" 6 + "fmt" 7 + "image" 8 + "image/png" 9 + ) 10 + 11 + type IconDir struct { 12 + Reserved uint16 // must be 0 13 + Type uint16 // 1 for ICO, 2 for CUR 14 + Count uint16 // number of images 15 + } 16 + 17 + type IconDirEntry struct { 18 + Width uint8 // 0 means 256 19 + Height uint8 // 0 means 256 20 + ColorCount uint8 21 + Reserved uint8 // must be 0 22 + ColorPlanes uint16 // 0 or 1 23 + BitsPerPixel uint16 24 + SizeInBytes uint32 25 + Offset uint32 26 + } 27 + 28 + func ImageToIco(img image.Image) ([]byte, error) { 29 + // encode image as png 30 + var pngBuf bytes.Buffer 31 + if err := png.Encode(&pngBuf, img); err != nil { 32 + return nil, fmt.Errorf("failed to encode PNG: %w", err) 33 + } 34 + pngData := pngBuf.Bytes() 35 + 36 + // get image dimensions 37 + bounds := img.Bounds() 38 + width := bounds.Dx() 39 + height := bounds.Dy() 40 + 41 + // prepare output buffer 42 + var icoBuf bytes.Buffer 43 + 44 + iconDir := IconDir{ 45 + Reserved: 0, 46 + Type: 1, // ICO format 47 + Count: 1, // One image 48 + } 49 + 50 + w := uint8(width) 51 + h := uint8(height) 52 + 53 + // width/height of 256 should be stored as 0 54 + if width == 256 { 55 + w = 0 56 + } 57 + if height == 256 { 58 + h = 0 59 + } 60 + 61 + iconDirEntry := IconDirEntry{ 62 + Width: w, 63 + Height: h, 64 + ColorCount: 0, // 0 for PNG (32-bit) 65 + Reserved: 0, 66 + ColorPlanes: 1, 67 + BitsPerPixel: 32, // PNG with alpha 68 + SizeInBytes: uint32(len(pngData)), 69 + Offset: 6 + 16, // Size of ICONDIR + ICONDIRENTRY 70 + } 71 + 72 + // write IconDir 73 + if err := binary.Write(&icoBuf, binary.LittleEndian, iconDir); err != nil { 74 + return nil, fmt.Errorf("failed to write ICONDIR: %w", err) 75 + } 76 + 77 + // write IconDirEntry 78 + if err := binary.Write(&icoBuf, binary.LittleEndian, iconDirEntry); err != nil { 79 + return nil, fmt.Errorf("failed to write ICONDIRENTRY: %w", err) 80 + } 81 + 82 + // write PNG data directly 83 + if _, err := icoBuf.Write(pngData); err != nil { 84 + return nil, fmt.Errorf("failed to write PNG data: %w", err) 85 + } 86 + 87 + return icoBuf.Bytes(), nil 88 + }
-51
lexicons/comment/comment.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "sh.tangled.comment", 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 - "body", 15 - "createdAt" 16 - ], 17 - "properties": { 18 - "subject": { 19 - "type": "string", 20 - "format": "at-uri" 21 - }, 22 - "body": { 23 - "type": "string" 24 - }, 25 - "createdAt": { 26 - "type": "string", 27 - "format": "datetime" 28 - }, 29 - "replyTo": { 30 - "type": "string", 31 - "format": "at-uri" 32 - }, 33 - "mentions": { 34 - "type": "array", 35 - "items": { 36 - "type": "string", 37 - "format": "did" 38 - } 39 - }, 40 - "references": { 41 - "type": "array", 42 - "items": { 43 - "type": "string", 44 - "format": "at-uri" 45 - } 46 - } 47 - } 48 - } 49 - } 50 - } 51 - }
+6 -1
nix/pkgs/appview-static-files.nix
··· 8 8 actor-typeahead-src, 9 9 sqlite-lib, 10 10 tailwindcss, 11 + dolly, 11 12 src, 12 13 }: 13 14 runCommandLocal "appview-static-files" { ··· 17 18 (allow file-read* (subpath "/System/Library/OpenSSL")) 18 19 ''; 19 20 } '' 20 - mkdir -p $out/{fonts,icons} && cd $out 21 + mkdir -p $out/{fonts,icons,logos} && cd $out 21 22 cp -f ${htmx-src} htmx.min.js 22 23 cp -f ${htmx-ws-src} htmx-ext-ws.min.js 23 24 cp -rf ${lucide-src}/*.svg icons/ ··· 26 27 cp -f ${inter-fonts-src}/InterVariable*.ttf fonts/ 27 28 cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono*.woff2 fonts/ 28 29 cp -f ${actor-typeahead-src}/actor-typeahead.js . 30 + 31 + ${dolly}/bin/dolly -output logos/dolly.png -size 180x180 32 + ${dolly}/bin/dolly -output logos/dolly.ico -size 48x48 33 + ${dolly}/bin/dolly -output logos/dolly.svg -color currentColor 29 34 # tailwindcss -c $src/tailwind.config.js -i $src/input.css -o tw.css won't work 30 35 # for whatever reason (produces broken css), so we are doing this instead 31 36 cd ${src} && ${tailwindcss}/bin/tailwindcss -i input.css -o $out/tw.css
+4
nix/pkgs/docs.nix
··· 5 5 inter-fonts-src, 6 6 ibm-plex-mono-src, 7 7 lucide-src, 8 + dolly, 8 9 src, 9 10 }: 10 11 runCommandLocal "docs" {} '' ··· 17 18 18 19 # icons 19 20 cp -rf ${lucide-src}/*.svg working/ 21 + 22 + # logo 23 + ${dolly}/bin/dolly -output working/dolly.svg -color currentColor 20 24 21 25 # content - chunked 22 26 ${pandoc}/bin/pandoc ${src}/docs/DOCS.md \
+21
nix/pkgs/dolly.nix
··· 1 + { 2 + buildGoApplication, 3 + modules, 4 + src, 5 + }: 6 + buildGoApplication { 7 + pname = "dolly"; 8 + version = "0.1.0"; 9 + inherit src modules; 10 + 11 + # patch the static dir 12 + postUnpack = '' 13 + pushd source 14 + mkdir -p appview/pages/static 15 + touch appview/pages/static/x 16 + popd 17 + ''; 18 + 19 + doCheck = false; 20 + subPackages = ["cmd/dolly"]; 21 + }