diff --git a/appview/pages/pages.go b/appview/pages/pages.go index d8c9f475..ce43444d 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -178,6 +178,15 @@ func (p *Pages) parseProfileBase(top string) (*template.Template, error) { return p.parse(stack...) } +func (p *Pages) parseLoginBase(top string) (*template.Template, error) { + stack := []string{ + "layouts/base", + "layouts/loginbase", + top, + } + return p.parse(stack...) +} + func (p *Pages) executePlain(name string, w io.Writer, params any) error { tpl, err := p.parse(name) if err != nil { @@ -187,6 +196,15 @@ func (p *Pages) executePlain(name string, w io.Writer, params any) error { return tpl.Execute(w, params) } +func (p *Pages) executeLogin(name string, w io.Writer, params any) error { + tpl, err := p.parseLoginBase(name) + if err != nil { + return err + } + + return tpl.ExecuteTemplate(w, "layouts/base", params) +} + func (p *Pages) execute(name string, w io.Writer, params any) error { tpl, err := p.parseBase(name) if err != nil { @@ -237,7 +255,7 @@ type LoginParams struct { } func (p *Pages) Login(w io.Writer, params LoginParams) error { - return p.executePlain("user/login", w, params) + return p.executeLogin("user/login", w, params) } type SignupParams struct { @@ -245,11 +263,11 @@ type SignupParams struct { } func (p *Pages) Signup(w io.Writer, params SignupParams) error { - return p.executePlain("user/signup", w, params) + return p.executeLogin("user/signup", w, params) } func (p *Pages) CompleteSignup(w io.Writer) error { - return p.executePlain("user/completeSignup", w, nil) + return p.executeLogin("user/completeSignup", w, nil) } type TermsOfServiceParams struct { diff --git a/appview/pages/templates/layouts/loginbase.html b/appview/pages/templates/layouts/loginbase.html new file mode 100644 index 00000000..fd5bb86a --- /dev/null +++ b/appview/pages/templates/layouts/loginbase.html @@ -0,0 +1,26 @@ +{{ define "mainLayout" }} +
+
+ {{ template "logo" }} + {{ block "content" . }}{{ end }} +
+
+{{ end }} + +{{ define "topbarLayout" }} + +{{ end }} + +{{ define "footerLayout" }} + +{{ end }} + +{{ define "logo" }} +

+ {{ template "fragments/logotype" }} +

+

+ tightly-knit social coding. +

+{{ end }} + diff --git a/appview/pages/templates/user/completeSignup.html b/appview/pages/templates/user/completeSignup.html index 1f0e9744..03cb6177 100644 --- a/appview/pages/templates/user/completeSignup.html +++ b/appview/pages/templates/user/completeSignup.html @@ -1,103 +1,66 @@ -{{ define "user/completeSignup" }} - - - - - - - - - - - - complete signup · tangled - - -
-

- {{ template "fragments/logotype" }} -

-

- tightly-knit social coding. -

-
-
- - - - Enter the code sent to your email. - -
+{{ define "title" }} complete signup {{ end }} -
- - - - Your complete handle will be of the form user.tngl.sh. - -
+{{ define "content" }} + +
+ + + + Enter the code sent to your email. + +
-
- - - - Choose a strong password for your account. - -
+
+ + + + Your complete handle will be of the form user.tngl.sh. + +
- -
-

-

-
- - +
+ + + + Choose a strong password for your account. + +
+ + + +

+

{{ end }} diff --git a/appview/pages/templates/user/login.html b/appview/pages/templates/user/login.html index 53f58975..4b045c7f 100644 --- a/appview/pages/templates/user/login.html +++ b/appview/pages/templates/user/login.html @@ -1,138 +1,117 @@ -{{ define "user/login" }} - - - - - - - - - - - - login · tangled - - -
-

- {{ template "fragments/logotype" }} -

-

- tightly-knit social coding. -

+{{ define "title" }} login {{ end }} - {{ if .AddAccount }} -
- {{ i "user-plus" "w-4 h-4" }} -
-
Add another account
-

Sign in with a different account to add it to your account list.

-
-
- {{ end }} +{{ define "content" }} + {{ if .AddAccount }} +
+ {{ i "user-plus" "w-4 h-4" }} +
+
Add another account
+

Sign in with a different account to add it to your account list.

+
+
+ {{ end }} - {{ if and .LoggedInUser .LoggedInUser.Accounts }} - {{ $accounts := .LoggedInUser.Accounts }} - {{ if $accounts }} -
-
- Saved accounts -
-
- {{ range $accounts }} -
- - -
- {{ end }} -
-
- {{ end }} - {{ end }} + {{ if and .LoggedInUser .LoggedInUser.Accounts }} + {{ $accounts := .LoggedInUser.Accounts }} + {{ if $accounts }} +
+
+ Saved accounts +
+
+ {{ range $accounts }} +
+ + +
+ {{ end }} +
+
+ {{ end }} + {{ end }} -
-
- - - - Use your AT Protocol - handle to log in. If you're unsure, this is likely - your Tangled (.tngl.sh) or Bluesky (.bsky.social) account. - -
- - + +
+ + + + Use your AT Protocol + handle to log in. If you're unsure, this is likely + your Tangled (.tngl.sh) or Bluesky (.bsky.social) account. + +
+ + - -
- {{ if .ErrorCode }} -
- {{ i "circle-alert" "w-4 h-4" }} -
-
Login error
-

- {{ if eq .ErrorCode "access_denied" }} - You have not authorized the app. - {{ else if eq .ErrorCode "session" }} - Server failed to create user session. - {{ else if eq .ErrorCode "max_accounts" }} - You have reached the maximum of 20 linked accounts. Please remove an account before adding a new one. - {{ else }} - Internal Server error. - {{ end }} - Please try again. -

-
-
- {{ end }} -

- Don't have an account? Create an account on Tangled now! -

+ + + {{ if .ErrorCode }} +
+ {{ i "circle-alert" "w-4 h-4" }} +
+
Login error
+

+ {{ if eq .ErrorCode "access_denied" }} + You have not authorized the app. + {{ else if eq .ErrorCode "session" }} + Server failed to create user session. + {{ else if eq .ErrorCode "max_accounts" }} + You have reached the maximum of 20 linked accounts. Please remove an account before adding a new one. + {{ else }} + Internal Server error. + {{ end }} + Please try again. +

+
+
+ {{ end }} +

+ Don't have an account? Create an account on Tangled now! +

-

-
- - +

{{ end }} + diff --git a/appview/pages/templates/user/signup.html b/appview/pages/templates/user/signup.html index 520fca86..4bf4370f 100644 --- a/appview/pages/templates/user/signup.html +++ b/appview/pages/templates/user/signup.html @@ -1,63 +1,46 @@ -{{ define "user/signup" }} - - - - - - - - - - - - sign up · tangled +{{ define "title" }} signup {{ end }} - - - -
-

- {{ template "fragments/logotype" }} -

-

tightly-knit social coding.

-
-
- - -
- - You will receive an email with an invite code. Enter your - invite code, desired username, and password in the next - page to complete your registration. - -
-
-
- -

- Already have an AT Protocol account? Login to Tangled. -

+{{ define "extrameta" }} + +{{ end }} + +{{ define "content" }} + +
+ + +
+ + You will receive an email with an invite code. Enter your + invite code, desired username, and password in the next + page to complete your registration. + +
+
+
+ +

+ Already have an AT Protocol account? Login to Tangled. +

-

-

- By signing up, you agree to our Terms of Service and Privacy Policy. -

-
-
- - +

+

+ By signing up, you agree to our Terms of Service and Privacy Policy. +

+ {{ end }} -- 2.51.2 From c2049613392b034666f88e5c2b55d991f0c51040 Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Wed, 28 Jan 2026 09:01:44 +0000 Subject: [PATCH 002/122] appview,xrpc: rewrite RepoUploadBlob to accept content types uploading a blob without the content type results in a 403 ScopeMissingError, but indigo does not let us specify one! rewriting the API to allow specifying the blob mime type is a temporary fix until upstream allows this. Signed-off-by: oppiliappan --- appview/pulls/pulls.go | 13 ++++++++----- appview/repo/artifact.go | 9 +++++---- appview/state/profile.go | 11 ++++++----- xrpc/blob.go | 19 +++++++++++++++++++ 4 files changed, 38 insertions(+), 14 deletions(-) create mode 100644 xrpc/blob.go diff --git a/appview/pulls/pulls.go b/appview/pulls/pulls.go index feef02bb..441a9dd7 100644 --- a/appview/pulls/pulls.go +++ b/appview/pulls/pulls.go @@ -39,6 +39,7 @@ import ( "tangled.org/core/rbac" "tangled.org/core/tid" "tangled.org/core/types" + "tangled.org/core/xrpc" comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/syntax" @@ -48,6 +49,8 @@ import ( "github.com/google/uuid" ) +const ApplicationGzip = "application/gzip" + type Pulls struct { oauth *oauth.OAuth repoResolver *reporesolver.RepoResolver @@ -1227,7 +1230,7 @@ func (s *Pulls) createPullRequest( return } - blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(patch)) + blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(patch), ApplicationGzip) if err != nil { log.Println("failed to upload patch", err) s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") @@ -1321,7 +1324,7 @@ func (s *Pulls) createStackedPullRequest( // apply all record creations at once var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem for _, p := range stack { - blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(p.LatestPatch())) + blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(p.LatestPatch()), ApplicationGzip) if err != nil { log.Println("failed to upload patch blob", err) s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") @@ -1871,7 +1874,7 @@ func (s *Pulls) resubmitPullHelper( return } - blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(patch)) + blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(patch), ApplicationGzip) if err != nil { log.Println("failed to upload patch blob", err) s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") @@ -2014,7 +2017,7 @@ func (s *Pulls) resubmitStackedPullHelper( return } - blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(patch)) + blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(patch), ApplicationGzip) if err != nil { log.Println("failed to upload patch blob", err) s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") @@ -2056,7 +2059,7 @@ func (s *Pulls) resubmitStackedPullHelper( return } - blob, err := comatproto.RepoUploadBlob(r.Context(), client, gz(patch)) + blob, err := xrpc.RepoUploadBlob(r.Context(), client, gz(patch), ApplicationGzip) if err != nil { log.Println("failed to upload patch blob", err) s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") diff --git a/appview/repo/artifact.go b/appview/repo/artifact.go index 365f01f3..d91f9983 100644 --- a/appview/repo/artifact.go +++ b/appview/repo/artifact.go @@ -18,6 +18,7 @@ import ( "tangled.org/core/orm" "tangled.org/core/tid" "tangled.org/core/types" + "tangled.org/core/xrpc" comatproto "github.com/bluesky-social/indigo/api/atproto" lexutil "github.com/bluesky-social/indigo/lex/util" @@ -46,7 +47,7 @@ func (rp *Repo) AttachArtifact(w http.ResponseWriter, r *http.Request) { return } - file, handler, err := r.FormFile("artifact") + file, header, err := r.FormFile("artifact") if err != nil { log.Println("failed to upload artifact", err) rp.pages.Notice(w, "upload", "failed to upload artifact") @@ -61,7 +62,7 @@ func (rp *Repo) AttachArtifact(w http.ResponseWriter, r *http.Request) { return } - uploadBlobResp, err := comatproto.RepoUploadBlob(r.Context(), client, file) + uploadBlobResp, err := xrpc.RepoUploadBlob(r.Context(), client, file, header.Header.Get("Content-Type")) if err != nil { log.Println("failed to upload blob", err) rp.pages.Notice(w, "upload", "Failed to upload blob to your PDS. Try again later.") @@ -81,7 +82,7 @@ func (rp *Repo) AttachArtifact(w http.ResponseWriter, r *http.Request) { Val: &tangled.RepoArtifact{ Artifact: uploadBlobResp.Blob, CreatedAt: createdAt.Format(time.RFC3339), - Name: handler.Filename, + Name: header.Filename, Repo: f.RepoAt().String(), Tag: tag.Tag.Hash[:], }, @@ -110,7 +111,7 @@ func (rp *Repo) AttachArtifact(w http.ResponseWriter, r *http.Request) { Tag: tag.Tag.Hash, CreatedAt: createdAt, BlobCid: cid.Cid(uploadBlobResp.Blob.Ref), - Name: handler.Filename, + Name: header.Filename, Size: uint64(uploadBlobResp.Blob.Size), MimeType: uploadBlobResp.Blob.MimeType, } diff --git a/appview/state/profile.go b/appview/state/profile.go index 0caa38c7..7bc6c4d5 100644 --- a/appview/state/profile.go +++ b/appview/state/profile.go @@ -20,6 +20,7 @@ import ( "tangled.org/core/appview/models" "tangled.org/core/appview/pages" "tangled.org/core/orm" + "tangled.org/core/xrpc" ) func (s *State) Profile(w http.ResponseWriter, r *http.Request) { @@ -741,7 +742,7 @@ func (s *State) UploadProfileAvatar(w http.ResponseWriter, r *http.Request) { return } - file, handler, err := r.FormFile("avatar") + file, header, err := r.FormFile("avatar") if err != nil { l.Error("failed to read avatar file", "err", err) s.pages.Notice(w, "avatar-error", "Failed to read avatar file") @@ -749,13 +750,13 @@ func (s *State) UploadProfileAvatar(w http.ResponseWriter, r *http.Request) { } defer file.Close() - if handler.Size > 1000000 { - l.Warn("avatar file too large", "size", handler.Size) + if header.Size > 1000000 { + l.Warn("avatar file too large", "size", header.Size) s.pages.Notice(w, "avatar-error", "Avatar file too large (max 1MB)") return } - contentType := handler.Header.Get("Content-Type") + contentType := header.Header.Get("Content-Type") if contentType != "image/png" && contentType != "image/jpeg" { l.Warn("invalid image type", "contentType", contentType) s.pages.Notice(w, "avatar-error", "Invalid image type (only PNG and JPEG allowed)") @@ -769,7 +770,7 @@ func (s *State) UploadProfileAvatar(w http.ResponseWriter, r *http.Request) { return } - uploadBlobResp, err := comatproto.RepoUploadBlob(r.Context(), client, file) + uploadBlobResp, err := xrpc.RepoUploadBlob(r.Context(), client, file, header.Header.Get("Content-Type")) if err != nil { l.Error("failed to upload avatar blob", "err", err) s.pages.Notice(w, "avatar-error", "Failed to upload avatar to your PDS") diff --git a/xrpc/blob.go b/xrpc/blob.go new file mode 100644 index 00000000..745341d3 --- /dev/null +++ b/xrpc/blob.go @@ -0,0 +1,19 @@ +package xrpc + +import ( + "context" + "io" + + comatproto "github.com/bluesky-social/indigo/api/atproto" + "github.com/bluesky-social/indigo/lex/util" +) + +// RepoUploadBlob calls the XRPC method "com.atproto.repo.uploadBlob". +func RepoUploadBlob(ctx context.Context, c util.LexClient, input io.Reader, contentType string) (*comatproto.RepoUploadBlob_Output, error) { + var out comatproto.RepoUploadBlob_Output + if err := c.LexDo(ctx, util.Procedure, contentType, "com.atproto.repo.uploadBlob", nil, input, &out); err != nil { + return nil, err + } + + return &out, nil +} -- 2.51.2 From 493f22094b3c531ea4b3cf371fc28e37f26af3b0 Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Wed, 28 Jan 2026 15:17:15 +0000 Subject: [PATCH 003/122] appview/pages: add script to create resizable panels elements marked with `data-resizer` will now have a few event handlers attached to them on load. when mouse operations take place on this element, the target (`data-target`) is resized along the provided axis, the width of the element can be no lesser than `data-min` and no greater than `data-max`. Signed-off-by: oppiliappan --- .../pages/templates/fragments/resizeable.html | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 appview/pages/templates/fragments/resizeable.html diff --git a/appview/pages/templates/fragments/resizeable.html b/appview/pages/templates/fragments/resizeable.html new file mode 100644 index 00000000..b48a0d64 --- /dev/null +++ b/appview/pages/templates/fragments/resizeable.html @@ -0,0 +1,113 @@ +{{ define "fragments/resizable" }} + +{{ end }} -- 2.51.2 From 6fdd37625f33d894d21c21c6ee81164450df7fd4 Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Wed, 28 Jan 2026 15:17:15 +0000 Subject: [PATCH 004/122] appview/pages: add the resizable script to pages that utilize diffs the pulls page includes an additional resizer to mutate the size of the submissions panel. Signed-off-by: oppiliappan --- .../pages/templates/repo/fragments/diff.html | 21 +++++++++++++-- appview/pages/templates/repo/pulls/pull.html | 27 +++++++++++++++---- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/appview/pages/templates/repo/fragments/diff.html b/appview/pages/templates/repo/fragments/diff.html index af614302..a737b297 100644 --- a/appview/pages/templates/repo/fragments/diff.html +++ b/appview/pages/templates/repo/fragments/diff.html @@ -3,12 +3,14 @@ #filesToggle:checked ~ div label[for="filesToggle"] .show-text { display: none; } #filesToggle:checked ~ div label[for="filesToggle"] .hide-text { display: inline; } #filesToggle:not(:checked) ~ div label[for="filesToggle"] .hide-text { display: none; } - #filesToggle:checked ~ div div#files { width: fit-content; max-width: 15vw; margin-right: 1rem; } + #filesToggle:checked ~ div div#files { width: fit-content; max-width: 15vw; } #filesToggle:not(:checked) ~ div div#files { width: 0; display: none; margin-right: 0; } + #filesToggle:not(:checked) ~ div div#resize-files { display: none; } {{ template "diffTopbar" . }} {{ block "diffLayout" . }} {{ end }} + {{ template "fragments/resizable" }} {{ end }} {{ define "diffTopbar" }} @@ -78,6 +80,19 @@ {{ end }} +{{ define "resize-grip" }} + {{ $id := index . 0 }} + {{ $target := index . 1 }} + {{ $direction := index . 2 }} + +{{ end }} + {{ define "diffLayout" }} {{ $diff := index . 0 }} {{ $opts := index . 1 }} @@ -90,8 +105,10 @@ + {{ template "resize-grip" (list "resize-files" "files" "before") }} + -
+
{{ template "diffFiles" (list $diff $opts) }}
diff --git a/appview/pages/templates/repo/pulls/pull.html b/appview/pages/templates/repo/pulls/pull.html index b13d15c2..0f6f5ca9 100644 --- a/appview/pages/templates/repo/pulls/pull.html +++ b/appview/pages/templates/repo/pulls/pull.html @@ -111,6 +111,19 @@ {{ end }} {{ end }} +{{ define "resize-grip" }} + {{ $id := index . 0 }} + {{ $target := index . 1 }} + {{ $direction := index . 2 }} + +{{ end }} + {{ define "diffLayout" }} {{ $diff := index . 0 }} {{ $opts := index . 1 }} @@ -124,11 +137,15 @@
+ {{ template "resize-grip" (list "resize-files" "files" "before") }} + -
+
{{ template "diffFiles" (list $diff $opts) }}
+ {{ template "resize-grip" (list "resize-subs" "subs" "after") }} + {{ template "subsPanel" $ }}
@@ -187,7 +204,6 @@ {{ define "subsToggle" }}