diff options
author | 2024-03-25 18:32:24 +0100 | |
---|---|---|
committer | 2024-03-25 17:32:24 +0000 | |
commit | 8953f57d887c060c3b58f83c38d2010d27a45ef3 (patch) | |
tree | 05f8b1157a86afaa3ed0d6d0b87c9d0d37030362 /internal | |
parent | [feature] Add healthcheck endpoints `/livez` and `/readyz` (#2783) (diff) | |
download | gotosocial-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.go | 4 | ||||
-rw-r--r-- | internal/api/client/accounts/accountupdate.go | 1 | ||||
-rw-r--r-- | internal/api/client/accounts/lists.go | 2 | ||||
-rw-r--r-- | internal/api/client/accounts/statuses.go | 2 | ||||
-rw-r--r-- | internal/api/client/accounts/themesget.go | 77 | ||||
-rw-r--r-- | internal/api/model/account.go | 6 | ||||
-rw-r--r-- | internal/api/model/theme.go | 32 | ||||
-rw-r--r-- | internal/db/bundb/migrations/20240320114447_preset_css_themes.go | 55 | ||||
-rw-r--r-- | internal/gtsmodel/account.go | 14 | ||||
-rw-r--r-- | internal/gtsmodel/accountsettings.go | 1 | ||||
-rw-r--r-- | internal/processing/account/account.go | 2 | ||||
-rw-r--r-- | internal/processing/account/themes.go | 151 | ||||
-rw-r--r-- | internal/processing/account/themes_test.go | 52 | ||||
-rw-r--r-- | internal/processing/account/update.go | 16 | ||||
-rw-r--r-- | internal/typeutils/internaltofrontend.go | 18 | ||||
-rw-r--r-- | internal/web/profile.go | 42 | ||||
-rw-r--r-- | internal/web/thread.go | 41 | ||||
-rw-r--r-- | internal/web/web.go | 1 |
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 := >smodel.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" |