[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
parent b415337d40
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

@ -1526,6 +1526,16 @@ definitions:
example: picture of a cute lil' friendly sloth example: picture of a cute lil' friendly sloth
type: string type: string
x-go-name: ThumbnailDescription 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: thumbnail_type:
description: MIME type of the instance thumbnail. description: MIME type of the instance thumbnail.
example: image/png example: image/png
@ -1759,6 +1769,11 @@ definitions:
example: UeKUpFxuo~R%0nW;WCnhF6RjaJt757oJodS$ example: UeKUpFxuo~R%0nW;WCnhF6RjaJt757oJodS$
type: string type: string
x-go-name: Blurhash 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: thumbnail_description:
description: |- description: |-
Description of the instance thumbnail. Description of the instance thumbnail.
@ -1766,6 +1781,13 @@ definitions:
example: picture of a cute lil' friendly sloth example: picture of a cute lil' friendly sloth
type: string type: string
x-go-name: Description 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: thumbnail_type:
description: |- description: |-
MIME type of the instance thumbnail. MIME type of the instance thumbnail.

View file

@ -762,6 +762,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
}, },
"thumbnail": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/original/`+instanceAccount.AvatarMediaAttachment.ID+`.gif",`+` "thumbnail": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/original/`+instanceAccount.AvatarMediaAttachment.ID+`.gif",`+`
"thumbnail_type": "image/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.", "thumbnail_description": "A bouncing little green peglin.",
"contact_account": { "contact_account": {
"id": "01F8MH17FWEB39HZJ76B6VXSKF", "id": "01F8MH17FWEB39HZJ76B6VXSKF",
@ -818,6 +820,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
suite.Equal(`{ suite.Equal(`{
"url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/original/`+instanceAccount.AvatarMediaAttachment.ID+`.gif",`+` "url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/original/`+instanceAccount.AvatarMediaAttachment.ID+`.gif",`+`
"thumbnail_type": "image/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.", "thumbnail_description": "A bouncing little green peglin.",
"blurhash": "LE9kG#M}4YtO%dRkWEt5Dmoxx?WC" "blurhash": "LE9kG#M}4YtO%dRkWEt5Dmoxx?WC"
}`, string(instanceV2ThumbnailJson)) }`, string(instanceV2ThumbnailJson))

View file

@ -110,17 +110,27 @@ type Account struct {
// If set, indicates that this account is currently inactive, and has migrated to the given account. // 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. // Key/value omitted for accounts that haven't moved, and for suspended accounts.
Moved *Account `json:"moved,omitempty"` Moved *Account `json:"moved,omitempty"`
}
// Additional fields not exposed via JSON // WebAccount is like Account, but with
// (used only internally for templating etc). // additional fields not exposed via JSON;
// used only internally for templating etc.
//
// swagger:ignore
type WebAccount struct {
*Account
// Proper attachment model for the avatar. // Proper attachment model for the avatar.
// //
// Only set if this model was converted via // Only set if this account had an avatar set
// AccountToWebAccount, AND this account had // (and not just the default "blank" image.)
// an avatar set (and not just the default AvatarAttachment *WebAttachment `json:"-"`
// "blank" avatar image.)
AvatarAttachment *Attachment `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. // MutedAccount extends Account with a field used only by the muted user list.

View file

@ -107,6 +107,10 @@ type WebAttachment struct {
// MIME type of // MIME type of
// the attachment. // the attachment.
MIMEType string MIMEType string
// MIME type of
// the thumbnail.
PreviewMIMEType string
} }
// MediaMeta models media metadata. // MediaMeta models media metadata.

View file

@ -85,6 +85,12 @@ type InstanceV1 struct {
// MIME type of the instance thumbnail. // MIME type of the instance thumbnail.
// example: image/png // example: image/png
ThumbnailType string `json:"thumbnail_type,omitempty"` 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. // Description of the instance thumbnail.
// example: picture of a cute lil' friendly sloth // example: picture of a cute lil' friendly sloth
ThumbnailDescription string `json:"thumbnail_description,omitempty"` ThumbnailDescription string `json:"thumbnail_description,omitempty"`

View file

@ -102,6 +102,13 @@ type InstanceV2Thumbnail struct {
// Key/value not set if thumbnail image type unknown. // Key/value not set if thumbnail image type unknown.
// example: image/png // example: image/png
Type string `json:"thumbnail_type,omitempty"` 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. // Description of the instance thumbnail.
// Key/value not set if no description available. // Key/value not set if no description available.
// example: picture of a cute lil' friendly sloth // example: picture of a cute lil' friendly sloth

View file

@ -113,6 +113,9 @@ type Status struct {
type WebStatus struct { type WebStatus struct {
*Status *Status
// Override API account with web account.
Account *WebAccount `json:"account"`
// Web version of media // Web version of media
// attached to this status. // attached to this status.
MediaAttachments []*WebAttachment `json:"media_attachments"` MediaAttachments []*WebAttachment `json:"media_attachments"`

View file

@ -84,7 +84,7 @@ func OGBase(instance *apimodel.InstanceV1) *OGMeta {
// WithAccount uses the given account to build an ogMeta // WithAccount uses the given account to build an ogMeta
// struct specific to that account. It's suitable for serving // struct specific to that account. It's suitable for serving
// at account profile pages. // 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.Title = AccountTitle(account, og.SiteName)
og.Type = "profile" og.Type = "profile"
og.URL = account.URL 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 // 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 user := "@" + account.Acct + "@" + accountDomain
if len(account.DisplayName) == 0 { if len(account.DisplayName) == 0 {

View file

@ -51,13 +51,15 @@ func (suite *OpenGraphTestSuite) TestWithAccountWithNote() {
Languages: []string{"en"}, Languages: []string{"en"},
}) })
accountMeta := baseMeta.WithAccount(&apimodel.Account{ acct := &apimodel.Account{
Acct: "example_account", Acct: "example_account",
DisplayName: "example person!!", DisplayName: "example person!!",
URL: "https://example.org/@example_account", URL: "https://example.org/@example_account",
Note: "<p>This is my profile, read it and weep! Weep then!</p>", Note: "<p>This is my profile, read it and weep! Weep then!</p>",
Username: "example_account", Username: "example_account",
}) }
accountMeta := baseMeta.WithAccount(&apimodel.WebAccount{Account: acct})
suite.EqualValues(OGMeta{ suite.EqualValues(OGMeta{
Title: "example person!!, @example_account@example.org", Title: "example person!!, @example_account@example.org",
@ -84,13 +86,15 @@ func (suite *OpenGraphTestSuite) TestWithAccountNoNote() {
Languages: []string{"en"}, Languages: []string{"en"},
}) })
accountMeta := baseMeta.WithAccount(&apimodel.Account{ acct := &apimodel.Account{
Acct: "example_account", Acct: "example_account",
DisplayName: "example person!!", DisplayName: "example person!!",
URL: "https://example.org/@example_account", URL: "https://example.org/@example_account",
Note: "", // <- empty Note: "", // <- empty
Username: "example_account", Username: "example_account",
}) }
accountMeta := baseMeta.WithAccount(&apimodel.WebAccount{Account: acct})
suite.EqualValues(OGMeta{ suite.EqualValues(OGMeta{
Title: "example person!!, @example_account@example.org", Title: "example person!!, @example_account@example.org",

View file

@ -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. // 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, "") targetAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, username, "")
if err != nil { if err != nil {
if errors.Is(err, db.ErrNoEntries) { if errors.Is(err, db.ErrNoEntries) {

View file

@ -35,7 +35,36 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() {
feed, err := getFeed() feed, err := getFeed()
suite.NoError(err) 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: &#34;🐕🐕🐕🐕🐕&#34;</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: &#34;hello world! #welcome ! first post on the instance :rainbow: !&#34;</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: &#34;🐕🐕🐕🐕🐕&#34;</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: &#34;hello world! #welcome ! first post on the instance :rainbow: !&#34;</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() { func (suite *GetRSSTestSuite) TestGetAccountRSSZork() {
@ -45,7 +74,75 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSZork() {
feed, err := getFeed() feed, err := getFeed()
suite.NoError(err) 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: &#34;Here&#39;s a bunch of HTML, read it and weep, weep then!&#xA;&#xA;```html&#xA;&lt;section class=&#34;about-user&#34;&gt;&#xA; &lt;div class=&#34;col-header&#34;&gt;&#xA; &lt;h2&gt;About&lt;/h2&gt;&#xA; &lt;/div&gt; &#xA; &lt;div class=&#34;fields&#34;&gt;&#xA; &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;&#xA; &lt;dl&gt;&#xA;...</description>\n <content:encoded><![CDATA[<p>Here's a bunch of HTML, read it and weep, weep then!</p><pre><code class=\"language-html\">&lt;section class=&#34;about-user&#34;&gt;\n &lt;div class=&#34;col-header&#34;&gt;\n &lt;h2&gt;About&lt;/h2&gt;\n &lt;/div&gt; \n &lt;div class=&#34;fields&#34;&gt;\n &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;\n &lt;dl&gt;\n &lt;div class=&#34;field&#34;&gt;\n &lt;dt&gt;should you follow me?&lt;/dt&gt;\n &lt;dd&gt;maybe!&lt;/dd&gt;\n &lt;/div&gt;\n &lt;div class=&#34;field&#34;&gt;\n &lt;dt&gt;age&lt;/dt&gt;\n &lt;dd&gt;120&lt;/dd&gt;\n &lt;/div&gt;\n &lt;/dl&gt;\n &lt;/div&gt;\n &lt;div class=&#34;bio&#34;&gt;\n &lt;h3 class=&#34;sr-only&#34;&gt;Bio&lt;/h3&gt;\n &lt;p&gt;i post about things that concern me&lt;/p&gt;\n &lt;/div&gt;\n &lt;div class=&#34;sr-only&#34; role=&#34;group&#34;&gt;\n &lt;h3 class=&#34;sr-only&#34;&gt;Stats&lt;/h3&gt;\n &lt;span&gt;Joined in Jun, 2022.&lt;/span&gt;\n &lt;span&gt;8 posts.&lt;/span&gt;\n &lt;span&gt;Followed by 1.&lt;/span&gt;\n &lt;span&gt;Following 1.&lt;/span&gt;\n &lt;/div&gt;\n &lt;div class=&#34;accountstats&#34; aria-hidden=&#34;true&#34;&gt;\n &lt;b&gt;Joined&lt;/b&gt;&lt;time datetime=&#34;2022-06-04T13:12:00.000Z&#34;&gt;Jun, 2022&lt;/time&gt;\n &lt;b&gt;Posts&lt;/b&gt;&lt;span&gt;8&lt;/span&gt;\n &lt;b&gt;Followed by&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;\n &lt;b&gt;Following&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;\n &lt;/div&gt;\n&lt;/section&gt;\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: &#34;hello everyone!&#34;</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: &#34;Here&#39;s a bunch of HTML, read it and weep, weep then!&#xA;&#xA;`+"```"+`html&#xA;&lt;section class=&#34;about-user&#34;&gt;&#xA; &lt;div class=&#34;col-header&#34;&gt;&#xA; &lt;h2&gt;About&lt;/h2&gt;&#xA; &lt;/div&gt; &#xA; &lt;div class=&#34;fields&#34;&gt;&#xA; &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;&#xA; &lt;dl&gt;&#xA;...</description>
<content:encoded><![CDATA[<p>Here's a bunch of HTML, read it and weep, weep then!</p><pre><code class="language-html">&lt;section class=&#34;about-user&#34;&gt;
&lt;div class=&#34;col-header&#34;&gt;
&lt;h2&gt;About&lt;/h2&gt;
&lt;/div&gt;
&lt;div class=&#34;fields&#34;&gt;
&lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;
&lt;dl&gt;
&lt;div class=&#34;field&#34;&gt;
&lt;dt&gt;should you follow me?&lt;/dt&gt;
&lt;dd&gt;maybe!&lt;/dd&gt;
&lt;/div&gt;
&lt;div class=&#34;field&#34;&gt;
&lt;dt&gt;age&lt;/dt&gt;
&lt;dd&gt;120&lt;/dd&gt;
&lt;/div&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&#34;bio&#34;&gt;
&lt;h3 class=&#34;sr-only&#34;&gt;Bio&lt;/h3&gt;
&lt;p&gt;i post about things that concern me&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&#34;sr-only&#34; role=&#34;group&#34;&gt;
&lt;h3 class=&#34;sr-only&#34;&gt;Stats&lt;/h3&gt;
&lt;span&gt;Joined in Jun, 2022.&lt;/span&gt;
&lt;span&gt;8 posts.&lt;/span&gt;
&lt;span&gt;Followed by 1.&lt;/span&gt;
&lt;span&gt;Following 1.&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&#34;accountstats&#34; aria-hidden=&#34;true&#34;&gt;
&lt;b&gt;Joined&lt;/b&gt;&lt;time datetime=&#34;2022-06-04T13:12:00.000Z&#34;&gt;Jun, 2022&lt;/time&gt;
&lt;b&gt;Posts&lt;/b&gt;&lt;span&gt;8&lt;/span&gt;
&lt;b&gt;Followed by&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
&lt;b&gt;Following&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
&lt;/div&gt;
&lt;/section&gt;
</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: &#34;hello everyone!&#34;</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() { func (suite *GetRSSTestSuite) TestGetAccountRSSZorkNoPosts() {
@ -70,7 +167,20 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSZorkNoPosts() {
feed, err := getFeed() feed, err := getFeed()
suite.NoError(err) 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) { func TestGetRSSTestSuite(t *testing.T) {

View file

@ -32,20 +32,44 @@ func EmojifyWeb(emojis []apimodel.Emoji, html template.HTML) template.HTML {
out := emojify( out := emojify(
emojis, emojis,
string(html), string(html),
func(url, code string, buf *bytes.Buffer) { func(url, staticURL, code string, buf *bytes.Buffer) {
buf.WriteString(`<img src="`) // Open a picture tag so we
buf.WriteString(url) // can present multiple options.
buf.WriteString(`" title=":`) buf.WriteString(`<picture>`)
buf.WriteString(code)
buf.WriteString(`:" alt=":`) // Static version.
buf.WriteString(code) buf.WriteString(`<source `)
buf.WriteString(`:" class="emoji" `) {
// Lazy load emojis when buf.WriteString(`class="emoji" `)
// they scroll into view. buf.WriteString(`srcset="` + staticURL + `" `)
buf.WriteString(`loading="lazy" `) buf.WriteString(`type="image/png" `)
// Limit size to avoid showing // Show this version when user
// huge emojis when unstyled. // doesn't want an animated emoji.
buf.WriteString(`width="25" height="25"/>`) 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( return emojify(
emojis, emojis,
text, text,
func(url, code string, buf *bytes.Buffer) { func(url, staticURL, code string, buf *bytes.Buffer) {
buf.WriteString(`<img src="`) // Original image source.
buf.WriteString(url) buf.WriteString(`<img `)
buf.WriteString(`" title=":`) {
buf.WriteString(code) buf.WriteString(`src="` + url + `" `)
buf.WriteString(`:" alt=":`) buf.WriteString(`title=":` + code + `:" `)
buf.WriteString(code) buf.WriteString(`alt=":` + code + `:" `)
buf.WriteString(`:" `) // Limit size to avoid showing
// Limit size to avoid showing // huge emojis in RSS readers.
// huge emojis in RSS readers. buf.WriteString(`width="25" height="25" `)
buf.WriteString(`width="25" height="25"/>`) }
buf.WriteString(`/>`)
}, },
) )
} }
@ -85,7 +110,7 @@ func Demojify(text string) string {
func emojify( func emojify(
emojis []apimodel.Emoji, emojis []apimodel.Emoji,
input string, input string,
write func(url, code string, buf *bytes.Buffer), write func(url, staticURL, code string, buf *bytes.Buffer),
) string { ) string {
// Build map of shortcodes. Normalize each // Build map of shortcodes. Normalize each
// shortcode by readding closing colons. // shortcode by readding closing colons.
@ -107,10 +132,11 @@ func(shortcode string, buf *bytes.Buffer) string {
// Escape raw emoji content. // Escape raw emoji content.
url := html.EscapeString(emoji.URL) url := html.EscapeString(emoji.URL)
staticURL := html.EscapeString(emoji.StaticURL)
code := html.EscapeString(emoji.Shortcode) code := html.EscapeString(emoji.Shortcode)
// Write emoji repr to buffer. // Write emoji repr to buffer.
write(url, code, buf) write(url, staticURL, code, buf)
return buf.String() return buf.String()
}, },
) )

View file

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

View file

@ -981,28 +981,6 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
"pinned": false, "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", "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, "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": [ "mentions": [
{ {
"id": "01F8MH17FWEB39HZJ76B6VXSKF", "id": "01F8MH17FWEB39HZJ76B6VXSKF",
@ -1035,6 +1013,28 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
"with_approval": [] "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": [ "media_attachments": [
{ {
"id": "01HE7Y3C432WRSNS10EZM86SA5", "id": "01HE7Y3C432WRSNS10EZM86SA5",
@ -1065,7 +1065,8 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
"description": "Photograph of a sloth, Public Domain.", "description": "Photograph of a sloth, Public Domain.",
"blurhash": "LKE3VIw}0KD%a2o{M|t7NFWps:t7", "blurhash": "LKE3VIw}0KD%a2o{M|t7NFWps:t7",
"Sensitive": true, "Sensitive": true,
"MIMEType": "image/jpg" "MIMEType": "image/jpg",
"PreviewMIMEType": "image/webp"
}, },
{ {
"id": "01HE7ZFX9GKA5ZZVD4FACABSS9", "id": "01HE7ZFX9GKA5ZZVD4FACABSS9",
@ -1079,7 +1080,8 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
"description": "SVG line art of a sloth, public domain", "description": "SVG line art of a sloth, public domain",
"blurhash": "L26*j+~qE1RP?wxut7ofRlM{R*of", "blurhash": "L26*j+~qE1RP?wxut7ofRlM{R*of",
"Sensitive": true, "Sensitive": true,
"MIMEType": "" "MIMEType": "",
"PreviewMIMEType": ""
}, },
{ {
"id": "01HE88YG74PVAB81PX2XA9F3FG", "id": "01HE88YG74PVAB81PX2XA9F3FG",
@ -1093,7 +1095,8 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
"description": "Jolly salsa song, public domain.", "description": "Jolly salsa song, public domain.",
"blurhash": null, "blurhash": null,
"Sensitive": true, "Sensitive": true,
"MIMEType": "" "MIMEType": "",
"PreviewMIMEType": ""
} }
], ],
"LanguageTag": "en", "LanguageTag": "en",

View file

@ -81,7 +81,7 @@ func (suite *InternalToRSSTestSuite) TestStatusToRSSItem2() {
suite.Equal("62529", item.Enclosure.Length) suite.Equal("62529", item.Enclosure.Length)
suite.Equal("image/jpeg", item.Enclosure.Type) suite.Equal("image/jpeg", item.Enclosure.Type)
suite.Equal("http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg", item.Enclosure.Url) 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() { func (suite *InternalToRSSTestSuite) TestStatusToRSSItem3() {

View file

@ -108,7 +108,7 @@ func (m *Module) threadGETHandler(c *gin.Context) {
} }
// Ensure status actually belongs to target account. // 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) err := fmt.Errorf("target account %s does not own status %s", targetUsername, targetStatusID)
apiutil.WebErrorHandler(c, gtserror.NewErrorNotFound(err), instanceGet) apiutil.WebErrorHandler(c, gtserror.NewErrorNotFound(err), instanceGet)
return return

View file

@ -187,18 +187,20 @@ input, select, textarea, .input {
margin: -0.2em 0.02em 0; margin: -0.2em 0.02em 0;
object-fit: contain; object-fit: contain;
vertical-align: middle; vertical-align: middle;
transition: 0.1s;
@media (prefers-reduced-motion: no-preference) {
/* /*
Enlarge emojis on hover to give Enlarge emojis on hover to give
viewer a good look at them. viewer a good look at them.
*/ */
&:hover, &:active { transition: 0.1s;
transform: scale(2); &:hover, &:active {
background-color: $bg; transform: scale(2);
box-shadow: $boxshadow; background-color: $bg;
border: $boxshadow-border; box-shadow: $boxshadow;
border-radius: $br-inner; border: $boxshadow-border;
border-radius: $br-inner;
}
} }
} }

View file

@ -30,7 +30,8 @@
line-height: 2rem; line-height: 2rem;
} }
& > img { img,
picture {
align-self: center; align-self: center;
max-height: 6rem; max-height: 6rem;
} }

View file

@ -48,7 +48,8 @@
gap: 1rem; gap: 1rem;
justify-content: center; justify-content: center;
img { img,
picture {
align-self: center; align-self: center;
/* /*

View file

@ -193,14 +193,6 @@ main {
font-size: 1rem; font-size: 1rem;
line-height: initial; line-height: initial;
} }
img {
max-width: 100%;
margin: 5px auto;
}
img[alt~="!center"] {
display: block;
}
} }
.poll { .poll {

View file

@ -29,7 +29,7 @@
{{- if .instance.ThumbnailType -}} {{- if .instance.ThumbnailType -}}
{{- .instance.ThumbnailType -}} {{- .instance.ThumbnailType -}}
{{- else -}} {{- else -}}
image/png image/webp
{{- end -}} {{- end -}}
{{- end -}} {{- end -}}

View file

@ -57,11 +57,20 @@ Instance Logo
{{- with . }} {{- with . }}
<a aria-label="{{- .instance.Title -}}. Go to instance homepage" href="/" class="nounderline"> <a aria-label="{{- .instance.Title -}}. Go to instance homepage" href="/" class="nounderline">
<img <picture>
src="{{- .instance.Thumbnail -}}" {{- if .instance.ThumbnailStatic }}
alt="{{- template "thumbnailDescription" . -}}" <source
title="{{- template "thumbnailDescription" . -}}" 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> <h1>{{- .instance.Title -}}</h1>
</a> </a>
{{- if .showStrap }} {{- if .showStrap }}

View file

@ -94,14 +94,26 @@
alt="{{- template "avatarAlt" . -}}" alt="{{- template "avatarAlt" . -}}"
title="{{- template "avatarAlt" . -}}" title="{{- template "avatarAlt" . -}}"
> >
<img <picture
class="avatar" aria-hidden="true"
src="{{- .account.Avatar -}}" >
alt="{{- template "avatarAlt" . -}}" {{- if .account.AvatarAttachment }}
title="{{- template "avatarAlt" . -}}" <source
width="{{- template "avatarWidth" . -}}" class="avatar"
height="{{- template "avatarHeight" . -}}" 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> </a>
</div> </div>
{{- end }} {{- end }}
@ -115,11 +127,20 @@
{{- include "profileMovedTo" . | indent 2 }} {{- include "profileMovedTo" . | indent 2 }}
{{- end }} {{- end }}
<div class="header-image-wrapper"> <div class="header-image-wrapper">
<img <picture>
src="{{- .account.Header -}}" {{- if .account.HeaderAttachment }}
alt="{{- template "headerAlt" . -}}" <source
title="{{- template "headerAlt" . -}}" 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>
<div class="basic-info"> <div class="basic-info">
{{- with . }} {{- with . }}

View file

@ -32,13 +32,23 @@
title="Open remote profile (opens in a new window)" title="Open remote profile (opens in a new window)"
> >
{{- end }} {{- end }}
<img <picture
class="avatar" class="avatar"
aria-hidden="true" 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"> <div class="author-strap">
<span class="displayname text-cutoff"> <span class="displayname text-cutoff">
{{- if .DisplayName -}} {{- if .DisplayName -}}