summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2024-03-25 18:32:24 +0100
committerLibravatar GitHub <noreply@github.com>2024-03-25 17:32:24 +0000
commit8953f57d887c060c3b58f83c38d2010d27a45ef3 (patch)
tree05f8b1157a86afaa3ed0d6d0b87c9d0d37030362 /internal
parent[feature] Add healthcheck endpoints `/livez` and `/readyz` (#2783) (diff)
downloadgotosocial-8953f57d887c060c3b58f83c38d2010d27a45ef3.tar.xz
[feature] User-selectable preset CSS themes for accounts (#2777)
* [feature] User-selectable preset themes * docs, more theme stuff * lint, tests * fix css name * correct some little issues * add another theme * fix poll background * okay last theme i swear * make retrieval of apimodel themes more conventional * preallocate stylesheet slices
Diffstat (limited to 'internal')
-rw-r--r--internal/api/client/accounts/accounts.go4
-rw-r--r--internal/api/client/accounts/accountupdate.go1
-rw-r--r--internal/api/client/accounts/lists.go2
-rw-r--r--internal/api/client/accounts/statuses.go2
-rw-r--r--internal/api/client/accounts/themesget.go77
-rw-r--r--internal/api/model/account.go6
-rw-r--r--internal/api/model/theme.go32
-rw-r--r--internal/db/bundb/migrations/20240320114447_preset_css_themes.go55
-rw-r--r--internal/gtsmodel/account.go14
-rw-r--r--internal/gtsmodel/accountsettings.go1
-rw-r--r--internal/processing/account/account.go2
-rw-r--r--internal/processing/account/themes.go151
-rw-r--r--internal/processing/account/themes_test.go52
-rw-r--r--internal/processing/account/update.go16
-rw-r--r--internal/typeutils/internaltofrontend.go18
-rw-r--r--internal/web/profile.go42
-rw-r--r--internal/web/thread.go41
-rw-r--r--internal/web/web.go1
18 files changed, 496 insertions, 21 deletions
diff --git a/internal/api/client/accounts/accounts.go b/internal/api/client/accounts/accounts.go
index c94fbfbaa..8b92bd7a5 100644
--- a/internal/api/client/accounts/accounts.go
+++ b/internal/api/client/accounts/accounts.go
@@ -55,6 +55,7 @@ const (
VerifyPath = BasePath + "/verify_credentials"
MovePath = BasePath + "/move"
AliasPath = BasePath + "/alias"
+ ThemesPath = BasePath + "/themes"
)
type Module struct {
@@ -114,4 +115,7 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
// migration handlers
attachHandler(http.MethodPost, AliasPath, m.AccountAliasPOSTHandler)
attachHandler(http.MethodPost, MovePath, m.AccountMovePOSTHandler)
+
+ // account themes
+ attachHandler(http.MethodGet, ThemesPath, m.AccountThemesGETHandler)
}
diff --git a/internal/api/client/accounts/accountupdate.go b/internal/api/client/accounts/accountupdate.go
index 02c0fc5ac..905d11479 100644
--- a/internal/api/client/accounts/accountupdate.go
+++ b/internal/api/client/accounts/accountupdate.go
@@ -309,6 +309,7 @@ func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateCredentialsRequest,
form.Source.Language == nil &&
form.Source.StatusContentType == nil &&
form.FieldsAttributes == nil &&
+ form.Theme == nil &&
form.CustomCSS == nil &&
form.EnableRSS == nil) {
return nil, errors.New("empty form submitted")
diff --git a/internal/api/client/accounts/lists.go b/internal/api/client/accounts/lists.go
index d42fdd3f9..7bd1227a8 100644
--- a/internal/api/client/accounts/lists.go
+++ b/internal/api/client/accounts/lists.go
@@ -69,7 +69,7 @@ import (
// '500':
// description: internal server error
func (m *Module) AccountListsGETHandler(c *gin.Context) {
- authed, err := oauth.Authed(c, false, false, false, false)
+ authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/accounts/statuses.go b/internal/api/client/accounts/statuses.go
index 7dd4cbe37..a72a464ed 100644
--- a/internal/api/client/accounts/statuses.go
+++ b/internal/api/client/accounts/statuses.go
@@ -134,7 +134,7 @@ import (
// '500':
// description: internal server error
func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
- authed, err := oauth.Authed(c, false, false, false, false)
+ authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/accounts/themesget.go b/internal/api/client/accounts/themesget.go
new file mode 100644
index 000000000..5a0cb6d94
--- /dev/null
+++ b/internal/api/client/accounts/themesget.go
@@ -0,0 +1,77 @@
+// 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 accounts
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// AccountThemesGETHandler swagger:operation GET /api/v1/accounts/themes accountThemes
+//
+// See preset CSS themes available to accounts on this instance.
+//
+// ---
+// tags:
+// - accounts
+//
+// produces:
+// - application/json
+//
+// security:
+// - OAuth2 Bearer:
+// - read:accounts
+//
+// responses:
+// '200':
+// name: statuses
+// description: Array of themes.
+// schema:
+// type: array
+// items:
+// "$ref": "#/definitions/theme"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) AccountThemesGETHandler(c *gin.Context) {
+ _, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ // Retrieve available themes.
+ themes := m.processor.Account().ThemesGet()
+ apiutil.JSON(c, http.StatusOK, themes)
+}
diff --git a/internal/api/model/account.go b/internal/api/model/account.go
index 7d3fa4b62..af2a394af 100644
--- a/internal/api/model/account.go
+++ b/internal/api/model/account.go
@@ -89,6 +89,8 @@ type Account struct {
MuteExpiresAt string `json:"mute_expires_at,omitempty"`
// Extra profile information. Shown only if the requester owns the account being requested.
Source *Source `json:"source,omitempty"`
+ // Filename of user-selected CSS theme to include when rendering this account's profile or statuses. Eg., `blurple-light.css`.
+ Theme string `json:"theme,omitempty"`
// CustomCSS to include when rendering this account's profile or statuses.
CustomCSS string `json:"custom_css,omitempty"`
// Account has enabled RSS feed.
@@ -162,7 +164,11 @@ type UpdateCredentialsRequest struct {
FieldsAttributes *[]UpdateField `form:"fields_attributes" json:"-"`
// Profile metadata names and values, parsed from JSON.
JSONFieldsAttributes *map[string]UpdateField `form:"-" json:"fields_attributes"`
+ // Theme file name to be used when rendering this account's profile or statuses.
+ // Use empty string to unset.
+ Theme *string `form:"theme" json:"theme"`
// Custom CSS to be included when rendering this account's profile or statuses.
+ // Use empty string to unset.
CustomCSS *string `form:"custom_css" json:"custom_css"`
// Enable RSS feed of public toots for this account at /@[username]/feed.rss
EnableRSS *bool `form:"enable_rss" json:"enable_rss"`
diff --git a/internal/api/model/theme.go b/internal/api/model/theme.go
new file mode 100644
index 000000000..b98e7395a
--- /dev/null
+++ b/internal/api/model/theme.go
@@ -0,0 +1,32 @@
+// 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 model
+
+// Theme represents one user-selectable preset CSS theme.
+//
+// swagger:model theme
+type Theme struct {
+ // User-facing title of this theme.
+ Title string `json:"title"`
+
+ // User-facing description of this theme.
+ Description string `json:"description"`
+
+ // FileName of this theme in the themes directory.
+ FileName string `json:"file_name"`
+}
diff --git a/internal/db/bundb/migrations/20240320114447_preset_css_themes.go b/internal/db/bundb/migrations/20240320114447_preset_css_themes.go
new file mode 100644
index 000000000..5d3b9f83a
--- /dev/null
+++ b/internal/db/bundb/migrations/20240320114447_preset_css_themes.go
@@ -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/>.
+
+package migrations
+
+import (
+ "context"
+ "strings"
+
+ "github.com/uptrace/bun"
+)
+
+func init() {
+ up := func(ctx context.Context, db *bun.DB) error {
+ // Add theme to account settings table.
+ _, err := db.ExecContext(ctx,
+ "ALTER TABLE ? ADD COLUMN ? TEXT",
+ bun.Ident("account_settings"), bun.Ident("theme"),
+ )
+ if err != nil {
+ e := err.Error()
+ if !(strings.Contains(e, "already exists") ||
+ strings.Contains(e, "duplicate column name") ||
+ strings.Contains(e, "SQLSTATE 42701")) {
+ return err
+ }
+ }
+
+ return nil
+ }
+
+ down := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ return nil
+ })
+ }
+
+ if err := Migrations.Register(up, down); err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go
index 2ac107e56..3bbcb37e3 100644
--- a/internal/gtsmodel/account.go
+++ b/internal/gtsmodel/account.go
@@ -220,3 +220,17 @@ type Relationship struct {
Endorsed bool // Are you featuring this user on your profile?
Note string // Your note on this account.
}
+
+// Theme represents a user-selected
+// CSS theme for an account.
+type Theme struct {
+ // User-facing title of this theme.
+ Title string
+
+ // User-facing description of this theme.
+ Description string
+
+ // FileName of this theme in the themes
+ // directory (eg., `light-blurple.css`).
+ FileName string
+}
diff --git a/internal/gtsmodel/accountsettings.go b/internal/gtsmodel/accountsettings.go
index cb5411050..218767023 100644
--- a/internal/gtsmodel/accountsettings.go
+++ b/internal/gtsmodel/accountsettings.go
@@ -29,6 +29,7 @@ type AccountSettings struct {
Sensitive *bool `bun:",nullzero,notnull,default:false"` // Set posts from this account to sensitive by default?
Language string `bun:",nullzero,notnull,default:'en'"` // What language does this account post in?
StatusContentType string `bun:",nullzero"` // What is the default format for statuses posted by this account (only for local accounts).
+ Theme string `bun:",nullzero"` // Preset CSS theme filename selected by this Account (empty string if nothing set).
CustomCSS string `bun:",nullzero"` // Custom CSS that should be displayed for this Account's profile and statuses.
EnableRSS *bool `bun:",nullzero,notnull,default:false"` // enable RSS feed subscription for this account's public posts at [URL]/feed
HideCollections *bool `bun:",nullzero,notnull,default:false"` // Hide this account's followers/following collections.
diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go
index 79f6ecfc1..dbcecdb0a 100644
--- a/internal/processing/account/account.go
+++ b/internal/processing/account/account.go
@@ -44,6 +44,7 @@ type Processor struct {
formatter *text.Formatter
federator *federation.Federator
parseMention gtsmodel.ParseMentionFunc
+ themes *Themes
}
// New returns a new account processor.
@@ -67,5 +68,6 @@ func New(
formatter: text.NewFormatter(state.DB),
federator: federator,
parseMention: parseMention,
+ themes: PopulateThemes(),
}
}
diff --git a/internal/processing/account/themes.go b/internal/processing/account/themes.go
new file mode 100644
index 000000000..4f8cc49a1
--- /dev/null
+++ b/internal/processing/account/themes.go
@@ -0,0 +1,151 @@
+// 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 account
+
+import (
+ "cmp"
+ "os"
+ "path/filepath"
+ "regexp"
+ "slices"
+ "strings"
+
+ "codeberg.org/gruf/go-bytesize"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
+)
+
+var (
+ themeTitleRegex = regexp.MustCompile(`(?m)^\ *theme-title:(.*)$`)
+ themeDescriptionRegex = regexp.MustCompile(`(?m)^\ *theme-description:(.*)$`)
+)
+
+// GetThemes returns available account css themes.
+func (p *Processor) ThemesGet() []apimodel.Theme {
+ return p.converter.ThemesToAPIThemes(p.themes.SortedByTitle)
+}
+
+// Themes represents an in-memory
+// storage structure for themes.
+type Themes struct {
+ // Themes sorted alphabetically
+ // by title (case insensitive).
+ SortedByTitle []*gtsmodel.Theme
+
+ // ByFileName contains themes retrievable
+ // by their filename eg., `light-blurple.css`.
+ ByFileName map[string]*gtsmodel.Theme
+}
+
+// PopulateThemes parses available account CSS
+// themes from the web assets themes directory.
+func PopulateThemes() *Themes {
+ webAssetsAbsFilePath, err := filepath.Abs(config.GetWebAssetBaseDir())
+ if err != nil {
+ log.Panicf(nil, "error getting abs path for web assets: %v", err)
+ }
+
+ themesAbsFilePath := filepath.Join(webAssetsAbsFilePath, "themes")
+ themesFiles, err := os.ReadDir(themesAbsFilePath)
+ if err != nil {
+ log.Warnf(nil, "error reading themes at %s: %v", themesAbsFilePath, err)
+ return nil
+ }
+
+ themes := &Themes{
+ ByFileName: make(map[string]*gtsmodel.Theme),
+ }
+
+ for _, f := range themesFiles {
+ // Ignore nested directories.
+ if f.IsDir() {
+ continue
+ }
+
+ // Ignore weird files.
+ info, err := f.Info()
+ if err != nil {
+ continue
+ }
+
+ // Ignore really big files.
+ if info.Size() > int64(bytesize.MiB) {
+ continue
+ }
+
+ // Get just the name of the
+ // file, eg `blurple-light.css`.
+ fileName := f.Name()
+
+ // Get just the `.css` part.
+ extensionWithDot := filepath.Ext(fileName)
+
+ // Remove any leading `.`
+ extension := strings.TrimPrefix(extensionWithDot, ".")
+
+ // Ignore non-css files.
+ if extension != "css" {
+ continue
+ }
+
+ // Load the file contents.
+ path := filepath.Join(themesAbsFilePath, fileName)
+ contents, err := os.ReadFile(path)
+ if err != nil {
+ log.Warnf(nil, "error reading css theme at %s: %v", path, err)
+ continue
+ }
+
+ // Try to parse a title and description
+ // for this theme from the file itself.
+ var themeTitle string
+ titleMatches := themeTitleRegex.FindSubmatch(contents)
+ if len(titleMatches) == 2 {
+ themeTitle = strings.TrimSpace(string(titleMatches[1]))
+ } else {
+ // Fall back to file name
+ // without `.css` suffix.
+ themeTitle = strings.TrimSuffix(fileName, ".css")
+ }
+
+ var themeDescription string
+ descMatches := themeDescriptionRegex.FindSubmatch(contents)
+ if len(descMatches) == 2 {
+ themeDescription = strings.TrimSpace(string(descMatches[1]))
+ }
+
+ theme := &gtsmodel.Theme{
+ Title: themeTitle,
+ Description: themeDescription,
+ FileName: fileName,
+ }
+
+ themes.SortedByTitle = append(themes.SortedByTitle, theme)
+ themes.ByFileName[fileName] = theme
+ }
+
+ // Sort themes alphabetically
+ // by title (case insensitive).
+ slices.SortFunc(themes.SortedByTitle, func(a, b *gtsmodel.Theme) int {
+ return cmp.Compare(strings.ToLower(a.Title), strings.ToLower(b.Title))
+ })
+
+ return themes
+}
diff --git a/internal/processing/account/themes_test.go b/internal/processing/account/themes_test.go
new file mode 100644
index 000000000..9506aee50
--- /dev/null
+++ b/internal/processing/account/themes_test.go
@@ -0,0 +1,52 @@
+// 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 account_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/processing/account"
+)
+
+type ThemesTestSuite struct {
+ AccountStandardTestSuite
+}
+
+func (suite *ThemesTestSuite) TestPopulateThemes() {
+ config.SetWebAssetBaseDir("../../../web/assets")
+
+ themes := account.PopulateThemes()
+ if themes == nil {
+ suite.FailNow("themes was nil")
+ }
+
+ suite.NotEmpty(themes.SortedByTitle)
+ theme := themes.ByFileName["blurple-light.css"]
+ if theme == nil {
+ suite.FailNow("theme was nil")
+ }
+ suite.Equal("Blurple (light)", theme.Title)
+ suite.Equal("Official light blurple theme", theme.Description)
+ suite.Equal("blurple-light.css", theme.FileName)
+}
+
+func TestThemesTestSuite(t *testing.T) {
+ suite.Run(t, new(ThemesTestSuite))
+}
diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go
index 7b5561138..076b6d7f4 100644
--- a/internal/processing/account/update.go
+++ b/internal/processing/account/update.go
@@ -256,6 +256,22 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
}
}
+ if form.Theme != nil {
+ theme := *form.Theme
+ if theme == "" {
+ // Empty is easy, just clear this.
+ account.Settings.Theme = ""
+ } else {
+ // Theme was provided, check
+ // against known available themes.
+ if _, ok := p.themes.ByFileName[theme]; !ok {
+ err := fmt.Errorf("theme %s not available on this instance, see /api/v1/accounts/themes for available themes", theme)
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
+ }
+ account.Settings.Theme = theme
+ }
+ }
+
if form.CustomCSS != nil {
customCSS := *form.CustomCSS
if err := validate.CustomCSS(customCSS); err != nil {
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index 0ff3c2268..bf44c7254 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -170,12 +170,13 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
// Bits that vary between remote + local accounts:
// - Account (acct) string.
// - Role.
- // - Settings things (enableRSS, customCSS).
+ // - Settings things (enableRSS, theme, customCSS).
var (
acct string
role *apimodel.AccountRole
enableRSS bool
+ theme string
customCSS string
)
@@ -208,6 +209,7 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
}
enableRSS = *a.Settings.EnableRSS
+ theme = a.Settings.Theme
customCSS = a.Settings.CustomCSS
}
@@ -272,6 +274,7 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
Emojis: apiEmojis,
Fields: fields,
Suspended: !a.SuspendedAt.IsZero(),
+ Theme: theme,
CustomCSS: customCSS,
EnableRSS: enableRSS,
Role: role,
@@ -1771,3 +1774,16 @@ func (c *Converter) convertTagsToAPITags(ctx context.Context, tags []*gtsmodel.T
return apiTags, errs.Combine()
}
+
+// ThemesToAPIThemes converts a slice of gtsmodel Themes into apimodel Themes.
+func (c *Converter) ThemesToAPIThemes(themes []*gtsmodel.Theme) []apimodel.Theme {
+ apiThemes := make([]apimodel.Theme, len(themes))
+ for i, theme := range themes {
+ apiThemes[i] = apimodel.Theme{
+ Title: theme.Title,
+ Description: theme.Description,
+ FileName: theme.FileName,
+ }
+ }
+ return apiThemes
+}
diff --git a/internal/web/profile.go b/internal/web/profile.go
index b629c98b9..a4809a72d 100644
--- a/internal/web/profile.go
+++ b/internal/web/profile.go
@@ -140,16 +140,40 @@ func (m *Module) profileGETHandler(c *gin.Context) {
return
}
+ // Prepare stylesheets for profile.
+ stylesheets := make([]string, 0, 6)
+
+ // Basic profile stylesheets.
+ stylesheets = append(
+ stylesheets,
+ []string{
+ cssFA,
+ cssStatus,
+ cssThread,
+ cssProfile,
+ }...,
+ )
+
+ // User-selected theme if set.
+ if theme := targetAccount.Theme; theme != "" {
+ stylesheets = append(
+ stylesheets,
+ themesPathPrefix+"/"+theme,
+ )
+ }
+
+ // Custom CSS for this user last in cascade.
+ stylesheets = append(
+ stylesheets,
+ "/@"+targetAccount.Username+"/custom.css",
+ )
+
page := apiutil.WebPage{
- Template: "profile.tmpl",
- Instance: instance,
- OGMeta: apiutil.OGBase(instance).WithAccount(targetAccount),
- Stylesheets: []string{
- cssFA, cssStatus, cssThread, cssProfile,
- // Custom CSS for this user last in cascade.
- "/@" + targetAccount.Username + "/custom.css",
- },
- Javascript: []string{jsFrontend},
+ Template: "profile.tmpl",
+ Instance: instance,
+ OGMeta: apiutil.OGBase(instance).WithAccount(targetAccount),
+ Stylesheets: stylesheets,
+ Javascript: []string{jsFrontend},
Extra: map[string]any{
"account": targetAccount,
"rssFeed": rssFeed,
diff --git a/internal/web/thread.go b/internal/web/thread.go
index 4dcd1d221..ffec565e6 100644
--- a/internal/web/thread.go
+++ b/internal/web/thread.go
@@ -138,16 +138,39 @@ func (m *Module) threadGETHandler(c *gin.Context) {
return
}
+ // Prepare stylesheets for thread.
+ stylesheets := make([]string, 0, 5)
+
+ // Basic thread stylesheets.
+ stylesheets = append(
+ stylesheets,
+ []string{
+ cssFA,
+ cssStatus,
+ cssThread,
+ }...,
+ )
+
+ // User-selected theme if set.
+ if theme := targetAccount.Theme; theme != "" {
+ stylesheets = append(
+ stylesheets,
+ themesPathPrefix+"/"+theme,
+ )
+ }
+
+ // Custom CSS for this user last in cascade.
+ stylesheets = append(
+ stylesheets,
+ "/@"+targetAccount.Username+"/custom.css",
+ )
+
page := apiutil.WebPage{
- Template: "thread.tmpl",
- Instance: instance,
- OGMeta: apiutil.OGBase(instance).WithStatus(status),
- Stylesheets: []string{
- cssFA, cssStatus, cssThread,
- // Custom CSS for this user last in cascade.
- "/@" + targetUsername + "/custom.css",
- },
- Javascript: []string{jsFrontend},
+ Template: "thread.tmpl",
+ Instance: instance,
+ OGMeta: apiutil.OGBase(instance).WithStatus(status),
+ Stylesheets: stylesheets,
+ Javascript: []string{jsFrontend},
Extra: map[string]any{
"status": status,
"context": context,
diff --git a/internal/web/web.go b/internal/web/web.go
index 5e57d08c6..19df63332 100644
--- a/internal/web/web.go
+++ b/internal/web/web.go
@@ -44,6 +44,7 @@ const (
rssFeedPath = profileGroupPath + "/feed.rss"
assetsPathPrefix = "/assets"
distPathPrefix = assetsPathPrefix + "/dist"
+ themesPathPrefix = assetsPathPrefix + "/themes"
settingsPathPrefix = "/settings"
settingsPanelGlob = settingsPathPrefix + "/*panel"
userPanelPath = settingsPathPrefix + "/user"