From 027a93facc73b78e3c3747ab796f4a14b1b4936f Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Sun, 21 Jul 2024 14:22:08 +0200 Subject: [PATCH] [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 --- docs/api/swagger.yaml | 22 ++++ .../api/client/instance/instancepatch_test.go | 4 + internal/api/model/account.go | 24 ++-- internal/api/model/attachment.go | 4 + internal/api/model/instancev1.go | 6 + internal/api/model/instancev2.go | 7 ++ internal/api/model/status.go | 3 + internal/api/util/opengraph.go | 4 +- internal/api/util/opengraph_test.go | 12 +- internal/processing/account/get.go | 2 +- internal/processing/account/rss_test.go | 116 +++++++++++++++++- internal/text/emojify.go | 80 ++++++++---- internal/typeutils/internaltofrontend.go | 108 ++++++++++++---- internal/typeutils/internaltofrontend_test.go | 53 ++++---- internal/typeutils/internaltorss_test.go | 2 +- internal/web/thread.go | 2 +- web/source/css/base.css | 26 ++-- web/source/css/index.css | 3 +- web/source/css/page.css | 3 +- web/source/css/status.css | 8 -- web/template/page.tmpl | 2 +- web/template/page_header.tmpl | 19 ++- web/template/profile.tmpl | 47 +++++-- web/template/status_header.tmpl | 18 ++- 24 files changed, 435 insertions(+), 140 deletions(-) diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index 66f7e53a5..9fcc8bab3 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -1526,6 +1526,16 @@ definitions: example: picture of a cute lil' friendly sloth type: string x-go-name: ThumbnailDescription + thumbnail_static: + description: URL of the static instance avatar/banner image. + example: https://example.org/files/instance/static/thumbnail.webp + type: string + x-go-name: ThumbnailStatic + thumbnail_static_type: + description: MIME type of the static instance thumbnail. + example: image/webp + type: string + x-go-name: ThumbnailStaticType thumbnail_type: description: MIME type of the instance thumbnail. example: image/png @@ -1759,6 +1769,11 @@ definitions: example: UeKUpFxuo~R%0nW;WCnhF6RjaJt757oJodS$ type: string x-go-name: Blurhash + static_url: + description: StaticURL version of the thumbnail image. + example: https://example.org/fileserver/01BPSX2MKCRVMD4YN4D71G9CP5/attachment/static/01H88X0KQ2DFYYDSWYP93VDJZA.webp + type: string + x-go-name: StaticURL thumbnail_description: description: |- Description of the instance thumbnail. @@ -1766,6 +1781,13 @@ definitions: example: picture of a cute lil' friendly sloth type: string x-go-name: Description + thumbnail_static_type: + description: |- + MIME type of the instance thumbnail. + Key/value not set if thumbnail image type unknown. + example: image/png + type: string + x-go-name: StaticType thumbnail_type: description: |- MIME type of the instance thumbnail. diff --git a/internal/api/client/instance/instancepatch_test.go b/internal/api/client/instance/instancepatch_test.go index ba0d026f3..e68508fc2 100644 --- a/internal/api/client/instance/instancepatch_test.go +++ b/internal/api/client/instance/instancepatch_test.go @@ -762,6 +762,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() { }, "thumbnail": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/original/`+instanceAccount.AvatarMediaAttachment.ID+`.gif",`+` "thumbnail_type": "image/gif", + "thumbnail_static": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/small/`+instanceAccount.AvatarMediaAttachment.ID+`.webp",`+` + "thumbnail_static_type": "image/webp", "thumbnail_description": "A bouncing little green peglin.", "contact_account": { "id": "01F8MH17FWEB39HZJ76B6VXSKF", @@ -818,6 +820,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() { suite.Equal(`{ "url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/original/`+instanceAccount.AvatarMediaAttachment.ID+`.gif",`+` "thumbnail_type": "image/gif", + "static_url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/small/`+instanceAccount.AvatarMediaAttachment.ID+`.webp",`+` + "thumbnail_static_type": "image/webp", "thumbnail_description": "A bouncing little green peglin.", "blurhash": "LE9kG#M}4YtO%dRkWEt5Dmoxx?WC" }`, string(instanceV2ThumbnailJson)) diff --git a/internal/api/model/account.go b/internal/api/model/account.go index cf39dd08e..c5b629db0 100644 --- a/internal/api/model/account.go +++ b/internal/api/model/account.go @@ -110,17 +110,27 @@ 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). +// WebAccount is like Account, but with +// additional fields not exposed via JSON; +// used only internally for templating etc. +// +// swagger:ignore +type WebAccount struct { + *Account // 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:"-"` + // Only set if this account had an avatar set + // (and not just the default "blank" image.) + AvatarAttachment *WebAttachment `json:"-"` + + // Proper attachment model for the header. + // + // Only set if this account had a header set + // (and not just the default "blank" image.) + HeaderAttachment *WebAttachment `json:"-"` } // MutedAccount extends Account with a field used only by the muted user list. diff --git a/internal/api/model/attachment.go b/internal/api/model/attachment.go index d0b0c81e5..21523a58e 100644 --- a/internal/api/model/attachment.go +++ b/internal/api/model/attachment.go @@ -107,6 +107,10 @@ type WebAttachment struct { // MIME type of // the attachment. MIMEType string + + // MIME type of + // the thumbnail. + PreviewMIMEType string } // MediaMeta models media metadata. diff --git a/internal/api/model/instancev1.go b/internal/api/model/instancev1.go index beb4f430d..efa6d6faa 100644 --- a/internal/api/model/instancev1.go +++ b/internal/api/model/instancev1.go @@ -85,6 +85,12 @@ type InstanceV1 struct { // MIME type of the instance thumbnail. // example: image/png ThumbnailType string `json:"thumbnail_type,omitempty"` + // URL of the static instance avatar/banner image. + // example: https://example.org/files/instance/static/thumbnail.webp + ThumbnailStatic string `json:"thumbnail_static,omitempty"` + // MIME type of the static instance thumbnail. + // example: image/webp + ThumbnailStaticType string `json:"thumbnail_static_type,omitempty"` // Description of the instance thumbnail. // example: picture of a cute lil' friendly sloth ThumbnailDescription string `json:"thumbnail_description,omitempty"` diff --git a/internal/api/model/instancev2.go b/internal/api/model/instancev2.go index fce801117..8d6873497 100644 --- a/internal/api/model/instancev2.go +++ b/internal/api/model/instancev2.go @@ -102,6 +102,13 @@ type InstanceV2Thumbnail struct { // Key/value not set if thumbnail image type unknown. // example: image/png Type string `json:"thumbnail_type,omitempty"` + // StaticURL version of the thumbnail image. + // example: https://example.org/fileserver/01BPSX2MKCRVMD4YN4D71G9CP5/attachment/static/01H88X0KQ2DFYYDSWYP93VDJZA.webp + StaticURL string `json:"static_url,omitempty"` + // MIME type of the instance thumbnail. + // Key/value not set if thumbnail image type unknown. + // example: image/png + StaticType string `json:"thumbnail_static_type,omitempty"` // Description of the instance thumbnail. // Key/value not set if no description available. // example: picture of a cute lil' friendly sloth diff --git a/internal/api/model/status.go b/internal/api/model/status.go index 7358916ab..b3ac746d7 100644 --- a/internal/api/model/status.go +++ b/internal/api/model/status.go @@ -113,6 +113,9 @@ type Status struct { type WebStatus struct { *Status + // Override API account with web account. + Account *WebAccount `json:"account"` + // Web version of media // attached to this status. MediaAttachments []*WebAttachment `json:"media_attachments"` diff --git a/internal/api/util/opengraph.go b/internal/api/util/opengraph.go index 062151836..094c80021 100644 --- a/internal/api/util/opengraph.go +++ b/internal/api/util/opengraph.go @@ -84,7 +84,7 @@ func OGBase(instance *apimodel.InstanceV1) *OGMeta { // WithAccount uses the given account to build an ogMeta // struct specific to that account. It's suitable for serving // at account profile pages. -func (og *OGMeta) WithAccount(account *apimodel.Account) *OGMeta { +func (og *OGMeta) WithAccount(account *apimodel.WebAccount) *OGMeta { og.Title = AccountTitle(account, og.SiteName) og.Type = "profile" og.URL = account.URL @@ -148,7 +148,7 @@ func (og *OGMeta) WithStatus(status *apimodel.WebStatus) *OGMeta { } // AccountTitle parses a page title from account and accountDomain -func AccountTitle(account *apimodel.Account, accountDomain string) string { +func AccountTitle(account *apimodel.WebAccount, accountDomain string) string { user := "@" + account.Acct + "@" + accountDomain if len(account.DisplayName) == 0 { diff --git a/internal/api/util/opengraph_test.go b/internal/api/util/opengraph_test.go index 2ecd6a740..4e94d78ef 100644 --- a/internal/api/util/opengraph_test.go +++ b/internal/api/util/opengraph_test.go @@ -51,13 +51,15 @@ func (suite *OpenGraphTestSuite) TestWithAccountWithNote() { Languages: []string{"en"}, }) - accountMeta := baseMeta.WithAccount(&apimodel.Account{ + acct := &apimodel.Account{ Acct: "example_account", DisplayName: "example person!!", URL: "https://example.org/@example_account", Note: "

This is my profile, read it and weep! Weep then!

", Username: "example_account", - }) + } + + accountMeta := baseMeta.WithAccount(&apimodel.WebAccount{Account: acct}) suite.EqualValues(OGMeta{ Title: "example person!!, @example_account@example.org", @@ -84,13 +86,15 @@ func (suite *OpenGraphTestSuite) TestWithAccountNoNote() { Languages: []string{"en"}, }) - accountMeta := baseMeta.WithAccount(&apimodel.Account{ + acct := &apimodel.Account{ Acct: "example_account", DisplayName: "example person!!", URL: "https://example.org/@example_account", Note: "", // <- empty Username: "example_account", - }) + } + + accountMeta := baseMeta.WithAccount(&apimodel.WebAccount{Account: acct}) suite.EqualValues(OGMeta{ Title: "example person!!, @example_account@example.org", diff --git a/internal/processing/account/get.go b/internal/processing/account/get.go index 32d45054d..eac0f0c3f 100644 --- a/internal/processing/account/get.go +++ b/internal/processing/account/get.go @@ -98,7 +98,7 @@ func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account } // GetWeb returns the web model of a local account by username. -func (p *Processor) GetWeb(ctx context.Context, username string) (*apimodel.Account, gtserror.WithCode) { +func (p *Processor) GetWeb(ctx context.Context, username string) (*apimodel.WebAccount, gtserror.WithCode) { targetAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, username, "") if err != nil { if errors.Is(err, db.ErrNoEntries) { diff --git a/internal/processing/account/rss_test.go b/internal/processing/account/rss_test.go index 0b1ae561d..e4706d3b7 100644 --- a/internal/processing/account/rss_test.go +++ b/internal/processing/account/rss_test.go @@ -35,7 +35,36 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() { feed, err := getFeed() suite.NoError(err) - suite.Equal("\n \n Posts from @admin@localhost:8080\n http://localhost:8080/@admin\n Posts from @admin@localhost:8080\n Wed, 20 Oct 2021 10:41:37 +0000\n Wed, 20 Oct 2021 10:41:37 +0000\n \n open to see some puppies\n http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37\n @admin@localhost:8080 made a new post: "🐕🐕🐕🐕🐕"\n \n @admin@localhost:8080\n http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37\n Wed, 20 Oct 2021 12:36:45 +0000\n http://localhost:8080/@admin/feed.rss\n \n \n hello world! #welcome ! first post on the instance :rainbow: !\n http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R\n @admin@localhost:8080 posted 1 attachment: "hello world! #welcome ! first post on the instance :rainbow: !"\n !]]>\n @admin@localhost:8080\n \n http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R\n Wed, 20 Oct 2021 11:36:45 +0000\n http://localhost:8080/@admin/feed.rss\n \n \n", feed) + suite.Equal(` + + Posts from @admin@localhost:8080 + http://localhost:8080/@admin + Posts from @admin@localhost:8080 + Wed, 20 Oct 2021 10:41:37 +0000 + Wed, 20 Oct 2021 10:41:37 +0000 + + open to see some puppies + http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37 + @admin@localhost:8080 made a new post: "🐕🐕🐕🐕🐕" + + @admin@localhost:8080 + http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37 + Wed, 20 Oct 2021 12:36:45 +0000 + http://localhost:8080/@admin/feed.rss + + + hello world! #welcome ! first post on the instance :rainbow: ! + http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R + @admin@localhost:8080 posted 1 attachment: "hello world! #welcome ! first post on the instance :rainbow: !" + !]]> + @admin@localhost:8080 + + http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R + Wed, 20 Oct 2021 11:36:45 +0000 + http://localhost:8080/@admin/feed.rss + + +`, feed) } func (suite *GetRSSTestSuite) TestGetAccountRSSZork() { @@ -45,7 +74,75 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSZork() { feed, err := getFeed() suite.NoError(err) - suite.Equal("\n \n Posts from @the_mighty_zork@localhost:8080\n http://localhost:8080/@the_mighty_zork\n Posts from @the_mighty_zork@localhost:8080\n Wed, 10 Jan 2024 09:24:00 +0000\n Wed, 10 Jan 2024 09:24:00 +0000\n \n http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp\n Avatar for @the_mighty_zork@localhost:8080\n http://localhost:8080/@the_mighty_zork\n \n \n HTML in post\n http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40\n @the_mighty_zork@localhost:8080 made a new post: "Here's a bunch of HTML, read it and weep, weep then! ```html <section class="about-user"> <div class="col-header"> <h2>About</h2> </div> <div class="fields"> <h3 class="sr-only">Fields</h3> <dl> ...\n Here's a bunch of HTML, read it and weep, weep then!

<section class="about-user">\n    <div class="col-header">\n        <h2>About</h2>\n    </div>            \n    <div class="fields">\n        <h3 class="sr-only">Fields</h3>\n        <dl>\n            <div class="field">\n                <dt>should you follow me?</dt>\n                <dd>maybe!</dd>\n            </div>\n            <div class="field">\n                <dt>age</dt>\n                <dd>120</dd>\n            </div>\n        </dl>\n    </div>\n    <div class="bio">\n        <h3 class="sr-only">Bio</h3>\n        <p>i post about things that concern me</p>\n    </div>\n    <div class="sr-only" role="group">\n        <h3 class="sr-only">Stats</h3>\n        <span>Joined in Jun, 2022.</span>\n        <span>8 posts.</span>\n        <span>Followed by 1.</span>\n        <span>Following 1.</span>\n    </div>\n    <div class="accountstats" aria-hidden="true">\n        <b>Joined</b><time datetime="2022-06-04T13:12:00.000Z">Jun, 2022</time>\n        <b>Posts</b><span>8</span>\n        <b>Followed by</b><span>1</span>\n        <b>Following</b><span>1</span>\n    </div>\n</section>\n

There, hope you liked that!

]]>
\n @the_mighty_zork@localhost:8080\n http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40\n Sun, 10 Dec 2023 09:24:00 +0000\n http://localhost:8080/@the_mighty_zork/feed.rss\n
\n \n introduction post\n http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY\n @the_mighty_zork@localhost:8080 made a new post: "hello everyone!"\n \n @the_mighty_zork@localhost:8080\n http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY\n Wed, 20 Oct 2021 10:40:37 +0000\n http://localhost:8080/@the_mighty_zork/feed.rss\n \n
\n
", feed) + suite.Equal(` + + Posts from @the_mighty_zork@localhost:8080 + http://localhost:8080/@the_mighty_zork + Posts from @the_mighty_zork@localhost:8080 + Wed, 10 Jan 2024 09:24:00 +0000 + Wed, 10 Jan 2024 09:24:00 +0000 + + http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp + Avatar for @the_mighty_zork@localhost:8080 + http://localhost:8080/@the_mighty_zork + + + HTML in post + http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40 + @the_mighty_zork@localhost:8080 made a new post: "Here's a bunch of HTML, read it and weep, weep then! `+"```"+`html <section class="about-user"> <div class="col-header"> <h2>About</h2> </div> <div class="fields"> <h3 class="sr-only">Fields</h3> <dl> ... + Here's a bunch of HTML, read it and weep, weep then!

<section class="about-user">
+    <div class="col-header">
+        <h2>About</h2>
+    </div>            
+    <div class="fields">
+        <h3 class="sr-only">Fields</h3>
+        <dl>
+            <div class="field">
+                <dt>should you follow me?</dt>
+                <dd>maybe!</dd>
+            </div>
+            <div class="field">
+                <dt>age</dt>
+                <dd>120</dd>
+            </div>
+        </dl>
+    </div>
+    <div class="bio">
+        <h3 class="sr-only">Bio</h3>
+        <p>i post about things that concern me</p>
+    </div>
+    <div class="sr-only" role="group">
+        <h3 class="sr-only">Stats</h3>
+        <span>Joined in Jun, 2022.</span>
+        <span>8 posts.</span>
+        <span>Followed by 1.</span>
+        <span>Following 1.</span>
+    </div>
+    <div class="accountstats" aria-hidden="true">
+        <b>Joined</b><time datetime="2022-06-04T13:12:00.000Z">Jun, 2022</time>
+        <b>Posts</b><span>8</span>
+        <b>Followed by</b><span>1</span>
+        <b>Following</b><span>1</span>
+    </div>
+</section>
+

There, hope you liked that!

]]>
+ @the_mighty_zork@localhost:8080 + http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40 + Sun, 10 Dec 2023 09:24:00 +0000 + http://localhost:8080/@the_mighty_zork/feed.rss +
+ + introduction post + http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY + @the_mighty_zork@localhost:8080 made a new post: "hello everyone!" + + @the_mighty_zork@localhost:8080 + http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY + Wed, 20 Oct 2021 10:40:37 +0000 + http://localhost:8080/@the_mighty_zork/feed.rss + +
+
`, feed) } func (suite *GetRSSTestSuite) TestGetAccountRSSZorkNoPosts() { @@ -70,7 +167,20 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSZorkNoPosts() { feed, err := getFeed() suite.NoError(err) - suite.Equal("\n \n Posts from @the_mighty_zork@localhost:8080\n http://localhost:8080/@the_mighty_zork\n Posts from @the_mighty_zork@localhost:8080\n Fri, 20 May 2022 11:09:18 +0000\n Fri, 20 May 2022 11:09:18 +0000\n \n http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp\n Avatar for @the_mighty_zork@localhost:8080\n http://localhost:8080/@the_mighty_zork\n \n \n", feed) + suite.Equal(` + + Posts from @the_mighty_zork@localhost:8080 + http://localhost:8080/@the_mighty_zork + Posts from @the_mighty_zork@localhost:8080 + Fri, 20 May 2022 11:09:18 +0000 + Fri, 20 May 2022 11:09:18 +0000 + + http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp + Avatar for @the_mighty_zork@localhost:8080 + http://localhost:8080/@the_mighty_zork + + +`, feed) } func TestGetRSSTestSuite(t *testing.T) { diff --git a/internal/text/emojify.go b/internal/text/emojify.go index 23730eaf9..961ce8771 100644 --- a/internal/text/emojify.go +++ b/internal/text/emojify.go @@ -32,20 +32,44 @@ func EmojifyWeb(emojis []apimodel.Emoji, html template.HTML) template.HTML { out := emojify( emojis, string(html), - func(url, code string, buf *bytes.Buffer) { - buf.WriteString(`:`)
-			buf.WriteString(code)
-			buf.WriteString(`:`) + func(url, staticURL, code string, buf *bytes.Buffer) { + // Open a picture tag so we + // can present multiple options. + buf.WriteString(``) + + // Static version. + buf.WriteString(``) + + // Original image source. + buf.WriteString(``) + + // Close the picture tag. + buf.WriteString(``) }, ) @@ -60,17 +84,18 @@ func EmojifyRSS(emojis []apimodel.Emoji, text string) string { return emojify( emojis, text, - func(url, code string, buf *bytes.Buffer) { - buf.WriteString(`:`)
-			buf.WriteString(code)
-			buf.WriteString(`:`) + func(url, staticURL, code string, buf *bytes.Buffer) { + // Original image source. + buf.WriteString(``) }, ) } @@ -85,7 +110,7 @@ func Demojify(text string) string { func emojify( emojis []apimodel.Emoji, input string, - write func(url, code string, buf *bytes.Buffer), + write func(url, staticURL, code string, buf *bytes.Buffer), ) string { // Build map of shortcodes. Normalize each // shortcode by readding closing colons. @@ -107,10 +132,11 @@ func emojify( // Escape raw emoji content. url := html.EscapeString(emoji.URL) + staticURL := html.EscapeString(emoji.StaticURL) code := html.EscapeString(emoji.Shortcode) // Write emoji repr to buffer. - write(url, code, buf) + write(url, staticURL, code, buf) return buf.String() }, ) diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index f64f4acff..7d2889b05 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -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 . + 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 . + 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 { diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go index 4d40fa0b6..3599e7b56 100644 --- a/internal/typeutils/internaltofrontend_test.go +++ b/internal/typeutils/internaltofrontend_test.go @@ -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", diff --git a/internal/typeutils/internaltorss_test.go b/internal/typeutils/internaltorss_test.go index 0988b8ecb..5c4d27208 100644 --- a/internal/typeutils/internaltorss_test.go +++ b/internal/typeutils/internaltorss_test.go @@ -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 \":rainbow:\" !", item.Content) + suite.Equal("hello world! #welcome ! first post on the instance \":rainbow:\" !", item.Content) } func (suite *InternalToRSSTestSuite) TestStatusToRSSItem3() { diff --git a/internal/web/thread.go b/internal/web/thread.go index de3d1b361..d3ba6ea5e 100644 --- a/internal/web/thread.go +++ b/internal/web/thread.go @@ -108,7 +108,7 @@ func (m *Module) threadGETHandler(c *gin.Context) { } // Ensure status actually belongs to target account. - if context.Status.GetAccountID() != targetAccount.ID { + if context.Status.Account.ID != targetAccount.ID { err := fmt.Errorf("target account %s does not own status %s", targetUsername, targetStatusID) apiutil.WebErrorHandler(c, gtserror.NewErrorNotFound(err), instanceGet) return diff --git a/web/source/css/base.css b/web/source/css/base.css index 522820f15..630e4a6d2 100644 --- a/web/source/css/base.css +++ b/web/source/css/base.css @@ -187,18 +187,20 @@ input, select, textarea, .input { margin: -0.2em 0.02em 0; object-fit: contain; vertical-align: middle; - transition: 0.1s; - - /* - Enlarge emojis on hover to give - viewer a good look at them. - */ - &:hover, &:active { - transform: scale(2); - background-color: $bg; - box-shadow: $boxshadow; - border: $boxshadow-border; - border-radius: $br-inner; + + @media (prefers-reduced-motion: no-preference) { + /* + Enlarge emojis on hover to give + viewer a good look at them. + */ + transition: 0.1s; + &:hover, &:active { + transform: scale(2); + background-color: $bg; + box-shadow: $boxshadow; + border: $boxshadow-border; + border-radius: $br-inner; + } } } diff --git a/web/source/css/index.css b/web/source/css/index.css index 59909ec33..382cd68c6 100644 --- a/web/source/css/index.css +++ b/web/source/css/index.css @@ -30,7 +30,8 @@ line-height: 2rem; } - & > img { + img, + picture { align-self: center; max-height: 6rem; } diff --git a/web/source/css/page.css b/web/source/css/page.css index cc1d4efe3..642586048 100644 --- a/web/source/css/page.css +++ b/web/source/css/page.css @@ -48,7 +48,8 @@ gap: 1rem; justify-content: center; - img { + img, + picture { align-self: center; /* diff --git a/web/source/css/status.css b/web/source/css/status.css index 5c7400654..21dc3416e 100644 --- a/web/source/css/status.css +++ b/web/source/css/status.css @@ -193,14 +193,6 @@ main { font-size: 1rem; line-height: initial; } - - img { - max-width: 100%; - margin: 5px auto; - } - img[alt~="!center"] { - display: block; - } } .poll { diff --git a/web/template/page.tmpl b/web/template/page.tmpl index 347caf33e..d2edc5277 100644 --- a/web/template/page.tmpl +++ b/web/template/page.tmpl @@ -29,7 +29,7 @@ {{- if .instance.ThumbnailType -}} {{- .instance.ThumbnailType -}} {{- else -}} -image/png +image/webp {{- end -}} {{- end -}} diff --git a/web/template/page_header.tmpl b/web/template/page_header.tmpl index e605b349f..388587aaf 100644 --- a/web/template/page_header.tmpl +++ b/web/template/page_header.tmpl @@ -57,11 +57,20 @@ Instance Logo {{- with . }} - {{- template + + {{- if .instance.ThumbnailStatic }} + + {{- end }} + {{- template +

{{- .instance.Title -}}

{{- if .showStrap }} diff --git a/web/template/profile.tmpl b/web/template/profile.tmpl index 256bbdccf..a06c842ab 100644 --- a/web/template/profile.tmpl +++ b/web/template/profile.tmpl @@ -94,14 +94,26 @@ alt="{{- template "avatarAlt" . -}}" title="{{- template "avatarAlt" . -}}" > - {{- template + {{- end }} @@ -115,11 +127,20 @@ {{- include "profileMovedTo" . | indent 2 }} {{- end }}
- {{- template + + {{- if .account.HeaderAttachment }} + + {{- end }} + {{- template +
{{- with . }} diff --git a/web/template/status_header.tmpl b/web/template/status_header.tmpl index 8946a1030..01b73aea0 100644 --- a/web/template/status_header.tmpl +++ b/web/template/status_header.tmpl @@ -32,13 +32,23 @@ title="Open remote profile (opens in a new window)" > {{- end }} - + {{- if .AvatarAttachment }} + + {{- end }} + Avatar for {{ .Username -}} +
{{- if .DisplayName -}}