diff options
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:        "<p>This is my profile, read it and weep! Weep then!</p>",  		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("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n  <channel>\n    <title>Posts from @admin@localhost:8080</title>\n    <link>http://localhost:8080/@admin</link>\n    <description>Posts from @admin@localhost:8080</description>\n    <pubDate>Wed, 20 Oct 2021 10:41:37 +0000</pubDate>\n    <lastBuildDate>Wed, 20 Oct 2021 10:41:37 +0000</lastBuildDate>\n    <item>\n      <title>open to see some puppies</title>\n      <link>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</link>\n      <description>@admin@localhost:8080 made a new post: "🐕🐕🐕🐕🐕"</description>\n      <content:encoded><![CDATA[🐕🐕🐕🐕🐕]]></content:encoded>\n      <author>@admin@localhost:8080</author>\n      <guid isPermaLink=\"true\">http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</guid>\n      <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>\n      <source>http://localhost:8080/@admin/feed.rss</source>\n    </item>\n    <item>\n      <title>hello world! #welcome ! first post on the instance :rainbow: !</title>\n      <link>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</link>\n      <description>@admin@localhost:8080 posted 1 attachment: "hello world! #welcome ! first post on the instance :rainbow: !"</description>\n      <content:encoded><![CDATA[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\"/> !]]></content:encoded>\n      <author>@admin@localhost:8080</author>\n      <enclosure url=\"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg\" length=\"62529\" type=\"image/jpeg\"></enclosure>\n      <guid isPermaLink=\"true\">http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</guid>\n      <pubDate>Wed, 20 Oct 2021 11:36:45 +0000</pubDate>\n      <source>http://localhost:8080/@admin/feed.rss</source>\n    </item>\n  </channel>\n</rss>", feed) +	suite.Equal(`<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"> +  <channel> +    <title>Posts from @admin@localhost:8080</title> +    <link>http://localhost:8080/@admin</link> +    <description>Posts from @admin@localhost:8080</description> +    <pubDate>Wed, 20 Oct 2021 10:41:37 +0000</pubDate> +    <lastBuildDate>Wed, 20 Oct 2021 10:41:37 +0000</lastBuildDate> +    <item> +      <title>open to see some puppies</title> +      <link>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</link> +      <description>@admin@localhost:8080 made a new post: "🐕🐕🐕🐕🐕"</description> +      <content:encoded><![CDATA[🐕🐕🐕🐕🐕]]></content:encoded> +      <author>@admin@localhost:8080</author> +      <guid isPermaLink="true">http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</guid> +      <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate> +      <source>http://localhost:8080/@admin/feed.rss</source> +    </item> +    <item> +      <title>hello world! #welcome ! first post on the instance :rainbow: !</title> +      <link>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</link> +      <description>@admin@localhost:8080 posted 1 attachment: "hello world! #welcome ! first post on the instance :rainbow: !"</description> +      <content:encoded><![CDATA[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" /> !]]></content:encoded> +      <author>@admin@localhost:8080</author> +      <enclosure url="http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg" length="62529" type="image/jpeg"></enclosure> +      <guid isPermaLink="true">http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</guid> +      <pubDate>Wed, 20 Oct 2021 11:36:45 +0000</pubDate> +      <source>http://localhost:8080/@admin/feed.rss</source> +    </item> +  </channel> +</rss>`, feed)  }  func (suite *GetRSSTestSuite) TestGetAccountRSSZork() { @@ -45,7 +74,75 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSZork() {  	feed, err := getFeed()  	suite.NoError(err) -	suite.Equal("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n  <channel>\n    <title>Posts from @the_mighty_zork@localhost:8080</title>\n    <link>http://localhost:8080/@the_mighty_zork</link>\n    <description>Posts from @the_mighty_zork@localhost:8080</description>\n    <pubDate>Wed, 10 Jan 2024 09:24:00 +0000</pubDate>\n    <lastBuildDate>Wed, 10 Jan 2024 09:24:00 +0000</lastBuildDate>\n    <image>\n      <url>http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp</url>\n      <title>Avatar for @the_mighty_zork@localhost:8080</title>\n      <link>http://localhost:8080/@the_mighty_zork</link>\n    </image>\n    <item>\n      <title>HTML in post</title>\n      <link>http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40</link>\n      <description>@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>
...</description>\n      <content:encoded><![CDATA[<p>Here's a bunch of HTML, read it and weep, weep then!</p><pre><code class=\"language-html\"><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</code></pre><p>There, hope you liked that!</p>]]></content:encoded>\n      <author>@the_mighty_zork@localhost:8080</author>\n      <guid isPermaLink=\"true\">http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40</guid>\n      <pubDate>Sun, 10 Dec 2023 09:24:00 +0000</pubDate>\n      <source>http://localhost:8080/@the_mighty_zork/feed.rss</source>\n    </item>\n    <item>\n      <title>introduction post</title>\n      <link>http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</link>\n      <description>@the_mighty_zork@localhost:8080 made a new post: "hello everyone!"</description>\n      <content:encoded><![CDATA[hello everyone!]]></content:encoded>\n      <author>@the_mighty_zork@localhost:8080</author>\n      <guid isPermaLink=\"true\">http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</guid>\n      <pubDate>Wed, 20 Oct 2021 10:40:37 +0000</pubDate>\n      <source>http://localhost:8080/@the_mighty_zork/feed.rss</source>\n    </item>\n  </channel>\n</rss>", feed) +	suite.Equal(`<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"> +  <channel> +    <title>Posts from @the_mighty_zork@localhost:8080</title> +    <link>http://localhost:8080/@the_mighty_zork</link> +    <description>Posts from @the_mighty_zork@localhost:8080</description> +    <pubDate>Wed, 10 Jan 2024 09:24:00 +0000</pubDate> +    <lastBuildDate>Wed, 10 Jan 2024 09:24:00 +0000</lastBuildDate> +    <image> +      <url>http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp</url> +      <title>Avatar for @the_mighty_zork@localhost:8080</title> +      <link>http://localhost:8080/@the_mighty_zork</link> +    </image> +    <item> +      <title>HTML in post</title> +      <link>http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40</link> +      <description>@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>
...</description> +      <content:encoded><![CDATA[<p>Here's a bunch of HTML, read it and weep, weep then!</p><pre><code class="language-html"><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> +</code></pre><p>There, hope you liked that!</p>]]></content:encoded> +      <author>@the_mighty_zork@localhost:8080</author> +      <guid isPermaLink="true">http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40</guid> +      <pubDate>Sun, 10 Dec 2023 09:24:00 +0000</pubDate> +      <source>http://localhost:8080/@the_mighty_zork/feed.rss</source> +    </item> +    <item> +      <title>introduction post</title> +      <link>http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</link> +      <description>@the_mighty_zork@localhost:8080 made a new post: "hello everyone!"</description> +      <content:encoded><![CDATA[hello everyone!]]></content:encoded> +      <author>@the_mighty_zork@localhost:8080</author> +      <guid isPermaLink="true">http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</guid> +      <pubDate>Wed, 20 Oct 2021 10:40:37 +0000</pubDate> +      <source>http://localhost:8080/@the_mighty_zork/feed.rss</source> +    </item> +  </channel> +</rss>`, feed)  }  func (suite *GetRSSTestSuite) TestGetAccountRSSZorkNoPosts() { @@ -70,7 +167,20 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSZorkNoPosts() {  	feed, err := getFeed()  	suite.NoError(err) -	suite.Equal("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n  <channel>\n    <title>Posts from @the_mighty_zork@localhost:8080</title>\n    <link>http://localhost:8080/@the_mighty_zork</link>\n    <description>Posts from @the_mighty_zork@localhost:8080</description>\n    <pubDate>Fri, 20 May 2022 11:09:18 +0000</pubDate>\n    <lastBuildDate>Fri, 20 May 2022 11:09:18 +0000</lastBuildDate>\n    <image>\n      <url>http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp</url>\n      <title>Avatar for @the_mighty_zork@localhost:8080</title>\n      <link>http://localhost:8080/@the_mighty_zork</link>\n    </image>\n  </channel>\n</rss>", feed) +	suite.Equal(`<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"> +  <channel> +    <title>Posts from @the_mighty_zork@localhost:8080</title> +    <link>http://localhost:8080/@the_mighty_zork</link> +    <description>Posts from @the_mighty_zork@localhost:8080</description> +    <pubDate>Fri, 20 May 2022 11:09:18 +0000</pubDate> +    <lastBuildDate>Fri, 20 May 2022 11:09:18 +0000</lastBuildDate> +    <image> +      <url>http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp</url> +      <title>Avatar for @the_mighty_zork@localhost:8080</title> +      <link>http://localhost:8080/@the_mighty_zork</link> +    </image> +  </channel> +</rss>`, 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(`<img src="`) -			buf.WriteString(url) -			buf.WriteString(`" title=":`) -			buf.WriteString(code) -			buf.WriteString(`:" alt=":`) -			buf.WriteString(code) -			buf.WriteString(`:" class="emoji" `) -			// Lazy load emojis when -			// they scroll into view. -			buf.WriteString(`loading="lazy" `) -			// Limit size to avoid showing -			// huge emojis when unstyled. -			buf.WriteString(`width="25" height="25"/>`) +		func(url, staticURL, code string, buf *bytes.Buffer) { +			// Open a picture tag so we +			// can present multiple options. +			buf.WriteString(`<picture>`) + +			// Static version. +			buf.WriteString(`<source `) +			{ +				buf.WriteString(`class="emoji" `) +				buf.WriteString(`srcset="` + staticURL + `" `) +				buf.WriteString(`type="image/png" `) +				// Show this version when user +				// doesn't want an animated emoji. +				buf.WriteString(`media="(prefers-reduced-motion: reduce)" `) +				// Limit size to avoid showing +				// huge emojis when unstyled. +				buf.WriteString(`width="25" height="25" `) +			} +			buf.WriteString(`/>`) + +			// Original image source. +			buf.WriteString(`<img `) +			{ +				buf.WriteString(`class="emoji" `) +				buf.WriteString(`src="` + url + `" `) +				buf.WriteString(`title=":` + code + `:" `) +				buf.WriteString(`alt=":` + code + `:" `) +				// Lazy load emojis when +				// they scroll into view. +				buf.WriteString(`loading="lazy" `) +				// Limit size to avoid showing +				// huge emojis when unstyled. +				buf.WriteString(`width="25" height="25" `) +			} +			buf.WriteString(`/>`) + +			// Close the picture tag. +			buf.WriteString(`</picture>`)  		},  	) @@ -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(`<img src="`) -			buf.WriteString(url) -			buf.WriteString(`" title=":`) -			buf.WriteString(code) -			buf.WriteString(`:" alt=":`) -			buf.WriteString(code) -			buf.WriteString(`:" `) -			// Limit size to avoid showing -			// huge emojis in RSS readers. -			buf.WriteString(`width="25" height="25"/>`) +		func(url, staticURL, code string, buf *bytes.Buffer) { +			// Original image source. +			buf.WriteString(`<img `) +			{ +				buf.WriteString(`src="` + url + `" `) +				buf.WriteString(`title=":` + code + `:" `) +				buf.WriteString(`alt=":` + code + `:" `) +				// Limit size to avoid showing +				// huge emojis in RSS readers. +				buf.WriteString(`width="25" height="25" `) +			} +			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 <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 { 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 <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() { 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 . }}  <a aria-label="{{- .instance.Title -}}. Go to instance homepage" href="/" class="nounderline"> -    <img -        src="{{- .instance.Thumbnail -}}" -        alt="{{- template "thumbnailDescription" . -}}" -        title="{{- template "thumbnailDescription" . -}}" -    /> +    <picture> +        {{- if .instance.ThumbnailStatic }} +        <source +            srcset="{{- .instance.ThumbnailStatic -}}" +            type="{{- .instance.ThumbnailStaticType -}}" +            media="(prefers-reduced-motion: reduce)" +        /> +        {{- end }} +        <img +            src="{{- .instance.Thumbnail -}}" +            alt="{{- template "thumbnailDescription" . -}}" +            title="{{- template "thumbnailDescription" . -}}" +        /> +    </picture>      <h1>{{- .instance.Title -}}</h1>  </a>  {{- 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" . -}}"      > -        <img -            class="avatar" -            src="{{- .account.Avatar -}}" -            alt="{{- template "avatarAlt" . -}}" -            title="{{- template "avatarAlt" . -}}" -            width="{{- template "avatarWidth" . -}}" -            height="{{- template "avatarHeight" . -}}" -        /> +        <picture +            aria-hidden="true" +        > +            {{- if .account.AvatarAttachment }} +            <source +                class="avatar" +                srcset="{{- .account.AvatarStatic -}}" +                type="{{- .account.AvatarAttachment.PreviewMIMEType -}}" +                media="(prefers-reduced-motion: reduce)" +            /> +            {{- end }} +            <img +                class="avatar" +                src="{{- .account.Avatar -}}" +                alt="{{- template "avatarAlt" . -}}" +                title="{{- template "avatarAlt" . -}}" +                width="{{- template "avatarWidth" . -}}" +                height="{{- template "avatarHeight" . -}}" +            /> +        </picture>      </a>  </div>  {{- end }} @@ -115,11 +127,20 @@          {{- include "profileMovedTo" . | indent 2 }}          {{- end }}          <div class="header-image-wrapper"> -            <img -                src="{{- .account.Header -}}" -                alt="{{- template "headerAlt" . -}}" -                title="{{- template "headerAlt" . -}}" -            /> +            <picture> +                {{- if .account.HeaderAttachment }} +                <source +                    srcset="{{- .account.HeaderStatic -}}" +                    type="{{- .account.HeaderAttachment.PreviewMIMEType -}}" +                    media="(prefers-reduced-motion: reduce)" +                /> +                {{- end }} +                <img +                    src="{{- .account.Header -}}" +                    alt="{{- template "headerAlt" . -}}" +                    title="{{- template "headerAlt" . -}}" +                /> +            </picture>          </div>          <div class="basic-info">              {{- 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 }} -        <img +        <picture              class="avatar"              aria-hidden="true" -            src="{{- .Avatar -}}" -            alt="Avatar for {{ .Username -}}" -            title="Avatar for {{ .Username -}}"          > +            {{- if .AvatarAttachment }} +            <source +                srcset="{{- .AvatarStatic -}}" +                type="{{- .AvatarAttachment.PreviewMIMEType -}}" +                media="(prefers-reduced-motion: reduce)" +            /> +            {{- end }} +            <img +                src="{{- .Avatar -}}" +                alt="Avatar for {{ .Username -}}" +                title="Avatar for {{ .Username -}}" +            > +        </picture>          <div class="author-strap">              <span class="displayname text-cutoff">                  {{- if .DisplayName -}}  | 
