+34
appview/db/language.go
+34
appview/db/language.go
···
1
1
package db
2
2
3
3
import (
4
+
"database/sql"
4
5
"fmt"
5
6
"strings"
6
7
8
+
"github.com/bluesky-social/indigo/atproto/syntax"
7
9
"tangled.org/core/appview/models"
8
10
)
9
11
···
82
84
83
85
return nil
84
86
}
87
+
88
+
func DeleteRepoLanguages(e Execer, filters ...filter) error {
89
+
var conditions []string
90
+
var args []any
91
+
for _, filter := range filters {
92
+
conditions = append(conditions, filter.Condition())
93
+
args = append(args, filter.Arg()...)
94
+
}
95
+
96
+
whereClause := ""
97
+
if conditions != nil {
98
+
whereClause = " where " + strings.Join(conditions, " and ")
99
+
}
100
+
101
+
query := fmt.Sprintf(`delete from repo_languages %s`, whereClause)
102
+
103
+
_, err := e.Exec(query, args...)
104
+
return err
105
+
}
106
+
107
+
func UpdateRepoLanguages(tx *sql.Tx, repoAt syntax.ATURI, ref string, langs []models.RepoLanguage) error {
108
+
err := DeleteRepoLanguages(
109
+
tx,
110
+
FilterEq("repo_at", repoAt),
111
+
FilterEq("ref", ref),
112
+
)
113
+
if err != nil {
114
+
return fmt.Errorf("failed to delete existing languages: %w", err)
115
+
}
116
+
117
+
return InsertRepoLanguages(tx, langs)
118
+
}
+2
-2
appview/db/pulls.go
+2
-2
appview/db/pulls.go
···
246
246
// collect pull source for all pulls that need it
247
247
var sourceAts []syntax.ATURI
248
248
for _, p := range pulls {
249
-
if p.PullSource.RepoAt != nil {
249
+
if p.PullSource != nil && p.PullSource.RepoAt != nil {
250
250
sourceAts = append(sourceAts, *p.PullSource.RepoAt)
251
251
}
252
252
}
···
259
259
sourceRepoMap[r.RepoAt()] = &r
260
260
}
261
261
for _, p := range pulls {
262
-
if p.PullSource.RepoAt != nil {
262
+
if p.PullSource != nil && p.PullSource.RepoAt != nil {
263
263
if sourceRepo, ok := sourceRepoMap[*p.PullSource.RepoAt]; ok {
264
264
p.PullSource.Repo = sourceRepo
265
265
}
+34
-7
appview/db/reaction.go
+34
-7
appview/db/reaction.go
···
62
62
return count, nil
63
63
}
64
64
65
-
func GetReactionCountMap(e Execer, threadAt syntax.ATURI) (map[models.ReactionKind]int, error) {
66
-
countMap := map[models.ReactionKind]int{}
65
+
func GetReactionMap(e Execer, userLimit int, threadAt syntax.ATURI) (map[models.ReactionKind]models.ReactionDisplayData, error) {
66
+
query := `
67
+
select kind, reacted_by_did,
68
+
row_number() over (partition by kind order by created asc) as rn,
69
+
count(*) over (partition by kind) as total
70
+
from reactions
71
+
where thread_at = ?
72
+
order by kind, created asc`
73
+
74
+
rows, err := e.Query(query, threadAt)
75
+
if err != nil {
76
+
return nil, err
77
+
}
78
+
defer rows.Close()
79
+
80
+
reactionMap := map[models.ReactionKind]models.ReactionDisplayData{}
67
81
for _, kind := range models.OrderedReactionKinds {
68
-
count, err := GetReactionCount(e, threadAt, kind)
69
-
if err != nil {
70
-
return map[models.ReactionKind]int{}, nil
82
+
reactionMap[kind] = models.ReactionDisplayData{Count: 0, Users: []string{}}
83
+
}
84
+
85
+
for rows.Next() {
86
+
var kind models.ReactionKind
87
+
var did string
88
+
var rn, total int
89
+
if err := rows.Scan(&kind, &did, &rn, &total); err != nil {
90
+
return nil, err
71
91
}
72
-
countMap[kind] = count
92
+
93
+
data := reactionMap[kind]
94
+
data.Count = total
95
+
if userLimit > 0 && rn <= userLimit {
96
+
data.Users = append(data.Users, did)
97
+
}
98
+
reactionMap[kind] = data
73
99
}
74
-
return countMap, nil
100
+
101
+
return reactionMap, rows.Err()
75
102
}
76
103
77
104
func GetReactionStatus(e Execer, userDid string, threadAt syntax.ATURI, kind models.ReactionKind) bool {
+2
-2
appview/issues/issues.go
+2
-2
appview/issues/issues.go
···
83
83
return
84
84
}
85
85
86
-
reactionCountMap, err := db.GetReactionCountMap(rp.db, issue.AtUri())
86
+
reactionMap, err := db.GetReactionMap(rp.db, 20, issue.AtUri())
87
87
if err != nil {
88
88
l.Error("failed to get issue reactions", "err", err)
89
89
}
···
115
115
Issue: issue,
116
116
CommentList: issue.CommentList(),
117
117
OrderedReactionKinds: models.OrderedReactionKinds,
118
-
Reactions: reactionCountMap,
118
+
Reactions: reactionMap,
119
119
UserReacted: userReactions,
120
120
LabelDefs: defs,
121
121
})
+14
-13
appview/models/label.go
+14
-13
appview/models/label.go
···
461
461
return result
462
462
}
463
463
464
+
var (
465
+
LabelWontfix = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "wontfix")
466
+
LabelDuplicate = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "duplicate")
467
+
LabelAssignee = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "assignee")
468
+
LabelGoodFirstIssue = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "good-first-issue")
469
+
LabelDocumentation = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "documentation")
470
+
)
471
+
464
472
func DefaultLabelDefs() []string {
465
-
rkeys := []string{
466
-
"wontfix",
467
-
"duplicate",
468
-
"assignee",
469
-
"good-first-issue",
470
-
"documentation",
473
+
return []string{
474
+
LabelWontfix,
475
+
LabelDuplicate,
476
+
LabelAssignee,
477
+
LabelGoodFirstIssue,
478
+
LabelDocumentation,
471
479
}
472
-
473
-
defs := make([]string, len(rkeys))
474
-
for i, r := range rkeys {
475
-
defs[i] = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, r)
476
-
}
477
-
478
-
return defs
479
480
}
480
481
481
482
func FetchDefaultDefs(r *idresolver.Resolver) ([]LabelDefinition, error) {
+5
appview/models/reaction.go
+5
appview/models/reaction.go
+5
appview/models/repo.go
+5
appview/models/repo.go
+2
-2
appview/oauth/client/oauth_client.go
+2
-2
appview/oauth/client/oauth_client.go
+1
-1
appview/oauth/handler/handler.go
+1
-1
appview/oauth/handler/handler.go
···
16
16
"github.com/gorilla/sessions"
17
17
"github.com/lestrrat-go/jwx/v2/jwk"
18
18
"github.com/posthog/posthog-go"
19
+
"tangled.org/anirudh.fi/atproto-oauth/helpers"
19
20
tangled "tangled.org/core/api/tangled"
20
21
sessioncache "tangled.org/core/appview/cache/session"
21
22
"tangled.org/core/appview/config"
···
28
29
"tangled.org/core/idresolver"
29
30
"tangled.org/core/rbac"
30
31
"tangled.org/core/tid"
31
-
"tangled.sh/icyphox.sh/atproto-oauth/helpers"
32
32
)
33
33
34
34
const (
+2
-2
appview/oauth/oauth.go
+2
-2
appview/oauth/oauth.go
···
9
9
10
10
indigo_xrpc "github.com/bluesky-social/indigo/xrpc"
11
11
"github.com/gorilla/sessions"
12
+
oauth "tangled.org/anirudh.fi/atproto-oauth"
13
+
"tangled.org/anirudh.fi/atproto-oauth/helpers"
12
14
sessioncache "tangled.org/core/appview/cache/session"
13
15
"tangled.org/core/appview/config"
14
16
"tangled.org/core/appview/oauth/client"
15
17
xrpc "tangled.org/core/appview/xrpcclient"
16
-
oauth "tangled.sh/icyphox.sh/atproto-oauth"
17
-
"tangled.sh/icyphox.sh/atproto-oauth/helpers"
18
18
)
19
19
20
20
type OAuth struct {
+17
-2
appview/pages/pages.go
+17
-2
appview/pages/pages.go
···
306
306
LoggedInUser *oauth.User
307
307
Timeline []models.TimelineEvent
308
308
Repos []models.Repo
309
+
GfiLabel *models.LabelDefinition
309
310
}
310
311
311
312
func (p *Pages) Timeline(w io.Writer, params TimelineParams) error {
312
313
return p.execute("timeline/timeline", w, params)
314
+
}
315
+
316
+
type GoodFirstIssuesParams struct {
317
+
LoggedInUser *oauth.User
318
+
Issues []models.Issue
319
+
RepoGroups []*models.RepoGroup
320
+
LabelDefs map[string]*models.LabelDefinition
321
+
GfiLabel *models.LabelDefinition
322
+
Page pagination.Page
323
+
}
324
+
325
+
func (p *Pages) GoodFirstIssues(w io.Writer, params GoodFirstIssuesParams) error {
326
+
return p.execute("goodfirstissues/index", w, params)
313
327
}
314
328
315
329
type UserProfileSettingsParams struct {
···
971
985
LabelDefs map[string]*models.LabelDefinition
972
986
973
987
OrderedReactionKinds []models.ReactionKind
974
-
Reactions map[models.ReactionKind]int
988
+
Reactions map[models.ReactionKind]models.ReactionDisplayData
975
989
UserReacted map[models.ReactionKind]bool
976
990
}
977
991
···
996
1010
ThreadAt syntax.ATURI
997
1011
Kind models.ReactionKind
998
1012
Count int
1013
+
Users []string
999
1014
IsReacted bool
1000
1015
}
1001
1016
···
1124
1139
Pipelines map[string]models.Pipeline
1125
1140
1126
1141
OrderedReactionKinds []models.ReactionKind
1127
-
Reactions map[models.ReactionKind]int
1142
+
Reactions map[models.ReactionKind]models.ReactionDisplayData
1128
1143
UserReacted map[models.ReactionKind]bool
1129
1144
1130
1145
LabelDefs map[string]*models.LabelDefinition
+167
appview/pages/templates/goodfirstissues/index.html
+167
appview/pages/templates/goodfirstissues/index.html
···
1
+
{{ define "title" }}good first issues{{ end }}
2
+
3
+
{{ define "extrameta" }}
4
+
<meta property="og:title" content="good first issues · tangled" />
5
+
<meta property="og:type" content="object" />
6
+
<meta property="og:url" content="https://tangled.org/goodfirstissues" />
7
+
<meta property="og:description" content="Find good first issues to contribute to open source projects" />
8
+
{{ end }}
9
+
10
+
{{ define "content" }}
11
+
<div class="grid grid-cols-10">
12
+
<header class="col-span-full md:col-span-10 px-6 py-2 text-center flex flex-col items-center justify-center py-8">
13
+
<h1 class="scale-150 dark:text-white mb-4">
14
+
{{ template "labels/fragments/label" (dict "def" .GfiLabel "val" "" "withPrefix" true) }}
15
+
</h1>
16
+
<p class="text-gray-600 dark:text-gray-400 mb-2">
17
+
Find beginner-friendly issues across all repositories to get started with open source contributions.
18
+
</p>
19
+
</header>
20
+
21
+
<div class="col-span-full md:col-span-10 space-y-6">
22
+
{{ if eq (len .RepoGroups) 0 }}
23
+
<div class="bg-white dark:bg-gray-800 drop-shadow-sm rounded p-6 md:px-10">
24
+
<div class="text-center py-16">
25
+
<div class="text-gray-500 dark:text-gray-400 mb-4">
26
+
{{ i "circle-dot" "w-16 h-16 mx-auto" }}
27
+
</div>
28
+
<h3 class="text-xl font-medium text-gray-900 dark:text-white mb-2">No good first issues available</h3>
29
+
<p class="text-gray-600 dark:text-gray-400 mb-3 max-w-md mx-auto">
30
+
There are currently no open issues labeled as "good-first-issue" across all repositories.
31
+
</p>
32
+
<p class="text-gray-500 dark:text-gray-500 text-sm max-w-md mx-auto">
33
+
Repository maintainers can add the "good-first-issue" label to beginner-friendly issues to help newcomers get started.
34
+
</p>
35
+
</div>
36
+
</div>
37
+
{{ else }}
38
+
{{ range .RepoGroups }}
39
+
<div class="mb-4 gap-1 flex flex-col drop-shadow-sm rounded bg-white dark:bg-gray-800">
40
+
<div class="flex px-6 pt-4 flex-row gap-1 items-center justify-between flex-wrap">
41
+
<div class="font-medium dark:text-white flex items-center justify-between">
42
+
<div class="flex items-center min-w-0 flex-1 mr-2">
43
+
{{ if .Repo.Source }}
44
+
{{ i "git-fork" "w-4 h-4 mr-1.5 shrink-0" }}
45
+
{{ else }}
46
+
{{ i "book-marked" "w-4 h-4 mr-1.5 shrink-0" }}
47
+
{{ end }}
48
+
{{ $repoOwner := resolve .Repo.Did }}
49
+
<a href="/{{ $repoOwner }}/{{ .Repo.Name }}" class="truncate min-w-0">{{ $repoOwner }}/{{ .Repo.Name }}</a>
50
+
</div>
51
+
</div>
52
+
53
+
54
+
{{ if .Repo.RepoStats }}
55
+
<div class="text-gray-400 text-sm font-mono inline-flex gap-4">
56
+
{{ with .Repo.RepoStats.Language }}
57
+
<div class="flex gap-2 items-center text-sm">
58
+
{{ template "repo/fragments/colorBall" (dict "color" (langColor .)) }}
59
+
<span>{{ . }}</span>
60
+
</div>
61
+
{{ end }}
62
+
{{ with .Repo.RepoStats.StarCount }}
63
+
<div class="flex gap-1 items-center text-sm">
64
+
{{ i "star" "w-3 h-3 fill-current" }}
65
+
<span>{{ . }}</span>
66
+
</div>
67
+
{{ end }}
68
+
{{ with .Repo.RepoStats.IssueCount.Open }}
69
+
<div class="flex gap-1 items-center text-sm">
70
+
{{ i "circle-dot" "w-3 h-3" }}
71
+
<span>{{ . }}</span>
72
+
</div>
73
+
{{ end }}
74
+
{{ with .Repo.RepoStats.PullCount.Open }}
75
+
<div class="flex gap-1 items-center text-sm">
76
+
{{ i "git-pull-request" "w-3 h-3" }}
77
+
<span>{{ . }}</span>
78
+
</div>
79
+
{{ end }}
80
+
</div>
81
+
{{ end }}
82
+
</div>
83
+
84
+
{{ with .Repo.Description }}
85
+
<div class="pl-6 pb-2 text-gray-600 dark:text-gray-300 text-sm line-clamp-2">
86
+
{{ . | description }}
87
+
</div>
88
+
{{ end }}
89
+
90
+
{{ if gt (len .Issues) 0 }}
91
+
<div class="grid grid-cols-1 rounded-b border-b border-t border-gray-200 dark:border-gray-900 divide-y divide-gray-200 dark:divide-gray-900">
92
+
{{ range .Issues }}
93
+
<a href="/{{ resolve .Repo.Did }}/{{ .Repo.Name }}/issues/{{ .IssueId }}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25">
94
+
<div class="py-2 px-6">
95
+
<div class="flex-grow min-w-0 w-full">
96
+
<div class="flex text-sm items-center justify-between w-full">
97
+
<div class="flex items-center gap-2 min-w-0 flex-1 pr-2">
98
+
<span class="truncate text-sm text-gray-800 dark:text-gray-200">
99
+
<span class="text-gray-500 dark:text-gray-400">#{{ .IssueId }}</span>
100
+
{{ .Title | description }}
101
+
</span>
102
+
</div>
103
+
<div class="flex-shrink-0 flex items-center gap-2 text-gray-500 dark:text-gray-400">
104
+
<span>
105
+
<div class="inline-flex items-center gap-1">
106
+
{{ i "message-square" "w-3 h-3" }}
107
+
{{ len .Comments }}
108
+
</div>
109
+
</span>
110
+
<span class="before:content-['·'] before:select-none"></span>
111
+
<span class="text-sm">
112
+
{{ template "repo/fragments/shortTimeAgo" .Created }}
113
+
</span>
114
+
<div class="hidden md:inline-flex md:gap-1">
115
+
{{ $labelState := .Labels }}
116
+
{{ range $k, $d := $.LabelDefs }}
117
+
{{ range $v, $s := $labelState.GetValSet $d.AtUri.String }}
118
+
{{ template "labels/fragments/label" (dict "def" $d "val" $v "withPrefix" true) }}
119
+
{{ end }}
120
+
{{ end }}
121
+
</div>
122
+
</div>
123
+
</div>
124
+
</div>
125
+
</div>
126
+
</a>
127
+
{{ end }}
128
+
</div>
129
+
{{ end }}
130
+
</div>
131
+
{{ end }}
132
+
133
+
{{ if or (gt .Page.Offset 0) (eq (len .RepoGroups) .Page.Limit) }}
134
+
<div class="flex justify-center mt-8">
135
+
<div class="flex gap-2">
136
+
{{ if gt .Page.Offset 0 }}
137
+
{{ $prev := .Page.Previous }}
138
+
<a
139
+
class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700"
140
+
hx-boost="true"
141
+
href="/goodfirstissues?offset={{ $prev.Offset }}&limit={{ $prev.Limit }}"
142
+
>
143
+
{{ i "chevron-left" "w-4 h-4" }}
144
+
previous
145
+
</a>
146
+
{{ else }}
147
+
<div></div>
148
+
{{ end }}
149
+
150
+
{{ if eq (len .RepoGroups) .Page.Limit }}
151
+
{{ $next := .Page.Next }}
152
+
<a
153
+
class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700"
154
+
hx-boost="true"
155
+
href="/goodfirstissues?offset={{ $next.Offset }}&limit={{ $next.Limit }}"
156
+
>
157
+
next
158
+
{{ i "chevron-right" "w-4 h-4" }}
159
+
</a>
160
+
{{ end }}
161
+
</div>
162
+
</div>
163
+
{{ end }}
164
+
{{ end }}
165
+
</div>
166
+
</div>
167
+
{{ end }}
+1
-1
appview/pages/templates/labels/fragments/label.html
+1
-1
appview/pages/templates/labels/fragments/label.html
···
2
2
{{ $d := .def }}
3
3
{{ $v := .val }}
4
4
{{ $withPrefix := .withPrefix }}
5
-
<span class="flex items-center gap-2 font-normal normal-case rounded py-1 px-2 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm">
5
+
<span class="w-fit flex items-center gap-2 font-normal normal-case rounded py-1 px-2 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm">
6
6
{{ template "repo/fragments/colorBall" (dict "color" $d.GetColor) }}
7
7
8
8
{{ $lhs := printf "%s" $d.Name }}
+16
-12
appview/pages/templates/layouts/base.html
+16
-12
appview/pages/templates/layouts/base.html
···
14
14
<link rel="preconnect" href="https://avatar.tangled.sh" />
15
15
<link rel="preconnect" href="https://camo.tangled.sh" />
16
16
17
+
<!-- pwa manifest -->
18
+
<link rel="manifest" href="/pwa-manifest.json" />
19
+
17
20
<!-- preload main font -->
18
21
<link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin />
19
22
···
21
24
<title>{{ block "title" . }}{{ end }} · tangled</title>
22
25
{{ block "extrameta" . }}{{ end }}
23
26
</head>
24
-
<body class="min-h-screen grid grid-cols-1 grid-rows-[min-content_auto_min-content] gap-4 bg-slate-100 dark:bg-gray-900 dark:text-white transition-colors duration-200"
25
-
style="grid-template-columns: minmax(1rem, 1fr) minmax(auto, 1024px) minmax(1rem, 1fr);">
27
+
<body class="min-h-screen flex flex-col gap-4 bg-slate-100 dark:bg-gray-900 dark:text-white transition-colors duration-200">
26
28
{{ block "topbarLayout" . }}
27
-
<header class="px-1 col-span-full md:col-span-1 md:col-start-2" style="z-index: 20;">
29
+
<header class="w-full bg-white dark:bg-gray-800 col-span-full md:col-span-1 md:col-start-2" style="z-index: 20;">
28
30
29
31
{{ if .LoggedInUser }}
30
32
<div id="upgrade-banner"
···
38
40
{{ end }}
39
41
40
42
{{ block "mainLayout" . }}
41
-
<div class="px-1 col-span-full md:col-span-1 md:col-start-2 flex flex-col gap-4">
42
-
{{ block "contentLayout" . }}
43
-
<main class="col-span-1 md:col-span-8">
43
+
<div class="flex-grow">
44
+
<div class="max-w-screen-lg mx-auto flex flex-col gap-4">
45
+
{{ block "contentLayout" . }}
46
+
<main>
44
47
{{ block "content" . }}{{ end }}
45
48
</main>
46
-
{{ end }}
47
-
48
-
{{ block "contentAfterLayout" . }}
49
-
<main class="col-span-1 md:col-span-8">
49
+
{{ end }}
50
+
51
+
{{ block "contentAfterLayout" . }}
52
+
<main>
50
53
{{ block "contentAfter" . }}{{ end }}
51
54
</main>
52
-
{{ end }}
55
+
{{ end }}
56
+
</div>
53
57
</div>
54
58
{{ end }}
55
59
56
60
{{ block "footerLayout" . }}
57
-
<footer class="px-1 col-span-full md:col-span-1 md:col-start-2 mt-12">
61
+
<footer class="bg-white dark:bg-gray-800 mt-12">
58
62
{{ template "layouts/fragments/footer" . }}
59
63
</footer>
60
64
{{ end }}
+1
-1
appview/pages/templates/layouts/fragments/topbar.html
+1
-1
appview/pages/templates/layouts/fragments/topbar.html
···
1
1
{{ define "layouts/fragments/topbar" }}
2
-
<nav class="space-x-4 px-6 py-2 rounded-b bg-white dark:bg-gray-800 dark:text-white drop-shadow-sm">
2
+
<nav class="mx-auto space-x-4 px-6 py-2 rounded-b dark:text-white drop-shadow-sm">
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">
+1
-1
appview/pages/templates/repo/fragments/cloneDropdown.html
+1
-1
appview/pages/templates/repo/fragments/cloneDropdown.html
+6
-1
appview/pages/templates/repo/fragments/reaction.html
+6
-1
appview/pages/templates/repo/fragments/reaction.html
···
2
2
<button
3
3
id="reactIndi-{{ .Kind }}"
4
4
class="flex justify-center items-center min-w-8 min-h-8 rounded border
5
-
leading-4 px-3 gap-1
5
+
leading-4 px-3 gap-1 relative group
6
6
{{ if eq .Count 0 }}
7
7
hidden
8
8
{{ end }}
···
20
20
dark:hover:border-gray-600
21
21
{{ end }}
22
22
"
23
+
{{ if gt (length .Users) 0 }}
24
+
title="{{ range $i, $did := .Users }}{{ if ne $i 0 }}, {{ end }}{{ resolve $did }}{{ end }}{{ if gt .Count (length .Users) }}, and {{ sub .Count (length .Users) }} more{{ end }}"
25
+
{{ else }}
26
+
title="{{ .Kind }}"
27
+
{{ end }}
23
28
{{ if .IsReacted }}
24
29
hx-delete="/react?subject={{ .ThreadAt }}&kind={{ .Kind }}"
25
30
{{ else }}
+63
appview/pages/templates/repo/issues/fragments/globalIssueListing.html
+63
appview/pages/templates/repo/issues/fragments/globalIssueListing.html
···
1
+
{{ define "repo/issues/fragments/globalIssueListing" }}
2
+
<div class="flex flex-col gap-2">
3
+
{{ range .Issues }}
4
+
<div class="rounded drop-shadow-sm bg-white px-6 py-4 dark:bg-gray-800 dark:border-gray-700">
5
+
<div class="pb-2 mb-3">
6
+
<div class="flex items-center gap-3 mb-2">
7
+
<a
8
+
href="/{{ resolve .Repo.Did }}/{{ .Repo.Name }}"
9
+
class="text-blue-600 dark:text-blue-400 font-medium hover:underline text-sm"
10
+
>
11
+
{{ resolve .Repo.Did }}/{{ .Repo.Name }}
12
+
</a>
13
+
</div>
14
+
<a
15
+
href="/{{ resolve .Repo.Did }}/{{ .Repo.Name }}/issues/{{ .IssueId }}"
16
+
class="no-underline hover:underline"
17
+
>
18
+
{{ .Title | description }}
19
+
<span class="text-gray-500">#{{ .IssueId }}</span>
20
+
</a>
21
+
</div>
22
+
<div class="text-sm text-gray-500 dark:text-gray-400 flex flex-wrap items-center gap-1">
23
+
{{ $bgColor := "bg-gray-800 dark:bg-gray-700" }}
24
+
{{ $icon := "ban" }}
25
+
{{ $state := "closed" }}
26
+
{{ if .Open }}
27
+
{{ $bgColor = "bg-green-600 dark:bg-green-700" }}
28
+
{{ $icon = "circle-dot" }}
29
+
{{ $state = "open" }}
30
+
{{ end }}
31
+
32
+
<span class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }} text-sm">
33
+
{{ i $icon "w-3 h-3 mr-1.5 text-white dark:text-white" }}
34
+
<span class="text-white dark:text-white">{{ $state }}</span>
35
+
</span>
36
+
37
+
<span class="ml-1">
38
+
{{ template "user/fragments/picHandleLink" .Did }}
39
+
</span>
40
+
41
+
<span class="before:content-['·']">
42
+
{{ template "repo/fragments/time" .Created }}
43
+
</span>
44
+
45
+
<span class="before:content-['·']">
46
+
{{ $s := "s" }}
47
+
{{ if eq (len .Comments) 1 }}
48
+
{{ $s = "" }}
49
+
{{ end }}
50
+
<a href="/{{ resolve .Repo.Did }}/{{ .Repo.Name }}/issues/{{ .IssueId }}" class="text-gray-500 dark:text-gray-400">{{ len .Comments }} comment{{$s}}</a>
51
+
</span>
52
+
53
+
{{ $state := .Labels }}
54
+
{{ range $k, $d := $.LabelDefs }}
55
+
{{ range $v, $s := $state.GetValSet $d.AtUri.String }}
56
+
{{ template "labels/fragments/label" (dict "def" $d "val" $v "withPrefix" true) }}
57
+
{{ end }}
58
+
{{ end }}
59
+
</div>
60
+
</div>
61
+
{{ end }}
62
+
</div>
63
+
{{ end }}
+55
appview/pages/templates/repo/issues/fragments/issueListing.html
+55
appview/pages/templates/repo/issues/fragments/issueListing.html
···
1
+
{{ define "repo/issues/fragments/issueListing" }}
2
+
<div class="flex flex-col gap-2">
3
+
{{ range .Issues }}
4
+
<div class="rounded drop-shadow-sm bg-white px-6 py-4 dark:bg-gray-800 dark:border-gray-700">
5
+
<div class="pb-2">
6
+
<a
7
+
href="/{{ $.RepoPrefix }}/issues/{{ .IssueId }}"
8
+
class="no-underline hover:underline"
9
+
>
10
+
{{ .Title | description }}
11
+
<span class="text-gray-500">#{{ .IssueId }}</span>
12
+
</a>
13
+
</div>
14
+
<div class="text-sm text-gray-500 dark:text-gray-400 flex flex-wrap items-center gap-1">
15
+
{{ $bgColor := "bg-gray-800 dark:bg-gray-700" }}
16
+
{{ $icon := "ban" }}
17
+
{{ $state := "closed" }}
18
+
{{ if .Open }}
19
+
{{ $bgColor = "bg-green-600 dark:bg-green-700" }}
20
+
{{ $icon = "circle-dot" }}
21
+
{{ $state = "open" }}
22
+
{{ end }}
23
+
24
+
<span class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }} text-sm">
25
+
{{ i $icon "w-3 h-3 mr-1.5 text-white dark:text-white" }}
26
+
<span class="text-white dark:text-white">{{ $state }}</span>
27
+
</span>
28
+
29
+
<span class="ml-1">
30
+
{{ template "user/fragments/picHandleLink" .Did }}
31
+
</span>
32
+
33
+
<span class="before:content-['·']">
34
+
{{ template "repo/fragments/time" .Created }}
35
+
</span>
36
+
37
+
<span class="before:content-['·']">
38
+
{{ $s := "s" }}
39
+
{{ if eq (len .Comments) 1 }}
40
+
{{ $s = "" }}
41
+
{{ end }}
42
+
<a href="/{{ $.RepoPrefix }}/issues/{{ .IssueId }}" class="text-gray-500 dark:text-gray-400">{{ len .Comments }} comment{{$s}}</a>
43
+
</span>
44
+
45
+
{{ $state := .Labels }}
46
+
{{ range $k, $d := $.LabelDefs }}
47
+
{{ range $v, $s := $state.GetValSet $d.AtUri.String }}
48
+
{{ template "labels/fragments/label" (dict "def" $d "val" $v "withPrefix" true) }}
49
+
{{ end }}
50
+
{{ end }}
51
+
</div>
52
+
</div>
53
+
{{ end }}
54
+
</div>
55
+
{{ end }}
+4
-2
appview/pages/templates/repo/issues/issue.html
+4
-2
appview/pages/templates/repo/issues/issue.html
···
110
110
<div class="flex items-center gap-2">
111
111
{{ template "repo/fragments/reactionsPopUp" .OrderedReactionKinds }}
112
112
{{ range $kind := .OrderedReactionKinds }}
113
+
{{ $reactionData := index $.Reactions $kind }}
113
114
{{
114
115
template "repo/fragments/reaction"
115
116
(dict
116
117
"Kind" $kind
117
-
"Count" (index $.Reactions $kind)
118
+
"Count" $reactionData.Count
118
119
"IsReacted" (index $.UserReacted $kind)
119
-
"ThreadAt" $.Issue.AtUri)
120
+
"ThreadAt" $.Issue.AtUri
121
+
"Users" $reactionData.Users)
120
122
}}
121
123
{{ end }}
122
124
</div>
+2
-52
appview/pages/templates/repo/issues/issues.html
+2
-52
appview/pages/templates/repo/issues/issues.html
···
37
37
{{ end }}
38
38
39
39
{{ define "repoAfter" }}
40
-
<div class="flex flex-col gap-2 mt-2">
41
-
{{ range .Issues }}
42
-
<div class="rounded drop-shadow-sm bg-white px-6 py-4 dark:bg-gray-800 dark:border-gray-700">
43
-
<div class="pb-2">
44
-
<a
45
-
href="/{{ $.RepoInfo.FullName }}/issues/{{ .IssueId }}"
46
-
class="no-underline hover:underline"
47
-
>
48
-
{{ .Title | description }}
49
-
<span class="text-gray-500">#{{ .IssueId }}</span>
50
-
</a>
51
-
</div>
52
-
<div class="text-sm text-gray-500 dark:text-gray-400 flex flex-wrap items-center gap-1">
53
-
{{ $bgColor := "bg-gray-800 dark:bg-gray-700" }}
54
-
{{ $icon := "ban" }}
55
-
{{ $state := "closed" }}
56
-
{{ if .Open }}
57
-
{{ $bgColor = "bg-green-600 dark:bg-green-700" }}
58
-
{{ $icon = "circle-dot" }}
59
-
{{ $state = "open" }}
60
-
{{ end }}
61
-
62
-
<span class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }} text-sm">
63
-
{{ i $icon "w-3 h-3 mr-1.5 text-white dark:text-white" }}
64
-
<span class="text-white dark:text-white">{{ $state }}</span>
65
-
</span>
66
-
67
-
<span class="ml-1">
68
-
{{ template "user/fragments/picHandleLink" .Did }}
69
-
</span>
70
-
71
-
<span class="before:content-['·']">
72
-
{{ template "repo/fragments/time" .Created }}
73
-
</span>
74
-
75
-
<span class="before:content-['·']">
76
-
{{ $s := "s" }}
77
-
{{ if eq (len .Comments) 1 }}
78
-
{{ $s = "" }}
79
-
{{ end }}
80
-
<a href="/{{ $.RepoInfo.FullName }}/issues/{{ .IssueId }}" class="text-gray-500 dark:text-gray-400">{{ len .Comments }} comment{{$s}}</a>
81
-
</span>
82
-
83
-
{{ $state := .Labels }}
84
-
{{ range $k, $d := $.LabelDefs }}
85
-
{{ range $v, $s := $state.GetValSet $d.AtUri.String }}
86
-
{{ template "labels/fragments/label" (dict "def" $d "val" $v "withPrefix" true) }}
87
-
{{ end }}
88
-
{{ end }}
89
-
</div>
90
-
</div>
91
-
{{ end }}
40
+
<div class="mt-2">
41
+
{{ template "repo/issues/fragments/issueListing" (dict "Issues" .Issues "RepoPrefix" .RepoInfo.FullName "LabelDefs" .LabelDefs) }}
92
42
</div>
93
43
{{ block "pagination" . }} {{ end }}
94
44
{{ end }}
+4
-2
appview/pages/templates/repo/pulls/fragments/pullHeader.html
+4
-2
appview/pages/templates/repo/pulls/fragments/pullHeader.html
···
66
66
<div class="flex items-center gap-2 mt-2">
67
67
{{ template "repo/fragments/reactionsPopUp" . }}
68
68
{{ range $kind := . }}
69
+
{{ $reactionData := index $.Reactions $kind }}
69
70
{{
70
71
template "repo/fragments/reaction"
71
72
(dict
72
73
"Kind" $kind
73
-
"Count" (index $.Reactions $kind)
74
+
"Count" $reactionData.Count
74
75
"IsReacted" (index $.UserReacted $kind)
75
-
"ThreadAt" $.Pull.PullAt)
76
+
"ThreadAt" $.Pull.PullAt
77
+
"Users" $reactionData.Users)
76
78
}}
77
79
{{ end }}
78
80
</div>
+30
appview/pages/templates/timeline/fragments/goodfirstissues.html
+30
appview/pages/templates/timeline/fragments/goodfirstissues.html
···
1
+
{{ define "timeline/fragments/goodfirstissues" }}
2
+
{{ if .GfiLabel }}
3
+
<a href="/goodfirstissues" class="no-underline hover:no-underline">
4
+
<div class="flex items-center justify-between gap-2 bg-purple-200 dark:bg-purple-900 border border-purple-400 dark:border-purple-500 rounded mb-4 py-4 px-6 ">
5
+
<div class="flex-1 flex flex-col gap-2">
6
+
<div class="text-purple-500 dark:text-purple-400">Oct 2025</div>
7
+
<p>
8
+
Make your first contribution to an open-source project this October.
9
+
<em>good-first-issue</em> helps new contributors find easy ways to
10
+
start contributing to open-source projects.
11
+
</p>
12
+
<span class="flex items-center gap-2 text-purple-500 dark:text-purple-400">
13
+
Browse issues {{ i "arrow-right" "size-4" }}
14
+
</span>
15
+
</div>
16
+
<div class="hidden md:block relative px-16 scale-150">
17
+
<div class="relative opacity-60">
18
+
{{ template "labels/fragments/label" (dict "def" .GfiLabel "val" "" "withPrefix" true) }}
19
+
</div>
20
+
<div class="relative -mt-4 ml-2 opacity-80">
21
+
{{ template "labels/fragments/label" (dict "def" .GfiLabel "val" "" "withPrefix" true) }}
22
+
</div>
23
+
<div class="relative -mt-4 ml-4">
24
+
{{ template "labels/fragments/label" (dict "def" .GfiLabel "val" "" "withPrefix" true) }}
25
+
</div>
26
+
</div>
27
+
</div>
28
+
</a>
29
+
{{ end }}
30
+
{{ end }}
+1
appview/pages/templates/timeline/home.html
+1
appview/pages/templates/timeline/home.html
···
12
12
<div class="flex flex-col gap-4">
13
13
{{ template "timeline/fragments/hero" . }}
14
14
{{ template "features" . }}
15
+
{{ template "timeline/fragments/goodfirstissues" . }}
15
16
{{ template "timeline/fragments/trending" . }}
16
17
{{ template "timeline/fragments/timeline" . }}
17
18
<div class="flex justify-end">
+1
appview/pages/templates/timeline/timeline.html
+1
appview/pages/templates/timeline/timeline.html
+1
appview/pages/templates/user/completeSignup.html
+1
appview/pages/templates/user/completeSignup.html
+1
appview/pages/templates/user/login.html
+1
appview/pages/templates/user/login.html
···
8
8
<meta property="og:url" content="https://tangled.org/login" />
9
9
<meta property="og:description" content="login to for tangled" />
10
10
<script src="/static/htmx.min.js"></script>
11
+
<link rel="manifest" href="/pwa-manifest.json" />
11
12
<link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" />
12
13
<title>login · tangled</title>
13
14
</head>
+1
appview/pages/templates/user/signup.html
+1
appview/pages/templates/user/signup.html
···
8
8
<meta property="og:url" content="https://tangled.org/signup" />
9
9
<meta property="og:description" content="sign up for tangled" />
10
10
<script src="/static/htmx.min.js"></script>
11
+
<link rel="manifest" href="/pwa-manifest.json" />
11
12
<link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" />
12
13
<title>sign up · tangled</title>
13
14
+9
-5
appview/pulls/pulls.go
+9
-5
appview/pulls/pulls.go
···
189
189
m[p.Sha] = p
190
190
}
191
191
192
-
reactionCountMap, err := db.GetReactionCountMap(s.db, pull.PullAt())
192
+
reactionMap, err := db.GetReactionMap(s.db, 20, pull.PullAt())
193
193
if err != nil {
194
194
log.Println("failed to get pull reactions")
195
195
s.pages.Notice(w, "pulls", "Failed to load pull. Try again later.")
···
227
227
Pipelines: m,
228
228
229
229
OrderedReactionKinds: models.OrderedReactionKinds,
230
-
Reactions: reactionCountMap,
230
+
Reactions: reactionMap,
231
231
UserReacted: userReactions,
232
232
233
233
LabelDefs: defs,
···
1093
1093
1094
1094
// We've already checked earlier if it's diff-based and title is empty,
1095
1095
// so if it's still empty now, it's intentionally skipped owing to format-patch.
1096
-
if title == "" {
1096
+
if title == "" || body == "" {
1097
1097
formatPatches, err := patchutil.ExtractPatches(patch)
1098
1098
if err != nil {
1099
1099
s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err))
···
1104
1104
return
1105
1105
}
1106
1106
1107
-
title = formatPatches[0].Title
1108
-
body = formatPatches[0].Body
1107
+
if title == "" {
1108
+
title = formatPatches[0].Title
1109
+
}
1110
+
if body == "" {
1111
+
body = formatPatches[0].Body
1112
+
}
1109
1113
}
1110
1114
1111
1115
rkey := tid.TID()
+12
-1
appview/repo/index.go
+12
-1
appview/repo/index.go
···
200
200
})
201
201
}
202
202
203
+
tx, err := rp.db.Begin()
204
+
if err != nil {
205
+
return nil, err
206
+
}
207
+
defer tx.Rollback()
208
+
203
209
// update appview's cache
204
-
err = db.InsertRepoLanguages(rp.db, langs)
210
+
err = db.UpdateRepoLanguages(tx, f.RepoAt(), currentRef, langs)
205
211
if err != nil {
206
212
// non-fatal
207
213
log.Println("failed to cache lang results", err)
214
+
}
215
+
216
+
err = tx.Commit()
217
+
if err != nil {
218
+
return nil, err
208
219
}
209
220
}
210
221
+1
-1
appview/signup/signup.go
+1
-1
appview/signup/signup.go
···
133
133
noticeId := "signup-msg"
134
134
135
135
if err := s.validateCaptcha(cfToken, r); err != nil {
136
-
s.l.Warn("turnstile validation failed", "error", err)
136
+
s.l.Warn("turnstile validation failed", "error", err, "email", emailId)
137
137
s.pages.Notice(w, noticeId, "Captcha validation failed.")
138
138
return
139
139
}
+151
appview/state/gfi.go
+151
appview/state/gfi.go
···
1
+
package state
2
+
3
+
import (
4
+
"fmt"
5
+
"log"
6
+
"net/http"
7
+
"sort"
8
+
9
+
"github.com/bluesky-social/indigo/atproto/syntax"
10
+
"tangled.org/core/api/tangled"
11
+
"tangled.org/core/appview/db"
12
+
"tangled.org/core/appview/models"
13
+
"tangled.org/core/appview/pages"
14
+
"tangled.org/core/appview/pagination"
15
+
"tangled.org/core/consts"
16
+
)
17
+
18
+
func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) {
19
+
user := s.oauth.GetUser(r)
20
+
21
+
page, ok := r.Context().Value("page").(pagination.Page)
22
+
if !ok {
23
+
page = pagination.FirstPage()
24
+
}
25
+
26
+
goodFirstIssueLabel := fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "good-first-issue")
27
+
28
+
repoLabels, err := db.GetRepoLabels(s.db, db.FilterEq("label_at", goodFirstIssueLabel))
29
+
if err != nil {
30
+
log.Println("failed to get repo labels", err)
31
+
s.pages.Error503(w)
32
+
return
33
+
}
34
+
35
+
if len(repoLabels) == 0 {
36
+
s.pages.GoodFirstIssues(w, pages.GoodFirstIssuesParams{
37
+
LoggedInUser: user,
38
+
RepoGroups: []*models.RepoGroup{},
39
+
LabelDefs: make(map[string]*models.LabelDefinition),
40
+
Page: page,
41
+
})
42
+
return
43
+
}
44
+
45
+
repoUris := make([]string, 0, len(repoLabels))
46
+
for _, rl := range repoLabels {
47
+
repoUris = append(repoUris, rl.RepoAt.String())
48
+
}
49
+
50
+
allIssues, err := db.GetIssuesPaginated(
51
+
s.db,
52
+
pagination.Page{
53
+
Limit: 500,
54
+
},
55
+
db.FilterIn("repo_at", repoUris),
56
+
db.FilterEq("open", 1),
57
+
)
58
+
if err != nil {
59
+
log.Println("failed to get issues", err)
60
+
s.pages.Error503(w)
61
+
return
62
+
}
63
+
64
+
var goodFirstIssues []models.Issue
65
+
for _, issue := range allIssues {
66
+
if issue.Labels.ContainsLabel(goodFirstIssueLabel) {
67
+
goodFirstIssues = append(goodFirstIssues, issue)
68
+
}
69
+
}
70
+
71
+
repoGroups := make(map[syntax.ATURI]*models.RepoGroup)
72
+
for _, issue := range goodFirstIssues {
73
+
if group, exists := repoGroups[issue.Repo.RepoAt()]; exists {
74
+
group.Issues = append(group.Issues, issue)
75
+
} else {
76
+
repoGroups[issue.Repo.RepoAt()] = &models.RepoGroup{
77
+
Repo: issue.Repo,
78
+
Issues: []models.Issue{issue},
79
+
}
80
+
}
81
+
}
82
+
83
+
var sortedGroups []*models.RepoGroup
84
+
for _, group := range repoGroups {
85
+
sortedGroups = append(sortedGroups, group)
86
+
}
87
+
88
+
sort.Slice(sortedGroups, func(i, j int) bool {
89
+
iIsTangled := sortedGroups[i].Repo.Did == consts.TangledDid
90
+
jIsTangled := sortedGroups[j].Repo.Did == consts.TangledDid
91
+
92
+
// If one is tangled and the other isn't, non-tangled comes first
93
+
if iIsTangled != jIsTangled {
94
+
return jIsTangled // true if j is tangled (i should come first)
95
+
}
96
+
97
+
// Both tangled or both not tangled: sort by name
98
+
return sortedGroups[i].Repo.Name < sortedGroups[j].Repo.Name
99
+
})
100
+
101
+
groupStart := page.Offset
102
+
groupEnd := page.Offset + page.Limit
103
+
if groupStart > len(sortedGroups) {
104
+
groupStart = len(sortedGroups)
105
+
}
106
+
if groupEnd > len(sortedGroups) {
107
+
groupEnd = len(sortedGroups)
108
+
}
109
+
110
+
paginatedGroups := sortedGroups[groupStart:groupEnd]
111
+
112
+
var allIssuesFromGroups []models.Issue
113
+
for _, group := range paginatedGroups {
114
+
allIssuesFromGroups = append(allIssuesFromGroups, group.Issues...)
115
+
}
116
+
117
+
var allLabelDefs []models.LabelDefinition
118
+
if len(allIssuesFromGroups) > 0 {
119
+
labelDefUris := make(map[string]bool)
120
+
for _, issue := range allIssuesFromGroups {
121
+
for labelDefUri := range issue.Labels.Inner() {
122
+
labelDefUris[labelDefUri] = true
123
+
}
124
+
}
125
+
126
+
uriList := make([]string, 0, len(labelDefUris))
127
+
for uri := range labelDefUris {
128
+
uriList = append(uriList, uri)
129
+
}
130
+
131
+
if len(uriList) > 0 {
132
+
allLabelDefs, err = db.GetLabelDefinitions(s.db, db.FilterIn("at_uri", uriList))
133
+
if err != nil {
134
+
log.Println("failed to fetch labels", err)
135
+
}
136
+
}
137
+
}
138
+
139
+
labelDefsMap := make(map[string]*models.LabelDefinition)
140
+
for i := range allLabelDefs {
141
+
labelDefsMap[allLabelDefs[i].AtUri().String()] = &allLabelDefs[i]
142
+
}
143
+
144
+
s.pages.GoodFirstIssues(w, pages.GoodFirstIssuesParams{
145
+
LoggedInUser: user,
146
+
RepoGroups: paginatedGroups,
147
+
LabelDefs: labelDefsMap,
148
+
Page: page,
149
+
GfiLabel: labelDefsMap[goodFirstIssueLabel],
150
+
})
151
+
}
+14
-1
appview/state/knotstream.go
+14
-1
appview/state/knotstream.go
···
172
172
})
173
173
}
174
174
175
-
return db.InsertRepoLanguages(d, langs)
175
+
tx, err := d.Begin()
176
+
if err != nil {
177
+
return err
178
+
}
179
+
defer tx.Rollback()
180
+
181
+
// update appview's cache
182
+
err = db.UpdateRepoLanguages(tx, repo.RepoAt(), ref.Short(), langs)
183
+
if err != nil {
184
+
fmt.Printf("failed; %s\n", err)
185
+
// non-fatal
186
+
}
187
+
188
+
return tx.Commit()
176
189
}
177
190
178
191
func ingestPipeline(d *db.DB, source ec.Source, msg ec.Message) error {
+8
-6
appview/state/reaction.go
+8
-6
appview/state/reaction.go
···
70
70
return
71
71
}
72
72
73
-
count, err := db.GetReactionCount(s.db, subjectUri, reactionKind)
73
+
reactionMap, err := db.GetReactionMap(s.db, 20, subjectUri)
74
74
if err != nil {
75
-
log.Println("failed to get reaction count for ", subjectUri)
75
+
log.Println("failed to get reactions for ", subjectUri)
76
76
}
77
77
78
78
log.Println("created atproto record: ", resp.Uri)
···
80
80
s.pages.ThreadReactionFragment(w, pages.ThreadReactionFragmentParams{
81
81
ThreadAt: subjectUri,
82
82
Kind: reactionKind,
83
-
Count: count,
83
+
Count: reactionMap[reactionKind].Count,
84
+
Users: reactionMap[reactionKind].Users,
84
85
IsReacted: true,
85
86
})
86
87
···
109
110
// this is not an issue, the firehose event might have already done this
110
111
}
111
112
112
-
count, err := db.GetReactionCount(s.db, subjectUri, reactionKind)
113
+
reactionMap, err := db.GetReactionMap(s.db, 20, subjectUri)
113
114
if err != nil {
114
-
log.Println("failed to get reaction count for ", subjectUri)
115
+
log.Println("failed to get reactions for ", subjectUri)
115
116
return
116
117
}
117
118
118
119
s.pages.ThreadReactionFragment(w, pages.ThreadReactionFragmentParams{
119
120
ThreadAt: subjectUri,
120
121
Kind: reactionKind,
121
-
Count: count,
122
+
Count: reactionMap[reactionKind].Count,
123
+
Users: reactionMap[reactionKind].Users,
122
124
IsReacted: false,
123
125
})
124
126
+3
appview/state/router.go
+3
appview/state/router.go
···
37
37
router.Use(middleware.TryRefreshSession())
38
38
router.Get("/favicon.svg", s.Favicon)
39
39
router.Get("/favicon.ico", s.Favicon)
40
+
router.Get("/pwa-manifest.json", s.PWAManifest)
40
41
41
42
userRouter := s.UserRouter(&middleware)
42
43
standardRouter := s.StandardRouter(&middleware)
···
130
131
})
131
132
// r.Post("/import", s.ImportRepo)
132
133
})
134
+
135
+
r.Get("/goodfirstissues", s.GoodFirstIssues)
133
136
134
137
r.With(middleware.AuthMiddleware(s.oauth)).Route("/follow", func(r chi.Router) {
135
138
r.Post("/", s.Follow)
+31
-2
appview/state/state.go
+31
-2
appview/state/state.go
···
198
198
s.pages.Favicon(w)
199
199
}
200
200
201
+
// https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest
202
+
const manifestJson = `{
203
+
"name": "tangled",
204
+
"description": "tightly-knit social coding.",
205
+
"icons": [
206
+
{
207
+
"src": "/favicon.svg",
208
+
"sizes": "144x144"
209
+
}
210
+
],
211
+
"start_url": "/",
212
+
"id": "org.tangled",
213
+
214
+
"display": "standalone",
215
+
"background_color": "#111827",
216
+
"theme_color": "#111827"
217
+
}`
218
+
219
+
func (p *State) PWAManifest(w http.ResponseWriter, r *http.Request) {
220
+
w.Header().Set("Content-Type", "application/json")
221
+
w.Write([]byte(manifestJson))
222
+
}
223
+
201
224
func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) {
202
225
user := s.oauth.GetUser(r)
203
226
s.pages.TermsOfService(w, pages.TermsOfServiceParams{
···
247
270
return
248
271
}
249
272
250
-
s.pages.Timeline(w, pages.TimelineParams{
273
+
gfiLabel, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", models.LabelGoodFirstIssue))
274
+
if err != nil {
275
+
// non-fatal
276
+
}
277
+
278
+
fmt.Println(s.pages.Timeline(w, pages.TimelineParams{
251
279
LoggedInUser: user,
252
280
Timeline: timeline,
253
281
Repos: repos,
254
-
})
282
+
GfiLabel: gfiLabel,
283
+
}))
255
284
}
256
285
257
286
func (s *State) UpgradeBanner(w http.ResponseWriter, r *http.Request) {
+1
-1
appview/xrpcclient/xrpc.go
+1
-1
appview/xrpcclient/xrpc.go
+1
-1
cmd/genjwks/main.go
+1
-1
cmd/genjwks/main.go
+1
-1
docs/spindle/pipeline.md
+1
-1
docs/spindle/pipeline.md
···
21
21
- `manual`: The workflow can be triggered manually.
22
22
- `branch`: This is a **required** field that defines which branches the workflow should run for. If used with the `push` event, commits to the branch(es) listed here will trigger the workflow. If used with the `pull_request` event, updates to pull requests targeting the branch(es) listed here will trigger the workflow. This field has no effect with the `manual` event.
23
23
24
-
For example, if you'd like define a workflow that runs when commits are pushed to the `main` and `develop` branches, or when pull requests that target the `main` branch are updated, or manually, you can do so with:
24
+
For example, if you'd like to define a workflow that runs when commits are pushed to the `main` and `develop` branches, or when pull requests that target the `main` branch are updated, or manually, you can do so with:
25
25
26
26
```yaml
27
27
when:
+3
-3
go.mod
+3
-3
go.mod
···
40
40
github.com/urfave/cli/v3 v3.3.3
41
41
github.com/whyrusleeping/cbor-gen v0.3.1
42
42
github.com/wyatt915/goldmark-treeblood v0.0.0-20250825231212-5dcbdb2f4b57
43
-
github.com/yuin/goldmark v1.7.12
43
+
github.com/yuin/goldmark v1.7.13
44
44
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
45
45
golang.org/x/crypto v0.40.0
46
+
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
46
47
golang.org/x/net v0.42.0
47
48
golang.org/x/sync v0.16.0
48
49
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da
49
50
gopkg.in/yaml.v3 v3.0.1
50
-
tangled.sh/icyphox.sh/atproto-oauth v0.0.0-20250724194903-28e660378cb1
51
+
tangled.org/anirudh.fi/atproto-oauth v0.0.0-20251004062652-69f4561572b5
51
52
)
52
53
53
54
require (
···
168
169
go.uber.org/atomic v1.11.0 // indirect
169
170
go.uber.org/multierr v1.11.0 // indirect
170
171
go.uber.org/zap v1.27.0 // indirect
171
-
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
172
172
golang.org/x/sys v0.34.0 // indirect
173
173
golang.org/x/text v0.27.0 // indirect
174
174
golang.org/x/time v0.12.0 // indirect
+4
-4
go.sum
+4
-4
go.sum
···
436
436
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
437
437
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
438
438
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
439
-
github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY=
440
-
github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
439
+
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
440
+
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
441
441
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
442
442
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
443
443
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA=
···
652
652
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
653
653
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
654
654
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
655
-
tangled.sh/icyphox.sh/atproto-oauth v0.0.0-20250724194903-28e660378cb1 h1:z1os1aRIqeo5e8d0Tx7hk+LH8OdZZeIOY0zw9VB/ZoU=
656
-
tangled.sh/icyphox.sh/atproto-oauth v0.0.0-20250724194903-28e660378cb1/go.mod h1:+oQi9S6IIDll0nxLZVhuzOPX8WKLCYEnE6M5kUKupDg=
655
+
tangled.org/anirudh.fi/atproto-oauth v0.0.0-20251004062652-69f4561572b5 h1:EpQ9MT09jSf4Zjs1+yFvB4CD/fBkFdx8UaDJDwO1Jk8=
656
+
tangled.org/anirudh.fi/atproto-oauth v0.0.0-20251004062652-69f4561572b5/go.mod h1:BQFGoN2V+h5KtgKsQgWU73R55ILdDy/R5RZTrZi6wog=
657
657
tangled.sh/oppi.li/go-gitdiff v0.8.2 h1:pASJJNWaFn6EmEIUNNjHZQ3stRu6BqTO2YyjKvTcxIc=
658
658
tangled.sh/oppi.li/go-gitdiff v0.8.2/go.mod h1:WWAk1Mc6EgWarCrPFO+xeYlujPu98VuLW3Tu+B/85AE=
+1
-1
knotserver/config/config.go
+1
-1
knotserver/config/config.go
···
41
41
Repo Repo `env:",prefix=KNOT_REPO_"`
42
42
Server Server `env:",prefix=KNOT_SERVER_"`
43
43
Git Git `env:",prefix=KNOT_GIT_"`
44
-
AppViewEndpoint string `env:"APPVIEW_ENDPOINT, default=https://tangled.sh"`
44
+
AppViewEndpoint string `env:"APPVIEW_ENDPOINT, default=https://tangled.org"`
45
45
}
46
46
47
47
func Load(ctx context.Context) (*Config, error) {
+1
-1
nix/gomod2nix.toml
+1
-1
nix/gomod2nix.toml
···
527
527
[mod."lukechampine.com/blake3"]
528
528
version = "v1.4.1"
529
529
hash = "sha256-HaZGo9L44ptPsgxIhvKy3+0KZZm1+xt+cZC1rDQA9Yc="
530
-
[mod."tangled.sh/icyphox.sh/atproto-oauth"]
530
+
[mod."tangled.org/anirudh.fi/atproto-oauth"]
531
531
version = "v0.0.0-20250724194903-28e660378cb1"
532
532
hash = "sha256-z7huwCTTHqLb1hxQW62lz9GQ3Orqt4URfeOVhQVd1f8="