+8
-5
appview/db/issues.go
+8
-5
appview/db/issues.go
···
101
pLower := FilterGte("row_num", page.Offset+1)
102
pUpper := FilterLte("row_num", page.Offset+page.Limit)
103
104
-
args = append(args, pLower.Arg()...)
105
-
args = append(args, pUpper.Arg()...)
106
-
pagination := " where " + pLower.Condition() + " and " + pUpper.Condition()
107
108
query := fmt.Sprintf(
109
`
···
128
%s
129
`,
130
whereClause,
131
-
pagination,
132
)
133
134
rows, err := e.Query(query, args...)
···
244
}
245
246
func GetIssues(e Execer, filters ...filter) ([]models.Issue, error) {
247
-
return GetIssuesPaginated(e, pagination.FirstPage(), filters...)
248
}
249
250
func AddIssueComment(e Execer, c models.IssueComment) (int64, error) {
···
101
pLower := FilterGte("row_num", page.Offset+1)
102
pUpper := FilterLte("row_num", page.Offset+page.Limit)
103
104
+
pageClause := ""
105
+
if page.Limit > 0 {
106
+
args = append(args, pLower.Arg()...)
107
+
args = append(args, pUpper.Arg()...)
108
+
pageClause = " where " + pLower.Condition() + " and " + pUpper.Condition()
109
+
}
110
111
query := fmt.Sprintf(
112
`
···
131
%s
132
`,
133
whereClause,
134
+
pageClause,
135
)
136
137
rows, err := e.Query(query, args...)
···
247
}
248
249
func GetIssues(e Execer, filters ...filter) ([]models.Issue, error) {
250
+
return GetIssuesPaginated(e, pagination.Page{}, filters...)
251
}
252
253
func AddIssueComment(e Execer, c models.IssueComment) (int64, error) {
+7
-4
appview/db/notifications.go
+7
-4
appview/db/notifications.go
···
60
whereClause += " AND " + condition
61
}
62
}
63
64
query := fmt.Sprintf(`
65
select id, recipient_did, actor_did, type, entity_type, entity_id, read, created, repo_id, issue_id, pull_id
66
from notifications
67
%s
68
order by created desc
69
-
limit ? offset ?
70
-
`, whereClause)
71
-
72
-
args = append(args, page.Limit, page.Offset)
73
74
rows, err := e.QueryContext(context.Background(), query, args...)
75
if err != nil {
···
60
whereClause += " AND " + condition
61
}
62
}
63
+
pageClause := ""
64
+
if page.Limit > 0 {
65
+
pageClause = " limit ? offset ? "
66
+
args = append(args, page.Limit, page.Offset)
67
+
}
68
69
query := fmt.Sprintf(`
70
select id, recipient_did, actor_did, type, entity_type, entity_id, read, created, repo_id, issue_id, pull_id
71
from notifications
72
%s
73
order by created desc
74
+
%s
75
+
`, whereClause, pageClause)
76
77
rows, err := e.QueryContext(context.Background(), query, args...)
78
if err != nil {
+1
-5
appview/issues/issues.go
+1
-5
appview/issues/issues.go
+1
-1
appview/middleware/middleware.go
+1
-1
appview/middleware/middleware.go
+1
-5
appview/notifications/notifications.go
+1
-5
appview/notifications/notifications.go
+42
-50
appview/notify/merged_notifier.go
+42
-50
appview/notify/merged_notifier.go
···
2
3
import (
4
"context"
5
6
"tangled.org/core/appview/models"
7
)
···
16
17
var _ Notifier = &mergedNotifier{}
18
19
-
func (m *mergedNotifier) NewRepo(ctx context.Context, repo *models.Repo) {
20
-
for _, notifier := range m.notifiers {
21
-
notifier.NewRepo(ctx, repo)
22
}
23
}
24
25
func (m *mergedNotifier) NewStar(ctx context.Context, star *models.Star) {
26
-
for _, notifier := range m.notifiers {
27
-
notifier.NewStar(ctx, star)
28
-
}
29
}
30
func (m *mergedNotifier) DeleteStar(ctx context.Context, star *models.Star) {
31
-
for _, notifier := range m.notifiers {
32
-
notifier.DeleteStar(ctx, star)
33
-
}
34
}
35
36
func (m *mergedNotifier) NewIssue(ctx context.Context, issue *models.Issue) {
37
-
for _, notifier := range m.notifiers {
38
-
notifier.NewIssue(ctx, issue)
39
-
}
40
}
41
func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) {
42
-
for _, notifier := range m.notifiers {
43
-
notifier.NewIssueComment(ctx, comment)
44
-
}
45
}
46
47
func (m *mergedNotifier) NewIssueClosed(ctx context.Context, issue *models.Issue) {
48
-
for _, notifier := range m.notifiers {
49
-
notifier.NewIssueClosed(ctx, issue)
50
-
}
51
}
52
53
func (m *mergedNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
54
-
for _, notifier := range m.notifiers {
55
-
notifier.NewFollow(ctx, follow)
56
-
}
57
}
58
func (m *mergedNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {
59
-
for _, notifier := range m.notifiers {
60
-
notifier.DeleteFollow(ctx, follow)
61
-
}
62
}
63
64
func (m *mergedNotifier) NewPull(ctx context.Context, pull *models.Pull) {
65
-
for _, notifier := range m.notifiers {
66
-
notifier.NewPull(ctx, pull)
67
-
}
68
}
69
func (m *mergedNotifier) NewPullComment(ctx context.Context, comment *models.PullComment) {
70
-
for _, notifier := range m.notifiers {
71
-
notifier.NewPullComment(ctx, comment)
72
-
}
73
}
74
75
func (m *mergedNotifier) NewPullMerged(ctx context.Context, pull *models.Pull) {
76
-
for _, notifier := range m.notifiers {
77
-
notifier.NewPullMerged(ctx, pull)
78
-
}
79
}
80
81
func (m *mergedNotifier) NewPullClosed(ctx context.Context, pull *models.Pull) {
82
-
for _, notifier := range m.notifiers {
83
-
notifier.NewPullClosed(ctx, pull)
84
-
}
85
}
86
87
func (m *mergedNotifier) UpdateProfile(ctx context.Context, profile *models.Profile) {
88
-
for _, notifier := range m.notifiers {
89
-
notifier.UpdateProfile(ctx, profile)
90
-
}
91
}
92
93
-
func (m *mergedNotifier) NewString(ctx context.Context, string *models.String) {
94
-
for _, notifier := range m.notifiers {
95
-
notifier.NewString(ctx, string)
96
-
}
97
}
98
99
-
func (m *mergedNotifier) EditString(ctx context.Context, string *models.String) {
100
-
for _, notifier := range m.notifiers {
101
-
notifier.EditString(ctx, string)
102
-
}
103
}
104
105
func (m *mergedNotifier) DeleteString(ctx context.Context, did, rkey string) {
106
-
for _, notifier := range m.notifiers {
107
-
notifier.DeleteString(ctx, did, rkey)
108
-
}
109
}
···
2
3
import (
4
"context"
5
+
"reflect"
6
+
"sync"
7
8
"tangled.org/core/appview/models"
9
)
···
18
19
var _ Notifier = &mergedNotifier{}
20
21
+
// fanout calls the same method on all notifiers concurrently
22
+
func (m *mergedNotifier) fanout(method string, args ...any) {
23
+
var wg sync.WaitGroup
24
+
for _, n := range m.notifiers {
25
+
wg.Add(1)
26
+
go func(notifier Notifier) {
27
+
defer wg.Done()
28
+
v := reflect.ValueOf(notifier).MethodByName(method)
29
+
in := make([]reflect.Value, len(args))
30
+
for i, arg := range args {
31
+
in[i] = reflect.ValueOf(arg)
32
+
}
33
+
v.Call(in)
34
+
}(n)
35
}
36
+
wg.Wait()
37
+
}
38
+
39
+
func (m *mergedNotifier) NewRepo(ctx context.Context, repo *models.Repo) {
40
+
m.fanout("NewRepo", ctx, repo)
41
}
42
43
func (m *mergedNotifier) NewStar(ctx context.Context, star *models.Star) {
44
+
m.fanout("NewStar", ctx, star)
45
}
46
+
47
func (m *mergedNotifier) DeleteStar(ctx context.Context, star *models.Star) {
48
+
m.fanout("DeleteStar", ctx, star)
49
}
50
51
func (m *mergedNotifier) NewIssue(ctx context.Context, issue *models.Issue) {
52
+
m.fanout("NewIssue", ctx, issue)
53
}
54
+
55
func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) {
56
+
m.fanout("NewIssueComment", ctx, comment)
57
}
58
59
func (m *mergedNotifier) NewIssueClosed(ctx context.Context, issue *models.Issue) {
60
+
m.fanout("NewIssueClosed", ctx, issue)
61
}
62
63
func (m *mergedNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
64
+
m.fanout("NewFollow", ctx, follow)
65
}
66
+
67
func (m *mergedNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {
68
+
m.fanout("DeleteFollow", ctx, follow)
69
}
70
71
func (m *mergedNotifier) NewPull(ctx context.Context, pull *models.Pull) {
72
+
m.fanout("NewPull", ctx, pull)
73
}
74
+
75
func (m *mergedNotifier) NewPullComment(ctx context.Context, comment *models.PullComment) {
76
+
m.fanout("NewPullComment", ctx, comment)
77
}
78
79
func (m *mergedNotifier) NewPullMerged(ctx context.Context, pull *models.Pull) {
80
+
m.fanout("NewPullMerged", ctx, pull)
81
}
82
83
func (m *mergedNotifier) NewPullClosed(ctx context.Context, pull *models.Pull) {
84
+
m.fanout("NewPullClosed", ctx, pull)
85
}
86
87
func (m *mergedNotifier) UpdateProfile(ctx context.Context, profile *models.Profile) {
88
+
m.fanout("UpdateProfile", ctx, profile)
89
}
90
91
+
func (m *mergedNotifier) NewString(ctx context.Context, s *models.String) {
92
+
m.fanout("NewString", ctx, s)
93
}
94
95
+
func (m *mergedNotifier) EditString(ctx context.Context, s *models.String) {
96
+
m.fanout("EditString", ctx, s)
97
}
98
99
func (m *mergedNotifier) DeleteString(ctx context.Context, did, rkey string) {
100
+
m.fanout("DeleteString", ctx, did, rkey)
101
}
+13
-2
appview/oauth/handler.go
+13
-2
appview/oauth/handler.go
···
4
"bytes"
5
"context"
6
"encoding/json"
7
"fmt"
8
"net/http"
9
"slices"
10
"time"
11
12
"github.com/go-chi/chi/v5"
13
"github.com/lestrrat-go/jwx/v2/jwk"
14
"github.com/posthog/posthog-go"
···
58
59
func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) {
60
ctx := r.Context()
61
62
sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query())
63
if err != nil {
64
-
http.Error(w, err.Error(), http.StatusInternalServerError)
65
return
66
}
67
68
if err := o.SaveSession(w, r, sessData); err != nil {
69
-
http.Error(w, err.Error(), http.StatusInternalServerError)
70
return
71
}
72
···
4
"bytes"
5
"context"
6
"encoding/json"
7
+
"errors"
8
"fmt"
9
"net/http"
10
"slices"
11
"time"
12
13
+
"github.com/bluesky-social/indigo/atproto/auth/oauth"
14
"github.com/go-chi/chi/v5"
15
"github.com/lestrrat-go/jwx/v2/jwk"
16
"github.com/posthog/posthog-go"
···
60
61
func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) {
62
ctx := r.Context()
63
+
l := o.Logger.With("query", r.URL.Query())
64
65
sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query())
66
if err != nil {
67
+
var callbackErr *oauth.AuthRequestCallbackError
68
+
if errors.As(err, &callbackErr) {
69
+
l.Debug("callback error", "err", callbackErr)
70
+
http.Redirect(w, r, fmt.Sprintf("/login?error=%s", callbackErr.ErrorCode), http.StatusFound)
71
+
return
72
+
}
73
+
l.Error("failed to process callback", "err", err)
74
+
http.Redirect(w, r, "/login?error=oauth", http.StatusFound)
75
return
76
}
77
78
if err := o.SaveSession(w, r, sessData); err != nil {
79
+
l.Error("failed to save session", "data", sessData, "err", err)
80
+
http.Redirect(w, r, "/login?error=session", http.StatusFound)
81
return
82
}
83
+4
-1
appview/oauth/oauth.go
+4
-1
appview/oauth/oauth.go
+3
-2
appview/pages/funcmap.go
+3
-2
appview/pages/funcmap.go
···
297
},
298
299
"normalizeForHtmlId": func(s string) string {
300
+
normalized := strings.ReplaceAll(s, ":", "_")
301
+
normalized = strings.ReplaceAll(normalized, ".", "_")
302
+
return normalized
303
},
304
"sshFingerprint": func(pubKey string) string {
305
fp, err := crypto.SSHFingerprint(pubKey)
+1
appview/pages/pages.go
+1
appview/pages/pages.go
+2
-2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
+2
-2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
···
34
35
{{ define "editIssueComment" }}
36
<a
37
-
class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group"
38
hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/edit"
39
hx-swap="outerHTML"
40
hx-target="#comment-body-{{.Comment.Id}}">
···
44
45
{{ define "deleteIssueComment" }}
46
<a
47
-
class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group"
48
hx-delete="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/"
49
hx-confirm="Are you sure you want to delete your comment?"
50
hx-swap="outerHTML"
···
34
35
{{ define "editIssueComment" }}
36
<a
37
+
class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group cursor-pointer"
38
hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/edit"
39
hx-swap="outerHTML"
40
hx-target="#comment-body-{{.Comment.Id}}">
···
44
45
{{ define "deleteIssueComment" }}
46
<a
47
+
class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group cursor-pointer"
48
hx-delete="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/"
49
hx-confirm="Are you sure you want to delete your comment?"
50
hx-swap="outerHTML"
+2
-2
appview/pages/templates/repo/issues/issue.html
+2
-2
appview/pages/templates/repo/issues/issue.html
···
84
85
{{ define "editIssue" }}
86
<a
87
-
class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group"
88
hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/edit"
89
hx-swap="innerHTML"
90
hx-target="#issue-{{.Issue.IssueId}}">
···
94
95
{{ define "deleteIssue" }}
96
<a
97
-
class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group"
98
hx-delete="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/"
99
hx-confirm="Are you sure you want to delete your issue?"
100
hx-swap="none">
···
84
85
{{ define "editIssue" }}
86
<a
87
+
class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group cursor-pointer"
88
hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/edit"
89
hx-swap="innerHTML"
90
hx-target="#issue-{{.Issue.IssueId}}">
···
94
95
{{ define "deleteIssue" }}
96
<a
97
+
class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group cursor-pointer"
98
hx-delete="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/"
99
hx-confirm="Are you sure you want to delete your issue?"
100
hx-swap="none">
+2
appview/pages/templates/repo/settings/access.html
+2
appview/pages/templates/repo/settings/access.html
+2
appview/pages/templates/spindles/fragments/addMemberModal.html
+2
appview/pages/templates/spindles/fragments/addMemberModal.html
+1
-1
appview/pages/templates/strings/string.html
+1
-1
appview/pages/templates/strings/string.html
···
47
</span>
48
</section>
49
<section class="bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full dark:text-white">
50
-
<div class="flex justify-between items-center text-gray-500 dark:text-gray-400 text-sm md:text-base pb-2 mb-3 text-base border-b border-gray-200 dark:border-gray-700">
51
<span>
52
{{ .String.Filename }}
53
<span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span>
···
47
</span>
48
</section>
49
<section class="bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full dark:text-white">
50
+
<div class="flex flex-col md:flex-row md:justify-between md:items-center text-gray-500 dark:text-gray-400 text-sm md:text-base pb-2 mb-3 text-base border-b border-gray-200 dark:border-gray-700">
51
<span>
52
{{ .String.Filename }}
53
<span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span>
+1
-1
appview/pages/templates/user/fragments/followCard.html
+1
-1
appview/pages/templates/user/fragments/followCard.html
···
3
<div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 rounded-sm">
4
<div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex items-center gap-4">
5
<div class="flex-shrink-0 max-h-full w-24 h-24">
6
-
<img class="object-cover rounded-full p-2" src="{{ fullAvatar $userIdent }}" />
7
</div>
8
9
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 w-full">
···
3
<div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 rounded-sm">
4
<div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex items-center gap-4">
5
<div class="flex-shrink-0 max-h-full w-24 h-24">
6
+
<img class="object-cover rounded-full p-2" src="{{ fullAvatar $userIdent }}" alt="{{ $userIdent }}" />
7
</div>
8
9
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 w-full">
+23
-2
appview/pages/templates/user/login.html
+23
-2
appview/pages/templates/user/login.html
···
13
<title>login · tangled</title>
14
</head>
15
<body class="flex items-center justify-center min-h-screen">
16
-
<main class="max-w-md px-6 -mt-4">
17
<h1 class="flex place-content-center text-3xl font-semibold italic dark:text-white" >
18
{{ template "fragments/logotype" }}
19
</h1>
···
21
tightly-knit social coding.
22
</h2>
23
<form
24
-
class="mt-4 max-w-sm mx-auto"
25
hx-post="/login"
26
hx-swap="none"
27
hx-disabled-elt="#login-button"
···
29
<div class="flex flex-col">
30
<label for="handle">handle</label>
31
<input
32
type="text"
33
id="handle"
34
name="handle"
···
53
<span>login</span>
54
</button>
55
</form>
56
<p class="text-sm text-gray-500">
57
Don't have an account? <a href="/signup" class="underline">Create an account</a> on Tangled now!
58
</p>
···
13
<title>login · tangled</title>
14
</head>
15
<body class="flex items-center justify-center min-h-screen">
16
+
<main class="max-w-md px-7 mt-4">
17
<h1 class="flex place-content-center text-3xl font-semibold italic dark:text-white" >
18
{{ template "fragments/logotype" }}
19
</h1>
···
21
tightly-knit social coding.
22
</h2>
23
<form
24
+
class="mt-4"
25
hx-post="/login"
26
hx-swap="none"
27
hx-disabled-elt="#login-button"
···
29
<div class="flex flex-col">
30
<label for="handle">handle</label>
31
<input
32
+
autocapitalize="none"
33
+
autocorrect="off"
34
+
autocomplete="username"
35
type="text"
36
id="handle"
37
name="handle"
···
56
<span>login</span>
57
</button>
58
</form>
59
+
{{ if .ErrorCode }}
60
+
<div class="flex gap-2 my-2 bg-red-50 dark:bg-red-900 border border-red-500 rounded drop-shadow-sm px-3 py-2 text-red-500 dark:text-red-300">
61
+
<span class="py-1">{{ i "circle-alert" "w-4 h-4" }}</span>
62
+
<div>
63
+
<h5 class="font-medium">Login error</h5>
64
+
<p class="text-sm">
65
+
{{ if eq .ErrorCode "access_denied" }}
66
+
You have not authorized the app.
67
+
{{ else if eq .ErrorCode "session" }}
68
+
Server failed to create user session.
69
+
{{ else }}
70
+
Internal Server error.
71
+
{{ end }}
72
+
Please try again.
73
+
</p>
74
+
</div>
75
+
</div>
76
+
{{ end }}
77
<p class="text-sm text-gray-500">
78
Don't have an account? <a href="/signup" class="underline">Create an account</a> on Tangled now!
79
</p>
+23
appview/pagination/page.go
+23
appview/pagination/page.go
···
1
package pagination
2
3
+
import "context"
4
+
5
type Page struct {
6
Offset int // where to start from
7
Limit int // number of items in a page
···
12
Offset: 0,
13
Limit: 30,
14
}
15
+
}
16
+
17
+
type ctxKey struct{}
18
+
19
+
func IntoContext(ctx context.Context, page Page) context.Context {
20
+
return context.WithValue(ctx, ctxKey{}, page)
21
+
}
22
+
23
+
func FromContext(ctx context.Context) Page {
24
+
if ctx == nil {
25
+
return FirstPage()
26
+
}
27
+
v := ctx.Value(ctxKey{})
28
+
if v == nil {
29
+
return FirstPage()
30
+
}
31
+
page, ok := v.(Page)
32
+
if !ok {
33
+
return FirstPage()
34
+
}
35
+
return page
36
}
37
38
func (p Page) Previous() Page {
+1
appview/state/follow.go
+1
appview/state/follow.go
+1
-4
appview/state/gfi.go
+1
-4
appview/state/gfi.go
···
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
+2
appview/state/login.go
+2
appview/state/login.go
+1
-1
spindle/engines/nixery/engine.go
+1
-1
spindle/engines/nixery/engine.go