Skip to content

Commit

Permalink
[chore] Refactor HTML templates and CSS (#2480)
Browse files Browse the repository at this point in the history
* [chore] Refactor HTML templates and CSS

* eslint

* ignore "Local"

* rss tests

* fiddle with OG just a tiny bit

* dick around with polls a bit more so SR stops saying "clickable"

* remove break

* oh lord

* don't lazy load avatar

* fix ogmeta tests

* clean up some cruft

* catch remaining calls to c.HTML

* fix error rendering + stack overflow in tag

* allow templating attributes

* fix indent

* set aria-hidden on status complementary content, since it's already present in the label anyway

* tidy up templating calls a little

* try to make styling a bit more consistent + readable

* fix up some remaining CSS issues

* fix up reports
  • Loading branch information
tsmethurst committed Dec 27, 2023
1 parent 97a1fd9 commit 0ff52b7
Show file tree
Hide file tree
Showing 77 changed files with 3,250 additions and 1,724 deletions.
2 changes: 2 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ builds:
- static_build
- kvformat
- timetzdata
- >-
{{ if and (index .Env "DEBUG") (.Env.DEBUG) }}debugenv{{ end }}
env:
- CGO_ENABLED=0
goos:
Expand Down
30 changes: 19 additions & 11 deletions internal/api/auth/authorize.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,17 +144,25 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
return
}

// the authorize template will display a form to the user where they can get some information
// about the app that's trying to authorize, and the scope of the request.
// They can then approve it if it looks OK to them, which will POST to the AuthorizePOSTHandler
c.HTML(http.StatusOK, "authorize.tmpl", gin.H{
"appname": app.Name,
"appwebsite": app.Website,
"redirect": redirect,
"scope": scope,
"user": acct.Username,
"instance": instance,
})
// The authorize template will display a form
// to the user where they can see some info
// about the app that's trying to authorize,
// and the scope of the request. They can then
// approve it if it looks OK to them, which
// will POST to the AuthorizePOSTHandler.
page := apiutil.WebPage{
Template: "authorize.tmpl",
Instance: instance,
Extra: map[string]any{
"appname": app.Name,
"appwebsite": app.Website,
"redirect": redirect,
"scope": scope,
"user": acct.Username,
},
}

apiutil.TemplateWebPage(c, page)
}

// AuthorizePOSTHandler should be served as POST at https://example.org/oauth/authorize
Expand Down
34 changes: 23 additions & 11 deletions internal/api/auth/callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,17 @@ func (m *Module) CallbackGETHandler(c *gin.Context) {
apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
return
}
c.HTML(http.StatusOK, "finalize.tmpl", gin.H{
"instance": instance,
"name": claims.Name,
"preferredUsername": claims.PreferredUsername,
})

page := apiutil.WebPage{
Template: "finalize.tmpl",
Instance: instance,
Extra: map[string]any{
"name": claims.Name,
"preferredUsername": claims.PreferredUsername,
},
}

apiutil.TemplateWebPage(c, page)
return
}
s.Set(sessionUserID, user.ID)
Expand Down Expand Up @@ -177,12 +183,18 @@ func (m *Module) FinalizePOSTHandler(c *gin.Context) {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
c.HTML(http.StatusOK, "finalize.tmpl", gin.H{
"instance": instance,
"name": form.Name,
"preferredUsername": form.Username,
"error": err,
})

page := apiutil.WebPage{
Template: "finalize.tmpl",
Instance: instance,
Extra: map[string]any{
"name": form.Name,
"preferredUsername": form.Username,
"error": err,
},
}

apiutil.TemplateWebPage(c, page)
}

// check if the username conforms to the spec
Expand Down
18 changes: 11 additions & 7 deletions internal/api/auth/oob.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"context"
"errors"
"fmt"
"net/http"

"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -101,10 +100,15 @@ func (m *Module) OobHandler(c *gin.Context) {
// we're done with the session now, so just clear it out
m.clearSession(s)

c.HTML(http.StatusOK, "oob.tmpl", gin.H{
"instance": instance,
"user": acct.Username,
"oobToken": oobToken,
"scope": scope,
})
page := apiutil.WebPage{
Template: "oob.tmpl",
Instance: instance,
Extra: map[string]any{
"user": acct.Username,
"oobToken": oobToken,
"scope": scope,
},
}

apiutil.TemplateWebPage(c, page)
}
18 changes: 10 additions & 8 deletions internal/api/auth/signin.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ import (
"golang.org/x/crypto/bcrypt"
)

// login just wraps a form-submitted username (we want an email) and password
type login struct {
// signIn just wraps a form-submitted username (we want an email) and password
type signIn struct {
Email string `form:"username"`
Password string `form:"password"`
}
Expand All @@ -55,10 +55,12 @@ func (m *Module) SignInGETHandler(c *gin.Context) {
return
}

// no idp provider, use our own funky little sign in page
c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{
"instance": instance,
})
page := apiutil.WebPage{
Template: "sign-in.tmpl",
Instance: instance,
}

apiutil.TemplateWebPage(c, page)
return
}

Expand All @@ -83,7 +85,7 @@ func (m *Module) SignInGETHandler(c *gin.Context) {
func (m *Module) SignInPOSTHandler(c *gin.Context) {
s := sessions.Default(c)

form := &login{}
form := &signIn{}
if err := c.ShouldBind(form); err != nil {
m.clearSession(s)
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGetV1)
Expand Down Expand Up @@ -129,7 +131,7 @@ func (m *Module) ValidatePassword(ctx context.Context, email string, password st
}

if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(password)); err != nil {
err := fmt.Errorf("password hash didn't match for user %s during login attempt: %s", user.Email, err)
err := fmt.Errorf("password hash didn't match for user %s during sign in attempt: %s", user.Email, err)
return incorrectPassword(err)
}

Expand Down
6 changes: 6 additions & 0 deletions internal/api/model/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ type Status struct {
//
// swagger:ignore
WebPollOptions []WebPollOption `json:"-"`

// Status is from a local account.
// Always false for non-web statuses.
//
// swagger:ignore
Local bool `json:"-"`
}

/*
Expand Down
20 changes: 10 additions & 10 deletions internal/api/util/errorhandling.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ func NotFoundHandler(c *gin.Context, instanceGet func(ctx context.Context) (*api
panic(err)
}

c.HTML(http.StatusNotFound, "404.tmpl", gin.H{
"instance": instance,
"requestID": gtscontext.RequestID(ctx),
})
template404Page(c,
instance,
gtscontext.RequestID(ctx),
)
default:
JSON(c, http.StatusNotFound, map[string]string{
"error": errWithCode.Safe(),
Expand All @@ -73,12 +73,12 @@ func genericErrorHandler(c *gin.Context, instanceGet func(ctx context.Context) (
panic(err)
}

c.HTML(errWithCode.Code(), "error.tmpl", gin.H{
"instance": instance,
"code": errWithCode.Code(),
"error": errWithCode.Safe(),
"requestID": gtscontext.RequestID(ctx),
})
templateErrorPage(c,
instance,
errWithCode.Code(),
errWithCode.Safe(),
gtscontext.RequestID(ctx),
)
default:
JSON(c, errWithCode.Code(), map[string]string{
"error": errWithCode.Safe(),
Expand Down
56 changes: 30 additions & 26 deletions internal/web/opengraph.go → internal/api/util/opengraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package web
package util

import (
"html"
Expand All @@ -28,10 +28,10 @@ import (

const maxOGDescriptionLength = 300

// ogMeta represents supported OpenGraph Meta tags
// OGMeta represents supported OpenGraph Meta tags
//
// see eg https://ogp.me/
type ogMeta struct {
type OGMeta struct {
// vanilla og tags
Title string // og:title
Type string // og:type
Expand All @@ -56,23 +56,23 @@ type ogMeta struct {
ProfileUsername string // profile:username
}

// ogBase returns an *ogMeta suitable for serving at
// OGBase returns an *ogMeta suitable for serving at
// the base root of an instance. It also serves as a
// foundation for building account / status ogMeta on
// top of.
func ogBase(instance *apimodel.InstanceV1) *ogMeta {
func OGBase(instance *apimodel.InstanceV1) *OGMeta {
var locale string
if len(instance.Languages) > 0 {
locale = instance.Languages[0]
}

og := &ogMeta{
og := &OGMeta{
Title: text.SanitizeToPlaintext(instance.Title) + " - GoToSocial",
Type: "website",
Locale: locale,
URL: instance.URI,
SiteName: instance.AccountDomain,
Description: parseDescription(instance.ShortDescription),
Description: ParseDescription(instance.ShortDescription),

Image: instance.Thumbnail,
ImageAlt: instance.ThumbnailDescription,
Expand All @@ -81,15 +81,15 @@ func ogBase(instance *apimodel.InstanceV1) *ogMeta {
return og
}

// withAccount uses the given account to build an ogMeta
// WithAccount uses the given account to build an ogMeta
// struct specific to that account. It's suitable for serving
// at account profile pages.
func (og *ogMeta) withAccount(account *apimodel.Account) *ogMeta {
og.Title = parseTitle(account, og.SiteName)
func (og *OGMeta) WithAccount(account *apimodel.Account) *OGMeta {
og.Title = AccountTitle(account, og.SiteName)
og.Type = "profile"
og.URL = account.URL
if account.Note != "" {
og.Description = parseDescription(account.Note)
og.Description = ParseDescription(account.Note)
} else {
og.Description = `content="This GoToSocial user hasn't written a bio yet!"`
}
Expand All @@ -102,21 +102,21 @@ func (og *ogMeta) withAccount(account *apimodel.Account) *ogMeta {
return og
}

// withStatus uses the given status to build an ogMeta
// WithStatus uses the given status to build an ogMeta
// struct specific to that status. It's suitable for serving
// at status pages.
func (og *ogMeta) withStatus(status *apimodel.Status) *ogMeta {
og.Title = "Post by " + parseTitle(status.Account, og.SiteName)
func (og *OGMeta) WithStatus(status *apimodel.Status) *OGMeta {
og.Title = "Post by " + AccountTitle(status.Account, og.SiteName)
og.Type = "article"
if status.Language != nil {
og.Locale = *status.Language
}
og.URL = status.URL
switch {
case status.SpoilerText != "":
og.Description = parseDescription("CW: " + status.SpoilerText)
og.Description = ParseDescription("CW: " + status.SpoilerText)
case status.Text != "":
og.Description = parseDescription(status.Text)
og.Description = ParseDescription(status.Text)
default:
og.Description = og.Title
}
Expand Down Expand Up @@ -147,34 +147,38 @@ func (og *ogMeta) withStatus(status *apimodel.Status) *ogMeta {
return og
}

// parseTitle parses a page title from account and accountDomain
func parseTitle(account *apimodel.Account, accountDomain string) string {
// AccountTitle parses a page title from account and accountDomain
func AccountTitle(account *apimodel.Account, accountDomain string) string {
user := "@" + account.Acct + "@" + accountDomain

if len(account.DisplayName) == 0 {
return user
}

return account.DisplayName + " (" + user + ")"
return account.DisplayName + ", " + user
}

// parseDescription returns a string description which is
// ParseDescription returns a string description which is
// safe to use as a template.HTMLAttr inside templates.
func parseDescription(in string) string {
func ParseDescription(in string) string {
i := text.SanitizeToPlaintext(in)
i = strings.ReplaceAll(i, "\n", " ")
i = strings.Join(strings.Fields(i), " ")
i = html.EscapeString(i)
i = strings.ReplaceAll(i, `\`, "&bsol;")
i = trim(i, maxOGDescriptionLength)
i = truncate(i, maxOGDescriptionLength)
return `content="` + i + `"`
}

// trim strings trim s to specified length
func trim(s string, length int) string {
if len(s) < length {
// truncate trims given string to
// specified length (in runes).
func truncate(s string, l int) string {
r := []rune(s)
if len(r) < l {
// No need
// to trim.
return s
}

return s[:length]
return string(r[:l]) + "..."
}

0 comments on commit 0ff52b7

Please sign in to comment.