From d70f4e166dd9ce2f11a6ac2d7a2e500515657041 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:47:03 +0200 Subject: [PATCH] [feature/frontend] Allow setting alt-text for avatar + header (#3086) --- docs/api/swagger.yaml | 30 +++++ internal/api/client/accounts/accountupdate.go | 14 +++ .../api/client/admin/accountsgetv2_test.go | 3 + .../api/client/statuses/statushistory_test.go | 2 + .../api/client/statuses/statusmute_test.go | 4 + internal/api/model/account.go | 21 ++++ internal/processing/account/get.go | 111 ++++++++++-------- internal/processing/account/update.go | 36 +++++- internal/typeutils/internaltofrontend.go | 90 ++++++++++---- internal/typeutils/internaltofrontend_test.go | 10 ++ internal/web/profile.go | 11 +- internal/web/thread.go | 11 +- testrig/testmodels.go | 2 +- web/source/css/profile.css | 29 ++++- web/source/settings/components/profile.tsx | 8 +- web/source/settings/style.css | 23 ++-- web/source/settings/views/user/profile.tsx | 44 ++++--- web/template/profile.tmpl | 86 ++++++++++++-- 18 files changed, 395 insertions(+), 140 deletions(-) diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index 367dae72f..4ce234374 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -193,6 +193,11 @@ definitions: example: https://example.org/media/some_user/avatar/original/avatar.jpeg type: string x-go-name: Avatar + avatar_description: + description: Description of this account's avatar, for alt text. + example: A cute drawing of a smiling sloth. + type: string + x-go-name: AvatarDescription avatar_static: description: |- Web location of a static version of the account's avatar. @@ -259,6 +264,11 @@ definitions: example: https://example.org/media/some_user/header/original/header.jpeg type: string x-go-name: Header + header_description: + description: Description of this account's header, for alt text. + example: A sunlit field with purple flowers. + type: string + x-go-name: HeaderDescription header_static: description: |- Web location of a static version of the account's header. @@ -1948,6 +1958,11 @@ definitions: example: https://example.org/media/some_user/avatar/original/avatar.jpeg type: string x-go-name: Avatar + avatar_description: + description: Description of this account's avatar, for alt text. + example: A cute drawing of a smiling sloth. + type: string + x-go-name: AvatarDescription avatar_static: description: |- Web location of a static version of the account's avatar. @@ -2014,6 +2029,11 @@ definitions: example: https://example.org/media/some_user/header/original/header.jpeg type: string x-go-name: Header + header_description: + description: Description of this account's header, for alt text. + example: A sunlit field with purple flowers. + type: string + x-go-name: HeaderDescription header_static: description: |- Web location of a static version of the account's header. @@ -4072,10 +4092,20 @@ paths: in: formData name: avatar type: file + - allowEmptyValue: true + description: Description of avatar image, for alt-text. + in: formData + name: avatar_description + type: string - description: Header of the user. in: formData name: header type: file + - allowEmptyValue: true + description: Description of header image, for alt-text. + in: formData + name: header_description + type: string - description: Require manual approval of follow requests. in: formData name: locked diff --git a/internal/api/client/accounts/accountupdate.go b/internal/api/client/accounts/accountupdate.go index cd8ee35f4..f81f54db0 100644 --- a/internal/api/client/accounts/accountupdate.go +++ b/internal/api/client/accounts/accountupdate.go @@ -78,11 +78,23 @@ import ( // description: Avatar of the user. // type: file // - +// name: avatar_description +// in: formData +// description: Description of avatar image, for alt-text. +// type: string +// allowEmptyValue: true +// - // name: header // in: formData // description: Header of the user. // type: file // - +// name: header_description +// in: formData +// description: Description of header image, for alt-text. +// type: string +// allowEmptyValue: true +// - // name: locked // in: formData // description: Require manual approval of follow requests. @@ -315,7 +327,9 @@ func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateCredentialsRequest, form.DisplayName == nil && form.Note == nil && form.Avatar == nil && + form.AvatarDescription == nil && form.Header == nil && + form.HeaderDescription == nil && form.Locked == nil && form.Source.Privacy == nil && form.Source.Sensitive == nil && diff --git a/internal/api/client/admin/accountsgetv2_test.go b/internal/api/client/admin/accountsgetv2_test.go index fdd6c6c30..85d58cce8 100644 --- a/internal/api/client/admin/accountsgetv2_test.go +++ b/internal/api/client/admin/accountsgetv2_test.go @@ -234,8 +234,10 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() { "url": "http://localhost:8080/@the_mighty_zork", "avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", "avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg", + "avatar_description": "a green goblin looking nasty", "header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", + "header_description": "A very old-school screenshot of the original team fortress mod for quake", "followers_count": 2, "following_count": 2, "statuses_count": 7, @@ -409,6 +411,7 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() { "avatar_static": "", "header": "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/original/01PFPMWK2FF0D9WMHEJHR07C3R.jpg", "header_static": "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/small/01PFPMWK2FF0D9WMHEJHR07C3R.jpg", + "header_description": "tweet from thoughts of dog: i drank. all the water. in my bowl. earlier. but just now. i returned. to the same bowl. and it was. full again.. the bowl. is haunted", "followers_count": 0, "following_count": 0, "statuses_count": 0, diff --git a/internal/api/client/statuses/statushistory_test.go b/internal/api/client/statuses/statushistory_test.go index a0cb3d482..a88abdb8f 100644 --- a/internal/api/client/statuses/statushistory_test.go +++ b/internal/api/client/statuses/statushistory_test.go @@ -108,8 +108,10 @@ func (suite *StatusHistoryTestSuite) TestGetHistory() { "url": "http://localhost:8080/@the_mighty_zork", "avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", "avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg", + "avatar_description": "a green goblin looking nasty", "header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", + "header_description": "A very old-school screenshot of the original team fortress mod for quake", "followers_count": 2, "following_count": 2, "statuses_count": 7, diff --git a/internal/api/client/statuses/statusmute_test.go b/internal/api/client/statuses/statusmute_test.go index a83720a20..83effd0c2 100644 --- a/internal/api/client/statuses/statusmute_test.go +++ b/internal/api/client/statuses/statusmute_test.go @@ -126,8 +126,10 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() { "url": "http://localhost:8080/@the_mighty_zork", "avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", "avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg", + "avatar_description": "a green goblin looking nasty", "header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", + "header_description": "A very old-school screenshot of the original team fortress mod for quake", "followers_count": 2, "following_count": 2, "statuses_count": 7, @@ -189,8 +191,10 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() { "url": "http://localhost:8080/@the_mighty_zork", "avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", "avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg", + "avatar_description": "a green goblin looking nasty", "header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", + "header_description": "A very old-school screenshot of the original team fortress mod for quake", "followers_count": 2, "following_count": 2, "statuses_count": 7, diff --git a/internal/api/model/account.go b/internal/api/model/account.go index b3a92d36f..cf39dd08e 100644 --- a/internal/api/model/account.go +++ b/internal/api/model/account.go @@ -62,6 +62,9 @@ type Account struct { // Only relevant when the account's main avatar is a video or a gif. // example: https://example.org/media/some_user/avatar/static/avatar.png AvatarStatic string `json:"avatar_static"` + // Description of this account's avatar, for alt text. + // example: A cute drawing of a smiling sloth. + AvatarDescription string `json:"avatar_description,omitempty"` // Web location of the account's header image. // example: https://example.org/media/some_user/header/original/header.jpeg Header string `json:"header"` @@ -69,6 +72,9 @@ type Account struct { // Only relevant when the account's main header is a video or a gif. // example: https://example.org/media/some_user/header/static/header.png HeaderStatic string `json:"header_static"` + // Description of this account's header, for alt text. + // example: A sunlit field with purple flowers. + HeaderDescription string `json:"header_description,omitempty"` // Number of accounts following this account, according to our instance. FollowersCount int `json:"followers_count"` // Number of account's followed by this account, according to our instance. @@ -104,6 +110,17 @@ type Account struct { // If set, indicates that this account is currently inactive, and has migrated to the given account. // Key/value omitted for accounts that haven't moved, and for suspended accounts. Moved *Account `json:"moved,omitempty"` + + // Additional fields not exposed via JSON + // (used only internally for templating etc). + + // Proper attachment model for the avatar. + // + // Only set if this model was converted via + // AccountToWebAccount, AND this account had + // an avatar set (and not just the default + // "blank" avatar image.) + AvatarAttachment *Attachment `json:"-"` } // MutedAccount extends Account with a field used only by the muted user list. @@ -168,8 +185,12 @@ type UpdateCredentialsRequest struct { Note *string `form:"note" json:"note"` // Avatar image encoded using multipart/form-data. Avatar *multipart.FileHeader `form:"avatar" json:"-"` + // Description of the avatar image, for alt-text. + AvatarDescription *string `form:"avatar_description" json:"avatar_description"` // Header image encoded using multipart/form-data Header *multipart.FileHeader `form:"header" json:"-"` + // Description of the header image, for alt-text. + HeaderDescription *string `form:"header_description" json:"header_description"` // Require manual approval of follow requests. Locked *bool `form:"locked" json:"locked"` // New Source values for this account. diff --git a/internal/processing/account/get.go b/internal/processing/account/get.go index 500c0c2e5..32d45054d 100644 --- a/internal/processing/account/get.go +++ b/internal/processing/account/get.go @@ -20,7 +20,6 @@ package account import ( "context" "errors" - "fmt" "net/url" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" @@ -36,66 +35,42 @@ func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account targetAccount, err := p.state.DB.GetAccountByID(ctx, targetAccountID) if err != nil { if errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorNotFound(errors.New("account not found")) + err := gtserror.New("account not found") + return nil, gtserror.NewErrorNotFound(err) } - return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error: %w", err)) + err := gtserror.Newf("db error getting account: %w", err) + return nil, gtserror.NewErrorInternalError(err) } - return p.getFor(ctx, requestingAccount, targetAccount) -} - -// GetLocalByUsername processes the given request for account information targeting a local account by username. -func (p *Processor) GetLocalByUsername(ctx context.Context, requestingAccount *gtsmodel.Account, username string) (*apimodel.Account, gtserror.WithCode) { - targetAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, username, "") + blocked, err := p.state.DB.IsEitherBlocked(ctx, requestingAccount.ID, targetAccount.ID) if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorNotFound(errors.New("account not found")) - } - return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error: %w", err)) + err := gtserror.Newf("db error checking blocks: %w", err) + return nil, gtserror.NewErrorInternalError(err) } - return p.getFor(ctx, requestingAccount, targetAccount) -} - -// GetCustomCSSForUsername returns custom css for the given local username. -func (p *Processor) GetCustomCSSForUsername(ctx context.Context, username string) (string, gtserror.WithCode) { - customCSS, err := p.state.DB.GetAccountCustomCSSByUsername(ctx, username) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return "", gtserror.NewErrorNotFound(errors.New("account not found")) - } - return "", gtserror.NewErrorInternalError(fmt.Errorf("db error: %w", err)) - } - - return customCSS, nil -} - -func (p *Processor) getFor(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (*apimodel.Account, gtserror.WithCode) { - var err error - - if requestingAccount != nil { - blocked, err := p.state.DB.IsEitherBlocked(ctx, requestingAccount.ID, targetAccount.ID) + if blocked { + apiAccount, err := p.converter.AccountToAPIAccountBlocked(ctx, targetAccount) if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking account block: %w", err)) - } - - if blocked { - apiAccount, err := p.converter.AccountToAPIAccountBlocked(ctx, targetAccount) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting account: %w", err)) - } - return apiAccount, nil + err := gtserror.Newf("error converting account: %w", err) + return nil, gtserror.NewErrorInternalError(err) } + return apiAccount, nil } if targetAccount.Domain != "" { targetAccountURI, err := url.Parse(targetAccount.URI) if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %w", targetAccount.URI, err)) + err := gtserror.Newf("error parsing account URI: %w", err) + return nil, gtserror.NewErrorInternalError(err) } - // Perform a last-minute fetch of target account to ensure remote account header / avatar is cached. - latest, _, err := p.federator.GetAccountByURI(gtscontext.SetFastFail(ctx), requestingAccount.Username, targetAccountURI) + // Perform a last-minute fetch of target account to + // ensure remote account header / avatar is cached. + latest, _, err := p.federator.GetAccountByURI( + gtscontext.SetFastFail(ctx), + requestingAccount.Username, + targetAccountURI, + ) if err != nil { log.Errorf(ctx, "error fetching latest target account: %v", err) } else { @@ -105,15 +80,53 @@ func (p *Processor) getFor(ctx context.Context, requestingAccount *gtsmodel.Acco } var apiAccount *apimodel.Account - - if requestingAccount != nil && targetAccount.ID == requestingAccount.ID { + if targetAccount.ID == requestingAccount.ID { + // This is requester's own account, + // show additional details. apiAccount, err = p.converter.AccountToAPIAccountSensitive(ctx, targetAccount) } else { + // This is a different account, + // show the "public" view. apiAccount, err = p.converter.AccountToAPIAccountPublic(ctx, targetAccount) } if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting account: %w", err)) + err := gtserror.Newf("error converting account: %w", err) + return nil, gtserror.NewErrorInternalError(err) } return apiAccount, nil } + +// GetWeb returns the web model of a local account by username. +func (p *Processor) GetWeb(ctx context.Context, username string) (*apimodel.Account, gtserror.WithCode) { + targetAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, username, "") + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + err := gtserror.New("account not found") + return nil, gtserror.NewErrorNotFound(err) + } + err := gtserror.Newf("db error getting account: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + webAccount, err := p.converter.AccountToWebAccount(ctx, targetAccount) + if err != nil { + err := gtserror.Newf("error converting account: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + return webAccount, nil +} + +// GetCustomCSSForUsername returns custom css for the given local username. +func (p *Processor) GetCustomCSSForUsername(ctx context.Context, username string) (string, gtserror.WithCode) { + customCSS, err := p.state.DB.GetAccountCustomCSSByUsername(ctx, username) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + return "", gtserror.NewErrorNotFound(gtserror.New("account not found")) + } + return "", gtserror.NewErrorInternalError(gtserror.Newf("db error: %w", err)) + } + + return customCSS, nil +} diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go index 61e88501f..ba9360c36 100644 --- a/internal/processing/account/update.go +++ b/internal/processing/account/update.go @@ -204,11 +204,16 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form } } + if form.AvatarDescription != nil { + desc := text.SanitizeToPlaintext(*form.AvatarDescription) + form.AvatarDescription = util.Ptr(desc) + } + if form.Avatar != nil && form.Avatar.Size != 0 { avatarInfo, errWithCode := p.UpdateAvatar(ctx, account, form.Avatar, - nil, + form.AvatarDescription, ) if errWithCode != nil { return nil, errWithCode @@ -216,13 +221,29 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form account.AvatarMediaAttachmentID = avatarInfo.ID account.AvatarMediaAttachment = avatarInfo log.Tracef(ctx, "new avatar info for account %s is %+v", account.ID, avatarInfo) + } else if form.AvatarDescription != nil && account.AvatarMediaAttachment != nil { + // Update just existing description if possible. + account.AvatarMediaAttachment.Description = *form.AvatarDescription + if err := p.state.DB.UpdateAttachment( + ctx, + account.AvatarMediaAttachment, + "description", + ); err != nil { + err := gtserror.Newf("db error updating account avatar description: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + } + + if form.HeaderDescription != nil { + desc := text.SanitizeToPlaintext(*form.HeaderDescription) + form.HeaderDescription = util.Ptr(desc) } if form.Header != nil && form.Header.Size != 0 { headerInfo, errWithCode := p.UpdateHeader(ctx, account, form.Header, - nil, + form.HeaderDescription, ) if errWithCode != nil { return nil, errWithCode @@ -230,6 +251,17 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form account.HeaderMediaAttachmentID = headerInfo.ID account.HeaderMediaAttachment = headerInfo log.Tracef(ctx, "new header info for account %s is %+v", account.ID, headerInfo) + } else if form.HeaderDescription != nil && account.HeaderMediaAttachment != nil { + // Update just existing description if possible. + account.HeaderMediaAttachment.Description = *form.HeaderDescription + if err := p.state.DB.UpdateAttachment( + ctx, + account.HeaderMediaAttachment, + "description", + ); err != nil { + err := gtserror.Newf("db error updating account avatar description: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } } if form.Locked != nil { diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index a8f9b7f8f..733a21b75 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -162,6 +162,38 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A return account, nil } +// AccountToWebAccount converts a gts model account into an +// api representation suitable for serving into a web template. +// +// Should only be used when preparing to template an account, +// callers looking to serialize an account into a model for +// serving over the client API should always use one of the +// AccountToAPIAccount functions instead. +func (c *Converter) AccountToWebAccount( + ctx context.Context, + a *gtsmodel.Account, +) (*apimodel.Account, error) { + webAccount, err := c.AccountToAPIAccountPublic(ctx, a) + if err != nil { + return nil, err + } + + // Set additional avatar information for + // serving the avatar in a nice photobox. + if a.AvatarMediaAttachment != nil { + avatarAttachment, err := c.AttachmentToAPIAttachment(ctx, a.AvatarMediaAttachment) + if err != nil { + // This is just extra data so just + // log but don't return any error. + log.Errorf(ctx, "error converting account avatar attachment: %v", err) + } else { + webAccount.AvatarAttachment = &avatarAttachment + } + } + + return webAccount, nil +} + // accountToAPIAccountPublic provides all the logic for AccountToAPIAccount, MINUS fetching moved account, to prevent possible recursion. func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) { @@ -210,18 +242,22 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A var ( aviURL string aviURLStatic string + aviDesc string headerURL string headerURLStatic string + headerDesc string ) if a.AvatarMediaAttachment != nil { aviURL = a.AvatarMediaAttachment.URL aviURLStatic = a.AvatarMediaAttachment.Thumbnail.URL + aviDesc = a.AvatarMediaAttachment.Description } if a.HeaderMediaAttachment != nil { headerURL = a.HeaderMediaAttachment.URL headerURLStatic = a.HeaderMediaAttachment.Thumbnail.URL + headerDesc = a.HeaderMediaAttachment.Description } // convert account gts model fields to front api model fields @@ -294,32 +330,34 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A // can be populated directly below. accountFrontend := &apimodel.Account{ - ID: a.ID, - Username: a.Username, - Acct: acct, - DisplayName: a.DisplayName, - Locked: locked, - Discoverable: discoverable, - Bot: bot, - CreatedAt: util.FormatISO8601(a.CreatedAt), - Note: a.Note, - URL: a.URL, - Avatar: aviURL, - AvatarStatic: aviURLStatic, - Header: headerURL, - HeaderStatic: headerURLStatic, - FollowersCount: followersCount, - FollowingCount: followingCount, - StatusesCount: statusesCount, - LastStatusAt: lastStatusAt, - Emojis: apiEmojis, - Fields: fields, - Suspended: !a.SuspendedAt.IsZero(), - Theme: theme, - CustomCSS: customCSS, - EnableRSS: enableRSS, - HideCollections: hideCollections, - Role: role, + ID: a.ID, + Username: a.Username, + Acct: acct, + DisplayName: a.DisplayName, + Locked: locked, + Discoverable: discoverable, + Bot: bot, + CreatedAt: util.FormatISO8601(a.CreatedAt), + Note: a.Note, + URL: a.URL, + Avatar: aviURL, + AvatarStatic: aviURLStatic, + AvatarDescription: aviDesc, + Header: headerURL, + HeaderStatic: headerURLStatic, + HeaderDescription: headerDesc, + FollowersCount: followersCount, + FollowingCount: followingCount, + StatusesCount: statusesCount, + LastStatusAt: lastStatusAt, + Emojis: apiEmojis, + Fields: fields, + Suspended: !a.SuspendedAt.IsZero(), + Theme: theme, + CustomCSS: customCSS, + EnableRSS: enableRSS, + HideCollections: hideCollections, + Role: role, } // Bodge default avatar + header in, diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go index 16dc27c87..522bf6401 100644 --- a/internal/typeutils/internaltofrontend_test.go +++ b/internal/typeutils/internaltofrontend_test.go @@ -57,8 +57,10 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontend() { "url": "http://localhost:8080/@the_mighty_zork", "avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", "avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg", + "avatar_description": "a green goblin looking nasty", "header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", + "header_description": "A very old-school screenshot of the original team fortress mod for quake", "followers_count": 2, "following_count": 2, "statuses_count": 7, @@ -108,8 +110,10 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendAliasedAndMoved() "url": "http://localhost:8080/@the_mighty_zork", "avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", "avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg", + "avatar_description": "a green goblin looking nasty", "header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", + "header_description": "A very old-school screenshot of the original team fortress mod for quake", "followers_count": 2, "following_count": 2, "statuses_count": 7, @@ -199,8 +203,10 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiStruct() "url": "http://localhost:8080/@the_mighty_zork", "avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", "avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg", + "avatar_description": "a green goblin looking nasty", "header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", + "header_description": "A very old-school screenshot of the original team fortress mod for quake", "followers_count": 2, "following_count": 2, "statuses_count": 7, @@ -247,8 +253,10 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiIDs() { "url": "http://localhost:8080/@the_mighty_zork", "avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", "avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg", + "avatar_description": "a green goblin looking nasty", "header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", + "header_description": "A very old-school screenshot of the original team fortress mod for quake", "followers_count": 2, "following_count": 2, "statuses_count": 7, @@ -291,8 +299,10 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() { "url": "http://localhost:8080/@the_mighty_zork", "avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", "avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg", + "avatar_description": "a green goblin looking nasty", "header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", + "header_description": "A very old-school screenshot of the original team fortress mod for quake", "followers_count": 2, "following_count": 2, "statuses_count": 7, diff --git a/internal/web/profile.go b/internal/web/profile.go index 1dbf5c73d..ca613900f 100644 --- a/internal/web/profile.go +++ b/internal/web/profile.go @@ -28,7 +28,6 @@ import ( apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/oauth" ) func (m *Module) profileGETHandler(c *gin.Context) { @@ -79,16 +78,8 @@ func (m *Module) profileGETHandler(c *gin.Context) { // text/html has been requested. Proceed with getting the web view of the account. - // Don't require auth for web endpoints, but do take it if it was provided. - // authed.Account might end up nil here, but that's fine in case of public pages. - authed, err := oauth.Authed(c, false, false, false, false) - if err != nil { - apiutil.WebErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1) - return - } - // Fetch the target account so we can do some checks on it. - targetAccount, errWithCode := m.processor.Account().GetLocalByUsername(ctx, authed.Account, targetUsername) + targetAccount, errWithCode := m.processor.Account().GetWeb(ctx, targetUsername) if errWithCode != nil { apiutil.WebErrorHandler(c, errWithCode, instanceGet) return diff --git a/internal/web/thread.go b/internal/web/thread.go index 05bd63ebe..492d40103 100644 --- a/internal/web/thread.go +++ b/internal/web/thread.go @@ -29,7 +29,6 @@ import ( apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/oauth" ) func (m *Module) threadGETHandler(c *gin.Context) { @@ -88,16 +87,8 @@ func (m *Module) threadGETHandler(c *gin.Context) { // text/html has been requested. Proceed with getting the web view of the status. - // Don't require auth for web endpoints, but do take it if it was provided. - // authed.Account might end up nil here, but that's fine in case of public pages. - authed, err := oauth.Authed(c, false, false, false, false) - if err != nil { - apiutil.WebErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1) - return - } - // Fetch the target account so we can do some checks on it. - targetAccount, errWithCode := m.processor.Account().GetLocalByUsername(ctx, authed.Account, targetUsername) + targetAccount, errWithCode := m.processor.Account().GetWeb(ctx, targetUsername) if errWithCode != nil { apiutil.WebErrorHandler(c, errWithCode, instanceGet) return diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 3db8ef62f..de6e97142 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -969,7 +969,7 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { }, }, AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", - Description: "A very old-school screenshot of the original team fortress mod for quake ", + Description: "A very old-school screenshot of the original team fortress mod for quake", ScheduledStatusID: "", Blurhash: "L26j{^WCs+R-N}jsxWj@4;WWxDoK", Processing: 2, diff --git a/web/source/css/profile.css b/web/source/css/profile.css index a966d768a..3f7f43d0d 100644 --- a/web/source/css/profile.css +++ b/web/source/css/profile.css @@ -82,18 +82,37 @@ margin-top: calc(-1 * $overlap); gap: 0 1rem; - .avatar { + .avatar-image-wrapper { grid-area: avatar; - height: $avatar-size; - width: $avatar-size; + border: 0.2rem solid $avatar-border; border-radius: $br; - overflow: hidden; /* prevents image extending beyond rounded borders */ + + /* + Wrapper always same + size + proportions no + matter image inside. + */ + height: $avatar-size; + width: $avatar-size; - img { + .avatar { + /* + Fit 100% of the wrapper. + */ height: 100%; width: 100%; + + /* + Normalize non-square images. + */ object-fit: cover; + + /* + Prevent image extending + beyond rounded borders. + */ + border-radius: $br-inner; } } diff --git a/web/source/settings/components/profile.tsx b/web/source/settings/components/profile.tsx index 4a5157378..24cb3c4c2 100644 --- a/web/source/settings/components/profile.tsx +++ b/web/source/settings/components/profile.tsx @@ -27,9 +27,11 @@ export default function FakeProfile({ avatar, header, display_name, username, ro {header