[feature/frontend] Respect prefers-reduced-motion for avatars, headers, and emojis (#3118)

* [feature/frontend] Respect `prefers-reduced-motion` for avatars, headers, and emojis

* go fmt

* fix tests

* use static version of instance thumbnail when appropriate

* use prefers-reduced-motion

* simplify account conversion a bit

* fix c&p error
This commit is contained in:
tobi 2024-07-21 14:22:08 +02:00 committed by GitHub
commit 027a93facc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 435 additions and 140 deletions

View file

@ -170,22 +170,47 @@ func (c *Converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
func (c *Converter) AccountToWebAccount(
ctx context.Context,
a *gtsmodel.Account,
) (*apimodel.Account, error) {
webAccount, err := c.AccountToAPIAccountPublic(ctx, a)
) (*apimodel.WebAccount, error) {
apiAccount, err := c.AccountToAPIAccountPublic(ctx, a)
if err != nil {
return nil, err
}
webAccount := &apimodel.WebAccount{
Account: apiAccount,
}
// Set additional avatar information for
// serving the avatar in a nice photobox.
if a.AvatarMediaAttachment != nil {
avatarAttachment, err := c.AttachmentToAPIAttachment(ctx, a.AvatarMediaAttachment)
// serving the avatar in a nice <picture>.
if ogAvi := a.AvatarMediaAttachment; ogAvi != nil {
avatarAttachment, err := c.AttachmentToAPIAttachment(ctx, ogAvi)
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
webAccount.AvatarAttachment = &apimodel.WebAttachment{
Attachment: &avatarAttachment,
MIMEType: ogAvi.File.ContentType,
PreviewMIMEType: ogAvi.Thumbnail.ContentType,
}
}
}
// Set additional header information for
// serving the header in a nice <picture>.
if ogHeader := a.HeaderMediaAttachment; ogHeader != nil {
headerAttachment, err := c.AttachmentToAPIAttachment(ctx, ogHeader)
if err != nil {
// This is just extra data so just
// log but don't return any error.
log.Errorf(ctx, "error converting account header attachment: %v", err)
} else {
webAccount.HeaderAttachment = &apimodel.WebAttachment{
Attachment: &headerAttachment,
MIMEType: ogHeader.File.ContentType,
PreviewMIMEType: ogHeader.Thumbnail.ContentType,
}
}
}
@ -747,11 +772,35 @@ func (c *Converter) StatusToAPIStatus(
filters []*gtsmodel.Filter,
mutes *usermute.CompiledUserMuteList,
) (*apimodel.Status, error) {
apiStatus, err := c.statusToFrontend(ctx, s, requestingAccount, filterContext, filters, mutes)
apiStatus, err := c.statusToFrontend(
ctx,
s,
requestingAccount, // Can be nil.
filterContext, // Can be empty.
filters,
mutes,
)
if err != nil {
return nil, err
}
// Convert author to API model.
acct, err := c.AccountToAPIAccountPublic(ctx, s.Account)
if err != nil {
return nil, gtserror.Newf("error converting status acct: %w", err)
}
apiStatus.Account = acct
// Convert author of boosted
// status (if set) to API model.
if apiStatus.Reblog != nil {
boostAcct, err := c.AccountToAPIAccountPublic(ctx, s.BoostOfAccount)
if err != nil {
return nil, gtserror.Newf("error converting boost acct: %w", err)
}
apiStatus.Reblog.Account = boostAcct
}
// Normalize status for API by pruning
// attachments that were not locally
// stored, replacing them with a helpful
@ -958,20 +1007,25 @@ func (c *Converter) StatusToWebStatus(
ctx context.Context,
s *gtsmodel.Status,
) (*apimodel.WebStatus, error) {
apiStatus, err := c.statusToFrontend(
ctx,
s,
nil, // No authed requester.
statusfilter.FilterContextNone,
nil, // No filters.
nil, // No mutes.
apiStatus, err := c.statusToFrontend(ctx, s,
nil, // No authed requester.
statusfilter.FilterContextNone, // No filters.
nil, // No filters.
nil, // No mutes.
)
if err != nil {
return nil, err
}
// Convert status author to web model.
acct, err := c.AccountToWebAccount(ctx, s.Account)
if err != nil {
return nil, err
}
webStatus := &apimodel.WebStatus{
Status: apiStatus,
Status: apiStatus,
Account: acct,
}
// Whack a newline before and after each "pre" to make it easier to outdent it.
@ -1062,9 +1116,10 @@ func (c *Converter) StatusToWebStatus(
for i, apiAttachment := range apiStatus.MediaAttachments {
ogAttachment := ogAttachments[apiAttachment.ID]
webStatus.MediaAttachments[i] = &apimodel.WebAttachment{
Attachment: apiAttachment,
Sensitive: apiStatus.Sensitive,
MIMEType: ogAttachment.File.ContentType,
Attachment: apiAttachment,
Sensitive: apiStatus.Sensitive,
MIMEType: ogAttachment.File.ContentType,
PreviewMIMEType: ogAttachment.Thumbnail.ContentType,
}
}
@ -1090,6 +1145,9 @@ func (c *Converter) StatusToAPIStatusSource(ctx context.Context, s *gtsmodel.Sta
// parsing a status into its initial frontend representation.
//
// Requesting account can be nil.
//
// This function also doesn't handle converting the
// account to api/web model -- the caller must do that.
func (c *Converter) statusToFrontend(
ctx context.Context,
status *gtsmodel.Status,
@ -1142,6 +1200,9 @@ func (c *Converter) statusToFrontend(
// baseStatusToFrontend performs the main logic
// of statusToFrontend() without handling of boost
// logic, to prevent *possible* recursion issues.
//
// This function also doesn't handle converting the
// account to api/web model -- the caller must do that.
func (c *Converter) baseStatusToFrontend(
ctx context.Context,
s *gtsmodel.Status,
@ -1169,11 +1230,6 @@ func (c *Converter) baseStatusToFrontend(
}
}
apiAuthorAccount, err := c.AccountToAPIAccountPublic(ctx, s.Account)
if err != nil {
return nil, gtserror.Newf("error converting status author: %w", err)
}
repliesCount, err := c.state.DB.CountStatusReplies(ctx, s.ID)
if err != nil {
return nil, gtserror.Newf("error counting replies: %w", err)
@ -1240,7 +1296,7 @@ func (c *Converter) baseStatusToFrontend(
Content: s.Content,
Reblog: nil, // Set below.
Application: nil, // Set below.
Account: apiAuthorAccount,
Account: nil, // Caller must do this.
MediaAttachments: apiAttachments,
Mentions: apiMentions,
Tags: apiTags,
@ -1464,6 +1520,8 @@ func (c *Converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Ins
instance.Thumbnail = iAccount.AvatarMediaAttachment.URL
instance.ThumbnailType = iAccount.AvatarMediaAttachment.File.ContentType
instance.ThumbnailStatic = iAccount.AvatarMediaAttachment.Thumbnail.URL
instance.ThumbnailStaticType = iAccount.AvatarMediaAttachment.Thumbnail.ContentType
instance.ThumbnailDescription = iAccount.AvatarMediaAttachment.Description
} else {
instance.Thumbnail = config.GetProtocol() + "://" + i.Domain + "/assets/logo.webp" // default thumb
@ -1533,6 +1591,8 @@ func (c *Converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Ins
thumbnail.URL = iAccount.AvatarMediaAttachment.URL
thumbnail.Type = iAccount.AvatarMediaAttachment.File.ContentType
thumbnail.StaticURL = iAccount.AvatarMediaAttachment.Thumbnail.URL
thumbnail.StaticType = iAccount.AvatarMediaAttachment.Thumbnail.ContentType
thumbnail.Description = iAccount.AvatarMediaAttachment.Description
thumbnail.Blurhash = iAccount.AvatarMediaAttachment.Blurhash
} else {

View file

@ -981,28 +981,6 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
"pinned": false,
"content": "\u003cp\u003ehi \u003cspan class=\"h-card\"\u003e\u003ca href=\"http://localhost:8080/@admin\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e@\u003cspan\u003eadmin\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e here's some media for ya\u003c/p\u003e",
"reblog": null,
"account": {
"id": "01FHMQX3GAABWSM0S2VZEC2SWC",
"username": "Some_User",
"acct": "Some_User@example.org",
"display_name": "some user",
"locked": true,
"discoverable": true,
"bot": false,
"created_at": "2020-08-10T12:13:28.000Z",
"note": "i'm a real son of a gun",
"url": "http://example.org/@Some_User",
"avatar": "",
"avatar_static": "",
"header": "http://localhost:8080/assets/default_header.webp",
"header_static": "http://localhost:8080/assets/default_header.webp",
"followers_count": 0,
"following_count": 0,
"statuses_count": 1,
"last_status_at": "2023-11-02T10:44:25.000Z",
"emojis": [],
"fields": []
},
"mentions": [
{
"id": "01F8MH17FWEB39HZJ76B6VXSKF",
@ -1035,6 +1013,28 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
"with_approval": []
}
},
"account": {
"id": "01FHMQX3GAABWSM0S2VZEC2SWC",
"username": "Some_User",
"acct": "Some_User@example.org",
"display_name": "some user",
"locked": true,
"discoverable": true,
"bot": false,
"created_at": "2020-08-10T12:13:28.000Z",
"note": "i'm a real son of a gun",
"url": "http://example.org/@Some_User",
"avatar": "",
"avatar_static": "",
"header": "http://localhost:8080/assets/default_header.webp",
"header_static": "http://localhost:8080/assets/default_header.webp",
"followers_count": 0,
"following_count": 0,
"statuses_count": 1,
"last_status_at": "2023-11-02T10:44:25.000Z",
"emojis": [],
"fields": []
},
"media_attachments": [
{
"id": "01HE7Y3C432WRSNS10EZM86SA5",
@ -1065,7 +1065,8 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
"description": "Photograph of a sloth, Public Domain.",
"blurhash": "LKE3VIw}0KD%a2o{M|t7NFWps:t7",
"Sensitive": true,
"MIMEType": "image/jpg"
"MIMEType": "image/jpg",
"PreviewMIMEType": "image/webp"
},
{
"id": "01HE7ZFX9GKA5ZZVD4FACABSS9",
@ -1079,7 +1080,8 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
"description": "SVG line art of a sloth, public domain",
"blurhash": "L26*j+~qE1RP?wxut7ofRlM{R*of",
"Sensitive": true,
"MIMEType": ""
"MIMEType": "",
"PreviewMIMEType": ""
},
{
"id": "01HE88YG74PVAB81PX2XA9F3FG",
@ -1093,7 +1095,8 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
"description": "Jolly salsa song, public domain.",
"blurhash": null,
"Sensitive": true,
"MIMEType": ""
"MIMEType": "",
"PreviewMIMEType": ""
}
],
"LanguageTag": "en",

View file

@ -81,7 +81,7 @@ func (suite *InternalToRSSTestSuite) TestStatusToRSSItem2() {
suite.Equal("62529", item.Enclosure.Length)
suite.Equal("image/jpeg", item.Enclosure.Type)
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:\" width=\"25\" height=\"25\"/> !", 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() {