mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-23 17:56:20 +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
|
- static_build
|
||||||
- kvformat
|
- kvformat
|
||||||
- timetzdata
|
- timetzdata
|
||||||
|
- >-
|
||||||
|
{{ if and (index .Env "DEBUG") (.Env.DEBUG) }}debugenv{{ end }}
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
goos:
|
goos:
|
||||||
|
|
|
@ -144,17 +144,25 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// the authorize template will display a form to the user where they can get some information
|
// The authorize template will display a form
|
||||||
// about the app that's trying to authorize, and the scope of the request.
|
// to the user where they can see some info
|
||||||
// They can then approve it if it looks OK to them, which will POST to the AuthorizePOSTHandler
|
// about the app that's trying to authorize,
|
||||||
c.HTML(http.StatusOK, "authorize.tmpl", gin.H{
|
// and the scope of the request. They can then
|
||||||
"appname": app.Name,
|
// approve it if it looks OK to them, which
|
||||||
"appwebsite": app.Website,
|
// will POST to the AuthorizePOSTHandler.
|
||||||
"redirect": redirect,
|
page := apiutil.WebPage{
|
||||||
"scope": scope,
|
Template: "authorize.tmpl",
|
||||||
"user": acct.Username,
|
Instance: instance,
|
||||||
"instance": instance,
|
Extra: map[string]any{
|
||||||
})
|
"appname": app.Name,
|
||||||
|
"appwebsite": app.Website,
|
||||||
|
"redirect": redirect,
|
||||||
|
"scope": scope,
|
||||||
|
"user": acct.Username,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
apiutil.TemplateWebPage(c, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizePOSTHandler should be served as POST at https://example.org/oauth/authorize
|
// 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)
|
apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "finalize.tmpl", gin.H{
|
|
||||||
"instance": instance,
|
page := apiutil.WebPage{
|
||||||
"name": claims.Name,
|
Template: "finalize.tmpl",
|
||||||
"preferredUsername": claims.PreferredUsername,
|
Instance: instance,
|
||||||
})
|
Extra: map[string]any{
|
||||||
|
"name": claims.Name,
|
||||||
|
"preferredUsername": claims.PreferredUsername,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
apiutil.TemplateWebPage(c, page)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.Set(sessionUserID, user.ID)
|
s.Set(sessionUserID, user.ID)
|
||||||
|
@ -177,12 +183,18 @@ func (m *Module) FinalizePOSTHandler(c *gin.Context) {
|
||||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "finalize.tmpl", gin.H{
|
|
||||||
"instance": instance,
|
page := apiutil.WebPage{
|
||||||
"name": form.Name,
|
Template: "finalize.tmpl",
|
||||||
"preferredUsername": form.Username,
|
Instance: instance,
|
||||||
"error": err,
|
Extra: map[string]any{
|
||||||
})
|
"name": form.Name,
|
||||||
|
"preferredUsername": form.Username,
|
||||||
|
"error": err,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
apiutil.TemplateWebPage(c, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the username conforms to the spec
|
// check if the username conforms to the spec
|
||||||
|
|
|
@ -21,7 +21,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"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
|
// we're done with the session now, so just clear it out
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "oob.tmpl", gin.H{
|
page := apiutil.WebPage{
|
||||||
"instance": instance,
|
Template: "oob.tmpl",
|
||||||
"user": acct.Username,
|
Instance: instance,
|
||||||
"oobToken": oobToken,
|
Extra: map[string]any{
|
||||||
"scope": scope,
|
"user": acct.Username,
|
||||||
})
|
"oobToken": oobToken,
|
||||||
|
"scope": scope,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
apiutil.TemplateWebPage(c, page)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,8 +32,8 @@ import (
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// login just wraps a form-submitted username (we want an email) and password
|
// signIn just wraps a form-submitted username (we want an email) and password
|
||||||
type login struct {
|
type signIn struct {
|
||||||
Email string `form:"username"`
|
Email string `form:"username"`
|
||||||
Password string `form:"password"`
|
Password string `form:"password"`
|
||||||
}
|
}
|
||||||
|
@ -55,10 +55,12 @@ func (m *Module) SignInGETHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// no idp provider, use our own funky little sign in page
|
page := apiutil.WebPage{
|
||||||
c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{
|
Template: "sign-in.tmpl",
|
||||||
"instance": instance,
|
Instance: instance,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
apiutil.TemplateWebPage(c, page)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +85,7 @@ func (m *Module) SignInGETHandler(c *gin.Context) {
|
||||||
func (m *Module) SignInPOSTHandler(c *gin.Context) {
|
func (m *Module) SignInPOSTHandler(c *gin.Context) {
|
||||||
s := sessions.Default(c)
|
s := sessions.Default(c)
|
||||||
|
|
||||||
form := &login{}
|
form := &signIn{}
|
||||||
if err := c.ShouldBind(form); err != nil {
|
if err := c.ShouldBind(form); err != nil {
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGetV1)
|
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 {
|
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)
|
return incorrectPassword(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -116,6 +116,12 @@ type Status struct {
|
||||||
//
|
//
|
||||||
// swagger:ignore
|
// swagger:ignore
|
||||||
WebPollOptions []WebPollOption `json:"-"`
|
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)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.HTML(http.StatusNotFound, "404.tmpl", gin.H{
|
template404Page(c,
|
||||||
"instance": instance,
|
instance,
|
||||||
"requestID": gtscontext.RequestID(ctx),
|
gtscontext.RequestID(ctx),
|
||||||
})
|
)
|
||||||
default:
|
default:
|
||||||
JSON(c, http.StatusNotFound, map[string]string{
|
JSON(c, http.StatusNotFound, map[string]string{
|
||||||
"error": errWithCode.Safe(),
|
"error": errWithCode.Safe(),
|
||||||
|
@ -73,12 +73,12 @@ func genericErrorHandler(c *gin.Context, instanceGet func(ctx context.Context) (
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.HTML(errWithCode.Code(), "error.tmpl", gin.H{
|
templateErrorPage(c,
|
||||||
"instance": instance,
|
instance,
|
||||||
"code": errWithCode.Code(),
|
errWithCode.Code(),
|
||||||
"error": errWithCode.Safe(),
|
errWithCode.Safe(),
|
||||||
"requestID": gtscontext.RequestID(ctx),
|
gtscontext.RequestID(ctx),
|
||||||
})
|
)
|
||||||
default:
|
default:
|
||||||
JSON(c, errWithCode.Code(), map[string]string{
|
JSON(c, errWithCode.Code(), map[string]string{
|
||||||
"error": errWithCode.Safe(),
|
"error": errWithCode.Safe(),
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package web
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"html"
|
"html"
|
||||||
|
@ -28,10 +28,10 @@ import (
|
||||||
|
|
||||||
const maxOGDescriptionLength = 300
|
const maxOGDescriptionLength = 300
|
||||||
|
|
||||||
// ogMeta represents supported OpenGraph Meta tags
|
// OGMeta represents supported OpenGraph Meta tags
|
||||||
//
|
//
|
||||||
// see eg https://ogp.me/
|
// see eg https://ogp.me/
|
||||||
type ogMeta struct {
|
type OGMeta struct {
|
||||||
// vanilla og tags
|
// vanilla og tags
|
||||||
Title string // og:title
|
Title string // og:title
|
||||||
Type string // og:type
|
Type string // og:type
|
||||||
|
@ -56,23 +56,23 @@ type ogMeta struct {
|
||||||
ProfileUsername string // profile:username
|
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
|
// the base root of an instance. It also serves as a
|
||||||
// foundation for building account / status ogMeta on
|
// foundation for building account / status ogMeta on
|
||||||
// top of.
|
// top of.
|
||||||
func ogBase(instance *apimodel.InstanceV1) *ogMeta {
|
func OGBase(instance *apimodel.InstanceV1) *OGMeta {
|
||||||
var locale string
|
var locale string
|
||||||
if len(instance.Languages) > 0 {
|
if len(instance.Languages) > 0 {
|
||||||
locale = instance.Languages[0]
|
locale = instance.Languages[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
og := &ogMeta{
|
og := &OGMeta{
|
||||||
Title: text.SanitizeToPlaintext(instance.Title) + " - GoToSocial",
|
Title: text.SanitizeToPlaintext(instance.Title) + " - GoToSocial",
|
||||||
Type: "website",
|
Type: "website",
|
||||||
Locale: locale,
|
Locale: locale,
|
||||||
URL: instance.URI,
|
URL: instance.URI,
|
||||||
SiteName: instance.AccountDomain,
|
SiteName: instance.AccountDomain,
|
||||||
Description: parseDescription(instance.ShortDescription),
|
Description: ParseDescription(instance.ShortDescription),
|
||||||
|
|
||||||
Image: instance.Thumbnail,
|
Image: instance.Thumbnail,
|
||||||
ImageAlt: instance.ThumbnailDescription,
|
ImageAlt: instance.ThumbnailDescription,
|
||||||
|
@ -81,15 +81,15 @@ func ogBase(instance *apimodel.InstanceV1) *ogMeta {
|
||||||
return og
|
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
|
// struct specific to that account. It's suitable for serving
|
||||||
// at account profile pages.
|
// at account profile pages.
|
||||||
func (og *ogMeta) withAccount(account *apimodel.Account) *ogMeta {
|
func (og *OGMeta) WithAccount(account *apimodel.Account) *OGMeta {
|
||||||
og.Title = parseTitle(account, og.SiteName)
|
og.Title = AccountTitle(account, og.SiteName)
|
||||||
og.Type = "profile"
|
og.Type = "profile"
|
||||||
og.URL = account.URL
|
og.URL = account.URL
|
||||||
if account.Note != "" {
|
if account.Note != "" {
|
||||||
og.Description = parseDescription(account.Note)
|
og.Description = ParseDescription(account.Note)
|
||||||
} else {
|
} else {
|
||||||
og.Description = `content="This GoToSocial user hasn't written a bio yet!"`
|
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
|
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
|
// struct specific to that status. It's suitable for serving
|
||||||
// at status pages.
|
// at status pages.
|
||||||
func (og *ogMeta) withStatus(status *apimodel.Status) *ogMeta {
|
func (og *OGMeta) WithStatus(status *apimodel.Status) *OGMeta {
|
||||||
og.Title = "Post by " + parseTitle(status.Account, og.SiteName)
|
og.Title = "Post by " + AccountTitle(status.Account, og.SiteName)
|
||||||
og.Type = "article"
|
og.Type = "article"
|
||||||
if status.Language != nil {
|
if status.Language != nil {
|
||||||
og.Locale = *status.Language
|
og.Locale = *status.Language
|
||||||
|
@ -114,9 +114,9 @@ func (og *ogMeta) withStatus(status *apimodel.Status) *ogMeta {
|
||||||
og.URL = status.URL
|
og.URL = status.URL
|
||||||
switch {
|
switch {
|
||||||
case status.SpoilerText != "":
|
case status.SpoilerText != "":
|
||||||
og.Description = parseDescription("CW: " + status.SpoilerText)
|
og.Description = ParseDescription("CW: " + status.SpoilerText)
|
||||||
case status.Text != "":
|
case status.Text != "":
|
||||||
og.Description = parseDescription(status.Text)
|
og.Description = ParseDescription(status.Text)
|
||||||
default:
|
default:
|
||||||
og.Description = og.Title
|
og.Description = og.Title
|
||||||
}
|
}
|
||||||
|
@ -147,34 +147,38 @@ func (og *ogMeta) withStatus(status *apimodel.Status) *ogMeta {
|
||||||
return og
|
return og
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseTitle parses a page title from account and accountDomain
|
// AccountTitle parses a page title from account and accountDomain
|
||||||
func parseTitle(account *apimodel.Account, accountDomain string) string {
|
func AccountTitle(account *apimodel.Account, accountDomain string) string {
|
||||||
user := "@" + account.Acct + "@" + accountDomain
|
user := "@" + account.Acct + "@" + accountDomain
|
||||||
|
|
||||||
if len(account.DisplayName) == 0 {
|
if len(account.DisplayName) == 0 {
|
||||||
return user
|
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.
|
// safe to use as a template.HTMLAttr inside templates.
|
||||||
func parseDescription(in string) string {
|
func ParseDescription(in string) string {
|
||||||
i := text.SanitizeToPlaintext(in)
|
i := text.SanitizeToPlaintext(in)
|
||||||
i = strings.ReplaceAll(i, "\n", " ")
|
i = strings.ReplaceAll(i, "\n", " ")
|
||||||
i = strings.Join(strings.Fields(i), " ")
|
i = strings.Join(strings.Fields(i), " ")
|
||||||
i = html.EscapeString(i)
|
i = html.EscapeString(i)
|
||||||
i = strings.ReplaceAll(i, `\`, "\")
|
i = strings.ReplaceAll(i, `\`, "\")
|
||||||
i = trim(i, maxOGDescriptionLength)
|
i = truncate(i, maxOGDescriptionLength)
|
||||||
return `content="` + i + `"`
|
return `content="` + i + `"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// trim strings trim s to specified length
|
// truncate trims given string to
|
||||||
func trim(s string, length int) string {
|
// specified length (in runes).
|
||||||
if len(s) < length {
|
func truncate(s string, l int) string {
|
||||||
|
r := []rune(s)
|
||||||
|
if len(r) < l {
|
||||||
|
// No need
|
||||||
|
// to trim.
|
||||||
return s
|
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
|
// 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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package web
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -40,18 +40,18 @@ func (suite *OpenGraphTestSuite) TestParseDescription() {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
tt := tt
|
tt := tt
|
||||||
suite.Run(tt.name, func() {
|
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() {
|
func (suite *OpenGraphTestSuite) TestWithAccountWithNote() {
|
||||||
baseMeta := ogBase(&apimodel.InstanceV1{
|
baseMeta := OGBase(&apimodel.InstanceV1{
|
||||||
AccountDomain: "example.org",
|
AccountDomain: "example.org",
|
||||||
Languages: []string{"en"},
|
Languages: []string{"en"},
|
||||||
})
|
})
|
||||||
|
|
||||||
accountMeta := baseMeta.withAccount(&apimodel.Account{
|
accountMeta := baseMeta.WithAccount(&apimodel.Account{
|
||||||
Acct: "example_account",
|
Acct: "example_account",
|
||||||
DisplayName: "example person!!",
|
DisplayName: "example person!!",
|
||||||
URL: "https://example.org/@example_account",
|
URL: "https://example.org/@example_account",
|
||||||
|
@ -59,8 +59,8 @@ func (suite *OpenGraphTestSuite) TestWithAccountWithNote() {
|
||||||
Username: "example_account",
|
Username: "example_account",
|
||||||
})
|
})
|
||||||
|
|
||||||
suite.EqualValues(ogMeta{
|
suite.EqualValues(OGMeta{
|
||||||
Title: "example person!! (@example_account@example.org)",
|
Title: "example person!!, @example_account@example.org",
|
||||||
Type: "profile",
|
Type: "profile",
|
||||||
Locale: "en",
|
Locale: "en",
|
||||||
URL: "https://example.org/@example_account",
|
URL: "https://example.org/@example_account",
|
||||||
|
@ -79,12 +79,12 @@ func (suite *OpenGraphTestSuite) TestWithAccountWithNote() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *OpenGraphTestSuite) TestWithAccountNoNote() {
|
func (suite *OpenGraphTestSuite) TestWithAccountNoNote() {
|
||||||
baseMeta := ogBase(&apimodel.InstanceV1{
|
baseMeta := OGBase(&apimodel.InstanceV1{
|
||||||
AccountDomain: "example.org",
|
AccountDomain: "example.org",
|
||||||
Languages: []string{"en"},
|
Languages: []string{"en"},
|
||||||
})
|
})
|
||||||
|
|
||||||
accountMeta := baseMeta.withAccount(&apimodel.Account{
|
accountMeta := baseMeta.WithAccount(&apimodel.Account{
|
||||||
Acct: "example_account",
|
Acct: "example_account",
|
||||||
DisplayName: "example person!!",
|
DisplayName: "example person!!",
|
||||||
URL: "https://example.org/@example_account",
|
URL: "https://example.org/@example_account",
|
||||||
|
@ -92,8 +92,8 @@ func (suite *OpenGraphTestSuite) TestWithAccountNoNote() {
|
||||||
Username: "example_account",
|
Username: "example_account",
|
||||||
})
|
})
|
||||||
|
|
||||||
suite.EqualValues(ogMeta{
|
suite.EqualValues(OGMeta{
|
||||||
Title: "example person!! (@example_account@example.org)",
|
Title: "example person!!, @example_account@example.org",
|
||||||
Type: "profile",
|
Type: "profile",
|
||||||
Locale: "en",
|
Locale: "en",
|
||||||
URL: "https://example.org/@example_account",
|
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
|
OOBTokenPath = "/oauth/oob" // #nosec G101 else we get a hardcoded credentials warning
|
||||||
// HelpfulAdvice is a handy hint to users;
|
// HelpfulAdvice is a handy hint to users;
|
||||||
// particularly important during the login flow
|
// 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"
|
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 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"
|
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
|
// 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)
|
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() {
|
func (suite *GetRSSTestSuite) TestGetAccountRSSZork() {
|
||||||
|
|
|
@ -83,7 +83,6 @@ func New(ctx context.Context) (*Router, error) {
|
||||||
|
|
||||||
// Attach functions used by HTML templating,
|
// Attach functions used by HTML templating,
|
||||||
// and load HTML templates into the engine.
|
// and load HTML templates into the engine.
|
||||||
LoadTemplateFunctions(engine)
|
|
||||||
if err := LoadTemplates(engine); err != nil {
|
if err := LoadTemplates(engine); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,52 +18,121 @@
|
||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gin-gonic/gin/render"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/regexes"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/text"
|
"github.com/superseriousbusiness/gotosocial/internal/text"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// LoadTemplates loads templates found at `web-template-base-dir`
|
||||||
justTime = "15:04"
|
// into the Gin engine, or errors if templates cannot be loaded.
|
||||||
dateYear = "Jan 02, 2006"
|
//
|
||||||
dateTime = "Jan 02, 15:04"
|
// The special functions "include" and "includeAttr" will be added
|
||||||
dateYearTime = "Jan 02, 2006, 15:04"
|
// to the template funcMap for use in any template. Use these "include"
|
||||||
monthYear = "Jan, 2006"
|
// functions when you need to pass a template through a pipeline.
|
||||||
badTimestamp = "bad timestamp"
|
// Otherwise, prefer the built-in "template" function.
|
||||||
)
|
|
||||||
|
|
||||||
// LoadTemplates loads html templates for use by the given engine
|
|
||||||
func LoadTemplates(engine *gin.Engine) error {
|
func LoadTemplates(engine *gin.Engine) error {
|
||||||
templateBaseDir := config.GetWebTemplateBaseDir()
|
templateBaseDir := config.GetWebTemplateBaseDir()
|
||||||
if templateBaseDir == "" {
|
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 {
|
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 {
|
indexTmplPath := filepath.Join(templateDirAbs, "index.tmpl")
|
||||||
return fmt.Errorf("%s doesn't seem to contain the templates; index.tmpl is missing: %w", templateBaseDir, err)
|
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
|
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 {
|
func oddOrEven(n int) string {
|
||||||
if n%2 == 0 {
|
if n%2 == 0 {
|
||||||
return "even"
|
return "even"
|
||||||
|
@ -71,21 +140,40 @@ func oddOrEven(n int) string {
|
||||||
return "odd"
|
return "odd"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// escape HTML escapes the given string,
|
||||||
|
// returning a trusted template.
|
||||||
func escape(str string) template.HTML {
|
func escape(str string) template.HTML {
|
||||||
/* #nosec G203 */
|
/* #nosec G203 */
|
||||||
return template.HTML(template.HTMLEscapeString(str))
|
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 {
|
func noescape(str string) template.HTML {
|
||||||
/* #nosec G203 */
|
/* #nosec G203 */
|
||||||
return template.HTML(str)
|
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 {
|
func noescapeAttr(str string) template.HTMLAttr {
|
||||||
/* #nosec G203 */
|
/* #nosec G203 */
|
||||||
return template.HTMLAttr(str)
|
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 {
|
func timestamp(stamp string) string {
|
||||||
t, err := util.ParseISO8601(stamp)
|
t, err := util.ParseISO8601(stamp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -127,38 +215,55 @@ func timestampVague(stamp string) string {
|
||||||
return t.Format(monthYear)
|
return t.Format(monthYear)
|
||||||
}
|
}
|
||||||
|
|
||||||
type iconWithLabel struct {
|
|
||||||
faIcon string
|
|
||||||
label string
|
|
||||||
}
|
|
||||||
|
|
||||||
func visibilityIcon(visibility apimodel.Visibility) template.HTML {
|
func visibilityIcon(visibility apimodel.Visibility) template.HTML {
|
||||||
var icon iconWithLabel
|
var (
|
||||||
|
label string
|
||||||
|
icon string
|
||||||
|
)
|
||||||
|
|
||||||
switch visibility {
|
switch visibility {
|
||||||
case apimodel.VisibilityPublic:
|
case apimodel.VisibilityPublic:
|
||||||
icon = iconWithLabel{"globe", "public"}
|
label = "public"
|
||||||
|
icon = "globe"
|
||||||
case apimodel.VisibilityUnlisted:
|
case apimodel.VisibilityUnlisted:
|
||||||
icon = iconWithLabel{"unlock", "unlisted"}
|
label = "unlisted"
|
||||||
|
icon = "unlock"
|
||||||
case apimodel.VisibilityPrivate:
|
case apimodel.VisibilityPrivate:
|
||||||
icon = iconWithLabel{"lock", "private"}
|
label = "private"
|
||||||
|
icon = "lock"
|
||||||
case apimodel.VisibilityMutualsOnly:
|
case apimodel.VisibilityMutualsOnly:
|
||||||
icon = iconWithLabel{"handshake-o", "mutuals only"}
|
label = "mutuals-only"
|
||||||
|
icon = "handshake-o"
|
||||||
case apimodel.VisibilityDirect:
|
case apimodel.VisibilityDirect:
|
||||||
icon = iconWithLabel{"envelope", "direct"}
|
label = "direct"
|
||||||
|
icon = "envelope"
|
||||||
}
|
}
|
||||||
|
|
||||||
/* #nosec G203 */
|
/* #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
|
// emojify replaces emojis in the given
|
||||||
func emojify(emojis []apimodel.Emoji, inputText template.HTML) template.HTML {
|
// html fragment with suitable <img> tags.
|
||||||
out := text.Emojify(emojis, string(inputText))
|
//
|
||||||
|
// 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 */
|
// demojify replaces emoji shortcodes in
|
||||||
// (this is escaped above)
|
// the given fragment with empty strings.
|
||||||
return template.HTML(out)
|
//
|
||||||
|
// Output must then be escaped as appropriate.
|
||||||
|
func demojify(input string) string {
|
||||||
|
return text.Demojify(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
func acctInstance(acct string) string {
|
func acctInstance(acct string) string {
|
||||||
|
@ -170,10 +275,79 @@ func acctInstance(acct string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// increment adds 1
|
||||||
|
// to the given int.
|
||||||
func increment(i int) int {
|
func increment(i int) int {
|
||||||
return i + 1
|
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
|
// isNil will safely check if 'v' is nil without
|
||||||
// dealing with weird Go interface nil bullshit.
|
// dealing with weird Go interface nil bullshit.
|
||||||
func isNil(i interface{}) bool {
|
func isNil(i interface{}) bool {
|
||||||
|
@ -193,21 +367,3 @@ func deref(i any) any {
|
||||||
|
|
||||||
return vOf.Elem()
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"html"
|
"html"
|
||||||
|
"html/template"
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/regexes"
|
"github.com/superseriousbusiness/gotosocial/internal/regexes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Emojify replaces shortcodes in `inputText` with the emoji in `emojis`.
|
// EmojifyWeb replaces emoji shortcodes like `:example:` in the given HTML
|
||||||
//
|
// fragment with `<img>` tags suitable for rendering on the web frontend.
|
||||||
// Callers should ensure that inputText and resulting text are escaped
|
func EmojifyWeb(emojis []apimodel.Emoji, html template.HTML) template.HTML {
|
||||||
// appropriately depending on what they're used for.
|
out := emojify(
|
||||||
func Emojify(emojis []apimodel.Emoji, inputText string) string {
|
emojis,
|
||||||
emojisMap := make(map[string]apimodel.Emoji, len(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 {
|
for _, emoji := range emojis {
|
||||||
shortcode := ":" + emoji.Shortcode + ":"
|
shortcode := ":" + emoji.Shortcode + ":"
|
||||||
emojisMap[shortcode] = emoji
|
emojisMap[shortcode] = emoji
|
||||||
|
@ -39,27 +97,20 @@ func Emojify(emojis []apimodel.Emoji, inputText string) string {
|
||||||
|
|
||||||
return regexes.ReplaceAllStringFunc(
|
return regexes.ReplaceAllStringFunc(
|
||||||
regexes.EmojiFinder,
|
regexes.EmojiFinder,
|
||||||
inputText,
|
input,
|
||||||
func(shortcode string, buf *bytes.Buffer) string {
|
func(shortcode string, buf *bytes.Buffer) string {
|
||||||
// Look for emoji according to this shortcode
|
// Look for emoji with this shortcode.
|
||||||
emoji, ok := emojisMap[shortcode]
|
emoji, ok := emojisMap[shortcode]
|
||||||
if !ok {
|
if !ok {
|
||||||
return shortcode
|
return shortcode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escape raw emoji content
|
// Escape raw emoji content.
|
||||||
safeURL := html.EscapeString(emoji.URL)
|
url := html.EscapeString(emoji.URL)
|
||||||
safeCode := html.EscapeString(emoji.Shortcode)
|
code := 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"/>`)
|
|
||||||
|
|
||||||
|
// Write emoji repr to buffer.
|
||||||
|
write(url, code, buf)
|
||||||
return buf.String()
|
return buf.String()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -662,6 +662,10 @@ func (c *Converter) StatusToWebStatus(
|
||||||
return nil, err
|
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.
|
// Add additional information for template.
|
||||||
// Assume empty langs, hope for not empty language.
|
// Assume empty langs, hope for not empty language.
|
||||||
webStatus.LanguageTag = new(language.Language)
|
webStatus.LanguageTag = new(language.Language)
|
||||||
|
@ -727,6 +731,8 @@ func (c *Converter) StatusToWebStatus(
|
||||||
a.Sensitive = webStatus.Sensitive
|
a.Sensitive = webStatus.Sensitive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
webStatus.Local = *s.Local
|
||||||
|
|
||||||
return webStatus, nil
|
return webStatus, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -151,7 +151,7 @@ func (c *Converter) StatusToRSSItem(ctx context.Context, s *gtsmodel.Status) (*f
|
||||||
apiEmojis = append(apiEmojis, apiEmoji)
|
apiEmojis = append(apiEmojis, apiEmoji)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
content := text.Emojify(apiEmojis, s.Content)
|
content := text.EmojifyRSS(apiEmojis, s.Content)
|
||||||
|
|
||||||
return &feeds.Item{
|
return &feeds.Item{
|
||||||
Title: title,
|
Title: title,
|
||||||
|
|
|
@ -81,7 +81,7 @@ func (suite *InternalToRSSTestSuite) TestStatusToRSSItem2() {
|
||||||
suite.Equal("62529", item.Enclosure.Length)
|
suite.Equal("62529", item.Enclosure.Length)
|
||||||
suite.Equal("image/jpeg", item.Enclosure.Type)
|
suite.Equal("image/jpeg", item.Enclosure.Type)
|
||||||
suite.Equal("http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg", item.Enclosure.Url)
|
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() {
|
func (suite *InternalToRSSTestSuite) TestStatusToRSSItem3() {
|
||||||
|
|
|
@ -18,9 +18,10 @@
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"context"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
@ -31,20 +32,35 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Module) aboutGETHandler(c *gin.Context) {
|
func (m *Module) aboutGETHandler(c *gin.Context) {
|
||||||
instance, err := m.processor.InstanceGetV1(c.Request.Context())
|
instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context())
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
|
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "about.tmpl", gin.H{
|
// Return instance we already got from the db,
|
||||||
"instance": instance,
|
// don't try to fetch it again when erroring.
|
||||||
"languages": config.GetInstanceLanguages().DisplayStrs(),
|
instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
|
||||||
"ogMeta": ogBase(instance),
|
return instance, nil
|
||||||
"blocklistExposed": config.GetInstanceExposeSuspendedWeb(),
|
}
|
||||||
"stylesheets": []string{
|
|
||||||
assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css",
|
// 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(),
|
||||||
|
"languages": config.GetInstanceLanguages().DisplayStrs(),
|
||||||
},
|
},
|
||||||
"javascript": []string{distPathPrefix + "/frontend.js"},
|
}
|
||||||
})
|
|
||||||
|
apiutil.TemplateWebPage(c, page)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,39 +18,58 @@
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Module) confirmEmailGETHandler(c *gin.Context) {
|
func (m *Module) confirmEmailGETHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
instance, errWithCode := m.processor.InstanceGetV1(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)
|
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
instance, err := m.processor.InstanceGetV1(ctx)
|
// Return instance we already got from the db,
|
||||||
if err != nil {
|
// don't try to fetch it again when erroring.
|
||||||
apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "confirmed.tmpl", gin.H{
|
// If there's no token in the query,
|
||||||
"instance": instance,
|
// just serve the 404 web handler.
|
||||||
"email": user.Email,
|
token := c.Query("token")
|
||||||
"username": user.Account.Username,
|
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
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
|
@ -31,31 +29,29 @@ import (
|
||||||
const textCSSUTF8 = string(apiutil.TextCSS + "; charset=utf-8")
|
const textCSSUTF8 = string(apiutil.TextCSS + "; charset=utf-8")
|
||||||
|
|
||||||
func (m *Module) customCSSGETHandler(c *gin.Context) {
|
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 {
|
if _, err := apiutil.NegotiateAccept(c, apiutil.TextCSS); err != nil {
|
||||||
apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// usernames on our instance will always be lowercase
|
targetUsername, errWithCode := apiutil.ParseWebUsername(c.Param(apiutil.WebUsernameKey))
|
||||||
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)
|
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
return
|
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.Header(cacheControlHeader, cacheControlNoCache)
|
||||||
c.Data(http.StatusOK, textCSSUTF8, []byte(customCSS))
|
c.Data(http.StatusOK, textCSSUTF8, []byte(customCSS))
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,14 +18,14 @@
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -33,37 +33,44 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Module) domainBlockListGETHandler(c *gin.Context) {
|
func (m *Module) domainBlockListGETHandler(c *gin.Context) {
|
||||||
authed, err := oauth.Authed(c, false, false, false, false)
|
instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context())
|
||||||
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)
|
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "domain-blocklist.tmpl", gin.H{
|
// Return instance we already got from the db,
|
||||||
"instance": instance,
|
// don't try to fetch it again when erroring.
|
||||||
"ogMeta": ogBase(instance),
|
instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
|
||||||
"blocklist": domainBlocks,
|
return instance, nil
|
||||||
"stylesheets": []string{
|
}
|
||||||
assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css",
|
|
||||||
},
|
// We only serve text/html at this endpoint.
|
||||||
"javascript": []string{distPathPrefix + "/frontend.js"},
|
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
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Module) baseHandler(c *gin.Context) {
|
func (m *Module) indexHandler(c *gin.Context) {
|
||||||
// if a landingPageUser is set in the config, redirect to that user's profile
|
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 != "" {
|
if landingPageUser := config.GetLandingPageUser(); landingPageUser != "" {
|
||||||
c.Redirect(http.StatusFound, "/@"+strings.ToLower(landingPageUser))
|
c.Redirect(http.StatusFound, "/@"+strings.ToLower(landingPageUser))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
instance, err := m.processor.InstanceGetV1(c.Request.Context())
|
page := apiutil.WebPage{
|
||||||
if err != nil {
|
Template: "index.tmpl",
|
||||||
apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
|
Instance: instance,
|
||||||
return
|
OGMeta: apiutil.OGBase(instance),
|
||||||
|
Stylesheets: []string{cssAbout, cssIndex},
|
||||||
|
Extra: map[string]any{"showStrap": true},
|
||||||
}
|
}
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "index.tmpl", gin.H{
|
apiutil.TemplateWebPage(c, page)
|
||||||
"instance": instance,
|
|
||||||
"ogMeta": ogBase(instance),
|
|
||||||
"stylesheets": []string{
|
|
||||||
distPathPrefix + "/index.css",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
|
@ -27,7 +27,6 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
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/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
@ -141,28 +140,28 @@ func (m *Module) profileGETHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stylesheets := []string{
|
page := apiutil.WebPage{
|
||||||
assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css",
|
Template: "profile.tmpl",
|
||||||
distPathPrefix + "/status.css",
|
Instance: instance,
|
||||||
distPathPrefix + "/profile.css",
|
OGMeta: apiutil.OGBase(instance).WithAccount(targetAccount),
|
||||||
}
|
Stylesheets: []string{
|
||||||
if config.GetAccountsAllowCustomCSS() {
|
cssFA, cssStatus, cssThread, cssProfile,
|
||||||
stylesheets = append(stylesheets, "/@"+targetAccount.Username+"/custom.css")
|
// Custom CSS for this user last in cascade.
|
||||||
|
"/@" + targetAccount.Username + "/custom.css",
|
||||||
|
},
|
||||||
|
Javascript: []string{jsFrontend},
|
||||||
|
Extra: map[string]any{
|
||||||
|
"account": targetAccount,
|
||||||
|
"rssFeed": rssFeed,
|
||||||
|
"robotsMeta": robotsMeta,
|
||||||
|
"statuses": statusResp.Items,
|
||||||
|
"statuses_next": statusResp.NextLink,
|
||||||
|
"pinned_statuses": pinnedStatuses,
|
||||||
|
"show_back_to_top": paging,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "profile.tmpl", gin.H{
|
apiutil.TemplateWebPage(c, page)
|
||||||
"instance": instance,
|
|
||||||
"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"},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// returnAPAccount returns an ActivityPub representation of
|
// returnAPAccount returns an ActivityPub representation of
|
||||||
|
|
|
@ -71,7 +71,7 @@ Crawl-delay: 500
|
||||||
# API endpoints.
|
# API endpoints.
|
||||||
Disallow: /api/
|
Disallow: /api/
|
||||||
|
|
||||||
# Auth/login endpoints.
|
# Auth/Sign in endpoints.
|
||||||
Disallow: /auth/
|
Disallow: /auth/
|
||||||
Disallow: /oauth/
|
Disallow: /oauth/
|
||||||
Disallow: /check_your_email
|
Disallow: /check_your_email
|
||||||
|
|
|
@ -18,30 +18,44 @@
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"context"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Module) SettingsPanelHandler(c *gin.Context) {
|
func (m *Module) SettingsPanelHandler(c *gin.Context) {
|
||||||
instance, err := m.processor.InstanceGetV1(c.Request.Context())
|
instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context())
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
|
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "frontend.tmpl", gin.H{
|
// Return instance we already got from the db,
|
||||||
"instance": instance,
|
// don't try to fetch it again when erroring.
|
||||||
"stylesheets": []string{
|
instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
|
||||||
assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css",
|
return instance, nil
|
||||||
distPathPrefix + "/_colors.css",
|
}
|
||||||
distPathPrefix + "/base.css",
|
|
||||||
distPathPrefix + "/profile.css",
|
// We only serve text/html at this endpoint.
|
||||||
distPathPrefix + "/status.css",
|
if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil {
|
||||||
distPathPrefix + "/settings-style.css",
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
@ -56,16 +55,13 @@ func (m *Module) tagGETHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stylesheets := []string{
|
page := apiutil.WebPage{
|
||||||
assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css",
|
Template: "tag.tmpl",
|
||||||
distPathPrefix + "/status.css",
|
Instance: instance,
|
||||||
distPathPrefix + "/tag.css",
|
OGMeta: apiutil.OGBase(instance),
|
||||||
|
Stylesheets: []string{cssFA, cssThread, cssTag},
|
||||||
|
Extra: map[string]any{"tagName": tagName},
|
||||||
}
|
}
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "tag.tmpl", gin.H{
|
apiutil.TemplateWebPage(c, page)
|
||||||
"instance": instance,
|
|
||||||
"ogMeta": ogBase(instance),
|
|
||||||
"tagName": tagName,
|
|
||||||
"stylesheets": stylesheets,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,6 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
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/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
@ -139,22 +138,23 @@ func (m *Module) threadGETHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stylesheets := []string{
|
page := apiutil.WebPage{
|
||||||
assetsPathPrefix + "/Fork-Awesome/css/fork-awesome.min.css",
|
Template: "thread.tmpl",
|
||||||
distPathPrefix + "/status.css",
|
Instance: instance,
|
||||||
}
|
OGMeta: apiutil.OGBase(instance).WithStatus(status),
|
||||||
if config.GetAccountsAllowCustomCSS() {
|
Stylesheets: []string{
|
||||||
stylesheets = append(stylesheets, "/@"+targetUsername+"/custom.css")
|
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,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "thread.tmpl", gin.H{
|
apiutil.TemplateWebPage(c, page)
|
||||||
"instance": instance,
|
|
||||||
"status": status,
|
|
||||||
"context": context,
|
|
||||||
"ogMeta": ogBase(instance).withStatus(status),
|
|
||||||
"stylesheets": stylesheets,
|
|
||||||
"javascript": []string{distPathPrefix + "/frontend.js"},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// returnAPStatus returns an ActivityPub representation of target status,
|
// returnAPStatus returns an ActivityPub representation of target status,
|
||||||
|
|
|
@ -37,7 +37,7 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
confirmEmailPath = "/" + uris.ConfirmEmailPath
|
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
|
statusPath = "/statuses/:" + apiutil.WebStatusIDKey // leave out the '/@:username' prefix as this will be served within the profile group
|
||||||
tagsPath = "/tags/:" + apiutil.TagNameKey
|
tagsPath = "/tags/:" + apiutil.TagNameKey
|
||||||
customCSSPath = profileGroupPath + "/custom.css"
|
customCSSPath = profileGroupPath + "/custom.css"
|
||||||
|
@ -49,15 +49,24 @@ const (
|
||||||
userPanelPath = settingsPathPrefix + "/user"
|
userPanelPath = settingsPathPrefix + "/user"
|
||||||
adminPanelPath = settingsPathPrefix + "/admin"
|
adminPanelPath = settingsPathPrefix + "/admin"
|
||||||
|
|
||||||
tokenParam = "token"
|
|
||||||
usernameKey = "username"
|
|
||||||
|
|
||||||
cacheControlHeader = "Cache-Control" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
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
|
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
|
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
|
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
|
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
|
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 {
|
type Module struct {
|
||||||
|
@ -99,7 +108,7 @@ func (m *Module) Route(r *router.Router, mi ...gin.HandlerFunc) {
|
||||||
profileGroup.Handle(http.MethodGet, statusPath, m.threadGETHandler)
|
profileGroup.Handle(http.MethodGet, statusPath, m.threadGETHandler)
|
||||||
|
|
||||||
// Attach individual web handlers which require no specific middlewares
|
// 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, settingsPathPrefix, m.SettingsPanelHandler)
|
||||||
r.AttachHandler(http.MethodGet, settingsPanelGlob, m.SettingsPanelHandler)
|
r.AttachHandler(http.MethodGet, settingsPanelGlob, m.SettingsPanelHandler)
|
||||||
r.AttachHandler(http.MethodGet, customCSSPath, m.customCSSGETHandler)
|
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.
|
// 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) {
|
func CreateGinTestContext(rw http.ResponseWriter, r *http.Request) (*gin.Context, *gin.Engine) {
|
||||||
ctx, eng := gin.CreateTestContext(rw)
|
ctx, eng := gin.CreateTestContext(rw)
|
||||||
router.LoadTemplateFunctions(eng)
|
|
||||||
if err := router.LoadTemplates(eng); err != nil {
|
if err := router.LoadTemplates(eng); err != nil {
|
||||||
panic(err)
|
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
|
// ConfigureTemplatesWithGin will panic on any errors related to template loading during tests
|
||||||
func ConfigureTemplatesWithGin(engine *gin.Engine, templatePath string) {
|
func ConfigureTemplatesWithGin(engine *gin.Engine, templatePath string) {
|
||||||
router.LoadTemplateFunctions(engine)
|
|
||||||
engine.LoadHTMLGlob(filepath.Join(templatePath, "*"))
|
engine.LoadHTMLGlob(filepath.Join(templatePath, "*"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
node_modules
|
node_modules
|
||||||
|
prism.js
|
||||||
|
prism.css
|
|
@ -82,11 +82,11 @@ $button-danger-bg: $error3;
|
||||||
$button-danger-fg: $white1;
|
$button-danger-fg: $white1;
|
||||||
$button-danger-hover-bg: $error2;
|
$button-danger-hover-bg: $error2;
|
||||||
|
|
||||||
$toot-bg: $gray3;
|
$status-bg: $gray3;
|
||||||
$toot-info-bg: $gray2;
|
$status-info-bg: $gray2;
|
||||||
|
|
||||||
$toot-focus-bg: $gray5;
|
$status-focus-bg: $gray5;
|
||||||
$toot-focus-info-bg: $gray4;
|
$status-focus-info-bg: $gray4;
|
||||||
|
|
||||||
$no-img-desc-bg: $orange1;
|
$no-img-desc-bg: $orange1;
|
||||||
$no-img-desc-fg: $gray1;
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,35 +20,52 @@
|
||||||
|
|
||||||
/* noto-sans-regular - latin */
|
/* noto-sans-regular - latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Noto Sans";
|
font-family: "Noto Sans";
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
src: url('../fonts/noto-sans-v27-latin-regular.woff2') format('woff2'),
|
src: url('../fonts/noto-sans-v27-latin-regular.woff2') format('woff2'),
|
||||||
url('../fonts/noto-sans-v27-latin-regular.woff') format('woff');
|
url('../fonts/noto-sans-v27-latin-regular.woff') format('woff');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* noto-sans-700 - latin */
|
/* noto-sans-700 - latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Noto Sans";
|
font-family: "Noto Sans";
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
src: url('../fonts/noto-sans-v27-latin-700.woff2') format('woff2'),
|
src: url('../fonts/noto-sans-v27-latin-700.woff2') format('woff2'),
|
||||||
url('../fonts/noto-sans-v27-latin-700.woff') format('woff');
|
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;
|
$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;
|
$br-inner: 0.2rem;
|
||||||
|
|
||||||
/* Fork-Awesome 'fa-fw' fixed icon width
|
/*
|
||||||
keep in sync with https://github.com/ForkAwesome/Fork-Awesome/blob/a99579ae3e735ee70e51ed62dfcee3172b5b2db7/css/fork-awesome.css#L50
|
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;
|
$fa-fw: 1.28571429em;
|
||||||
|
|
||||||
|
/******************************************
|
||||||
|
***** SECTION 2: BASIC GLOBAL STYLING *****
|
||||||
|
*******************************************/
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -63,90 +80,28 @@ body {
|
||||||
position: relative;
|
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 {
|
a {
|
||||||
color: $link-fg;
|
color: $link-fg;
|
||||||
}
|
}
|
||||||
|
|
||||||
header, footer {
|
/*
|
||||||
grid-column: 1 / span 3;
|
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
|
||||||
.content {
|
close it with a bottom margin.
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
main {
|
||||||
p:first-child {
|
p:first-child, ol:first-child, ul:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
p:last-child {
|
p:last-child, ol:last-child, ul:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button, button {
|
.button, button {
|
||||||
border-radius: 0.2rem;
|
border-radius: $br-inner;
|
||||||
color: $button-fg;
|
color: $button-fg;
|
||||||
background: $button-bg;
|
background: $button-bg;
|
||||||
box-shadow: $boxshadow;
|
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 {
|
.nounderline {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
@ -192,57 +307,37 @@ main {
|
||||||
color: $acc1;
|
color: $acc1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.text-cutoff {
|
||||||
justify-self: center;
|
text-overflow: ellipsis;
|
||||||
img {
|
overflow: hidden;
|
||||||
height: 30vh;
|
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;
|
EVERYTHING BELOW THIS POINT:
|
||||||
grid-template-columns: 1fr 1fr;
|
Should be moved somewhere else
|
||||||
grid-gap: 0.5rem;
|
to avoid cluttering up this file.
|
||||||
align-content: start;
|
*/
|
||||||
|
|
||||||
.entry {
|
/*
|
||||||
display: grid;
|
Below section stylings are used
|
||||||
grid-template-columns: 25% 1fr;
|
in transient/error templates.
|
||||||
gap: 1.5rem;
|
*/
|
||||||
padding: 0.5rem;
|
section.sign-in {
|
||||||
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 {
|
|
||||||
form {
|
form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -291,98 +386,11 @@ section.oob-token {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-text {
|
/*
|
||||||
color: $error1;
|
TODO: This is only used in the "finalize"
|
||||||
background: $error2;
|
template for new signups; move this elsewhere
|
||||||
border-radius: 0.1rem;
|
when that stuff is finished up.
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.callout {
|
.callout {
|
||||||
margin: 1.5rem 0;
|
margin: 1.5rem 0;
|
||||||
border: .05rem solid $border-accent;
|
border: .05rem solid $border-accent;
|
||||||
|
@ -397,22 +405,11 @@ footer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
/*
|
||||||
cursor: pointer;
|
TODO: list and blocklist are only used
|
||||||
}
|
in settings panel and on blocklist page;
|
||||||
|
consider moving them somewhere else.
|
||||||
@media (prefers-reduced-motion) {
|
*/
|
||||||
.fa-spin {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-cutoff {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -495,21 +492,18 @@ label {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.about {
|
@media screen and (max-width: 30rem) {
|
||||||
display: flex;
|
.domain-blocklist .entry {
|
||||||
flex-direction: column;
|
grid-template-columns: 1fr;
|
||||||
gap: 1rem;
|
gap: 0;
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO: this is only used on About
|
||||||
|
page and in settings application;
|
||||||
|
consider moving it somewhere else.
|
||||||
|
*/
|
||||||
.account-card {
|
.account-card {
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
|
@ -541,61 +535,3 @@ label {
|
||||||
grid-row: 1 / span 2;
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
header a {
|
/*
|
||||||
margin: 2rem;
|
Render instance title a
|
||||||
gap: 2rem;
|
bit bigger on index page.
|
||||||
|
*/
|
||||||
img {
|
.page-header a h1 {
|
||||||
height: 6rem;
|
font-size: 2rem;
|
||||||
}
|
line-height: 2rem;
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 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;
|
background: $bg-accent;
|
||||||
box-shadow: $boxshadow;
|
box-shadow: $boxshadow;
|
||||||
border: $boxshadow-border;
|
border: $boxshadow-border;
|
||||||
border-radius: $br;
|
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 {
|
.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;
|
grid-template-columns: 1fr min(92%, 65rem) 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile {
|
.profile .column-split {
|
||||||
padding: 0.5rem;
|
display: flex;
|
||||||
border-radius: $br;
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
.column-split {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile .header {
|
.profile .profile-header {
|
||||||
background: $profile-bg;
|
background: $profile-bg;
|
||||||
border-radius: $br;
|
border-radius: $br;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
.header-image {
|
.header-image-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-top: 33.33%; /* aspect-ratio 1/3 */
|
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
|
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;
|
$avatar-size: 8.5rem;
|
||||||
$name-size: 3rem;
|
$name-size: 3rem;
|
||||||
$username-size: 2rem;
|
$username-size: 2rem;
|
||||||
|
|
||||||
$overlap: calc($avatar-size - $name-size - $username-size);
|
$overlap: calc($avatar-size - $name-size - $username-size);
|
||||||
|
|
||||||
.basic-info {
|
.basic-info {
|
||||||
|
@ -71,8 +69,8 @@
|
||||||
grid-template-rows: $overlap $name-size auto;
|
grid-template-rows: $overlap $name-size auto;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"avatar . ."
|
"avatar . ."
|
||||||
"avatar displayname displayname"
|
"avatar namerole namerole"
|
||||||
"avatar username role";
|
"avatar namerole namerole";
|
||||||
|
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
margin-top: calc(-1 * $overlap);
|
margin-top: calc(-1 * $overlap);
|
||||||
|
@ -93,131 +91,119 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.displayname {
|
.namerole {
|
||||||
grid-area: displayname;
|
grid-area: namerole;
|
||||||
line-height: $name-size;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.username {
|
display: grid;
|
||||||
min-width: 0;
|
gap: 0 1rem;
|
||||||
grid-area: username;
|
box-sizing: border-box;
|
||||||
line-height: $username-size;
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-template-rows: $name-size auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"displayname displayname"
|
||||||
|
"username role";
|
||||||
|
|
||||||
font-size: 1rem;
|
.displayname {
|
||||||
font-weight: bold;
|
grid-area: displayname;
|
||||||
color: $fg-accent;
|
line-height: $name-size;
|
||||||
user-select: all;
|
font-size: 1.5rem;
|
||||||
}
|
font-weight: bold;
|
||||||
|
|
||||||
.role {
|
|
||||||
background: $bg;
|
|
||||||
color: $fg;
|
|
||||||
border: 0.13rem solid $bg;
|
|
||||||
|
|
||||||
grid-area: role;
|
|
||||||
align-self: center;
|
|
||||||
justify-self: start;
|
|
||||||
border-radius: $br;
|
|
||||||
padding: 0.3rem;
|
|
||||||
|
|
||||||
line-height: 1.1rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-variant: small-caps;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
&.admin {
|
|
||||||
color: $role-admin;
|
|
||||||
border-color: $role-admin;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.moderator {
|
.username {
|
||||||
color: $role-mod;
|
min-width: 0;
|
||||||
border-color: $role-mod;
|
grid-area: username;
|
||||||
|
line-height: $username-size;
|
||||||
|
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: $fg-accent;
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role {
|
||||||
|
background: $bg;
|
||||||
|
color: $fg;
|
||||||
|
border: 0.13rem solid $bg;
|
||||||
|
|
||||||
|
grid-area: role;
|
||||||
|
align-self: center;
|
||||||
|
justify-self: start;
|
||||||
|
border-radius: $br;
|
||||||
|
padding: 0.3rem;
|
||||||
|
|
||||||
|
line-height: 1.1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-variant: small-caps;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&.admin {
|
||||||
|
color: $role-admin;
|
||||||
|
border-color: $role-admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.moderator {
|
||||||
|
color: $role-mod;
|
||||||
|
border-color: $role-mod;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 750px) {
|
@media screen and (max-width: 750px) {
|
||||||
.profile .header {
|
.profile .profile-header {
|
||||||
.basic-info {
|
.basic-info {
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
grid-template-rows: $avatar-size $name-size auto;
|
grid-template-rows: $avatar-size $name-size auto;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"avatar avatar"
|
"avatar avatar"
|
||||||
"displayname displayname"
|
"namerole namerole"
|
||||||
"username role";
|
"namerole namerole";
|
||||||
|
|
||||||
.displayname {
|
.namerole {
|
||||||
font-size: 1.4rem;
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: $name-size auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"displayname displayname"
|
||||||
|
"username role";
|
||||||
|
|
||||||
|
.displayname {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile .col-header {
|
.profile .statuses-wrapper {
|
||||||
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 {
|
|
||||||
flex: 65 25rem;
|
flex: 65 25rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
min-width: 0%;
|
min-width: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
.col-header {
|
.profile .statuses {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: auto 1fr;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 0.4rem;
|
||||||
|
|
||||||
a {
|
.rss-icon {
|
||||||
justify-self: end;
|
display: block;
|
||||||
}
|
margin: -0.25rem 0;
|
||||||
|
|
||||||
.rss-icon {
|
.fa {
|
||||||
display: block;
|
font-size: 2rem;
|
||||||
margin: -0.25rem 0;
|
object-fit: contain;
|
||||||
|
vertical-align: middle;
|
||||||
.fa {
|
color: $orange2;
|
||||||
font-size: 2rem;
|
/*
|
||||||
object-fit: contain;
|
Can't size a single-color background, so we use
|
||||||
vertical-align: middle;
|
a linear-gradient that's effectively white.
|
||||||
color: $orange2;
|
*/
|
||||||
/* 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: linear-gradient(to right, $white1 100%, transparent 0) no-repeat center center;
|
background-size: 1.2rem 1.4rem;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,6 +226,10 @@
|
||||||
margin-bottom: -0.25rem;
|
margin-bottom: -0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.fields {
|
.fields {
|
||||||
background: $profile-bg;
|
background: $profile-bg;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -19,25 +19,19 @@
|
||||||
@import "photoswipe/dist/photoswipe.css";
|
@import "photoswipe/dist/photoswipe.css";
|
||||||
@import "photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css";
|
@import "photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css";
|
||||||
@import "plyr/dist/plyr.css";
|
@import "plyr/dist/plyr.css";
|
||||||
|
@import "./prism.css";
|
||||||
|
|
||||||
main {
|
main {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
grid-auto-rows: auto;
|
grid-auto-rows: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread {
|
.status {
|
||||||
display: flex;
|
background: $status-bg;
|
||||||
flex-direction: column;
|
|
||||||
border-radius: $br;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toot {
|
|
||||||
background: $toot-bg;
|
|
||||||
box-shadow: $boxshadow;
|
box-shadow: $boxshadow;
|
||||||
border: $boxshadow-border;
|
border: $boxshadow-border;
|
||||||
border-radius: $br;
|
border-radius: $br;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: $br;
|
|
||||||
padding-top: 0.75rem;
|
padding-top: 0.75rem;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -47,66 +41,75 @@ main {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.author > a {
|
.status-header > address {
|
||||||
padding: 0 0.75rem;
|
/*
|
||||||
display: grid;
|
Avoid stretching so wide that user
|
||||||
grid-template-columns: 3.5rem 1fr auto;
|
can't click on open thread link that's
|
||||||
grid-template-rows: auto auto;
|
behind the status header link.
|
||||||
grid-template-areas:
|
*/
|
||||||
"avatar display date"
|
width: fit-content;
|
||||||
"avatar user .";
|
|
||||||
gap: 0 0.5rem;
|
|
||||||
|
|
||||||
.avatar {
|
> a {
|
||||||
grid-area: avatar;
|
padding: 0 0.75rem;
|
||||||
height: 3.5rem;
|
display: grid;
|
||||||
width: 3.5rem;
|
grid-template-columns: 3.5rem 1fr auto;
|
||||||
object-fit: cover;
|
grid-template-rows: auto auto;
|
||||||
|
grid-template-areas:
|
||||||
border: 0.15rem solid $avatar-border;
|
"avatar author-strap author-strap"
|
||||||
border-radius: $br;
|
"avatar author-strap author-strap";
|
||||||
overflow: hidden; /* hides corners from img overflowing */
|
gap: 0 0.5rem;
|
||||||
|
font-style: normal;
|
||||||
img {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
background: $bg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.displayname, .username {
|
|
||||||
justify-self: start;
|
|
||||||
align-self: start;
|
|
||||||
|
|
||||||
max-width: 100%;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.displayname {
|
.avatar {
|
||||||
grid-area: display;
|
grid-area: avatar;
|
||||||
font-weight: bold;
|
height: 3.5rem;
|
||||||
font-size: 1rem;
|
width: 3.5rem;
|
||||||
line-height: 1.3rem;
|
object-fit: cover;
|
||||||
/* margin-top: -0.5rem; */
|
|
||||||
}
|
border: 0.15rem solid $avatar-border;
|
||||||
|
border-radius: $br;
|
||||||
.username {
|
overflow: hidden; /* hides corners from img overflowing */
|
||||||
grid-area: user;
|
|
||||||
color: $link-fg;
|
img {
|
||||||
font-size: 1rem;
|
height: 100%;
|
||||||
line-height: 1.3rem;
|
width: 100%;
|
||||||
}
|
object-fit: cover;
|
||||||
|
background: $bg;
|
||||||
.timestamp {
|
}
|
||||||
grid-area: date;
|
}
|
||||||
color: $fg-reduced;
|
|
||||||
|
.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%;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.displayname {
|
||||||
|
grid-area: display;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
grid-area: user;
|
||||||
|
color: $link-fg;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.status-body {
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -157,6 +160,10 @@ main {
|
||||||
line-height: 1.6rem;
|
line-height: 1.6rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Normalize header sizes to fit better
|
||||||
|
with the line-height we use for statuses.
|
||||||
|
*/
|
||||||
h1 {
|
h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
|
@ -187,35 +194,63 @@ main {
|
||||||
line-height: initial;
|
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 {
|
pre, code {
|
||||||
background-color: $gray2;
|
background-color: $gray2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Just code on its own inside status
|
||||||
|
content, ie, `here is some code`.
|
||||||
|
*/
|
||||||
code {
|
code {
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
border-radius: $br-inner;
|
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;
|
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;
|
border-radius: $br;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
white-space: pre;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
code {
|
/*
|
||||||
padding: 0.5rem;
|
Code inside a pre block, ie.,
|
||||||
|
|
||||||
|
```
|
||||||
|
here is some code
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
code {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
border-radius: 0;
|
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
-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 {
|
.poll {
|
||||||
background-color: $gray2;
|
background-color: $gray2;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
@ -451,41 +474,41 @@ main {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.status-info {
|
||||||
display: flex;
|
background: $status-info-bg;
|
||||||
background: $toot-info-bg;
|
|
||||||
color: $fg-reduced;
|
color: $fg-reduced;
|
||||||
border-top: 0.15rem solid $toot-info-border;
|
border-top: 0.15rem solid $status-info-border;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
|
|
||||||
time {
|
.status-stats {
|
||||||
padding-right: 1rem;
|
display: flex;
|
||||||
}
|
|
||||||
|
|
||||||
.stats {
|
|
||||||
display: inline-flex;
|
|
||||||
flex: 1;
|
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
|
.stats-grouping {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
column-gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.stats-item {
|
.stats-item {
|
||||||
span {
|
display: flex;
|
||||||
white-space: nowrap;
|
gap: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stats-item:not(.published-at) {
|
||||||
|
z-index: 1;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language {
|
.language {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
z-index: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
grid-column: span 3;
|
grid-column: span 3;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.toot-link {
|
.status-link {
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
@ -508,15 +531,12 @@ main {
|
||||||
/* bottom left, bottom right */
|
/* bottom left, bottom right */
|
||||||
border-bottom-left-radius: $br;
|
border-bottom-left-radius: $br;
|
||||||
border-bottom-right-radius: $br;
|
border-bottom-right-radius: $br;
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.expanded {
|
&.expanded {
|
||||||
background: $toot-focus-bg;
|
background: $status-focus-bg;
|
||||||
padding-bottom: 0;
|
.status-info {
|
||||||
|
background: $status-focus-info-bg;
|
||||||
.info {
|
|
||||||
background: $toot-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 PhotoswipeLightbox = require("photoswipe/dist/umd/photoswipe-lightbox.umd.min.js");
|
||||||
const PhotoswipeCaptionPlugin = require("photoswipe-dynamic-caption-plugin").default;
|
const PhotoswipeCaptionPlugin = require("photoswipe-dynamic-caption-plugin").default;
|
||||||
const Plyr = require("plyr");
|
const Plyr = require("plyr");
|
||||||
|
const Prism = require("./prism.js");
|
||||||
|
|
||||||
|
Prism.manual = true;
|
||||||
|
Prism.highlightAll();
|
||||||
|
|
||||||
let [_, _user, type, id] = window.location.pathname.split("/");
|
let [_, _user, type, id] = window.location.pathname.split("/");
|
||||||
if (type == "statuses") {
|
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;
|
const account = toot.account;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="toot expanded">
|
<article className="status expanded">
|
||||||
<section className="author">
|
<header className="status-header">
|
||||||
<a>
|
<address>
|
||||||
<img className="avatar" src={account.avatar} alt="" />
|
<a style={{margin: 0}}>
|
||||||
<span className="displayname">
|
<img className="avatar" src={account.avatar} alt="" />
|
||||||
{account.display_name.trim().length > 0 ? account.display_name : account.username}
|
<dl className="author-strap">
|
||||||
<span className="sr-only">.</span>
|
<dt className="sr-only">Display name</dt>
|
||||||
</span>
|
<dd className="displayname text-cutoff">
|
||||||
<span className="username">@{account.username}</span>
|
{account.display_name.trim().length > 0 ? account.display_name : account.username}
|
||||||
</a>
|
</dd>
|
||||||
</section>
|
<dt className="sr-only">Username</dt>
|
||||||
<section className="body">
|
<dd className="username text-cutoff">@{account.username}</dd>
|
||||||
|
</dl>
|
||||||
|
</a>
|
||||||
|
</address>
|
||||||
|
</header>
|
||||||
|
<section className="status-body">
|
||||||
<div className="text">
|
<div className="text">
|
||||||
<div className="content">
|
<div className="content">
|
||||||
{toot.spoiler_text?.length > 0
|
{toot.spoiler_text?.length > 0
|
||||||
|
@ -164,8 +169,17 @@ function ReportedToot({ toot }) {
|
||||||
<TootMedia media={toot.media_attachments} sensitive={toot.sensitive} />
|
<TootMedia media={toot.media_attachments} sensitive={toot.sensitive} />
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
<aside className="info">
|
<aside className="status-info">
|
||||||
<time dateTime={toot.created_at}>{new Date(toot.created_at).toLocaleString()}</time>
|
<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>
|
</aside>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
|
|
|
@ -83,7 +83,7 @@ function ReportEntry({ report }) {
|
||||||
<div className="usernames">
|
<div className="usernames">
|
||||||
<Username user={from} link={false} /> reported <Username user={target} link={false} />
|
<Username user={from} link={false} /> reported <Username user={target} link={false} />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="status">
|
<h3 className="report-status">
|
||||||
{report.action_taken ? "Resolved" : "Open"}
|
{report.action_taken ? "Resolved" : "Open"}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -22,24 +22,29 @@ const React = require("react");
|
||||||
module.exports = function FakeProfile({ avatar, header, display_name, username, role }) {
|
module.exports = function FakeProfile({ avatar, header, display_name, username, role }) {
|
||||||
return ( // Keep in sync with web/template/profile.tmpl
|
return ( // Keep in sync with web/template/profile.tmpl
|
||||||
<div className="profile">
|
<div className="profile">
|
||||||
<div className="header">
|
<div className="profile-header">
|
||||||
<div className="header-image">
|
<div className="header-image-wrapper">
|
||||||
<img src={header} alt={header ? `header image for ${username}` : "None set"} />
|
<img src={header} alt={header ? `header image for ${username}` : "None set"} />
|
||||||
</div>
|
</div>
|
||||||
<div className="basic-info" aria-hidden="true">
|
<div className="basic-info" aria-hidden="true">
|
||||||
<a className="avatar" href={avatar}>
|
<a className="avatar" href={avatar}>
|
||||||
<img src={avatar} alt={avatar ? `avatar image for ${username}` : "None set"} />
|
<img src={avatar} alt={avatar ? `avatar image for ${username}` : "None set"} />
|
||||||
</a>
|
</a>
|
||||||
<span className="displayname text-cutoff">
|
<dl className="namerole">
|
||||||
{display_name.trim().length > 0 ? display_name : username}
|
<dt className="sr-only">Display name</dt>
|
||||||
<span className="sr-only">.</span>
|
<dd className="displayname text-cutoff">{display_name.trim().length > 0 ? display_name : username}</dd>
|
||||||
</span>
|
<dt className="sr-only">Username</dt>
|
||||||
<span className="username text-cutoff">@{username}</span>
|
<dd className="username text-cutoff">@{username}</dd>
|
||||||
{(role && role.name != "user") &&
|
<dt className="sr-only">Role</dt>
|
||||||
<div className={`role ${role.name}`}>
|
{
|
||||||
<span className="sr-only">Role: </span>{role.name}
|
(role && role.name != "user") ?
|
||||||
</div>
|
<>
|
||||||
}
|
<dd className="sr-only">Role</dd>
|
||||||
|
<dt className={`role ${role.name}`}>{role.name}</dt>
|
||||||
|
</>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -29,20 +29,27 @@ module.exports = function FakeToot({ children }) {
|
||||||
} } = query.useVerifyCredentialsQuery();
|
} } = query.useVerifyCredentialsQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="toot expanded">
|
<article className="status expanded">
|
||||||
<section className="author">
|
<header className="status-header">
|
||||||
<a>
|
<address>
|
||||||
<img className="avatar" src={account.avatar} alt="" />
|
<a style={{margin: 0}}>
|
||||||
<span className="displayname">
|
<img className="avatar" src={account.avatar} alt="" />
|
||||||
{account.display_name.trim().length > 0 ? account.display_name : account.username}
|
<dl className="author-strap">
|
||||||
<span className="sr-only">.</span>
|
<dt className="sr-only">Display name</dt>
|
||||||
</span>
|
<dd className="displayname text-cutoff">
|
||||||
<span className="username">@{account.username}</span>
|
{account.display_name.trim().length > 0 ? account.display_name : account.username}
|
||||||
</a>
|
</dd>
|
||||||
</section>
|
<dt className="sr-only">Username</dt>
|
||||||
<section className="body">
|
<dd className="username text-cutoff">@{account.username}</dd>
|
||||||
|
</dl>
|
||||||
|
</a>
|
||||||
|
</address>
|
||||||
|
</header>
|
||||||
|
<section className="status-body">
|
||||||
<div className="text">
|
<div className="text">
|
||||||
{children}
|
<div className="content">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -20,26 +20,14 @@ body {
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.page-content {
|
||||||
grid-column: 1 / span 3; /* stretch entire width, to fit panel + sidebar nav */
|
grid-column: 1 / span 3; /* stretch entire width, to fit panel + sidebar nav */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
/* Don't inherit orange dot from base.css. */
|
||||||
justify-content: start;
|
ul li::before {
|
||||||
|
content: initial;
|
||||||
a {
|
|
||||||
margin: 1.5rem;
|
|
||||||
gap: 1rem;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 3rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
|
@ -1007,7 +995,7 @@ button.with-padding {
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
.status {
|
.report-status {
|
||||||
color: $border-accent;
|
color: $border-accent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1029,7 +1017,7 @@ button.with-padding {
|
||||||
color: $fg-reduced;
|
color: $fg-reduced;
|
||||||
border-left: 0.4rem solid $bg;
|
border-left: 0.4rem solid $bg;
|
||||||
|
|
||||||
.byline .status {
|
.byline .report-status {
|
||||||
color: $fg-reduced;
|
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) {
|
@media screen and (orientation: portrait) {
|
||||||
.reports .report .byline {
|
.reports .report .byline {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
||||||
.status {
|
.report-status {
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1162,4 +1201,14 @@ button.with-padding {
|
||||||
to {
|
to {
|
||||||
opacity: 0;
|
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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
{{ template "header.tmpl" .}}
|
{{- with . }}
|
||||||
<main>
|
<main>
|
||||||
<section>
|
<section>
|
||||||
<h1>404: Page Not Found</h1>
|
<h1>404: Not Found</h1>
|
||||||
<p>
|
<p>
|
||||||
GoToSocial only serves Public statuses via the web.
|
GoToSocial only serves Public statuses via the web.
|
||||||
If you reached this page by clicking on a status link,
|
</p>
|
||||||
it's possible that the status is not Public, has been
|
<p>
|
||||||
deleted by the author, you don't have permission to see
|
If you reached this page by clicking on a status link,
|
||||||
it, or it just doesn't exist at all.
|
it's likely that the status is not Public. You can try
|
||||||
</p>
|
entering the status URL in your client's search bar,
|
||||||
<p>
|
to view the status from your account. If that doesn't
|
||||||
If you believe this 404 was an error, you can contact
|
work, it's possible that the status has been deleted by
|
||||||
the instance admin. Provide them with the following request
|
the author, you don't have permission to view it, or it
|
||||||
Request ID: <code>{{.requestID}}</code>.
|
doesn't exist at all.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
<p>
|
||||||
|
If you believe this 404 was an error, you can contact
|
||||||
|
the instance admin. Provide them with the following
|
||||||
|
request ID: <code>{{- .requestID -}}</code>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
{{- end }}
|
||||||
{{ template "footer.tmpl" .}}
|
|
|
@ -17,105 +17,133 @@
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
{{ template "header.tmpl" .}}
|
{{- define "description" -}}
|
||||||
<main>
|
{{- if .instance.Description }}
|
||||||
<section class="about">
|
{{ .instance.Description | noescape }}
|
||||||
<h1>About</h1>
|
{{- else }}
|
||||||
<div>
|
<p>No description has yet been set for this instance.<p>
|
||||||
{{.instance.Description |noescape}}
|
{{- end }}
|
||||||
</div>
|
{{- end -}}
|
||||||
|
|
||||||
<div>
|
{{- define "registrationLimits" -}}
|
||||||
<h2 id="languages">Languages</h2>
|
{{- if .instance.Registrations -}}
|
||||||
<p>
|
Registration is enabled; new signups can be submitted to this instance.<br/>
|
||||||
{{ if .languages }}
|
{{- if .instance.ApprovalRequired -}}
|
||||||
This instance prefers the following languages:
|
Admin approval is required for new registrations.
|
||||||
<ol>
|
{{- else -}}
|
||||||
{{range .languages}}
|
Admin approval is not required for registrations; new signups will be automatically approved (pending email confirmation).
|
||||||
<li>{{.}}</li>
|
{{- end -}}
|
||||||
{{end}}
|
{{- else -}}
|
||||||
</ol>
|
Registration is disabled; new signups are currently closed for this instance.
|
||||||
{{ else }}
|
{{- end -}}
|
||||||
This instance does not have any preferred languages.
|
{{- end -}}
|
||||||
{{ end }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
{{- define "customCSSLimits" -}}
|
||||||
<h2 id="contact">Admin Contact</h2>
|
{{- if .instance.Configuration.Accounts.AllowCustomCSS -}}
|
||||||
{{if .instance.ContactAccount}}
|
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.
|
||||||
<a href="{{.instance.ContactAccount.URL}}" class="account-card">
|
{{- else -}}
|
||||||
<img class="avatar" src="{{.instance.ContactAccount.Avatar}}" alt="" />
|
<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.
|
||||||
<h3>
|
{{- end -}}
|
||||||
{{if .instance.ContactAccount.DisplayName}}{{emojify .instance.ContactAccount.Emojis (escape .instance.ContactAccount.DisplayName)}}{{else}}{{.instance.ContactAccount.Username}}{{end}}
|
{{- 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>
|
{{- define "statusLimits" -}}
|
||||||
<h2 id="rules">Rules</h2>
|
Statuses can contain up to
|
||||||
<ol>
|
{{- .instance.Configuration.Statuses.MaxCharacters }} characters, and
|
||||||
{{range .instance.Rules}}
|
{{- .instance.Configuration.Statuses.MaxMediaAttachments }} media attachments.
|
||||||
<li>{{.Text}}</li>
|
{{- end -}}
|
||||||
{{end}}
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
{{- define "pollLimits" -}}
|
||||||
<h2 id="features">Features</h2>
|
Polls can have up to
|
||||||
<ul>
|
{{- .instance.Configuration.Polls.MaxOptions }} options, with
|
||||||
<li>
|
{{- .instance.Configuration.Polls.MaxCharactersPerOption }} characters per option.
|
||||||
Registration is
|
{{- end -}}
|
||||||
{{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>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
{{- with . }}
|
||||||
<h2 id="moderated-servers">Moderated servers</h2>
|
<main class="about">
|
||||||
<p>
|
<section class="about-section" role="region" aria-labelledby="about">
|
||||||
ActivityPub instances exchange (federate) data with other instances, including accounts and toots.
|
<h3 id="about">About {{ .instance.Title -}}</h3>
|
||||||
This can be prevented for specific domains by suspending them. None of their content is stored,
|
{{- with . }}
|
||||||
and interaction with their users is blocked both ways.</br>
|
{{- include "description" . | indent 2 }}
|
||||||
{{if .blocklistExposed}}
|
{{- end }}
|
||||||
<a href="/about/suspended">View the list of suspended domains</a>
|
</section>
|
||||||
{{else}}
|
<section class="about-section" role="region" aria-labelledby="contact">
|
||||||
This instance does not publically share this list.
|
<h3 id="contact">Admin Contact</h3>
|
||||||
{{end}}
|
{{- if .instance.ContactAccount }}
|
||||||
</p>
|
<a href="{{- .instance.ContactAccount.URL -}}" class="account-card">
|
||||||
</div>
|
<img class="avatar" src="{{- .instance.ContactAccount.Avatar -}}" alt=""/>
|
||||||
|
<h3>
|
||||||
<div>
|
{{- if .instance.ContactAccount.DisplayName -}}
|
||||||
<h2 id="stats">Instance Statistics</h2>
|
{{- emojify .instance.ContactAccount.Emojis (escape .instance.ContactAccount.DisplayName) -}}
|
||||||
<ul>
|
{{- else -}}
|
||||||
<li>Users: <span class="count">{{.instance.Stats.user_count}}</span></li>
|
{{- .instance.ContactAccount.Username -}}
|
||||||
<li>Posts: <span class="count">{{.instance.Stats.status_count}}</span></li>
|
{{- end -}}
|
||||||
<li>Federates with: <span class="count">{{.instance.Stats.domain_count}}</span> instances</li>
|
</h3>
|
||||||
</ul>
|
<span>@{{- .instance.ContactAccount.Username -}}</span>
|
||||||
</div>
|
</a>
|
||||||
</section>
|
{{- 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 .languages }}
|
||||||
|
<li>{{- . -}}</li>
|
||||||
|
{{- end }}
|
||||||
|
</ol>
|
||||||
|
{{- 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>{{- template "registrationLimits" . -}}</li>
|
||||||
|
<li>{{- template "customCSSLimits" . -}}</li>
|
||||||
|
<li>{{- template "statusLimits" . -}}</li>
|
||||||
|
<li>{{- template "pollLimits" . -}}</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section class="about-section" role="region" aria-labelledby="moderated-servers">
|
||||||
|
<h3 id="moderated-servers">Moderated servers</h3>
|
||||||
|
<p>
|
||||||
|
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>
|
||||||
|
<ul>
|
||||||
|
<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>
|
||||||
|
<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>
|
</main>
|
||||||
{{ template "footer.tmpl" .}}
|
{{- end }}
|
|
@ -17,26 +17,24 @@
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
{{ template "header.tmpl" .}}
|
{{- with . }}
|
||||||
<main>
|
<main>
|
||||||
<form action="/oauth/authorize" method="POST">
|
<form action="/oauth/authorize" method="POST">
|
||||||
<h1>Hi {{.user}}!</h1>
|
<h1>Hi {{ .user -}}!</h1>
|
||||||
<p>
|
<p>
|
||||||
Application <b>{{.appname}}</b>
|
Application
|
||||||
{{if len .appwebsite | eq 0 | not}}
|
{{- if .appwebsite }}
|
||||||
({{.appwebsite}})
|
<a href="{{- .appwebsite -}}" rel="nofollow noreferrer noopener" target="_blank">{{- .appname -}}</a>
|
||||||
{{end}}
|
{{- else }}
|
||||||
would like to perform actions on your behalf, with scope <em>{{.scope}}</em>.
|
<b>{{- .appname -}}</b>
|
||||||
</p>
|
{{- end }}
|
||||||
<p>The application will redirect to {{.redirect}} to continue.</p>
|
would like to perform actions on your behalf, with scope
|
||||||
<p>
|
<em>{{- .scope -}}</em>.
|
||||||
<button
|
</p>
|
||||||
type="submit"
|
<p>
|
||||||
style="width:200px;"
|
To continue, the application will redirect to: <code>{{- .redirect -}}</code>
|
||||||
>
|
</p>
|
||||||
Allow
|
<button type="submit" style="width:200px;">Allow</button>
|
||||||
</button>
|
</form>
|
||||||
</p>
|
</main>
|
||||||
</form>
|
{{- end }}
|
||||||
</main>
|
|
||||||
{{ template "footer.tmpl" .}}
|
|
|
@ -17,12 +17,11 @@
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
{{ template "header.tmpl" .}}
|
{{- with . }}
|
||||||
<main>
|
<main>
|
||||||
<section>
|
<section>
|
||||||
<h1>Email Address Confirmed</h1>
|
<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>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
{{- end }}
|
||||||
{{ template "footer.tmpl" .}}
|
|
|
@ -17,36 +17,36 @@
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
{{ template "header.tmpl" .}}
|
{{- with . }}
|
||||||
<main>
|
<main>
|
||||||
<section>
|
<section>
|
||||||
<h1>Suspended Instances</h1>
|
<h1>Suspended Instances</h1>
|
||||||
<p>
|
<p>
|
||||||
The following list of domains have been suspended by the administrator(s) of this server.
|
The following list of domains have been suspended
|
||||||
</p>
|
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
|
<p>
|
||||||
servers.
|
All current and future accounts on these instances are
|
||||||
This extends to subdomains, so an entry for 'example.com' includes 'social.example.com' as well.
|
blocked, and no more data is federated to the remote servers.
|
||||||
</p>
|
This extends to subdomains, so an entry for 'example.com'
|
||||||
<div class="list domain-blocklist">
|
includes 'social.example.com' as well.
|
||||||
<div class="header entry">
|
</p>
|
||||||
<div class="domain">Domain</div>
|
<div class="list domain-blocklist">
|
||||||
<div class="public_comment">Public comment</div>
|
<div class="header entry">
|
||||||
</div>
|
<div class="domain">Domain</div>
|
||||||
{{range .blocklist}}
|
<div class="public_comment">Public comment</div>
|
||||||
<div class="entry" id="{{.Domain}}">
|
</div>
|
||||||
<div class="domain">
|
{{- range .blocklist }}
|
||||||
<a class="text-cutoff" href="#{{.Domain}}" title="{{.Domain}}">{{.Domain}}</a>
|
<div class="entry" id="{{- .Domain -}}">
|
||||||
</div>
|
<div class="domain">
|
||||||
<div class="public_comment">
|
<a class="text-cutoff" href="#{{- .Domain -}}" title="{{- .Domain -}}">{{- .Domain -}}</a>
|
||||||
<p>
|
</div>
|
||||||
{{.PublicComment}}
|
<div class="public_comment">
|
||||||
</p>
|
<p>{{- .PublicComment -}}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{- end }}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
{{ template "footer.tmpl" .}}
|
{{- end }}
|
|
@ -17,16 +17,16 @@
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
{{ template "header.tmpl" .}}
|
{{- with . }}
|
||||||
<main>
|
<main>
|
||||||
<section class="error">
|
<section class="error">
|
||||||
<h1>An error occured:</h1>
|
<h1>An error occured:</h1>
|
||||||
<pre>{{.error}}</pre>
|
<pre>{{- .error -}}</pre>
|
||||||
{{if .requestID}}
|
{{- if .requestID }}
|
||||||
<div>
|
<div>
|
||||||
<span>Request ID:</span> <code>{{.requestID}}</code>
|
<span>Request ID:</span> <code>{{- .requestID -}}</code>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{- end }}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
{{ template "footer.tmpl" .}}
|
{{- end }}
|
|
@ -17,34 +17,31 @@
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
{{ template "header.tmpl" .}}
|
{{- with . }}
|
||||||
<main>
|
<main>
|
||||||
<form action="/oauth/finalize" method="POST">
|
<form action="/oauth/finalize" method="POST">
|
||||||
<h1>Hi {{.name}}!</h1>
|
<h1>Hi {{ .name -}}!</h1>
|
||||||
<p>
|
<p>
|
||||||
You are about to sign-up to {{ .instance.Title }} (<code>{{ .instance.URI }}</code>)
|
You are about to sign-up to {{ .instance.Title -}}.
|
||||||
<br>
|
To ensure the best experience for you, we need you to provide some additional details.
|
||||||
To ensure the best experience for you, we need you to provide some additional details.
|
</p>
|
||||||
</p>
|
<div class="callout">
|
||||||
{{if .error}}
|
<p class="callout-title">Important</p>
|
||||||
<section class="error">
|
<p>Due to the way the ActivityPub standard works, you <strong>cannot</strong> change your username after it has been set.</p>
|
||||||
<span>❌</span> <pre>{{.error}}</pre>
|
</div>
|
||||||
</section>
|
<div class="labelinput">
|
||||||
{{end}}
|
<label for="username">Username <small>(must contain only lowercase letters, numbers, and underscores)</small></label>
|
||||||
<div class="callout">
|
<input
|
||||||
<p class="callout-title">Important</p>
|
type="text"
|
||||||
<p>Due to the way the ActivityPub standard works, you <strong>cannot</strong> change your username after it has been set.</p>
|
class="form-control"
|
||||||
</div>
|
name="username"
|
||||||
<div class="labelinput">
|
required
|
||||||
<label for="username">Username <small>(must contain only lowercase letters, numbers, and underscores)</small></label>
|
placeholder="Please enter your desired username"
|
||||||
<input type="text"
|
value="{{- .preferredUsername -}}"
|
||||||
class="form-control"
|
>
|
||||||
name="username"
|
</div>
|
||||||
required
|
<input type="hidden" name="name" value="{{- .name -}}">
|
||||||
placeholder="Please enter your desired username" value="{{ .preferredUsername }}">
|
<button type="submit" style="width: 100%; margin-top: 1rem;" class="btn btn-success">Submit</button>
|
||||||
</div>
|
</form>
|
||||||
<input type="hidden" name="name" value="{{ .name }}">
|
</main>
|
||||||
<button type="submit" style="width: 100%; margin-top: 1rem;" class="btn btn-success">Submit</button>
|
{{- end }}
|
||||||
</form>
|
|
||||||
</main>
|
|
||||||
{{ template "footer.tmpl" .}}
|
|
|
@ -17,9 +17,8 @@
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
{{ template "header.tmpl" .}}
|
{{- with . }}
|
||||||
<main class="lightgray">
|
<main class="lightgray">
|
||||||
<div id="root">
|
<div id="root"></div>
|
||||||
</div>
|
|
||||||
</main>
|
</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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
{{ template "header.tmpl" .}}
|
{{- define "shortDescription" -}}
|
||||||
<section class="excerpt-top">
|
{{- if .instance.ShortDescription }}
|
||||||
home to <span class="count">{{.instance.Stats.user_count}}</span> users
|
{{ .instance.ShortDescription | noescape }}
|
||||||
who posted <span class="count">{{.instance.Stats.status_count}}</span> statuses,
|
{{- else }}
|
||||||
federating with <span class="count">{{.instance.Stats.domain_count}}</span> other instances.
|
<p>No short description has yet been set for this instance.<p>
|
||||||
</section>
|
{{- end }}
|
||||||
<main class="lightgray">
|
{{- end -}}
|
||||||
<section>
|
|
||||||
<div className="short-description">
|
{{- with . }}
|
||||||
{{.instance.ShortDescription |noescape}}
|
<main class="about">
|
||||||
</div>
|
<section class="about-section" role="region" aria-labelledby="about">
|
||||||
</section>
|
<h3 id="about">About this instance</h3>
|
||||||
<section class="apps">
|
{{- include "shortDescription" . | indent 2 }}
|
||||||
<p>
|
<a href="/about">See more details</a>
|
||||||
GoToSocial does not provide its own webclient, but implements the Mastodon client API.
|
</section>
|
||||||
You can use this server through a variety of other clients:
|
{{- include "index_apps.tmpl" . | indent 1 }}
|
||||||
</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>
|
|
||||||
</section>
|
|
||||||
</main>
|
</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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
{{ template "header.tmpl" .}}
|
{{- with . }}
|
||||||
<main>
|
<main>
|
||||||
<section class="oob-token">
|
<section class="oob-token">
|
||||||
<h1>Hi {{ .user }}!</h1>
|
<h1>Hi {{ .user -}}!</h1>
|
||||||
<p>Here's your out-of-band token with scope "<em>{{.scope}}</em>", use it wisely:</p>
|
<p>Here's your out-of-band token with scope "<em>{{- .scope -}}</em>", use it wisely:</p>
|
||||||
<code>{{ .oobToken }}</code>
|
<code>{{- .oobToken -}}</code>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
{{ template "header.tmpl" .}}
|
{{- with . }}
|
||||||
|
|
||||||
<main class="profile">
|
<main class="profile">
|
||||||
<div class="header">
|
<h2 class="sr-only">Profile for {{ .account.Username -}}</h2>
|
||||||
<div class="header-image">
|
<section class="profile-header" role="region" aria-label="Basic info">
|
||||||
{{ if .account.Header }}
|
<div class="header-image-wrapper">
|
||||||
<img src="{{.account.Header}}" alt="" />
|
<img
|
||||||
{{ end }}
|
src="{{- .account.Header -}}"
|
||||||
</div>
|
alt="Header for {{ .account.Username -}}"
|
||||||
<div class="basic-info" aria-hidden="true">
|
title="Header for {{ .account.Username -}}"
|
||||||
<a class="avatar" href="{{.account.Avatar}}">
|
/>
|
||||||
<img src="{{.account.Avatar}}" alt="">
|
</div>
|
||||||
</a>
|
<div class="basic-info">
|
||||||
<span class="displayname text-cutoff">
|
<a class="avatar" href="{{- .account.Avatar -}}">
|
||||||
{{if .account.DisplayName}}
|
<img
|
||||||
{{emojify .account.Emojis (escape .account.DisplayName)}}
|
src="{{- .account.Avatar -}}"
|
||||||
{{else}}
|
alt="Avatar for {{ .account.Username -}}"
|
||||||
{{.account.Username}}
|
title="Avatar for {{ .account.Username -}}"
|
||||||
{{end}}
|
/>
|
||||||
</span>
|
</a>
|
||||||
<span class="username text-cutoff">@{{.account.Username}}@{{.instance.AccountDomain}}</span>
|
<dl class="namerole">
|
||||||
{{- /* Only render account role if 1. it's present and 2. it's not equal to the standard 'user' role */ -}}
|
<dt class="sr-only">Display name</dt>
|
||||||
{{ if and (.account.Role) (ne .account.Role.Name "user") }}
|
<dd class="displayname text-cutoff">
|
||||||
<div class="role {{ .account.Role.Name }}">
|
{{- if .account.DisplayName -}}
|
||||||
{{ .account.Role.Name }}
|
{{- emojify .account.Emojis (escape .account.DisplayName) -}}
|
||||||
</div>
|
{{- else -}}
|
||||||
{{ end }}
|
{{- .account.Username -}}
|
||||||
</div>
|
{{- end -}}
|
||||||
<div class="sr-only">
|
</dd>
|
||||||
Profile for
|
<dt class="sr-only">Username</dt>
|
||||||
{{if .account.DisplayName}}{{.account.DisplayName}}{{else}}{{.account.Username}}{{end}}.
|
<dd class="username text-cutoff">@{{- .account.Username -}}@{{- .instance.AccountDomain -}}</dd>
|
||||||
Username @{{.account.Username}}, {{.instance.AccountDomain}}.
|
{{- if and (.account.Role) (ne .account.Role.Name "user") }}
|
||||||
{{ if and (.account.Role) (ne .account.Role.Name "user") }}
|
<dt class="sr-only">Role</dt>
|
||||||
Role: {{ .account.Role.Name }}
|
<dd class="role {{ .account.Role.Name -}}">{{- .account.Role.Name -}}</dd>
|
||||||
{{ end }}
|
{{- end }}
|
||||||
</div>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
<div class="column-split">
|
<div class="column-split">
|
||||||
|
<section class="about-user" role="region" aria-labelledby="about-header">
|
||||||
<section class="about-user">
|
<div class="col-header">
|
||||||
<div class="col-header">
|
<h3 id="about-header">About<span class="sr-only"> {{- .account.Username -}}</span></h3>
|
||||||
<h1>About</h1>
|
</div>
|
||||||
</div>
|
{{- if .account.Fields }}
|
||||||
|
{{- include "profile_fields.tmpl" . | indent 3 }}
|
||||||
<div class="fields">
|
{{- end }}
|
||||||
{{ range .account.Fields }}
|
<h4 class="sr-only">Bio</h4>
|
||||||
<div class="field">
|
<div class="bio">
|
||||||
<b>{{emojify $.account.Emojis (noescape .Name)}}</b>
|
{{- if .account.Note }}
|
||||||
<span>{{emojify $.account.Emojis (noescape .Value)}}</span>
|
{{ emojify .account.Emojis (noescape .account.Note) }}
|
||||||
</div>
|
{{- else }}
|
||||||
{{ end }}
|
<p>This GoToSocial user hasn't written a bio yet!</p>
|
||||||
</div>
|
{{- end }}
|
||||||
|
</div>
|
||||||
<div class="bio">
|
<h4 class="sr-only">Stats</h4>
|
||||||
{{ if .account.Note }}
|
<dl class="accountstats">
|
||||||
{{emojify .account.Emojis (noescape .account.Note)}}
|
<dt>Joined</dt>
|
||||||
{{else}}
|
<dd><time datetime="{{- .account.CreatedAt -}}">{{- .account.CreatedAt | timestampVague -}}</time></dd>
|
||||||
This GoToSocial user hasn't written a bio yet!
|
<dt>Posts</dt>
|
||||||
{{end}}
|
<dd>{{- .account.StatusesCount -}}</dd>
|
||||||
</div>
|
<dt>Followed by</dt>
|
||||||
|
<dd>{{- .account.FollowersCount -}}</dd>
|
||||||
<div class="sr-only" role="group">
|
<dt>Following</dt>
|
||||||
<span>Joined on {{.account.CreatedAt | timestampVague}}.</span>
|
<dd>{{- .account.FollowingCount -}}</dd>
|
||||||
<span>{{.account.StatusesCount}} post{{if .account.StatusesCount | eq 1 | not}}s{{end}}.</span>
|
</dl>
|
||||||
<span>Followed by {{.account.FollowersCount}}.</span>
|
</section>
|
||||||
<span>Following {{.account.FollowingCount}}.</span>
|
<div class="statuses-wrapper" role="region" aria-label="Posts by {{ .account.Username -}}">
|
||||||
</div>
|
{{- if .pinned_statuses }}
|
||||||
|
<section class="pinned statuses" aria-labelledby="pinned">
|
||||||
<div class="accountstats" aria-hidden="true">
|
<div class="col-header">
|
||||||
<b>Joined</b><time datetime="{{.account.CreatedAt}}">{{.account.CreatedAt | timestampVague}}</time>
|
<h3 id="pinned">Pinned posts</h3>
|
||||||
<b>Posts</b><span>{{.account.StatusesCount}}</span>
|
<a href="#recent">jump to recent</a>
|
||||||
<b>Followed by</b><span>{{.account.FollowersCount}}</span>
|
</div>
|
||||||
<b>Following</b><span>{{.account.FollowingCount}}</span>
|
<div class="thread">
|
||||||
</div>
|
{{- range .pinned_statuses }}
|
||||||
</section>
|
<article
|
||||||
|
class="status expanded"
|
||||||
<section class="toots">
|
{{- includeAttr "status_attributes.tmpl" . | indentAttr 6 }}
|
||||||
{{ if .pinned_statuses }}
|
>
|
||||||
<div class="col-header">
|
{{- include "status.tmpl" . | indent 6 }}
|
||||||
<h2>Pinned posts</h2>
|
</article>
|
||||||
<a href="#recent">jump to recent</a>
|
{{- end }}
|
||||||
</div>
|
</div>
|
||||||
<section class="thread">
|
</section>
|
||||||
{{ range .pinned_statuses }}
|
{{- end }}
|
||||||
<article class="toot expanded" id="{{.ID}}">
|
<section class="recent statuses" aria-labelledby="recent">
|
||||||
{{ template "status.tmpl" .}}
|
<div class="col-header">
|
||||||
</article>
|
<h3 id="recent" tabindex="-1">Recent posts</h3>
|
||||||
{{ end }}
|
{{- if .rssFeed }}
|
||||||
</section>
|
<a href="{{- .rssFeed -}}" class="rss-icon" aria-label="RSS feed">
|
||||||
{{ end }}
|
<i class="fa fa-rss-square" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
<div class="col-header">
|
{{- end }}
|
||||||
<h2 id="recent" tabindex="-1">Recent posts</h2>
|
</div>
|
||||||
{{ if .rssFeed }}
|
<div class="thread">
|
||||||
<a href="{{ .rssFeed }}" class="rss-icon" aria-label="RSS feed">
|
{{- if not .statuses }}
|
||||||
<i class="fa fa-rss-square" aria-hidden="true"></i>
|
<div data-nosnippet class="nothinghere">Nothing here!</div>
|
||||||
</a>
|
{{- else }}
|
||||||
{{ end }}
|
{{- range .statuses }}
|
||||||
</div>
|
<article
|
||||||
|
class="status expanded"
|
||||||
<section class="thread">
|
{{- includeAttr "status_attributes.tmpl" . | indentAttr 6 }}
|
||||||
{{ if not .statuses }}
|
>
|
||||||
<div data-nosnippet class="nothinghere">Nothing here!</div>
|
{{- include "status.tmpl" . | indent 6 }}
|
||||||
{{ else }}
|
</article>
|
||||||
{{ range .statuses }}
|
{{- end }}
|
||||||
<article class="toot expanded" id="{{.ID}}">
|
{{- end }}
|
||||||
{{ template "status.tmpl" .}}
|
</div>
|
||||||
</article>
|
<nav class="backnextlinks">
|
||||||
{{ end }}
|
{{- if .show_back_to_top }}
|
||||||
{{ end }}
|
<a href="/@{{- .account.Username -}}">Back to top</a>
|
||||||
</section>
|
{{- end }}
|
||||||
|
{{- if .statuses_next }}
|
||||||
<div class="backnextlinks">
|
<a href="{{- .statuses_next -}}" class="next">Show older</a>
|
||||||
{{ if .show_back_to_top }}
|
{{- end }}
|
||||||
<a href="/@{{ .account.Username }}">Back to top</a>
|
</nav>
|
||||||
{{ end }}
|
</section>
|
||||||
{{ if .statuses_next }}
|
</div>
|
||||||
<a href="{{ .statuses_next }}" class="next">Show older</a>
|
</div>
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
|
{{- end }}
|
||||||
{{ template "footer.tmpl" .}}
|
|
|
@ -17,30 +17,16 @@
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
<!-- footer.tmpl -->
|
{{- with . }}
|
||||||
</div>
|
<div class="fields">
|
||||||
<footer>
|
<h4 class="sr-only">Fields</h4>
|
||||||
<div id="version">
|
<dl>
|
||||||
<a name="Source code" href="https://github.com/superseriousbusiness/gotosocial">
|
{{- range .account.Fields }}
|
||||||
GoToSocial <span class="accent">{{.instance.Version}}</span>
|
<div class="field">
|
||||||
</a>
|
<dt>{{- emojify $.account.Emojis (noescape .Name) -}}</dt>
|
||||||
</div>
|
<dd>{{- emojify $.account.Emojis (noescape .Value) -}}</dd>
|
||||||
{{ if .instance.ContactAccount }}
|
</div>
|
||||||
<div id="contact">
|
{{- end }}
|
||||||
Contact: <a href="{{.instance.ContactAccount.URL}}" class="nounderline">{{.instance.ContactAccount.Username}}</a><br>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{- 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>
|
|
|
@ -17,10 +17,10 @@
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
{{ template "header.tmpl" .}}
|
{{- with . }}
|
||||||
<main>
|
<main>
|
||||||
<section class="login">
|
<section class="sign-in" aria-labelledby="sign-in">
|
||||||
<h1>Login</h1>
|
<h2 id="sign-in">Sign in</h2>
|
||||||
<form action="/auth/sign_in" method="POST">
|
<form action="/auth/sign_in" method="POST">
|
||||||
<div class="labelinput">
|
<div class="labelinput">
|
||||||
<label for="email">Email</label>
|
<label for="email">Email</label>
|
||||||
|
@ -30,8 +30,8 @@
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
<input type="password" class="form-control" name="password" required placeholder="Please enter your password">
|
<input type="password" class="form-control" name="password" required placeholder="Please enter your password">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-success">Login</button>
|
<button type="submit" class="btn btn-success">Sign in</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
{{ template "footer.tmpl" .}}
|
{{- end }}
|
|
@ -17,88 +17,74 @@
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
<a data-nosnippet href="{{- .URL -}}" class="toot-link">Open thread</a>
|
{{- define "statusContent" -}}
|
||||||
<section class="author">
|
{{- with .Content }}
|
||||||
<a href="{{- .Account.URL -}}">
|
<div class="content" lang="{{- $.LanguageTag.TagStr -}}">
|
||||||
<img class="avatar" src="{{- .Account.Avatar -}}" alt="">
|
{{ noescape . | emojify $.Emojis }}
|
||||||
<span aria-hidden="true" class="displayname">
|
</div>
|
||||||
{{- if .Account.DisplayName -}}
|
{{- end }}
|
||||||
{{- emojify .Account.Emojis (escape .Account.DisplayName) -}}
|
{{- end -}}
|
||||||
{{- else -}}
|
|
||||||
{{- .Account.Username -}}
|
{{- /*
|
||||||
{{- end -}}
|
When including this template, always wrap
|
||||||
</span>
|
it in an appropriate <article></article>!
|
||||||
<span aria-hidden="true" class="username">@{{- .Account.Username -}}</span>
|
*/ -}}
|
||||||
<span class="sr-only">
|
|
||||||
{{- if .Account.DisplayName -}}
|
{{- with . }}
|
||||||
{{- emojify .Account.Emojis (escape .Account.DisplayName) -}}. Username: @{{ .Account.Acct -}}.
|
<header class="status-header">
|
||||||
{{- else -}}
|
{{- include "status_header.tmpl" . | indent 1 }}
|
||||||
@{{- .Account.Acct -}}.
|
</header>
|
||||||
{{- end -}}
|
<div class="status-body">
|
||||||
</span>
|
{{- if .SpoilerText }}
|
||||||
</a>
|
<details class="text-spoiler">
|
||||||
</section>
|
<summary>
|
||||||
<section class="body">
|
<span class="spoiler-text" lang="{{- .LanguageTag.TagStr -}}">{{- emojify .Emojis (escape .SpoilerText) -}}</span>
|
||||||
{{- if .SpoilerText }}
|
<span class="button" role="button" tabindex="0">Toggle visibility</span>
|
||||||
<details class="text-spoiler">
|
</summary>
|
||||||
<summary>
|
<div class="text">
|
||||||
<span class="spoiler-text" lang="{{- .LanguageTag.TagStr -}}">{{- emojify .Emojis (escape .SpoilerText) -}}</span>
|
{{- with . }}
|
||||||
<span class="button" role="button" tabindex="0">Toggle visibility</span>
|
{{- include "statusContent" . | indent 3 }}
|
||||||
</summary>
|
{{- end }}
|
||||||
<div class="text">
|
{{- if .Poll }}
|
||||||
<div class="content" lang="{{- .LanguageTag.TagStr -}}">
|
{{- include "status_poll.tmpl" . | indent 3 }}
|
||||||
{{ emojify .Emojis (noescape .Content) }}
|
{{- end }}
|
||||||
</div>
|
</div>
|
||||||
{{- if .Poll }}
|
</details>
|
||||||
{{ template "status_poll.tmpl" . }}
|
{{- else }}
|
||||||
{{- end }}
|
<div class="text">
|
||||||
</div>
|
{{- with . }}
|
||||||
</details>
|
{{- include "statusContent" . | indent 2 }}
|
||||||
{{- else }}
|
{{- end }}
|
||||||
<div class="text">
|
{{- if .Poll }}
|
||||||
<div class="content" lang="{{- .LanguageTag.TagStr -}}">
|
{{- include "status_poll.tmpl" . | indent 2 }}
|
||||||
{{ emojify .Emojis (noescape .Content) }}
|
{{- end }}
|
||||||
</div>
|
</div>
|
||||||
{{- if .Poll }}
|
{{- end }}
|
||||||
{{ template "status_poll.tmpl" . }}
|
{{- if .MediaAttachments }}
|
||||||
{{- end }}
|
{{- include "status_attachments.tmpl" . | indent 1 }}
|
||||||
</div>
|
{{- end }}
|
||||||
{{- end }}
|
</div>
|
||||||
{{- if .MediaAttachments }}
|
<aside class="status-info" aria-hidden="true">
|
||||||
{{ template "status_attachments.tmpl" . }}
|
{{- include "status_info.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>
|
|
||||||
</aside>
|
</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 }}
|
|
@ -18,77 +18,119 @@
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
{{- /*
|
{{- /*
|
||||||
Template for rendering a gallery of status media attachments.
|
Template for rendering a gallery of status media attachments.
|
||||||
To use this template, pass a web view status into it.
|
To use this template, pass a web view status into it.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
{{ with .MediaAttachments }}
|
{{- define "imagePreview" }}
|
||||||
<div class="media photoswipe-gallery {{ (len .) | oddOrEven }} {{ if eq (len .) 1 }}single{{ else if eq (len .) 2 }}double{{- end -}}">
|
<img
|
||||||
{{- range $index, $media := . }}
|
src="{{- .PreviewURL -}}"
|
||||||
<div class="media-wrapper">
|
loading="lazy"
|
||||||
<details class="{{- $media.Type -}}-spoiler media-spoiler" {{- if not $media.Sensitive }} open{{ end -}}>
|
{{- if .Description }}
|
||||||
<summary>
|
alt="{{- .Description -}}"
|
||||||
<div class="show sensitive button" aria-hidden="true">Show sensitive media</div>
|
title="{{- .Description -}}"
|
||||||
<span class="eye button" role="button" tabindex="0" aria-label="Toggle media">
|
{{- end }}
|
||||||
<i class="hide fa fa-fw fa-eye-slash" aria-hidden="true"></i>
|
width="{{- .Meta.Original.Width -}}"
|
||||||
<i class="show fa fa-fw fa-eye" aria-hidden="true"></i>
|
height="{{- .Meta.Original.Height -}}"
|
||||||
</span>
|
/>
|
||||||
{{- if eq .Type "video" }}
|
|
||||||
<video {{- if .Description }} title="{{- $media.Description -}}" {{- end -}}>
|
|
||||||
<source type="video/mp4" src="{{- $media.URL -}}"/>
|
|
||||||
</video>
|
|
||||||
{{- else if eq .Type "image" }}
|
|
||||||
<img src="{{- $media.PreviewURL -}}" {{- if .Description }} title="{{- $media.Description -}}" {{- end }}/>
|
|
||||||
{{- end }}
|
|
||||||
</summary>
|
|
||||||
{{- if eq .Type "video" }}
|
|
||||||
<video
|
|
||||||
class="plyr-video photoswipe-slide"
|
|
||||||
controls
|
|
||||||
data-pswp-index="{{- $index -}}"
|
|
||||||
data-pswp-width="{{- $media.Meta.Original.Width -}}px"
|
|
||||||
data-pswp-height="{{- $media.Meta.Original.Height -}}px"
|
|
||||||
{{- if .Description }}
|
|
||||||
alt="{{- $media.Description -}}"
|
|
||||||
title="{{- $media.Description -}}"
|
|
||||||
{{- end }}
|
|
||||||
>
|
|
||||||
<source type="video/mp4" src="{{- $media.URL -}}"/>
|
|
||||||
</video>
|
|
||||||
{{- else if eq .Type "image" }}
|
|
||||||
<a
|
|
||||||
class="photoswipe-slide"
|
|
||||||
href="{{- $media.URL -}}"
|
|
||||||
target="_blank"
|
|
||||||
data-pswp-width="{{- $media.Meta.Original.Width -}}px"
|
|
||||||
data-pswp-height="{{- $media.Meta.Original.Height -}}px"
|
|
||||||
data-cropped="true"
|
|
||||||
{{- if .Description }}
|
|
||||||
title="{{- $media.Description -}}"
|
|
||||||
{{- end }}
|
|
||||||
>
|
|
||||||
<img src="{{$media.PreviewURL}}" {{if .Description}}alt="{{$media.Description}}" {{end}} />
|
|
||||||
</a>
|
|
||||||
{{- else }}
|
|
||||||
<a
|
|
||||||
class="unknown-attachment"
|
|
||||||
href="{{- $media.RemoteURL -}}"
|
|
||||||
target="_blank"
|
|
||||||
{{- if .Description }}
|
|
||||||
title="Link to external media: {{ $media.Description -}} {{- $media.RemoteURL -}}"
|
|
||||||
{{- else }}
|
|
||||||
title="Link to external media. {{- $media.RemoteURL -}}"
|
|
||||||
{{- end }}
|
|
||||||
>
|
|
||||||
<div class="placeholder" aria-hidden="true">
|
|
||||||
<i class="placeholder-external-link fa fa-external-link"></i>
|
|
||||||
<i class="placeholder-icon fa fa-file-text"></i>
|
|
||||||
<div class="placeholder-link-to">External media</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{{- end }}
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
{{- end }}
|
|
||||||
</div>
|
|
||||||
{{- end }}
|
{{- 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 -}}>
|
||||||
|
<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">
|
||||||
|
<i class="hide fa fa-fw fa-eye-slash" aria-hidden="true"></i>
|
||||||
|
<i class="show fa fa-fw fa-eye" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
{{- if eq .Type "video" }}
|
||||||
|
{{- include "videoPreview" $media | indent 4 }}
|
||||||
|
{{- else if eq .Type "image" }}
|
||||||
|
{{- include "imagePreview" $media | indent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
</summary>
|
||||||
|
{{- if eq .Type "video" }}
|
||||||
|
<video
|
||||||
|
class="plyr-video photoswipe-slide"
|
||||||
|
controls
|
||||||
|
data-pswp-index="{{- $index -}}"
|
||||||
|
data-pswp-width="{{- $media.Meta.Original.Width -}}px"
|
||||||
|
data-pswp-height="{{- $media.Meta.Original.Height -}}px"
|
||||||
|
{{- if .Description }}
|
||||||
|
alt="{{- $media.Description -}}"
|
||||||
|
title="{{- $media.Description -}}"
|
||||||
|
{{- end }}
|
||||||
|
>
|
||||||
|
<source type="video/mp4" src="{{- $media.URL -}}"/>
|
||||||
|
</video>
|
||||||
|
{{- else if eq .Type "image" }}
|
||||||
|
<a
|
||||||
|
class="photoswipe-slide"
|
||||||
|
href="{{- $media.URL -}}"
|
||||||
|
target="_blank"
|
||||||
|
data-pswp-width="{{- $media.Meta.Original.Width -}}px"
|
||||||
|
data-pswp-height="{{- $media.Meta.Original.Height -}}px"
|
||||||
|
data-cropped="true"
|
||||||
|
{{- if .Description }}
|
||||||
|
alt="{{- $media.Description -}}"
|
||||||
|
title="{{- $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="Open external media: {{ $media.Description -}} {{- $media.RemoteURL -}}"
|
||||||
|
{{- else }}
|
||||||
|
title="Open external media. {{- $media.RemoteURL -}}"
|
||||||
|
{{- end }}
|
||||||
|
>
|
||||||
|
<div class="placeholder" aria-hidden="true">
|
||||||
|
<i class="placeholder-external-link fa fa-external-link"></i>
|
||||||
|
<i class="placeholder-icon fa fa-file-text"></i>
|
||||||
|
<div class="placeholder-link-to">External media</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{{- end }}
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
{{- end }}
|
||||||
|
</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 }}
|
|
@ -18,51 +18,64 @@
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
{{- /*
|
{{- /*
|
||||||
Template for rendering a web view of a poll.
|
Template for rendering a web view of a poll.
|
||||||
To use this template, pass a web view status into it.
|
To use this template, pass a web view status into it.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
<figure class="poll">
|
{{- define "votes" -}}
|
||||||
<figcaption class="poll-info">
|
{{- if eq . 1 -}}
|
||||||
<span class="poll-expiry">
|
{{- . -}} vote
|
||||||
{{- if .Poll.Multiple -}}
|
{{- else -}}
|
||||||
Multiple-choice poll
|
{{- . }} votes
|
||||||
{{- else -}}
|
{{- end -}}
|
||||||
Poll
|
{{- end -}}
|
||||||
{{- end -}}
|
|
||||||
{{- if .Poll.Expired -}}
|
{{- with . }}
|
||||||
closed {{ .Poll.ExpiresAt | timestampPrecise -}}
|
<figure class="poll">
|
||||||
{{- else if .Poll.ExpiresAt -}}
|
<figcaption class="poll-info">
|
||||||
open until {{ .Poll.ExpiresAt | timestampPrecise -}}
|
<span class="poll-expiry">
|
||||||
{{- else -}}
|
{{- if .Poll.Multiple -}}
|
||||||
open forever
|
Multiple-choice poll
|
||||||
{{- end -}}
|
{{- else -}}
|
||||||
</span>
|
Poll
|
||||||
<span class="total-votes">Total votes: {{ .Poll.VotesCount }}</span>
|
{{- end -}}
|
||||||
</figcaption>
|
{{- if .Poll.Expired -}}
|
||||||
<ul class="poll-options">
|
closed <time datetime="{{- .Poll.ExpiresAt -}}">{{- .Poll.ExpiresAt | timestampPrecise -}}</time>
|
||||||
{{- range $index, $pollOption := .WebPollOptions }}
|
{{- else if .Poll.ExpiresAt -}}
|
||||||
<li class="poll-option">
|
open until <time datetime="{{- .Poll.ExpiresAt -}}">{{- .Poll.ExpiresAt | timestampPrecise -}}</time>
|
||||||
<label aria-hidden="true" for="poll-{{- $pollOption.PollID -}}-option-{{- increment $index -}}" lang="{{- .LanguageTag.TagStr -}}">{{- emojify .Emojis (noescape $pollOption.Title) -}}</label>
|
{{- else -}}
|
||||||
<meter aria-hidden="true" id="poll-{{- $pollOption.PollID -}}-option-{{- increment $index -}}" min="0" max="100" value="{{- $pollOption.VoteShare -}}">{{- $pollOption.VoteShare -}}%</meter>
|
open forever
|
||||||
<div class="sr-only">Option {{ increment $index }}: <span lang="{{ .LanguageTag.TagStr }}">{{ emojify .Emojis (noescape $pollOption.Title) -}}</span></div>
|
{{- end -}}
|
||||||
<div class="poll-vote-summary">
|
</span>
|
||||||
{{- if isNil $pollOption.VotesCount }}
|
<span class="sr-only">,</span>
|
||||||
Results not yet published.
|
<span class="total-votes">
|
||||||
{{- else -}}
|
{{- template "votes" .Poll.VotesCount -}}
|
||||||
{{- with deref $pollOption.VotesCount }}
|
{{- if .Poll.Expired -}}
|
||||||
<span class="poll-vote-share">{{- $pollOption.VoteShareStr -}}%</span>
|
total
|
||||||
<span class="poll-vote-count">
|
{{- else -}}
|
||||||
{{- if eq . 1 -}}
|
so far
|
||||||
{{- . }} vote
|
{{- end -}}
|
||||||
{{- else -}}
|
</span>
|
||||||
{{- . }} votes
|
</figcaption>
|
||||||
{{- end -}}
|
<ul class="poll-options nodot">
|
||||||
</span>
|
{{- range $index, $pollOption := .WebPollOptions }}
|
||||||
{{- end -}}
|
<li class="poll-option">
|
||||||
{{- end }}
|
<span class="sr-only">Option {{ increment $index }},</span>
|
||||||
</div>
|
<span lang="{{- .LanguageTag.TagStr -}}">{{ emojify .Emojis (noescape $pollOption.Title) }}</span>
|
||||||
</li>
|
<meter aria-hidden="true" min="0" max="100" value="{{- $pollOption.VoteShare -}}"></meter>
|
||||||
{{- end }}
|
<div class="poll-vote-summary">
|
||||||
</ul>
|
{{- if isNil $pollOption.VotesCount }}
|
||||||
</figure>
|
Results not yet published.
|
||||||
|
{{- else }}
|
||||||
|
{{- with deref $pollOption.VotesCount }}
|
||||||
|
<span class="poll-vote-share">{{- $pollOption.VoteShareStr -}}%</span>
|
||||||
|
<span class="sr-only">,</span>
|
||||||
|
<span class="poll-vote-count">{{- template "votes" . -}}</span>
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{{- end }}
|
||||||
|
</ul>
|
||||||
|
</figure>
|
||||||
|
{{- end }}
|
|
@ -17,11 +17,13 @@
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
{{ template "header.tmpl" .}}
|
{{- with . }}
|
||||||
|
<main>
|
||||||
<main class="thread">
|
<h2 id="tag-name" tabindex="-1">#{{- .tagName -}}</h2>
|
||||||
<h2 id="tag-name" tabindex="-1">#{{.tagName}}</h2>
|
<p>There's nothing here!</p>
|
||||||
<p>There's nothing here yet!</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>
|
</main>
|
||||||
|
{{- end }}
|
||||||
{{ template "footer.tmpl" .}}
|
|
|
@ -17,22 +17,45 @@
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
{{ template "header.tmpl" .}}
|
{{- define "threadLength" -}}
|
||||||
<main>
|
{{- with $length := add (len $.context.Ancestors) (len $.context.Descendants) | increment -}}
|
||||||
<section data-nosnippet class="thread">
|
{{- if eq $length 1 -}}
|
||||||
{{range .context.Ancestors}}
|
{{- $length }} post
|
||||||
<article class="toot" id="{{.ID}}">
|
{{- else -}}
|
||||||
{{ template "status.tmpl" .}}
|
{{- $length }} posts
|
||||||
</article>
|
{{- end -}}
|
||||||
{{end}}
|
{{- end -}}
|
||||||
<article class="toot expanded" id="{{.status.ID}}">
|
{{- end -}}
|
||||||
{{ template "status.tmpl" .status}}
|
|
||||||
</article>
|
{{- with . }}
|
||||||
{{range .context.Descendants}}
|
<main data-nosnippet class="thread" aria-labelledby="thread-summary">
|
||||||
<article class="toot" id="{{.ID}}">
|
<div class="col-header">
|
||||||
{{ template "status.tmpl" .}}
|
<h2 id="thread-summary">Thread with {{ template "threadLength" . -}}</h2>
|
||||||
</article>
|
<a href="#{{- .status.ID -}}">jump to expanded post</a>
|
||||||
{{end}}
|
</div>
|
||||||
</section>
|
{{- range .context.Ancestors }}
|
||||||
|
<article
|
||||||
|
class="status"
|
||||||
|
{{- includeAttr "status_attributes.tmpl" . | indentAttr 2 }}
|
||||||
|
>
|
||||||
|
{{- include "status.tmpl" . | indent 2 }}
|
||||||
|
</article>
|
||||||
|
{{- end }}
|
||||||
|
{{- with .status }}
|
||||||
|
<article
|
||||||
|
class="status expanded"
|
||||||
|
{{- includeAttr "status_attributes.tmpl" . | indentAttr 2 }}
|
||||||
|
>
|
||||||
|
{{- include "status.tmpl" . | indent 2 }}
|
||||||
|
</article>
|
||||||
|
{{- end }}
|
||||||
|
{{- range .context.Descendants }}
|
||||||
|
<article
|
||||||
|
class="status"
|
||||||
|
{{- includeAttr "status_attributes.tmpl" . | indentAttr 2 }}
|
||||||
|
>
|
||||||
|
{{- include "status.tmpl" . | indent 2 }}
|
||||||
|
</article>
|
||||||
|
{{- end }}
|
||||||
</main>
|
</main>
|
||||||
{{ template "footer.tmpl" .}}
|
{{- end }}
|
Loading…
Reference in a new issue