+8
-9
appview/issues/issues.go
+8
-9
appview/issues/issues.go
···
129
}
130
131
rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
132
-
LoggedInUser: user,
133
-
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
134
-
Issue: issue,
135
-
CommentList: issue.CommentList(),
136
-
Backlinks: backlinks,
137
-
OrderedReactionKinds: models.OrderedReactionKinds,
138
-
Reactions: reactionMap,
139
-
UserReacted: userReactions,
140
-
LabelDefs: defs,
141
})
142
}
143
···
129
}
130
131
rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
132
+
LoggedInUser: user,
133
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
134
+
Issue: issue,
135
+
CommentList: issue.CommentList(),
136
+
Backlinks: backlinks,
137
+
Reactions: reactionMap,
138
+
UserReacted: userReactions,
139
+
LabelDefs: defs,
140
})
141
}
142
-15
appview/knots/knots.go
-15
appview/knots/knots.go
···
40
Knotstream *eventconsumer.Consumer
41
}
42
43
-
type tab = map[string]any
44
-
45
-
var (
46
-
knotsTabs []tab = []tab{
47
-
{"Name": "profile", "Icon": "user"},
48
-
{"Name": "keys", "Icon": "key"},
49
-
{"Name": "emails", "Icon": "mail"},
50
-
{"Name": "notifications", "Icon": "bell"},
51
-
{"Name": "knots", "Icon": "volleyball"},
52
-
{"Name": "spindles", "Icon": "spool"},
53
-
}
54
-
)
55
-
56
func (k *Knots) Router() http.Handler {
57
r := chi.NewRouter()
58
···
84
k.Pages.Knots(w, pages.KnotsParams{
85
LoggedInUser: user,
86
Registrations: registrations,
87
-
Tabs: knotsTabs,
88
Tab: "knots",
89
})
90
}
···
148
Members: members,
149
Repos: repoMap,
150
IsOwner: true,
151
-
Tabs: knotsTabs,
152
Tab: "knots",
153
})
154
}
···
40
Knotstream *eventconsumer.Consumer
41
}
42
43
func (k *Knots) Router() http.Handler {
44
r := chi.NewRouter()
45
···
71
k.Pages.Knots(w, pages.KnotsParams{
72
LoggedInUser: user,
73
Registrations: registrations,
74
Tab: "knots",
75
})
76
}
···
134
Members: members,
135
Repos: repoMap,
136
IsOwner: true,
137
Tab: "knots",
138
})
139
}
+22
appview/pages/funcmap.go
+22
appview/pages/funcmap.go
···
32
"tangled.org/core/crypto"
33
)
34
35
+
type tab map[string]string
36
+
37
func (p *Pages) funcMap() template.FuncMap {
38
return template.FuncMap{
39
"split": func(s string) []string {
···
386
return "error"
387
}
388
return fp
389
+
},
390
+
// constant values used to define a template
391
+
"const": func() map[string]any {
392
+
return map[string]any{
393
+
"OrderedReactionKinds": models.OrderedReactionKinds,
394
+
// would be great to have ordered maps right about now
395
+
"UserSettingsTabs": []tab{
396
+
{"Name": "profile", "Icon": "user"},
397
+
{"Name": "keys", "Icon": "key"},
398
+
{"Name": "emails", "Icon": "mail"},
399
+
{"Name": "notifications", "Icon": "bell"},
400
+
{"Name": "knots", "Icon": "volleyball"},
401
+
{"Name": "spindles", "Icon": "spool"},
402
+
},
403
+
"RepoSettingsTabs": []tab{
404
+
{"Name": "general", "Icon": "sliders-horizontal"},
405
+
{"Name": "access", "Icon": "users"},
406
+
{"Name": "pipelines", "Icon": "layers-2"},
407
+
},
408
+
}
409
},
410
}
411
}
+23
-42
appview/pages/pages.go
+23
-42
appview/pages/pages.go
···
336
337
type UserProfileSettingsParams struct {
338
LoggedInUser *oauth.User
339
-
Tabs []map[string]any
340
Tab string
341
}
342
···
375
type UserKeysSettingsParams struct {
376
LoggedInUser *oauth.User
377
PubKeys []models.PublicKey
378
-
Tabs []map[string]any
379
Tab string
380
}
381
···
386
type UserEmailsSettingsParams struct {
387
LoggedInUser *oauth.User
388
Emails []models.Email
389
-
Tabs []map[string]any
390
Tab string
391
}
392
···
397
type UserNotificationSettingsParams struct {
398
LoggedInUser *oauth.User
399
Preferences *models.NotificationPreferences
400
-
Tabs []map[string]any
401
Tab string
402
}
403
···
417
type KnotsParams struct {
418
LoggedInUser *oauth.User
419
Registrations []models.Registration
420
-
Tabs []map[string]any
421
Tab string
422
}
423
···
431
Members []string
432
Repos map[string][]models.Repo
433
IsOwner bool
434
-
Tabs []map[string]any
435
Tab string
436
}
437
···
450
type SpindlesParams struct {
451
LoggedInUser *oauth.User
452
Spindles []models.Spindle
453
-
Tabs []map[string]any
454
Tab string
455
}
456
···
460
461
type SpindleListingParams struct {
462
models.Spindle
463
-
Tabs []map[string]any
464
-
Tab string
465
}
466
467
func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error {
···
473
Spindle models.Spindle
474
Members []string
475
Repos map[string][]models.Repo
476
-
Tabs []map[string]any
477
Tab string
478
}
479
···
612
}
613
614
type FollowFragmentParams struct {
615
-
UserDid string
616
-
FollowStatus models.FollowStatus
617
-
FollowersCount int64
618
}
619
620
func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error {
621
-
return p.executePlain("user/fragments/follow-oob", w, params)
622
}
623
624
type EditBioParams struct {
···
649
IsStarred bool
650
SubjectAt syntax.ATURI
651
StarCount int
652
-
HxSwapOob bool
653
}
654
655
func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error {
656
-
params.HxSwapOob = true
657
-
return p.executePlain("fragments/starBtn", w, params)
658
}
659
660
type RepoIndexParams struct {
···
884
SubscribedLabels map[string]struct{}
885
ShouldSubscribeAll bool
886
Active string
887
-
Tabs []map[string]any
888
Tab string
889
Branches []types.Branch
890
}
···
898
LoggedInUser *oauth.User
899
RepoInfo repoinfo.RepoInfo
900
Active string
901
-
Tabs []map[string]any
902
Tab string
903
Collaborators []Collaborator
904
}
···
912
LoggedInUser *oauth.User
913
RepoInfo repoinfo.RepoInfo
914
Active string
915
-
Tabs []map[string]any
916
Tab string
917
Spindles []string
918
CurrentSpindle string
···
950
Backlinks []models.RichReferenceLink
951
LabelDefs map[string]*models.LabelDefinition
952
953
-
OrderedReactionKinds []models.ReactionKind
954
-
Reactions map[models.ReactionKind]models.ReactionDisplayData
955
-
UserReacted map[models.ReactionKind]bool
956
}
957
958
func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error {
···
1107
ResubmitCheck ResubmitResult
1108
Pipelines map[string]models.Pipeline
1109
1110
-
OrderedReactionKinds []models.ReactionKind
1111
-
Reactions map[models.ReactionKind]models.ReactionDisplayData
1112
-
UserReacted map[models.ReactionKind]bool
1113
1114
LabelDefs map[string]*models.LabelDefinition
1115
}
···
1120
}
1121
1122
type RepoPullPatchParams struct {
1123
-
LoggedInUser *oauth.User
1124
-
RepoInfo repoinfo.RepoInfo
1125
-
Pull *models.Pull
1126
-
Stack models.Stack
1127
-
Diff *types.NiceDiff
1128
-
Round int
1129
-
Submission *models.PullSubmission
1130
-
OrderedReactionKinds []models.ReactionKind
1131
-
DiffOpts types.DiffOpts
1132
}
1133
1134
// this name is a mouthful
···
1137
}
1138
1139
type RepoPullInterdiffParams struct {
1140
-
LoggedInUser *oauth.User
1141
-
RepoInfo repoinfo.RepoInfo
1142
-
Pull *models.Pull
1143
-
Round int
1144
-
Interdiff *patchutil.InterdiffResult
1145
-
OrderedReactionKinds []models.ReactionKind
1146
-
DiffOpts types.DiffOpts
1147
}
1148
1149
// this name is a mouthful
···
336
337
type UserProfileSettingsParams struct {
338
LoggedInUser *oauth.User
339
Tab string
340
}
341
···
374
type UserKeysSettingsParams struct {
375
LoggedInUser *oauth.User
376
PubKeys []models.PublicKey
377
Tab string
378
}
379
···
384
type UserEmailsSettingsParams struct {
385
LoggedInUser *oauth.User
386
Emails []models.Email
387
Tab string
388
}
389
···
394
type UserNotificationSettingsParams struct {
395
LoggedInUser *oauth.User
396
Preferences *models.NotificationPreferences
397
Tab string
398
}
399
···
413
type KnotsParams struct {
414
LoggedInUser *oauth.User
415
Registrations []models.Registration
416
Tab string
417
}
418
···
426
Members []string
427
Repos map[string][]models.Repo
428
IsOwner bool
429
Tab string
430
}
431
···
444
type SpindlesParams struct {
445
LoggedInUser *oauth.User
446
Spindles []models.Spindle
447
Tab string
448
}
449
···
453
454
type SpindleListingParams struct {
455
models.Spindle
456
+
Tab string
457
}
458
459
func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error {
···
465
Spindle models.Spindle
466
Members []string
467
Repos map[string][]models.Repo
468
Tab string
469
}
470
···
603
}
604
605
type FollowFragmentParams struct {
606
+
UserDid string
607
+
FollowStatus models.FollowStatus
608
}
609
610
func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error {
611
+
return p.executePlain("user/fragments/follow", w, params)
612
}
613
614
type EditBioParams struct {
···
639
IsStarred bool
640
SubjectAt syntax.ATURI
641
StarCount int
642
}
643
644
func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error {
645
+
return p.executePlain("fragments/starBtn-oob", w, params)
646
}
647
648
type RepoIndexParams struct {
···
872
SubscribedLabels map[string]struct{}
873
ShouldSubscribeAll bool
874
Active string
875
Tab string
876
Branches []types.Branch
877
}
···
885
LoggedInUser *oauth.User
886
RepoInfo repoinfo.RepoInfo
887
Active string
888
Tab string
889
Collaborators []Collaborator
890
}
···
898
LoggedInUser *oauth.User
899
RepoInfo repoinfo.RepoInfo
900
Active string
901
Tab string
902
Spindles []string
903
CurrentSpindle string
···
935
Backlinks []models.RichReferenceLink
936
LabelDefs map[string]*models.LabelDefinition
937
938
+
Reactions map[models.ReactionKind]models.ReactionDisplayData
939
+
UserReacted map[models.ReactionKind]bool
940
}
941
942
func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error {
···
1091
ResubmitCheck ResubmitResult
1092
Pipelines map[string]models.Pipeline
1093
1094
+
Reactions map[models.ReactionKind]models.ReactionDisplayData
1095
+
UserReacted map[models.ReactionKind]bool
1096
1097
LabelDefs map[string]*models.LabelDefinition
1098
}
···
1103
}
1104
1105
type RepoPullPatchParams struct {
1106
+
LoggedInUser *oauth.User
1107
+
RepoInfo repoinfo.RepoInfo
1108
+
Pull *models.Pull
1109
+
Stack models.Stack
1110
+
Diff *types.NiceDiff
1111
+
Round int
1112
+
Submission *models.PullSubmission
1113
+
DiffOpts types.DiffOpts
1114
}
1115
1116
// this name is a mouthful
···
1119
}
1120
1121
type RepoPullInterdiffParams struct {
1122
+
LoggedInUser *oauth.User
1123
+
RepoInfo repoinfo.RepoInfo
1124
+
Pull *models.Pull
1125
+
Round int
1126
+
Interdiff *patchutil.InterdiffResult
1127
+
DiffOpts types.DiffOpts
1128
}
1129
1130
// this name is a mouthful
+5
appview/pages/templates/fragments/starBtn-oob.html
+5
appview/pages/templates/fragments/starBtn-oob.html
-1
appview/pages/templates/fragments/starBtn.html
-1
appview/pages/templates/fragments/starBtn.html
+50
appview/pages/templates/repo/fragments/reactions.html
+50
appview/pages/templates/repo/fragments/reactions.html
···
···
1
+
{{ define "repo/fragments/reactions" }}
2
+
<div class="flex flex-wrap items-center gap-2">
3
+
{{- $reactions := .Reactions -}}
4
+
{{- $userReacted := .UserReacted -}}
5
+
{{- $threadAt := .ThreadAt -}}
6
+
7
+
{{ template "reactionsPopup" }}
8
+
{{ range $kind := const.OrderedReactionKinds }}
9
+
{{ $reactionData := index $reactions $kind }}
10
+
{{ template "repo/fragments/reaction"
11
+
(dict
12
+
"Kind" $kind
13
+
"Count" $reactionData.Count
14
+
"IsReacted" (index $userReacted $kind)
15
+
"ThreadAt" $threadAt
16
+
"Users" $reactionData.Users) }}
17
+
{{ end }}
18
+
</div>
19
+
{{ end }}
20
+
21
+
{{ define "reactionsPopup" }}
22
+
<details
23
+
id="reactionsPopUp"
24
+
class="relative inline-block"
25
+
>
26
+
<summary
27
+
class="flex justify-center items-center min-w-8 min-h-8 rounded border border-gray-200 dark:border-gray-700
28
+
hover:bg-gray-50
29
+
hover:border-gray-300
30
+
dark:hover:bg-gray-700
31
+
dark:hover:border-gray-600
32
+
cursor-pointer list-none"
33
+
>
34
+
{{ i "smile" "size-4" }}
35
+
</summary>
36
+
<div
37
+
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"
38
+
>
39
+
{{ range $kind := const.OrderedReactionKinds }}
40
+
<button
41
+
id="reactBtn-{{ $kind }}"
42
+
class="size-12 hover:bg-gray-100 dark:hover:bg-gray-700"
43
+
hx-on:click="this.parentElement.parentElement.removeAttribute('open')"
44
+
>
45
+
{{ $kind }}
46
+
</button>
47
+
{{ end }}
48
+
</div>
49
+
</details>
50
+
{{ 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 }}
···
+5
-21
appview/pages/templates/repo/issues/issue.html
+5
-21
appview/pages/templates/repo/issues/issue.html
···
35
{{ if .Issue.Body }}
36
<article id="body" class="mt-4 prose dark:prose-invert">{{ .Issue.Body | markdown }}</article>
37
{{ end }}
38
-
<div class="flex flex-wrap gap-2 items-stretch mt-4">
39
-
{{ template "issueReactions" . }}
40
</div>
41
</section>
42
{{ end }}
···
106
{{ i "loader-circle" "size-3 animate-spin hidden group-[.htmx-request]:inline" }}
107
</a>
108
{{ end }}
109
-
110
-
{{ define "issueReactions" }}
111
-
<div class="flex items-center gap-2">
112
-
{{ template "repo/fragments/reactionsPopUp" .OrderedReactionKinds }}
113
-
{{ range $kind := .OrderedReactionKinds }}
114
-
{{ $reactionData := index $.Reactions $kind }}
115
-
{{
116
-
template "repo/fragments/reaction"
117
-
(dict
118
-
"Kind" $kind
119
-
"Count" $reactionData.Count
120
-
"IsReacted" (index $.UserReacted $kind)
121
-
"ThreadAt" $.Issue.AtUri
122
-
"Users" $reactionData.Users)
123
-
}}
124
-
{{ end }}
125
-
</div>
126
-
{{ end }}
127
-
128
129
{{ define "repoAfter" }}
130
<div class="flex flex-col gap-4 mt-4">
···
35
{{ if .Issue.Body }}
36
<article id="body" class="mt-4 prose dark:prose-invert">{{ .Issue.Body | markdown }}</article>
37
{{ end }}
38
+
<div class="mt-4">
39
+
{{ template "repo/fragments/reactions"
40
+
(dict "Reactions" .Reactions
41
+
"UserReacted" .UserReacted
42
+
"ThreadAt" .Issue.AtUri) }}
43
</div>
44
</section>
45
{{ end }}
···
109
{{ i "loader-circle" "size-3 animate-spin hidden group-[.htmx-request]:inline" }}
110
</a>
111
{{ end }}
112
113
{{ define "repoAfter" }}
114
<div class="flex flex-col gap-4 mt-4">
+5
-16
appview/pages/templates/repo/pulls/fragments/pullHeader.html
+5
-16
appview/pages/templates/repo/pulls/fragments/pullHeader.html
···
64
</article>
65
{{ end }}
66
67
-
{{ with .OrderedReactionKinds }}
68
-
<div class="flex items-center gap-2 mt-2">
69
-
{{ template "repo/fragments/reactionsPopUp" . }}
70
-
{{ range $kind := . }}
71
-
{{ $reactionData := index $.Reactions $kind }}
72
-
{{
73
-
template "repo/fragments/reaction"
74
-
(dict
75
-
"Kind" $kind
76
-
"Count" $reactionData.Count
77
-
"IsReacted" (index $.UserReacted $kind)
78
-
"ThreadAt" $.Pull.AtUri
79
-
"Users" $reactionData.Users)
80
-
}}
81
-
{{ end }}
82
</div>
83
-
{{ end }}
84
</section>
85
86
-6
appview/pages/templates/user/fragments/follow-oob.html
-6
appview/pages/templates/user/fragments/follow-oob.html
+3
-5
appview/pages/templates/user/fragments/followCard.html
+3
-5
appview/pages/templates/user/fragments/followCard.html
···
9
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 w-full min-w-0">
10
<div class="flex-1 min-h-0 justify-around flex flex-col">
11
<a href="/{{ $userIdent }}">
12
-
<span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{
13
-
$userIdent | truncateAt30 }}</span>
14
</a>
15
{{ with .Profile }}
16
<p class="text-sm pb-2 md:pb-2 break-words">{{.Description}}</p>
17
{{ end }}
18
<div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
19
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
20
-
<span id="followers" data-followers-did="{{ .UserDid }}"><a href="/{{ $userIdent }}?tab=followers">{{
21
-
.FollowersCount }} followers</a></span>
22
<span class="select-none after:content-['ยท']"></span>
23
<span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .FollowingCount }} following</a></span>
24
</div>
···
31
</div>
32
</div>
33
</div>
34
-
{{ end }}
···
9
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 w-full min-w-0">
10
<div class="flex-1 min-h-0 justify-around flex flex-col">
11
<a href="/{{ $userIdent }}">
12
+
<span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $userIdent | truncateAt30 }}</span>
13
</a>
14
{{ with .Profile }}
15
<p class="text-sm pb-2 md:pb-2 break-words">{{.Description}}</p>
16
{{ end }}
17
<div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
18
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
19
+
<span id="followers"><a href="/{{ $userIdent }}?tab=followers">{{ .FollowersCount }} followers</a></span>
20
<span class="select-none after:content-['ยท']"></span>
21
<span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .FollowingCount }} following</a></span>
22
</div>
···
29
</div>
30
</div>
31
</div>
32
+
{{ end }}
+99
-97
appview/pages/templates/user/fragments/profileCard.html
+99
-97
appview/pages/templates/user/fragments/profileCard.html
···
1
{{ define "user/fragments/profileCard" }}
2
-
{{ $userIdent := resolve .UserDid }}
3
-
<div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center">
4
-
<div id="avatar" class="col-span-1 flex justify-center items-center">
5
-
<div class="w-3/4 aspect-square relative">
6
-
<img class="absolute inset-0 w-full h-full object-cover rounded-full p-2" src="{{ fullAvatar .UserDid }}" />
7
-
</div>
8
-
</div>
9
-
<div class="col-span-2">
10
-
<div class="flex items-center flex-row flex-nowrap gap-2">
11
-
<p title="{{ $userIdent }}"
12
-
class="text-lg font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
13
-
{{ $userIdent }}
14
-
</p>
15
-
{{ with .Profile }}
16
-
{{ if .Pronouns }}
17
-
<p class="text-gray-500 dark:text-gray-400">{{ .Pronouns }}</p>
18
-
{{ end }}
19
-
{{ end }}
20
-
</div>
21
22
-
<div class="md:hidden">
23
-
{{ block "followerFollowing" (list . $userIdent) }} {{ end }}
24
-
</div>
25
-
</div>
26
-
<div class="col-span-3 md:col-span-full">
27
-
<div id="profile-bio" class="text-sm">
28
-
{{ $profile := .Profile }}
29
-
{{ with .Profile }}
30
31
-
{{ if .Description }}
32
-
<p class="text-base pb-4 md:pb-2">{{ .Description }}</p>
33
-
{{ end }}
34
35
-
<div class="hidden md:block">
36
-
{{ block "followerFollowing" (list $ $userIdent) }} {{ end }}
37
-
</div>
38
39
-
<div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
40
-
{{ if .Location }}
41
-
<div class="flex items-center gap-2">
42
-
<span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span>
43
-
<span>{{ .Location }}</span>
44
-
</div>
45
-
{{ end }}
46
-
{{ if .IncludeBluesky }}
47
-
<div class="flex items-center gap-2">
48
-
<span class="flex-shrink-0">{{ template "user/fragments/bluesky" "w-4 h-4 text-black dark:text-white"
49
-
}}</span>
50
-
<a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">{{ $userIdent }}</a>
51
-
</div>
52
-
{{ end }}
53
-
{{ range $link := .Links }}
54
-
{{ if $link }}
55
-
<div class="flex items-center gap-2">
56
-
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
57
-
<a href="{{ $link }}">{{ $link }}</a>
58
-
</div>
59
-
{{ end }}
60
-
{{ end }}
61
-
{{ if not $profile.IsStatsEmpty }}
62
-
<div class="flex items-center justify-evenly gap-2 py-2">
63
-
{{ range $stat := .Stats }}
64
-
{{ if $stat.Kind }}
65
-
<div class="flex flex-col items-center gap-2">
66
-
<span class="text-xl font-bold">{{ $stat.Value }}</span>
67
-
<span>{{ $stat.Kind.String }}</span>
68
</div>
69
{{ end }}
70
-
{{ end }}
71
-
</div>
72
-
{{ end }}
73
-
</div>
74
-
{{ end }}
75
76
-
<div class="flex mt-2 items-center gap-2">
77
-
{{ if ne .FollowStatus.String "IsSelf" }}
78
-
{{ template "user/fragments/follow" . }}
79
-
{{ else }}
80
-
<button id="editBtn" class="btn w-full flex items-center gap-2 group" hx-target="#profile-bio"
81
-
hx-get="/profile/edit-bio" hx-swap="innerHTML">
82
-
{{ i "pencil" "w-4 h-4" }}
83
-
edit
84
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
85
-
</button>
86
-
{{ end }}
87
88
-
<a class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group"
89
-
href="/{{ $userIdent }}/feed.atom">
90
-
{{ i "rss" "size-4" }}
91
-
</a>
92
-
</div>
93
94
</div>
95
-
<div id="update-profile" class="text-red-400 dark:text-red-500"></div>
96
-
</div>
97
-
</div>
98
{{ end }}
99
100
{{ define "followerFollowing" }}
101
-
{{ $root := index . 0 }}
102
-
{{ $userIdent := index . 1 }}
103
-
{{ with $root }}
104
-
<div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm">
105
-
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
106
-
<span id="followers" data-followers-did="{{ .UserDid }}"><a href="/{{ $userIdent }}?tab=followers">{{
107
-
.Stats.FollowersCount }} followers</a></span>
108
-
<span class="select-none after:content-['ยท']"></span>
109
-
<span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .Stats.FollowingCount }} following</a></span>
110
-
</div>
111
{{ end }}
112
-
{{ end }}
···
1
{{ define "user/fragments/profileCard" }}
2
+
{{ $userIdent := resolve .UserDid }}
3
+
<div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center">
4
+
<div id="avatar" class="col-span-1 flex justify-center items-center">
5
+
<div class="w-3/4 aspect-square relative">
6
+
<img class="absolute inset-0 w-full h-full object-cover rounded-full p-2" src="{{ fullAvatar .UserDid }}" />
7
+
</div>
8
+
</div>
9
+
<div class="col-span-2">
10
+
<div class="flex items-center flex-row flex-nowrap gap-2">
11
+
<p title="{{ $userIdent }}"
12
+
class="text-lg font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
13
+
{{ $userIdent }}
14
+
</p>
15
+
{{ with .Profile }}
16
+
{{ if .Pronouns }}
17
+
<p class="text-gray-500 dark:text-gray-400">{{ .Pronouns }}</p>
18
+
{{ end }}
19
+
{{ end }}
20
+
</div>
21
22
+
<div class="md:hidden">
23
+
{{ block "followerFollowing" (list . $userIdent) }} {{ end }}
24
+
</div>
25
+
</div>
26
+
<div class="col-span-3 md:col-span-full">
27
+
<div id="profile-bio" class="text-sm">
28
+
{{ $profile := .Profile }}
29
+
{{ with .Profile }}
30
31
+
{{ if .Description }}
32
+
<p class="text-base pb-4 md:pb-2">{{ .Description }}</p>
33
+
{{ end }}
34
35
+
<div class="hidden md:block">
36
+
{{ block "followerFollowing" (list $ $userIdent) }} {{ end }}
37
+
</div>
38
39
+
<div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
40
+
{{ if .Location }}
41
+
<div class="flex items-center gap-2">
42
+
<span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span>
43
+
<span>{{ .Location }}</span>
44
+
</div>
45
+
{{ end }}
46
+
{{ if .IncludeBluesky }}
47
+
<div class="flex items-center gap-2">
48
+
<span class="flex-shrink-0">{{ template "user/fragments/bluesky" "w-4 h-4 text-black dark:text-white" }}</span>
49
+
<a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">{{ $userIdent }}</a>
50
+
</div>
51
+
{{ end }}
52
+
{{ range $link := .Links }}
53
+
{{ if $link }}
54
+
<div class="flex items-center gap-2">
55
+
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
56
+
<a href="{{ $link }}">{{ $link }}</a>
57
+
</div>
58
+
{{ end }}
59
+
{{ end }}
60
+
{{ if not $profile.IsStatsEmpty }}
61
+
<div class="flex items-center justify-evenly gap-2 py-2">
62
+
{{ range $stat := .Stats }}
63
+
{{ if $stat.Kind }}
64
+
<div class="flex flex-col items-center gap-2">
65
+
<span class="text-xl font-bold">{{ $stat.Value }}</span>
66
+
<span>{{ $stat.Kind.String }}</span>
67
+
</div>
68
+
{{ end }}
69
+
{{ end }}
70
+
</div>
71
+
{{ end }}
72
</div>
73
{{ end }}
74
75
+
<div class="flex mt-2 items-center gap-2">
76
+
{{ if ne .FollowStatus.String "IsSelf" }}
77
+
{{ template "user/fragments/follow" . }}
78
+
{{ else }}
79
+
<button id="editBtn"
80
+
class="btn w-full flex items-center gap-2 group"
81
+
hx-target="#profile-bio"
82
+
hx-get="/profile/edit-bio"
83
+
hx-swap="innerHTML">
84
+
{{ i "pencil" "w-4 h-4" }}
85
+
edit
86
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
87
+
</button>
88
+
{{ end }}
89
90
+
<a class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group"
91
+
href="/{{ $userIdent }}/feed.atom">
92
+
{{ i "rss" "size-4" }}
93
+
</a>
94
+
</div>
95
96
+
</div>
97
+
<div id="update-profile" class="text-red-400 dark:text-red-500"></div>
98
+
</div>
99
</div>
100
{{ end }}
101
102
{{ define "followerFollowing" }}
103
+
{{ $root := index . 0 }}
104
+
{{ $userIdent := index . 1 }}
105
+
{{ with $root }}
106
+
<div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm">
107
+
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
108
+
<span id="followers"><a href="/{{ $userIdent }}?tab=followers">{{ .Stats.FollowersCount }} followers</a></span>
109
+
<span class="select-none after:content-['ยท']"></span>
110
+
<span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .Stats.FollowingCount }} following</a></span>
111
+
</div>
112
+
{{ end }}
113
{{ end }}
114
+
+7
-19
appview/pulls/pulls.go
+7
-19
appview/pulls/pulls.go
···
1
package pulls
2
3
import (
4
-
"bytes"
5
-
"compress/gzip"
6
"context"
7
"database/sql"
8
"encoding/json"
9
"errors"
10
"fmt"
11
-
"io"
12
"log"
13
"log/slog"
14
"net/http"
···
247
ResubmitCheck: resubmitResult,
248
Pipelines: m,
249
250
-
OrderedReactionKinds: models.OrderedReactionKinds,
251
-
Reactions: reactionMap,
252
-
UserReacted: userReactions,
253
254
LabelDefs: defs,
255
})
···
1244
return
1245
}
1246
1247
-
blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(patch))
1248
if err != nil {
1249
log.Println("failed to upload patch", err)
1250
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
···
1338
// apply all record creations at once
1339
var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem
1340
for _, p := range stack {
1341
-
blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(p.LatestPatch()))
1342
if err != nil {
1343
log.Println("failed to upload patch blob", err)
1344
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
···
1888
return
1889
}
1890
1891
-
blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(patch))
1892
if err != nil {
1893
log.Println("failed to upload patch blob", err)
1894
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
···
2030
return
2031
}
2032
2033
-
blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(patch))
2034
if err != nil {
2035
log.Println("failed to upload patch blob", err)
2036
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
···
2072
return
2073
}
2074
2075
-
blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(patch))
2076
if err != nil {
2077
log.Println("failed to upload patch blob", err)
2078
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
···
2455
2456
return stack, nil
2457
}
2458
-
2459
-
func gz(s string) io.Reader {
2460
-
var b bytes.Buffer
2461
-
w := gzip.NewWriter(&b)
2462
-
w.Write([]byte(s))
2463
-
w.Close()
2464
-
return &b
2465
-
}
···
1
package pulls
2
3
import (
4
"context"
5
"database/sql"
6
"encoding/json"
7
"errors"
8
"fmt"
9
"log"
10
"log/slog"
11
"net/http"
···
244
ResubmitCheck: resubmitResult,
245
Pipelines: m,
246
247
+
Reactions: reactionMap,
248
+
UserReacted: userReactions,
249
250
LabelDefs: defs,
251
})
···
1240
return
1241
}
1242
1243
+
blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch))
1244
if err != nil {
1245
log.Println("failed to upload patch", err)
1246
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
···
1334
// apply all record creations at once
1335
var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem
1336
for _, p := range stack {
1337
+
blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(p.LatestPatch()))
1338
if err != nil {
1339
log.Println("failed to upload patch blob", err)
1340
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
···
1884
return
1885
}
1886
1887
+
blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch))
1888
if err != nil {
1889
log.Println("failed to upload patch blob", err)
1890
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
···
2026
return
2027
}
2028
2029
+
blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch))
2030
if err != nil {
2031
log.Println("failed to upload patch blob", err)
2032
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
···
2068
return
2069
}
2070
2071
+
blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch))
2072
if err != nil {
2073
log.Println("failed to upload patch blob", err)
2074
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
···
2451
2452
return stack, nil
2453
}
-14
appview/repo/settings.go
-14
appview/repo/settings.go
···
22
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
23
)
24
25
-
type tab = map[string]any
26
-
27
-
var (
28
-
// would be great to have ordered maps right about now
29
-
settingsTabs []tab = []tab{
30
-
{"Name": "general", "Icon": "sliders-horizontal"},
31
-
{"Name": "access", "Icon": "users"},
32
-
{"Name": "pipelines", "Icon": "layers-2"},
33
-
}
34
-
)
35
-
36
func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
37
l := rp.logger.With("handler", "SetDefaultBranch")
38
···
262
DefaultLabels: defaultLabels,
263
SubscribedLabels: subscribedLabels,
264
ShouldSubscribeAll: shouldSubscribeAll,
265
-
Tabs: settingsTabs,
266
Tab: "general",
267
})
268
}
···
308
rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{
309
LoggedInUser: user,
310
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
311
-
Tabs: settingsTabs,
312
Tab: "access",
313
Collaborators: collaborators,
314
})
···
369
rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{
370
LoggedInUser: user,
371
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
372
-
Tabs: settingsTabs,
373
Tab: "pipelines",
374
Spindles: spindles,
375
CurrentSpindle: f.Spindle,
···
22
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
23
)
24
25
func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
26
l := rp.logger.With("handler", "SetDefaultBranch")
27
···
251
DefaultLabels: defaultLabels,
252
SubscribedLabels: subscribedLabels,
253
ShouldSubscribeAll: shouldSubscribeAll,
254
Tab: "general",
255
})
256
}
···
296
rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{
297
LoggedInUser: user,
298
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
299
Tab: "access",
300
Collaborators: collaborators,
301
})
···
356
rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{
357
LoggedInUser: user,
358
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
359
Tab: "pipelines",
360
Spindles: spindles,
361
CurrentSpindle: f.Spindle,
-17
appview/settings/settings.go
-17
appview/settings/settings.go
···
35
Config *config.Config
36
}
37
38
-
type tab = map[string]any
39
-
40
-
var (
41
-
settingsTabs []tab = []tab{
42
-
{"Name": "profile", "Icon": "user"},
43
-
{"Name": "keys", "Icon": "key"},
44
-
{"Name": "emails", "Icon": "mail"},
45
-
{"Name": "notifications", "Icon": "bell"},
46
-
{"Name": "knots", "Icon": "volleyball"},
47
-
{"Name": "spindles", "Icon": "spool"},
48
-
}
49
-
)
50
-
51
func (s *Settings) Router() http.Handler {
52
r := chi.NewRouter()
53
···
85
86
s.Pages.UserProfileSettings(w, pages.UserProfileSettingsParams{
87
LoggedInUser: user,
88
-
Tabs: settingsTabs,
89
Tab: "profile",
90
})
91
}
···
104
s.Pages.UserNotificationSettings(w, pages.UserNotificationSettingsParams{
105
LoggedInUser: user,
106
Preferences: prefs,
107
-
Tabs: settingsTabs,
108
Tab: "notifications",
109
})
110
}
···
146
s.Pages.UserKeysSettings(w, pages.UserKeysSettingsParams{
147
LoggedInUser: user,
148
PubKeys: pubKeys,
149
-
Tabs: settingsTabs,
150
Tab: "keys",
151
})
152
}
···
161
s.Pages.UserEmailsSettings(w, pages.UserEmailsSettingsParams{
162
LoggedInUser: user,
163
Emails: emails,
164
-
Tabs: settingsTabs,
165
Tab: "emails",
166
})
167
}
···
35
Config *config.Config
36
}
37
38
func (s *Settings) Router() http.Handler {
39
r := chi.NewRouter()
40
···
72
73
s.Pages.UserProfileSettings(w, pages.UserProfileSettingsParams{
74
LoggedInUser: user,
75
Tab: "profile",
76
})
77
}
···
90
s.Pages.UserNotificationSettings(w, pages.UserNotificationSettingsParams{
91
LoggedInUser: user,
92
Preferences: prefs,
93
Tab: "notifications",
94
})
95
}
···
131
s.Pages.UserKeysSettings(w, pages.UserKeysSettingsParams{
132
LoggedInUser: user,
133
PubKeys: pubKeys,
134
Tab: "keys",
135
})
136
}
···
145
s.Pages.UserEmailsSettings(w, pages.UserEmailsSettingsParams{
146
LoggedInUser: user,
147
Emails: emails,
148
Tab: "emails",
149
})
150
}
-15
appview/spindles/spindles.go
-15
appview/spindles/spindles.go
···
39
Logger *slog.Logger
40
}
41
42
-
type tab = map[string]any
43
-
44
-
var (
45
-
spindlesTabs []tab = []tab{
46
-
{"Name": "profile", "Icon": "user"},
47
-
{"Name": "keys", "Icon": "key"},
48
-
{"Name": "emails", "Icon": "mail"},
49
-
{"Name": "notifications", "Icon": "bell"},
50
-
{"Name": "knots", "Icon": "volleyball"},
51
-
{"Name": "spindles", "Icon": "spool"},
52
-
}
53
-
)
54
-
55
func (s *Spindles) Router() http.Handler {
56
r := chi.NewRouter()
57
···
83
s.Pages.Spindles(w, pages.SpindlesParams{
84
LoggedInUser: user,
85
Spindles: all,
86
-
Tabs: spindlesTabs,
87
Tab: "spindles",
88
})
89
}
···
143
Spindle: spindle,
144
Members: members,
145
Repos: repoMap,
146
-
Tabs: spindlesTabs,
147
Tab: "spindles",
148
})
149
}
···
39
Logger *slog.Logger
40
}
41
42
func (s *Spindles) Router() http.Handler {
43
r := chi.NewRouter()
44
···
70
s.Pages.Spindles(w, pages.SpindlesParams{
71
LoggedInUser: user,
72
Spindles: all,
73
Tab: "spindles",
74
})
75
}
···
129
Spindle: spindle,
130
Members: members,
131
Repos: repoMap,
132
Tab: "spindles",
133
})
134
}
+4
-16
appview/state/follow.go
+4
-16
appview/state/follow.go
···
75
76
s.notifier.NewFollow(r.Context(), follow)
77
78
-
followStats, err := db.GetFollowerFollowingCount(s.db, subjectIdent.DID.String())
79
-
if err != nil {
80
-
log.Println("failed to get follow stats", err)
81
-
}
82
-
83
s.pages.FollowFragment(w, pages.FollowFragmentParams{
84
-
UserDid: subjectIdent.DID.String(),
85
-
FollowStatus: models.IsFollowing,
86
-
FollowersCount: followStats.Followers,
87
})
88
89
return
···
112
// this is not an issue, the firehose event might have already done this
113
}
114
115
-
followStats, err := db.GetFollowerFollowingCount(s.db, subjectIdent.DID.String())
116
-
if err != nil {
117
-
log.Println("failed to get follow stats", err)
118
-
}
119
-
120
s.pages.FollowFragment(w, pages.FollowFragmentParams{
121
-
UserDid: subjectIdent.DID.String(),
122
-
FollowStatus: models.IsNotFollowing,
123
-
FollowersCount: followStats.Followers,
124
})
125
126
s.notifier.DeleteFollow(r.Context(), follow)
···
75
76
s.notifier.NewFollow(r.Context(), follow)
77
78
s.pages.FollowFragment(w, pages.FollowFragmentParams{
79
+
UserDid: subjectIdent.DID.String(),
80
+
FollowStatus: models.IsFollowing,
81
})
82
83
return
···
106
// this is not an issue, the firehose event might have already done this
107
}
108
109
s.pages.FollowFragment(w, pages.FollowFragmentParams{
110
+
UserDid: subjectIdent.DID.String(),
111
+
FollowStatus: models.IsNotFollowing,
112
})
113
114
s.notifier.DeleteFollow(r.Context(), follow)
-3
docs/DOCS.md
-3
docs/DOCS.md
-4
input.css
-4
input.css
-3
nix/modules/appview.nix
-3
nix/modules/appview.nix