mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-23 09:46:28 +03:00
[chore] Refactor HTML templates and CSS (#2480)
* [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
This commit is contained in:
parent
97a1fd9a29
commit
0ff52b71f2
77 changed files with 3262 additions and 1736 deletions
|
@ -27,6 +27,8 @@ builds:
|
|||
- static_build
|
||||
- kvformat
|
||||
- timetzdata
|
||||
- >-
|
||||
{{ if and (index .Env "DEBUG") (.Env.DEBUG) }}debugenv{{ end }}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
|
|
|
@ -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{
|
||||
// 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,
|
||||
"instance": instance,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
apiutil.TemplateWebPage(c, page)
|
||||
}
|
||||
|
||||
// AuthorizePOSTHandler should be served as POST at https://example.org/oauth/authorize
|
||||
|
|
|
@ -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,
|
||||
|
||||
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)
|
||||
|
@ -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,
|
||||
|
||||
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
|
||||
|
|
|
@ -21,7 +21,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
@ -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,
|
||||
page := apiutil.WebPage{
|
||||
Template: "oob.tmpl",
|
||||
Instance: instance,
|
||||
Extra: map[string]any{
|
||||
"user": acct.Username,
|
||||
"oobToken": oobToken,
|
||||
"scope": scope,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
apiutil.TemplateWebPage(c, page)
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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:"-"`
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -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(),
|
||||
|
@ -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(),
|
||||
|
|
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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!"`
|
||||
}
|
||||
|
@ -102,11 +102,11 @@ 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
|
||||
|
@ -114,9 +114,9 @@ func (og *ogMeta) withStatus(status *apimodel.Status) *ogMeta {
|
|||
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
|
||||
}
|
||||
|
@ -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, `\`, "\")
|
||||
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]) + "..."
|
||||
}
|
|
@ -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 (
|
||||
"fmt"
|
||||
|
@ -40,18 +40,18 @@ func (suite *OpenGraphTestSuite) TestParseDescription() {
|
|||
for _, tt := range tests {
|
||||
tt := tt
|
||||
suite.Run(tt.name, func() {
|
||||
suite.Equal(fmt.Sprintf("content=\"%s\"", tt.exp), parseDescription(tt.in))
|
||||
suite.Equal(fmt.Sprintf("content=\"%s\"", tt.exp), ParseDescription(tt.in))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *OpenGraphTestSuite) TestWithAccountWithNote() {
|
||||
baseMeta := ogBase(&apimodel.InstanceV1{
|
||||
baseMeta := OGBase(&apimodel.InstanceV1{
|
||||
AccountDomain: "example.org",
|
||||
Languages: []string{"en"},
|
||||
})
|
||||
|
||||
accountMeta := baseMeta.withAccount(&apimodel.Account{
|
||||
accountMeta := baseMeta.WithAccount(&apimodel.Account{
|
||||
Acct: "example_account",
|
||||
DisplayName: "example person!!",
|
||||
URL: "https://example.org/@example_account",
|
||||
|
@ -59,8 +59,8 @@ func (suite *OpenGraphTestSuite) TestWithAccountWithNote() {
|
|||
Username: "example_account",
|
||||
})
|
||||
|
||||
suite.EqualValues(ogMeta{
|
||||
Title: "example person!! (@example_account@example.org)",
|
||||
suite.EqualValues(OGMeta{
|
||||
Title: "example person!!, @example_account@example.org",
|
||||
Type: "profile",
|
||||
Locale: "en",
|
||||
URL: "https://example.org/@example_account",
|
||||
|
@ -79,12 +79,12 @@ func (suite *OpenGraphTestSuite) TestWithAccountWithNote() {
|
|||
}
|
||||
|
||||
func (suite *OpenGraphTestSuite) TestWithAccountNoNote() {
|
||||
baseMeta := ogBase(&apimodel.InstanceV1{
|
||||
baseMeta := OGBase(&apimodel.InstanceV1{
|
||||
AccountDomain: "example.org",
|
||||
Languages: []string{"en"},
|
||||
})
|
||||
|
||||
accountMeta := baseMeta.withAccount(&apimodel.Account{
|
||||
accountMeta := baseMeta.WithAccount(&apimodel.Account{
|
||||
Acct: "example_account",
|
||||
DisplayName: "example person!!",
|
||||
URL: "https://example.org/@example_account",
|
||||
|
@ -92,8 +92,8 @@ func (suite *OpenGraphTestSuite) TestWithAccountNoNote() {
|
|||
Username: "example_account",
|
||||
})
|
||||
|
||||
suite.EqualValues(ogMeta{
|
||||
Title: "example person!! (@example_account@example.org)",
|
||||
suite.EqualValues(OGMeta{
|
||||
Title: "example person!!, @example_account@example.org",
|
||||
Type: "profile",
|
||||
Locale: "en",
|
||||
URL: "https://example.org/@example_account",
|
135
internal/api/util/template.go
Normal file
135
internal/api/util/template.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// 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 util
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
)
|
||||
|
||||
// WebPage encapsulates variables for
|
||||
// rendering an HTML template within
|
||||
// a standard GtS "page" template.
|
||||
type WebPage struct {
|
||||
// Name of the template for rendering
|
||||
// the page. Eg., "example.tmpl".
|
||||
Template string
|
||||
|
||||
// Instance model for rendering header,
|
||||
// footer, and "about" information.
|
||||
Instance *apimodel.InstanceV1
|
||||
|
||||
// OGMeta for rendering page
|
||||
// "meta:og*" tags. Can be nil.
|
||||
OGMeta *OGMeta
|
||||
|
||||
// Paths to CSS files to add to
|
||||
// the page as "stylesheet" entries.
|
||||
// Can be nil.
|
||||
Stylesheets []string
|
||||
|
||||
// Paths to JS files to add to
|
||||
// the page as "script" entries.
|
||||
// Can be nil.
|
||||
Javascript []string
|
||||
|
||||
// Extra parameters to pass to
|
||||
// the template for rendering,
|
||||
// eg., "account": *Account etc.
|
||||
// Can be nil.
|
||||
Extra map[string]any
|
||||
}
|
||||
|
||||
// TemplateWebPage renders the given HTML template and
|
||||
// page params within the standard GtS "page" template.
|
||||
//
|
||||
// ogMeta, stylesheets, javascript, and any extra
|
||||
// properties will be provided to the template if
|
||||
// set, but can all be nil.
|
||||
func TemplateWebPage(
|
||||
c *gin.Context,
|
||||
page WebPage,
|
||||
) {
|
||||
obj := map[string]any{
|
||||
"instance": page.Instance,
|
||||
"ogMeta": page.OGMeta,
|
||||
"stylesheets": page.Stylesheets,
|
||||
"javascript": page.Javascript,
|
||||
}
|
||||
|
||||
for k, v := range page.Extra {
|
||||
obj[k] = v
|
||||
}
|
||||
|
||||
templatePage(c, page.Template, http.StatusOK, obj)
|
||||
}
|
||||
|
||||
// templateErrorPage renders the given
|
||||
// HTTP code, error, and request ID
|
||||
// within the standard error template.
|
||||
func templateErrorPage(
|
||||
c *gin.Context,
|
||||
instance *apimodel.InstanceV1,
|
||||
code int,
|
||||
err string,
|
||||
requestID string,
|
||||
) {
|
||||
const errorTmpl = "error.tmpl"
|
||||
|
||||
obj := map[string]any{
|
||||
"instance": instance,
|
||||
"code": code,
|
||||
"error": err,
|
||||
"requestID": requestID,
|
||||
}
|
||||
|
||||
templatePage(c, errorTmpl, code, obj)
|
||||
}
|
||||
|
||||
// template404Page renders
|
||||
// a standard 404 page.
|
||||
func template404Page(
|
||||
c *gin.Context,
|
||||
instance *apimodel.InstanceV1,
|
||||
requestID string,
|
||||
) {
|
||||
const notFoundTmpl = "404.tmpl"
|
||||
|
||||
obj := map[string]any{
|
||||
"instance": instance,
|
||||
"requestID": requestID,
|
||||
}
|
||||
|
||||
templatePage(c, notFoundTmpl, http.StatusNotFound, obj)
|
||||
}
|
||||
|
||||
// render the given template inside
|
||||
// "page.tmpl" with the provided
|
||||
// code and template object.
|
||||
func templatePage(
|
||||
c *gin.Context,
|
||||
template string,
|
||||
code int,
|
||||
obj map[string]any,
|
||||
) {
|
||||
const pageTmpl = "page.tmpl"
|
||||
obj["pageContent"] = template
|
||||
c.HTML(code, pageTmpl, obj)
|
||||
}
|
|
@ -56,8 +56,8 @@ const (
|
|||
OOBTokenPath = "/oauth/oob" // #nosec G101 else we get a hardcoded credentials warning
|
||||
// HelpfulAdvice is a handy hint to users;
|
||||
// particularly important during the login flow
|
||||
HelpfulAdvice = "If you arrived at this error during a login/oauth flow, please try clearing your session cookies and logging in again; if problems persist, make sure you're using the correct credentials"
|
||||
HelpfulAdviceGrant = "If you arrived at this error during a login/oauth flow, your client is trying to use an unsupported OAuth grant type. Supported grant types are: authorization_code, client_credentials; please reach out to developer of your client"
|
||||
HelpfulAdvice = "If you arrived at this error during a sign in/oauth flow, please try clearing your session cookies and signing in again; if problems persist, make sure you're using the correct credentials"
|
||||
HelpfulAdviceGrant = "If you arrived at this error during a sign in/oauth flow, your client is trying to use an unsupported OAuth grant type. Supported grant types are: authorization_code, client_credentials; please reach out to developer of your client"
|
||||
)
|
||||
|
||||
// Server wraps some oauth2 server functions in an interface, exposing only what is needed
|
||||
|
|
|
@ -39,7 +39,7 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() {
|
|||
|
||||
fmt.Println(feed)
|
||||
|
||||
suite.Equal("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n <channel>\n <title>Posts from @admin@localhost:8080</title>\n <link>http://localhost:8080/@admin</link>\n <description>Posts from @admin@localhost:8080</description>\n <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>\n <lastBuildDate>Wed, 20 Oct 2021 12:36:45 +0000</lastBuildDate>\n <item>\n <title>open to see some puppies</title>\n <link>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</link>\n <description>@admin@localhost:8080 made a new post: "🐕🐕🐕🐕🐕"</description>\n <content:encoded><![CDATA[🐕🐕🐕🐕🐕]]></content:encoded>\n <author>@admin@localhost:8080</author>\n <guid>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</guid>\n <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>\n <source>http://localhost:8080/@admin/feed.rss</source>\n </item>\n <item>\n <title>hello world! #welcome ! first post on the instance :rainbow: !</title>\n <link>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</link>\n <description>@admin@localhost:8080 posted 1 attachment: "hello world! #welcome ! first post on the instance :rainbow: !"</description>\n <content:encoded><![CDATA[hello world! #welcome ! first post on the instance <img src=\"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png\" title=\":rainbow:\" alt=\":rainbow:\" class=\"emoji\"/> !]]></content:encoded>\n <author>@admin@localhost:8080</author>\n <enclosure url=\"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg\" length=\"62529\" type=\"image/jpeg\"></enclosure>\n <guid>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</guid>\n <pubDate>Wed, 20 Oct 2021 11:36:45 +0000</pubDate>\n <source>http://localhost:8080/@admin/feed.rss</source>\n </item>\n </channel>\n</rss>", feed)
|
||||
suite.Equal("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n <channel>\n <title>Posts from @admin@localhost:8080</title>\n <link>http://localhost:8080/@admin</link>\n <description>Posts from @admin@localhost:8080</description>\n <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>\n <lastBuildDate>Wed, 20 Oct 2021 12:36:45 +0000</lastBuildDate>\n <item>\n <title>open to see some puppies</title>\n <link>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</link>\n <description>@admin@localhost:8080 made a new post: "🐕🐕🐕🐕🐕"</description>\n <content:encoded><![CDATA[🐕🐕🐕🐕🐕]]></content:encoded>\n <author>@admin@localhost:8080</author>\n <guid>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</guid>\n <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>\n <source>http://localhost:8080/@admin/feed.rss</source>\n </item>\n <item>\n <title>hello world! #welcome ! first post on the instance :rainbow: !</title>\n <link>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</link>\n <description>@admin@localhost:8080 posted 1 attachment: "hello world! #welcome ! first post on the instance :rainbow: !"</description>\n <content:encoded><![CDATA[hello world! #welcome ! first post on the instance <img src=\"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png\" title=\":rainbow:\" alt=\":rainbow:\" width=\"25\" height=\"25\"/> !]]></content:encoded>\n <author>@admin@localhost:8080</author>\n <enclosure url=\"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg\" length=\"62529\" type=\"image/jpeg\"></enclosure>\n <guid>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</guid>\n <pubDate>Wed, 20 Oct 2021 11:36:45 +0000</pubDate>\n <source>http://localhost:8080/@admin/feed.rss</source>\n </item>\n </channel>\n</rss>", feed)
|
||||
}
|
||||
|
||||
func (suite *GetRSSTestSuite) TestGetAccountRSSZork() {
|
||||
|
|
|
@ -83,7 +83,6 @@ func New(ctx context.Context) (*Router, error) {
|
|||
|
||||
// Attach functions used by HTML templating,
|
||||
// and load HTML templates into the engine.
|
||||
LoadTemplateFunctions(engine)
|
||||
if err := LoadTemplates(engine); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -18,52 +18,121 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/render"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/regexes"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/text"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
const (
|
||||
justTime = "15:04"
|
||||
dateYear = "Jan 02, 2006"
|
||||
dateTime = "Jan 02, 15:04"
|
||||
dateYearTime = "Jan 02, 2006, 15:04"
|
||||
monthYear = "Jan, 2006"
|
||||
badTimestamp = "bad timestamp"
|
||||
)
|
||||
|
||||
// LoadTemplates loads html templates for use by the given engine
|
||||
// LoadTemplates loads templates found at `web-template-base-dir`
|
||||
// into the Gin engine, or errors if templates cannot be loaded.
|
||||
//
|
||||
// The special functions "include" and "includeAttr" will be added
|
||||
// to the template funcMap for use in any template. Use these "include"
|
||||
// functions when you need to pass a template through a pipeline.
|
||||
// Otherwise, prefer the built-in "template" function.
|
||||
func LoadTemplates(engine *gin.Engine) error {
|
||||
templateBaseDir := config.GetWebTemplateBaseDir()
|
||||
if templateBaseDir == "" {
|
||||
return fmt.Errorf("%s cannot be empty and must be a relative or absolute path", config.WebTemplateBaseDirFlag())
|
||||
return gtserror.Newf(
|
||||
"%s cannot be empty and must be a relative or absolute path",
|
||||
config.WebTemplateBaseDirFlag(),
|
||||
)
|
||||
}
|
||||
|
||||
templateBaseDir, err := filepath.Abs(templateBaseDir)
|
||||
templateDirAbs, err := filepath.Abs(templateBaseDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting absolute path of %s: %s", templateBaseDir, err)
|
||||
return gtserror.Newf(
|
||||
"error getting absolute path of web-template-base-dir %s: %w",
|
||||
templateBaseDir, err,
|
||||
)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(templateBaseDir, "index.tmpl")); err != nil {
|
||||
return fmt.Errorf("%s doesn't seem to contain the templates; index.tmpl is missing: %w", templateBaseDir, err)
|
||||
indexTmplPath := filepath.Join(templateDirAbs, "index.tmpl")
|
||||
if _, err := os.Stat(indexTmplPath); err != nil {
|
||||
return gtserror.Newf(
|
||||
"cannot find index.tmpl in web template directory %s: %w",
|
||||
templateDirAbs, err,
|
||||
)
|
||||
}
|
||||
|
||||
engine.LoadHTMLGlob(filepath.Join(templateBaseDir, "*"))
|
||||
// Bring base template into scope.
|
||||
tmpl := template.New("base")
|
||||
|
||||
// Set additional "include" functions to render
|
||||
// provided template name using the base template.
|
||||
funcMap["include"] = func(name string, data any) (template.HTML, error) {
|
||||
var buf strings.Builder
|
||||
err := tmpl.ExecuteTemplate(&buf, name, data)
|
||||
|
||||
// Template was already escaped by
|
||||
// ExecuteTemplate so we can trust it.
|
||||
return noescape(buf.String()), err
|
||||
}
|
||||
|
||||
funcMap["includeAttr"] = func(name string, data any) (template.HTMLAttr, error) {
|
||||
var buf strings.Builder
|
||||
err := tmpl.ExecuteTemplate(&buf, name, data)
|
||||
|
||||
// Template was already escaped by
|
||||
// ExecuteTemplate so we can trust it.
|
||||
return noescapeAttr(buf.String()), err
|
||||
}
|
||||
|
||||
// Load functions into the base template, and
|
||||
// associate other templates with base template.
|
||||
templateGlob := filepath.Join(templateDirAbs, "*")
|
||||
tmpl, err = tmpl.Funcs(funcMap).ParseGlob(templateGlob)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error loading templates: %w", err)
|
||||
}
|
||||
|
||||
// Almost done; teach the
|
||||
// engine how to render.
|
||||
engine.SetFuncMap(funcMap)
|
||||
engine.HTMLRender = render.HTMLProduction{Template: tmpl}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var funcMap = template.FuncMap{
|
||||
"add": add,
|
||||
"acctInstance": acctInstance,
|
||||
"demojify": demojify,
|
||||
"deref": deref,
|
||||
"emojify": emojify,
|
||||
"escape": escape,
|
||||
"increment": increment,
|
||||
"indent": indent,
|
||||
"indentAttr": indentAttr,
|
||||
"isNil": isNil,
|
||||
"outdentPre": outdentPre,
|
||||
"noescapeAttr": noescapeAttr,
|
||||
"noescape": noescape,
|
||||
"oddOrEven": oddOrEven,
|
||||
"subtract": subtract,
|
||||
"timestampPrecise": timestampPrecise,
|
||||
"timestamp": timestamp,
|
||||
"timestampVague": timestampVague,
|
||||
"visibilityIcon": visibilityIcon,
|
||||
}
|
||||
|
||||
func oddOrEven(n int) string {
|
||||
if n%2 == 0 {
|
||||
return "even"
|
||||
|
@ -71,21 +140,40 @@ func oddOrEven(n int) string {
|
|||
return "odd"
|
||||
}
|
||||
|
||||
// escape HTML escapes the given string,
|
||||
// returning a trusted template.
|
||||
func escape(str string) template.HTML {
|
||||
/* #nosec G203 */
|
||||
return template.HTML(template.HTMLEscapeString(str))
|
||||
}
|
||||
|
||||
// noescape marks the given string as a
|
||||
// trusted template. The provided string
|
||||
// MUST have already passed through a
|
||||
// template or escaping function.
|
||||
func noescape(str string) template.HTML {
|
||||
/* #nosec G203 */
|
||||
return template.HTML(str)
|
||||
}
|
||||
|
||||
// noescapeAttr marks the given string as a
|
||||
// trusted HTML attribute. The provided string
|
||||
// MUST have already passed through a template
|
||||
// or escaping function.
|
||||
func noescapeAttr(str string) template.HTMLAttr {
|
||||
/* #nosec G203 */
|
||||
return template.HTMLAttr(str)
|
||||
}
|
||||
|
||||
const (
|
||||
justTime = "15:04"
|
||||
dateYear = "Jan 02, 2006"
|
||||
dateTime = "Jan 02, 15:04"
|
||||
dateYearTime = "Jan 02, 2006, 15:04"
|
||||
monthYear = "Jan, 2006"
|
||||
badTimestamp = "bad timestamp"
|
||||
)
|
||||
|
||||
func timestamp(stamp string) string {
|
||||
t, err := util.ParseISO8601(stamp)
|
||||
if err != nil {
|
||||
|
@ -127,38 +215,55 @@ func timestampVague(stamp string) string {
|
|||
return t.Format(monthYear)
|
||||
}
|
||||
|
||||
type iconWithLabel struct {
|
||||
faIcon string
|
||||
label string
|
||||
}
|
||||
|
||||
func visibilityIcon(visibility apimodel.Visibility) template.HTML {
|
||||
var icon iconWithLabel
|
||||
var (
|
||||
label string
|
||||
icon string
|
||||
)
|
||||
|
||||
switch visibility {
|
||||
case apimodel.VisibilityPublic:
|
||||
icon = iconWithLabel{"globe", "public"}
|
||||
label = "public"
|
||||
icon = "globe"
|
||||
case apimodel.VisibilityUnlisted:
|
||||
icon = iconWithLabel{"unlock", "unlisted"}
|
||||
label = "unlisted"
|
||||
icon = "unlock"
|
||||
case apimodel.VisibilityPrivate:
|
||||
icon = iconWithLabel{"lock", "private"}
|
||||
label = "private"
|
||||
icon = "lock"
|
||||
case apimodel.VisibilityMutualsOnly:
|
||||
icon = iconWithLabel{"handshake-o", "mutuals only"}
|
||||
label = "mutuals-only"
|
||||
icon = "handshake-o"
|
||||
case apimodel.VisibilityDirect:
|
||||
icon = iconWithLabel{"envelope", "direct"}
|
||||
label = "direct"
|
||||
icon = "envelope"
|
||||
}
|
||||
|
||||
/* #nosec G203 */
|
||||
return template.HTML(fmt.Sprintf(`<i aria-label="Visibility: %v" class="fa fa-%v"></i>`, icon.label, icon.faIcon))
|
||||
return template.HTML(fmt.Sprintf(
|
||||
`<i aria-label="Visibility: %s" class="fa fa-%s"></i>`,
|
||||
label, icon,
|
||||
))
|
||||
}
|
||||
|
||||
// text is a template.HTML to affirm that the input of this function is already escaped
|
||||
func emojify(emojis []apimodel.Emoji, inputText template.HTML) template.HTML {
|
||||
out := text.Emojify(emojis, string(inputText))
|
||||
// emojify replaces emojis in the given
|
||||
// html fragment with suitable <img> tags.
|
||||
//
|
||||
// The provided input must have been
|
||||
// escaped / templated already!
|
||||
func emojify(
|
||||
emojis []apimodel.Emoji,
|
||||
html template.HTML,
|
||||
) template.HTML {
|
||||
return text.EmojifyWeb(emojis, html)
|
||||
}
|
||||
|
||||
/* #nosec G203 */
|
||||
// (this is escaped above)
|
||||
return template.HTML(out)
|
||||
// demojify replaces emoji shortcodes in
|
||||
// the given fragment with empty strings.
|
||||
//
|
||||
// Output must then be escaped as appropriate.
|
||||
func demojify(input string) string {
|
||||
return text.Demojify(input)
|
||||
}
|
||||
|
||||
func acctInstance(acct string) string {
|
||||
|
@ -170,10 +275,79 @@ func acctInstance(acct string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// increment adds 1
|
||||
// to the given int.
|
||||
func increment(i int) int {
|
||||
return i + 1
|
||||
}
|
||||
|
||||
// add adds n2 to n1.
|
||||
func add(n1 int, n2 int) int {
|
||||
return n1 + n2
|
||||
}
|
||||
|
||||
// subtract subtracts n2 from n1.
|
||||
func subtract(n1 int, n2 int) int {
|
||||
return n1 - n2
|
||||
}
|
||||
|
||||
var (
|
||||
indentRegex = regexp.MustCompile(`(?m)^`)
|
||||
indentStr = " "
|
||||
indentStrLen = len(indentStr)
|
||||
indents = strings.Repeat(indentStr, 12)
|
||||
indentPre = regexp.MustCompile(fmt.Sprintf(`(?Ums)^((?:%s)+)<pre>.*</pre>`, indentStr))
|
||||
)
|
||||
|
||||
// indent appropriately indents the given html
|
||||
// by prepending each line with the indentStr.
|
||||
func indent(n int, html template.HTML) template.HTML {
|
||||
out := indentRegex.ReplaceAllString(
|
||||
string(html),
|
||||
indents[:n*indentStrLen],
|
||||
)
|
||||
return noescape(out)
|
||||
}
|
||||
|
||||
// indentAttr appropriately indents the given html
|
||||
// attribute by prepending each line with the indentStr.
|
||||
func indentAttr(n int, html template.HTMLAttr) template.HTMLAttr {
|
||||
out := indentRegex.ReplaceAllString(
|
||||
string(html),
|
||||
indents[:n*indentStrLen],
|
||||
)
|
||||
return noescapeAttr(out)
|
||||
}
|
||||
|
||||
// outdentPre outdents all `<pre></pre>` tags in the
|
||||
// given HTML so that they render correctly in code
|
||||
// blocks, even if they were indented before.
|
||||
func outdentPre(html template.HTML) template.HTML {
|
||||
input := string(html)
|
||||
output := regexes.ReplaceAllStringFunc(indentPre, input,
|
||||
func(match string, buf *bytes.Buffer) string {
|
||||
// Reuse the regex to pull out submatches.
|
||||
matches := indentPre.FindAllStringSubmatch(match, -1)
|
||||
if len(matches) != 1 {
|
||||
return match
|
||||
}
|
||||
|
||||
var (
|
||||
indented = matches[0][0]
|
||||
indent = matches[0][1]
|
||||
)
|
||||
|
||||
// Outdent everything in the inner match, add
|
||||
// a newline at the end to make it a bit neater.
|
||||
outdented := strings.ReplaceAll(indented, indent, "")
|
||||
|
||||
// Replace original match with the outdented version.
|
||||
return strings.ReplaceAll(match, indented, outdented)
|
||||
},
|
||||
)
|
||||
return noescape(output)
|
||||
}
|
||||
|
||||
// isNil will safely check if 'v' is nil without
|
||||
// dealing with weird Go interface nil bullshit.
|
||||
func isNil(i interface{}) bool {
|
||||
|
@ -193,21 +367,3 @@ func deref(i any) any {
|
|||
|
||||
return vOf.Elem()
|
||||
}
|
||||
|
||||
func LoadTemplateFunctions(engine *gin.Engine) {
|
||||
engine.SetFuncMap(template.FuncMap{
|
||||
"escape": escape,
|
||||
"noescape": noescape,
|
||||
"noescapeAttr": noescapeAttr,
|
||||
"oddOrEven": oddOrEven,
|
||||
"visibilityIcon": visibilityIcon,
|
||||
"timestamp": timestamp,
|
||||
"timestampVague": timestampVague,
|
||||
"timestampPrecise": timestampPrecise,
|
||||
"emojify": emojify,
|
||||
"acctInstance": acctInstance,
|
||||
"increment": increment,
|
||||
"isNil": isNil,
|
||||
"deref": deref,
|
||||
})
|
||||
}
|
||||
|
|
204
internal/router/template_test.go
Normal file
204
internal/router/template_test.go
Normal file
|
@ -0,0 +1,204 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// 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 router
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOutdentPre(t *testing.T) {
|
||||
const html = template.HTML(`
|
||||
<div class="text">
|
||||
<div class="content" lang="en">
|
||||
<p>Here's a bunch of HTML, read it and weep, weep then!</p>
|
||||
<pre><code class="language-html"><section class="about-user">
|
||||
<div class="col-header">
|
||||
<h2>About</h2>
|
||||
</div>
|
||||
<div class="fields">
|
||||
<h3 class="sr-only">Fields</h3>
|
||||
<dl>
|
||||
<div class="field">
|
||||
<dt>should you follow me?</dt>
|
||||
<dd>maybe!</dd>
|
||||
</div>
|
||||
<div class="field">
|
||||
<dt>age</dt>
|
||||
<dd>120</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="bio">
|
||||
<h3 class="sr-only">Bio</h3>
|
||||
<p>i post about things that concern me</p>
|
||||
</div>
|
||||
<div class="sr-only" role="group">
|
||||
<h3 class="sr-only">Stats</h3>
|
||||
<span>Joined in Jun, 2022.</span>
|
||||
<span>8 posts.</span>
|
||||
<span>Followed by 1.</span>
|
||||
<span>Following 1.</span>
|
||||
</div>
|
||||
<div class="accountstats" aria-hidden="true">
|
||||
<b>Joined</b><time datetime="2022-06-04T13:12:00.000Z">Jun, 2022</time>
|
||||
<b>Posts</b><span>8</span>
|
||||
<b>Followed by</b><span>1</span>
|
||||
<b>Following</b><span>1</span>
|
||||
</div>
|
||||
</section>
|
||||
</code></pre>
|
||||
<p>There, hope you liked that!</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text">
|
||||
<div class="content" lang="en">
|
||||
<p>Here's a bunch of HTML, read it and weep, weep then!</p>
|
||||
<pre><code class="language-html"><section class="about-user">
|
||||
<div class="col-header">
|
||||
<h2>About</h2>
|
||||
</div>
|
||||
<div class="fields">
|
||||
<h3 class="sr-only">Fields</h3>
|
||||
<dl>
|
||||
<div class="field">
|
||||
<dt>should you follow me?</dt>
|
||||
<dd>maybe!</dd>
|
||||
</div>
|
||||
<div class="field">
|
||||
<dt>age</dt>
|
||||
<dd>120</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="bio">
|
||||
<h3 class="sr-only">Bio</h3>
|
||||
<p>i post about things that concern me</p>
|
||||
</div>
|
||||
<div class="sr-only" role="group">
|
||||
<h3 class="sr-only">Stats</h3>
|
||||
<span>Joined in Jun, 2022.</span>
|
||||
<span>8 posts.</span>
|
||||
<span>Followed by 1.</span>
|
||||
<span>Following 1.</span>
|
||||
</div>
|
||||
<div class="accountstats" aria-hidden="true">
|
||||
<b>Joined</b><time datetime="2022-06-04T13:12:00.000Z">Jun, 2022</time>
|
||||
<b>Posts</b><span>8</span>
|
||||
<b>Followed by</b><span>1</span>
|
||||
<b>Following</b><span>1</span>
|
||||
</div>
|
||||
</section>
|
||||
</code></pre>
|
||||
<p>There, hope you liked that!</p>
|
||||
</div>
|
||||
</div>
|
||||
`)
|
||||
|
||||
const expected = template.HTML(`
|
||||
<div class="text">
|
||||
<div class="content" lang="en">
|
||||
<p>Here's a bunch of HTML, read it and weep, weep then!</p>
|
||||
<pre><code class="language-html"><section class="about-user">
|
||||
<div class="col-header">
|
||||
<h2>About</h2>
|
||||
</div>
|
||||
<div class="fields">
|
||||
<h3 class="sr-only">Fields</h3>
|
||||
<dl>
|
||||
<div class="field">
|
||||
<dt>should you follow me?</dt>
|
||||
<dd>maybe!</dd>
|
||||
</div>
|
||||
<div class="field">
|
||||
<dt>age</dt>
|
||||
<dd>120</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="bio">
|
||||
<h3 class="sr-only">Bio</h3>
|
||||
<p>i post about things that concern me</p>
|
||||
</div>
|
||||
<div class="sr-only" role="group">
|
||||
<h3 class="sr-only">Stats</h3>
|
||||
<span>Joined in Jun, 2022.</span>
|
||||
<span>8 posts.</span>
|
||||
<span>Followed by 1.</span>
|
||||
<span>Following 1.</span>
|
||||
</div>
|
||||
<div class="accountstats" aria-hidden="true">
|
||||
<b>Joined</b><time datetime="2022-06-04T13:12:00.000Z">Jun, 2022</time>
|
||||
<b>Posts</b><span>8</span>
|
||||
<b>Followed by</b><span>1</span>
|
||||
<b>Following</b><span>1</span>
|
||||
</div>
|
||||
</section>
|
||||
</code></pre>
|
||||
<p>There, hope you liked that!</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text">
|
||||
<div class="content" lang="en">
|
||||
<p>Here's a bunch of HTML, read it and weep, weep then!</p>
|
||||
<pre><code class="language-html"><section class="about-user">
|
||||
<div class="col-header">
|
||||
<h2>About</h2>
|
||||
</div>
|
||||
<div class="fields">
|
||||
<h3 class="sr-only">Fields</h3>
|
||||
<dl>
|
||||
<div class="field">
|
||||
<dt>should you follow me?</dt>
|
||||
<dd>maybe!</dd>
|
||||
</div>
|
||||
<div class="field">
|
||||
<dt>age</dt>
|
||||
<dd>120</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="bio">
|
||||
<h3 class="sr-only">Bio</h3>
|
||||
<p>i post about things that concern me</p>
|
||||
</div>
|
||||
<div class="sr-only" role="group">
|
||||
<h3 class="sr-only">Stats</h3>
|
||||
<span>Joined in Jun, 2022.</span>
|
||||
<span>8 posts.</span>
|
||||
<span>Followed by 1.</span>
|
||||
<span>Following 1.</span>
|
||||
</div>
|
||||
<div class="accountstats" aria-hidden="true">
|
||||
<b>Joined</b><time datetime="2022-06-04T13:12:00.000Z">Jun, 2022</time>
|
||||
<b>Posts</b><span>8</span>
|
||||
<b>Followed by</b><span>1</span>
|
||||
<b>Following</b><span>1</span>
|
||||
</div>
|
||||
</section>
|
||||
</code></pre>
|
||||
<p>There, hope you liked that!</p>
|
||||
</div>
|
||||
</div>
|
||||
`)
|
||||
|
||||
out := outdentPre(html)
|
||||
if out != expected {
|
||||
t.Fatalf("unexpected output:\n`%s`\n", out)
|
||||
}
|
||||
}
|
|
@ -20,18 +20,76 @@ package text
|
|||
import (
|
||||
"bytes"
|
||||
"html"
|
||||
"html/template"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/regexes"
|
||||
)
|
||||
|
||||
// Emojify replaces shortcodes in `inputText` with the emoji in `emojis`.
|
||||
//
|
||||
// Callers should ensure that inputText and resulting text are escaped
|
||||
// appropriately depending on what they're used for.
|
||||
func Emojify(emojis []apimodel.Emoji, inputText string) string {
|
||||
emojisMap := make(map[string]apimodel.Emoji, len(emojis))
|
||||
// EmojifyWeb replaces emoji shortcodes like `:example:` in the given HTML
|
||||
// fragment with `<img>` tags suitable for rendering on the web frontend.
|
||||
func EmojifyWeb(emojis []apimodel.Emoji, html template.HTML) template.HTML {
|
||||
out := emojify(
|
||||
emojis,
|
||||
string(html),
|
||||
func(url, code string, buf *bytes.Buffer) {
|
||||
buf.WriteString(`<img src="`)
|
||||
buf.WriteString(url)
|
||||
buf.WriteString(`" title=":`)
|
||||
buf.WriteString(code)
|
||||
buf.WriteString(`:" alt=":`)
|
||||
buf.WriteString(code)
|
||||
buf.WriteString(`:" class="emoji" `)
|
||||
// Lazy load emojis when
|
||||
// they scroll into view.
|
||||
buf.WriteString(`loading="lazy" `)
|
||||
// Limit size to avoid showing
|
||||
// huge emojis when unstyled.
|
||||
buf.WriteString(`width="25" height="25"/>`)
|
||||
},
|
||||
)
|
||||
|
||||
// If input was safe,
|
||||
// we can trust output.
|
||||
return template.HTML(out) // #nosec G203
|
||||
}
|
||||
|
||||
// EmojifyRSS replaces emoji shortcodes like `:example:` in the given text
|
||||
// fragment with `<img>` tags suitable for rendering as RSS content.
|
||||
func EmojifyRSS(emojis []apimodel.Emoji, text string) string {
|
||||
return emojify(
|
||||
emojis,
|
||||
text,
|
||||
func(url, code string, buf *bytes.Buffer) {
|
||||
buf.WriteString(`<img src="`)
|
||||
buf.WriteString(url)
|
||||
buf.WriteString(`" title=":`)
|
||||
buf.WriteString(code)
|
||||
buf.WriteString(`:" alt=":`)
|
||||
buf.WriteString(code)
|
||||
buf.WriteString(`:" `)
|
||||
// Limit size to avoid showing
|
||||
// huge emojis in RSS readers.
|
||||
buf.WriteString(`width="25" height="25"/>`)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Demojify replaces emoji shortcodes like `:example:` in the given text
|
||||
// fragment with empty strings, essentially stripping them from the text.
|
||||
// This is useful for text used in OG Meta headers.
|
||||
func Demojify(text string) string {
|
||||
return regexes.EmojiFinder.ReplaceAllString(text, "")
|
||||
}
|
||||
|
||||
func emojify(
|
||||
emojis []apimodel.Emoji,
|
||||
input string,
|
||||
write func(url, code string, buf *bytes.Buffer),
|
||||
) string {
|
||||
// Build map of shortcodes. Normalize each
|
||||
// shortcode by readding closing colons.
|
||||
emojisMap := make(map[string]apimodel.Emoji, len(emojis))
|
||||
for _, emoji := range emojis {
|
||||
shortcode := ":" + emoji.Shortcode + ":"
|
||||
emojisMap[shortcode] = emoji
|
||||
|
@ -39,27 +97,20 @@ func Emojify(emojis []apimodel.Emoji, inputText string) string {
|
|||
|
||||
return regexes.ReplaceAllStringFunc(
|
||||
regexes.EmojiFinder,
|
||||
inputText,
|
||||
input,
|
||||
func(shortcode string, buf *bytes.Buffer) string {
|
||||
// Look for emoji according to this shortcode
|
||||
// Look for emoji with this shortcode.
|
||||
emoji, ok := emojisMap[shortcode]
|
||||
if !ok {
|
||||
return shortcode
|
||||
}
|
||||
|
||||
// Escape raw emoji content
|
||||
safeURL := html.EscapeString(emoji.URL)
|
||||
safeCode := html.EscapeString(emoji.Shortcode)
|
||||
|
||||
// Write HTML emoji repr to buffer
|
||||
buf.WriteString(`<img src="`)
|
||||
buf.WriteString(safeURL)
|
||||
buf.WriteString(`" title=":`)
|
||||
buf.WriteString(safeCode)
|
||||
buf.WriteString(`:" alt=":`)
|
||||
buf.WriteString(safeCode)
|
||||
buf.WriteString(`:" class="emoji"/>`)
|
||||
// Escape raw emoji content.
|
||||
url := html.EscapeString(emoji.URL)
|
||||
code := html.EscapeString(emoji.Shortcode)
|
||||
|
||||
// Write emoji repr to buffer.
|
||||
write(url, code, buf)
|
||||
return buf.String()
|
||||
},
|
||||
)
|
||||
|
|
|
@ -662,6 +662,10 @@ func (c *Converter) StatusToWebStatus(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Whack a newline before and after each "pre" to make it easier to outdent it.
|
||||
webStatus.Content = strings.ReplaceAll(webStatus.Content, "<pre>", "\n<pre>")
|
||||
webStatus.Content = strings.ReplaceAll(webStatus.Content, "</pre>", "</pre>\n")
|
||||
|
||||
// Add additional information for template.
|
||||
// Assume empty langs, hope for not empty language.
|
||||
webStatus.LanguageTag = new(language.Language)
|
||||
|
@ -727,6 +731,8 @@ func (c *Converter) StatusToWebStatus(
|
|||
a.Sensitive = webStatus.Sensitive
|
||||
}
|
||||
|
||||
webStatus.Local = *s.Local
|
||||
|
||||
return webStatus, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -151,7 +151,7 @@ func (c *Converter) StatusToRSSItem(ctx context.Context, s *gtsmodel.Status) (*f
|
|||
apiEmojis = append(apiEmojis, apiEmoji)
|
||||
}
|
||||
}
|
||||
content := text.Emojify(apiEmojis, s.Content)
|
||||
content := text.EmojifyRSS(apiEmojis, s.Content)
|
||||
|
||||
return &feeds.Item{
|
||||
Title: title,
|
||||
|
|
|
@ -81,7 +81,7 @@ func (suite *InternalToRSSTestSuite) TestStatusToRSSItem2() {
|
|||
suite.Equal("62529", item.Enclosure.Length)
|
||||
suite.Equal("image/jpeg", item.Enclosure.Type)
|
||||
suite.Equal("http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg", item.Enclosure.Url)
|
||||
suite.Equal("hello world! #welcome ! first post on the instance <img src=\"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png\" title=\":rainbow:\" alt=\":rainbow:\" class=\"emoji\"/> !", item.Content)
|
||||
suite.Equal("hello world! #welcome ! first post on the instance <img src=\"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png\" title=\":rainbow:\" alt=\":rainbow:\" width=\"25\" height=\"25\"/> !", item.Content)
|
||||
}
|
||||
|
||||
func (suite *InternalToRSSTestSuite) TestStatusToRSSItem3() {
|
||||
|
|
|
@ -18,9 +18,10 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"context"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
|
@ -31,20 +32,35 @@ const (
|
|||
)
|
||||
|
||||
func (m *Module) aboutGETHandler(c *gin.Context) {
|
||||
instance, err := m.processor.InstanceGetV1(c.Request.Context())
|
||||
if err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
|
||||
instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context())
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "about.tmpl", gin.H{
|
||||
"instance": instance,
|
||||
"languages": config.GetInstanceLanguages().DisplayStrs(),
|
||||
"ogMeta": ogBase(instance),
|
||||
// Return instance we already got from the db,
|
||||
// don't try to fetch it again when erroring.
|
||||
instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// We only serve text/html at this endpoint.
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
page := apiutil.WebPage{
|
||||
Template: "about.tmpl",
|
||||
Instance: instance,
|
||||
OGMeta: apiutil.OGBase(instance),
|
||||
Stylesheets: []string{cssAbout},
|
||||
Extra: map[string]any{
|
||||
"showStrap": true,
|
||||
"blocklistExposed": config.GetInstanceExposeSuspendedWeb(),
|
||||
"stylesheets": []string{
|
||||
assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css",
|
||||
"languages": config.GetInstanceLanguages().DisplayStrs(),
|
||||
},
|
||||
"javascript": []string{distPathPrefix + "/frontend.js"},
|
||||
})
|
||||
}
|
||||
|
||||
apiutil.TemplateWebPage(c, page)
|
||||
}
|
||||
|
|
|
@ -18,39 +18,58 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
func (m *Module) confirmEmailGETHandler(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// if there's no token in the query, just serve the 404 web handler
|
||||
token := c.Query(tokenParam)
|
||||
if token == "" {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound))), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
user, errWithCode := m.processor.User().EmailConfirm(ctx, token)
|
||||
instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context())
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
instance, err := m.processor.InstanceGetV1(ctx)
|
||||
if err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
|
||||
// Return instance we already got from the db,
|
||||
// don't try to fetch it again when erroring.
|
||||
instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// We only serve text/html at this endpoint.
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "confirmed.tmpl", gin.H{
|
||||
"instance": instance,
|
||||
// If there's no token in the query,
|
||||
// just serve the 404 web handler.
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
errWithCode := gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound)))
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
user, errWithCode := m.processor.User().EmailConfirm(c.Request.Context(), token)
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
page := apiutil.WebPage{
|
||||
Template: "confirmed.tmpl",
|
||||
Instance: instance,
|
||||
Extra: map[string]any{
|
||||
"email": user.Email,
|
||||
"username": user.Account.Username,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
apiutil.TemplateWebPage(c, page)
|
||||
}
|
||||
|
|
|
@ -18,9 +18,7 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
|
@ -31,31 +29,29 @@ import (
|
|||
const textCSSUTF8 = string(apiutil.TextCSS + "; charset=utf-8")
|
||||
|
||||
func (m *Module) customCSSGETHandler(c *gin.Context) {
|
||||
if !config.GetAccountsAllowCustomCSS() {
|
||||
err := errors.New("accounts-allow-custom-css is not enabled on this instance")
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.TextCSS); err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// usernames on our instance will always be lowercase
|
||||
username := strings.ToLower(c.Param(usernameKey))
|
||||
if username == "" {
|
||||
err := errors.New("no account username specified")
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
customCSS, errWithCode := m.processor.Account().GetCustomCSSForUsername(c.Request.Context(), username)
|
||||
targetUsername, errWithCode := apiutil.ParseWebUsername(c.Param(apiutil.WebUsernameKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve customCSS if enabled on the instance.
|
||||
// Else use an empty string, to help with caching
|
||||
// when custom CSS gets toggled on or off.
|
||||
var customCSS string
|
||||
if config.GetAccountsAllowCustomCSS() {
|
||||
customCSS, errWithCode = m.processor.Account().GetCustomCSSForUsername(c.Request.Context(), targetUsername)
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Header(cacheControlHeader, cacheControlNoCache)
|
||||
c.Data(http.StatusOK, textCSSUTF8, []byte(customCSS))
|
||||
}
|
||||
|
|
|
@ -18,14 +18,14 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -33,37 +33,44 @@ const (
|
|||
)
|
||||
|
||||
func (m *Module) domainBlockListGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, false, false, false, false)
|
||||
if err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !config.GetInstanceExposeSuspendedWeb() && (authed.Account == nil || authed.User == nil) {
|
||||
err := fmt.Errorf("this instance does not expose the list of suspended domains publicly")
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
instance, err := m.processor.InstanceGetV1(c.Request.Context())
|
||||
if err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
domainBlocks, errWithCode := m.processor.InstancePeersGet(c.Request.Context(), true, false, false)
|
||||
instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context())
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "domain-blocklist.tmpl", gin.H{
|
||||
"instance": instance,
|
||||
"ogMeta": ogBase(instance),
|
||||
"blocklist": domainBlocks,
|
||||
"stylesheets": []string{
|
||||
assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css",
|
||||
},
|
||||
"javascript": []string{distPathPrefix + "/frontend.js"},
|
||||
})
|
||||
// Return instance we already got from the db,
|
||||
// don't try to fetch it again when erroring.
|
||||
instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// We only serve text/html at this endpoint.
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if !config.GetInstanceExposeSuspendedWeb() {
|
||||
err := fmt.Errorf("this instance does not publicy expose its blocklist")
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
domainBlocks, errWithCode := m.processor.InstancePeersGet(c.Request.Context(), true, false, false)
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
page := apiutil.WebPage{
|
||||
Template: "domain-blocklist.tmpl",
|
||||
Instance: instance,
|
||||
OGMeta: apiutil.OGBase(instance),
|
||||
Stylesheets: []string{cssFA},
|
||||
Javascript: []string{jsFrontend},
|
||||
Extra: map[string]any{"blocklist": domainBlocks},
|
||||
}
|
||||
|
||||
apiutil.TemplateWebPage(c, page)
|
||||
}
|
||||
|
|
|
@ -18,33 +18,50 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
func (m *Module) baseHandler(c *gin.Context) {
|
||||
// if a landingPageUser is set in the config, redirect to that user's profile
|
||||
func (m *Module) indexHandler(c *gin.Context) {
|
||||
instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context())
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Return instance we already got from the db,
|
||||
// don't try to fetch it again when erroring.
|
||||
instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// We only serve text/html at this endpoint.
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// If a landingPageUser is set in the config, redirect to
|
||||
// that user's profile instead of rendering landing/index page.
|
||||
if landingPageUser := config.GetLandingPageUser(); landingPageUser != "" {
|
||||
c.Redirect(http.StatusFound, "/@"+strings.ToLower(landingPageUser))
|
||||
return
|
||||
}
|
||||
|
||||
instance, err := m.processor.InstanceGetV1(c.Request.Context())
|
||||
if err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
|
||||
return
|
||||
page := apiutil.WebPage{
|
||||
Template: "index.tmpl",
|
||||
Instance: instance,
|
||||
OGMeta: apiutil.OGBase(instance),
|
||||
Stylesheets: []string{cssAbout, cssIndex},
|
||||
Extra: map[string]any{"showStrap": true},
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "index.tmpl", gin.H{
|
||||
"instance": instance,
|
||||
"ogMeta": ogBase(instance),
|
||||
"stylesheets": []string{
|
||||
distPathPrefix + "/index.css",
|
||||
},
|
||||
})
|
||||
apiutil.TemplateWebPage(c, page)
|
||||
}
|
|
@ -27,7 +27,6 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
@ -141,28 +140,28 @@ func (m *Module) profileGETHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
stylesheets := []string{
|
||||
assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css",
|
||||
distPathPrefix + "/status.css",
|
||||
distPathPrefix + "/profile.css",
|
||||
}
|
||||
if config.GetAccountsAllowCustomCSS() {
|
||||
stylesheets = append(stylesheets, "/@"+targetAccount.Username+"/custom.css")
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "profile.tmpl", gin.H{
|
||||
"instance": instance,
|
||||
page := apiutil.WebPage{
|
||||
Template: "profile.tmpl",
|
||||
Instance: instance,
|
||||
OGMeta: apiutil.OGBase(instance).WithAccount(targetAccount),
|
||||
Stylesheets: []string{
|
||||
cssFA, cssStatus, cssThread, cssProfile,
|
||||
// Custom CSS for this user last in cascade.
|
||||
"/@" + targetAccount.Username + "/custom.css",
|
||||
},
|
||||
Javascript: []string{jsFrontend},
|
||||
Extra: map[string]any{
|
||||
"account": targetAccount,
|
||||
"ogMeta": ogBase(instance).withAccount(targetAccount),
|
||||
"rssFeed": rssFeed,
|
||||
"robotsMeta": robotsMeta,
|
||||
"statuses": statusResp.Items,
|
||||
"statuses_next": statusResp.NextLink,
|
||||
"pinned_statuses": pinnedStatuses,
|
||||
"show_back_to_top": paging,
|
||||
"stylesheets": stylesheets,
|
||||
"javascript": []string{distPathPrefix + "/frontend.js"},
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
apiutil.TemplateWebPage(c, page)
|
||||
}
|
||||
|
||||
// returnAPAccount returns an ActivityPub representation of
|
||||
|
|
|
@ -71,7 +71,7 @@ Crawl-delay: 500
|
|||
# API endpoints.
|
||||
Disallow: /api/
|
||||
|
||||
# Auth/login endpoints.
|
||||
# Auth/Sign in endpoints.
|
||||
Disallow: /auth/
|
||||
Disallow: /oauth/
|
||||
Disallow: /check_your_email
|
||||
|
|
|
@ -18,30 +18,44 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"context"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
func (m *Module) SettingsPanelHandler(c *gin.Context) {
|
||||
instance, err := m.processor.InstanceGetV1(c.Request.Context())
|
||||
if err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
|
||||
instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context())
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "frontend.tmpl", gin.H{
|
||||
"instance": instance,
|
||||
"stylesheets": []string{
|
||||
assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css",
|
||||
distPathPrefix + "/_colors.css",
|
||||
distPathPrefix + "/base.css",
|
||||
distPathPrefix + "/profile.css",
|
||||
distPathPrefix + "/status.css",
|
||||
distPathPrefix + "/settings-style.css",
|
||||
// Return instance we already got from the db,
|
||||
// don't try to fetch it again when erroring.
|
||||
instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// We only serve text/html at this endpoint.
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
page := apiutil.WebPage{
|
||||
Template: "frontend.tmpl",
|
||||
Instance: instance,
|
||||
Stylesheets: []string{
|
||||
cssFA,
|
||||
cssProfile, // Used for rendering stub/fake profiles.
|
||||
cssStatus, // Used for rendering stub/fake statuses.
|
||||
cssSettings,
|
||||
},
|
||||
"javascript": []string{distPathPrefix + "/settings.js"},
|
||||
})
|
||||
Javascript: []string{jsSettings},
|
||||
}
|
||||
|
||||
apiutil.TemplateWebPage(c, page)
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ package web
|
|||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
|
@ -56,16 +55,13 @@ func (m *Module) tagGETHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
stylesheets := []string{
|
||||
assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css",
|
||||
distPathPrefix + "/status.css",
|
||||
distPathPrefix + "/tag.css",
|
||||
page := apiutil.WebPage{
|
||||
Template: "tag.tmpl",
|
||||
Instance: instance,
|
||||
OGMeta: apiutil.OGBase(instance),
|
||||
Stylesheets: []string{cssFA, cssThread, cssTag},
|
||||
Extra: map[string]any{"tagName": tagName},
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "tag.tmpl", gin.H{
|
||||
"instance": instance,
|
||||
"ogMeta": ogBase(instance),
|
||||
"tagName": tagName,
|
||||
"stylesheets": stylesheets,
|
||||
})
|
||||
apiutil.TemplateWebPage(c, page)
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
@ -139,22 +138,23 @@ func (m *Module) threadGETHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
stylesheets := []string{
|
||||
assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css",
|
||||
distPathPrefix + "/status.css",
|
||||
}
|
||||
if config.GetAccountsAllowCustomCSS() {
|
||||
stylesheets = append(stylesheets, "/@"+targetUsername+"/custom.css")
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "thread.tmpl", gin.H{
|
||||
"instance": instance,
|
||||
page := apiutil.WebPage{
|
||||
Template: "thread.tmpl",
|
||||
Instance: instance,
|
||||
OGMeta: apiutil.OGBase(instance).WithStatus(status),
|
||||
Stylesheets: []string{
|
||||
cssFA, cssStatus, cssThread,
|
||||
// Custom CSS for this user last in cascade.
|
||||
"/@" + targetUsername + "/custom.css",
|
||||
},
|
||||
Javascript: []string{jsFrontend},
|
||||
Extra: map[string]any{
|
||||
"status": status,
|
||||
"context": context,
|
||||
"ogMeta": ogBase(instance).withStatus(status),
|
||||
"stylesheets": stylesheets,
|
||||
"javascript": []string{distPathPrefix + "/frontend.js"},
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
apiutil.TemplateWebPage(c, page)
|
||||
}
|
||||
|
||||
// returnAPStatus returns an ActivityPub representation of target status,
|
||||
|
|
|
@ -37,7 +37,7 @@ import (
|
|||
|
||||
const (
|
||||
confirmEmailPath = "/" + uris.ConfirmEmailPath
|
||||
profileGroupPath = "/@:" + usernameKey
|
||||
profileGroupPath = "/@:username"
|
||||
statusPath = "/statuses/:" + apiutil.WebStatusIDKey // leave out the '/@:username' prefix as this will be served within the profile group
|
||||
tagsPath = "/tags/:" + apiutil.TagNameKey
|
||||
customCSSPath = profileGroupPath + "/custom.css"
|
||||
|
@ -49,15 +49,24 @@ const (
|
|||
userPanelPath = settingsPathPrefix + "/user"
|
||||
adminPanelPath = settingsPathPrefix + "/admin"
|
||||
|
||||
tokenParam = "token"
|
||||
usernameKey = "username"
|
||||
|
||||
cacheControlHeader = "Cache-Control" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
||||
cacheControlNoCache = "no-cache" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives
|
||||
ifModifiedSinceHeader = "If-Modified-Since" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
|
||||
ifNoneMatchHeader = "If-None-Match" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match
|
||||
eTagHeader = "ETag" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
|
||||
lastModifiedHeader = "Last-Modified" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified
|
||||
|
||||
cssFA = assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css"
|
||||
cssAbout = distPathPrefix + "/about.css"
|
||||
cssIndex = distPathPrefix + "/index.css"
|
||||
cssStatus = distPathPrefix + "/status.css"
|
||||
cssThread = distPathPrefix + "/thread.css"
|
||||
cssProfile = distPathPrefix + "/profile.css"
|
||||
cssSettings = distPathPrefix + "/settings-style.css"
|
||||
cssTag = distPathPrefix + "/tag.css"
|
||||
|
||||
jsFrontend = distPathPrefix + "/frontend.js" // Progressive enhancement frontend JS.
|
||||
jsSettings = distPathPrefix + "/settings.js" // Settings panel React application.
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
|
@ -99,7 +108,7 @@ func (m *Module) Route(r *router.Router, mi ...gin.HandlerFunc) {
|
|||
profileGroup.Handle(http.MethodGet, statusPath, m.threadGETHandler)
|
||||
|
||||
// Attach individual web handlers which require no specific middlewares
|
||||
r.AttachHandler(http.MethodGet, "/", m.baseHandler) // front-page
|
||||
r.AttachHandler(http.MethodGet, "/", m.indexHandler) // front-page
|
||||
r.AttachHandler(http.MethodGet, settingsPathPrefix, m.SettingsPanelHandler)
|
||||
r.AttachHandler(http.MethodGet, settingsPanelGlob, m.SettingsPanelHandler)
|
||||
r.AttachHandler(http.MethodGet, customCSSPath, m.customCSSGETHandler)
|
||||
|
|
|
@ -27,7 +27,6 @@ import (
|
|||
// CreateGinTextContext creates a new gin.Context suitable for a test, with an instantiated gin.Engine.
|
||||
func CreateGinTestContext(rw http.ResponseWriter, r *http.Request) (*gin.Context, *gin.Engine) {
|
||||
ctx, eng := gin.CreateTestContext(rw)
|
||||
router.LoadTemplateFunctions(eng)
|
||||
if err := router.LoadTemplates(eng); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
|
@ -59,6 +59,5 @@ func NewTestRouter(db db.DB) *router.Router {
|
|||
|
||||
// ConfigureTemplatesWithGin will panic on any errors related to template loading during tests
|
||||
func ConfigureTemplatesWithGin(engine *gin.Engine, templatePath string) {
|
||||
router.LoadTemplateFunctions(engine)
|
||||
engine.LoadHTMLGlob(filepath.Join(templatePath, "*"))
|
||||
}
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
node_modules
|
||||
prism.js
|
||||
prism.css
|
|
@ -82,11 +82,11 @@ $button-danger-bg: $error3;
|
|||
$button-danger-fg: $white1;
|
||||
$button-danger-hover-bg: $error2;
|
||||
|
||||
$toot-bg: $gray3;
|
||||
$toot-info-bg: $gray2;
|
||||
$status-bg: $gray3;
|
||||
$status-info-bg: $gray2;
|
||||
|
||||
$toot-focus-bg: $gray5;
|
||||
$toot-focus-info-bg: $gray4;
|
||||
$status-focus-bg: $gray5;
|
||||
$status-focus-info-bg: $gray4;
|
||||
|
||||
$no-img-desc-bg: $orange1;
|
||||
$no-img-desc-fg: $gray1;
|
||||
|
|
39
web/source/css/about.css
Normal file
39
web/source/css/about.css
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
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/>.
|
||||
*/
|
||||
|
||||
.about {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
padding: 2rem;
|
||||
|
||||
background: $bg-accent;
|
||||
box-shadow: $boxshadow;
|
||||
border: $boxshadow-border;
|
||||
border-radius: $br;
|
||||
|
||||
.about-section {
|
||||
ul, ol {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h3, h4 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,17 +38,34 @@
|
|||
url('../fonts/noto-sans-v27-latin-700.woff') format('woff');
|
||||
}
|
||||
|
||||
/* standard border radius for nice squircles */
|
||||
/*************************************
|
||||
***** SECTION 1: HANDY VARIABLES *****
|
||||
**************************************/
|
||||
|
||||
/*
|
||||
Standard border radius
|
||||
for nice squircles.
|
||||
*/
|
||||
$br: 0.4rem;
|
||||
/* border radius for items that are framed/bordered
|
||||
inside something with $br, eg avatar, header img */
|
||||
|
||||
/*
|
||||
Border radius for items that
|
||||
are framed/bordered inside
|
||||
something with $br, eg avatar,
|
||||
header img, etc.
|
||||
*/
|
||||
$br-inner: 0.2rem;
|
||||
|
||||
/* Fork-Awesome 'fa-fw' fixed icon width
|
||||
/*
|
||||
Fork-Awesome 'fa-fw' fixed icon width;
|
||||
keep in sync with https://github.com/ForkAwesome/Fork-Awesome/blob/a99579ae3e735ee70e51ed62dfcee3172b5b2db7/css/fork-awesome.css#L50
|
||||
*/
|
||||
$fa-fw: 1.28571429em;
|
||||
|
||||
/******************************************
|
||||
***** SECTION 2: BASIC GLOBAL STYLING *****
|
||||
*******************************************/
|
||||
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
@ -63,90 +80,28 @@ body {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page {
|
||||
display: grid;
|
||||
min-height: 100vh;
|
||||
|
||||
grid-template-columns: 1fr minmax(auto, 50rem) 1fr;
|
||||
grid-template-columns: 1fr min(92%, 50rem) 1fr;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
line-height: 2.4rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $link-fg;
|
||||
}
|
||||
|
||||
header, footer {
|
||||
grid-column: 1 / span 3;
|
||||
}
|
||||
|
||||
.content {
|
||||
grid-column: 2;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
header a {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 1.5rem;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
align-self: center;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
flex-grow: 1;
|
||||
align-self: center;
|
||||
text-align: center;
|
||||
|
||||
font-size: 1.5rem;
|
||||
word-wrap: anywhere;
|
||||
color: $fg;
|
||||
}
|
||||
}
|
||||
|
||||
.excerpt-top {
|
||||
margin-bottom: 2rem;
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
|
||||
.count {
|
||||
font-weight: bold;
|
||||
color: $fg-accent;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Normalize margins of first and last children.
|
||||
We generally don't want to open a paragraph or
|
||||
paragraph-like element with a top margin or
|
||||
close it with a bottom margin.
|
||||
*/
|
||||
main {
|
||||
p:first-child {
|
||||
p:first-child, ol:first-child, ul:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
p:last-child, ol:last-child, ul:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.button, button {
|
||||
border-radius: 0.2rem;
|
||||
border-radius: $br-inner;
|
||||
color: $button-fg;
|
||||
background: $button-bg;
|
||||
box-shadow: $boxshadow;
|
||||
|
@ -184,6 +139,166 @@ main {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Form styling - used in settings frontend as well.
|
||||
*/
|
||||
input, select, textarea, .input {
|
||||
box-sizing: border-box;
|
||||
border: 0.15rem solid $input-border;
|
||||
border-radius: 0.1rem;
|
||||
color: $fg;
|
||||
background: $input-bg;
|
||||
width: 100%;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
font-size: 1rem;
|
||||
padding: 0.3rem;
|
||||
|
||||
&:focus, &:active {
|
||||
border-color: $input-focus-border;
|
||||
}
|
||||
|
||||
&:invalid, .invalid & {
|
||||
border-color: $input-error-border;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
opacity: 1;
|
||||
color: $fg-reduced
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Squeeze emojis so they fit inline in text.
|
||||
*/
|
||||
.emoji {
|
||||
width: 1.45em;
|
||||
height: 1.45em;
|
||||
margin: -0.2em 0.02em 0;
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
transition: 0.1s;
|
||||
|
||||
/*
|
||||
Enlarge emojis on hover to give
|
||||
viewer a good look at them.
|
||||
*/
|
||||
&:hover, &:active {
|
||||
transform: scale(2);
|
||||
background-color: $bg;
|
||||
box-shadow: $boxshadow;
|
||||
border: $boxshadow-border;
|
||||
border-radius: $br-inner;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Restyle unordered lists; outdent
|
||||
and replace dot with orange dot.
|
||||
*/
|
||||
ul {
|
||||
padding-left: 2.5rem;
|
||||
list-style: none;
|
||||
|
||||
li::before {
|
||||
content: "\2022";
|
||||
color: $border-accent;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
margin-left: -1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Mirror the same styling a little bit
|
||||
for ordered lists by making marker bold.
|
||||
*/
|
||||
ol {
|
||||
padding-left: 2.5rem;
|
||||
|
||||
li::marker {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Outdent block quotes a bit; use
|
||||
orange stripe for left border.
|
||||
*/
|
||||
blockquote {
|
||||
padding: 0.5rem 0 0.5rem 0.5rem;
|
||||
border-left: 0.2rem solid $border-accent;
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/*
|
||||
Nice dashed orange line
|
||||
for horizontal rules.
|
||||
*/
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 1px dashed $border-accent;
|
||||
}
|
||||
|
||||
/*
|
||||
Don't indent definition
|
||||
lists and definitions.
|
||||
*/
|
||||
dl {
|
||||
margin: 0;
|
||||
|
||||
dd {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*************************************
|
||||
***** SECTION 3: UTILITY CLASSES *****
|
||||
**************************************/
|
||||
|
||||
/*
|
||||
Column header that appears at the top
|
||||
of threads, at the top of sections of
|
||||
profiles (About, Pinned Posts, etc).
|
||||
*/
|
||||
.col-header {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 1rem;
|
||||
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
|
||||
margin: 0;
|
||||
background: $profile-bg;
|
||||
border-top-left-radius: $br;
|
||||
border-top-right-radius: $br;
|
||||
padding: 0.75rem;
|
||||
|
||||
a {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.3rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nounderline {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
@ -192,57 +307,37 @@ main {
|
|||
color: $acc1;
|
||||
}
|
||||
|
||||
.logo {
|
||||
justify-self: center;
|
||||
img {
|
||||
height: 30vh;
|
||||
.text-cutoff {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/*
|
||||
Class for lists that don't
|
||||
want the orange dot.
|
||||
*/
|
||||
.nodot {
|
||||
li::before {
|
||||
content: initial;
|
||||
}
|
||||
}
|
||||
|
||||
section.apps {
|
||||
align-self: start;
|
||||
/***********************************
|
||||
***** SECTION 4: SHAMEFUL MESS *****
|
||||
************************************/
|
||||
|
||||
.applist {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 0.5rem;
|
||||
align-content: start;
|
||||
/*
|
||||
EVERYTHING BELOW THIS POINT:
|
||||
Should be moved somewhere else
|
||||
to avoid cluttering up this file.
|
||||
*/
|
||||
|
||||
.entry {
|
||||
display: grid;
|
||||
grid-template-columns: 25% 1fr;
|
||||
gap: 1.5rem;
|
||||
padding: 0.5rem;
|
||||
background: $bg-accent;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
.logo {
|
||||
align-self: center;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.logo.redraw {
|
||||
fill: $fg;
|
||||
stroke: $fg;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div {
|
||||
padding: 0;
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.login {
|
||||
/*
|
||||
Below section stylings are used
|
||||
in transient/error templates.
|
||||
*/
|
||||
section.sign-in {
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -291,98 +386,11 @@ section.oob-token {
|
|||
}
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: $error1;
|
||||
background: $error2;
|
||||
border-radius: 0.1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input, select, textarea, .input {
|
||||
box-sizing: border-box;
|
||||
border: 0.15rem solid $input-border;
|
||||
border-radius: 0.1rem;
|
||||
color: $fg;
|
||||
background: $input-bg;
|
||||
width: 100%;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
font-size: 1rem;
|
||||
padding: 0.3rem;
|
||||
|
||||
&:focus, &:active {
|
||||
border-color: $input-focus-border;
|
||||
}
|
||||
|
||||
&:invalid, .invalid & {
|
||||
border-color: $input-error-border;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
opacity: 1;
|
||||
color: $fg-reduced
|
||||
}
|
||||
|
||||
hr {
|
||||
color: transparent;
|
||||
width: 100%;
|
||||
border-bottom: 0.02rem solid $border-accent;
|
||||
}
|
||||
|
||||
footer {
|
||||
align-self: end;
|
||||
padding: 2rem 0 1rem 0;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
div {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
div {
|
||||
text-align: initial;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
section.apps .applist {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji {
|
||||
width: 1.45em;
|
||||
height: 1.45em;
|
||||
margin: -0.2em 0.02em 0;
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: This is only used in the "finalize"
|
||||
template for new signups; move this elsewhere
|
||||
when that stuff is finished up.
|
||||
*/
|
||||
.callout {
|
||||
margin: 1.5rem 0;
|
||||
border: .05rem solid $border-accent;
|
||||
|
@ -397,22 +405,11 @@ footer {
|
|||
}
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.fa-spin {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.text-cutoff {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: list and blocklist are only used
|
||||
in settings panel and on blocklist page;
|
||||
consider moving them somewhere else.
|
||||
*/
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -495,21 +492,18 @@ label {
|
|||
}
|
||||
}
|
||||
|
||||
.about {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
h2 {
|
||||
margin: 0.5rem 0;
|
||||
@media screen and (max-width: 30rem) {
|
||||
.domain-blocklist .entry {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: this is only used on About
|
||||
page and in settings application;
|
||||
consider moving it somewhere else.
|
||||
*/
|
||||
.account-card {
|
||||
display: inline-grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
|
@ -541,61 +535,3 @@ label {
|
|||
grid-row: 1 / span 2;
|
||||
}
|
||||
}
|
||||
|
||||
.instance-rules {
|
||||
list-style-position: inside;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
a.rule {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
color: $fg;
|
||||
text-decoration: none;
|
||||
background: $toot-bg;
|
||||
padding: 1rem;
|
||||
margin: 0.5rem 0;
|
||||
border-radius: $br;
|
||||
line-height: 2rem;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
color: $fg-accent;
|
||||
|
||||
.edit-icon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
display: none;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: 1.75rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
margin-top: 0 !important;
|
||||
display: inline-block;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
color: $fg-reduced;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 30rem) {
|
||||
.domain-blocklist .entry {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
|
@ -16,26 +16,85 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
header a {
|
||||
margin: 2rem;
|
||||
gap: 2rem;
|
||||
|
||||
img {
|
||||
height: 6rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
/*
|
||||
Render instance title a
|
||||
bit bigger on index page.
|
||||
*/
|
||||
.page-header a h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
main {
|
||||
section {
|
||||
/*
|
||||
Reuse about styling, but rework it
|
||||
to separate sections a bit more.
|
||||
*/
|
||||
.about {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
padding: 0;
|
||||
|
||||
background: initial;
|
||||
box-shadow: initial;
|
||||
border: initial;
|
||||
border-radius: initial;
|
||||
|
||||
.about-section {
|
||||
padding: 2rem;
|
||||
background: $bg-accent;
|
||||
box-shadow: $boxshadow;
|
||||
border: $boxshadow-border;
|
||||
border-radius: $br;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.apps {
|
||||
align-self: start;
|
||||
|
||||
.applist {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 0.5rem;
|
||||
align-content: start;
|
||||
|
||||
.applist-entry {
|
||||
display: grid;
|
||||
grid-template-columns: 25% 1fr;
|
||||
grid-template-areas: "logo text";
|
||||
gap: 1.5rem;
|
||||
padding: 0.5rem;
|
||||
|
||||
.applist-logo {
|
||||
grid-area: logo;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.applist-logo.redraw {
|
||||
fill: $fg;
|
||||
stroke: $fg;
|
||||
}
|
||||
|
||||
.applist-text {
|
||||
grid-area: text;
|
||||
|
||||
a {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.apps .applist {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
107
web/source/css/page.css
Normal file
107
web/source/css/page.css
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
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/>.
|
||||
*/
|
||||
|
||||
.page {
|
||||
display: grid;
|
||||
min-height: 100vh;
|
||||
|
||||
grid-template-columns: 1fr minmax(auto, 50rem) 1fr;
|
||||
grid-template-columns: 1fr min(92%, 50rem) 1fr;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
|
||||
.page-header, .page-footer {
|
||||
grid-column: 1 / span 3;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
grid-column: 2;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
gap: 1rem;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
align-self: center;
|
||||
text-align: center;
|
||||
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.5rem;
|
||||
word-wrap: anywhere;
|
||||
color: $fg;
|
||||
}
|
||||
}
|
||||
|
||||
aside {
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
|
||||
.count {
|
||||
font-weight: bold;
|
||||
color: $fg-accent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-footer {
|
||||
align-self: end;
|
||||
|
||||
nav ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
|
||||
/* Override list styling */
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
|
||||
li {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
flex-grow: 1;
|
||||
|
||||
a {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.page-header {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
5
web/source/css/prism.css
Normal file
5
web/source/css/prism.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
/* PrismJS 1.29.0
|
||||
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+bash+c+csharp+cpp+docker+elixir+erlang+go+go-module+ini+java+json+kotlin+lua+makefile+markup-templating+nginx+nix+perl+php+promql+python+r+jsx+tsx+ruby+rust+scala+sql+swift+typescript&plugins=show-invisibles+show-language+toolbar+copy-to-clipboard */
|
||||
code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
|
||||
.token.cr,.token.lf,.token.space,.token.tab:not(:empty){position:relative}.token.cr:before,.token.lf:before,.token.space:before,.token.tab:not(:empty):before{color:grey;opacity:.6;position:absolute}.token.tab:not(:empty):before{content:'\21E5'}.token.cr:before{content:'\240D'}.token.crlf:before{content:'\240D\240A'}.token.lf:before{content:'\240A'}.token.space:before{content:'\00B7'}
|
||||
div.code-toolbar{position:relative}div.code-toolbar>.toolbar{position:absolute;z-index:10;top:.3em;right:.2em;transition:opacity .3s ease-in-out;opacity:0}div.code-toolbar:hover>.toolbar{opacity:1}div.code-toolbar:focus-within>.toolbar{opacity:1}div.code-toolbar>.toolbar>.toolbar-item{display:inline-block}div.code-toolbar>.toolbar>.toolbar-item>a{cursor:pointer}div.code-toolbar>.toolbar>.toolbar-item>button{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}div.code-toolbar>.toolbar>.toolbar-item>a,div.code-toolbar>.toolbar>.toolbar-item>button,div.code-toolbar>.toolbar>.toolbar-item>span{color:#bbb;font-size:.8em;padding:0 .5em;background:#f5f2f0;background:rgba(224,224,224,.2);box-shadow:0 2px 0 0 rgba(0,0,0,.2);border-radius:.5em}div.code-toolbar>.toolbar>.toolbar-item>a:focus,div.code-toolbar>.toolbar>.toolbar-item>a:hover,div.code-toolbar>.toolbar>.toolbar-item>button:focus,div.code-toolbar>.toolbar>.toolbar-item>button:hover,div.code-toolbar>.toolbar>.toolbar-item>span:focus,div.code-toolbar>.toolbar>.toolbar-item>span:hover{color:inherit;text-decoration:none}
|
|
@ -17,28 +17,27 @@
|
|||
*/
|
||||
|
||||
.page {
|
||||
grid-template-columns: 1fr minmax(auto, 60rem) 1fr; /* fallback for lack of min() support */
|
||||
/*
|
||||
Profile page can be a little wider than default
|
||||
page, since we're using a side-by-side column view.
|
||||
*/
|
||||
grid-template-columns: 1fr minmax(auto, 60rem) 1fr;
|
||||
grid-template-columns: 1fr min(92%, 65rem) 1fr;
|
||||
}
|
||||
|
||||
.profile {
|
||||
padding: 0.5rem;
|
||||
border-radius: $br;
|
||||
|
||||
.column-split {
|
||||
.profile .column-split {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.profile .header {
|
||||
.profile .profile-header {
|
||||
background: $profile-bg;
|
||||
border-radius: $br;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.header-image {
|
||||
.header-image-wrapper {
|
||||
position: relative;
|
||||
padding-top: 33.33%; /* aspect-ratio 1/3 */
|
||||
|
||||
|
@ -55,12 +54,11 @@
|
|||
|
||||
/*
|
||||
Basic info container has the user's avatar, display- and username, and role
|
||||
It's partially overlapped over the header image, by a negative margin-top
|
||||
It's partially overlapped over the header image, by a negative margin-top.
|
||||
*/
|
||||
$avatar-size: 8.5rem;
|
||||
$name-size: 3rem;
|
||||
$username-size: 2rem;
|
||||
|
||||
$overlap: calc($avatar-size - $name-size - $username-size);
|
||||
|
||||
.basic-info {
|
||||
|
@ -71,8 +69,8 @@
|
|||
grid-template-rows: $overlap $name-size auto;
|
||||
grid-template-areas:
|
||||
"avatar . ."
|
||||
"avatar displayname displayname"
|
||||
"avatar username role";
|
||||
"avatar namerole namerole"
|
||||
"avatar namerole namerole";
|
||||
|
||||
margin: 1rem;
|
||||
margin-top: calc(-1 * $overlap);
|
||||
|
@ -93,6 +91,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
.namerole {
|
||||
grid-area: namerole;
|
||||
|
||||
display: grid;
|
||||
gap: 0 1rem;
|
||||
box-sizing: border-box;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: $name-size auto;
|
||||
grid-template-areas:
|
||||
"displayname displayname"
|
||||
"username role";
|
||||
|
||||
.displayname {
|
||||
grid-area: displayname;
|
||||
line-height: $name-size;
|
||||
|
@ -138,15 +148,23 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
.profile .header {
|
||||
.profile .profile-header {
|
||||
.basic-info {
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: $avatar-size $name-size auto;
|
||||
grid-template-areas:
|
||||
"avatar avatar"
|
||||
"namerole namerole"
|
||||
"namerole namerole";
|
||||
|
||||
.namerole {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: $name-size auto;
|
||||
grid-template-areas:
|
||||
"displayname displayname"
|
||||
"username role";
|
||||
|
||||
|
@ -155,42 +173,21 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profile .col-header {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
|
||||
margin: 0;
|
||||
background: $profile-bg;
|
||||
border-top-left-radius: $br;
|
||||
border-top-right-radius: $br;
|
||||
padding: 0.75rem;
|
||||
|
||||
h1, h2 {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.3rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.profile .toots {
|
||||
.profile .statuses-wrapper {
|
||||
flex: 65 25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
min-width: 0%;
|
||||
}
|
||||
|
||||
.col-header {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 1rem;
|
||||
|
||||
a {
|
||||
justify-self: end;
|
||||
}
|
||||
.profile .statuses {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
|
||||
.rss-icon {
|
||||
display: block;
|
||||
|
@ -201,25 +198,14 @@
|
|||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
color: $orange2;
|
||||
/* can't size a single-color background, so we use a linear-gradient that's effectively white */
|
||||
/*
|
||||
Can't size a single-color background, so we use
|
||||
a linear-gradient that's effectively white.
|
||||
*/
|
||||
background: linear-gradient(to right, $white1 100%, transparent 0) no-repeat center center;
|
||||
background-size: 1.2rem 1.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toot {
|
||||
border-radius: 0;
|
||||
|
||||
.info {
|
||||
padding: 0.3rem 0.75rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-left-radius: $br;
|
||||
border-bottom-right-radius: $br;
|
||||
}
|
||||
}
|
||||
|
||||
.backnextlinks {
|
||||
display: flex;
|
||||
|
@ -240,6 +226,10 @@
|
|||
margin-bottom: -0.25rem;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.fields {
|
||||
background: $profile-bg;
|
||||
display: flex;
|
||||
|
|
|
@ -19,25 +19,19 @@
|
|||
@import "photoswipe/dist/photoswipe.css";
|
||||
@import "photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css";
|
||||
@import "plyr/dist/plyr.css";
|
||||
@import "./prism.css";
|
||||
|
||||
main {
|
||||
background: transparent;
|
||||
grid-auto-rows: auto;
|
||||
}
|
||||
|
||||
.thread {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: $br;
|
||||
}
|
||||
|
||||
.toot {
|
||||
background: $toot-bg;
|
||||
.status {
|
||||
background: $status-bg;
|
||||
box-shadow: $boxshadow;
|
||||
border: $boxshadow-border;
|
||||
border-radius: $br;
|
||||
position: relative;
|
||||
margin-bottom: $br;
|
||||
padding-top: 0.75rem;
|
||||
|
||||
a {
|
||||
|
@ -47,15 +41,24 @@ main {
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
.author > a {
|
||||
.status-header > address {
|
||||
/*
|
||||
Avoid stretching so wide that user
|
||||
can't click on open thread link that's
|
||||
behind the status header link.
|
||||
*/
|
||||
width: fit-content;
|
||||
|
||||
> a {
|
||||
padding: 0 0.75rem;
|
||||
display: grid;
|
||||
grid-template-columns: 3.5rem 1fr auto;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-areas:
|
||||
"avatar display date"
|
||||
"avatar user .";
|
||||
"avatar author-strap author-strap"
|
||||
"avatar author-strap author-strap";
|
||||
gap: 0 0.5rem;
|
||||
font-style: normal;
|
||||
|
||||
.avatar {
|
||||
grid-area: avatar;
|
||||
|
@ -75,38 +78,38 @@ main {
|
|||
}
|
||||
}
|
||||
|
||||
.author-strap {
|
||||
grid-area: author-strap;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas:
|
||||
"display display"
|
||||
"user user";
|
||||
gap: 0 0.5rem;
|
||||
|
||||
.displayname, .username {
|
||||
justify-self: start;
|
||||
align-self: start;
|
||||
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 1rem;
|
||||
line-height: 1.3rem;
|
||||
}
|
||||
|
||||
.displayname {
|
||||
grid-area: display;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
line-height: 1.3rem;
|
||||
/* margin-top: -0.5rem; */
|
||||
}
|
||||
|
||||
.username {
|
||||
grid-area: user;
|
||||
color: $link-fg;
|
||||
font-size: 1rem;
|
||||
line-height: 1.3rem;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
grid-area: date;
|
||||
color: $fg-reduced;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
.status-body {
|
||||
padding: 0.5rem 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -157,6 +160,10 @@ main {
|
|||
line-height: 1.6rem;
|
||||
width: 100%;
|
||||
|
||||
/*
|
||||
Normalize header sizes to fit better
|
||||
with the line-height we use for statuses.
|
||||
*/
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
|
@ -187,35 +194,63 @@ main {
|
|||
line-height: initial;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding: 0.5rem 0 0.5rem 0.5rem;
|
||||
border-left: 0.2rem solid $border-accent;
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px dashed $border-accent;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
background-color: $gray2;
|
||||
}
|
||||
|
||||
/*
|
||||
Just code on its own inside status
|
||||
content, ie, `here is some code`.
|
||||
*/
|
||||
code {
|
||||
padding: 0.25rem;
|
||||
border-radius: $br-inner;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
pre {
|
||||
/*
|
||||
Restyle Prism code highlighting toolbar
|
||||
plugin buttons to our own button style.
|
||||
*/
|
||||
.code-toolbar .toolbar {
|
||||
margin-right: 0.5rem;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
|
||||
.toolbar-item {
|
||||
span, button {
|
||||
color: $button-fg;
|
||||
background: $button-bg;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.copy-to-clipboard-button, span {
|
||||
box-shadow: $boxshadow;
|
||||
}
|
||||
|
||||
.copy-to-clipboard-button:hover, .copy-to-clipboard-button:hover span {
|
||||
background: $button-hover-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pre, pre[class*="language-"] {
|
||||
border-radius: $br;
|
||||
padding: 0.5rem;
|
||||
|
||||
code {
|
||||
padding: 0.5rem;
|
||||
white-space: pre;
|
||||
border-radius: 0;
|
||||
overflow-x: auto;
|
||||
|
||||
/*
|
||||
Code inside a pre block, ie.,
|
||||
|
||||
```
|
||||
here is some code
|
||||
```
|
||||
*/
|
||||
code {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
@ -230,18 +265,6 @@ main {
|
|||
}
|
||||
}
|
||||
|
||||
.emoji {
|
||||
transition: 0.1s;
|
||||
}
|
||||
|
||||
.emoji:hover, .emoji:active {
|
||||
transform: scale(2);
|
||||
background-color: $bg;
|
||||
box-shadow: $boxshadow;
|
||||
border: $boxshadow-border;
|
||||
border-radius: $br-inner;
|
||||
}
|
||||
|
||||
.poll {
|
||||
background-color: $gray2;
|
||||
z-index: 2;
|
||||
|
@ -451,41 +474,41 @@ main {
|
|||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
background: $toot-info-bg;
|
||||
.status-info {
|
||||
background: $status-info-bg;
|
||||
color: $fg-reduced;
|
||||
border-top: 0.15rem solid $toot-info-border;
|
||||
border-top: 0.15rem solid $status-info-border;
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
||||
time {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: inline-flex;
|
||||
flex: 1;
|
||||
.status-stats {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
.stats-item {
|
||||
span {
|
||||
white-space: nowrap;
|
||||
.stats-grouping {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 1rem;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.stats-item:not(.published-at) {
|
||||
z-index: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.language {
|
||||
margin-left: auto;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
grid-column: span 3;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toot-link {
|
||||
.status-link {
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
@ -508,15 +531,12 @@ main {
|
|||
/* bottom left, bottom right */
|
||||
border-bottom-left-radius: $br;
|
||||
border-bottom-right-radius: $br;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
background: $toot-focus-bg;
|
||||
padding-bottom: 0;
|
||||
|
||||
.info {
|
||||
background: $toot-focus-info-bg;
|
||||
background: $status-focus-bg;
|
||||
.status-info {
|
||||
background: $status-focus-info-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
56
web/source/css/thread.css
Normal file
56
web/source/css/thread.css
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
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/>.
|
||||
*/
|
||||
|
||||
.thread {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
|
||||
/*
|
||||
This column header might contain
|
||||
quite some info, so let it wrap.
|
||||
*/
|
||||
.col-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 1rem;
|
||||
row-gap: 0.5rem;
|
||||
|
||||
box-shadow: $boxshadow;
|
||||
border: $boxshadow-border;
|
||||
|
||||
h2 {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
border-radius: 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom-left-radius: $br;
|
||||
border-bottom-right-radius: $br;
|
||||
|
||||
.status-info {
|
||||
border-bottom-left-radius: $br;
|
||||
border-bottom-right-radius: $br;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,6 +21,10 @@ const Photoswipe = require("photoswipe/dist/umd/photoswipe.umd.min.js");
|
|||
const PhotoswipeLightbox = require("photoswipe/dist/umd/photoswipe-lightbox.umd.min.js");
|
||||
const PhotoswipeCaptionPlugin = require("photoswipe-dynamic-caption-plugin").default;
|
||||
const Plyr = require("plyr");
|
||||
const Prism = require("./prism.js");
|
||||
|
||||
Prism.manual = true;
|
||||
Prism.highlightAll();
|
||||
|
||||
let [_, _user, type, id] = window.location.pathname.split("/");
|
||||
if (type == "statuses") {
|
||||
|
|
42
web/source/frontend/prism.js
Normal file
42
web/source/frontend/prism.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -140,18 +140,23 @@ function ReportedToot({ toot }) {
|
|||
const account = toot.account;
|
||||
|
||||
return (
|
||||
<article className="toot expanded">
|
||||
<section className="author">
|
||||
<a>
|
||||
<article className="status expanded">
|
||||
<header className="status-header">
|
||||
<address>
|
||||
<a style={{margin: 0}}>
|
||||
<img className="avatar" src={account.avatar} alt="" />
|
||||
<span className="displayname">
|
||||
<dl className="author-strap">
|
||||
<dt className="sr-only">Display name</dt>
|
||||
<dd className="displayname text-cutoff">
|
||||
{account.display_name.trim().length > 0 ? account.display_name : account.username}
|
||||
<span className="sr-only">.</span>
|
||||
</span>
|
||||
<span className="username">@{account.username}</span>
|
||||
</dd>
|
||||
<dt className="sr-only">Username</dt>
|
||||
<dd className="username text-cutoff">@{account.username}</dd>
|
||||
</dl>
|
||||
</a>
|
||||
</section>
|
||||
<section className="body">
|
||||
</address>
|
||||
</header>
|
||||
<section className="status-body">
|
||||
<div className="text">
|
||||
<div className="content">
|
||||
{toot.spoiler_text?.length > 0
|
||||
|
@ -164,8 +169,17 @@ function ReportedToot({ toot }) {
|
|||
<TootMedia media={toot.media_attachments} sensitive={toot.sensitive} />
|
||||
}
|
||||
</section>
|
||||
<aside className="info">
|
||||
<aside className="status-info">
|
||||
<dl class="status-stats">
|
||||
<div class="stats-grouping">
|
||||
<div class="stats-item published-at text-cutoff">
|
||||
<dt class="sr-only">Published</dt>
|
||||
<dd>
|
||||
<time dateTime={toot.created_at}>{new Date(toot.created_at).toLocaleString()}</time>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
</aside>
|
||||
</article>
|
||||
);
|
||||
|
|
|
@ -83,7 +83,7 @@ function ReportEntry({ report }) {
|
|||
<div className="usernames">
|
||||
<Username user={from} link={false} /> reported <Username user={target} link={false} />
|
||||
</div>
|
||||
<h3 className="status">
|
||||
<h3 className="report-status">
|
||||
{report.action_taken ? "Resolved" : "Open"}
|
||||
</h3>
|
||||
</div>
|
||||
|
|
|
@ -22,24 +22,29 @@ const React = require("react");
|
|||
module.exports = function FakeProfile({ avatar, header, display_name, username, role }) {
|
||||
return ( // Keep in sync with web/template/profile.tmpl
|
||||
<div className="profile">
|
||||
<div className="header">
|
||||
<div className="header-image">
|
||||
<div className="profile-header">
|
||||
<div className="header-image-wrapper">
|
||||
<img src={header} alt={header ? `header image for ${username}` : "None set"} />
|
||||
</div>
|
||||
<div className="basic-info" aria-hidden="true">
|
||||
<a className="avatar" href={avatar}>
|
||||
<img src={avatar} alt={avatar ? `avatar image for ${username}` : "None set"} />
|
||||
</a>
|
||||
<span className="displayname text-cutoff">
|
||||
{display_name.trim().length > 0 ? display_name : username}
|
||||
<span className="sr-only">.</span>
|
||||
</span>
|
||||
<span className="username text-cutoff">@{username}</span>
|
||||
{(role && role.name != "user") &&
|
||||
<div className={`role ${role.name}`}>
|
||||
<span className="sr-only">Role: </span>{role.name}
|
||||
</div>
|
||||
<dl className="namerole">
|
||||
<dt className="sr-only">Display name</dt>
|
||||
<dd className="displayname text-cutoff">{display_name.trim().length > 0 ? display_name : username}</dd>
|
||||
<dt className="sr-only">Username</dt>
|
||||
<dd className="username text-cutoff">@{username}</dd>
|
||||
<dt className="sr-only">Role</dt>
|
||||
{
|
||||
(role && role.name != "user") ?
|
||||
<>
|
||||
<dd className="sr-only">Role</dd>
|
||||
<dt className={`role ${role.name}`}>{role.name}</dt>
|
||||
</>
|
||||
: null
|
||||
}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -29,21 +29,28 @@ module.exports = function FakeToot({ children }) {
|
|||
} } = query.useVerifyCredentialsQuery();
|
||||
|
||||
return (
|
||||
<article className="toot expanded">
|
||||
<section className="author">
|
||||
<a>
|
||||
<article className="status expanded">
|
||||
<header className="status-header">
|
||||
<address>
|
||||
<a style={{margin: 0}}>
|
||||
<img className="avatar" src={account.avatar} alt="" />
|
||||
<span className="displayname">
|
||||
<dl className="author-strap">
|
||||
<dt className="sr-only">Display name</dt>
|
||||
<dd className="displayname text-cutoff">
|
||||
{account.display_name.trim().length > 0 ? account.display_name : account.username}
|
||||
<span className="sr-only">.</span>
|
||||
</span>
|
||||
<span className="username">@{account.username}</span>
|
||||
</dd>
|
||||
<dt className="sr-only">Username</dt>
|
||||
<dd className="username text-cutoff">@{account.username}</dd>
|
||||
</dl>
|
||||
</a>
|
||||
</section>
|
||||
<section className="body">
|
||||
</address>
|
||||
</header>
|
||||
<section className="status-body">
|
||||
<div className="text">
|
||||
<div className="content">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
);
|
||||
|
|
|
@ -20,26 +20,14 @@ body {
|
|||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
.content {
|
||||
.page-content {
|
||||
grid-column: 1 / span 3; /* stretch entire width, to fit panel + sidebar nav */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
header {
|
||||
justify-content: start;
|
||||
|
||||
a {
|
||||
margin: 1.5rem;
|
||||
gap: 1rem;
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 3rem;
|
||||
}
|
||||
}
|
||||
/* Don't inherit orange dot from base.css. */
|
||||
ul li::before {
|
||||
content: initial;
|
||||
}
|
||||
|
||||
#root {
|
||||
|
@ -1007,7 +995,7 @@ button.with-padding {
|
|||
grid-template-columns: 1fr auto;
|
||||
gap: 0.5rem;
|
||||
|
||||
.status {
|
||||
.report-status {
|
||||
color: $border-accent;
|
||||
}
|
||||
}
|
||||
|
@ -1029,7 +1017,7 @@ button.with-padding {
|
|||
color: $fg-reduced;
|
||||
border-left: 0.4rem solid $bg;
|
||||
|
||||
.byline .status {
|
||||
.byline .report-status {
|
||||
color: $fg-reduced;
|
||||
}
|
||||
|
||||
|
@ -1141,11 +1129,62 @@ button.with-padding {
|
|||
}
|
||||
}
|
||||
|
||||
.instance-rules {
|
||||
list-style-position: inside;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
a.rule {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
color: $fg;
|
||||
text-decoration: none;
|
||||
background: $status-bg;
|
||||
padding: 1rem;
|
||||
margin: 0.5rem 0;
|
||||
border-radius: $br;
|
||||
line-height: 2rem;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
color: $fg-accent;
|
||||
|
||||
.edit-icon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
display: none;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: 1.75rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
margin-top: 0 !important;
|
||||
display: inline-block;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
color: $fg-reduced;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (orientation: portrait) {
|
||||
.reports .report .byline {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
.status {
|
||||
.report-status {
|
||||
grid-row: 1;
|
||||
}
|
||||
}
|
||||
|
@ -1163,3 +1202,13 @@ button.with-padding {
|
|||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.fa-spin {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.monospace {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
|
|
@ -17,23 +17,27 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
{{ template "header.tmpl" .}}
|
||||
{{- with . }}
|
||||
<main>
|
||||
<section>
|
||||
<h1>404: Page Not Found</h1>
|
||||
<h1>404: Not Found</h1>
|
||||
<p>
|
||||
GoToSocial only serves Public statuses via the web.
|
||||
</p>
|
||||
<p>
|
||||
If you reached this page by clicking on a status link,
|
||||
it's possible that the status is not Public, has been
|
||||
deleted by the author, you don't have permission to see
|
||||
it, or it just doesn't exist at all.
|
||||
it's likely that the status is not Public. You can try
|
||||
entering the status URL in your client's search bar,
|
||||
to view the status from your account. If that doesn't
|
||||
work, it's possible that the status has been deleted by
|
||||
the author, you don't have permission to view it, or it
|
||||
doesn't exist at all.
|
||||
</p>
|
||||
<p>
|
||||
If you believe this 404 was an error, you can contact
|
||||
the instance admin. Provide them with the following request
|
||||
Request ID: <code>{{.requestID}}</code>.
|
||||
the instance admin. Provide them with the following
|
||||
request ID: <code>{{- .requestID -}}</code>.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{{ template "footer.tmpl" .}}
|
||||
{{- end }}
|
|
@ -17,105 +17,133 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
{{ template "header.tmpl" .}}
|
||||
<main>
|
||||
<section class="about">
|
||||
<h1>About</h1>
|
||||
<div>
|
||||
{{.instance.Description |noescape}}
|
||||
</div>
|
||||
{{- define "description" -}}
|
||||
{{- if .instance.Description }}
|
||||
{{ .instance.Description | noescape }}
|
||||
{{- else }}
|
||||
<p>No description has yet been set for this instance.<p>
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
<div>
|
||||
<h2 id="languages">Languages</h2>
|
||||
<p>
|
||||
{{ if .languages }}
|
||||
This instance prefers the following languages:
|
||||
<ol>
|
||||
{{range .languages}}
|
||||
<li>{{.}}</li>
|
||||
{{end}}
|
||||
</ol>
|
||||
{{ else }}
|
||||
This instance does not have any preferred languages.
|
||||
{{ end }}
|
||||
</p>
|
||||
</div>
|
||||
{{- define "registrationLimits" -}}
|
||||
{{- if .instance.Registrations -}}
|
||||
Registration is enabled; new signups can be submitted to this instance.<br/>
|
||||
{{- if .instance.ApprovalRequired -}}
|
||||
Admin approval is required for new registrations.
|
||||
{{- else -}}
|
||||
Admin approval is not required for registrations; new signups will be automatically approved (pending email confirmation).
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
Registration is disabled; new signups are currently closed for this instance.
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
<div>
|
||||
<h2 id="contact">Admin Contact</h2>
|
||||
{{if .instance.ContactAccount}}
|
||||
<a href="{{.instance.ContactAccount.URL}}" class="account-card">
|
||||
<img class="avatar" src="{{.instance.ContactAccount.Avatar}}" alt="" />
|
||||
{{- define "customCSSLimits" -}}
|
||||
{{- if .instance.Configuration.Accounts.AllowCustomCSS -}}
|
||||
Users are allowed to set <a href="https://docs.gotosocial.org/en/latest/user_guide/custom_css/" target="_blank" rel="noopener noreferrer">Custom CSS</a> for their profiles.
|
||||
{{- else -}}
|
||||
<a href="https://docs.gotosocial.org/en/latest/user_guide/custom_css/" target="_blank" rel="noopener noreferrer">Custom CSS</a> is not enabled for user profiles.
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "statusLimits" -}}
|
||||
Statuses can contain up to
|
||||
{{- .instance.Configuration.Statuses.MaxCharacters }} characters, and
|
||||
{{- .instance.Configuration.Statuses.MaxMediaAttachments }} media attachments.
|
||||
{{- end -}}
|
||||
|
||||
{{- define "pollLimits" -}}
|
||||
Polls can have up to
|
||||
{{- .instance.Configuration.Polls.MaxOptions }} options, with
|
||||
{{- .instance.Configuration.Polls.MaxCharactersPerOption }} characters per option.
|
||||
{{- end -}}
|
||||
|
||||
{{- with . }}
|
||||
<main class="about">
|
||||
<section class="about-section" role="region" aria-labelledby="about">
|
||||
<h3 id="about">About {{ .instance.Title -}}</h3>
|
||||
{{- with . }}
|
||||
{{- include "description" . | indent 2 }}
|
||||
{{- end }}
|
||||
</section>
|
||||
<section class="about-section" role="region" aria-labelledby="contact">
|
||||
<h3 id="contact">Admin Contact</h3>
|
||||
{{- if .instance.ContactAccount }}
|
||||
<a href="{{- .instance.ContactAccount.URL -}}" class="account-card">
|
||||
<img class="avatar" src="{{- .instance.ContactAccount.Avatar -}}" alt=""/>
|
||||
<h3>
|
||||
{{if .instance.ContactAccount.DisplayName}}{{emojify .instance.ContactAccount.Emojis (escape .instance.ContactAccount.DisplayName)}}{{else}}{{.instance.ContactAccount.Username}}{{end}}
|
||||
{{- if .instance.ContactAccount.DisplayName -}}
|
||||
{{- emojify .instance.ContactAccount.Emojis (escape .instance.ContactAccount.DisplayName) -}}
|
||||
{{- else -}}
|
||||
{{- .instance.ContactAccount.Username -}}
|
||||
{{- end -}}
|
||||
</h3>
|
||||
<span>@{{.instance.ContactAccount.Username}}</span>
|
||||
</a><br />
|
||||
{{end}}
|
||||
{{if .instance.Email}}
|
||||
Email: <a href="mailto:{{.instance.Email}}">{{.instance.Email}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 id="rules">Rules</h2>
|
||||
<span>@{{- .instance.ContactAccount.Username -}}</span>
|
||||
</a>
|
||||
{{- else }}
|
||||
<p>This instance has not yet set a contact account.</p>
|
||||
{{- end }}
|
||||
{{- if .instance.Email }}
|
||||
<p>Email: <a href="mailto:{{- .instance.Email -}}">{{- .instance.Email -}}</a></p>
|
||||
{{- else }}
|
||||
<p>This instance has not yet set a contact email address.</p>
|
||||
{{- end }}
|
||||
</section>
|
||||
<section class="about-section" role="region" aria-labelledby="languages">
|
||||
<h3 id="languages">Languages</h3>
|
||||
{{- if .languages }}
|
||||
<p>This instance prefers the following languages:</p>
|
||||
<ol>
|
||||
{{range .instance.Rules}}
|
||||
<li>{{.Text}}</li>
|
||||
{{end}}
|
||||
{{- range .languages }}
|
||||
<li>{{- . -}}</li>
|
||||
{{- end }}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 id="features">Features</h2>
|
||||
{{- else }}
|
||||
<p>This instance does not have any preferred languages.</p>
|
||||
{{- end }}
|
||||
</section>
|
||||
<section class="about-section" role="region" aria-labelledby="rules">
|
||||
<h3 id="rules">Instance Rules</h3>
|
||||
<p>This instance has the following rules:</p>
|
||||
{{- if .instance.Rules }}
|
||||
<ol>
|
||||
{{- range .instance.Rules }}
|
||||
<li>{{- .Text -}}</li>
|
||||
{{- end }}
|
||||
</ol>
|
||||
{{- else }}
|
||||
<p>This instance has not yet set any rules.</p>
|
||||
{{- end }}
|
||||
</section>
|
||||
<section class="about-section" role="region" aria-labelledby="features">
|
||||
<h3 id="features">Instance Features</h3>
|
||||
<ul>
|
||||
<li>
|
||||
Registration is
|
||||
{{if .instance.Registrations}}
|
||||
enabled{{if .instance.ApprovalRequired}}, but requires admin approval{{end}}.
|
||||
{{else}}
|
||||
disabled.
|
||||
{{end}}
|
||||
</li>
|
||||
{{if .instance.Configuration.Accounts.AllowCustomCSS}}
|
||||
<li>
|
||||
Users are allowed to set <a href="https://docs.gotosocial.org/en/latest/user_guide/custom_css/"
|
||||
target="_blank" rel="noopener noreferrer">Custom CSS</a> for their profiles.
|
||||
</li>
|
||||
{{end}}
|
||||
<li>
|
||||
Toots can contain up to {{.instance.Configuration.Statuses.MaxCharacters}} characters and
|
||||
{{.instance.Configuration.Statuses.MaxMediaAttachments}} media attachments.
|
||||
</li>
|
||||
<li>
|
||||
Polls can have up to {{.instance.Configuration.Polls.MaxOptions}} options, with
|
||||
{{.instance.Configuration.Polls.MaxCharactersPerOption}} characters each.
|
||||
</li>
|
||||
<li>{{- template "registrationLimits" . -}}</li>
|
||||
<li>{{- template "customCSSLimits" . -}}</li>
|
||||
<li>{{- template "statusLimits" . -}}</li>
|
||||
<li>{{- template "pollLimits" . -}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 id="moderated-servers">Moderated servers</h2>
|
||||
</section>
|
||||
<section class="about-section" role="region" aria-labelledby="moderated-servers">
|
||||
<h3 id="moderated-servers">Moderated servers</h3>
|
||||
<p>
|
||||
ActivityPub instances exchange (federate) data with other instances, including accounts and toots.
|
||||
This can be prevented for specific domains by suspending them. None of their content is stored,
|
||||
and interaction with their users is blocked both ways.</br>
|
||||
{{if .blocklistExposed}}
|
||||
<a href="/about/suspended">View the list of suspended domains</a>
|
||||
{{else}}
|
||||
This instance does not publically share this list.
|
||||
{{end}}
|
||||
ActivityPub instances federate with other instances by exchanging data with them over the network.
|
||||
Exchanged data includes things like accounts, statuses, likes, boosts, and media attachments.
|
||||
This exchange of data can prevented for instances on specific domains via a domain block created
|
||||
by an instance admin. When an instance is domain blocked by another instance:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 id="stats">Instance Statistics</h2>
|
||||
<ul>
|
||||
<li>Users: <span class="count">{{.instance.Stats.user_count}}</span></li>
|
||||
<li>Posts: <span class="count">{{.instance.Stats.status_count}}</span></li>
|
||||
<li>Federates with: <span class="count">{{.instance.Stats.domain_count}}</span> instances</li>
|
||||
<li>Any existing data from the blocked instance is deleted from the storage of the instance doing the blocking.</li>
|
||||
<li>Interaction between the two instances is cut off in both directions; neither instance can interact with the other.</li>
|
||||
<li>No new data from the blocked instance will be created on the instance that blocks it.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
{{- if .blocklistExposed }}
|
||||
<a href="/about/suspended">View the list of domains blocked by this instance</a>
|
||||
{{- else }}
|
||||
This instance does not publically share their list of blocked domains.
|
||||
{{- end }}
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
{{ template "footer.tmpl" .}}
|
||||
{{- end }}
|
|
@ -17,26 +17,24 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
{{ template "header.tmpl" .}}
|
||||
<main>
|
||||
{{- with . }}
|
||||
<main>
|
||||
<form action="/oauth/authorize" method="POST">
|
||||
<h1>Hi {{.user}}!</h1>
|
||||
<h1>Hi {{ .user -}}!</h1>
|
||||
<p>
|
||||
Application <b>{{.appname}}</b>
|
||||
{{if len .appwebsite | eq 0 | not}}
|
||||
({{.appwebsite}})
|
||||
{{end}}
|
||||
would like to perform actions on your behalf, with scope <em>{{.scope}}</em>.
|
||||
Application
|
||||
{{- if .appwebsite }}
|
||||
<a href="{{- .appwebsite -}}" rel="nofollow noreferrer noopener" target="_blank">{{- .appname -}}</a>
|
||||
{{- else }}
|
||||
<b>{{- .appname -}}</b>
|
||||
{{- end }}
|
||||
would like to perform actions on your behalf, with scope
|
||||
<em>{{- .scope -}}</em>.
|
||||
</p>
|
||||
<p>The application will redirect to {{.redirect}} to continue.</p>
|
||||
<p>
|
||||
<button
|
||||
type="submit"
|
||||
style="width:200px;"
|
||||
>
|
||||
Allow
|
||||
</button>
|
||||
To continue, the application will redirect to: <code>{{- .redirect -}}</code>
|
||||
</p>
|
||||
<button type="submit" style="width:200px;">Allow</button>
|
||||
</form>
|
||||
</main>
|
||||
{{ template "footer.tmpl" .}}
|
||||
</main>
|
||||
{{- end }}
|
|
@ -17,12 +17,11 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
{{ template "header.tmpl" .}}
|
||||
{{- with . }}
|
||||
<main>
|
||||
<section>
|
||||
<h1>Email Address Confirmed</h1>
|
||||
<p>Thanks {{.username}}! Your email address <b>{{.email}}</b> has been confirmed.<p>
|
||||
<p>Thanks {{ .username -}}! Your email address <b>{{- .email -}}</b> has been confirmed.<p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{{ template "footer.tmpl" .}}
|
||||
{{- end }}
|
|
@ -17,36 +17,36 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
{{ template "header.tmpl" .}}
|
||||
{{- with . }}
|
||||
<main>
|
||||
<section>
|
||||
<h1>Suspended Instances</h1>
|
||||
<p>
|
||||
The following list of domains have been suspended by the administrator(s) of this server.
|
||||
The following list of domains have been suspended
|
||||
by the administrator(s) of this server.
|
||||
</p>
|
||||
<p>
|
||||
All current and future accounts on these instances are blocked, and no more data is federated to the remote
|
||||
servers.
|
||||
This extends to subdomains, so an entry for 'example.com' includes 'social.example.com' as well.
|
||||
All current and future accounts on these instances are
|
||||
blocked, and no more data is federated to the remote servers.
|
||||
This extends to subdomains, so an entry for 'example.com'
|
||||
includes 'social.example.com' as well.
|
||||
</p>
|
||||
<div class="list domain-blocklist">
|
||||
<div class="header entry">
|
||||
<div class="domain">Domain</div>
|
||||
<div class="public_comment">Public comment</div>
|
||||
</div>
|
||||
{{range .blocklist}}
|
||||
<div class="entry" id="{{.Domain}}">
|
||||
{{- range .blocklist }}
|
||||
<div class="entry" id="{{- .Domain -}}">
|
||||
<div class="domain">
|
||||
<a class="text-cutoff" href="#{{.Domain}}" title="{{.Domain}}">{{.Domain}}</a>
|
||||
<a class="text-cutoff" href="#{{- .Domain -}}" title="{{- .Domain -}}">{{- .Domain -}}</a>
|
||||
</div>
|
||||
<div class="public_comment">
|
||||
<p>
|
||||
{{.PublicComment}}
|
||||
</p>
|
||||
<p>{{- .PublicComment -}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{- end }}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{{ template "footer.tmpl" .}}
|
||||
{{- end }}
|
|
@ -17,16 +17,16 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
{{ template "header.tmpl" .}}
|
||||
{{- with . }}
|
||||
<main>
|
||||
<section class="error">
|
||||
<h1>An error occured:</h1>
|
||||
<pre>{{.error}}</pre>
|
||||
{{if .requestID}}
|
||||
<pre>{{- .error -}}</pre>
|
||||
{{- if .requestID }}
|
||||
<div>
|
||||
<span>Request ID:</span> <code>{{.requestID}}</code>
|
||||
<span>Request ID:</span> <code>{{- .requestID -}}</code>
|
||||
</div>
|
||||
{{end}}
|
||||
{{- end }}
|
||||
</section>
|
||||
</main>
|
||||
{{ template "footer.tmpl" .}}
|
||||
{{- end }}
|
|
@ -17,34 +17,31 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
{{ template "header.tmpl" .}}
|
||||
<main>
|
||||
{{- with . }}
|
||||
<main>
|
||||
<form action="/oauth/finalize" method="POST">
|
||||
<h1>Hi {{.name}}!</h1>
|
||||
<h1>Hi {{ .name -}}!</h1>
|
||||
<p>
|
||||
You are about to sign-up to {{ .instance.Title }} (<code>{{ .instance.URI }}</code>)
|
||||
<br>
|
||||
You are about to sign-up to {{ .instance.Title -}}.
|
||||
To ensure the best experience for you, we need you to provide some additional details.
|
||||
</p>
|
||||
{{if .error}}
|
||||
<section class="error">
|
||||
<span>❌</span> <pre>{{.error}}</pre>
|
||||
</section>
|
||||
{{end}}
|
||||
<div class="callout">
|
||||
<p class="callout-title">Important</p>
|
||||
<p>Due to the way the ActivityPub standard works, you <strong>cannot</strong> change your username after it has been set.</p>
|
||||
</div>
|
||||
<div class="labelinput">
|
||||
<label for="username">Username <small>(must contain only lowercase letters, numbers, and underscores)</small></label>
|
||||
<input type="text"
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="username"
|
||||
required
|
||||
placeholder="Please enter your desired username" value="{{ .preferredUsername }}">
|
||||
placeholder="Please enter your desired username"
|
||||
value="{{- .preferredUsername -}}"
|
||||
>
|
||||
</div>
|
||||
<input type="hidden" name="name" value="{{ .name }}">
|
||||
<input type="hidden" name="name" value="{{- .name -}}">
|
||||
<button type="submit" style="width: 100%; margin-top: 1rem;" class="btn btn-success">Submit</button>
|
||||
</form>
|
||||
</main>
|
||||
{{ template "footer.tmpl" .}}
|
||||
</main>
|
||||
{{- end }}
|
|
@ -17,9 +17,8 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
{{ template "header.tmpl" .}}
|
||||
{{- with . }}
|
||||
<main class="lightgray">
|
||||
<div id="root">
|
||||
</div>
|
||||
<div id="root"></div>
|
||||
</main>
|
||||
{{ template "footer.tmpl" .}}
|
||||
{{- end }}
|
|
@ -1,122 +0,0 @@
|
|||
{{- /*
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
*/ -}}
|
||||
|
||||
|
||||
{{- /*
|
||||
NESTED TEMPLATE DECLARATIONS
|
||||
If some if/else macro is used multiple times, declare it once here instead.
|
||||
When invoking these nested templates, remember to pass in the values passed
|
||||
to the executing template, ie., use '{{ template "example" . }}' not
|
||||
'{{ template "example" }}', otherwise you'll end up with empty variables.
|
||||
*/ -}}
|
||||
{{ define "thumbnailType" }}{{ if .instance.ThumbnailType }}{{ .instance.ThumbnailType }}{{ else }}image/png{{ end }}{{ end }}
|
||||
{{ define "instanceTitle" }}{{ if .ogMeta }}{{ .ogMeta.Title }}{{ else }}{{ .instance.Title }} - GoToSocial{{ end }}{{ end }}
|
||||
|
||||
{{- /*
|
||||
BOILERPLATE GOES HERE
|
||||
*/ -}}
|
||||
<!DOCTYPE html>
|
||||
<!-- header.tmpl -->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
{{- /*
|
||||
ROBOTS META TAGS
|
||||
If this template was provided with a specific robots meta policy, use that.
|
||||
Otherwise, fall back to a default restrictive policy.
|
||||
See: https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag
|
||||
*/ -}}
|
||||
<meta name="robots" content="{{ if .robotsMeta }}{{ .robotsMeta }}{{ else }}noindex, nofollow{{ end }}">
|
||||
|
||||
{{- /*
|
||||
OPEN GRAPH META TAGS
|
||||
To enable fancy previews of links to GtS posts/profiles shared via instant
|
||||
messaging, or other social media, parse out provided Open Graph meta tags.
|
||||
*/ -}}
|
||||
{{ if .ogMeta -}}
|
||||
{{ if .ogMeta.Locale }}<meta name="og:locale" content="{{ .ogMeta.Locale }}">{{ end }}
|
||||
<meta property="og:type" content="{{ .ogMeta.Type }}">
|
||||
<meta property="og:title" content="{{ .ogMeta.Title }}">
|
||||
<meta property="og:url" content="{{ .ogMeta.URL }}">
|
||||
<meta property="og:site_name" content="{{ .ogMeta.SiteName }}">
|
||||
<meta property="og:description" {{ .ogMeta.Description | noescapeAttr }}>
|
||||
{{ if .ogMeta.ArticlePublisher }}
|
||||
<meta property="og:article:publisher" content="{{ .ogMeta.ArticlePublisher }}">
|
||||
<meta property="og:article:author" content="{{ .ogMeta.ArticleAuthor }}">
|
||||
<meta property="og:article:modified_time" content="{{ .ogMeta.ArticleModifiedTime }}">
|
||||
<meta property="og:article:published_time" content="{{ .ogMeta.ArticlePublishedTime }}">
|
||||
{{ end }}
|
||||
{{ if .ogMeta.ProfileUsername }}<meta property="og:profile:username" content="{{ .ogMeta.ProfileUsername }}">{{ end }}
|
||||
<meta property="og:image" content="{{ .ogMeta.Image }}">
|
||||
{{ if .ogMeta.ImageAlt }}<meta property="og:image:alt" content="{{ .ogMeta.ImageAlt }}">{{ end }}
|
||||
{{ if .ogMeta.ImageWidth }}
|
||||
<meta property="og:image:width" content="{{ .ogMeta.ImageWidth }}">
|
||||
<meta property="og:image:height" content="{{ .ogMeta.ImageHeight }}">
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
|
||||
{{- /*
|
||||
ICON
|
||||
For icon, provide a link to the instance thumbnail. If the instance admin has
|
||||
set a custom thumbnail, use the type they uploaded, else assume image/png.
|
||||
See: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel#icon
|
||||
*/ -}}
|
||||
<link rel="icon" href="{{ .instance.Thumbnail }}" type="{{ template "thumbnailType" . }}">
|
||||
<link rel="apple-touch-icon" href="{{ .instance.Thumbnail }}" type="{{ template "thumbnailType" . }}">
|
||||
<link rel="apple-touch-startup-image" href="{{ .instance.Thumbnail }}" type="{{ template "thumbnailType" . }}">
|
||||
|
||||
{{- /*
|
||||
RSS FEED
|
||||
To enable automatic rss feed discovery for feed readers, provide the 'alternate'
|
||||
link only if rss is enabled.
|
||||
See: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel#alternate
|
||||
*/ -}}
|
||||
{{ if .rssFeed -}}
|
||||
<link rel="alternate" type="application/rss+xml" href="{{ .rssFeed }}" title="{{ template "instanceTitle" . }}">
|
||||
{{- end }}
|
||||
|
||||
{{- /*
|
||||
STYLESHEET STUFF
|
||||
To try to speed up rendering a little bit, offer a preload for each stylesheet.
|
||||
See: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload.
|
||||
*/ -}}
|
||||
<link rel="preload" href="/assets/dist/_colors.css" as="style">
|
||||
<link rel="preload" href="/assets/dist/base.css" as="style">
|
||||
{{ range .stylesheets }}<link rel="preload" href="{{ . }}" as="style">{{ end }}
|
||||
<link rel="stylesheet" href="/assets/dist/_colors.css">
|
||||
<link rel="stylesheet" href="/assets/dist/base.css">
|
||||
{{ range .stylesheets }}<link rel="stylesheet" href="{{ . }}">{{ end }}
|
||||
<title>{{ template "instanceTitle" . }}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="page">
|
||||
<header>
|
||||
<a aria-label="{{ .instance.Title }}. Go to instance homepage" href="/" class="nounderline header">
|
||||
<img src="{{ .instance.Thumbnail }}"
|
||||
alt="{{ if .instance.ThumbnailDescription }}{{ .instance.ThumbnailDescription }}{{ else }}Instance Logo{{ end }}" />
|
||||
<h1>
|
||||
{{ .instance.Title }}
|
||||
</h1>
|
||||
</a>
|
||||
</header>
|
||||
<div class="content">
|
|
@ -17,61 +17,21 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
{{ template "header.tmpl" .}}
|
||||
<section class="excerpt-top">
|
||||
home to <span class="count">{{.instance.Stats.user_count}}</span> users
|
||||
who posted <span class="count">{{.instance.Stats.status_count}}</span> statuses,
|
||||
federating with <span class="count">{{.instance.Stats.domain_count}}</span> other instances.
|
||||
</section>
|
||||
<main class="lightgray">
|
||||
<section>
|
||||
<div className="short-description">
|
||||
{{.instance.ShortDescription |noescape}}
|
||||
</div>
|
||||
</section>
|
||||
<section class="apps">
|
||||
<p>
|
||||
GoToSocial does not provide its own webclient, but implements the Mastodon client API.
|
||||
You can use this server through a variety of other clients:
|
||||
</p>
|
||||
<div class="applist">
|
||||
<div class="entry">
|
||||
<svg role="img" aria-labelledby="semaphoreTitle semaphoreDesc" class="logo redraw" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 146 120">
|
||||
<title id="semaphoreTitle">The Semaphore logo</title>
|
||||
<desc id="semaphoreDesc">A waving flag</desc>
|
||||
<path d="M68.13 0C53.94 0 42.81 20 13.9 27.1l-2.23-5.29a6.5 6.5 0 0 0-5.17-10.4 6.5 6.5 0 0 0-.81 12.95L46.2 120l5.99-2.5-14.42-33.33c22.8-6.86 32.51-22.16 49.83-20.58 9.9.9 4.87 19.56 8.11 17.93 16.22-8.15 32.44-11.41 50.29-11.41-7.96-9.78-17.38-20.55-22.71-31.74L120.8 32c-2.32-7.33-2.56-14.75.87-22.22-9.74-3.26-21.1 0-32.45 4.9C82.2 9.77 79.5 0 68.13 0zM15.26 30.42c8.95 6.63 13.63 13.86 16.07 20.94l1.62 6.32c1.24 6.58 1.07 12.8 1.27 18.03z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h2>Semaphore</h2>
|
||||
<p>Semaphore is a web client designed for speed and simplicity.</p>
|
||||
<a href="https://semaphore.social/" target="_blank" rel="noopener">Use Semaphore</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<img class="logo" src="/assets/tusky.svg" alt="The Tusky mascot, a cartoon elephant tooting happily"/>
|
||||
<div>
|
||||
<h2>Tusky</h2>
|
||||
<p>Tusky is a lightweight mobile client for Android.</p>
|
||||
<a href="https://tusky.app" target="_blank" rel="noopener">Get Tusky</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<img class="logo" src="/assets/feditext.svg" alt="The Feditext logo, the characters ft at a slight angle">
|
||||
<div>
|
||||
<h2>Feditext</h2>
|
||||
<p>Feditext (beta) is a beautiful client for iOS, iPadOS and macOS.</p>
|
||||
<a href="https://fedi.software/@Feditext" target="_blank" rel="noopener">Get Feditext</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry">
|
||||
<img class="logo" src="/assets/mastodon.svg" alt="The Mastodon logo, the character M in a speech bubble">
|
||||
<div>
|
||||
<h2>More clients</h2>
|
||||
<p>Or try one of the clients listed on the official Mastodon page.</p>
|
||||
<a href="https://joinmastodon.org/apps" target="_blank" rel="noopener">Get Mastodon apps</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{- define "shortDescription" -}}
|
||||
{{- if .instance.ShortDescription }}
|
||||
{{ .instance.ShortDescription | noescape }}
|
||||
{{- else }}
|
||||
<p>No short description has yet been set for this instance.<p>
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{- with . }}
|
||||
<main class="about">
|
||||
<section class="about-section" role="region" aria-labelledby="about">
|
||||
<h3 id="about">About this instance</h3>
|
||||
{{- include "shortDescription" . | indent 2 }}
|
||||
<a href="/about">See more details</a>
|
||||
</section>
|
||||
{{- include "index_apps.tmpl" . | indent 1 }}
|
||||
</main>
|
||||
{{ template "footer.tmpl" .}}
|
||||
{{- end }}
|
115
web/template/index_apps.tmpl
Normal file
115
web/template/index_apps.tmpl
Normal file
|
@ -0,0 +1,115 @@
|
|||
{{- /*
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
*/ -}}
|
||||
|
||||
{{- with . }}
|
||||
<section role="region" class="about-section apps" aria-labelledby="apps">
|
||||
<h3 id="apps">Client applications</h3>
|
||||
<p>
|
||||
GoToSocial does not provide its own webclient, but implements the Mastodon client API.
|
||||
You can use this server through a variety of other clients:
|
||||
</p>
|
||||
<ul class="applist nodot" role="group">
|
||||
<li class="applist-entry">
|
||||
<div class="applist-text">
|
||||
<p><strong>Semaphore</strong> is a web client designed for speed and simplicity.</p>
|
||||
<a
|
||||
href="https://semaphore.social/"
|
||||
rel="nofollow noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Use Semaphore
|
||||
</a>
|
||||
</div>
|
||||
<svg
|
||||
role="img"
|
||||
aria-labelledby="semaphore-title semaphore-desc"
|
||||
class="applist-logo redraw"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 146 120"
|
||||
width="100"
|
||||
height="100"
|
||||
>
|
||||
<title id="semaphore-title">The Semaphore logo</title>
|
||||
<desc id="semaphore-desc">A waving flag</desc>
|
||||
<path d="M68.13 0C53.94 0 42.81 20 13.9 27.1l-2.23-5.29a6.5 6.5 0 0 0-5.17-10.4 6.5 6.5 0 0 0-.81 12.95L46.2 120l5.99-2.5-14.42-33.33c22.8-6.86 32.51-22.16 49.83-20.58 9.9.9 4.87 19.56 8.11 17.93 16.22-8.15 32.44-11.41 50.29-11.41-7.96-9.78-17.38-20.55-22.71-31.74L120.8 32c-2.32-7.33-2.56-14.75.87-22.22-9.74-3.26-21.1 0-32.45 4.9C82.2 9.77 79.5 0 68.13 0zM15.26 30.42c8.95 6.63 13.63 13.86 16.07 20.94l1.62 6.32c1.24 6.58 1.07 12.8 1.27 18.03z"></path>
|
||||
</svg>
|
||||
</li>
|
||||
<li class="applist-entry">
|
||||
<div class="applist-text">
|
||||
<p><strong>Tusky</strong> is a lightweight mobile client for Android.</p>
|
||||
<a
|
||||
href="https://tusky.app"
|
||||
rel="nofollow noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Get Tusky
|
||||
</a>
|
||||
</div>
|
||||
<img
|
||||
class="applist-logo"
|
||||
src="/assets/tusky.svg"
|
||||
alt="The Tusky mascot, a cartoon elephant tooting happily"
|
||||
title="The Tusky mascot, a cartoon elephant tooting happily"
|
||||
width="100"
|
||||
height="100"
|
||||
/>
|
||||
</li>
|
||||
<li class="applist-entry">
|
||||
<div class="applist-text">
|
||||
<p><strong>Feditext</strong> (beta) is a beautiful client for iOS, iPadOS and macOS.</p>
|
||||
<a
|
||||
href="https://fedi.software/@Feditext"
|
||||
rel="nofollow noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Get Feditext
|
||||
</a>
|
||||
</div>
|
||||
<img
|
||||
class="applist-logo"
|
||||
src="/assets/feditext.svg"
|
||||
alt="The Feditext logo, the characters 'ft' at a slight angle"
|
||||
title="The Feditext logo, the characters 'ft' at a slight angle"
|
||||
width="100"
|
||||
height="100"
|
||||
/>
|
||||
</li>
|
||||
<li class="applist-entry">
|
||||
<div class="applist-text">
|
||||
<p>Or try one of the <strong>Mastodon clients</strong> listed on the official Mastodon page.</p>
|
||||
<a
|
||||
href="https://joinmastodon.org/apps"
|
||||
rel="nofollow noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Get Mastodon apps
|
||||
</a>
|
||||
</div>
|
||||
<img
|
||||
class="applist-logo"
|
||||
src="/assets/mastodon.svg"
|
||||
alt="The Mastodon logo, the character 'M' in a speech bubble"
|
||||
title="The Mastodon logo, the character 'M' in a speech bubble"
|
||||
width="100"
|
||||
height="100"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
{{- end }}
|
|
@ -17,12 +17,12 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
{{ template "header.tmpl" .}}
|
||||
{{- with . }}
|
||||
<main>
|
||||
<section class="oob-token">
|
||||
<h1>Hi {{ .user }}!</h1>
|
||||
<p>Here's your out-of-band token with scope "<em>{{.scope}}</em>", use it wisely:</p>
|
||||
<code>{{ .oobToken }}</code>
|
||||
<h1>Hi {{ .user -}}!</h1>
|
||||
<p>Here's your out-of-band token with scope "<em>{{- .scope -}}</em>", use it wisely:</p>
|
||||
<code>{{- .oobToken -}}</code>
|
||||
</section>
|
||||
</main>
|
||||
{{ template "footer.tmpl" .}}
|
||||
{{- end }}
|
85
web/template/page.tmpl
Normal file
85
web/template/page.tmpl
Normal file
|
@ -0,0 +1,85 @@
|
|||
{{- /*
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
*/ -}}
|
||||
|
||||
{{- /*
|
||||
NESTED TEMPLATE DECLARATIONS
|
||||
If some if/else macro is used multiple times, declare it once here instead.
|
||||
When invoking these nested templates, remember to pass in the values passed
|
||||
to the executing template, ie., use '{{ template "example" . }}' not
|
||||
'{{ template "example" }}', otherwise you'll end up with empty variables.
|
||||
*/ -}}
|
||||
|
||||
{{- define "thumbnailType" -}}
|
||||
{{- if .instance.ThumbnailType -}}
|
||||
{{- .instance.ThumbnailType -}}
|
||||
{{- else -}}
|
||||
image/png
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "instanceTitle" -}}
|
||||
{{- if .ogMeta -}}
|
||||
{{- demojify .ogMeta.Title | noescape -}}
|
||||
{{- else -}}
|
||||
{{- .instance.Title }} - GoToSocial
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="{{- if .robotsMeta -}}{{- .robotsMeta -}}{{- else -}}noindex, nofollow{{- end -}}">
|
||||
{{- if .ogMeta }}
|
||||
{{- include "page_ogmeta.tmpl" . | indent 2 }}
|
||||
{{- else }}
|
||||
{{- end }}
|
||||
{{- if .rssFeed }}
|
||||
<link rel="alternate" type="application/rss+xml" href="{{- .rssFeed -}}" title="{{- template "instanceTitle" . -}}">
|
||||
{{- else }}
|
||||
{{- end }}
|
||||
{{- if .account }}
|
||||
<link rel="alternate" type="application/activity+json" href="/users/{{- .account.Username -}}">
|
||||
{{- else if .status }}
|
||||
<link rel="alternate" type="application/activity+json" href="/users/{{- .status.Account.Username -}}/statuses/{{- .status.ID -}}">
|
||||
{{- else }}
|
||||
{{- end }}
|
||||
<link rel="icon" href="{{- .instance.Thumbnail -}}" type="{{- template "thumbnailType" . -}}">
|
||||
<link rel="apple-touch-icon" href="{{- .instance.Thumbnail -}}" type="{{- template "thumbnailType" . -}}">
|
||||
<link rel="apple-touch-startup-image" href="{{- .instance.Thumbnail -}}" type="{{- template "thumbnailType" . -}}">
|
||||
{{- include "page_stylesheets.tmpl" . | indent 2 }}
|
||||
{{- range .javascript }}
|
||||
<script type="text/javascript" src="{{- . -}}" async="" defer=""></script>
|
||||
{{- end }}
|
||||
<title>{{- template "instanceTitle" . -}}</title>
|
||||
</head>
|
||||
<body class="page">
|
||||
<header class="page-header">
|
||||
{{- include "page_header.tmpl" . | indent 3 }}
|
||||
</header>
|
||||
<div class="page-content">
|
||||
{{- include .pageContent . | indent 3 | outdentPre }}
|
||||
</div>
|
||||
<footer class="page-footer">
|
||||
{{- include "page_footer.tmpl" . | indent 3 }}
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
67
web/template/page_footer.tmpl
Normal file
67
web/template/page_footer.tmpl
Normal file
|
@ -0,0 +1,67 @@
|
|||
{{- /*
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
*/ -}}
|
||||
|
||||
{{- with . }}
|
||||
<nav>
|
||||
<ul class="nodot">
|
||||
<li id="about">
|
||||
<a
|
||||
href="/about"
|
||||
class="nounderline"
|
||||
>
|
||||
About {{ .instance.Title }}
|
||||
</a>
|
||||
</li>
|
||||
<li id="version">
|
||||
<a
|
||||
href="https://github.com/superseriousbusiness/gotosocial"
|
||||
class="nounderline"
|
||||
rel="nofollow noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<span aria-hidden="true">🦥</span>
|
||||
Source - GoToSocial {{ .instance.Version }}
|
||||
<span aria-hidden="true">🦥</span>
|
||||
</a>
|
||||
</li>
|
||||
{{- if .instance.ContactAccount }}
|
||||
<li id="contact">
|
||||
<a
|
||||
href="/@{{- .instance.ContactAccount.Username -}}"
|
||||
class="nounderline"
|
||||
>
|
||||
Contact account - {{ .instance.ContactAccount.Username }}
|
||||
</a>
|
||||
</li>
|
||||
{{- end }}
|
||||
{{- if .instance.Email }}
|
||||
<li id="email">
|
||||
<a
|
||||
href="mailto:{{- .instance.Email -}}"
|
||||
class="nounderline"
|
||||
rel="nofollow noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Email - {{ .instance.Email }}
|
||||
</a>
|
||||
</li>
|
||||
{{- end }}
|
||||
</ul>
|
||||
</nav>
|
||||
{{- end }}
|
72
web/template/page_header.tmpl
Normal file
72
web/template/page_header.tmpl
Normal file
|
@ -0,0 +1,72 @@
|
|||
{{- /*
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
*/ -}}
|
||||
|
||||
{{- define "thumbnailDescription" -}}
|
||||
{{- if .instance.ThumbnailDescription -}}
|
||||
{{- .instance.ThumbnailDescription -}}
|
||||
{{- else -}}
|
||||
Instance Logo
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "strapUsers" -}}
|
||||
{{- with .instance.Stats.user_count -}}
|
||||
{{- if eq . 1 -}}
|
||||
<span class="count">{{- . -}}</span> user
|
||||
{{- else -}}
|
||||
<span class="count">{{- . -}}</span> users
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "strapPosts" -}}
|
||||
{{- with .instance.Stats.status_count -}}
|
||||
{{- if eq . 1 -}}
|
||||
<span class="count">{{- . -}}</span> post
|
||||
{{- else -}}
|
||||
<span class="count">{{- . -}}</span> posts
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "strapInstances" -}}
|
||||
{{- with .instance.Stats.domain_count -}}
|
||||
{{- if eq . 1 -}}
|
||||
<span class="count">{{- . -}}</span> other instance
|
||||
{{- else -}}
|
||||
<span class="count">{{- . -}}</span> other instances
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- with . }}
|
||||
<a aria-label="{{- .instance.Title -}}. Go to instance homepage" href="/" class="nounderline">
|
||||
<img
|
||||
src="{{- .instance.Thumbnail -}}"
|
||||
alt="{{- template "thumbnailDescription" . -}}"
|
||||
title="{{- template "thumbnailDescription" . -}}"
|
||||
width="100"
|
||||
height="100"
|
||||
/>
|
||||
<h1>{{- .instance.Title -}}</h1>
|
||||
</a>
|
||||
{{- if .showStrap }}
|
||||
<aside>home to {{ template "strapUsers" . }} who wrote {{ template "strapPosts" . }}, federating with {{ template "strapInstances" . }}</aside>
|
||||
{{- end }}
|
||||
{{- end }}
|
57
web/template/page_ogmeta.tmpl
Normal file
57
web/template/page_ogmeta.tmpl
Normal file
|
@ -0,0 +1,57 @@
|
|||
{{- /*
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
*/ -}}
|
||||
|
||||
{{- /*
|
||||
OPEN GRAPH META TAGS
|
||||
To enable fancy previews of links to GtS posts/profiles shared via instant
|
||||
messaging, or other social media, parse out provided Open Graph meta tags.
|
||||
*/ -}}
|
||||
|
||||
{{- with .ogMeta }}
|
||||
{{- if .Locale }}
|
||||
<meta name="og:locale" content="{{- .Locale -}}">
|
||||
{{- else }}
|
||||
{{- end }}
|
||||
<meta property="og:type" content="{{- .Type -}}">
|
||||
<meta property="og:title" content="{{- demojify .Title | noescape -}}">
|
||||
<meta property="og:url" content="{{- .URL -}}">
|
||||
<meta property="og:site_name" content="{{- .SiteName -}}">
|
||||
<meta property="og:description" {{ demojify .Description | noescapeAttr -}}>
|
||||
{{- if .ArticlePublisher }}
|
||||
<meta property="og:article:publisher" content="{{ .ArticlePublisher }}">
|
||||
<meta property="og:article:author" content="{{ .ArticleAuthor }}">
|
||||
<meta property="og:article:modified_time" content="{{ .ArticleModifiedTime }}">
|
||||
<meta property="og:article:published_time" content="{{ .ArticlePublishedTime }}">
|
||||
{{- else }}
|
||||
{{- end }}
|
||||
{{- if .ProfileUsername }}
|
||||
<meta property="og:profile:username" content="{{- .ProfileUsername -}}">
|
||||
{{- else }}
|
||||
{{- end }}
|
||||
<meta property="og:image" content="{{- .Image -}}">
|
||||
{{- if .ImageAlt }}
|
||||
<meta property="og:image:alt" content="{{- .ImageAlt -}}">
|
||||
{{- else }}
|
||||
{{- end }}
|
||||
{{- if .ImageWidth }}
|
||||
<meta property="og:image:width" content="{{ .ImageWidth }}">
|
||||
<meta property="og:image:height" content="{{ .ImageHeight }}">
|
||||
{{- else }}
|
||||
{{- end }}
|
||||
{{- end }}
|
41
web/template/page_stylesheets.tmpl
Normal file
41
web/template/page_stylesheets.tmpl
Normal file
|
@ -0,0 +1,41 @@
|
|||
{{- /*
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
*/ -}}
|
||||
|
||||
{{- /*
|
||||
Order of stylesheet loading is important: _colors and base should always be loaded
|
||||
before any other provided sheets, since the latter cascade from the former.
|
||||
|
||||
To try to speed up rendering a little bit, offer a preload for each stylesheet.
|
||||
See: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload.
|
||||
*/ -}}
|
||||
|
||||
{{- with . }}
|
||||
<link rel="preload" href="/assets/dist/_colors.css" as="style">
|
||||
<link rel="preload" href="/assets/dist/base.css" as="style">
|
||||
<link rel="preload" href="/assets/dist/page.css" as="style">
|
||||
{{- range .stylesheets }}
|
||||
<link rel="preload" href="{{- . -}}" as="style">
|
||||
{{- end }}
|
||||
<link rel="stylesheet" href="/assets/dist/_colors.css">
|
||||
<link rel="stylesheet" href="/assets/dist/base.css">
|
||||
<link rel="stylesheet" href="/assets/dist/page.css">
|
||||
{{- range .stylesheets }}
|
||||
<link rel="stylesheet" href="{{- . -}}">
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -17,129 +17,123 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
{{ template "header.tmpl" .}}
|
||||
|
||||
{{- with . }}
|
||||
<main class="profile">
|
||||
<div class="header">
|
||||
<div class="header-image">
|
||||
{{ if .account.Header }}
|
||||
<img src="{{.account.Header}}" alt="" />
|
||||
{{ end }}
|
||||
<h2 class="sr-only">Profile for {{ .account.Username -}}</h2>
|
||||
<section class="profile-header" role="region" aria-label="Basic info">
|
||||
<div class="header-image-wrapper">
|
||||
<img
|
||||
src="{{- .account.Header -}}"
|
||||
alt="Header for {{ .account.Username -}}"
|
||||
title="Header for {{ .account.Username -}}"
|
||||
/>
|
||||
</div>
|
||||
<div class="basic-info" aria-hidden="true">
|
||||
<a class="avatar" href="{{.account.Avatar}}">
|
||||
<img src="{{.account.Avatar}}" alt="">
|
||||
<div class="basic-info">
|
||||
<a class="avatar" href="{{- .account.Avatar -}}">
|
||||
<img
|
||||
src="{{- .account.Avatar -}}"
|
||||
alt="Avatar for {{ .account.Username -}}"
|
||||
title="Avatar for {{ .account.Username -}}"
|
||||
/>
|
||||
</a>
|
||||
<span class="displayname text-cutoff">
|
||||
{{if .account.DisplayName}}
|
||||
{{emojify .account.Emojis (escape .account.DisplayName)}}
|
||||
{{else}}
|
||||
{{.account.Username}}
|
||||
{{end}}
|
||||
</span>
|
||||
<span class="username text-cutoff">@{{.account.Username}}@{{.instance.AccountDomain}}</span>
|
||||
{{- /* Only render account role if 1. it's present and 2. it's not equal to the standard 'user' role */ -}}
|
||||
{{ if and (.account.Role) (ne .account.Role.Name "user") }}
|
||||
<div class="role {{ .account.Role.Name }}">
|
||||
{{ .account.Role.Name }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="sr-only">
|
||||
Profile for
|
||||
{{if .account.DisplayName}}{{.account.DisplayName}}{{else}}{{.account.Username}}{{end}}.
|
||||
Username @{{.account.Username}}, {{.instance.AccountDomain}}.
|
||||
{{ if and (.account.Role) (ne .account.Role.Name "user") }}
|
||||
Role: {{ .account.Role.Name }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column-split">
|
||||
|
||||
<section class="about-user">
|
||||
<div class="col-header">
|
||||
<h1>About</h1>
|
||||
</div>
|
||||
|
||||
<div class="fields">
|
||||
{{ range .account.Fields }}
|
||||
<div class="field">
|
||||
<b>{{emojify $.account.Emojis (noescape .Name)}}</b>
|
||||
<span>{{emojify $.account.Emojis (noescape .Value)}}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="bio">
|
||||
{{ if .account.Note }}
|
||||
{{emojify .account.Emojis (noescape .account.Note)}}
|
||||
{{else}}
|
||||
This GoToSocial user hasn't written a bio yet!
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="sr-only" role="group">
|
||||
<span>Joined on {{.account.CreatedAt | timestampVague}}.</span>
|
||||
<span>{{.account.StatusesCount}} post{{if .account.StatusesCount | eq 1 | not}}s{{end}}.</span>
|
||||
<span>Followed by {{.account.FollowersCount}}.</span>
|
||||
<span>Following {{.account.FollowingCount}}.</span>
|
||||
</div>
|
||||
|
||||
<div class="accountstats" aria-hidden="true">
|
||||
<b>Joined</b><time datetime="{{.account.CreatedAt}}">{{.account.CreatedAt | timestampVague}}</time>
|
||||
<b>Posts</b><span>{{.account.StatusesCount}}</span>
|
||||
<b>Followed by</b><span>{{.account.FollowersCount}}</span>
|
||||
<b>Following</b><span>{{.account.FollowingCount}}</span>
|
||||
<dl class="namerole">
|
||||
<dt class="sr-only">Display name</dt>
|
||||
<dd class="displayname text-cutoff">
|
||||
{{- if .account.DisplayName -}}
|
||||
{{- emojify .account.Emojis (escape .account.DisplayName) -}}
|
||||
{{- else -}}
|
||||
{{- .account.Username -}}
|
||||
{{- end -}}
|
||||
</dd>
|
||||
<dt class="sr-only">Username</dt>
|
||||
<dd class="username text-cutoff">@{{- .account.Username -}}@{{- .instance.AccountDomain -}}</dd>
|
||||
{{- if and (.account.Role) (ne .account.Role.Name "user") }}
|
||||
<dt class="sr-only">Role</dt>
|
||||
<dd class="role {{ .account.Role.Name -}}">{{- .account.Role.Name -}}</dd>
|
||||
{{- end }}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="toots">
|
||||
{{ if .pinned_statuses }}
|
||||
<div class="column-split">
|
||||
<section class="about-user" role="region" aria-labelledby="about-header">
|
||||
<div class="col-header">
|
||||
<h2>Pinned posts</h2>
|
||||
<h3 id="about-header">About<span class="sr-only"> {{- .account.Username -}}</span></h3>
|
||||
</div>
|
||||
{{- if .account.Fields }}
|
||||
{{- include "profile_fields.tmpl" . | indent 3 }}
|
||||
{{- end }}
|
||||
<h4 class="sr-only">Bio</h4>
|
||||
<div class="bio">
|
||||
{{- if .account.Note }}
|
||||
{{ emojify .account.Emojis (noescape .account.Note) }}
|
||||
{{- else }}
|
||||
<p>This GoToSocial user hasn't written a bio yet!</p>
|
||||
{{- end }}
|
||||
</div>
|
||||
<h4 class="sr-only">Stats</h4>
|
||||
<dl class="accountstats">
|
||||
<dt>Joined</dt>
|
||||
<dd><time datetime="{{- .account.CreatedAt -}}">{{- .account.CreatedAt | timestampVague -}}</time></dd>
|
||||
<dt>Posts</dt>
|
||||
<dd>{{- .account.StatusesCount -}}</dd>
|
||||
<dt>Followed by</dt>
|
||||
<dd>{{- .account.FollowersCount -}}</dd>
|
||||
<dt>Following</dt>
|
||||
<dd>{{- .account.FollowingCount -}}</dd>
|
||||
</dl>
|
||||
</section>
|
||||
<div class="statuses-wrapper" role="region" aria-label="Posts by {{ .account.Username -}}">
|
||||
{{- if .pinned_statuses }}
|
||||
<section class="pinned statuses" aria-labelledby="pinned">
|
||||
<div class="col-header">
|
||||
<h3 id="pinned">Pinned posts</h3>
|
||||
<a href="#recent">jump to recent</a>
|
||||
</div>
|
||||
<section class="thread">
|
||||
{{ range .pinned_statuses }}
|
||||
<article class="toot expanded" id="{{.ID}}">
|
||||
{{ template "status.tmpl" .}}
|
||||
<div class="thread">
|
||||
{{- range .pinned_statuses }}
|
||||
<article
|
||||
class="status expanded"
|
||||
{{- includeAttr "status_attributes.tmpl" . | indentAttr 6 }}
|
||||
>
|
||||
{{- include "status.tmpl" . | indent 6 }}
|
||||
</article>
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
</div>
|
||||
</section>
|
||||
{{ end }}
|
||||
|
||||
{{- end }}
|
||||
<section class="recent statuses" aria-labelledby="recent">
|
||||
<div class="col-header">
|
||||
<h2 id="recent" tabindex="-1">Recent posts</h2>
|
||||
{{ if .rssFeed }}
|
||||
<a href="{{ .rssFeed }}" class="rss-icon" aria-label="RSS feed">
|
||||
<h3 id="recent" tabindex="-1">Recent posts</h3>
|
||||
{{- if .rssFeed }}
|
||||
<a href="{{- .rssFeed -}}" class="rss-icon" aria-label="RSS feed">
|
||||
<i class="fa fa-rss-square" aria-hidden="true"></i>
|
||||
</a>
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
</div>
|
||||
|
||||
<section class="thread">
|
||||
{{ if not .statuses }}
|
||||
<div class="thread">
|
||||
{{- if not .statuses }}
|
||||
<div data-nosnippet class="nothinghere">Nothing here!</div>
|
||||
{{ else }}
|
||||
{{ range .statuses }}
|
||||
<article class="toot expanded" id="{{.ID}}">
|
||||
{{ template "status.tmpl" .}}
|
||||
{{- else }}
|
||||
{{- range .statuses }}
|
||||
<article
|
||||
class="status expanded"
|
||||
{{- includeAttr "status_attributes.tmpl" . | indentAttr 6 }}
|
||||
>
|
||||
{{- include "status.tmpl" . | indent 6 }}
|
||||
</article>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</section>
|
||||
|
||||
<div class="backnextlinks">
|
||||
{{ if .show_back_to_top }}
|
||||
<a href="/@{{ .account.Username }}">Back to top</a>
|
||||
{{ end }}
|
||||
{{ if .statuses_next }}
|
||||
<a href="{{ .statuses_next }}" class="next">Show older</a>
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
</div>
|
||||
<nav class="backnextlinks">
|
||||
{{- if .show_back_to_top }}
|
||||
<a href="/@{{- .account.Username -}}">Back to top</a>
|
||||
{{- end }}
|
||||
{{- if .statuses_next }}
|
||||
<a href="{{- .statuses_next -}}" class="next">Show older</a>
|
||||
{{- end }}
|
||||
</nav>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{ template "footer.tmpl" .}}
|
||||
{{- end }}
|
|
@ -17,30 +17,16 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
<!-- footer.tmpl -->
|
||||
{{- with . }}
|
||||
<div class="fields">
|
||||
<h4 class="sr-only">Fields</h4>
|
||||
<dl>
|
||||
{{- range .account.Fields }}
|
||||
<div class="field">
|
||||
<dt>{{- emojify $.account.Emojis (noescape .Name) -}}</dt>
|
||||
<dd>{{- emojify $.account.Emojis (noescape .Value) -}}</dd>
|
||||
</div>
|
||||
<footer>
|
||||
<div id="version">
|
||||
<a name="Source code" href="https://github.com/superseriousbusiness/gotosocial">
|
||||
GoToSocial <span class="accent">{{.instance.Version}}</span>
|
||||
</a>
|
||||
</div>
|
||||
{{ if .instance.ContactAccount }}
|
||||
<div id="contact">
|
||||
Contact: <a href="{{.instance.ContactAccount.URL}}" class="nounderline">{{.instance.ContactAccount.Username}}</a><br>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .instance.Email }}
|
||||
<div id="email">
|
||||
Email: <a href="mailto:{{.instance.Email}}" class="nounderline">{{.instance.Email}}</a><br>
|
||||
</div>
|
||||
{{ end }}
|
||||
</footer>
|
||||
</div>
|
||||
{{ if .javascript }}
|
||||
{{ range .javascript }}
|
||||
<script src="{{.}}"></script>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</body>
|
||||
</html>
|
||||
{{- end }}
|
||||
</dl>
|
||||
</div>
|
||||
{{- end }}
|
|
@ -17,10 +17,10 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
{{ template "header.tmpl" .}}
|
||||
{{- with . }}
|
||||
<main>
|
||||
<section class="login">
|
||||
<h1>Login</h1>
|
||||
<section class="sign-in" aria-labelledby="sign-in">
|
||||
<h2 id="sign-in">Sign in</h2>
|
||||
<form action="/auth/sign_in" method="POST">
|
||||
<div class="labelinput">
|
||||
<label for="email">Email</label>
|
||||
|
@ -30,8 +30,8 @@
|
|||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control" name="password" required placeholder="Please enter your password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">Login</button>
|
||||
<button type="submit" class="btn btn-success">Sign in</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
{{ template "footer.tmpl" .}}
|
||||
{{- end }}
|
|
@ -17,28 +17,24 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
<a data-nosnippet href="{{- .URL -}}" class="toot-link">Open thread</a>
|
||||
<section class="author">
|
||||
<a href="{{- .Account.URL -}}">
|
||||
<img class="avatar" src="{{- .Account.Avatar -}}" alt="">
|
||||
<span aria-hidden="true" class="displayname">
|
||||
{{- if .Account.DisplayName -}}
|
||||
{{- emojify .Account.Emojis (escape .Account.DisplayName) -}}
|
||||
{{- else -}}
|
||||
{{- .Account.Username -}}
|
||||
{{- end -}}
|
||||
</span>
|
||||
<span aria-hidden="true" class="username">@{{- .Account.Username -}}</span>
|
||||
<span class="sr-only">
|
||||
{{- if .Account.DisplayName -}}
|
||||
{{- emojify .Account.Emojis (escape .Account.DisplayName) -}}. Username: @{{ .Account.Acct -}}.
|
||||
{{- else -}}
|
||||
@{{- .Account.Acct -}}.
|
||||
{{- end -}}
|
||||
</span>
|
||||
</a>
|
||||
</section>
|
||||
<section class="body">
|
||||
{{- define "statusContent" -}}
|
||||
{{- with .Content }}
|
||||
<div class="content" lang="{{- $.LanguageTag.TagStr -}}">
|
||||
{{ noescape . | emojify $.Emojis }}
|
||||
</div>
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{- /*
|
||||
When including this template, always wrap
|
||||
it in an appropriate <article></article>!
|
||||
*/ -}}
|
||||
|
||||
{{- with . }}
|
||||
<header class="status-header">
|
||||
{{- include "status_header.tmpl" . | indent 1 }}
|
||||
</header>
|
||||
<div class="status-body">
|
||||
{{- if .SpoilerText }}
|
||||
<details class="text-spoiler">
|
||||
<summary>
|
||||
|
@ -46,59 +42,49 @@
|
|||
<span class="button" role="button" tabindex="0">Toggle visibility</span>
|
||||
</summary>
|
||||
<div class="text">
|
||||
<div class="content" lang="{{- .LanguageTag.TagStr -}}">
|
||||
{{ emojify .Emojis (noescape .Content) }}
|
||||
</div>
|
||||
{{- with . }}
|
||||
{{- include "statusContent" . | indent 3 }}
|
||||
{{- end }}
|
||||
{{- if .Poll }}
|
||||
{{ template "status_poll.tmpl" . }}
|
||||
{{- include "status_poll.tmpl" . | indent 3 }}
|
||||
{{- end }}
|
||||
</div>
|
||||
</details>
|
||||
{{- else }}
|
||||
<div class="text">
|
||||
<div class="content" lang="{{- .LanguageTag.TagStr -}}">
|
||||
{{ emojify .Emojis (noescape .Content) }}
|
||||
</div>
|
||||
{{- with . }}
|
||||
{{- include "statusContent" . | indent 2 }}
|
||||
{{- end }}
|
||||
{{- if .Poll }}
|
||||
{{ template "status_poll.tmpl" . }}
|
||||
{{- include "status_poll.tmpl" . | indent 2 }}
|
||||
{{- end }}
|
||||
</div>
|
||||
{{- end }}
|
||||
{{- if .MediaAttachments }}
|
||||
{{ template "status_attachments.tmpl" . }}
|
||||
{{- include "status_attachments.tmpl" . | indent 1 }}
|
||||
{{- end }}
|
||||
</section>
|
||||
<aside class="info">
|
||||
<dl class="sr-only">
|
||||
<dt>Published<dt>
|
||||
<dd>{{- .CreatedAt | timestampPrecise -}}</dd>
|
||||
{{- if .LanguageTag.DisplayStr }}
|
||||
<dt>Language</dt>
|
||||
<dd>{{ .LanguageTag.DisplayStr }}</dd>
|
||||
{{- end }}
|
||||
</dl>
|
||||
<time aria-hidden="true" datetime="{{- .CreatedAt -}}">{{- .CreatedAt | timestampPrecise -}}</time>
|
||||
<div class="stats" role="group">
|
||||
<div class="stats-item">
|
||||
<span aria-hidden="true"><i class="fa fa-reply-all"></i> {{ .RepliesCount -}}</span>
|
||||
<span class="sr-only">{{- .RepliesCount }} {{ if .RepliesCount | eq 1 }}reply{{ else }}replies{{ end -}}</span>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<span aria-hidden="true"><i class="fa fa-star"></i> {{ .FavouritesCount -}}</span>
|
||||
<span class="sr-only">{{- .FavouritesCount }} {{ if .FavouritesCount | eq 1 }}favourite{{ else }}favourites{{ end -}}</span>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<span aria-hidden="true"><i class="fa fa-retweet"></i> {{ .ReblogsCount -}}</span>
|
||||
<span class="sr-only">{{- .ReblogsCount }} {{ if .ReblogsCount | eq 1 }}boost{{ else }}boosts{{ end -}}</span>
|
||||
</div>
|
||||
{{- if .Pinned }}
|
||||
<div class="stats-item">
|
||||
<i class="fa fa-thumb-tack" aria-hidden="true"></i>
|
||||
<span class="sr-only">pinned</span>
|
||||
</div>
|
||||
{{- end }}
|
||||
{{- if .LanguageTag.DisplayStr }}
|
||||
<div aria-hidden="true" class="stats-item language" title="Language: {{ .LanguageTag.DisplayStr }}">{{ .LanguageTag.TagStr }}</div>
|
||||
{{- end }}
|
||||
</div>
|
||||
</div>
|
||||
<aside class="status-info" aria-hidden="true">
|
||||
{{- include "status_info.tmpl" . | indent 1 }}
|
||||
</aside>
|
||||
{{- if .Local }}
|
||||
<a
|
||||
href="{{- .URL -}}"
|
||||
class="status-link"
|
||||
data-nosnippet
|
||||
title="Open thread at this post"
|
||||
>
|
||||
Open thread at this post
|
||||
</a>
|
||||
{{- else }}
|
||||
<a
|
||||
href="{{- .URL -}}"
|
||||
class="status-link"
|
||||
data-nosnippet
|
||||
rel="nofollow noreferrer noopener" target="_blank"
|
||||
title="Open remote post (opens in a new window)"
|
||||
>
|
||||
Open remote post (opens in a new window)
|
||||
</a>
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -22,11 +22,51 @@
|
|||
To use this template, pass a web view status into it.
|
||||
*/ -}}
|
||||
|
||||
{{ with .MediaAttachments }}
|
||||
<div class="media photoswipe-gallery {{ (len .) | oddOrEven }} {{ if eq (len .) 1 }}single{{ else if eq (len .) 2 }}double{{- end -}}">
|
||||
{{- define "imagePreview" }}
|
||||
<img
|
||||
src="{{- .PreviewURL -}}"
|
||||
loading="lazy"
|
||||
{{- if .Description }}
|
||||
alt="{{- .Description -}}"
|
||||
title="{{- .Description -}}"
|
||||
{{- end }}
|
||||
width="{{- .Meta.Original.Width -}}"
|
||||
height="{{- .Meta.Original.Height -}}"
|
||||
/>
|
||||
{{- end }}
|
||||
|
||||
{{- define "videoPreview" }}
|
||||
<video
|
||||
{{- if .Description }}
|
||||
alt="{{- .Description -}}"
|
||||
title="{{- .Description -}}"
|
||||
{{- end }}
|
||||
width="{{- .Meta.Original.Width -}}"
|
||||
height="{{- .Meta.Original.Height -}}"
|
||||
>
|
||||
<source type="video/mp4" src="{{- .URL -}}"/>
|
||||
</video>
|
||||
{{- end }}
|
||||
|
||||
{{- /* Produces something like "1 attachment", "2 attachments", etc */ -}}
|
||||
{{- define "attachmentsLength" -}}
|
||||
{{- (len .) }}{{- if eq (len .) 1 }} attachment{{- else }} attachments{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /* Produces something like "media photoswipe-gallery odd single" */ -}}
|
||||
{{- define "galleryClass" -}}
|
||||
media photoswipe-gallery {{ (len .) | oddOrEven }} {{ if eq (len .) 1 }}single{{ else if eq (len .) 2 }}double{{ end }}
|
||||
{{- end -}}
|
||||
|
||||
{{- with .MediaAttachments }}
|
||||
<div
|
||||
class="{{- template "galleryClass" . -}}"
|
||||
role="group"
|
||||
aria-label="{{- template "attachmentsLength" . -}}"
|
||||
>
|
||||
{{- range $index, $media := . }}
|
||||
<div class="media-wrapper">
|
||||
<details class="{{- $media.Type -}}-spoiler media-spoiler" {{- if not $media.Sensitive }} open{{ end -}}>
|
||||
<details class="{{- $media.Type -}}-spoiler media-spoiler" {{- if not $media.Sensitive }} open{{- end -}}>
|
||||
<summary>
|
||||
<div class="show sensitive button" aria-hidden="true">Show sensitive media</div>
|
||||
<span class="eye button" role="button" tabindex="0" aria-label="Toggle media">
|
||||
|
@ -34,11 +74,9 @@
|
|||
<i class="show fa fa-fw fa-eye" aria-hidden="true"></i>
|
||||
</span>
|
||||
{{- if eq .Type "video" }}
|
||||
<video {{- if .Description }} title="{{- $media.Description -}}" {{- end -}}>
|
||||
<source type="video/mp4" src="{{- $media.URL -}}"/>
|
||||
</video>
|
||||
{{- include "videoPreview" $media | indent 4 }}
|
||||
{{- else if eq .Type "image" }}
|
||||
<img src="{{- $media.PreviewURL -}}" {{- if .Description }} title="{{- $media.Description -}}" {{- end }}/>
|
||||
{{- include "imagePreview" $media | indent 4 }}
|
||||
{{- end }}
|
||||
</summary>
|
||||
{{- if eq .Type "video" }}
|
||||
|
@ -64,20 +102,24 @@
|
|||
data-pswp-height="{{- $media.Meta.Original.Height -}}px"
|
||||
data-cropped="true"
|
||||
{{- if .Description }}
|
||||
alt="{{- $media.Description -}}"
|
||||
title="{{- $media.Description -}}"
|
||||
{{- end }}
|
||||
>
|
||||
<img src="{{$media.PreviewURL}}" {{if .Description}}alt="{{$media.Description}}" {{end}} />
|
||||
{{- with $media }}
|
||||
{{- include "imagePreview" . | indent 4 }}
|
||||
{{- end }}
|
||||
</a>
|
||||
{{- else }}
|
||||
<a
|
||||
class="unknown-attachment"
|
||||
href="{{- $media.RemoteURL -}}"
|
||||
rel="nofollow noreferrer noopener"
|
||||
target="_blank"
|
||||
{{- if .Description }}
|
||||
title="Link to external media: {{ $media.Description -}} {{- $media.RemoteURL -}}"
|
||||
title="Open external media: {{ $media.Description -}} {{- $media.RemoteURL -}}"
|
||||
{{- else }}
|
||||
title="Link to external media. {{- $media.RemoteURL -}}"
|
||||
title="Open external media. {{- $media.RemoteURL -}}"
|
||||
{{- end }}
|
||||
>
|
||||
<div class="placeholder" aria-hidden="true">
|
||||
|
@ -90,5 +132,5 @@
|
|||
</details>
|
||||
</div>
|
||||
{{- end }}
|
||||
</div>
|
||||
</div>
|
||||
{{- end }}
|
55
web/template/status_attributes.tmpl
Normal file
55
web/template/status_attributes.tmpl
Normal file
|
@ -0,0 +1,55 @@
|
|||
{{- /*
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
*/ -}}
|
||||
|
||||
{{- define "ariaLabel" -}}
|
||||
@{{ .Account.Acct -}}, {{ timestamp .CreatedAt -}}
|
||||
{{- if .LanguageTag -}}
|
||||
, language {{ .LanguageTag.DisplayStr -}}
|
||||
{{- end -}}
|
||||
{{- if .MediaAttachments -}}
|
||||
, has media
|
||||
{{- end -}}
|
||||
{{- if .RepliesCount -}}
|
||||
{{- if eq .RepliesCount 1 -}}
|
||||
, 1 reply
|
||||
{{- else -}}
|
||||
, {{ .RepliesCount }} replies
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if .FavouritesCount -}}
|
||||
{{- if eq .FavouritesCount 1 -}}
|
||||
, 1 favourite
|
||||
{{- else -}}
|
||||
, {{ .FavouritesCount }} favourites
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if .ReblogsCount -}}
|
||||
{{- if eq .ReblogsCount 1 -}}
|
||||
, 1 boost
|
||||
{{- else -}}
|
||||
, {{ .ReblogsCount }} boosts
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- with . }}
|
||||
id="{{- .ID -}}{{- if .Pinned -}}-pinned{{- end -}}"
|
||||
role="region"
|
||||
aria-label="{{- template "ariaLabel" . -}}"
|
||||
{{- end }}
|
56
web/template/status_header.tmpl
Normal file
56
web/template/status_header.tmpl
Normal file
|
@ -0,0 +1,56 @@
|
|||
{{- /*
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
*/ -}}
|
||||
|
||||
{{- with .Account }}
|
||||
<address>
|
||||
{{- if $.Local }}
|
||||
<a
|
||||
href="{{- .URL -}}"
|
||||
rel="author"
|
||||
title="Open profile"
|
||||
>
|
||||
{{- else }}
|
||||
<a
|
||||
href="{{- .URL -}}"
|
||||
rel="author nofollow noreferrer noopener" target="_blank"
|
||||
title="Open remote profile (opens in a new window)"
|
||||
>
|
||||
{{- end }}
|
||||
<img
|
||||
class="avatar"
|
||||
aria-hidden="true"
|
||||
src="{{- .Avatar -}}"
|
||||
alt="Avatar for {{ .Username -}}"
|
||||
title="Avatar for {{ .Username -}}"
|
||||
>
|
||||
<div class="author-strap">
|
||||
<span class="displayname text-cutoff">
|
||||
{{- if .DisplayName -}}
|
||||
{{- emojify .Emojis (escape .DisplayName) -}}
|
||||
{{- else -}}
|
||||
{{- .Username -}}
|
||||
{{- end -}}
|
||||
</span>
|
||||
<span class="sr-only">,</span>
|
||||
<span class="username text-cutoff">@{{- .Acct -}}</span>
|
||||
</div>
|
||||
<span class="sr-only">(open profile)</span>
|
||||
</a>
|
||||
</address>
|
||||
{{- end }}
|
74
web/template/status_info.tmpl
Normal file
74
web/template/status_info.tmpl
Normal file
|
@ -0,0 +1,74 @@
|
|||
{{- /*
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
*/ -}}
|
||||
|
||||
{{- with . }}
|
||||
<dl class="status-stats">
|
||||
<div class="stats-grouping">
|
||||
<div class="stats-item published-at text-cutoff">
|
||||
<dt class="sr-only">Published</dt>
|
||||
<dd>
|
||||
<time datetime="{{- .CreatedAt -}}">{{- .CreatedAt | timestampPrecise -}}</time>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="stats-grouping">
|
||||
<div class="stats-item" title="Replies">
|
||||
<dt>
|
||||
<span class="sr-only">Replies</span>
|
||||
<i class="fa fa-reply-all" aria-hidden="true"></i>
|
||||
</dt>
|
||||
<dd>{{- .RepliesCount -}}</dd>
|
||||
</div>
|
||||
<div class="stats-item" title="Faves">
|
||||
<dt>
|
||||
<span class="sr-only">Favourites</span>
|
||||
<i class="fa fa-star" aria-hidden="true"></i>
|
||||
</dt>
|
||||
<dd>{{- .FavouritesCount -}}</dd>
|
||||
</div>
|
||||
<div class="stats-item" title="Boosts">
|
||||
<dt>
|
||||
<span class="sr-only">Reblogs</span>
|
||||
<i class="fa fa-retweet" aria-hidden="true"></i>
|
||||
</dt>
|
||||
<dd>{{- .ReblogsCount -}}</dd>
|
||||
</div>
|
||||
{{- if .Pinned }}
|
||||
<div class="stats-item" title="Pinned">
|
||||
<dt>
|
||||
<span class="sr-only">Pinned</span>
|
||||
<i class="fa fa-thumb-tack" aria-hidden="true"></i>
|
||||
</dt>
|
||||
<dd class="sr-only">{{- .Pinned -}}</dd>
|
||||
</div>
|
||||
{{- else }}
|
||||
{{- end }}
|
||||
</div>
|
||||
</div>
|
||||
{{- if .LanguageTag.DisplayStr }}
|
||||
<div class="stats-item language" title="{{ .LanguageTag.DisplayStr }}">
|
||||
<dt class="sr-only">Language</dt>
|
||||
<dd>
|
||||
<span class="sr-only">{{ .LanguageTag.DisplayStr }}</span>
|
||||
<span aria-hidden="true">{{- .LanguageTag.TagStr -}}</span>
|
||||
</dd>
|
||||
</div>
|
||||
{{- else }}
|
||||
{{- end }}
|
||||
</dl>
|
||||
{{- end }}
|
|
@ -22,7 +22,16 @@
|
|||
To use this template, pass a web view status into it.
|
||||
*/ -}}
|
||||
|
||||
<figure class="poll">
|
||||
{{- define "votes" -}}
|
||||
{{- if eq . 1 -}}
|
||||
{{- . -}} vote
|
||||
{{- else -}}
|
||||
{{- . }} votes
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- with . }}
|
||||
<figure class="poll">
|
||||
<figcaption class="poll-info">
|
||||
<span class="poll-expiry">
|
||||
{{- if .Poll.Multiple -}}
|
||||
|
@ -31,38 +40,42 @@
|
|||
Poll
|
||||
{{- end -}}
|
||||
{{- if .Poll.Expired -}}
|
||||
closed {{ .Poll.ExpiresAt | timestampPrecise -}}
|
||||
closed <time datetime="{{- .Poll.ExpiresAt -}}">{{- .Poll.ExpiresAt | timestampPrecise -}}</time>
|
||||
{{- else if .Poll.ExpiresAt -}}
|
||||
open until {{ .Poll.ExpiresAt | timestampPrecise -}}
|
||||
open until <time datetime="{{- .Poll.ExpiresAt -}}">{{- .Poll.ExpiresAt | timestampPrecise -}}</time>
|
||||
{{- else -}}
|
||||
open forever
|
||||
{{- end -}}
|
||||
</span>
|
||||
<span class="total-votes">Total votes: {{ .Poll.VotesCount }}</span>
|
||||
<span class="sr-only">,</span>
|
||||
<span class="total-votes">
|
||||
{{- template "votes" .Poll.VotesCount -}}
|
||||
{{- if .Poll.Expired -}}
|
||||
total
|
||||
{{- else -}}
|
||||
so far
|
||||
{{- end -}}
|
||||
</span>
|
||||
</figcaption>
|
||||
<ul class="poll-options">
|
||||
<ul class="poll-options nodot">
|
||||
{{- range $index, $pollOption := .WebPollOptions }}
|
||||
<li class="poll-option">
|
||||
<label aria-hidden="true" for="poll-{{- $pollOption.PollID -}}-option-{{- increment $index -}}" lang="{{- .LanguageTag.TagStr -}}">{{- emojify .Emojis (noescape $pollOption.Title) -}}</label>
|
||||
<meter aria-hidden="true" id="poll-{{- $pollOption.PollID -}}-option-{{- increment $index -}}" min="0" max="100" value="{{- $pollOption.VoteShare -}}">{{- $pollOption.VoteShare -}}%</meter>
|
||||
<div class="sr-only">Option {{ increment $index }}: <span lang="{{ .LanguageTag.TagStr }}">{{ emojify .Emojis (noescape $pollOption.Title) -}}</span></div>
|
||||
<span class="sr-only">Option {{ increment $index }},</span>
|
||||
<span lang="{{- .LanguageTag.TagStr -}}">{{ emojify .Emojis (noescape $pollOption.Title) }}</span>
|
||||
<meter aria-hidden="true" min="0" max="100" value="{{- $pollOption.VoteShare -}}"></meter>
|
||||
<div class="poll-vote-summary">
|
||||
{{- if isNil $pollOption.VotesCount }}
|
||||
Results not yet published.
|
||||
{{- else -}}
|
||||
{{- else }}
|
||||
{{- with deref $pollOption.VotesCount }}
|
||||
<span class="poll-vote-share">{{- $pollOption.VoteShareStr -}}%</span>
|
||||
<span class="poll-vote-count">
|
||||
{{- if eq . 1 -}}
|
||||
{{- . }} vote
|
||||
{{- else -}}
|
||||
{{- . }} votes
|
||||
{{- end -}}
|
||||
</span>
|
||||
{{- end -}}
|
||||
<span class="sr-only">,</span>
|
||||
<span class="poll-vote-count">{{- template "votes" . -}}</span>
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
</div>
|
||||
</li>
|
||||
{{- end }}
|
||||
</ul>
|
||||
</figure>
|
||||
</figure>
|
||||
{{- end }}
|
|
@ -17,11 +17,13 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
{{ template "header.tmpl" .}}
|
||||
|
||||
<main class="thread">
|
||||
<h2 id="tag-name" tabindex="-1">#{{.tagName}}</h2>
|
||||
<p>There's nothing here yet!</p>
|
||||
{{- with . }}
|
||||
<main>
|
||||
<h2 id="tag-name" tabindex="-1">#{{- .tagName -}}</h2>
|
||||
<p>There's nothing here!</p>
|
||||
<p>
|
||||
For privacy reasons, GoToSocial doesn't (yet) implement public web views of tag timelines.
|
||||
To soften the blow, here's a tongue twister: "I squeeze the soft sloth often in the mothy loft" 🦥
|
||||
</p>
|
||||
</main>
|
||||
|
||||
{{ template "footer.tmpl" .}}
|
||||
{{- end }}
|
|
@ -17,22 +17,45 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
{{ template "header.tmpl" .}}
|
||||
<main>
|
||||
<section data-nosnippet class="thread">
|
||||
{{range .context.Ancestors}}
|
||||
<article class="toot" id="{{.ID}}">
|
||||
{{ template "status.tmpl" .}}
|
||||
{{- define "threadLength" -}}
|
||||
{{- with $length := add (len $.context.Ancestors) (len $.context.Descendants) | increment -}}
|
||||
{{- if eq $length 1 -}}
|
||||
{{- $length }} post
|
||||
{{- else -}}
|
||||
{{- $length }} posts
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- with . }}
|
||||
<main data-nosnippet class="thread" aria-labelledby="thread-summary">
|
||||
<div class="col-header">
|
||||
<h2 id="thread-summary">Thread with {{ template "threadLength" . -}}</h2>
|
||||
<a href="#{{- .status.ID -}}">jump to expanded post</a>
|
||||
</div>
|
||||
{{- range .context.Ancestors }}
|
||||
<article
|
||||
class="status"
|
||||
{{- includeAttr "status_attributes.tmpl" . | indentAttr 2 }}
|
||||
>
|
||||
{{- include "status.tmpl" . | indent 2 }}
|
||||
</article>
|
||||
{{end}}
|
||||
<article class="toot expanded" id="{{.status.ID}}">
|
||||
{{ template "status.tmpl" .status}}
|
||||
{{- end }}
|
||||
{{- with .status }}
|
||||
<article
|
||||
class="status expanded"
|
||||
{{- includeAttr "status_attributes.tmpl" . | indentAttr 2 }}
|
||||
>
|
||||
{{- include "status.tmpl" . | indent 2 }}
|
||||
</article>
|
||||
{{range .context.Descendants}}
|
||||
<article class="toot" id="{{.ID}}">
|
||||
{{ template "status.tmpl" .}}
|
||||
{{- end }}
|
||||
{{- range .context.Descendants }}
|
||||
<article
|
||||
class="status"
|
||||
{{- includeAttr "status_attributes.tmpl" . | indentAttr 2 }}
|
||||
>
|
||||
{{- include "status.tmpl" . | indent 2 }}
|
||||
</article>
|
||||
{{end}}
|
||||
</section>
|
||||
{{- end }}
|
||||
</main>
|
||||
{{ template "footer.tmpl" .}}
|
||||
{{- end }}
|
Loading…
Reference in a new issue