-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
-
}
980
564
func (t *FeedReaction) MarshalCBOR(w io.Writer) error {
981
565
if t == nil {
982
566
_, err := w.Write(cbg.CborNull)
-27
api/tangled/tangledcomment.go
-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
-
1257
1176
return &DB{
1258
1177
db,
1259
1178
logger,
+186
-6
appview/db/issues.go
+186
-6
appview/db/issues.go
···
100
100
}
101
101
102
102
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Issue, error) {
103
-
issueMap := make(map[syntax.ATURI]*models.Issue) // at-uri -> issue
103
+
issueMap := make(map[string]*models.Issue) // at-uri -> issue
104
104
105
105
var conditions []string
106
106
var args []any
···
196
196
}
197
197
}
198
198
199
-
issueMap[issue.AtUri()] = &issue
199
+
atUri := issue.AtUri().String()
200
+
issueMap[atUri] = &issue
200
201
}
201
202
202
203
// collect reverse repos
···
228
229
// collect comments
229
230
issueAts := slices.Collect(maps.Keys(issueMap))
230
231
231
-
comments, err := GetComments(e, orm.FilterIn("subject_at", issueAts))
232
+
comments, err := GetIssueComments(e, orm.FilterIn("issue_at", issueAts))
232
233
if err != nil {
233
234
return nil, fmt.Errorf("failed to query comments: %w", err)
234
235
}
235
236
for i := range comments {
236
-
issueAt := comments[i].Subject
237
+
issueAt := comments[i].IssueAt
237
238
if issue, ok := issueMap[issueAt]; ok {
238
239
issue.Comments = append(issue.Comments, comments[i])
239
240
}
···
245
246
return nil, fmt.Errorf("failed to query labels: %w", err)
246
247
}
247
248
for issueAt, labels := range allLabels {
248
-
if issue, ok := issueMap[issueAt]; ok {
249
+
if issue, ok := issueMap[issueAt.String()]; ok {
249
250
issue.Labels = labels
250
251
}
251
252
}
···
256
257
return nil, fmt.Errorf("failed to query reference_links: %w", err)
257
258
}
258
259
for issueAt, references := range allReferencs {
259
-
if issue, ok := issueMap[issueAt]; ok {
260
+
if issue, ok := issueMap[issueAt.String()]; ok {
260
261
issue.References = references
261
262
}
262
263
}
···
348
349
}
349
350
350
351
return ids, nil
352
+
}
353
+
354
+
func AddIssueComment(tx *sql.Tx, c models.IssueComment) (int64, error) {
355
+
result, err := tx.Exec(
356
+
`insert into issue_comments (
357
+
did,
358
+
rkey,
359
+
issue_at,
360
+
body,
361
+
reply_to,
362
+
created,
363
+
edited
364
+
)
365
+
values (?, ?, ?, ?, ?, ?, null)
366
+
on conflict(did, rkey) do update set
367
+
issue_at = excluded.issue_at,
368
+
body = excluded.body,
369
+
edited = case
370
+
when
371
+
issue_comments.issue_at != excluded.issue_at
372
+
or issue_comments.body != excluded.body
373
+
or issue_comments.reply_to != excluded.reply_to
374
+
then ?
375
+
else issue_comments.edited
376
+
end`,
377
+
c.Did,
378
+
c.Rkey,
379
+
c.IssueAt,
380
+
c.Body,
381
+
c.ReplyTo,
382
+
c.Created.Format(time.RFC3339),
383
+
time.Now().Format(time.RFC3339),
384
+
)
385
+
if err != nil {
386
+
return 0, err
387
+
}
388
+
389
+
id, err := result.LastInsertId()
390
+
if err != nil {
391
+
return 0, err
392
+
}
393
+
394
+
if err := putReferences(tx, c.AtUri(), c.References); err != nil {
395
+
return 0, fmt.Errorf("put reference_links: %w", err)
396
+
}
397
+
398
+
return id, nil
399
+
}
400
+
401
+
func DeleteIssueComments(e Execer, filters ...orm.Filter) error {
402
+
var conditions []string
403
+
var args []any
404
+
for _, filter := range filters {
405
+
conditions = append(conditions, filter.Condition())
406
+
args = append(args, filter.Arg()...)
407
+
}
408
+
409
+
whereClause := ""
410
+
if conditions != nil {
411
+
whereClause = " where " + strings.Join(conditions, " and ")
412
+
}
413
+
414
+
query := fmt.Sprintf(`update issue_comments set body = "", deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause)
415
+
416
+
_, err := e.Exec(query, args...)
417
+
return err
418
+
}
419
+
420
+
func GetIssueComments(e Execer, filters ...orm.Filter) ([]models.IssueComment, error) {
421
+
commentMap := make(map[string]*models.IssueComment)
422
+
423
+
var conditions []string
424
+
var args []any
425
+
for _, filter := range filters {
426
+
conditions = append(conditions, filter.Condition())
427
+
args = append(args, filter.Arg()...)
428
+
}
429
+
430
+
whereClause := ""
431
+
if conditions != nil {
432
+
whereClause = " where " + strings.Join(conditions, " and ")
433
+
}
434
+
435
+
query := fmt.Sprintf(`
436
+
select
437
+
id,
438
+
did,
439
+
rkey,
440
+
issue_at,
441
+
reply_to,
442
+
body,
443
+
created,
444
+
edited,
445
+
deleted
446
+
from
447
+
issue_comments
448
+
%s
449
+
`, whereClause)
450
+
451
+
rows, err := e.Query(query, args...)
452
+
if err != nil {
453
+
return nil, err
454
+
}
455
+
defer rows.Close()
456
+
457
+
for rows.Next() {
458
+
var comment models.IssueComment
459
+
var created string
460
+
var rkey, edited, deleted, replyTo sql.Null[string]
461
+
err := rows.Scan(
462
+
&comment.Id,
463
+
&comment.Did,
464
+
&rkey,
465
+
&comment.IssueAt,
466
+
&replyTo,
467
+
&comment.Body,
468
+
&created,
469
+
&edited,
470
+
&deleted,
471
+
)
472
+
if err != nil {
473
+
return nil, err
474
+
}
475
+
476
+
// this is a remnant from old times, newer comments always have rkey
477
+
if rkey.Valid {
478
+
comment.Rkey = rkey.V
479
+
}
480
+
481
+
if t, err := time.Parse(time.RFC3339, created); err == nil {
482
+
comment.Created = t
483
+
}
484
+
485
+
if edited.Valid {
486
+
if t, err := time.Parse(time.RFC3339, edited.V); err == nil {
487
+
comment.Edited = &t
488
+
}
489
+
}
490
+
491
+
if deleted.Valid {
492
+
if t, err := time.Parse(time.RFC3339, deleted.V); err == nil {
493
+
comment.Deleted = &t
494
+
}
495
+
}
496
+
497
+
if replyTo.Valid {
498
+
comment.ReplyTo = &replyTo.V
499
+
}
500
+
501
+
atUri := comment.AtUri().String()
502
+
commentMap[atUri] = &comment
503
+
}
504
+
505
+
if err = rows.Err(); err != nil {
506
+
return nil, err
507
+
}
508
+
509
+
// collect references for each comments
510
+
commentAts := slices.Collect(maps.Keys(commentMap))
511
+
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts))
512
+
if err != nil {
513
+
return nil, fmt.Errorf("failed to query reference_links: %w", err)
514
+
}
515
+
for commentAt, references := range allReferencs {
516
+
if comment, ok := commentMap[commentAt.String()]; ok {
517
+
comment.References = references
518
+
}
519
+
}
520
+
521
+
var comments []models.IssueComment
522
+
for _, c := range commentMap {
523
+
comments = append(comments, *c)
524
+
}
525
+
526
+
sort.Slice(comments, func(i, j int) bool {
527
+
return comments[i].Created.After(comments[j].Created)
528
+
})
529
+
530
+
return comments, nil
351
531
}
352
532
353
533
func DeleteIssues(tx *sql.Tx, did, rkey string) error {
+121
-6
appview/db/pulls.go
+121
-6
appview/db/pulls.go
···
447
447
return nil, err
448
448
}
449
449
450
-
// Get comments for all submissions using GetComments
450
+
// Get comments for all submissions using GetPullComments
451
451
submissionIds := slices.Collect(maps.Keys(submissionMap))
452
-
comments, err := GetComments(e, orm.FilterIn("pull_submission_id", submissionIds))
452
+
comments, err := GetPullComments(e, orm.FilterIn("submission_id", submissionIds))
453
453
if err != nil {
454
454
return nil, fmt.Errorf("failed to get pull comments: %w", err)
455
455
}
456
456
for _, comment := range comments {
457
-
if comment.PullSubmissionId != nil {
458
-
if submission, ok := submissionMap[*comment.PullSubmissionId]; ok {
459
-
submission.Comments = append(submission.Comments, comment)
460
-
}
457
+
if submission, ok := submissionMap[comment.SubmissionId]; ok {
458
+
submission.Comments = append(submission.Comments, comment)
461
459
}
462
460
}
463
461
···
477
475
return m, nil
478
476
}
479
477
478
+
func GetPullComments(e Execer, filters ...orm.Filter) ([]models.PullComment, error) {
479
+
var conditions []string
480
+
var args []any
481
+
for _, filter := range filters {
482
+
conditions = append(conditions, filter.Condition())
483
+
args = append(args, filter.Arg()...)
484
+
}
485
+
486
+
whereClause := ""
487
+
if conditions != nil {
488
+
whereClause = " where " + strings.Join(conditions, " and ")
489
+
}
490
+
491
+
query := fmt.Sprintf(`
492
+
select
493
+
id,
494
+
pull_id,
495
+
submission_id,
496
+
repo_at,
497
+
owner_did,
498
+
comment_at,
499
+
body,
500
+
created
501
+
from
502
+
pull_comments
503
+
%s
504
+
order by
505
+
created asc
506
+
`, whereClause)
507
+
508
+
rows, err := e.Query(query, args...)
509
+
if err != nil {
510
+
return nil, err
511
+
}
512
+
defer rows.Close()
513
+
514
+
commentMap := make(map[string]*models.PullComment)
515
+
for rows.Next() {
516
+
var comment models.PullComment
517
+
var createdAt string
518
+
err := rows.Scan(
519
+
&comment.ID,
520
+
&comment.PullId,
521
+
&comment.SubmissionId,
522
+
&comment.RepoAt,
523
+
&comment.OwnerDid,
524
+
&comment.CommentAt,
525
+
&comment.Body,
526
+
&createdAt,
527
+
)
528
+
if err != nil {
529
+
return nil, err
530
+
}
531
+
532
+
if t, err := time.Parse(time.RFC3339, createdAt); err == nil {
533
+
comment.Created = t
534
+
}
535
+
536
+
atUri := comment.AtUri().String()
537
+
commentMap[atUri] = &comment
538
+
}
539
+
540
+
if err := rows.Err(); err != nil {
541
+
return nil, err
542
+
}
543
+
544
+
// collect references for each comments
545
+
commentAts := slices.Collect(maps.Keys(commentMap))
546
+
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts))
547
+
if err != nil {
548
+
return nil, fmt.Errorf("failed to query reference_links: %w", err)
549
+
}
550
+
for commentAt, references := range allReferencs {
551
+
if comment, ok := commentMap[commentAt.String()]; ok {
552
+
comment.References = references
553
+
}
554
+
}
555
+
556
+
var comments []models.PullComment
557
+
for _, c := range commentMap {
558
+
comments = append(comments, *c)
559
+
}
560
+
561
+
sort.Slice(comments, func(i, j int) bool {
562
+
return comments[i].Created.Before(comments[j].Created)
563
+
})
564
+
565
+
return comments, nil
566
+
}
567
+
480
568
// timeframe here is directly passed into the sql query filter, and any
481
569
// timeframe in the past should be negative; e.g.: "-3 months"
482
570
func GetPullsByOwnerDid(e Execer, did, timeframe string) ([]models.Pull, error) {
···
551
639
}
552
640
553
641
return pulls, nil
642
+
}
643
+
644
+
func NewPullComment(tx *sql.Tx, comment *models.PullComment) (int64, error) {
645
+
query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)`
646
+
res, err := tx.Exec(
647
+
query,
648
+
comment.OwnerDid,
649
+
comment.RepoAt,
650
+
comment.SubmissionId,
651
+
comment.CommentAt,
652
+
comment.PullId,
653
+
comment.Body,
654
+
)
655
+
if err != nil {
656
+
return 0, err
657
+
}
658
+
659
+
i, err := res.LastInsertId()
660
+
if err != nil {
661
+
return 0, err
662
+
}
663
+
664
+
if err := putReferences(tx, comment.AtUri(), comment.References); err != nil {
665
+
return 0, fmt.Errorf("put reference_links: %w", err)
666
+
}
667
+
668
+
return i, nil
554
669
}
555
670
556
671
func SetPullState(e Execer, repoAt syntax.ATURI, pullId int, pullState models.PullState) error {
+32
-20
appview/db/reference.go
+32
-20
appview/db/reference.go
···
11
11
"tangled.org/core/orm"
12
12
)
13
13
14
-
// ValidateReferenceLinks resolves refLinks to Issue/PR/Comment ATURIs.
14
+
// ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs.
15
15
// It will ignore missing refLinks.
16
16
func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
17
17
var (
···
53
53
values %s
54
54
)
55
55
select
56
-
i.at_uri, c.at_uri
56
+
i.did, i.rkey,
57
+
c.did, c.rkey
57
58
from input inp
58
59
join repos r
59
60
on r.did = inp.owner_did
···
61
62
join issues i
62
63
on i.repo_at = r.at_uri
63
64
and i.issue_id = inp.issue_id
64
-
left join comments c
65
+
left join issue_comments c
65
66
on inp.comment_id is not null
66
-
and c.subject_at = i.at_uri
67
+
and c.issue_at = i.at_uri
67
68
and c.id = inp.comment_id
68
69
`,
69
70
strings.Join(vals, ","),
···
78
79
79
80
for rows.Next() {
80
81
// Scan rows
81
-
var issueUri string
82
-
var commentUri sql.NullString
82
+
var issueOwner, issueRkey string
83
+
var commentOwner, commentRkey sql.NullString
83
84
var uri syntax.ATURI
84
-
if err := rows.Scan(&issueUri, &commentUri); err != nil {
85
+
if err := rows.Scan(&issueOwner, &issueRkey, &commentOwner, &commentRkey); err != nil {
85
86
return nil, err
86
87
}
87
-
if commentUri.Valid {
88
-
uri = syntax.ATURI(commentUri.String)
88
+
if commentOwner.Valid && commentRkey.Valid {
89
+
uri = syntax.ATURI(fmt.Sprintf(
90
+
"at://%s/%s/%s",
91
+
commentOwner.String,
92
+
tangled.RepoIssueCommentNSID,
93
+
commentRkey.String,
94
+
))
89
95
} else {
90
-
uri = syntax.ATURI(issueUri)
96
+
uri = syntax.ATURI(fmt.Sprintf(
97
+
"at://%s/%s/%s",
98
+
issueOwner,
99
+
tangled.RepoIssueNSID,
100
+
issueRkey,
101
+
))
91
102
}
92
103
uris = append(uris, uri)
93
104
}
···
113
124
values %s
114
125
)
115
126
select
116
-
p.owner_did, p.rkey, c.at_uri
127
+
p.owner_did, p.rkey,
128
+
c.comment_at
117
129
from input inp
118
130
join repos r
119
131
on r.did = inp.owner_did
···
121
133
join pulls p
122
134
on p.repo_at = r.at_uri
123
135
and p.pull_id = inp.pull_id
124
-
left join comments c
136
+
left join pull_comments c
125
137
on inp.comment_id is not null
126
-
and c.subject_at = ('at://' || p.owner_did || '/' || 'sh.tangled.repo.pull' || '/' || p.rkey)
138
+
and c.repo_at = r.at_uri and c.pull_id = p.pull_id
127
139
and c.id = inp.comment_id
128
140
`,
129
141
strings.Join(vals, ","),
···
271
283
return nil, fmt.Errorf("get issue backlinks: %w", err)
272
284
}
273
285
backlinks = append(backlinks, ls...)
274
-
ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.CommentNSID])
286
+
ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.RepoIssueCommentNSID])
275
287
if err != nil {
276
288
return nil, fmt.Errorf("get issue_comment backlinks: %w", err)
277
289
}
···
281
293
return nil, fmt.Errorf("get pull backlinks: %w", err)
282
294
}
283
295
backlinks = append(backlinks, ls...)
284
-
ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.CommentNSID])
296
+
ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.RepoPullCommentNSID])
285
297
if err != nil {
286
298
return nil, fmt.Errorf("get pull_comment backlinks: %w", err)
287
299
}
···
340
352
rows, err := e.Query(
341
353
fmt.Sprintf(
342
354
`select r.did, r.name, i.issue_id, c.id, i.title, i.open
343
-
from comments c
355
+
from issue_comments c
344
356
join issues i
345
-
on i.at_uri = c.subject_at
357
+
on i.at_uri = c.issue_at
346
358
join repos r
347
359
on r.at_uri = i.repo_at
348
360
where %s`,
···
416
428
if len(aturis) == 0 {
417
429
return nil, nil
418
430
}
419
-
filter := orm.FilterIn("c.at_uri", aturis)
431
+
filter := orm.FilterIn("c.comment_at", aturis)
420
432
rows, err := e.Query(
421
433
fmt.Sprintf(
422
434
`select r.did, r.name, p.pull_id, c.id, p.title, p.state
423
435
from repos r
424
436
join pulls p
425
437
on r.at_uri = p.repo_at
426
-
join comments c
427
-
on ('at://' || p.owner_did || '/' || 'sh.tangled.repo.pull' || '/' || p.rkey) = c.subject_at
438
+
join pull_comments c
439
+
on r.at_uri = c.repo_at and p.pull_id = c.pull_id
428
440
where %s`,
429
441
filter.Condition(),
430
442
),
+11
-19
appview/ingester.go
+11
-19
appview/ingester.go
···
79
79
err = i.ingestString(e)
80
80
case tangled.RepoIssueNSID:
81
81
err = i.ingestIssue(ctx, e)
82
-
case tangled.CommentNSID:
83
-
err = i.ingestComment(e)
82
+
case tangled.RepoIssueCommentNSID:
83
+
err = i.ingestIssueComment(e)
84
84
case tangled.LabelDefinitionNSID:
85
85
err = i.ingestLabelDefinition(e)
86
86
case tangled.LabelOpNSID:
···
868
868
return nil
869
869
}
870
870
871
-
func (i *Ingester) ingestComment(e *jmodels.Event) error {
871
+
func (i *Ingester) ingestIssueComment(e *jmodels.Event) error {
872
872
did := e.Did
873
873
rkey := e.Commit.RKey
874
874
875
875
var err error
876
876
877
-
l := i.Logger.With("handler", "ingestComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey)
877
+
l := i.Logger.With("handler", "ingestIssueComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey)
878
878
l.Info("ingesting record")
879
879
880
880
ddb, ok := i.Db.Execer.(*db.DB)
···
885
885
switch e.Commit.Operation {
886
886
case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate:
887
887
raw := json.RawMessage(e.Commit.Record)
888
-
record := tangled.Comment{}
888
+
record := tangled.RepoIssueComment{}
889
889
err = json.Unmarshal(raw, &record)
890
890
if err != nil {
891
891
return fmt.Errorf("invalid record: %w", err)
892
892
}
893
893
894
-
comment, err := models.CommentFromRecord(did, rkey, record)
894
+
comment, err := models.IssueCommentFromRecord(did, rkey, record)
895
895
if err != nil {
896
896
return fmt.Errorf("failed to parse comment from record: %w", err)
897
897
}
898
898
899
-
// TODO: ingest pull comments
900
-
// we aren't ingesting pull comments yet because pull itself isn't fully atprotated.
901
-
// so we cannot know which round this comment is pointing to
902
-
if comment.Subject.Collection().String() == tangled.RepoPullNSID {
903
-
l.Info("skip ingesting pull comments")
904
-
return nil
905
-
}
906
-
907
-
if err := comment.Validate(); err != nil {
899
+
if err := i.Validator.ValidateIssueComment(comment); err != nil {
908
900
return fmt.Errorf("failed to validate comment: %w", err)
909
901
}
910
902
···
914
906
}
915
907
defer tx.Rollback()
916
908
917
-
err = db.PutComment(tx, comment)
909
+
_, err = db.AddIssueComment(tx, *comment)
918
910
if err != nil {
919
-
return fmt.Errorf("failed to create comment: %w", err)
911
+
return fmt.Errorf("failed to create issue comment: %w", err)
920
912
}
921
913
922
914
return tx.Commit()
923
915
924
916
case jmodels.CommitOperationDelete:
925
-
if err := db.DeleteComments(
917
+
if err := db.DeleteIssueComments(
926
918
ddb,
927
919
orm.FilterEq("did", did),
928
920
orm.FilterEq("rkey", rkey),
929
921
); err != nil {
930
-
return fmt.Errorf("failed to delete comment record: %w", err)
922
+
return fmt.Errorf("failed to delete issue comment record: %w", err)
931
923
}
932
924
933
925
return nil
+28
-30
appview/issues/issues.go
+28
-30
appview/issues/issues.go
···
403
403
404
404
body := r.FormValue("body")
405
405
if body == "" {
406
-
rp.pages.Notice(w, "issue-comment", "Body is required")
406
+
rp.pages.Notice(w, "issue", "Body is required")
407
407
return
408
408
}
409
409
410
-
var replyTo *syntax.ATURI
411
-
replyToRaw := r.FormValue("reply-to")
412
-
if replyToRaw != "" {
413
-
aturi, err := syntax.ParseATURI(r.FormValue("reply-to"))
414
-
if err != nil {
415
-
rp.pages.Notice(w, "issue-comment", "reply-to should be valid AT-URI")
416
-
return
417
-
}
418
-
replyTo = &aturi
410
+
replyToUri := r.FormValue("reply-to")
411
+
var replyTo *string
412
+
if replyToUri != "" {
413
+
replyTo = &replyToUri
419
414
}
420
415
421
416
mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
422
417
423
-
comment := models.Comment{
424
-
Did: syntax.DID(user.Did),
418
+
comment := models.IssueComment{
419
+
Did: user.Did,
425
420
Rkey: tid.TID(),
426
-
Subject: issue.AtUri(),
421
+
IssueAt: issue.AtUri().String(),
427
422
ReplyTo: replyTo,
428
423
Body: body,
429
424
Created: time.Now(),
430
425
Mentions: mentions,
431
426
References: references,
432
427
}
433
-
if err = comment.Validate(); err != nil {
428
+
if err = rp.validator.ValidateIssueComment(&comment); err != nil {
434
429
l.Error("failed to validate comment", "err", err)
435
430
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
436
431
return
···
446
441
447
442
// create a record first
448
443
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
449
-
Collection: tangled.CommentNSID,
450
-
Repo: user.Did,
444
+
Collection: tangled.RepoIssueCommentNSID,
445
+
Repo: comment.Did,
451
446
Rkey: comment.Rkey,
452
447
Record: &lexutil.LexiconTypeDecoder{
453
448
Val: &record,
···
473
468
}
474
469
defer tx.Rollback()
475
470
476
-
err = db.PutComment(tx, &comment)
471
+
commentId, err := db.AddIssueComment(tx, comment)
477
472
if err != nil {
478
473
l.Error("failed to create comment", "err", err)
479
474
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
···
489
484
// reset atUri to make rollback a no-op
490
485
atUri = ""
491
486
487
+
// notify about the new comment
488
+
comment.Id = commentId
489
+
492
490
rp.notifier.NewIssueComment(r.Context(), &comment, mentions)
493
491
494
492
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
495
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, comment.Id))
493
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, commentId))
496
494
}
497
495
498
496
func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
···
507
505
}
508
506
509
507
commentId := chi.URLParam(r, "commentId")
510
-
comments, err := db.GetComments(
508
+
comments, err := db.GetIssueComments(
511
509
rp.db,
512
510
orm.FilterEq("id", commentId),
513
511
)
···
543
541
}
544
542
545
543
commentId := chi.URLParam(r, "commentId")
546
-
comments, err := db.GetComments(
544
+
comments, err := db.GetIssueComments(
547
545
rp.db,
548
546
orm.FilterEq("id", commentId),
549
547
)
···
559
557
}
560
558
comment := comments[0]
561
559
562
-
if comment.Did.String() != user.Did {
560
+
if comment.Did != user.Did {
563
561
l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did)
564
562
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
565
563
return
···
599
597
}
600
598
defer tx.Rollback()
601
599
602
-
err = db.PutComment(tx, &newComment)
600
+
_, err = db.AddIssueComment(tx, newComment)
603
601
if err != nil {
604
602
l.Error("failed to perferom update-description query", "err", err)
605
603
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
···
610
608
// rkey is optional, it was introduced later
611
609
if newComment.Rkey != "" {
612
610
// update the record on pds
613
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.CommentNSID, user.Did, comment.Rkey)
611
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey)
614
612
if err != nil {
615
613
l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey)
616
614
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
···
618
616
}
619
617
620
618
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
621
-
Collection: tangled.CommentNSID,
619
+
Collection: tangled.RepoIssueCommentNSID,
622
620
Repo: user.Did,
623
621
Rkey: newComment.Rkey,
624
622
SwapRecord: ex.Cid,
···
653
651
}
654
652
655
653
commentId := chi.URLParam(r, "commentId")
656
-
comments, err := db.GetComments(
654
+
comments, err := db.GetIssueComments(
657
655
rp.db,
658
656
orm.FilterEq("id", commentId),
659
657
)
···
689
687
}
690
688
691
689
commentId := chi.URLParam(r, "commentId")
692
-
comments, err := db.GetComments(
690
+
comments, err := db.GetIssueComments(
693
691
rp.db,
694
692
orm.FilterEq("id", commentId),
695
693
)
···
725
723
}
726
724
727
725
commentId := chi.URLParam(r, "commentId")
728
-
comments, err := db.GetComments(
726
+
comments, err := db.GetIssueComments(
729
727
rp.db,
730
728
orm.FilterEq("id", commentId),
731
729
)
···
741
739
}
742
740
comment := comments[0]
743
741
744
-
if comment.Did.String() != user.Did {
742
+
if comment.Did != user.Did {
745
743
l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did)
746
744
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
747
745
return
···
754
752
755
753
// optimistic deletion
756
754
deleted := time.Now()
757
-
err = db.DeleteComments(rp.db, orm.FilterEq("id", comment.Id))
755
+
err = db.DeleteIssueComments(rp.db, orm.FilterEq("id", comment.Id))
758
756
if err != nil {
759
757
l.Error("failed to delete comment", "err", err)
760
758
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
···
770
768
return
771
769
}
772
770
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
773
-
Collection: tangled.CommentNSID,
771
+
Collection: tangled.RepoIssueCommentNSID,
774
772
Repo: user.Did,
775
773
Rkey: comment.Rkey,
776
774
})
+2
-2
appview/issues/opengraph.go
+2
-2
appview/issues/opengraph.go
···
193
193
dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2)
194
194
dollyY := statsY + iconBaselineOffset - dollySize/2 + 25
195
195
dollyColor := color.RGBA{180, 180, 180, 255} // light gray
196
-
err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor)
196
+
err = dollyArea.DrawDolly(dollyX, dollyY, dollySize, dollyColor)
197
197
if err != nil {
198
-
log.Printf("dolly silhouette not available (this is ok): %v", err)
198
+
log.Printf("dolly not available (this is ok): %v", err)
199
199
}
200
200
201
201
// Draw "opened by @author" and date at the bottom with more spacing
-117
appview/models/comment.go
-117
appview/models/comment.go
···
1
-
package models
2
-
3
-
import (
4
-
"fmt"
5
-
"strings"
6
-
"time"
7
-
8
-
"github.com/bluesky-social/indigo/atproto/syntax"
9
-
"tangled.org/core/api/tangled"
10
-
)
11
-
12
-
type Comment struct {
13
-
Id int64
14
-
Did syntax.DID
15
-
Rkey string
16
-
Subject syntax.ATURI
17
-
ReplyTo *syntax.ATURI
18
-
Body string
19
-
Created time.Time
20
-
Edited *time.Time
21
-
Deleted *time.Time
22
-
Mentions []syntax.DID
23
-
References []syntax.ATURI
24
-
PullSubmissionId *int
25
-
}
26
-
27
-
func (c *Comment) AtUri() syntax.ATURI {
28
-
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", c.Did, tangled.CommentNSID, c.Rkey))
29
-
}
30
-
31
-
func (c *Comment) AsRecord() tangled.Comment {
32
-
mentions := make([]string, len(c.Mentions))
33
-
for i, did := range c.Mentions {
34
-
mentions[i] = string(did)
35
-
}
36
-
references := make([]string, len(c.References))
37
-
for i, uri := range c.References {
38
-
references[i] = string(uri)
39
-
}
40
-
var replyTo *string
41
-
if c.ReplyTo != nil {
42
-
replyToStr := c.ReplyTo.String()
43
-
replyTo = &replyToStr
44
-
}
45
-
return tangled.Comment{
46
-
Subject: c.Subject.String(),
47
-
Body: c.Body,
48
-
CreatedAt: c.Created.Format(time.RFC3339),
49
-
ReplyTo: replyTo,
50
-
Mentions: mentions,
51
-
References: references,
52
-
}
53
-
}
54
-
55
-
func (c *Comment) IsTopLevel() bool {
56
-
return c.ReplyTo == nil
57
-
}
58
-
59
-
func (c *Comment) IsReply() bool {
60
-
return c.ReplyTo != nil
61
-
}
62
-
63
-
func (c *Comment) Validate() error {
64
-
// TODO: sanitize the body and then trim space
65
-
if sb := strings.TrimSpace(c.Body); sb == "" {
66
-
return fmt.Errorf("body is empty after HTML sanitization")
67
-
}
68
-
69
-
// if it's for PR, PullSubmissionId should not be nil
70
-
if c.Subject.Collection().String() == tangled.RepoPullNSID {
71
-
if c.PullSubmissionId == nil {
72
-
return fmt.Errorf("PullSubmissionId should not be nil")
73
-
}
74
-
}
75
-
return nil
76
-
}
77
-
78
-
func CommentFromRecord(did, rkey string, record tangled.Comment) (*Comment, error) {
79
-
created, err := time.Parse(time.RFC3339, record.CreatedAt)
80
-
if err != nil {
81
-
created = time.Now()
82
-
}
83
-
84
-
ownerDid := did
85
-
86
-
if _, err = syntax.ParseATURI(record.Subject); err != nil {
87
-
return nil, err
88
-
}
89
-
90
-
i := record
91
-
mentions := make([]syntax.DID, len(record.Mentions))
92
-
for i, did := range record.Mentions {
93
-
mentions[i] = syntax.DID(did)
94
-
}
95
-
references := make([]syntax.ATURI, len(record.References))
96
-
for i, uri := range i.References {
97
-
references[i] = syntax.ATURI(uri)
98
-
}
99
-
var replyTo *syntax.ATURI
100
-
if record.ReplyTo != nil {
101
-
replyToAtUri := syntax.ATURI(*record.ReplyTo)
102
-
replyTo = &replyToAtUri
103
-
}
104
-
105
-
comment := Comment{
106
-
Did: syntax.DID(ownerDid),
107
-
Rkey: rkey,
108
-
Body: record.Body,
109
-
Subject: syntax.ATURI(record.Subject),
110
-
ReplyTo: replyTo,
111
-
Created: created,
112
-
Mentions: mentions,
113
-
References: references,
114
-
}
115
-
116
-
return &comment, nil
117
-
}
+89
-8
appview/models/issue.go
+89
-8
appview/models/issue.go
···
26
26
27
27
// optionally, populate this when querying for reverse mappings
28
28
// like comment counts, parent repo etc.
29
-
Comments []Comment
29
+
Comments []IssueComment
30
30
Labels LabelState
31
31
Repo *Repo
32
32
}
···
62
62
}
63
63
64
64
type CommentListItem struct {
65
-
Self *Comment
66
-
Replies []*Comment
65
+
Self *IssueComment
66
+
Replies []*IssueComment
67
67
}
68
68
69
69
func (it *CommentListItem) Participants() []syntax.DID {
···
88
88
89
89
func (i *Issue) CommentList() []CommentListItem {
90
90
// Create a map to quickly find comments by their aturi
91
-
toplevel := make(map[syntax.ATURI]*CommentListItem)
92
-
var replies []*Comment
91
+
toplevel := make(map[string]*CommentListItem)
92
+
var replies []*IssueComment
93
93
94
94
// collect top level comments into the map
95
95
for _, comment := range i.Comments {
96
96
if comment.IsTopLevel() {
97
-
toplevel[comment.AtUri()] = &CommentListItem{
97
+
toplevel[comment.AtUri().String()] = &CommentListItem{
98
98
Self: &comment,
99
99
}
100
100
} else {
···
115
115
}
116
116
117
117
// sort everything
118
-
sortFunc := func(a, b *Comment) bool {
118
+
sortFunc := func(a, b *IssueComment) bool {
119
119
return a.Created.Before(b.Created)
120
120
}
121
121
sort.Slice(listing, func(i, j int) bool {
···
144
144
addParticipant(i.Did)
145
145
146
146
for _, c := range i.Comments {
147
-
addParticipant(c.Did.String())
147
+
addParticipant(c.Did)
148
148
}
149
149
150
150
return participants
···
171
171
Open: true, // new issues are open by default
172
172
}
173
173
}
174
+
175
+
type IssueComment struct {
176
+
Id int64
177
+
Did string
178
+
Rkey string
179
+
IssueAt string
180
+
ReplyTo *string
181
+
Body string
182
+
Created time.Time
183
+
Edited *time.Time
184
+
Deleted *time.Time
185
+
Mentions []syntax.DID
186
+
References []syntax.ATURI
187
+
}
188
+
189
+
func (i *IssueComment) AtUri() syntax.ATURI {
190
+
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueCommentNSID, i.Rkey))
191
+
}
192
+
193
+
func (i *IssueComment) AsRecord() tangled.RepoIssueComment {
194
+
mentions := make([]string, len(i.Mentions))
195
+
for i, did := range i.Mentions {
196
+
mentions[i] = string(did)
197
+
}
198
+
references := make([]string, len(i.References))
199
+
for i, uri := range i.References {
200
+
references[i] = string(uri)
201
+
}
202
+
return tangled.RepoIssueComment{
203
+
Body: i.Body,
204
+
Issue: i.IssueAt,
205
+
CreatedAt: i.Created.Format(time.RFC3339),
206
+
ReplyTo: i.ReplyTo,
207
+
Mentions: mentions,
208
+
References: references,
209
+
}
210
+
}
211
+
212
+
func (i *IssueComment) IsTopLevel() bool {
213
+
return i.ReplyTo == nil
214
+
}
215
+
216
+
func (i *IssueComment) IsReply() bool {
217
+
return i.ReplyTo != nil
218
+
}
219
+
220
+
func IssueCommentFromRecord(did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) {
221
+
created, err := time.Parse(time.RFC3339, record.CreatedAt)
222
+
if err != nil {
223
+
created = time.Now()
224
+
}
225
+
226
+
ownerDid := did
227
+
228
+
if _, err = syntax.ParseATURI(record.Issue); err != nil {
229
+
return nil, err
230
+
}
231
+
232
+
i := record
233
+
mentions := make([]syntax.DID, len(record.Mentions))
234
+
for i, did := range record.Mentions {
235
+
mentions[i] = syntax.DID(did)
236
+
}
237
+
references := make([]syntax.ATURI, len(record.References))
238
+
for i, uri := range i.References {
239
+
references[i] = syntax.ATURI(uri)
240
+
}
241
+
242
+
comment := IssueComment{
243
+
Did: ownerDid,
244
+
Rkey: rkey,
245
+
Body: record.Body,
246
+
IssueAt: record.Issue,
247
+
ReplyTo: record.ReplyTo,
248
+
Created: created,
249
+
Mentions: mentions,
250
+
References: references,
251
+
}
252
+
253
+
return &comment, nil
254
+
}
+46
-2
appview/models/pull.go
+46
-2
appview/models/pull.go
···
138
138
RoundNumber int
139
139
Patch string
140
140
Combined string
141
-
Comments []Comment
141
+
Comments []PullComment
142
142
SourceRev string // include the rev that was used to create this submission: only for branch/fork PRs
143
143
144
144
// meta
145
145
Created time.Time
146
146
}
147
+
148
+
type PullComment struct {
149
+
// ids
150
+
ID int
151
+
PullId int
152
+
SubmissionId int
153
+
154
+
// at ids
155
+
RepoAt string
156
+
OwnerDid string
157
+
CommentAt string
158
+
159
+
// content
160
+
Body string
161
+
162
+
// meta
163
+
Mentions []syntax.DID
164
+
References []syntax.ATURI
165
+
166
+
// meta
167
+
Created time.Time
168
+
}
169
+
170
+
func (p *PullComment) AtUri() syntax.ATURI {
171
+
return syntax.ATURI(p.CommentAt)
172
+
}
173
+
174
+
// func (p *PullComment) AsRecord() tangled.RepoPullComment {
175
+
// mentions := make([]string, len(p.Mentions))
176
+
// for i, did := range p.Mentions {
177
+
// mentions[i] = string(did)
178
+
// }
179
+
// references := make([]string, len(p.References))
180
+
// for i, uri := range p.References {
181
+
// references[i] = string(uri)
182
+
// }
183
+
// return tangled.RepoPullComment{
184
+
// Pull: p.PullAt,
185
+
// Body: p.Body,
186
+
// Mentions: mentions,
187
+
// References: references,
188
+
// CreatedAt: p.Created.Format(time.RFC3339),
189
+
// }
190
+
// }
147
191
148
192
func (p *Pull) LastRoundNumber() int {
149
193
return len(p.Submissions) - 1
···
245
289
addParticipant(s.PullAt.Authority().String())
246
290
247
291
for _, c := range s.Comments {
248
-
addParticipant(c.Did.String())
292
+
addParticipant(c.OwnerDid)
249
293
}
250
294
251
295
return participants
+10
-15
appview/notify/db/db.go
+10
-15
appview/notify/db/db.go
···
122
122
)
123
123
}
124
124
125
-
func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) {
126
-
issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.Subject))
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))
127
127
if err != nil {
128
128
log.Printf("NewIssueComment: failed to get issues: %v", err)
129
129
return
130
130
}
131
131
if len(issues) == 0 {
132
-
log.Printf("NewIssueComment: no issue found for %s", comment.Subject)
132
+
log.Printf("NewIssueComment: no issue found for %s", comment.IssueAt)
133
133
return
134
134
}
135
135
issue := issues[0]
···
147
147
148
148
// find the parent thread, and add all DIDs from here to the recipient list
149
149
for _, t := range issue.CommentList() {
150
-
if t.Self.AtUri() == parentAtUri {
150
+
if t.Self.AtUri().String() == parentAtUri {
151
151
for _, p := range t.Participants() {
152
152
recipients.Insert(p)
153
153
}
···
260
260
)
261
261
}
262
262
263
-
func (n *databaseNotifier) NewPullComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) {
264
-
pulls, err := db.GetPulls(n.db,
265
-
orm.FilterEq("owner_did", comment.Subject.Authority()),
266
-
orm.FilterEq("rkey", comment.Subject.RecordKey()),
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
267
)
268
268
if err != nil {
269
269
log.Printf("NewPullComment: failed to get pulls: %v", err)
270
270
return
271
271
}
272
-
if len(pulls) == 0 {
273
-
log.Printf("NewPullComment: no pull found for %s", comment.Subject)
274
-
return
275
-
}
276
-
pull := pulls[0]
277
272
278
-
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", pull.RepoAt))
273
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", comment.RepoAt))
279
274
if err != nil {
280
275
log.Printf("NewPullComment: failed to get repos: %v", err)
281
276
return
···
293
288
recipients.Remove(m)
294
289
}
295
290
296
-
actorDid := comment.Did
291
+
actorDid := syntax.DID(comment.OwnerDid)
297
292
eventType := models.NotificationTypePullCommented
298
293
entityType := "pull"
299
294
entityId := pull.AtUri().String()
+2
-2
appview/notify/merged_notifier.go
+2
-2
appview/notify/merged_notifier.go
···
57
57
m.fanout("NewIssue", ctx, issue, mentions)
58
58
}
59
59
60
-
func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) {
60
+
func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
61
61
m.fanout("NewIssueComment", ctx, comment, mentions)
62
62
}
63
63
···
81
81
m.fanout("NewPull", ctx, pull)
82
82
}
83
83
84
-
func (m *mergedNotifier) NewPullComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) {
84
+
func (m *mergedNotifier) NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) {
85
85
m.fanout("NewPullComment", ctx, comment, mentions)
86
86
}
87
87
+4
-4
appview/notify/notifier.go
+4
-4
appview/notify/notifier.go
···
14
14
DeleteStar(ctx context.Context, star *models.Star)
15
15
16
16
NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID)
17
-
NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID)
17
+
NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID)
18
18
NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue)
19
19
DeleteIssue(ctx context.Context, issue *models.Issue)
20
20
···
22
22
DeleteFollow(ctx context.Context, follow *models.Follow)
23
23
24
24
NewPull(ctx context.Context, pull *models.Pull)
25
-
NewPullComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID)
25
+
NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID)
26
26
NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull)
27
27
28
28
UpdateProfile(ctx context.Context, profile *models.Profile)
···
43
43
func (m *BaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {}
44
44
45
45
func (m *BaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {}
46
-
func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) {
46
+
func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
47
47
}
48
48
func (m *BaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {}
49
49
func (m *BaseNotifier) DeleteIssue(ctx context.Context, issue *models.Issue) {}
···
52
52
func (m *BaseNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {}
53
53
54
54
func (m *BaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {}
55
-
func (m *BaseNotifier) NewPullComment(ctx context.Context, models *models.Comment, mentions []syntax.DID) {
55
+
func (m *BaseNotifier) NewPullComment(ctx context.Context, models *models.PullComment, mentions []syntax.DID) {
56
56
}
57
57
func (m *BaseNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {}
58
58
+7
-6
appview/notify/posthog/notifier.go
+7
-6
appview/notify/posthog/notifier.go
···
86
86
}
87
87
}
88
88
89
-
func (n *posthogNotifier) NewPullComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) {
89
+
func (n *posthogNotifier) NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) {
90
90
err := n.client.Enqueue(posthog.Capture{
91
-
DistinctId: comment.Did.String(),
91
+
DistinctId: comment.OwnerDid,
92
92
Event: "new_pull_comment",
93
93
Properties: posthog.Properties{
94
-
"pull_at": comment.Subject,
94
+
"repo_at": comment.RepoAt,
95
+
"pull_id": comment.PullId,
95
96
"mentions": mentions,
96
97
},
97
98
})
···
179
180
}
180
181
}
181
182
182
-
func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) {
183
+
func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
183
184
err := n.client.Enqueue(posthog.Capture{
184
-
DistinctId: comment.Did.String(),
185
+
DistinctId: comment.Did,
185
186
Event: "new_issue_comment",
186
187
Properties: posthog.Properties{
187
-
"issue_at": comment.Subject,
188
+
"issue_at": comment.IssueAt,
188
189
"mentions": mentions,
189
190
},
190
191
})
+9
-9
appview/ogcard/card.go
+9
-9
appview/ogcard/card.go
···
334
334
return nil
335
335
}
336
336
337
-
func (c *Card) DrawDollySilhouette(x, y, size int, iconColor color.Color) error {
337
+
func (c *Card) DrawDolly(x, y, size int, iconColor color.Color) error {
338
338
tpl, err := template.New("dolly").
339
-
ParseFS(pages.Files, "templates/fragments/dolly/silhouette.html")
339
+
ParseFS(pages.Files, "templates/fragments/dolly/logo.html")
340
340
if err != nil {
341
-
return fmt.Errorf("failed to read dolly silhouette template: %w", err)
341
+
return fmt.Errorf("failed to read dolly template: %w", err)
342
342
}
343
343
344
344
var svgData bytes.Buffer
345
-
if err = tpl.ExecuteTemplate(&svgData, "fragments/dolly/silhouette", nil); err != nil {
346
-
return fmt.Errorf("failed to execute dolly silhouette template: %w", err)
345
+
if err = tpl.ExecuteTemplate(&svgData, "fragments/dolly/logo", nil); err != nil {
346
+
return fmt.Errorf("failed to execute dolly template: %w", err)
347
347
}
348
348
349
349
icon, err := BuildSVGIconFromData(svgData.Bytes(), iconColor)
···
453
453
454
454
// Handle SVG separately
455
455
if contentType == "image/svg+xml" || strings.HasSuffix(url, ".svg") {
456
-
return c.convertSVGToPNG(bodyBytes)
456
+
return convertSVGToPNG(bodyBytes)
457
457
}
458
458
459
459
// Support content types are in-sync with the allowed custom avatar file types
···
493
493
}
494
494
495
495
// convertSVGToPNG converts SVG data to a PNG image
496
-
func (c *Card) convertSVGToPNG(svgData []byte) (image.Image, bool) {
496
+
func convertSVGToPNG(svgData []byte) (image.Image, bool) {
497
497
// Parse the SVG
498
498
icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData))
499
499
if err != nil {
···
547
547
draw.CatmullRom.Scale(scaledImg, scaledImg.Bounds(), img, srcBounds, draw.Src, nil)
548
548
549
549
// Draw the image with circular clipping
550
-
for cy := 0; cy < size; cy++ {
551
-
for cx := 0; cx < size; cx++ {
550
+
for cy := range size {
551
+
for cx := range size {
552
552
// Calculate distance from center
553
553
dx := float64(cx - center)
554
554
dy := float64(cy - center)
+16
-5
appview/pages/pages.go
+16
-5
appview/pages/pages.go
···
210
210
return tpl.ExecuteTemplate(w, "layouts/base", params)
211
211
}
212
212
213
+
type DollyParams struct {
214
+
Classes string
215
+
FillColor string
216
+
}
217
+
218
+
func (p *Pages) Dolly(w io.Writer, params DollyParams) error {
219
+
return p.executePlain("fragments/dolly/logo", w, params)
220
+
}
221
+
213
222
func (p *Pages) Favicon(w io.Writer) error {
214
-
return p.executePlain("fragments/dolly/silhouette", w, nil)
223
+
return p.Dolly(w, DollyParams{
224
+
Classes: "text-black dark:text-white",
225
+
})
215
226
}
216
227
217
228
type LoginParams struct {
···
988
999
LoggedInUser *oauth.User
989
1000
RepoInfo repoinfo.RepoInfo
990
1001
Issue *models.Issue
991
-
Comment *models.Comment
1002
+
Comment *models.IssueComment
992
1003
}
993
1004
994
1005
func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error {
···
999
1010
LoggedInUser *oauth.User
1000
1011
RepoInfo repoinfo.RepoInfo
1001
1012
Issue *models.Issue
1002
-
Comment *models.Comment
1013
+
Comment *models.IssueComment
1003
1014
}
1004
1015
1005
1016
func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error {
···
1010
1021
LoggedInUser *oauth.User
1011
1022
RepoInfo repoinfo.RepoInfo
1012
1023
Issue *models.Issue
1013
-
Comment *models.Comment
1024
+
Comment *models.IssueComment
1014
1025
}
1015
1026
1016
1027
func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error {
···
1021
1032
LoggedInUser *oauth.User
1022
1033
RepoInfo repoinfo.RepoInfo
1023
1034
Issue *models.Issue
1024
-
Comment *models.Comment
1035
+
Comment *models.IssueComment
1025
1036
}
1026
1037
1027
1038
func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error {
+9
-29
appview/pages/templates/brand/brand.html
+9
-29
appview/pages/templates/brand/brand.html
···
4
4
<div class="grid grid-cols-10">
5
5
<header class="col-span-full md:col-span-10 px-6 py-2 mb-4">
6
6
<h1 class="text-2xl font-bold dark:text-white mb-1">Brand</h1>
7
-
<p class="text-gray-600 dark:text-gray-400 mb-1">
7
+
<p class="text-gray-500 dark:text-gray-300 mb-1">
8
8
Assets and guidelines for using Tangled's logo and brand elements.
9
9
</p>
10
10
</header>
···
14
14
15
15
<!-- Introduction Section -->
16
16
<section>
17
-
<p class="text-gray-600 dark:text-gray-400 mb-2">
17
+
<p class="text-gray-500 dark:text-gray-300 mb-2">
18
18
Tangled's logo and mascot is <strong>Dolly</strong>, the first ever <em>cloned</em> mammal. Please
19
19
follow the below guidelines when using Dolly and the logotype.
20
20
</p>
21
-
<p class="text-gray-600 dark:text-gray-400 mb-2">
21
+
<p class="text-gray-500 dark:text-gray-300 mb-2">
22
22
All assets are served as SVGs, and can be downloaded by right-clicking and clicking "Save image as".
23
23
</p>
24
24
</section>
···
34
34
</div>
35
35
<div class="order-1 lg:order-2">
36
36
<h2 class="text-xl font-semibold dark:text-white mb-3">Black logotype</h2>
37
-
<p class="text-gray-600 dark:text-gray-400 mb-4">For use on light-colored backgrounds.</p>
37
+
<p class="text-gray-500 dark:text-gray-300 mb-4">For use on light-colored backgrounds.</p>
38
38
<p class="text-gray-700 dark:text-gray-300">
39
39
This is the preferred version of the logotype, featuring dark text and elements, ideal for light
40
40
backgrounds and designs.
···
53
53
</div>
54
54
<div class="order-1 lg:order-2">
55
55
<h2 class="text-xl font-semibold dark:text-white mb-3">White logotype</h2>
56
-
<p class="text-gray-600 dark:text-gray-400 mb-4">For use on dark-colored backgrounds.</p>
56
+
<p class="text-gray-500 dark:text-gray-300 mb-4">For use on dark-colored backgrounds.</p>
57
57
<p class="text-gray-700 dark:text-gray-300">
58
58
This version features white text and elements, ideal for dark backgrounds
59
59
and inverted designs.
···
81
81
</div>
82
82
<div class="order-1 lg:order-2">
83
83
<h2 class="text-xl font-semibold dark:text-white mb-3">Mark only</h2>
84
-
<p class="text-gray-600 dark:text-gray-400 mb-4">
84
+
<p class="text-gray-500 dark:text-gray-300 mb-4">
85
85
When a smaller 1:1 logo or icon is needed, Dolly's face may be used on its own.
86
86
</p>
87
87
<p class="text-gray-700 dark:text-gray-300 mb-4">
···
123
123
</div>
124
124
<div class="order-1 lg:order-2">
125
125
<h2 class="text-xl font-semibold dark:text-white mb-3">Colored backgrounds</h2>
126
-
<p class="text-gray-600 dark:text-gray-400 mb-4">
126
+
<p class="text-gray-500 dark:text-gray-300 mb-4">
127
127
White logo mark on colored backgrounds.
128
128
</p>
129
129
<p class="text-gray-700 dark:text-gray-300 mb-4">
···
165
165
</div>
166
166
<div class="order-1 lg:order-2">
167
167
<h2 class="text-xl font-semibold dark:text-white mb-3">Lighter backgrounds</h2>
168
-
<p class="text-gray-600 dark:text-gray-400 mb-4">
168
+
<p class="text-gray-500 dark:text-gray-300 mb-4">
169
169
Dark logo mark on lighter, pastel backgrounds.
170
170
</p>
171
171
<p class="text-gray-700 dark:text-gray-300 mb-4">
···
186
186
</div>
187
187
<div class="order-1 lg:order-2">
188
188
<h2 class="text-xl font-semibold dark:text-white mb-3">Recoloring</h2>
189
-
<p class="text-gray-600 dark:text-gray-400 mb-4">
189
+
<p class="text-gray-500 dark:text-gray-300 mb-4">
190
190
Custom coloring of the logotype is permitted.
191
191
</p>
192
192
<p class="text-gray-700 dark:text-gray-300 mb-4">
···
194
194
</p>
195
195
<p class="text-gray-700 dark:text-gray-300 text-sm">
196
196
<strong>Example:</strong> Gray/sand colored logotype on a light yellow/tan background.
197
-
</p>
198
-
</div>
199
-
</section>
200
-
201
-
<!-- Silhouette Section -->
202
-
<section class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">
203
-
<div class="order-2 lg:order-1">
204
-
<div class="border border-gray-200 dark:border-gray-700 p-8 sm:p-16 bg-gray-50 dark:bg-gray-100 rounded">
205
-
<img src="https://assets.tangled.network/tangled_dolly_silhouette.svg"
206
-
alt="Dolly silhouette"
207
-
class="w-full max-w-32 mx-auto" />
208
-
</div>
209
-
</div>
210
-
<div class="order-1 lg:order-2">
211
-
<h2 class="text-xl font-semibold dark:text-white mb-3">Dolly silhouette</h2>
212
-
<p class="text-gray-600 dark:text-gray-400 mb-4">A minimalist version of Dolly.</p>
213
-
<p class="text-gray-700 dark:text-gray-300">
214
-
The silhouette can be used where a subtle brand presence is needed,
215
-
or as a background element. Works on any background color with proper contrast.
216
-
For example, we use this as the site's favicon.
217
197
</p>
218
198
</div>
219
199
</section>
+14
-2
appview/pages/templates/fragments/dolly/logo.html
+14
-2
appview/pages/templates/fragments/dolly/logo.html
···
2
2
<svg
3
3
version="1.1"
4
4
id="svg1"
5
-
class="{{ . }}"
5
+
class="{{ .Classes }}"
6
6
width="25"
7
7
height="25"
8
8
viewBox="0 0 25 25"
···
17
17
xmlns:svg="http://www.w3.org/2000/svg"
18
18
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
19
19
xmlns:cc="http://creativecommons.org/ns#">
20
+
<style>
21
+
.dolly {
22
+
color: #000000;
23
+
}
24
+
25
+
@media (prefers-color-scheme: dark) {
26
+
.dolly {
27
+
color: #ffffff;
28
+
}
29
+
}
30
+
</style>
20
31
<sodipodi:namedview
21
32
id="namedview1"
22
33
pagecolor="#ffffff"
···
51
62
id="g1"
52
63
transform="translate(-0.42924038,-0.87777209)">
53
64
<path
54
-
fill="currentColor"
65
+
class="dolly"
66
+
fill="{{ or .FillColor "currentColor" }}"
55
67
style="stroke-width:0.111183;"
56
68
d="m 16.775491,24.987061 c -0.78517,-0.0064 -1.384202,-0.234614 -2.033994,-0.631295 -0.931792,-0.490188 -1.643475,-1.31368 -2.152014,-2.221647 C 11.781409,23.136647 10.701392,23.744942 9.4922931,24.0886 8.9774725,24.238111 8.0757679,24.389777 6.5811304,23.84827 4.4270703,23.124679 2.8580086,20.883331 3.0363279,18.599583 3.0037061,17.652919 3.3488675,16.723769 3.8381157,15.925061 2.5329485,15.224503 1.4686756,14.048584 1.0611184,12.606459 0.81344502,11.816973 0.82385989,10.966486 0.91519098,10.154906 1.2422711,8.2387903 2.6795811,6.5725716 4.5299585,5.9732484 5.2685364,4.290122 6.8802592,3.0349975 8.706276,2.7794663 c 1.2124148,-0.1688264 2.46744,0.084987 3.52811,0.7011837 1.545426,-1.7139736 4.237779,-2.2205077 6.293579,-1.1676231 1.568222,0.7488935 2.689625,2.3113526 2.961888,4.0151464 1.492195,0.5977882 2.749007,1.8168898 3.242225,3.3644951 0.329805,0.9581836 0.340709,2.0135956 0.127128,2.9974286 -0.381606,1.535184 -1.465322,2.842146 -2.868035,3.556463 0.0034,0.273204 0.901506,2.243045 0.751284,3.729647 -0.03281,1.858525 -1.211631,3.619894 -2.846433,4.475452 -0.953967,0.556812 -2.084452,0.546309 -3.120531,0.535398 z m -4.470079,-5.349839 c 1.322246,-0.147248 2.189053,-1.300106 2.862307,-2.338363 0.318287,-0.472954 0.561404,-1.002348 0.803,-1.505815 0.313265,0.287151 0.578698,0.828085 1.074141,0.956909 0.521892,0.162542 1.133743,0.03052 1.45325,-0.443554 0.611414,-1.140449 0.31004,-2.516537 -0.04602,-3.698347 C 18.232844,11.92927 17.945151,11.232927 17.397785,10.751793 17.514522,9.9283111 17.026575,9.0919791 16.332883,8.6609491 15.741721,9.1323278 14.842258,9.1294949 14.271975,8.6252369 13.178927,9.7400102 12.177239,9.7029996 11.209704,8.8195135 10.992255,8.6209543 10.577326,10.031484 9.1211947,9.2324497 8.2846288,9.9333947 7.6359672,10.607693 7.0611981,11.578553 6.5026891,12.62523 5.9177873,13.554793 5.867393,14.69141 c -0.024234,0.66432 0.4948601,1.360337 1.1982269,1.306329 0.702996,0.06277 1.1815208,-0.629091 1.7138087,-0.916491 0.079382,0.927141 0.1688108,1.923227 0.4821259,2.828358 0.3596254,1.171275 1.6262605,1.915695 2.8251855,1.745211 0.08481,-0.0066 0.218672,-0.01769 0.218672,-0.0176 z m 0.686342,-3.497495 c -0.643126,-0.394168 -0.33365,-1.249599 -0.359402,-1.870938 0.064,-0.749774 0.115321,-1.538054 0.452402,-2.221125 0.356724,-0.487008 1.226721,-0.299139 1.265134,0.325689 -0.02558,0.628509 -0.314101,1.25416 -0.279646,1.9057 -0.07482,0.544043 0.05418,1.155133 -0.186476,1.652391 -0.197455,0.275121 -0.599638,0.355105 -0.892012,0.208283 z m -2.808766,-0.358124 c -0.605767,-0.328664 -0.4133176,-1.155655 -0.5083256,-1.73063 0.078762,-0.66567 0.013203,-1.510085 0.5705316,-1.976886 0.545037,-0.380109 1.286917,0.270803 1.029164,0.868384 -0.274913,0.755214 -0.09475,1.580345 -0.08893,2.34609 -0.104009,0.451702 -0.587146,0.691508 -1.002445,0.493042 z"
57
69
id="path4"
-95
appview/pages/templates/fragments/dolly/silhouette.html
-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" "size-16 text-black dark:text-white" }}
3
+
{{ template "fragments/dolly/logo" (dict "Classes" "size-16 text-black dark:text-white") }}
4
4
<span class="font-bold text-4xl not-italic">tangled</span>
5
5
<span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1">
6
6
alpha
+1
-1
appview/pages/templates/fragments/logotypeSmall.html
+1
-1
appview/pages/templates/fragments/logotypeSmall.html
···
1
1
{{ define "fragments/logotypeSmall" }}
2
2
<span class="flex items-center gap-2">
3
-
{{ template "fragments/dolly/logo" "size-8 text-black dark:text-white" }}
3
+
{{ template "fragments/dolly/logo" (dict "Classes" "size-8 text-black dark:text-white")}}
4
4
<span class="font-bold text-xl not-italic">tangled</span>
5
5
<span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1">
6
6
alpha
+4
appview/pages/templates/layouts/base.html
+4
appview/pages/templates/layouts/base.html
···
11
11
<script defer src="/static/htmx-ext-ws.min.js"></script>
12
12
<script defer src="/static/actor-typeahead.js" type="module"></script>
13
13
14
+
<link rel="icon" href="/static/logos/dolly.ico" sizes="48x48"/>
15
+
<link rel="icon" href="/static/logos/dolly.svg" sizes="any" type="image/svg+xml"/>
16
+
<link rel="apple-touch-icon" href="/static/logos/dolly.png"/>
17
+
14
18
<!-- preconnect to image cdn -->
15
19
<link rel="preconnect" href="https://avatar.tangled.sh" />
16
20
<link rel="preconnect" href="https://camo.tangled.sh" />
+1
-5
appview/pages/templates/layouts/fragments/topbar.html
+1
-5
appview/pages/templates/layouts/fragments/topbar.html
···
3
3
<div class="flex justify-between p-0 items-center">
4
4
<div id="left-items">
5
5
<a href="/" hx-boost="true" class="text-2xl no-underline hover:no-underline flex items-center gap-2">
6
-
{{ template "fragments/dolly/logo" "size-8 text-black dark:text-white" }}
7
-
<span class="font-bold text-xl not-italic hidden md:inline">tangled</span>
8
-
<span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1 hidden md:inline">
9
-
alpha
10
-
</span>
6
+
{{ template "fragments/logotypeSmall" }}
11
7
</a>
12
8
</div>
13
9
+2
-2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
+2
-2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
···
1
1
{{ define "repo/issues/fragments/issueCommentHeader" }}
2
2
<div class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400 ">
3
-
{{ template "user/fragments/picHandleLink" .Comment.Did.String }}
3
+
{{ template "user/fragments/picHandleLink" .Comment.Did }}
4
4
{{ template "hats" $ }}
5
5
{{ template "timestamp" . }}
6
-
{{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did.String) }}
6
+
{{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }}
7
7
{{ if and $isCommentOwner (not .Comment.Deleted) }}
8
8
{{ template "editIssueComment" . }}
9
9
{{ template "deleteIssueComment" . }}
+3
-3
appview/pages/templates/repo/pulls/pull.html
+3
-3
appview/pages/templates/repo/pulls/pull.html
···
165
165
166
166
<div class="md:pl-[3.5rem] flex flex-col gap-2 mt-2 relative">
167
167
{{ range $cidx, $c := .Comments }}
168
-
<div id="comment-{{$c.Id}}" class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-2 px-4 relative w-full">
168
+
<div id="comment-{{$c.ID}}" class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-2 px-4 relative w-full">
169
169
{{ if gt $cidx 0 }}
170
170
<div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div>
171
171
{{ end }}
172
172
<div class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1">
173
-
{{ template "user/fragments/picHandleLink" $c.Did.String }}
173
+
{{ template "user/fragments/picHandleLink" $c.OwnerDid }}
174
174
<span class="before:content-['ยท']"></span>
175
-
<a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.Id}}">{{ template "repo/fragments/time" $c.Created }}</a>
175
+
<a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.ID}}">{{ template "repo/fragments/time" $c.Created }}</a>
176
176
</div>
177
177
<div class="prose dark:prose-invert">
178
178
{{ $c.Body | markdown }}
+2
-2
appview/pulls/opengraph.go
+2
-2
appview/pulls/opengraph.go
···
242
242
dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2)
243
243
dollyY := statsY + iconBaselineOffset - dollySize/2 + 25
244
244
dollyColor := color.RGBA{180, 180, 180, 255} // light gray
245
-
err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor)
245
+
err = dollyArea.DrawDolly(dollyX, dollyY, dollySize, dollyColor)
246
246
if err != nil {
247
247
log.Printf("dolly silhouette not available (this is ok): %v", err)
248
248
}
···
277
277
}
278
278
279
279
// Get comment count from database
280
-
comments, err := db.GetComments(s.db, orm.FilterEq("subject_at", pull.AtUri()))
280
+
comments, err := db.GetPullComments(s.db, orm.FilterEq("pull_id", pull.ID))
281
281
if err != nil {
282
282
log.Printf("failed to get pull comments: %v", err)
283
283
}
+23
-24
appview/pulls/pulls.go
+23
-24
appview/pulls/pulls.go
···
741
741
}
742
742
defer tx.Rollback()
743
743
744
-
comment := models.Comment{
745
-
Did: syntax.DID(user.Did),
746
-
Rkey: tid.TID(),
747
-
Subject: pull.AtUri(),
748
-
ReplyTo: nil,
749
-
Body: body,
750
-
Created: time.Now(),
751
-
Mentions: mentions,
752
-
References: references,
753
-
PullSubmissionId: &pull.Submissions[roundNumber].ID,
754
-
}
755
-
if err = comment.Validate(); err != nil {
756
-
log.Println("failed to validate comment", err)
757
-
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
758
-
return
759
-
}
760
-
record := comment.AsRecord()
744
+
createdAt := time.Now().Format(time.RFC3339)
761
745
762
746
client, err := s.oauth.AuthorizedClient(r)
763
747
if err != nil {
···
765
749
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
766
750
return
767
751
}
768
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
769
-
Collection: tangled.CommentNSID,
752
+
atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
753
+
Collection: tangled.RepoPullCommentNSID,
770
754
Repo: user.Did,
771
-
Rkey: comment.Rkey,
755
+
Rkey: tid.TID(),
772
756
Record: &lexutil.LexiconTypeDecoder{
773
-
Val: &record,
757
+
Val: &tangled.RepoPullComment{
758
+
Pull: pull.AtUri().String(),
759
+
Body: body,
760
+
CreatedAt: createdAt,
761
+
},
774
762
},
775
763
})
776
764
if err != nil {
···
779
767
return
780
768
}
781
769
770
+
comment := &models.PullComment{
771
+
OwnerDid: user.Did,
772
+
RepoAt: f.RepoAt().String(),
773
+
PullId: pull.PullId,
774
+
Body: body,
775
+
CommentAt: atResp.Uri,
776
+
SubmissionId: pull.Submissions[roundNumber].ID,
777
+
Mentions: mentions,
778
+
References: references,
779
+
}
780
+
782
781
// Create the pull comment in the database with the commentAt field
783
-
err = db.PutComment(tx, &comment)
782
+
commentId, err := db.NewPullComment(tx, comment)
784
783
if err != nil {
785
784
log.Println("failed to create pull comment", err)
786
785
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
···
794
793
return
795
794
}
796
795
797
-
s.notifier.NewPullComment(r.Context(), &comment, mentions)
796
+
s.notifier.NewPullComment(r.Context(), comment, mentions)
798
797
799
798
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
800
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, comment.Id))
799
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, commentId))
801
800
return
802
801
}
803
802
}
+1
-1
appview/repo/opengraph.go
+1
-1
appview/repo/opengraph.go
···
237
237
dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2)
238
238
dollyY := statsY + iconBaselineOffset - dollySize/2 + 25
239
239
dollyColor := color.RGBA{180, 180, 180, 255} // light gray
240
-
err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor)
240
+
err = dollyArea.DrawDolly(dollyX, dollyY, dollySize, dollyColor)
241
241
if err != nil {
242
242
log.Printf("dolly silhouette not available (this is ok): %v", err)
243
243
}
+29
appview/state/manifest.go
+29
appview/state/manifest.go
···
1
+
package state
2
+
3
+
import (
4
+
"encoding/json"
5
+
"net/http"
6
+
)
7
+
8
+
// https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest
9
+
// https://www.w3.org/TR/appmanifest/
10
+
var manifestData = map[string]any{
11
+
"name": "tangled",
12
+
"description": "tightly-knit social coding.",
13
+
"icons": []map[string]string{
14
+
{
15
+
"src": "/static/logos/dolly.svg",
16
+
"sizes": "144x144",
17
+
},
18
+
},
19
+
"start_url": "/",
20
+
"id": "https://tangled.org",
21
+
"display": "standalone",
22
+
"background_color": "#111827",
23
+
"theme_color": "#111827",
24
+
}
25
+
26
+
func (p *State) WebAppManifest(w http.ResponseWriter, r *http.Request) {
27
+
w.Header().Set("Content-Type", "application/manifest+json")
28
+
json.NewEncoder(w).Encode(manifestData)
29
+
}
+1
-3
appview/state/router.go
+1
-3
appview/state/router.go
···
32
32
s.pages,
33
33
)
34
34
35
-
router.Get("/favicon.svg", s.Favicon)
36
-
router.Get("/favicon.ico", s.Favicon)
37
-
router.Get("/pwa-manifest.json", s.PWAManifest)
35
+
router.Get("/pwa-manifest.json", s.WebAppManifest)
38
36
router.Get("/robots.txt", s.RobotsTxt)
39
37
40
38
userRouter := s.UserRouter(&middleware)
+1
-37
appview/state/state.go
+1
-37
appview/state/state.go
···
117
117
tangled.SpindleNSID,
118
118
tangled.StringNSID,
119
119
tangled.RepoIssueNSID,
120
-
tangled.CommentNSID,
120
+
tangled.RepoIssueCommentNSID,
121
121
tangled.LabelDefinitionNSID,
122
122
tangled.LabelOpNSID,
123
123
},
···
202
202
return s.db.Close()
203
203
}
204
204
205
-
func (s *State) Favicon(w http.ResponseWriter, r *http.Request) {
206
-
w.Header().Set("Content-Type", "image/svg+xml")
207
-
w.Header().Set("Cache-Control", "public, max-age=31536000") // one year
208
-
w.Header().Set("ETag", `"favicon-svg-v1"`)
209
-
210
-
if match := r.Header.Get("If-None-Match"); match == `"favicon-svg-v1"` {
211
-
w.WriteHeader(http.StatusNotModified)
212
-
return
213
-
}
214
-
215
-
s.pages.Favicon(w)
216
-
}
217
-
218
205
func (s *State) RobotsTxt(w http.ResponseWriter, r *http.Request) {
219
206
w.Header().Set("Content-Type", "text/plain")
220
207
w.Header().Set("Cache-Control", "public, max-age=86400") // one day
···
223
210
Allow: /
224
211
`
225
212
w.Write([]byte(robotsTxt))
226
-
}
227
-
228
-
// https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest
229
-
const manifestJson = `{
230
-
"name": "tangled",
231
-
"description": "tightly-knit social coding.",
232
-
"icons": [
233
-
{
234
-
"src": "/favicon.svg",
235
-
"sizes": "144x144"
236
-
}
237
-
],
238
-
"start_url": "/",
239
-
"id": "org.tangled",
240
-
241
-
"display": "standalone",
242
-
"background_color": "#111827",
243
-
"theme_color": "#111827"
244
-
}`
245
-
246
-
func (p *State) PWAManifest(w http.ResponseWriter, r *http.Request) {
247
-
w.Header().Set("Content-Type", "application/json")
248
-
w.Write([]byte(manifestJson))
249
213
}
250
214
251
215
func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) {
+27
appview/validator/issue.go
+27
appview/validator/issue.go
···
4
4
"fmt"
5
5
"strings"
6
6
7
+
"tangled.org/core/appview/db"
7
8
"tangled.org/core/appview/models"
9
+
"tangled.org/core/orm"
8
10
)
11
+
12
+
func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error {
13
+
// if comments have parents, only ingest ones that are 1 level deep
14
+
if comment.ReplyTo != nil {
15
+
parents, err := db.GetIssueComments(v.db, orm.FilterEq("at_uri", *comment.ReplyTo))
16
+
if err != nil {
17
+
return fmt.Errorf("failed to fetch parent comment: %w", err)
18
+
}
19
+
if len(parents) != 1 {
20
+
return fmt.Errorf("incorrect number of parent comments returned: %d", len(parents))
21
+
}
22
+
23
+
// depth check
24
+
parent := parents[0]
25
+
if parent.ReplyTo != nil {
26
+
return fmt.Errorf("incorrect depth, this comment is replying at depth >1")
27
+
}
28
+
}
29
+
30
+
if sb := strings.TrimSpace(v.sanitizer.SanitizeDefault(comment.Body)); sb == "" {
31
+
return fmt.Errorf("body is empty after HTML sanitization")
32
+
}
33
+
34
+
return nil
35
+
}
9
36
10
37
func (v *Validator) ValidateIssue(issue *models.Issue) error {
11
38
if issue.Title == "" {
-1
cmd/cborgen/cborgen.go
-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 !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
+
}
+17
-2
flake.nix
+17
-2
flake.nix
···
94
94
spindle = self.callPackage ./nix/pkgs/spindle.nix {};
95
95
knot-unwrapped = self.callPackage ./nix/pkgs/knot-unwrapped.nix {};
96
96
knot = self.callPackage ./nix/pkgs/knot.nix {};
97
+
dolly = self.callPackage ./nix/pkgs/dolly.nix {};
97
98
});
98
99
in {
99
100
overlays.default = final: prev: {
100
-
inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs;
101
+
inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs dolly;
101
102
};
102
103
103
104
packages = forAllSystems (system: let
···
106
107
staticPackages = mkPackageSet pkgs.pkgsStatic;
107
108
crossPackages = mkPackageSet pkgs.pkgsCross.gnu64.pkgsStatic;
108
109
in {
109
-
inherit (packages) appview appview-static-files lexgen goat spindle knot knot-unwrapped sqlite-lib docs;
110
+
inherit
111
+
(packages)
112
+
appview
113
+
appview-static-files
114
+
lexgen
115
+
goat
116
+
spindle
117
+
knot
118
+
knot-unwrapped
119
+
sqlite-lib
120
+
docs
121
+
dolly
122
+
;
110
123
111
124
pkgsStatic-appview = staticPackages.appview;
112
125
pkgsStatic-knot = staticPackages.knot;
113
126
pkgsStatic-knot-unwrapped = staticPackages.knot-unwrapped;
114
127
pkgsStatic-spindle = staticPackages.spindle;
115
128
pkgsStatic-sqlite-lib = staticPackages.sqlite-lib;
129
+
pkgsStatic-dolly = staticPackages.dolly;
116
130
117
131
pkgsCross-gnu64-pkgsStatic-appview = crossPackages.appview;
118
132
pkgsCross-gnu64-pkgsStatic-knot = crossPackages.knot;
119
133
pkgsCross-gnu64-pkgsStatic-knot-unwrapped = crossPackages.knot-unwrapped;
120
134
pkgsCross-gnu64-pkgsStatic-spindle = crossPackages.spindle;
135
+
pkgsCross-gnu64-pkgsStatic-dolly = crossPackages.dolly;
121
136
122
137
treefmt-wrapper = pkgs.treefmt.withConfig {
123
138
settings.formatter = {
+88
ico/ico.go
+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
-
}
+6
-1
nix/pkgs/appview-static-files.nix
+6
-1
nix/pkgs/appview-static-files.nix
···
8
8
actor-typeahead-src,
9
9
sqlite-lib,
10
10
tailwindcss,
11
+
dolly,
11
12
src,
12
13
}:
13
14
runCommandLocal "appview-static-files" {
···
17
18
(allow file-read* (subpath "/System/Library/OpenSSL"))
18
19
'';
19
20
} ''
20
-
mkdir -p $out/{fonts,icons} && cd $out
21
+
mkdir -p $out/{fonts,icons,logos} && cd $out
21
22
cp -f ${htmx-src} htmx.min.js
22
23
cp -f ${htmx-ws-src} htmx-ext-ws.min.js
23
24
cp -rf ${lucide-src}/*.svg icons/
···
26
27
cp -f ${inter-fonts-src}/InterVariable*.ttf fonts/
27
28
cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono*.woff2 fonts/
28
29
cp -f ${actor-typeahead-src}/actor-typeahead.js .
30
+
31
+
${dolly}/bin/dolly -output logos/dolly.png -size 180x180
32
+
${dolly}/bin/dolly -output logos/dolly.ico -size 48x48
33
+
${dolly}/bin/dolly -output logos/dolly.svg
29
34
# tailwindcss -c $src/tailwind.config.js -i $src/input.css -o tw.css won't work
30
35
# for whatever reason (produces broken css), so we are doing this instead
31
36
cd ${src} && ${tailwindcss}/bin/tailwindcss -i input.css -o $out/tw.css
+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
+
}