diff options
author | 2024-07-21 14:22:08 +0200 | |
---|---|---|
committer | 2024-07-21 14:22:08 +0200 | |
commit | 027a93facc73b78e3c3747ab796f4a14b1b4936f (patch) | |
tree | 51838cd9fdf607ad46fdfa77a9424fdd8f5cd98d /internal | |
parent | [bugfix] update common get target account / status doing refresh async (#3124) (diff) | |
download | gotosocial-027a93facc73b78e3c3747ab796f4a14b1b4936f.tar.xz |
[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
Diffstat (limited to 'internal')
-rw-r--r-- | internal/api/client/instance/instancepatch_test.go | 4 | ||||
-rw-r--r-- | internal/api/model/account.go | 24 | ||||
-rw-r--r-- | internal/api/model/attachment.go | 4 | ||||
-rw-r--r-- | internal/api/model/instancev1.go | 6 | ||||
-rw-r--r-- | internal/api/model/instancev2.go | 7 | ||||
-rw-r--r-- | internal/api/model/status.go | 3 | ||||
-rw-r--r-- | internal/api/util/opengraph.go | 4 | ||||
-rw-r--r-- | internal/api/util/opengraph_test.go | 12 | ||||
-rw-r--r-- | internal/processing/account/get.go | 2 | ||||
-rw-r--r-- | internal/processing/account/rss_test.go | 116 | ||||
-rw-r--r-- | internal/text/emojify.go | 80 | ||||
-rw-r--r-- | internal/typeutils/internaltofrontend.go | 108 | ||||
-rw-r--r-- | internal/typeutils/internaltofrontend_test.go | 53 | ||||
-rw-r--r-- | internal/typeutils/internaltorss_test.go | 2 | ||||
-rw-r--r-- | internal/web/thread.go | 2 |
15 files changed, 332 insertions, 95 deletions
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 |