[feature/frontend] Add player for audio files; use thumbnail for poster (#3099)

* [feature/frontend] Audio player for audio media types

* use video preview images for previews instead of video itself

* don't preload

* update tests for new zork status

* collapse media gallery into single row when small
This commit is contained in:
tobi 2024-07-15 11:47:57 +02:00 committed by GitHub
parent 16421f7576
commit 9efb11d848
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 327 additions and 95 deletions

View file

@ -82,7 +82,7 @@ func (suite *OutboxGetTestSuite) TestGetOutbox() {
"@context": "https://www.w3.org/ns/activitystreams",
"first": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40",
"id": "http://localhost:8080/users/the_mighty_zork/outbox",
"totalItems": 7,
"totalItems": 8,
"type": "OrderedCollection"
}`, dst.String())
@ -161,7 +161,7 @@ func (suite *OutboxGetTestSuite) TestGetOutboxFirstPage() {
],
"partOf": "http://localhost:8080/users/the_mighty_zork/outbox",
"prev": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40\u0026min_id=01HH9KYNQPA416TNJ53NSATP40",
"totalItems": 7,
"totalItems": 8,
"type": "OrderedCollectionPage"
}`, dst.String())
@ -224,7 +224,7 @@ func (suite *OutboxGetTestSuite) TestGetOutboxNextPage() {
"id": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40&max_id=01F8MHAMCHF6Y650WCRSCP4WMY",
"orderedItems": [],
"partOf": "http://localhost:8080/users/the_mighty_zork/outbox",
"totalItems": 7,
"totalItems": 8,
"type": "OrderedCollectionPage"
}`, dst.String())

View file

@ -79,7 +79,7 @@ func (suite *AccountVerifyTestSuite) TestAccountVerifyGet() {
suite.Equal("http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", apimodelAccount.HeaderStatic)
suite.Equal(2, apimodelAccount.FollowersCount)
suite.Equal(2, apimodelAccount.FollowingCount)
suite.Equal(7, apimodelAccount.StatusesCount)
suite.Equal(8, apimodelAccount.StatusesCount)
suite.EqualValues(gtsmodel.VisibilityPublic, apimodelAccount.Source.Privacy)
suite.Equal(testAccount.Settings.Language, apimodelAccount.Source.Language)
suite.Equal(testAccount.NoteRaw, apimodelAccount.Source.Note)

View file

@ -240,8 +240,8 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
"followers_count": 2,
"following_count": 2,
"statuses_count": 7,
"last_status_at": "2023-12-10T09:24:00.000Z",
"statuses_count": 8,
"last_status_at": "2024-01-10T09:24:00.000Z",
"emojis": [],
"fields": [],
"enable_rss": true,

View file

@ -135,7 +135,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
},
"stats": {
"domain_count": 2,
"status_count": 19,
"status_count": 20,
"user_count": 4
},
"thumbnail": "http://localhost:8080/assets/logo.png",
@ -256,7 +256,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
},
"stats": {
"domain_count": 2,
"status_count": 19,
"status_count": 20,
"user_count": 4
},
"thumbnail": "http://localhost:8080/assets/logo.png",
@ -377,7 +377,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
},
"stats": {
"domain_count": 2,
"status_count": 19,
"status_count": 20,
"user_count": 4
},
"thumbnail": "http://localhost:8080/assets/logo.png",
@ -549,7 +549,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
},
"stats": {
"domain_count": 2,
"status_count": 19,
"status_count": 20,
"user_count": 4
},
"thumbnail": "http://localhost:8080/assets/logo.png",
@ -692,7 +692,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
},
"stats": {
"domain_count": 2,
"status_count": 19,
"status_count": 20,
"user_count": 4
},
"thumbnail": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/original/`+instanceAccount.AvatarMediaAttachment.ID+`.gif",`+`
@ -850,7 +850,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() {
},
"stats": {
"domain_count": 2,
"status_count": 19,
"status_count": 20,
"user_count": 4
},
"thumbnail": "http://localhost:8080/assets/logo.png",

View file

@ -916,7 +916,7 @@ func (suite *SearchGetTestSuite) TestSearchAAny() {
}
suite.Len(searchResult.Accounts, 5)
suite.Len(searchResult.Statuses, 6)
suite.Len(searchResult.Statuses, 7)
suite.Len(searchResult.Hashtags, 0)
}
@ -959,7 +959,7 @@ func (suite *SearchGetTestSuite) TestSearchAAnyFollowingOnly() {
}
suite.Len(searchResult.Accounts, 2)
suite.Len(searchResult.Statuses, 6)
suite.Len(searchResult.Statuses, 7)
suite.Len(searchResult.Hashtags, 0)
}
@ -1002,7 +1002,7 @@ func (suite *SearchGetTestSuite) TestSearchAStatuses() {
}
suite.Len(searchResult.Accounts, 0)
suite.Len(searchResult.Statuses, 6)
suite.Len(searchResult.Statuses, 7)
suite.Len(searchResult.Hashtags, 0)
}

View file

@ -114,8 +114,8 @@ func (suite *StatusHistoryTestSuite) TestGetHistory() {
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
"followers_count": 2,
"following_count": 2,
"statuses_count": 7,
"last_status_at": "2023-12-10T09:24:00.000Z",
"statuses_count": 8,
"last_status_at": "2024-01-10T09:24:00.000Z",
"emojis": [],
"fields": [],
"enable_rss": true,

View file

@ -132,8 +132,8 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() {
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
"followers_count": 2,
"following_count": 2,
"statuses_count": 7,
"last_status_at": "2023-12-10T09:24:00.000Z",
"statuses_count": 8,
"last_status_at": "2024-01-10T09:24:00.000Z",
"emojis": [],
"fields": [],
"enable_rss": true,
@ -197,8 +197,8 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() {
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
"followers_count": 2,
"following_count": 2,
"statuses_count": 7,
"last_status_at": "2023-12-10T09:24:00.000Z",
"statuses_count": 8,
"last_status_at": "2024-01-10T09:24:00.000Z",
"emojis": [],
"fields": [],
"enable_rss": true,

View file

@ -90,12 +90,23 @@ type Attachment struct {
// A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet.
// See https://github.com/woltapp/blurhash
Blurhash *string `json:"blurhash"`
}
// Additional fields not exposed via JSON
// (used only internally for templating etc).
// WebAttachment is like Attachment, but with
// additional fields not exposed via JSON;
// used only internally for templating etc.
//
// swagger:ignore
type WebAttachment struct {
*Attachment
// Parent status of this media is sensitive.
Sensitive bool `json:"-"`
// Parent status of this
// media is sensitive.
Sensitive bool
// MIME type of
// the attachment.
MIMEType string
}
// MediaMeta models media metadata.

View file

@ -111,6 +111,10 @@ type Status struct {
type WebStatus struct {
*Status
// Web version of media
// attached to this status.
MediaAttachments []*WebAttachment `json:"media_attachments"`
// Template-ready language tag and
// string, based on *status.Language.
LanguageTag *language.Language

View file

@ -46,7 +46,7 @@ type AccountTestSuite struct {
func (suite *AccountTestSuite) TestGetAccountStatuses() {
statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, false, false, "", "", false, false)
suite.NoError(err)
suite.Len(statuses, 7)
suite.Len(statuses, 8)
}
func (suite *AccountTestSuite) TestGetAccountStatusesPageDown() {
@ -69,7 +69,7 @@ func (suite *AccountTestSuite) TestGetAccountStatusesPageDown() {
if err != nil {
suite.FailNow(err.Error())
}
suite.Len(statuses, 1)
suite.Len(statuses, 2)
// try to get the last page (should be empty)
statuses, err = suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 3, false, false, statuses[len(statuses)-1].ID, "", false, false)
@ -187,7 +187,7 @@ func (suite *AccountTestSuite) TestGetAccountStatusesExcludeRepliesExcludesSelfR
func (suite *AccountTestSuite) TestGetAccountStatusesMediaOnly() {
statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, false, false, "", "", true, false)
suite.NoError(err)
suite.Len(statuses, 1)
suite.Len(statuses, 2)
}
func (suite *AccountTestSuite) TestGetAccountBy() {

View file

@ -114,7 +114,7 @@ func (suite *BasicTestSuite) TestGetAllStatuses() {
s := []*gtsmodel.Status{}
err := suite.db.GetAll(context.Background(), &s)
suite.NoError(err)
suite.Len(s, 23)
suite.Len(s, 24)
}
func (suite *BasicTestSuite) TestGetAllNotNull() {

View file

@ -47,7 +47,7 @@ func (suite *InstanceTestSuite) TestCountInstanceUsersRemote() {
func (suite *InstanceTestSuite) TestCountInstanceStatuses() {
count, err := suite.db.CountInstanceStatuses(context.Background(), config.GetHost())
suite.NoError(err)
suite.Equal(19, count)
suite.Equal(20, count)
}
func (suite *InstanceTestSuite) TestCountInstanceStatusesRemote() {

View file

@ -169,12 +169,7 @@ func (suite *StatusTestSuite) TestGetStatusChildren() {
targetStatus := suite.testStatuses["local_account_1_status_1"]
children, err := suite.db.GetStatusChildren(context.Background(), targetStatus.ID)
suite.NoError(err)
suite.Len(children, 2)
for _, c := range children {
suite.Equal(targetStatus.URI, c.InReplyToURI)
suite.Equal(targetStatus.AccountID, c.InReplyToAccountID)
suite.Equal(targetStatus.ID, c.InReplyToID)
}
suite.Len(children, 3)
}
func (suite *StatusTestSuite) TestDeleteStatus() {

View file

@ -155,7 +155,7 @@ func (suite *TimelineTestSuite) TestGetHomeTimeline() {
suite.FailNow(err.Error())
}
suite.checkStatuses(s, id.Highest, id.Lowest, 19)
suite.checkStatuses(s, id.Highest, id.Lowest, 20)
}
func (suite *TimelineTestSuite) TestGetHomeTimelineNoFollowing() {
@ -187,7 +187,7 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineNoFollowing() {
suite.FailNow(err.Error())
}
suite.checkStatuses(s, id.Highest, id.Lowest, 7)
suite.checkStatuses(s, id.Highest, id.Lowest, 8)
}
func (suite *TimelineTestSuite) TestGetHomeTimelineWithFutureStatus() {
@ -209,7 +209,7 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineWithFutureStatus() {
}
suite.NotContains(s, futureStatus)
suite.checkStatuses(s, id.Highest, id.Lowest, 19)
suite.checkStatuses(s, id.Highest, id.Lowest, 20)
}
func (suite *TimelineTestSuite) TestGetHomeTimelineBackToFront() {
@ -240,8 +240,8 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineFromHighest() {
}
suite.checkStatuses(s, id.Highest, id.Lowest, 5)
suite.Equal("01HH9KYNQPA416TNJ53NSATP40", s[0].ID)
suite.Equal("01G20ZM733MGN8J344T4ZDDFY1", s[len(s)-1].ID)
suite.Equal("01J2M1HPFSS54S60Y0KYV23KJE", s[0].ID)
suite.Equal("01G36SF3V6Y6V5BF9P4R7PQG7G", s[len(s)-1].ID)
}
func (suite *TimelineTestSuite) TestGetListTimelineNoParams() {

View file

@ -18,6 +18,7 @@
package media
import (
"cmp"
"context"
"encoding/json"
"errors"
@ -198,6 +199,30 @@ func (res *ffprobeResult) ImageMeta() (width int, height int, err error) {
return
}
// EmbeddedImageMeta extracts embedded image metadata contained within ffprobe'd media result
// streams, should be used for pulling album image (can be animated image) from audio files.
func (res *ffprobeResult) EmbeddedImageMeta() (width int, height int, framerate float32, err error) {
for _, stream := range res.Streams {
if stream.Width > width {
width = stream.Width
}
if stream.Height > height {
height = stream.Height
}
if fr := stream.GetFrameRate(); fr > 0 {
if framerate == 0 || fr < framerate {
framerate = fr
}
}
}
// Need width + height but
// no framerate is fine.
if width == 0 || height == 0 {
err = errors.New("invalid image stream(s)")
}
return
}
// VideoMeta extracts video metadata contained within ffprobe'd media result streams.
func (res *ffprobeResult) VideoMeta() (width, height int, framerate float32, err error) {
for _, stream := range res.Streams {
@ -222,6 +247,7 @@ func (res *ffprobeResult) VideoMeta() (width, height int, framerate float32, err
type ffprobeStream struct {
CodecName string `json:"codec_name"`
AvgFrameRate string `json:"avg_frame_rate"`
RFrameRate string `json:"r_frame_rate"`
Width int `json:"width"`
Height int `json:"height"`
// + unused fields.
@ -229,7 +255,7 @@ type ffprobeStream struct {
// GetFrameRate calculates float32 framerate value from stream json string.
func (str *ffprobeStream) GetFrameRate() float32 {
if str.AvgFrameRate != "" {
numDen := func(strFR string) (float32, float32) {
var (
// numerator
num float32
@ -239,7 +265,7 @@ func (str *ffprobeStream) GetFrameRate() float32 {
)
// Check for a provided inequality, i.e. numerator / denominator.
if p := strings.SplitN(str.AvgFrameRate, "/", 2); len(p) == 2 {
if p := strings.SplitN(strFR, "/", 2); len(p) == 2 {
n, _ := strconv.ParseFloat(p[0], 32)
d, _ := strconv.ParseFloat(p[1], 32)
num, den = float32(n), float32(d)
@ -248,8 +274,26 @@ func (str *ffprobeStream) GetFrameRate() float32 {
num = float32(n)
}
return num / den
return num, den
}
var num, den float32
if str.AvgFrameRate != "" {
// Check if we have avg_frame_rate.
num, den = numDen(str.AvgFrameRate)
}
if num == 0 && str.RFrameRate != "" {
// Check if we have r_frame_rate.
num, den = numDen(str.RFrameRate)
}
if num != 0 {
// Found it.
// Avoid divide by zero.
return num / cmp.Or(den, 1)
}
return 0
}

View file

@ -299,8 +299,14 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
// Extract image metadata from streams (if any),
// this will only exist for embedded album art.
width, height, _ := result.ImageMeta()
width, height, framerate, _ := result.EmbeddedImageMeta()
if width > 0 && height > 0 {
// Unlikely to need these but masto API includes them.
p.media.FileMeta.Original.Width = width
p.media.FileMeta.Original.Height = height
if framerate != 0 {
p.media.FileMeta.Original.Framerate = &framerate
}
// Determine thumbnail dimensions to use.
thumbWidth, thumbHeight := thumbSize(width, height)

View file

@ -41,11 +41,11 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() {
func (suite *GetRSSTestSuite) TestGetAccountRSSZork() {
getFeed, lastModified, err := suite.accountProcessor.GetRSSFeedForUsername(context.Background(), "the_mighty_zork")
suite.NoError(err)
suite.EqualValues(1702200240, lastModified.Unix())
suite.EqualValues(1704878640, lastModified.Unix())
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>Sun, 10 Dec 2023 09:24:00 +0000</pubDate>\n <lastBuildDate>Sun, 10 Dec 2023 09:24:00 +0000</lastBuildDate>\n <image>\n <url>http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg</url>\n <title>Avatar for @the_mighty_zork@localhost:8080</title>\n <link>http://localhost:8080/@the_mighty_zork</link>\n </image>\n <item>\n <title>HTML in post</title>\n <link>http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40</link>\n <description>@the_mighty_zork@localhost:8080 made a new post: &#34;Here&#39;s a bunch of HTML, read it and weep, weep then!&#xA;&#xA;```html&#xA;&lt;section class=&#34;about-user&#34;&gt;&#xA; &lt;div class=&#34;col-header&#34;&gt;&#xA; &lt;h2&gt;About&lt;/h2&gt;&#xA; &lt;/div&gt; &#xA; &lt;div class=&#34;fields&#34;&gt;&#xA; &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;&#xA; &lt;dl&gt;&#xA;...</description>\n <content:encoded><![CDATA[<p>Here's a bunch of HTML, read it and weep, weep then!</p><pre><code class=\"language-html\">&lt;section class=&#34;about-user&#34;&gt;\n &lt;div class=&#34;col-header&#34;&gt;\n &lt;h2&gt;About&lt;/h2&gt;\n &lt;/div&gt; \n &lt;div class=&#34;fields&#34;&gt;\n &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;\n &lt;dl&gt;\n &lt;div class=&#34;field&#34;&gt;\n &lt;dt&gt;should you follow me?&lt;/dt&gt;\n &lt;dd&gt;maybe!&lt;/dd&gt;\n &lt;/div&gt;\n &lt;div class=&#34;field&#34;&gt;\n &lt;dt&gt;age&lt;/dt&gt;\n &lt;dd&gt;120&lt;/dd&gt;\n &lt;/div&gt;\n &lt;/dl&gt;\n &lt;/div&gt;\n &lt;div class=&#34;bio&#34;&gt;\n &lt;h3 class=&#34;sr-only&#34;&gt;Bio&lt;/h3&gt;\n &lt;p&gt;i post about things that concern me&lt;/p&gt;\n &lt;/div&gt;\n &lt;div class=&#34;sr-only&#34; role=&#34;group&#34;&gt;\n &lt;h3 class=&#34;sr-only&#34;&gt;Stats&lt;/h3&gt;\n &lt;span&gt;Joined in Jun, 2022.&lt;/span&gt;\n &lt;span&gt;8 posts.&lt;/span&gt;\n &lt;span&gt;Followed by 1.&lt;/span&gt;\n &lt;span&gt;Following 1.&lt;/span&gt;\n &lt;/div&gt;\n &lt;div class=&#34;accountstats&#34; aria-hidden=&#34;true&#34;&gt;\n &lt;b&gt;Joined&lt;/b&gt;&lt;time datetime=&#34;2022-06-04T13:12:00.000Z&#34;&gt;Jun, 2022&lt;/time&gt;\n &lt;b&gt;Posts&lt;/b&gt;&lt;span&gt;8&lt;/span&gt;\n &lt;b&gt;Followed by&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;\n &lt;b&gt;Following&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;\n &lt;/div&gt;\n&lt;/section&gt;\n</code></pre><p>There, hope you liked that!</p>]]></content:encoded>\n <author>@the_mighty_zork@localhost:8080</author>\n <guid isPermaLink=\"true\">http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40</guid>\n <pubDate>Sun, 10 Dec 2023 09:24:00 +0000</pubDate>\n <source>http://localhost:8080/@the_mighty_zork/feed.rss</source>\n </item>\n <item>\n <title>introduction post</title>\n <link>http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</link>\n <description>@the_mighty_zork@localhost:8080 made a new post: &#34;hello everyone!&#34;</description>\n <content:encoded><![CDATA[hello everyone!]]></content:encoded>\n <author>@the_mighty_zork@localhost:8080</author>\n <guid isPermaLink=\"true\">http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</guid>\n <pubDate>Wed, 20 Oct 2021 10:40:37 +0000</pubDate>\n <source>http://localhost:8080/@the_mighty_zork/feed.rss</source>\n </item>\n </channel>\n</rss>", feed)
suite.Equal("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\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.jpg</url>\n <title>Avatar for @the_mighty_zork@localhost:8080</title>\n <link>http://localhost:8080/@the_mighty_zork</link>\n </image>\n <item>\n <title>HTML in post</title>\n <link>http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40</link>\n <description>@the_mighty_zork@localhost:8080 made a new post: &#34;Here&#39;s a bunch of HTML, read it and weep, weep then!&#xA;&#xA;```html&#xA;&lt;section class=&#34;about-user&#34;&gt;&#xA; &lt;div class=&#34;col-header&#34;&gt;&#xA; &lt;h2&gt;About&lt;/h2&gt;&#xA; &lt;/div&gt; &#xA; &lt;div class=&#34;fields&#34;&gt;&#xA; &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;&#xA; &lt;dl&gt;&#xA;...</description>\n <content:encoded><![CDATA[<p>Here's a bunch of HTML, read it and weep, weep then!</p><pre><code class=\"language-html\">&lt;section class=&#34;about-user&#34;&gt;\n &lt;div class=&#34;col-header&#34;&gt;\n &lt;h2&gt;About&lt;/h2&gt;\n &lt;/div&gt; \n &lt;div class=&#34;fields&#34;&gt;\n &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;\n &lt;dl&gt;\n &lt;div class=&#34;field&#34;&gt;\n &lt;dt&gt;should you follow me?&lt;/dt&gt;\n &lt;dd&gt;maybe!&lt;/dd&gt;\n &lt;/div&gt;\n &lt;div class=&#34;field&#34;&gt;\n &lt;dt&gt;age&lt;/dt&gt;\n &lt;dd&gt;120&lt;/dd&gt;\n &lt;/div&gt;\n &lt;/dl&gt;\n &lt;/div&gt;\n &lt;div class=&#34;bio&#34;&gt;\n &lt;h3 class=&#34;sr-only&#34;&gt;Bio&lt;/h3&gt;\n &lt;p&gt;i post about things that concern me&lt;/p&gt;\n &lt;/div&gt;\n &lt;div class=&#34;sr-only&#34; role=&#34;group&#34;&gt;\n &lt;h3 class=&#34;sr-only&#34;&gt;Stats&lt;/h3&gt;\n &lt;span&gt;Joined in Jun, 2022.&lt;/span&gt;\n &lt;span&gt;8 posts.&lt;/span&gt;\n &lt;span&gt;Followed by 1.&lt;/span&gt;\n &lt;span&gt;Following 1.&lt;/span&gt;\n &lt;/div&gt;\n &lt;div class=&#34;accountstats&#34; aria-hidden=&#34;true&#34;&gt;\n &lt;b&gt;Joined&lt;/b&gt;&lt;time datetime=&#34;2022-06-04T13:12:00.000Z&#34;&gt;Jun, 2022&lt;/time&gt;\n &lt;b&gt;Posts&lt;/b&gt;&lt;span&gt;8&lt;/span&gt;\n &lt;b&gt;Followed by&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;\n &lt;b&gt;Following&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;\n &lt;/div&gt;\n&lt;/section&gt;\n</code></pre><p>There, hope you liked that!</p>]]></content:encoded>\n <author>@the_mighty_zork@localhost:8080</author>\n <guid isPermaLink=\"true\">http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40</guid>\n <pubDate>Sun, 10 Dec 2023 09:24:00 +0000</pubDate>\n <source>http://localhost:8080/@the_mighty_zork/feed.rss</source>\n </item>\n <item>\n <title>introduction post</title>\n <link>http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</link>\n <description>@the_mighty_zork@localhost:8080 made a new post: &#34;hello everyone!&#34;</description>\n <content:encoded><![CDATA[hello everyone!]]></content:encoded>\n <author>@the_mighty_zork@localhost:8080</author>\n <guid isPermaLink=\"true\">http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</guid>\n <pubDate>Wed, 20 Oct 2021 10:40:37 +0000</pubDate>\n <source>http://localhost:8080/@the_mighty_zork/feed.rss</source>\n </item>\n </channel>\n</rss>", feed)
}
func (suite *GetRSSTestSuite) TestGetAccountRSSZorkNoPosts() {

View file

@ -228,7 +228,7 @@ func (suite *GetTestSuite) TestGetNewTimelineMoreThanPossible() {
if err != nil {
suite.FailNow(err.Error())
}
suite.checkStatuses(statuses, id.Highest, id.Lowest, 19)
suite.checkStatuses(statuses, id.Highest, id.Lowest, 20)
}
func (suite *GetTestSuite) TestGetNewTimelineMoreThanPossiblePageUp() {
@ -255,7 +255,7 @@ func (suite *GetTestSuite) TestGetNewTimelineMoreThanPossiblePageUp() {
if err != nil {
suite.FailNow(err.Error())
}
suite.checkStatuses(statuses, id.Highest, id.Lowest, 19)
suite.checkStatuses(statuses, id.Highest, id.Lowest, 20)
}
func (suite *GetTestSuite) TestGetNewTimelineNoFollowing() {
@ -284,7 +284,7 @@ func (suite *GetTestSuite) TestGetNewTimelineNoFollowing() {
if err != nil {
suite.FailNow(err.Error())
}
suite.checkStatuses(statuses, id.Highest, id.Lowest, 7)
suite.checkStatuses(statuses, id.Highest, id.Lowest, 8)
for _, s := range statuses {
if s.GetAccountID() != testAccount.ID {

View file

@ -40,7 +40,7 @@ func (suite *PruneTestSuite) TestPrune() {
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
suite.NoError(err)
suite.Equal(18, pruned)
suite.Equal(19, pruned)
suite.Equal(5, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
}
@ -56,7 +56,7 @@ func (suite *PruneTestSuite) TestPruneTwice() {
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
suite.NoError(err)
suite.Equal(18, pruned)
suite.Equal(19, pruned)
suite.Equal(5, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
// Prune same again, nothing should be pruned this time.
@ -78,7 +78,7 @@ func (suite *PruneTestSuite) TestPruneTo0() {
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
suite.NoError(err)
suite.Equal(23, pruned)
suite.Equal(24, pruned)
suite.Equal(0, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
}
@ -95,7 +95,7 @@ func (suite *PruneTestSuite) TestPruneToInfinityAndBeyond() {
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
suite.NoError(err)
suite.Equal(0, pruned)
suite.Equal(23, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
suite.Equal(24, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
}
func TestPruneTestSuite(t *testing.T) {

View file

@ -624,7 +624,7 @@ func (c *Converter) AttachmentToAPIAttachment(ctx context.Context, a *gtsmodel.M
Y: a.FileMeta.Focus.Y,
}
case gtsmodel.FileTypeVideo:
case gtsmodel.FileTypeVideo, gtsmodel.FileTypeAudio:
if i := a.FileMeta.Original.Duration; i != nil {
apiAttachment.Meta.Original.Duration = *i
}
@ -1062,14 +1062,36 @@ func (c *Converter) StatusToWebStatus(
webStatus.PollOptions = PollOptions
}
// Mark local.
webStatus.Local = *s.Local
// Set additional templating
// variables on media attachments.
for _, a := range webStatus.MediaAttachments {
a.Sensitive = webStatus.Sensitive
// Get gtsmodel attachments
// into a convenient map.
ogAttachments := make(
map[string]*gtsmodel.MediaAttachment,
len(s.Attachments),
)
for _, a := range s.Attachments {
ogAttachments[a.ID] = a
}
// Mark this as a local status.
webStatus.Local = *s.Local
// Convert each API attachment
// into a web attachment.
webStatus.MediaAttachments = make(
[]*apimodel.WebAttachment,
len(apiStatus.MediaAttachments),
)
for i, apiAttachment := range apiStatus.MediaAttachments {
ogAttachment := ogAttachments[apiAttachment.ID]
webStatus.MediaAttachments[i] = &apimodel.WebAttachment{
Attachment: apiAttachment,
Sensitive: apiStatus.Sensitive,
MIMEType: ogAttachment.File.ContentType,
}
}
return webStatus, nil
}

View file

@ -63,8 +63,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontend() {
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
"followers_count": 2,
"following_count": 2,
"statuses_count": 7,
"last_status_at": "2023-12-10T09:24:00.000Z",
"statuses_count": 8,
"last_status_at": "2024-01-10T09:24:00.000Z",
"emojis": [],
"fields": [],
"enable_rss": true,
@ -116,8 +116,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendAliasedAndMoved()
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
"followers_count": 2,
"following_count": 2,
"statuses_count": 7,
"last_status_at": "2023-12-10T09:24:00.000Z",
"statuses_count": 8,
"last_status_at": "2024-01-10T09:24:00.000Z",
"emojis": [],
"fields": [],
"source": {
@ -209,8 +209,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiStruct()
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
"followers_count": 2,
"following_count": 2,
"statuses_count": 7,
"last_status_at": "2023-12-10T09:24:00.000Z",
"statuses_count": 8,
"last_status_at": "2024-01-10T09:24:00.000Z",
"emojis": [
{
"shortcode": "rainbow",
@ -259,8 +259,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiIDs() {
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
"followers_count": 2,
"following_count": 2,
"statuses_count": 7,
"last_status_at": "2023-12-10T09:24:00.000Z",
"statuses_count": 8,
"last_status_at": "2024-01-10T09:24:00.000Z",
"emojis": [
{
"shortcode": "rainbow",
@ -305,8 +305,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() {
"header_description": "A very old-school screenshot of the original team fortress mod for quake",
"followers_count": 2,
"following_count": 2,
"statuses_count": 7,
"last_status_at": "2023-12-10T09:24:00.000Z",
"statuses_count": 8,
"last_status_at": "2024-01-10T09:24:00.000Z",
"emojis": [],
"fields": [],
"source": {
@ -943,6 +943,18 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
"emojis": [],
"fields": []
},
"mentions": [
{
"id": "01F8MH17FWEB39HZJ76B6VXSKF",
"username": "admin",
"url": "http://localhost:8080/@admin",
"acct": "admin"
}
],
"tags": [],
"emojis": [],
"card": null,
"poll": null,
"media_attachments": [
{
"id": "01HE7Y3C432WRSNS10EZM86SA5",
@ -971,7 +983,9 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
}
},
"description": "Photograph of a sloth, Public Domain.",
"blurhash": "LNEC{|w}0K9GsEtPM|j[NFbHoeof"
"blurhash": "LNEC{|w}0K9GsEtPM|j[NFbHoeof",
"Sensitive": true,
"MIMEType": "image/jpg"
},
{
"id": "01HE7ZFX9GKA5ZZVD4FACABSS9",
@ -983,7 +997,9 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
"preview_remote_url": null,
"meta": null,
"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,
"MIMEType": "image/svg"
},
{
"id": "01HE88YG74PVAB81PX2XA9F3FG",
@ -995,21 +1011,11 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
"preview_remote_url": null,
"meta": null,
"description": "Jolly salsa song, public domain.",
"blurhash": null
"blurhash": null,
"Sensitive": true,
"MIMEType": "audio/mpeg"
}
],
"mentions": [
{
"id": "01F8MH17FWEB39HZJ76B6VXSKF",
"username": "admin",
"url": "http://localhost:8080/@admin",
"acct": "admin"
}
],
"tags": [],
"emojis": [],
"card": null,
"poll": null,
"LanguageTag": "en",
"PollOptions": null,
"Local": false,
@ -1249,7 +1255,7 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() {
},
"stats": {
"domain_count": 2,
"status_count": 19,
"status_count": 20,
"user_count": 4
},
"thumbnail": "http://localhost:8080/assets/logo.png",

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

View file

@ -989,6 +989,53 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
Header: util.Ptr(true),
Cached: util.Ptr(true),
},
"local_account_1_status_8_attachment_1": {
ID: "01J2M20K6K9XQC4WSB961YJHV6",
StatusID: "01J2M1HPFSS54S60Y0KYV23KJE",
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01J2M20K6K9XQC4WSB961YJHV6.mp3",
RemoteURL: "",
CreatedAt: TimeMustParse("2024-01-10T11:24:00+02:00"),
UpdatedAt: TimeMustParse("2024-01-10T11:24:00+02:00"),
Type: gtsmodel.FileTypeAudio,
FileMeta: gtsmodel.FileMeta{
Original: gtsmodel.Original{
Width: 500,
Height: 500,
Size: 0,
Aspect: 0,
},
Small: gtsmodel.Small{
Width: 500,
Height: 500,
Size: 250000,
Aspect: 1,
},
Focus: gtsmodel.Focus{
X: 0,
Y: 0,
},
},
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
Description: "This is a track from Nine Inch Nail's \"Ghosts I-V\" album. This is the third track from \"Ghosts II\".",
ScheduledStatusID: "",
Blurhash: "LeDvfpayIUof01j[xuayxuayaxj[",
Processing: 2,
File: gtsmodel.File{
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01J2M20K6K9XQC4WSB961YJHV6.mp3",
ContentType: "audio/mpeg",
FileSize: 7483917,
},
Thumbnail: gtsmodel.Thumbnail{
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01J2M20K6K9XQC4WSB961YJHV6.jpg",
ContentType: "image/jpeg",
FileSize: 6132,
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01J2M20K6K9XQC4WSB961YJHV6.jpg",
RemoteURL: "",
},
Avatar: util.Ptr(false),
Header: util.Ptr(false),
Cached: util.Ptr(true),
},
"remote_account_1_status_1_attachment_1": {
ID: "01FVW7RXPQ8YJHTEXYPE7Q8ZY0",
StatusID: "01FVW7JHQFSFK166WWKR8CBA6M",
@ -1347,6 +1394,10 @@ func newTestStoredAttachments() map[string]filenames {
Original: "team-fortress-original.jpg",
Small: "team-fortress-small.jpg",
},
"local_account_1_status_8_attachment_1": {
Original: "ghosts-original.mp3",
Small: "ghosts-small.jpg",
},
"remote_account_1_status_1_attachment_1": {
Original: "thoughtsofdog-original.jpg",
Small: "thoughtsofdog-small.jpg",
@ -1644,6 +1695,31 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Federated: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
},
"local_account_1_status_8": {
ID: "01J2M1HPFSS54S60Y0KYV23KJE",
URI: "http://localhost:8080/users/the_mighty_zork/statuses/01J2M1HPFSS54S60Y0KYV23KJE",
URL: "http://localhost:8080/@the_mighty_zork/statuses/01J2M1HPFSS54S60Y0KYV23KJE",
Content: "<p>Thanks! Here's a NIN track</p>",
Text: "Thanks! Here's a NIN track",
AttachmentIDs: []string{"01J2M20K6K9XQC4WSB961YJHV6"},
CreatedAt: TimeMustParse("2024-01-10T11:24:00+02:00"),
UpdatedAt: TimeMustParse("2024-01-10T11:24:00+02:00"),
Local: util.Ptr(true),
AccountURI: "http://localhost:8080/users/the_mighty_zork",
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
InReplyToID: "01FF25D5Q0DH7CHD57CTRS6WK0",
InReplyToAccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
InReplyToURI: "http://localhost:8080/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0",
BoostOfID: "",
ThreadID: "01HCWDKKBWECZJQ93E262N36VN",
ContentWarning: "",
Visibility: gtsmodel.VisibilityPublic,
Sensitive: util.Ptr(false),
Language: "en",
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
Federated: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
},
"local_account_2_status_1": {
ID: "01F8MHBQCBTDKN6X5VHGMMN4MA",
URI: "http://localhost:8080/users/1happyturtle/statuses/01F8MHBQCBTDKN6X5VHGMMN4MA",
@ -2208,6 +2284,10 @@ func NewTestThreadToStatus() []*gtsmodel.ThreadToStatus {
ThreadID: "01HCWDKKBWECZJQ93E262N36VN",
StatusID: "01FCQSQ667XHJ9AV9T27SJJSX5",
},
{
ThreadID: "01HCWDKKBWECZJQ93E262N36VN",
StatusID: "01J2M1HPFSS54S60Y0KYV23KJE",
},
{
ThreadID: "01HCWE71MGRRDSHBKXFD5DDSWR",
StatusID: "01FN3VJGFH10KR7S2PB0GFJZYG",

View file

@ -336,6 +336,10 @@ main {
grid-area: sensitive;
align-self: center;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
.button {
cursor: pointer;
align-self: center;
@ -401,10 +405,18 @@ main {
grid-column: span 2;
}
&.odd .media-wrapper:first-child, &.double .media-wrapper {
&.odd .media-wrapper:first-child,
&.double .media-wrapper {
grid-row: span 2;
}
@media screen and (max-width: 42rem) {
.media-wrapper {
grid-column: span 2;
grid-row: span 2;
}
}
img {
width: 100%;
height: 100%;

View file

@ -36,16 +36,42 @@
{{- end }}
{{- define "videoPreview" }}
<video
<img
src="{{- .PreviewURL -}}"
loading="lazy"
{{- if .Description }}
alt="{{- .Description -}}"
title="{{- .Description -}}"
{{- end }}
width="{{- .Meta.Original.Width -}}"
height="{{- .Meta.Original.Height -}}"
>
<source type="video/mp4" src="{{- .URL -}}"/>
</video>
width="{{- .Meta.Small.Width -}}"
height="{{- .Meta.Small.Height -}}"
/>
{{- end }}
{{- define "audioPreview" }}
{{- if and .PreviewURL .Meta.Small.Width }}
<img
src="{{- .PreviewURL -}}"
loading="lazy"
{{- if .Description }}
alt="{{- .Description -}}"
title="{{- .Description -}}"
{{- end }}
width="{{- .Meta.Small.Width -}}"
height="{{- .Meta.Small.Height -}}"
/>
{{- else }}
<img
src="/assets/logo.png"
loading="lazy"
{{- if .Description }}
alt="{{- .Description -}}"
title="{{- .Description -}}"
{{- end }}
width="518"
height="460"
/>
{{- end }}
{{- end }}
{{- /* Produces something like "1 attachment", "2 attachments", etc */ -}}
@ -77,21 +103,47 @@ media photoswipe-gallery {{ (len .) | oddOrEven }} {{ if eq (len .) 1 }}single{{
{{- include "videoPreview" $media | indent 4 }}
{{- else if eq .Type "image" }}
{{- include "imagePreview" $media | indent 4 }}
{{- else if eq .Type "audio" }}
{{- include "audioPreview" $media | indent 4 }}
{{- end }}
</summary>
{{- if eq .Type "video" }}
<video
preload="none"
class="plyr-video photoswipe-slide"
controls
data-pswp-index="{{- $index -}}"
data-pswp-width="{{- $media.Meta.Original.Width -}}px"
data-pswp-height="{{- $media.Meta.Original.Height -}}px"
poster="{{- .PreviewURL -}}"
data-pswp-width="{{- $media.Meta.Small.Width -}}px"
data-pswp-height="{{- $media.Meta.Small.Height -}}px"
{{- if .Description }}
alt="{{- $media.Description -}}"
title="{{- $media.Description -}}"
{{- end }}
>
<source type="video/mp4" src="{{- $media.URL -}}"/>
<source type="{{- $media.MIMEType -}}" src="{{- $media.URL -}}"/>
</video>
{{- else if eq .Type "audio" }}
<video
preload="none"
class="plyr-video photoswipe-slide"
controls
data-pswp-index="{{- $index -}}"
{{- if and $media.PreviewURL $media.Meta.Small.Width }}
poster="{{- .PreviewURL -}}"
data-pswp-width="{{- $media.Meta.Small.Width -}}px"
data-pswp-height="{{- $media.Meta.Small.Height -}}px"
{{- else }}
poster="/assets/logo.png"
width="518px"
height="460px"
{{- end }}
{{- if .Description }}
alt="{{- $media.Description -}}"
title="{{- $media.Description -}}"
{{- end }}
>
<source type="{{- $media.MIMEType -}}" src="{{- $media.URL -}}"/>
</video>
{{- else if eq .Type "image" }}
<a