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