mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-21 16:55:38 +03:00
[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:
parent
b415337d40
commit
027a93facc
24 changed files with 435 additions and 140 deletions
|
@ -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.
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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: "🐕🐕🐕🐕🐕"</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() {
|
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: "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() {
|
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) {
|
||||||
|
|
|
@ -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 emojify(
|
||||||
|
|
||||||
// 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()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,8 @@
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
img {
|
img,
|
||||||
|
picture {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
{{- if .instance.ThumbnailType -}}
|
{{- if .instance.ThumbnailType -}}
|
||||||
{{- .instance.ThumbnailType -}}
|
{{- .instance.ThumbnailType -}}
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
image/png
|
image/webp
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -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 . }}
|
||||||
|
|
|
@ -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 -}}
|
||||||
|
|
Loading…
Reference in a new issue