mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-30 23:12:25 -05: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: | ||||||
|  | 				"avatar author-strap author-strap" | ||||||
|  | 				"avatar author-strap author-strap"; | ||||||
|  | 			gap: 0 0.5rem; | ||||||
|  | 			font-style: normal; | ||||||
| 	 | 	 | ||||||
| 			border: 0.15rem solid $avatar-border; | 			.avatar { | ||||||
| 			border-radius: $br; | 				grid-area: avatar; | ||||||
| 			overflow: hidden; /* hides corners from img overflowing */ | 				height: 3.5rem; | ||||||
| 
 | 				width: 3.5rem; | ||||||
| 			img { |  | ||||||
| 				height: 100%; |  | ||||||
| 				width: 100%; |  | ||||||
| 				object-fit: cover; | 				object-fit: cover; | ||||||
| 				background: $bg; | 	 | ||||||
|  | 				border: 0.15rem solid $avatar-border; | ||||||
|  | 				border-radius: $br; | ||||||
|  | 				overflow: hidden; /* hides corners from img overflowing */ | ||||||
|  | 	 | ||||||
|  | 				img { | ||||||
|  | 					height: 100%; | ||||||
|  | 					width: 100%; | ||||||
|  | 					object-fit: cover; | ||||||
|  | 					background: $bg; | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		} |  | ||||||
| 	 | 	 | ||||||
| 		.displayname, .username { | 			.author-strap { | ||||||
| 			justify-self: start; | 				grid-area: author-strap; | ||||||
| 			align-self: start; | 				display: grid; | ||||||
|  | 				grid-template-columns: 1fr auto; | ||||||
|  | 				grid-template-rows: auto; | ||||||
|  | 				grid-template-areas: | ||||||
|  | 					"display display" | ||||||
|  | 					"user    user"; | ||||||
|  | 				gap: 0 0.5rem; | ||||||
| 	 | 	 | ||||||
| 			max-width: 100%; | 				.displayname, .username { | ||||||
| 			white-space: nowrap; | 					justify-self: start; | ||||||
| 			overflow: hidden; | 					align-self: start; | ||||||
| 			text-overflow: ellipsis; | 					max-width: 100%; | ||||||
| 		} | 					font-size: 1rem; | ||||||
|  | 					line-height: 1.3rem; | ||||||
|  | 				} | ||||||
| 			 | 			 | ||||||
| 		.displayname { | 				.displayname { | ||||||
| 			grid-area: display; | 					grid-area: display; | ||||||
| 			font-weight: bold; | 					font-weight: bold; | ||||||
| 			font-size: 1rem; | 				} | ||||||
| 			line-height: 1.3rem; |  | ||||||
| 			/* margin-top: -0.5rem; */ |  | ||||||
| 		} |  | ||||||
| 		 | 		 | ||||||
| 		.username { | 				.username { | ||||||
| 			grid-area: user; | 					grid-area: user; | ||||||
| 			color: $link-fg; | 					color: $link-fg; | ||||||
| 			font-size: 1rem; | 				} | ||||||
| 			line-height: 1.3rem; | 			} | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		.timestamp { |  | ||||||
| 			grid-area: date; |  | ||||||
| 			color: $fg-reduced; |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	.body { | 	.status-body { | ||||||
| 		padding: 0.5rem 0.75rem; | 		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 inside a pre block, ie., | ||||||
|  | 					 | ||||||
|  | 					``` | ||||||
|  | 					here is some code | ||||||
|  | 					``` | ||||||
|  | 				*/ | ||||||
| 				code { | 				code { | ||||||
| 					padding: 0.5rem; | 					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; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | @ -1163,3 +1202,13 @@ button.with-padding { | ||||||
| 		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" }} | {{- end }} | ||||||
| 					<video {{- if .Description }} title="{{- $media.Description -}}" {{- end -}}> | 
 | ||||||
| 						<source type="video/mp4" src="{{- $media.URL -}}"/> | {{- define "videoPreview" }} | ||||||
| 					</video> | <video | ||||||
| 					{{- else if eq .Type "image" }} |     {{- if .Description }} | ||||||
| 					<img src="{{- $media.PreviewURL -}}" {{- if .Description }} title="{{- $media.Description -}}" {{- end }}/> |     alt="{{- .Description -}}" | ||||||
| 					{{- end }} |     title="{{- .Description -}}" | ||||||
| 				</summary> |     {{- end }} | ||||||
| 				{{- if eq .Type "video" }} |     width="{{- .Meta.Original.Width -}}" | ||||||
| 				<video |     height="{{- .Meta.Original.Height -}}" | ||||||
| 					class="plyr-video photoswipe-slide" | > | ||||||
| 					controls |     <source type="video/mp4" src="{{- .URL -}}"/> | ||||||
| 					data-pswp-index="{{- $index -}}" | </video> | ||||||
| 					data-pswp-width="{{- $media.Meta.Original.Width -}}px" | {{- end }} | ||||||
| 					data-pswp-height="{{- $media.Meta.Original.Height -}}px" | 
 | ||||||
| 					{{- if .Description }} | {{- /* Produces something like "1 attachment", "2 attachments", etc */ -}} | ||||||
| 					alt="{{- $media.Description -}}" | {{- define "attachmentsLength" -}} | ||||||
| 					title="{{- $media.Description -}}" | {{- (len .) }}{{- if eq (len .) 1 }} attachment{{- else }} attachments{{- end -}} | ||||||
| 					{{- end }} | {{- end -}} | ||||||
| 				> | 
 | ||||||
| 					<source type="video/mp4" src="{{- $media.URL -}}"/> | {{- /* Produces something like "media photoswipe-gallery odd single" */ -}} | ||||||
| 				</video> | {{- define "galleryClass" -}} | ||||||
| 				{{- else if eq .Type "image" }} | media photoswipe-gallery {{ (len .) | oddOrEven }} {{ if eq (len .) 1 }}single{{ else if eq (len .) 2 }}double{{ end }} | ||||||
| 				<a | {{- end -}} | ||||||
| 					class="photoswipe-slide" | 
 | ||||||
| 					href="{{- $media.URL -}}" | {{- with .MediaAttachments }} | ||||||
| 					target="_blank" | <div | ||||||
| 					data-pswp-width="{{- $media.Meta.Original.Width -}}px" |     class="{{- template "galleryClass" . -}}" | ||||||
| 					data-pswp-height="{{- $media.Meta.Original.Height -}}px" |     role="group" | ||||||
| 					data-cropped="true" |     aria-label="{{- template "attachmentsLength" . -}}" | ||||||
| 					{{- if .Description }} | > | ||||||
| 					title="{{- $media.Description -}}" |     {{- range $index, $media := . }} | ||||||
| 					{{- end }} |     <div class="media-wrapper"> | ||||||
| 				> |         <details class="{{- $media.Type -}}-spoiler media-spoiler" {{- if not $media.Sensitive }} open{{- end -}}> | ||||||
| 					<img src="{{$media.PreviewURL}}" {{if .Description}}alt="{{$media.Description}}" {{end}} /> |             <summary> | ||||||
| 				</a> |                 <div class="show sensitive button" aria-hidden="true">Show sensitive media</div> | ||||||
| 				{{- else }} |                 <span class="eye button" role="button" tabindex="0" aria-label="Toggle media"> | ||||||
| 				<a |                     <i class="hide fa fa-fw fa-eye-slash" aria-hidden="true"></i> | ||||||
| 					class="unknown-attachment" |                     <i class="show fa fa-fw fa-eye" aria-hidden="true"></i> | ||||||
| 					href="{{- $media.RemoteURL -}}" |                 </span> | ||||||
| 					target="_blank" |                 {{- if eq .Type "video" }} | ||||||
| 					{{- if .Description }} |                 {{- include "videoPreview" $media | indent 4 }} | ||||||
| 					title="Link to external media: {{ $media.Description -}}

{{- $media.RemoteURL -}}" |                 {{- else if eq .Type "image" }} | ||||||
| 					{{- else }} |                 {{- include "imagePreview" $media | indent 4 }} | ||||||
| 					title="Link to external media.

{{- $media.RemoteURL -}}" |                 {{- end }} | ||||||
| 					{{- end }} |             </summary> | ||||||
| 				> |             {{- if eq .Type "video" }} | ||||||
| 					<div class="placeholder" aria-hidden="true"> |             <video | ||||||
| 						<i class="placeholder-external-link fa fa-external-link"></i> |                 class="plyr-video photoswipe-slide" | ||||||
| 						<i class="placeholder-icon fa fa-file-text"></i> |                 controls | ||||||
| 						<div class="placeholder-link-to">External media</div> |                 data-pswp-index="{{- $index -}}" | ||||||
| 					</div> |                 data-pswp-width="{{- $media.Meta.Original.Width -}}px" | ||||||
| 				</a> |                 data-pswp-height="{{- $media.Meta.Original.Height -}}px" | ||||||
| 				{{- end }} |                 {{- if .Description }} | ||||||
| 			</details> |                 alt="{{- $media.Description -}}" | ||||||
| 		</div> |                 title="{{- $media.Description -}}" | ||||||
| 	{{- end }} |                 {{- end }} | ||||||
| 	</div> |             > | ||||||
|  |                 <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 }} | {{- 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…
	
	Add table
		Add a link
		
	
		Reference in a new issue