+416
api/tangled/cbor_gen.go
+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
+
}
564
980
func (t *FeedReaction) MarshalCBOR(w io.Writer) error {
565
981
if t == nil {
566
982
_, err := w.Write(cbg.CborNull)
+27
api/tangled/tangledcomment.go
+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
+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
+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
+
1176
1257
return &DB{
1177
1258
db,
1178
1259
logger,
+6
-186
appview/db/issues.go
+6
-186
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[string]*models.Issue) // at-uri -> issue
103
+
issueMap := make(map[syntax.ATURI]*models.Issue) // at-uri -> issue
104
104
105
105
var conditions []string
106
106
var args []any
···
196
196
}
197
197
}
198
198
199
-
atUri := issue.AtUri().String()
200
-
issueMap[atUri] = &issue
199
+
issueMap[issue.AtUri()] = &issue
201
200
}
202
201
203
202
// collect reverse repos
···
229
228
// collect comments
230
229
issueAts := slices.Collect(maps.Keys(issueMap))
231
230
232
-
comments, err := GetIssueComments(e, orm.FilterIn("issue_at", issueAts))
231
+
comments, err := GetComments(e, orm.FilterIn("subject_at", issueAts))
233
232
if err != nil {
234
233
return nil, fmt.Errorf("failed to query comments: %w", err)
235
234
}
236
235
for i := range comments {
237
-
issueAt := comments[i].IssueAt
236
+
issueAt := comments[i].Subject
238
237
if issue, ok := issueMap[issueAt]; ok {
239
238
issue.Comments = append(issue.Comments, comments[i])
240
239
}
···
246
245
return nil, fmt.Errorf("failed to query labels: %w", err)
247
246
}
248
247
for issueAt, labels := range allLabels {
249
-
if issue, ok := issueMap[issueAt.String()]; ok {
248
+
if issue, ok := issueMap[issueAt]; ok {
250
249
issue.Labels = labels
251
250
}
252
251
}
···
257
256
return nil, fmt.Errorf("failed to query reference_links: %w", err)
258
257
}
259
258
for issueAt, references := range allReferencs {
260
-
if issue, ok := issueMap[issueAt.String()]; ok {
259
+
if issue, ok := issueMap[issueAt]; ok {
261
260
issue.References = references
262
261
}
263
262
}
···
349
348
}
350
349
351
350
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
531
351
}
532
352
533
353
func DeleteIssues(tx *sql.Tx, did, rkey string) error {
+6
-121
appview/db/pulls.go
+6
-121
appview/db/pulls.go
···
447
447
return nil, err
448
448
}
449
449
450
-
// Get comments for all submissions using GetPullComments
450
+
// Get comments for all submissions using GetComments
451
451
submissionIds := slices.Collect(maps.Keys(submissionMap))
452
-
comments, err := GetPullComments(e, orm.FilterIn("submission_id", submissionIds))
452
+
comments, err := GetComments(e, orm.FilterIn("pull_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 submission, ok := submissionMap[comment.SubmissionId]; ok {
458
-
submission.Comments = append(submission.Comments, comment)
457
+
if comment.PullSubmissionId != nil {
458
+
if submission, ok := submissionMap[*comment.PullSubmissionId]; ok {
459
+
submission.Comments = append(submission.Comments, comment)
460
+
}
459
461
}
460
462
}
461
463
···
475
477
return m, nil
476
478
}
477
479
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
-
568
480
// timeframe here is directly passed into the sql query filter, and any
569
481
// timeframe in the past should be negative; e.g.: "-3 months"
570
482
func GetPullsByOwnerDid(e Execer, did, timeframe string) ([]models.Pull, error) {
···
639
551
}
640
552
641
553
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
669
554
}
670
555
671
556
func SetPullState(e Execer, repoAt syntax.ATURI, pullId int, pullState models.PullState) error {
+20
-32
appview/db/reference.go
+20
-32
appview/db/reference.go
···
11
11
"tangled.org/core/orm"
12
12
)
13
13
14
-
// ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs.
14
+
// ValidateReferenceLinks resolves refLinks to Issue/PR/Comment 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.did, i.rkey,
57
-
c.did, c.rkey
56
+
i.at_uri, c.at_uri
58
57
from input inp
59
58
join repos r
60
59
on r.did = inp.owner_did
···
62
61
join issues i
63
62
on i.repo_at = r.at_uri
64
63
and i.issue_id = inp.issue_id
65
-
left join issue_comments c
64
+
left join comments c
66
65
on inp.comment_id is not null
67
-
and c.issue_at = i.at_uri
66
+
and c.subject_at = i.at_uri
68
67
and c.id = inp.comment_id
69
68
`,
70
69
strings.Join(vals, ","),
···
79
78
80
79
for rows.Next() {
81
80
// Scan rows
82
-
var issueOwner, issueRkey string
83
-
var commentOwner, commentRkey sql.NullString
81
+
var issueUri string
82
+
var commentUri sql.NullString
84
83
var uri syntax.ATURI
85
-
if err := rows.Scan(&issueOwner, &issueRkey, &commentOwner, &commentRkey); err != nil {
84
+
if err := rows.Scan(&issueUri, &commentUri); err != nil {
86
85
return nil, err
87
86
}
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
-
))
87
+
if commentUri.Valid {
88
+
uri = syntax.ATURI(commentUri.String)
95
89
} else {
96
-
uri = syntax.ATURI(fmt.Sprintf(
97
-
"at://%s/%s/%s",
98
-
issueOwner,
99
-
tangled.RepoIssueNSID,
100
-
issueRkey,
101
-
))
90
+
uri = syntax.ATURI(issueUri)
102
91
}
103
92
uris = append(uris, uri)
104
93
}
···
124
113
values %s
125
114
)
126
115
select
127
-
p.owner_did, p.rkey,
128
-
c.comment_at
116
+
p.owner_did, p.rkey, c.at_uri
129
117
from input inp
130
118
join repos r
131
119
on r.did = inp.owner_did
···
133
121
join pulls p
134
122
on p.repo_at = r.at_uri
135
123
and p.pull_id = inp.pull_id
136
-
left join pull_comments c
124
+
left join comments c
137
125
on inp.comment_id is not null
138
-
and c.repo_at = r.at_uri and c.pull_id = p.pull_id
126
+
and c.subject_at = ('at://' || p.owner_did || '/' || 'sh.tangled.repo.pull' || '/' || p.rkey)
139
127
and c.id = inp.comment_id
140
128
`,
141
129
strings.Join(vals, ","),
···
283
271
return nil, fmt.Errorf("get issue backlinks: %w", err)
284
272
}
285
273
backlinks = append(backlinks, ls...)
286
-
ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.RepoIssueCommentNSID])
274
+
ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.CommentNSID])
287
275
if err != nil {
288
276
return nil, fmt.Errorf("get issue_comment backlinks: %w", err)
289
277
}
···
293
281
return nil, fmt.Errorf("get pull backlinks: %w", err)
294
282
}
295
283
backlinks = append(backlinks, ls...)
296
-
ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.RepoPullCommentNSID])
284
+
ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.CommentNSID])
297
285
if err != nil {
298
286
return nil, fmt.Errorf("get pull_comment backlinks: %w", err)
299
287
}
···
352
340
rows, err := e.Query(
353
341
fmt.Sprintf(
354
342
`select r.did, r.name, i.issue_id, c.id, i.title, i.open
355
-
from issue_comments c
343
+
from comments c
356
344
join issues i
357
-
on i.at_uri = c.issue_at
345
+
on i.at_uri = c.subject_at
358
346
join repos r
359
347
on r.at_uri = i.repo_at
360
348
where %s`,
···
428
416
if len(aturis) == 0 {
429
417
return nil, nil
430
418
}
431
-
filter := orm.FilterIn("c.comment_at", aturis)
419
+
filter := orm.FilterIn("c.at_uri", aturis)
432
420
rows, err := e.Query(
433
421
fmt.Sprintf(
434
422
`select r.did, r.name, p.pull_id, c.id, p.title, p.state
435
423
from repos r
436
424
join pulls p
437
425
on r.at_uri = p.repo_at
438
-
join pull_comments c
439
-
on r.at_uri = c.repo_at and p.pull_id = c.pull_id
426
+
join comments c
427
+
on ('at://' || p.owner_did || '/' || 'sh.tangled.repo.pull' || '/' || p.rkey) = c.subject_at
440
428
where %s`,
441
429
filter.Condition(),
442
430
),
+19
-11
appview/ingester.go
+19
-11
appview/ingester.go
···
79
79
err = i.ingestString(e)
80
80
case tangled.RepoIssueNSID:
81
81
err = i.ingestIssue(ctx, e)
82
-
case tangled.RepoIssueCommentNSID:
83
-
err = i.ingestIssueComment(e)
82
+
case tangled.CommentNSID:
83
+
err = i.ingestComment(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) ingestIssueComment(e *jmodels.Event) error {
871
+
func (i *Ingester) ingestComment(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", "ingestIssueComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey)
877
+
l := i.Logger.With("handler", "ingestComment", "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.RepoIssueComment{}
888
+
record := tangled.Comment{}
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.IssueCommentFromRecord(did, rkey, record)
894
+
comment, err := models.CommentFromRecord(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
-
if err := i.Validator.ValidateIssueComment(comment); err != nil {
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 {
900
908
return fmt.Errorf("failed to validate comment: %w", err)
901
909
}
902
910
···
906
914
}
907
915
defer tx.Rollback()
908
916
909
-
_, err = db.AddIssueComment(tx, *comment)
917
+
err = db.PutComment(tx, comment)
910
918
if err != nil {
911
-
return fmt.Errorf("failed to create issue comment: %w", err)
919
+
return fmt.Errorf("failed to create comment: %w", err)
912
920
}
913
921
914
922
return tx.Commit()
915
923
916
924
case jmodels.CommitOperationDelete:
917
-
if err := db.DeleteIssueComments(
925
+
if err := db.DeleteComments(
918
926
ddb,
919
927
orm.FilterEq("did", did),
920
928
orm.FilterEq("rkey", rkey),
921
929
); err != nil {
922
-
return fmt.Errorf("failed to delete issue comment record: %w", err)
930
+
return fmt.Errorf("failed to delete comment record: %w", err)
923
931
}
924
932
925
933
return nil
+31
-29
appview/issues/issues.go
+31
-29
appview/issues/issues.go
···
403
403
404
404
body := r.FormValue("body")
405
405
if body == "" {
406
-
rp.pages.Notice(w, "issue", "Body is required")
406
+
rp.pages.Notice(w, "issue-comment", "Body is required")
407
407
return
408
408
}
409
409
410
-
replyToUri := r.FormValue("reply-to")
411
-
var replyTo *string
412
-
if replyToUri != "" {
413
-
replyTo = &replyToUri
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
414
419
}
415
420
416
421
mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
417
422
418
-
comment := models.IssueComment{
419
-
Did: user.Did,
423
+
comment := models.Comment{
424
+
Did: syntax.DID(user.Did),
420
425
Rkey: tid.TID(),
421
-
IssueAt: issue.AtUri().String(),
426
+
Subject: issue.AtUri(),
422
427
ReplyTo: replyTo,
423
428
Body: body,
424
429
Created: time.Now(),
425
430
Mentions: mentions,
426
431
References: references,
427
432
}
428
-
if err = rp.validator.ValidateIssueComment(&comment); err != nil {
433
+
if err = comment.Validate(); err != nil {
429
434
l.Error("failed to validate comment", "err", err)
430
435
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
431
436
return
···
441
446
442
447
// create a record first
443
448
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
444
-
Collection: tangled.RepoIssueCommentNSID,
445
-
Repo: comment.Did,
449
+
Collection: tangled.CommentNSID,
450
+
Repo: user.Did,
446
451
Rkey: comment.Rkey,
447
452
Record: &lexutil.LexiconTypeDecoder{
448
453
Val: &record,
···
468
473
}
469
474
defer tx.Rollback()
470
475
471
-
commentId, err := db.AddIssueComment(tx, comment)
476
+
err = db.PutComment(tx, &comment)
472
477
if err != nil {
473
478
l.Error("failed to create comment", "err", err)
474
479
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
···
484
489
// reset atUri to make rollback a no-op
485
490
atUri = ""
486
491
487
-
// notify about the new comment
488
-
comment.Id = commentId
489
-
490
-
rp.notifier.NewIssueComment(r.Context(), &comment, mentions)
492
+
rp.notifier.NewComment(r.Context(), &comment)
491
493
492
494
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
493
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, commentId))
495
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, comment.Id))
494
496
}
495
497
496
498
func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
···
505
507
}
506
508
507
509
commentId := chi.URLParam(r, "commentId")
508
-
comments, err := db.GetIssueComments(
510
+
comments, err := db.GetComments(
509
511
rp.db,
510
512
orm.FilterEq("id", commentId),
511
513
)
···
541
543
}
542
544
543
545
commentId := chi.URLParam(r, "commentId")
544
-
comments, err := db.GetIssueComments(
546
+
comments, err := db.GetComments(
545
547
rp.db,
546
548
orm.FilterEq("id", commentId),
547
549
)
···
557
559
}
558
560
comment := comments[0]
559
561
560
-
if comment.Did != user.Did {
562
+
if comment.Did.String() != user.Did {
561
563
l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did)
562
564
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
563
565
return
···
597
599
}
598
600
defer tx.Rollback()
599
601
600
-
_, err = db.AddIssueComment(tx, newComment)
602
+
err = db.PutComment(tx, &newComment)
601
603
if err != nil {
602
604
l.Error("failed to perferom update-description query", "err", err)
603
605
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
···
608
610
// rkey is optional, it was introduced later
609
611
if newComment.Rkey != "" {
610
612
// update the record on pds
611
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey)
613
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.CommentNSID, user.Did, comment.Rkey)
612
614
if err != nil {
613
615
l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey)
614
616
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
···
616
618
}
617
619
618
620
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
619
-
Collection: tangled.RepoIssueCommentNSID,
621
+
Collection: tangled.CommentNSID,
620
622
Repo: user.Did,
621
623
Rkey: newComment.Rkey,
622
624
SwapRecord: ex.Cid,
···
651
653
}
652
654
653
655
commentId := chi.URLParam(r, "commentId")
654
-
comments, err := db.GetIssueComments(
656
+
comments, err := db.GetComments(
655
657
rp.db,
656
658
orm.FilterEq("id", commentId),
657
659
)
···
687
689
}
688
690
689
691
commentId := chi.URLParam(r, "commentId")
690
-
comments, err := db.GetIssueComments(
692
+
comments, err := db.GetComments(
691
693
rp.db,
692
694
orm.FilterEq("id", commentId),
693
695
)
···
723
725
}
724
726
725
727
commentId := chi.URLParam(r, "commentId")
726
-
comments, err := db.GetIssueComments(
728
+
comments, err := db.GetComments(
727
729
rp.db,
728
730
orm.FilterEq("id", commentId),
729
731
)
···
739
741
}
740
742
comment := comments[0]
741
743
742
-
if comment.Did != user.Did {
744
+
if comment.Did.String() != user.Did {
743
745
l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did)
744
746
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
745
747
return
···
752
754
753
755
// optimistic deletion
754
756
deleted := time.Now()
755
-
err = db.DeleteIssueComments(rp.db, orm.FilterEq("id", comment.Id))
757
+
err = db.DeleteComments(rp.db, orm.FilterEq("id", comment.Id))
756
758
if err != nil {
757
759
l.Error("failed to delete comment", "err", err)
758
760
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
···
768
770
return
769
771
}
770
772
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
771
-
Collection: tangled.RepoIssueCommentNSID,
773
+
Collection: tangled.CommentNSID,
772
774
Repo: user.Did,
773
775
Rkey: comment.Rkey,
774
776
})
+2
-2
appview/issues/opengraph.go
+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.DrawDolly(dollyX, dollyY, dollySize, dollyColor)
196
+
err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor)
197
197
if err != nil {
198
-
log.Printf("dolly not available (this is ok): %v", err)
198
+
log.Printf("dolly silhouette 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
+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
+
}
+8
-89
appview/models/issue.go
+8
-89
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 []IssueComment
29
+
Comments []Comment
30
30
Labels LabelState
31
31
Repo *Repo
32
32
}
···
62
62
}
63
63
64
64
type CommentListItem struct {
65
-
Self *IssueComment
66
-
Replies []*IssueComment
65
+
Self *Comment
66
+
Replies []*Comment
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[string]*CommentListItem)
92
-
var replies []*IssueComment
91
+
toplevel := make(map[syntax.ATURI]*CommentListItem)
92
+
var replies []*Comment
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().String()] = &CommentListItem{
97
+
toplevel[comment.AtUri()] = &CommentListItem{
98
98
Self: &comment,
99
99
}
100
100
} else {
···
115
115
}
116
116
117
117
// sort everything
118
-
sortFunc := func(a, b *IssueComment) bool {
118
+
sortFunc := func(a, b *Comment) 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)
147
+
addParticipant(c.Did.String())
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
-
}
+2
-46
appview/models/pull.go
+2
-46
appview/models/pull.go
···
138
138
RoundNumber int
139
139
Patch string
140
140
Combined string
141
-
Comments []PullComment
141
+
Comments []Comment
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
-
// }
191
147
192
148
func (p *Pull) LastRoundNumber() int {
193
149
return len(p.Submissions) - 1
···
289
245
addParticipant(s.PullAt.Authority().String())
290
246
291
247
for _, c := range s.Comments {
292
-
addParticipant(c.OwnerDid)
248
+
addParticipant(c.Did.String())
293
249
}
294
250
295
251
return participants
+111
-113
appview/notify/db/db.go
+111
-113
appview/notify/db/db.go
···
74
74
// no-op
75
75
}
76
76
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()))
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()
79
93
if err != nil {
80
-
log.Printf("failed to fetch collaborators: %v", err)
94
+
log.Printf("NewComment: expected did based at-uri for comment.subject")
81
95
return
82
96
}
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]
83
113
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)
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
91
171
}
92
-
for _, m := range mentions {
172
+
173
+
for _, m := range comment.Mentions {
93
174
recipients.Remove(m)
94
175
}
95
176
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
-
103
177
n.notifyEvent(
104
-
actorDid,
178
+
comment.Did,
105
179
recipients,
106
-
models.NotificationTypeIssueCreated,
180
+
models.NotificationTypeIssueCommented,
107
181
entityType,
108
182
entityId,
109
183
repoId,
···
111
185
pullId,
112
186
)
113
187
n.notifyEvent(
114
-
actorDid,
115
-
sets.Collect(slices.Values(mentions)),
188
+
comment.Did,
189
+
sets.Collect(slices.Values(comment.Mentions)),
116
190
models.NotificationTypeUserMentioned,
117
191
entityType,
118
192
entityId,
···
122
196
)
123
197
}
124
198
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))
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()))
127
205
if err != nil {
128
-
log.Printf("NewIssueComment: failed to get issues: %v", err)
206
+
log.Printf("failed to fetch collaborators: %v", err)
129
207
return
130
208
}
131
-
if len(issues) == 0 {
132
-
log.Printf("NewIssueComment: no issue found for %s", comment.IssueAt)
133
-
return
134
-
}
135
-
issue := issues[0]
136
209
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
210
+
// build the recipients list
211
+
// - owner of the repo
212
+
// - collaborators in the repo
213
+
// - remove users already mentioned
142
214
recipients := sets.Singleton(syntax.DID(issue.Repo.Did))
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))
215
+
for _, c := range collaborators {
216
+
recipients.Insert(c.SubjectDid)
159
217
}
160
-
161
218
for _, m := range mentions {
162
219
recipients.Remove(m)
163
220
}
164
221
165
-
actorDid := syntax.DID(comment.Did)
222
+
actorDid := syntax.DID(issue.Did)
166
223
entityType := "issue"
167
224
entityId := issue.AtUri().String()
168
225
repoId := &issue.Repo.Id
···
172
229
n.notifyEvent(
173
230
actorDid,
174
231
recipients,
175
-
models.NotificationTypeIssueCommented,
232
+
models.NotificationTypeIssueCreated,
176
233
entityType,
177
234
entityId,
178
235
repoId,
···
252
309
actorDid,
253
310
recipients,
254
311
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,
314
312
entityType,
315
313
entityId,
316
314
repoId,
+8
-8
appview/notify/merged_notifier.go
+8
-8
appview/notify/merged_notifier.go
···
53
53
m.fanout("DeleteStar", ctx, star)
54
54
}
55
55
56
-
func (m *mergedNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {
57
-
m.fanout("NewIssue", ctx, issue, mentions)
56
+
func (m *mergedNotifier) NewComment(ctx context.Context, comment *models.Comment) {
57
+
m.fanout("NewComment", ctx, comment)
58
58
}
59
59
60
-
func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
61
-
m.fanout("NewIssueComment", ctx, comment, mentions)
60
+
func (m *mergedNotifier) DeleteComment(ctx context.Context, comment *models.Comment) {
61
+
m.fanout("DeleteComment", ctx, comment)
62
+
}
63
+
64
+
func (m *mergedNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {
65
+
m.fanout("NewIssue", ctx, issue, mentions)
62
66
}
63
67
64
68
func (m *mergedNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {
···
79
83
80
84
func (m *mergedNotifier) NewPull(ctx context.Context, pull *models.Pull) {
81
85
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
+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
+
16
19
NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID)
17
-
NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID)
18
20
NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue)
19
21
DeleteIssue(ctx context.Context, issue *models.Issue)
20
22
···
22
24
DeleteFollow(ctx context.Context, follow *models.Follow)
23
25
24
26
NewPull(ctx context.Context, pull *models.Pull)
25
-
NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID)
26
27
NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull)
27
28
28
29
UpdateProfile(ctx context.Context, profile *models.Profile)
···
42
43
func (m *BaseNotifier) NewStar(ctx context.Context, star *models.Star) {}
43
44
func (m *BaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {}
44
45
46
+
func (m *BaseNotifier) NewComment(ctx context.Context, comment *models.Comment) {}
47
+
func (m *BaseNotifier) DeleteComment(ctx context.Context, comment *models.Comment) {}
48
+
45
49
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
-
}
48
50
func (m *BaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {}
49
51
func (m *BaseNotifier) DeleteIssue(ctx context.Context, issue *models.Issue) {}
50
52
51
53
func (m *BaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {}
52
54
func (m *BaseNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {}
53
55
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
-
}
56
+
func (m *BaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {}
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) {}
+5
-20
appview/notify/posthog/notifier.go
+5
-20
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
-
104
89
func (n *posthogNotifier) NewPullClosed(ctx context.Context, pull *models.Pull) {
105
90
err := n.client.Enqueue(posthog.Capture{
106
91
DistinctId: pull.OwnerDid,
···
180
165
}
181
166
}
182
167
183
-
func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
168
+
func (n *posthogNotifier) NewComment(ctx context.Context, comment *models.Comment) {
184
169
err := n.client.Enqueue(posthog.Capture{
185
-
DistinctId: comment.Did,
186
-
Event: "new_issue_comment",
170
+
DistinctId: comment.Did.String(),
171
+
Event: "new_comment",
187
172
Properties: posthog.Properties{
188
-
"issue_at": comment.IssueAt,
189
-
"mentions": mentions,
173
+
"subject_at": comment.Subject,
174
+
"mentions": comment.Mentions,
190
175
},
191
176
})
192
177
if err != nil {
+9
-9
appview/ogcard/card.go
+9
-9
appview/ogcard/card.go
···
334
334
return nil
335
335
}
336
336
337
-
func (c *Card) DrawDolly(x, y, size int, iconColor color.Color) error {
337
+
func (c *Card) DrawDollySilhouette(x, y, size int, iconColor color.Color) error {
338
338
tpl, err := template.New("dolly").
339
-
ParseFS(pages.Files, "templates/fragments/dolly/logo.html")
339
+
ParseFS(pages.Files, "templates/fragments/dolly/silhouette.html")
340
340
if err != nil {
341
-
return fmt.Errorf("failed to read dolly template: %w", err)
341
+
return fmt.Errorf("failed to read dolly silhouette template: %w", err)
342
342
}
343
343
344
344
var svgData bytes.Buffer
345
-
if err = tpl.ExecuteTemplate(&svgData, "fragments/dolly/logo", nil); err != nil {
346
-
return fmt.Errorf("failed to execute dolly template: %w", err)
345
+
if err = tpl.ExecuteTemplate(&svgData, "fragments/dolly/silhouette", nil); err != nil {
346
+
return fmt.Errorf("failed to execute dolly silhouette 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 convertSVGToPNG(bodyBytes)
456
+
return c.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 convertSVGToPNG(svgData []byte) (image.Image, bool) {
496
+
func (c *Card) 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 := range size {
551
-
for cx := range size {
550
+
for cy := 0; cy < size; cy++ {
551
+
for cx := 0; cx < size; cx++ {
552
552
// Calculate distance from center
553
553
dx := float64(cx - center)
554
554
dy := float64(cy - center)
+5
-16
appview/pages/pages.go
+5
-16
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
-
222
213
func (p *Pages) Favicon(w io.Writer) error {
223
-
return p.Dolly(w, DollyParams{
224
-
Classes: "text-black dark:text-white",
225
-
})
214
+
return p.executePlain("fragments/dolly/silhouette", w, nil)
226
215
}
227
216
228
217
type LoginParams struct {
···
999
988
LoggedInUser *oauth.User
1000
989
RepoInfo repoinfo.RepoInfo
1001
990
Issue *models.Issue
1002
-
Comment *models.IssueComment
991
+
Comment *models.Comment
1003
992
}
1004
993
1005
994
func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error {
···
1010
999
LoggedInUser *oauth.User
1011
1000
RepoInfo repoinfo.RepoInfo
1012
1001
Issue *models.Issue
1013
-
Comment *models.IssueComment
1002
+
Comment *models.Comment
1014
1003
}
1015
1004
1016
1005
func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error {
···
1021
1010
LoggedInUser *oauth.User
1022
1011
RepoInfo repoinfo.RepoInfo
1023
1012
Issue *models.Issue
1024
-
Comment *models.IssueComment
1013
+
Comment *models.Comment
1025
1014
}
1026
1015
1027
1016
func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error {
···
1032
1021
LoggedInUser *oauth.User
1033
1022
RepoInfo repoinfo.RepoInfo
1034
1023
Issue *models.Issue
1035
-
Comment *models.IssueComment
1024
+
Comment *models.Comment
1036
1025
}
1037
1026
1038
1027
func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error {
+29
-9
appview/pages/templates/brand/brand.html
+29
-9
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-500 dark:text-gray-300 mb-1">
7
+
<p class="text-gray-600 dark:text-gray-400 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-500 dark:text-gray-300 mb-2">
17
+
<p class="text-gray-600 dark:text-gray-400 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-500 dark:text-gray-300 mb-2">
21
+
<p class="text-gray-600 dark:text-gray-400 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-500 dark:text-gray-300 mb-4">For use on light-colored backgrounds.</p>
37
+
<p class="text-gray-600 dark:text-gray-400 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-500 dark:text-gray-300 mb-4">For use on dark-colored backgrounds.</p>
56
+
<p class="text-gray-600 dark:text-gray-400 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-500 dark:text-gray-300 mb-4">
84
+
<p class="text-gray-600 dark:text-gray-400 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-500 dark:text-gray-300 mb-4">
126
+
<p class="text-gray-600 dark:text-gray-400 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-500 dark:text-gray-300 mb-4">
168
+
<p class="text-gray-600 dark:text-gray-400 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-500 dark:text-gray-300 mb-4">
189
+
<p class="text-gray-600 dark:text-gray-400 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.
197
217
</p>
198
218
</div>
199
219
</section>
+2
-14
appview/pages/templates/fragments/dolly/logo.html
+2
-14
appview/pages/templates/fragments/dolly/logo.html
···
2
2
<svg
3
3
version="1.1"
4
4
id="svg1"
5
-
class="{{ .Classes }}"
5
+
class="{{ . }}"
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>
31
20
<sodipodi:namedview
32
21
id="namedview1"
33
22
pagecolor="#ffffff"
···
62
51
id="g1"
63
52
transform="translate(-0.42924038,-0.87777209)">
64
53
<path
65
-
class="dolly"
66
-
fill="{{ or .FillColor "currentColor" }}"
54
+
fill="currentColor"
67
55
style="stroke-width:0.111183;"
68
56
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"
69
57
id="path4"
+95
appview/pages/templates/fragments/dolly/silhouette.html
+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
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" (dict "Classes" "size-16 text-black dark:text-white") }}
3
+
{{ template "fragments/dolly/logo" "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
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" (dict "Classes" "size-8 text-black dark:text-white")}}
3
+
{{ template "fragments/dolly/logo" "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
-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
-
18
14
<!-- preconnect to image cdn -->
19
15
<link rel="preconnect" href="https://avatar.tangled.sh" />
20
16
<link rel="preconnect" href="https://camo.tangled.sh" />
+5
-1
appview/pages/templates/layouts/fragments/topbar.html
+5
-1
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/logotypeSmall" }}
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>
7
11
</a>
8
12
</div>
9
13
+2
-2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
+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 }}
3
+
{{ template "user/fragments/picHandleLink" .Comment.Did.String }}
4
4
{{ template "hats" $ }}
5
5
{{ template "timestamp" . }}
6
-
{{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }}
6
+
{{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did.String) }}
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
+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.OwnerDid }}
173
+
{{ template "user/fragments/picHandleLink" $c.Did.String }}
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
+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.DrawDolly(dollyX, dollyY, dollySize, dollyColor)
245
+
err = dollyArea.DrawDollySilhouette(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.GetPullComments(s.db, orm.FilterEq("pull_id", pull.ID))
280
+
comments, err := db.GetComments(s.db, orm.FilterEq("subject_at", pull.AtUri()))
281
281
if err != nil {
282
282
log.Printf("failed to get pull comments: %v", err)
283
283
}
+24
-23
appview/pulls/pulls.go
+24
-23
appview/pulls/pulls.go
···
741
741
}
742
742
defer tx.Rollback()
743
743
744
-
createdAt := time.Now().Format(time.RFC3339)
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()
745
761
746
762
client, err := s.oauth.AuthorizedClient(r)
747
763
if err != nil {
···
749
765
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
750
766
return
751
767
}
752
-
atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
753
-
Collection: tangled.RepoPullCommentNSID,
768
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
769
+
Collection: tangled.CommentNSID,
754
770
Repo: user.Did,
755
-
Rkey: tid.TID(),
771
+
Rkey: comment.Rkey,
756
772
Record: &lexutil.LexiconTypeDecoder{
757
-
Val: &tangled.RepoPullComment{
758
-
Pull: pull.AtUri().String(),
759
-
Body: body,
760
-
CreatedAt: createdAt,
761
-
},
773
+
Val: &record,
762
774
},
763
775
})
764
776
if err != nil {
···
767
779
return
768
780
}
769
781
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
-
781
782
// Create the pull comment in the database with the commentAt field
782
-
commentId, err := db.NewPullComment(tx, comment)
783
+
err = db.PutComment(tx, &comment)
783
784
if err != nil {
784
785
log.Println("failed to create pull comment", err)
785
786
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
···
793
794
return
794
795
}
795
796
796
-
s.notifier.NewPullComment(r.Context(), comment, mentions)
797
+
s.notifier.NewComment(r.Context(), &comment)
797
798
798
799
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
799
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, commentId))
800
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, comment.Id))
800
801
return
801
802
}
802
803
}
+1
-1
appview/repo/opengraph.go
+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.DrawDolly(dollyX, dollyY, dollySize, dollyColor)
240
+
err = dollyArea.DrawDollySilhouette(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
-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
-
}
+3
-1
appview/state/router.go
+3
-1
appview/state/router.go
···
32
32
s.pages,
33
33
)
34
34
35
-
router.Get("/pwa-manifest.json", s.WebAppManifest)
35
+
router.Get("/favicon.svg", s.Favicon)
36
+
router.Get("/favicon.ico", s.Favicon)
37
+
router.Get("/pwa-manifest.json", s.PWAManifest)
36
38
router.Get("/robots.txt", s.RobotsTxt)
37
39
38
40
userRouter := s.UserRouter(&middleware)
+37
-1
appview/state/state.go
+37
-1
appview/state/state.go
···
117
117
tangled.SpindleNSID,
118
118
tangled.StringNSID,
119
119
tangled.RepoIssueNSID,
120
-
tangled.RepoIssueCommentNSID,
120
+
tangled.CommentNSID,
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
+
205
218
func (s *State) RobotsTxt(w http.ResponseWriter, r *http.Request) {
206
219
w.Header().Set("Content-Type", "text/plain")
207
220
w.Header().Set("Cache-Control", "public, max-age=86400") // one day
···
210
223
Allow: /
211
224
`
212
225
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))
213
249
}
214
250
215
251
func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) {
-27
appview/validator/issue.go
-27
appview/validator/issue.go
···
4
4
"fmt"
5
5
"strings"
6
6
7
-
"tangled.org/core/appview/db"
8
7
"tangled.org/core/appview/models"
9
-
"tangled.org/core/orm"
10
8
)
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
-
}
36
9
37
10
func (v *Validator) ValidateIssue(issue *models.Issue) error {
38
11
if issue.Title == "" {
+1
cmd/cborgen/cborgen.go
+1
cmd/cborgen/cborgen.go
-182
cmd/dolly/main.go
-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
-6
docs/logo.html
-2
docs/template.html
-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() }
78
77
${ search.html() }
79
78
${ table-of-contents:toc.html() }
80
79
</div>
···
89
88
class="hidden md:flex md:flex-col gap-4 fixed left-0 top-0 w-80 h-screen
90
89
bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700
91
90
p-4 z-50 overflow-y-auto">
92
-
${ logo.html() }
93
91
${ search.html() }
94
92
<div class="flex-1">
95
93
$if(toc-title)$
+2
-17
flake.nix
+2
-17
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 {};
98
97
});
99
98
in {
100
99
overlays.default = final: prev: {
101
-
inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs dolly;
100
+
inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs;
102
101
};
103
102
104
103
packages = forAllSystems (system: let
···
107
106
staticPackages = mkPackageSet pkgs.pkgsStatic;
108
107
crossPackages = mkPackageSet pkgs.pkgsCross.gnu64.pkgsStatic;
109
108
in {
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
-
;
109
+
inherit (packages) appview appview-static-files lexgen goat spindle knot knot-unwrapped sqlite-lib docs;
123
110
124
111
pkgsStatic-appview = staticPackages.appview;
125
112
pkgsStatic-knot = staticPackages.knot;
126
113
pkgsStatic-knot-unwrapped = staticPackages.knot-unwrapped;
127
114
pkgsStatic-spindle = staticPackages.spindle;
128
115
pkgsStatic-sqlite-lib = staticPackages.sqlite-lib;
129
-
pkgsStatic-dolly = staticPackages.dolly;
130
116
131
117
pkgsCross-gnu64-pkgsStatic-appview = crossPackages.appview;
132
118
pkgsCross-gnu64-pkgsStatic-knot = crossPackages.knot;
133
119
pkgsCross-gnu64-pkgsStatic-knot-unwrapped = crossPackages.knot-unwrapped;
134
120
pkgsCross-gnu64-pkgsStatic-spindle = crossPackages.spindle;
135
-
pkgsCross-gnu64-pkgsStatic-dolly = crossPackages.dolly;
136
121
137
122
treefmt-wrapper = pkgs.treefmt.withConfig {
138
123
settings.formatter = {
-88
ico/ico.go
-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
+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
+
}
+1
-6
nix/pkgs/appview-static-files.nix
+1
-6
nix/pkgs/appview-static-files.nix
···
8
8
actor-typeahead-src,
9
9
sqlite-lib,
10
10
tailwindcss,
11
-
dolly,
12
11
src,
13
12
}:
14
13
runCommandLocal "appview-static-files" {
···
18
17
(allow file-read* (subpath "/System/Library/OpenSSL"))
19
18
'';
20
19
} ''
21
-
mkdir -p $out/{fonts,icons,logos} && cd $out
20
+
mkdir -p $out/{fonts,icons} && cd $out
22
21
cp -f ${htmx-src} htmx.min.js
23
22
cp -f ${htmx-ws-src} htmx-ext-ws.min.js
24
23
cp -rf ${lucide-src}/*.svg icons/
···
27
26
cp -f ${inter-fonts-src}/InterVariable*.ttf fonts/
28
27
cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono*.woff2 fonts/
29
28
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
34
29
# tailwindcss -c $src/tailwind.config.js -i $src/input.css -o tw.css won't work
35
30
# for whatever reason (produces broken css), so we are doing this instead
36
31
cd ${src} && ${tailwindcss}/bin/tailwindcss -i input.css -o $out/tw.css
-4
nix/pkgs/docs.nix
-4
nix/pkgs/docs.nix
···
5
5
inter-fonts-src,
6
6
ibm-plex-mono-src,
7
7
lucide-src,
8
-
dolly,
9
8
src,
10
9
}:
11
10
runCommandLocal "docs" {} ''
···
18
17
19
18
# icons
20
19
cp -rf ${lucide-src}/*.svg working/
21
-
22
-
# logo
23
-
${dolly}/bin/dolly -output working/dolly.svg -color currentColor
24
20
25
21
# content - chunked
26
22
${pandoc}/bin/pandoc ${src}/docs/DOCS.md \
-21
nix/pkgs/dolly.nix
-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
-
}